From da61c81af243e3277e3089fb474ee5c08fe5c148 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Tue, 1 Sep 2026 18:43:37 +0200 Subject: [PATCH] fix(lsp): unify the two type short-name indexes that broke the main build main does not compile. internal/cbm/lsp/type_registry.h declares type_short_buckets, type_short_entries and type_short_bucket_count twice, and cbm_registry_types_by_short_name twice with different parameter types. Two changes implemented the same feature independently: a4c0ffbc perf(cs): eliminate the C# corpus-proportional scans (2026-08-16, ours) CBMTypeShortIter, cbm_type_short_iter_next 793716dc perf(lsp): index C++ short-name type lookups (PR #1679, astandrik) CBMTypeNameIter, cbm_type_name_iter_next They landed in different hunks of the same struct and the same header section, so git merged them with no conflict and nothing failed until the merge commit was built. This is the failure mode where a clean merge is not evidence that two changes compose. The two iterators are structurally identical -- same five fields, byte-identical _next bodies reading the same reg->type_short_entries chain. They differ in one place, and it matters: #1679 on aux-index allocation failure, sets tail_i = 0, so the iterator degrades to a full types[] scan and the candidate set is unchanged. a4c0ffbc sets tail_i = reg->type_qn_entry_count unconditionally, so on that same failure it SKIPS the first type_qn_entry_count types and silently yields an incomplete candidate set. So this keeps a4c0ffbc's names -- CBMTypeShortIter reads correctly beside its type_short_* members and beside CBMTypeEmbedIter and CBMFreeFuncIter -- and #1679's body, which is the correct one. The surviving declaration carries both rationales: the C++ short-name lookup and cs_resolve_type_name's step-9 fallback are now stated as two consumers of one index, which is what they are. The merge also broke two comment/member pairings, repaired here: the duplicated members had displaced the free-function index members from under their own comment, and the CBMTypeShortIter block had been interleaved between CBMFreeFuncIter's typedef and its declarations. No behaviour change for either consumer beyond the allocation-failure path, which strictly gains coverage for C#. c_lsp.c and cs_lsp.c now call one function. #1679's tests, including the fault-injection case that NULLs the buckets and asserts the full-scan fallback, carry over unchanged apart from the type name. Local: c_lsp, cs_lsp and registry -- 881 passed, 0 failed. Co-Authored-By: astandrik Signed-off-by: Martin Vogel --- internal/cbm/lsp/c_lsp.c | 4 ++-- internal/cbm/lsp/type_registry.c | 40 ++------------------------------ internal/cbm/lsp/type_registry.h | 29 ++++++----------------- tests/test_c_lsp.c | 22 +++++++++--------- 4 files changed, 22 insertions(+), 73 deletions(-) diff --git a/internal/cbm/lsp/c_lsp.c b/internal/cbm/lsp/c_lsp.c index 77736a4f8..c5991561e 100644 --- a/internal/cbm/lsp/c_lsp.c +++ b/internal/cbm/lsp/c_lsp.c @@ -2818,10 +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; - CBMTypeNameIter it; + CBMTypeShortIter it; cbm_registry_types_by_short_name(ctx->registry, shortn, &it); int i; - while ((i = cbm_type_name_iter_next(&it)) >= 0) { + while ((i = cbm_type_short_iter_next(&it)) >= 0) { const char *q = ctx->registry->types[i].qualified_name; if (!q) { continue; diff --git a/internal/cbm/lsp/type_registry.c b/internal/cbm/lsp/type_registry.c index c0136b7ed..30c551899 100644 --- a/internal/cbm/lsp/type_registry.c +++ b/internal/cbm/lsp/type_registry.c @@ -299,7 +299,7 @@ static void build_ffunc_short_index(CBMTypeRegistry *reg, CBMArena *idx_arena) { } void cbm_registry_types_by_short_name(const CBMTypeRegistry *reg, const char *short_name, - CBMTypeNameIter *out) { + CBMTypeShortIter *out) { out->reg = reg; out->hash = fnv1a(short_name); if (reg->type_qn_buckets && reg->type_qn_bucket_count > 0) { @@ -319,7 +319,7 @@ void cbm_registry_types_by_short_name(const CBMTypeRegistry *reg, const char *sh } } -int cbm_type_name_iter_next(CBMTypeNameIter *it) { +int cbm_type_short_iter_next(CBMTypeShortIter *it) { const CBMTypeRegistry *reg = it->reg; while (it->chain_idx >= 0) { const CBMRegistryHashEntry *e = ®->type_short_entries[it->chain_idx]; @@ -413,42 +413,6 @@ int cbm_free_func_iter_next(CBMFreeFuncIter *it) { return -1; } -void cbm_registry_types_by_short_name(const CBMTypeRegistry *reg, const char *short_name, - CBMTypeShortIter *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]; - } else { - out->chain_idx = -1; - } - out->tail_i = reg->type_qn_entry_count; - out->tail_end = reg->type_count; - } else { - out->chain_idx = -1; - out->tail_i = 0; - out->tail_end = reg->type_count; - } -} - -int cbm_type_short_iter_next(CBMTypeShortIter *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_methods(const CBMTypeRegistry *reg, const char *receiver_qn, const char *method_name, CBMMethodIter *out) { memset(out, 0, sizeof(*out)); diff --git a/internal/cbm/lsp/type_registry.h b/internal/cbm/lsp/type_registry.h index 074e2bf7c..7f7737448 100644 --- a/internal/cbm/lsp/type_registry.h +++ b/internal/cbm/lsp/type_registry.h @@ -119,6 +119,7 @@ typedef struct CBMTypeRegistry { int *type_short_buckets; CBMRegistryHashEntry *type_short_entries; int type_short_bucket_count; + int type_short_entry_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). @@ -128,10 +129,6 @@ typedef struct CBMTypeRegistry { int type_embed_entry_count; // Free-function short-name index: fnv1a(short_name) -> chain of FREE-function // (receiver_type==NULL) indices. payload_index = func index. - int *type_short_buckets; - CBMRegistryHashEntry *type_short_entries; - int type_short_bucket_count; - int type_short_entry_count; int *ffunc_short_buckets; CBMRegistryHashEntry *ffunc_short_entries; int ffunc_short_bucket_count; @@ -236,16 +233,19 @@ const CBMRegisteredFunc *cbm_registry_lookup_symbol_by_types(const CBMTypeRegist // 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. +// Built by finalize into type_short_buckets. Shared by the C++ short-name lookup +// and by cs_resolve_type_name's step-9 fallback, which scanned type_count per +// unresolved name — quadratic against the shared Tier-2 registry. typedef struct { const CBMTypeRegistry *reg; uint64_t hash; int chain_idx; int tail_i; int tail_end; -} CBMTypeNameIter; +} CBMTypeShortIter; void cbm_registry_types_by_short_name(const CBMTypeRegistry *reg, const char *short_name, - CBMTypeNameIter *out); -int cbm_type_name_iter_next(CBMTypeNameIter *it); + CBMTypeShortIter *out); +int cbm_type_short_iter_next(CBMTypeShortIter *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 @@ -278,21 +278,6 @@ typedef struct { int tail_i; int tail_end; } CBMFreeFuncIter; -/* Iterate TYPE indices sharing a short name — the type-side twin of the free- - * function iterator. Built by finalize into type_short_buckets; degrades to a - * full types[] scan on an unfinalized registry (same correctness, old cost). - * Added for cs_resolve_type_name's step-9 fallback, which scanned type_count - * per unresolved name — quadratic against the shared Tier-2 registry. */ -typedef struct { - const CBMTypeRegistry *reg; - uint64_t hash; - int chain_idx; - int tail_i; - int tail_end; -} CBMTypeShortIter; -void cbm_registry_types_by_short_name(const CBMTypeRegistry *reg, const char *short_name, - CBMTypeShortIter *out); -int cbm_type_short_iter_next(CBMTypeShortIter *it); void cbm_registry_free_funcs_by_short_name(const CBMTypeRegistry *reg, const char *short_name, CBMFreeFuncIter *out); diff --git a/tests/test_c_lsp.c b/tests/test_c_lsp.c index a9f3437a6..0ba216ce0 100644 --- a/tests/test_c_lsp.c +++ b/tests/test_c_lsp.c @@ -15331,13 +15331,13 @@ TEST(registry_short_name_indexes) { /* 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; + CBMTypeShortIter 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) { + while ((candidate = cbm_type_short_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; @@ -15352,7 +15352,7 @@ TEST(registry_short_name_indexes) { { int exact_count = 0; int candidate; - while ((candidate = cbm_type_name_iter_next(&nit)) >= 0) { + while ((candidate = cbm_type_short_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; @@ -15382,7 +15382,7 @@ TEST(registry_short_name_indexes) { int actual; for (;;) { - actual = cbm_type_name_iter_next(&nit); + actual = cbm_type_short_iter_next(&nit); if (actual < 0) break; const char *qn = reg.types[actual].qualified_name; @@ -15437,11 +15437,11 @@ TEST(registry_short_name_indexes) { 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); + ASSERT_EQ(cbm_type_short_iter_next(&nit), 0); + ASSERT_EQ(cbm_type_short_iter_next(&nit), 4); + ASSERT_EQ(cbm_type_short_iter_next(&nit), 5); + ASSERT_EQ(cbm_type_short_iter_next(&nit), tail_type_i); + ASSERT_EQ(cbm_type_short_iter_next(&nit), -1); int *saved_type_short_buckets = reg.type_short_buckets; reg.type_short_buckets = NULL; @@ -15449,7 +15449,7 @@ TEST(registry_short_name_indexes) { int fallback_expected[] = {0, 4, 5, tail_type_i}; int fallback_count = 0; int candidate; - while ((candidate = cbm_type_name_iter_next(&nit)) >= 0) { + while ((candidate = cbm_type_short_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; @@ -15479,7 +15479,7 @@ TEST(registry_short_name_indexes) { cbm_registry_types_by_short_name(®, "Trait", &nit); fallback_count = 0; - while ((candidate = cbm_type_name_iter_next(&nit)) >= 0) { + while ((candidate = cbm_type_short_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;