Merge origin/main into fix/py-namespace-reexports-v2 (pick up #2697)

py_lsp.c: #2697 moved py_lookup_attribute below the C3 code (own members, C3 order, then the depth-first walk) and declares it up front; the trivial definition this branch still carried next to its module-member lookup is dropped, the module-member block is kept, and the C3 section takes main's header.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-10-19 05:07:32 +02:00
3 changed files with 263 additions and 70 deletions
+160 -70
View File
@@ -1268,11 +1268,6 @@ static const CBMRegisteredFunc *py_lookup_attribute_depth(PyLSPContext *ctx, con
return NULL;
}
static const CBMRegisteredFunc *py_lookup_attribute(PyLSPContext *ctx, const char *type_qn,
const char *member_name) {
return py_lookup_attribute_depth(ctx, type_qn, member_name, 0);
}
static PyModuleMember py_module_member_exact(PyLSPContext *ctx, const char *module_qn,
const char *name) {
PyModuleMember m = {0};
@@ -1352,20 +1347,85 @@ static void py_emit_module_member_call(PyLSPContext *ctx, const char *callee_qn,
}
}
/* ── super(): the C3 method resolution order ──────────────────────── */
/* ── 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. */
* the depth-first walk above. */
#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;
enum { PY_MRO_CACHE_INITIAL_CAP = 64 }; /* power of two (index masking) */
enum { PY_MRO_CACHE_MAX_CAP = 1 << 20 }; /* hard stop for adversarial files */
static uint32_t py_mro_hash(const char *qn) {
uint32_t h = 2166136261u; /* FNV-1a */
for (const unsigned char *p = (const unsigned char *)qn; *p; p++) {
h = (h ^ *p) * 16777619u;
}
return h;
}
static const CBMPyMroCacheEntry *py_mro_cache_find(const PyLSPContext *ctx, const char *qn,
uint32_t h) {
if (!ctx->mro_cache || ctx->mro_cache_cap == 0)
return NULL;
uint32_t mask = (uint32_t)ctx->mro_cache_cap - 1;
uint32_t idx = h & mask;
for (int probed = 0; probed < ctx->mro_cache_cap; probed++) {
const CBMPyMroCacheEntry *e = &ctx->mro_cache[idx];
if (!e->qn)
return NULL;
if (e->hash == h && strcmp(e->qn, qn) == 0)
return e;
idx = (idx + 1) & mask;
}
return NULL;
}
/* Doubles the table (arena allocation: the old table is freed with the
* per-file arena). */
static bool py_mro_cache_grow(PyLSPContext *ctx) {
if (ctx->mro_cache_cap >= PY_MRO_CACHE_MAX_CAP)
return false;
int new_cap = ctx->mro_cache_cap ? ctx->mro_cache_cap * 2 : PY_MRO_CACHE_INITIAL_CAP;
CBMPyMroCacheEntry *entries = (CBMPyMroCacheEntry *)cbm_arena_alloc(
ctx->arena, (size_t)new_cap * sizeof(CBMPyMroCacheEntry));
if (!entries)
return false;
memset(entries, 0, (size_t)new_cap * sizeof(CBMPyMroCacheEntry));
uint32_t mask = (uint32_t)new_cap - 1;
for (int i = 0; i < ctx->mro_cache_cap; i++) {
const CBMPyMroCacheEntry *e = &ctx->mro_cache[i];
if (!e->qn)
continue;
uint32_t idx = e->hash & mask;
while (entries[idx].qn)
idx = (idx + 1) & mask;
entries[idx] = *e;
}
ctx->mro_cache = entries;
ctx->mro_cache_cap = new_cap;
return true;
}
/* Records the order of `qn` (len -1: none). A table that cannot grow only
* costs a recomputation later. */
static void py_mro_cache_put(PyLSPContext *ctx, const char *qn, uint32_t h, const char **mro,
int len) {
if ((ctx->mro_cache_count + 1) * 4 > ctx->mro_cache_cap * 3 && !py_mro_cache_grow(ctx))
return;
const char *key = cbm_arena_strdup(ctx->arena, qn);
if (!key)
return;
uint32_t mask = (uint32_t)ctx->mro_cache_cap - 1;
uint32_t idx = h & mask;
while (ctx->mro_cache[idx].qn)
idx = (idx + 1) & mask;
ctx->mro_cache[idx].qn = key;
ctx->mro_cache[idx].hash = h;
ctx->mro_cache[idx].len = len;
ctx->mro_cache[idx].mro = mro;
ctx->mro_cache_count++;
}
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++) {
@@ -1393,74 +1453,106 @@ static const char *py_c3_pick(const char **const *seqs, const int *lens, const i
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;
/* The C3 merge of `nseq` sequences into mro[1..]: returns the order's length,
* or -1 when there is none or it is longer than PY_C3_MAX. */
static int py_c3_merge(const char **const *seqs, const int *lens, int *heads, int nseq,
const char **mro) {
int n = 1;
for (;;) {
bool pending = false;
for (int s = 0; s <= nb && !pending; s++) {
for (int s = 0; s < nseq && !pending; s++) {
pending = heads[s] < lens[s];
}
if (!pending)
break;
const char *pick = py_c3_pick((const char **const *)seqs, lens, heads, nb + 1);
return n;
const char *pick = py_c3_pick(seqs, lens, heads, nseq);
if (!pick || n >= PY_C3_MAX)
return -1;
mro[n++] = pick;
for (int s = 0; s <= nb; s++) {
for (int s = 0; s < nseq; 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++;
}
/* 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. Computed once per class and file
* (ctx->mro_cache). 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. Only an
* order computed from the top (depth 0) is cached as missing: below it, the
* depth cap may have cut a hierarchy that is fine on its own. */
static int py_c3_linearize(PyLSPContext *ctx, const char *type_qn, const char ***out, int depth) {
uint32_t h = py_mro_hash(type_qn);
const CBMPyMroCacheEntry *hit = py_mro_cache_find(ctx, type_qn, h);
if (hit) {
*out = hit->mro;
return hit->len;
}
*out = mro;
int n = -1;
const char **mro = NULL;
const CBMRegisteredType *rt =
depth <= CBM_LSP_MAX_LOOKUP_DEPTH ? cbm_registry_lookup_type(ctx->registry, type_qn) : NULL;
const char **bases = rt ? rt->embedded_types : NULL;
int nb = 0;
while (bases && bases[nb])
nb++;
if (depth > CBM_LSP_MAX_LOOKUP_DEPTH || nb >= PY_C3_MAX) {
n = -1;
} else if (rt && rt->alias_of) {
n = py_c3_linearize(ctx, rt->alias_of, &mro, depth + 1);
} else {
const char ***seqs =
(const char ***)cbm_arena_alloc(ctx->arena, (size_t)(nb + 1) * sizeof(const char **));
int *lens = (int *)cbm_arena_alloc(ctx->arena, (size_t)(nb + 1) * 2 * sizeof(int));
mro = (const char **)cbm_arena_alloc(ctx->arena, PY_C3_MAX * sizeof(const char *));
bool ok = seqs && lens && mro;
int *heads = lens ? lens + nb + 1 : NULL;
for (int b = 0; ok && b < nb; b++) {
lens[b] = py_c3_linearize(ctx, bases[b], &seqs[b], depth + 1);
heads[b] = 0;
ok = lens[b] >= 0;
}
if (ok) {
seqs[nb] = bases;
lens[nb] = nb;
heads[nb] = 0;
mro[0] = cbm_arena_strdup(ctx->arena, type_qn);
n = mro[0] ? py_c3_merge((const char **const *)seqs, lens, heads, nb + 1, mro) : -1;
}
}
if (n >= 0 || depth == 0)
py_mro_cache_put(ctx, type_qn, h, n >= 0 ? mro : NULL, n);
*out = n >= 0 ? mro : NULL;
return n;
}
/* Attribute lookup along the class's C3 order: the class itself, then each
* class of its order that declares the member. A hierarchy without a C3 order
* keeps the depth-first walk. */
static const CBMRegisteredFunc *py_lookup_attribute(PyLSPContext *ctx, const char *type_qn,
const char *member_name) {
if (!ctx || !type_qn || !member_name)
return NULL;
const CBMRegisteredFunc *f = cbm_registry_lookup_method(ctx->registry, type_qn, member_name);
if (f)
return f;
const char **mro = NULL;
int n = py_c3_linearize(ctx, type_qn, &mro, 0);
if (n < 0)
return py_lookup_attribute_depth(ctx, type_qn, member_name, 0);
/* mro[0] is the class itself, already asked, or an alias's target. */
for (int i = strcmp(mro[0], type_qn) == 0 ? 1 : 0; i < n; i++) {
f = cbm_registry_lookup_method(ctx->registry, mro[i], member_name);
if (f)
return f;
}
return NULL;
}
/* 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. */
@@ -1469,10 +1561,8 @@ static const CBMRegisteredFunc *py_lookup_super_attribute(PyLSPContext *ctx, con
*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);
int n = py_c3_linearize(ctx, class_qn, &mro, 0);
if (n < 0)
return NULL;
*have_mro = true;
+15
View File
@@ -38,6 +38,16 @@ typedef struct {
const CBMType *result; // full-fidelity result, never NULL in a live entry
} CBMPyTypeCacheEntry;
// A class's C3 method resolution order (py_c3_linearize), computed once per
// class and file. The registry is complete before the walk starts, so an entry
// never goes stale.
typedef struct {
const char *qn; // class QN; NULL marks an empty slot
uint32_t hash; // hash of qn
int len; // entries in mro; -1 = the class has no C3 order
const char **mro; // arena-allocated, mro[0] is the class (or its alias target)
} CBMPyMroCacheEntry;
// PyLSPContext holds state for one Python file's type evaluation.
typedef struct {
CBMArena *arena;
@@ -113,6 +123,11 @@ typedef struct {
int eval_depth; // evaluator recursion depth (PY_LSP_MAX_EVAL_DEPTH)
uint32_t eval_truncations; // depth-cap cutoff count — gates memo inserts
CBMLSPStatus eval_failure; // sticky: explicit failure makes this walk incomplete
// C3 orders of the classes whose attributes this file looks up: one
// linearization per class, open addressing, arena-allocated.
CBMPyMroCacheEntry *mro_cache;
int mro_cache_count; // occupied slots (kept < 75% of cap)
int mro_cache_cap; // power-of-two capacity
// Pending links of the left-associative chains being evaluated
// iteratively (operator, receiver, subscript, conditional and
// parenthesis chains): one stack shared by nested evaluations.
+88
View File
@@ -15938,6 +15938,92 @@ TEST(pipeline_python_super_follows_c3_mro_parallel) {
return pym_run_super_c3(52);
}
/* ── attribute lookup follows the C3 method resolution order ─────── */
/* The same diamond as the super() test, seen from an ordinary attribute:
* `self.write_state()` in RadarrCalendarEntity and `e.write_state()` on a
* RadarrCalendarEntity both look the method up along the class's C3 order,
* which reaches CalendarEntity before the shared base Entity. */
static const char pya_radarr_py[] = "from base import CoordinatorEntity, CalendarEntity\n"
"\n"
"class RadarrEntity(CoordinatorEntity):\n"
" pass\n"
"\n"
"class RadarrCalendarEntity(RadarrEntity, CalendarEntity):\n"
" def refresh(self):\n"
" return self.write_state()\n"
"\n"
"class PlainCalendar(CalendarEntity):\n"
" def refresh(self):\n"
" return self.write_state()\n"
"\n"
"def publish(e: RadarrCalendarEntity):\n"
" return e.write_state()\n";
static int pya_run_attr_c3(int fillers) {
char tmp[256];
snprintf(tmp, sizeof(tmp), "/tmp/cbm_py_attrc3_XXXXXX");
if (!cbm_mkdtemp(tmp)) {
FAIL("tmpdir");
}
write_temp_file(tmp, "base.py", pym_base_py);
write_temp_file(tmp, "radarr.py", pya_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/attrc3.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 *c3 = "base.CalendarEntity.write_state";
const char *dfs = "base.Entity.write_state";
const char *self_src = "radarr.RadarrCalendarEntity.refresh";
bool self_c3 = s && pyn_edge(s, pr, self_src, c3, "CALLS");
bool self_dfs = s && pyn_edge(s, pr, self_src, dfs, "CALLS");
bool typed_c3 = s && pyn_edge(s, pr, "radarr.publish", c3, "CALLS");
bool typed_dfs = s && pyn_edge(s, pr, "radarr.publish", dfs, "CALLS");
bool plain = s && pyn_edge(s, pr, "radarr.PlainCalendar.refresh", c3, "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(self_c3); /* self.write_state(): CalendarEntity before Entity */
ASSERT_FALSE(self_dfs); /* ... never Entity through RadarrEntity's ancestry */
ASSERT_TRUE(typed_c3); /* e.write_state() on a RadarrCalendarEntity */
ASSERT_FALSE(typed_dfs);
ASSERT_TRUE(plain); /* control: single inheritance */
PASS();
}
TEST(pipeline_python_attribute_follows_c3_mro) {
return pya_run_attr_c3(0);
}
TEST(pipeline_python_attribute_follows_c3_mro_parallel) {
return pya_run_attr_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
@@ -25896,6 +25982,8 @@ SUITE(pipeline) {
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_attribute_follows_c3_mro);
RUN_TEST(pipeline_python_attribute_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_class_body_sibling_base);