From 29e7832c04c80771042463f60ad0565fa8453361 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sun, 18 Oct 2026 22:53:00 +0200 Subject: [PATCH 1/2] test(python): super() follows the C3 method resolution order The home-assistant radarr shape: RadarrCalendarEntity(RadarrEntity, CalendarEntity) where RadarrEntity reaches the common base Entity through CoordinatorEntity and CalendarEntity overrides Entity. In C3 order a common base comes after every class deriving from it, so super().write_state() and super().__init__() reach CalendarEntity, not Entity through the first base's ancestry. Single inheritance as the control; sequential and parallel. Signed-off-by: Martin Vogel --- tests/test_pipeline.c | 110 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/tests/test_pipeline.c b/tests/test_pipeline.c index 4f8c656ad..48561435a 100644 --- a/tests/test_pipeline.c +++ b/tests/test_pipeline.c @@ -15123,6 +15123,114 @@ TEST(pipeline_python_local_def_shadows_module_def_parallel_issue1548) { return pyn_run_shadowing(52); } +/* ── super() follows the C3 method resolution order ──────────────── */ + +/* The home-assistant radarr shape: RadarrCalendarEntity(RadarrEntity, + * CalendarEntity), where RadarrEntity reaches the common base Entity through + * CoordinatorEntity and CalendarEntity overrides Entity's method. Python's C3 + * order is [RadarrCalendarEntity, RadarrEntity, CoordinatorEntity, + * CalendarEntity, Entity]: a common base comes after every class that + * derives from it, so super() reaches CalendarEntity, not Entity through the + * first base's ancestry. */ +static const char pym_base_py[] = "class Entity:\n" + " def __init__(self):\n" + " self.ready = False\n" + "\n" + " def write_state(self):\n" + " return 0\n" + "\n" + "class CoordinatorEntity(Entity):\n" + " pass\n" + "\n" + "class CalendarEntity(Entity):\n" + " def __init__(self):\n" + " self.events = []\n" + "\n" + " def write_state(self):\n" + " return 1\n"; + +static const char pym_radarr_py[] = "from base import CoordinatorEntity, CalendarEntity\n" + "\n" + "class RadarrEntity(CoordinatorEntity):\n" + " pass\n" + "\n" + "class RadarrCalendarEntity(RadarrEntity, CalendarEntity):\n" + " def __init__(self):\n" + " super().__init__()\n" + "\n" + " def write_state(self):\n" + " return super().write_state()\n" + "\n" + "class PlainCalendar(CalendarEntity):\n" + " def write_state(self):\n" + " return super().write_state()\n"; + +static int pym_run_super_c3(int fillers) { + char tmp[256]; + snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_superc3_XXXXXX"); + if (!cbm_mkdtemp(tmp)) { + FAIL("tmpdir"); + } + write_temp_file(tmp, "base.py", pym_base_py); + write_temp_file(tmp, "radarr.py", pym_radarr_py); + for (int i = 0; i < fillers; i++) { + char name[64]; + char body[128]; + snprintf(name, sizeof(name), "filler%d.py", i); + snprintf(body, sizeof(body), "def filler%d():\n return %d\n", i, i); + write_temp_file(tmp, name, body); + } + char *old_workers = getenv("CBM_WORKERS"); + char *saved = old_workers ? strdup(old_workers) : NULL; + if (fillers > 0) { + cbm_setenv("CBM_WORKERS", "4", 1); + } + char db_path[512]; + snprintf(db_path, sizeof(db_path), "%s/superc3.db", tmp); + cbm_pipeline_t *p = cbm_pipeline_new(tmp, db_path, CBM_MODE_FULL); + int rc = p ? cbm_pipeline_run(p) : -1; + cbm_store_t *s = rc == 0 ? cbm_store_open_path(db_path) : NULL; + const char *pr = p ? cbm_pipeline_project_name(p) : NULL; + const char *rce = "radarr.RadarrCalendarEntity"; + char src[256]; + snprintf(src, sizeof(src), "%s.write_state", rce); + bool m_c3 = s && pyn_edge(s, pr, src, "base.CalendarEntity.write_state", "CALLS"); + bool m_dfs = s && pyn_edge(s, pr, src, "base.Entity.write_state", "CALLS"); + snprintf(src, sizeof(src), "%s.__init__", rce); + bool i_c3 = s && pyn_edge(s, pr, src, "base.CalendarEntity.__init__", "CALLS"); + bool i_dfs = s && pyn_edge(s, pr, src, "base.Entity.__init__", "CALLS"); + bool plain = s && pyn_edge(s, pr, "radarr.PlainCalendar.write_state", + "base.CalendarEntity.write_state", "CALLS"); + if (s) { + cbm_store_close(s); + } + if (p) { + cbm_pipeline_free(p); + } + if (saved) { + cbm_setenv("CBM_WORKERS", saved, 1); + free(saved); + } else { + cbm_unsetenv("CBM_WORKERS"); + } + th_rmtree(tmp); + ASSERT_EQ(rc, 0); + ASSERT_TRUE(m_c3); /* super().write_state(): CalendarEntity comes before Entity */ + ASSERT_FALSE(m_dfs); /* ... never Entity through RadarrEntity's ancestry */ + ASSERT_TRUE(i_c3); /* super().__init__(): the same order */ + ASSERT_FALSE(i_dfs); + ASSERT_TRUE(plain); /* control: single inheritance */ + PASS(); +} + +TEST(pipeline_python_super_follows_c3_mro) { + return pym_run_super_c3(0); +} + +TEST(pipeline_python_super_follows_c3_mro_parallel) { + return pym_run_super_c3(52); +} + /* L7 review of #1548: a package __init__.py function can share a submodule's QN * (poetry: layouts/__init__.py `def layout` is the QN prefix of every symbol in * layouts/layout.py). The function-local detection is bounded at the symbol's @@ -24824,6 +24932,8 @@ SUITE(pipeline) { RUN_TEST(pipeline_python_nested_def_local_after_incremental_issue1548); RUN_TEST(pipeline_python_local_def_shadows_module_def_issue1548); RUN_TEST(pipeline_python_local_def_shadows_module_def_parallel_issue1548); + RUN_TEST(pipeline_python_super_follows_c3_mro); + RUN_TEST(pipeline_python_super_follows_c3_mro_parallel); RUN_TEST(pipeline_python_init_function_shares_submodule_qn_issue1548); RUN_TEST(pipeline_python_init_function_shares_submodule_qn_parallel_issue1548); RUN_TEST(pipeline_python_nested_helper_keeps_tests_issue1548); From 7fa5f89374e366d6972fcdbb8085bafab43f21b8 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sun, 18 Oct 2026 22:57:12 +0200 Subject: [PATCH 2/2] fix(python-lsp): resolve super() through the C3 method resolution order super() binds to the enclosing class's method resolution order, Python's C3 linearization, in which a common base follows every class deriving from it. The super() resolver walked the bases one by one, each through its whole ancestry, so with RadarrCalendarEntity(RadarrEntity, CalendarEntity) the first base reached Entity through CoordinatorEntity before CalendarEntity, which overrides it, was ever looked at. py_c3_linearize computes the order (memoized per lookup, capped in depth and length; -1 when the hierarchy has none), and super() takes the first class after the enclosing one that declares the member. A hierarchy without a C3 order keeps the base-by-base walk, as does the synthesized __init__ of an undeclared constructor. Signed-off-by: Martin Vogel --- internal/cbm/lsp/py_lsp.c | 148 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/internal/cbm/lsp/py_lsp.c b/internal/cbm/lsp/py_lsp.c index 18384d82e..6be9ad565 100644 --- a/internal/cbm/lsp/py_lsp.c +++ b/internal/cbm/lsp/py_lsp.c @@ -1229,6 +1229,138 @@ static const CBMRegisteredFunc *py_lookup_attribute(PyLSPContext *ctx, const cha return py_lookup_attribute_depth(ctx, type_qn, member_name, 0); } +/* ── super(): the C3 method resolution order ──────────────────────── */ + +/* Longest linearization followed. A longer one (or more bases than this) keeps + * the base-by-base walk of the caller. */ +#define PY_C3_MAX 64 + +/* The linearizations computed during one super() lookup: every class of a + * diamond is linearized once, however many paths reach it. */ +typedef struct { + const char *qn[PY_C3_MAX]; + const char **mro[PY_C3_MAX]; + int len[PY_C3_MAX]; + int count; +} py_c3_cache_t; + +static bool py_c3_in_tail(const char *qn, const char *const *seq, int head, int len) { + for (int k = head + 1; k < len; k++) { + if (strcmp(seq[k], qn) == 0) + return true; + } + return false; +} + +/* The C3 head to take next: the first sequence head that is in no sequence's + * tail. NULL when every head is blocked (Python rejects such a class). */ +static const char *py_c3_pick(const char **const *seqs, const int *lens, const int *heads, + int nseq) { + for (int s = 0; s < nseq; s++) { + if (heads[s] >= lens[s]) + continue; + const char *cand = seqs[s][heads[s]]; + bool blocked = false; + for (int t = 0; t < nseq && !blocked; t++) { + blocked = heads[t] < lens[t] && py_c3_in_tail(cand, seqs[t], heads[t], lens[t]); + } + if (!blocked) + return cand; + } + return NULL; +} + +/* Python's method resolution order of `type_qn`: the class, then the C3 merge + * of its bases' linearizations and of the base list. A common base therefore + * follows every class deriving from it. Sets *out to an arena array and returns + * its length, or -1 when there is no C3 order, the hierarchy is deeper than + * CBM_LSP_MAX_LOOKUP_DEPTH, or the order is longer than PY_C3_MAX. A base the + * registry does not know (external, unresolved) linearizes to itself. */ +static int py_c3_linearize(PyLSPContext *ctx, py_c3_cache_t *cache, const char *type_qn, + const char ***out, int depth) { + for (int c = 0; c < cache->count; c++) { + if (strcmp(cache->qn[c], type_qn) == 0) { + *out = cache->mro[c]; + return cache->len[c]; + } + } + if (depth > CBM_LSP_MAX_LOOKUP_DEPTH) + return -1; + const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, type_qn); + if (rt && rt->alias_of) + return py_c3_linearize(ctx, cache, rt->alias_of, out, depth + 1); + const char **bases = rt ? rt->embedded_types : NULL; + int nb = 0; + while (bases && bases[nb]) + nb++; + if (nb >= PY_C3_MAX) + return -1; + const char **seqs[PY_C3_MAX]; + int lens[PY_C3_MAX]; + int heads[PY_C3_MAX]; + for (int b = 0; b < nb; b++) { + lens[b] = py_c3_linearize(ctx, cache, bases[b], &seqs[b], depth + 1); + if (lens[b] < 0) + return -1; + heads[b] = 0; + } + seqs[nb] = bases; + lens[nb] = nb; + heads[nb] = 0; + const char **mro = (const char **)cbm_arena_alloc(ctx->arena, PY_C3_MAX * sizeof(char *)); + if (!mro) + return -1; + int n = 0; + mro[n++] = type_qn; + for (;;) { + bool pending = false; + for (int s = 0; s <= nb && !pending; s++) { + pending = heads[s] < lens[s]; + } + if (!pending) + break; + const char *pick = py_c3_pick((const char **const *)seqs, lens, heads, nb + 1); + if (!pick || n >= PY_C3_MAX) + return -1; + mro[n++] = pick; + for (int s = 0; s <= nb; s++) { + if (heads[s] < lens[s] && strcmp(seqs[s][heads[s]], pick) == 0) + heads[s]++; + } + } + if (cache->count < PY_C3_MAX) { + cache->qn[cache->count] = type_qn; + cache->mro[cache->count] = mro; + cache->len[cache->count] = n; + cache->count++; + } + *out = mro; + return n; +} + +/* The method super() reaches from inside `class_qn`: the first class AFTER it in + * its C3 order that declares `member` itself. *have_mro is false when the + * hierarchy has no C3 order; the caller then keeps its base-by-base walk. */ +static const CBMRegisteredFunc *py_lookup_super_attribute(PyLSPContext *ctx, const char *class_qn, + const char *member, bool *have_mro) { + *have_mro = false; + if (!ctx || !class_qn || !member) + return NULL; + py_c3_cache_t cache; + cache.count = 0; + const char **mro = NULL; + int n = py_c3_linearize(ctx, &cache, class_qn, &mro, 0); + if (n < 0) + return NULL; + *have_mro = true; + for (int i = 1; i < n; i++) { + const CBMRegisteredFunc *f = cbm_registry_lookup_method(ctx->registry, mro[i], member); + if (f) + return f; + } + return NULL; +} + /* Per-file field overlay: look up a (class_qn, field_name) recorded during resolve * against the sealed shared registry. Linear scan — the overlay only holds fields * discovered in THIS file's own classes, so it is small. */ @@ -2964,6 +3096,22 @@ static void py_emit_call_for(PyLSPContext *ctx, TSNode call_node) { const CBMRegisteredType *enclosing = cbm_registry_lookup_type(ctx->registry, ctx->enclosing_class_qn); if (enclosing && enclosing->embedded_types) { + // The proxy walks the enclosing class's C3 order, in which a + // diamond's shared base follows every class deriving from it: + // RadarrCalendarEntity(RadarrEntity, CalendarEntity) reaches + // CalendarEntity before Entity, which the first base reaches + // through its own ancestry. The base-by-base walk below stays for + // a hierarchy without a C3 order and for an undeclared __init__. + bool have_mro = false; + const CBMRegisteredFunc *sf = py_lookup_super_attribute( + ctx, ctx->enclosing_class_qn, attr_name, &have_mro); + if (sf) { + bool is_init = strcmp(attr_name, "__init__") == 0; + py_emit_resolved_call(ctx, sf->qualified_name, + is_init ? "lsp_super_init" : "lsp_super", + is_init ? 0.90f : 0.88f, call_node); + return; + } for (int i = 0; enclosing->embedded_types[i]; i++) { // super().__init__() is a constructor delegation: // lsp_super_init is the MORE SPECIFIC, more accurate