/* * cs_lsp.c — C# Light Semantic Pass. * * Reverse-engineered from Roslyn (dotnet/roslyn). Roslyn's resolution * pipeline is built on a chain of `Binder` objects representing lexical * scopes (namespace, type, member, block). LookupSymbolsInternal walks * the chain inside-out and merges results. We model the same chain with * a CBMScope stack plus a CSLSPContext that tracks type/namespace/using * state that does not naturally live in the value scope. * * Resolution order for a bare identifier `X` inside member `M` in type * `T` in namespace `N`: * * 1. local variables / parameters in the current block (CBMScope) * 2. type members of T (and base types, transitively) (registry) * 3. type members of any enclosing-type chain (registry) * 4. members of N, then enclosing namespaces outward (registry) * 5. types/aliases in `using` directives (in declaration order) (CSUsing) * 6. static members imported by `using static` (CSUsing) * 7. extension methods imported by `using` directives (registry) * * All lookups eventually short-circuit to a registry. * * tree-sitter-c-sharp grammar reference (relevant node kinds): * compilation_unit, namespace_declaration, file_scoped_namespace_declaration, * using_directive, class_declaration, struct_declaration, record_declaration, * interface_declaration, enum_declaration, base_list, type_parameter_list, * method_declaration, constructor_declaration, property_declaration, * indexer_declaration, field_declaration, event_declaration, * parameter_list, parameter, type_parameter_constraints_clause, * block, local_declaration_statement, variable_declaration, * variable_declarator, expression_statement, return_statement, * if_statement, switch_statement, for_statement, foreach_statement, * identifier, qualified_name, generic_name, member_access_expression, * invocation_expression, object_creation_expression, anonymous_object_creation_expression, * implicit_object_creation_expression, lambda_expression, conditional_expression, * tuple_expression, await_expression, cast_expression, as_expression, * is_expression, predefined_type, nullable_type, array_type, pointer_type, * tuple_type, function_pointer_type. */ #include "cs_lsp.h" #include "lsp_node_iter.h" #include "../helpers.h" #include #include #include #include #define CS_EVAL_MAX_DEPTH 64 #define CS_USING_INITIAL_CAP 16 #define CS_NAMESPACE_INITIAL_CAP 8 #define CS_LSP_PARENT_WALK_MAX 32 extern const TSLanguage *tree_sitter_c_sharp(void); /* ── forward decls ──────────────────────────────────────────────── */ static void cs_resolve_calls_in_node(CSLSPContext *ctx, TSNode node); static void cs_process_function_like(CSLSPContext *ctx, TSNode node); static void cs_process_type_decl(CSLSPContext *ctx, TSNode node); static const CBMType *cs_eval_invocation_type(CSLSPContext *ctx, TSNode call); static const CBMType *cs_eval_member_access_type(CSLSPContext *ctx, TSNode node); static const CBMType *cs_eval_object_creation_type(CSLSPContext *ctx, TSNode node); static const CBMType *cs_eval_identifier_type(CSLSPContext *ctx, TSNode node); static const CBMType *cs_substitute_type_params(CBMArena *arena, const CBMType *t, const char **param_names, const CBMType **param_args); static void cs_collect_imports(CSLSPContext *ctx, TSNode root); static void cs_collect_namespace(CSLSPContext *ctx, TSNode ns_node, bool file_scoped); static const char *cs_namespace_qn(CSLSPContext *ctx); static void cs_register_type_decls(CSLSPContext *ctx, CBMTypeRegistry *reg, TSNode root); static char *cs_node_text_cached(CSLSPContext *ctx, TSNode node); static const CBMType *cs_unwrap_task(CSLSPContext *ctx, const CBMType *t); static const CBMType *cs_unwrap_nullable(const CBMType *t); /* ── small helpers ──────────────────────────────────────────────── */ static char *cs_node_text(CSLSPContext *ctx, TSNode node) { return cbm_node_text(ctx->arena, node, ctx->source); } static char *cs_node_text_cached(CSLSPContext *ctx, TSNode node) { return cs_node_text(ctx, node); } static bool cs_node_is(TSNode n, const char *kind) { if (ts_node_is_null(n)) return false; return strcmp(ts_node_type(n), kind) == 0; } static TSNode cs_child_named_kind(TSNode parent, const char *kind) { if (ts_node_is_null(parent)) return parent; uint32_t nc = ts_node_child_count(parent); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(parent, i); if (!ts_node_is_null(c) && strcmp(ts_node_type(c), kind) == 0) return c; } TSNode null_node; memset(&null_node, 0, sizeof(null_node)); return null_node; } static TSNode cs_first_named_child(TSNode parent) { if (ts_node_is_null(parent)) return parent; uint32_t nc = ts_node_child_count(parent); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(parent, i); if (!ts_node_is_null(c) && ts_node_is_named(c)) return c; } TSNode null_node; memset(&null_node, 0, sizeof(null_node)); return null_node; } /* Last segment after '.'. */ static const char *cs_short_name(const char *qn) { if (!qn) return NULL; const char *last = qn; for (const char *p = qn; *p; p++) { if (*p == '.') last = p + 1; } return last; } /* C# uses '.' separators natively; no conversion needed for identifiers. * Strip leading "global::" / "::" if present. */ static const char *cs_strip_global(const char *name) { if (!name) return name; if (strncmp(name, "global::", 8) == 0) return name + 8; if (strncmp(name, "::", 2) == 0) return name + 2; return name; } static char *cs_normalize_name(CBMArena *a, const char *name) { if (!name) return NULL; name = cs_strip_global(name); /* Replace "::" with "." (C# scope qualifier in source forms like * `global::System.Foo` becomes `System.Foo`). */ size_t n = strlen(name); char *out = (char *)cbm_arena_alloc(a, n + 1); if (!out) return NULL; size_t w = 0; for (size_t i = 0; i < n; i++) { if (name[i] == ':' && i + 1 < n && name[i + 1] == ':') { out[w++] = '.'; i++; continue; } out[w++] = name[i]; } out[w] = '\0'; return out; } /* Walk up from `name` past any leading '<' or whitespace. */ static char *cs_strip_generic_args(CBMArena *a, const char *name) { if (!name) return NULL; const char *lt = strchr(name, '<'); if (!lt) return cbm_arena_strdup(a, name); size_t len = (size_t)(lt - name); return cbm_arena_strndup(a, name, len); } /* Predefined C# type aliases: int → System.Int32, etc. */ static const char *cs_predefined_alias(const char *name) { if (!name) return NULL; /* Roslyn predefined-type list: Binder_Symbols.cs BindPredefinedTypeSymbol */ if (strcmp(name, "int") == 0) return "System.Int32"; if (strcmp(name, "uint") == 0) return "System.UInt32"; if (strcmp(name, "long") == 0) return "System.Int64"; if (strcmp(name, "ulong") == 0) return "System.UInt64"; if (strcmp(name, "short") == 0) return "System.Int16"; if (strcmp(name, "ushort") == 0) return "System.UInt16"; if (strcmp(name, "byte") == 0) return "System.Byte"; if (strcmp(name, "sbyte") == 0) return "System.SByte"; if (strcmp(name, "float") == 0) return "System.Single"; if (strcmp(name, "double") == 0) return "System.Double"; if (strcmp(name, "decimal") == 0) return "System.Decimal"; if (strcmp(name, "bool") == 0) return "System.Boolean"; if (strcmp(name, "char") == 0) return "System.Char"; if (strcmp(name, "string") == 0) return "System.String"; if (strcmp(name, "object") == 0) return "System.Object"; if (strcmp(name, "nint") == 0) return "System.IntPtr"; if (strcmp(name, "nuint") == 0) return "System.UIntPtr"; if (strcmp(name, "void") == 0) return "System.Void"; if (strcmp(name, "dynamic") == 0) return "System.Object"; /* dynamic ≈ object */ return NULL; } static bool cs_is_keyword_self(const char *name) { if (!name) return false; return strcmp(name, "this") == 0 || strcmp(name, "base") == 0; } /* ── init ───────────────────────────────────────────────────────── */ void cs_lsp_init(CSLSPContext *ctx, CBMArena *arena, const char *source, int source_len, const CBMTypeRegistry *registry, const char *module_qn, CBMResolvedCallArray *out) { memset(ctx, 0, sizeof(*ctx)); ctx->arena = arena; ctx->source = source; ctx->source_len = source_len; ctx->registry = registry; ctx->module_qn = module_qn; ctx->resolved_calls = out; ctx->current_scope = cbm_scope_push(arena, NULL); /* Implicit `using System;` — C# always brings in System. We pretend * Program.cs has it explicitly so primitive aliases and System.Console * resolve out of the box. The real LSP picks this up from the project * file's ; we just always include it. */ cs_lsp_add_using(ctx, CBM_CS_USING_NAMESPACE, "", "System", false); const char *dbg = getenv("CBM_LSP_DEBUG"); ctx->debug = (dbg && dbg[0]); } /* ── using management ───────────────────────────────────────────── */ void cs_lsp_add_using(CSLSPContext *ctx, CBMCSUsingKind kind, const char *local_name, const char *target_qn, bool is_global) { if (!target_qn) return; /* Dedupe by (kind, local_name, target). */ for (int i = 0; i < ctx->using_count; i++) { CBMCSUsing *u = &ctx->usings[i]; if (u->kind != kind) continue; if (strcmp(u->target_qn, target_qn) != 0) continue; const char *a = local_name ? local_name : ""; const char *b = u->local_name ? u->local_name : ""; if (strcmp(a, b) != 0) continue; return; } if (ctx->using_count >= ctx->using_cap) { int new_cap = ctx->using_cap ? ctx->using_cap * 2 : CS_USING_INITIAL_CAP; CBMCSUsing *nu = (CBMCSUsing *)cbm_arena_alloc(ctx->arena, (size_t)new_cap * sizeof(*nu)); if (!nu) return; for (int i = 0; i < ctx->using_count; i++) nu[i] = ctx->usings[i]; ctx->usings = nu; ctx->using_cap = new_cap; } CBMCSUsing *slot = &ctx->usings[ctx->using_count++]; slot->kind = kind; slot->local_name = local_name ? cbm_arena_strdup(ctx->arena, local_name) : ""; slot->target_qn = cbm_arena_strdup(ctx->arena, target_qn); slot->is_global = is_global; } /* ── namespace stack ────────────────────────────────────────────── */ static void cs_namespace_push(CSLSPContext *ctx, const char *ns_name) { if (!ns_name || !*ns_name) return; if (ctx->namespace_count >= ctx->namespace_cap) { int new_cap = ctx->namespace_cap ? ctx->namespace_cap * 2 : CS_NAMESPACE_INITIAL_CAP; const char **arr = (const char **)cbm_arena_alloc(ctx->arena, (size_t)new_cap * sizeof(*arr)); if (!arr) return; for (int i = 0; i < ctx->namespace_count; i++) arr[i] = ctx->namespace_stack[i]; ctx->namespace_stack = arr; ctx->namespace_cap = new_cap; } ctx->namespace_stack[ctx->namespace_count++] = cbm_arena_strdup(ctx->arena, ns_name); } static void cs_namespace_pop(CSLSPContext *ctx) { if (ctx->namespace_count > 0) ctx->namespace_count--; } /* Concatenate the namespace stack into a dotted QN (outer to inner). */ static const char *cs_namespace_qn(CSLSPContext *ctx) { if (ctx->namespace_count == 0) return ""; /* Compute total length first. */ size_t total = 0; for (int i = 0; i < ctx->namespace_count; i++) { total += strlen(ctx->namespace_stack[i]); if (i > 0) total += 1; /* dot */ } char *out = (char *)cbm_arena_alloc(ctx->arena, total + 1); if (!out) return ""; size_t w = 0; for (int i = 0; i < ctx->namespace_count; i++) { if (i > 0) out[w++] = '.'; size_t len = strlen(ctx->namespace_stack[i]); memcpy(out + w, ctx->namespace_stack[i], len); w += len; } out[w] = '\0'; return out; } /* ── type-name resolution ───────────────────────────────────────── */ static const CBMRegisteredType *cs_lookup_type_qn(CSLSPContext *ctx, const char *qn) { if (!ctx->registry || !qn) return NULL; return cbm_registry_lookup_type(ctx->registry, qn); } /* Try a candidate QN; if found, return it (interned in arena). */ static const char *cs_try_type_qn(CSLSPContext *ctx, const char *qn) { if (!qn) return NULL; if (cs_lookup_type_qn(ctx, qn)) return qn; return NULL; } /* Returns a fully-qualified type name resolved against: * 1. predefined alias (int -> System.Int32) * 2. type-parameter substitution map * 3. exact registry hit * 4. nested type / current type (for unqualified inside-class refs) * 5. each namespace prefix from innermost outward * 6. module-prefixed (file-local QN that the unified extractor produced) * 7. each `using namespace X` prefix * 8. each `using A = X` alias substitution * 9. fall back to bare name (registry will fail; pipeline drops the call) */ const char *cs_resolve_type_name(CSLSPContext *ctx, const char *raw) { if (!raw || !*raw) return NULL; char *name = cs_normalize_name(ctx->arena, raw); if (!name) return NULL; /* Strip generic args for lookup purposes; the caller can re-attach * a TEMPLATE wrapping. */ char *bare = cs_strip_generic_args(ctx->arena, name); if (!bare) bare = name; /* 1. Predefined. */ const char *pre = cs_predefined_alias(bare); if (pre) return pre; /* 2. Type-parameter substitution. */ for (int i = 0; i < ctx->type_param_count; i++) { if (strcmp(ctx->type_param_names[i], bare) == 0) { const CBMType *arg = ctx->type_param_args[i]; if (arg && arg->kind == CBM_TYPE_NAMED) return arg->data.named.qualified_name; /* Builtin or unknown — fall through. */ } } /* 3. Exact registry hit. */ if (cs_lookup_type_qn(ctx, bare)) return bare; /* 4. Inside the current class? Try enclosing class's nested type. */ if (ctx->enclosing_class_qn) { const char *try_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->enclosing_class_qn, bare); if (cs_lookup_type_qn(ctx, try_qn)) return try_qn; } /* 5. Each namespace prefix from innermost outward. */ if (ctx->namespace_count > 0) { const char *ns = cs_namespace_qn(ctx); for (;;) { if (!ns || !*ns) break; const char *try_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ns, bare); if (cs_lookup_type_qn(ctx, try_qn)) return try_qn; const char *dot = strrchr(ns, '.'); if (!dot) break; char *trim = cbm_arena_strndup(ctx->arena, ns, (size_t)(dot - ns)); ns = trim; } } /* 6. Module-prefixed (matches what unified extractor produces for file- * local types). */ if (ctx->module_qn && ctx->module_qn[0]) { const char *try_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, bare); if (cs_lookup_type_qn(ctx, try_qn)) return try_qn; } /* 7. using namespace X — try X.bare. */ for (int i = 0; i < ctx->using_count; i++) { const CBMCSUsing *u = &ctx->usings[i]; if (u->kind != CBM_CS_USING_NAMESPACE) continue; const char *try_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", u->target_qn, bare); if (cs_lookup_type_qn(ctx, try_qn)) return try_qn; } /* 8. using A = X — substitute. The target may include generic args * (e.g. `using IL = ...List`). The registry stores the bare * type name only, so we strip generic args before returning. */ for (int i = 0; i < ctx->using_count; i++) { const CBMCSUsing *u = &ctx->usings[i]; if (u->kind != CBM_CS_USING_ALIAS) continue; if (!u->local_name) continue; size_t alias_len = strlen(u->local_name); if (strncmp(bare, u->local_name, alias_len) == 0) { if (bare[alias_len] == '\0') { /* Whole name is the alias. Strip generic args from target. */ char *target_bare = cs_strip_generic_args(ctx->arena, u->target_qn); if (target_bare && cs_lookup_type_qn(ctx, target_bare)) return target_bare; return cbm_arena_strdup(ctx->arena, u->target_qn); } if (bare[alias_len] == '.') { /* `Alias.Sub` -> `Target.Sub` */ char *target_bare = cs_strip_generic_args(ctx->arena, u->target_qn); const char *base = target_bare ? target_bare : u->target_qn; const char *try_qn = cbm_arena_sprintf(ctx->arena, "%s%s", base, bare + alias_len); if (cs_lookup_type_qn(ctx, try_qn)) return try_qn; } } } /* 9. Short-name fallback: scan the registry for any type whose * short_name equals `bare` and whose QN shares the longest namespace * prefix with the file's namespace stack. This recovers cross-file * lookups that the namespace heuristics miss when the file's * `namespace` declaration doesn't line up with the directory tree. */ { const char *the_short = cs_short_name(bare); if (the_short && *the_short) { const CBMRegisteredType *best = NULL; int best_score = -1; const char *namespace_dotted = cs_namespace_qn(ctx); for (int i = 0; ctx->registry && i < ctx->registry->type_count; i++) { const CBMRegisteredType *cand = &ctx->registry->types[i]; if (!cand->short_name || strcmp(cand->short_name, the_short) != 0) continue; int score = 0; if (cand->qualified_name && namespace_dotted && *namespace_dotted) { const char *m = namespace_dotted; const char *q = cand->qualified_name; while (*m && *q && *m == *q) { if (*m == '.') score++; m++; q++; } } if (score > best_score) { best_score = score; best = cand; } } if (best) return best->qualified_name; } } /* 10. Last-resort: return bare so the caller can build a NAMED type even * if not registered. The registry-miss path simply drops the edge. */ return bare; } /* Look up a method on a type, walking inheritance chain. */ const CBMRegisteredFunc *cs_lookup_method(CSLSPContext *ctx, const char *type_qn, const char *method_name) { if (!type_qn || !method_name) return NULL; const CBMRegisteredFunc *f = cbm_registry_lookup_method(ctx->registry, type_qn, method_name); if (f) return f; /* Walk inheritance chain (base + interfaces + transitive bases). */ const CBMRegisteredType *t = cs_lookup_type_qn(ctx, type_qn); if (!t) return NULL; const char *visited[CS_LSP_PARENT_WALK_MAX]; int visited_count = 0; const char *frontier[CS_LSP_PARENT_WALK_MAX]; int frontier_count = 0; if (t->embedded_types) { for (int i = 0; t->embedded_types[i] && frontier_count < CS_LSP_PARENT_WALK_MAX; i++) { frontier[frontier_count++] = t->embedded_types[i]; } } /* Always try System.Object as the universal root. */ while (frontier_count > 0 && visited_count < CS_LSP_PARENT_WALK_MAX) { const char *parent = frontier[--frontier_count]; bool seen = false; for (int v = 0; v < visited_count; v++) { if (strcmp(visited[v], parent) == 0) { seen = true; break; } } if (seen) continue; visited[visited_count++] = parent; f = cbm_registry_lookup_method(ctx->registry, parent, method_name); if (f) return f; const CBMRegisteredType *next = cs_lookup_type_qn(ctx, parent); if (!next) continue; if (next->embedded_types) { for (int i = 0; next->embedded_types[i] && frontier_count < CS_LSP_PARENT_WALK_MAX; i++) { frontier[frontier_count++] = next->embedded_types[i]; } } } /* Fall back to System.Object root members (ToString, Equals, GetHashCode). */ if (strcmp(type_qn, "System.Object") != 0) { f = cbm_registry_lookup_method(ctx->registry, "System.Object", method_name); if (f) return f; } return NULL; } /* ── extension methods ──────────────────────────────────────────── */ /* Search for a static extension method `M(this U self, ...)` accessible * via using-imported namespaces. Returns the func entry or NULL. */ static const CBMRegisteredFunc *cs_lookup_extension(CSLSPContext *ctx, const char *receiver_qn, const char *method_name) { if (!ctx->registry || !receiver_qn || !method_name) return NULL; /* Walk every static class accessible in scope. We approximate accessibility * by checking that the function's qualified_name's first dotted prefixes * line up with one of: the file's namespace, an imported using namespace, * or the file's module. */ for (int i = 0; i < ctx->registry->func_count; i++) { const CBMRegisteredFunc *cand = &ctx->registry->funcs[i]; if (!cand->short_name || strcmp(cand->short_name, method_name) != 0) continue; if (cand->receiver_type) continue; /* must be a free static method */ if (!cand->signature || cand->signature->kind != CBM_TYPE_FUNC) continue; const CBMType *sig = cand->signature; if (!sig->data.func.param_types) continue; const CBMType *first = sig->data.func.param_types[0]; if (!first) continue; /* Only consider candidates marked as extensions (param[0] is named * "this" in our convention). param_names[0] == "this self" or "this". */ if (!sig->data.func.param_names || !sig->data.func.param_names[0]) continue; const char *p0 = sig->data.func.param_names[0]; if (strncmp(p0, "this", 4) != 0) continue; /* Receiver compatibility: NAMED receiver must match receiver_qn or * one of receiver_qn's bases. TEMPLATE receiver (e.g. IEnumerable) * matches if receiver type or any base shares the template name. */ bool match = false; if (first->kind == CBM_TYPE_NAMED && first->data.named.qualified_name) { if (strcmp(first->data.named.qualified_name, receiver_qn) == 0) match = true; if (!match && strcmp(first->data.named.qualified_name, "System.Object") == 0) match = true; /* Walk receiver's bases. */ const CBMRegisteredType *rt = cs_lookup_type_qn(ctx, receiver_qn); if (rt && rt->embedded_types && !match) { for (int j = 0; rt->embedded_types[j]; j++) { if (strcmp(rt->embedded_types[j], first->data.named.qualified_name) == 0) { match = true; break; } } } } else if (first->kind == CBM_TYPE_TEMPLATE) { const char *tn = first->data.template_type.template_name; const CBMRegisteredType *rt = cs_lookup_type_qn(ctx, receiver_qn); if (rt && rt->embedded_types) { for (int j = 0; rt->embedded_types[j]; j++) { if (strcmp(rt->embedded_types[j], tn) == 0) { match = true; break; } } } if (!match && strcmp(receiver_qn, tn) == 0) match = true; } if (!match) continue; /* Accessibility check: the candidate's namespace must be in * usings or namespace stack. We'll be lenient here — the user's * project tree generally has consistent namespaces. */ return cand; } return NULL; } /* ── type AST parsing ───────────────────────────────────────────── */ const CBMType *cs_parse_type_node(CSLSPContext *ctx, TSNode node) { if (ts_node_is_null(node)) return cbm_type_unknown(); const char *kind = ts_node_type(node); if (strcmp(kind, "predefined_type") == 0) { char *t = cs_node_text(ctx, node); if (!t) return cbm_type_unknown(); const char *aliased = cs_predefined_alias(t); if (aliased) return cbm_type_named(ctx->arena, aliased); return cbm_type_builtin(ctx->arena, t); } if (strcmp(kind, "nullable_type") == 0) { /* Recurse into the underlying. */ TSNode inner = ts_node_child_by_field_name(node, "type", 4); if (ts_node_is_null(inner)) inner = cs_first_named_child(node); if (ts_node_is_null(inner)) return cbm_type_unknown(); return cs_parse_type_node(ctx, inner); } if (strcmp(kind, "array_type") == 0) { TSNode inner = ts_node_child_by_field_name(node, "type", 4); if (ts_node_is_null(inner)) inner = cs_first_named_child(node); const CBMType *elem = cs_parse_type_node(ctx, inner); return cbm_type_template(ctx->arena, "System.Array", (const CBMType *[]){elem, NULL}, 1); } if (strcmp(kind, "pointer_type") == 0) { TSNode inner = ts_node_child_by_field_name(node, "type", 4); if (ts_node_is_null(inner)) inner = cs_first_named_child(node); return cs_parse_type_node(ctx, inner); } if (strcmp(kind, "tuple_type") == 0) { /* Build a TUPLE of element types. */ uint32_t nc = ts_node_child_count(node); const CBMType **elems = (const CBMType **)cbm_arena_alloc( ctx->arena, (size_t)(nc + 1) * sizeof(*elems)); if (!elems) return cbm_type_unknown(); int count = 0; for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(node, i); if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; const char *ck = ts_node_type(c); if (strcmp(ck, "tuple_element") != 0) continue; TSNode te = ts_node_child_by_field_name(c, "type", 4); if (ts_node_is_null(te)) te = cs_first_named_child(c); elems[count++] = cs_parse_type_node(ctx, te); } return cbm_type_tuple(ctx->arena, elems, count); } if (strcmp(kind, "generic_name") == 0) { TSNode name_node = ts_node_child_by_field_name(node, "name", 4); if (ts_node_is_null(name_node)) name_node = cs_child_named_kind(node, "identifier"); TSNode args_node = ts_node_child_by_field_name(node, "type_arguments", 14); if (ts_node_is_null(args_node)) args_node = cs_child_named_kind(node, "type_argument_list"); if (ts_node_is_null(name_node)) return cbm_type_unknown(); char *raw = cs_node_text(ctx, name_node); if (!raw) return cbm_type_unknown(); const char *resolved = cs_resolve_type_name(ctx, raw); if (!resolved) return cbm_type_unknown(); if (ts_node_is_null(args_node)) { return cbm_type_named(ctx->arena, resolved); } /* Parse args. */ uint32_t nc = ts_node_child_count(args_node); const CBMType **args = (const CBMType **)cbm_arena_alloc( ctx->arena, (size_t)(nc + 1) * sizeof(*args)); if (!args) return cbm_type_named(ctx->arena, resolved); int count = 0; for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(args_node, i); if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; args[count++] = cs_parse_type_node(ctx, c); } args[count] = NULL; return cbm_type_template(ctx->arena, resolved, args, count); } if (strcmp(kind, "qualified_name") == 0 || strcmp(kind, "identifier") == 0 || strcmp(kind, "alias_qualified_name") == 0 || strcmp(kind, "name") == 0) { char *t = cs_node_text(ctx, node); if (!t) return cbm_type_unknown(); const char *resolved = cs_resolve_type_name(ctx, t); if (!resolved) return cbm_type_unknown(); const char *pre = cs_predefined_alias(resolved); if (pre) return cbm_type_named(ctx->arena, pre); return cbm_type_named(ctx->arena, resolved); } if (strcmp(kind, "ref_type") == 0) { TSNode inner = ts_node_child_by_field_name(node, "type", 4); if (ts_node_is_null(inner)) inner = cs_first_named_child(node); const CBMType *t = cs_parse_type_node(ctx, inner); return cbm_type_reference(ctx->arena, t); } if (strcmp(kind, "implicit_type") == 0 || strcmp(kind, "var") == 0) { return cbm_type_unknown(); } /* Fallback: read raw text + resolve. */ char *t = cs_node_text(ctx, node); if (!t) return cbm_type_unknown(); const char *resolved = cs_resolve_type_name(ctx, t); if (!resolved) return cbm_type_unknown(); const char *pre = cs_predefined_alias(resolved); if (pre) return cbm_type_named(ctx->arena, pre); return cbm_type_named(ctx->arena, resolved); } /* ── expression evaluation ──────────────────────────────────────── */ static const CBMType *cs_unwrap_task(CSLSPContext *ctx, const CBMType *t) { (void)ctx; if (!t) return cbm_type_unknown(); if (t->kind == CBM_TYPE_TEMPLATE) { const char *n = t->data.template_type.template_name; if (n && (strcmp(n, "System.Threading.Tasks.Task") == 0 || strcmp(n, "System.Threading.Tasks.ValueTask") == 0)) { if (t->data.template_type.template_args && t->data.template_type.template_args[0]) { return t->data.template_type.template_args[0]; } } } if (t->kind == CBM_TYPE_NAMED) { const char *qn = t->data.named.qualified_name; if (qn && (strcmp(qn, "System.Threading.Tasks.Task") == 0 || strcmp(qn, "System.Threading.Tasks.ValueTask") == 0)) { return cbm_type_unknown(); } } return t; } static const CBMType *cs_unwrap_nullable(const CBMType *t) { if (!t) return t; if (t->kind == CBM_TYPE_TEMPLATE) { const char *n = t->data.template_type.template_name; if (n && strcmp(n, "System.Nullable") == 0) { if (t->data.template_type.template_args && t->data.template_type.template_args[0]) { return t->data.template_type.template_args[0]; } } } return t; } const CBMType *cs_eval_expr_type(CSLSPContext *ctx, TSNode node) { if (ts_node_is_null(node) || ctx->eval_depth >= CS_EVAL_MAX_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) { result = cs_eval_identifier_type(ctx, node); } else if (strcmp(kind, "this_expression") == 0 || strcmp(kind, "this") == 0) { if (ctx->enclosing_class_qn) { result = cbm_type_named(ctx->arena, ctx->enclosing_class_qn); } } else if (strcmp(kind, "base_expression") == 0 || strcmp(kind, "base") == 0) { if (ctx->enclosing_base_qn) { result = cbm_type_named(ctx->arena, ctx->enclosing_base_qn); } } else if (strcmp(kind, "invocation_expression") == 0) { result = cs_eval_invocation_type(ctx, node); } else if (strcmp(kind, "member_access_expression") == 0 || strcmp(kind, "conditional_access_expression") == 0) { result = cs_eval_member_access_type(ctx, node); } else if (strcmp(kind, "object_creation_expression") == 0 || strcmp(kind, "implicit_object_creation_expression") == 0) { result = cs_eval_object_creation_type(ctx, node); } else if (strcmp(kind, "string_literal") == 0 || strcmp(kind, "verbatim_string_literal") == 0 || strcmp(kind, "interpolated_string_expression") == 0 || strcmp(kind, "raw_string_literal") == 0) { result = cbm_type_named(ctx->arena, "System.String"); } else if (strcmp(kind, "character_literal") == 0) { result = cbm_type_named(ctx->arena, "System.Char"); } else if (strcmp(kind, "integer_literal") == 0) { result = cbm_type_named(ctx->arena, "System.Int32"); } else if (strcmp(kind, "real_literal") == 0) { result = cbm_type_named(ctx->arena, "System.Double"); } else if (strcmp(kind, "boolean_literal") == 0) { result = cbm_type_named(ctx->arena, "System.Boolean"); } else if (strcmp(kind, "null_literal") == 0) { result = cbm_type_unknown(); } else if (strcmp(kind, "parenthesized_expression") == 0) { TSNode c = cs_first_named_child(node); if (!ts_node_is_null(c)) result = cs_eval_expr_type(ctx, c); } else if (strcmp(kind, "cast_expression") == 0) { TSNode tnode = ts_node_child_by_field_name(node, "type", 4); if (!ts_node_is_null(tnode)) result = cs_parse_type_node(ctx, tnode); } else if (strcmp(kind, "as_expression") == 0) { /* x as T → T */ TSNode rhs = ts_node_child_by_field_name(node, "right", 5); if (ts_node_is_null(rhs)) { uint32_t nc = ts_node_child_count(node); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(node, i); if (!ts_node_is_null(c) && ts_node_is_named(c)) rhs = c; } } if (!ts_node_is_null(rhs)) result = cs_parse_type_node(ctx, rhs); } else if (strcmp(kind, "is_expression") == 0 || strcmp(kind, "is_pattern_expression") == 0) { result = cbm_type_named(ctx->arena, "System.Boolean"); } else if (strcmp(kind, "await_expression") == 0) { TSNode inner = cs_first_named_child(node); if (!ts_node_is_null(inner)) { const CBMType *t = cs_eval_expr_type(ctx, inner); result = cs_unwrap_task(ctx, t); } } else if (strcmp(kind, "binary_expression") == 0 || strcmp(kind, "prefix_unary_expression") == 0 || strcmp(kind, "postfix_unary_expression") == 0) { /* Best-effort: take left's type. For comparisons, return Boolean. */ TSNode op = ts_node_child_by_field_name(node, "operator", 8); char *opt = ts_node_is_null(op) ? NULL : cs_node_text(ctx, op); if (opt && (strcmp(opt, "==") == 0 || strcmp(opt, "!=") == 0 || strcmp(opt, "<") == 0 || strcmp(opt, ">") == 0 || strcmp(opt, "<=") == 0 || strcmp(opt, ">=") == 0 || strcmp(opt, "&&") == 0 || strcmp(opt, "||") == 0 || strcmp(opt, "!") == 0)) { result = cbm_type_named(ctx->arena, "System.Boolean"); } else { TSNode left = ts_node_child_by_field_name(node, "left", 4); if (ts_node_is_null(left)) left = cs_first_named_child(node); if (!ts_node_is_null(left)) result = cs_eval_expr_type(ctx, left); } } else if (strcmp(kind, "assignment_expression") == 0) { TSNode rhs = ts_node_child_by_field_name(node, "right", 5); if (!ts_node_is_null(rhs)) result = cs_eval_expr_type(ctx, rhs); } else if (strcmp(kind, "conditional_expression") == 0 || strcmp(kind, "switch_expression") == 0) { /* a ? b : c → take b's type. */ TSNode b = ts_node_child_by_field_name(node, "consequence", 11); if (ts_node_is_null(b)) b = ts_node_child_by_field_name(node, "alternative", 11); if (ts_node_is_null(b)) { uint32_t nc = ts_node_child_count(node); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(node, i); if (!ts_node_is_null(c) && ts_node_is_named(c)) { const CBMType *t = cs_eval_expr_type(ctx, c); if (t && t->kind != CBM_TYPE_UNKNOWN) { result = t; break; } } } } else { result = cs_eval_expr_type(ctx, b); } } else if (strcmp(kind, "tuple_expression") == 0) { /* (a, b) -> tuple of element types. */ uint32_t nc = ts_node_child_count(node); const CBMType **elems = (const CBMType **)cbm_arena_alloc(ctx->arena, (size_t)(nc + 1) * sizeof(*elems)); if (!elems) { ctx->eval_depth--; return cbm_type_unknown(); } int count = 0; for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(node, i); if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; elems[count++] = cs_eval_expr_type(ctx, c); } elems[count] = NULL; result = cbm_type_tuple(ctx->arena, elems, count); } else if (strcmp(kind, "lambda_expression") == 0 || strcmp(kind, "anonymous_method_expression") == 0) { /* We approximate the lambda's type as System.Func; we don't * walk the body here. */ result = cbm_type_unknown(); } else if (strcmp(kind, "typeof_expression") == 0) { result = cbm_type_named(ctx->arena, "System.Type"); } else if (strcmp(kind, "nameof_expression") == 0) { result = cbm_type_named(ctx->arena, "System.String"); } else if (strcmp(kind, "default_expression") == 0) { TSNode tnode = ts_node_child_by_field_name(node, "type", 4); if (!ts_node_is_null(tnode)) result = cs_parse_type_node(ctx, tnode); } else if (strcmp(kind, "element_access_expression") == 0) { /* arr[i] — return element type of receiver. */ TSNode obj = ts_node_child_by_field_name(node, "expression", 10); if (ts_node_is_null(obj)) obj = cs_first_named_child(node); if (!ts_node_is_null(obj)) { const CBMType *recv = cs_eval_expr_type(ctx, obj); if (recv && recv->kind == CBM_TYPE_TEMPLATE) { const CBMType *const *args = recv->data.template_type.template_args; if (args) { int n = 0; while (args[n]) n++; if (n >= 2 && args[1]) result = args[1]; else if (n >= 1 && args[0]) result = args[0]; } } } } else if (strcmp(kind, "checked_expression") == 0 || strcmp(kind, "unchecked_expression") == 0) { TSNode c = cs_first_named_child(node); if (!ts_node_is_null(c)) result = cs_eval_expr_type(ctx, c); } else if (strcmp(kind, "array_creation_expression") == 0) { TSNode tnode = ts_node_child_by_field_name(node, "type", 4); if (!ts_node_is_null(tnode)) { const CBMType *elem = cs_parse_type_node(ctx, tnode); result = cbm_type_template(ctx->arena, "System.Array", (const CBMType *[]){elem, NULL}, 1); } } else if (strcmp(kind, "implicit_array_creation_expression") == 0) { result = cbm_type_template(ctx->arena, "System.Array", (const CBMType *[]){cbm_type_unknown(), NULL}, 1); } ctx->eval_depth--; return result ? result : cbm_type_unknown(); } /* ── identifier resolution ──────────────────────────────────────── */ static const CBMType *cs_eval_identifier_type(CSLSPContext *ctx, TSNode node) { char *name = cs_node_text(ctx, node); if (!name) return cbm_type_unknown(); /* Local / parameter scope. cbm_scope_lookup returns cbm_type_unknown() * (a non-NULL singleton) when the name isn't bound, so we need to * check the kind, not just the pointer. */ const CBMType *bound = cbm_scope_lookup(ctx->current_scope, name); if (bound && bound->kind != CBM_TYPE_UNKNOWN) return bound; /* Implicit `this` member. Try to find a field on the enclosing class. */ if (ctx->enclosing_class_qn) { const CBMRegisteredType *rt = cs_lookup_type_qn(ctx, ctx->enclosing_class_qn); if (rt && rt->field_names) { for (int i = 0; rt->field_names[i]; i++) { if (strcmp(rt->field_names[i], name) == 0) { if (rt->field_types && rt->field_types[i]) return rt->field_types[i]; return cbm_type_unknown(); } } } } /* Predefined alias: e.g. `int.Parse(...)` — but that's parsed as a * predefined_type, not identifier. Still: handle bare type names. */ const char *resolved = cs_resolve_type_name(ctx, name); if (resolved) { const CBMRegisteredType *rt = cs_lookup_type_qn(ctx, resolved); if (rt) return cbm_type_named(ctx->arena, rt->qualified_name); } return cbm_type_unknown(); } /* ── invocation ─────────────────────────────────────────────────── */ static int cs_count_args(TSNode args_node) { if (ts_node_is_null(args_node)) return 0; int count = 0; uint32_t nc = ts_node_child_count(args_node); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(args_node, i); if (!ts_node_is_null(c) && ts_node_is_named(c)) { const char *k = ts_node_type(c); if (strcmp(k, "argument") == 0) count++; } } return count; } static const CBMType *cs_eval_invocation_type(CSLSPContext *ctx, TSNode call) { TSNode fn = ts_node_child_by_field_name(call, "function", 8); if (ts_node_is_null(fn)) { /* tree-sitter sometimes uses "expression" instead of "function". */ uint32_t nc = ts_node_child_count(call); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(call, i); if (!ts_node_is_null(c) && ts_node_is_named(c)) { const char *k = ts_node_type(c); if (strcmp(k, "argument_list") == 0) continue; fn = c; break; } } } if (ts_node_is_null(fn)) return cbm_type_unknown(); const char *fk = ts_node_type(fn); /* Member call: `recv.Method` or `recv?.Method`. */ if (strcmp(fk, "member_access_expression") == 0 || strcmp(fk, "conditional_access_expression") == 0) { TSNode recv = ts_node_child_by_field_name(fn, "expression", 10); TSNode name = ts_node_child_by_field_name(fn, "name", 4); if (ts_node_is_null(recv)) recv = cs_first_named_child(fn); if (ts_node_is_null(name)) { uint32_t fnc = ts_node_child_count(fn); for (uint32_t i = 0; i < fnc; i++) { TSNode c = ts_node_child(fn, i); if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; const char *k = ts_node_type(c); if (strcmp(k, "identifier") == 0 || strcmp(k, "generic_name") == 0) name = c; } } if (ts_node_is_null(name)) return cbm_type_unknown(); char *mname = cs_node_text(ctx, name); if (!mname) return cbm_type_unknown(); char *bare_name = cs_strip_generic_args(ctx->arena, mname); const CBMType *recv_type = cbm_type_unknown(); if (!ts_node_is_null(recv)) recv_type = cs_eval_expr_type(ctx, recv); recv_type = cs_unwrap_nullable(recv_type); const char *type_qn = NULL; if (recv_type && recv_type->kind == CBM_TYPE_NAMED) { type_qn = recv_type->data.named.qualified_name; } else if (recv_type && recv_type->kind == CBM_TYPE_TEMPLATE) { type_qn = recv_type->data.template_type.template_name; } if (!type_qn) return cbm_type_unknown(); const CBMRegisteredFunc *f = cs_lookup_method(ctx, type_qn, bare_name); if (!f) f = cs_lookup_extension(ctx, type_qn, bare_name); if (!f || !f->signature) return cbm_type_unknown(); if (f->signature->kind == CBM_TYPE_FUNC && f->signature->data.func.return_types && f->signature->data.func.return_types[0]) { const CBMType *ret = f->signature->data.func.return_types[0]; /* If receiver is templated and method's return type contains type * parameters, substitute. */ if (recv_type && recv_type->kind == CBM_TYPE_TEMPLATE) { const CBMRegisteredType *rt = cs_lookup_type_qn(ctx, type_qn); if (rt && rt->type_param_names && recv_type->data.template_type.template_args) { return cs_substitute_type_params( ctx->arena, ret, rt->type_param_names, recv_type->data.template_type.template_args); } } return ret; } return cbm_type_unknown(); } /* Bare invocation: `Method(args)`. */ if (strcmp(fk, "identifier") == 0 || strcmp(fk, "generic_name") == 0) { char *fname = cs_node_text(ctx, fn); if (!fname) return cbm_type_unknown(); char *bare = cs_strip_generic_args(ctx->arena, fname); /* Contextual keywords that tree-sitter parses as invocation * expressions but the C# language treats specially. */ if (strcmp(bare, "nameof") == 0) { return cbm_type_named(ctx->arena, "System.String"); } if (strcmp(bare, "typeof") == 0) { return cbm_type_named(ctx->arena, "System.Type"); } /* Try enclosing class member. */ if (ctx->enclosing_class_qn) { const CBMRegisteredFunc *f = cs_lookup_method(ctx, ctx->enclosing_class_qn, bare); if (f && f->signature && f->signature->kind == CBM_TYPE_FUNC && f->signature->data.func.return_types && f->signature->data.func.return_types[0]) { return f->signature->data.func.return_types[0]; } } /* Try free function in current namespace / using static targets. */ const char *ns = cs_namespace_qn(ctx); if (ns && *ns) { const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ns, bare); const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, qn); if (f && f->signature && f->signature->data.func.return_types && f->signature->data.func.return_types[0]) { return f->signature->data.func.return_types[0]; } } for (int i = 0; i < ctx->using_count; i++) { const CBMCSUsing *u = &ctx->usings[i]; if (u->kind != CBM_CS_USING_STATIC) continue; const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", u->target_qn, bare); const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, qn); if (f && f->signature && f->signature->data.func.return_types && f->signature->data.func.return_types[0]) { return f->signature->data.func.return_types[0]; } } } return cbm_type_unknown(); } /* ── member access (no invocation) ──────────────────────────────── */ static const CBMType *cs_eval_member_access_type(CSLSPContext *ctx, TSNode node) { TSNode obj = ts_node_child_by_field_name(node, "expression", 10); TSNode name = ts_node_child_by_field_name(node, "name", 4); if (ts_node_is_null(obj)) obj = cs_first_named_child(node); if (ts_node_is_null(name)) { uint32_t nc = ts_node_child_count(node); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(node, i); if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; const char *k = ts_node_type(c); if (strcmp(k, "identifier") == 0 || strcmp(k, "generic_name") == 0) name = c; } } if (ts_node_is_null(obj) || ts_node_is_null(name)) return cbm_type_unknown(); char *fname = cs_node_text(ctx, name); if (!fname) return cbm_type_unknown(); fname = cs_strip_generic_args(ctx->arena, fname); /* If obj is a type identifier → static member access */ char *obj_text = cs_node_text(ctx, obj); if (obj_text) { const char *type_qn = cs_resolve_type_name(ctx, obj_text); if (type_qn && cs_lookup_type_qn(ctx, type_qn)) { const CBMRegisteredType *rt = cs_lookup_type_qn(ctx, type_qn); if (rt && rt->field_names) { for (int i = 0; rt->field_names[i]; i++) { if (strcmp(rt->field_names[i], fname) == 0) { if (rt->field_types && rt->field_types[i]) return rt->field_types[i]; } } } /* Try as method (returns its return type, which is what * `Math.Sqrt(...)` would propagate via invocation). */ const CBMRegisteredFunc *f = cs_lookup_method(ctx, type_qn, fname); if (f && f->signature && f->signature->kind == CBM_TYPE_FUNC && f->signature->data.func.return_types && f->signature->data.func.return_types[0]) { return f->signature->data.func.return_types[0]; } } } const CBMType *recv = cs_eval_expr_type(ctx, obj); recv = cs_unwrap_nullable(recv); if (!recv) return cbm_type_unknown(); const char *type_qn = NULL; if (recv->kind == CBM_TYPE_NAMED) { type_qn = recv->data.named.qualified_name; } else if (recv->kind == CBM_TYPE_TEMPLATE) { type_qn = recv->data.template_type.template_name; } if (!type_qn) return cbm_type_unknown(); const CBMRegisteredType *rt = cs_lookup_type_qn(ctx, type_qn); if (rt && rt->field_names) { for (int i = 0; rt->field_names[i]; i++) { if (strcmp(rt->field_names[i], fname) == 0) { if (rt->field_types && rt->field_types[i]) { const CBMType *ft = rt->field_types[i]; if (recv->kind == CBM_TYPE_TEMPLATE && rt->type_param_names && recv->data.template_type.template_args) { return cs_substitute_type_params( ctx->arena, ft, rt->type_param_names, recv->data.template_type.template_args); } return ft; } } } } /* Treat as property — call lookup_method with `get_` or just `name`. * Properties are stored as methods in our registry with return type. */ const CBMRegisteredFunc *f = cs_lookup_method(ctx, type_qn, fname); if (f && f->signature && f->signature->kind == CBM_TYPE_FUNC && f->signature->data.func.return_types && f->signature->data.func.return_types[0]) { const CBMType *ret = f->signature->data.func.return_types[0]; if (recv->kind == CBM_TYPE_TEMPLATE && rt && rt->type_param_names && recv->data.template_type.template_args) { return cs_substitute_type_params(ctx->arena, ret, rt->type_param_names, recv->data.template_type.template_args); } return ret; } return cbm_type_unknown(); } /* ── object creation ────────────────────────────────────────────── */ static const CBMType *cs_eval_object_creation_type(CSLSPContext *ctx, TSNode node) { if (cs_node_is(node, "implicit_object_creation_expression")) { /* `new(...)` — target-typed; we don't have the target binding here. */ return cbm_type_unknown(); } TSNode tnode = ts_node_child_by_field_name(node, "type", 4); if (ts_node_is_null(tnode)) { uint32_t nc = ts_node_child_count(node); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(node, i); if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; const char *k = ts_node_type(c); if (strcmp(k, "argument_list") == 0 || strcmp(k, "initializer_expression") == 0) continue; tnode = c; break; } } if (ts_node_is_null(tnode)) return cbm_type_unknown(); return cs_parse_type_node(ctx, tnode); } /* ── generic substitution ───────────────────────────────────────── */ static const CBMType *cs_substitute_type_params(CBMArena *arena, const CBMType *t, const char **param_names, const CBMType **param_args) { if (!t || !param_names || !param_args) return t; if (t->kind == CBM_TYPE_NAMED) { const char *qn = t->data.named.qualified_name; if (!qn) return t; for (int i = 0; param_names[i]; i++) { if (strcmp(param_names[i], qn) == 0) { /* If the type is `T` and we have an arg for T, substitute. */ return param_args[i] ? param_args[i] : t; } } return t; } if (t->kind == CBM_TYPE_TEMPLATE) { const CBMType *const *old_args = t->data.template_type.template_args; if (!old_args) return t; int n = 0; while (old_args[n]) n++; const CBMType **new_args = (const CBMType **)cbm_arena_alloc( arena, (size_t)(n + 1) * sizeof(*new_args)); if (!new_args) return t; for (int i = 0; i < n; i++) { new_args[i] = cs_substitute_type_params(arena, old_args[i], param_names, param_args); } new_args[n] = NULL; return cbm_type_template(arena, t->data.template_type.template_name, new_args, n); } if (t->kind == CBM_TYPE_TYPE_PARAM) { const char *p = t->data.type_param.name; if (!p) return t; for (int i = 0; param_names[i]; i++) { if (strcmp(param_names[i], p) == 0) return param_args[i] ? param_args[i] : t; } } return t; } /* ── parameter binding ──────────────────────────────────────────── */ static void cs_bind_parameters(CSLSPContext *ctx, TSNode params_node, bool is_extension) { if (ts_node_is_null(params_node)) return; uint32_t nc = ts_node_child_count(params_node); int idx = 0; for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(params_node, i); if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; const char *k = ts_node_type(c); if (strcmp(k, "parameter") != 0) continue; /* Detect `this` modifier (extension methods). */ bool has_this = is_extension && (idx == 0); TSNode tnode = ts_node_child_by_field_name(c, "type", 4); TSNode nnode = ts_node_child_by_field_name(c, "name", 4); if (ts_node_is_null(tnode) || ts_node_is_null(nnode)) { uint32_t pc = ts_node_child_count(c); for (uint32_t j = 0; j < pc; j++) { TSNode cc = ts_node_child(c, j); if (ts_node_is_null(cc) || !ts_node_is_named(cc)) continue; const char *ck = ts_node_type(cc); if (strcmp(ck, "identifier") == 0 && ts_node_is_null(nnode)) nnode = cc; else if (ts_node_is_null(tnode) && strcmp(ck, "identifier") != 0) tnode = cc; } } if (ts_node_is_null(nnode)) { idx++; continue; } char *pname = cs_node_text(ctx, nnode); if (!pname) { idx++; continue; } const CBMType *ptype = cbm_type_unknown(); if (!ts_node_is_null(tnode)) ptype = cs_parse_type_node(ctx, tnode); cbm_scope_bind(ctx->current_scope, pname, ptype); (void)has_this; idx++; } } /* ── statement processing ───────────────────────────────────────── */ static void cs_process_local_decl(CSLSPContext *ctx, TSNode node) { /* local_declaration_statement -> variable_declaration */ TSNode vd = cs_child_named_kind(node, "variable_declaration"); if (ts_node_is_null(vd)) return; TSNode tnode = ts_node_child_by_field_name(vd, "type", 4); if (ts_node_is_null(tnode)) tnode = cs_first_named_child(vd); /* For each declarator, bind the variable to (rhs_type or declared type). */ uint32_t nc = ts_node_child_count(vd); bool is_var = false; if (!ts_node_is_null(tnode)) { char *tt = cs_node_text(ctx, tnode); if (tt && (strcmp(tt, "var") == 0 || strcmp(ts_node_type(tnode), "implicit_type") == 0)) { is_var = true; } } const CBMType *declared_type = ts_node_is_null(tnode) ? cbm_type_unknown() : cs_parse_type_node(ctx, tnode); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(vd, i); if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; const char *k = ts_node_type(c); if (strcmp(k, "variable_declarator") != 0) continue; TSNode nm = cs_child_named_kind(c, "identifier"); if (ts_node_is_null(nm)) { nm = ts_node_child_by_field_name(c, "name", 4); } if (ts_node_is_null(nm)) continue; char *vname = cs_node_text(ctx, nm); if (!vname) continue; /* Find initializer (= rhs). variable_declarator children depend on * the grammar: tree-sitter-c-sharp emits `_identifier_or_global` * (the name) + optional `=` token + value-expression; the value * may also be wrapped in equals_value_clause in some grammar * variants. Try the field-name path first, then fall back to * walking named children for any expression after the identifier. */ TSNode init = ts_node_child_by_field_name(c, "value", 5); const CBMType *rhs_t = NULL; if (!ts_node_is_null(init)) { if (strcmp(ts_node_type(init), "equals_value_clause") == 0) { TSNode rhs = cs_first_named_child(init); if (!ts_node_is_null(rhs)) rhs_t = cs_eval_expr_type(ctx, rhs); } else { rhs_t = cs_eval_expr_type(ctx, init); } } if (!rhs_t || rhs_t->kind == CBM_TYPE_UNKNOWN) { /* Walk named children: skip identifier (the name), evaluate * the next named child as the rhs expression. Also catch the * legacy equals_value_clause shape. */ uint32_t cc = ts_node_child_count(c); int seen_named = 0; for (uint32_t j = 0; j < cc; j++) { TSNode cn = ts_node_child(c, j); if (ts_node_is_null(cn) || !ts_node_is_named(cn)) continue; const char *ck = ts_node_type(cn); if (strcmp(ck, "equals_value_clause") == 0) { TSNode rhs = cs_first_named_child(cn); if (!ts_node_is_null(rhs)) { const CBMType *t = cs_eval_expr_type(ctx, rhs); if (t && t->kind != CBM_TYPE_UNKNOWN) rhs_t = t; } break; } if (seen_named == 0) { seen_named++; continue; /* the identifier name */ } if (seen_named >= 1) { const CBMType *t = cs_eval_expr_type(ctx, cn); if (t && t->kind != CBM_TYPE_UNKNOWN) { rhs_t = t; break; } } } } const CBMType *bind = is_var ? (rhs_t ? rhs_t : declared_type) : declared_type; if (!bind) bind = cbm_type_unknown(); cbm_scope_bind(ctx->current_scope, vname, bind); } } static void cs_process_foreach(CSLSPContext *ctx, TSNode node) { /* foreach_statement: type, identifier, expression. */ TSNode tnode = ts_node_child_by_field_name(node, "type", 4); TSNode nnode = ts_node_child_by_field_name(node, "left", 4); if (ts_node_is_null(nnode)) nnode = cs_child_named_kind(node, "identifier"); TSNode iter = ts_node_child_by_field_name(node, "right", 5); if (ts_node_is_null(iter)) { /* Fallback: look for an expression-shaped child after the type. */ uint32_t nc = ts_node_child_count(node); bool past_id = false; for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(node, i); if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; const char *k = ts_node_type(c); if (strcmp(k, "identifier") == 0 && !past_id) { past_id = true; continue; } if (past_id) { if (strcmp(k, "block") == 0) break; iter = c; break; } } } if (ts_node_is_null(nnode)) return; char *vname = cs_node_text(ctx, nnode); if (!vname) return; const CBMType *element_t = cbm_type_unknown(); if (!ts_node_is_null(iter)) { const CBMType *iter_t = cs_eval_expr_type(ctx, iter); if (iter_t && iter_t->kind == CBM_TYPE_TEMPLATE) { const CBMType *const *args = iter_t->data.template_type.template_args; if (args && args[0]) element_t = args[0]; } } if (!ts_node_is_null(tnode)) { const CBMType *declared = cs_parse_type_node(ctx, tnode); char *tt = cs_node_text(ctx, tnode); bool is_var = (tt && (strcmp(tt, "var") == 0)); if (!is_var && declared && declared->kind != CBM_TYPE_UNKNOWN) element_t = declared; } cbm_scope_bind(ctx->current_scope, vname, element_t); } static void cs_process_using_statement(CSLSPContext *ctx, TSNode node) { /* `using (var x = expr) { ... }` — bind x. */ TSNode vd = cs_child_named_kind(node, "variable_declaration"); if (!ts_node_is_null(vd)) { /* Wrap as if it were a local_declaration_statement and dispatch. */ TSNode wrapper; memset(&wrapper, 0, sizeof(wrapper)); cs_process_local_decl(ctx, node); } } static void cs_process_assignment(CSLSPContext *ctx, TSNode node) { TSNode lhs = ts_node_child_by_field_name(node, "left", 4); TSNode rhs = ts_node_child_by_field_name(node, "right", 5); if (ts_node_is_null(lhs) || ts_node_is_null(rhs)) return; if (!cs_node_is(lhs, "identifier")) return; char *vname = cs_node_text(ctx, lhs); if (!vname) return; const CBMType *t = cs_eval_expr_type(ctx, rhs); if (!t || t->kind == CBM_TYPE_UNKNOWN) return; /* Don't override more-specific bindings with weaker ones. */ const CBMType *existing = cbm_scope_lookup(ctx->current_scope, vname); if (existing && existing->kind == CBM_TYPE_NAMED && t->kind == CBM_TYPE_UNKNOWN) return; cbm_scope_bind(ctx->current_scope, vname, t); } /* ── call resolution + emit ─────────────────────────────────────── */ static void cs_emit_resolved(CSLSPContext *ctx, const char *callee_qn, const char *strategy, float confidence) { if (!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; rc.confidence = confidence; rc.reason = NULL; cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc); } static void cs_resolve_invocation(CSLSPContext *ctx, TSNode call) { TSNode fn = ts_node_child_by_field_name(call, "function", 8); if (ts_node_is_null(fn)) { uint32_t nc = ts_node_child_count(call); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(call, i); if (!ts_node_is_null(c) && ts_node_is_named(c)) { const char *k = ts_node_type(c); if (strcmp(k, "argument_list") == 0) continue; fn = c; break; } } } if (ts_node_is_null(fn)) return; const char *fk = ts_node_type(fn); /* Member call. */ if (strcmp(fk, "member_access_expression") == 0 || strcmp(fk, "conditional_access_expression") == 0) { TSNode recv = ts_node_child_by_field_name(fn, "expression", 10); TSNode name = ts_node_child_by_field_name(fn, "name", 4); if (ts_node_is_null(recv)) recv = cs_first_named_child(fn); if (ts_node_is_null(name)) { uint32_t fnc = ts_node_child_count(fn); for (uint32_t i = 0; i < fnc; i++) { TSNode c = ts_node_child(fn, i); if (!ts_node_is_null(c) && ts_node_is_named(c)) { const char *k = ts_node_type(c); if (strcmp(k, "identifier") == 0 || strcmp(k, "generic_name") == 0) name = c; } } } if (ts_node_is_null(name)) return; char *mname = cs_node_text(ctx, name); if (!mname) return; char *bare = cs_strip_generic_args(ctx->arena, mname); /* Static member call: receiver is a type identifier. */ char *recv_text = ts_node_is_null(recv) ? NULL : cs_node_text(ctx, recv); if (recv_text) { const char *type_qn = cs_resolve_type_name(ctx, recv_text); if (type_qn && cs_lookup_type_qn(ctx, type_qn)) { const CBMRegisteredFunc *f = cs_lookup_method(ctx, type_qn, bare); if (f) { cs_emit_resolved(ctx, f->qualified_name, "cs_static_typed", 0.95f); return; } /* Type known, method not in registry — synth a call. */ cs_emit_resolved(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, bare), "cs_static_typed_unindexed", 0.55f); return; } } /* Instance call. */ const CBMType *recv_type = cbm_type_unknown(); if (!ts_node_is_null(recv)) recv_type = cs_eval_expr_type(ctx, recv); recv_type = cs_unwrap_nullable(recv_type); const char *type_qn = NULL; if (recv_type && recv_type->kind == CBM_TYPE_NAMED) { type_qn = recv_type->data.named.qualified_name; } else if (recv_type && recv_type->kind == CBM_TYPE_TEMPLATE) { type_qn = recv_type->data.template_type.template_name; } if (!type_qn) return; const CBMRegisteredFunc *f = cs_lookup_method(ctx, type_qn, bare); if (f) { const char *strategy = (f->receiver_type && strcmp(f->receiver_type, type_qn) == 0) ? "cs_method_typed" : "cs_method_inherited"; cs_emit_resolved(ctx, f->qualified_name, strategy, 0.95f); return; } /* Try extension method. */ f = cs_lookup_extension(ctx, type_qn, bare); if (f) { cs_emit_resolved(ctx, f->qualified_name, "cs_extension_method", 0.90f); return; } /* Type known, method missing — emit unindexed marker so the textual * fallback in the pipeline is suppressed. */ cs_emit_resolved(ctx, cbm_arena_sprintf(ctx->arena, "%s.%s", type_qn, bare), "cs_method_typed_unindexed", 0.55f); return; } /* Bare invocation: `Method()` */ if (strcmp(fk, "identifier") == 0 || strcmp(fk, "generic_name") == 0) { char *fname = cs_node_text(ctx, fn); if (!fname) return; char *bare = cs_strip_generic_args(ctx->arena, fname); /* Try enclosing class member. cs_lookup_method walks the base chain, so * a bare call may resolve to an INHERITED method. Distinguish, exactly * as the instance-call path does: a method actually declared on the * enclosing class is cs_self_method; one found on a base is * cs_inherited_method. */ if (ctx->enclosing_class_qn) { const CBMRegisteredFunc *f = cs_lookup_method(ctx, ctx->enclosing_class_qn, bare); if (f) { bool own = f->receiver_type && strcmp(f->receiver_type, ctx->enclosing_class_qn) == 0; cs_emit_resolved(ctx, f->qualified_name, own ? "cs_self_method" : "cs_inherited_method", own ? 0.95f : 0.92f); return; } } /* Try base class chain explicitly (for `base.Foo()` style calls * happen via base_expression handled above; this is for inherited * methods invoked without `this.`). */ if (ctx->enclosing_base_qn) { const CBMRegisteredFunc *f = cs_lookup_method(ctx, ctx->enclosing_base_qn, bare); if (f) { cs_emit_resolved(ctx, f->qualified_name, "cs_inherited_method", 0.92f); return; } } /* Try `using static` imports. The directive target is the namespace- * qualified name as written ("Demo.MathUtil"), but types register under * the file-stem QN ("proj.Client.MathUtil"); resolve the target through * the type-name resolver (its short-name fallback bridges the two) * before the method lookup. */ for (int i = 0; i < ctx->using_count; i++) { const CBMCSUsing *u = &ctx->usings[i]; if (u->kind != CBM_CS_USING_STATIC) continue; const char *host = cs_resolve_type_name(ctx, u->target_qn); const CBMRegisteredFunc *f = cs_lookup_method(ctx, host ? host : u->target_qn, bare); if (f) { cs_emit_resolved(ctx, f->qualified_name, "cs_using_static", 0.90f); return; } } /* Free function in current namespace. */ const char *ns = cs_namespace_qn(ctx); if (ns && *ns) { const char *qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ns, bare); const CBMRegisteredFunc *f = cbm_registry_lookup_func(ctx->registry, qn); if (f) { cs_emit_resolved(ctx, f->qualified_name, "cs_namespace_func", 0.92f); return; } } /* Last resort: any free function with this short name in registry. */ const CBMRegisteredFunc *best = NULL; int best_score = -1; for (int i = 0; ctx->registry && i < ctx->registry->func_count; i++) { const CBMRegisteredFunc *cand = &ctx->registry->funcs[i]; if (cand->receiver_type) continue; if (!cand->short_name || strcmp(cand->short_name, bare) != 0) continue; int score = 0; if (cand->qualified_name && ctx->module_qn) { const char *m = ctx->module_qn; const char *q = cand->qualified_name; while (*m && *q && *m == *q) { if (*m == '.') score++; m++; q++; } } if (score > best_score) { best_score = score; best = cand; } } if (best) { cs_emit_resolved(ctx, best->qualified_name, "cs_free_func_fallback", 0.65f); return; } } } static void cs_resolve_object_creation(CSLSPContext *ctx, TSNode call) { /* `new Foo(...)` adds an implicit constructor CALLS edge: to Foo's ctor * Method node when one is indexed, otherwise to the Foo class node. */ TSNode tnode = ts_node_child_by_field_name(call, "type", 4); if (ts_node_is_null(tnode)) return; const CBMType *t = cs_parse_type_node(ctx, tnode); const char *tqn = NULL; if (t && t->kind == CBM_TYPE_NAMED) tqn = t->data.named.qualified_name; else if (t && t->kind == CBM_TYPE_TEMPLATE) tqn = t->data.template_type.template_name; if (!tqn) return; /* A C# constructor is extracted as a Method whose short name is the class's * short name (the constructor_declaration `name` field is the class * identifier), so the ctor QN is `.` — never ".ctor". * Look it up by the class short name, mirroring the Java resolver. */ const char *dot = strrchr(tqn, '.'); const char *short_name = dot ? dot + 1 : tqn; const CBMRegisteredFunc *f = cs_lookup_method(ctx, tqn, short_name); if (f) { cs_emit_resolved(ctx, f->qualified_name, "cs_ctor", 0.95f); return; } /* No explicit constructor in the registry. Resolve the `new Foo()` call to * the Foo CLASS node (`tqn`): its short name equals the call's textual * callee_name ("Foo"), so the pipeline join matches, and the class node * always exists, so a CALLS edge forms carrying the strategy — rather than * the old `Foo..ctor`, whose ".ctor" short name joined nothing and resolved * to no node. */ cs_emit_resolved(ctx, tqn, "cs_ctor_synthetic", 0.85f); } static void cs_resolve_calls_in_node(CSLSPContext *ctx, TSNode node) { if (ts_node_is_null(node)) return; const char *kind = ts_node_type(node); /* Scope-shaping nodes. */ if (strcmp(kind, "block") == 0) { CBMScope *prev = ctx->current_scope; ctx->current_scope = cbm_scope_push(ctx->arena, prev); // Cursor walk (O(n)); ts_node_child(node,i) is O(i) → O(n²) on a wide block. TSTreeCursor cursor = ts_tree_cursor_new(node); if (ts_tree_cursor_goto_first_child(&cursor)) { do { cs_resolve_calls_in_node(ctx, ts_tree_cursor_current_node(&cursor)); } while (ts_tree_cursor_goto_next_sibling(&cursor)); } ts_tree_cursor_delete(&cursor); ctx->current_scope = prev; return; } if (strcmp(kind, "local_declaration_statement") == 0) { cs_process_local_decl(ctx, node); } else if (strcmp(kind, "for_each_statement") == 0 || strcmp(kind, "foreach_statement") == 0) { cs_process_foreach(ctx, node); } else if (strcmp(kind, "using_statement") == 0) { cs_process_using_statement(ctx, node); } else if (strcmp(kind, "assignment_expression") == 0) { cs_process_assignment(ctx, node); } else if (strcmp(kind, "invocation_expression") == 0) { cs_resolve_invocation(ctx, node); } else if (strcmp(kind, "object_creation_expression") == 0) { cs_resolve_object_creation(ctx, node); } /* Recurse into children. We do NOT pre-bind anything that would only be * available after the child is processed — left-to-right walk is fine for * most C# constructs at a Light-Semantic-Pass level. */ // Cursor walk (O(n)); ts_node_child(node,i) is O(i) → O(n²) on a wide node. TSTreeCursor cursor = ts_tree_cursor_new(node); if (!ts_tree_cursor_goto_first_child(&cursor)) { ts_tree_cursor_delete(&cursor); return; } do { TSNode c = ts_tree_cursor_current_node(&cursor); const char *ck = ts_node_type(c); /* Don't recurse into nested type/method bodies — they're processed by * the pass-2 walker which rebuilds enclosing context. */ if (strcmp(ck, "class_declaration") == 0 || strcmp(ck, "struct_declaration") == 0 || strcmp(ck, "record_declaration") == 0 || strcmp(ck, "interface_declaration") == 0 || strcmp(ck, "enum_declaration") == 0 || strcmp(ck, "method_declaration") == 0 || strcmp(ck, "constructor_declaration") == 0 || strcmp(ck, "destructor_declaration") == 0 || strcmp(ck, "operator_declaration") == 0 || strcmp(ck, "conversion_operator_declaration") == 0 || strcmp(ck, "indexer_declaration") == 0 || strcmp(ck, "property_declaration") == 0 || strcmp(ck, "event_declaration") == 0 || strcmp(ck, "local_function_statement") == 0 || strcmp(ck, "namespace_declaration") == 0 || strcmp(ck, "file_scoped_namespace_declaration") == 0) { continue; } cs_resolve_calls_in_node(ctx, c); } while (ts_tree_cursor_goto_next_sibling(&cursor)); ts_tree_cursor_delete(&cursor); } /* ── method/constructor processing ──────────────────────────────── */ static void cs_collect_type_params(CSLSPContext *ctx, TSNode node, const char ***out_names, int *out_count) { *out_names = NULL; *out_count = 0; TSNode tplist = ts_node_child_by_field_name(node, "type_parameters", 15); if (ts_node_is_null(tplist)) tplist = cs_child_named_kind(node, "type_parameter_list"); if (ts_node_is_null(tplist)) return; uint32_t nc = ts_node_child_count(tplist); int cap = 4; const char **arr = (const char **)cbm_arena_alloc(ctx->arena, (size_t)cap * sizeof(*arr)); if (!arr) return; int n = 0; for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(tplist, i); if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; if (strcmp(ts_node_type(c), "type_parameter") != 0) continue; TSNode id = cs_first_named_child(c); if (ts_node_is_null(id)) continue; char *name = cs_node_text(ctx, id); if (!name) continue; if (n + 1 >= cap) { int new_cap = cap * 2; const char **ne = (const char **)cbm_arena_alloc(ctx->arena, (size_t)new_cap * sizeof(*ne)); if (!ne) break; for (int j = 0; j < n; j++) ne[j] = arr[j]; arr = ne; cap = new_cap; } arr[n++] = name; } arr[n] = NULL; *out_names = arr; *out_count = n; } static void cs_process_function_like(CSLSPContext *ctx, TSNode node) { const char *kind = ts_node_type(node); bool is_method = (strcmp(kind, "method_declaration") == 0 || strcmp(kind, "local_function_statement") == 0); bool is_ctor = strcmp(kind, "constructor_declaration") == 0; bool is_dtor = strcmp(kind, "destructor_declaration") == 0; bool is_property = strcmp(kind, "property_declaration") == 0; bool is_indexer = strcmp(kind, "indexer_declaration") == 0; bool is_op = strcmp(kind, "operator_declaration") == 0 || strcmp(kind, "conversion_operator_declaration") == 0; CBMScope *saved_scope = ctx->current_scope; const char *saved_func = ctx->enclosing_func_qn; const char **saved_tp = ctx->type_param_names; const CBMType **saved_tp_args = ctx->type_param_args; int saved_tp_count = ctx->type_param_count; ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope); /* Determine func short name + QN. */ const char *short_name = NULL; if (is_method) { TSNode nm = ts_node_child_by_field_name(node, "name", 4); if (!ts_node_is_null(nm)) short_name = cs_node_text(ctx, nm); } else if (is_ctor) { short_name = ".ctor"; } else if (is_dtor) { short_name = ".dtor"; } else if (is_property) { TSNode nm = ts_node_child_by_field_name(node, "name", 4); if (!ts_node_is_null(nm)) short_name = cs_node_text(ctx, nm); } else if (is_indexer) { short_name = "this[]"; } else if (is_op) { TSNode op = ts_node_child_by_field_name(node, "operator", 8); if (!ts_node_is_null(op)) short_name = cs_node_text(ctx, op); } if (short_name) { if (ctx->enclosing_class_qn) { ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->enclosing_class_qn, short_name); } else if (ctx->module_qn) { ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, short_name); } else { ctx->enclosing_func_qn = cbm_arena_strdup(ctx->arena, short_name); } } /* Collect generic type parameters. */ if (is_method) { const char **tp_names = NULL; int tp_count = 0; cs_collect_type_params(ctx, node, &tp_names, &tp_count); if (tp_count > 0) { const CBMType **args = (const CBMType **)cbm_arena_alloc(ctx->arena, (size_t)(tp_count + 1) * sizeof(*args)); if (args) { for (int i = 0; i < tp_count; i++) { args[i] = cbm_type_type_param(ctx->arena, tp_names[i]); } args[tp_count] = NULL; ctx->type_param_names = tp_names; ctx->type_param_args = args; ctx->type_param_count = tp_count; } } } /* Bind parameters. Detect extension method (first param has `this`). */ bool is_extension = false; if (is_method) { TSNode params = ts_node_child_by_field_name(node, "parameters", 10); if (ts_node_is_null(params)) params = cs_child_named_kind(node, "parameter_list"); if (!ts_node_is_null(params)) { /* Detect `this` modifier on first parameter. */ uint32_t nc = ts_node_child_count(params); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(params, i); if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; if (strcmp(ts_node_type(c), "parameter") != 0) continue; /* Look for "this" keyword child. */ uint32_t pc = ts_node_child_count(c); for (uint32_t j = 0; j < pc; j++) { TSNode cc = ts_node_child(c, j); if (ts_node_is_null(cc)) continue; if (strcmp(ts_node_type(cc), "this") == 0) { is_extension = true; break; } } break; } cs_bind_parameters(ctx, params, is_extension); } } else if (is_indexer) { TSNode params = cs_child_named_kind(node, "bracketed_parameter_list"); if (!ts_node_is_null(params)) cs_bind_parameters(ctx, params, false); } else if (is_ctor || is_op) { TSNode params = cs_child_named_kind(node, "parameter_list"); if (!ts_node_is_null(params)) cs_bind_parameters(ctx, params, false); } /* Walk body. */ TSNode body = ts_node_child_by_field_name(node, "body", 4); if (ts_node_is_null(body)) { /* Expression-bodied: => expr; */ TSNode arrow = cs_child_named_kind(node, "arrow_expression_clause"); if (!ts_node_is_null(arrow)) { TSNode expr = cs_first_named_child(arrow); if (!ts_node_is_null(expr)) cs_resolve_calls_in_node(ctx, expr); } } else { cs_resolve_calls_in_node(ctx, body); } /* Property accessors */ if (is_property || is_indexer) { TSNode accessors = cs_child_named_kind(node, "accessor_list"); if (!ts_node_is_null(accessors)) { uint32_t nc = ts_node_child_count(accessors); for (uint32_t i = 0; i < nc; i++) { TSNode a = ts_node_child(accessors, i); if (ts_node_is_null(a) || !ts_node_is_named(a)) continue; if (strcmp(ts_node_type(a), "accessor_declaration") != 0) continue; TSNode abody = ts_node_child_by_field_name(a, "body", 4); if (ts_node_is_null(abody)) abody = cs_child_named_kind(a, "block"); if (!ts_node_is_null(abody)) cs_resolve_calls_in_node(ctx, abody); else { TSNode arrow = cs_child_named_kind(a, "arrow_expression_clause"); if (!ts_node_is_null(arrow)) { TSNode expr = cs_first_named_child(arrow); if (!ts_node_is_null(expr)) cs_resolve_calls_in_node(ctx, expr); } } } } } /* Restore. */ ctx->current_scope = saved_scope; ctx->enclosing_func_qn = saved_func; ctx->type_param_names = saved_tp; ctx->type_param_args = saved_tp_args; ctx->type_param_count = saved_tp_count; } /* ── type declaration processing ────────────────────────────────── */ static void cs_process_type_decl(CSLSPContext *ctx, TSNode node) { const char *kind = ts_node_type(node); bool is_class = strcmp(kind, "class_declaration") == 0; bool is_struct = strcmp(kind, "struct_declaration") == 0; bool is_record = strcmp(kind, "record_declaration") == 0 || strcmp(kind, "record_struct_declaration") == 0; bool is_iface = strcmp(kind, "interface_declaration") == 0; bool is_enum = strcmp(kind, "enum_declaration") == 0; (void)is_struct; (void)is_record; (void)is_iface; TSNode nm = ts_node_child_by_field_name(node, "name", 4); if (ts_node_is_null(nm)) { ctx->debug && fprintf(stderr, "[cs_lsp] type decl missing name\n"); return; } char *cname = cs_node_text(ctx, nm); if (!cname) return; const char *saved_class = ctx->enclosing_class_qn; const char *saved_base = ctx->enclosing_base_qn; const char **saved_ifs = ctx->enclosing_iface_qns; const char **saved_tp = ctx->type_param_names; const CBMType **saved_tp_args = ctx->type_param_args; int saved_tp_count = ctx->type_param_count; /* Compute QN: prefer module-qn-prefixed (matches unified extractor), * even though the C# `namespace` is also tracked for resolution. */ if (ctx->module_qn) { ctx->enclosing_class_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, cname); } else { ctx->enclosing_class_qn = cbm_arena_strdup(ctx->arena, cname); } /* Type parameters. */ const char **tp_names = NULL; int tp_count = 0; cs_collect_type_params(ctx, node, &tp_names, &tp_count); if (tp_count > 0) { const CBMType **args = (const CBMType **)cbm_arena_alloc( ctx->arena, (size_t)(tp_count + 1) * sizeof(*args)); if (args) { for (int i = 0; i < tp_count; i++) { args[i] = cbm_type_type_param(ctx->arena, tp_names[i]); } args[tp_count] = NULL; ctx->type_param_names = tp_names; ctx->type_param_args = args; ctx->type_param_count = tp_count; } } /* Base list -> enclosing_base_qn + enclosing_iface_qns. */ { TSNode bl = cs_child_named_kind(node, "base_list"); if (!ts_node_is_null(bl)) { uint32_t nc = ts_node_child_count(bl); int icap = 4, icount = 0; const char **ifs = (const char **)cbm_arena_alloc(ctx->arena, (size_t)icap * sizeof(*ifs)); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(bl, i); if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; char *t = cs_node_text(ctx, c); if (!t) continue; /* Strip generic args for QN resolution. */ char *bare = cs_strip_generic_args(ctx->arena, t); const char *qn = cs_resolve_type_name(ctx, bare ? bare : t); if (!qn) continue; /* Heuristic: first base whose registry says is_interface=true * goes to interfaces; first non-interface goes to base. */ const CBMRegisteredType *rt = cs_lookup_type_qn(ctx, qn); if (rt && rt->is_interface) { if (icount + 1 >= icap) { int nc2 = icap * 2; const char **ne = (const char **)cbm_arena_alloc( ctx->arena, (size_t)nc2 * sizeof(*ne)); if (!ne) continue; for (int j = 0; j < icount; j++) ne[j] = ifs[j]; ifs = ne; icap = nc2; } ifs[icount++] = qn; } else { if (!ctx->enclosing_base_qn) ctx->enclosing_base_qn = qn; else { if (icount + 1 >= icap) { int nc2 = icap * 2; const char **ne = (const char **)cbm_arena_alloc( ctx->arena, (size_t)nc2 * sizeof(*ne)); if (!ne) continue; for (int j = 0; j < icount; j++) ne[j] = ifs[j]; ifs = ne; icap = nc2; } ifs[icount++] = qn; } } } ifs[icount] = NULL; ctx->enclosing_iface_qns = ifs; } } /* Walk body. Enums don't have methods worth resolving; skip recurse. */ if (!is_enum) { TSNode body = ts_node_child_by_field_name(node, "body", 4); if (ts_node_is_null(body)) body = cs_child_named_kind(node, "declaration_list"); if (!ts_node_is_null(body)) { uint32_t nc = ts_node_child_count(body); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(body, i); if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; const char *k = ts_node_type(c); if (strcmp(k, "method_declaration") == 0 || strcmp(k, "constructor_declaration") == 0 || strcmp(k, "destructor_declaration") == 0 || strcmp(k, "operator_declaration") == 0 || strcmp(k, "conversion_operator_declaration") == 0 || strcmp(k, "indexer_declaration") == 0 || strcmp(k, "property_declaration") == 0 || strcmp(k, "event_declaration") == 0) { cs_process_function_like(ctx, c); } else if (strcmp(k, "class_declaration") == 0 || strcmp(k, "struct_declaration") == 0 || strcmp(k, "record_declaration") == 0 || strcmp(k, "interface_declaration") == 0 || strcmp(k, "enum_declaration") == 0) { /* Nested type. Recurse, but with the OUTER class's * enclosing_class_qn replaced. */ cs_process_type_decl(ctx, c); } } } /* Records with primary constructor parameter list — bind in a synthetic * `.ctor` scope so any default-value expressions get resolved. */ TSNode pctor = cs_child_named_kind(node, "parameter_list"); if (!ts_node_is_null(pctor)) { CBMScope *prev = ctx->current_scope; ctx->current_scope = cbm_scope_push(ctx->arena, prev); const char *prev_func = ctx->enclosing_func_qn; ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s..ctor", ctx->enclosing_class_qn); cs_bind_parameters(ctx, pctor, false); ctx->enclosing_func_qn = prev_func; ctx->current_scope = prev; } } (void)is_class; ctx->enclosing_class_qn = saved_class; ctx->enclosing_base_qn = saved_base; ctx->enclosing_iface_qns = saved_ifs; ctx->type_param_names = saved_tp; ctx->type_param_args = saved_tp_args; ctx->type_param_count = saved_tp_count; } /* ── using collection ───────────────────────────────────────────── */ static void cs_collect_imports(CSLSPContext *ctx, TSNode root) { /* Walk the entire tree once at top — using directives can appear at file * scope or inside namespace blocks. */ TSNode stack[256]; int top = 0; stack[top++] = root; while (top > 0) { TSNode n = stack[--top]; if (ts_node_is_null(n)) continue; const char *k = ts_node_type(n); if (strcmp(k, "using_directive") == 0) { /* Inspect modifiers and target. tree-sitter-c-sharp uses the * `alias` field on using_directive for the alias name (in * `using A = X;` the field "alias" holds A). When the field * isn't populated (older grammar), we detect aliasing by the * presence of an `=` token between identifier children. */ bool is_global = false; bool is_static = false; bool is_alias = false; const char *alias_name = NULL; const char *target = NULL; TSNode alias_node = ts_node_child_by_field_name(n, "alias", 5); if (!ts_node_is_null(alias_node)) { is_alias = true; char *t = cs_node_text(ctx, alias_node); if (t) alias_name = t; } /* Detect `=` token between named children to flag aliasing * even when the `alias` field isn't populated. */ uint32_t nc = ts_node_child_count(n); bool seen_equals = false; TSNode pre_eq; memset(&pre_eq, 0, sizeof(pre_eq)); TSNode post_eq; memset(&post_eq, 0, sizeof(post_eq)); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(n, i); if (ts_node_is_null(c)) continue; const char *ck = ts_node_type(c); /* Tokens (anonymous and named alike) for keywords + `=`. */ if (strcmp(ck, "global") == 0) { is_global = true; continue; } if (strcmp(ck, "static") == 0) { is_static = true; continue; } if (strcmp(ck, "=") == 0) { seen_equals = true; continue; } if (!ts_node_is_named(c)) continue; if (strcmp(ck, "name_equals") == 0) { /* Older grammar variant. */ is_alias = true; TSNode id = cs_first_named_child(c); if (!ts_node_is_null(id)) alias_name = cs_node_text(ctx, id); continue; } /* identifier / qualified_name / generic_name. */ if (!seen_equals && ts_node_is_null(pre_eq)) { pre_eq = c; } else if (seen_equals && ts_node_is_null(post_eq)) { post_eq = c; } } if (seen_equals && !ts_node_is_null(pre_eq) && !ts_node_is_null(post_eq)) { /* `using = ;` — aliasing. */ is_alias = true; if (!alias_name) { char *t = cs_node_text(ctx, pre_eq); if (t) alias_name = t; } char *t = cs_node_text(ctx, post_eq); if (t) target = cs_normalize_name(ctx->arena, t); } else if (!ts_node_is_null(pre_eq) && !target) { /* Non-aliased: `using X;` — pre_eq is the target. */ char *t = cs_node_text(ctx, pre_eq); if (t) target = cs_normalize_name(ctx->arena, t); } if (target) { if (is_alias) { cs_lsp_add_using(ctx, CBM_CS_USING_ALIAS, alias_name ? alias_name : "", target, is_global); } else if (is_static) { cs_lsp_add_using(ctx, CBM_CS_USING_STATIC, "", target, is_global); } else { cs_lsp_add_using(ctx, CBM_CS_USING_NAMESPACE, "", target, is_global); } } } /* Push children for traversal. We don't recurse into method bodies * (using directives can't appear there), but namespace bodies may * contain more usings. */ uint32_t cnc = ts_node_child_count(n); for (uint32_t i = 0; i < cnc && top < 256; i++) { TSNode c = ts_node_child(n, i); if (ts_node_is_null(c)) continue; const char *ck = ts_node_type(c); if (strcmp(ck, "method_declaration") == 0 || strcmp(ck, "constructor_declaration") == 0 || strcmp(ck, "block") == 0) { continue; } stack[top++] = c; } } } /* ── namespace collection ───────────────────────────────────────── */ static void cs_collect_namespace(CSLSPContext *ctx, TSNode ns_node, bool file_scoped) { TSNode nm = ts_node_child_by_field_name(ns_node, "name", 4); if (ts_node_is_null(nm)) return; char *raw = cs_node_text(ctx, nm); if (!raw) return; const char *normalized = cs_normalize_name(ctx->arena, raw); cs_namespace_push(ctx, normalized); /* Walk body — file-scoped namespaces have body items as siblings of nm. * Block-style have a body node. */ if (file_scoped) { /* The file-scoped namespace's siblings (in the compilation_unit, after * the namespace decl) are the contents — handled by the caller's * top-level walk. We just push the namespace and return. */ return; } TSNode body = ts_node_child_by_field_name(ns_node, "body", 4); if (ts_node_is_null(body)) body = cs_child_named_kind(ns_node, "declaration_list"); if (!ts_node_is_null(body)) { uint32_t nc = ts_node_child_count(body); for (uint32_t i = 0; i < nc; i++) { TSNode c = ts_node_child(body, i); if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; const char *ck = ts_node_type(c); if (strcmp(ck, "namespace_declaration") == 0) { cs_collect_namespace(ctx, c, false); } else if (strcmp(ck, "file_scoped_namespace_declaration") == 0) { cs_collect_namespace(ctx, c, true); } else if (strcmp(ck, "class_declaration") == 0 || strcmp(ck, "struct_declaration") == 0 || strcmp(ck, "record_declaration") == 0 || strcmp(ck, "interface_declaration") == 0 || strcmp(ck, "enum_declaration") == 0) { cs_process_type_decl(ctx, c); } } } cs_namespace_pop(ctx); } /* ── top-level walk ─────────────────────────────────────────────── */ void cs_lsp_process_file(CSLSPContext *ctx, TSNode root) { if (ts_node_is_null(root)) return; /* Pass 1: collect using directives. */ cs_collect_imports(ctx, root); /* Pass 2: walk top-level. file-scoped namespaces apply to ALL siblings, * so we keep the namespace pushed for the rest of the walk. */ // Collect top-level children once (O(n)); ts_node_child(root,i) is O(i) → O(n²). uint32_t kn = 0; TSNode *kids = cbm_lsp_collect_children(ctx->arena, root, &kn); bool file_scoped_active = false; for (uint32_t i = 0; i < kn; i++) { TSNode c = kids[i]; if (!ts_node_is_named(c)) continue; const char *k = ts_node_type(c); if (strcmp(k, "using_directive") == 0) continue; if (strcmp(k, "file_scoped_namespace_declaration") == 0) { cs_collect_namespace(ctx, c, true); file_scoped_active = true; } else if (strcmp(k, "namespace_declaration") == 0) { cs_collect_namespace(ctx, c, false); } else if (strcmp(k, "class_declaration") == 0 || strcmp(k, "struct_declaration") == 0 || strcmp(k, "record_declaration") == 0 || strcmp(k, "interface_declaration") == 0 || strcmp(k, "enum_declaration") == 0) { cs_process_type_decl(ctx, c); } else if (strcmp(k, "global_statement") == 0 || strcmp(k, "expression_statement") == 0 || strcmp(k, "if_statement") == 0 || strcmp(k, "for_statement") == 0 || strcmp(k, "foreach_statement") == 0 || strcmp(k, "for_each_statement") == 0 || strcmp(k, "while_statement") == 0 || strcmp(k, "local_declaration_statement") == 0 || strcmp(k, "return_statement") == 0) { /* Top-level statements (Program.cs without explicit Main). * We treat them as a synthetic enclosing function `` so * the resolved-call array carries the caller correctly. */ const char *saved = ctx->enclosing_func_qn; if (!saved && ctx->module_qn) { ctx->enclosing_func_qn = ctx->module_qn; } cs_resolve_calls_in_node(ctx, c); ctx->enclosing_func_qn = saved; } } if (file_scoped_active && ctx->namespace_count > 0) cs_namespace_pop(ctx); } /* ── registry building from defs ─────────────────────────────────── */ /* Parse a parenthesized signature like `(int x, string s = "")` into * NULL-terminated arrays of param names + types. Best-effort: drops * default-value expressions, ignores ref/out/in modifiers. */ static void cs_parse_signature(CBMArena *arena, const char *signature, CSLSPContext *ctx, const char ***out_names, const CBMType ***out_types) { *out_names = NULL; *out_types = NULL; if (!signature) return; const char *p = signature; while (*p == ' ' || *p == '(') p++; /* Walk param-by-param. We split on top-level ',' (ignoring those inside * generic <> brackets). */ int cap = 8; int count = 0; const char **names = (const char **)cbm_arena_alloc(arena, (size_t)cap * sizeof(*names)); const CBMType **types = (const CBMType **)cbm_arena_alloc(arena, (size_t)cap * sizeof(*types)); if (!names || !types) return; while (*p && *p != ')') { /* Skip leading whitespace. */ while (*p == ' ' || *p == '\t' || *p == ',') p++; if (!*p || *p == ')') break; /* Skip param modifiers: ref, out, in, params, this. */ const char *modifiers[] = {"ref ", "out ", "in ", "params ", "this "}; bool ate; do { ate = false; for (int m = 0; m < 5; m++) { size_t ml = strlen(modifiers[m]); if (strncmp(p, modifiers[m], ml) == 0) { p += ml; ate = true; } } } while (ate); /* Read type tokens until we hit a name. The type may include '<...>' * or '[]'. The name is the last token before ',' or '=' or ')'. */ const char *type_start = p; int depth = 0; const char *last_space = NULL; while (*p && (depth > 0 || (*p != ',' && *p != ')' && *p != '='))) { if (*p == '<') depth++; else if (*p == '>') depth--; else if (depth == 0 && (*p == ' ' || *p == '\t')) last_space = p; p++; } const char *name_end = p; while (name_end > type_start && (name_end[-1] == ' ' || name_end[-1] == '\t')) name_end--; if (!last_space) { /* No name — treat the whole token as the type with synthetic name. */ char *type_text = cbm_arena_strndup(arena, type_start, (size_t)(name_end - type_start)); const CBMType *t = cbm_type_unknown(); if (ctx) t = cs_resolve_type_name(ctx, type_text) ? cbm_type_named(ctx->arena, cs_resolve_type_name(ctx, type_text)) : cbm_type_unknown(); (void)t; if (count + 1 >= cap) break; names[count] = cbm_arena_sprintf(arena, "_arg%d", count); types[count] = t; count++; } else { char *type_text = cbm_arena_strndup(arena, type_start, (size_t)(last_space - type_start)); char *pname = cbm_arena_strndup(arena, last_space + 1, (size_t)(name_end - last_space - 1)); const CBMType *t = cbm_type_unknown(); if (ctx) { const char *resolved = cs_resolve_type_name(ctx, type_text); if (resolved) t = cbm_type_named(ctx->arena, resolved); } if (count + 1 >= cap) break; names[count] = pname; types[count] = t; count++; } /* Skip default value if any. */ if (*p == '=') { int d = 0; while (*p && (d > 0 || (*p != ',' && *p != ')'))) { if (*p == '(' || *p == '<') d++; else if (*p == ')' || *p == '>') { if (d == 0) break; d--; } p++; } } if (*p == ',') p++; } if (count + 1 < cap) { names[count] = NULL; types[count] = NULL; *out_names = names; *out_types = types; } } static void cs_register_type_decls(CSLSPContext *ctx, CBMTypeRegistry *reg, TSNode root) { /* We rely on CBMFileResult.defs entries already being filled by the * unified extractor. This function is reserved for future expansions * (e.g. parsing field declarations directly from the AST). */ (void)ctx; (void)reg; (void)root; } /* ── field/property collection from AST ─────────────────────────── */ typedef struct { const char *class_qn; const char **field_names; const CBMType **field_types; int count; int cap; } cs_fields_t; typedef struct { cs_fields_t *items; int count; int cap; } cs_fields_table_t; static cs_fields_t *cs_fields_get(CBMArena *arena, cs_fields_table_t *tab, const char *class_qn) { for (int i = 0; i < tab->count; i++) { if (strcmp(tab->items[i].class_qn, class_qn) == 0) return &tab->items[i]; } if (tab->count + 1 >= tab->cap) { int new_cap = tab->cap ? tab->cap * 2 : 8; cs_fields_t *ne = (cs_fields_t *)cbm_arena_alloc(arena, (size_t)new_cap * sizeof(*ne)); if (!ne) return NULL; for (int i = 0; i < tab->count; i++) ne[i] = tab->items[i]; tab->items = ne; tab->cap = new_cap; } cs_fields_t *slot = &tab->items[tab->count++]; memset(slot, 0, sizeof(*slot)); slot->class_qn = class_qn; slot->cap = 8; slot->field_names = (const char **)cbm_arena_alloc(arena, (size_t)slot->cap * sizeof(*slot->field_names)); slot->field_types = (const CBMType **)cbm_arena_alloc(arena, (size_t)slot->cap * sizeof(*slot->field_types)); return slot; } static void cs_fields_add_debug(CBMArena *arena, cs_fields_t *f, const char *name, const CBMType *type, bool debug); static void cs_fields_add(CBMArena *arena, cs_fields_t *f, const char *name, const CBMType *type) { cs_fields_add_debug(arena, f, name, type, false); } static void cs_fields_add_debug(CBMArena *arena, cs_fields_t *f, const char *name, const CBMType *type, bool debug) { (void)debug; if (!f || !name) return; /* Dedupe. */ for (int i = 0; i < f->count; i++) { if (strcmp(f->field_names[i], name) == 0) return; } if (f->count + 2 >= f->cap) { int new_cap = f->cap * 2; const char **nn = (const char **)cbm_arena_alloc(arena, (size_t)new_cap * sizeof(*nn)); const CBMType **nt = (const CBMType **)cbm_arena_alloc(arena, (size_t)new_cap * sizeof(*nt)); if (!nn || !nt) return; for (int i = 0; i < f->count; i++) { nn[i] = f->field_names[i]; nt[i] = f->field_types[i]; } f->field_names = nn; f->field_types = nt; f->cap = new_cap; } f->field_names[f->count] = cbm_arena_strdup(arena, name); f->field_types[f->count] = type ? type : cbm_type_unknown(); f->count++; f->field_names[f->count] = NULL; f->field_types[f->count] = NULL; } static void cs_collect_class_fields(CSLSPContext *ctx, CBMTypeRegistry *reg, TSNode root, cs_fields_table_t *tab) { /* Walk the AST and collect field/property/event declarations into tab. * We need the namespace + using context to resolve types, so this runs * after cs_collect_imports. */ TSNode stack[512]; int top = 0; stack[top++] = root; /* Maintain enclosing class for each decl we visit. We simply re-derive it * via parent_chain inspection (limited to direct class parent). */ while (top > 0) { TSNode n = stack[--top]; if (ts_node_is_null(n)) continue; const char *k = ts_node_type(n); if (strcmp(k, "field_declaration") == 0 || strcmp(k, "property_declaration") == 0 || strcmp(k, "event_field_declaration") == 0) { /* Find enclosing class/struct/record. */ TSNode p = ts_node_parent(n); const char *cls_short = NULL; while (!ts_node_is_null(p)) { const char *pk = ts_node_type(p); if (strcmp(pk, "class_declaration") == 0 || strcmp(pk, "struct_declaration") == 0 || strcmp(pk, "record_declaration") == 0 || strcmp(pk, "record_struct_declaration") == 0 || strcmp(pk, "interface_declaration") == 0) { TSNode nm = ts_node_child_by_field_name(p, "name", 4); if (!ts_node_is_null(nm)) { cls_short = cs_node_text(ctx, nm); } break; } p = ts_node_parent(p); } if (cls_short) { const char *cls_qn = ctx->module_qn ? cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, cls_short) : cbm_arena_strdup(ctx->arena, cls_short); cs_fields_t *f = cs_fields_get(ctx->arena, tab, cls_qn); if (strcmp(k, "field_declaration") == 0 || strcmp(k, "event_field_declaration") == 0) { TSNode vd = cs_child_named_kind(n, "variable_declaration"); if (!ts_node_is_null(vd)) { TSNode tnode = ts_node_child_by_field_name(vd, "type", 4); if (ts_node_is_null(tnode)) tnode = cs_first_named_child(vd); const CBMType *t = ts_node_is_null(tnode) ? cbm_type_unknown() : cs_parse_type_node(ctx, tnode); uint32_t vc = ts_node_child_count(vd); for (uint32_t i = 0; i < vc; i++) { TSNode c = ts_node_child(vd, i); if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; if (strcmp(ts_node_type(c), "variable_declarator") != 0) continue; TSNode id = cs_child_named_kind(c, "identifier"); if (ts_node_is_null(id)) id = ts_node_child_by_field_name(c, "name", 4); if (ts_node_is_null(id)) continue; char *fn = cs_node_text(ctx, id); if (fn) cs_fields_add_debug(ctx->arena, f, fn, t, ctx->debug); } } } else { /* property */ TSNode tnode = ts_node_child_by_field_name(n, "type", 4); TSNode nm = ts_node_child_by_field_name(n, "name", 4); if (!ts_node_is_null(tnode) && !ts_node_is_null(nm)) { const CBMType *t = cs_parse_type_node(ctx, tnode); char *fn = cs_node_text(ctx, nm); if (fn) cs_fields_add_debug(ctx->arena, f, fn, t, ctx->debug); } } } } /* Primary constructor parameters of records / classes — register as fields */ if (strcmp(k, "record_declaration") == 0 || strcmp(k, "record_struct_declaration") == 0 || strcmp(k, "class_declaration") == 0 || strcmp(k, "struct_declaration") == 0) { TSNode params = cs_child_named_kind(n, "parameter_list"); TSNode nm = ts_node_child_by_field_name(n, "name", 4); if (!ts_node_is_null(params) && !ts_node_is_null(nm)) { char *cls_short = cs_node_text(ctx, nm); if (cls_short) { const char *cls_qn = ctx->module_qn ? cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, cls_short) : cbm_arena_strdup(ctx->arena, cls_short); cs_fields_t *f = cs_fields_get(ctx->arena, tab, cls_qn); uint32_t pc = ts_node_child_count(params); for (uint32_t i = 0; i < pc; i++) { TSNode c = ts_node_child(params, i); if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; if (strcmp(ts_node_type(c), "parameter") != 0) continue; TSNode tn = ts_node_child_by_field_name(c, "type", 4); TSNode pn = ts_node_child_by_field_name(c, "name", 4); if (ts_node_is_null(tn) || ts_node_is_null(pn)) continue; const CBMType *t = cs_parse_type_node(ctx, tn); char *pname = cs_node_text(ctx, pn); if (pname) cs_fields_add(ctx->arena, f, pname, t); } } } } uint32_t cnc = ts_node_child_count(n); for (uint32_t i = 0; i < cnc && top + 1 < 512; i++) { TSNode c = ts_node_child(n, i); if (ts_node_is_null(c)) continue; const char *ck = ts_node_type(c); /* Skip method/ctor bodies — fields can't be there. */ if (strcmp(ck, "method_declaration") == 0 || strcmp(ck, "constructor_declaration") == 0 || strcmp(ck, "destructor_declaration") == 0 || strcmp(ck, "operator_declaration") == 0 || strcmp(ck, "indexer_declaration") == 0 || strcmp(ck, "block") == 0) { continue; } stack[top++] = c; } } (void)reg; } /* ── extract method return types directly from AST ────────────── * * The unified extractor leaves CBMDefinition.return_type NULL for C# * methods (the C# code path takes a different branch). Without return * types, method-chain resolution like `a.GetB().Tag()` can't propagate * the result type from GetB into the next dispatch. To restore the * chain we do our own AST pass here, mapping method QNs to their * declared return type, then patching the registry's signatures. */ typedef struct { const char *qn; const CBMType *rt; } cs_method_rt_entry_t; typedef struct { cs_method_rt_entry_t *items; int count; int cap; } cs_method_rt_table_t; static void cs_method_rt_add(CBMArena *arena, cs_method_rt_table_t *tab, const char *qn, const CBMType *rt) { if (!qn || !rt) return; for (int i = 0; i < tab->count; i++) { if (strcmp(tab->items[i].qn, qn) == 0) return; } if (tab->count + 1 >= tab->cap) { int new_cap = tab->cap ? tab->cap * 2 : 16; cs_method_rt_entry_t *ne = (cs_method_rt_entry_t *)cbm_arena_alloc( arena, (size_t)new_cap * sizeof(*ne)); if (!ne) return; for (int i = 0; i < tab->count; i++) ne[i] = tab->items[i]; tab->items = ne; tab->cap = new_cap; } tab->items[tab->count].qn = qn; tab->items[tab->count].rt = rt; tab->count++; } /* Walk the tree, finding method_declaration / property_declaration nodes * and recording (parent_class_qn + "." + method_name → return type). */ static void cs_collect_method_return_types(CSLSPContext *ctx, TSNode root, cs_method_rt_table_t *tab) { TSNode stack[512]; int top = 0; stack[top++] = root; while (top > 0) { TSNode n = stack[--top]; if (ts_node_is_null(n)) continue; const char *k = ts_node_type(n); bool is_method = (strcmp(k, "method_declaration") == 0); bool is_property = (strcmp(k, "property_declaration") == 0); bool is_indexer = (strcmp(k, "indexer_declaration") == 0); if (is_method || is_property || is_indexer) { /* Find enclosing class. */ TSNode p = ts_node_parent(n); const char *cls_short = NULL; while (!ts_node_is_null(p)) { const char *pk = ts_node_type(p); if (strcmp(pk, "class_declaration") == 0 || strcmp(pk, "struct_declaration") == 0 || strcmp(pk, "record_declaration") == 0 || strcmp(pk, "record_struct_declaration") == 0 || strcmp(pk, "interface_declaration") == 0) { TSNode pn = ts_node_child_by_field_name(p, "name", 4); if (!ts_node_is_null(pn)) cls_short = cs_node_text(ctx, pn); break; } p = ts_node_parent(p); } if (!cls_short) { uint32_t cnc = ts_node_child_count(n); for (uint32_t i = 0; i < cnc && top + 1 < 512; i++) { TSNode c = ts_node_child(n, i); if (!ts_node_is_null(c)) stack[top++] = c; } continue; } const char *cls_qn = ctx->module_qn ? cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->module_qn, cls_short) : cbm_arena_strdup(ctx->arena, cls_short); TSNode tnode = ts_node_child_by_field_name(n, "type", 4); TSNode nm = ts_node_child_by_field_name(n, "name", 4); /* tree-sitter-c-sharp doesn't always set the type field on * method_declaration. Walk named children to find the return * type: it's the first non-modifier non-attribute named child * whose kind isn't an identifier matching the method name. */ if (ts_node_is_null(tnode)) { uint32_t cn = ts_node_child_count(n); for (uint32_t i = 0; i < cn; i++) { TSNode c = ts_node_child(n, i); if (ts_node_is_null(c) || !ts_node_is_named(c)) continue; const char *ck = ts_node_type(c); if (strcmp(ck, "modifier") == 0 || strcmp(ck, "attribute_list") == 0 || strcmp(ck, "type_parameter_list") == 0) continue; /* The name comes after the type; skip it. */ if (!ts_node_is_null(nm) && ts_node_eq(c, nm)) continue; if (strcmp(ck, "parameter_list") == 0 || strcmp(ck, "block") == 0 || strcmp(ck, "arrow_expression_clause") == 0 || strcmp(ck, "type_parameter_constraints_clause") == 0) break; /* Heuristic: the first remaining named child is the * return type. */ tnode = c; break; } } if (is_indexer) { const char *short_name = "this[]"; if (!ts_node_is_null(tnode)) { const CBMType *rt = cs_parse_type_node(ctx, tnode); cs_method_rt_add(ctx->arena, tab, cbm_arena_sprintf(ctx->arena, "%s.%s", cls_qn, short_name), rt); } } else if (!ts_node_is_null(tnode) && !ts_node_is_null(nm)) { char *short_name = cs_node_text(ctx, nm); if (short_name) { const CBMType *rt = cs_parse_type_node(ctx, tnode); cs_method_rt_add(ctx->arena, tab, cbm_arena_sprintf(ctx->arena, "%s.%s", cls_qn, short_name), rt); } } } /* Push children unless we're inside a method body. */ uint32_t cnc = ts_node_child_count(n); for (uint32_t i = 0; i < cnc && top + 1 < 512; i++) { TSNode c = ts_node_child(n, i); if (ts_node_is_null(c)) continue; const char *ck = ts_node_type(c); if (strcmp(ck, "block") == 0 || strcmp(ck, "arrow_expression_clause") == 0) { continue; } stack[top++] = c; } } } /* ── parse return type from CBMDefinition.return_type ──────────── */ static const CBMType *cs_parse_return_type_text(CSLSPContext *ctx, const char *text) { if (!text || !*text) return cbm_type_unknown(); const char *p = text; while (*p == ' ' || *p == ':') p++; if (!*p) return cbm_type_unknown(); /* Strip trailing whitespace + '?' nullability. */ size_t n = strlen(p); while (n > 0 && (p[n - 1] == ' ' || p[n - 1] == '?')) n--; if (n == 0) return cbm_type_unknown(); char *trimmed = cbm_arena_strndup(ctx->arena, p, n); const char *resolved = cs_resolve_type_name(ctx, trimmed); if (!resolved) return cbm_type_unknown(); const char *pre = cs_predefined_alias(resolved); if (pre) return cbm_type_named(ctx->arena, pre); return cbm_type_named(ctx->arena, resolved); } /* ── single-file entry: cbm_run_cs_lsp ──────────────────────────── */ void cbm_run_cs_lsp(CBMArena *arena, CBMFileResult *result, const char *source, int source_len, TSNode root) { if (!result || !arena || ts_node_is_null(root)) return; CBMTypeRegistry reg; cbm_registry_init(®, arena); /* Phase A: stdlib. */ cbm_csharp_stdlib_register(®, arena); const char *module_qn = result->module_qn; /* Phase B: register types + functions from this file's defs. */ for (int i = 0; i < result->defs.count; i++) { CBMDefinition *d = &result->defs.items[i]; if (!d->qualified_name || !d->name || !d->label) continue; if (strcmp(d->label, "Class") == 0 || strcmp(d->label, "Interface") == 0 || strcmp(d->label, "Struct") == 0 || strcmp(d->label, "Record") == 0 || strcmp(d->label, "Enum") == 0 || strcmp(d->label, "Type") == 0) { 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); 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 *base = d->base_classes[j]; const char *q = base; if (base[0] != '.' && !strchr(base, '.') && module_qn) { q = cbm_arena_sprintf(arena, "%s.%s", module_qn, base); } emb[j] = q; } emb[bc] = NULL; rt.embedded_types = emb; } } } cbm_registry_add_type(®, rt); } 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; if (strcmp(d->label, "Method") == 0 && d->parent_class) { rf.receiver_type = d->parent_class; } const CBMType *rt = d->return_type ? cbm_type_unknown() /* will refine below with ctx */ : cbm_type_unknown(); const CBMType **rets = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(const CBMType *)); if (rets) { rets[0] = rt; rets[1] = NULL; } rf.signature = cbm_type_func(arena, NULL, NULL, rets); cbm_registry_add_func(®, rf); } } /* Phase C: build context for type resolution + run the resolver. */ CSLSPContext ctx; cs_lsp_init(&ctx, arena, source, source_len, ®, module_qn, &result->resolved_calls); /* Pass 1: collect imports + namespaces (for type-resolution context). */ cs_collect_imports(&ctx, root); /* Now refine return types of registered funcs using ctx (which has using * directives). Also refine field types via collect_class_fields. */ for (int i = 0; i < result->defs.count; i++) { CBMDefinition *d = &result->defs.items[i]; if (!d->qualified_name || !d->return_type) continue; if (strcmp(d->label, "Function") != 0 && strcmp(d->label, "Method") != 0) continue; const CBMType *rt = cs_parse_return_type_text(&ctx, d->return_type); /* Find the registered func and patch its signature. */ for (int j = 0; j < reg.func_count; j++) { if (strcmp(reg.funcs[j].qualified_name, d->qualified_name) == 0) { const CBMType **rets = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(const CBMType *)); if (rets) { rets[0] = rt; rets[1] = NULL; reg.funcs[j].signature = cbm_type_func(arena, NULL, NULL, rets); } break; } } } /* Phase B.1.5: AST-driven return-type patch. The unified extractor * leaves CBMDefinition.return_type NULL for C# methods, so we walk the * AST ourselves to recover declared return types. */ { cs_method_rt_table_t mtab = {0}; cs_collect_method_return_types(&ctx, root, &mtab); for (int i = 0; i < mtab.count; i++) { const char *qn = mtab.items[i].qn; const CBMType *rt = mtab.items[i].rt; bool patched = false; for (int j = 0; j < reg.func_count; j++) { if (strcmp(reg.funcs[j].qualified_name, qn) == 0) { const CBMType **rets = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(const CBMType *)); if (rets) { rets[0] = rt; rets[1] = NULL; reg.funcs[j].signature = cbm_type_func(arena, NULL, NULL, rets); } patched = true; break; } } (void)patched; } } /* Phase B.1: collect typed properties + fields. */ { cs_fields_table_t tab = {0}; cs_collect_class_fields(&ctx, ®, root, &tab); for (int i = 0; i < tab.count; i++) { cs_fields_t *f = &tab.items[i]; if (f->count == 0) continue; for (int t = 0; t < reg.type_count; t++) { if (strcmp(reg.types[t].qualified_name, f->class_qn) == 0) { reg.types[t].field_names = f->field_names; reg.types[t].field_types = f->field_types; break; } } } } /* Re-bind ctx state since collect_class_fields may have temporarily * adjusted namespace stack (it doesn't, but be safe). */ ctx.namespace_count = 0; /* Restore implicit `using System;`. */ cs_lsp_process_file(&ctx, root); if (ctx.debug) { fprintf(stderr, "[cs_lsp] module=%s defs=%d types=%d funcs=%d resolved=%d\n", module_qn ? module_qn : "?", result->defs.count, reg.type_count, reg.func_count, result->resolved_calls.count); for (int i = 0; i < result->resolved_calls.count; i++) { const CBMResolvedCall *rc = &result->resolved_calls.items[i]; fprintf(stderr, "[cs_lsp] %s -> %s [%s %.2f]\n", rc->caller_qn ? rc->caller_qn : "?", rc->callee_qn ? rc->callee_qn : "?", rc->strategy ? rc->strategy : "?", rc->confidence); } } } /* ── cross-file entry ───────────────────────────────────────────── */ /* Register one batch of CBMLSPDef[] into a registry. Shared by the * per-file cross-LSP path and the Tier 2 pre-built registry builder. * Def-driven (no per-file AST mutation) so deterministic per def set. */ static void cs_register_lsp_defs(CBMArena *arena, CBMTypeRegistry *reg, CBMLSPDef *defs, int def_count) { for (int i = 0; i < def_count; i++) { 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 || strcmp(d->label, "Struct") == 0 || strcmp(d->label, "Record") == 0 || strcmp(d->label, "Enum") == 0 || strcmp(d->label, "Type") == 0) { CBMRegisteredType rt; memset(&rt, 0, sizeof(rt)); rt.qualified_name = d->qualified_name; rt.short_name = d->short_name; rt.is_interface = d->is_interface || (strcmp(d->label, "Interface") == 0); if (d->embedded_types && *d->embedded_types) { /* Parse "|"-separated list. */ int n = 1; for (const char *p = d->embedded_types; *p; p++) if (*p == '|') n++; const char **arr = (const char **)cbm_arena_alloc(arena, (size_t)(n + 1) * sizeof(*arr)); if (arr) { int idx = 0; const char *start = d->embedded_types; for (const char *p = d->embedded_types; ; p++) { if (*p == '|' || *p == '\0') { size_t len = (size_t)(p - start); if (len > 0) { arr[idx++] = cbm_arena_strndup(arena, start, len); } if (*p == '\0') break; start = p + 1; } } arr[idx] = NULL; rt.embedded_types = arr; } } cbm_registry_add_type(reg, rt); } 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 (strcmp(d->label, "Method") == 0 && d->receiver_type) { rf.receiver_type = d->receiver_type; } const CBMType **rets = (const CBMType **)cbm_arena_alloc(arena, 2 * sizeof(const CBMType *)); if (rets) { rets[0] = cbm_type_unknown(); rets[1] = NULL; } rf.signature = cbm_type_func(arena, NULL, NULL, rets); cbm_registry_add_func(reg, rf); } } } /* Tier 2: build a project-wide C# registry ONCE from all defs (filters * by lang). Shared READ-ONLY across resolve workers. Def-driven → * identical entries to the per-file build, zero quality loss. */ CBMTypeRegistry *cbm_cs_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_csharp_stdlib_register(reg, arena); for (int i = 0; i < def_count; i++) { if (defs[i].lang != CBM_LANG_CSHARP) continue; cs_register_lsp_defs(arena, reg, &defs[i], 1); } cbm_registry_finalize(reg); reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */ return reg; } void cbm_run_cs_lsp_cross_with_registry(CBMArena *arena, const char *source, int source_len, const char *module_qn, CBMTypeRegistry *reg, const char **using_targets, int using_count, TSTree *cached_tree, CBMResolvedCallArray *out) { if (!source || !arena || !out || !reg) return; TSTree *tree = cached_tree; bool owns = false; if (!tree) { TSParser *parser = ts_parser_new(); if (!parser) return; ts_parser_set_language(parser, tree_sitter_c_sharp()); tree = ts_parser_parse_string(parser, NULL, source, source_len > 0 ? (uint32_t)source_len : (uint32_t)strlen(source)); ts_parser_delete(parser); owns = true; } if (!tree) return; TSNode root = ts_tree_root_node(tree); CSLSPContext ctx; cs_lsp_init(&ctx, arena, source, source_len, reg, module_qn, out); for (int i = 0; i < using_count; i++) { if (using_targets[i]) { cs_lsp_add_using(&ctx, CBM_CS_USING_NAMESPACE, "", using_targets[i], false); } } cs_lsp_process_file(&ctx, root); if (owns) ts_tree_delete(tree); } void cbm_run_cs_lsp_cross(CBMArena *arena, const char *source, int source_len, const char *module_qn, CBMLSPDef *defs, int def_count, const char **using_targets, int using_count, TSTree *cached_tree, CBMResolvedCallArray *out) { if (!source || !arena) return; CBMTypeRegistry reg; cbm_registry_init(®, arena); cbm_csharp_stdlib_register(®, arena); cs_register_lsp_defs(arena, ®, defs, def_count); /* Parse if needed. */ TSTree *tree = cached_tree; bool owns = false; if (!tree) { TSParser *parser = ts_parser_new(); if (!parser) return; ts_parser_set_language(parser, tree_sitter_c_sharp()); tree = ts_parser_parse_string(parser, NULL, source, source_len > 0 ? (uint32_t)source_len : (uint32_t)strlen(source)); ts_parser_delete(parser); owns = true; } if (!tree) return; TSNode root = ts_tree_root_node(tree); /* Finalize registry — O(1) lookups. See go_lsp.c "3c. Finalize" * comment for the rationale. */ cbm_registry_finalize(®); CSLSPContext ctx; cs_lsp_init(&ctx, arena, source, source_len, ®, module_qn, out); /* Pre-populate using directives from the supplied list. */ for (int i = 0; i < using_count; i++) { if (using_targets[i]) { cs_lsp_add_using(&ctx, CBM_CS_USING_NAMESPACE, "", using_targets[i], false); } } cs_lsp_process_file(&ctx, root); if (owns) ts_tree_delete(tree); } void cbm_batch_cs_lsp_cross(CBMArena *arena, CBMBatchCSLSPFile *files, int file_count, CBMResolvedCallArray *out) { if (!arena || !files) return; for (int i = 0; i < file_count; i++) { CBMBatchCSLSPFile *f = &files[i]; cbm_run_cs_lsp_cross(arena, f->source, f->source_len, f->module_qn, f->defs, f->def_count, f->using_targets, f->using_count, f->cached_tree, &out[i]); } }