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
4449static _Thread_local int py_test_memo_allocations_left = -1 ;
4550static _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+
15911593static 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 ;
0 commit comments