Skip to content
Merged
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
12 changes: 10 additions & 2 deletions internal/cbm/lsp/c_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -4432,8 +4432,15 @@ static void c_process_body_child(CLSPContext *ctx, TSNode child) {
!strstr(func_qn, ctx->current_namespace))
func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s",
ctx->current_namespace, fname);
// Only register if not already registered
if (!cbm_registry_lookup_func(ctx->registry, func_qn)) {
// Only register if not already registered. On the
// shared Tier-2 registry, skip entirely: it is
// finalized + read-only, the def is already present
// from the project-wide build, and the lookup + add
// would otherwise hit/grow the post-finalize tail ->
// O(files*defs) on large C codebases (the Linux-kernel
// full-index hang) plus a cross-worker heap race.
if (!ctx->registry_shared &&
!cbm_registry_lookup_func(ctx->registry, func_qn)) {
const CBMType **rets = (const CBMType **)cbm_arena_alloc(
ctx->arena, 2 * sizeof(const CBMType *));
rets[0] = ret_type;
Expand Down Expand Up @@ -5315,6 +5322,7 @@ CBMTypeRegistry *cbm_c_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, in
c_register_lsp_defs(arena, reg, "", d, 1);
}
cbm_registry_finalize(reg);
reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */
return reg;
}

Expand Down
1 change: 1 addition & 0 deletions internal/cbm/lsp/cs_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -2954,6 +2954,7 @@ CBMTypeRegistry *cbm_cs_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, i
cs_register_lsp_defs(arena, reg, &defs[i], 1);
}
cbm_registry_finalize(reg);
reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */
return reg;
}

Expand Down
1 change: 1 addition & 0 deletions internal/cbm/lsp/go_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -2835,6 +2835,7 @@ CBMTypeRegistry* cbm_go_build_cross_registry(
}

cbm_registry_finalize(reg);
reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */
return reg;
}

Expand Down
1 change: 1 addition & 0 deletions internal/cbm/lsp/py_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -3607,6 +3607,7 @@ CBMTypeRegistry *cbm_py_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, i
}

cbm_registry_finalize(reg);
reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */
return reg;
}

Expand Down
1 change: 1 addition & 0 deletions internal/cbm/lsp/ts_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -4986,6 +4986,7 @@ CBMTypeRegistry *cbm_ts_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, i
ts_register_lsp_defs(arena, reg, d, 1);
}
cbm_registry_finalize(reg);
reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */
return reg;
}

Expand Down
8 changes: 8 additions & 0 deletions internal/cbm/lsp/type_registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,10 @@ void cbm_registry_init(CBMTypeRegistry *reg, CBMArena *arena) {
}

void cbm_registry_add_func(CBMTypeRegistry *reg, CBMRegisteredFunc func) {
if (reg->read_only) {
return; /* sealed Tier-2 shared registry: refuse post-finalize mutation (O(n^2)+race guard)
*/
}
if (reg->func_count >= reg->func_cap) {
int new_cap = reg->func_cap == 0 ? 64 : reg->func_cap * 2;
CBMRegisteredFunc *new_items = (CBMRegisteredFunc *)cbm_arena_alloc(
Expand All @@ -151,6 +155,10 @@ void cbm_registry_add_func(CBMTypeRegistry *reg, CBMRegisteredFunc func) {
}

void cbm_registry_add_type(CBMTypeRegistry *reg, CBMRegisteredType type) {
if (reg->read_only) {
return; /* sealed Tier-2 shared registry: refuse post-finalize mutation (O(n^2)+race guard)
*/
}
if (reg->type_count >= reg->type_cap) {
int new_cap = reg->type_cap == 0 ? 64 : reg->type_cap * 2;
CBMRegisteredType *new_items = (CBMRegisteredType *)cbm_arena_alloc(
Expand Down
11 changes: 11 additions & 0 deletions internal/cbm/lsp/type_registry.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include "type_rep.h"
#include "../arena.h"
#include <stdbool.h>

// Decorator-derived flags (Python). Added at struct tail so existing
// callers that memset to zero before populating other fields keep working.
Expand Down Expand Up @@ -99,6 +100,16 @@ typedef struct CBMTypeRegistry {
CBMRegistryHashEntry *method_entries;
int method_bucket_count;
int method_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.
* cbm_registry_add_func/_type no-op on a sealed registry, so a per-file
* resolver can never mutate the shared, finalized registry. Without this,
* post-finalize adds accumulate in a tail the hash index does not cover ->
* every lookup linear-scans it -> O(files*defs) (the Linux-kernel full-index
* hang) plus a heap data race across workers. */
bool read_only;
} CBMTypeRegistry;

// Initialize a registry.
Expand Down
157 changes: 157 additions & 0 deletions tests/test_c_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@
*/
#include "test_framework.h"
#include "cbm.h"
#include "lsp/c_lsp.h"
#include "lsp/py_lsp.h"
#include "lsp/cs_lsp.h"
#include "lsp/ts_lsp.h"
#include "lsp/go_lsp.h"
#include "lsp/type_registry.h"
#include "arena.h"
#include <stdio.h>
#include <stdlib.h>

Expand Down Expand Up @@ -15182,9 +15189,159 @@ TEST(clsp_easy_win_sfinaeconditional_return) {
PASS();
}

/* Reproduce-first guard for the Linux-kernel full-index O(n^2) hang.
*
* In `full` mode the pipeline builds ONE project-wide C cross-registry
* (cbm_c_build_cross_registry), FINALIZES it (O(1) hash lookups), then shares it
* READ-ONLY across the parallel resolve workers (ctx.registry_shared = true).
* cbm_run_c_lsp_cross_with_registry must therefore NOT mutate it.
*
* Bug: c_lsp.c:4323 (and 4628/4201/4426) ignore registry_shared and add_func into
* the shared, already-finalized registry. Each post-finalize add lands in a tail
* the hash index does not cover, so lookup_func_self/lookup_method_self linear-scan
* that ever-growing tail on every lookup (type_registry.c:280-286, 186-195). Across
* 89k kernel files doing millions of lookups => O(files * defs) (the >6-min hang),
* plus an 11-thread heap race (c_lsp.c:4146-4152 warns of the SIGSEGV).
*
* INVARIANT (green <=> fixed): resolve leaves the finalized shared registry's
* func_count/type_count unchanged. RED on the unguarded code; GREEN once every
* mutation site honors !registry_shared. */
TEST(clsp_tier2_shared_registry_readonly_c) {
CBMArena arena;
cbm_arena_init(&arena);
/* stdlib-only project registry, finalized inside the builder */
CBMTypeRegistry *reg = cbm_c_build_cross_registry(&arena, NULL, 0);
ASSERT_NOT_NULL(reg);
int funcs_before = reg->func_count;
int types_before = reg->type_count;
/* A C translation unit defining functions absent from the shared registry; a
* correct read-only resolve must not register them into it. */
const char *src = "struct Node { int v; };\n"
"struct Node *make_node(int v);\n"
"int helper(int x) { return x + 1; }\n"
"int caller(void) { return helper(make_node(7)->v); }\n";
CBMResolvedCallArray out = {0};
cbm_run_c_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.mod",
/*cpp_mode=*/false, reg, /*include_paths=*/NULL,
/*include_ns_qns=*/NULL, /*include_count=*/0,
/*cached_tree=*/NULL, &out);
ASSERT_EQ(reg->func_count, funcs_before);
ASSERT_EQ(reg->type_count, types_before);
cbm_arena_destroy(&arena);
PASS();
}

/* ── Suite ─────────────────────────────────────────────────────── */

/* C++ sibling guard: the same shared-registry read-only invariant for the
* C++-only mutation sites — method registration (c_lsp.c:4760), template
* type-param scan (4558), default-arg min_params scan (4333). RED while those
* sites ignore registry_shared; GREEN once guarded. (Latent O(n^2)+race for large
* C++ codebases like LLVM/bitcoin — not the kernel, which is C.) */
TEST(clsp_tier2_shared_registry_readonly_cpp) {
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry *reg = cbm_c_build_cross_registry(&arena, NULL, 0);
ASSERT_NOT_NULL(reg);
int funcs_before = reg->func_count;
int types_before = reg->type_count;
const char *src = "template <typename T> struct Holder { T get(); };\n"
"struct Box { int unwrap(); };\n"
"int Box::unwrap() { return 0; }\n"
"int with_default(int a, int b = 2) { return a + b; }\n"
"int caller(Box *b) { return b->unwrap() + with_default(1); }\n";
CBMResolvedCallArray out = {0};
cbm_run_c_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.mod",
/*cpp_mode=*/true, reg, /*include_paths=*/NULL,
/*include_ns_qns=*/NULL, /*include_count=*/0,
/*cached_tree=*/NULL, &out);
ASSERT_EQ(reg->func_count, funcs_before);
ASSERT_EQ(reg->type_count, types_before);
cbm_arena_destroy(&arena);
PASS();
}

/* Cross-language check of the same Tier-2 shared-registry read-only invariant for
* the other languages that build a shared cross-registry (py, c#, ts, go). The
* registry-level seal (type_registry.c: add_func/_type no-op when reg->read_only)
* guards all of them at one chokepoint. Each test: build+finalize the shared
* registry, resolve a source, assert func_count/type_count are unchanged. (TS
* deliberately mutates a per-file OVERLAY chained to the base — the base must
* still be untouched.) */
TEST(seal_py_shared_registry_readonly) {
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry *reg = cbm_py_build_cross_registry(&arena, NULL, 0);
ASSERT_NOT_NULL(reg);
int fb = reg->func_count, tb = reg->type_count;
const char *src = "def helper(x):\n return x + 1\n\ndef caller():\n return helper(1)\n";
CBMResolvedCallArray out = {0};
cbm_run_py_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.mod", reg, NULL, NULL,
0, NULL, &out);
ASSERT_EQ(reg->func_count, fb);
ASSERT_EQ(reg->type_count, tb);
cbm_arena_destroy(&arena);
PASS();
}

TEST(seal_cs_shared_registry_readonly) {
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry *reg = cbm_cs_build_cross_registry(&arena, NULL, 0);
ASSERT_NOT_NULL(reg);
int fb = reg->func_count, tb = reg->type_count;
const char *src = "namespace N { class Box { int Unwrap() { return 0; }\n"
" int Caller() { return Unwrap(); } } }\n";
CBMResolvedCallArray out = {0};
cbm_run_cs_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.mod", reg, NULL, 0,
NULL, &out);
ASSERT_EQ(reg->func_count, fb);
ASSERT_EQ(reg->type_count, tb);
cbm_arena_destroy(&arena);
PASS();
}

TEST(seal_ts_shared_registry_readonly) {
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry *reg = cbm_ts_build_cross_registry(&arena, NULL, 0);
ASSERT_NOT_NULL(reg);
int fb = reg->func_count, tb = reg->type_count;
const char *src = "class Box { unwrap(): number { return 0; } }\n"
"function caller(b: Box): number { return b.unwrap(); }\n";
CBMResolvedCallArray out = {0};
cbm_run_ts_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.mod", false, false,
false, reg, NULL, 0, NULL, NULL, 0, NULL, &out);
ASSERT_EQ(reg->func_count, fb);
ASSERT_EQ(reg->type_count, tb);
cbm_arena_destroy(&arena);
PASS();
}

TEST(seal_go_shared_registry_readonly) {
CBMArena arena;
cbm_arena_init(&arena);
CBMTypeRegistry *reg = cbm_go_build_cross_registry(&arena, NULL, 0);
ASSERT_NOT_NULL(reg);
int fb = reg->func_count, tb = reg->type_count;
const char *src = "package main\nfunc helper(x int) int { return x + 1 }\n"
"func caller() int { return helper(1) }\n";
CBMResolvedCallArray out = {0};
cbm_run_go_lsp_cross_with_registry(&arena, src, (int)strlen(src), "test.mod", reg, NULL, NULL,
0, NULL, &out);
ASSERT_EQ(reg->func_count, fb);
ASSERT_EQ(reg->type_count, tb);
cbm_arena_destroy(&arena);
PASS();
}

SUITE(c_lsp) {
RUN_TEST(clsp_tier2_shared_registry_readonly_c);
RUN_TEST(clsp_tier2_shared_registry_readonly_cpp);
RUN_TEST(seal_py_shared_registry_readonly);
RUN_TEST(seal_cs_shared_registry_readonly);
RUN_TEST(seal_ts_shared_registry_readonly);
RUN_TEST(seal_go_shared_registry_readonly);
RUN_TEST(clsp_simple_var_decl);
RUN_TEST(clsp_pointer_arrow);
RUN_TEST(clsp_dot_access);
Expand Down
Loading