perf(registry,rust-lsp): embedded-type + free-func short-name indexes

The Rust cross-file resolver's trait-method, sole-trait-impl, and free-func
fallback loops scanned the ENTIRE type/func registry per call site on a miss.
On kernel-scale registries (~10^6 entries, macro-inflated) four tiny trait-heavy
rust files burned ~96 s EACH (~200k eval-capped call sites x full-registry
strcmp scans).

Fix: two hash indexes built at registry finalize, mirroring the existing
bucket+entries idiom -- fnv1a(bare(embedded_type)) -> chain of declaring type
indices (trait -> implementers), and fnv1a(short_name) -> chain of free-func
indices. Chains preserve ascending registry order (first-match tie-break
unchanged); allocation-free iterators fall back to the post-finalize tail and
to a full scan on unfinalized registries, so the candidate set is identical in
every mode. The four rust loops now walk the iterator and re-apply their exact
original predicates -- byte-identical results, loop bodies untouched.

Verified: graph equivalence on the kernel rust/ subtree (nodes 9654, edges
44798 identical with and without the change); registry_short_name_indexes unit
test pins iterator contents/order/dedup; suites c_lsp 750, rust_lsp 505,
registry 54, pipeline 216 -- zero failures.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel committed 2026-07-06 16:39:25 +02:00
1 parent bd005b4b09
commit 2abcebc821
4 files changed
+345 -5

No files matched your search

+24 -5
View File
@@ -2334,10 +2334,16 @@ static const CBMRegisteredFunc *rust_resolve_trait_method(RustLSPContext *ctx,
}
/* Look at every type whose embedded_types include the receiver_type_qn
* (treated as a trait): pick the single-impl case. */
* (treated as a trait): pick the single-impl case. Prefilter to the types whose
* embedded_types carry a matching BARE name via the registry index; the exact
* full-QN check below is unchanged, so the result set is identical. */
const CBMRegisteredFunc *unique = NULL;
int impls = 0;
for (int ti = 0; ti < reg->type_count && impls < 3; ti++) {
const char *rdot = strrchr(receiver_type_qn, '.');
const char *rbare = rdot ? rdot + 1 : receiver_type_qn;
CBMTypeEmbedIter eit;
cbm_registry_types_by_embedded_bare(reg, rbare, &eit);
for (int ti; impls < 3 && (ti = cbm_type_embed_iter_next(&eit)) >= 0;) {
const CBMRegisteredType *t = &reg->types[ti];
if (!t->embedded_types)
continue;
@@ -2400,7 +2406,12 @@ static const CBMRegisteredFunc *rust_find_sole_trait_impl(RustLSPContext *ctx, c
const CBMRegisteredFunc *first = NULL;
const char *first_qn = NULL;
int n = 0;
for (int ti = 0; ti < reg->type_count && n < 2; ti++) {
/* Prefilter to types whose embedded_types carry the trait's BARE name; the
* exact (full-QN OR bare) check below is unchanged. tbare-keyed index captures
* every original match (a full-QN match implies a bare-name match). */
CBMTypeEmbedIter eit;
cbm_registry_types_by_embedded_bare(reg, tbare, &eit);
for (int ti; n < 2 && (ti = cbm_type_embed_iter_next(&eit)) >= 0;) {
const CBMRegisteredType *t = &reg->types[ti];
if (!t->embedded_types || !t->qualified_name)
continue;
@@ -3762,7 +3773,11 @@ static void rust_resolve_call_expression(RustLSPContext *ctx, TSNode node) {
char *needle = cbm_arena_sprintf(ctx->arena, ".%s.", head);
const CBMRegisteredFunc *mem_unique = NULL;
int mem_matches = 0;
for (int i = 0; i < ctx->registry->func_count && mem_matches < 2; i++) {
/* Iterate only free funcs whose short_name == tail via the index;
* the receiver/short_name/needle re-checks below are unchanged. */
CBMFreeFuncIter ffit;
cbm_registry_free_funcs_by_short_name(ctx->registry, tail, &ffit);
for (int i; mem_matches < 2 && (i = cbm_free_func_iter_next(&ffit)) >= 0;) {
const CBMRegisteredFunc *f = &ctx->registry->funcs[i];
if (!f->short_name || !f->qualified_name)
continue;
@@ -3800,7 +3815,11 @@ static void rust_resolve_call_expression(RustLSPContext *ctx, TSNode node) {
first_dot ? (size_t)(first_dot - ctx->module_qn) : strlen(ctx->module_qn);
const CBMRegisteredFunc *unique = NULL;
int matches = 0;
for (int i = 0; i < ctx->registry->func_count && matches < 2; i++) {
/* Iterate only free funcs whose short_name == tail via the index; the
* receiver/short_name/crate-prefix re-checks below are unchanged. */
CBMFreeFuncIter ffit;
cbm_registry_free_funcs_by_short_name(ctx->registry, tail, &ffit);
for (int i; matches < 2 && (i = cbm_free_func_iter_next(&ffit)) >= 0;) {
const CBMRegisteredFunc *f = &ctx->registry->funcs[i];
if (!f->short_name || !f->qualified_name)
continue;
+173
View File
@@ -115,12 +115,185 @@ static void build_method_index(CBMTypeRegistry *reg, CBMArena *idx_arena) {
reg->method_entry_count = idx;
}
/* Index: bare(embedded_type) -> chain of TYPE indices declaring it. One entry per
* (type, embedded_type). Types are iterated DESCENDING and prepended so each bucket
* chain ends up in ASCENDING type-index order (preserves the Rust loops' first-match
* tie-break); a type's multiple entries land adjacent so the iterator can dedup them. */
static void build_type_embed_index(CBMTypeRegistry *reg, CBMArena *idx_arena) {
int ecount = 0;
for (int i = 0; i < reg->type_count; i++) {
const char **e = reg->types[i].embedded_types;
if (!e)
continue;
for (int j = 0; e[j]; j++)
ecount++;
}
if (ecount == 0)
return;
int bucket_count = next_pow2(ecount * 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)ecount * 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 **e = reg->types[i].embedded_types;
if (!e)
continue;
for (int j = 0; e[j]; j++) {
const char *s = e[j];
if (!s)
continue;
const char *dot = strrchr(s, '.');
const char *bare = dot ? dot + 1 : s;
uint64_t h = fnv1a(bare);
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_embed_buckets = buckets;
reg->type_embed_entries = entries;
reg->type_embed_bucket_count = bucket_count;
reg->type_embed_entry_count = idx;
}
/* 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) {
int fcount = 0;
for (int i = 0; i < reg->func_count; i++) {
const CBMRegisteredFunc *f = &reg->funcs[i];
if (!f->receiver_type && f->short_name)
fcount++;
}
if (fcount == 0)
return;
int bucket_count = next_pow2(fcount * 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)fcount * sizeof(CBMRegistryHashEntry));
if (!buckets || !entries)
return;
for (int i = 0; i < bucket_count; i++)
buckets[i] = -1;
int idx = 0;
for (int i = reg->func_count - 1; i >= 0; i--) {
const CBMRegisteredFunc *f = &reg->funcs[i];
if (f->receiver_type || !f->short_name)
continue;
uint64_t h = fnv1a(f->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->ffunc_short_buckets = buckets;
reg->ffunc_short_entries = entries;
reg->ffunc_short_bucket_count = bucket_count;
reg->ffunc_short_entry_count = idx;
}
void cbm_registry_types_by_embedded_bare(const CBMTypeRegistry *reg, const char *bare,
CBMTypeEmbedIter *out) {
out->reg = reg;
out->hash = fnv1a(bare);
out->prev_type = -1;
if (reg->type_qn_buckets && reg->type_qn_bucket_count > 0) {
/* finalized: walk the embed chain (if built) then the post-finalize tail */
if (reg->type_embed_buckets && reg->type_embed_bucket_count > 0) {
int slot = (int)(out->hash & (uint64_t)(reg->type_embed_bucket_count - 1));
out->chain_idx = reg->type_embed_buckets[slot];
} else {
out->chain_idx = -1;
}
out->tail_i = reg->type_qn_entry_count;
out->tail_end = reg->type_count;
} else {
/* unfinalized: full linear scan over all types (identical candidate set) */
out->chain_idx = -1;
out->tail_i = 0;
out->tail_end = reg->type_count;
}
}
int cbm_type_embed_iter_next(CBMTypeEmbedIter *it) {
const CBMTypeRegistry *reg = it->reg;
while (it->chain_idx >= 0) {
const CBMRegistryHashEntry *e = &reg->type_embed_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; /* hash collision */
if (p == it->prev_type)
continue; /* same type, multiple matching embeds — yield once */
it->prev_type = p;
return p;
}
if (it->tail_i < it->tail_end)
return it->tail_i++;
return -1;
}
void cbm_registry_free_funcs_by_short_name(const CBMTypeRegistry *reg, const char *short_name,
CBMFreeFuncIter *out) {
out->reg = reg;
out->hash = fnv1a(short_name);
if (reg->func_qn_buckets && reg->func_qn_bucket_count > 0) {
if (reg->ffunc_short_buckets && reg->ffunc_short_bucket_count > 0) {
int slot = (int)(out->hash & (uint64_t)(reg->ffunc_short_bucket_count - 1));
out->chain_idx = reg->ffunc_short_buckets[slot];
} else {
out->chain_idx = -1;
}
out->tail_i = reg->func_qn_entry_count;
out->tail_end = reg->func_count;
} else {
out->chain_idx = -1;
out->tail_i = 0;
out->tail_end = reg->func_count;
}
}
int cbm_free_func_iter_next(CBMFreeFuncIter *it) {
const CBMTypeRegistry *reg = it->reg;
while (it->chain_idx >= 0) {
const CBMRegistryHashEntry *e = &reg->ffunc_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; /* each free func has one short_name → no dedup needed */
}
if (it->tail_i < it->tail_end)
return it->tail_i++;
return -1;
}
void cbm_registry_finalize_into(CBMTypeRegistry *reg, CBMArena *idx_arena) {
if (!reg || !idx_arena)
return;
build_qn_index(reg, idx_arena, /*for_funcs=*/true);
build_qn_index(reg, idx_arena, /*for_funcs=*/false);
build_method_index(reg, idx_arena);
build_type_embed_index(reg, idx_arena);
build_ffunc_short_index(reg, idx_arena);
}
void cbm_registry_finalize(CBMTypeRegistry *reg) {
+54
View File
@@ -101,6 +101,23 @@ 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.
// 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).
int *type_embed_buckets;
CBMRegistryHashEntry *type_embed_entries;
int type_embed_bucket_count;
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 *ffunc_short_buckets;
CBMRegistryHashEntry *ffunc_short_entries;
int ffunc_short_bucket_count;
int ffunc_short_entry_count;
/* Sealed / read-only. Set true by the cbm_X_build_cross_registry builders
* (c/cpp, python, c#, ts, go) right after finalize: a Tier-2 cross-registry
* is built ONCE and shared READ-ONLY across the parallel resolve workers.
@@ -188,6 +205,43 @@ 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) ---
//
// 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
// degrades to a full linear scan over all types (identical candidate set). Each
// matching type index is yielded at most once, in ascending registry order. The
// index is a bare-name PREFILTER — the caller MUST still apply its own exact
// predicate on each yielded type. Read-only, allocation-free. Usage:
// CBMTypeEmbedIter it; cbm_registry_types_by_embedded_bare(reg, bare, &it);
// int ti; while ((ti = cbm_type_embed_iter_next(&it)) >= 0) { ... reg->types[ti] ... }
typedef struct {
const CBMTypeRegistry *reg;
uint64_t hash;
int chain_idx; // next entry in the embed chain, or -1
int tail_i; // next tail/linear type index
int tail_end; // reg->type_count snapshot
int prev_type; // last yielded type index (adjacent-dedup); -1 = none
} CBMTypeEmbedIter;
void cbm_registry_types_by_embedded_bare(const CBMTypeRegistry *reg, const char *bare,
CBMTypeEmbedIter *out);
int cbm_type_embed_iter_next(CBMTypeEmbedIter *it);
// Iterate FREE-function (receiver_type==NULL) indices whose short_name equals
// `short_name`. Same finalized/unfinalized behavior as above; caller re-checks its
// own predicate. Read-only, allocation-free.
typedef struct {
const CBMTypeRegistry *reg;
uint64_t hash;
int chain_idx;
int tail_i;
int tail_end;
} CBMFreeFuncIter;
void cbm_registry_free_funcs_by_short_name(const CBMTypeRegistry *reg, const char *short_name,
CBMFreeFuncIter *out);
int cbm_free_func_iter_next(CBMFreeFuncIter *it);
// --- TS-specific helpers (return NULL for types without these signatures) ---
// If the type has a call signature (e.g., `interface F { (x:number): string }`), return
+94
View File
@@ -15231,6 +15231,99 @@ 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. */
TEST(registry_short_name_indexes) {
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry reg;
cbm_registry_init(&reg, &arena);
/* type 0: Trait (no embeds). type 1: A impl "pkg.Trait". type 2: B impl bare
* "Trait" AND a second embed also bare "Trait" (dedup case). type 3: C impl
* "pkg.Other". */
const char *a_emb[] = {"pkg.Trait", NULL};
const char *b_emb[] = {"other.Trait", "misc.Trait", NULL}; /* two entries, bare "Trait" */
const char *c_emb[] = {"pkg.Other", NULL};
CBMRegisteredType t;
memset(&t, 0, sizeof(t));
t.qualified_name = "pkg.Trait";
t.short_name = "Trait";
cbm_registry_add_type(&reg, t);
memset(&t, 0, sizeof(t));
t.qualified_name = "pkg.A";
t.short_name = "A";
t.embedded_types = a_emb;
cbm_registry_add_type(&reg, t);
memset(&t, 0, sizeof(t));
t.qualified_name = "pkg.B";
t.short_name = "B";
t.embedded_types = b_emb;
cbm_registry_add_type(&reg, t);
memset(&t, 0, sizeof(t));
t.qualified_name = "pkg.C";
t.short_name = "C";
t.embedded_types = c_emb;
cbm_registry_add_type(&reg, t);
/* free func "helper" (x2 — different QNs), method "M.helper", free func "other". */
CBMRegisteredFunc f;
memset(&f, 0, sizeof(f));
f.qualified_name = "pkg.helper";
f.short_name = "helper";
cbm_registry_add_func(&reg, f);
memset(&f, 0, sizeof(f));
f.qualified_name = "pkg.sub.helper";
f.short_name = "helper";
cbm_registry_add_func(&reg, f);
memset(&f, 0, sizeof(f));
f.qualified_name = "pkg.M.helper";
f.short_name = "helper";
f.receiver_type = "pkg.M"; /* method — must NOT appear in the free-func index */
cbm_registry_add_func(&reg, f);
memset(&f, 0, sizeof(f));
f.qualified_name = "pkg.other";
f.short_name = "other";
cbm_registry_add_func(&reg, f);
cbm_registry_finalize(&reg);
/* Embed index for bare "Trait": types 1 (A) then 2 (B), ascending, B once. */
CBMTypeEmbedIter it;
cbm_registry_types_by_embedded_bare(&reg, "Trait", &it);
ASSERT_EQ(cbm_type_embed_iter_next(&it), 1);
ASSERT_EQ(cbm_type_embed_iter_next(&it), 2);
ASSERT_EQ(cbm_type_embed_iter_next(&it), -1);
/* Bare "Other": only type 3 (C). */
cbm_registry_types_by_embedded_bare(&reg, "Other", &it);
ASSERT_EQ(cbm_type_embed_iter_next(&it), 3);
ASSERT_EQ(cbm_type_embed_iter_next(&it), -1);
/* Bare with no implementers: empty. */
cbm_registry_types_by_embedded_bare(&reg, "Nope", &it);
ASSERT_EQ(cbm_type_embed_iter_next(&it), -1);
/* Free-func index for "helper": func 0 then 1 (ascending), NOT the method (2). */
CBMFreeFuncIter fit;
cbm_registry_free_funcs_by_short_name(&reg, "helper", &fit);
ASSERT_EQ(cbm_free_func_iter_next(&fit), 0);
ASSERT_EQ(cbm_free_func_iter_next(&fit), 1);
ASSERT_EQ(cbm_free_func_iter_next(&fit), -1);
/* Free-func "other": only func 3. */
cbm_registry_free_funcs_by_short_name(&reg, "other", &fit);
ASSERT_EQ(cbm_free_func_iter_next(&fit), 3);
ASSERT_EQ(cbm_free_func_iter_next(&fit), -1);
cbm_arena_destroy(&arena);
PASS();
}
/* ── Suite ─────────────────────────────────────────────────────── */
/* C++ sibling guard: the same shared-registry read-only invariant for the
@@ -15417,6 +15510,7 @@ SUITE(c_lsp) {
RUN_TEST(seal_cs_shared_registry_readonly);
RUN_TEST(seal_ts_shared_registry_readonly);
RUN_TEST(seal_go_shared_registry_readonly);
RUN_TEST(registry_short_name_indexes);
RUN_TEST(clsp_simple_var_decl);
RUN_TEST(clsp_pointer_arrow);
RUN_TEST(clsp_dot_access);