Skip to content

Commit 496abe7

Browse files
authored
Break-glass merge PR #2528
2 parents 08587cd + 82c2b83 commit 496abe7

14 files changed

Lines changed: 1385 additions & 140 deletions

‎internal/cbm/cbm.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,7 @@ typedef enum {
543543
CBM_LSP_COMPLETE = 0,
544544
CBM_LSP_MEMO_FAILED,
545545
CBM_LSP_DEPTH_EXCEEDED,
546+
CBM_LSP_SCOPE_FAILED,
546547
} CBMLSPStatus;
547548

548549
// Growable arrays used during extraction.
@@ -1286,4 +1287,9 @@ bool cbm_label_is_relation(const char *label);
12861287
// `label` may be NULL (returns false). Defined in helpers.c.
12871288
bool cbm_label_is_registry_symbol(const char *label);
12881289

1290+
/* Python symbol scope only: callers must establish the language explicitly.
1291+
* A trailing .__init__ is omitted when a nonempty prefix precedes it; raw
1292+
* Module/file identity is unchanged. NULL returns zero. */
1293+
size_t cbm_fqn_symbol_scope_len(const char *module_qn);
1294+
12891295
#endif // CBM_H

‎internal/cbm/helpers.c‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1816,6 +1816,17 @@ char *cbm_fqn_compute(CBMArena *a, const char *project, const char *rel_path, co
18161816
return buf;
18171817
}
18181818

1819+
size_t cbm_fqn_symbol_scope_len(const char *module_qn) {
1820+
if (!module_qn)
1821+
return 0;
1822+
size_t len = strlen(module_qn);
1823+
static const char suffix[] = ".__init__";
1824+
size_t suffix_len = sizeof(suffix) - 1;
1825+
if (len > suffix_len && memcmp(module_qn + len - suffix_len, suffix, suffix_len) == 0)
1826+
return len - suffix_len;
1827+
return len;
1828+
}
1829+
18191830
char *cbm_fqn_module(CBMArena *a, const char *project, const char *rel_path) {
18201831
return cbm_fqn_compute(a, project, rel_path, NULL);
18211832
}

‎internal/cbm/lsp/py_lsp.c‎

Lines changed: 99 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@
4343
#ifdef CBM_ENABLE_TEST_SEAMS
4444
static _Thread_local int py_test_memo_allocations_left = -1;
4545
static _Thread_local bool py_test_depth_failure;
46+
static _Thread_local int py_test_scope_allocations_left = -1;
47+
void cbm_py_lsp_test_scope_fail_after(int successful_allocations) {
48+
py_test_scope_allocations_left = successful_allocations;
49+
}
4650
void cbm_py_lsp_test_depth_fail(bool enabled) {
4751
py_test_depth_failure = enabled;
4852
}
@@ -69,8 +73,9 @@ void cbm_py_lsp_record_failure(CBMFileResult *result, CBMLSPStatus status) {
6973
result->has_error = true;
7074
result->lsp_skipped = true;
7175
/* Static lifetime also works when the result arena cannot allocate. */
72-
result->error_msg =
73-
status == CBM_LSP_DEPTH_EXCEEDED ? CBM_PY_LSP_DEPTH_ERROR : CBM_PY_LSP_MEMO_ERROR;
76+
result->error_msg = status == CBM_LSP_SCOPE_FAILED ? CBM_PY_LSP_SCOPE_ERROR
77+
: status == CBM_LSP_DEPTH_EXCEEDED ? CBM_PY_LSP_DEPTH_ERROR
78+
: CBM_PY_LSP_MEMO_ERROR;
7479
}
7580

7681
// Forward decls
@@ -294,9 +299,34 @@ static void py_scope_restore(PyLSPContext *ctx, CBMScope *saved) {
294299
ctx->current_scope = saved;
295300
}
296301

297-
void py_lsp_init(PyLSPContext *ctx, CBMArena *arena, const char *source, int source_len,
298-
const CBMTypeRegistry *registry, const char *module_qn,
299-
CBMResolvedCallArray *out) {
302+
/* Only a changed Python scope needs storage. Failure is explicit: never
303+
* register or walk a package using the raw file scope as an OOM fallback. */
304+
static bool py_symbol_scope(CBMArena *arena, const char *module_qn, const char *stage,
305+
const char **out) {
306+
*out = module_qn;
307+
size_t len = cbm_fqn_symbol_scope_len(module_qn);
308+
if (!module_qn || module_qn[len] == '\0')
309+
return true;
310+
#ifdef CBM_ENABLE_TEST_SEAMS
311+
const char *fail_stage = getenv("CBM_TEST_PY_LSP_SCOPE_FAIL_STAGE");
312+
if ((fail_stage && strcmp(fail_stage, stage) == 0) || py_test_scope_allocations_left == 0)
313+
return false;
314+
if (py_test_scope_allocations_left > 0)
315+
py_test_scope_allocations_left--;
316+
#else
317+
(void)stage;
318+
#endif
319+
char *scope = cbm_arena_strndup(arena, module_qn, len);
320+
if (!scope)
321+
return false;
322+
*out = scope;
323+
return true;
324+
}
325+
326+
static void py_lsp_init_for_stage(PyLSPContext *ctx, CBMArena *arena, const char *source,
327+
int source_len, const CBMTypeRegistry *registry,
328+
const char *module_qn, CBMResolvedCallArray *out,
329+
const char *stage) {
300330
if (!ctx)
301331
return;
302332
memset(ctx, 0, sizeof(PyLSPContext));
@@ -310,13 +340,23 @@ void py_lsp_init(PyLSPContext *ctx, CBMArena *arena, const char *source, int sou
310340
ctx->source_len = source_len;
311341
ctx->registry = registry;
312342
ctx->registry_head = (CBMTypeRegistry *)registry;
313-
ctx->module_qn = module_qn;
343+
ctx->file_module_qn = module_qn;
314344
ctx->resolved_calls = out;
345+
if (!py_symbol_scope(arena, module_qn, stage, &ctx->module_qn)) {
346+
ctx->eval_failure = CBM_LSP_SCOPE_FAILED;
347+
return;
348+
}
315349
ctx->current_scope = py_scope_push_checked(ctx);
316350
const char *dbg = getenv("CBM_LSP_DEBUG");
317351
ctx->debug = dbg && dbg[0] && dbg[0] != '0';
318352
}
319353

354+
void py_lsp_init(PyLSPContext *ctx, CBMArena *arena, const char *source, int source_len,
355+
const CBMTypeRegistry *registry, const char *module_qn,
356+
CBMResolvedCallArray *out) {
357+
py_lsp_init_for_stage(ctx, arena, source, source_len, registry, module_qn, out, "context");
358+
}
359+
320360
void py_lsp_add_import(PyLSPContext *ctx, const char *local_name, const char *module_qn) {
321361
if (!ctx || !local_name || !module_qn)
322362
return;
@@ -2952,17 +2992,18 @@ static void py_emit_call_for(PyLSPContext *ctx, TSNode call_node) {
29522992
// segment) and look up "<root>.<mod>". A genuinely-external module
29532993
// (requests, os) has no such project def, so it correctly stays
29542994
// lsp_module_attr_unresolved.
2955-
if (mod && ctx->module_qn) {
2956-
const char *last_dot = strrchr(ctx->module_qn, '.');
2957-
if (last_dot && last_dot > ctx->module_qn) {
2958-
size_t root_len = (size_t)(last_dot - ctx->module_qn);
2995+
if (mod && ctx->file_module_qn) {
2996+
const char *last_dot = strrchr(ctx->file_module_qn, '.');
2997+
if (last_dot && last_dot > ctx->file_module_qn) {
2998+
size_t root_len = (size_t)(last_dot - ctx->file_module_qn);
29592999
// Skip if mod is already rooted under the project to avoid
29603000
// "<root>.<root>.mod".
2961-
if (!(strncmp(mod, ctx->module_qn, root_len) == 0 && mod[root_len] == '.')) {
3001+
if (!(strncmp(mod, ctx->file_module_qn, root_len) == 0 &&
3002+
mod[root_len] == '.')) {
29623003
char *qual_mod =
29633004
(char *)cbm_arena_alloc(ctx->arena, root_len + 1 + strlen(mod) + 1);
29643005
if (qual_mod) {
2965-
memcpy(qual_mod, ctx->module_qn, root_len);
3006+
memcpy(qual_mod, ctx->file_module_qn, root_len);
29663007
qual_mod[root_len] = '.';
29673008
strcpy(qual_mod + root_len + 1, mod);
29683009
const CBMRegisteredFunc *qf =
@@ -4421,6 +4462,7 @@ static void py_bind_external_module_classes(PyLSPContext *ctx, const PyKids *rk)
44214462
return;
44224463
}
44234464
size_t prefix_len = strlen(ctx->module_qn);
4465+
bool package_scope = ctx->file_module_qn && strcmp(ctx->file_module_qn, ctx->module_qn) != 0;
44244466
CBMTypeShortIter all_types;
44254467
cbm_registry_all_types_chain(ctx->registry, &all_types);
44264468
for (int i = -1; (i = cbm_type_short_iter_next(&all_types)) >= 0;) {
@@ -4431,6 +4473,8 @@ static void py_bind_external_module_classes(PyLSPContext *ctx, const PyKids *rk)
44314473
cbm_scope_lookup_local(defined, name)) {
44324474
continue;
44334475
}
4476+
if (package_scope && strchr(qn + prefix_len + 1, '.'))
4477+
continue; /* submodule/nested classes are not package globals */
44344478
py_scope_bind(ctx, name, cbm_type_named(ctx->arena, qn));
44354479
}
44364480
}
@@ -4935,7 +4979,7 @@ void py_lsp_process_file(PyLSPContext *ctx, TSNode root) {
49354979
const char *prev_func = ctx->enclosing_func_qn;
49364980
/* Extraction uses the file's Module QN at top level. Keep the caller
49374981
* arena-owned for cross-file APIs whose module_qn input may be temporary. */
4938-
ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s", ctx->module_qn);
4982+
ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s", ctx->file_module_qn);
49394983
// Pass 1: execute top-level binding effects in source order. Function
49404984
// bodies remain deferred until the final module scope has been assembled.
49414985
for (uint32_t i = 0; i < nc && !ctx->eval_failure; i++) {
@@ -5169,6 +5213,16 @@ void cbm_run_py_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
51695213
if (!arena || !result || result->lsp_skipped)
51705214
return;
51715215

5216+
CBMTypeRegistry reg;
5217+
cbm_registry_init(&reg, arena);
5218+
PyLSPContext ctx;
5219+
py_lsp_init_for_stage(&ctx, arena, source, source_len, &reg, result->module_qn,
5220+
&result->resolved_calls, "raw");
5221+
if (ctx.eval_failure) {
5222+
cbm_py_lsp_record_failure(result, ctx.eval_failure);
5223+
return;
5224+
}
5225+
51725226
/* Inject minimal builtin definitions as real graph nodes (builtins.len,
51735227
* builtins.str, builtins.str.upper, ...). The typeshed registry already
51745228
* RESOLVES builtin calls (emitting the strategy + a "builtins.*" callee_qn),
@@ -5178,12 +5232,9 @@ void cbm_run_py_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
51785232
* builtin/constructor/method edges point at. Upsert dedups by QN. */
51795233
py_builtins_inject_defs(result, arena);
51805234

5181-
CBMTypeRegistry reg;
5182-
cbm_registry_init(&reg, arena);
5183-
51845235
cbm_python_stdlib_register(&reg, arena);
51855236

5186-
const char *module_qn = result->module_qn;
5237+
const char *module_qn = ctx.module_qn;
51875238

51885239
// Register the file's own definitions so calls inside this file can
51895240
// resolve via the registry.
@@ -5204,8 +5255,6 @@ void cbm_run_py_lsp(CBMArena *arena, CBMFileResult *result, const char *source,
52045255
reg.index_first_registered = true;
52055256
cbm_registry_finalize_into(&reg, &idx_arena);
52065257

5207-
PyLSPContext ctx;
5208-
py_lsp_init(&ctx, arena, source, source_len, &reg, module_qn, &result->resolved_calls);
52095258
/* Let the resolver inject synthetic syntactic calls for operator/subscript
52105259
* dunder desugaring so those recovered calls reach the CALLS-edge pipeline. */
52115260
ctx.syn_calls = &result->calls;
@@ -5303,7 +5352,7 @@ static void py_split_field_defs(CBMArena *arena, const char *field_defs, const c
53035352

53045353
/* Build a registry from CBMLSPDef[] supplied by the caller — covers both
53055354
* the source file's own defs and cross-file referenced defs. */
5306-
static void py_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeRegistry *reg,
5355+
static bool py_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeRegistry *reg,
53075356
CBMLSPDef *defs, int def_count) {
53085357
/* Pass 1: types only — the method pass probes the registry per Method def
53095358
* (receiver auto-registration), which is a LINEAR scan pre-finalize:
@@ -5367,7 +5416,13 @@ static void py_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeRe
53675416
ret_types[n] = NULL;
53685417
}
53695418
}
5370-
PySignatureParamParserContext parser_ctx = {.module_qn = d->def_module_qn};
5419+
const char *symbol_scope = d->def_module_qn;
5420+
/* Fallback definitions can be mixed-language. Only authoritative
5421+
* Python metadata opts into package scope normalization. */
5422+
if (d->lang == CBM_LANG_PYTHON &&
5423+
!py_symbol_scope(arena, d->def_module_qn, "register", &symbol_scope))
5424+
return false;
5425+
PySignatureParamParserContext parser_ctx = {.module_qn = symbol_scope};
53715426
const CBMType **param_types = cbm_type_materialize_signature_params(
53725427
arena, d->signature_param_types, d->signature_param_count,
53735428
py_signature_param_type_adapter, &parser_ctx);
@@ -5387,8 +5442,16 @@ static void py_register_lsp_defs(CBMArena *arena, CBMArena *idx_arena, CBMTypeRe
53875442
cbm_registry_add_func(reg, rf);
53885443
}
53895444
}
5445+
return true;
53905446
}
53915447

5448+
#ifdef CBM_ENABLE_TEST_SEAMS
5449+
bool cbm_py_lsp_test_register_defs(CBMArena *arena, CBMTypeRegistry *reg, CBMLSPDef *defs,
5450+
int def_count) {
5451+
return py_register_lsp_defs(arena, NULL, reg, defs, def_count);
5452+
}
5453+
#endif
5454+
53925455
bool cbm_run_py_lsp_cross(CBMArena *arena, const char *source, int source_len,
53935456
const char *module_qn, CBMLSPDef *defs, int def_count,
53945457
const char **import_names, const char **import_qns, int import_count,
@@ -5431,15 +5494,20 @@ CBMLSPStatus cbm_run_py_lsp_cross_status(CBMArena *arena, const char *source, in
54315494
/* Index allocations go to a per-call scratch arena (see php_lsp_cross). */
54325495
CBMArena idx_arena;
54335496
cbm_arena_init(&idx_arena);
5434-
py_register_lsp_defs(arena, &idx_arena, &reg, defs, def_count);
5497+
PyLSPContext ctx;
5498+
py_lsp_init_for_stage(&ctx, arena, source, source_len, &reg, module_qn, out, "cross");
5499+
if (ctx.eval_failure)
5500+
goto cross_cleanup;
5501+
if (!py_register_lsp_defs(arena, &idx_arena, &reg, defs, def_count)) {
5502+
ctx.eval_failure = CBM_LSP_SCOPE_FAILED;
5503+
goto cross_cleanup;
5504+
}
54355505
py_mark_ambiguous_callable_bindings(&reg);
54365506

54375507
/* Finalize registry — O(1) lookups. See go_lsp.c "3c. Finalize"
54385508
* comment for the rationale. */
54395509
cbm_registry_finalize_into(&reg, &idx_arena);
54405510

5441-
PyLSPContext ctx;
5442-
py_lsp_init(&ctx, arena, source, source_len, &reg, module_qn, out);
54435511
ctx.syn_calls = synthetic_calls;
54445512
for (int i = 0; i < import_count; i++) {
54455513
if (import_names && import_qns && import_names[i] && import_qns[i]) {
@@ -5448,6 +5516,7 @@ CBMLSPStatus cbm_run_py_lsp_cross_status(CBMArena *arena, const char *source, in
54485516
}
54495517
py_memo_test_stage(&ctx, "cross");
54505518
py_lsp_process_file(&ctx, root);
5519+
cross_cleanup:
54515520
cbm_arena_destroy(&idx_arena);
54525521

54535522
if (owns_tree && tree)
@@ -5473,7 +5542,8 @@ CBMTypeRegistry *cbm_py_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, i
54735542
if (d->lang != CBM_LANG_PYTHON)
54745543
continue;
54755544
/* Reuse the existing register fn on a single-def slice (n=1 inline). */
5476-
py_register_lsp_defs(arena, NULL, reg, d, 1);
5545+
if (!py_register_lsp_defs(arena, NULL, reg, d, 1))
5546+
return NULL; /* caller owns the shared arena and other registries */
54775547
}
54785548

54795549
py_mark_ambiguous_callable_bindings(reg);
@@ -5517,7 +5587,9 @@ CBMLSPStatus cbm_run_py_lsp_cross_with_registry_status(
55175587
TSNode root = ts_tree_root_node(tree);
55185588

55195589
PyLSPContext ctx;
5520-
py_lsp_init(&ctx, arena, source, source_len, reg, module_qn, out);
5590+
py_lsp_init_for_stage(&ctx, arena, source, source_len, reg, module_qn, out, "cross");
5591+
if (ctx.eval_failure)
5592+
goto prebuilt_cleanup;
55215593
ctx.syn_calls = synthetic_calls;
55225594
for (int i = 0; i < import_count; i++) {
55235595
if (import_names && import_qns && import_names[i] && import_qns[i]) {
@@ -5526,7 +5598,7 @@ CBMLSPStatus cbm_run_py_lsp_cross_with_registry_status(
55265598
}
55275599
py_memo_test_stage(&ctx, "cross");
55285600
py_lsp_process_file(&ctx, root);
5529-
5601+
prebuilt_cleanup:
55305602
if (owns_tree && tree)
55315603
ts_tree_delete(tree);
55325604
if (parser)

‎internal/cbm/lsp/py_lsp.h‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ typedef struct {
6161
// caller QNs.
6262
const char *enclosing_func_qn;
6363
const char *enclosing_class_qn;
64-
const char *module_qn;
64+
const char *module_qn; // Python symbol scope (package __init__ omitted)
65+
const char *file_module_qn; // raw file identity; borrowed for this API call
6566

6667
// Output: resolved calls accumulate here.
6768
CBMResolvedCallArray *resolved_calls;
@@ -102,7 +103,7 @@ typedef struct {
102103
uint32_t type_cache_gen; // bumped on every scope mutation (O(1) flush)
103104
int eval_depth; // evaluator recursion depth (PY_LSP_MAX_EVAL_DEPTH)
104105
uint32_t eval_truncations; // depth-cap cutoff count — gates memo inserts
105-
CBMLSPStatus eval_failure; // sticky: memo/depth failure makes this walk incomplete
106+
CBMLSPStatus eval_failure; // sticky: explicit failure makes this walk incomplete
106107
// Pending links of the left-associative chains being evaluated
107108
// iteratively (operator, receiver, subscript, conditional and
108109
// parenthesis chains): one stack shared by nested evaluations.
@@ -173,10 +174,14 @@ void cbm_python_stdlib_register(CBMTypeRegistry *reg, CBMArena *arena);
173174

174175
#define CBM_PY_LSP_DEPTH_ERROR "Python expression recursion depth limit exceeded"
175176
#define CBM_PY_LSP_MEMO_ERROR "Python expression memo allocation or capacity failure"
177+
#define CBM_PY_LSP_SCOPE_ERROR "Python symbol scope allocation failure"
176178
void cbm_py_lsp_record_failure(CBMFileResult *result, CBMLSPStatus status);
177179
#ifdef CBM_ENABLE_TEST_SEAMS
178180
void cbm_py_lsp_test_memo_fail_after(int successful_allocations);
179181
void cbm_py_lsp_test_depth_fail(bool enabled);
182+
void cbm_py_lsp_test_scope_fail_after(int successful_allocations);
183+
bool cbm_py_lsp_test_register_defs(CBMArena *arena, CBMTypeRegistry *reg, CBMLSPDef *defs,
184+
int def_count);
180185
const CBMType *cbm_py_lsp_test_eval(PyLSPContext *ctx, TSNode node);
181186
void cbm_py_lsp_test_bind_external_classes(PyLSPContext *ctx, TSNode root);
182187
#endif

0 commit comments

Comments
 (0)