4343#ifdef CBM_ENABLE_TEST_SEAMS
4444static _Thread_local int py_test_memo_allocations_left = -1 ;
4545static _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+ }
4650void 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+
320360void 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+
53925455bool 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 )
0 commit comments