Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion internal/cbm/lsp/c_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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(&reg);
cbm_registry_build_type_short_index(&reg);

// Initialize context and run
CLSPContext ctx;
Expand Down
87 changes: 87 additions & 0 deletions internal/cbm/lsp/type_registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Comment thread
astandrik marked this conversation as resolved.

/* 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) {
Expand Down Expand Up @@ -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;
}
}
Comment thread
astandrik marked this conversation as resolved.

int cbm_type_name_iter_next(CBMTypeNameIter *it) {
const CBMTypeRegistry *reg = it->reg;
while (it->chain_idx >= 0) {
const CBMRegistryHashEntry *e = &reg->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;
Expand Down
34 changes: 30 additions & 4 deletions internal/cbm/lsp/type_registry.h
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand Down
163 changes: 157 additions & 6 deletions tests/test_c_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -15288,6 +15285,25 @@ TEST(registry_short_name_indexes) {
t.short_name = "C";
t.embedded_types = c_emb;
cbm_registry_add_type(&reg, t);
memset(&t, 0, sizeof(t));
t.qualified_name = "other.Trait";
t.short_name = "Trait";
cbm_registry_add_type(&reg, t);
memset(&t, 0, sizeof(t));
t.qualified_name = "Trait";
t.short_name = "Trait";
cbm_registry_add_type(&reg, 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(&reg, t);
}

/* free func "helper" (x2 — different QNs), method "M.helper", free func "other". */
CBMRegisteredFunc f;
Expand All @@ -15310,6 +15326,77 @@ TEST(registry_short_name_indexes) {
cbm_registry_add_func(&reg, f);

cbm_registry_finalize(&reg);
cbm_registry_build_type_short_index(&reg);

/* 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(&reg, "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(&reg, "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(&reg, 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;
Expand Down Expand Up @@ -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(&reg, t);
cbm_registry_types_by_short_name(&reg, "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(&reg, "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(&reg);
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(&reg);
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(&reg, "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();
}
Expand Down
Loading