Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions internal/cbm/lsp/py_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down Expand Up @@ -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
Expand Down
110 changes: 110 additions & 0 deletions tests/test_pipeline.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
Loading