diff --git a/internal/cbm/lsp/c_lsp.c b/internal/cbm/lsp/c_lsp.c index 5437e3e99..77736a4f8 100644 --- a/internal/cbm/lsp/c_lsp.c +++ b/internal/cbm/lsp/c_lsp.c @@ -2818,7 +2818,10 @@ static const CBMRegisteredFunc *c_lookup_member_depth(CLSPContext *ctx, const ch const char *shortn = dot ? dot + 1 : type_qn; size_t slen = strlen(shortn); const char *best_qn = NULL; - for (int i = 0; i < ctx->registry->type_count; i++) { + CBMTypeNameIter it; + cbm_registry_types_by_short_name(ctx->registry, shortn, &it); + int i; + while ((i = cbm_type_name_iter_next(&it)) >= 0) { const char *q = ctx->registry->types[i].qualified_name; if (!q) { continue; @@ -5972,6 +5975,7 @@ CBMTypeRegistry *cbm_c_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, in c_register_lsp_defs(arena, reg, "", d, 1); } cbm_registry_finalize(reg); + cbm_registry_build_type_short_index(reg); reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */ return reg; } @@ -6055,6 +6059,7 @@ void cbm_run_c_lsp_cross(CBMArena *arena, const char *source, int source_len, co // Finalize registry — O(1) lookups. See go_lsp.c "3c. Finalize" // comment for the rationale (linear-scan fallback otherwise). cbm_registry_finalize(®); + cbm_registry_build_type_short_index(®); // Initialize context and run CLSPContext ctx; diff --git a/internal/cbm/lsp/type_registry.c b/internal/cbm/lsp/type_registry.c index 28353d591..ed7eddd9b 100644 --- a/internal/cbm/lsp/type_registry.c +++ b/internal/cbm/lsp/type_registry.c @@ -167,6 +167,56 @@ static void build_type_embed_index(CBMTypeRegistry *reg, CBMArena *idx_arena) { reg->type_embed_entry_count = idx; } +/* Optional index: final qualified-name segment -> chain of TYPE indices. + * Descending iteration plus prepend preserves ascending registry scan order. + * Consumers opt in after the QN index captures the finalized tail boundary. */ +void cbm_registry_build_type_short_index(CBMTypeRegistry *reg) { + if (!reg) + return; + reg->type_short_buckets = NULL; + reg->type_short_entries = NULL; + reg->type_short_bucket_count = 0; + if (!reg->arena || !reg->type_qn_buckets || reg->type_qn_bucket_count <= 0) + return; + CBMArena *idx_arena = reg->arena; + int count = 0; + for (int i = 0; i < reg->type_count; i++) { + if (reg->types[i].qualified_name) + count++; + } + if (count == 0) + return; + int bucket_count = next_pow2(count * 2); + if (bucket_count < 16) + bucket_count = 16; + int *buckets = (int *)cbm_arena_alloc(idx_arena, (size_t)bucket_count * sizeof(int)); + CBMRegistryHashEntry *entries = (CBMRegistryHashEntry *)cbm_arena_alloc( + idx_arena, (size_t)count * sizeof(CBMRegistryHashEntry)); + if (!buckets || !entries) + return; + for (int i = 0; i < bucket_count; i++) + buckets[i] = -1; + int idx = 0; + for (int i = reg->type_count - 1; i >= 0; i--) { + const char *qn = reg->types[i].qualified_name; + if (!qn) + continue; + const char *dot = strrchr(qn, '.'); + const char *short_name = dot ? dot + 1 : qn; + uint64_t h = fnv1a(short_name); + int slot = (int)(h & (uint64_t)(bucket_count - 1)); + entries[idx].hash = h; + entries[idx].payload_index = i; + entries[idx].next_index = buckets[slot]; + entries[idx].slot = slot; + buckets[slot] = idx; + idx++; + } + reg->type_short_buckets = buckets; + reg->type_short_entries = entries; + reg->type_short_bucket_count = bucket_count; +} + /* Index: short_name -> chain of FREE-function (receiver_type==NULL) indices. * Descending-iterate + prepend for ascending chain order (as above). */ static void build_ffunc_short_index(CBMTypeRegistry *reg, CBMArena *idx_arena) { @@ -208,6 +258,43 @@ static void build_ffunc_short_index(CBMTypeRegistry *reg, CBMArena *idx_arena) { reg->ffunc_short_entry_count = idx; } +void cbm_registry_types_by_short_name(const CBMTypeRegistry *reg, const char *short_name, + CBMTypeNameIter *out) { + out->reg = reg; + out->hash = fnv1a(short_name); + if (reg->type_qn_buckets && reg->type_qn_bucket_count > 0) { + if (reg->type_short_buckets && reg->type_short_bucket_count > 0) { + int slot = (int)(out->hash & (uint64_t)(reg->type_short_bucket_count - 1)); + out->chain_idx = reg->type_short_buckets[slot]; + out->tail_i = reg->type_qn_entry_count; + } else { + out->chain_idx = -1; + out->tail_i = 0; /* auxiliary-index allocation failed: preserve full-scan semantics */ + } + out->tail_end = reg->type_count; + } else { + out->chain_idx = -1; + out->tail_i = 0; + out->tail_end = reg->type_count; + } +} + +int cbm_type_name_iter_next(CBMTypeNameIter *it) { + const CBMTypeRegistry *reg = it->reg; + while (it->chain_idx >= 0) { + const CBMRegistryHashEntry *e = ®->type_short_entries[it->chain_idx]; + int p = e->payload_index; + uint64_t h = e->hash; + it->chain_idx = e->next_index; + if (h != it->hash) + continue; + return p; + } + if (it->tail_i < it->tail_end) + return it->tail_i++; + return -1; +} + void cbm_registry_types_by_embedded_bare(const CBMTypeRegistry *reg, const char *bare, CBMTypeEmbedIter *out) { out->reg = reg; diff --git a/internal/cbm/lsp/type_registry.h b/internal/cbm/lsp/type_registry.h index 154c37a00..9043dae80 100644 --- a/internal/cbm/lsp/type_registry.h +++ b/internal/cbm/lsp/type_registry.h @@ -111,9 +111,14 @@ typedef struct CBMTypeRegistry { int method_bucket_count; int method_entry_count; - // Auxiliary short-name / embedded-type indexes (built by finalize alongside the - // QN buckets). Turn the Rust trait- and free-function fallback scans from - // O(type_count)/O(func_count) into O(chain). Read-only after finalize. + // Auxiliary short-name / embedded-type indexes. The type short-name index is + // opt-in after finalize; the others are built by finalize. Current C/C++ cross + // registries opt in, while other languages incur no construction cost. + // Type short-name index: fnv1a(last-'.'-segment of qualified_name) -> chain + // of TYPE indices. payload_index = type index. + int *type_short_buckets; + CBMRegistryHashEntry *type_short_entries; + int type_short_bucket_count; // Embedded-type index: fnv1a(bare last-'.'-segment of each embedded_type) -> chain // of TYPE indices declaring it. payload_index = type index (a type may appear once // per embedded entry; consumers dedup adjacent same-type via the iterator). @@ -156,6 +161,11 @@ void cbm_registry_finalize(CBMTypeRegistry *reg); // there add GBs across a large repo (FastAPI incremental test: +1.1 GB RSS). void cbm_registry_finalize_into(CBMTypeRegistry *reg, CBMArena *idx_arena); +// Build the optional final-QN-segment type index. Call immediately after +// finalization. Consumers that do not opt in avoid its type_count-sized allocation +// and scan. Allocation failure preserves the iterator's linear fallback. +void cbm_registry_build_type_short_index(CBMTypeRegistry *reg); + // Register a function/method. void cbm_registry_add_func(CBMTypeRegistry *reg, CBMRegisteredFunc func); @@ -215,8 +225,24 @@ const CBMRegisteredFunc *cbm_registry_lookup_symbol_by_types(const CBMTypeRegist const CBMType **arg_types, int arg_count); -// --- Auxiliary index iterators (Rust trait / free-function fallback fast paths) --- +// --- Auxiliary index iterators (language fallback fast paths) --- // +// Iterate registry TYPE indices whose qualified-name final segment may equal +// `short_name`. When the optional index is present this walks its hash chain plus +// any post-finalize tail; otherwise it degrades to the original full scan. Results +// preserve ascending registry order. The index is a hash prefilter; callers must +// re-check their exact predicate, including tail entries. +typedef struct { + const CBMTypeRegistry *reg; + uint64_t hash; + int chain_idx; + int tail_i; + int tail_end; +} CBMTypeNameIter; +void cbm_registry_types_by_short_name(const CBMTypeRegistry *reg, const char *short_name, + CBMTypeNameIter *out); +int cbm_type_name_iter_next(CBMTypeNameIter *it); + // Iterate registry TYPE indices whose embedded_types contain an entry whose BARE // name (last '.'-segment) equals `bare`. On a finalized registry this walks the // embedded-type index plus any post-finalize tail; on an unfinalized registry it diff --git a/tests/test_c_lsp.c b/tests/test_c_lsp.c index db2ac2f2e..a9f3437a6 100644 --- a/tests/test_c_lsp.c +++ b/tests/test_c_lsp.c @@ -15250,12 +15250,9 @@ TEST(clsp_tier2_shared_registry_readonly_c) { PASS(); } -/* Direct guard for the finalize-time short-name / embedded-type indexes and their - * iterators (type_registry.c), which the Rust trait/free-func fast paths rely on. - * Verifies: embed index yields exactly the types whose embedded_types carry a - * matching BARE name, in ascending registry order, deduped when a type lists the - * same bare twice; free-func index yields only free funcs (receiver_type==NULL) with - * the given short_name, not methods. */ +/* Direct guard for the type-name / embedded-type / free-function registry + * indexes and their iterators (type_registry.c). Verifies that every iterator + * preserves ascending registry order, which is part of resolver tie-breaking. */ TEST(registry_short_name_indexes) { CBMArena arena; cbm_arena_init(&arena); @@ -15288,6 +15285,25 @@ TEST(registry_short_name_indexes) { t.short_name = "C"; t.embedded_types = c_emb; cbm_registry_add_type(®, t); + memset(&t, 0, sizeof(t)); + t.qualified_name = "other.Trait"; + t.short_name = "Trait"; + cbm_registry_add_type(®, t); + memset(&t, 0, sizeof(t)); + t.qualified_name = "Trait"; + t.short_name = "Trait"; + cbm_registry_add_type(®, t); + + /* Populate enough distinct names to force many hash-bucket collisions, with + * repeated short names spread across the registry. The differential check + * below compares the index against the exact former linear-scan order. */ + for (int i = 0; i < 512; i++) { + memset(&t, 0, sizeof(t)); + t.qualified_name = cbm_arena_sprintf(&arena, "bulk.mod%d.Name%d", i, i % 23); + ASSERT_NOT_NULL(t.qualified_name); + t.short_name = strrchr(t.qualified_name, '.') + 1; + cbm_registry_add_type(®, t); + } /* free func "helper" (x2 — different QNs), method "M.helper", free func "other". */ CBMRegisteredFunc f; @@ -15310,6 +15326,77 @@ TEST(registry_short_name_indexes) { cbm_registry_add_func(®, f); cbm_registry_finalize(®); + cbm_registry_build_type_short_index(®); + + /* Qualified-name bare segment "Trait": types 0, 4, then the bare-QN type + * 5. The iterator is a hash prefilter, so apply the exact predicate before + * asserting its order just like production consumers do. */ + CBMTypeNameIter nit; + cbm_registry_types_by_short_name(®, "Trait", &nit); + { + int expected[] = {0, 4, 5}; + int exact_count = 0; + int candidate; + while ((candidate = cbm_type_name_iter_next(&nit)) >= 0) { + const char *qn = reg.types[candidate].qualified_name; + const char *last_dot = qn ? strrchr(qn, '.') : NULL; + const char *candidate_short = last_dot ? last_dot + 1 : qn; + if (!candidate_short || strcmp(candidate_short, "Trait") != 0) + continue; + ASSERT_TRUE(exact_count < 3); + ASSERT_EQ(candidate, expected[exact_count++]); + } + ASSERT_EQ(exact_count, 3); + } + cbm_registry_types_by_short_name(®, "Missing", &nit); + { + int exact_count = 0; + int candidate; + while ((candidate = cbm_type_name_iter_next(&nit)) >= 0) { + const char *qn = reg.types[candidate].qualified_name; + const char *last_dot = qn ? strrchr(qn, '.') : NULL; + const char *candidate_short = last_dot ? last_dot + 1 : qn; + if (candidate_short && strcmp(candidate_short, "Missing") == 0) + exact_count++; + } + ASSERT_EQ(exact_count, 0); + } + + for (int name_i = 0; name_i < 23; name_i++) { + char short_name[32]; + snprintf(short_name, sizeof(short_name), "Name%d", name_i); + cbm_registry_types_by_short_name(®, short_name, &nit); + int linear_i = 0; + for (;;) { + int expected = -1; + while (linear_i < reg.type_count) { + int candidate = linear_i++; + const char *qn = reg.types[candidate].qualified_name; + const char *last_dot = qn ? strrchr(qn, '.') : NULL; + const char *candidate_short = last_dot ? last_dot + 1 : qn; + if (candidate_short && strcmp(candidate_short, short_name) == 0) { + expected = candidate; + break; + } + } + + int actual; + for (;;) { + actual = cbm_type_name_iter_next(&nit); + if (actual < 0) + break; + const char *qn = reg.types[actual].qualified_name; + const char *last_dot = qn ? strrchr(qn, '.') : NULL; + const char *candidate_short = last_dot ? last_dot + 1 : qn; + if (candidate_short && strcmp(candidate_short, short_name) == 0) + break; + } + + ASSERT_EQ(actual, expected); + if (expected < 0) + break; + } + } /* Embed index for bare "Trait": types 1 (A) then 2 (B), ascending, B once. */ CBMTypeEmbedIter it; @@ -15339,6 +15426,70 @@ TEST(registry_short_name_indexes) { ASSERT_EQ(cbm_free_func_iter_next(&fit), 3); ASSERT_EQ(cbm_free_func_iter_next(&fit), -1); + /* A type appended after finalize is covered by the iterator tail. If the + * auxiliary allocation is unavailable, the iterator falls back to the same + * complete linear scan rather than silently dropping candidates. Keep this + * after the other iterator assertions because their tails intentionally + * expose post-finalize candidates for caller-side exact filtering too. */ + int tail_type_i = reg.type_count; + memset(&t, 0, sizeof(t)); + t.qualified_name = "tail.Trait"; + t.short_name = "Trait"; + cbm_registry_add_type(®, t); + cbm_registry_types_by_short_name(®, "Trait", &nit); + ASSERT_EQ(cbm_type_name_iter_next(&nit), 0); + ASSERT_EQ(cbm_type_name_iter_next(&nit), 4); + ASSERT_EQ(cbm_type_name_iter_next(&nit), 5); + ASSERT_EQ(cbm_type_name_iter_next(&nit), tail_type_i); + ASSERT_EQ(cbm_type_name_iter_next(&nit), -1); + + int *saved_type_short_buckets = reg.type_short_buckets; + reg.type_short_buckets = NULL; + cbm_registry_types_by_short_name(®, "Trait", &nit); + int fallback_expected[] = {0, 4, 5, tail_type_i}; + int fallback_count = 0; + int candidate; + while ((candidate = cbm_type_name_iter_next(&nit)) >= 0) { + const char *qn = reg.types[candidate].qualified_name; + const char *last_dot = qn ? strrchr(qn, '.') : NULL; + const char *candidate_short = last_dot ? last_dot + 1 : qn; + if (!candidate_short || strcmp(candidate_short, "Trait") != 0) + continue; + ASSERT_TRUE(fallback_count < 4); + ASSERT_EQ(candidate, fallback_expected[fallback_count++]); + } + ASSERT_EQ(fallback_count, 4); + reg.type_short_buckets = saved_type_short_buckets; + + /* A failed rebuild must discard the previous auxiliary index. Re-finalize + * after adding the tail type so the QN boundary advances, then force arena + * exhaustion for the optional rebuild. The iterator must fall back to the + * complete linear scan instead of retaining the stale pre-tail index. */ + cbm_registry_finalize(®); + int saved_nblocks = arena.nblocks; + size_t saved_used = arena.used; + arena.nblocks = CBM_ARENA_MAX_BLOCKS; + arena.used = arena.block_size; + cbm_registry_build_type_short_index(®); + arena.nblocks = saved_nblocks; + arena.used = saved_used; + ASSERT_NULL(reg.type_short_buckets); + ASSERT_NULL(reg.type_short_entries); + ASSERT_EQ(reg.type_short_bucket_count, 0); + + cbm_registry_types_by_short_name(®, "Trait", &nit); + fallback_count = 0; + while ((candidate = cbm_type_name_iter_next(&nit)) >= 0) { + const char *qn = reg.types[candidate].qualified_name; + const char *last_dot = qn ? strrchr(qn, '.') : NULL; + const char *candidate_short = last_dot ? last_dot + 1 : qn; + if (!candidate_short || strcmp(candidate_short, "Trait") != 0) + continue; + ASSERT_TRUE(fallback_count < 4); + ASSERT_EQ(candidate, fallback_expected[fallback_count++]); + } + ASSERT_EQ(fallback_count, 4); + cbm_arena_destroy(&arena); PASS(); }