@@ -64,6 +64,7 @@ void c_lsp_init(CLSPContext *ctx, CBMArena *arena, const char *source, int sourc
6464 ctx -> source_len = source_len ;
6565 ctx -> registry = registry ;
6666 ctx -> module_qn = module_qn ;
67+ ctx -> module_qn_len = module_qn ? strlen (module_qn ) : 0 ;
6768 ctx -> cpp_mode = cpp_mode ;
6869 ctx -> resolved_calls = out ;
6970 ctx -> current_scope = cbm_scope_push (arena , NULL );
@@ -2506,28 +2507,148 @@ static const CBMType *c_eval_expr_type_inner(CLSPContext *ctx, TSNode node) {
25062507// c_lookup_member: method/field lookup with base class traversal
25072508// ============================================================================
25082509
2510+ // Build "<module_qn>.<type_qn>" into a caller-provided stack buffer, replacing the
2511+ // vsnprintf("%s.%s") path (whose %s does a strlen of both args) that otherwise
2512+ // dominates the kernel cross-file resolve miss cascade. Uses the cached module_qn_len.
2513+ // Returns buf on success, or NULL if the result would not fit (caller falls back to
2514+ // cbm_arena_sprintf). The buffer is only read by the immediate registry lookup — a
2515+ // stack lifetime is sufficient (the QN is never retained).
2516+ static const char * c_build_module_prefixed (const CLSPContext * ctx , const char * type_qn , char * buf ,
2517+ size_t bufsz ) {
2518+ size_t mlen = ctx -> module_qn_len ;
2519+ size_t tlen = strlen (type_qn );
2520+ if (mlen + 1 + tlen + 1 > bufsz )
2521+ return NULL ; // would truncate → fall back to arena sprintf
2522+ memcpy (buf , ctx -> module_qn , mlen );
2523+ buf [mlen ] = '.' ;
2524+ memcpy (buf + mlen + 1 , type_qn , tlen );
2525+ buf [mlen + 1 + tlen ] = '\0' ;
2526+ return buf ;
2527+ }
2528+
2529+ // FNV-1a over (type_qn, 0xff separator, member_name) for the negative-lookup memo.
2530+ // 0 is remapped to 1 so it can serve as the empty-slot sentinel.
2531+ static uint64_t c_neg_memo_hash (const char * type_qn , const char * member_name ) {
2532+ uint64_t h = 1469598103934665603ULL ; // FNV-1a offset basis
2533+ for (const unsigned char * p = (const unsigned char * )type_qn ; * p ; p ++ ) {
2534+ h ^= (uint64_t )* p ;
2535+ h *= 1099511628211ULL ;
2536+ }
2537+ h ^= (uint64_t )0xffu ;
2538+ h *= 1099511628211ULL ;
2539+ for (const unsigned char * p = (const unsigned char * )member_name ; * p ; p ++ ) {
2540+ h ^= (uint64_t )* p ;
2541+ h *= 1099511628211ULL ;
2542+ }
2543+ return h ? h : 1ULL ;
2544+ }
2545+
2546+ static bool c_neg_memo_contains (const CLSPContext * ctx , uint64_t h ) {
2547+ if (!ctx -> neg_memo || ctx -> neg_memo_cap == 0 )
2548+ return false;
2549+ uint64_t mask = (uint64_t )ctx -> neg_memo_cap - 1 ;
2550+ for (uint64_t i = h & mask ;; i = (i + 1 ) & mask ) {
2551+ uint64_t slot = ctx -> neg_memo [i ];
2552+ if (slot == 0 )
2553+ return false; // empty slot → not present
2554+ if (slot == h )
2555+ return true;
2556+ }
2557+ }
2558+
2559+ static void c_neg_memo_insert (CLSPContext * ctx , uint64_t h ) {
2560+ // Lazy alloc / grow-by-rehash at 70% load. On OOM, silently disable the memo
2561+ // (correctness is unaffected — the full cascade still runs).
2562+ if (ctx -> neg_memo == NULL ) {
2563+ uint64_t * nm = (uint64_t * )calloc (1024 , sizeof (uint64_t ));
2564+ if (!nm )
2565+ return ;
2566+ ctx -> neg_memo = nm ;
2567+ ctx -> neg_memo_cap = 1024 ;
2568+ ctx -> neg_memo_count = 0 ;
2569+ } else if ((ctx -> neg_memo_count + 1 ) * 10 >= ctx -> neg_memo_cap * 7 ) {
2570+ int new_cap = ctx -> neg_memo_cap * 2 ;
2571+ uint64_t * nm = (uint64_t * )calloc ((size_t )new_cap , sizeof (uint64_t ));
2572+ if (nm ) {
2573+ uint64_t nmask = (uint64_t )new_cap - 1 ;
2574+ for (int j = 0 ; j < ctx -> neg_memo_cap ; j ++ ) {
2575+ uint64_t v = ctx -> neg_memo [j ];
2576+ if (v == 0 )
2577+ continue ;
2578+ uint64_t k = v & nmask ;
2579+ while (nm [k ] != 0 )
2580+ k = (k + 1 ) & nmask ;
2581+ nm [k ] = v ;
2582+ }
2583+ free (ctx -> neg_memo );
2584+ ctx -> neg_memo = nm ;
2585+ ctx -> neg_memo_cap = new_cap ;
2586+ }
2587+ // if calloc failed: keep the existing table (load may exceed 70%, still correct)
2588+ }
2589+ uint64_t mask = (uint64_t )ctx -> neg_memo_cap - 1 ;
2590+ for (uint64_t i = h & mask ;; i = (i + 1 ) & mask ) {
2591+ if (ctx -> neg_memo [i ] == 0 ) {
2592+ ctx -> neg_memo [i ] = h ;
2593+ ctx -> neg_memo_count ++ ;
2594+ return ;
2595+ }
2596+ if (ctx -> neg_memo [i ] == h )
2597+ return ; // already recorded
2598+ }
2599+ }
2600+
2601+ static void c_neg_memo_free (CLSPContext * ctx ) {
2602+ if (ctx -> neg_memo )
2603+ free (ctx -> neg_memo );
2604+ ctx -> neg_memo = NULL ;
2605+ ctx -> neg_memo_cap = 0 ;
2606+ ctx -> neg_memo_count = 0 ;
2607+ }
2608+
25092609static const CBMRegisteredFunc * c_lookup_member_depth (CLSPContext * ctx , const char * type_qn ,
25102610 const char * member_name , int depth ) {
25112611 if (!type_qn || !member_name )
25122612 return NULL ;
25132613 if (depth > CBM_LSP_MAX_LOOKUP_DEPTH )
25142614 return NULL ;
25152615
2516- // Direct method lookup
2616+ // Direct method lookup. Runs FIRST, before consulting the memo, so a real
2617+ // direct-resolvable member (incl. a hash-collision victim, or one registered
2618+ // mid-file) can never be skipped: it is returned here regardless of the memo.
25172619 const CBMRegisteredFunc * f = cbm_registry_lookup_method (ctx -> registry , type_qn , member_name );
25182620 if (f )
25192621 return f ;
25202622
2521- // Try module-prefixed QN (e.g., "Container" -> "test.main.Container")
2522- if (ctx -> module_qn ) {
2523- const char * prefixed = cbm_arena_sprintf (ctx -> arena , "%s.%s" , ctx -> module_qn , type_qn );
2623+ // Negative-lookup memo (depth==0, shared read-only registry only). A recorded
2624+ // hit means this exact (type_qn, member) already failed the whole cascade. Under
2625+ // the sealed Tier-2 registry the module-prefix (below), base-class and short-name
2626+ // cascades are pure, immutable functions of (type_qn, registry), so they are
2627+ // provably still NULL and can be skipped. Only the SCOPE-ALIAS path is
2628+ // context-dependent, so it is still evaluated below before we trust the memo.
2629+ // The direct lookup above already served as the collision/staleness verification.
2630+ bool neg_memo_hit = false;
2631+ uint64_t neg_h = 0 ;
2632+ if (depth == 0 && ctx -> registry_shared ) {
2633+ neg_h = c_neg_memo_hash (type_qn , member_name );
2634+ neg_memo_hit = c_neg_memo_contains (ctx , neg_h );
2635+ }
2636+
2637+ // Try module-prefixed QN (e.g., "Container" -> "test.main.Container").
2638+ // Skipped on a memo hit: provably NULL under the read-only shared registry.
2639+ if (!neg_memo_hit && ctx -> module_qn ) {
2640+ char sbuf [1024 ];
2641+ const char * prefixed = c_build_module_prefixed (ctx , type_qn , sbuf , sizeof (sbuf ));
2642+ if (!prefixed )
2643+ prefixed = cbm_arena_sprintf (ctx -> arena , "%s.%s" , ctx -> module_qn , type_qn );
25242644 f = cbm_registry_lookup_method (ctx -> registry , prefixed , member_name );
25252645 if (f )
25262646 return f ;
25272647 }
25282648
25292649 // Scope-based alias: using Vec = std::vector<T>;
2530- // Vec is in scope as ALIAS → follow to underlying type's QN
2650+ // Vec is in scope as ALIAS → follow to underlying type's QN.
2651+ // ALWAYS evaluated (scope is context-dependent — not covered by the memo).
25312652 {
25322653 const CBMType * scoped = cbm_scope_lookup (ctx -> current_scope , type_qn );
25332654 if (scoped && scoped -> kind == CBM_TYPE_ALIAS ) {
@@ -2543,11 +2664,21 @@ static const CBMRegisteredFunc *c_lookup_member_depth(CLSPContext *ctx, const ch
25432664 }
25442665 }
25452666
2667+ // Memo hit and the scope-alias path also missed: the remaining base-class and
2668+ // short-name cascades are registry-only and provably NULL → skip them (this is
2669+ // where the O(type_count) short-name scan is avoided). Idempotent re-insert is
2670+ // unnecessary since neg_h is already present.
2671+ if (neg_memo_hit )
2672+ return NULL ;
2673+
25462674 // Check registered type for alias and base classes
25472675 const CBMRegisteredType * rt = cbm_registry_lookup_type (ctx -> registry , type_qn );
25482676 if (!rt && ctx -> module_qn ) {
2549- rt = cbm_registry_lookup_type (
2550- ctx -> registry , cbm_arena_sprintf (ctx -> arena , "%s.%s" , ctx -> module_qn , type_qn ));
2677+ char sbuf [1024 ];
2678+ const char * prefixed = c_build_module_prefixed (ctx , type_qn , sbuf , sizeof (sbuf ));
2679+ if (!prefixed )
2680+ prefixed = cbm_arena_sprintf (ctx -> arena , "%s.%s" , ctx -> module_qn , type_qn );
2681+ rt = cbm_registry_lookup_type (ctx -> registry , prefixed );
25512682 }
25522683 if (rt ) {
25532684 // Alias chain
@@ -2606,6 +2737,10 @@ static const CBMRegisteredFunc *c_lookup_member_depth(CLSPContext *ctx, const ch
26062737 }
26072738 }
26082739
2740+ // Whole cascade missed: record the negative result so a repeat of this exact
2741+ // (type_qn, member) can skip the module-prefix/base-class/short-name work.
2742+ if (depth == 0 && ctx -> registry_shared )
2743+ c_neg_memo_insert (ctx , neg_h );
26092744 return NULL ;
26102745}
26112746
@@ -4845,6 +4980,10 @@ __attribute__((no_sanitize("address"))) void c_lsp_process_file(CLSPContext *ctx
48454980 child = kids [i ];
48464981 c_process_body_child (ctx , child );
48474982 }
4983+
4984+ // Release the per-file negative-lookup memo (malloc-owned; only allocated in
4985+ // shared-registry mode). No-op when it was never populated.
4986+ c_neg_memo_free (ctx );
48484987}
48494988
48504989// ============================================================================
0 commit comments