diff --git a/Makefile.cbm b/Makefile.cbm index c79e35673..5b90c388e 100644 --- a/Makefile.cbm +++ b/Makefile.cbm @@ -505,6 +505,7 @@ TEST_FOUNDATION_SRCS = \ tests/test_workspace.c \ tests/test_platform.c \ tests/test_diagnostics.c \ + tests/test_complexity.c \ tests/test_dump_verify.c \ tests/test_subprocess.c \ tests/test_private_file_lock.c \ @@ -712,9 +713,9 @@ BUILD_DIR = build/c # Grammar + tree-sitter runtime: compiled without -Werror (upstream code has warnings) GRAMMAR_CFLAGS = -std=c11 -D_DEFAULT_SOURCE -O2 -w -I$(CBM_DIR) -I$(TS_INCLUDE) -I$(TS_SRC) GRAMMAR_CFLAGS_TEST = -std=c11 -D_DEFAULT_SOURCE -g -O1 -w -I$(CBM_DIR) -I$(TS_INCLUDE) -I$(TS_SRC) \ - $(SANITIZED_DEFINE) $(SANITIZE) + -DCBM_ENABLE_TEST_SEAMS=1 $(SANITIZED_DEFINE) $(SANITIZE) GRAMMAR_CFLAGS_TSAN = -std=c11 -D_DEFAULT_SOURCE -g -O1 -w -I$(CBM_DIR) -I$(TS_INCLUDE) -I$(TS_SRC) \ - -DCBM_SANITIZED_BUILD=1 $(TSAN_SANITIZE) + -DCBM_ENABLE_TEST_SEAMS=1 -DCBM_SANITIZED_BUILD=1 $(TSAN_SANITIZE) # Object files for grammars + ts_runtime + lsp_all + preprocessor GRAMMAR_OBJS_TEST = $(patsubst $(CBM_DIR)/%.c,$(BUILD_DIR)/%.o,$(GRAMMAR_SRCS)) diff --git a/internal/cbm/extract_unified.c b/internal/cbm/extract_unified.c index 2812d0544..20e294c93 100644 --- a/internal/cbm/extract_unified.c +++ b/internal/cbm/extract_unified.c @@ -1646,6 +1646,39 @@ static bool is_actual_import_boundary(CBMExtractCtx *ctx, TSNode node, const CBM char *name = ts_node_is_null(head) ? NULL : cbm_node_text(ctx->arena, head, ctx->source); switch (ctx->language) { + case CBM_LANG_JAVASCRIPT: + case CBM_LANG_TYPESCRIPT: + case CBM_LANG_TSX: + if (strcmp(kind, "export_statement") == 0) { + /* An export is an import CONTEXT only in its re-export forms: + * `export ... from 'mod'` (source field) or a bare specifier list + * `export { a, b }` with no declaration. An export OF a declaration + * must not put the declaration's body behind inside_import — the + * old kind-blacklist (is_export_of_declaration) missed the TS-only + * forms (ambient_declaration, function_signature, + * module_declaration), so declare-heavy code (.d.ts, baselines, + * `export namespace`) ran whole subtrees as import context: + * suppressed usages plus per-identifier ancestor walks. Positive + * detection replaces the blacklist. */ + if (!ts_node_is_null(ts_node_child_by_field_name(node, TS_FIELD("source")))) { + return true; + } + return !ts_node_is_null(cbm_find_child_by_kind(node, "export_clause")) && + ts_node_is_null(ts_node_child_by_field_name(node, TS_FIELD("declaration"))); + } + return true; /* import_statement / import / require / extends: unchanged */ + case CBM_LANG_CSHARP: + /* cs_import_types also lists namespace_declaration (so the import pass + * can map namespace names) and using_statement (C#'s RAII block, a + * grammar-name collision with using_directive). Neither is an import + * CONTEXT: treating them as one put every namespaced C# file's whole + * body behind inside_import, which both suppressed ordinary usage + * extraction there and sent every identifier through the ancestor- + * walking import-binding check — 92% of extract time on wide files + * (dotnet/runtime JIT torture tests, 490 s for one 147 KB file). Only + * the using DIRECTIVE opens an import scope. */ + return strcmp(kind, "using_directive") == 0 || + strcmp(kind, "namespace_use_declaration") == 0; case CBM_LANG_ELIXIR: return strcmp(kind, "call") != 0 || (name && (strcmp(name, "import") == 0 || strcmp(name, "alias") == 0 || diff --git a/internal/cbm/extract_usages.c b/internal/cbm/extract_usages.c index fd1a8ab42..98ebc9c3b 100644 --- a/internal/cbm/extract_usages.c +++ b/internal/cbm/extract_usages.c @@ -49,36 +49,6 @@ static void walk_usages(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec static bool is_direct_argument_value(TSNode node); static TSNode python_direct_callable_attribute_site(TSNode node); -// Check if a node is inside a call expression (to avoid double-counting as usage) -static bool is_inside_call(TSNode node, const CBMLangSpec *spec) { - TSNode cur = ts_node_parent(node); - while (!ts_node_is_null(cur)) { - if (cbm_kind_in_set(cur, spec->call_node_types)) { - return true; - } - cur = ts_node_parent(cur); - } - return false; -} - -// Check if a node is inside an import statement -static bool is_inside_import(TSNode node, const CBMLangSpec *spec) { - bool has_imports = spec->import_node_types && spec->import_node_types[0]; - bool has_from_imports = spec->import_from_types && spec->import_from_types[0]; - if (!has_imports && !has_from_imports) { - return false; - } - TSNode cur = ts_node_parent(node); - while (!ts_node_is_null(cur)) { - if ((has_imports && cbm_kind_in_set(cur, spec->import_node_types)) || - (has_from_imports && cbm_kind_in_set(cur, spec->import_from_types))) { - return true; - } - cur = ts_node_parent(cur); - } - return false; -} - // Is this an identifier-like node that represents a reference? static bool is_reference_node(TSNode node, CBMLanguage lang) { const char *kind = ts_node_type(node); @@ -2426,7 +2396,8 @@ static bool emit_direct_perl_coderef_usage(CBMExtractCtx *ctx, TSNode node, } // Try to emit a usage for a reference node. Returns early if the node should be skipped. -static void try_emit_usage(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) { +static void try_emit_usage(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec, + bool inside_call, bool inside_import) { if (emit_direct_perl_coderef_usage(ctx, node, cbm_enclosing_func_qn_cached(ctx, node), 0)) { return; } @@ -2439,7 +2410,7 @@ static void try_emit_usage(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *s if (is_call_argument_label(node)) { return; } - if (is_inside_call(node, spec) || is_inside_import(node, spec)) { + if (inside_call || inside_import) { return; } if (is_binding_occurrence(ctx, node, spec, NULL) || @@ -2456,18 +2427,79 @@ static void try_emit_usage(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *s } } -// Iterative usage walker — explicit stack +// Iterative usage walker — explicit stack. +// +// The call/import ancestry that gates usage emission is maintained as ENTER/ +// EXIT counters on the walk instead of per-node ancestor re-walks: the old +// is_inside_call/is_inside_import helpers climbed every ancestor via +// ts_node_parent, and tree-sitter's ts_node_parent RE-DESCENDS from the root +// scanning siblings — O(depth x sibling-position) per node, which went +// quadratic on wide nodes (a 1,536-argument call in dotnet/runtime's JIT +// torture tests put 92% of extract time into these walks; 490 s for one +// 147 KB file). Counter semantics match the helpers exactly: strict ancestors +// only — a node is emitted BEFORE its own kind increments the counters, so a +// call node itself does not count as "inside a call". static void walk_usages(CBMExtractCtx *ctx, TSNode root, const CBMLangSpec *spec) { - TSNodeStack stack; - ts_nstack_init(&stack, ctx->arena, 4096); - ts_nstack_push(&stack, ctx->arena, root); - - while (stack.count > 0) { - TSNode node = ts_nstack_pop(&stack); - try_emit_usage(ctx, node, spec); - uint32_t count = ts_node_child_count(node); - for (int i = (int)count - LAST_IDX; i >= 0; i--) { - ts_nstack_push(&stack, ctx->arena, ts_node_child(node, (uint32_t)i)); + typedef struct { + TSNode node; + uint32_t next_child; + bool counts_call; + bool counts_import; + } UsageFrame; + int cap = 256; + UsageFrame *frames = (UsageFrame *)cbm_arena_alloc(ctx->arena, (size_t)cap * sizeof(*frames)); + if (!frames) { + return; + } + bool has_imports = spec->import_node_types && spec->import_node_types[0]; + bool has_from_imports = spec->import_from_types && spec->import_from_types[0]; + int call_depth = 0; + int import_depth = 0; + int top = 0; + frames[top++] = (UsageFrame){root, 0, false, false}; + bool entering = true; + + while (top > 0) { + UsageFrame *f = &frames[top - 1]; + if (entering) { + try_emit_usage(ctx, f->node, spec, call_depth > 0, import_depth > 0); + f->counts_call = cbm_kind_in_set(f->node, spec->call_node_types); + f->counts_import = + (has_imports && cbm_kind_in_set(f->node, spec->import_node_types)) || + (has_from_imports && cbm_kind_in_set(f->node, spec->import_from_types)); + if (f->counts_call) { + call_depth++; + } + if (f->counts_import) { + import_depth++; + } + } + uint32_t count = ts_node_child_count(f->node); + if (f->next_child < count) { + TSNode child = ts_node_child(f->node, f->next_child); + f->next_child++; + if (top == cap) { + int new_cap = cap * 2; + UsageFrame *grown = + (UsageFrame *)cbm_arena_alloc(ctx->arena, (size_t)new_cap * sizeof(*grown)); + if (!grown) { + return; + } + memcpy(grown, frames, (size_t)cap * sizeof(*frames)); + frames = grown; + cap = new_cap; + } + frames[top++] = (UsageFrame){child, 0, false, false}; + entering = true; + } else { + if (f->counts_call) { + call_depth--; + } + if (f->counts_import) { + import_depth--; + } + top--; + entering = false; } } } diff --git a/internal/cbm/lsp/cs_lsp.c b/internal/cbm/lsp/cs_lsp.c index e5667165f..0beed0a56 100644 --- a/internal/cbm/lsp/cs_lsp.c +++ b/internal/cbm/lsp/cs_lsp.c @@ -64,8 +64,8 @@ static const CBMType *cs_eval_member_access_type(CSLSPContext *ctx, TSNode node) static const CBMType *cs_eval_object_creation_type(CSLSPContext *ctx, TSNode node); static const CBMType *cs_eval_identifier_type(CSLSPContext *ctx, TSNode node); static const CBMType *cs_substitute_type_params(CBMArena *arena, const CBMType *t, - const char **param_names, - const CBMType **param_args); + const char **param_names, + const CBMType **param_args); static void cs_collect_imports(CSLSPContext *ctx, TSNode root); static void cs_collect_namespace(CSLSPContext *ctx, TSNode ns_node, bool file_scoped); static const char *cs_namespace_qn(CSLSPContext *ctx); @@ -86,16 +86,19 @@ static char *cs_node_text_cached(CSLSPContext *ctx, TSNode node) { } static bool cs_node_is(TSNode n, const char *kind) { - if (ts_node_is_null(n)) return false; + if (ts_node_is_null(n)) + return false; return strcmp(ts_node_type(n), kind) == 0; } static TSNode cs_child_named_kind(TSNode parent, const char *kind) { - if (ts_node_is_null(parent)) return parent; + if (ts_node_is_null(parent)) + return parent; uint32_t nc = ts_node_child_count(parent); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(parent, i); - if (!ts_node_is_null(c) && strcmp(ts_node_type(c), kind) == 0) return c; + if (!ts_node_is_null(c) && strcmp(ts_node_type(c), kind) == 0) + return c; } TSNode null_node; memset(&null_node, 0, sizeof(null_node)); @@ -103,11 +106,13 @@ static TSNode cs_child_named_kind(TSNode parent, const char *kind) { } static TSNode cs_first_named_child(TSNode parent) { - if (ts_node_is_null(parent)) return parent; + if (ts_node_is_null(parent)) + return parent; uint32_t nc = ts_node_child_count(parent); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(parent, i); - if (!ts_node_is_null(c) && ts_node_is_named(c)) return c; + if (!ts_node_is_null(c) && ts_node_is_named(c)) + return c; } TSNode null_node; memset(&null_node, 0, sizeof(null_node)); @@ -116,10 +121,12 @@ static TSNode cs_first_named_child(TSNode parent) { /* Last segment after '.'. */ static const char *cs_short_name(const char *qn) { - if (!qn) return NULL; + if (!qn) + return NULL; const char *last = qn; for (const char *p = qn; *p; p++) { - if (*p == '.') last = p + 1; + if (*p == '.') + last = p + 1; } return last; } @@ -127,20 +134,25 @@ static const char *cs_short_name(const char *qn) { /* C# uses '.' separators natively; no conversion needed for identifiers. * Strip leading "global::" / "::" if present. */ static const char *cs_strip_global(const char *name) { - if (!name) return name; - if (strncmp(name, "global::", 8) == 0) return name + 8; - if (strncmp(name, "::", 2) == 0) return name + 2; + if (!name) + return name; + if (strncmp(name, "global::", 8) == 0) + return name + 8; + if (strncmp(name, "::", 2) == 0) + return name + 2; return name; } static char *cs_normalize_name(CBMArena *a, const char *name) { - if (!name) return NULL; + if (!name) + return NULL; name = cs_strip_global(name); /* Replace "::" with "." (C# scope qualifier in source forms like * `global::System.Foo` becomes `System.Foo`). */ size_t n = strlen(name); char *out = (char *)cbm_arena_alloc(a, n + 1); - if (!out) return NULL; + if (!out) + return NULL; size_t w = 0; for (size_t i = 0; i < n; i++) { if (name[i] == ':' && i + 1 < n && name[i + 1] == ':') { @@ -156,41 +168,64 @@ static char *cs_normalize_name(CBMArena *a, const char *name) { /* Walk up from `name` past any leading '<' or whitespace. */ static char *cs_strip_generic_args(CBMArena *a, const char *name) { - if (!name) return NULL; + if (!name) + return NULL; const char *lt = strchr(name, '<'); - if (!lt) return cbm_arena_strdup(a, name); + if (!lt) + return cbm_arena_strdup(a, name); size_t len = (size_t)(lt - name); return cbm_arena_strndup(a, name, len); } /* Predefined C# type aliases: int → System.Int32, etc. */ static const char *cs_predefined_alias(const char *name) { - if (!name) return NULL; + if (!name) + return NULL; /* Roslyn predefined-type list: Binder_Symbols.cs BindPredefinedTypeSymbol */ - if (strcmp(name, "int") == 0) return "System.Int32"; - if (strcmp(name, "uint") == 0) return "System.UInt32"; - if (strcmp(name, "long") == 0) return "System.Int64"; - if (strcmp(name, "ulong") == 0) return "System.UInt64"; - if (strcmp(name, "short") == 0) return "System.Int16"; - if (strcmp(name, "ushort") == 0) return "System.UInt16"; - if (strcmp(name, "byte") == 0) return "System.Byte"; - if (strcmp(name, "sbyte") == 0) return "System.SByte"; - if (strcmp(name, "float") == 0) return "System.Single"; - if (strcmp(name, "double") == 0) return "System.Double"; - if (strcmp(name, "decimal") == 0) return "System.Decimal"; - if (strcmp(name, "bool") == 0) return "System.Boolean"; - if (strcmp(name, "char") == 0) return "System.Char"; - if (strcmp(name, "string") == 0) return "System.String"; - if (strcmp(name, "object") == 0) return "System.Object"; - if (strcmp(name, "nint") == 0) return "System.IntPtr"; - if (strcmp(name, "nuint") == 0) return "System.UIntPtr"; - if (strcmp(name, "void") == 0) return "System.Void"; - if (strcmp(name, "dynamic") == 0) return "System.Object"; /* dynamic ≈ object */ + if (strcmp(name, "int") == 0) + return "System.Int32"; + if (strcmp(name, "uint") == 0) + return "System.UInt32"; + if (strcmp(name, "long") == 0) + return "System.Int64"; + if (strcmp(name, "ulong") == 0) + return "System.UInt64"; + if (strcmp(name, "short") == 0) + return "System.Int16"; + if (strcmp(name, "ushort") == 0) + return "System.UInt16"; + if (strcmp(name, "byte") == 0) + return "System.Byte"; + if (strcmp(name, "sbyte") == 0) + return "System.SByte"; + if (strcmp(name, "float") == 0) + return "System.Single"; + if (strcmp(name, "double") == 0) + return "System.Double"; + if (strcmp(name, "decimal") == 0) + return "System.Decimal"; + if (strcmp(name, "bool") == 0) + return "System.Boolean"; + if (strcmp(name, "char") == 0) + return "System.Char"; + if (strcmp(name, "string") == 0) + return "System.String"; + if (strcmp(name, "object") == 0) + return "System.Object"; + if (strcmp(name, "nint") == 0) + return "System.IntPtr"; + if (strcmp(name, "nuint") == 0) + return "System.UIntPtr"; + if (strcmp(name, "void") == 0) + return "System.Void"; + if (strcmp(name, "dynamic") == 0) + return "System.Object"; /* dynamic ≈ object */ return NULL; } static bool cs_is_keyword_self(const char *name) { - if (!name) return false; + if (!name) + return false; return strcmp(name, "this") == 0 || strcmp(name, "base") == 0; } @@ -222,22 +257,28 @@ void cs_lsp_init(CSLSPContext *ctx, CBMArena *arena, const char *source, int sou void cs_lsp_add_using(CSLSPContext *ctx, CBMCSUsingKind kind, const char *local_name, const char *target_qn, bool is_global) { - if (!target_qn) return; + if (!target_qn) + return; /* Dedupe by (kind, local_name, target). */ for (int i = 0; i < ctx->using_count; i++) { CBMCSUsing *u = &ctx->usings[i]; - if (u->kind != kind) continue; - if (strcmp(u->target_qn, target_qn) != 0) continue; + if (u->kind != kind) + continue; + if (strcmp(u->target_qn, target_qn) != 0) + continue; const char *a = local_name ? local_name : ""; const char *b = u->local_name ? u->local_name : ""; - if (strcmp(a, b) != 0) continue; + if (strcmp(a, b) != 0) + continue; return; } if (ctx->using_count >= ctx->using_cap) { int new_cap = ctx->using_cap ? ctx->using_cap * 2 : CS_USING_INITIAL_CAP; CBMCSUsing *nu = (CBMCSUsing *)cbm_arena_alloc(ctx->arena, (size_t)new_cap * sizeof(*nu)); - if (!nu) return; - for (int i = 0; i < ctx->using_count; i++) nu[i] = ctx->usings[i]; + if (!nu) + return; + for (int i = 0; i < ctx->using_count; i++) + nu[i] = ctx->usings[i]; ctx->usings = nu; ctx->using_cap = new_cap; } @@ -251,13 +292,16 @@ void cs_lsp_add_using(CSLSPContext *ctx, CBMCSUsingKind kind, const char *local_ /* ── namespace stack ────────────────────────────────────────────── */ static void cs_namespace_push(CSLSPContext *ctx, const char *ns_name) { - if (!ns_name || !*ns_name) return; + if (!ns_name || !*ns_name) + return; if (ctx->namespace_count >= ctx->namespace_cap) { int new_cap = ctx->namespace_cap ? ctx->namespace_cap * 2 : CS_NAMESPACE_INITIAL_CAP; - const char **arr = (const char **)cbm_arena_alloc(ctx->arena, - (size_t)new_cap * sizeof(*arr)); - if (!arr) return; - for (int i = 0; i < ctx->namespace_count; i++) arr[i] = ctx->namespace_stack[i]; + const char **arr = + (const char **)cbm_arena_alloc(ctx->arena, (size_t)new_cap * sizeof(*arr)); + if (!arr) + return; + for (int i = 0; i < ctx->namespace_count; i++) + arr[i] = ctx->namespace_stack[i]; ctx->namespace_stack = arr; ctx->namespace_cap = new_cap; } @@ -265,23 +309,28 @@ static void cs_namespace_push(CSLSPContext *ctx, const char *ns_name) { } static void cs_namespace_pop(CSLSPContext *ctx) { - if (ctx->namespace_count > 0) ctx->namespace_count--; + if (ctx->namespace_count > 0) + ctx->namespace_count--; } /* Concatenate the namespace stack into a dotted QN (outer to inner). */ static const char *cs_namespace_qn(CSLSPContext *ctx) { - if (ctx->namespace_count == 0) return ""; + if (ctx->namespace_count == 0) + return ""; /* Compute total length first. */ size_t total = 0; for (int i = 0; i < ctx->namespace_count; i++) { total += strlen(ctx->namespace_stack[i]); - if (i > 0) total += 1; /* dot */ + if (i > 0) + total += 1; /* dot */ } char *out = (char *)cbm_arena_alloc(ctx->arena, total + 1); - if (!out) return ""; + if (!out) + return ""; size_t w = 0; for (int i = 0; i < ctx->namespace_count; i++) { - if (i > 0) out[w++] = '.'; + if (i > 0) + out[w++] = '.'; size_t len = strlen(ctx->namespace_stack[i]); memcpy(out + w, ctx->namespace_stack[i], len); w += len; @@ -293,14 +342,17 @@ static const char *cs_namespace_qn(CSLSPContext *ctx) { /* ── type-name resolution ───────────────────────────────────────── */ static const CBMRegisteredType *cs_lookup_type_qn(CSLSPContext *ctx, const char *qn) { - if (!ctx->registry || !qn) return NULL; + if (!ctx->registry || !qn) + return NULL; return cbm_registry_lookup_type(ctx->registry, qn); } /* Try a candidate QN; if found, return it (interned in arena). */ static const char *cs_try_type_qn(CSLSPContext *ctx, const char *qn) { - if (!qn) return NULL; - if (cs_lookup_type_qn(ctx, qn)) return qn; + if (!qn) + return NULL; + if (cs_lookup_type_qn(ctx, qn)) + return qn; return NULL; } @@ -316,47 +368,56 @@ static const char *cs_try_type_qn(CSLSPContext *ctx, const char *qn) { * 9. fall back to bare name (registry will fail; pipeline drops the call) */ const char *cs_resolve_type_name(CSLSPContext *ctx, const char *raw) { - if (!raw || !*raw) return NULL; + if (!raw || !*raw) + return NULL; char *name = cs_normalize_name(ctx->arena, raw); - if (!name) return NULL; + if (!name) + return NULL; /* Strip generic args for lookup purposes; the caller can re-attach * a TEMPLATE wrapping. */ char *bare = cs_strip_generic_args(ctx->arena, name); - if (!bare) bare = name; + if (!bare) + bare = name; /* 1. Predefined. */ const char *pre = cs_predefined_alias(bare); - if (pre) return pre; + if (pre) + return pre; /* 2. Type-parameter substitution. */ for (int i = 0; i < ctx->type_param_count; i++) { if (strcmp(ctx->type_param_names[i], bare) == 0) { const CBMType *arg = ctx->type_param_args[i]; - if (arg && arg->kind == CBM_TYPE_NAMED) return arg->data.named.qualified_name; + if (arg && arg->kind == CBM_TYPE_NAMED) + return arg->data.named.qualified_name; /* Builtin or unknown — fall through. */ } } /* 3. Exact registry hit. */ - if (cs_lookup_type_qn(ctx, bare)) return bare; + if (cs_lookup_type_qn(ctx, bare)) + return bare; /* 4. Inside the current class? Try enclosing class's nested type. */ if (ctx->enclosing_class_qn) { - const char *try_qn = - cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->enclosing_class_qn, bare); - if (cs_lookup_type_qn(ctx, try_qn)) return try_qn; + const char *try_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->enclosing_class_qn, bare); + if (cs_lookup_type_qn(ctx, try_qn)) + return try_qn; } /* 5. Each namespace prefix from innermost outward. */ if (ctx->namespace_count > 0) { const char *ns = cs_namespace_qn(ctx); for (;;) { - if (!ns || !*ns) break; + if (!ns || !*ns) + break; const char *try_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ns, bare); - if (cs_lookup_type_qn(ctx, try_qn)) return try_qn; + if (cs_lookup_type_qn(ctx, try_qn)) + return try_qn; const char *dot = strrchr(ns, '.'); - if (!dot) break; + if (!dot) + break; char *trim = cbm_arena_strndup(ctx->arena, ns, (size_t)(dot - ns)); ns = trim; } @@ -366,15 +427,18 @@ const char *cs_resolve_type_name(CSLSPContext *ctx, const char *raw) { * local types). */ if (ctx->module_qn && ctx->module_qn[0]) { const char *try_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, bare); - if (cs_lookup_type_qn(ctx, try_qn)) return try_qn; + if (cs_lookup_type_qn(ctx, try_qn)) + return try_qn; } /* 7. using namespace X — try X.bare. */ for (int i = 0; i < ctx->using_count; i++) { const CBMCSUsing *u = &ctx->usings[i]; - if (u->kind != CBM_CS_USING_NAMESPACE) continue; + if (u->kind != CBM_CS_USING_NAMESPACE) + continue; const char *try_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", u->target_qn, bare); - if (cs_lookup_type_qn(ctx, try_qn)) return try_qn; + if (cs_lookup_type_qn(ctx, try_qn)) + return try_qn; } /* 8. using A = X — substitute. The target may include generic args @@ -382,8 +446,10 @@ const char *cs_resolve_type_name(CSLSPContext *ctx, const char *raw) { * type name only, so we strip generic args before returning. */ for (int i = 0; i < ctx->using_count; i++) { const CBMCSUsing *u = &ctx->usings[i]; - if (u->kind != CBM_CS_USING_ALIAS) continue; - if (!u->local_name) continue; + if (u->kind != CBM_CS_USING_ALIAS) + continue; + if (!u->local_name) + continue; size_t alias_len = strlen(u->local_name); if (strncmp(bare, u->local_name, alias_len) == 0) { if (bare[alias_len] == '\0') { @@ -397,9 +463,9 @@ const char *cs_resolve_type_name(CSLSPContext *ctx, const char *raw) { /* `Alias.Sub` -> `Target.Sub` */ char *target_bare = cs_strip_generic_args(ctx->arena, u->target_qn); const char *base = target_bare ? target_bare : u->target_qn; - const char *try_qn = cbm_arena_sprintf(ctx->arena, "%s%s", base, - bare + alias_len); - if (cs_lookup_type_qn(ctx, try_qn)) return try_qn; + const char *try_qn = cbm_arena_sprintf(ctx->arena, "%s%s", base, bare + alias_len); + if (cs_lookup_type_qn(ctx, try_qn)) + return try_qn; } } } @@ -414,26 +480,43 @@ const char *cs_resolve_type_name(CSLSPContext *ctx, const char *raw) { if (the_short && *the_short) { const CBMRegisteredType *best = NULL; int best_score = -1; + int best_idx = -1; const char *namespace_dotted = cs_namespace_qn(ctx); - for (int i = 0; ctx->registry && i < ctx->registry->type_count; i++) { + /* Candidates come from the type short-name index, not a scan over + * every registered type: against the shared Tier-2 registry this + * fallback ran per unresolved type name during BOTH the registry + * build and per-file resolution, making each linear in the whole + * corpus (#1669 follow-up). The iterator degrades to the same full + * scan on an unfinalized registry, and best-score ties keep the + * scan's first-in-registration-order winner via best_idx. */ + CBMTypeShortIter ts_it; + int i; + if (ctx->registry) { + cbm_registry_types_by_short_name(ctx->registry, the_short, &ts_it); + } + while (ctx->registry && (i = cbm_type_short_iter_next(&ts_it)) >= 0) { const CBMRegisteredType *cand = &ctx->registry->types[i]; - if (!cand->short_name || strcmp(cand->short_name, the_short) != 0) continue; + if (!cand->short_name || strcmp(cand->short_name, the_short) != 0) + continue; int score = 0; if (cand->qualified_name && namespace_dotted && *namespace_dotted) { const char *m = namespace_dotted; const char *q = cand->qualified_name; while (*m && *q && *m == *q) { - if (*m == '.') score++; + if (*m == '.') + score++; m++; q++; } } - if (score > best_score) { + if (score > best_score || (score == best_score && best_idx >= 0 && i < best_idx)) { best_score = score; best = cand; + best_idx = i; } } - if (best) return best->qualified_name; + if (best) + return best->qualified_name; } } @@ -444,16 +527,18 @@ const char *cs_resolve_type_name(CSLSPContext *ctx, const char *raw) { /* Look up a method on a type, walking inheritance chain. */ const CBMRegisteredFunc *cs_lookup_method(CSLSPContext *ctx, const char *type_qn, - const char *method_name) { - if (!type_qn || !method_name) return NULL; + const char *method_name) { + if (!type_qn || !method_name) + return NULL; - const CBMRegisteredFunc *f = - cbm_registry_lookup_method(ctx->registry, type_qn, method_name); - if (f) return f; + const CBMRegisteredFunc *f = cbm_registry_lookup_method(ctx->registry, type_qn, method_name); + if (f) + return f; /* Walk inheritance chain (base + interfaces + transitive bases). */ const CBMRegisteredType *t = cs_lookup_type_qn(ctx, type_qn); - if (!t) return NULL; + if (!t) + return NULL; const char *visited[CS_LSP_PARENT_WALK_MAX]; int visited_count = 0; @@ -476,17 +561,20 @@ const CBMRegisteredFunc *cs_lookup_method(CSLSPContext *ctx, const char *type_qn break; } } - if (seen) continue; + if (seen) + continue; visited[visited_count++] = parent; f = cbm_registry_lookup_method(ctx->registry, parent, method_name); - if (f) return f; + if (f) + return f; const CBMRegisteredType *next = cs_lookup_type_qn(ctx, parent); - if (!next) continue; + if (!next) + continue; if (next->embedded_types) { - for (int i = 0; - next->embedded_types[i] && frontier_count < CS_LSP_PARENT_WALK_MAX; i++) { + for (int i = 0; next->embedded_types[i] && frontier_count < CS_LSP_PARENT_WALK_MAX; + i++) { frontier[frontier_count++] = next->embedded_types[i]; } } @@ -495,7 +583,8 @@ const CBMRegisteredFunc *cs_lookup_method(CSLSPContext *ctx, const char *type_qn /* Fall back to System.Object root members (ToString, Equals, GetHashCode). */ if (strcmp(type_qn, "System.Object") != 0) { f = cbm_registry_lookup_method(ctx->registry, "System.Object", method_name); - if (f) return f; + if (f) + return f; } return NULL; } @@ -536,43 +625,64 @@ static const CBMRegisteredFunc *cs_lookup_using_static_method(CSLSPContext *ctx, /* Search for a static extension method `M(this U self, ...)` accessible * via using-imported namespaces. Returns the func entry or NULL. */ -static const CBMRegisteredFunc *cs_lookup_extension(CSLSPContext *ctx, - const char *receiver_qn, - const char *method_name) { - if (!ctx->registry || !receiver_qn || !method_name) return NULL; +static const CBMRegisteredFunc *cs_lookup_extension(CSLSPContext *ctx, const char *receiver_qn, + const char *method_name) { + if (!ctx->registry || !receiver_qn || !method_name) + return NULL; /* Walk every static class accessible in scope. We approximate accessibility * by checking that the function's qualified_name's first dotted prefixes * line up with one of: the file's namespace, an imported using namespace, - * or the file's module. */ - for (int i = 0; i < ctx->registry->func_count; i++) { + * or the file's module. + * + * Candidates come from the free-function short-name index, NOT a scan over + * reg->funcs: against the shared Tier-2 registry (963k funcs on + * dotnet/runtime) the old full scan ran per unresolved invocation and made + * per-file resolve cost proportional to the whole corpus (#1669 follow-up). + * The iterator degrades to the same full scan on an unfinalized registry, + * and the min-index selection below preserves the scan's first-match-in- + * registration-order tie-break exactly (bucket chains are not in funcs[] + * order). */ + CBMFreeFuncIter ext_it; + cbm_registry_free_funcs_by_short_name(ctx->registry, method_name, &ext_it); + const CBMRegisteredFunc *best = NULL; + int best_idx = -1; + int i; + while ((i = cbm_free_func_iter_next(&ext_it)) >= 0) { const CBMRegisteredFunc *cand = &ctx->registry->funcs[i]; - if (!cand->short_name || strcmp(cand->short_name, method_name) != 0) continue; - if (cand->receiver_type) continue; /* must be a free static method */ - if (!cand->signature || cand->signature->kind != CBM_TYPE_FUNC) continue; + if (!cand->short_name || strcmp(cand->short_name, method_name) != 0) + continue; + if (cand->receiver_type) + continue; /* must be a free static method */ + if (!cand->signature || cand->signature->kind != CBM_TYPE_FUNC) + continue; const CBMType *sig = cand->signature; - if (!sig->data.func.param_types) continue; + if (!sig->data.func.param_types) + continue; const CBMType *first = sig->data.func.param_types[0]; - if (!first) continue; + if (!first) + continue; /* Only consider candidates marked as extensions (param[0] is named * "this" in our convention). param_names[0] == "this self" or "this". */ - if (!sig->data.func.param_names || !sig->data.func.param_names[0]) continue; + if (!sig->data.func.param_names || !sig->data.func.param_names[0]) + continue; const char *p0 = sig->data.func.param_names[0]; - if (strncmp(p0, "this", 4) != 0) continue; + if (strncmp(p0, "this", 4) != 0) + continue; /* Receiver compatibility: NAMED receiver must match receiver_qn or * one of receiver_qn's bases. TEMPLATE receiver (e.g. IEnumerable) * matches if receiver type or any base shares the template name. */ bool match = false; if (first->kind == CBM_TYPE_NAMED && first->data.named.qualified_name) { - if (strcmp(first->data.named.qualified_name, receiver_qn) == 0) match = true; + if (strcmp(first->data.named.qualified_name, receiver_qn) == 0) + match = true; if (!match && strcmp(first->data.named.qualified_name, "System.Object") == 0) match = true; /* Walk receiver's bases. */ const CBMRegisteredType *rt = cs_lookup_type_qn(ctx, receiver_qn); if (rt && rt->embedded_types && !match) { for (int j = 0; rt->embedded_types[j]; j++) { - if (strcmp(rt->embedded_types[j], - first->data.named.qualified_name) == 0) { + if (strcmp(rt->embedded_types[j], first->data.named.qualified_name) == 0) { match = true; break; } @@ -589,92 +699,113 @@ static const CBMRegisteredFunc *cs_lookup_extension(CSLSPContext *ctx, } } } - if (!match && strcmp(receiver_qn, tn) == 0) match = true; + if (!match && strcmp(receiver_qn, tn) == 0) + match = true; } - if (!match) continue; + if (!match) + continue; /* Accessibility check: the candidate's namespace must be in * usings or namespace stack. We'll be lenient here — the user's * project tree generally has consistent namespaces. */ - return cand; + if (best_idx < 0 || i < best_idx) { + best = cand; + best_idx = i; + } } - return NULL; + return best; } /* ── type AST parsing ───────────────────────────────────────────── */ const CBMType *cs_parse_type_node(CSLSPContext *ctx, TSNode node) { - if (ts_node_is_null(node)) return cbm_type_unknown(); + if (ts_node_is_null(node)) + return cbm_type_unknown(); const char *kind = ts_node_type(node); if (strcmp(kind, "predefined_type") == 0) { char *t = cs_node_text(ctx, node); - if (!t) return cbm_type_unknown(); + if (!t) + return cbm_type_unknown(); const char *aliased = cs_predefined_alias(t); - if (aliased) return cbm_type_named(ctx->arena, aliased); + if (aliased) + return cbm_type_named(ctx->arena, aliased); return cbm_type_builtin(ctx->arena, t); } if (strcmp(kind, "nullable_type") == 0) { /* Recurse into the underlying. */ TSNode inner = ts_node_child_by_field_name(node, "type", 4); - if (ts_node_is_null(inner)) inner = cs_first_named_child(node); - if (ts_node_is_null(inner)) return cbm_type_unknown(); + if (ts_node_is_null(inner)) + inner = cs_first_named_child(node); + if (ts_node_is_null(inner)) + return cbm_type_unknown(); return cs_parse_type_node(ctx, inner); } if (strcmp(kind, "array_type") == 0) { TSNode inner = ts_node_child_by_field_name(node, "type", 4); - if (ts_node_is_null(inner)) inner = cs_first_named_child(node); + if (ts_node_is_null(inner)) + inner = cs_first_named_child(node); const CBMType *elem = cs_parse_type_node(ctx, inner); - return cbm_type_template(ctx->arena, "System.Array", - (const CBMType *[]){elem, NULL}, 1); + return cbm_type_template(ctx->arena, "System.Array", (const CBMType *[]){elem, NULL}, 1); } if (strcmp(kind, "pointer_type") == 0) { TSNode inner = ts_node_child_by_field_name(node, "type", 4); - if (ts_node_is_null(inner)) inner = cs_first_named_child(node); + if (ts_node_is_null(inner)) + inner = cs_first_named_child(node); return cs_parse_type_node(ctx, inner); } if (strcmp(kind, "tuple_type") == 0) { /* Build a TUPLE of element types. */ uint32_t nc = ts_node_child_count(node); - const CBMType **elems = (const CBMType **)cbm_arena_alloc( - ctx->arena, (size_t)(nc + 1) * sizeof(*elems)); - if (!elems) return cbm_type_unknown(); + const CBMType **elems = + (const CBMType **)cbm_arena_alloc(ctx->arena, (size_t)(nc + 1) * sizeof(*elems)); + if (!elems) + return cbm_type_unknown(); int count = 0; for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(node, i); - if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; + if (ts_node_is_null(c) || !ts_node_is_named(c)) + continue; const char *ck = ts_node_type(c); - if (strcmp(ck, "tuple_element") != 0) continue; + if (strcmp(ck, "tuple_element") != 0) + continue; TSNode te = ts_node_child_by_field_name(c, "type", 4); - if (ts_node_is_null(te)) te = cs_first_named_child(c); + if (ts_node_is_null(te)) + te = cs_first_named_child(c); elems[count++] = cs_parse_type_node(ctx, te); } return cbm_type_tuple(ctx->arena, elems, count); } if (strcmp(kind, "generic_name") == 0) { TSNode name_node = ts_node_child_by_field_name(node, "name", 4); - if (ts_node_is_null(name_node)) name_node = cs_child_named_kind(node, "identifier"); + if (ts_node_is_null(name_node)) + name_node = cs_child_named_kind(node, "identifier"); TSNode args_node = ts_node_child_by_field_name(node, "type_arguments", 14); if (ts_node_is_null(args_node)) args_node = cs_child_named_kind(node, "type_argument_list"); - if (ts_node_is_null(name_node)) return cbm_type_unknown(); + if (ts_node_is_null(name_node)) + return cbm_type_unknown(); char *raw = cs_node_text(ctx, name_node); - if (!raw) return cbm_type_unknown(); + if (!raw) + return cbm_type_unknown(); const char *resolved = cs_resolve_type_name(ctx, raw); - if (!resolved) return cbm_type_unknown(); + if (!resolved) + return cbm_type_unknown(); if (ts_node_is_null(args_node)) { return cbm_type_named(ctx->arena, resolved); } /* Parse args. */ uint32_t nc = ts_node_child_count(args_node); - const CBMType **args = (const CBMType **)cbm_arena_alloc( - ctx->arena, (size_t)(nc + 1) * sizeof(*args)); - if (!args) return cbm_type_named(ctx->arena, resolved); + const CBMType **args = + (const CBMType **)cbm_arena_alloc(ctx->arena, (size_t)(nc + 1) * sizeof(*args)); + if (!args) + return cbm_type_named(ctx->arena, resolved); int count = 0; for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(args_node, i); - if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; + if (ts_node_is_null(c) || !ts_node_is_named(c)) + continue; args[count++] = cs_parse_type_node(ctx, c); } args[count] = NULL; @@ -683,16 +814,20 @@ const CBMType *cs_parse_type_node(CSLSPContext *ctx, TSNode node) { if (strcmp(kind, "qualified_name") == 0 || strcmp(kind, "identifier") == 0 || strcmp(kind, "alias_qualified_name") == 0 || strcmp(kind, "name") == 0) { char *t = cs_node_text(ctx, node); - if (!t) return cbm_type_unknown(); + if (!t) + return cbm_type_unknown(); const char *resolved = cs_resolve_type_name(ctx, t); - if (!resolved) return cbm_type_unknown(); + if (!resolved) + return cbm_type_unknown(); const char *pre = cs_predefined_alias(resolved); - if (pre) return cbm_type_named(ctx->arena, pre); + if (pre) + return cbm_type_named(ctx->arena, pre); return cbm_type_named(ctx->arena, resolved); } if (strcmp(kind, "ref_type") == 0) { TSNode inner = ts_node_child_by_field_name(node, "type", 4); - if (ts_node_is_null(inner)) inner = cs_first_named_child(node); + if (ts_node_is_null(inner)) + inner = cs_first_named_child(node); const CBMType *t = cs_parse_type_node(ctx, inner); return cbm_type_reference(ctx->arena, t); } @@ -701,11 +836,14 @@ const CBMType *cs_parse_type_node(CSLSPContext *ctx, TSNode node) { } /* Fallback: read raw text + resolve. */ char *t = cs_node_text(ctx, node); - if (!t) return cbm_type_unknown(); + if (!t) + return cbm_type_unknown(); const char *resolved = cs_resolve_type_name(ctx, t); - if (!resolved) return cbm_type_unknown(); + if (!resolved) + return cbm_type_unknown(); const char *pre = cs_predefined_alias(resolved); - if (pre) return cbm_type_named(ctx->arena, pre); + if (pre) + return cbm_type_named(ctx->arena, pre); return cbm_type_named(ctx->arena, resolved); } @@ -713,11 +851,12 @@ const CBMType *cs_parse_type_node(CSLSPContext *ctx, TSNode node) { static const CBMType *cs_unwrap_task(CSLSPContext *ctx, const CBMType *t) { (void)ctx; - if (!t) return cbm_type_unknown(); + if (!t) + return cbm_type_unknown(); if (t->kind == CBM_TYPE_TEMPLATE) { const char *n = t->data.template_type.template_name; if (n && (strcmp(n, "System.Threading.Tasks.Task") == 0 || - strcmp(n, "System.Threading.Tasks.ValueTask") == 0)) { + strcmp(n, "System.Threading.Tasks.ValueTask") == 0)) { if (t->data.template_type.template_args && t->data.template_type.template_args[0]) { return t->data.template_type.template_args[0]; } @@ -726,7 +865,7 @@ static const CBMType *cs_unwrap_task(CSLSPContext *ctx, const CBMType *t) { if (t->kind == CBM_TYPE_NAMED) { const char *qn = t->data.named.qualified_name; if (qn && (strcmp(qn, "System.Threading.Tasks.Task") == 0 || - strcmp(qn, "System.Threading.Tasks.ValueTask") == 0)) { + strcmp(qn, "System.Threading.Tasks.ValueTask") == 0)) { return cbm_type_unknown(); } } @@ -734,7 +873,8 @@ static const CBMType *cs_unwrap_task(CSLSPContext *ctx, const CBMType *t) { } static const CBMType *cs_unwrap_nullable(const CBMType *t) { - if (!t) return t; + if (!t) + return t; if (t->kind == CBM_TYPE_TEMPLATE) { const char *n = t->data.template_type.template_name; if (n && strcmp(n, "System.Nullable") == 0) { @@ -789,10 +929,12 @@ const CBMType *cs_eval_expr_type(CSLSPContext *ctx, TSNode node) { result = cbm_type_unknown(); } else if (strcmp(kind, "parenthesized_expression") == 0) { TSNode c = cs_first_named_child(node); - if (!ts_node_is_null(c)) result = cs_eval_expr_type(ctx, c); + if (!ts_node_is_null(c)) + result = cs_eval_expr_type(ctx, c); } else if (strcmp(kind, "cast_expression") == 0) { TSNode tnode = ts_node_child_by_field_name(node, "type", 4); - if (!ts_node_is_null(tnode)) result = cs_parse_type_node(ctx, tnode); + if (!ts_node_is_null(tnode)) + result = cs_parse_type_node(ctx, tnode); } else if (strcmp(kind, "as_expression") == 0) { /* x as T → T */ TSNode rhs = ts_node_child_by_field_name(node, "right", 5); @@ -800,10 +942,12 @@ const CBMType *cs_eval_expr_type(CSLSPContext *ctx, TSNode node) { uint32_t nc = ts_node_child_count(node); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(node, i); - if (!ts_node_is_null(c) && ts_node_is_named(c)) rhs = c; + if (!ts_node_is_null(c) && ts_node_is_named(c)) + rhs = c; } } - if (!ts_node_is_null(rhs)) result = cs_parse_type_node(ctx, rhs); + if (!ts_node_is_null(rhs)) + result = cs_parse_type_node(ctx, rhs); } else if (strcmp(kind, "is_expression") == 0 || strcmp(kind, "is_pattern_expression") == 0) { result = cbm_type_named(ctx->arena, "System.Boolean"); } else if (strcmp(kind, "await_expression") == 0) { @@ -812,30 +956,33 @@ const CBMType *cs_eval_expr_type(CSLSPContext *ctx, TSNode node) { const CBMType *t = cs_eval_expr_type(ctx, inner); result = cs_unwrap_task(ctx, t); } - } else if (strcmp(kind, "binary_expression") == 0 || strcmp(kind, "prefix_unary_expression") == 0 || + } else if (strcmp(kind, "binary_expression") == 0 || + strcmp(kind, "prefix_unary_expression") == 0 || strcmp(kind, "postfix_unary_expression") == 0) { /* Best-effort: take left's type. For comparisons, return Boolean. */ TSNode op = ts_node_child_by_field_name(node, "operator", 8); char *opt = ts_node_is_null(op) ? NULL : cs_node_text(ctx, op); - if (opt && (strcmp(opt, "==") == 0 || strcmp(opt, "!=") == 0 || - strcmp(opt, "<") == 0 || strcmp(opt, ">") == 0 || - strcmp(opt, "<=") == 0 || strcmp(opt, ">=") == 0 || - strcmp(opt, "&&") == 0 || strcmp(opt, "||") == 0 || - strcmp(opt, "!") == 0)) { + if (opt && (strcmp(opt, "==") == 0 || strcmp(opt, "!=") == 0 || strcmp(opt, "<") == 0 || + strcmp(opt, ">") == 0 || strcmp(opt, "<=") == 0 || strcmp(opt, ">=") == 0 || + strcmp(opt, "&&") == 0 || strcmp(opt, "||") == 0 || strcmp(opt, "!") == 0)) { result = cbm_type_named(ctx->arena, "System.Boolean"); } else { TSNode left = ts_node_child_by_field_name(node, "left", 4); - if (ts_node_is_null(left)) left = cs_first_named_child(node); - if (!ts_node_is_null(left)) result = cs_eval_expr_type(ctx, left); + if (ts_node_is_null(left)) + left = cs_first_named_child(node); + if (!ts_node_is_null(left)) + result = cs_eval_expr_type(ctx, left); } } else if (strcmp(kind, "assignment_expression") == 0) { TSNode rhs = ts_node_child_by_field_name(node, "right", 5); - if (!ts_node_is_null(rhs)) result = cs_eval_expr_type(ctx, rhs); + if (!ts_node_is_null(rhs)) + result = cs_eval_expr_type(ctx, rhs); } else if (strcmp(kind, "conditional_expression") == 0 || strcmp(kind, "switch_expression") == 0) { /* a ? b : c → take b's type. */ TSNode b = ts_node_child_by_field_name(node, "consequence", 11); - if (ts_node_is_null(b)) b = ts_node_child_by_field_name(node, "alternative", 11); + if (ts_node_is_null(b)) + b = ts_node_child_by_field_name(node, "alternative", 11); if (ts_node_is_null(b)) { uint32_t nc = ts_node_child_count(node); for (uint32_t i = 0; i < nc; i++) { @@ -863,7 +1010,8 @@ const CBMType *cs_eval_expr_type(CSLSPContext *ctx, TSNode node) { int count = 0; for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(node, i); - if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; + if (ts_node_is_null(c) || !ts_node_is_named(c)) + continue; elems[count++] = cs_eval_expr_type(ctx, c); } elems[count] = NULL; @@ -879,33 +1027,39 @@ const CBMType *cs_eval_expr_type(CSLSPContext *ctx, TSNode node) { result = cbm_type_named(ctx->arena, "System.String"); } else if (strcmp(kind, "default_expression") == 0) { TSNode tnode = ts_node_child_by_field_name(node, "type", 4); - if (!ts_node_is_null(tnode)) result = cs_parse_type_node(ctx, tnode); + if (!ts_node_is_null(tnode)) + result = cs_parse_type_node(ctx, tnode); } else if (strcmp(kind, "element_access_expression") == 0) { /* arr[i] — return element type of receiver. */ TSNode obj = ts_node_child_by_field_name(node, "expression", 10); - if (ts_node_is_null(obj)) obj = cs_first_named_child(node); + if (ts_node_is_null(obj)) + obj = cs_first_named_child(node); if (!ts_node_is_null(obj)) { const CBMType *recv = cs_eval_expr_type(ctx, obj); if (recv && recv->kind == CBM_TYPE_TEMPLATE) { const CBMType *const *args = recv->data.template_type.template_args; if (args) { int n = 0; - while (args[n]) n++; - if (n >= 2 && args[1]) result = args[1]; - else if (n >= 1 && args[0]) result = args[0]; + while (args[n]) + n++; + if (n >= 2 && args[1]) + result = args[1]; + else if (n >= 1 && args[0]) + result = args[0]; } } } } else if (strcmp(kind, "checked_expression") == 0 || strcmp(kind, "unchecked_expression") == 0) { TSNode c = cs_first_named_child(node); - if (!ts_node_is_null(c)) result = cs_eval_expr_type(ctx, c); + if (!ts_node_is_null(c)) + result = cs_eval_expr_type(ctx, c); } else if (strcmp(kind, "array_creation_expression") == 0) { TSNode tnode = ts_node_child_by_field_name(node, "type", 4); if (!ts_node_is_null(tnode)) { const CBMType *elem = cs_parse_type_node(ctx, tnode); - result = cbm_type_template(ctx->arena, "System.Array", - (const CBMType *[]){elem, NULL}, 1); + result = + cbm_type_template(ctx->arena, "System.Array", (const CBMType *[]){elem, NULL}, 1); } } else if (strcmp(kind, "implicit_array_creation_expression") == 0) { result = cbm_type_template(ctx->arena, "System.Array", @@ -920,13 +1074,15 @@ const CBMType *cs_eval_expr_type(CSLSPContext *ctx, TSNode node) { static const CBMType *cs_eval_identifier_type(CSLSPContext *ctx, TSNode node) { char *name = cs_node_text(ctx, node); - if (!name) return cbm_type_unknown(); + if (!name) + return cbm_type_unknown(); /* Local / parameter scope. cbm_scope_lookup returns cbm_type_unknown() * (a non-NULL singleton) when the name isn't bound, so we need to * check the kind, not just the pointer. */ const CBMType *bound = cbm_scope_lookup(ctx->current_scope, name); - if (bound && bound->kind != CBM_TYPE_UNKNOWN) return bound; + if (bound && bound->kind != CBM_TYPE_UNKNOWN) + return bound; /* Implicit `this` member. Try to find a field on the enclosing class. */ if (ctx->enclosing_class_qn) { @@ -934,7 +1090,8 @@ static const CBMType *cs_eval_identifier_type(CSLSPContext *ctx, TSNode node) { if (rt && rt->field_names) { for (int i = 0; rt->field_names[i]; i++) { if (strcmp(rt->field_names[i], name) == 0) { - if (rt->field_types && rt->field_types[i]) return rt->field_types[i]; + if (rt->field_types && rt->field_types[i]) + return rt->field_types[i]; return cbm_type_unknown(); } } @@ -946,7 +1103,8 @@ static const CBMType *cs_eval_identifier_type(CSLSPContext *ctx, TSNode node) { const char *resolved = cs_resolve_type_name(ctx, name); if (resolved) { const CBMRegisteredType *rt = cs_lookup_type_qn(ctx, resolved); - if (rt) return cbm_type_named(ctx->arena, rt->qualified_name); + if (rt) + return cbm_type_named(ctx->arena, rt->qualified_name); } return cbm_type_unknown(); @@ -955,14 +1113,16 @@ static const CBMType *cs_eval_identifier_type(CSLSPContext *ctx, TSNode node) { /* ── invocation ─────────────────────────────────────────────────── */ static int cs_count_args(TSNode args_node) { - if (ts_node_is_null(args_node)) return 0; + if (ts_node_is_null(args_node)) + return 0; int count = 0; uint32_t nc = ts_node_child_count(args_node); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(args_node, i); if (!ts_node_is_null(c) && ts_node_is_named(c)) { const char *k = ts_node_type(c); - if (strcmp(k, "argument") == 0) count++; + if (strcmp(k, "argument") == 0) + count++; } } return count; @@ -977,13 +1137,15 @@ static const CBMType *cs_eval_invocation_type(CSLSPContext *ctx, TSNode call) { TSNode c = ts_node_child(call, i); if (!ts_node_is_null(c) && ts_node_is_named(c)) { const char *k = ts_node_type(c); - if (strcmp(k, "argument_list") == 0) continue; + if (strcmp(k, "argument_list") == 0) + continue; fn = c; break; } } } - if (ts_node_is_null(fn)) return cbm_type_unknown(); + if (ts_node_is_null(fn)) + return cbm_type_unknown(); const char *fk = ts_node_type(fn); /* Member call: `recv.Method` or `recv?.Method`. */ @@ -991,23 +1153,29 @@ static const CBMType *cs_eval_invocation_type(CSLSPContext *ctx, TSNode call) { strcmp(fk, "conditional_access_expression") == 0) { TSNode recv = ts_node_child_by_field_name(fn, "expression", 10); TSNode name = ts_node_child_by_field_name(fn, "name", 4); - if (ts_node_is_null(recv)) recv = cs_first_named_child(fn); + if (ts_node_is_null(recv)) + recv = cs_first_named_child(fn); if (ts_node_is_null(name)) { uint32_t fnc = ts_node_child_count(fn); for (uint32_t i = 0; i < fnc; i++) { TSNode c = ts_node_child(fn, i); - if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; + if (ts_node_is_null(c) || !ts_node_is_named(c)) + continue; const char *k = ts_node_type(c); - if (strcmp(k, "identifier") == 0 || strcmp(k, "generic_name") == 0) name = c; + if (strcmp(k, "identifier") == 0 || strcmp(k, "generic_name") == 0) + name = c; } } - if (ts_node_is_null(name)) return cbm_type_unknown(); + if (ts_node_is_null(name)) + return cbm_type_unknown(); char *mname = cs_node_text(ctx, name); - if (!mname) return cbm_type_unknown(); + if (!mname) + return cbm_type_unknown(); char *bare_name = cs_strip_generic_args(ctx->arena, mname); const CBMType *recv_type = cbm_type_unknown(); - if (!ts_node_is_null(recv)) recv_type = cs_eval_expr_type(ctx, recv); + if (!ts_node_is_null(recv)) + recv_type = cs_eval_expr_type(ctx, recv); recv_type = cs_unwrap_nullable(recv_type); const char *type_qn = NULL; @@ -1016,11 +1184,14 @@ static const CBMType *cs_eval_invocation_type(CSLSPContext *ctx, TSNode call) { } else if (recv_type && recv_type->kind == CBM_TYPE_TEMPLATE) { type_qn = recv_type->data.template_type.template_name; } - if (!type_qn) return cbm_type_unknown(); + if (!type_qn) + return cbm_type_unknown(); const CBMRegisteredFunc *f = cs_lookup_method(ctx, type_qn, bare_name); - if (!f) f = cs_lookup_extension(ctx, type_qn, bare_name); - if (!f || !f->signature) return cbm_type_unknown(); + if (!f) + f = cs_lookup_extension(ctx, type_qn, bare_name); + if (!f || !f->signature) + return cbm_type_unknown(); if (f->signature->kind == CBM_TYPE_FUNC && f->signature->data.func.return_types && f->signature->data.func.return_types[0]) { const CBMType *ret = f->signature->data.func.return_types[0]; @@ -1028,11 +1199,9 @@ static const CBMType *cs_eval_invocation_type(CSLSPContext *ctx, TSNode call) { * parameters, substitute. */ if (recv_type && recv_type->kind == CBM_TYPE_TEMPLATE) { const CBMRegisteredType *rt = cs_lookup_type_qn(ctx, type_qn); - if (rt && rt->type_param_names && - recv_type->data.template_type.template_args) { - return cs_substitute_type_params( - ctx->arena, ret, rt->type_param_names, - recv_type->data.template_type.template_args); + if (rt && rt->type_param_names && recv_type->data.template_type.template_args) { + return cs_substitute_type_params(ctx->arena, ret, rt->type_param_names, + recv_type->data.template_type.template_args); } } return ret; @@ -1043,7 +1212,8 @@ static const CBMType *cs_eval_invocation_type(CSLSPContext *ctx, TSNode call) { /* Bare invocation: `Method(args)`. */ if (strcmp(fk, "identifier") == 0 || strcmp(fk, "generic_name") == 0) { char *fname = cs_node_text(ctx, fn); - if (!fname) return cbm_type_unknown(); + if (!fname) + return cbm_type_unknown(); char *bare = cs_strip_generic_args(ctx->arena, fname); /* Contextual keywords that tree-sitter parses as invocation @@ -1059,8 +1229,7 @@ static const CBMType *cs_eval_invocation_type(CSLSPContext *ctx, TSNode call) { if (ctx->enclosing_class_qn) { const CBMRegisteredFunc *f = cs_lookup_method(ctx, ctx->enclosing_class_qn, bare); if (f && f->signature && f->signature->kind == CBM_TYPE_FUNC && - f->signature->data.func.return_types && - f->signature->data.func.return_types[0]) { + f->signature->data.func.return_types && f->signature->data.func.return_types[0]) { return f->signature->data.func.return_types[0]; } } @@ -1092,19 +1261,24 @@ static const CBMType *cs_eval_invocation_type(CSLSPContext *ctx, TSNode call) { static const CBMType *cs_eval_member_access_type(CSLSPContext *ctx, TSNode node) { TSNode obj = ts_node_child_by_field_name(node, "expression", 10); TSNode name = ts_node_child_by_field_name(node, "name", 4); - if (ts_node_is_null(obj)) obj = cs_first_named_child(node); + if (ts_node_is_null(obj)) + obj = cs_first_named_child(node); if (ts_node_is_null(name)) { uint32_t nc = ts_node_child_count(node); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(node, i); - if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; + if (ts_node_is_null(c) || !ts_node_is_named(c)) + continue; const char *k = ts_node_type(c); - if (strcmp(k, "identifier") == 0 || strcmp(k, "generic_name") == 0) name = c; + if (strcmp(k, "identifier") == 0 || strcmp(k, "generic_name") == 0) + name = c; } } - if (ts_node_is_null(obj) || ts_node_is_null(name)) return cbm_type_unknown(); + if (ts_node_is_null(obj) || ts_node_is_null(name)) + return cbm_type_unknown(); char *fname = cs_node_text(ctx, name); - if (!fname) return cbm_type_unknown(); + if (!fname) + return cbm_type_unknown(); fname = cs_strip_generic_args(ctx->arena, fname); /* If obj is a type identifier → static member access */ @@ -1116,7 +1290,8 @@ static const CBMType *cs_eval_member_access_type(CSLSPContext *ctx, TSNode node) if (rt && rt->field_names) { for (int i = 0; rt->field_names[i]; i++) { if (strcmp(rt->field_names[i], fname) == 0) { - if (rt->field_types && rt->field_types[i]) return rt->field_types[i]; + if (rt->field_types && rt->field_types[i]) + return rt->field_types[i]; } } } @@ -1124,8 +1299,7 @@ static const CBMType *cs_eval_member_access_type(CSLSPContext *ctx, TSNode node) * `Math.Sqrt(...)` would propagate via invocation). */ const CBMRegisteredFunc *f = cs_lookup_method(ctx, type_qn, fname); if (f && f->signature && f->signature->kind == CBM_TYPE_FUNC && - f->signature->data.func.return_types && - f->signature->data.func.return_types[0]) { + f->signature->data.func.return_types && f->signature->data.func.return_types[0]) { return f->signature->data.func.return_types[0]; } } @@ -1133,7 +1307,8 @@ static const CBMType *cs_eval_member_access_type(CSLSPContext *ctx, TSNode node) const CBMType *recv = cs_eval_expr_type(ctx, obj); recv = cs_unwrap_nullable(recv); - if (!recv) return cbm_type_unknown(); + if (!recv) + return cbm_type_unknown(); const char *type_qn = NULL; if (recv->kind == CBM_TYPE_NAMED) { @@ -1141,7 +1316,8 @@ static const CBMType *cs_eval_member_access_type(CSLSPContext *ctx, TSNode node) } else if (recv->kind == CBM_TYPE_TEMPLATE) { type_qn = recv->data.template_type.template_name; } - if (!type_qn) return cbm_type_unknown(); + if (!type_qn) + return cbm_type_unknown(); const CBMRegisteredType *rt = cs_lookup_type_qn(ctx, type_qn); if (rt && rt->field_names) { for (int i = 0; rt->field_names[i]; i++) { @@ -1150,9 +1326,8 @@ static const CBMType *cs_eval_member_access_type(CSLSPContext *ctx, TSNode node) const CBMType *ft = rt->field_types[i]; if (recv->kind == CBM_TYPE_TEMPLATE && rt->type_param_names && recv->data.template_type.template_args) { - return cs_substitute_type_params( - ctx->arena, ft, rt->type_param_names, - recv->data.template_type.template_args); + return cs_substitute_type_params(ctx->arena, ft, rt->type_param_names, + recv->data.template_type.template_args); } return ft; } @@ -1188,7 +1363,8 @@ static const CBMType *cs_eval_object_creation_type(CSLSPContext *ctx, TSNode nod uint32_t nc = ts_node_child_count(node); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(node, i); - if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; + if (ts_node_is_null(c) || !ts_node_is_named(c)) + continue; const char *k = ts_node_type(c); if (strcmp(k, "argument_list") == 0 || strcmp(k, "initializer_expression") == 0) continue; @@ -1196,19 +1372,22 @@ static const CBMType *cs_eval_object_creation_type(CSLSPContext *ctx, TSNode nod break; } } - if (ts_node_is_null(tnode)) return cbm_type_unknown(); + if (ts_node_is_null(tnode)) + return cbm_type_unknown(); return cs_parse_type_node(ctx, tnode); } /* ── generic substitution ───────────────────────────────────────── */ static const CBMType *cs_substitute_type_params(CBMArena *arena, const CBMType *t, - const char **param_names, - const CBMType **param_args) { - if (!t || !param_names || !param_args) return t; + const char **param_names, + const CBMType **param_args) { + if (!t || !param_names || !param_args) + return t; if (t->kind == CBM_TYPE_NAMED) { const char *qn = t->data.named.qualified_name; - if (!qn) return t; + if (!qn) + return t; for (int i = 0; param_names[i]; i++) { if (strcmp(param_names[i], qn) == 0) { /* If the type is `T` and we have an arg for T, substitute. */ @@ -1219,12 +1398,15 @@ static const CBMType *cs_substitute_type_params(CBMArena *arena, const CBMType * } if (t->kind == CBM_TYPE_TEMPLATE) { const CBMType *const *old_args = t->data.template_type.template_args; - if (!old_args) return t; + if (!old_args) + return t; int n = 0; - while (old_args[n]) n++; - const CBMType **new_args = (const CBMType **)cbm_arena_alloc( - arena, (size_t)(n + 1) * sizeof(*new_args)); - if (!new_args) return t; + while (old_args[n]) + n++; + const CBMType **new_args = + (const CBMType **)cbm_arena_alloc(arena, (size_t)(n + 1) * sizeof(*new_args)); + if (!new_args) + return t; for (int i = 0; i < n; i++) { new_args[i] = cs_substitute_type_params(arena, old_args[i], param_names, param_args); } @@ -1233,9 +1415,11 @@ static const CBMType *cs_substitute_type_params(CBMArena *arena, const CBMType * } if (t->kind == CBM_TYPE_TYPE_PARAM) { const char *p = t->data.type_param.name; - if (!p) return t; + if (!p) + return t; for (int i = 0; param_names[i]; i++) { - if (strcmp(param_names[i], p) == 0) return param_args[i] ? param_args[i] : t; + if (strcmp(param_names[i], p) == 0) + return param_args[i] ? param_args[i] : t; } } return t; @@ -1244,14 +1428,17 @@ static const CBMType *cs_substitute_type_params(CBMArena *arena, const CBMType * /* ── parameter binding ──────────────────────────────────────────── */ static void cs_bind_parameters(CSLSPContext *ctx, TSNode params_node, bool is_extension) { - if (ts_node_is_null(params_node)) return; + if (ts_node_is_null(params_node)) + return; uint32_t nc = ts_node_child_count(params_node); int idx = 0; for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(params_node, i); - if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; + if (ts_node_is_null(c) || !ts_node_is_named(c)) + continue; const char *k = ts_node_type(c); - if (strcmp(k, "parameter") != 0) continue; + if (strcmp(k, "parameter") != 0) + continue; /* Detect `this` modifier (extension methods). */ bool has_this = is_extension && (idx == 0); @@ -1261,10 +1448,13 @@ static void cs_bind_parameters(CSLSPContext *ctx, TSNode params_node, bool is_ex uint32_t pc = ts_node_child_count(c); for (uint32_t j = 0; j < pc; j++) { TSNode cc = ts_node_child(c, j); - if (ts_node_is_null(cc) || !ts_node_is_named(cc)) continue; + if (ts_node_is_null(cc) || !ts_node_is_named(cc)) + continue; const char *ck = ts_node_type(cc); - if (strcmp(ck, "identifier") == 0 && ts_node_is_null(nnode)) nnode = cc; - else if (ts_node_is_null(tnode) && strcmp(ck, "identifier") != 0) tnode = cc; + if (strcmp(ck, "identifier") == 0 && ts_node_is_null(nnode)) + nnode = cc; + else if (ts_node_is_null(tnode) && strcmp(ck, "identifier") != 0) + tnode = cc; } } if (ts_node_is_null(nnode)) { @@ -1277,7 +1467,8 @@ static void cs_bind_parameters(CSLSPContext *ctx, TSNode params_node, bool is_ex continue; } const CBMType *ptype = cbm_type_unknown(); - if (!ts_node_is_null(tnode)) ptype = cs_parse_type_node(ctx, tnode); + if (!ts_node_is_null(tnode)) + ptype = cs_parse_type_node(ctx, tnode); cbm_scope_bind(ctx->current_scope, pname, ptype); (void)has_this; idx++; @@ -1449,9 +1640,11 @@ static const char *cs_resolve_callable_value(CSLSPContext *ctx, TSNode node, static void cs_process_local_decl(CSLSPContext *ctx, TSNode node) { /* local_declaration_statement -> variable_declaration */ TSNode vd = cs_child_named_kind(node, "variable_declaration"); - if (ts_node_is_null(vd)) return; + if (ts_node_is_null(vd)) + return; TSNode tnode = ts_node_child_by_field_name(vd, "type", 4); - if (ts_node_is_null(tnode)) tnode = cs_first_named_child(vd); + if (ts_node_is_null(tnode)) + tnode = cs_first_named_child(vd); /* For each declarator, bind the variable to (rhs_type or declared type). */ uint32_t nc = ts_node_child_count(vd); bool is_var = false; @@ -1461,20 +1654,24 @@ static void cs_process_local_decl(CSLSPContext *ctx, TSNode node) { is_var = true; } } - const CBMType *declared_type = ts_node_is_null(tnode) ? cbm_type_unknown() - : cs_parse_type_node(ctx, tnode); + const CBMType *declared_type = + ts_node_is_null(tnode) ? cbm_type_unknown() : cs_parse_type_node(ctx, tnode); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(vd, i); - if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; + if (ts_node_is_null(c) || !ts_node_is_named(c)) + continue; const char *k = ts_node_type(c); - if (strcmp(k, "variable_declarator") != 0) continue; + if (strcmp(k, "variable_declarator") != 0) + continue; TSNode nm = cs_child_named_kind(c, "identifier"); if (ts_node_is_null(nm)) { nm = ts_node_child_by_field_name(c, "name", 4); } - if (ts_node_is_null(nm)) continue; + if (ts_node_is_null(nm)) + continue; char *vname = cs_node_text(ctx, nm); - if (!vname) continue; + if (!vname) + continue; /* Find initializer (= rhs). variable_declarator children depend on * the grammar: tree-sitter-c-sharp emits `_identifier_or_global` * (the name) + optional `=` token + value-expression; the value @@ -1505,14 +1702,16 @@ static void cs_process_local_decl(CSLSPContext *ctx, TSNode node) { int seen_named = 0; for (uint32_t j = 0; j < cc; j++) { TSNode cn = ts_node_child(c, j); - if (ts_node_is_null(cn) || !ts_node_is_named(cn)) continue; + if (ts_node_is_null(cn) || !ts_node_is_named(cn)) + continue; const char *ck = ts_node_type(cn); if (strcmp(ck, "equals_value_clause") == 0) { TSNode rhs = cs_first_named_child(cn); if (!ts_node_is_null(rhs)) { rhs_node = rhs; const CBMType *t = cs_eval_expr_type(ctx, rhs); - if (t && t->kind != CBM_TYPE_UNKNOWN) rhs_t = t; + if (t && t->kind != CBM_TYPE_UNKNOWN) + rhs_t = t; } break; } @@ -1531,7 +1730,8 @@ static void cs_process_local_decl(CSLSPContext *ctx, TSNode node) { } } const CBMType *bind = is_var ? (rhs_t ? rhs_t : declared_type) : declared_type; - if (!bind) bind = cbm_type_unknown(); + if (!bind) + bind = cbm_type_unknown(); const char *callable_qn = ts_node_is_null(rhs_node) ? NULL : cs_resolve_callable_value(ctx, rhs_node, NULL); if (callable_qn) { @@ -1546,7 +1746,8 @@ static void cs_process_foreach(CSLSPContext *ctx, TSNode node) { /* foreach_statement: type, identifier, expression. */ TSNode tnode = ts_node_child_by_field_name(node, "type", 4); TSNode nnode = ts_node_child_by_field_name(node, "left", 4); - if (ts_node_is_null(nnode)) nnode = cs_child_named_kind(node, "identifier"); + if (ts_node_is_null(nnode)) + nnode = cs_child_named_kind(node, "identifier"); TSNode iter = ts_node_child_by_field_name(node, "right", 5); if (ts_node_is_null(iter)) { /* Fallback: look for an expression-shaped child after the type. */ @@ -1554,33 +1755,42 @@ static void cs_process_foreach(CSLSPContext *ctx, TSNode node) { bool past_id = false; for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(node, i); - if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; + if (ts_node_is_null(c) || !ts_node_is_named(c)) + continue; const char *k = ts_node_type(c); - if (strcmp(k, "identifier") == 0 && !past_id) { past_id = true; continue; } + if (strcmp(k, "identifier") == 0 && !past_id) { + past_id = true; + continue; + } if (past_id) { - if (strcmp(k, "block") == 0) break; + if (strcmp(k, "block") == 0) + break; iter = c; break; } } } - if (ts_node_is_null(nnode)) return; + if (ts_node_is_null(nnode)) + return; char *vname = cs_node_text(ctx, nnode); - if (!vname) return; + if (!vname) + return; const CBMType *element_t = cbm_type_unknown(); if (!ts_node_is_null(iter)) { const CBMType *iter_t = cs_eval_expr_type(ctx, iter); if (iter_t && iter_t->kind == CBM_TYPE_TEMPLATE) { const CBMType *const *args = iter_t->data.template_type.template_args; - if (args && args[0]) element_t = args[0]; + if (args && args[0]) + element_t = args[0]; } } if (!ts_node_is_null(tnode)) { const CBMType *declared = cs_parse_type_node(ctx, tnode); char *tt = cs_node_text(ctx, tnode); bool is_var = (tt && (strcmp(tt, "var") == 0)); - if (!is_var && declared && declared->kind != CBM_TYPE_UNKNOWN) element_t = declared; + if (!is_var && declared && declared->kind != CBM_TYPE_UNKNOWN) + element_t = declared; } cbm_scope_bind(ctx->current_scope, vname, element_t); } @@ -1599,10 +1809,13 @@ static void cs_process_using_statement(CSLSPContext *ctx, TSNode node) { static void cs_process_assignment(CSLSPContext *ctx, TSNode node) { TSNode lhs = ts_node_child_by_field_name(node, "left", 4); TSNode rhs = ts_node_child_by_field_name(node, "right", 5); - if (ts_node_is_null(lhs) || ts_node_is_null(rhs)) return; - if (!cs_node_is(lhs, "identifier")) return; + if (ts_node_is_null(lhs) || ts_node_is_null(rhs)) + return; + if (!cs_node_is(lhs, "identifier")) + return; char *vname = cs_node_text(ctx, lhs); - if (!vname) return; + if (!vname) + return; const char *callable_qn = cs_resolve_callable_value(ctx, rhs, NULL); (void)cs_eval_expr_type(ctx, rhs); if (callable_qn) { @@ -1649,7 +1862,8 @@ static void cs_invalidate_conditionally_assigned_callables(CSLSPContext *ctx, TS static void cs_emit_resolved_kind_reason(CSLSPContext *ctx, const char *callee_qn, const char *strategy, float confidence, CBMResolvedKind kind, const char *reason) { - if (!ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn) return; + if (!ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn) + return; CBMResolvedCall rc = {0}; rc.caller_qn = ctx->enclosing_func_qn; rc.callee_qn = callee_qn; @@ -1768,13 +1982,15 @@ static void cs_resolve_invocation(CSLSPContext *ctx, TSNode call) { TSNode c = ts_node_child(call, i); if (!ts_node_is_null(c) && ts_node_is_named(c)) { const char *k = ts_node_type(c); - if (strcmp(k, "argument_list") == 0) continue; + if (strcmp(k, "argument_list") == 0) + continue; fn = c; break; } } } - if (ts_node_is_null(fn)) return; + if (ts_node_is_null(fn)) + return; const char *fk = ts_node_type(fn); /* Member call. */ @@ -1782,20 +1998,24 @@ static void cs_resolve_invocation(CSLSPContext *ctx, TSNode call) { strcmp(fk, "conditional_access_expression") == 0) { TSNode recv = ts_node_child_by_field_name(fn, "expression", 10); TSNode name = ts_node_child_by_field_name(fn, "name", 4); - if (ts_node_is_null(recv)) recv = cs_first_named_child(fn); + if (ts_node_is_null(recv)) + recv = cs_first_named_child(fn); if (ts_node_is_null(name)) { uint32_t fnc = ts_node_child_count(fn); for (uint32_t i = 0; i < fnc; i++) { TSNode c = ts_node_child(fn, i); if (!ts_node_is_null(c) && ts_node_is_named(c)) { const char *k = ts_node_type(c); - if (strcmp(k, "identifier") == 0 || strcmp(k, "generic_name") == 0) name = c; + if (strcmp(k, "identifier") == 0 || strcmp(k, "generic_name") == 0) + name = c; } } } - if (ts_node_is_null(name)) return; + if (ts_node_is_null(name)) + return; char *mname = cs_node_text(ctx, name); - if (!mname) return; + if (!mname) + return; char *bare = cs_strip_generic_args(ctx->arena, mname); /* Static member call: receiver is a type identifier. */ @@ -1809,16 +2029,16 @@ static void cs_resolve_invocation(CSLSPContext *ctx, TSNode call) { return; } /* Type known, method not in registry — synth a call. */ - cs_emit_resolved(ctx, - cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, bare), - "cs_static_typed_unindexed", 0.55f); + cs_emit_resolved(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, bare), + "cs_static_typed_unindexed", 0.55f); return; } } /* Instance call. */ const CBMType *recv_type = cbm_type_unknown(); - if (!ts_node_is_null(recv)) recv_type = cs_eval_expr_type(ctx, recv); + if (!ts_node_is_null(recv)) + recv_type = cs_eval_expr_type(ctx, recv); recv_type = cs_unwrap_nullable(recv_type); const char *type_qn = NULL; if (recv_type && recv_type->kind == CBM_TYPE_NAMED) { @@ -1826,13 +2046,13 @@ static void cs_resolve_invocation(CSLSPContext *ctx, TSNode call) { } else if (recv_type && recv_type->kind == CBM_TYPE_TEMPLATE) { type_qn = recv_type->data.template_type.template_name; } - if (!type_qn) return; + if (!type_qn) + return; const CBMRegisteredFunc *f = cs_lookup_method(ctx, type_qn, bare); if (f) { - const char *strategy = - (f->receiver_type && strcmp(f->receiver_type, type_qn) == 0) - ? "cs_method_typed" - : "cs_method_inherited"; + const char *strategy = (f->receiver_type && strcmp(f->receiver_type, type_qn) == 0) + ? "cs_method_typed" + : "cs_method_inherited"; cs_emit_resolved(ctx, f->qualified_name, strategy, 0.95f); return; } @@ -1844,16 +2064,16 @@ static void cs_resolve_invocation(CSLSPContext *ctx, TSNode call) { } /* Type known, method missing — emit unindexed marker so the textual * fallback in the pipeline is suppressed. */ - cs_emit_resolved(ctx, - cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, bare), - "cs_method_typed_unindexed", 0.55f); + cs_emit_resolved(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, bare), + "cs_method_typed_unindexed", 0.55f); return; } /* Bare invocation: `Method()` */ if (strcmp(fk, "identifier") == 0 || strcmp(fk, "generic_name") == 0) { char *fname = cs_node_text(ctx, fn); - if (!fname) return; + if (!fname) + return; char *bare = cs_strip_generic_args(ctx->arena, fname); /* Callable-valued locals and parameters shadow class/static methods. @@ -1924,14 +2144,17 @@ static void cs_resolve_invocation(CSLSPContext *ctx, TSNode call) { int best_score = -1; for (int i = 0; ctx->registry && i < ctx->registry->func_count; i++) { const CBMRegisteredFunc *cand = &ctx->registry->funcs[i]; - if (cand->receiver_type) continue; - if (!cand->short_name || strcmp(cand->short_name, bare) != 0) continue; + if (cand->receiver_type) + continue; + if (!cand->short_name || strcmp(cand->short_name, bare) != 0) + continue; int score = 0; if (cand->qualified_name && ctx->module_qn) { const char *m = ctx->module_qn; const char *q = cand->qualified_name; while (*m && *q && *m == *q) { - if (*m == '.') score++; + if (*m == '.') + score++; m++; q++; } @@ -1952,12 +2175,16 @@ static void cs_resolve_object_creation(CSLSPContext *ctx, TSNode call) { /* `new Foo(...)` adds an implicit constructor CALLS edge: to Foo's ctor * Method node when one is indexed, otherwise to the Foo class node. */ TSNode tnode = ts_node_child_by_field_name(call, "type", 4); - if (ts_node_is_null(tnode)) return; + if (ts_node_is_null(tnode)) + return; const CBMType *t = cs_parse_type_node(ctx, tnode); const char *tqn = NULL; - if (t && t->kind == CBM_TYPE_NAMED) tqn = t->data.named.qualified_name; - else if (t && t->kind == CBM_TYPE_TEMPLATE) tqn = t->data.template_type.template_name; - if (!tqn) return; + if (t && t->kind == CBM_TYPE_NAMED) + tqn = t->data.named.qualified_name; + else if (t && t->kind == CBM_TYPE_TEMPLATE) + tqn = t->data.template_type.template_name; + if (!tqn) + return; /* A C# constructor is extracted as a Method whose short name is the class's * short name (the constructor_declaration `name` field is the class * identifier), so the ctor QN is `.` — never ".ctor". @@ -1979,7 +2206,8 @@ static void cs_resolve_object_creation(CSLSPContext *ctx, TSNode call) { } static void cs_resolve_calls_in_node(CSLSPContext *ctx, TSNode node) { - if (ts_node_is_null(node)) return; + if (ts_node_is_null(node)) + return; const char *kind = ts_node_type(node); /* Scope-shaping nodes. */ @@ -2035,12 +2263,10 @@ static void cs_resolve_calls_in_node(CSLSPContext *ctx, TSNode node) { strcmp(ck, "record_declaration") == 0 || strcmp(ck, "interface_declaration") == 0 || strcmp(ck, "enum_declaration") == 0 || strcmp(ck, "method_declaration") == 0 || strcmp(ck, "constructor_declaration") == 0 || - strcmp(ck, "destructor_declaration") == 0 || - strcmp(ck, "operator_declaration") == 0 || + strcmp(ck, "destructor_declaration") == 0 || strcmp(ck, "operator_declaration") == 0 || strcmp(ck, "conversion_operator_declaration") == 0 || strcmp(ck, "indexer_declaration") == 0 || strcmp(ck, "property_declaration") == 0 || - strcmp(ck, "event_declaration") == 0 || - strcmp(ck, "local_function_statement") == 0 || + strcmp(ck, "event_declaration") == 0 || strcmp(ck, "local_function_statement") == 0 || strcmp(ck, "namespace_declaration") == 0 || strcmp(ck, "file_scoped_namespace_declaration") == 0) { continue; @@ -2060,31 +2286,40 @@ static void cs_resolve_calls_in_node(CSLSPContext *ctx, TSNode node) { /* ── method/constructor processing ──────────────────────────────── */ static void cs_collect_type_params(CSLSPContext *ctx, TSNode node, const char ***out_names, - int *out_count) { + int *out_count) { *out_names = NULL; *out_count = 0; TSNode tplist = ts_node_child_by_field_name(node, "type_parameters", 15); - if (ts_node_is_null(tplist)) tplist = cs_child_named_kind(node, "type_parameter_list"); - if (ts_node_is_null(tplist)) return; + if (ts_node_is_null(tplist)) + tplist = cs_child_named_kind(node, "type_parameter_list"); + if (ts_node_is_null(tplist)) + return; uint32_t nc = ts_node_child_count(tplist); int cap = 4; const char **arr = (const char **)cbm_arena_alloc(ctx->arena, (size_t)cap * sizeof(*arr)); - if (!arr) return; + if (!arr) + return; int n = 0; for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(tplist, i); - if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; - if (strcmp(ts_node_type(c), "type_parameter") != 0) continue; + if (ts_node_is_null(c) || !ts_node_is_named(c)) + continue; + if (strcmp(ts_node_type(c), "type_parameter") != 0) + continue; TSNode id = cs_first_named_child(c); - if (ts_node_is_null(id)) continue; + if (ts_node_is_null(id)) + continue; char *name = cs_node_text(ctx, id); - if (!name) continue; + if (!name) + continue; if (n + 1 >= cap) { int new_cap = cap * 2; const char **ne = (const char **)cbm_arena_alloc(ctx->arena, (size_t)new_cap * sizeof(*ne)); - if (!ne) break; - for (int j = 0; j < n; j++) ne[j] = arr[j]; + if (!ne) + break; + for (int j = 0; j < n; j++) + ne[j] = arr[j]; arr = ne; cap = new_cap; } @@ -2097,8 +2332,8 @@ static void cs_collect_type_params(CSLSPContext *ctx, TSNode node, const char ** static void cs_process_function_like(CSLSPContext *ctx, TSNode node) { const char *kind = ts_node_type(node); - bool is_method = (strcmp(kind, "method_declaration") == 0 || - strcmp(kind, "local_function_statement") == 0); + bool is_method = + (strcmp(kind, "method_declaration") == 0 || strcmp(kind, "local_function_statement") == 0); bool is_ctor = strcmp(kind, "constructor_declaration") == 0; bool is_dtor = strcmp(kind, "destructor_declaration") == 0; bool is_property = strcmp(kind, "property_declaration") == 0; @@ -2118,19 +2353,22 @@ static void cs_process_function_like(CSLSPContext *ctx, TSNode node) { const char *short_name = NULL; if (is_method) { TSNode nm = ts_node_child_by_field_name(node, "name", 4); - if (!ts_node_is_null(nm)) short_name = cs_node_text(ctx, nm); + if (!ts_node_is_null(nm)) + short_name = cs_node_text(ctx, nm); } else if (is_ctor) { short_name = ".ctor"; } else if (is_dtor) { short_name = ".dtor"; } else if (is_property) { TSNode nm = ts_node_child_by_field_name(node, "name", 4); - if (!ts_node_is_null(nm)) short_name = cs_node_text(ctx, nm); + if (!ts_node_is_null(nm)) + short_name = cs_node_text(ctx, nm); } else if (is_indexer) { short_name = "this[]"; } else if (is_op) { TSNode op = ts_node_child_by_field_name(node, "operator", 8); - if (!ts_node_is_null(op)) short_name = cs_node_text(ctx, op); + if (!ts_node_is_null(op)) + short_name = cs_node_text(ctx, op); } if (short_name) { @@ -2151,9 +2389,8 @@ static void cs_process_function_like(CSLSPContext *ctx, TSNode node) { int tp_count = 0; cs_collect_type_params(ctx, node, &tp_names, &tp_count); if (tp_count > 0) { - const CBMType **args = - (const CBMType **)cbm_arena_alloc(ctx->arena, - (size_t)(tp_count + 1) * sizeof(*args)); + const CBMType **args = (const CBMType **)cbm_arena_alloc( + ctx->arena, (size_t)(tp_count + 1) * sizeof(*args)); if (args) { for (int i = 0; i < tp_count; i++) { args[i] = cbm_type_type_param(ctx->arena, tp_names[i]); @@ -2170,19 +2407,23 @@ static void cs_process_function_like(CSLSPContext *ctx, TSNode node) { bool is_extension = false; if (is_method) { TSNode params = ts_node_child_by_field_name(node, "parameters", 10); - if (ts_node_is_null(params)) params = cs_child_named_kind(node, "parameter_list"); + if (ts_node_is_null(params)) + params = cs_child_named_kind(node, "parameter_list"); if (!ts_node_is_null(params)) { /* Detect `this` modifier on first parameter. */ uint32_t nc = ts_node_child_count(params); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(params, i); - if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; - if (strcmp(ts_node_type(c), "parameter") != 0) continue; + if (ts_node_is_null(c) || !ts_node_is_named(c)) + continue; + if (strcmp(ts_node_type(c), "parameter") != 0) + continue; /* Look for "this" keyword child. */ uint32_t pc = ts_node_child_count(c); for (uint32_t j = 0; j < pc; j++) { TSNode cc = ts_node_child(c, j); - if (ts_node_is_null(cc)) continue; + if (ts_node_is_null(cc)) + continue; if (strcmp(ts_node_type(cc), "this") == 0) { is_extension = true; break; @@ -2194,10 +2435,12 @@ static void cs_process_function_like(CSLSPContext *ctx, TSNode node) { } } else if (is_indexer) { TSNode params = cs_child_named_kind(node, "bracketed_parameter_list"); - if (!ts_node_is_null(params)) cs_bind_parameters(ctx, params, false); + if (!ts_node_is_null(params)) + cs_bind_parameters(ctx, params, false); } else if (is_ctor || is_op) { TSNode params = cs_child_named_kind(node, "parameter_list"); - if (!ts_node_is_null(params)) cs_bind_parameters(ctx, params, false); + if (!ts_node_is_null(params)) + cs_bind_parameters(ctx, params, false); } /* Walk body. */ @@ -2207,7 +2450,8 @@ static void cs_process_function_like(CSLSPContext *ctx, TSNode node) { TSNode arrow = cs_child_named_kind(node, "arrow_expression_clause"); if (!ts_node_is_null(arrow)) { TSNode expr = cs_first_named_child(arrow); - if (!ts_node_is_null(expr)) cs_resolve_calls_in_node(ctx, expr); + if (!ts_node_is_null(expr)) + cs_resolve_calls_in_node(ctx, expr); } } else { cs_resolve_calls_in_node(ctx, body); @@ -2220,16 +2464,21 @@ static void cs_process_function_like(CSLSPContext *ctx, TSNode node) { uint32_t nc = ts_node_child_count(accessors); for (uint32_t i = 0; i < nc; i++) { TSNode a = ts_node_child(accessors, i); - if (ts_node_is_null(a) || !ts_node_is_named(a)) continue; - if (strcmp(ts_node_type(a), "accessor_declaration") != 0) continue; + if (ts_node_is_null(a) || !ts_node_is_named(a)) + continue; + if (strcmp(ts_node_type(a), "accessor_declaration") != 0) + continue; TSNode abody = ts_node_child_by_field_name(a, "body", 4); - if (ts_node_is_null(abody)) abody = cs_child_named_kind(a, "block"); - if (!ts_node_is_null(abody)) cs_resolve_calls_in_node(ctx, abody); + if (ts_node_is_null(abody)) + abody = cs_child_named_kind(a, "block"); + if (!ts_node_is_null(abody)) + cs_resolve_calls_in_node(ctx, abody); else { TSNode arrow = cs_child_named_kind(a, "arrow_expression_clause"); if (!ts_node_is_null(arrow)) { TSNode expr = cs_first_named_child(arrow); - if (!ts_node_is_null(expr)) cs_resolve_calls_in_node(ctx, expr); + if (!ts_node_is_null(expr)) + cs_resolve_calls_in_node(ctx, expr); } } } @@ -2250,19 +2499,22 @@ static void cs_process_type_decl(CSLSPContext *ctx, TSNode node) { const char *kind = ts_node_type(node); bool is_class = strcmp(kind, "class_declaration") == 0; bool is_struct = strcmp(kind, "struct_declaration") == 0; - bool is_record = strcmp(kind, "record_declaration") == 0 || - strcmp(kind, "record_struct_declaration") == 0; + bool is_record = + strcmp(kind, "record_declaration") == 0 || strcmp(kind, "record_struct_declaration") == 0; bool is_iface = strcmp(kind, "interface_declaration") == 0; bool is_enum = strcmp(kind, "enum_declaration") == 0; - (void)is_struct; (void)is_record; (void)is_iface; + (void)is_struct; + (void)is_record; + (void)is_iface; TSNode nm = ts_node_child_by_field_name(node, "name", 4); if (ts_node_is_null(nm)) { - ctx->debug && fprintf(stderr, "[cs_lsp] type decl missing name\n"); + ctx->debug &&fprintf(stderr, "[cs_lsp] type decl missing name\n"); return; } char *cname = cs_node_text(ctx, nm); - if (!cname) return; + if (!cname) + return; const char *saved_class = ctx->enclosing_class_qn; const char *saved_base = ctx->enclosing_base_qn; @@ -2274,8 +2526,7 @@ static void cs_process_type_decl(CSLSPContext *ctx, TSNode node) { /* Compute QN: prefer module-qn-prefixed (matches unified extractor), * even though the C# `namespace` is also tracked for resolution. */ if (ctx->module_qn) { - ctx->enclosing_class_qn = - cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, cname); + ctx->enclosing_class_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, cname); } else { ctx->enclosing_class_qn = cbm_arena_strdup(ctx->arena, cname); } @@ -2285,8 +2536,8 @@ static void cs_process_type_decl(CSLSPContext *ctx, TSNode node) { int tp_count = 0; cs_collect_type_params(ctx, node, &tp_names, &tp_count); if (tp_count > 0) { - const CBMType **args = (const CBMType **)cbm_arena_alloc( - ctx->arena, (size_t)(tp_count + 1) * sizeof(*args)); + const CBMType **args = + (const CBMType **)cbm_arena_alloc(ctx->arena, (size_t)(tp_count + 1) * sizeof(*args)); if (args) { for (int i = 0; i < tp_count; i++) { args[i] = cbm_type_type_param(ctx->arena, tp_names[i]); @@ -2308,36 +2559,44 @@ static void cs_process_type_decl(CSLSPContext *ctx, TSNode node) { (const char **)cbm_arena_alloc(ctx->arena, (size_t)icap * sizeof(*ifs)); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(bl, i); - if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; + if (ts_node_is_null(c) || !ts_node_is_named(c)) + continue; char *t = cs_node_text(ctx, c); - if (!t) continue; + if (!t) + continue; /* Strip generic args for QN resolution. */ char *bare = cs_strip_generic_args(ctx->arena, t); const char *qn = cs_resolve_type_name(ctx, bare ? bare : t); - if (!qn) continue; + if (!qn) + continue; /* Heuristic: first base whose registry says is_interface=true * goes to interfaces; first non-interface goes to base. */ const CBMRegisteredType *rt = cs_lookup_type_qn(ctx, qn); if (rt && rt->is_interface) { if (icount + 1 >= icap) { int nc2 = icap * 2; - const char **ne = (const char **)cbm_arena_alloc( - ctx->arena, (size_t)nc2 * sizeof(*ne)); - if (!ne) continue; - for (int j = 0; j < icount; j++) ne[j] = ifs[j]; + const char **ne = + (const char **)cbm_arena_alloc(ctx->arena, (size_t)nc2 * sizeof(*ne)); + if (!ne) + continue; + for (int j = 0; j < icount; j++) + ne[j] = ifs[j]; ifs = ne; icap = nc2; } ifs[icount++] = qn; } else { - if (!ctx->enclosing_base_qn) ctx->enclosing_base_qn = qn; + if (!ctx->enclosing_base_qn) + ctx->enclosing_base_qn = qn; else { if (icount + 1 >= icap) { int nc2 = icap * 2; const char **ne = (const char **)cbm_arena_alloc( ctx->arena, (size_t)nc2 * sizeof(*ne)); - if (!ne) continue; - for (int j = 0; j < icount; j++) ne[j] = ifs[j]; + if (!ne) + continue; + for (int j = 0; j < icount; j++) + ne[j] = ifs[j]; ifs = ne; icap = nc2; } @@ -2353,12 +2612,14 @@ static void cs_process_type_decl(CSLSPContext *ctx, TSNode node) { /* Walk body. Enums don't have methods worth resolving; skip recurse. */ if (!is_enum) { TSNode body = ts_node_child_by_field_name(node, "body", 4); - if (ts_node_is_null(body)) body = cs_child_named_kind(node, "declaration_list"); + if (ts_node_is_null(body)) + body = cs_child_named_kind(node, "declaration_list"); if (!ts_node_is_null(body)) { uint32_t nc = ts_node_child_count(body); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(body, i); - if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; + if (ts_node_is_null(c) || !ts_node_is_named(c)) + continue; const char *k = ts_node_type(c); if (strcmp(k, "method_declaration") == 0 || strcmp(k, "constructor_declaration") == 0 || @@ -2366,8 +2627,7 @@ static void cs_process_type_decl(CSLSPContext *ctx, TSNode node) { strcmp(k, "operator_declaration") == 0 || strcmp(k, "conversion_operator_declaration") == 0 || strcmp(k, "indexer_declaration") == 0 || - strcmp(k, "property_declaration") == 0 || - strcmp(k, "event_declaration") == 0) { + strcmp(k, "property_declaration") == 0 || strcmp(k, "event_declaration") == 0) { cs_process_function_like(ctx, c); } else if (strcmp(k, "class_declaration") == 0 || strcmp(k, "struct_declaration") == 0 || @@ -2414,7 +2674,8 @@ static void cs_collect_imports(CSLSPContext *ctx, TSNode root) { stack[top++] = root; while (top > 0) { TSNode n = stack[--top]; - if (ts_node_is_null(n)) continue; + if (ts_node_is_null(n)) + continue; const char *k = ts_node_type(n); if (strcmp(k, "using_directive") == 0) { /* Inspect modifiers and target. tree-sitter-c-sharp uses the @@ -2431,7 +2692,8 @@ static void cs_collect_imports(CSLSPContext *ctx, TSNode root) { if (!ts_node_is_null(alias_node)) { is_alias = true; char *t = cs_node_text(ctx, alias_node); - if (t) alias_name = t; + if (t) + alias_name = t; } /* Detect `=` token between named children to flag aliasing * even when the `alias` field isn't populated. */ @@ -2443,18 +2705,30 @@ static void cs_collect_imports(CSLSPContext *ctx, TSNode root) { memset(&post_eq, 0, sizeof(post_eq)); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(n, i); - if (ts_node_is_null(c)) continue; + if (ts_node_is_null(c)) + continue; const char *ck = ts_node_type(c); /* Tokens (anonymous and named alike) for keywords + `=`. */ - if (strcmp(ck, "global") == 0) { is_global = true; continue; } - if (strcmp(ck, "static") == 0) { is_static = true; continue; } - if (strcmp(ck, "=") == 0) { seen_equals = true; continue; } - if (!ts_node_is_named(c)) continue; + if (strcmp(ck, "global") == 0) { + is_global = true; + continue; + } + if (strcmp(ck, "static") == 0) { + is_static = true; + continue; + } + if (strcmp(ck, "=") == 0) { + seen_equals = true; + continue; + } + if (!ts_node_is_named(c)) + continue; if (strcmp(ck, "name_equals") == 0) { /* Older grammar variant. */ is_alias = true; TSNode id = cs_first_named_child(c); - if (!ts_node_is_null(id)) alias_name = cs_node_text(ctx, id); + if (!ts_node_is_null(id)) + alias_name = cs_node_text(ctx, id); continue; } /* identifier / qualified_name / generic_name. */ @@ -2469,19 +2743,22 @@ static void cs_collect_imports(CSLSPContext *ctx, TSNode root) { is_alias = true; if (!alias_name) { char *t = cs_node_text(ctx, pre_eq); - if (t) alias_name = t; + if (t) + alias_name = t; } char *t = cs_node_text(ctx, post_eq); - if (t) target = cs_normalize_name(ctx->arena, t); + if (t) + target = cs_normalize_name(ctx->arena, t); } else if (!ts_node_is_null(pre_eq) && !target) { /* Non-aliased: `using X;` — pre_eq is the target. */ char *t = cs_node_text(ctx, pre_eq); - if (t) target = cs_normalize_name(ctx->arena, t); + if (t) + target = cs_normalize_name(ctx->arena, t); } if (target) { if (is_alias) { - cs_lsp_add_using(ctx, CBM_CS_USING_ALIAS, alias_name ? alias_name : "", - target, is_global); + cs_lsp_add_using(ctx, CBM_CS_USING_ALIAS, alias_name ? alias_name : "", target, + is_global); } else if (is_static) { cs_lsp_add_using(ctx, CBM_CS_USING_STATIC, "", target, is_global); } else { @@ -2495,11 +2772,11 @@ static void cs_collect_imports(CSLSPContext *ctx, TSNode root) { uint32_t cnc = ts_node_child_count(n); for (uint32_t i = 0; i < cnc && top < 256; i++) { TSNode c = ts_node_child(n, i); - if (ts_node_is_null(c)) continue; + if (ts_node_is_null(c)) + continue; const char *ck = ts_node_type(c); if (strcmp(ck, "method_declaration") == 0 || - strcmp(ck, "constructor_declaration") == 0 || - strcmp(ck, "block") == 0) { + strcmp(ck, "constructor_declaration") == 0 || strcmp(ck, "block") == 0) { continue; } stack[top++] = c; @@ -2511,9 +2788,11 @@ static void cs_collect_imports(CSLSPContext *ctx, TSNode root) { static void cs_collect_namespace(CSLSPContext *ctx, TSNode ns_node, bool file_scoped) { TSNode nm = ts_node_child_by_field_name(ns_node, "name", 4); - if (ts_node_is_null(nm)) return; + if (ts_node_is_null(nm)) + return; char *raw = cs_node_text(ctx, nm); - if (!raw) return; + if (!raw) + return; const char *normalized = cs_normalize_name(ctx->arena, raw); cs_namespace_push(ctx, normalized); /* Walk body — file-scoped namespaces have body items as siblings of nm. @@ -2525,12 +2804,14 @@ static void cs_collect_namespace(CSLSPContext *ctx, TSNode ns_node, bool file_sc return; } TSNode body = ts_node_child_by_field_name(ns_node, "body", 4); - if (ts_node_is_null(body)) body = cs_child_named_kind(ns_node, "declaration_list"); + if (ts_node_is_null(body)) + body = cs_child_named_kind(ns_node, "declaration_list"); if (!ts_node_is_null(body)) { uint32_t nc = ts_node_child_count(body); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(body, i); - if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; + if (ts_node_is_null(c) || !ts_node_is_named(c)) + continue; const char *ck = ts_node_type(c); if (strcmp(ck, "namespace_declaration") == 0) { cs_collect_namespace(ctx, c, false); @@ -2551,7 +2832,8 @@ static void cs_collect_namespace(CSLSPContext *ctx, TSNode ns_node, bool file_sc /* ── top-level walk ─────────────────────────────────────────────── */ void cs_lsp_process_file(CSLSPContext *ctx, TSNode root) { - if (ts_node_is_null(root)) return; + if (ts_node_is_null(root)) + return; /* Pass 1: collect using directives. */ cs_collect_imports(ctx, root); @@ -2564,24 +2846,23 @@ void cs_lsp_process_file(CSLSPContext *ctx, TSNode root) { bool file_scoped_active = false; for (uint32_t i = 0; i < kn; i++) { TSNode c = kids[i]; - if (!ts_node_is_named(c)) continue; + if (!ts_node_is_named(c)) + continue; const char *k = ts_node_type(c); - if (strcmp(k, "using_directive") == 0) continue; + if (strcmp(k, "using_directive") == 0) + continue; if (strcmp(k, "file_scoped_namespace_declaration") == 0) { cs_collect_namespace(ctx, c, true); file_scoped_active = true; } else if (strcmp(k, "namespace_declaration") == 0) { cs_collect_namespace(ctx, c, false); - } else if (strcmp(k, "class_declaration") == 0 || - strcmp(k, "struct_declaration") == 0 || + } else if (strcmp(k, "class_declaration") == 0 || strcmp(k, "struct_declaration") == 0 || strcmp(k, "record_declaration") == 0 || - strcmp(k, "interface_declaration") == 0 || - strcmp(k, "enum_declaration") == 0) { + strcmp(k, "interface_declaration") == 0 || strcmp(k, "enum_declaration") == 0) { cs_process_type_decl(ctx, c); } else if (strcmp(k, "global_statement") == 0 || strcmp(k, "expression_statement") == 0 || strcmp(k, "if_statement") == 0 || strcmp(k, "for_statement") == 0 || - strcmp(k, "foreach_statement") == 0 || - strcmp(k, "for_each_statement") == 0 || + strcmp(k, "foreach_statement") == 0 || strcmp(k, "for_each_statement") == 0 || strcmp(k, "while_statement") == 0 || strcmp(k, "local_declaration_statement") == 0 || strcmp(k, "return_statement") == 0) { @@ -2596,7 +2877,8 @@ void cs_lsp_process_file(CSLSPContext *ctx, TSNode root) { ctx->enclosing_func_qn = saved; } } - if (file_scoped_active && ctx->namespace_count > 0) cs_namespace_pop(ctx); + if (file_scoped_active && ctx->namespace_count > 0) + cs_namespace_pop(ctx); } /* ── registry building from defs ─────────────────────────────────── */ @@ -2604,26 +2886,30 @@ void cs_lsp_process_file(CSLSPContext *ctx, TSNode root) { /* Parse a parenthesized signature like `(int x, string s = "")` into * NULL-terminated arrays of param names + types. Best-effort: drops * default-value expressions, ignores ref/out/in modifiers. */ -static void cs_parse_signature(CBMArena *arena, const char *signature, - CSLSPContext *ctx, const char ***out_names, - const CBMType ***out_types) { +static void cs_parse_signature(CBMArena *arena, const char *signature, CSLSPContext *ctx, + const char ***out_names, const CBMType ***out_types) { *out_names = NULL; *out_types = NULL; - if (!signature) return; + if (!signature) + return; const char *p = signature; - while (*p == ' ' || *p == '(') p++; + while (*p == ' ' || *p == '(') + p++; /* Walk param-by-param. We split on top-level ',' (ignoring those inside * generic <> brackets). */ int cap = 8; int count = 0; const char **names = (const char **)cbm_arena_alloc(arena, (size_t)cap * sizeof(*names)); const CBMType **types = (const CBMType **)cbm_arena_alloc(arena, (size_t)cap * sizeof(*types)); - if (!names || !types) return; + if (!names || !types) + return; while (*p && *p != ')') { /* Skip leading whitespace. */ - while (*p == ' ' || *p == '\t' || *p == ',') p++; - if (!*p || *p == ')') break; + while (*p == ' ' || *p == '\t' || *p == ',') + p++; + if (!*p || *p == ')') + break; /* Skip param modifiers: ref, out, in, params, this. */ const char *modifiers[] = {"ref ", "out ", "in ", "params ", "this "}; bool ate; @@ -2643,34 +2929,44 @@ static void cs_parse_signature(CBMArena *arena, const char *signature, int depth = 0; const char *last_space = NULL; while (*p && (depth > 0 || (*p != ',' && *p != ')' && *p != '='))) { - if (*p == '<') depth++; - else if (*p == '>') depth--; - else if (depth == 0 && (*p == ' ' || *p == '\t')) last_space = p; + if (*p == '<') + depth++; + else if (*p == '>') + depth--; + else if (depth == 0 && (*p == ' ' || *p == '\t')) + last_space = p; p++; } const char *name_end = p; - while (name_end > type_start && (name_end[-1] == ' ' || name_end[-1] == '\t')) name_end--; + while (name_end > type_start && (name_end[-1] == ' ' || name_end[-1] == '\t')) + name_end--; if (!last_space) { /* No name — treat the whole token as the type with synthetic name. */ char *type_text = cbm_arena_strndup(arena, type_start, (size_t)(name_end - type_start)); const CBMType *t = cbm_type_unknown(); - if (ctx) t = cs_resolve_type_name(ctx, type_text) - ? cbm_type_named(ctx->arena, cs_resolve_type_name(ctx, type_text)) - : cbm_type_unknown(); + if (ctx) + t = cs_resolve_type_name(ctx, type_text) + ? cbm_type_named(ctx->arena, cs_resolve_type_name(ctx, type_text)) + : cbm_type_unknown(); (void)t; - if (count + 1 >= cap) break; + if (count + 1 >= cap) + break; names[count] = cbm_arena_sprintf(arena, "_arg%d", count); types[count] = t; count++; } else { - char *type_text = cbm_arena_strndup(arena, type_start, (size_t)(last_space - type_start)); - char *pname = cbm_arena_strndup(arena, last_space + 1, (size_t)(name_end - last_space - 1)); + char *type_text = + cbm_arena_strndup(arena, type_start, (size_t)(last_space - type_start)); + char *pname = + cbm_arena_strndup(arena, last_space + 1, (size_t)(name_end - last_space - 1)); const CBMType *t = cbm_type_unknown(); if (ctx) { const char *resolved = cs_resolve_type_name(ctx, type_text); - if (resolved) t = cbm_type_named(ctx->arena, resolved); + if (resolved) + t = cbm_type_named(ctx->arena, resolved); } - if (count + 1 >= cap) break; + if (count + 1 >= cap) + break; names[count] = pname; types[count] = t; count++; @@ -2679,15 +2975,18 @@ static void cs_parse_signature(CBMArena *arena, const char *signature, if (*p == '=') { int d = 0; while (*p && (d > 0 || (*p != ',' && *p != ')'))) { - if (*p == '(' || *p == '<') d++; + if (*p == '(' || *p == '<') + d++; else if (*p == ')' || *p == '>') { - if (d == 0) break; + if (d == 0) + break; d--; } p++; } } - if (*p == ',') p++; + if (*p == ',') + p++; } if (count + 1 < cap) { names[count] = NULL; @@ -2701,7 +3000,9 @@ static void cs_register_type_decls(CSLSPContext *ctx, CBMTypeRegistry *reg, TSNo /* We rely on CBMFileResult.defs entries already being filled by the * unified extractor. This function is reserved for future expansions * (e.g. parsing field declarations directly from the AST). */ - (void)ctx; (void)reg; (void)root; + (void)ctx; + (void)reg; + (void)root; } /* ── field/property collection from AST ─────────────────────────── */ @@ -2720,17 +3021,18 @@ typedef struct { int cap; } cs_fields_table_t; -static cs_fields_t *cs_fields_get(CBMArena *arena, cs_fields_table_t *tab, - const char *class_qn) { +static cs_fields_t *cs_fields_get(CBMArena *arena, cs_fields_table_t *tab, const char *class_qn) { for (int i = 0; i < tab->count; i++) { - if (strcmp(tab->items[i].class_qn, class_qn) == 0) return &tab->items[i]; + if (strcmp(tab->items[i].class_qn, class_qn) == 0) + return &tab->items[i]; } if (tab->count + 1 >= tab->cap) { int new_cap = tab->cap ? tab->cap * 2 : 8; - cs_fields_t *ne = - (cs_fields_t *)cbm_arena_alloc(arena, (size_t)new_cap * sizeof(*ne)); - if (!ne) return NULL; - for (int i = 0; i < tab->count; i++) ne[i] = tab->items[i]; + cs_fields_t *ne = (cs_fields_t *)cbm_arena_alloc(arena, (size_t)new_cap * sizeof(*ne)); + if (!ne) + return NULL; + for (int i = 0; i < tab->count; i++) + ne[i] = tab->items[i]; tab->items = ne; tab->cap = new_cap; } @@ -2746,26 +3048,29 @@ static cs_fields_t *cs_fields_get(CBMArena *arena, cs_fields_table_t *tab, } static void cs_fields_add_debug(CBMArena *arena, cs_fields_t *f, const char *name, - const CBMType *type, bool debug); + const CBMType *type, bool debug); -static void cs_fields_add(CBMArena *arena, cs_fields_t *f, const char *name, - const CBMType *type) { +static void cs_fields_add(CBMArena *arena, cs_fields_t *f, const char *name, const CBMType *type) { cs_fields_add_debug(arena, f, name, type, false); } static void cs_fields_add_debug(CBMArena *arena, cs_fields_t *f, const char *name, - const CBMType *type, bool debug) { + const CBMType *type, bool debug) { (void)debug; - if (!f || !name) return; + if (!f || !name) + return; /* Dedupe. */ for (int i = 0; i < f->count; i++) { - if (strcmp(f->field_names[i], name) == 0) return; + if (strcmp(f->field_names[i], name) == 0) + return; } if (f->count + 2 >= f->cap) { int new_cap = f->cap * 2; const char **nn = (const char **)cbm_arena_alloc(arena, (size_t)new_cap * sizeof(*nn)); - const CBMType **nt = (const CBMType **)cbm_arena_alloc(arena, (size_t)new_cap * sizeof(*nt)); - if (!nn || !nt) return; + const CBMType **nt = + (const CBMType **)cbm_arena_alloc(arena, (size_t)new_cap * sizeof(*nt)); + if (!nn || !nt) + return; for (int i = 0; i < f->count; i++) { nn[i] = f->field_names[i]; nt[i] = f->field_types[i]; @@ -2782,7 +3087,7 @@ static void cs_fields_add_debug(CBMArena *arena, cs_fields_t *f, const char *nam } static void cs_collect_class_fields(CSLSPContext *ctx, CBMTypeRegistry *reg, TSNode root, - cs_fields_table_t *tab) { + cs_fields_table_t *tab) { /* Walk the AST and collect field/property/event declarations into tab. * We need the namespace + using context to resolve types, so this runs * after cs_collect_imports. */ @@ -2793,7 +3098,8 @@ static void cs_collect_class_fields(CSLSPContext *ctx, CBMTypeRegistry *reg, TSN * via parent_chain inspection (limited to direct class parent). */ while (top > 0) { TSNode n = stack[--top]; - if (ts_node_is_null(n)) continue; + if (ts_node_is_null(n)) + continue; const char *k = ts_node_type(n); if (strcmp(k, "field_declaration") == 0 || strcmp(k, "property_declaration") == 0 || @@ -2803,8 +3109,7 @@ static void cs_collect_class_fields(CSLSPContext *ctx, CBMTypeRegistry *reg, TSN const char *cls_short = NULL; while (!ts_node_is_null(p)) { const char *pk = ts_node_type(p); - if (strcmp(pk, "class_declaration") == 0 || - strcmp(pk, "struct_declaration") == 0 || + if (strcmp(pk, "class_declaration") == 0 || strcmp(pk, "struct_declaration") == 0 || strcmp(pk, "record_declaration") == 0 || strcmp(pk, "record_struct_declaration") == 0 || strcmp(pk, "interface_declaration") == 0) { @@ -2817,30 +3122,34 @@ static void cs_collect_class_fields(CSLSPContext *ctx, CBMTypeRegistry *reg, TSN p = ts_node_parent(p); } if (cls_short) { - const char *cls_qn = - ctx->module_qn - ? cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, cls_short) - : cbm_arena_strdup(ctx->arena, cls_short); + const char *cls_qn = ctx->module_qn ? cbm_arena_sprintf(ctx->arena, "%s.%s", + ctx->module_qn, cls_short) + : cbm_arena_strdup(ctx->arena, cls_short); cs_fields_t *f = cs_fields_get(ctx->arena, tab, cls_qn); if (strcmp(k, "field_declaration") == 0 || strcmp(k, "event_field_declaration") == 0) { TSNode vd = cs_child_named_kind(n, "variable_declaration"); if (!ts_node_is_null(vd)) { TSNode tnode = ts_node_child_by_field_name(vd, "type", 4); - if (ts_node_is_null(tnode)) tnode = cs_first_named_child(vd); - const CBMType *t = ts_node_is_null(tnode) - ? cbm_type_unknown() - : cs_parse_type_node(ctx, tnode); + if (ts_node_is_null(tnode)) + tnode = cs_first_named_child(vd); + const CBMType *t = ts_node_is_null(tnode) ? cbm_type_unknown() + : cs_parse_type_node(ctx, tnode); uint32_t vc = ts_node_child_count(vd); for (uint32_t i = 0; i < vc; i++) { TSNode c = ts_node_child(vd, i); - if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; - if (strcmp(ts_node_type(c), "variable_declarator") != 0) continue; + if (ts_node_is_null(c) || !ts_node_is_named(c)) + continue; + if (strcmp(ts_node_type(c), "variable_declarator") != 0) + continue; TSNode id = cs_child_named_kind(c, "identifier"); - if (ts_node_is_null(id)) id = ts_node_child_by_field_name(c, "name", 4); - if (ts_node_is_null(id)) continue; + if (ts_node_is_null(id)) + id = ts_node_child_by_field_name(c, "name", 4); + if (ts_node_is_null(id)) + continue; char *fn = cs_node_text(ctx, id); - if (fn) cs_fields_add_debug(ctx->arena, f, fn, t, ctx->debug); + if (fn) + cs_fields_add_debug(ctx->arena, f, fn, t, ctx->debug); } } } else { @@ -2850,17 +3159,16 @@ static void cs_collect_class_fields(CSLSPContext *ctx, CBMTypeRegistry *reg, TSN if (!ts_node_is_null(tnode) && !ts_node_is_null(nm)) { const CBMType *t = cs_parse_type_node(ctx, tnode); char *fn = cs_node_text(ctx, nm); - if (fn) cs_fields_add_debug(ctx->arena, f, fn, t, ctx->debug); + if (fn) + cs_fields_add_debug(ctx->arena, f, fn, t, ctx->debug); } } } } /* Primary constructor parameters of records / classes — register as fields */ - if (strcmp(k, "record_declaration") == 0 || - strcmp(k, "record_struct_declaration") == 0 || - strcmp(k, "class_declaration") == 0 || - strcmp(k, "struct_declaration") == 0) { + if (strcmp(k, "record_declaration") == 0 || strcmp(k, "record_struct_declaration") == 0 || + strcmp(k, "class_declaration") == 0 || strcmp(k, "struct_declaration") == 0) { TSNode params = cs_child_named_kind(n, "parameter_list"); TSNode nm = ts_node_child_by_field_name(n, "name", 4); if (!ts_node_is_null(params) && !ts_node_is_null(nm)) { @@ -2874,14 +3182,18 @@ static void cs_collect_class_fields(CSLSPContext *ctx, CBMTypeRegistry *reg, TSN uint32_t pc = ts_node_child_count(params); for (uint32_t i = 0; i < pc; i++) { TSNode c = ts_node_child(params, i); - if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; - if (strcmp(ts_node_type(c), "parameter") != 0) continue; + if (ts_node_is_null(c) || !ts_node_is_named(c)) + continue; + if (strcmp(ts_node_type(c), "parameter") != 0) + continue; TSNode tn = ts_node_child_by_field_name(c, "type", 4); TSNode pn = ts_node_child_by_field_name(c, "name", 4); - if (ts_node_is_null(tn) || ts_node_is_null(pn)) continue; + if (ts_node_is_null(tn) || ts_node_is_null(pn)) + continue; const CBMType *t = cs_parse_type_node(ctx, tn); char *pname = cs_node_text(ctx, pn); - if (pname) cs_fields_add(ctx->arena, f, pname, t); + if (pname) + cs_fields_add(ctx->arena, f, pname, t); } } } @@ -2890,14 +3202,14 @@ static void cs_collect_class_fields(CSLSPContext *ctx, CBMTypeRegistry *reg, TSN uint32_t cnc = ts_node_child_count(n); for (uint32_t i = 0; i < cnc && top + 1 < 512; i++) { TSNode c = ts_node_child(n, i); - if (ts_node_is_null(c)) continue; + if (ts_node_is_null(c)) + continue; const char *ck = ts_node_type(c); /* Skip method/ctor bodies — fields can't be there. */ if (strcmp(ck, "method_declaration") == 0 || strcmp(ck, "constructor_declaration") == 0 || strcmp(ck, "destructor_declaration") == 0 || - strcmp(ck, "operator_declaration") == 0 || - strcmp(ck, "indexer_declaration") == 0 || + strcmp(ck, "operator_declaration") == 0 || strcmp(ck, "indexer_declaration") == 0 || strcmp(ck, "block") == 0) { continue; } @@ -2928,18 +3240,22 @@ typedef struct { int cap; } cs_method_rt_table_t; -static void cs_method_rt_add(CBMArena *arena, cs_method_rt_table_t *tab, - const char *qn, const CBMType *rt) { - if (!qn || !rt) return; +static void cs_method_rt_add(CBMArena *arena, cs_method_rt_table_t *tab, const char *qn, + const CBMType *rt) { + if (!qn || !rt) + return; for (int i = 0; i < tab->count; i++) { - if (strcmp(tab->items[i].qn, qn) == 0) return; + if (strcmp(tab->items[i].qn, qn) == 0) + return; } if (tab->count + 1 >= tab->cap) { int new_cap = tab->cap ? tab->cap * 2 : 16; - cs_method_rt_entry_t *ne = (cs_method_rt_entry_t *)cbm_arena_alloc( - arena, (size_t)new_cap * sizeof(*ne)); - if (!ne) return; - for (int i = 0; i < tab->count; i++) ne[i] = tab->items[i]; + cs_method_rt_entry_t *ne = + (cs_method_rt_entry_t *)cbm_arena_alloc(arena, (size_t)new_cap * sizeof(*ne)); + if (!ne) + return; + for (int i = 0; i < tab->count; i++) + ne[i] = tab->items[i]; tab->items = ne; tab->cap = new_cap; } @@ -2951,13 +3267,14 @@ static void cs_method_rt_add(CBMArena *arena, cs_method_rt_table_t *tab, /* Walk the tree, finding method_declaration / property_declaration nodes * and recording (parent_class_qn + "." + method_name → return type). */ static void cs_collect_method_return_types(CSLSPContext *ctx, TSNode root, - cs_method_rt_table_t *tab) { + cs_method_rt_table_t *tab) { TSNode stack[512]; int top = 0; stack[top++] = root; while (top > 0) { TSNode n = stack[--top]; - if (ts_node_is_null(n)) continue; + if (ts_node_is_null(n)) + continue; const char *k = ts_node_type(n); bool is_method = (strcmp(k, "method_declaration") == 0); @@ -2969,13 +3286,13 @@ static void cs_collect_method_return_types(CSLSPContext *ctx, TSNode root, const char *cls_short = NULL; while (!ts_node_is_null(p)) { const char *pk = ts_node_type(p); - if (strcmp(pk, "class_declaration") == 0 || - strcmp(pk, "struct_declaration") == 0 || + if (strcmp(pk, "class_declaration") == 0 || strcmp(pk, "struct_declaration") == 0 || strcmp(pk, "record_declaration") == 0 || strcmp(pk, "record_struct_declaration") == 0 || strcmp(pk, "interface_declaration") == 0) { TSNode pn = ts_node_child_by_field_name(p, "name", 4); - if (!ts_node_is_null(pn)) cls_short = cs_node_text(ctx, pn); + if (!ts_node_is_null(pn)) + cls_short = cs_node_text(ctx, pn); break; } p = ts_node_parent(p); @@ -2984,14 +3301,14 @@ static void cs_collect_method_return_types(CSLSPContext *ctx, TSNode root, uint32_t cnc = ts_node_child_count(n); for (uint32_t i = 0; i < cnc && top + 1 < 512; i++) { TSNode c = ts_node_child(n, i); - if (!ts_node_is_null(c)) stack[top++] = c; + if (!ts_node_is_null(c)) + stack[top++] = c; } continue; } const char *cls_qn = - ctx->module_qn - ? cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, cls_short) - : cbm_arena_strdup(ctx->arena, cls_short); + ctx->module_qn ? cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, cls_short) + : cbm_arena_strdup(ctx->arena, cls_short); TSNode tnode = ts_node_child_by_field_name(n, "type", 4); TSNode nm = ts_node_child_by_field_name(n, "name", 4); /* tree-sitter-c-sharp doesn't always set the type field on @@ -3002,16 +3319,19 @@ static void cs_collect_method_return_types(CSLSPContext *ctx, TSNode root, uint32_t cn = ts_node_child_count(n); for (uint32_t i = 0; i < cn; i++) { TSNode c = ts_node_child(n, i); - if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; + if (ts_node_is_null(c) || !ts_node_is_named(c)) + continue; const char *ck = ts_node_type(c); if (strcmp(ck, "modifier") == 0 || strcmp(ck, "attribute_list") == 0 || - strcmp(ck, "type_parameter_list") == 0) continue; + strcmp(ck, "type_parameter_list") == 0) + continue; /* The name comes after the type; skip it. */ - if (!ts_node_is_null(nm) && ts_node_eq(c, nm)) continue; - if (strcmp(ck, "parameter_list") == 0 || - strcmp(ck, "block") == 0 || + if (!ts_node_is_null(nm) && ts_node_eq(c, nm)) + continue; + if (strcmp(ck, "parameter_list") == 0 || strcmp(ck, "block") == 0 || strcmp(ck, "arrow_expression_clause") == 0 || - strcmp(ck, "type_parameter_constraints_clause") == 0) break; + strcmp(ck, "type_parameter_constraints_clause") == 0) + break; /* Heuristic: the first remaining named child is the * return type. */ tnode = c; @@ -3023,18 +3343,16 @@ static void cs_collect_method_return_types(CSLSPContext *ctx, TSNode root, if (!ts_node_is_null(tnode)) { const CBMType *rt = cs_parse_type_node(ctx, tnode); cs_method_rt_add(ctx->arena, tab, - cbm_arena_sprintf(ctx->arena, "%s.%s", cls_qn, - short_name), - rt); + cbm_arena_sprintf(ctx->arena, "%s.%s", cls_qn, short_name), + rt); } } else if (!ts_node_is_null(tnode) && !ts_node_is_null(nm)) { char *short_name = cs_node_text(ctx, nm); if (short_name) { const CBMType *rt = cs_parse_type_node(ctx, tnode); cs_method_rt_add(ctx->arena, tab, - cbm_arena_sprintf(ctx->arena, "%s.%s", cls_qn, - short_name), - rt); + cbm_arena_sprintf(ctx->arena, "%s.%s", cls_qn, short_name), + rt); } } } @@ -3043,7 +3361,8 @@ static void cs_collect_method_return_types(CSLSPContext *ctx, TSNode root, uint32_t cnc = ts_node_child_count(n); for (uint32_t i = 0; i < cnc && top + 1 < 512; i++) { TSNode c = ts_node_child(n, i); - if (ts_node_is_null(c)) continue; + if (ts_node_is_null(c)) + continue; const char *ck = ts_node_type(c); if (strcmp(ck, "block") == 0 || strcmp(ck, "arrow_expression_clause") == 0) { continue; @@ -3084,19 +3403,26 @@ void cbm_cs_refine_ast_return_types(CSLSPContext *ctx, CBMTypeRegistry *reg, TSN /* ── parse return type from CBMDefinition.return_type ──────────── */ static const CBMType *cs_parse_return_type_text(CSLSPContext *ctx, const char *text) { - if (!text || !*text) return cbm_type_unknown(); + if (!text || !*text) + return cbm_type_unknown(); const char *p = text; - while (*p == ' ' || *p == ':') p++; - if (!*p) return cbm_type_unknown(); + while (*p == ' ' || *p == ':') + p++; + if (!*p) + return cbm_type_unknown(); /* Strip trailing whitespace + '?' nullability. */ size_t n = strlen(p); - while (n > 0 && (p[n - 1] == ' ' || p[n - 1] == '?')) n--; - if (n == 0) return cbm_type_unknown(); + while (n > 0 && (p[n - 1] == ' ' || p[n - 1] == '?')) + n--; + if (n == 0) + return cbm_type_unknown(); char *trimmed = cbm_arena_strndup(ctx->arena, p, n); const char *resolved = cs_resolve_type_name(ctx, trimmed); - if (!resolved) return cbm_type_unknown(); + if (!resolved) + return cbm_type_unknown(); const char *pre = cs_predefined_alias(resolved); - if (pre) return cbm_type_named(ctx->arena, pre); + if (pre) + return cbm_type_named(ctx->arena, pre); return cbm_type_named(ctx->arena, resolved); } @@ -3111,7 +3437,8 @@ static const CBMType *cs_signature_param_type_adapter(CBMArena *arena, const cha void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len, TSNode root) { - if (!result || !arena || ts_node_is_null(root)) return; + if (!result || !arena || ts_node_is_null(root)) + return; CBMTypeRegistry reg; cbm_registry_init(®, arena); @@ -3124,7 +3451,8 @@ void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source, /* Phase B: register types + functions from this file's defs. */ for (int i = 0; i < result->defs.count; i++) { CBMDefinition *d = &result->defs.items[i]; - if (!d->qualified_name || !d->name || !d->label) continue; + if (!d->qualified_name || !d->name || !d->label) + continue; if (strcmp(d->label, "Class") == 0 || strcmp(d->label, "Interface") == 0 || strcmp(d->label, "Struct") == 0 || strcmp(d->label, "Record") == 0 || @@ -3136,7 +3464,8 @@ void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source, rt.is_interface = (strcmp(d->label, "Interface") == 0); if (d->base_classes) { int bc = 0; - while (d->base_classes[bc]) bc++; + while (d->base_classes[bc]) + bc++; if (bc > 0) { const char **emb = (const char **)cbm_arena_alloc( arena, (size_t)(bc + 1) * sizeof(const char *)); @@ -3166,9 +3495,8 @@ void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source, if (strcmp(d->label, "Method") == 0 && d->parent_class) { rf.receiver_type = d->parent_class; } - const CBMType *rt = - d->return_type ? cbm_type_unknown() /* will refine below with ctx */ - : cbm_type_unknown(); + const CBMType *rt = d->return_type ? cbm_type_unknown() /* will refine below with ctx */ + : cbm_type_unknown(); const CBMType **rets = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(const CBMType *)); if (rets) { @@ -3218,8 +3546,10 @@ void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source, * directives). Also refine field types via collect_class_fields. */ for (int i = 0; i < result->defs.count; i++) { CBMDefinition *d = &result->defs.items[i]; - if (!d->qualified_name || !d->return_type) continue; - if (strcmp(d->label, "Function") != 0 && strcmp(d->label, "Method") != 0) continue; + if (!d->qualified_name || !d->return_type) + continue; + if (strcmp(d->label, "Function") != 0 && strcmp(d->label, "Method") != 0) + continue; const CBMType *rt = cs_parse_return_type_text(&ctx, d->return_type); /* Find the registered func and patch its signature. */ for (int j = 0; j < reg.func_count; j++) { @@ -3248,7 +3578,8 @@ void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source, cs_collect_class_fields(&ctx, ®, root, &tab); for (int i = 0; i < tab.count; i++) { cs_fields_t *f = &tab.items[i]; - if (f->count == 0) continue; + if (f->count == 0) + continue; for (int t = 0; t < reg.type_count; t++) { if (strcmp(reg.types[t].qualified_name, f->class_qn) == 0) { reg.types[t].field_names = f->field_names; @@ -3268,14 +3599,13 @@ void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source, if (ctx.debug) { fprintf(stderr, "[cs_lsp] module=%s defs=%d types=%d funcs=%d resolved=%d\n", - module_qn ? module_qn : "?", result->defs.count, reg.type_count, - reg.func_count, result->resolved_calls.count); + module_qn ? module_qn : "?", result->defs.count, reg.type_count, reg.func_count, + result->resolved_calls.count); for (int i = 0; i < result->resolved_calls.count; i++) { const CBMResolvedCall *rc = &result->resolved_calls.items[i]; - fprintf(stderr, "[cs_lsp] %s -> %s [%s %.2f]\n", - rc->caller_qn ? rc->caller_qn : "?", - rc->callee_qn ? rc->callee_qn : "?", - rc->strategy ? rc->strategy : "?", rc->confidence); + fprintf(stderr, "[cs_lsp] %s -> %s [%s %.2f]\n", rc->caller_qn ? rc->caller_qn : "?", + rc->callee_qn ? rc->callee_qn : "?", rc->strategy ? rc->strategy : "?", + rc->confidence); } } } @@ -3285,11 +3615,12 @@ void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source, /* Register one batch of CBMLSPDef[] into a registry. Shared by the * per-file cross-LSP path and the Tier 2 pre-built registry builder. * Def-driven (no per-file AST mutation) so deterministic per def set. */ -static void cs_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, - CBMLSPDef *defs, int def_count) { +static void cs_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, CBMLSPDef *defs, + int def_count) { for (int i = 0; i < def_count; i++) { CBMLSPDef *d = &defs[i]; - if (!d->qualified_name || !d->short_name || !d->label) continue; + if (!d->qualified_name || !d->short_name || !d->label) + continue; if (strcmp(d->label, "Class") == 0 || strcmp(d->label, "Interface") == 0 || strcmp(d->label, "Struct") == 0 || strcmp(d->label, "Record") == 0 || strcmp(d->label, "Enum") == 0 || strcmp(d->label, "Type") == 0) { @@ -3301,19 +3632,22 @@ static void cs_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, if (d->embedded_types && *d->embedded_types) { /* Parse "|"-separated list. */ int n = 1; - for (const char *p = d->embedded_types; *p; p++) if (*p == '|') n++; + for (const char *p = d->embedded_types; *p; p++) + if (*p == '|') + n++; const char **arr = (const char **)cbm_arena_alloc(arena, (size_t)(n + 1) * sizeof(*arr)); if (arr) { int idx = 0; const char *start = d->embedded_types; - for (const char *p = d->embedded_types; ; p++) { + for (const char *p = d->embedded_types;; p++) { if (*p == '|' || *p == '\0') { size_t len = (size_t)(p - start); if (len > 0) { arr[idx++] = cbm_arena_strndup(arena, start, len); } - if (*p == '\0') break; + if (*p == '\0') + break; start = p + 1; } } @@ -3356,16 +3690,52 @@ static void cs_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, /* Tier 2: build a project-wide C# registry ONCE from all defs (filters * by lang). Shared READ-ONLY across resolve workers. Def-driven → * identical entries to the per-file build, zero quality loss. */ +/* True iff a CS def registers as a TYPE — must mirror cs_register_lsp_defs' + * own label branch exactly, or the two-phase split below silently mis-buckets. */ +static bool cs_def_is_type(const CBMLSPDef *d) { + return d->label && (strcmp(d->label, "Class") == 0 || strcmp(d->label, "Interface") == 0 || + strcmp(d->label, "Struct") == 0 || strcmp(d->label, "Record") == 0 || + strcmp(d->label, "Enum") == 0 || strcmp(d->label, "Type") == 0); +} + CBMTypeRegistry *cbm_cs_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, int def_count) { - if (!arena) return NULL; + if (!arena) + return NULL; CBMTypeRegistry *reg = (CBMTypeRegistry *)cbm_arena_alloc(arena, sizeof(*reg)); - if (!reg) return NULL; + if (!reg) + return NULL; cbm_registry_init(reg, arena); cbm_csharp_stdlib_register(reg, arena); + /* Two-phase registration, same fix as the Java builder: func registration + * parses signatures, and type-name qualification does registry lookups + * that are LINEAR scans until finalize builds the hash buckets — one + * mixed pass over a 963k-def corpus measured ~280 s of lsp_cross_prepare. + * Register all TYPES (no lookups in their registration), finalize once so + * the type buckets exist, then register FUNCS with O(1) lookups, and + * finalize again to index them. Stable order per phase: overload ties + * resolve to the first registered QN match. */ + /* def_count == 0 is a valid corpus (no C# files): arena_alloc(0) returns + * NULL, which must not be mistaken for OOM — the empty registry is still + * built, finalized, and shared. */ + CBMLSPDef *cs = NULL; + if (def_count > 0) { + cs = (CBMLSPDef *)cbm_arena_alloc(arena, (size_t)def_count * sizeof(*cs)); + if (!cs) + return NULL; + } + int total = 0; + for (int i = 0; i < def_count; i++) { + if (defs[i].lang == CBM_LANG_CSHARP && cs_def_is_type(&defs[i])) + cs[total++] = defs[i]; + } + int type_count = total; for (int i = 0; i < def_count; i++) { - if (defs[i].lang != CBM_LANG_CSHARP) continue; - cs_register_lsp_defs(arena, reg, &defs[i], 1); + if (defs[i].lang == CBM_LANG_CSHARP && !cs_def_is_type(&defs[i])) + cs[total++] = defs[i]; } + cs_register_lsp_defs(arena, reg, cs, type_count); + cbm_registry_finalize(reg); + cs_register_lsp_defs(arena, reg, cs + type_count, total - type_count); cbm_registry_finalize(reg); reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */ return reg; @@ -3375,20 +3745,23 @@ void cbm_run_cs_lsp_cross_with_registry(CBMArena *arena, const char *source, int const char *module_qn, CBMTypeRegistry *reg, const char **using_targets, int using_count, TSTree *cached_tree, CBMResolvedCallArray *out) { - if (!source || !arena || !out || !reg) return; + if (!source || !arena || !out || !reg) + return; TSTree *tree = cached_tree; bool owns = false; if (!tree) { TSParser *parser = ts_parser_new(); - if (!parser) return; + if (!parser) + return; ts_parser_set_language(parser, tree_sitter_c_sharp()); - tree = ts_parser_parse_string(parser, NULL, source, - source_len > 0 ? (uint32_t)source_len : (uint32_t)strlen(source)); + tree = ts_parser_parse_string( + parser, NULL, source, source_len > 0 ? (uint32_t)source_len : (uint32_t)strlen(source)); ts_parser_delete(parser); owns = true; } - if (!tree) return; + if (!tree) + return; TSNode root = ts_tree_root_node(tree); CSLSPContext ctx; @@ -3400,14 +3773,16 @@ void cbm_run_cs_lsp_cross_with_registry(CBMArena *arena, const char *source, int } cs_lsp_process_file(&ctx, root); - if (owns) ts_tree_delete(tree); + if (owns) + ts_tree_delete(tree); } void cbm_run_cs_lsp_cross(CBMArena *arena, const char *source, int source_len, - const char *module_qn, CBMLSPDef *defs, int def_count, - const char **using_targets, int using_count, - TSTree *cached_tree, CBMResolvedCallArray *out) { - if (!source || !arena) return; + const char *module_qn, CBMLSPDef *defs, int def_count, + const char **using_targets, int using_count, TSTree *cached_tree, + CBMResolvedCallArray *out) { + if (!source || !arena) + return; CBMTypeRegistry reg; cbm_registry_init(®, arena); @@ -3419,14 +3794,16 @@ void cbm_run_cs_lsp_cross(CBMArena *arena, const char *source, int source_len, bool owns = false; if (!tree) { TSParser *parser = ts_parser_new(); - if (!parser) return; + if (!parser) + return; ts_parser_set_language(parser, tree_sitter_c_sharp()); - tree = ts_parser_parse_string(parser, NULL, source, - source_len > 0 ? (uint32_t)source_len : (uint32_t)strlen(source)); + tree = ts_parser_parse_string( + parser, NULL, source, source_len > 0 ? (uint32_t)source_len : (uint32_t)strlen(source)); ts_parser_delete(parser); owns = true; } - if (!tree) return; + if (!tree) + return; TSNode root = ts_tree_root_node(tree); /* Finalize registry — O(1) lookups. See go_lsp.c "3c. Finalize" @@ -3444,15 +3821,17 @@ void cbm_run_cs_lsp_cross(CBMArena *arena, const char *source, int source_len, } cs_lsp_process_file(&ctx, root); - if (owns) ts_tree_delete(tree); + if (owns) + ts_tree_delete(tree); } void cbm_batch_cs_lsp_cross(CBMArena *arena, CBMBatchCSLSPFile *files, int file_count, - CBMResolvedCallArray *out) { - if (!arena || !files) return; + CBMResolvedCallArray *out) { + if (!arena || !files) + return; for (int i = 0; i < file_count; i++) { CBMBatchCSLSPFile *f = &files[i]; cbm_run_cs_lsp_cross(arena, f->source, f->source_len, f->module_qn, f->defs, f->def_count, - f->using_targets, f->using_count, f->cached_tree, &out[i]); + f->using_targets, f->using_count, f->cached_tree, &out[i]); } } diff --git a/internal/cbm/lsp/java_lsp.c b/internal/cbm/lsp/java_lsp.c index a24a93cc5..9a79783ff 100644 --- a/internal/cbm/lsp/java_lsp.c +++ b/internal/cbm/lsp/java_lsp.c @@ -3806,6 +3806,156 @@ void cbm_java_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, const CBM } } +/* ── Tier 2: shared cross-registry + per-file overlay (#1669) ────────── + * + * Without this, every Java file rebuilt a whole registry from its filtered def + * set. That set is reduced by a constant FACTOR, not to a constant SIZE, so it + * grew with the corpus (measured 1,292 -> 5,031 defs/file as the tree went + * 179k -> 689k defs) and cross-file LSP became O(files x corpus_defs): 3.7 + * ms/file on v0.9.0 versus 208.9 ms/file on v0.10.5. + * + * Build the JVM def universe ONCE, seal it read-only, and share it across + * workers. Kotlin defs are included deliberately: a mixed source root resolves + * Java->Kotlin calls, and dropping them here would silently lose those edges. */ +/* Declared in src/pipeline/pass_lsp_cross.h; extern'd here to keep the + * internal/cbm layer free of src/pipeline includes. */ +extern void cbm_pxc_count_perfile_defs(uint64_t defs); + +CBMTypeRegistry *cbm_java_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, int def_count) { + if (!arena) { + return NULL; + } + CBMTypeRegistry *reg = (CBMTypeRegistry *)cbm_arena_alloc(arena, sizeof(*reg)); + if (!reg) { + return NULL; + } + cbm_registry_init(reg, arena); + cbm_java_stdlib_register(reg, arena); + /* Two-phase registration, same idiom as the per-file Pass 1a/1b split. + * + * Func registration parses signatures, and parse_param_text_full qualifies + * bare type names via cbm_registry_lookup_type. Before finalize that lookup + * is a LINEAR scan over every type registered so far, so one mixed pass is + * O(defs x types) — measured 0.44 ms/def at 689k defs, i.e. a ~300 s + * sequential build that erased the win of sharing the registry at all. + * + * Register all TYPES first (their registration does no lookups), finalize + * once so the type-QN buckets exist, then register FUNCS with O(1) type + * lookups, and finalize again to index the funcs. parse_param_text_full + * takes the registry const — no stub types appear post-finalize, so the + * post-finalize tail stays empty. Bonus over the old one-pass order: a + * signature can now resolve a type that appears LATER in defs[], the same + * source-order independence the per-file path already guarantees. */ + /* def_count == 0 is a valid corpus (no Java files): arena_alloc(0) returns + * NULL, which must not be mistaken for OOM — the empty registry is still + * built, finalized, and shared. */ + CBMLSPDef *jvm = NULL; + if (def_count > 0) { + jvm = (CBMLSPDef *)cbm_arena_alloc(arena, (size_t)def_count * sizeof(*jvm)); + if (!jvm) { + return NULL; + } + } + /* Stable two-pass partition: types in defs[] order, then funcs in defs[] + * order. Func registration order is load-bearing — overload ties resolve + * to the FIRST registered QN match — so no swap-based partition. */ + int total = 0; + for (int i = 0; i < def_count; i++) { + const char *lb = defs[i].label; + if ((defs[i].lang == CBM_LANG_JAVA || defs[i].lang == CBM_LANG_KOTLIN) && lb && + (strcmp(lb, "Class") == 0 || strcmp(lb, "Interface") == 0 || strcmp(lb, "Enum") == 0 || + strcmp(lb, "Type") == 0)) { + jvm[total++] = defs[i]; + } + } + int type_count = total; + for (int i = 0; i < def_count; i++) { + const char *lb = defs[i].label; + if ((defs[i].lang == CBM_LANG_JAVA || defs[i].lang == CBM_LANG_KOTLIN) && + !(lb && (strcmp(lb, "Class") == 0 || strcmp(lb, "Interface") == 0 || + strcmp(lb, "Enum") == 0 || strcmp(lb, "Type") == 0))) { + jvm[total++] = defs[i]; + } + } + cbm_java_register_lsp_defs(arena, reg, jvm, type_count); + cbm_registry_finalize(reg); + cbm_java_register_lsp_defs(arena, reg, jvm + type_count, total - type_count); + cbm_registry_finalize(reg); + reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */ + return reg; +} + +/* Per-file resolve against the shared base. The overlay holds exactly THIS + * FILE's own definitions (from the extract result), because patch_one_method() + * refines signatures from the AST and those writes must land on a private + * copy: its types live in the per-file arena, and the shared base is sealed + * read-only. Everything else — same-package siblings included — resolves + * through overlay.fallback into the shared registry. + * + * Scope matters for complexity, not just safety: a MODULE- or NAMESPACE-scoped + * overlay pays package-size per file, which is quadratic on repos that + * concentrate files in large packages (the #1669 growth pattern; pinned by + * test_complexity.c's shared-package gate). Own-file scope is O(file). */ +void cbm_run_java_lsp_cross_with_registry(CBMArena *arena, CBMFileResult *result, + const char *source, int source_len, const char *module_qn, + CBMTypeRegistry *reg, const char **import_names, + const char **import_qns, int import_count, + TSTree *cached_tree, CBMResolvedCallArray *out) { + if (!arena || !result || !source || !reg || !out) { + return; + } + + CBMTypeRegistry overlay; + cbm_registry_init(&overlay, arena); + overlay.fallback = reg; + + JavaLSPContext ctx; + java_lsp_init(&ctx, arena, source, source_len, &overlay, NULL, module_qn, out); + + register_local_func_or_type_from_file(&ctx, &overlay, result); + cbm_pxc_count_perfile_defs((uint64_t)result->defs.count); + + /* Index the overlay only — the base is already finalized. Scratch arena so + * per-file bucket allocations do not accumulate in the pipeline-lifetime + * arena across a large repo. */ + CBMArena idx_arena; + cbm_arena_init(&idx_arena); + cbm_registry_finalize_into(&overlay, &idx_arena); + + TSTree *tree = cached_tree; + bool owns_tree = false; + if (!tree) { + TSParser *parser = ts_parser_new(); + if (!parser) { + cbm_arena_destroy(&idx_arena); + return; + } + ts_parser_set_language(parser, tree_sitter_java()); + tree = ts_parser_parse_string(parser, NULL, source, (uint32_t)source_len); + ts_parser_delete(parser); + owns_tree = true; + } + if (!tree) { + cbm_arena_destroy(&idx_arena); + return; + } + TSNode root = ts_tree_root_node(tree); + + for (int i = 0; i < import_count; i++) { + if (!import_names[i] || !import_qns[i]) { + continue; + } + java_lsp_add_import(&ctx, import_names[i], import_qns[i], CBM_JAVA_IMPORT_TYPE); + } + + java_lsp_process_file(&ctx, root); + cbm_arena_destroy(&idx_arena); + + if (owns_tree) { + ts_tree_delete(tree); + } +} + void cbm_run_java_lsp_cross(CBMArena *arena, const char *source, int source_len, const char *module_qn, CBMLSPDef *defs, int def_count, const char **import_names, const char **import_qns, int import_count, diff --git a/internal/cbm/lsp/java_lsp.h b/internal/cbm/lsp/java_lsp.h index fd1a16c75..ea304c64b 100644 --- a/internal/cbm/lsp/java_lsp.h +++ b/internal/cbm/lsp/java_lsp.h @@ -142,6 +142,16 @@ void cbm_run_java_lsp(CBMArena *arena, CBMFileResult *result, const char *source void cbm_java_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, const CBMLSPDef *defs, int def_count); +/* Tier 2 (#1669): build the shared JVM cross registry ONCE per run, then + * resolve each file against it with a small own-module overlay. Skips the + * per-file registry build that made cross-file LSP O(files x corpus_defs). */ +CBMTypeRegistry *cbm_java_build_cross_registry(CBMArena *arena, CBMLSPDef *defs, int def_count); +void cbm_run_java_lsp_cross_with_registry(CBMArena *arena, CBMFileResult *result, + const char *source, int source_len, const char *module_qn, + CBMTypeRegistry *reg, const char **import_names, + const char **import_qns, int import_count, + TSTree *cached_tree, CBMResolvedCallArray *out); + void cbm_run_java_lsp_cross(CBMArena *arena, const char *source, int source_len, const char *module_qn, CBMLSPDef *defs, int def_count, const char **import_names, const char **import_qns, int import_count, diff --git a/internal/cbm/lsp/ts_lsp.c b/internal/cbm/lsp/ts_lsp.c index 912e0dcfc..fc8b8911f 100644 --- a/internal/cbm/lsp/ts_lsp.c +++ b/internal/cbm/lsp/ts_lsp.c @@ -70,6 +70,107 @@ static void ts_type_budget_reset(size_t source_len) { g_ts_type_budget_warned = false; } +/* Bumped on every degraded eval return (depth cap or budget exhaustion). An + * evaluation whose subtree bumped this counter must NOT be memoized: its value + * embeds the degradation, and first-eval-wins would freeze it for call sites + * that could have evaluated the node fully. */ +static _Thread_local unsigned long g_ts_eval_degraded; + +/* Expression-type memo (per file): node.id -> evaluated CBMType. + * + * ts_signature_for_call re-evaluates a call's argument expressions while + * resolving overloads, and boolean/spread chains re-enter the same + * subexpression once per enclosing alternative — the TS suite's + * objectSpreadRepeatedComplexity.js builds a union of 2^(n-1) members and + * measured 11+ s for a 3.6 KB file while emitting the same 5-node graph + * v0.9.0 emits in 0.14 s. Expression types are position-pure within a file + * pass (one node = one scope path, and the per-file walk is single-threaded + * and deterministic), so one eval per node is the correct semantics, not a + * cache trade-off. Degraded results are never stored (see + * g_ts_eval_degraded). Arena-backed, linear-probed, power-of-two capacity; + * grows by rehash and dies with the file pass. */ +typedef struct TsEvalMemo { + const void **keys; /* TSNode.id; NULL = empty slot */ + const CBMType **vals; + uint32_t cap; + uint32_t count; +} TsEvalMemo; + +static uint32_t ts_memo_slot(const TsEvalMemo *m, const void *id) { + uint64_t x = (uint64_t)(uintptr_t)id; + x ^= x >> 33; + x *= 0xff51afd7ed558ccdULL; + x ^= x >> 33; + uint32_t i = (uint32_t)x & (m->cap - 1); + while (m->keys[i] && m->keys[i] != id) + i = (i + 1) & (m->cap - 1); + return i; +} + +static const CBMType *ts_memo_get(const TsEvalMemo *m, const void *id) { + if (!m || !m->cap) + return NULL; + uint32_t i = ts_memo_slot(m, id); + return m->keys[i] ? m->vals[i] : NULL; +} + +static void ts_memo_put(TSLSPContext *ctx, const void *id, const CBMType *t) { + TsEvalMemo *m = ctx->eval_memo; + if (!m) { + m = (TsEvalMemo *)cbm_arena_alloc(ctx->arena, sizeof(*m)); + if (!m) + return; + m->cap = 1024; + m->count = 0; + m->keys = (const void **)cbm_arena_alloc(ctx->arena, m->cap * sizeof(*m->keys)); + m->vals = (const CBMType **)cbm_arena_alloc(ctx->arena, m->cap * sizeof(*m->vals)); + if (!m->keys || !m->vals) + return; + memset(m->keys, 0, m->cap * sizeof(*m->keys)); + memset(m->vals, 0, m->cap * sizeof(*m->vals)); + ctx->eval_memo = m; + } + if (m->count * 10 >= m->cap * 7) { /* grow at 70% load */ + TsEvalMemo bigger = {0}; + bigger.cap = m->cap * 2; + bigger.keys = (const void **)cbm_arena_alloc(ctx->arena, bigger.cap * sizeof(*bigger.keys)); + bigger.vals = + (const CBMType **)cbm_arena_alloc(ctx->arena, bigger.cap * sizeof(*bigger.vals)); + if (!bigger.keys || !bigger.vals) + return; /* keep serving from the full table; stores stop */ + memset(bigger.keys, 0, bigger.cap * sizeof(*bigger.keys)); + memset(bigger.vals, 0, bigger.cap * sizeof(*bigger.vals)); + for (uint32_t i = 0; i < m->cap; i++) { + if (!m->keys[i]) + continue; + uint32_t j = ts_memo_slot(&bigger, m->keys[i]); + bigger.keys[j] = m->keys[i]; + bigger.vals[j] = m->vals[i]; + } + bigger.count = m->count; + *m = bigger; /* old arrays stay in the arena */ + } + uint32_t i = ts_memo_slot(m, id); + if (!m->keys[i]) { + m->keys[i] = id; + m->vals[i] = t; + m->count++; + } +} + +#ifdef CBM_ENABLE_TEST_SEAMS +/* Complexity-regression seam: expose the thread's remaining eval budget and + * the warned flag after a cbm_run_ts_lsp call. Budget units consumed are a + * deterministic work counter — the memo turns the spread-bomb class from + * O(2^n) evals into O(nodes), which tests assert without wall-clock. */ +long cbm_ts_lsp_test_budget_remaining(void) { + return g_ts_type_budget; +} +bool cbm_ts_lsp_test_budget_warned(void) { + return g_ts_type_budget_warned; +} +#endif + #define TS_LSP_MAX_EVAL_DEPTH 64 #define TS_LSP_FIELD_LEN(s) ((uint32_t)(sizeof(s) - 1)) @@ -1978,9 +2079,43 @@ static const CBMType *return_type_of(CBMArena *arena, const CBMType *sig) { const CBMType *ts_eval_expr_type(TSLSPContext *ctx, TSNode node) { if (!ctx || ts_node_is_null(node)) return cbm_type_unknown(); - if (ctx->eval_depth > TS_LSP_MAX_EVAL_DEPTH) + /* Memo hit: O(1), charges no budget, ignores depth — the value came from a + * completed, non-degraded evaluation of this exact node. */ + { + const CBMType *memo = ts_memo_get(ctx->eval_memo, node.id); + if (memo) + return memo; + } + if (ctx->eval_depth > TS_LSP_MAX_EVAL_DEPTH) { + g_ts_eval_degraded++; return cbm_type_unknown(); + } + /* Depth alone does not bound WORK: crafted expressions (e.g. the TS test + * suite's repeated object spreads) stay under the depth cap while fanning + * out exponentially — one 3.6 KB baseline file measured 11 s here. Charge + * the same per-file budget the type-text parser uses and degrade to + * UNKNOWN on exhaustion, exactly like that path. */ + if (g_ts_type_budget >= 0) { + /* An eval entry does ~two orders of magnitude more work than one + * text-parse unit (allocations, lookups, recursion bookkeeping), so it + * charges accordingly — otherwise the default per-file budget still + * permits ~12 s of evaluation on a crafted file. Normal files run + * hundreds-to-thousands of entries, far under any budget. */ + g_ts_type_budget -= 16; + if (g_ts_type_budget < 0) { + if (!g_ts_type_budget_warned) { + g_ts_type_budget_warned = true; + fprintf(stderr, " [tslsp] expression-eval budget exhausted; degrading to " + "unknown\n"); + } + g_ts_eval_degraded++; + return cbm_type_unknown(); + } + } ctx->eval_depth++; + /* Snapshot for the memo-store guard: any degraded return in this subtree + * bumps the counter and blocks the store below. */ + unsigned long degraded_before = g_ts_eval_degraded; const CBMType *result = cbm_type_unknown(); const char *kind = ts_node_type(node); @@ -2240,6 +2375,11 @@ const CBMType *ts_eval_expr_type(TSLSPContext *ctx, TSNode node) { } ctx->eval_depth--; + /* Store only completed, non-degraded evaluations (see g_ts_eval_degraded); + * result is never NULL here (initialized to unknown), so NULL stays the + * empty-slot sentinel. */ + if (result && g_ts_eval_degraded == degraded_before) + ts_memo_put(ctx, node.id, result); return result; } diff --git a/internal/cbm/lsp/ts_lsp.h b/internal/cbm/lsp/ts_lsp.h index 63fc14476..c34ceb4ad 100644 --- a/internal/cbm/lsp/ts_lsp.h +++ b/internal/cbm/lsp/ts_lsp.h @@ -61,12 +61,23 @@ typedef struct { // Recursion guard for ts_eval_expr_type (mirrors c_lsp). int eval_depth; + // Expression-type memo: node.id -> evaluated type (lazily created on the + // first completed eval; see TsEvalMemo in ts_lsp.c). Kills the exponential + // re-evaluation of shared subexpressions under overload resolution. + struct TsEvalMemo *eval_memo; // Recursion guard for lookup_member_type: cyclic type graphs (mutually // recursive unions/wrappers across registered types) otherwise recurse // without bound — stack overflow on real repos. int member_depth; } TSLSPContext; +#ifdef CBM_ENABLE_TEST_SEAMS +// Complexity-regression seam: remaining expression-eval budget / warned flag +// for the calling thread, valid after a cbm_run_ts_lsp on the same thread. +long cbm_ts_lsp_test_budget_remaining(void); +bool cbm_ts_lsp_test_budget_warned(void); +#endif + // --- Initialization --- // Initialise a TSLSPContext for processing one file. Mode flags select dialect. diff --git a/internal/cbm/lsp/type_registry.c b/internal/cbm/lsp/type_registry.c index 28353d591..9a3fbfaa1 100644 --- a/internal/cbm/lsp/type_registry.c +++ b/internal/cbm/lsp/type_registry.c @@ -169,6 +169,46 @@ static void build_type_embed_index(CBMTypeRegistry *reg, CBMArena *idx_arena) { /* Index: short_name -> chain of FREE-function (receiver_type==NULL) indices. * Descending-iterate + prepend for ascending chain order (as above). */ +static void build_type_short_index(CBMTypeRegistry *reg, CBMArena *idx_arena) { + int tcount = 0; + for (int i = 0; i < reg->type_count; i++) { + if (reg->types[i].short_name) + tcount++; + } + if (tcount == 0) + return; + int bucket_count = next_pow2(tcount * 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)tcount * sizeof(CBMRegistryHashEntry)); + if (!buckets || !entries) + return; + for (int i = 0; i < bucket_count; i++) + buckets[i] = -1; + int idx = 0; + /* Reverse insertion so each chain yields ASCENDING types[] order — callers + * that used first-match-in-registration-order keep their tie-breaks. */ + for (int i = reg->type_count - 1; i >= 0; i--) { + const CBMRegisteredType *t = ®->types[i]; + if (!t->short_name) + continue; + uint64_t h = fnv1a(t->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; + reg->type_short_entry_count = idx; +} + static void build_ffunc_short_index(CBMTypeRegistry *reg, CBMArena *idx_arena) { int fcount = 0; for (int i = 0; i < reg->func_count; i++) { @@ -286,6 +326,42 @@ 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)); @@ -346,6 +422,7 @@ void cbm_registry_finalize_into(CBMTypeRegistry *reg, CBMArena *idx_arena) { build_method_index(reg, idx_arena); build_type_embed_index(reg, idx_arena); build_ffunc_short_index(reg, idx_arena); + build_type_short_index(reg, idx_arena); } void cbm_registry_finalize(CBMTypeRegistry *reg) { diff --git a/internal/cbm/lsp/type_registry.h b/internal/cbm/lsp/type_registry.h index 154c37a00..3ab31cf6d 100644 --- a/internal/cbm/lsp/type_registry.h +++ b/internal/cbm/lsp/type_registry.h @@ -123,6 +123,10 @@ 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; @@ -248,6 +252,22 @@ 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); int cbm_free_func_iter_next(CBMFreeFuncIter *it); diff --git a/src/foundation/profile.c b/src/foundation/profile.c index 174a1bcc6..d10c02b9b 100644 --- a/src/foundation/profile.c +++ b/src/foundation/profile.c @@ -5,6 +5,7 @@ #include "foundation/log.h" #include "foundation/compat.h" +#include #include #include #include @@ -64,3 +65,122 @@ void cbm_profile_log_elapsed(const char *phase, const char *sub, const struct ti cbm_log_info("prof", "phase", phase, "sub", sub, "ms", ms_buf, "us", us_buf); } } + +/* ── Scaling probe ──────────────────────────────────────────────────── */ + +enum { + /* Below this, scheduling noise swamps the fit and every short pass would + * cry wolf. A pass with fewer items than this is not where an O(n^2) + * regression hurts anyone. */ + SCALE_MIN_ITEMS = 512, + /* Likewise in time: a first checkpoint under a millisecond is measuring the + * clock, not the workload. */ + SCALE_MIN_FIRST_US = 1000, +}; + +static long scale_elapsed_us(const struct timespec *start) { + struct timespec now; + cbm_clock_gettime(CLOCK_MONOTONIC, &now); + return ((long)(now.tv_sec - start->tv_sec) * PROF_US_PER_SEC) + + ((now.tv_nsec - start->tv_nsec) / PROF_NS_PER_US); +} + +void cbm_scale_begin(cbm_scale_probe_t *probe, const char *phase, long total) { + if (!probe) { + return; + } + probe->phase = phase; + probe->total = total; + atomic_init(&probe->next_cp, 0); + for (int i = 0; i < CBM_SCALE_CHECKPOINTS; i++) { + probe->cp_us[i] = 0; + probe->cp_items[i] = 0; + } + cbm_clock_gettime(CLOCK_MONOTONIC, &probe->start); +} + +void cbm_scale_tick(cbm_scale_probe_t *probe, long done) { + if (!probe || probe->total < SCALE_MIN_ITEMS) { + return; + } + int cp = atomic_load_explicit(&probe->next_cp, memory_order_relaxed); + if (cp >= CBM_SCALE_CHECKPOINTS) { + return; + } + /* cp 0..3 -> total/8, total/4, total/2, total */ + long threshold = probe->total >> (CBM_SCALE_CHECKPOINTS - 1 - cp); + if (done < threshold) { + return; + } + /* Exactly one thread records each checkpoint; a loser simply moves on and + * will re-evaluate against the next threshold on its following tick. */ + if (!atomic_compare_exchange_strong_explicit(&probe->next_cp, &cp, cp + 1, memory_order_relaxed, + memory_order_relaxed)) { + return; + } + probe->cp_us[cp] = scale_elapsed_us(&probe->start); + probe->cp_items[cp] = done; +} + +double cbm_scale_fit_k(long first_n, long first_us, long last_n, long last_us) { + if (first_n <= 0 || first_us <= 0 || last_us <= 0 || last_n <= first_n) { + return -1.0; + } + /* k = log(T_last / T_first) / log(n_last / n_first) */ + return log((double)last_us / (double)first_us) / log((double)last_n / (double)first_n); +} + +void cbm_scale_end(cbm_scale_probe_t *probe) { + if (!probe || probe->total < SCALE_MIN_ITEMS) { + return; + } + int recorded = atomic_load_explicit(&probe->next_cp, memory_order_relaxed); + if (recorded < 2) { + return; /* need two points to speak about growth at all */ + } + long first_us = probe->cp_us[0]; + long first_n = probe->cp_items[0]; + long last_us = probe->cp_us[recorded - 1]; + long last_n = probe->cp_items[recorded - 1]; + if (first_us < SCALE_MIN_FIRST_US || first_n <= 0 || last_n <= first_n || last_us <= 0) { + return; + } + + double k = cbm_scale_fit_k(first_n, first_us, last_n, last_us); + if (k < 0.0) { + return; + } + long us_per_item = last_us / last_n; + + char k_buf[PROF_BUF_LEN]; + char n_buf[PROF_BUF_LEN]; + char per_buf[PROF_BUF_LEN]; + char ms_buf[PROF_BUF_LEN]; + snprintf(k_buf, sizeof(k_buf), "%.2f", k); + snprintf(n_buf, sizeof(n_buf), "%ld", last_n); + snprintf(per_buf, sizeof(per_buf), "%ld", us_per_item); + snprintf(ms_buf, sizeof(ms_buf), "%ld", last_us / PROF_US_PER_MS); + + if (k >= CBM_SCALE_WARN_K) { + /* Unflagged on purpose: this is the alarm the next accidental O(n^2) + * should trip in an ordinary user's log. */ + cbm_log_warn("scaling.superlinear", "phase", probe->phase, "k", k_buf, "items", n_buf, + "elapsed_ms", ms_buf, "us_per_item", per_buf); + } + if (!cbm_profile_active) { + return; + } + char curve[PROF_BUF_LEN * CBM_SCALE_CHECKPOINTS]; + int used = 0; + for (int i = 0; i < recorded && used < (int)sizeof(curve) - 1; i++) { + int wrote = + snprintf(curve + used, sizeof(curve) - (size_t)used, "%s%ld:%ld", i == 0 ? "" : ",", + probe->cp_items[i], probe->cp_us[i] / PROF_US_PER_MS); + if (wrote < 0) { + break; + } + used += wrote; + } + cbm_log_info("scaling", "phase", probe->phase, "k", k_buf, "items", n_buf, "elapsed_ms", ms_buf, + "us_per_item", per_buf, "curve_items_ms", curve); +} diff --git a/src/foundation/profile.h b/src/foundation/profile.h index 66b5471ec..dbf920c4f 100644 --- a/src/foundation/profile.h +++ b/src/foundation/profile.h @@ -15,12 +15,88 @@ #ifndef CBM_PROFILE_H #define CBM_PROFILE_H +#include #include #include /* Runtime-active flag. Set once by cbm_profile_init() from CBM_PROFILE env. */ extern bool cbm_profile_active; +/* ── Scaling probe — single-run superlinearity detector ────────────── + * + * Per-pass elapsed time (pass.timing) tells you a pass is slow. It cannot tell + * you WHY: big corpus, or O(n^2)? Establishing that for #1669 took an 11-corpus + * A/B across two release binaries, where parallel_resolve turned out to be 87% + * of a Java index. + * + * This makes the same question answerable from ONE run. A pass processing + * `total` items records cumulative elapsed at 1/8, 1/4, 1/2 and 1 of them, then + * fits the exponent k in T ~ n^k: + * + * k ~ 1.0 linear — cost per item is flat + * k ~ 1.5 superlinear — e.g. a candidate set that grows with the corpus + * k ~ 2.0 quadratic — every item compared against every other + * + * A pass whose k reaches CBM_SCALE_WARN_K logs `scaling.superlinear` at WARN in + * SHIPPED builds with no flag — that is the point: the next accidental O(n^2) + * should announce itself in an ordinary user's log rather than needing a + * two-binary bench to find. The full checkpoint curve is emitted only under + * CBM_PROFILE / --profile, so normal runs stay quiet. + * + * KNOW WHAT THIS DOES NOT CATCH. It measures growth WITHIN one run, so it sees + * a pass whose per-item cost rises as the run proceeds. It does NOT see the + * other quadratic shape — per-item work proportional to the WHOLE corpus, where + * every item costs the same large amount and the in-run curve is therefore + * flat. #1669 was exactly that shape: cross-file LSP rebuilt a registry from a + * corpus-scaled def set for every file, measured k=1.86 across corpus SIZES + * while this probe reported a harmless-looking 1.26 within a single run. + * + * For that shape the comparable number is `us_per_item`, which is emitted on + * every line: it is a property of the corpus, not the machine, so the same + * pass measured on a small and a large repo tells you immediately whether + * per-item cost tracks repo size. Two runs, no rebuild, no A/B of binaries. + * + * Cost when nothing is wrong: four clock reads per pass, plus one relaxed + * atomic load and an integer compare per tick. Tick from a site that is already + * throttled (a progress-log point), never from the innermost loop. */ + +/* Checkpoints at total/8, total/4, total/2, total. */ +enum { CBM_SCALE_CHECKPOINTS = 4 }; + +/* k at or above this is reported. 1.35 sits clear of measured linear passes + * (parallel_extract runs ~1.0-1.15 with normal scheduling jitter) while still + * catching the mild-but-real 1.4-1.5 cases early. */ +#define CBM_SCALE_WARN_K 1.35 + +typedef struct { + const char *phase; + long total; + struct timespec start; + _Atomic int next_cp; + long cp_us[CBM_SCALE_CHECKPOINTS]; + long cp_items[CBM_SCALE_CHECKPOINTS]; +} cbm_scale_probe_t; + +/* Arm a probe for a pass that will process `total` items. Safe with total <= 0 + * (the probe then no-ops). */ +void cbm_scale_begin(cbm_scale_probe_t *probe, const char *phase, long total); + +/* Record progress. Thread-safe and cheap: only the thread that crosses a + * checkpoint boundary does any work, and only CBM_SCALE_CHECKPOINTS times. */ +void cbm_scale_tick(cbm_scale_probe_t *probe, long done); + +/* Fit the exponent and report. Emits `scaling.superlinear` at WARN when + * k >= CBM_SCALE_WARN_K (always), and `scaling` at INFO with the full curve + * when profiling is active. */ +void cbm_scale_end(cbm_scale_probe_t *probe); + +/* The fit itself, as a pure function of two (items, microseconds) points: + * k such that T ~ n^k. Separated from the probe so the arithmetic is testable + * without depending on wall-clock timing — a test that had to *produce* a + * quadratic workload to check this would be measuring the scheduler. + * Returns -1.0 for degenerate input (non-positive, or no growth in n). */ +double cbm_scale_fit_k(long first_n, long first_us, long last_n, long last_us); + /* Initialize profiling — reads CBM_PROFILE env var. Call once at startup. */ void cbm_profile_init(void); diff --git a/src/pipeline/lsp_resolve.h b/src/pipeline/lsp_resolve.h index 4e4fab0cb..96f08e84e 100644 --- a/src/pipeline/lsp_resolve.h +++ b/src/pipeline/lsp_resolve.h @@ -28,6 +28,7 @@ #include #include #include +#include #include /* Confidence floor below which LSP-resolved calls are ignored and the @@ -856,6 +857,18 @@ static inline const CBMResolvedCall *cbm_pipeline_find_lsp_reference( * graph's file-shaped QN, accept one globally unique callable short name. * * Returns the matching node, or NULL if neither lookup hits. */ +/* Tail-match cost counters (#1669). + * + * The scan below is the one candidate loop in the resolve path with NO cap — + * cbm_registry_resolve bails at REG_MAX_CANDIDATES=256, this does not. Its + * candidate set is every node sharing a short name, which grows with the + * corpus, so `candidates scanned` is the quantity that turns resolve from O(n) + * into O(n^2). Counted always (two relaxed adds), surfaced by pass_parallel at + * end of resolve: a candidates/lookup ratio that grows with corpus size IS the + * regression, visible in one run. */ +extern _Atomic uint64_t g_lsp_tail_lookups; +extern _Atomic uint64_t g_lsp_tail_candidates; + static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node_policy( const cbm_gbuf_t *gbuf, const char *project_name, const char *callee_qn, bool allow_tail_match, bool allow_unique_callable_fallback) { @@ -901,6 +914,8 @@ static inline const cbm_gbuf_node_t *cbm_pipeline_lsp_target_node_policy( if (cbm_gbuf_find_by_name(gbuf, short_name, &hits, &hit_count) != 0 || hit_count == 0) { return NULL; } + atomic_fetch_add_explicit(&g_lsp_tail_lookups, 1, memory_order_relaxed); + atomic_fetch_add_explicit(&g_lsp_tail_candidates, (uint64_t)hit_count, memory_order_relaxed); const cbm_gbuf_node_t *match = NULL; const cbm_gbuf_node_t *unique_callable = NULL; diff --git a/src/pipeline/pass_lsp_cross.c b/src/pipeline/pass_lsp_cross.c index e6b4eea77..17491a9ea 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -36,6 +36,7 @@ #include #include #include +#include #include /* ── Constants ─────────────────────────────────────────────────── */ @@ -1001,6 +1002,34 @@ void cbm_pxc_run_one_ts(CBMFileResult *r, const char *source, int source_len, co * `rust_shared_get` supplies the lazily-built shared Rust all-defs registry * (the parallel resolver owns its once-guard); NULL means "no shared rust * registry available" and rust NULL-filter files take the per-file build. */ +/* Per-file registry-build cost counters (#1669). Surfaced by pass_parallel at + * end of resolve. */ +_Atomic uint64_t g_pxc_defs_registered = 0; +_Atomic uint64_t g_pxc_build_files = 0; +_Atomic uint64_t g_pxc_filter_files = 0; +_Atomic uint64_t g_pxc_filter_failed = 0; + +/* Overlay registrations count as per-file registry work too: the complexity + * gate (test_complexity.c) sums ALL defs registered per file, whichever path + * built them. Without this the shared-registry languages would report zero and + * the linearity gate would pass vacuously. */ +void cbm_pxc_count_perfile_defs(uint64_t defs) { + atomic_fetch_add_explicit(&g_pxc_defs_registered, defs, memory_order_relaxed); + atomic_fetch_add_explicit(&g_pxc_build_files, 1, memory_order_relaxed); +} + +void cbm_pxc_filter_stats(uint64_t *defs_registered, uint64_t *build_files, uint64_t *filter_files, + uint64_t *filter_failed) { + if (defs_registered) + *defs_registered = atomic_load_explicit(&g_pxc_defs_registered, memory_order_relaxed); + if (build_files) + *build_files = atomic_load_explicit(&g_pxc_build_files, memory_order_relaxed); + if (filter_files) + *filter_files = atomic_load_explicit(&g_pxc_filter_files, memory_order_relaxed); + if (filter_failed) + *filter_failed = atomic_load_explicit(&g_pxc_filter_failed, memory_order_relaxed); +} + void cbm_pxc_dispatch_file(CBMLanguage lang, CBMFileResult *result, const char *source, int source_len, const char *rel, const char *def_module, const CBMCrossLspRegistries *cross_registries, @@ -1050,6 +1079,14 @@ void cbm_pxc_dispatch_file(CBMLanguage lang, CBMFileResult *result, const char * &result->resolved_calls); used_prebuilt = true; break; + case CBM_LANG_JAVA: + /* Own-module defs go into a per-file overlay; imports and stdlib + * resolve through the shared base (#1669). */ + cbm_run_java_lsp_cross_with_registry( + &result->arena, result, source, source_len, def_module, prebuilt, imp_keys, + imp_vals, imp_count, result->cached_tree, &result->resolved_calls); + used_prebuilt = true; + break; case CBM_LANG_JAVASCRIPT: case CBM_LANG_TYPESCRIPT: case CBM_LANG_TSX: { @@ -1112,7 +1149,17 @@ void cbm_pxc_dispatch_file(CBMLanguage lang, CBMFileResult *result, const char * file_defs = filtered; file_def_count = filtered_count; } + atomic_fetch_add_explicit(&g_pxc_filter_files, 1, memory_order_relaxed); + if (!filter_succeeded) { + atomic_fetch_add_explicit(&g_pxc_filter_failed, 1, memory_order_relaxed); + } } + /* Per-file registry build cost is driven by THIS number. If it tracks the + * corpus instead of the file's own module + imports, cross-file LSP is + * O(files x corpus_defs) — see #1669. */ + atomic_fetch_add_explicit(&g_pxc_defs_registered, (uint64_t)file_def_count, + memory_order_relaxed); + atomic_fetch_add_explicit(&g_pxc_build_files, 1, memory_order_relaxed); if (lang == CBM_LANG_RUST) { CBMTypeRegistry *shared = rust_shared_get ? rust_shared_get(rust_shared_ctx) : NULL; if (shared) { diff --git a/src/pipeline/pass_lsp_cross.h b/src/pipeline/pass_lsp_cross.h index 11269ce62..149959bce 100644 --- a/src/pipeline/pass_lsp_cross.h +++ b/src/pipeline/pass_lsp_cross.h @@ -35,6 +35,7 @@ #include "lsp/c_lsp.h" /* cbm_c_build_cross_registry / cbm_run_c_lsp_cross_with_registry */ #include "lsp/cs_lsp.h" /* cbm_cs_build_cross_registry / cbm_run_cs_lsp_cross_with_registry */ #include "lsp/ts_lsp.h" /* cbm_ts_build_cross_registry / cbm_run_ts_lsp_cross_with_registry */ +#include "lsp/java_lsp.h" /* cbm_java_build_cross_registry / cbm_run_java_lsp_cross_with_registry */ #include "lsp/rust_lsp.h" /* cbm_rust_build_cross_registry / cbm_run_rust_lsp_cross_with_registry */ #include "pipeline/pipeline_internal.h" #include @@ -125,12 +126,21 @@ typedef struct { CBMTypeRegistry *ts; /* CBM_LANG_JAVASCRIPT, TYPESCRIPT, TSX */ CBMTypeRegistry *php; /* CBM_LANG_PHP */ CBMTypeRegistry *cs; /* CBM_LANG_CSHARP */ + CBMTypeRegistry *java; /* CBM_LANG_JAVA (JVM def universe incl. Kotlin defs) */ /* CBM_LANG_RUST: intentionally absent — the shared rust registry is built * LAZILY inside cbm_parallel_resolve (first NULL-filter rust file), not eagerly. */ } CBMCrossLspRegistries; /* Return the appropriate pre-built registry for a language, or NULL * if none was built (or language has no cross-LSP entrypoint). */ +/* Per-file registry-build cost (#1669): how many defs the per-file cross-LSP + * path actually registered, and how often the module filter failed. */ +/* Count defs an overlay registered for one file (complexity-gate telemetry). */ +void cbm_pxc_count_perfile_defs(uint64_t defs); + +void cbm_pxc_filter_stats(uint64_t *defs_registered, uint64_t *build_files, uint64_t *filter_files, + uint64_t *filter_failed); + static inline CBMTypeRegistry *cbm_pxc_registry_for_lang(const CBMCrossLspRegistries *r, CBMLanguage lang) { if (!r) @@ -152,6 +162,8 @@ static inline CBMTypeRegistry *cbm_pxc_registry_for_lang(const CBMCrossLspRegist return r->php; case CBM_LANG_CSHARP: return r->cs; + case CBM_LANG_JAVA: + return r->java; default: return NULL; /* incl. CBM_LANG_RUST — its shared registry is built lazily */ } diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index f40e86ebc..c3eb2aeec 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -106,6 +106,10 @@ enum { PP_CSHARP_M_PREFIX_LEN = 2 }; * memory (the resident floor, not in-flight transients, holds the budget). */ static _Atomic long g_bp_nap_cycles = 0; static _Atomic uint64_t g_lsp_linear_fallback_rows = 0; +/* Defined here, declared in lsp_resolve.h — the uncapped tail-match scan's + * cost, surfaced at end of resolve (#1669). */ +_Atomic uint64_t g_lsp_tail_lookups = 0; +_Atomic uint64_t g_lsp_tail_candidates = 0; long cbm_pp_bp_nap_cycles(void) { return atomic_load_explicit(&g_bp_nap_cycles, memory_order_relaxed); @@ -121,6 +125,8 @@ uint64_t cbm_pp_lsp_linear_fallback_rows(void) { void cbm_pp_lsp_linear_fallback_rows_reset(void) { atomic_store_explicit(&g_lsp_linear_fallback_rows, 0, memory_order_relaxed); + atomic_store_explicit(&g_lsp_tail_lookups, 0, memory_order_relaxed); + atomic_store_explicit(&g_lsp_tail_candidates, 0, memory_order_relaxed); } /* Parse a positive MB-valued retention env knob (CBM_RETAIN_*_MB) into bytes. @@ -663,6 +669,9 @@ typedef struct { const CBMMacroTable *macro_table; /* ObjectScript $$$macros (NULL if none) */ const CBMReturnTypeTable *return_type_table; /* ObjectScript return types (NULL if none) */ + + /* Superlinearity probe — see profile.h. Ticked on each file claim below. */ + cbm_scale_probe_t scale; } extract_ctx_t; /* Cap on the number of index.file_oversized WARN lines (the full list still goes @@ -728,6 +737,7 @@ static void extract_worker(int worker_id, void *ctx_ptr) { if (sort_pos >= ec->file_count) { break; } + cbm_scale_tick(&ec->scale, sort_pos); if (atomic_load_explicit(ec->cancelled, memory_order_relaxed)) { break; } @@ -1120,7 +1130,9 @@ int cbm_parallel_extract_ex(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *file /* Sub-phase: Dispatch workers (parse + extract per file, PARALLEL) */ CBM_PROF_START(t_dispatch); cbm_parallel_for_opts_t parallel_opts = {.max_workers = worker_count, .force_pthreads = false}; + cbm_scale_begin(&ec.scale, "parallel_extract", (long)file_count); cbm_parallel_for(worker_count, extract_worker, &ec, parallel_opts); + cbm_scale_end(&ec.scale); CBM_PROF_END_N("parallel_extract", "3_dispatch_workers_parallel", t_dispatch, file_count); /* Sub-phase: Merge all local gbufs into main gbuf (SEQUENTIAL, gbuf not thread-safe) */ @@ -1432,6 +1444,11 @@ typedef struct { _Atomic uint64_t time_ns_rc_target; /* gbuf_find_by_qn for target */ _Atomic uint64_t time_ns_rc_emit; /* emit_service_edge */ _Atomic uint64_t time_ns_rc_source; /* find_source_node */ + + /* Superlinearity probe — see profile.h. This is the pass that made it + * necessary (#1669: 87% of a Java index), so it is the one that must never + * again grow superlinear without saying so. */ + cbm_scale_probe_t scale; } resolve_ctx_t; /* Minimum buffer space needed per arg JSON object */ @@ -2934,6 +2951,7 @@ static void resolve_worker(int worker_id, void *ctx_ptr) { if (file_idx >= rc->file_count) { break; } + cbm_scale_tick(&rc->scale, file_idx); if (atomic_load_explicit(rc->cancelled, memory_order_relaxed)) { break; } @@ -3212,7 +3230,9 @@ int cbm_parallel_resolve(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, /* Sub-phase: Dispatch resolve workers (per-file call/usage resolution, PARALLEL) */ CBM_PROF_START(t_resolve_dispatch); cbm_parallel_for_opts_t opts = {.max_workers = worker_count, .force_pthreads = false}; + cbm_scale_begin(&rc.scale, "parallel_resolve", (long)file_count); cbm_parallel_for(worker_count, resolve_worker, &rc, opts); + cbm_scale_end(&rc.scale); CBM_PROF_END_N("parallel_resolve", "1_dispatch_workers_parallel", t_resolve_dispatch, file_count); /* Workers joined: the shared Rust registry (if built) is no longer read. @@ -3267,6 +3287,56 @@ int cbm_parallel_resolve(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, itoa_log(atomic_load_explicit(&rc.lsp_cross_skipped_no_source, memory_order_relaxed)), "defs_total", itoa_log(def_count)); + /* Cross-LSP cost, NORMALISED (#1669). Wall time alone cannot distinguish + * "big repo" from "superlinear pass"; us_per_file can. For a pass that is + * linear in file count this number is roughly CONSTANT across repo sizes. + * When cross-file LSP rebuilds its registry from a corpus-scaled def set, + * per-file cost tracks defs_total instead — measured 35ms/file at 5.8k + * files and 209ms/file at 46k on the same tree, which is the O(n^2) that + * cost a 6x java regression and took an 11-corpus two-binary A/B to find. + * Emitted next to defs_total so one grep on two differently sized repos + * answers it. */ + int cross_files = atomic_load_explicit(&rc.lsp_cross_processed, memory_order_relaxed); + uint64_t cross_us = atomic_load_explicit(&rc.time_ns_cross_lsp, memory_order_relaxed) / 1000ULL; + if (cross_files > 0) { + char cf_buf[CBM_SZ_32]; + char nf_buf[CBM_SZ_32]; + char cu_buf[CBM_SZ_32]; + char pk_buf[CBM_SZ_32]; + snprintf(cf_buf, sizeof(cf_buf), "%llu", + (unsigned long long)(cross_us / (uint64_t)cross_files)); + snprintf(nf_buf, sizeof(nf_buf), "%d", cross_files); + snprintf(cu_buf, sizeof(cu_buf), "%llu", (unsigned long long)(cross_us / 1000ULL)); + snprintf(pk_buf, sizeof(pk_buf), "%llu", + (unsigned long long)(def_count > 0 + ? (cross_us * 1000ULL) / + ((uint64_t)cross_files * (uint64_t)def_count) + : 0ULL)); + cbm_log_info("parallel.resolve.cross_lsp_cost", "cross_lsp_ms", cu_buf, "files", nf_buf, + "us_per_file", cf_buf, "defs_total", itoa_log(def_count), + "us_per_file_per_kdef", pk_buf); + + /* What the per-file registry build actually cost. defs_per_file is the + * lever: if it tracks defs_total rather than the file's own module plus + * imports, the module filter is not containing the work and cross-file + * LSP is O(files x corpus_defs). */ + uint64_t reg_defs = 0; + uint64_t reg_files = 0; + uint64_t flt_files = 0; + uint64_t flt_failed = 0; + cbm_pxc_filter_stats(®_defs, ®_files, &flt_files, &flt_failed); + if (reg_files > 0) { + char rd_buf[CBM_SZ_32]; + char ff_buf[CBM_SZ_32]; + char fp_buf[CBM_SZ_32]; + snprintf(rd_buf, sizeof(rd_buf), "%llu", (unsigned long long)(reg_defs / reg_files)); + snprintf(ff_buf, sizeof(ff_buf), "%llu", (unsigned long long)flt_failed); + snprintf(fp_buf, sizeof(fp_buf), "%llu", (unsigned long long)flt_files); + cbm_log_info("parallel.resolve.perfile_registry", "defs_per_file", rd_buf, "defs_total", + itoa_log(def_count), "filtered_files", fp_buf, "filter_failed", ff_buf); + } + } + cbm_log_info("parallel.resolve.done", "calls", itoa_log(total_calls), "usages", itoa_log(total_usages), "semantic", itoa_log(total_semantic + go_impl), "lsp_overrides", itoa_log(total_lsp_overrides)); @@ -3344,5 +3414,27 @@ int cbm_parallel_resolve(cbm_pipeline_ctx_t *ctx, const cbm_file_info_t *files, "resolve", rsv_buf); cbm_log_info("parallel.resolve.calls_breakdown2", "field_hint", hnt_buf, "find_target", tgt_buf, "emit_edge", emt_buf); + + /* Candidate-scan cost (#1669). `per_lookup` is the diagnostic that matters: + * it is a property of the CORPUS, not of the machine, so it is comparable + * across runs and versions. If it grows with repo size, the tail-match scan + * is turning resolve superlinear — which is precisely what took an + * 11-corpus two-binary A/B to establish the first time. `fallback_rows` + * was already counted but, until now, readable only from a test. */ + uint64_t tail_lookups = atomic_load_explicit(&g_lsp_tail_lookups, memory_order_relaxed); + uint64_t tail_cands = atomic_load_explicit(&g_lsp_tail_candidates, memory_order_relaxed); + char tl_buf[CBM_SZ_32]; + char tc_buf[CBM_SZ_32]; + char tp_buf[CBM_SZ_32]; + char fb_buf[CBM_SZ_32]; + snprintf(tl_buf, sizeof(tl_buf), "%llu", (unsigned long long)tail_lookups); + snprintf(tc_buf, sizeof(tc_buf), "%llu", (unsigned long long)tail_cands); + snprintf(tp_buf, sizeof(tp_buf), "%llu", + (unsigned long long)(tail_lookups ? tail_cands / tail_lookups : 0ULL)); + snprintf(fb_buf, sizeof(fb_buf), "%llu", + (unsigned long long)atomic_load_explicit(&g_lsp_linear_fallback_rows, + memory_order_relaxed)); + cbm_log_info("parallel.resolve.scan_cost", "tail_lookups", tl_buf, "tail_candidates", tc_buf, + "per_lookup", tp_buf, "fallback_rows", fb_buf); return 0; } diff --git a/src/pipeline/pipeline.c b/src/pipeline/pipeline.c index f38f4151f..597c5e767 100644 --- a/src/pipeline/pipeline.c +++ b/src/pipeline/pipeline.c @@ -1254,17 +1254,44 @@ static int run_parallel_pipeline(cbm_pipeline_t *p, cbm_pipeline_ctx_t *ctx, * Built ONCE here; shared READ-ONLY across all files of that language * during resolve. Per-file work is then: parse + AST walk + O(1) lookups * — no registry build, no Phase 1b mutations. Languages added so far: - * Go, Python. Others (C/C++, TS/JS, PHP, C#) fall back to per-file. */ + * Go, Python, C/C++, C#, TS/JS, Java. Others (Kotlin, PHP) fall back to per-file. */ CBMArena cross_lsp_arena; cbm_arena_init(&cross_lsp_arena); CBMCrossLspRegistries cross_registries = {0}; if (all_defs) { + /* Per-builder split of lsp_cross_prepare — attributes a slow prepare to + * ONE language instead of re-diagnosing the whole pass (the cs builder + * hid ~140 s behind the pass total, #1669 follow-up). */ + struct timespec t_b; + long b_ms[6]; + cbm_clock_gettime(CLOCK_MONOTONIC, &t_b); cross_registries.go = cbm_go_build_cross_registry(&cross_lsp_arena, all_defs, def_count); + b_ms[0] = (long)elapsed_ms(t_b); + cbm_clock_gettime(CLOCK_MONOTONIC, &t_b); cross_registries.python = cbm_py_build_cross_registry(&cross_lsp_arena, all_defs, def_count); + b_ms[1] = (long)elapsed_ms(t_b); + cbm_clock_gettime(CLOCK_MONOTONIC, &t_b); cross_registries.c = cbm_c_build_cross_registry(&cross_lsp_arena, all_defs, def_count); + b_ms[2] = (long)elapsed_ms(t_b); + cbm_clock_gettime(CLOCK_MONOTONIC, &t_b); cross_registries.cs = cbm_cs_build_cross_registry(&cross_lsp_arena, all_defs, def_count); + b_ms[3] = (long)elapsed_ms(t_b); + cbm_clock_gettime(CLOCK_MONOTONIC, &t_b); cross_registries.ts = cbm_ts_build_cross_registry(&cross_lsp_arena, all_defs, def_count); + b_ms[4] = (long)elapsed_ms(t_b); + cbm_clock_gettime(CLOCK_MONOTONIC, &t_b); + cross_registries.java = + cbm_java_build_cross_registry(&cross_lsp_arena, all_defs, def_count); + b_ms[5] = (long)elapsed_ms(t_b); + char b_buf[6][CBM_SZ_16]; + const char *b_name[6] = {"go", "python", "c", "cs", "ts", "java"}; + for (int bi = 0; bi < 6; bi++) { + snprintf(b_buf[bi], sizeof(b_buf[bi]), "%ld", b_ms[bi]); + } + cbm_log_info("lsp_cross_prepare.builders", b_name[0], b_buf[0], b_name[1], b_buf[1], + b_name[2], b_buf[2], b_name[3], b_buf[3], b_name[4], b_buf[4], b_name[5], + b_buf[5]); /* Rust: NOT built here. The shared all_defs registry is built LAZILY on the * first NULL-filter rust file (the amplifier files) inside cbm_parallel_resolve * — repos whose rust files all filter to subsets never pay the build/RSS. */ diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index 3e4610677..26cfbc16a 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -1334,6 +1334,8 @@ static int run_extract_resolve(cbm_pipeline_ctx_t *ctx, cbm_file_info_t *changed cross_registries.c = cbm_c_build_cross_registry(xa, all_defs, all_def_count); cross_registries.cs = cbm_cs_build_cross_registry(xa, all_defs, all_def_count); cross_registries.ts = cbm_ts_build_cross_registry(xa, all_defs, all_def_count); + cross_registries.java = + cbm_java_build_cross_registry(xa, all_defs, all_def_count); registries_arg = &cross_registries; } } else { diff --git a/src/store/store.c b/src/store/store.c index d91346e67..4e2b12d62 100644 --- a/src/store/store.c +++ b/src/store/store.c @@ -2668,6 +2668,20 @@ static void cov_failure_fingerprint(const cbm_coverage_row_t *rows, int count, out[CBM_SHA256_HEX_LEN] = 0; } +static void cov_dir_ids_free_entry(const char *key, void *val, void *ud) { + (void)ud; + free((char *)key); + free(val); +} + +static void cov_dir_ids_free(CBMHashTable *ht) { + if (!ht) { + return; + } + cbm_ht_foreach(ht, cov_dir_ids_free_entry, NULL); + cbm_ht_free(ht); +} + static int cov_rebuild_shadow_graph(cbm_store_t *s, const char *project) { char covproj[CBM_SZ_512]; cbm_store_coverage_shadow_project(covproj, sizeof(covproj), project); @@ -2765,6 +2779,12 @@ static int cov_rebuild_shadow_graph(cbm_store_t *s, const char *project) { return CBM_STORE_ERR; } + /* Directory nodes repeat massively across failure rows (13k baseline files + * under one tests/ subtree meant ~80k redundant upsert/edge round-trips — + * 9.1 s of a 9.2 s coverage_replace on the TypeScript corpus). Dedup them + * in-memory for the rebuild: first sight of a directory creates its node + * and containment edge; every later file under it is a hash hit. */ + CBMHashTable *dir_ids = cbm_ht_create(CBM_SZ_256); for (int i = 0; i < count; i++) { const char *rel = rows[i].rel_path; if (!rel || !rel[0]) { @@ -2787,6 +2807,12 @@ static int cov_rebuild_shadow_graph(cbm_store_t *s, const char *project) { /* Truncate at this slash → pathbuf is the directory prefix; the * upsert binds copies, so restore the slash right after. */ *p = '\0'; + int64_t *cached = dir_ids ? (int64_t *)cbm_ht_get(dir_ids, pathbuf) : NULL; + if (cached) { + parent = *cached; + *p = '/'; + continue; + } const char *seg = strrchr(pathbuf, '/'); cbm_node_t folder = {.project = covproj, .label = "Folder", @@ -2795,8 +2821,9 @@ static int cov_rebuild_shadow_graph(cbm_store_t *s, const char *project) { .file_path = pathbuf, .properties_json = "{}"}; int64_t fid = cbm_store_upsert_node(s, &folder); - *p = '/'; if (fid <= 0) { + *p = '/'; + cov_dir_ids_free(dir_ids); cbm_store_free_coverage(rows, count); return CBM_STORE_ERR; } @@ -2806,9 +2833,24 @@ static int cov_rebuild_shadow_graph(cbm_store_t *s, const char *project) { .type = "CONTAINS_FOLDER", .properties_json = "{}"}; if (cbm_store_insert_edge(s, &e) <= 0) { + *p = '/'; + cov_dir_ids_free(dir_ids); cbm_store_free_coverage(rows, count); return CBM_STORE_ERR; } + if (dir_ids) { + int64_t *idv = (int64_t *)malloc(sizeof(*idv)); + if (idv) { + *idv = fid; + char *kdup = strdup(pathbuf); + if (kdup) { + cbm_ht_set(dir_ids, kdup, idv); + } else { + free(idv); + } + } + } + *p = '/'; parent = fid; } @@ -2826,6 +2868,7 @@ static int cov_rebuild_shadow_graph(cbm_store_t *s, const char *project) { .properties_json = props}; int64_t file_id = cbm_store_upsert_node(s, &file); if (file_id <= 0) { + cov_dir_ids_free(dir_ids); cbm_store_free_coverage(rows, count); return CBM_STORE_ERR; } @@ -2835,10 +2878,12 @@ static int cov_rebuild_shadow_graph(cbm_store_t *s, const char *project) { .type = "CONTAINS_FILE", .properties_json = "{}"}; if (cbm_store_insert_edge(s, &e) <= 0) { + cov_dir_ids_free(dir_ids); cbm_store_free_coverage(rows, count); return CBM_STORE_ERR; } } + cov_dir_ids_free(dir_ids); cbm_store_free_coverage(rows, count); { sqlite3_stmt *set = NULL; @@ -2862,6 +2907,15 @@ int cbm_store_coverage_replace_ex(cbm_store_t *s, const char *project, if (exec_sql(s, "BEGIN;") != CBM_STORE_OK) { return CBM_STORE_ERR; } + /* Sub-block timings (publish.timing style): coverage_replace measured 9 s + * on the TypeScript corpus and the caller-level block could not say WHY — + * delete, 13k row inserts with large error_ranges payloads, the NOT-IN + * prune, meta, and COMMIT are very different suspects. */ + struct timespec cov_t0; + struct timespec cov_t1; + long cov_ms[5] = {0, 0, 0, 0, 0}; + long long cov_detail_bytes = 0; + cbm_clock_gettime(CLOCK_MONOTONIC, &cov_t0); sqlite3_stmt *del = NULL; if (sqlite3_prepare_v2(s->db, "DELETE FROM index_coverage WHERE project = ?1;", CBM_NOT_FOUND, &del, NULL) != SQLITE_OK) { @@ -2877,6 +2931,10 @@ int cbm_store_coverage_replace_ex(cbm_store_t *s, const char *project, (void)exec_sql(s, "ROLLBACK;"); return CBM_STORE_ERR; } + cbm_clock_gettime(CLOCK_MONOTONIC, &cov_t1); + cov_ms[0] = + (cov_t1.tv_sec - cov_t0.tv_sec) * 1000 + (cov_t1.tv_nsec - cov_t0.tv_nsec) / 1000000; + cov_t0 = cov_t1; sqlite3_stmt *ins = NULL; if (sqlite3_prepare_v2( s->db, @@ -2894,6 +2952,7 @@ int cbm_store_coverage_replace_ex(cbm_store_t *s, const char *project, bind_text(ins, SKIP_ONE, project); bind_text(ins, ST_COL_2, rows[i].rel_path); bind_text(ins, ST_COL_3, rows[i].kind); + cov_detail_bytes += rows[i].detail ? (long long)strlen(rows[i].detail) : 0; bind_text(ins, CBM_SZ_4, rows[i].detail ? rows[i].detail : ""); if (sqlite3_step(ins) != SQLITE_DONE) { store_set_error_sqlite(s, "coverage insert"); @@ -2904,6 +2963,10 @@ int cbm_store_coverage_replace_ex(cbm_store_t *s, const char *project, sqlite3_reset(ins); } sqlite3_finalize(ins); + cbm_clock_gettime(CLOCK_MONOTONIC, &cov_t1); + cov_ms[1] = + (cov_t1.tv_sec - cov_t0.tv_sec) * 1000 + (cov_t1.tv_nsec - cov_t0.tv_nsec) / 1000000; + cov_t0 = cov_t1; /* Prune FAILURE rows for files no longer known to the index (deleted from * the repo): file_hashes is the authoritative live-file set after * persist. By-design not_indexed_* rows are exempt — deliberately @@ -2927,6 +2990,10 @@ int cbm_store_coverage_replace_ex(cbm_store_t *s, const char *project, (void)exec_sql(s, "ROLLBACK;"); return CBM_STORE_ERR; } + cbm_clock_gettime(CLOCK_MONOTONIC, &cov_t1); + cov_ms[2] = + (cov_t1.tv_sec - cov_t0.tv_sec) * 1000 + (cov_t1.tv_nsec - cov_t0.tv_nsec) / 1000000; + cov_t0 = cov_t1; if (meta) { char recorded_at[CBM_SZ_64]; @@ -3002,7 +3069,33 @@ int cbm_store_coverage_replace_ex(cbm_store_t *s, const char *project, (void)exec_sql(s, "ROLLBACK;"); return CBM_STORE_ERR; } - return exec_sql(s, "COMMIT;"); + cbm_clock_gettime(CLOCK_MONOTONIC, &cov_t1); + cov_ms[3] = + (cov_t1.tv_sec - cov_t0.tv_sec) * 1000 + (cov_t1.tv_nsec - cov_t0.tv_nsec) / 1000000; + cov_t0 = cov_t1; + int commit_rc = exec_sql(s, "COMMIT;"); + cbm_clock_gettime(CLOCK_MONOTONIC, &cov_t1); + cov_ms[4] = + (cov_t1.tv_sec - cov_t0.tv_sec) * 1000 + (cov_t1.tv_nsec - cov_t0.tv_nsec) / 1000000; + { + char b0[ST_BUF_64]; + char b1[ST_BUF_64]; + char b2[ST_BUF_64]; + char b3[ST_BUF_64]; + char b4[ST_BUF_64]; + char bn[ST_BUF_64]; + char bb[ST_BUF_64]; + snprintf(b0, sizeof(b0), "%ld", cov_ms[0]); + snprintf(b1, sizeof(b1), "%ld", cov_ms[1]); + snprintf(b2, sizeof(b2), "%ld", cov_ms[2]); + snprintf(b3, sizeof(b3), "%ld", cov_ms[3]); + snprintf(b4, sizeof(b4), "%ld", cov_ms[4]); + snprintf(bn, sizeof(bn), "%d", count); + snprintf(bb, sizeof(bb), "%lld", cov_detail_bytes); + cbm_log_info("publish.timing.coverage", "del", b0, "rows", b1, "prune", b2, "meta", b3, + "commit", b4, "row_count", bn, "detail_bytes", bb); + } + return commit_rc; } int cbm_store_coverage_replace(cbm_store_t *s, const char *project, const cbm_coverage_row_t *rows, diff --git a/tests/test_complexity.c b/tests/test_complexity.c new file mode 100644 index 000000000..1fe68a71e --- /dev/null +++ b/tests/test_complexity.c @@ -0,0 +1,725 @@ +/* + * test_complexity.c — Complexity guard: superlinearity detection at tiny input + * sizes, gated on DETERMINISTIC work counters — never time. + * + * Why this suite exists: the v0.9.0→v0.10.x indexing regression was a + * files×corpus_defs coupling in cross-file LSP (#1669). Finding it took an + * 11-corpus A/B across two release binaries, because nothing in CI could see + * an O(n^2) forming. This suite makes that class of bug fail a unit test. + * + * Method — replicated independent modules: + * Build a synthetic corpus of k INDEPENDENT module copies per language + * (module i never references module j). Then every extensive quantity — + * nodes, edges, and Σ per-file work — MUST grow linearly in k. Run the full + * in-process pipeline at k and 2k and assert counter RATIOS: + * + * linear pipeline ratio ≈ 2 (gate: within [lo, hi]) + * files×corpus bug ratio ≈ 4 (per-file work itself grows with k) + * + * Ratios expose the exponent at TINY sizes (dozens of files, seconds of + * runtime): a counter that counts the quadratic term directly doubles its + * growth per doubling regardless of absolute scale. No large corpora needed. + * + * Determinism doctrine (O9): a verdict must be a pure function of + * (code, input). Work counters are sums over per-file work and do not depend + * on scheduling; wall time does. Therefore ONLY counters gate. Throughput + * (nodes/s, edges/s) is measured and written to a LOCAL report under private/ + * as information for trend comparison — it never gates, and the report step is + * skipped entirely under CBM_SKIP_PERF (starved fidelity legs would record + * meaningless rates). + * + * Dynamic coverage: languages come from two providers, iterated over the full + * CBM_LANG_COUNT enum — + * 1. embedded templates below (the LSP-hybrid languages, where cross-file + * machinery — and therefore files×corpus coupling risk — lives); + * 2. auto-discovered fixture dirs tests/fixtures/complexity// + * (drop files there when adding a language; no test edit needed). + * Languages with neither provider are recorded in the report as skipped with + * the reason "no complexity template": their extractors are per-file by + * construction (grammar-only, no cross-file resolution), so the coupling this + * suite hunts cannot arise from them; the shared passes they feed (registry, + * similarity, semantic) are exercised by the template corpus. + */ +#include "test_framework.h" +#include "test_helpers.h" + +#include "../src/foundation/compat.h" +#include "../src/foundation/compat_fs.h" +#include "../src/foundation/log.h" +#include "../src/foundation/profile.h" +#include "cbm.h" +#include "discover/discover.h" +#include "pipeline/pass_lsp_cross.h" +#include "pipeline/pipeline.h" +#include "pipeline/pipeline_internal.h" +#include "store/store.h" + +#include +#include +#include +#include +#include +#include + +/* Tail-match scan counters (defined in pass_parallel.c, declared in + * lsp_resolve.h — re-declared here to avoid pulling that header's statics). */ +extern _Atomic uint64_t g_lsp_tail_lookups; +extern _Atomic uint64_t g_lsp_tail_candidates; + +/* ── Corpus scale ────────────────────────────────────────────────────── + * Small on purpose: the gate reads exponents from ratios, not magnitudes. + * K_BASE modules vs 2*K_BASE modules, CX_FILES_PER_MOD files each, per + * provider language. Total runtime target for the whole suite: seconds. */ +enum { + CX_K_BASE = 3, + CX_FILES_PER_MOD = 5, + /* Gate only when the base run produced enough work for the ratio to be + * meaningful — a near-zero denominator would make the gate noise. The + * false-guard audit rule applies: a separate assertion below proves the + * counter is genuinely nonzero so this gate can never pass vacuously. */ + CX_MIN_BASE_WORK = 40, + /* The pipeline resolves sequentially below MIN_FILES_FOR_PARALLEL (50 + * files, pipeline.c) and that path builds no shared registries — so both + * legs of every corpus pair must exceed it or the gates measure the wrong + * code path. The sequential path's own per-file registry work is bounded + * by the 50-file ceiling and is deliberately out of scope here. */ + CX_BIGPKG_FILES_PER_K = 20, +}; + +/* Linear growth bounds for a 2x input doubling. Fixed structural overhead + * (Project/root nodes) pulls node ratios slightly under 2; template boundary + * effects push edge ratios slightly around 2. A files×corpus coupling lands + * at ~4 — far outside. */ +#define CX_RATIO_LO 1.45 +#define CX_RATIO_HI 2.75 + +/* ── Language templates ──────────────────────────────────────────────── + * Each emitter writes file j of a module. Files reference file j-1 of the + * SAME module (cross-file resolution work); modules never reference each + * other (independence — the property the linearity gate rests on). */ + +static void cx_emit_java(FILE *f, int m, int j) { + fprintf(f, "package mod%d;\n\npublic class C%d {\n", m, j); + fprintf(f, " public int val%d(int x) {\n return x + %d;\n }\n", j, j); + if (j > 0) { + fprintf(f, " public int chain() {\n"); + fprintf(f, " C%d prev = new C%d();\n", j - 1, j - 1); + fprintf(f, " return prev.val%d(1) + val%d(2);\n }\n", j - 1, j); + } else { + fprintf(f, " public int chain() {\n return val0(2);\n }\n"); + } + fprintf(f, "}\n"); +} + +static void cx_emit_python(FILE *f, int m, int j) { + (void)m; + if (j > 0) { + fprintf(f, "import f%d\n\n", j - 1); + } + fprintf(f, "def val%d(x):\n return x + %d\n\n", j, j); + if (j > 0) { + fprintf(f, "def chain%d():\n return f%d.val%d(1) + val%d(2)\n", j, j - 1, j - 1, j); + } else { + fprintf(f, "def chain0():\n return val0(2)\n"); + } +} + +static void cx_emit_go(FILE *f, int m, int j) { + fprintf(f, "package mod%d\n\n", m); + fprintf(f, "func Val%d(x int) int {\n\treturn x + %d\n}\n\n", j, j); + if (j > 0) { + fprintf(f, "func Chain%d() int {\n\treturn Val%d(1) + Val%d(2)\n}\n", j, j - 1, j); + } else { + fprintf(f, "func Chain0() int {\n\treturn Val0(2)\n}\n"); + } +} + +static void cx_emit_ts(FILE *f, int m, int j) { + (void)m; + if (j > 0) { + fprintf(f, "import { val%d } from \"./f%d\";\n\n", j - 1, j - 1); + } + fprintf(f, "export function val%d(x: number): number {\n return x + %d;\n}\n\n", j, j); + if (j > 0) { + fprintf(f, "export function chain%d(): number {\n return val%d(1) + val%d(2);\n}\n", j, + j - 1, j); + } else { + fprintf(f, "export function chain0(): number {\n return val0(2);\n}\n"); + } +} + +typedef struct { + CBMLanguage lang; + const char *dirname; /* corpus subdir, doubles as the per-lang scope */ + const char *file_prefix; + const char *ext; + void (*emit)(FILE *f, int m, int j); +} CxTemplate; + +static const CxTemplate CX_TEMPLATES[] = { + {CBM_LANG_JAVA, "javasrc", "C", ".java", cx_emit_java}, + {CBM_LANG_PYTHON, "pysrc", "f", ".py", cx_emit_python}, + {CBM_LANG_GO, "gosrc", "f", ".go", cx_emit_go}, + {CBM_LANG_TYPESCRIPT, "tssrc", "f", ".ts", cx_emit_ts}, +}; +enum { CX_TEMPLATE_COUNT = sizeof(CX_TEMPLATES) / sizeof(CX_TEMPLATES[0]) }; + +/* Fixture-dir provider: tests/fixtures/complexity// — every file in + * it is copied verbatim into each module dir. Lets a new language join the + * guard by dropping fixtures, with no edit to this suite. */ +static bool cx_fixture_dir_for(CBMLanguage lang, char *out, size_t cap) { + const char *name = cbm_language_name(lang); + if (!name || !name[0]) { + return false; + } + snprintf(out, cap, "tests/fixtures/complexity/%s", name); + cbm_dir_t *d = cbm_opendir(out); + if (!d) { + return false; + } + cbm_closedir(d); + return true; +} + +static int cx_copy_file(const char *src, const char *dst) { + FILE *in = fopen(src, "rb"); + if (!in) { + return -1; + } + FILE *out = fopen(dst, "wb"); + if (!out) { + fclose(in); + return -1; + } + char buf[4096]; + size_t n; + while ((n = fread(buf, 1, sizeof(buf), in)) > 0) { + fwrite(buf, 1, n, out); + } + fclose(in); + fclose(out); + return 0; +} + +/* ── Corpus builder ──────────────────────────────────────────────────── */ + +static int cx_build_corpus(const char *root, int k_modules) { + char dir[1024]; + char path[1200]; + for (int t = 0; t < CX_TEMPLATE_COUNT; t++) { + const CxTemplate *tp = &CX_TEMPLATES[t]; + for (int m = 0; m < k_modules; m++) { + snprintf(dir, sizeof(dir), "%s/%s/mod%d", root, tp->dirname, m); + if (th_mkdir_p(dir) != 0) { + return -1; + } + for (int j = 0; j < CX_FILES_PER_MOD; j++) { + snprintf(path, sizeof(path), "%s/%s%d%s", dir, tp->file_prefix, j, tp->ext); + FILE *f = fopen(path, "w"); + if (!f) { + return -1; + } + tp->emit(f, m, j); + fclose(f); + } + } + } + /* Fixture-dir providers: replicate each discovered language dir into + * k module copies. Distinct paths make distinct modules/QNs, which is all + * the independence argument needs. */ + for (int lang = 0; lang < CBM_LANG_COUNT; lang++) { + bool templated = false; + for (int t = 0; t < CX_TEMPLATE_COUNT; t++) { + if (CX_TEMPLATES[t].lang == (CBMLanguage)lang) { + templated = true; + } + } + if (templated) { + continue; + } + char fixdir[512]; + if (!cx_fixture_dir_for((CBMLanguage)lang, fixdir, sizeof(fixdir))) { + continue; + } + cbm_dir_t *d = cbm_opendir(fixdir); + if (!d) { + continue; + } + cbm_dirent_t *entry; + while ((entry = cbm_readdir(d)) != NULL) { + if (entry->name[0] == '.') { + continue; + } + for (int m = 0; m < k_modules; m++) { + snprintf(dir, sizeof(dir), "%s/fx_%s/mod%d", root, + cbm_language_name((CBMLanguage)lang), m); + if (th_mkdir_p(dir) != 0) { + continue; + } + char src[1024]; + snprintf(src, sizeof(src), "%s/%s", fixdir, entry->name); + snprintf(path, sizeof(path), "%s/%s", dir, entry->name); + (void)cx_copy_file(src, path); + } + } + cbm_closedir(d); + } + return 0; +} + +/* Corpus shape #2 — the growing shared package. Real monorepos concentrate + * files in a few large packages (org..common, …), and the JVM filter + * branch includes every def sharing the file's namespace — so per-file work + * tracks PACKAGE size. A package whose file count scales with the corpus is + * therefore the honest reproducer for the #1669 growth pattern that fully + * independent modules cannot show: here every extensive quantity must STILL + * be linear in k, while a namespace/module-scoped per-file registry build + * goes quadratic. */ +static int cx_build_bigpkg(const char *root, int k) { + char dir[1024]; + char path[1200]; + snprintf(dir, sizeof(dir), "%s/bigsrc/bigpkg", root); + if (th_mkdir_p(dir) != 0) { + return -1; + } + int files = k * CX_BIGPKG_FILES_PER_K; + for (int j = 0; j < files; j++) { + snprintf(path, sizeof(path), "%s/B%d.java", dir, j); + FILE *f = fopen(path, "w"); + if (!f) { + return -1; + } + fprintf(f, "package bigpkg;\n\npublic class B%d {\n", j); + fprintf(f, " public int val%d(int x) {\n return x + %d;\n }\n", j, j); + if (j > 0) { + fprintf(f, " public int chain() {\n"); + fprintf(f, " B%d prev = new B%d();\n", j - 1, j - 1); + fprintf(f, " return prev.val%d(1) + val%d(2);\n }\n", j - 1, j); + } else { + fprintf(f, " public int chain() {\n return val0(2);\n }\n"); + } + fprintf(f, "}\n"); + fclose(f); + } + return 0; +} + +/* ── Metrics ─────────────────────────────────────────────────────────── */ + +/* Per-pass timing capture: a TEE log sink parses `pass.timing` lines during a + * run. Information for the report only — pass timings are wall-clock and never + * gate (O9). */ +enum { CX_MAX_PASSES = 48 }; +typedef struct { + char name[64]; + long ms; +} CxPassMs; +static CxPassMs g_cx_passes[CX_MAX_PASSES]; +static _Atomic int g_cx_pass_count = 0; + +static void cx_pass_sink(const char *line) { + if (!line || !strstr(line, "pass.timing")) { + return; + } + const char *pp = strstr(line, "pass="); + const char *ee = strstr(line, "elapsed_ms="); + if (!pp || !ee) { + return; + } + int n = atomic_fetch_add_explicit(&g_cx_pass_count, 1, memory_order_relaxed); + if (n >= CX_MAX_PASSES) { + return; + } + size_t i = 0; + pp += 5; + while (pp[i] && pp[i] != ' ' && i < sizeof(g_cx_passes[n].name) - 1) { + g_cx_passes[n].name[i] = pp[i]; + i++; + } + g_cx_passes[n].name[i] = '\0'; + g_cx_passes[n].ms = atol(ee + 11); +} + +typedef struct { + int nodes; + int edges; + int lang_nodes[CX_TEMPLATE_COUNT]; + int lang_edges[CX_TEMPLATE_COUNT]; + CxPassMs passes[CX_MAX_PASSES]; + int pass_count; + uint64_t perfile_defs; /* Σ defs registered by per-file/overlay registry builds */ + uint64_t build_files; + uint64_t filter_failed; + uint64_t tail_lookups; + uint64_t tail_candidates; + uint64_t fallback_rows; + double wall_s; +} CxMetrics; + +static double cx_now_s(void) { + struct timespec ts; + cbm_profile_now(&ts); + return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9; +} + +static int cx_run(const char *root, const char *db_path, CxMetrics *out) { + memset(out, 0, sizeof(*out)); + + uint64_t d0; + uint64_t b0; + uint64_t f0; + uint64_t x0; + cbm_pxc_filter_stats(&d0, &b0, &f0, &x0); + uint64_t tl0 = atomic_load_explicit(&g_lsp_tail_lookups, memory_order_relaxed); + uint64_t tc0 = atomic_load_explicit(&g_lsp_tail_candidates, memory_order_relaxed); + uint64_t fb0 = cbm_pp_lsp_linear_fallback_rows(); + + atomic_store_explicit(&g_cx_pass_count, 0, memory_order_relaxed); + cbm_log_set_sink_ex(cx_pass_sink, CBM_LOG_SINK_TEE); + + double t0 = cx_now_s(); + cbm_pipeline_t *p = cbm_pipeline_new(root, db_path, CBM_MODE_FULL); + if (!p) { + return -1; + } + int rc = cbm_pipeline_run(p); + out->wall_s = cx_now_s() - t0; + cbm_log_set_sink(NULL); + int captured = atomic_load_explicit(&g_cx_pass_count, memory_order_relaxed); + out->pass_count = captured < CX_MAX_PASSES ? captured : CX_MAX_PASSES; + memcpy(out->passes, g_cx_passes, (size_t)out->pass_count * sizeof(CxPassMs)); + + char project[512]; + snprintf(project, sizeof(project), "%s", cbm_pipeline_project_name(p)); + cbm_pipeline_free(p); + if (rc != 0) { + return rc; + } + + uint64_t d1; + uint64_t b1; + uint64_t f1; + uint64_t x1; + cbm_pxc_filter_stats(&d1, &b1, &f1, &x1); + out->perfile_defs = d1 - d0; + out->build_files = b1 - b0; + out->filter_failed = x1 - x0; + out->tail_lookups = atomic_load_explicit(&g_lsp_tail_lookups, memory_order_relaxed) - tl0; + out->tail_candidates = atomic_load_explicit(&g_lsp_tail_candidates, memory_order_relaxed) - tc0; + out->fallback_rows = cbm_pp_lsp_linear_fallback_rows() - fb0; + + cbm_store_t *s = cbm_store_open_path(db_path); + if (!s) { + return -1; + } + out->nodes = cbm_store_count_nodes(s, project); + out->edges = cbm_store_count_edges(s, project); + for (int t = 0; t < CX_TEMPLATE_COUNT; t++) { + out->lang_nodes[t] = cbm_store_count_nodes_scoped(s, project, CX_TEMPLATES[t].dirname); + out->lang_edges[t] = cbm_store_count_edges_scoped(s, project, CX_TEMPLATES[t].dirname); + } + cbm_store_close(s); + return 0; +} + +static double cx_ratio(double num, double den) { + return den > 0.0 ? num / den : 0.0; +} + +/* Shared across the suite so the report test reuses the measured pair instead + * of paying two more pipeline runs. */ +static CxMetrics g_cx_base; +static CxMetrics g_cx_doubled; +static bool g_cx_measured = false; +static char g_cx_root_base[512]; +static char g_cx_root_doubled[512]; + +static int cx_measure_pair(void) { + if (g_cx_measured) { + return 0; + } + const char *tmp = cbm_tmpdir(); + snprintf(g_cx_root_base, sizeof(g_cx_root_base), "%s/cbm_cx_base_XXXXXX", tmp); + snprintf(g_cx_root_doubled, sizeof(g_cx_root_doubled), "%s/cbm_cx_dbl_XXXXXX", tmp); + if (!cbm_mkdtemp(g_cx_root_base) || !cbm_mkdtemp(g_cx_root_doubled)) { + return -1; + } + if (cx_build_corpus(g_cx_root_base, CX_K_BASE) != 0 || + cx_build_corpus(g_cx_root_doubled, CX_K_BASE * 2) != 0) { + return -1; + } + char db1[600]; + char db2[600]; + snprintf(db1, sizeof(db1), "%s/cx.db", g_cx_root_base); + snprintf(db2, sizeof(db2), "%s/cx.db", g_cx_root_doubled); + if (cx_run(g_cx_root_base, db1, &g_cx_base) != 0) { + return -1; + } + if (cx_run(g_cx_root_doubled, db2, &g_cx_doubled) != 0) { + return -1; + } + g_cx_measured = true; + return 0; +} + +/* ── Tests ───────────────────────────────────────────────────────────── */ + +/* The flagship gate. Independent module copies ⇒ every extensive quantity must + * scale linearly in the copy count. A files×corpus coupling shows up as a + * ratio near 4 on the counter that sums per-file work. */ +TEST(complexity_replicated_modules_scale_linearly) { + if (cx_measure_pair() != 0) { + FAIL("failed to build/run the complexity corpus pair"); + } + const CxMetrics *a = &g_cx_base; + const CxMetrics *b = &g_cx_doubled; + + /* Sanity: the corpus is real. */ + ASSERT_GT(a->nodes, 50); + ASSERT_GT(a->edges, 20); + + double node_r = cx_ratio(b->nodes, a->nodes); + double edge_r = cx_ratio(b->edges, a->edges); + printf(" nodes %d -> %d (ratio %.2f) edges %d -> %d (ratio %.2f)\n", a->nodes, b->nodes, + node_r, a->edges, b->edges, edge_r); + ASSERT_TRUE(node_r >= CX_RATIO_LO && node_r <= CX_RATIO_HI); + ASSERT_TRUE(edge_r >= CX_RATIO_LO && edge_r <= CX_RATIO_HI); + + /* Per-language linearity, from the same run pair (scoped by subtree). If + * scoped counting returns 0 for the base run the scope semantics changed — + * surface that rather than silently skipping. */ + for (int t = 0; t < CX_TEMPLATE_COUNT; t++) { + ASSERT_GT(a->lang_nodes[t], 0); + double lr = cx_ratio(b->lang_nodes[t], a->lang_nodes[t]); + printf(" %-8s nodes %d -> %d (ratio %.2f)\n", CX_TEMPLATES[t].dirname, a->lang_nodes[t], + b->lang_nodes[t], lr); + ASSERT_TRUE(lr >= CX_RATIO_LO && lr <= CX_RATIO_HI); + } + PASS(); +} + +/* Σ per-file registry work must be linear in independent copies. This is the + * counter that measured ~4x/doubling on the v0.10.x Java path (defs_per_file + * tracked defs_total — #1669). Guarded against vacuous passes: the base run + * must have produced real registry work, so an accidental zeroing of the + * counter fails loudly instead of green-washing the gate. */ +TEST(complexity_perfile_registry_work_is_linear) { + if (cx_measure_pair() != 0) { + FAIL("failed to build/run the complexity corpus pair"); + } + const CxMetrics *a = &g_cx_base; + const CxMetrics *b = &g_cx_doubled; + + printf(" perfile_defs %llu -> %llu build_files %llu -> %llu filter_failed %llu\n", + (unsigned long long)a->perfile_defs, (unsigned long long)b->perfile_defs, + (unsigned long long)a->build_files, (unsigned long long)b->build_files, + (unsigned long long)(a->filter_failed + b->filter_failed)); + + /* Non-vacuous: the corpus includes languages that register per-file defs + * (Java at minimum). If this is ever 0 the counter wiring broke — that is + * a test defect to fix, not a pass. */ + ASSERT_GT((long long)a->perfile_defs, (long long)CX_MIN_BASE_WORK); + + double defs_r = cx_ratio((double)b->perfile_defs, (double)a->perfile_defs); + printf(" perfile_defs ratio %.2f (linear ~2, files x corpus ~4)\n", defs_r); + ASSERT_TRUE(defs_r <= CX_RATIO_HI); + + /* Recorded, deliberately NOT gated (the O10 note): tail_candidates and + * fallback_rows are legitimately superlinear under replication TODAY — + * same-short-name candidate sets grow with the copy count by design of + * the current tail scan. Both measured ~1 ns/unit (#1669: indexing the + * scan away cut 242M visits for 0% wall). Gate them only after those + * scans are bounded; until then they are trend data for the report. */ + printf(" [info] tail_lookups %llu -> %llu tail_candidates %llu -> %llu fallback_rows " + "%llu -> %llu\n", + (unsigned long long)a->tail_lookups, (unsigned long long)b->tail_lookups, + (unsigned long long)a->tail_candidates, (unsigned long long)b->tail_candidates, + (unsigned long long)a->fallback_rows, (unsigned long long)b->fallback_rows); + PASS(); +} + +static CxMetrics g_cx_big_base; +static CxMetrics g_cx_big_doubled; +static bool g_cx_big_measured = false; + +static int cx_measure_bigpkg_pair(void) { + if (g_cx_big_measured) { + return 0; + } + const char *tmp = cbm_tmpdir(); + char ra[512]; + char rb[512]; + snprintf(ra, sizeof(ra), "%s/cbm_cxbig_a_XXXXXX", tmp); + snprintf(rb, sizeof(rb), "%s/cbm_cxbig_b_XXXXXX", tmp); + if (!cbm_mkdtemp(ra) || !cbm_mkdtemp(rb)) { + return -1; + } + if (cx_build_bigpkg(ra, CX_K_BASE) != 0 || cx_build_bigpkg(rb, CX_K_BASE * 2) != 0) { + return -1; + } + char db1[600]; + char db2[600]; + snprintf(db1, sizeof(db1), "%s/cx.db", ra); + snprintf(db2, sizeof(db2), "%s/cx.db", rb); + if (cx_run(ra, db1, &g_cx_big_base) != 0) { + return -1; + } + if (cx_run(rb, db2, &g_cx_big_doubled) != 0) { + return -1; + } + g_cx_big_measured = true; + return 0; +} + +/* One growing package instead of independent modules. Nodes and edges must + * still be linear in file count — and so must Σ per-file registry work: a + * build scoped to the file's MODULE or NAMESPACE pays package-size per file + * and lands at ratio ~4 here. Only a per-FILE-scoped build stays at ~2. This + * is the exact #1669 growth pattern (per-file work tracking corpus share). */ +TEST(complexity_shared_package_growth_stays_linear) { + if (cx_measure_bigpkg_pair() != 0) { + FAIL("failed to build/run the big-package corpus pair"); + } + const CxMetrics *a = &g_cx_big_base; + const CxMetrics *b = &g_cx_big_doubled; + + double node_r = cx_ratio(b->nodes, a->nodes); + double edge_r = cx_ratio(b->edges, a->edges); + printf(" nodes %d -> %d (ratio %.2f) edges %d -> %d (ratio %.2f)\n", a->nodes, b->nodes, + node_r, a->edges, b->edges, edge_r); + ASSERT_GT(a->nodes, 30); + ASSERT_TRUE(node_r >= CX_RATIO_LO && node_r <= CX_RATIO_HI); + ASSERT_TRUE(edge_r >= CX_RATIO_LO && edge_r <= CX_RATIO_HI); + + printf(" perfile_defs %llu -> %llu (ratio %.2f; linear ~2, namespace/module-scoped ~4)\n", + (unsigned long long)a->perfile_defs, (unsigned long long)b->perfile_defs, + cx_ratio((double)b->perfile_defs, (double)a->perfile_defs)); + /* Non-vacuous floor (false-guard audit): the package produces real + * registry work; zero means the counter wiring broke. */ + ASSERT_GT((long long)a->perfile_defs, (long long)CX_MIN_BASE_WORK); + double defs_r = cx_ratio((double)b->perfile_defs, (double)a->perfile_defs); + ASSERT_TRUE(defs_r <= CX_RATIO_HI); + PASS(); +} + +/* Throughput report — information only, never a gate (CI-determinism rule: + * rates depend on the machine and scheduler, so a threshold would be a + * lottery). Written locally under private/ (gitignored); CBM_COMPLEXITY_ + * REPORT_DIR overrides. Skipped on starved legs where rates are meaningless. */ +TEST(complexity_throughput_report_written) { + const char *skip_perf = getenv("CBM_SKIP_PERF"); + if (skip_perf && skip_perf[0] == '1') { + /* Deliberate operator config, not a hidden environment failure + * (no-skips policy): rates measured under CBM_SKIP_PERF starvation + * would only mislead, so reporting is OFF and there is nothing left + * for this test to assert. */ + fprintf(stderr, " [complexity] CBM_SKIP_PERF=1: throughput report disabled by config\n"); + PASS(); + } + if (cx_measure_pair() != 0) { + FAIL("failed to build/run the complexity corpus pair"); + } + const char *dir = getenv("CBM_COMPLEXITY_REPORT_DIR"); + if (!dir || !dir[0]) { + dir = "private/benchmarks"; + } + if (th_mkdir_p(dir) != 0) { + FAIL("report dir not creatable (set CBM_COMPLEXITY_REPORT_DIR to a writable path)"); + } + char path[1024]; + snprintf(path, sizeof(path), "%s/complexity-%lld.json", dir, (long long)time(NULL)); + FILE *f = fopen(path, "w"); + ASSERT_NOT_NULL(f); + + const CxMetrics *a = &g_cx_base; + const CxMetrics *b = &g_cx_doubled; +#if defined(__APPLE__) + const char *plat = "darwin"; +#elif defined(_WIN32) + const char *plat = "windows"; +#else + const char *plat = "linux"; +#endif +#if defined(__aarch64__) || defined(_M_ARM64) + const char *arch = "arm64"; +#else + const char *arch = "x86_64"; +#endif + fprintf(f, "{\n \"schema\": 1,\n \"suite\": \"complexity\",\n"); + fprintf(f, " \"timestamp\": %lld,\n \"platform\": \"%s\",\n \"arch\": \"%s\",\n", + (long long)time(NULL), plat, arch); + fprintf(f, " \"k_base\": %d,\n \"files_per_module\": %d,\n", CX_K_BASE, CX_FILES_PER_MOD); + fprintf(f, " \"runs\": [\n"); + const CxMetrics *runs[2] = {a, b}; + for (int i = 0; i < 2; i++) { + const CxMetrics *m = runs[i]; + fprintf(f, + " {\"k\": %d, \"nodes\": %d, \"edges\": %d, \"wall_s\": %.3f,\n" + " \"nodes_per_s\": %.0f, \"edges_per_s\": %.0f,\n" + " \"perfile_defs\": %llu, \"tail_candidates\": %llu, \"fallback_rows\": " + "%llu}%s\n", + i == 0 ? CX_K_BASE : CX_K_BASE * 2, m->nodes, m->edges, m->wall_s, + m->wall_s > 0 ? (double)m->nodes / m->wall_s : 0.0, + m->wall_s > 0 ? (double)m->edges / m->wall_s : 0.0, + (unsigned long long)m->perfile_defs, (unsigned long long)m->tail_candidates, + (unsigned long long)m->fallback_rows, i == 0 ? "," : ""); + } + fprintf(f, " ],\n"); + fprintf(f, " \"languages\": [\n"); + for (int t = 0; t < CX_TEMPLATE_COUNT; t++) { + fprintf(f, + " {\"name\": \"%s\", \"nodes\": [%d, %d], \"edges\": [%d, %d], " + "\"node_ratio\": %.3f}%s\n", + CX_TEMPLATES[t].dirname, a->lang_nodes[t], b->lang_nodes[t], a->lang_edges[t], + b->lang_edges[t], cx_ratio(b->lang_nodes[t], a->lang_nodes[t]), + t + 1 < CX_TEMPLATE_COUNT ? "," : ""); + } + fprintf(f, " ],\n"); + fprintf(f, " \"passes_ms\": {\n"); + for (int i = 0; i < 2; i++) { + const CxMetrics *m = runs[i]; + fprintf(f, " \"k%d\": {", i == 0 ? CX_K_BASE : CX_K_BASE * 2); + for (int j = 0; j < m->pass_count; j++) { + fprintf(f, "%s\"%s\": %ld", j == 0 ? "" : ", ", m->passes[j].name, m->passes[j].ms); + } + fprintf(f, "}%s\n", i == 0 ? "," : ""); + } + fprintf(f, " },\n"); + fprintf(f, " \"ratios\": {\"nodes\": %.3f, \"edges\": %.3f, \"perfile_defs\": %.3f},\n", + cx_ratio(b->nodes, a->nodes), cx_ratio(b->edges, a->edges), + cx_ratio((double)b->perfile_defs, (double)a->perfile_defs)); + /* Languages without a provider, so the coverage boundary is explicit in + * the artifact rather than implied. */ + fprintf(f, " \"skipped_languages\": ["); + bool first = true; + for (int lang = 0; lang < CBM_LANG_COUNT; lang++) { + bool covered = false; + for (int t = 0; t < CX_TEMPLATE_COUNT; t++) { + if (CX_TEMPLATES[t].lang == (CBMLanguage)lang) { + covered = true; + } + } + char fixdir[512]; + if (!covered && cx_fixture_dir_for((CBMLanguage)lang, fixdir, sizeof(fixdir))) { + covered = true; + } + if (!covered) { + const char *name = cbm_language_name((CBMLanguage)lang); + if (name && name[0]) { + fprintf(f, "%s\"%s\"", first ? "" : ", ", name); + first = false; + } + } + } + fprintf(f, "]\n}\n"); + fclose(f); + printf(" report: %s\n", path); + PASS(); +} + +SUITE(complexity) { + RUN_TEST(complexity_replicated_modules_scale_linearly); + RUN_TEST(complexity_perfile_registry_work_is_linear); + RUN_TEST(complexity_shared_package_growth_stays_linear); + RUN_TEST(complexity_throughput_report_written); +} diff --git a/tests/test_diagnostics.c b/tests/test_diagnostics.c index e05677fd3..f00caf041 100644 --- a/tests/test_diagnostics.c +++ b/tests/test_diagnostics.c @@ -9,6 +9,7 @@ #include "../src/foundation/diagnostics.h" #include "../src/foundation/log.h" #include "../src/foundation/platform.h" +#include "../src/foundation/profile.h" #include #include @@ -568,7 +569,88 @@ TEST(diagnostics_outputs_are_owner_private_windows) { } #endif + +/* ── scaling probe (profile.h) ────────────────────────────────────── + * + * These pin the arithmetic and the checkpoint bookkeeping, deliberately WITHOUT + * timing anything. A test that tried to prove the detector works by generating + * a genuinely quadratic workload would be asserting on the scheduler, and would + * go flaky on a loaded CI box — so the fit is a pure function and gets fed + * synthetic points instead. */ + +TEST(scale_fit_k_recognises_linear_and_quadratic_growth) { + /* 8x the items, 8x the time => k = 1 */ + ASSERT_TRUE(cbm_scale_fit_k(1000, 1000, 8000, 8000) > 0.99); + ASSERT_TRUE(cbm_scale_fit_k(1000, 1000, 8000, 8000) < 1.01); + + /* 8x the items, 64x the time => k = 2 */ + ASSERT_TRUE(cbm_scale_fit_k(1000, 1000, 8000, 64000) > 1.99); + ASSERT_TRUE(cbm_scale_fit_k(1000, 1000, 8000, 64000) < 2.01); + + /* n log n sits between, and must NOT be mistaken for quadratic */ + double k_nlogn = cbm_scale_fit_k(1000, 1000, 8000, 8000 * 13 / 10); + ASSERT_TRUE(k_nlogn > 1.0); + ASSERT_TRUE(k_nlogn < CBM_SCALE_WARN_K); + PASS(); +} + +TEST(scale_fit_k_rejects_degenerate_input) { + ASSERT_TRUE(cbm_scale_fit_k(0, 1000, 8000, 8000) < 0.0); /* no first point */ + ASSERT_TRUE(cbm_scale_fit_k(1000, 0, 8000, 8000) < 0.0); /* zero elapsed */ + ASSERT_TRUE(cbm_scale_fit_k(1000, 1000, 1000, 8000) < 0.0); /* n did not grow */ + ASSERT_TRUE(cbm_scale_fit_k(1000, 1000, 500, 8000) < 0.0); /* n went backwards */ + ASSERT_TRUE(cbm_scale_fit_k(1000, 1000, 8000, 0) < 0.0); /* zero total */ + PASS(); +} + +TEST(scale_probe_records_checkpoints_at_eighths) { + cbm_scale_probe_t p; + cbm_scale_begin(&p, "unit", 1024); + + /* Below the first threshold (1024/8 = 128), nothing is claimed. */ + for (long i = 0; i < 128; i++) { + cbm_scale_tick(&p, i); + } + ASSERT_EQ(atomic_load(&p.next_cp), 0); + + cbm_scale_tick(&p, 128); /* 1/8 */ + ASSERT_EQ(atomic_load(&p.next_cp), 1); + cbm_scale_tick(&p, 200); /* still short of 1/4 */ + ASSERT_EQ(atomic_load(&p.next_cp), 1); + cbm_scale_tick(&p, 256); /* 1/4 */ + ASSERT_EQ(atomic_load(&p.next_cp), 2); + cbm_scale_tick(&p, 512); /* 1/2 */ + ASSERT_EQ(atomic_load(&p.next_cp), 3); + cbm_scale_tick(&p, 1024); /* all */ + ASSERT_EQ(atomic_load(&p.next_cp), 4); + + /* Saturates rather than overrunning the array. */ + cbm_scale_tick(&p, 2048); + ASSERT_EQ(atomic_load(&p.next_cp), 4); + ASSERT_EQ(p.cp_items[0], 128); + ASSERT_EQ(p.cp_items[3], 1024); + cbm_scale_end(&p); + PASS(); +} + +TEST(scale_probe_ignores_runs_too_small_to_judge) { + /* Under SCALE_MIN_ITEMS a pass is not where an O(n^2) hurts anyone, and the + * fit would be measuring noise — so the probe must stay silent. */ + cbm_scale_probe_t p; + cbm_scale_begin(&p, "tiny", 64); + for (long i = 0; i <= 64; i++) { + cbm_scale_tick(&p, i); + } + ASSERT_EQ(atomic_load(&p.next_cp), 0); + cbm_scale_end(&p); /* must not emit, must not crash */ + PASS(); +} + SUITE(diagnostics) { + RUN_TEST(scale_fit_k_recognises_linear_and_quadratic_growth); + RUN_TEST(scale_fit_k_rejects_degenerate_input); + RUN_TEST(scale_probe_records_checkpoints_at_eighths); + RUN_TEST(scale_probe_ignores_runs_too_small_to_judge); #ifndef _WIN32 RUN_TEST(diagnostics_rejects_predictable_tmp_symlinks); #endif diff --git a/tests/test_main.c b/tests/test_main.c index fe0ccfc52..ba6f26d45 100644 --- a/tests/test_main.c +++ b/tests/test_main.c @@ -680,6 +680,7 @@ extern void suite_str_util(void); extern void suite_workspace(void); extern void suite_platform(void); extern void suite_diagnostics(void); +extern void suite_complexity(void); extern void suite_subprocess(void); extern void suite_private_file_lock(void); extern void suite_lock_registry(void); @@ -926,6 +927,7 @@ int main(int argc, char **argv) { RUN_SELECTED_SUITE(workspace); RUN_SELECTED_SUITE(platform); RUN_SELECTED_SUITE(diagnostics); + RUN_SELECTED_SUITE(complexity); RUN_SELECTED_SUITE(subprocess); RUN_SELECTED_SUITE(private_file_lock); RUN_SELECTED_SUITE(lock_registry); diff --git a/tests/test_ts_lsp.c b/tests/test_ts_lsp.c index 47d3b1d83..52c3bbacb 100644 --- a/tests/test_ts_lsp.c +++ b/tests/test_ts_lsp.c @@ -4131,6 +4131,49 @@ TEST(tslsp_nested_class_resolution) { /* JavaScript relies on local `new` inference here, while TypeScript and TSX use * explicit parameter types below. All three parser modes must preserve the * full ordinary call-expression span, independent of JSX-specific behavior. */ +/* ── Expression-eval complexity guard ───────────────────────────────────────── + * + * The objectSpreadRepeatedComplexity class (TS conformance suite): a chain of + * `...(c[i] && c[j] && {...})` spreads makes the union fan out 2^(n-1) ways, + * and overload resolution re-evaluates shared subexpressions once per + * alternative. The per-node memo turns that into one eval per node. Guarded + * via the deterministic budget counter, not wall-clock: without the memo this + * source burns the entire eval budget (warned=true); with it, consumption + * stays around the node count. */ +TEST(tslsp_eval_memo_bounds_spread_bomb_work) { + /* The tsc-compiled form of that file: nested Object.assign(...) member + * calls. Resolving the stdlib method evaluates every argument expression + * once per lookup path (method dispatch + namespace fallback), and the + * first argument is itself the next nested call — without the memo the + * shared subtree re-walks once per enclosing level: 2^n evals. */ + char src[16384]; + int off = snprintf(src, sizeof(src), "\"use strict\";\nfunction f(cnd) {\n return "); + for (int i = 18; i >= 0; i--) { + off += snprintf(src + off, sizeof(src) - (size_t)off, "Object.assign("); + } + off += snprintf(src + off, sizeof(src) - (size_t)off, "{}"); + for (int i = 0; i < 19; i++) { + off += snprintf(src + off, sizeof(src) - (size_t)off, + ", (cnd[%d] && cnd[%d] && {\n prop%da: 1,\n prop%db: 1,\n" + " }))", + 2 * i + 1, 2 * i + 2, i, i); + } + snprintf(src + off, sizeof(src) - (size_t)off, ";\n}\n"); + + CBMFileResult *r = extract_js(src); + ASSERT_NOT_NULL(r); + ASSERT_GTE(r->defs.count, 1); /* f extracted; the graph itself is tiny */ + + /* Same thread ran the eval: the seam reads its final budget state. */ + ASSERT_FALSE(cbm_ts_lsp_test_budget_warned()); + long budget0 = 1000000 + (long)strlen(src) * 64; + long consumed = budget0 - cbm_ts_lsp_test_budget_remaining(); + ASSERT_LT(consumed, 100000); + + cbm_free_result(r); + PASS(); +} + TEST(tslsp_js_ordinary_same_leaf_calls_join_by_exact_site) { static const char source[] = "class Alpha { render() {} }\n" "class Beta { render() {} }\n" @@ -4641,6 +4684,9 @@ SUITE(ts_lsp) { RUN_TEST(tslsp_void_returning_method); RUN_TEST(tslsp_nested_class_resolution); + /* Expression-eval complexity guard. */ + RUN_TEST(tslsp_eval_memo_bounds_spread_bomb_work); + /* Ordinary same-leaf occurrence identity (JS / TS / TSX). */ RUN_TEST(tslsp_js_ordinary_same_leaf_calls_join_by_exact_site); RUN_TEST(tslsp_ts_ordinary_same_leaf_calls_join_by_exact_site);