Skip to content

Commit 910333b

Browse files
authored
Break-glass merge PR #2532
2 parents a2679a1 + 14d0fda commit 910333b

7 files changed

Lines changed: 762 additions & 63 deletions

File tree

‎internal/cbm/lsp/py_lsp.c‎

Lines changed: 42 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@
4040
#define PY_LSP_MAX_EVAL_DEPTH 256
4141
#include "lsp_work.h"
4242

43+
#ifdef CBM_ENABLE_TEST_SEAMS
44+
_Atomic uint64_t cbm_py_submodule_probes = 0;
45+
_Atomic uint64_t cbm_py_submodule_probe_visits = 0;
46+
#endif
47+
4348
#ifdef CBM_ENABLE_TEST_SEAMS
4449
static _Thread_local int py_test_memo_allocations_left = -1;
4550
static _Thread_local bool py_test_depth_failure;
@@ -167,51 +172,32 @@ static bool py_func_is_exact_callable_value(const CBMRegisteredFunc *func) {
167172
(!func->decorator_qns || !func->decorator_qns[0]);
168173
}
169174

170-
static int py_func_qn_pointer_cmp(const void *left, const void *right) {
171-
const CBMRegisteredFunc *a = *(CBMRegisteredFunc *const *)left;
172-
const CBMRegisteredFunc *b = *(CBMRegisteredFunc *const *)right;
173-
const char *aqn = a ? a->qualified_name : NULL;
174-
const char *bqn = b ? b->qualified_name : NULL;
175-
if (!aqn || !bqn)
176-
return aqn ? 1 : bqn ? -1 : 0;
177-
return strcmp(aqn, bqn);
178-
}
179-
180-
/* The graph identifies functions by QN. When Python source redefines the
181-
* same QN, the runtime's last binding is knowable but the graph no longer has
182-
* one occurrence-exact definition target. Mark the whole duplicate group so
183-
* callable values fail closed without changing ordinary call overload/rebind
184-
* behavior. */
185-
static void py_mark_ambiguous_callable_bindings(CBMTypeRegistry *registry) {
186-
if (!registry || registry->func_count <= 1)
175+
/* Reuse the QN snapshot for duplicate marks and later submodule probes.
176+
* The graph cannot identify one exact callable value for a duplicate QN;
177+
* ordinary exact lookups retain their original registration-order policy. */
178+
static void py_mark_ambiguous_callable_bindings(CBMTypeRegistry *registry, CBMArena *idx_arena) {
179+
if (!registry)
180+
return;
181+
cbm_registry_build_func_qn_order(registry, idx_arena);
182+
if (registry->func_count <= 1)
187183
return;
188-
size_t count = (size_t)registry->func_count;
189-
CBMRegisteredFunc **sorted = count <= SIZE_MAX / sizeof(*sorted)
190-
? (CBMRegisteredFunc **)malloc(count * sizeof(*sorted))
191-
: NULL;
192-
if (!sorted) {
193-
/* Allocation failure must reduce precision, never fabricate it. */
184+
const CBMRegistryQNEntry *sorted = registry->func_qn_sorted;
185+
if (!sorted || registry->func_qn_sorted_upto != registry->func_count) {
194186
for (int i = 0; i < registry->func_count; i++)
195187
registry->funcs[i].flags |= CBM_FUNC_FLAG_AMBIGUOUS_BINDING;
196188
return;
197189
}
198-
for (int i = 0; i < registry->func_count; i++)
199-
sorted[i] = &registry->funcs[i];
200-
qsort(sorted, count, sizeof(*sorted), py_func_qn_pointer_cmp);
201-
for (size_t first = 0; first < count;) {
202-
size_t end = first + 1;
203-
const char *qn = sorted[first]->qualified_name;
204-
while (qn && end < count && sorted[end]->qualified_name &&
205-
strcmp(qn, sorted[end]->qualified_name) == 0) {
190+
int count = registry->func_qn_sorted_count;
191+
for (int first = 0; first < count;) {
192+
int end = first + 1;
193+
while (end < count && strcmp(sorted[first].qualified_name, sorted[end].qualified_name) == 0)
206194
end++;
207-
}
208-
if (qn && end - first > 1) {
209-
for (size_t i = first; i < end; i++)
210-
sorted[i]->flags |= CBM_FUNC_FLAG_AMBIGUOUS_BINDING;
195+
if (end - first > 1) {
196+
for (int i = first; i < end; i++)
197+
registry->funcs[sorted[i].func_index].flags |= CBM_FUNC_FLAG_AMBIGUOUS_BINDING;
211198
}
212199
first = end;
213200
}
214-
free(sorted);
215201
}
216202

217203
/* Depth-guarded entry for the AST call-resolution walk. The walk recurses once
@@ -1588,6 +1574,22 @@ static const CBMType *py_iterable_element_type(PyLSPContext *ctx, const CBMType
15881574
/* The real recursive-descent evaluator. Never call directly — go through
15891575
* the memoizing, depth-guarded py_eval_expr_type wrapper below
15901576
* (every recursive call inside this body already does). */
1577+
/* Existing function-only submodule predicate, after exact symbol/type misses. */
1578+
static bool py_names_submodule(PyLSPContext *ctx, const char *qn) {
1579+
const char *prefix = qn ? cbm_arena_sprintf(ctx->arena, "%s.", qn) : NULL;
1580+
if (!prefix)
1581+
return false;
1582+
#ifdef CBM_ENABLE_TEST_SEAMS
1583+
uint64_t visits = 0;
1584+
bool found = cbm_registry_has_func_qn_prefix(ctx->registry, prefix, &visits);
1585+
atomic_fetch_add_explicit(&cbm_py_submodule_probes, 1, memory_order_relaxed);
1586+
atomic_fetch_add_explicit(&cbm_py_submodule_probe_visits, visits, memory_order_relaxed);
1587+
return found;
1588+
#else
1589+
return cbm_registry_has_func_qn_prefix(ctx->registry, prefix, NULL);
1590+
#endif
1591+
}
1592+
15911593
static const CBMType *py_eval_expr_type_uncached(PyLSPContext *ctx, TSNode node) {
15921594
if (!ctx || ts_node_is_null(node))
15931595
return cbm_type_unknown();
@@ -1708,25 +1710,7 @@ static const CBMType *py_eval_expr_type_uncached(PyLSPContext *ctx, TSNode node)
17081710
const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, qn);
17091711
if (rt)
17101712
return cbm_type_named(ctx->arena, qn);
1711-
// Submodule: if any registered function/type has qn starting
1712-
// with "<mod>.<attr>." then mod.attr is itself a module.
1713-
// Linear scan over registry funcs is O(R) per access; we
1714-
// skip it for the common case where mod.attr is already
1715-
// matched as a function/type above. With ~900 stdlib funcs
1716-
// and many module-attr accesses per file, this can dominate
1717-
// — keeping the loop tight and bailing early on first match.
1718-
const char *prefix = cbm_arena_sprintf(ctx->arena, "%s.", qn);
1719-
size_t prefix_len = strlen(prefix);
1720-
bool is_submodule = false;
1721-
CBMFreeFuncIter all_funcs;
1722-
cbm_registry_all_funcs_chain(ctx->registry, &all_funcs);
1723-
for (int i = -1; !is_submodule && (i = cbm_free_func_iter_next(&all_funcs)) >= 0;) {
1724-
const char *fqn = all_funcs.reg->funcs[i].qualified_name;
1725-
if (fqn && strncmp(fqn, prefix, prefix_len) == 0) {
1726-
is_submodule = true;
1727-
}
1728-
}
1729-
if (is_submodule)
1713+
if (py_names_submodule(ctx, qn))
17301714
return cbm_type_module(ctx->arena, qn);
17311715
return cbm_type_unknown();
17321716
}
@@ -5459,7 +5443,6 @@ void cbm_run_py_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
54595443
for (int i = 0; i < result->defs.count; i++) {
54605444
py_register_def(arena, &reg, &result->defs.items[i], module_qn);
54615445
}
5462-
py_mark_ambiguous_callable_bindings(&reg);
54635446

54645447
/* Hash-index the registry before the walk (#1527). Unfinalized, every
54655448
* lookup scanned the stdlib plus all of this file's definitions, which
@@ -5470,6 +5453,7 @@ void cbm_run_py_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
54705453
* scratch arena that dies with this call, not in the result arena. */
54715454
CBMArena idx_arena;
54725455
cbm_arena_init(&idx_arena);
5456+
py_mark_ambiguous_callable_bindings(&reg, &idx_arena);
54735457
reg.index_first_registered = true;
54745458
cbm_registry_finalize_into(&reg, &idx_arena);
54755459

@@ -5720,7 +5704,7 @@ CBMLSPStatus cbm_run_py_lsp_cross_status(CBMArena *arena, const char *source, in
57205704
ctx.eval_failure = CBM_LSP_SCOPE_FAILED;
57215705
goto cross_cleanup;
57225706
}
5723-
py_mark_ambiguous_callable_bindings(&reg);
5707+
py_mark_ambiguous_callable_bindings(&reg, &idx_arena);
57245708

57255709
/* Finalize registry — O(1) lookups. See go_lsp.c "3c. Finalize"
57265710
* comment for the rationale. */
@@ -5764,7 +5748,7 @@ CBMTypeRegistry *cbm_py_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, i
57645748
return NULL; /* caller owns the shared arena and other registries */
57655749
}
57665750

5767-
py_mark_ambiguous_callable_bindings(reg);
5751+
py_mark_ambiguous_callable_bindings(reg, arena);
57685752
cbm_registry_finalize(reg);
57695753
reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */
57705754
return reg;

‎internal/cbm/lsp/py_lsp.h‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,4 +244,12 @@ typedef struct {
244244
bool cbm_batch_py_lsp_cross(CBMArena *arena, CBMBatchPyLSPFile *files, int file_count,
245245
CBMResolvedCallArray *out);
246246

247+
#ifdef CBM_ENABLE_TEST_SEAMS
248+
#include <stdatomic.h>
249+
/* Process-wide comparison counts for deterministic resolver operation tests.
250+
* Only missing exact symbol/type attributes reach this submodule predicate. */
251+
extern _Atomic uint64_t cbm_py_submodule_probes;
252+
extern _Atomic uint64_t cbm_py_submodule_probe_visits;
253+
#endif
254+
247255
#endif // CBM_LSP_PY_LSP_H

‎internal/cbm/lsp/type_registry.c‎

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -909,7 +909,8 @@ static const CBMRegisteredFunc *lookup_func_self(const CBMTypeRegistry *reg,
909909

910910
for (int i = 0; i < reg->func_count; i++) {
911911
CBM_LSP_WORK(1);
912-
if (strcmp(reg->funcs[i].qualified_name, qualified_name) == 0) {
912+
if (reg->funcs[i].qualified_name &&
913+
strcmp(reg->funcs[i].qualified_name, qualified_name) == 0) {
913914
return &reg->funcs[i];
914915
}
915916
}
@@ -985,6 +986,99 @@ const CBMRegisteredFunc *cbm_registry_lookup_symbol(const CBMTypeRegistry *reg,
985986
return cbm_registry_lookup_func(reg, buf);
986987
}
987988

989+
/* A stable total order, independent of qsort's treatment of equal keys. */
990+
static int registry_qn_entry_cmp(const void *left, const void *right) {
991+
const CBMRegistryQNEntry *a = left;
992+
const CBMRegistryQNEntry *b = right;
993+
int cmp = strcmp(a->qualified_name, b->qualified_name);
994+
return cmp ? cmp : (a->func_index > b->func_index) - (a->func_index < b->func_index);
995+
}
996+
997+
#ifdef CBM_ENABLE_TEST_SEAMS
998+
static _Thread_local int registry_qn_order_allocations_left = -1;
999+
void cbm_registry_test_qn_order_fail_after(int successful_allocations) {
1000+
registry_qn_order_allocations_left = successful_allocations;
1001+
}
1002+
#endif
1003+
1004+
void cbm_registry_build_func_qn_order(CBMTypeRegistry *reg, CBMArena *arena) {
1005+
if (!reg || !arena || reg->read_only)
1006+
return;
1007+
/* A failed rebuild must not leave an old snapshot authoritative. */
1008+
reg->func_qn_sorted = NULL;
1009+
reg->func_qn_sorted_count = 0;
1010+
reg->func_qn_sorted_upto = 0;
1011+
if (reg->func_count <= 0 || (size_t)reg->func_count > SIZE_MAX / sizeof(CBMRegistryQNEntry))
1012+
return;
1013+
#ifdef CBM_ENABLE_TEST_SEAMS
1014+
if (registry_qn_order_allocations_left == 0)
1015+
return;
1016+
if (registry_qn_order_allocations_left > 0)
1017+
registry_qn_order_allocations_left--;
1018+
#endif
1019+
CBMRegistryQNEntry *sorted = cbm_arena_alloc(arena, (size_t)reg->func_count * sizeof(*sorted));
1020+
if (!sorted)
1021+
return;
1022+
int count = 0;
1023+
for (int i = 0; i < reg->func_count; i++) {
1024+
if (reg->funcs[i].qualified_name) {
1025+
sorted[count].qualified_name = reg->funcs[i].qualified_name;
1026+
sorted[count].func_index = i;
1027+
count++;
1028+
}
1029+
}
1030+
qsort(sorted, (size_t)count, sizeof(*sorted), registry_qn_entry_cmp);
1031+
reg->func_qn_sorted = sorted;
1032+
reg->func_qn_sorted_count = count;
1033+
reg->func_qn_sorted_upto = reg->func_count;
1034+
}
1035+
1036+
static bool registry_link_has_func_qn_prefix(const CBMTypeRegistry *reg, const char *prefix,
1037+
size_t length, uint64_t *visits) {
1038+
int scan_from = 0;
1039+
if (reg->func_qn_sorted && reg->func_qn_sorted_upto <= reg->func_count) {
1040+
int lo = 0, hi = reg->func_qn_sorted_count;
1041+
while (lo < hi) {
1042+
int mid = lo + (hi - lo) / 2;
1043+
if (visits)
1044+
(*visits)++;
1045+
if (strcmp(reg->func_qn_sorted[mid].qualified_name, prefix) < 0)
1046+
lo = mid + 1;
1047+
else
1048+
hi = mid;
1049+
}
1050+
if (lo < reg->func_qn_sorted_count) {
1051+
if (visits)
1052+
(*visits)++;
1053+
if (strncmp(reg->func_qn_sorted[lo].qualified_name, prefix, length) == 0)
1054+
return true;
1055+
}
1056+
scan_from = reg->func_qn_sorted_upto;
1057+
}
1058+
for (int i = scan_from; i < reg->func_count; i++) {
1059+
if (visits)
1060+
(*visits)++;
1061+
const char *qn = reg->funcs[i].qualified_name;
1062+
if (qn && strncmp(qn, prefix, length) == 0)
1063+
return true;
1064+
}
1065+
return false;
1066+
}
1067+
1068+
bool cbm_registry_has_func_qn_prefix(const CBMTypeRegistry *reg, const char *prefix,
1069+
uint64_t *visits) {
1070+
if (!prefix)
1071+
return false;
1072+
size_t length = strlen(prefix);
1073+
/* The old iterator omits shadowed fallback QNs, whose equal head QN has
1074+
* the same prefix answer. This boolean union needs no winner selection. */
1075+
for (const CBMTypeRegistry *link = reg; link; link = link->fallback) {
1076+
if (registry_link_has_func_qn_prefix(link, prefix, length, visits))
1077+
return true;
1078+
}
1079+
return false;
1080+
}
1081+
9881082
// Count parameters in a FUNC signature.
9891083
static int count_func_params(const CBMRegisteredFunc *f) {
9901084
if (!f || !f->signature || f->signature->kind != CBM_TYPE_FUNC)

‎internal/cbm/lsp/type_registry.h‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ typedef struct {
8383
int slot; // bucket slot this entry sits in (for resize)
8484
} CBMRegistryHashEntry;
8585

86+
/* A borrowed immutable QN and stable array index, never a movable func pointer. */
87+
typedef struct {
88+
const char *qualified_name;
89+
int func_index;
90+
} CBMRegistryQNEntry;
91+
8692
// Cross-file type/function registry.
8793
typedef struct CBMTypeRegistry {
8894
CBMRegisteredFunc *funcs;
@@ -159,6 +165,13 @@ typedef struct CBMTypeRegistry {
159165
// hashed chains answer with the LAST registration (the Tier-2 behavior,
160166
// left unchanged).
161167
bool index_first_registered;
168+
169+
/* Additional prefix/duplicate index; exact lookup order is unchanged.
170+
* Covers non-NULL QNs in funcs[0..func_qn_sorted_upto), ordered by (QN,index).
171+
* Appended functions remain visible through a tail scan. */
172+
CBMRegistryQNEntry *func_qn_sorted;
173+
int func_qn_sorted_count;
174+
int func_qn_sorted_upto;
162175
} CBMTypeRegistry;
163176

164177
// Initialize a registry.
@@ -207,6 +220,25 @@ const CBMRegisteredFunc *cbm_registry_lookup_func(const CBMTypeRegistry *reg,
207220
const CBMRegisteredFunc *cbm_registry_lookup_symbol(const CBMTypeRegistry *reg,
208221
const char *package_qn, const char *name);
209222

223+
/* Build in an arena that outlives every use (owner arena for shared registries,
224+
* walk scratch for per-file registries). QN strings and registered identities
225+
* must remain immutable while a snapshot is used; end its use before changing
226+
* identities, then rebuild before querying again. Appends do not invalidate it.
227+
* Never frees caller arenas. Sealed registries are unchanged. A failed rebuild
228+
* discards the old order and queries scan the complete registry instead. */
229+
void cbm_registry_build_func_qn_order(CBMTypeRegistry *reg, CBMArena *arena);
230+
231+
/* Boolean starts-with over all function/method QNs in the fallback chain.
232+
* Does not allocate or mutate. NULL prefix is false; empty prefix matches any
233+
* non-NULL QN. Optional visits accumulates compared entries, including tail
234+
* scans; fully indexed links use a lower bound plus at most one candidate. */
235+
bool cbm_registry_has_func_qn_prefix(const CBMTypeRegistry *reg, const char *prefix,
236+
uint64_t *visits);
237+
#ifdef CBM_ENABLE_TEST_SEAMS
238+
/* -1 disables; zero fails this allocation until reset; positive counts down. */
239+
void cbm_registry_test_qn_order_fail_after(int successful_allocations);
240+
#endif
241+
210242
// Resolve type alias chain: follow alias_of until concrete type found (max 16 levels).
211243
const CBMRegisteredType *cbm_registry_resolve_alias(const CBMTypeRegistry *reg,
212244
const char *type_qn);

‎scripts/memory-core-baseline.txt‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ internal/cbm/extract_defs.c 4
1010
internal/cbm/helpers.c 3
1111
internal/cbm/lsp/c_lsp.c 4
1212
internal/cbm/lsp/perl_lsp.c 21
13-
internal/cbm/lsp/py_lsp.c 2
1413
internal/cbm/macro_table.c 1
1514
internal/cbm/service_patterns.c 2
1615
internal/cbm/sqlite_writer.c 74

0 commit comments

Comments
 (0)