mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-21 05:59:35 +08:00
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:
+160
-70
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user