/* * ts_lsp.c — TypeScript / JavaScript / JSX / TSX hybrid LSP type resolver. * * Phase 2 v1.0 resolver. Covers: * - Categories 1–4: param type inference, return type propagation, method chaining, * multi-return / destructuring. * - Categories 7–10: object literal property typing, type aliases, class field/method * dispatch (including `this`/`super`), interface dispatch. * - Category 11 (partial): explicit/implicit generic instantiation via TEMPLATE types. * - Category 12 (partial): hand-curated stdlib seeds (Promise, Array, Map, Set, etc.). * - Category 13: optional chaining `obj?.member()` propagates type through. * - Category 14: `await p` unwraps `Promise` → T. * - Category 15 (partial): `typeof` narrowing inside `if`/ternary. * - Category 16: union member access tries each branch. * - Category 17: literal types (string/number/bool literal types map to BUILTIN). * - Category 20: imports surface `module.symbol` resolution against the registry. * - Category 25: emits unresolved-call diagnostics (confidence 0). * * Cross-file (Phase 3) merges defs project-wide before per-file resolution; see * cbm_batch_ts_lsp_cross. * * Conventions (mirrors c_lsp / go_lsp): * - All allocations go through ctx->arena; no malloc / free. * - ts_emit_resolved_call requires a non-NULL enclosing_func_qn — calls outside any * function (module top-level) are not emitted. * - eval_depth caps at TS_LSP_MAX_EVAL_DEPTH to defend against pathological recursion. */ #include "ts_lsp.h" #include #include #include #include /* TEST HOOK: full per-file cross-registry builds (stdlib + every cross-file * def registered + finalized, per file). This is the sequential-mode quadratic * that ground an 81k-file TS corpus for hours — the shared-registry dispatch * must keep it at ZERO; any nonzero count means a resolve path regressed to * the per-file build. */ static _Atomic long g_ts_full_reg_builds; long cbm_ts_full_registry_builds(void) { return atomic_load(&g_ts_full_reg_builds); } void cbm_ts_full_registry_builds_reset(void) { atomic_store(&g_ts_full_reg_builds, 0); } /* Dynamic work budget for parse_ts_type_text (thread-local, reset at every * per-file/per-build entry point). The type-text parser is self-recursive * over string SLICES and re-derives lengths per call, so an adversarial or * generated type text can cost far more than its bytes suggest. Instead of * a fixed depth cap, the total work SCALES WITH THE INPUT: budget = * 1M + 64 x source_len units, each call charging 1 + slice_len/16 — i.e. * total scanned bytes are bounded at ~1024x the source size, generous * enough that no real-world file ever hits it. CBM_TS_TYPE_BUDGET (absolute * units) overrides. On exhaustion: WARN once per window, then return * UNKNOWN — the same graceful degradation the parser already uses for * constructs it does not model. -1 = unlimited (no entry point armed it). */ static _Thread_local long g_ts_type_budget = -1; static _Thread_local bool g_ts_type_budget_warned; static void ts_type_budget_reset(size_t source_len) { const char *e = getenv("CBM_TS_TYPE_BUDGET"); if (e && e[0]) { long v = atol(e); g_ts_type_budget = (v > 0) ? v : -1; } else { g_ts_type_budget = 1000000 + (long)source_len * 64; } g_ts_type_budget_warned = false; } #define TS_LSP_MAX_EVAL_DEPTH 64 #define TS_LSP_FIELD_LEN(s) ((uint32_t)(sizeof(s) - 1)) // Tree-sitter grammar entry points for the three TS dialects (compiled into the binary). extern const TSLanguage *tree_sitter_typescript(void); extern const TSLanguage *tree_sitter_tsx(void); extern const TSLanguage *tree_sitter_javascript(void); // ── Forward declarations ────────────────────────────────────────────────────── static const CBMType *parse_ts_type_text(CBMArena *arena, const char *text, const char *module_qn); static void process_node(TSLSPContext *ctx, TSNode node); static void process_function_body(TSLSPContext *ctx, TSNode body, const char *func_qn, const char *class_qn); static const CBMType *type_of_identifier(TSLSPContext *ctx, const char *name); static const CBMType *lookup_member_type(TSLSPContext *ctx, const CBMType *recv, const char *name); static const CBMRegisteredFunc *lookup_method(TSLSPContext *ctx, const CBMType *recv, const char *method_name); static char *node_text(TSLSPContext *ctx, TSNode node); // Collect a node's children into an arena array via a single O(n) cursor pass. // Returns NULL (and sets *out_n=0) for a childless node or on OOM. Use this in // place of the `for (i=0; iarena, node, ctx->source); } static bool node_kind_is(TSNode node, const char *kind) { if (ts_node_is_null(node)) return false; return strcmp(ts_node_type(node), kind) == 0; } // Find the first named child of `node` whose kind matches one of the listed kinds. static TSNode find_first_kind(TSNode node, const char *kind) { if (ts_node_is_null(node)) return node; uint32_t nc = ts_node_named_child_count(node); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_named_child(node, i); if (!ts_node_is_null(c) && strcmp(ts_node_type(c), kind) == 0) return c; } TSNode null_node = {0}; return null_node; } static void ts_emit_resolved_call(TSLSPContext *ctx, const char *callee_qn, const char *strategy, float confidence) { if (!ctx || !ctx->resolved_calls || !callee_qn || !ctx->enclosing_func_qn) return; CBMResolvedCall rc; rc.caller_qn = ctx->enclosing_func_qn; rc.callee_qn = callee_qn; rc.strategy = strategy ? strategy : "lsp_ts"; rc.confidence = confidence; rc.reason = NULL; cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc); } static void ts_emit_unresolved_call(TSLSPContext *ctx, const char *expr_text, const char *reason) { if (!ctx || !ctx->resolved_calls || !ctx->enclosing_func_qn) return; CBMResolvedCall rc; rc.caller_qn = ctx->enclosing_func_qn; rc.callee_qn = expr_text ? expr_text : "?"; rc.strategy = "lsp_unresolved"; rc.confidence = 0.0f; rc.reason = reason; cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc); } // Parse a textual TS type into a CBMType. Pragmatic v1: handle the common cases // (identifier, qualified.identifier, T[], T, Promise, T | U, T & U, predefined). // Anything more complex falls back to NAMED with the original text. // // Trims leading whitespace, leading ":" (TS type-annotation prefix produced by // `cbm_node_text` on a `type_annotation` AST node), and trailing whitespace/`;`. static const CBMType *parse_ts_type_text(CBMArena *arena, const char *text, const char *module_qn) { if (!text || !text[0]) return cbm_type_unknown(); // Strip leading whitespace, then a leading ':' (type annotation), then more whitespace. while (*text == ' ' || *text == '\t' || *text == '\n' || *text == '\r') text++; if (*text == ':') text++; while (*text == ' ' || *text == '\t' || *text == '\n' || *text == '\r') text++; size_t len = strlen(text); while (len > 0 && (text[len - 1] == ' ' || text[len - 1] == '\t' || text[len - 1] == '\n' || text[len - 1] == '\r' || text[len - 1] == ';' || text[len - 1] == ',')) len--; if (len == 0) return cbm_type_unknown(); /* Dynamic budget (see g_ts_type_budget): charge proportional to this * slice; on exhaustion degrade to UNKNOWN instead of grinding. */ if (g_ts_type_budget >= 0) { g_ts_type_budget -= 1 + (long)(len / 16); if (g_ts_type_budget < 0) { if (!g_ts_type_budget_warned) { g_ts_type_budget_warned = true; fprintf(stderr, " [tslsp] type-text parse budget exhausted (module %s); " "returning unknown\n", module_qn ? module_qn : "?"); } return cbm_type_unknown(); } } // Function type `(params) => returnType` (TS function type literal). if (text[0] == '(') { int depth = 0; size_t close = (size_t)-1; for (size_t i = 0; i < len; i++) { if (text[i] == '(') depth++; else if (text[i] == ')') { depth--; if (depth == 0) { close = i; break; } } } if (close == (size_t)-1) return cbm_type_unknown(); size_t after = close + 1; while (after < len && (text[after] == ' ' || text[after] == '\t')) after++; if (after + 2 <= len && text[after] == '=' && text[after + 1] == '>') { // Parse return type. const char *ret_text = text + after + 2; const CBMType *ret = parse_ts_type_text(arena, ret_text, module_qn); // Parse params: split by top-level commas, each "name: type" or just "type". const char *fn_param_names[16] = {0}; const CBMType *fn_param_types[16] = {0}; int pc = 0; const char *params_text = text + 1; size_t params_len = close - 1; if (params_len > 0) { size_t start = 0; int pdepth = 0; for (size_t i = 0; i <= params_len && pc < 15; i++) { char c = (i < params_len) ? params_text[i] : ','; if (c == '(' || c == '<' || c == '[') pdepth++; else if (c == ')' || c == '>' || c == ']') pdepth--; else if (c == ',' && pdepth == 0) { if (i > start) { char *part = cbm_arena_strndup(arena, params_text + start, i - start); char *colon = strchr(part, ':'); if (colon) { *colon = '\0'; const char *nm = part; while (*nm == ' ' || *nm == '\t') nm++; size_t nl = strlen(nm); while (nl > 0 && (nm[nl - 1] == ' ' || nm[nl - 1] == '?')) nl--; fn_param_names[pc] = cbm_arena_strndup(arena, nm, nl); fn_param_types[pc] = parse_ts_type_text(arena, colon + 1, module_qn); } else { fn_param_names[pc] = cbm_arena_sprintf(arena, "_%d", pc); fn_param_types[pc] = parse_ts_type_text(arena, part, module_qn); } pc++; } start = i + 1; } } } fn_param_names[pc] = NULL; fn_param_types[pc] = NULL; const CBMType *rets[2] = {ret, NULL}; return cbm_type_func(arena, fn_param_names, fn_param_types, rets); } // Parens without arrow → return UNKNOWN. return cbm_type_unknown(); } // Object type literal `{...}` and other unhandled — return UNKNOWN. if (text[0] == '{') return cbm_type_unknown(); // TS polymorphic `this` return type: emit a TYPE_PARAM("this") sentinel that // ts_eval_expr_type / lookup_method substitute back to the actual receiver at // call site. Used by fluent-builder patterns: `class C { a(): this { ... } }`. if (len == 4 && memcmp(text, "this", 4) == 0) { return cbm_type_type_param(arena, "this"); } // Conditional type `T extends U ? X : Y` — split on top-level " extends " // (followed downstream by " ? " and " : "). Detection: must have all three // keywords at depth 0, in order. { int depth = 0; size_t ext_pos = (size_t)-1; size_t q_pos = (size_t)-1; size_t colon_pos = (size_t)-1; for (size_t i = 0; i < len; i++) { char c = text[i]; if (c == '<' || c == '[' || c == '(') depth++; else if (c == '>' || c == ']' || c == ')') depth--; if (depth != 0) continue; if (ext_pos == (size_t)-1 && i > 0 && text[i] == ' ' && i + 9 <= len && memcmp(text + i, " extends ", 9) == 0) { ext_pos = i; i += 8; continue; } if (ext_pos != (size_t)-1 && q_pos == (size_t)-1 && i + 3 <= len && text[i] == ' ' && text[i + 1] == '?' && text[i + 2] == ' ') { q_pos = i; i += 2; continue; } if (q_pos != (size_t)-1 && colon_pos == (size_t)-1 && i + 3 <= len && text[i] == ' ' && text[i + 1] == ':' && text[i + 2] == ' ') { colon_pos = i; break; } } if (ext_pos != (size_t)-1 && q_pos != (size_t)-1 && colon_pos != (size_t)-1) { char *check_text = cbm_arena_strndup(arena, text, ext_pos); char *extends_text = cbm_arena_strndup(arena, text + ext_pos + 9, q_pos - (ext_pos + 9)); char *true_text = cbm_arena_strndup(arena, text + q_pos + 3, colon_pos - (q_pos + 3)); char *false_text = cbm_arena_strndup(arena, text + colon_pos + 3, len - (colon_pos + 3)); const CBMType *check = parse_ts_type_text(arena, check_text, module_qn); const CBMType *extends = parse_ts_type_text(arena, extends_text, module_qn); const CBMType *true_branch = parse_ts_type_text(arena, true_text, module_qn); const CBMType *false_branch = parse_ts_type_text(arena, false_text, module_qn); return cbm_type_conditional(arena, check, extends, true_branch, false_branch); } } // `infer X` — emit CBM_TYPE_INFER. Only meaningful inside conditional `extends` // patterns; emitted standalone here and matched later by eval_conditional. if (len > 6 && memcmp(text, "infer ", 6) == 0) { const char *rest = text + 6; while (*rest == ' ' || *rest == '\t') rest++; const char *end = rest; while (*end && *end != ' ' && *end != '\t') end++; char *name = cbm_arena_strndup(arena, rest, (size_t)(end - rest)); return cbm_type_infer(arena, name); } // `keyof T` — emit CBM_TYPE_KEYOF for downstream evaluation. if (len > 6 && memcmp(text, "keyof ", 6) == 0) { const char *rest = text + 6; while (*rest == ' ' || *rest == '\t') rest++; char *rest_term = cbm_arena_strdup(arena, rest); const CBMType *operand = parse_ts_type_text(arena, rest_term, module_qn); return cbm_type_keyof(arena, operand); } // `typeof X` in type position — emit CBM_TYPE_TYPEOF_QUERY. if (len > 7 && memcmp(text, "typeof ", 7) == 0) { const char *rest = text + 7; while (*rest == ' ' || *rest == '\t') rest++; return cbm_type_typeof_query(arena, rest); } // Tuple `[T, U]` — first char `[`, balanced. Build TUPLE. if (text[0] == '[' && len >= 2 && text[len - 1] == ']') { const char *inner_text = text + 1; size_t inner_len = len - 2; const CBMType *elems[16] = {0}; int ec = 0; size_t start = 0; int depth = 0; for (size_t i = 0; i <= inner_len && ec < 15; i++) { char c = (i < inner_len) ? inner_text[i] : ','; if (c == '<' || c == '[') depth++; else if (c == '>' || c == ']') depth--; else if (c == ',' && depth == 0) { char *part = cbm_arena_strndup(arena, inner_text + start, i - start); elems[ec++] = parse_ts_type_text(arena, part, module_qn); start = i + 1; } } elems[ec] = NULL; return cbm_type_tuple(arena, elems, ec); } // Top-level UNION ` | ` or INTERSECTION ` & ` — split at depth 0 (outside <>, []). { int depth = 0; const CBMType *members[16] = {0}; int mc = 0; size_t start = 0; bool is_union = false; bool is_inter = false; for (size_t i = 0; i < len; i++) { char c = text[i]; if (c == '<' || c == '[') depth++; else if (c == '>' || c == ']') depth--; else if (depth == 0 && (c == '|' || c == '&') && i > 0 && (text[i - 1] == ' ' || text[i - 1] == '\t')) { if (mc < 15) { char *part = cbm_arena_strndup(arena, text + start, i - start); members[mc++] = parse_ts_type_text(arena, part, module_qn); } if (c == '|') is_union = true; else is_inter = true; start = i + 1; } } if (mc > 0 && (is_union || is_inter)) { // Cap mirrors the in-loop guard: members[16] holds at most 15 // real entries + the trailing NULL sentinel. Without this guard, // a union/intersection with >=16 members overflows the array // (UBSan: index 16 out of bounds for 'const CBMType *[16]'). if (mc < 15) { char *part = cbm_arena_strndup(arena, text + start, len - start); members[mc++] = parse_ts_type_text(arena, part, module_qn); } members[mc] = NULL; return is_union ? cbm_type_union(arena, members, mc) : cbm_type_intersection(arena, members, mc); } } // Builtins / predefined type keywords. static const char *const builtins[] = { "string", "number", "boolean", "bigint", "any", "unknown", "void", "never", "null", "undefined", "object", "symbol", NULL, }; for (int i = 0; builtins[i]; i++) { size_t bl = strlen(builtins[i]); if (len == bl && memcmp(text, builtins[i], bl) == 0) { return cbm_type_builtin(arena, builtins[i]); } } // Trailing `[]` → Array. Strip and recurse. if (len >= 2 && text[len - 1] == ']' && text[len - 2] == '[') { char *inner = cbm_arena_strndup(arena, text, len - 2); const CBMType *elem = parse_ts_type_text(arena, inner, module_qn); const CBMType *args[2] = {elem, NULL}; return cbm_type_template(arena, "Array", args, 1); } // Generic instantiation `Foo<...>` — find balanced `<...>` at the end. if (len > 2 && text[len - 1] == '>') { int depth = 0; size_t open = (size_t)-1; for (size_t i = len; i-- > 0;) { char c = text[i]; if (c == '>') depth++; else if (c == '<') { depth--; if (depth == 0) { open = i; break; } } } if (open != (size_t)-1 && open > 0) { char *base = cbm_arena_strndup(arena, text, open); const char *args_text = text + open + 1; size_t args_len = (len - 1) - (open + 1); // Split args by top-level commas. const CBMType *args[16] = {0}; int arg_count = 0; size_t start = 0; int adepth = 0; for (size_t i = 0; i <= args_len && arg_count < 15; i++) { char c = (i < args_len) ? args_text[i] : ','; if (c == '<') adepth++; else if (c == '>') adepth--; else if (c == ',' && adepth == 0) { char *part = cbm_arena_strndup(arena, args_text + start, i - start); args[arg_count++] = parse_ts_type_text(arena, part, module_qn); start = i + 1; } } args[arg_count] = NULL; // Qualify project-local generic bases against the current module so // `Container` becomes TEMPLATE("test.main.Container", [number]). // Stdlib bases (Array, Promise, Map, Set, Object) stay bare so they match // the stdlib registration. const char *qualified_base = base; static const char *const stdlib_names[] = { "Array", "Promise", "Map", "Set", "WeakMap", "WeakSet", "ReadonlyArray", "Iterable", "Iterator", "Generator", "AsyncIterable", "AsyncIterator", "AsyncGenerator", "Object", "String", "Number", "Boolean", "BigInt", "Symbol", "Function", "Date", "RegExp", "Error", "Partial", "Required", "Readonly", "Pick", "Omit", "Record", "Exclude", "Extract", "NonNullable", "Parameters", "ReturnType", "Awaited", "ThisType", "InstanceType", "ConstructorParameters", "Uppercase", "Lowercase", "Capitalize", "Uncapitalize", "NodeList", "HTMLCollection", NULL, }; bool is_stdlib = false; for (int sl = 0; stdlib_names[sl]; sl++) { if (strcmp(base, stdlib_names[sl]) == 0) { is_stdlib = true; break; } } if (!is_stdlib && module_qn && strchr(base, '.') == NULL && base[0] >= 'A' && base[0] <= 'Z') { qualified_base = cbm_arena_sprintf(arena, "%s.%s", module_qn, base); } return cbm_type_template(arena, qualified_base, args, arg_count); } } // Qualified identifier (a.b.c) → NAMED with whatever text we have. Caller-side lookup // will check both the QN as-given and module-qualified variants. if (module_qn && strchr(text, '.') == NULL) { // Stdlib names stay bare so they match the stdlib registration QNs. static const char *const bare_stdlib_names[] = { "Array", "Promise", "Map", "Set", "WeakMap", "WeakSet", "ReadonlyArray", "Iterable", "Iterator", "Generator", "AsyncIterable", "AsyncIterator", "AsyncGenerator", "Object", "String", "Number", "Boolean", "BigInt", "Symbol", "Function", "Date", "RegExp", "Error", "Math", "JSON", "console", // DOM essentials "Element", "HTMLElement", "Document", "Window", "Node", "EventTarget", "Event", "Response", "Request", "Headers", "URL", "URLSearchParams", "FormData", "Blob", "File", "NodeList", "HTMLCollection", "JSX", NULL, }; char *terminated = cbm_arena_strndup(arena, text, len); if (!terminated) return cbm_type_unknown(); for (int i = 0; bare_stdlib_names[i]; i++) { if (strcmp(terminated, bare_stdlib_names[i]) == 0) { return cbm_type_named(arena, terminated); } } // Bare uppercase identifier — qualify against the current module if it looks // like a TypeIdent. Lower-case names are likely params/values, fall back. if ((terminated[0] >= 'A' && terminated[0] <= 'Z')) { const char *qn = cbm_arena_sprintf(arena, "%s.%s", module_qn, terminated); return cbm_type_named(arena, qn); } return cbm_type_named(arena, terminated); } return cbm_type_named(arena, cbm_arena_strndup(arena, text, len)); } // Return param-type CBMType array from a CBMDefinition's param_types text array. static const CBMType **parse_param_types_array(CBMArena *arena, const char **texts, const char *module_qn) { if (!texts) return NULL; int count = 0; while (texts[count]) count++; if (count == 0) return NULL; const CBMType **arr = (const CBMType **)cbm_arena_alloc(arena, (size_t)(count + 1) * sizeof(const CBMType *)); if (!arr) return NULL; for (int i = 0; i < count; i++) { arr[i] = parse_ts_type_text(arena, texts[i], module_qn); } arr[count] = NULL; return arr; } // ── Partial structural subtyping (relater) ──────────────────────────────────── // // Answers `is A assignable to B?` for the subset of TS types we support. Modeled // loosely on typescript-go's `internal/checker/relater.go` but limited to the cases // that matter for call-edge resolution. NOT a full type-checker — many edge cases // return false rather than recursing into undecidable territory. // // Used by: // - Conditional type evaluation (`T extends U ? X : Y`) // - Generic constraint checks (``) // - Future overload-by-types refinement // // Rules implemented: // - any/unknown/never on either side: liberal acceptance // - NAMED == NAMED: equal QN OR walk the source's `embedded_types` (extends chain) // - BUILTIN: identical name; LITERAL("string", X) ≤ BUILTIN("string"), etc. // - TUPLE: same arity, element-wise // - UNION source: every member assignable to target // - UNION target: source assignable to any member // - INTERSECTION source: any member assignable to target // - INTERSECTION target: source assignable to all members // - FUNC: param contravariance, return covariance, parameter-count tolerance // - TEMPLATE: same name + each arg assignable // - TYPE_PARAM: same name → true; otherwise check constraint when known // - ALIAS: resolve and recurse // - any UNKNOWN type: false // // Cycle/depth guard: max 64 nested calls. Pair-cache keyed on pointer identity // (sufficient given arena-stable types within a single resolver pass). #define TS_RELATER_MAX_DEPTH 64 #define TS_RELATER_CACHE_SIZE 256 typedef struct { const CBMType *a; const CBMType *b; int8_t result; // -1 = unknown, 0 = false, 1 = true } TSRelaterCacheSlot; static int ts_is_assignable_inner(TSLSPContext *ctx, const CBMType *a, const CBMType *b, int depth, TSRelaterCacheSlot *cache); static const CBMType *ts_relater_unwrap(const CBMType *t) { if (!t) return t; if (t->kind == CBM_TYPE_ALIAS) return cbm_type_resolve_alias(t); return t; } static bool ts_builtin_compatible(const char *a, const char *b) { if (!a || !b) return false; if (strcmp(a, b) == 0) return true; // `any` and `unknown` accept anything (handled by caller before this). return false; } static bool ts_literal_promotes_to_builtin(const CBMType *lit, const CBMType *bi) { if (!lit || lit->kind != CBM_TYPE_TS_LITERAL || !lit->data.literal_ts.tag) return false; if (!bi || bi->kind != CBM_TYPE_BUILTIN || !bi->data.builtin.name) return false; return strcmp(lit->data.literal_ts.tag, bi->data.builtin.name) == 0; } static bool ts_class_extends(TSLSPContext *ctx, const char *sub_qn, const char *super_qn, int depth) { if (!ctx || !sub_qn || !super_qn) return false; if (depth > TS_RELATER_MAX_DEPTH) return false; if (strcmp(sub_qn, super_qn) == 0) return true; const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, sub_qn); if (!rt || !rt->embedded_types) return false; for (int i = 0; rt->embedded_types[i]; i++) { if (ts_class_extends(ctx, rt->embedded_types[i], super_qn, depth + 1)) return true; } return false; } static int ts_is_assignable_inner(TSLSPContext *ctx, const CBMType *a, const CBMType *b, int depth, TSRelaterCacheSlot *cache) { if (depth > TS_RELATER_MAX_DEPTH) return 0; if (!a || !b) return 0; // Cache probe (pointer identity is sufficient — arena-stable within a resolver pass). int slot = (int)((((uintptr_t)a >> 4) ^ ((uintptr_t)b >> 4)) & (TS_RELATER_CACHE_SIZE - 1)); if (cache[slot].a == a && cache[slot].b == b && cache[slot].result >= 0) { return cache[slot].result; } a = ts_relater_unwrap(a); b = ts_relater_unwrap(b); if (!a || !b) return 0; // any/unknown/never short-circuits. if (b->kind == CBM_TYPE_BUILTIN && b->data.builtin.name) { if (strcmp(b->data.builtin.name, "any") == 0 || strcmp(b->data.builtin.name, "unknown") == 0) return 1; } if (a->kind == CBM_TYPE_BUILTIN && a->data.builtin.name) { if (strcmp(a->data.builtin.name, "any") == 0) return 1; if (strcmp(a->data.builtin.name, "never") == 0) return 1; } int result = 0; // Source unions: every member must be assignable to target. if (a->kind == CBM_TYPE_UNION && a->data.union_type.members) { result = 1; for (int i = 0; i < a->data.union_type.count; i++) { if (!ts_is_assignable_inner(ctx, a->data.union_type.members[i], b, depth + 1, cache)) { result = 0; break; } } goto done; } // Target unions: source assignable to any member. if (b->kind == CBM_TYPE_UNION && b->data.union_type.members) { for (int i = 0; i < b->data.union_type.count; i++) { if (ts_is_assignable_inner(ctx, a, b->data.union_type.members[i], depth + 1, cache)) { result = 1; break; } } goto done; } // Source intersection: any member assignable to target. if (a->kind == CBM_TYPE_INTERSECTION && a->data.union_type.members) { for (int i = 0; i < a->data.union_type.count; i++) { if (ts_is_assignable_inner(ctx, a->data.union_type.members[i], b, depth + 1, cache)) { result = 1; break; } } goto done; } // Target intersection: source assignable to all members. if (b->kind == CBM_TYPE_INTERSECTION && b->data.union_type.members) { result = 1; for (int i = 0; i < b->data.union_type.count; i++) { if (!ts_is_assignable_inner(ctx, a, b->data.union_type.members[i], depth + 1, cache)) { result = 0; break; } } goto done; } if (a->kind != b->kind) { // Cross-kind rules: LITERAL → BUILTIN, NAMED-class → NAMED-class via extends. if (a->kind == CBM_TYPE_TS_LITERAL && b->kind == CBM_TYPE_BUILTIN) { result = ts_literal_promotes_to_builtin(a, b) ? 1 : 0; goto done; } // TYPE_PARAM source: check constraint if known. if (a->kind == CBM_TYPE_TYPE_PARAM) { // Without registered constraint info available here, treat as "unknown" (fail). result = 0; goto done; } result = 0; goto done; } // Same-kind cases. switch (a->kind) { case CBM_TYPE_NAMED: { const char *aqn = a->data.named.qualified_name; const char *bqn = b->data.named.qualified_name; if (!aqn || !bqn) { result = 0; break; } if (strcmp(aqn, bqn) == 0) { result = 1; break; } // class hierarchy: A extends B? result = ts_class_extends(ctx, aqn, bqn, 0) ? 1 : 0; break; } case CBM_TYPE_BUILTIN: result = ts_builtin_compatible(a->data.builtin.name, b->data.builtin.name) ? 1 : 0; break; case CBM_TYPE_TS_LITERAL: if (a->data.literal_ts.tag && b->data.literal_ts.tag && strcmp(a->data.literal_ts.tag, b->data.literal_ts.tag) == 0 && a->data.literal_ts.value && b->data.literal_ts.value && strcmp(a->data.literal_ts.value, b->data.literal_ts.value) == 0) { result = 1; } else { result = 0; } break; case CBM_TYPE_TUPLE: { int ac = a->data.tuple.count; int bc = b->data.tuple.count; if (ac != bc) { result = 0; break; } result = 1; for (int i = 0; i < ac; i++) { if (!ts_is_assignable_inner(ctx, a->data.tuple.elems[i], b->data.tuple.elems[i], depth + 1, cache)) { result = 0; break; } } break; } case CBM_TYPE_TEMPLATE: { const char *an = a->data.template_type.template_name; const char *bn = b->data.template_type.template_name; if (!an || !bn || strcmp(an, bn) != 0) { result = 0; break; } int ac = a->data.template_type.arg_count; int bcount = b->data.template_type.arg_count; if (ac != bcount) { result = 0; break; } result = 1; for (int i = 0; i < ac; i++) { if (!ts_is_assignable_inner(ctx, a->data.template_type.template_args[i], b->data.template_type.template_args[i], depth + 1, cache)) { result = 0; break; } } break; } case CBM_TYPE_FUNC: { // Return covariance. const CBMType *aret = (a->data.func.return_types && a->data.func.return_types[0]) ? a->data.func.return_types[0] : NULL; const CBMType *bret = (b->data.func.return_types && b->data.func.return_types[0]) ? b->data.func.return_types[0] : NULL; if (aret && bret && !ts_is_assignable_inner(ctx, aret, bret, depth + 1, cache)) { result = 0; break; } // Param contravariance + arity tolerance. int ap = 0, bp = 0; if (a->data.func.param_types) while (a->data.func.param_types[ap]) ap++; if (b->data.func.param_types) while (b->data.func.param_types[bp]) bp++; if (ap > bp) { result = 0; break; } // source needs more params than target supplies result = 1; for (int i = 0; i < ap; i++) { if (!ts_is_assignable_inner(ctx, b->data.func.param_types[i], a->data.func.param_types[i], depth + 1, cache)) { result = 0; break; } } break; } case CBM_TYPE_TYPE_PARAM: { const char *an = a->data.type_param.name; const char *bn = b->data.type_param.name; result = (an && bn && strcmp(an, bn) == 0) ? 1 : 0; break; } default: result = 0; break; } done: cache[slot].a = a; cache[slot].b = b; cache[slot].result = (int8_t)result; return result; } // Public entry point. Allocates a small per-call cache on the stack. static bool ts_is_assignable(TSLSPContext *ctx, const CBMType *a, const CBMType *b) { if (!ctx || !a || !b) return false; TSRelaterCacheSlot cache[TS_RELATER_CACHE_SIZE]; for (int i = 0; i < TS_RELATER_CACHE_SIZE; i++) { cache[i].a = NULL; cache[i].b = NULL; cache[i].result = -1; } return ts_is_assignable_inner(ctx, a, b, 0, cache) ? true : false; } // ── Type evaluation ─────────────────────────────────────────────────────────── // `infer X` constraint solver — pattern-matches `source` against `pattern` (which // may contain CBM_TYPE_INFER nodes) and records bindings into a small fixed-size // table. Returns true on full match, false otherwise. Bindings table is OWNED by // caller (typically stack-allocated). typedef struct { const char *name; const CBMType *binding; } TSInferBinding; #define TS_INFER_MAX 8 static bool match_with_infer(TSLSPContext *ctx, const CBMType *source, const CBMType *pattern, TSInferBinding *binds, int *bind_count, int depth) { if (depth > 16 || !source || !pattern) return false; // Pattern is `infer X`: bind X to source. if (pattern->kind == CBM_TYPE_INFER) { if (*bind_count >= TS_INFER_MAX) return false; const char *nm = pattern->data.infer.name; if (!nm) return false; // Re-bind same name → require identical existing binding (else fail). for (int i = 0; i < *bind_count; i++) { if (binds[i].name && strcmp(binds[i].name, nm) == 0) { return ts_is_assignable(ctx, source, binds[i].binding) && ts_is_assignable(ctx, binds[i].binding, source); } } binds[*bind_count].name = nm; binds[*bind_count].binding = source; (*bind_count)++; return true; } // Same kind required for structural recursion. if (source->kind != pattern->kind) { // Non-INFER pattern with different kind: fall back to assignability. return ts_is_assignable(ctx, source, pattern); } switch (pattern->kind) { case CBM_TYPE_TEMPLATE: { const char *sn = source->data.template_type.template_name; const char *pn = pattern->data.template_type.template_name; if (!sn || !pn || strcmp(sn, pn) != 0) return false; int sc = source->data.template_type.arg_count; int pc = pattern->data.template_type.arg_count; if (sc != pc) return false; for (int i = 0; i < pc; i++) { if (!match_with_infer(ctx, source->data.template_type.template_args[i], pattern->data.template_type.template_args[i], binds, bind_count, depth + 1)) return false; } return true; } case CBM_TYPE_TUPLE: { int sc = source->data.tuple.count; int pc = pattern->data.tuple.count; if (sc != pc) return false; for (int i = 0; i < pc; i++) { if (!match_with_infer(ctx, source->data.tuple.elems[i], pattern->data.tuple.elems[i], binds, bind_count, depth + 1)) return false; } return true; } case CBM_TYPE_FUNC: { const CBMType *sret = source->data.func.return_types ? source->data.func.return_types[0] : NULL; const CBMType *pret = pattern->data.func.return_types ? pattern->data.func.return_types[0] : NULL; if (sret && pret && !match_with_infer(ctx, sret, pret, binds, bind_count, depth + 1)) return false; // Param-by-param match (positionally). int sp = 0, pp = 0; if (source->data.func.param_types) while (source->data.func.param_types[sp]) sp++; if (pattern->data.func.param_types) while (pattern->data.func.param_types[pp]) pp++; if (sp != pp) return sp >= pp; // tolerate source having more params than pattern for (int i = 0; i < pp; i++) { if (!match_with_infer(ctx, source->data.func.param_types[i], pattern->data.func.param_types[i], binds, bind_count, depth + 1)) return false; } return true; } default: return ts_is_assignable(ctx, source, pattern); } } // Substitute `infer X` bindings into a type tree (recursive). static const CBMType *subst_infer_bindings(TSLSPContext *ctx, const CBMType *t, const TSInferBinding *binds, int bind_count, int depth) { if (!t || depth > 16 || bind_count == 0) return t; // TYPE_PARAM matching the bound name → replace with binding's type. if (t->kind == CBM_TYPE_TYPE_PARAM && t->data.type_param.name) { for (int i = 0; i < bind_count; i++) { if (binds[i].name && strcmp(binds[i].name, t->data.type_param.name) == 0) { return binds[i].binding; } } return t; } // NAMED matching the bound name (the parser may module-qualify single-letter // names like `U` to `module.U` since it doesn't know about active infer binds). // Match on the bare short name. if (t->kind == CBM_TYPE_NAMED && t->data.named.qualified_name) { const char *qn = t->data.named.qualified_name; const char *dot = strrchr(qn, '.'); const char *bare = dot ? dot + 1 : qn; for (int i = 0; i < bind_count; i++) { if (binds[i].name && strcmp(binds[i].name, bare) == 0) { return binds[i].binding; } } return t; } if (t->kind == CBM_TYPE_TEMPLATE && t->data.template_type.template_args && t->data.template_type.arg_count > 0) { int ac = t->data.template_type.arg_count; const CBMType **new_args = (const CBMType **)cbm_arena_alloc( ctx->arena, (size_t)(ac + 1) * sizeof(const CBMType *)); if (!new_args) return t; for (int i = 0; i < ac; i++) { new_args[i] = subst_infer_bindings(ctx, t->data.template_type.template_args[i], binds, bind_count, depth + 1); } new_args[ac] = NULL; return cbm_type_template(ctx->arena, t->data.template_type.template_name, new_args, ac); } if (t->kind == CBM_TYPE_TUPLE && t->data.tuple.count > 0) { int n = t->data.tuple.count; const CBMType **new_elems = (const CBMType **)cbm_arena_alloc( ctx->arena, (size_t)(n + 1) * sizeof(const CBMType *)); if (!new_elems) return t; for (int i = 0; i < n; i++) { new_elems[i] = subst_infer_bindings(ctx, t->data.tuple.elems[i], binds, bind_count, depth + 1); } new_elems[n] = NULL; return cbm_type_tuple(ctx->arena, new_elems, n); } return t; } // Walk a type tree to detect any CBM_TYPE_INFER nodes. static bool contains_infer(const CBMType *t, int depth) { if (!t || depth > 32) return false; if (t->kind == CBM_TYPE_INFER) return true; if (t->kind == CBM_TYPE_TEMPLATE && t->data.template_type.template_args) { for (int i = 0; i < t->data.template_type.arg_count; i++) { if (contains_infer(t->data.template_type.template_args[i], depth + 1)) return true; } } if (t->kind == CBM_TYPE_TUPLE && t->data.tuple.elems) { for (int i = 0; i < t->data.tuple.count; i++) { if (contains_infer(t->data.tuple.elems[i], depth + 1)) return true; } } if (t->kind == CBM_TYPE_FUNC) { if (t->data.func.return_types) { for (int i = 0; t->data.func.return_types[i]; i++) { if (contains_infer(t->data.func.return_types[i], depth + 1)) return true; } } if (t->data.func.param_types) { for (int i = 0; t->data.func.param_types[i]; i++) { if (contains_infer(t->data.func.param_types[i], depth + 1)) return true; } } } return false; } // Evaluate a CBM_TYPE_CONDITIONAL `T extends U ? X : Y`. With distribution: when T is // a UNION, evaluate per member and union the results. Uses the partial relater for // the extends check. static const CBMType *eval_conditional(TSLSPContext *ctx, const CBMType *t, int depth) { if (!t || t->kind != CBM_TYPE_CONDITIONAL || depth > 16) return t; const CBMType *check = t->data.conditional.check; const CBMType *extends = t->data.conditional.extends; const CBMType *tb = t->data.conditional.true_branch; const CBMType *fb = t->data.conditional.false_branch; if (!check || !extends) return t; // Distribution: when check is a UNION, evaluate per member and union results. if (check->kind == CBM_TYPE_UNION && check->data.union_type.members && check->data.union_type.count > 0) { const CBMType **results = (const CBMType **)cbm_arena_alloc( ctx->arena, (size_t)(check->data.union_type.count + 1) * sizeof(const CBMType *)); if (!results) return t; int n = 0; for (int i = 0; i < check->data.union_type.count; i++) { const CBMType *member = check->data.union_type.members[i]; const CBMType *sub = cbm_type_conditional(ctx->arena, member, extends, tb, fb); results[n++] = eval_conditional(ctx, sub, depth + 1); } results[n] = NULL; if (n == 1) return results[0]; return cbm_type_union(ctx->arena, results, n); } // `infer X` in extends pattern: pattern-match check against extends, capturing // bindings, then substitute them in the true_branch. if (contains_infer(extends, 0)) { TSInferBinding binds[TS_INFER_MAX] = {{0}}; int bind_count = 0; if (match_with_infer(ctx, check, extends, binds, &bind_count, 0)) { return subst_infer_bindings(ctx, tb, binds, bind_count, 0); } return fb; } // Non-distributive: single resolution. bool yes = ts_is_assignable(ctx, check, extends); return yes ? tb : fb; } // Resolve a CBMType against the registry's alias chain when it's a NAMED type that's a // pure alias (`type Foo = Bar`). Also evaluates inline conditional types. static const CBMType *simplify_type(TSLSPContext *ctx, const CBMType *t) { if (!t || !ctx || !ctx->registry) return t; if (t->kind == CBM_TYPE_CONDITIONAL) { const CBMType *evaluated = eval_conditional(ctx, t, 0); if (evaluated && evaluated != t) return simplify_type(ctx, evaluated); return t; } if (t->kind == CBM_TYPE_ALIAS) return cbm_type_resolve_alias(t); if (t->kind != CBM_TYPE_NAMED) return t; for (int i = 0; i < 16; i++) { if (!t || t->kind != CBM_TYPE_NAMED) return t; const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, t->data.named.qualified_name); if (!rt || !rt->alias_of) return t; t = cbm_type_named(ctx->arena, rt->alias_of); } return t; } static const CBMType *type_of_identifier(TSLSPContext *ctx, const char *name) { if (!ctx || !name) return cbm_type_unknown(); // Scope lookup (params, locals). const CBMType *t = cbm_scope_lookup(ctx->current_scope, name); if (t && !cbm_type_is_unknown(t)) return t; // Imports: bare identifier matching an import binding. Compute the full symbol QN // and look it up in the registry. If it's a registered func, return its signature; // if it's a registered type, return NAMED. Falls back to NAMED(module_qn) for // namespace-style imports where no specific symbol matches. for (int i = 0; i < ctx->import_count; i++) { const char *local = ctx->import_local_names ? ctx->import_local_names[i] : NULL; const char *mqn = ctx->import_module_qns ? ctx->import_module_qns[i] : NULL; if (!local || !mqn || strcmp(local, name) != 0) continue; // Construct the full symbol QN: if mqn already ends in ".name" use it; else // append ".name". This matches `resolve_type_with_imports`' convention. size_t mqn_len = strlen(mqn); size_t name_len = strlen(name); const char *full_qn; if (mqn_len > name_len + 1 && mqn[mqn_len - name_len - 1] == '.' && strcmp(mqn + mqn_len - name_len, name) == 0) { full_qn = mqn; } else { full_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", mqn, name); } // Try the symbol lookup precedence: registered func, then registered type. const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, full_qn); if (f && f->signature) return f->signature; const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, full_qn); if (rt) return cbm_type_named(ctx->arena, full_qn); // No specific match — fall back to NAMED(module_qn), which is correct for // namespace-style imports (`import * as foo from './foo'`). return cbm_type_named(ctx->arena, mqn); } // Module-local function or class. if (ctx->module_qn) { const char *candidate = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, name); const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, candidate); if (rt) return cbm_type_named(ctx->arena, candidate); const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, candidate); if (f && f->signature) return f->signature; } // Global / stdlib bare-name lookup (Promise, Array, Map, Set, console, JSON, Object, ...). // The TS stdlib registers these with QN equal to the bare name. { const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, name); if (rt) return cbm_type_named(ctx->arena, name); const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, name); if (f && f->signature) return f->signature; } return cbm_type_unknown(); } // Map BUILTIN type names to their wrapper class for method dispatch. // `"x".split(...)` works because TS treats string primitives as having String prototype. static const char *builtin_wrapper_class(const char *builtin_name) { if (!builtin_name) return NULL; if (strcmp(builtin_name, "string") == 0) return "String"; if (strcmp(builtin_name, "number") == 0) return "Number"; if (strcmp(builtin_name, "boolean") == 0) return "Boolean"; if (strcmp(builtin_name, "bigint") == 0) return "BigInt"; if (strcmp(builtin_name, "symbol") == 0) return "Symbol"; return NULL; } // Look up a property `name` on a receiver type. Returns the property type or UNKNOWN. static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType *recv, const char *name); #define TS_LSP_MAX_MEMBER_DEPTH 64 /* Depth-guarded entry: member lookup recurses through wrapper classes, union * members and registered-type expansion; cyclic type graphs in real-world TS * (microsoft/TypeScript reallyLargeFile.ts) recursed without bound — SIGBUS * stack overflow under endless lookup_member_type frames. Past the cap the * member resolves as unknown — graceful degradation, not a crash. */ static const CBMType *lookup_member_type(TSLSPContext *ctx, const CBMType *recv, const char *name) { if (!ctx || ctx->member_depth >= TS_LSP_MAX_MEMBER_DEPTH) return cbm_type_unknown(); ctx->member_depth++; const CBMType *r = lookup_member_type_inner(ctx, recv, name); ctx->member_depth--; return r; } static const CBMType *lookup_member_type_inner(TSLSPContext *ctx, const CBMType *recv, const char *name) { if (!ctx || !recv || !name) return cbm_type_unknown(); const CBMType *base = simplify_type(ctx, recv); if (!base) return cbm_type_unknown(); base = unwrap_passthrough_template(base); if (!base) return cbm_type_unknown(); // BUILTIN primitives delegate to their wrapper class (e.g. string → String). if (base->kind == CBM_TYPE_BUILTIN) { const char *wrap = builtin_wrapper_class(base->data.builtin.name); if (wrap) { const CBMType *wrapped = cbm_type_named(ctx->arena, wrap); return lookup_member_type(ctx, wrapped, name); } return cbm_type_unknown(); } if (base->kind == CBM_TYPE_TEMPLATE) { // Look up on the template name (e.g., "Array", "Promise"); generic params are // applied via cbm_type_substitute when we know the registered type's params. const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, base->data.template_type.template_name); if (rt) { // Field? if (rt->field_names && rt->field_types) { for (int i = 0; rt->field_names[i]; i++) { if (strcmp(rt->field_names[i], name) == 0) { return cbm_type_substitute(ctx->arena, rt->field_types[i], rt->type_param_names, base->data.template_type.template_args); } } } // Method? if (rt->method_qns && rt->method_names) { for (int i = 0; rt->method_names[i]; i++) { if (strcmp(rt->method_names[i], name) == 0) { const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, rt->method_qns[i]); if (f && f->signature) { return cbm_type_substitute(ctx->arena, f->signature, rt->type_param_names, base->data.template_type.template_args); } } } } } return cbm_type_unknown(); } if (base->kind == CBM_TYPE_OBJECT_LIT) { if (base->data.object_lit.prop_names && base->data.object_lit.prop_types) { for (int i = 0; base->data.object_lit.prop_names[i]; i++) { if (strcmp(base->data.object_lit.prop_names[i], name) == 0) { return base->data.object_lit.prop_types[i]; } } } return cbm_type_unknown(); } if (base->kind == CBM_TYPE_UNION) { // Try each branch; if any has the member, return that. (Pragmatic v1 — returns // first hit instead of building a union of results.) if (base->data.union_type.members) { for (int i = 0; i < base->data.union_type.count; i++) { const CBMType *m = lookup_member_type(ctx, base->data.union_type.members[i], name); if (!cbm_type_is_unknown(m)) return m; } } return cbm_type_unknown(); } if (base->kind == CBM_TYPE_INTERSECTION) { if (base->data.union_type.members) { for (int i = 0; i < base->data.union_type.count; i++) { const CBMType *m = lookup_member_type(ctx, base->data.union_type.members[i], name); if (!cbm_type_is_unknown(m)) return m; } } return cbm_type_unknown(); } if (base->kind != CBM_TYPE_NAMED) return cbm_type_unknown(); const char *recv_qn = base->data.named.qualified_name; const CBMRegisteredType *rt = cbm_registry_resolve_alias(ctx->registry, recv_qn); if (!rt) { rt = cbm_registry_lookup_type(ctx->registry, recv_qn); if (!rt) return cbm_type_unknown(); } // Direct field lookup. if (rt->field_names && rt->field_types) { for (int i = 0; rt->field_names[i]; i++) { if (strcmp(rt->field_names[i], name) == 0) return rt->field_types[i]; } } // Method lookup → return the method's FUNC type. if (rt->method_qns && rt->method_names) { for (int i = 0; rt->method_names[i]; i++) { if (strcmp(rt->method_names[i], name) == 0) { const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, rt->method_qns[i]); if (f && f->signature) return f->signature; return cbm_type_unknown(); } } } // Walk extends/implements. if (rt->embedded_types) { for (int i = 0; rt->embedded_types[i]; i++) { const CBMType *parent = cbm_type_named(ctx->arena, rt->embedded_types[i]); const CBMType *m = lookup_member_type(ctx, parent, name); if (!cbm_type_is_unknown(m)) return m; } } return cbm_type_unknown(); } // TS utility types like Partial, Readonly, Required are essentially // transformations on T's properties — for call-edge resolution they pass through // to T's own method/property surface. NonNullable also unwraps to T. static const CBMType *unwrap_passthrough_template(const CBMType *t) { if (!t || t->kind != CBM_TYPE_TEMPLATE) return t; const char *name = t->data.template_type.template_name; if (!name || !t->data.template_type.template_args || t->data.template_type.arg_count < 1) return t; // Single-arg utility types pass through to T's surface for method dispatch. if (strcmp(name, "Partial") == 0 || strcmp(name, "Required") == 0 || strcmp(name, "Readonly") == 0 || strcmp(name, "NonNullable") == 0 || strcmp(name, "Pick") == 0 || strcmp(name, "Omit") == 0 || strcmp(name, "Awaited") == 0) { return t->data.template_type.template_args[0]; } // ReturnType: when F is FUNC, surface F's return type for method dispatch. if (strcmp(name, "ReturnType") == 0) { const CBMType *f = t->data.template_type.template_args[0]; if (f && f->kind == CBM_TYPE_FUNC && f->data.func.return_types && f->data.func.return_types[0]) { return f->data.func.return_types[0]; } return t; } // Exclude / Extract: T is a union; for method dispatch we treat the // result as T itself (any member's common methods still resolve via union dispatch). if (strcmp(name, "Exclude") == 0 || strcmp(name, "Extract") == 0) { return t->data.template_type.template_args[0]; } // Promise intentionally NOT unwrapped — methods (then/catch/finally) live on // the Promise registration. await-expression unwraps via ts_eval_expr_type. return t; } // Build a UNION of LITERAL string types representing keyof T, where T is a registered // class/interface. Returns UNKNOWN if T has no fields or isn't registered. static const CBMType *eval_keyof(TSLSPContext *ctx, const CBMType *operand) { if (!ctx || !operand) return cbm_type_unknown(); const CBMType *base = simplify_type(ctx, operand); if (!base || base->kind != CBM_TYPE_NAMED) return cbm_type_unknown(); const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, base->data.named.qualified_name); if (!rt || !rt->field_names || !rt->field_names[0]) return cbm_type_unknown(); int fc = 0; while (rt->field_names[fc]) fc++; const CBMType *members[64]; int mc = 0; for (int i = 0; i < fc && mc < 63; i++) { members[mc++] = cbm_type_ts_literal(ctx->arena, "string", rt->field_names[i]); } members[mc] = NULL; if (mc == 0) return cbm_type_unknown(); if (mc == 1) return members[0]; return cbm_type_union(ctx->arena, members, mc); } // Evaluate T[K]: when K is a string literal, look up that field on T. When K is a // type parameter (or `keyof T` itself), return UNKNOWN — pragmatic v1. static const CBMType *eval_indexed_access(TSLSPContext *ctx, const CBMType *obj, const CBMType *key) { if (!ctx || !obj || !key) return cbm_type_unknown(); if (key->kind == CBM_TYPE_TS_LITERAL && key->data.literal_ts.value && key->data.literal_ts.tag && strcmp(key->data.literal_ts.tag, "string") == 0) { return lookup_member_type(ctx, obj, key->data.literal_ts.value); } return cbm_type_unknown(); } // Look up a method on a receiver type — returns the registered func. static const CBMRegisteredFunc *lookup_method(TSLSPContext *ctx, const CBMType *recv, const char *method_name) { if (!ctx || !recv || !method_name) return NULL; const CBMType *base = simplify_type(ctx, recv); if (!base) return NULL; base = unwrap_passthrough_template(base); if (!base) return NULL; // BUILTIN primitives → look up via the wrapper class (string → String, etc.). if (base->kind == CBM_TYPE_BUILTIN) { const char *wrap = builtin_wrapper_class(base->data.builtin.name); if (wrap) { const CBMType *wrapped = cbm_type_named(ctx->arena, wrap); return lookup_method(ctx, wrapped, method_name); } return NULL; } if (ctx->debug) { const char *base_qn = (base->kind == CBM_TYPE_NAMED) ? base->data.named.qualified_name : (base->kind == CBM_TYPE_TEMPLATE) ? base->data.template_type.template_name : ""; fprintf(stderr, "[ts_lsp] lookup_method: recv_kind=%d recv_qn=%s method=%s\n", (int)base->kind, base_qn ? base_qn : "(null)", method_name); } if (base->kind == CBM_TYPE_TEMPLATE) { const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, base->data.template_type.template_name); if (rt && rt->method_qns && rt->method_names) { for (int i = 0; rt->method_names[i]; i++) { if (strcmp(rt->method_names[i], method_name) == 0) { return cbm_registry_lookup_func(ctx->registry, rt->method_qns[i]); } } } } if (base->kind == CBM_TYPE_UNION || base->kind == CBM_TYPE_INTERSECTION) { if (base->data.union_type.members) { for (int i = 0; i < base->data.union_type.count; i++) { const CBMRegisteredFunc *f = lookup_method(ctx, base->data.union_type.members[i], method_name); if (f) return f; } } return NULL; } if (base->kind != CBM_TYPE_NAMED) return NULL; const char *recv_qn = base->data.named.qualified_name; const CBMRegisteredFunc *f = cbm_registry_lookup_method_aliased(ctx->registry, recv_qn, method_name); if (f) return f; // Walk extends/implements via the registered type. const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, recv_qn); if (rt && rt->embedded_types) { for (int i = 0; rt->embedded_types[i]; i++) { const CBMType *parent = cbm_type_named(ctx->arena, rt->embedded_types[i]); const CBMRegisteredFunc *pf = lookup_method(ctx, parent, method_name); if (pf) return pf; } } return NULL; } // Pull the underlying return type out of a FUNC signature, collapsing single-element // return arrays to that element. static const CBMType *return_type_of(CBMArena *arena, const CBMType *sig) { if (!sig || sig->kind != CBM_TYPE_FUNC) return cbm_type_unknown(); if (!sig->data.func.return_types || !sig->data.func.return_types[0]) return cbm_type_unknown(); if (!sig->data.func.return_types[1]) return sig->data.func.return_types[0]; int count = 0; while (sig->data.func.return_types[count]) count++; // Multi-return → build a tuple. Needs a real arena: passing NULL here // crashed in cbm_arena_alloc when the cross-resolve path evaluated a // multi-return signature (only reachable once the per-file LSP O(n²) was // fixed and the resolve phase actually ran). return cbm_type_tuple(arena, sig->data.func.return_types, count); } 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) return cbm_type_unknown(); ctx->eval_depth++; const CBMType *result = cbm_type_unknown(); const char *kind = ts_node_type(node); if (strcmp(kind, "identifier") == 0 || strcmp(kind, "shorthand_property_identifier") == 0) { char *name = node_text(ctx, node); if (name) result = type_of_identifier(ctx, name); } else if (strcmp(kind, "this") == 0) { if (ctx->enclosing_class_qn) { result = cbm_type_named(ctx->arena, ctx->enclosing_class_qn); } } else if (strcmp(kind, "super") == 0) { if (ctx->enclosing_class_qn) { const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, ctx->enclosing_class_qn); if (rt && rt->embedded_types && rt->embedded_types[0]) { result = cbm_type_named(ctx->arena, rt->embedded_types[0]); } } } else if (strcmp(kind, "parenthesized_expression") == 0) { TSNode inner = ts_node_named_child(node, 0); result = ts_eval_expr_type(ctx, inner); } else if (strcmp(kind, "non_null_expression") == 0) { // `expr!` — strip the assertion, type unchanged. TSNode inner = ts_node_named_child(node, 0); result = ts_eval_expr_type(ctx, inner); } else if (strcmp(kind, "as_expression") == 0 || strcmp(kind, "satisfies_expression") == 0 || strcmp(kind, "type_assertion") == 0) { // `expr as T` / `expr` / `expr satisfies T`: type-position field gives the type. // For satisfies, we keep the asserted-type (TS narrows it but for resolution that's fine). TSNode type_node = ts_node_child_by_field_name(node, "type", TS_LSP_FIELD_LEN("type")); if (!ts_node_is_null(type_node)) { char *text = node_text(ctx, type_node); result = parse_ts_type_text(ctx->arena, text, ctx->module_qn); } else { // Last named child is the type for older grammars. uint32_t nc = ts_node_named_child_count(node); if (nc >= 2) { TSNode tn = ts_node_named_child(node, nc - 1); char *text = node_text(ctx, tn); result = parse_ts_type_text(ctx->arena, text, ctx->module_qn); } } } else if (strcmp(kind, "member_expression") == 0 || strcmp(kind, "subscript_expression") == 0) { TSNode obj = ts_node_child_by_field_name(node, "object", TS_LSP_FIELD_LEN("object")); TSNode prop = ts_node_child_by_field_name(node, "property", TS_LSP_FIELD_LEN("property")); if (!ts_node_is_null(obj) && !ts_node_is_null(prop)) { const CBMType *recv = ts_eval_expr_type(ctx, obj); char *pname = node_text(ctx, prop); if (pname) result = lookup_member_type(ctx, recv, pname); } } else if (strcmp(kind, "new_expression") == 0) { TSNode ctor = ts_node_child_by_field_name(node, "constructor", TS_LSP_FIELD_LEN("constructor")); if (!ts_node_is_null(ctor)) { char *cname = node_text(ctx, ctor); if (cname) { // Bare class name → qualify against module. if (strchr(cname, '.') == NULL && ctx->module_qn) { const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, cname); result = cbm_type_named(ctx->arena, qn); } else { result = cbm_type_named(ctx->arena, cname); } } } } else if (strcmp(kind, "call_expression") == 0) { TSNode fn = ts_node_child_by_field_name(node, "function", TS_LSP_FIELD_LEN("function")); if (!ts_node_is_null(fn)) { const CBMType *fn_type = ts_eval_expr_type(ctx, fn); if (ctx->debug) { fprintf(stderr, "[ts_lsp] eval call_expression: fn_kind=%s fn_type_kind=%d\n", ts_node_type(fn), fn_type ? (int)fn_type->kind : -1); } if (fn_type && fn_type->kind == CBM_TYPE_FUNC) { result = return_type_of(ctx->arena, fn_type); // Polymorphic `this` return type: when the registered signature uses // `this` (TYPE_PARAM "this"), substitute the actual receiver type // from the call site (fn is a member_expression — its `object` is // the receiver). Powers fluent-builder patterns. if (result && result->kind == CBM_TYPE_TYPE_PARAM && result->data.type_param.name && strcmp(result->data.type_param.name, "this") == 0 && strcmp(ts_node_type(fn), "member_expression") == 0) { TSNode obj = ts_node_child_by_field_name(fn, "object", TS_LSP_FIELD_LEN("object")); if (!ts_node_is_null(obj)) { const CBMType *recv = ts_eval_expr_type(ctx, obj); if (recv && !cbm_type_is_unknown(recv)) result = recv; } } // Generic inference at call site: if the function has type parameters // referenced in return / param types, infer T from concrete arg types // and substitute. This is the simplest form of typescript-go's // inferTypeArguments — single-pass, argument-driven. if (result && result->kind == CBM_TYPE_TYPE_PARAM && fn_type->data.func.param_types) { TSNode args = ts_node_child_by_field_name(node, "arguments", TS_LSP_FIELD_LEN("arguments")); if (!ts_node_is_null(args)) { // Build a list of inferred type-param names + arg types. const char *inf_names[8] = {0}; const CBMType *inf_args[8] = {0}; int ic = 0; uint32_t pc = 0; while (fn_type->data.func.param_types[pc]) pc++; uint32_t argc = ts_node_named_child_count(args); for (uint32_t i = 0; i < pc && i < argc && ic < 7; i++) { const CBMType *pt = fn_type->data.func.param_types[i]; if (!pt || pt->kind != CBM_TYPE_TYPE_PARAM) continue; TSNode arg = ts_node_named_child(args, i); if (ts_node_is_null(arg)) continue; const CBMType *at = ts_eval_expr_type(ctx, arg); if (cbm_type_is_unknown(at)) continue; inf_names[ic] = pt->data.type_param.name; inf_args[ic] = at; ic++; } if (ic > 0) { inf_names[ic] = NULL; inf_args[ic] = NULL; const CBMType *sub = cbm_type_substitute(ctx->arena, result, inf_names, inf_args); if (sub && !cbm_type_is_unknown(sub)) result = sub; } } } if (ctx->debug) { const char *res_qn = (result && result->kind == CBM_TYPE_NAMED) ? result->data.named.qualified_name : ""; fprintf(stderr, "[ts_lsp] returns kind=%d qn=%s\n", result ? (int)result->kind : -1, res_qn ? res_qn : "(null)"); } } } } else if (strcmp(kind, "await_expression") == 0) { TSNode inner = ts_node_named_child(node, 0); const CBMType *t = ts_eval_expr_type(ctx, inner); // Promise → T. if (t && t->kind == CBM_TYPE_TEMPLATE && t->data.template_type.template_name && strcmp(t->data.template_type.template_name, "Promise") == 0 && t->data.template_type.arg_count >= 1 && t->data.template_type.template_args) { result = t->data.template_type.template_args[0]; } else { result = t; } } else if (strcmp(kind, "string") == 0 || strcmp(kind, "template_string") == 0) { result = cbm_type_builtin(ctx->arena, "string"); } else if (strcmp(kind, "number") == 0) { result = cbm_type_builtin(ctx->arena, "number"); } else if (strcmp(kind, "true") == 0 || strcmp(kind, "false") == 0) { result = cbm_type_builtin(ctx->arena, "boolean"); } else if (strcmp(kind, "null") == 0) { result = cbm_type_builtin(ctx->arena, "null"); } else if (strcmp(kind, "undefined") == 0) { result = cbm_type_builtin(ctx->arena, "undefined"); } else if (strcmp(kind, "array") == 0) { // Use first element's type as Array argument. const CBMType *elem = cbm_type_unknown(); uint32_t nc = ts_node_named_child_count(node); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_named_child(node, i); if (!ts_node_is_null(c)) { elem = ts_eval_expr_type(ctx, c); if (!cbm_type_is_unknown(elem)) break; } } const CBMType *args[2] = {elem, NULL}; result = cbm_type_template(ctx->arena, "Array", args, 1); } else if (strcmp(kind, "object") == 0) { // Object literal — capture string-keyed property types. uint32_t nc = ts_node_named_child_count(node); const char *names[32] = {0}; const CBMType *types[32] = {0}; int count = 0; for (uint32_t i = 0; i < nc && count < 31; i++) { TSNode c = ts_node_named_child(node, i); if (ts_node_is_null(c)) continue; const char *ck = ts_node_type(c); if (strcmp(ck, "pair") == 0) { TSNode key = ts_node_child_by_field_name(c, "key", TS_LSP_FIELD_LEN("key")); TSNode val = ts_node_child_by_field_name(c, "value", TS_LSP_FIELD_LEN("value")); if (!ts_node_is_null(key) && !ts_node_is_null(val)) { char *k = node_text(ctx, key); if (k) { // Strip surrounding quotes if string key. size_t kl = strlen(k); if (kl >= 2 && (k[0] == '"' || k[0] == '\'') && k[kl - 1] == k[0]) { k[kl - 1] = '\0'; k++; } names[count] = k; types[count] = ts_eval_expr_type(ctx, val); count++; } } } else if (strcmp(ck, "shorthand_property_identifier") == 0) { char *k = node_text(ctx, c); if (k) { names[count] = k; types[count] = type_of_identifier(ctx, k); count++; } } } names[count] = NULL; types[count] = NULL; result = cbm_type_object_lit(ctx->arena, names, types, NULL, NULL); } else if (strcmp(kind, "ternary_expression") == 0) { TSNode cons = ts_node_child_by_field_name(node, "consequence", TS_LSP_FIELD_LEN("consequence")); TSNode alt = ts_node_child_by_field_name(node, "alternative", TS_LSP_FIELD_LEN("alternative")); const CBMType *a = ts_eval_expr_type(ctx, cons); const CBMType *b = ts_eval_expr_type(ctx, alt); if (cbm_type_is_unknown(a)) result = b; else if (cbm_type_is_unknown(b)) result = a; else if (a == b) result = a; else { const CBMType *members[3] = {a, b, NULL}; result = cbm_type_union(ctx->arena, members, 2); } } else if (strcmp(kind, "binary_expression") == 0) { // String concatenation `a + b` where either side is string → string. TSNode left = ts_node_child_by_field_name(node, "left", TS_LSP_FIELD_LEN("left")); TSNode right = ts_node_child_by_field_name(node, "right", TS_LSP_FIELD_LEN("right")); if (!ts_node_is_null(left) && !ts_node_is_null(right)) { const CBMType *l = ts_eval_expr_type(ctx, left); const CBMType *r = ts_eval_expr_type(ctx, right); if ((l && l->kind == CBM_TYPE_BUILTIN && l->data.builtin.name && strcmp(l->data.builtin.name, "string") == 0) || (r && r->kind == CBM_TYPE_BUILTIN && r->data.builtin.name && strcmp(r->data.builtin.name, "string") == 0)) { result = cbm_type_builtin(ctx->arena, "string"); } else { result = cbm_type_builtin(ctx->arena, "number"); } } } ctx->eval_depth--; return result; } // Walk a parsed type and replace NAMED references whose bare name matches a known // import binding. Recurses through TEMPLATE / UNION / INTERSECTION compound kinds. // // Naming convention: ctx->import_module_qns[i] is the imported module's QN (e.g. // "test.conn"); to get the full type QN, we append the local-binding name. Falls back // to the original type if no match. static const CBMType *resolve_type_with_imports(TSLSPContext *ctx, const CBMType *t) { if (!t || !ctx) return t; if (t->kind == CBM_TYPE_NAMED && t->data.named.qualified_name) { const char *qn = t->data.named.qualified_name; const char *dot = strrchr(qn, '.'); const char *bare = dot ? dot + 1 : qn; // TS semantics: locally-declared types shadow ambient/stdlib types with the // same name. parse_ts_type_text leaves stdlib names bare to match the stdlib // registration QN; if the local module has a type with the same short name // (e.g. test files declaring their own `Response`/`File`/etc.), prefer that. if (!dot && ctx->module_qn) { const char *local_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, bare); const CBMRegisteredType *local_rt = cbm_registry_lookup_type(ctx->registry, local_qn); if (local_rt) return cbm_type_named(ctx->arena, local_qn); } for (int i = 0; i < ctx->import_count; i++) { const char *lname = ctx->import_local_names ? ctx->import_local_names[i] : NULL; const char *mqn = ctx->import_module_qns ? ctx->import_module_qns[i] : NULL; if (!lname || !mqn) continue; if (strcmp(lname, bare) != 0) continue; // Heuristic: if mqn already ends in ".bare" use as-is; otherwise append. size_t mqn_len = strlen(mqn); size_t bare_len = strlen(bare); if (mqn_len > bare_len + 1 && mqn[mqn_len - bare_len - 1] == '.' && strcmp(mqn + mqn_len - bare_len, bare) == 0) { return cbm_type_named(ctx->arena, mqn); } return cbm_type_named(ctx->arena, cbm_arena_sprintf(ctx->arena, "%s.%s", mqn, bare)); } return t; } if (t->kind == CBM_TYPE_TEMPLATE && t->data.template_type.template_args && t->data.template_type.arg_count > 0) { int ac = t->data.template_type.arg_count; const CBMType **new_args = (const CBMType **)cbm_arena_alloc( ctx->arena, (size_t)(ac + 1) * sizeof(const CBMType *)); if (!new_args) return t; for (int i = 0; i < ac; i++) { new_args[i] = resolve_type_with_imports(ctx, t->data.template_type.template_args[i]); } new_args[ac] = NULL; return cbm_type_template(ctx->arena, t->data.template_type.template_name, new_args, ac); } if (t->kind == CBM_TYPE_UNION || t->kind == CBM_TYPE_INTERSECTION) { if (!t->data.union_type.members || t->data.union_type.count == 0) return t; int mc = t->data.union_type.count; const CBMType **new_members = (const CBMType **)cbm_arena_alloc( ctx->arena, (size_t)(mc + 1) * sizeof(const CBMType *)); if (!new_members) return t; for (int i = 0; i < mc; i++) { new_members[i] = resolve_type_with_imports(ctx, t->data.union_type.members[i]); } new_members[mc] = NULL; return t->kind == CBM_TYPE_UNION ? cbm_type_union(ctx->arena, new_members, mc) : cbm_type_intersection(ctx->arena, new_members, mc); } return t; } const CBMType *ts_parse_type_node(TSLSPContext *ctx, TSNode node) { if (!ctx || ts_node_is_null(node)) return cbm_type_unknown(); char *text = node_text(ctx, node); const CBMType *parsed = parse_ts_type_text(ctx->arena, text, ctx->module_qn); return resolve_type_with_imports(ctx, parsed); } // ── Statement processing ────────────────────────────────────────────────────── // Bind a variable_declarator: `let x: T = init` / `const x = init` / `var x = ...`. static void bind_variable_declarator(TSLSPContext *ctx, TSNode decl) { TSNode name = ts_node_child_by_field_name(decl, "name", TS_LSP_FIELD_LEN("name")); TSNode tann = ts_node_child_by_field_name(decl, "type", TS_LSP_FIELD_LEN("type")); TSNode value = ts_node_child_by_field_name(decl, "value", TS_LSP_FIELD_LEN("value")); if (ts_node_is_null(name)) return; const CBMType *declared = NULL; if (!ts_node_is_null(tann)) { // type_annotation contains the actual type as a named child. TSNode tch = ts_node_named_child(tann, 0); if (!ts_node_is_null(tch)) declared = ts_parse_type_node(ctx, tch); } const CBMType *inferred = ts_node_is_null(value) ? cbm_type_unknown() : ts_eval_expr_type(ctx, value); const CBMType *bound = (declared && !cbm_type_is_unknown(declared)) ? declared : inferred; if (node_kind_is(name, "identifier")) { char *nm = node_text(ctx, name); if (nm) cbm_scope_bind(ctx->current_scope, nm, bound); } else if (node_kind_is(name, "object_pattern")) { // Destructure object: `const { a, b } = obj` — bind a and b to obj.a and obj.b types. uint32_t nc = ts_node_named_child_count(name); for (uint32_t i = 0; i < nc; i++) { TSNode pn = ts_node_named_child(name, i); if (ts_node_is_null(pn)) continue; const char *pk = ts_node_type(pn); if (strcmp(pk, "shorthand_property_identifier_pattern") == 0) { char *pnm = node_text(ctx, pn); if (pnm) cbm_scope_bind(ctx->current_scope, pnm, lookup_member_type(ctx, bound, pnm)); } else if (strcmp(pk, "pair_pattern") == 0) { TSNode key = ts_node_child_by_field_name(pn, "key", TS_LSP_FIELD_LEN("key")); TSNode val = ts_node_child_by_field_name(pn, "value", TS_LSP_FIELD_LEN("value")); if (!ts_node_is_null(key) && !ts_node_is_null(val) && node_kind_is(val, "identifier")) { char *knm = node_text(ctx, key); char *vnm = node_text(ctx, val); if (knm && vnm) { cbm_scope_bind(ctx->current_scope, vnm, lookup_member_type(ctx, bound, knm)); } } } } } else if (node_kind_is(name, "array_pattern")) { // Destructure array / tuple: `const [a, b] = pair`. uint32_t nc = ts_node_named_child_count(name); for (uint32_t i = 0; i < nc; i++) { TSNode pn = ts_node_named_child(name, i); if (ts_node_is_null(pn) || !node_kind_is(pn, "identifier")) continue; char *pnm = node_text(ctx, pn); if (!pnm) continue; const CBMType *et = cbm_type_unknown(); if (bound && bound->kind == CBM_TYPE_TEMPLATE && bound->data.template_type.template_args && bound->data.template_type.arg_count >= 1) { et = bound->data.template_type.template_args[0]; } else if (bound && bound->kind == CBM_TYPE_TUPLE && bound->data.tuple.elems && (int)i < bound->data.tuple.count) { et = bound->data.tuple.elems[i]; } cbm_scope_bind(ctx->current_scope, pnm, et); } } } // Bind a single TS parameter into the current scope. Robust against grammar field-name // variations: tries `pattern`, `name`, then falls back to first named child of the param. static void bind_parameter(TSLSPContext *ctx, TSNode param) { const char *pk = ts_node_type(param); if (strcmp(pk, "required_parameter") != 0 && strcmp(pk, "optional_parameter") != 0 && strcmp(pk, "tuple_parameter") != 0 && strcmp(pk, "optional_tuple_parameter") != 0) return; // Find pattern: try field "pattern", then "name", then first non-type named child. TSNode pattern = ts_node_child_by_field_name(param, "pattern", TS_LSP_FIELD_LEN("pattern")); if (ts_node_is_null(pattern)) { pattern = ts_node_child_by_field_name(param, "name", TS_LSP_FIELD_LEN("name")); } if (ts_node_is_null(pattern)) { uint32_t nc = ts_node_named_child_count(param); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_named_child(param, i); if (ts_node_is_null(c)) continue; const char *k = ts_node_type(c); if (strcmp(k, "type_annotation") == 0) continue; if (strcmp(k, "decorator") == 0) continue; pattern = c; break; } } if (ts_node_is_null(pattern)) return; // Find the type annotation: prefer field "type", fall back to descending into a child // of kind "type_annotation". TSNode tann = ts_node_child_by_field_name(param, "type", TS_LSP_FIELD_LEN("type")); if (ts_node_is_null(tann)) { uint32_t nc = ts_node_named_child_count(param); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_named_child(param, i); if (!ts_node_is_null(c) && strcmp(ts_node_type(c), "type_annotation") == 0) { tann = c; break; } } } const CBMType *t = cbm_type_unknown(); if (!ts_node_is_null(tann)) { // The tree may give us either the type_annotation node or its inner type node. TSNode tch = (strcmp(ts_node_type(tann), "type_annotation") == 0) ? ts_node_named_child(tann, 0) : tann; if (!ts_node_is_null(tch)) { const char *tk = ts_node_type(tch); // Inline object type `{ a: T; b: U }` — walk members and build OBJECT_LIT // directly via AST. Falling back to text-parsing the node would yield UNKNOWN. if (strcmp(tk, "object_type") == 0) { const char *p_names[32] = {0}; const CBMType *p_types[32] = {0}; int pcount = 0; uint32_t omc = ts_node_named_child_count(tch); for (uint32_t oi = 0; oi < omc && pcount < 31; oi++) { TSNode m = ts_node_named_child(tch, oi); if (ts_node_is_null(m)) continue; const char *mk = ts_node_type(m); if (strcmp(mk, "property_signature") == 0) { TSNode pname = ts_node_child_by_field_name(m, "name", TS_LSP_FIELD_LEN("name")); TSNode ptype = ts_node_child_by_field_name(m, "type", TS_LSP_FIELD_LEN("type")); if (ts_node_is_null(pname)) continue; char *pnm_text = node_text(ctx, pname); if (!pnm_text) continue; const CBMType *pt = cbm_type_unknown(); if (!ts_node_is_null(ptype)) { TSNode ptch = (strcmp(ts_node_type(ptype), "type_annotation") == 0) ? ts_node_named_child(ptype, 0) : ptype; if (!ts_node_is_null(ptch)) pt = ts_parse_type_node(ctx, ptch); } p_names[pcount] = pnm_text; p_types[pcount] = pt; pcount++; } } p_names[pcount] = NULL; p_types[pcount] = NULL; t = cbm_type_object_lit(ctx->arena, p_names, p_types, NULL, NULL); } else { t = ts_parse_type_node(ctx, tch); } } } if (node_kind_is(pattern, "identifier")) { char *nm = node_text(ctx, pattern); if (nm) cbm_scope_bind(ctx->current_scope, nm, t); } else if (node_kind_is(pattern, "object_pattern")) { uint32_t nc = ts_node_named_child_count(pattern); for (uint32_t i = 0; i < nc; i++) { TSNode p = ts_node_named_child(pattern, i); if (node_kind_is(p, "shorthand_property_identifier_pattern")) { char *pnm = node_text(ctx, p); if (pnm) cbm_scope_bind(ctx->current_scope, pnm, lookup_member_type(ctx, t, pnm)); } else if (node_kind_is(p, "pair_pattern")) { TSNode key = ts_node_child_by_field_name(p, "key", TS_LSP_FIELD_LEN("key")); TSNode val = ts_node_child_by_field_name(p, "value", TS_LSP_FIELD_LEN("value")); if (!ts_node_is_null(key) && !ts_node_is_null(val) && node_kind_is(val, "identifier")) { char *knm = node_text(ctx, key); char *vnm = node_text(ctx, val); if (knm && vnm) { cbm_scope_bind(ctx->current_scope, vnm, lookup_member_type(ctx, t, knm)); } } } } } else if (node_kind_is(pattern, "array_pattern")) { uint32_t nc = ts_node_named_child_count(pattern); for (uint32_t i = 0; i < nc; i++) { TSNode p = ts_node_named_child(pattern, i); if (!node_kind_is(p, "identifier")) continue; char *pnm = node_text(ctx, p); if (!pnm) continue; const CBMType *et = cbm_type_unknown(); if (t && t->kind == CBM_TYPE_TEMPLATE && t->data.template_type.template_args && t->data.template_type.arg_count >= 1) { et = t->data.template_type.template_args[0]; } cbm_scope_bind(ctx->current_scope, pnm, et); } } } void ts_process_statement(TSLSPContext *ctx, TSNode node) { if (!ctx || ts_node_is_null(node)) return; const char *kind = ts_node_type(node); if (strcmp(kind, "lexical_declaration") == 0 || strcmp(kind, "variable_declaration") == 0) { uint32_t nc = ts_node_named_child_count(node); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_named_child(node, i); if (node_kind_is(c, "variable_declarator")) bind_variable_declarator(ctx, c); } } else if (strcmp(kind, "for_in_statement") == 0 || strcmp(kind, "for_of_statement") == 0) { TSNode left = ts_node_child_by_field_name(node, "left", TS_LSP_FIELD_LEN("left")); TSNode right = ts_node_child_by_field_name(node, "right", TS_LSP_FIELD_LEN("right")); if (!ts_node_is_null(left) && !ts_node_is_null(right)) { const CBMType *iter = ts_eval_expr_type(ctx, right); const CBMType *elem = cbm_type_unknown(); if (iter && iter->kind == CBM_TYPE_TEMPLATE && iter->data.template_type.template_args && iter->data.template_type.arg_count >= 1) { elem = iter->data.template_type.template_args[0]; } // left is typically `lexical_declaration` of `(let x of arr)` or bare identifier. if (node_kind_is(left, "lexical_declaration") || node_kind_is(left, "variable_declaration")) { TSNode v = ts_node_named_child(left, 0); if (node_kind_is(v, "variable_declarator")) { TSNode nm = ts_node_child_by_field_name(v, "name", TS_LSP_FIELD_LEN("name")); if (node_kind_is(nm, "identifier")) { char *s = node_text(ctx, nm); if (s) cbm_scope_bind(ctx->current_scope, s, elem); } } } else if (node_kind_is(left, "identifier")) { char *s = node_text(ctx, left); if (s) cbm_scope_bind(ctx->current_scope, s, elem); } } } } // ── Call resolution + tree walk ─────────────────────────────────────────────── static void resolve_call_at(TSLSPContext *ctx, TSNode call_node) { TSNode fn = ts_node_child_by_field_name(call_node, "function", TS_LSP_FIELD_LEN("function")); if (ts_node_is_null(fn)) return; // Argument count. int arg_count = 0; TSNode args = ts_node_child_by_field_name(call_node, "arguments", TS_LSP_FIELD_LEN("arguments")); if (!ts_node_is_null(args)) arg_count = (int)ts_node_named_child_count(args); const char *fk = ts_node_type(fn); // member_expression: obj.method(...) if (strcmp(fk, "member_expression") == 0) { TSNode obj = ts_node_child_by_field_name(fn, "object", TS_LSP_FIELD_LEN("object")); TSNode prop = ts_node_child_by_field_name(fn, "property", TS_LSP_FIELD_LEN("property")); if (!ts_node_is_null(obj) && !ts_node_is_null(prop)) { char *mname = node_text(ctx, prop); if (mname) { // Identifier as namespace import: `name.fn()` or `mod.fn()`. if (node_kind_is(obj, "identifier")) { char *on = node_text(ctx, obj); if (on) { for (int i = 0; i < ctx->import_count; i++) { const char *lname = ctx->import_local_names ? ctx->import_local_names[i] : NULL; const char *mqn = ctx->import_module_qns ? ctx->import_module_qns[i] : NULL; if (lname && mqn && strcmp(lname, on) == 0) { const CBMRegisteredFunc *f = cbm_registry_lookup_symbol_by_args( ctx->registry, mqn, mname, arg_count); if (f) { ts_emit_resolved_call(ctx, f->qualified_name, "lsp_ts_namespace", 0.95f); return; } ts_emit_unresolved_call( ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", on, mname), "module_symbol_not_in_registry"); return; } } } } // Type-based dispatch. const CBMType *recv = ts_eval_expr_type(ctx, obj); const CBMRegisteredFunc *f = lookup_method(ctx, recv, mname); // Overload resolution by arg types: when receiver is NAMED and the // registry has multiple overloads (same receiver+name), eval each // arg's type and let cbm_registry_lookup_method_by_types score them. // Falls back to the first match if scoring fails. if (f && recv) { const CBMType *base = simplify_type(ctx, recv); base = base ? unwrap_passthrough_template(base) : NULL; if (base && base->kind == CBM_TYPE_NAMED && base->data.named.qualified_name && arg_count > 0) { const CBMType *arg_types[16] = {0}; int n = arg_count > 16 ? 16 : arg_count; for (int i = 0; i < n; i++) { TSNode arg = ts_node_named_child(args, (uint32_t)i); arg_types[i] = ts_eval_expr_type(ctx, arg); } const CBMRegisteredFunc *better = cbm_registry_lookup_method_by_types( ctx->registry, base->data.named.qualified_name, mname, arg_types, n); if (better) f = better; } } if (f) { ts_emit_resolved_call(ctx, f->qualified_name, "lsp_ts_method", 0.95f); return; } ts_emit_unresolved_call(ctx, mname, "method_not_in_registry"); } } return; } // identifier: free function call. if (strcmp(fk, "identifier") == 0) { char *name = node_text(ctx, fn); if (!name) return; // Eval arg types up-front for type-aware overload resolution. const CBMType *arg_types[16] = {0}; int typed_arg_n = 0; if (!ts_node_is_null(args) && arg_count > 0) { typed_arg_n = arg_count > 16 ? 16 : arg_count; for (int i = 0; i < typed_arg_n; i++) { TSNode arg = ts_node_named_child(args, (uint32_t)i); arg_types[i] = ts_eval_expr_type(ctx, arg); } } // Module-local function: prefer by-types when args are typed. if (ctx->module_qn) { const CBMRegisteredFunc *f = NULL; if (typed_arg_n > 0) { f = cbm_registry_lookup_symbol_by_types(ctx->registry, ctx->module_qn, name, arg_types, typed_arg_n); } if (!f) { f = cbm_registry_lookup_symbol_by_args(ctx->registry, ctx->module_qn, name, arg_count); } if (f) { ts_emit_resolved_call(ctx, f->qualified_name, "lsp_ts_local", 0.95f); return; } } // Imported default/named function. for (int i = 0; i < ctx->import_count; i++) { const char *lname = ctx->import_local_names ? ctx->import_local_names[i] : NULL; const char *mqn = ctx->import_module_qns ? ctx->import_module_qns[i] : NULL; if (lname && mqn && strcmp(lname, name) == 0) { const CBMRegisteredFunc *f = cbm_registry_lookup_symbol_by_args(ctx->registry, mqn, name, arg_count); if (f) { ts_emit_resolved_call(ctx, f->qualified_name, "lsp_ts_import", 0.95f); return; } // Fall through to qualified_name fallback. const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", mqn, name); ts_emit_unresolved_call(ctx, qn, "import_symbol_not_in_registry"); return; } } ts_emit_unresolved_call(ctx, name, "func_not_in_registry"); } } // Process an arrow_function or function_expression as a callback whose param types are // contextually typed by the enclosing call (e.g. `arr.map(x => x.length)` — `x` should // be bound to `arr`'s element type via Array.map's `(x: T) => U` signature). The // caller passes `expected` = the FUNC type the callback must conform to. static void process_callback_arrow(TSLSPContext *ctx, TSNode arrow, const CBMType *expected) { if (ts_node_is_null(arrow) || !expected || expected->kind != CBM_TYPE_FUNC) return; CBMScope *saved = ctx->current_scope; ctx->current_scope = cbm_scope_push(ctx->arena, saved); // Tree-sitter typescript shapes for arrow functions: // `(x, y) => body` → parameters: formal_parameters // `x => body` → parameter: identifier (singular field) TSNode params = ts_node_child_by_field_name(arrow, "parameters", TS_LSP_FIELD_LEN("parameters")); TSNode bare_param = ts_node_child_by_field_name(arrow, "parameter", TS_LSP_FIELD_LEN("parameter")); int idx = 0; if (!ts_node_is_null(bare_param)) { // Single bare param: `x => ...`. const char *pk = ts_node_type(bare_param); if (strcmp(pk, "identifier") == 0) { char *nm = node_text(ctx, bare_param); const CBMType *pt = (expected->data.func.param_types && expected->data.func.param_types[0]) ? expected->data.func.param_types[0] : cbm_type_unknown(); if (nm) cbm_scope_bind(ctx->current_scope, nm, pt); } } else if (!ts_node_is_null(params)) { uint32_t pc = ts_node_named_child_count(params); for (uint32_t i = 0; i < pc; i++) { TSNode p = ts_node_named_child(params, i); if (ts_node_is_null(p)) continue; const char *pk = ts_node_type(p); const char *pname = NULL; const CBMType *expected_pt = (expected->data.func.param_types && expected->data.func.param_types[idx]) ? expected->data.func.param_types[idx] : NULL; if (strcmp(pk, "identifier") == 0) { pname = node_text(ctx, p); } else { // required_parameter / optional_parameter — extract pattern. TSNode pp = ts_node_child_by_field_name(p, "pattern", TS_LSP_FIELD_LEN("pattern")); if (ts_node_is_null(pp)) { pp = ts_node_child_by_field_name(p, "name", TS_LSP_FIELD_LEN("name")); } if (!ts_node_is_null(pp) && strcmp(ts_node_type(pp), "identifier") == 0) { pname = node_text(ctx, pp); } // If the param has its own annotation, use that. Otherwise use expected. TSNode tann = ts_node_child_by_field_name(p, "type", TS_LSP_FIELD_LEN("type")); if (!ts_node_is_null(tann)) { TSNode tch = (strcmp(ts_node_type(tann), "type_annotation") == 0) ? ts_node_named_child(tann, 0) : tann; if (!ts_node_is_null(tch)) { const CBMType *annotated = ts_parse_type_node(ctx, tch); if (!cbm_type_is_unknown(annotated)) { expected_pt = annotated; } } } } if (pname) { cbm_scope_bind(ctx->current_scope, pname, expected_pt ? expected_pt : cbm_type_unknown()); } idx++; } } TSNode body = ts_node_child_by_field_name(arrow, "body", TS_LSP_FIELD_LEN("body")); if (!ts_node_is_null(body)) { if (strcmp(ts_node_type(body), "statement_block") == 0) { TSTreeCursor cursor = ts_tree_cursor_new(body); if (ts_tree_cursor_goto_first_child(&cursor)) { do { process_node(ctx, ts_tree_cursor_current_node(&cursor)); } while (ts_tree_cursor_goto_next_sibling(&cursor)); } ts_tree_cursor_delete(&cursor); } else { process_node(ctx, body); } } ctx->current_scope = saved; } // Try to extract a narrowing fact from an `if`-condition: `x instanceof Foo`, // `typeof x === 'string'`, or `x.kind === 'lit'` (discriminated union). // On success: writes the narrowed variable name and target type, plus whether the // match polarity is `==` (true → consequence) or `!=` (true → alternative). // Returns true if narrowing applies. static bool extract_narrowing(TSLSPContext *ctx, TSNode condition, const char **out_var, const CBMType **out_type, bool *out_inverted) { *out_var = NULL; *out_type = NULL; *out_inverted = false; if (ts_node_is_null(condition)) return false; // Strip outer parens if any. while (!ts_node_is_null(condition) && strcmp(ts_node_type(condition), "parenthesized_expression") == 0) { condition = ts_node_named_child(condition, 0); } if (ts_node_is_null(condition)) return false; const char *k = ts_node_type(condition); // `x instanceof Foo`: binary_expression with operator "instanceof". if (strcmp(k, "binary_expression") == 0) { TSNode op = ts_node_child_by_field_name(condition, "operator", TS_LSP_FIELD_LEN("operator")); char *opt = ts_node_is_null(op) ? NULL : node_text(ctx, op); TSNode left = ts_node_child_by_field_name(condition, "left", TS_LSP_FIELD_LEN("left")); TSNode right = ts_node_child_by_field_name(condition, "right", TS_LSP_FIELD_LEN("right")); if (opt && strcmp(opt, "instanceof") == 0 && !ts_node_is_null(left) && !ts_node_is_null(right) && strcmp(ts_node_type(left), "identifier") == 0 && strcmp(ts_node_type(right), "identifier") == 0) { *out_var = node_text(ctx, left); char *class_name = node_text(ctx, right); if (class_name) { *out_type = parse_ts_type_text(ctx->arena, class_name, ctx->module_qn); *out_type = resolve_type_with_imports(ctx, *out_type); } return *out_var && *out_type; } // `typeof x === 'string'` / `typeof x !== 'number'` if (opt && (strcmp(opt, "===") == 0 || strcmp(opt, "==") == 0 || strcmp(opt, "!==") == 0 || strcmp(opt, "!=") == 0) && !ts_node_is_null(left) && !ts_node_is_null(right)) { bool inverted = (strcmp(opt, "!==") == 0 || strcmp(opt, "!=") == 0); // typeof x === 'string' if (strcmp(ts_node_type(left), "unary_expression") == 0) { TSNode op2 = ts_node_child_by_field_name(left, "operator", TS_LSP_FIELD_LEN("operator")); char *opt2 = ts_node_is_null(op2) ? NULL : node_text(ctx, op2); TSNode arg = ts_node_child_by_field_name(left, "argument", TS_LSP_FIELD_LEN("argument")); if (opt2 && strcmp(opt2, "typeof") == 0 && !ts_node_is_null(arg) && strcmp(ts_node_type(arg), "identifier") == 0 && strcmp(ts_node_type(right), "string") == 0) { *out_var = node_text(ctx, arg); char *lit = node_text(ctx, right); if (lit) { // Strip surrounding quotes. size_t ll = strlen(lit); if (ll >= 2 && (lit[0] == '"' || lit[0] == '\'') && lit[ll - 1] == lit[0]) { lit[ll - 1] = '\0'; lit++; } *out_type = cbm_type_builtin(ctx->arena, lit); } *out_inverted = inverted; return *out_var && *out_type; } } } } return false; } // Look up a union member that satisfies a discriminant: when narrowed via `x.kind === // 'foo'`, find the member of x's type whose `kind` property is the literal 'foo'. // Returns NULL if no match. Pragmatic v1: only handles `member_expression === string`. static const CBMType *narrow_discriminated_union(TSLSPContext *ctx, TSNode condition, const char **out_var) { *out_var = NULL; if (ts_node_is_null(condition)) return NULL; while (!ts_node_is_null(condition) && strcmp(ts_node_type(condition), "parenthesized_expression") == 0) { condition = ts_node_named_child(condition, 0); } if (ts_node_is_null(condition)) return NULL; if (strcmp(ts_node_type(condition), "binary_expression") != 0) return NULL; TSNode op = ts_node_child_by_field_name(condition, "operator", TS_LSP_FIELD_LEN("operator")); char *opt = ts_node_is_null(op) ? NULL : node_text(ctx, op); if (!opt || (strcmp(opt, "===") != 0 && strcmp(opt, "==") != 0)) return NULL; TSNode left = ts_node_child_by_field_name(condition, "left", TS_LSP_FIELD_LEN("left")); TSNode right = ts_node_child_by_field_name(condition, "right", TS_LSP_FIELD_LEN("right")); if (ts_node_is_null(left) || ts_node_is_null(right)) return NULL; // left = `x.kind`, right = string literal. if (strcmp(ts_node_type(left), "member_expression") != 0 || strcmp(ts_node_type(right), "string") != 0) return NULL; TSNode obj = ts_node_child_by_field_name(left, "object", TS_LSP_FIELD_LEN("object")); TSNode prop = ts_node_child_by_field_name(left, "property", TS_LSP_FIELD_LEN("property")); if (ts_node_is_null(obj) || strcmp(ts_node_type(obj), "identifier") != 0) return NULL; if (ts_node_is_null(prop)) return NULL; char *var_name = node_text(ctx, obj); char *prop_name = node_text(ctx, prop); char *lit = node_text(ctx, right); if (!var_name || !prop_name || !lit) return NULL; size_t ll = strlen(lit); if (ll >= 2 && (lit[0] == '"' || lit[0] == '\'') && lit[ll - 1] == lit[0]) { lit[ll - 1] = '\0'; lit++; } const CBMType *var_type = cbm_scope_lookup(ctx->current_scope, var_name); if (!var_type || var_type->kind != CBM_TYPE_UNION) return NULL; if (!var_type->data.union_type.members) return NULL; for (int i = 0; i < var_type->data.union_type.count; i++) { const CBMType *m = var_type->data.union_type.members[i]; if (!m || m->kind != CBM_TYPE_NAMED) continue; const CBMRegisteredType *rt = cbm_registry_lookup_type(ctx->registry, m->data.named.qualified_name); if (!rt || !rt->field_names || !rt->field_types) continue; for (int j = 0; rt->field_names[j]; j++) { if (strcmp(rt->field_names[j], prop_name) != 0) continue; const CBMType *ft = rt->field_types[j]; if (!ft) continue; // Discriminant fields are typically string-literal types (`'circle'`) or // the BUILTIN("string") fallback. For BUILTIN fallback we can't narrow // precisely, so skip unless the parsed text matches our literal. if (ft->kind == CBM_TYPE_TS_LITERAL && ft->data.literal_ts.value && strcmp(ft->data.literal_ts.value, lit) == 0) { *out_var = var_name; return m; } // Some interfaces declare `kind: 'circle'` as NAMED("'circle'") via // text fall-through; handle that too. if (ft->kind == CBM_TYPE_NAMED && ft->data.named.qualified_name) { const char *qn = ft->data.named.qualified_name; size_t qnl = strlen(qn); if (qnl >= 2 && (qn[0] == '\'' || qn[0] == '"') && qn[qnl - 1] == qn[0]) { if (strncmp(qn + 1, lit, qnl - 2) == 0 && strlen(lit) == qnl - 2) { *out_var = var_name; return m; } } } } } return NULL; } // Resolve a JSX element opening — emits a resolved call edge for component invocations. // `` and `...` both invoke the component as a function. // Lowercase first letter is treated as a JSX intrinsic (HTML element) and ignored. static void resolve_jsx_element(TSLSPContext *ctx, TSNode element_node) { if (!ctx->jsx_mode) return; // The element's first named child is the opening element (or self-closing). const char *kind = ts_node_type(element_node); TSNode tag = element_node; if (strcmp(kind, "jsx_element") == 0) { TSNode opening = ts_node_named_child(element_node, 0); if (!ts_node_is_null(opening)) tag = opening; } // Get the name from the opening / self_closing element. Field is "name". TSNode name_node = ts_node_child_by_field_name(tag, "name", TS_LSP_FIELD_LEN("name")); if (ts_node_is_null(name_node)) return; char *tag_name = node_text(ctx, name_node); if (!tag_name || !tag_name[0]) return; // Lowercase first → intrinsic (HTML element). Skip emission. if (tag_name[0] >= 'a' && tag_name[0] <= 'z') return; // Resolve as a free-function/component call. if (ctx->module_qn) { const CBMRegisteredFunc *f = cbm_registry_lookup_symbol(ctx->registry, ctx->module_qn, tag_name); if (f) { ts_emit_resolved_call(ctx, f->qualified_name, "lsp_ts_jsx", 0.95f); return; } } // Try imports. for (int i = 0; i < ctx->import_count; i++) { const char *lname = ctx->import_local_names ? ctx->import_local_names[i] : NULL; const char *mqn = ctx->import_module_qns ? ctx->import_module_qns[i] : NULL; if (lname && mqn && strcmp(lname, tag_name) == 0) { /* A relative module path ("./widget") is unresolved at the per-file * stage — it is the raw specifier, not a module QN, so "./widget.Widget" * matches no node and (winning the join on equal confidence) would drop * the edge. The cross-file pass re-runs with the path resolved to the * real module QN and emits the correct resolution, so skip the per-file * emission for relative specifiers and let that one stand. */ if (mqn[0] == '.') { ts_emit_unresolved_call(ctx, tag_name, "jsx_import_unresolved_path"); return; } const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", mqn, tag_name); ts_emit_resolved_call(ctx, qn, "lsp_ts_jsx_import", 0.85f); return; } } ts_emit_unresolved_call(ctx, tag_name, "jsx_component_not_in_registry"); } static void process_node(TSLSPContext *ctx, TSNode node) { if (!ctx || ts_node_is_null(node)) return; const char *kind = ts_node_type(node); // Scope-affecting statements bind first, then we recurse. ts_process_statement(ctx, node); if (strcmp(kind, "call_expression") == 0) { resolve_call_at(ctx, node); // Contextual callback typing: when an arg is an arrow_function and the // corresponding param of the called function is itself a FUNC, propagate the // expected callback param types into the arrow's body before walking it. // Handle arg processing manually for this path to avoid double-walking via // default recurse below. do { TSNode fn_node = ts_node_child_by_field_name(node, "function", TS_LSP_FIELD_LEN("function")); if (ts_node_is_null(fn_node)) break; const CBMType *fn_type = ts_eval_expr_type(ctx, fn_node); if (!fn_type || fn_type->kind != CBM_TYPE_FUNC) break; if (!fn_type->data.func.param_types) break; TSNode args = ts_node_child_by_field_name(node, "arguments", TS_LSP_FIELD_LEN("arguments")); if (ts_node_is_null(args)) break; // Process the function child for nested call resolution. process_node(ctx, fn_node); // param_types is NULL-terminated (no count field). Measure its // length so we never index past the terminator: a call may pass // more args than the function declares params (e.g. excess/variadic // args), and the extra args simply have no expected type. Indexing // param_types[i] by the raw arg count read out of bounds → garbage // CBMType* → crash on expected->kind. uint32_t param_count = 0; while (fn_type->data.func.param_types[param_count]) param_count++; uint32_t argc = ts_node_named_child_count(args); for (uint32_t i = 0; i < argc; i++) { TSNode arg = ts_node_named_child(args, i); if (ts_node_is_null(arg)) continue; const char *ak = ts_node_type(arg); const CBMType *expected = (i < param_count) ? fn_type->data.func.param_types[i] : NULL; if ((strcmp(ak, "arrow_function") == 0 || strcmp(ak, "function_expression") == 0) && expected && expected->kind == CBM_TYPE_FUNC) { process_callback_arrow(ctx, arg, expected); } else { process_node(ctx, arg); } } return; // Skip default recurse since we processed children manually. } while (0); } // JSX element resolution (TSX / JSX modes). if (ctx->jsx_mode && (strcmp(kind, "jsx_element") == 0 || strcmp(kind, "jsx_self_closing_element") == 0)) { resolve_jsx_element(ctx, node); } // Switch-statement narrowing: `switch (x.kind) { case 'foo': ... }` — narrow x // to the union member whose discriminant property matches the case literal. if (strcmp(kind, "switch_statement") == 0) { TSNode value = ts_node_child_by_field_name(node, "value", TS_LSP_FIELD_LEN("value")); TSNode body = ts_node_child_by_field_name(node, "body", TS_LSP_FIELD_LEN("body")); // Walk the value expression so any nested calls resolve. if (!ts_node_is_null(value)) process_node(ctx, value); // Identify discriminant: switch on `x.kind` member_expression. const char *disc_var = NULL; const char *disc_prop = NULL; const CBMType *var_union = NULL; if (!ts_node_is_null(value)) { // Strip parens. TSNode v = value; while (!ts_node_is_null(v) && strcmp(ts_node_type(v), "parenthesized_expression") == 0) { v = ts_node_named_child(v, 0); } if (!ts_node_is_null(v) && strcmp(ts_node_type(v), "member_expression") == 0) { TSNode obj = ts_node_child_by_field_name(v, "object", TS_LSP_FIELD_LEN("object")); TSNode prop = ts_node_child_by_field_name(v, "property", TS_LSP_FIELD_LEN("property")); if (!ts_node_is_null(obj) && !ts_node_is_null(prop) && strcmp(ts_node_type(obj), "identifier") == 0) { disc_var = node_text(ctx, obj); disc_prop = node_text(ctx, prop); if (disc_var) { const CBMType *t = cbm_scope_lookup(ctx->current_scope, disc_var); if (t && t->kind == CBM_TYPE_UNION) var_union = t; } } } } // Walk each switch_case in the body; narrow x in each case's scope. if (!ts_node_is_null(body)) { uint32_t bnc = ts_node_named_child_count(body); for (uint32_t i = 0; i < bnc; i++) { TSNode case_node = ts_node_named_child(body, i); if (ts_node_is_null(case_node)) continue; const char *ck = ts_node_type(case_node); if (strcmp(ck, "switch_case") != 0 && strcmp(ck, "switch_default") != 0) { process_node(ctx, case_node); continue; } // Find this case's literal value. const CBMType *narrowed = NULL; if (var_union && disc_prop && strcmp(ck, "switch_case") == 0) { TSNode case_value = ts_node_child_by_field_name(case_node, "value", TS_LSP_FIELD_LEN("value")); if (!ts_node_is_null(case_value) && strcmp(ts_node_type(case_value), "string") == 0) { char *lit = node_text(ctx, case_value); if (lit) { size_t ll = strlen(lit); if (ll >= 2 && (lit[0] == '"' || lit[0] == '\'') && lit[ll - 1] == lit[0]) { lit[ll - 1] = '\0'; lit++; } // Match member with matching discriminant field literal. for (int mi = 0; mi < var_union->data.union_type.count; mi++) { const CBMType *m = var_union->data.union_type.members[mi]; if (!m || m->kind != CBM_TYPE_NAMED) continue; const CBMRegisteredType *rt = cbm_registry_lookup_type( ctx->registry, m->data.named.qualified_name); if (!rt || !rt->field_names || !rt->field_types) continue; for (int fj = 0; rt->field_names[fj]; fj++) { if (strcmp(rt->field_names[fj], disc_prop) != 0) continue; const CBMType *ft = rt->field_types[fj]; if (!ft) continue; if (ft->kind == CBM_TYPE_TS_LITERAL && ft->data.literal_ts.value && strcmp(ft->data.literal_ts.value, lit) == 0) { narrowed = m; break; } if (ft->kind == CBM_TYPE_NAMED && ft->data.named.qualified_name) { const char *qn = ft->data.named.qualified_name; size_t qnl = strlen(qn); if (qnl >= 2 && (qn[0] == '\'' || qn[0] == '"') && qn[qnl - 1] == qn[0] && strncmp(qn + 1, lit, qnl - 2) == 0 && strlen(lit) == qnl - 2) { narrowed = m; break; } } } if (narrowed) break; } } } } // Walk case body with narrowed scope if applicable. CBMScope *saved = ctx->current_scope; if (narrowed && disc_var) { ctx->current_scope = cbm_scope_push(ctx->arena, saved); cbm_scope_bind(ctx->current_scope, disc_var, narrowed); } // case_node's children include the value and the case body. uint32_t cnc = ts_node_child_count(case_node); for (uint32_t j = 0; j < cnc; j++) { process_node(ctx, ts_node_child(case_node, j)); } ctx->current_scope = saved; } } return; } // Flow-sensitive narrowing for `if (...) { consequence } else { alternative }`. // Recognises `x instanceof T`, `typeof x === 'string'`, and `x.kind === 'lit'` // (discriminated union). Narrowed binding is pushed into a child scope for the // matching branch; the other branch sees the original type. if (strcmp(kind, "if_statement") == 0) { TSNode condition = ts_node_child_by_field_name(node, "condition", TS_LSP_FIELD_LEN("condition")); TSNode consequence = ts_node_child_by_field_name(node, "consequence", TS_LSP_FIELD_LEN("consequence")); TSNode alt = ts_node_child_by_field_name(node, "alternative", TS_LSP_FIELD_LEN("alternative")); // Walk the condition itself so any nested calls there resolve too. if (!ts_node_is_null(condition)) process_node(ctx, condition); // 1. Try simple narrowing (instanceof, typeof). const char *nv = NULL; const CBMType *nt = NULL; bool inverted = false; bool ok = extract_narrowing(ctx, condition, &nv, &nt, &inverted); // 2. Try discriminated-union narrowing if simple narrowing didn't apply. if (!ok) { const char *dv = NULL; const CBMType *dt = narrow_discriminated_union(ctx, condition, &dv); if (dv && dt) { nv = dv; nt = dt; inverted = false; ok = true; } } // Walk consequence with narrowed binding (for the truthy branch unless inverted). if (!ts_node_is_null(consequence)) { if (ok && nv && nt && !inverted) { CBMScope *saved = ctx->current_scope; ctx->current_scope = cbm_scope_push(ctx->arena, saved); cbm_scope_bind(ctx->current_scope, nv, nt); process_node(ctx, consequence); ctx->current_scope = saved; } else { process_node(ctx, consequence); } } // Walk alternative with negated narrowing for typeof case (instanceof+union // narrowing inverse is harder — skip). if (!ts_node_is_null(alt)) { if (ok && nv && nt && inverted) { CBMScope *saved = ctx->current_scope; ctx->current_scope = cbm_scope_push(ctx->arena, saved); cbm_scope_bind(ctx->current_scope, nv, nt); process_node(ctx, alt); ctx->current_scope = saved; } else { process_node(ctx, alt); } } return; // Skip default recurse. } // Function-introducing nodes get their own scope + enclosing_func_qn handling. if (strcmp(kind, "function_declaration") == 0 || strcmp(kind, "method_definition") == 0 || strcmp(kind, "function_expression") == 0 || strcmp(kind, "arrow_function") == 0 || strcmp(kind, "method_signature") == 0) { // Nested functions just get a fresh scope; top-level / class-method walks own // their enclosing_func_qn via process_function_body. CBMScope *saved = ctx->current_scope; ctx->current_scope = cbm_scope_push(ctx->arena, saved); TSNode params = ts_node_child_by_field_name(node, "parameters", TS_LSP_FIELD_LEN("parameters")); if (!ts_node_is_null(params)) { uint32_t nc = ts_node_named_child_count(params); for (uint32_t i = 0; i < nc; i++) bind_parameter(ctx, ts_node_named_child(params, i)); } TSNode body = ts_node_child_by_field_name(node, "body", TS_LSP_FIELD_LEN("body")); if (!ts_node_is_null(body)) { // Arrow functions with a single-expression body: body IS the expression // (often a call_expression). Recursing into children skips it. Always // process_node(body) so resolve_call_at fires for that expression. process_node(ctx, body); } ctx->current_scope = saved; return; } // Class / interface declarations are walked by the registry pass; here we just dive // into method_definition bodies for inner call resolution. if (strcmp(kind, "class_declaration") == 0) { // Find the class name to set enclosing_class_qn for methods. TSNode name = ts_node_child_by_field_name(node, "name", TS_LSP_FIELD_LEN("name")); const char *class_qn = NULL; if (!ts_node_is_null(name) && ctx->module_qn) { char *cn = node_text(ctx, name); if (cn) class_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, cn); } TSNode body = ts_node_child_by_field_name(node, "body", TS_LSP_FIELD_LEN("body")); if (!ts_node_is_null(body) && class_qn) { uint32_t nc = ts_node_named_child_count(body); for (uint32_t i = 0; i < nc; i++) { TSNode m = ts_node_named_child(body, i); if (!node_kind_is(m, "method_definition")) continue; TSNode mname = ts_node_child_by_field_name(m, "name", TS_LSP_FIELD_LEN("name")); if (ts_node_is_null(mname)) continue; char *mn = node_text(ctx, mname); if (!mn) continue; const char *mqn = strcmp(mn, "constructor") == 0 ? cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, mn) : cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, mn); process_function_body(ctx, m, mqn, class_qn); } } return; } // Default: recurse into children via a cursor (O(n)). ts_node_child(node,i) // is O(i) in tree-sitter → O(n²) when `node` is a wide block/root. { TSTreeCursor cursor = ts_tree_cursor_new(node); if (ts_tree_cursor_goto_first_child(&cursor)) { do { process_node(ctx, ts_tree_cursor_current_node(&cursor)); } while (ts_tree_cursor_goto_next_sibling(&cursor)); } ts_tree_cursor_delete(&cursor); } } static void process_function_body(TSLSPContext *ctx, TSNode func_node, const char *func_qn, const char *class_qn) { if (ts_node_is_null(func_node)) return; CBMScope *saved_scope = ctx->current_scope; const char *saved_func = ctx->enclosing_func_qn; const char *saved_class = ctx->enclosing_class_qn; ctx->current_scope = cbm_scope_push(ctx->arena, saved_scope); ctx->enclosing_func_qn = func_qn; ctx->enclosing_class_qn = class_qn ? class_qn : saved_class; if (class_qn) { // Bind `this` to the class type for member resolution within methods. cbm_scope_bind(ctx->current_scope, "this", cbm_type_named(ctx->arena, class_qn)); } TSNode params = ts_node_child_by_field_name(func_node, "parameters", TS_LSP_FIELD_LEN("parameters")); if (!ts_node_is_null(params)) { uint32_t nc = ts_node_named_child_count(params); for (uint32_t i = 0; i < nc; i++) bind_parameter(ctx, ts_node_named_child(params, i)); // JSDoc / signature-derived param-type fallback: if bind_parameter left a // param bound to UNKNOWN (no inline TS annotation), use the registered func's // signature param_types[i] (which apply_jsdoc_signatures may have populated). if (func_qn) { const CBMRegisteredFunc *rf = cbm_registry_lookup_func(ctx->registry, func_qn); if (rf && rf->signature && rf->signature->kind == CBM_TYPE_FUNC && rf->signature->data.func.param_names && rf->signature->data.func.param_types) { for (int i = 0; rf->signature->data.func.param_names[i] && rf->signature->data.func.param_types[i]; i++) { const char *pname = rf->signature->data.func.param_names[i]; const CBMType *ptype = rf->signature->data.func.param_types[i]; if (cbm_type_is_unknown(ptype)) continue; const CBMType *existing = cbm_scope_lookup(ctx->current_scope, pname); if (existing && !cbm_type_is_unknown(existing)) continue; cbm_scope_bind(ctx->current_scope, pname, ptype); } } } } TSNode body = ts_node_child_by_field_name(func_node, "body", TS_LSP_FIELD_LEN("body")); if (!ts_node_is_null(body)) { // statement_block: walk every child statement. Expression body (concise // arrow): walk the expression directly — process_node on that handles // call_expression resolution. if (strcmp(ts_node_type(body), "statement_block") == 0) { TSTreeCursor cursor = ts_tree_cursor_new(body); if (ts_tree_cursor_goto_first_child(&cursor)) { do { process_node(ctx, ts_tree_cursor_current_node(&cursor)); } while (ts_tree_cursor_goto_next_sibling(&cursor)); } ts_tree_cursor_delete(&cursor); } else { process_node(ctx, body); } } ctx->enclosing_class_qn = saved_class; ctx->enclosing_func_qn = saved_func; ctx->current_scope = saved_scope; } void ts_lsp_process_file(TSLSPContext *ctx, TSNode root) { if (!ctx || ts_node_is_null(root)) return; if (ctx->dts_mode) return; // Collect top-level children once (O(n)); both passes reuse the array. // See collect_children: indexing ts_node_child(root,i) here would be O(n²). uint32_t kn = 0; TSNode *kids = collect_children(ctx->arena, root, &kn); // Pass 1: bind module-level declarations into root scope. for (uint32_t i = 0; i < kn; i++) { TSNode child = kids[i]; const char *kind = ts_node_type(child); if (strcmp(kind, "lexical_declaration") == 0 || strcmp(kind, "variable_declaration") == 0) { ts_process_statement(ctx, child); } } // Pass 2: process functions, methods, and class bodies. for (uint32_t i = 0; i < kn; i++) { TSNode child = kids[i]; const char *kind = ts_node_type(child); if (strcmp(kind, "function_declaration") == 0) { TSNode name = ts_node_child_by_field_name(child, "name", TS_LSP_FIELD_LEN("name")); if (ts_node_is_null(name) || !ctx->module_qn) continue; char *fn = node_text(ctx, name); if (!fn) continue; const char *fqn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, fn); process_function_body(ctx, child, fqn, NULL); } else if (strcmp(kind, "class_declaration") == 0) { process_node(ctx, child); } else if (strcmp(kind, "export_statement") == 0) { // Walk the wrapped declaration. uint32_t enc = ts_node_named_child_count(child); for (uint32_t j = 0; j < enc; j++) { TSNode inner = ts_node_named_child(child, j); if (ts_node_is_null(inner)) continue; const char *ik = ts_node_type(inner); if (strcmp(ik, "function_declaration") == 0) { TSNode name = ts_node_child_by_field_name(inner, "name", TS_LSP_FIELD_LEN("name")); if (ts_node_is_null(name) || !ctx->module_qn) continue; char *fn = node_text(ctx, name); if (!fn) continue; const char *fqn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, fn); process_function_body(ctx, inner, fqn, NULL); } else if (strcmp(ik, "class_declaration") == 0) { process_node(ctx, inner); } } } // Top-level expression_statement bodies are not graphed as functions; skip. } } // ── Initialization ──────────────────────────────────────────────────────────── void ts_lsp_init(TSLSPContext *ctx, CBMArena *arena, const char *source, int source_len, const CBMTypeRegistry *registry, const char *module_qn, bool js_mode, bool jsx_mode, bool dts_mode, CBMResolvedCallArray *out) { if (!ctx) return; memset(ctx, 0, sizeof(TSLSPContext)); ctx->arena = arena; ctx->source = source; ctx->source_len = source_len; ctx->registry = registry; ctx->module_qn = module_qn; ctx->resolved_calls = out; ctx->js_mode = js_mode; ctx->jsx_mode = jsx_mode; ctx->dts_mode = dts_mode; ctx->current_scope = arena ? cbm_scope_push(arena, NULL) : NULL; const char *debug_env = getenv("CBM_LSP_DEBUG"); ctx->debug = (debug_env && debug_env[0]); } void ts_lsp_add_import(TSLSPContext *ctx, const char *local_name, const char *module_qn) { if (!ctx || !ctx->arena || !local_name || !module_qn) return; int new_count = ctx->import_count + 1; const char **names = (const char **)cbm_arena_alloc(ctx->arena, (size_t)new_count * sizeof(const char *)); const char **qns = (const char **)cbm_arena_alloc(ctx->arena, (size_t)new_count * sizeof(const char *)); if (!names || !qns) return; for (int i = 0; i < ctx->import_count; i++) { names[i] = ctx->import_local_names ? ctx->import_local_names[i] : NULL; qns[i] = ctx->import_module_qns ? ctx->import_module_qns[i] : NULL; } names[ctx->import_count] = cbm_arena_strdup(ctx->arena, local_name); qns[ctx->import_count] = cbm_arena_strdup(ctx->arena, module_qn); ctx->import_local_names = names; ctx->import_module_qns = qns; ctx->import_count = new_count; } // ── Stdlib seeds (Phase 5 will replace with generator) ──────────────────────── static const CBMType *tt_builtin(CBMArena *a, const char *n) { return cbm_type_builtin(a, n); } static const CBMType *tt_param(CBMArena *a, const char *n) { return cbm_type_type_param(a, n); } static void ts_reg_method(CBMTypeRegistry *reg, CBMArena *arena, const char *recv_qn, const char *name, const CBMType *ret_type) { CBMRegisteredFunc rf; memset(&rf, 0, sizeof(rf)); rf.qualified_name = cbm_arena_sprintf(arena, "%s.%s", recv_qn, name); rf.short_name = cbm_arena_strdup(arena, name); rf.receiver_type = cbm_arena_strdup(arena, recv_qn); rf.min_params = -1; const CBMType *rets[2] = {ret_type ? ret_type : cbm_type_unknown(), NULL}; rf.signature = cbm_type_func(arena, NULL, NULL, rets); cbm_registry_add_func(reg, rf); } static void reg_type_with_methods(CBMTypeRegistry *reg, CBMArena *arena, const char *qn, const char *short_name, const char **type_params, const char **method_names, const CBMType **method_returns) { CBMRegisteredType rt; memset(&rt, 0, sizeof(rt)); rt.qualified_name = cbm_arena_strdup(arena, qn); rt.short_name = cbm_arena_strdup(arena, short_name); rt.type_param_names = type_params; if (method_names) { int count = 0; while (method_names[count]) count++; const char **mqns = (const char **)cbm_arena_alloc(arena, (size_t)(count + 1) * sizeof(const char *)); const char **mnames_copy = (const char **)cbm_arena_alloc(arena, (size_t)(count + 1) * sizeof(const char *)); if (mqns && mnames_copy) { for (int i = 0; i < count; i++) { mnames_copy[i] = cbm_arena_strdup(arena, method_names[i]); mqns[i] = cbm_arena_sprintf(arena, "%s.%s", qn, method_names[i]); } mnames_copy[count] = NULL; mqns[count] = NULL; rt.method_names = mnames_copy; rt.method_qns = mqns; } } cbm_registry_add_type(reg, rt); if (method_names && method_returns) { for (int i = 0; method_names[i]; i++) { ts_reg_method(reg, arena, qn, method_names[i], method_returns[i]); } } } void cbm_ts_stdlib_register(CBMTypeRegistry *reg, CBMArena *arena) { if (!reg || !arena) return; const CBMType *t_string = tt_builtin(arena, "string"); const CBMType *t_number = tt_builtin(arena, "number"); const CBMType *t_boolean = tt_builtin(arena, "boolean"); const CBMType *t_void = tt_builtin(arena, "void"); const CBMType *t_unknown = cbm_type_unknown(); // Array: minimal method surface for v1. { const CBMType *tparam_T = tt_param(arena, "T"); const CBMType *tparam_U = tt_param(arena, "U"); const char **tparams = (const char **)cbm_arena_alloc(arena, 2 * sizeof(const char *)); tparams[0] = "T"; tparams[1] = NULL; static const char *methods[] = { "push", "pop", "shift", "unshift", "slice", "splice", "concat", "join", "indexOf", "includes", "forEach", "map", "filter", "find", "findIndex", "reduce", "some", "every", "sort", "reverse", "at", "flat", "flatMap", NULL, }; const CBMType *returns[24]; const CBMType *t_T = tparam_T; const CBMType *t_arr_T_args[2] = {t_T, NULL}; const CBMType *t_arr_T = cbm_type_template(arena, "Array", t_arr_T_args, 1); returns[0] = t_number; // push returns[1] = t_T; // pop returns[2] = t_T; // shift returns[3] = t_number; // unshift returns[4] = t_arr_T; // slice returns[5] = t_arr_T; // splice returns[6] = t_arr_T; // concat returns[7] = t_string; // join returns[8] = t_number; // indexOf returns[9] = t_boolean; // includes returns[10] = t_void; // forEach returns[11] = cbm_type_template(arena, "Array", (const CBMType *[]){tparam_U, NULL}, 1); // map returns[12] = t_arr_T; // filter returns[13] = t_T; // find returns[14] = t_number; // findIndex returns[15] = tparam_U; // reduce returns[16] = t_boolean; // some returns[17] = t_boolean; // every returns[18] = t_arr_T; // sort returns[19] = t_arr_T; // reverse returns[20] = t_T; // at returns[21] = t_arr_T; // flat returns[22] = t_arr_T; // flatMap returns[23] = NULL; reg_type_with_methods(reg, arena, "Array", "Array", tparams, methods, returns); // Patch the registered Array.{forEach,map,filter,find,findIndex,some,every, // flatMap,sort,reduce} signatures with a typed callback (x: T) => U so that // contextual typing can bind the callback's first param to T inside the // closure body. We rebuild rf.signature carrying through the existing return // type. This unlocks `arr.map(x => x.length)`-style resolution. const char *p_names[] = {"callback", NULL}; struct { const char *method; const CBMType *ret; const CBMType *cb_param0; const CBMType *cb_ret; } cb_specs[] = { {"forEach", t_void, t_T, t_void}, {"map", returns[11], t_T, tparam_U}, {"filter", t_arr_T, t_T, t_boolean}, {"find", t_T, t_T, t_boolean}, {"findIndex", t_number, t_T, t_boolean}, {"some", t_boolean, t_T, t_boolean}, {"every", t_boolean, t_T, t_boolean}, {"flatMap", t_arr_T, t_T, tparam_U}, {"sort", t_arr_T, t_T, t_number}, {"reduce", tparam_U, t_T, tparam_U}, {NULL, NULL, NULL, NULL}, }; for (int ci = 0; cb_specs[ci].method; ci++) { // Build callback FUNC type: (x: T) => returnType const CBMType *cb_param_types[2] = {cb_specs[ci].cb_param0, NULL}; const CBMType *cb_returns[2] = {cb_specs[ci].cb_ret, NULL}; const char *cb_param_names[2] = {"x", NULL}; const CBMType *cb_func = cbm_type_func(arena, cb_param_names, cb_param_types, cb_returns); // Rebuild Array.'s signature with this callback as param 0. const CBMType *outer_param_types[2] = {cb_func, NULL}; const CBMType *outer_returns[2] = {cb_specs[ci].ret, NULL}; const CBMType *new_sig = cbm_type_func(arena, p_names, outer_param_types, outer_returns); // Find Array. in the registry and patch its signature. char *qn = cbm_arena_sprintf(arena, "Array.%s", cb_specs[ci].method); for (int fi = 0; fi < reg->func_count; fi++) { if (reg->funcs[fi].qualified_name && strcmp(reg->funcs[fi].qualified_name, qn) == 0) { reg->funcs[fi].signature = new_sig; break; } } } } // Promise { const CBMType *tparam_T = tt_param(arena, "T"); const char **tparams = (const char **)cbm_arena_alloc(arena, 2 * sizeof(const char *)); tparams[0] = "T"; tparams[1] = NULL; static const char *methods[] = {"then", "catch", "finally", NULL}; const CBMType *returns[4]; const CBMType *t_promise_T_args[2] = {tparam_T, NULL}; const CBMType *t_promise_T = cbm_type_template(arena, "Promise", t_promise_T_args, 1); returns[0] = t_promise_T; returns[1] = t_promise_T; returns[2] = t_promise_T; returns[3] = NULL; reg_type_with_methods(reg, arena, "Promise", "Promise", tparams, methods, returns); } // Map { const char **tparams = (const char **)cbm_arena_alloc(arena, 3 * sizeof(const char *)); tparams[0] = "K"; tparams[1] = "V"; tparams[2] = NULL; static const char *methods[] = {"get", "set", "has", "delete", "clear", "forEach", NULL}; const CBMType *returns[7] = { tt_param(arena, "V"), cbm_type_template(arena, "Map", (const CBMType *[]){tt_param(arena, "K"), tt_param(arena, "V"), NULL}, 2), t_boolean, t_boolean, t_void, t_void, NULL, }; reg_type_with_methods(reg, arena, "Map", "Map", tparams, methods, returns); } // Set { const char **tparams = (const char **)cbm_arena_alloc(arena, 2 * sizeof(const char *)); tparams[0] = "T"; tparams[1] = NULL; static const char *methods[] = {"add", "has", "delete", "clear", "forEach", NULL}; const CBMType *returns[6] = { cbm_type_template(arena, "Set", (const CBMType *[]){tt_param(arena, "T"), NULL}, 1), t_boolean, t_boolean, t_void, t_void, NULL, }; reg_type_with_methods(reg, arena, "Set", "Set", tparams, methods, returns); } // Iterator / Iterable { const CBMType *tparam_T = tt_param(arena, "T"); const char **tparams = (const char **)cbm_arena_alloc(arena, 2 * sizeof(const char *)); tparams[0] = "T"; tparams[1] = NULL; // IteratorResult is loose — we fold it to T-or-undefined via the iterator method's // return. static const char *iter_methods[] = {"next", "return", "throw", NULL}; const CBMType *iter_returns[4] = {tparam_T, tparam_T, tparam_T, NULL}; reg_type_with_methods(reg, arena, "Iterator", "Iterator", tparams, iter_methods, iter_returns); const CBMType *iter_t = cbm_type_template(arena, "Iterator", (const CBMType *[]){tparam_T, NULL}, 1); static const char *iterable_methods[] = {NULL}; // [Symbol.iterator] not modeled by name const CBMType *iterable_returns[1] = {NULL}; (void)iter_t; reg_type_with_methods(reg, arena, "Iterable", "Iterable", tparams, iterable_methods, iterable_returns); } // AsyncIterator / AsyncIterable — methods return Promise { const CBMType *tparam_T = tt_param(arena, "T"); const char **tparams = (const char **)cbm_arena_alloc(arena, 2 * sizeof(const char *)); tparams[0] = "T"; tparams[1] = NULL; const CBMType *prom_T = cbm_type_template(arena, "Promise", (const CBMType *[]){tparam_T, NULL}, 1); static const char *methods[] = {"next", "return", "throw", NULL}; const CBMType *returns[4] = {prom_T, prom_T, prom_T, NULL}; reg_type_with_methods(reg, arena, "AsyncIterator", "AsyncIterator", tparams, methods, returns); reg_type_with_methods(reg, arena, "AsyncIterable", "AsyncIterable", tparams, methods, returns); } // Generator — extends Iterator with yield-based methods. { const CBMType *tparam_T = tt_param(arena, "T"); const char **tparams = (const char **)cbm_arena_alloc(arena, 2 * sizeof(const char *)); tparams[0] = "T"; tparams[1] = NULL; static const char *methods[] = {"next", "return", "throw", NULL}; const CBMType *returns[4] = {tparam_T, tparam_T, tparam_T, NULL}; reg_type_with_methods(reg, arena, "Generator", "Generator", tparams, methods, returns); reg_type_with_methods(reg, arena, "AsyncGenerator", "AsyncGenerator", tparams, methods, returns); } // String prototype methods (most common). { static const char *methods[] = { "charAt", "charCodeAt", "concat", "includes", "indexOf", "lastIndexOf", "padStart", "padEnd", "repeat", "replace", "slice", "split", "startsWith", "endsWith", "substring", "toLowerCase", "toUpperCase", "trim", "trimStart", "trimEnd", "match", "matchAll", "normalize", "at", NULL, }; // Simplified: most string methods return string or boolean — fold to string for v1. int mc = 0; while (methods[mc]) mc++; const CBMType **returns = (const CBMType **)cbm_arena_alloc(arena, (size_t)(mc + 1) * sizeof(const CBMType *)); for (int i = 0; i < mc; i++) { // booleans: includes, startsWith, endsWith const char *m = methods[i]; if (strcmp(m, "includes") == 0 || strcmp(m, "startsWith") == 0 || strcmp(m, "endsWith") == 0) { returns[i] = t_boolean; } else if (strcmp(m, "indexOf") == 0 || strcmp(m, "lastIndexOf") == 0 || strcmp(m, "charCodeAt") == 0) { returns[i] = t_number; } else if (strcmp(m, "split") == 0) { returns[i] = cbm_type_template(arena, "Array", (const CBMType *[]){t_string, NULL}, 1); } else { returns[i] = t_string; } } returns[mc] = NULL; reg_type_with_methods(reg, arena, "String", "String", NULL, methods, returns); } // console — used so frequently that resolving `console.log` directly is high value. { static const char *methods[] = {"log", "error", "warn", "info", "debug", NULL}; const CBMType *returns[6] = {t_void, t_void, t_void, t_void, t_void, NULL}; reg_type_with_methods(reg, arena, "console", "console", NULL, methods, returns); } // JSON namespace. { static const char *methods[] = {"parse", "stringify", NULL}; const CBMType *returns[3] = {t_unknown, t_string, NULL}; reg_type_with_methods(reg, arena, "JSON", "JSON", NULL, methods, returns); } // Object — constructor namespace methods that are commonly chained. { static const char *methods[] = {"keys", "values", "entries", "assign", "freeze", "isFrozen", "create", "getPrototypeOf", NULL}; const CBMType *returns[9] = { cbm_type_template(arena, "Array", (const CBMType *[]){t_string, NULL}, 1), cbm_type_template(arena, "Array", (const CBMType *[]){t_unknown, NULL}, 1), cbm_type_template(arena, "Array", (const CBMType *[]){t_unknown, NULL}, 1), t_unknown, t_unknown, t_boolean, t_unknown, t_unknown, NULL, }; reg_type_with_methods(reg, arena, "Object", "Object", NULL, methods, returns); } // Number wrapper class (so `(42).toString()` and similar resolve). { static const char *methods[] = { "toString", "toFixed", "toExponential", "toPrecision", "valueOf", NULL, }; const CBMType *returns[6] = {t_string, t_string, t_string, t_string, t_number, NULL}; reg_type_with_methods(reg, arena, "Number", "Number", NULL, methods, returns); } // Boolean wrapper class. { static const char *methods[] = {"toString", "valueOf", NULL}; const CBMType *returns[3] = {t_string, t_boolean, NULL}; reg_type_with_methods(reg, arena, "Boolean", "Boolean", NULL, methods, returns); } // Date wrapper class — common date operations. { static const char *methods[] = { "toISOString", "toString", "toJSON", "toDateString", "toTimeString", "getTime", "valueOf", "getFullYear", "getMonth", "getDate", "getDay", "getHours", "getMinutes", "getSeconds", "getMilliseconds", "setFullYear", "setMonth", "setDate", "setHours", "setMinutes", "setSeconds", NULL, }; int mc = 0; while (methods[mc]) mc++; const CBMType **returns = (const CBMType **)cbm_arena_alloc(arena, (size_t)(mc + 1) * sizeof(const CBMType *)); for (int i = 0; i < mc; i++) { const char *m = methods[i]; if (strcmp(m, "toISOString") == 0 || strcmp(m, "toString") == 0 || strcmp(m, "toJSON") == 0 || strcmp(m, "toDateString") == 0 || strcmp(m, "toTimeString") == 0) { returns[i] = t_string; } else { returns[i] = t_number; } } returns[mc] = NULL; reg_type_with_methods(reg, arena, "Date", "Date", NULL, methods, returns); } // RegExp wrapper class. { static const char *methods[] = {"test", "exec", "toString", "compile", NULL}; const CBMType *returns[5] = {t_boolean, t_unknown, t_string, t_unknown, NULL}; reg_type_with_methods(reg, arena, "RegExp", "RegExp", NULL, methods, returns); } // Error class — used heavily in `throw new Error(...)` and catch handlers. { static const char *methods[] = {"toString", NULL}; const CBMType *returns[2] = {t_string, NULL}; reg_type_with_methods(reg, arena, "Error", "Error", NULL, methods, returns); } // Math namespace — static math operations. { static const char *methods[] = { "floor", "ceil", "round", "trunc", "abs", "sqrt", "cbrt", "pow", "exp", "log", "log2", "log10", "sin", "cos", "tan", "asin", "acos", "atan", "atan2", "min", "max", "random", "hypot", "sign", NULL, }; int mc = 0; while (methods[mc]) mc++; const CBMType **returns = (const CBMType **)cbm_arena_alloc(arena, (size_t)(mc + 1) * sizeof(const CBMType *)); for (int i = 0; i < mc; i++) returns[i] = t_number; returns[mc] = NULL; reg_type_with_methods(reg, arena, "Math", "Math", NULL, methods, returns); } // Static methods that don't fit reg_type_with_methods cleanly: // Promise.resolve(x): Promise — generic in T derived from arg type // Promise.all(promises): Promise // Promise.reject(reason): Promise // Array.from(iter): Array // Array.isArray(x): boolean // Array.of(...items): Array { const CBMType *tparam_T = tt_param(arena, "T"); const CBMType *t_T = tparam_T; const CBMType *prom_T = cbm_type_template(arena, "Promise", (const CBMType *[]){t_T, NULL}, 1); const CBMType *arr_T = cbm_type_template(arena, "Array", (const CBMType *[]){t_T, NULL}, 1); const CBMType *prom_arr_T = cbm_type_template(arena, "Promise", (const CBMType *[]){arr_T, NULL}, 1); ts_reg_method(reg, arena, "Promise", "resolve", prom_T); ts_reg_method(reg, arena, "Promise", "all", prom_arr_T); ts_reg_method(reg, arena, "Promise", "reject", prom_T); ts_reg_method(reg, arena, "Promise", "race", prom_T); ts_reg_method(reg, arena, "Promise", "allSettled", prom_arr_T); ts_reg_method(reg, arena, "Array", "from", arr_T); ts_reg_method(reg, arena, "Array", "isArray", t_boolean); ts_reg_method(reg, arena, "Array", "of", arr_T); } // ── DOM essentials ─────────────────────────────────────────────────────── // Element + Document + Window + Event + Response cover the most-used DOM // surface for browser TS code. Hand-curated subset; full lib.dom.d.ts would // be Phase 5 (generator). { // EventTarget base static const char *et_methods[] = { "addEventListener", "removeEventListener", "dispatchEvent", NULL, }; const CBMType *et_returns[4] = {t_void, t_void, t_boolean, NULL}; reg_type_with_methods(reg, arena, "EventTarget", "EventTarget", NULL, et_methods, et_returns); // Node static const char *node_methods[] = { "appendChild", "removeChild", "replaceChild", "cloneNode", "contains", "hasChildNodes", "addEventListener", "removeEventListener", "dispatchEvent", NULL, }; const CBMType *node_t = cbm_type_named(arena, "Node"); const CBMType *node_returns[10] = { node_t, node_t, node_t, node_t, t_boolean, t_boolean, t_void, t_void, t_boolean, NULL, }; reg_type_with_methods(reg, arena, "Node", "Node", NULL, node_methods, node_returns); // Element static const char *el_methods[] = { "getAttribute", "setAttribute", "removeAttribute", "hasAttribute", "querySelector", "querySelectorAll", "closest", "matches", "appendChild", "removeChild", "replaceChild", "addEventListener", "removeEventListener", "dispatchEvent", "getBoundingClientRect", "scrollIntoView", "focus", "blur", "click", "remove", NULL, }; const CBMType *el_t = cbm_type_named(arena, "Element"); const CBMType *nodelist_t = cbm_type_template(arena, "NodeList", (const CBMType *[]){el_t, NULL}, 1); const CBMType *el_returns[21] = { t_string, t_void, t_void, t_boolean, el_t, nodelist_t, el_t, t_boolean, el_t, el_t, el_t, t_void, t_void, t_boolean, t_unknown, t_void, t_void, t_void, t_void, t_void, NULL, }; reg_type_with_methods(reg, arena, "Element", "Element", NULL, el_methods, el_returns); // HTMLElement (extends Element) static const char *html_methods[] = { "getAttribute", "setAttribute", "querySelector", "querySelectorAll", "closest", "addEventListener", "removeEventListener", "click", "focus", "blur", "remove", NULL, }; const CBMType *html_t = cbm_type_named(arena, "HTMLElement"); const CBMType *html_returns[12] = { t_string, t_void, html_t, cbm_type_template(arena, "NodeList", (const CBMType *[]){html_t, NULL}, 1), html_t, t_void, t_void, t_void, t_void, t_void, t_void, NULL, }; reg_type_with_methods(reg, arena, "HTMLElement", "HTMLElement", NULL, html_methods, html_returns); // Document static const char *doc_methods[] = { "getElementById", "querySelector", "querySelectorAll", "getElementsByClassName", "getElementsByTagName", "createElement", "createTextNode", "createDocumentFragment", "addEventListener", "removeEventListener", NULL, }; const CBMType *doc_returns[11] = { html_t, html_t, cbm_type_template(arena, "NodeList", (const CBMType *[]){html_t, NULL}, 1), cbm_type_template(arena, "HTMLCollection", (const CBMType *[]){html_t, NULL}, 1), cbm_type_template(arena, "HTMLCollection", (const CBMType *[]){html_t, NULL}, 1), html_t, node_t, node_t, t_void, t_void, NULL, }; reg_type_with_methods(reg, arena, "Document", "Document", NULL, doc_methods, doc_returns); // Event static const char *event_methods[] = { "preventDefault", "stopPropagation", "stopImmediatePropagation", "composedPath", NULL, }; const CBMType *event_returns[5] = { t_void, t_void, t_void, cbm_type_template( arena, "Array", (const CBMType *[]){ et_methods[0] ? cbm_type_named(arena, "EventTarget") : t_unknown, NULL}, 1), NULL, }; reg_type_with_methods(reg, arena, "Event", "Event", NULL, event_methods, event_returns); // Response (fetch API) static const char *resp_methods[] = { "json", "text", "blob", "arrayBuffer", "formData", "clone", NULL, }; const CBMType *resp_returns[7] = { cbm_type_template(arena, "Promise", (const CBMType *[]){t_unknown, NULL}, 1), cbm_type_template(arena, "Promise", (const CBMType *[]){t_string, NULL}, 1), cbm_type_template(arena, "Promise", (const CBMType *[]){t_unknown, NULL}, 1), cbm_type_template(arena, "Promise", (const CBMType *[]){t_unknown, NULL}, 1), cbm_type_template(arena, "Promise", (const CBMType *[]){t_unknown, NULL}, 1), cbm_type_named(arena, "Response"), NULL, }; reg_type_with_methods(reg, arena, "Response", "Response", NULL, resp_methods, resp_returns); // Window static const char *win_methods[] = { "addEventListener", "removeEventListener", "alert", "confirm", "prompt", "fetch", "setTimeout", "clearTimeout", "setInterval", "clearInterval", "requestAnimationFrame", "cancelAnimationFrame", NULL, }; const CBMType *win_returns[13] = { t_void, t_void, t_void, t_boolean, t_string, cbm_type_template(arena, "Promise", (const CBMType *[]){cbm_type_named(arena, "Response"), NULL}, 1), t_number, t_void, t_number, t_void, t_number, t_void, NULL, }; reg_type_with_methods(reg, arena, "Window", "Window", NULL, win_methods, win_returns); } } // ── Single-file entry point ─────────────────────────────────────────────────── // Build a registry from result->defs: register Class/Interface as types; Function/Method // as funcs. Walks AST briefly for class fields/extends. static void register_file_defs(CBMArena *arena, CBMTypeRegistry *reg, CBMFileResult *result, const char *module_qn) { 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 (strcmp(d->label, "Class") == 0 || strcmp(d->label, "Interface") == 0) { CBMRegisteredType rt; memset(&rt, 0, sizeof(rt)); rt.qualified_name = d->qualified_name; rt.short_name = d->name; rt.is_interface = (strcmp(d->label, "Interface") == 0); // base_classes → embedded_types (extends/implements). if (d->base_classes) { int bc = 0; 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 *)); if (emb) { for (int j = 0; j < bc; j++) { const char *b = d->base_classes[j]; // For TS, extract_defs returns base text like "extends Animal" // or "extends Foo" (raw class_heritage node text). Strip // the keyword and pull the first identifier. if (b) { while (*b == ' ' || *b == '\t' || *b == '\n') b++; if (strncmp(b, "extends", 7) == 0 && (b[7] == ' ' || b[7] == '\t')) { b += 7; } if (strncmp(b, "implements", 10) == 0 && (b[10] == ' ' || b[10] == '\t')) { b += 10; } while (*b == ' ' || *b == '\t' || *b == '\n') b++; size_t bl = 0; while (b[bl] && b[bl] != ' ' && b[bl] != ',' && b[bl] != '<' && b[bl] != '(' && b[bl] != '\n' && b[bl] != '\t') bl++; if (bl == 0) { emb[j] = NULL; continue; } char *base_name = cbm_arena_strndup(arena, b, bl); if (strchr(base_name, '.') == NULL) { emb[j] = cbm_arena_sprintf(arena, "%s.%s", module_qn, base_name); } else { emb[j] = base_name; } } else { emb[j] = NULL; } } emb[bc] = NULL; rt.embedded_types = emb; } } } cbm_registry_add_type(reg, rt); } else if (strcmp(d->label, "Function") == 0 || strcmp(d->label, "Method") == 0) { CBMRegisteredFunc rf; memset(&rf, 0, sizeof(rf)); rf.qualified_name = d->qualified_name; rf.short_name = d->name; rf.min_params = -1; // Build return type list. // // For TS, prefer the RAW d->return_type text over d->return_types[]: the // latter is post-cleaned via extract_defs.c::clean_type_name which truncates // at the first '<' (so `Promise` becomes `Promise`, losing the // generic args). The raw text preserves generics; parse_ts_type_text strips // the leading `:` annotation prefix. const CBMType **rets = NULL; if (d->return_type && d->return_type[0]) { rets = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(const CBMType *)); if (rets) { rets[0] = parse_ts_type_text(arena, d->return_type, module_qn); rets[1] = NULL; } } else if (d->return_types) { int n = 0; while (d->return_types[n]) n++; if (n > 0) { rets = (const CBMType **)cbm_arena_alloc(arena, (size_t)(n + 1) * sizeof(const CBMType *)); if (rets) { for (int j = 0; j < n; j++) { rets[j] = parse_ts_type_text(arena, d->return_types[j], module_qn); } rets[n] = NULL; } } } const CBMType **params_t = parse_param_types_array(arena, d->param_types, module_qn); rf.signature = cbm_type_func(arena, d->param_names, params_t, rets); // Methods: deduce receiver from QN ("module.Class.method" → receiver "module.Class"). if (strcmp(d->label, "Method") == 0) { const char *dot = strrchr(d->qualified_name, '.'); if (dot && dot != d->qualified_name) { char *recv = cbm_arena_strndup(arena, d->qualified_name, (size_t)(dot - d->qualified_name)); rf.receiver_type = recv; // Ensure the receiver type is registered (auto-create if missing). const CBMRegisteredType *existing = cbm_registry_lookup_type(reg, recv); if (!existing) { CBMRegisteredType auto_rt; memset(&auto_rt, 0, sizeof(auto_rt)); auto_rt.qualified_name = recv; const char *short_name = strrchr(recv, '.'); auto_rt.short_name = short_name ? short_name + 1 : recv; cbm_registry_add_type(reg, auto_rt); } } } cbm_registry_add_func(reg, rf); } } // Second pass: attach method_names / method_qns to each registered type from // the registered methods. Lets dispatch find them via lookup_member_type. for (int ti = 0; ti < reg->type_count; ti++) { CBMRegisteredType *rt = ®->types[ti]; if (!rt->qualified_name) continue; // Count methods first. int mcount = 0; for (int fi = 0; fi < reg->func_count; fi++) { const CBMRegisteredFunc *f = ®->funcs[fi]; if (f->receiver_type && strcmp(f->receiver_type, rt->qualified_name) == 0) { mcount++; } } if (mcount == 0) continue; const char **mnames = (const char **)cbm_arena_alloc(arena, (size_t)(mcount + 1) * sizeof(const char *)); const char **mqns = (const char **)cbm_arena_alloc(arena, (size_t)(mcount + 1) * sizeof(const char *)); if (!mnames || !mqns) continue; int idx = 0; for (int fi = 0; fi < reg->func_count && idx < mcount; fi++) { const CBMRegisteredFunc *f = ®->funcs[fi]; if (f->receiver_type && strcmp(f->receiver_type, rt->qualified_name) == 0) { mnames[idx] = f->short_name; mqns[idx] = f->qualified_name; idx++; } } mnames[mcount] = NULL; mqns[mcount] = NULL; rt->method_names = mnames; rt->method_qns = mqns; } } // Find the first `return EXPR` inside a body and evaluate EXPR's type. Returns // UNKNOWN if the body has no return or every return is unresolvable. static const CBMType *infer_return_type_from_body(TSLSPContext *ctx, TSNode body) { if (ts_node_is_null(body)) return cbm_type_unknown(); // Iterative DFS using a small fixed-size stack to avoid C-stack blowup on big bodies. enum { STACK_CAP = 256 }; TSNode stack[STACK_CAP]; int top = 0; stack[top++] = body; while (top > 0) { TSNode n = stack[--top]; if (ts_node_is_null(n)) continue; const char *k = ts_node_type(n); if (strcmp(k, "return_statement") == 0) { // The first named child is the return expression (if any). uint32_t rnc = ts_node_named_child_count(n); if (rnc == 0) continue; TSNode expr = ts_node_named_child(n, 0); if (ts_node_is_null(expr)) continue; const CBMType *t = ts_eval_expr_type(ctx, expr); if (t && !cbm_type_is_unknown(t)) return t; continue; } // Don't recurse into nested function bodies — their returns belong to them. if (strcmp(k, "function_declaration") == 0 || strcmp(k, "function_expression") == 0 || strcmp(k, "arrow_function") == 0 || strcmp(k, "method_definition") == 0) continue; uint32_t cnt = ts_node_child_count(n); for (uint32_t i = 0; i < cnt && top < STACK_CAP; i++) { stack[top++] = ts_node_child(n, i); } } return cbm_type_unknown(); } // Rebuild a function's CBMRegisteredFunc signature from the AST directly. extract_defs // only handles type_identifier / generic_type / predefined_type for params, so function // types (`(x: Foo) => void`), object types, tuple types etc. are missing from // def->param_types. This pass walks the AST and rebuilds signatures from scratch. // // Called after `register_file_defs` so the func entries already exist; we only mutate // in place. static void rebuild_signatures_from_ast(TSLSPContext *ctx, TSNode root, CBMTypeRegistry *reg) { if (ts_node_is_null(root) || !reg || !ctx->module_qn) return; enum { STACK_CAP = 256 }; TSNode stack[STACK_CAP]; int top = 0; uint32_t nc = ts_node_child_count(root); for (uint32_t i = 0; i < nc && top < STACK_CAP; i++) stack[top++] = ts_node_child(root, i); while (top > 0) { TSNode n = stack[--top]; if (ts_node_is_null(n)) continue; const char *k = ts_node_type(n); // Recurse into export_statement and class bodies. if (strcmp(k, "export_statement") == 0 || strcmp(k, "class_body") == 0) { uint32_t cnt = ts_node_child_count(n); for (uint32_t i = 0; i < cnt && top < STACK_CAP; i++) { stack[top++] = ts_node_child(n, i); } continue; } if (strcmp(k, "class_declaration") == 0) { TSNode body = ts_node_child_by_field_name(n, "body", TS_LSP_FIELD_LEN("body")); if (!ts_node_is_null(body) && top < STACK_CAP) stack[top++] = body; continue; } bool is_func = (strcmp(k, "function_declaration") == 0); bool is_method = (strcmp(k, "method_definition") == 0); if (!is_func && !is_method) continue; TSNode name_node = ts_node_child_by_field_name(n, "name", TS_LSP_FIELD_LEN("name")); if (ts_node_is_null(name_node)) continue; char *fname = node_text(ctx, name_node); if (!fname) continue; // Resolve this declaration's FQN to find its registered func. const char *fqn = NULL; if (is_func) { fqn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, fname); } else { // Method: climb to enclosing class_declaration. TSNode parent = ts_node_parent(n); while (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "class_declaration") != 0) { parent = ts_node_parent(parent); } if (ts_node_is_null(parent)) continue; TSNode cn = ts_node_child_by_field_name(parent, "name", TS_LSP_FIELD_LEN("name")); if (ts_node_is_null(cn)) continue; char *cname = node_text(ctx, cn); if (!cname) continue; fqn = cbm_arena_sprintf(ctx->arena, "%s.%s.%s", ctx->module_qn, cname, fname); } // Rebuild parameters from AST. TSNode params = ts_node_child_by_field_name(n, "parameters", TS_LSP_FIELD_LEN("parameters")); const char *p_names[32] = {0}; const CBMType *p_types[32] = {0}; int pc = 0; if (!ts_node_is_null(params)) { uint32_t pnc = ts_node_named_child_count(params); for (uint32_t i = 0; i < pnc && pc < 31; i++) { TSNode p = ts_node_named_child(params, i); if (ts_node_is_null(p)) continue; const char *pk = ts_node_type(p); if (strcmp(pk, "required_parameter") != 0 && strcmp(pk, "optional_parameter") != 0) continue; // Param name. TSNode pat = ts_node_child_by_field_name(p, "pattern", TS_LSP_FIELD_LEN("pattern")); if (ts_node_is_null(pat)) { pat = ts_node_child_by_field_name(p, "name", TS_LSP_FIELD_LEN("name")); } if (ts_node_is_null(pat) || strcmp(ts_node_type(pat), "identifier") != 0) continue; char *pn = node_text(ctx, pat); if (!pn) continue; // Param type. const CBMType *pt = cbm_type_unknown(); TSNode tann = ts_node_child_by_field_name(p, "type", TS_LSP_FIELD_LEN("type")); if (ts_node_is_null(tann)) { // Fall back to kind-based search. uint32_t pcnt = ts_node_named_child_count(p); for (uint32_t j = 0; j < pcnt; j++) { TSNode c = ts_node_named_child(p, j); if (!ts_node_is_null(c) && strcmp(ts_node_type(c), "type_annotation") == 0) { tann = c; break; } } } if (!ts_node_is_null(tann)) { TSNode tch = (strcmp(ts_node_type(tann), "type_annotation") == 0) ? ts_node_named_child(tann, 0) : tann; if (!ts_node_is_null(tch)) pt = ts_parse_type_node(ctx, tch); } p_names[pc] = pn; p_types[pc] = pt; pc++; } } p_names[pc] = NULL; p_types[pc] = NULL; // Rebuild return type from AST. const CBMType *ret = cbm_type_unknown(); TSNode rt_node = ts_node_child_by_field_name(n, "return_type", TS_LSP_FIELD_LEN("return_type")); if (!ts_node_is_null(rt_node)) { TSNode tch = (strcmp(ts_node_type(rt_node), "type_annotation") == 0) ? ts_node_named_child(rt_node, 0) : rt_node; if (!ts_node_is_null(tch)) ret = ts_parse_type_node(ctx, tch); } // Build new signature and patch in registry. const CBMType *rets[2] = {ret, NULL}; const CBMType *new_sig = cbm_type_func(ctx->arena, p_names, p_types, rets); for (int fi = 0; fi < reg->func_count; fi++) { if (!reg->funcs[fi].qualified_name) continue; if (strcmp(reg->funcs[fi].qualified_name, fqn) != 0) continue; // Preserve the ret type if AST gave UNKNOWN but the existing reg sig had one. if (cbm_type_is_unknown(ret) && reg->funcs[fi].signature && reg->funcs[fi].signature->kind == CBM_TYPE_FUNC && reg->funcs[fi].signature->data.func.return_types && reg->funcs[fi].signature->data.func.return_types[0] && !cbm_type_is_unknown(reg->funcs[fi].signature->data.func.return_types[0])) { const CBMType *preserved[2] = {reg->funcs[fi].signature->data.func.return_types[0], NULL}; new_sig = cbm_type_func(ctx->arena, p_names, p_types, preserved); } reg->funcs[fi].signature = new_sig; break; } } } // Convert NAMED references to TYPE_PARAM for any function whose declaration has a // `type_parameters` block. Without this, generic functions like `function id(x: T): T` // register T as NAMED("module.T") instead of TYPE_PARAM("T"), defeating substitution. static void convert_signature_type_params(TSLSPContext *ctx, TSNode root, CBMTypeRegistry *reg) { if (ts_node_is_null(root) || !reg || !ctx->module_qn) return; // Walk: function_declaration, class_declaration { method_definition }, plus exported. enum { STACK_CAP = 256 }; TSNode stack[STACK_CAP]; int top = 0; uint32_t nc = ts_node_child_count(root); for (uint32_t i = 0; i < nc && top < STACK_CAP; i++) stack[top++] = ts_node_child(root, i); while (top > 0) { TSNode n = stack[--top]; if (ts_node_is_null(n)) continue; const char *k = ts_node_type(n); // Recurse into export_statement and class_body. if (strcmp(k, "export_statement") == 0 || strcmp(k, "class_body") == 0) { uint32_t cnt = ts_node_child_count(n); for (uint32_t i = 0; i < cnt && top < STACK_CAP; i++) { stack[top++] = ts_node_child(n, i); } continue; } if (strcmp(k, "class_declaration") == 0) { TSNode body = ts_node_child_by_field_name(n, "body", TS_LSP_FIELD_LEN("body")); if (!ts_node_is_null(body) && top < STACK_CAP) stack[top++] = body; continue; } bool is_func = (strcmp(k, "function_declaration") == 0); bool is_method = (strcmp(k, "method_definition") == 0); if (!is_func && !is_method) continue; TSNode tparams = ts_node_child_by_field_name(n, "type_parameters", TS_LSP_FIELD_LEN("type_parameters")); if (ts_node_is_null(tparams)) continue; // Collect type param names. const char *names[16] = {0}; const CBMType *args[16] = {0}; int tpc = 0; uint32_t tnc = ts_node_named_child_count(tparams); for (uint32_t i = 0; i < tnc && tpc < 15; i++) { TSNode tp = ts_node_named_child(tparams, i); if (ts_node_is_null(tp)) continue; // type_parameter has child name (type_identifier or identifier). TSNode tname = ts_node_child_by_field_name(tp, "name", TS_LSP_FIELD_LEN("name")); if (ts_node_is_null(tname)) { // First named child fallback. if (ts_node_named_child_count(tp) > 0) { tname = ts_node_named_child(tp, 0); } } if (ts_node_is_null(tname)) continue; char *nm = node_text(ctx, tname); if (!nm) continue; names[tpc] = nm; args[tpc] = cbm_type_type_param(ctx->arena, nm); tpc++; } names[tpc] = NULL; args[tpc] = NULL; if (tpc == 0) continue; // Resolve the function's QN. TSNode name_node = ts_node_child_by_field_name(n, "name", TS_LSP_FIELD_LEN("name")); if (ts_node_is_null(name_node)) continue; char *fname = node_text(ctx, name_node); if (!fname) continue; // For methods, the receiver class must be in QN. For free functions, just module. const char *fqn = NULL; if (is_func) { fqn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, fname); } else { // Climb to enclosing class_declaration. TSNode parent = ts_node_parent(n); while (!ts_node_is_null(parent) && strcmp(ts_node_type(parent), "class_declaration") != 0) { parent = ts_node_parent(parent); } if (ts_node_is_null(parent)) continue; TSNode cn = ts_node_child_by_field_name(parent, "name", TS_LSP_FIELD_LEN("name")); if (ts_node_is_null(cn)) continue; char *cname = node_text(ctx, cn); if (!cname) continue; fqn = cbm_arena_sprintf(ctx->arena, "%s.%s.%s", ctx->module_qn, cname, fname); } // Patch the registered func's signature: rewrite NAMED → TYPE_PARAM. for (int fi = 0; fi < reg->func_count; fi++) { if (!reg->funcs[fi].qualified_name) continue; if (strcmp(reg->funcs[fi].qualified_name, fqn) != 0) continue; const CBMType *old_sig = reg->funcs[fi].signature; if (!old_sig) break; const CBMType *new_sig = cbm_type_substitute(ctx->arena, old_sig, names, args); if (new_sig) reg->funcs[fi].signature = new_sig; // Also store the type_param_names for later overload-by-types use. const char **stored = (const char **)cbm_arena_alloc( ctx->arena, (size_t)(tpc + 1) * sizeof(const char *)); if (stored) { for (int ti = 0; ti < tpc; ti++) stored[ti] = names[ti]; stored[tpc] = NULL; reg->funcs[fi].type_param_names = stored; } break; } } } // Parse a JSDoc comment block for `@param {T} name` and `@returns {T}` tags. For each // match, returns the type as a parsed CBMType. Used in `js_mode` to infer signatures // for JavaScript files where there are no inline type annotations. // // Pragmatic v1: scans the comment text linearly. Handles: // * @param {T} name — description // * @returns {T} — description // * @return {T} // Skips: nested braces in the type, complex JSDoc syntax (callback definitions, // @typedef, etc.). Falls back gracefully on UNKNOWN. typedef struct { const char *name; const CBMType *type; } JSDocParam; static void parse_jsdoc_block(TSLSPContext *ctx, const char *text, size_t len, JSDocParam *out_params, int *out_param_count, int max_params, const CBMType **out_return) { *out_return = NULL; *out_param_count = 0; if (!text || len == 0) return; const char *end = text + len; const char *p = text; while (p < end) { // Find next `@` that introduces a tag. while (p < end && *p != '@') p++; if (p >= end) return; p++; // skip @ // Match "param" or "returns"/"return". const char *tag_start = p; while (p < end && (*p >= 'a' && *p <= 'z')) p++; size_t tag_len = (size_t)(p - tag_start); bool is_param = (tag_len == 5 && memcmp(tag_start, "param", 5) == 0); bool is_return = ((tag_len == 7 && memcmp(tag_start, "returns", 7) == 0) || (tag_len == 6 && memcmp(tag_start, "return", 6) == 0)); if (!is_param && !is_return) continue; // Skip whitespace. while (p < end && (*p == ' ' || *p == '\t')) p++; if (p >= end || *p != '{') continue; p++; // skip { const char *type_start = p; int depth = 1; while (p < end && depth > 0) { if (*p == '{') depth++; else if (*p == '}') depth--; if (depth > 0) p++; } if (p >= end || *p != '}') continue; size_t type_len = (size_t)(p - type_start); char *type_text = cbm_arena_strndup(ctx->arena, type_start, type_len); const CBMType *parsed = parse_ts_type_text(ctx->arena, type_text, ctx->module_qn); p++; // skip } if (is_return) { *out_return = parsed; continue; } // Param: skip whitespace then read name. while (p < end && (*p == ' ' || *p == '\t')) p++; const char *name_start = p; while (p < end && ((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') || (*p >= '0' && *p <= '9') || *p == '_' || *p == '$' || *p == '.')) p++; size_t name_len = (size_t)(p - name_start); if (name_len == 0) continue; if (*out_param_count < max_params) { char *nm = cbm_arena_strndup(ctx->arena, name_start, name_len); out_params[*out_param_count].name = nm; out_params[*out_param_count].type = parsed; (*out_param_count)++; } } } // For each function_declaration in `js_mode` with a JSDoc-style comment immediately // before it, parse the comment and patch the registered func's signature. static void apply_jsdoc_signatures(TSLSPContext *ctx, TSNode root, CBMTypeRegistry *reg) { if (!ctx->js_mode || ts_node_is_null(root) || !reg || !ctx->module_qn) return; uint32_t kn = 0; TSNode *kids = collect_children(ctx->arena, root, &kn); for (uint32_t i = 0; i < kn; i++) { TSNode n = kids[i]; if (strcmp(ts_node_type(n), "function_declaration") != 0) continue; // Find the immediate preceding comment sibling (skip whitespace nodes which // tree-sitter doesn't expose as named children, but unnamed children may include // comment nodes). TSNode prev = {0}; for (int j = (int)i - 1; j >= 0; j--) { TSNode candidate = kids[j]; const char *ck = ts_node_type(candidate); if (strcmp(ck, "comment") == 0) { prev = candidate; break; } // Anything else: stop — JSDoc must be immediately preceding. if (ts_node_is_named(candidate)) break; } if (ts_node_is_null(prev)) continue; // Get comment text and check JSDoc shape (`/** ... */`). char *ctext = node_text(ctx, prev); if (!ctext) continue; size_t clen = strlen(ctext); if (clen < 4 || ctext[0] != '/' || ctext[1] != '*' || ctext[2] != '*') continue; JSDocParam params[16]; int pcount = 0; const CBMType *ret = NULL; parse_jsdoc_block(ctx, ctext + 3, clen - 3, params, &pcount, 16, &ret); // Resolve the function's QN and patch the registered signature. TSNode name_node = ts_node_child_by_field_name(n, "name", TS_LSP_FIELD_LEN("name")); if (ts_node_is_null(name_node)) continue; char *fname = node_text(ctx, name_node); if (!fname) continue; const char *fqn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, fname); for (int fi = 0; fi < reg->func_count; fi++) { if (!reg->funcs[fi].qualified_name) continue; if (strcmp(reg->funcs[fi].qualified_name, fqn) != 0) continue; const CBMType *old_sig = reg->funcs[fi].signature; const CBMType **rets = NULL; if (ret && !cbm_type_is_unknown(ret)) { rets = (const CBMType **)cbm_arena_alloc(ctx->arena, 2 * sizeof(const CBMType *)); if (rets) { rets[0] = ret; rets[1] = NULL; } } else if (old_sig && old_sig->kind == CBM_TYPE_FUNC) { rets = old_sig->data.func.return_types; } const CBMType **params_arr = NULL; const char **param_names_arr = NULL; if (pcount > 0) { params_arr = (const CBMType **)cbm_arena_alloc( ctx->arena, (size_t)(pcount + 1) * sizeof(const CBMType *)); param_names_arr = (const char **)cbm_arena_alloc( ctx->arena, (size_t)(pcount + 1) * sizeof(const char *)); if (params_arr && param_names_arr) { for (int j = 0; j < pcount; j++) { params_arr[j] = params[j].type; param_names_arr[j] = params[j].name; } params_arr[pcount] = NULL; param_names_arr[pcount] = NULL; } } else if (old_sig && old_sig->kind == CBM_TYPE_FUNC) { params_arr = old_sig->data.func.param_types; param_names_arr = old_sig->data.func.param_names; } reg->funcs[fi].signature = cbm_type_func(ctx->arena, param_names_arr, params_arr, rets); break; } } } // For each function_declaration whose registered signature has no return type, infer // it from the body and rebuild the signature. This mirrors typescript-go's // implicit return-type inference at a coarse level (single-return common case). static void infer_implicit_returns(TSLSPContext *ctx, TSNode root, CBMTypeRegistry *reg) { if (ts_node_is_null(root) || !reg || !ctx->module_qn) return; uint32_t kn = 0; TSNode *kids = collect_children(ctx->arena, root, &kn); for (uint32_t i = 0; i < kn; i++) { TSNode n = kids[i]; const char *k = ts_node_type(n); TSNode decl = n; if (strcmp(k, "export_statement") == 0) { TSNode d = ts_node_child_by_field_name(n, "declaration", TS_LSP_FIELD_LEN("declaration")); if (!ts_node_is_null(d)) { decl = d; k = ts_node_type(decl); } } if (strcmp(k, "function_declaration") != 0) continue; TSNode name_node = ts_node_child_by_field_name(decl, "name", TS_LSP_FIELD_LEN("name")); TSNode body = ts_node_child_by_field_name(decl, "body", TS_LSP_FIELD_LEN("body")); if (ts_node_is_null(name_node) || ts_node_is_null(body)) continue; char *fname = node_text(ctx, name_node); if (!fname) continue; const char *fqn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, fname); const CBMRegisteredFunc *existing = cbm_registry_lookup_func(reg, fqn); if (!existing) continue; // Only infer when the existing signature lacks an explicit return type. if (existing->signature && existing->signature->kind == CBM_TYPE_FUNC && existing->signature->data.func.return_types && existing->signature->data.func.return_types[0] && !cbm_type_is_unknown(existing->signature->data.func.return_types[0])) { continue; } const CBMType *inferred = infer_return_type_from_body(ctx, body); if (cbm_type_is_unknown(inferred)) continue; // Build a new FUNC signature carrying over params from the existing one (if any). const char **param_names = NULL; const CBMType **param_types = NULL; if (existing->signature && existing->signature->kind == CBM_TYPE_FUNC) { param_names = existing->signature->data.func.param_names; param_types = existing->signature->data.func.param_types; } const CBMType *rets[2] = {inferred, NULL}; const CBMType *new_sig = cbm_type_func(ctx->arena, param_names, param_types, rets); // Patch the registered func in place. for (int fi = 0; fi < reg->func_count; fi++) { if (reg->funcs[fi].qualified_name == fqn || (reg->funcs[fi].qualified_name && strcmp(reg->funcs[fi].qualified_name, fqn) == 0)) { reg->funcs[fi].signature = new_sig; break; } } } } // AST sweep: walk class/interface bodies to collect field names+types and refine // embedded_types / type_param_names. static void ast_sweep_shapes(TSLSPContext *ctx, TSNode root, CBMTypeRegistry *reg) { if (ts_node_is_null(root) || !reg) return; uint32_t kn = 0; TSNode *kids = collect_children(ctx->arena, root, &kn); for (uint32_t i = 0; i < kn; i++) { TSNode n = kids[i]; const char *k = ts_node_type(n); TSNode decl = n; if (strcmp(k, "export_statement") == 0) { // Look for declaration field. TSNode d = ts_node_child_by_field_name(n, "declaration", TS_LSP_FIELD_LEN("declaration")); if (!ts_node_is_null(d)) { decl = d; k = ts_node_type(decl); } } if (strcmp(k, "type_alias_declaration") == 0) { TSNode name = ts_node_child_by_field_name(decl, "name", TS_LSP_FIELD_LEN("name")); TSNode val = ts_node_child_by_field_name(decl, "value", TS_LSP_FIELD_LEN("value")); if (ts_node_is_null(name) || ts_node_is_null(val) || !ctx->module_qn) continue; char *nm = node_text(ctx, name); if (!nm) continue; const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, nm); // alias_of: resolved to QN if it's a NAMED text we can qualify. char *val_text = node_text(ctx, val); const char *alias_qn = NULL; if (val_text && val_text[0] && strchr(val_text, ' ') == NULL && strchr(val_text, '|') == NULL && strchr(val_text, '&') == NULL) { if (strchr(val_text, '.') == NULL && val_text[0] >= 'A' && val_text[0] <= 'Z') { alias_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, val_text); } else { alias_qn = cbm_arena_strdup(ctx->arena, val_text); } } // Find or insert. bool found = false; for (int ti = 0; ti < reg->type_count; ti++) { if (reg->types[ti].qualified_name && strcmp(reg->types[ti].qualified_name, qn) == 0) { if (alias_qn) reg->types[ti].alias_of = alias_qn; found = true; break; } } if (!found) { CBMRegisteredType rt; memset(&rt, 0, sizeof(rt)); rt.qualified_name = qn; rt.short_name = nm; rt.alias_of = alias_qn; cbm_registry_add_type(reg, rt); } continue; } if (strcmp(k, "class_declaration") != 0 && strcmp(k, "interface_declaration") != 0) { continue; } TSNode name_node = ts_node_child_by_field_name(decl, "name", TS_LSP_FIELD_LEN("name")); TSNode body = ts_node_child_by_field_name(decl, "body", TS_LSP_FIELD_LEN("body")); if (ts_node_is_null(name_node) || ts_node_is_null(body) || !ctx->module_qn) continue; char *cname = node_text(ctx, name_node); if (!cname) continue; const char *class_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, cname); // Find the registered type and augment with fields. CBMRegisteredType *rt = NULL; for (int ti = 0; ti < reg->type_count; ti++) { if (reg->types[ti].qualified_name && strcmp(reg->types[ti].qualified_name, class_qn) == 0) { rt = ®->types[ti]; break; } } if (!rt) continue; const char *field_names[64] = {0}; const CBMType *field_types[64] = {0}; int field_count = 0; // Interface method signatures we need to register as separate funcs. const char *iface_method_names[64] = {0}; const char *iface_method_qns[64] = {0}; const CBMType *iface_method_sigs[64] = {0}; int iface_method_count = 0; bool is_interface = (strcmp(k, "interface_declaration") == 0); uint32_t bnc = ts_node_named_child_count(body); for (uint32_t bi = 0; bi < bnc && field_count < 63; bi++) { TSNode m = ts_node_named_child(body, bi); if (ts_node_is_null(m)) continue; const char *mk = ts_node_type(m); // public_field_definition: TS class fields. // property_signature: interface fields. Interface fields can be either // a bare property (string name) OR a callable property (function-type field). if (strcmp(mk, "public_field_definition") == 0 || strcmp(mk, "property_signature") == 0) { TSNode fname = ts_node_child_by_field_name(m, "name", TS_LSP_FIELD_LEN("name")); TSNode ftype = ts_node_child_by_field_name(m, "type", TS_LSP_FIELD_LEN("type")); if (ts_node_is_null(fname)) continue; char *fnm = node_text(ctx, fname); if (!fnm) continue; const CBMType *ft = cbm_type_unknown(); if (!ts_node_is_null(ftype)) { TSNode tch = ts_node_named_child(ftype, 0); if (!ts_node_is_null(tch)) ft = ts_parse_type_node(ctx, tch); } field_names[field_count] = fnm; field_types[field_count] = ft; field_count++; continue; } // Interface method signature: `methodName(params): ReturnType;` // Or class method declaration in `.d.ts` ambient mode. if (is_interface && (strcmp(mk, "method_signature") == 0)) { if (iface_method_count >= 63) continue; TSNode mname = ts_node_child_by_field_name(m, "name", TS_LSP_FIELD_LEN("name")); if (ts_node_is_null(mname)) continue; char *mnm = node_text(ctx, mname); if (!mnm) continue; // Return type via field "return_type" — it's a type_annotation node. const CBMType *ret = cbm_type_unknown(); TSNode rt_node = ts_node_child_by_field_name(m, "return_type", TS_LSP_FIELD_LEN("return_type")); if (!ts_node_is_null(rt_node)) { TSNode tch = (strcmp(ts_node_type(rt_node), "type_annotation") == 0) ? ts_node_named_child(rt_node, 0) : rt_node; if (!ts_node_is_null(tch)) ret = ts_parse_type_node(ctx, tch); } const CBMType *rets[2] = {ret, NULL}; const CBMType *sig = cbm_type_func(ctx->arena, NULL, NULL, rets); const char *mqn = cbm_arena_sprintf(ctx->arena, "%s.%s", class_qn, mnm); iface_method_names[iface_method_count] = mnm; iface_method_qns[iface_method_count] = mqn; iface_method_sigs[iface_method_count] = sig; iface_method_count++; // Also register a CBMRegisteredFunc so cbm_registry_lookup_method picks it up. CBMRegisteredFunc rf; memset(&rf, 0, sizeof(rf)); rf.qualified_name = mqn; rf.short_name = mnm; rf.receiver_type = class_qn; rf.signature = sig; rf.min_params = -1; cbm_registry_add_func(reg, rf); } } if (field_count > 0) { const char **fn_arr = (const char **)cbm_arena_alloc( ctx->arena, (size_t)(field_count + 1) * sizeof(const char *)); const CBMType **ft_arr = (const CBMType **)cbm_arena_alloc( ctx->arena, (size_t)(field_count + 1) * sizeof(const CBMType *)); if (fn_arr && ft_arr) { for (int j = 0; j < field_count; j++) { fn_arr[j] = field_names[j]; ft_arr[j] = field_types[j]; } fn_arr[field_count] = NULL; ft_arr[field_count] = NULL; rt->field_names = fn_arr; rt->field_types = ft_arr; } } // Attach interface method names/qns to the registered type. if (iface_method_count > 0) { const char **mn_arr = (const char **)cbm_arena_alloc( ctx->arena, (size_t)(iface_method_count + 1) * sizeof(const char *)); const char **mq_arr = (const char **)cbm_arena_alloc( ctx->arena, (size_t)(iface_method_count + 1) * sizeof(const char *)); if (mn_arr && mq_arr) { for (int j = 0; j < iface_method_count; j++) { mn_arr[j] = iface_method_names[j]; mq_arr[j] = iface_method_qns[j]; } mn_arr[iface_method_count] = NULL; mq_arr[iface_method_count] = NULL; rt->method_names = mn_arr; rt->method_qns = mq_arr; } (void)iface_method_sigs; // sigs already attached via cbm_registry_add_func } } } void cbm_run_ts_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len, TSNode root, bool js_mode, bool jsx_mode, bool dts_mode) { if (!arena || !result || !source || ts_node_is_null(root)) return; ts_type_budget_reset((size_t)source_len); // Diagnostic / benchmarking knob: setting `CBM_LSP_DISABLED=1` skips the resolver. // This is used by the baseline-vs-LSP comparison tests to measure how many calls // the LSP-augmented path adds over plain tree-sitter extraction. const char *disabled = getenv("CBM_LSP_DISABLED"); if (disabled && disabled[0] && disabled[0] != '0') return; CBMTypeRegistry reg; cbm_registry_init(®, arena); cbm_ts_stdlib_register(®, arena); register_file_defs(arena, ®, result, result->module_qn); TSLSPContext ctx; ts_lsp_init(&ctx, arena, source, source_len, ®, result->module_qn, js_mode, jsx_mode, dts_mode, &result->resolved_calls); // Add imports early so type-resolution passes can rewrite NAMED references that // refer to imported symbols. ts_parse_type_node consults these via // resolve_type_with_imports. for (int i = 0; i < result->imports.count; i++) { const CBMImport *imp = &result->imports.items[i]; if (imp->local_name && imp->module_path) { ts_lsp_add_import(&ctx, imp->local_name, imp->module_path); } } ast_sweep_shapes(&ctx, root, ®); rebuild_signatures_from_ast(&ctx, root, ®); convert_signature_type_params(&ctx, root, ®); apply_jsdoc_signatures(&ctx, root, ®); infer_implicit_returns(&ctx, root, ®); ts_lsp_process_file(&ctx, root); } // ── Cross-file entry point (Phase 3) ────────────────────────────────────────── /* Register a batch of CBMLSPDef[] into a registry. Mirrors the inline * loop in cbm_run_ts_lsp_cross — KEEP IN SYNC. Shared by the Tier 2 * pre-built registry builder + per-file overlay builder. Def-driven * (return/field/embedded/method info from def strings). */ static void ts_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, CBMLSPDef *defs, int def_count) { for (int i = 0; i < def_count; i++) { const CBMLSPDef *d = &defs[i]; if (!d->qualified_name || !d->short_name || !d->label) continue; if (strcmp(d->label, "Class") == 0 || strcmp(d->label, "Interface") == 0) { CBMRegisteredType rt; memset(&rt, 0, sizeof(rt)); rt.qualified_name = d->qualified_name; rt.short_name = d->short_name; rt.is_interface = (strcmp(d->label, "Interface") == 0); if (d->embedded_types && d->embedded_types[0]) { int n = 1; for (const char *s = d->embedded_types; *s; s++) if (*s == '|') n++; const char **arr = (const char **)cbm_arena_alloc(arena, (size_t)(n + 1) * sizeof(const char *)); if (arr) { int idx = 0; const char *start = d->embedded_types; for (const char *s = d->embedded_types;; s++) { if (*s == '|' || *s == '\0') { arr[idx++] = cbm_arena_strndup(arena, start, (size_t)(s - start)); if (*s == '\0') break; start = s + 1; } } arr[idx] = NULL; rt.embedded_types = arr; } } if (d->field_defs && d->field_defs[0]) { int n = 1; for (const char *s = d->field_defs; *s; s++) if (*s == '|') n++; const char **fn_arr = (const char **)cbm_arena_alloc(arena, (size_t)(n + 1) * sizeof(const char *)); const CBMType **ft_arr = (const CBMType **)cbm_arena_alloc( arena, (size_t)(n + 1) * sizeof(const CBMType *)); if (fn_arr && ft_arr) { int idx = 0; const char *start = d->field_defs; for (const char *s = d->field_defs;; s++) { if (*s == '|' || *s == '\0') { char *pair = cbm_arena_strndup(arena, start, (size_t)(s - start)); char *colon = pair ? strchr(pair, ':') : NULL; if (pair && colon) { *colon = '\0'; fn_arr[idx] = pair; ft_arr[idx] = parse_ts_type_text(arena, colon + 1, d->def_module_qn); idx++; } if (*s == '\0') break; start = s + 1; } } fn_arr[idx] = NULL; ft_arr[idx] = NULL; rt.field_names = fn_arr; rt.field_types = ft_arr; } } cbm_registry_add_type(reg, rt); if (d->method_names_str && d->method_names_str[0]) { int n = 1; for (const char *s = d->method_names_str; *s; s++) if (*s == '|') n++; const char **mn_arr = (const char **)cbm_arena_alloc(arena, (size_t)(n + 1) * sizeof(const char *)); const char **mqn_arr = (const char **)cbm_arena_alloc(arena, (size_t)(n + 1) * sizeof(const char *)); if (mn_arr && mqn_arr) { int idx = 0; const char *start = d->method_names_str; for (const char *s = d->method_names_str;; s++) { if (*s == '|' || *s == '\0') { char *m = cbm_arena_strndup(arena, start, (size_t)(s - start)); mn_arr[idx] = m; mqn_arr[idx] = cbm_arena_sprintf(arena, "%s.%s", d->qualified_name, m); idx++; if (*s == '\0') break; start = s + 1; } } mn_arr[idx] = NULL; mqn_arr[idx] = NULL; if (reg->type_count > 0) { CBMRegisteredType *rt_just = ®->types[reg->type_count - 1]; rt_just->method_names = mn_arr; rt_just->method_qns = mqn_arr; } } } } else if (strcmp(d->label, "Function") == 0 || strcmp(d->label, "Method") == 0) { CBMRegisteredFunc rf; memset(&rf, 0, sizeof(rf)); rf.qualified_name = d->qualified_name; rf.short_name = d->short_name; rf.min_params = -1; if (d->return_types && d->return_types[0]) { int n = 1; for (const char *s = d->return_types; *s; s++) if (*s == '|') n++; const CBMType **rets = (const CBMType **)cbm_arena_alloc( arena, (size_t)(n + 1) * sizeof(const CBMType *)); if (rets) { int idx = 0; const char *start = d->return_types; for (const char *s = d->return_types;; s++) { if (*s == '|' || *s == '\0') { char *part = cbm_arena_strndup(arena, start, (size_t)(s - start)); rets[idx++] = parse_ts_type_text(arena, part, d->def_module_qn); if (*s == '\0') break; start = s + 1; } } rets[idx] = NULL; rf.signature = cbm_type_func(arena, NULL, NULL, rets); } } if (strcmp(d->label, "Method") == 0 && d->receiver_type) { rf.receiver_type = d->receiver_type; } cbm_registry_add_func(reg, rf); } } } /* Tier 2: build a project-wide TS/JS/TSX registry ONCE from all defs * (filters by lang). Shared READ-ONLY *base* across resolve workers. * Per-file overlays (built by cbm_run_ts_lsp_cross_with_registry) * chain to this via the registry fallback pointer. */ CBMTypeRegistry *cbm_ts_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_ts_stdlib_register(reg, arena); /* Budget scales with the def volume this build parses type texts for. */ ts_type_budget_reset((size_t)(def_count > 0 ? def_count : 1) * 1024); for (int i = 0; i < def_count; i++) { CBMLSPDef *d = &defs[i]; if (d->lang != CBM_LANG_JAVASCRIPT && d->lang != CBM_LANG_TYPESCRIPT && d->lang != CBM_LANG_TSX) { continue; } ts_register_lsp_defs(arena, reg, d, 1); } cbm_registry_finalize(reg); reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */ return reg; } /* Tier 2 per-file resolve. Builds a SMALL per-file overlay registry P * containing only this file's own-module defs (so the AST refinement * passes — which mutate type shapes/aliases of locally-declared types * — operate on P, not the shared base). P chains to the shared base * `reg` for cross-file + stdlib lookups. This preserves the per-file * AST refinement quality while avoiding re-registering imported * modules' defs in every file. */ void cbm_run_ts_lsp_cross_with_registry(CBMArena *arena, const char *source, int source_len, const char *module_qn, bool js_mode, bool jsx_mode, bool dts_mode, CBMTypeRegistry *reg, CBMLSPDef *defs, int def_count, const char **import_names, const char **import_qns, int import_count, TSTree *cached_tree, CBMResolvedCallArray *out) { if (!arena || !out || !reg) return; ts_type_budget_reset((size_t)source_len); /* Per-file overlay: register only the file's own-module defs so the * AST passes can refine them. Imports/stdlib resolve via fallback. */ CBMTypeRegistry overlay; cbm_registry_init(&overlay, arena); overlay.fallback = reg; for (int i = 0; i < def_count; i++) { CBMLSPDef *d = &defs[i]; const char *dm = d->def_module_qn ? d->def_module_qn : ""; if (module_qn && strcmp(dm, module_qn) == 0) { ts_register_lsp_defs(arena, &overlay, d, 1); } } TSTree *tree = cached_tree; bool owns_tree = false; if (!tree) { if (!source || source_len <= 0) return; TSParser *parser = ts_parser_new(); if (!parser) return; const TSLanguage *lang = jsx_mode ? (js_mode ? tree_sitter_javascript() : tree_sitter_tsx()) : (js_mode ? tree_sitter_javascript() : tree_sitter_typescript()); ts_parser_set_language(parser, lang); tree = ts_parser_parse_string(parser, NULL, source, source_len); ts_parser_delete(parser); if (!tree) return; owns_tree = true; } TSNode root = ts_tree_root_node(tree); if (ts_node_is_null(root)) { if (owns_tree) ts_tree_delete(tree); return; } cbm_registry_finalize(&overlay); TSLSPContext ctx; ts_lsp_init(&ctx, arena, source, source_len, &overlay, module_qn, js_mode, jsx_mode, dts_mode, out); for (int i = 0; i < import_count; i++) { if (import_names && import_qns && import_names[i] && import_qns[i]) { ts_lsp_add_import(&ctx, import_names[i], import_qns[i]); } } ast_sweep_shapes(&ctx, root, &overlay); rebuild_signatures_from_ast(&ctx, root, &overlay); convert_signature_type_params(&ctx, root, &overlay); apply_jsdoc_signatures(&ctx, root, &overlay); infer_implicit_returns(&ctx, root, &overlay); ts_lsp_process_file(&ctx, root); if (owns_tree) ts_tree_delete(tree); } void cbm_run_ts_lsp_cross(CBMArena *arena, const char *source, int source_len, const char *module_qn, bool js_mode, bool jsx_mode, bool dts_mode, CBMLSPDef *defs, int def_count, const char **import_names, const char **import_qns, int import_count, TSTree *cached_tree, CBMResolvedCallArray *out) { if (!arena || !out) return; atomic_fetch_add(&g_ts_full_reg_builds, 1); ts_type_budget_reset((size_t)source_len); CBMTypeRegistry reg; cbm_registry_init(®, arena); cbm_ts_stdlib_register(®, arena); // Register cross-file defs. for (int i = 0; i < def_count; i++) { const CBMLSPDef *d = &defs[i]; if (!d->qualified_name || !d->short_name || !d->label) continue; if (strcmp(d->label, "Class") == 0 || strcmp(d->label, "Interface") == 0) { CBMRegisteredType rt; memset(&rt, 0, sizeof(rt)); rt.qualified_name = d->qualified_name; rt.short_name = d->short_name; rt.is_interface = (strcmp(d->label, "Interface") == 0); // Embedded types (extends list) — pipe-separated. if (d->embedded_types && d->embedded_types[0]) { int n = 1; for (const char *s = d->embedded_types; *s; s++) if (*s == '|') n++; const char **arr = (const char **)cbm_arena_alloc(arena, (size_t)(n + 1) * sizeof(const char *)); if (arr) { int idx = 0; const char *start = d->embedded_types; for (const char *s = d->embedded_types;; s++) { if (*s == '|' || *s == '\0') { arr[idx++] = cbm_arena_strndup(arena, start, (size_t)(s - start)); if (*s == '\0') break; start = s + 1; } } arr[idx] = NULL; rt.embedded_types = arr; } } // Field defs (interface members, "name:type|name:type"). if (d->field_defs && d->field_defs[0]) { int n = 1; for (const char *s = d->field_defs; *s; s++) if (*s == '|') n++; const char **fn_arr = (const char **)cbm_arena_alloc(arena, (size_t)(n + 1) * sizeof(const char *)); const CBMType **ft_arr = (const CBMType **)cbm_arena_alloc( arena, (size_t)(n + 1) * sizeof(const CBMType *)); if (fn_arr && ft_arr) { int idx = 0; const char *start = d->field_defs; for (const char *s = d->field_defs;; s++) { if (*s == '|' || *s == '\0') { char *pair = cbm_arena_strndup(arena, start, (size_t)(s - start)); char *colon = pair ? strchr(pair, ':') : NULL; if (pair && colon) { *colon = '\0'; fn_arr[idx] = pair; ft_arr[idx] = parse_ts_type_text(arena, colon + 1, d->def_module_qn); idx++; } if (*s == '\0') break; start = s + 1; } } fn_arr[idx] = NULL; ft_arr[idx] = NULL; rt.field_names = fn_arr; rt.field_types = ft_arr; } } cbm_registry_add_type(®, rt); // Method names from method_names_str. if (d->method_names_str && d->method_names_str[0]) { int n = 1; for (const char *s = d->method_names_str; *s; s++) if (*s == '|') n++; const char **mn_arr = (const char **)cbm_arena_alloc(arena, (size_t)(n + 1) * sizeof(const char *)); const char **mqn_arr = (const char **)cbm_arena_alloc(arena, (size_t)(n + 1) * sizeof(const char *)); if (mn_arr && mqn_arr) { int idx = 0; const char *start = d->method_names_str; for (const char *s = d->method_names_str;; s++) { if (*s == '|' || *s == '\0') { char *m = cbm_arena_strndup(arena, start, (size_t)(s - start)); mn_arr[idx] = m; mqn_arr[idx] = cbm_arena_sprintf(arena, "%s.%s", d->qualified_name, m); idx++; if (*s == '\0') break; start = s + 1; } } mn_arr[idx] = NULL; mqn_arr[idx] = NULL; // Find the type we just inserted and attach. if (reg.type_count > 0) { CBMRegisteredType *rt_just = ®.types[reg.type_count - 1]; rt_just->method_names = mn_arr; rt_just->method_qns = mqn_arr; } } } } else if (strcmp(d->label, "Function") == 0 || strcmp(d->label, "Method") == 0) { CBMRegisteredFunc rf; memset(&rf, 0, sizeof(rf)); rf.qualified_name = d->qualified_name; rf.short_name = d->short_name; rf.min_params = -1; // Return types from pipe-separated return_types text. if (d->return_types && d->return_types[0]) { int n = 1; for (const char *s = d->return_types; *s; s++) if (*s == '|') n++; const CBMType **rets = (const CBMType **)cbm_arena_alloc( arena, (size_t)(n + 1) * sizeof(const CBMType *)); if (rets) { int idx = 0; const char *start = d->return_types; for (const char *s = d->return_types;; s++) { if (*s == '|' || *s == '\0') { char *part = cbm_arena_strndup(arena, start, (size_t)(s - start)); rets[idx++] = parse_ts_type_text(arena, part, d->def_module_qn); if (*s == '\0') break; start = s + 1; } } rets[idx] = NULL; rf.signature = cbm_type_func(arena, NULL, NULL, rets); } } if (strcmp(d->label, "Method") == 0 && d->receiver_type) { rf.receiver_type = d->receiver_type; } cbm_registry_add_func(®, rf); } } // Use cached tree if available; otherwise parse internally and own it. TSTree *tree = cached_tree; bool owns_tree = false; if (!tree) { if (!source || source_len <= 0) return; TSParser *parser = ts_parser_new(); if (!parser) return; const TSLanguage *lang = jsx_mode ? (js_mode ? tree_sitter_javascript() : tree_sitter_tsx()) : (js_mode ? tree_sitter_javascript() : tree_sitter_typescript()); ts_parser_set_language(parser, lang); tree = ts_parser_parse_string(parser, NULL, source, source_len); ts_parser_delete(parser); if (!tree) return; owns_tree = true; } TSNode root = ts_tree_root_node(tree); if (ts_node_is_null(root)) { if (owns_tree) ts_tree_delete(tree); return; } // Build a faux CBMFileResult so register_file_defs's downstream paths work; we don't // need it here because cross-file defs already came in as CBMLSPDef. // Finalize registry — O(1) lookups. See go_lsp.c "3c. Finalize" // comment for the rationale. cbm_registry_finalize(®); TSLSPContext ctx; ts_lsp_init(&ctx, arena, source, source_len, ®, module_qn, js_mode, jsx_mode, dts_mode, out); // Add imports early so type passes can resolve imported types. for (int i = 0; i < import_count; i++) { if (import_names && import_qns && import_names[i] && import_qns[i]) { ts_lsp_add_import(&ctx, import_names[i], import_qns[i]); } } ast_sweep_shapes(&ctx, root, ®); rebuild_signatures_from_ast(&ctx, root, ®); convert_signature_type_params(&ctx, root, ®); apply_jsdoc_signatures(&ctx, root, ®); infer_implicit_returns(&ctx, root, ®); ts_lsp_process_file(&ctx, root); if (owns_tree) ts_tree_delete(tree); } // ── Batch cross-file (Phase 3) ──────────────────────────────────────────────── void cbm_batch_ts_lsp_cross(CBMArena *arena, CBMBatchTSLSPFile *files, int file_count, CBMResolvedCallArray *out) { if (!arena || !files || !out || file_count <= 0) return; // Project-scope merging happens implicitly: every file's CBMLSPDef set already // includes both same-file and cross-file defs (built by the caller). The plan §17 // finding #4 fix ensures the *caller* (pass_calls.c) merges interface declarations // by QN before passing them in. for (int i = 0; i < file_count; i++) { CBMBatchTSLSPFile *f = &files[i]; cbm_run_ts_lsp_cross(arena, f->source, f->source_len, f->module_qn, f->js_mode, f->jsx_mode, f->dts_mode, f->defs, f->def_count, f->import_names, f->import_qns, f->import_count, f->cached_tree, &out[i]); } }