Files
wehub-resource-sync 41cb1c0170
OpenSSF Scorecard / scorecard (push) Failing after 0s
DCO / dco (push) Failing after 0s
CodeQL SAST / analyze (push) Failing after 1s
Deploy Pages / deploy (push) Failing after 1s
chore: import upstream snapshot with attribution
2026-07-13 12:28:05 +08:00

3048 lines
138 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "go_lsp.h"
#include "lsp_node_iter.h"
#include "../helpers.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
// Forward declarations
static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node);
/* Depth-guarded entry for the AST call-resolution walk. The walk recurses once
* per nesting level; a deeply-nested or cyclic file can overflow the native
* stack (SIGSEGV) and take down the whole index. Past the cap the subtree is
* skipped — its calls stay unresolved, which is graceful degradation, not a
* crash. The cap is CBM_LSP_MAX_WALK_DEPTH, env-overridable via the same name.
* The walk_depth-- runs after the inner returns, so early returns in the body
* never leak the counter. */
static void resolve_calls_in_node(GoLSPContext* ctx, TSNode node) {
if (ctx->walk_depth >= cbm_lsp_max_walk_depth())
return;
ctx->walk_depth++;
resolve_calls_in_node_inner(ctx, node);
ctx->walk_depth--;
}
static void emit_resolved_call(GoLSPContext* ctx, const char* callee_qn, const char* strategy, float confidence);
static const CBMType* go_lookup_field(GoLSPContext* ctx, const char* type_qn, const char* field_name, int depth);
static void extract_type_params_from_ast(CBMArena* arena, CBMTypeRegistry* reg,
TSNode root, const char* source, const char* module_qn);
// --- Initialization ---
void go_lsp_init(GoLSPContext* ctx, CBMArena* arena, const char* source, int source_len,
const CBMTypeRegistry* registry, const char* package_qn, CBMResolvedCallArray* out) {
memset(ctx, 0, sizeof(GoLSPContext));
ctx->arena = arena;
ctx->source = source;
ctx->source_len = source_len;
ctx->registry = registry;
ctx->package_qn = package_qn;
ctx->resolved_calls = out;
ctx->current_scope = cbm_scope_push(arena, NULL); // root scope
{
const char* debug_env = getenv("CBM_LSP_DEBUG");
ctx->debug = (debug_env && debug_env[0]);
}
}
void go_lsp_add_import(GoLSPContext* ctx, const char* local_name, const char* pkg_qn) {
// Store in parallel arrays (arena-allocated, grow by doubling)
if (ctx->import_count % 32 == 0) {
int new_cap = ctx->import_count + 32;
const char** new_names = (const char**)cbm_arena_alloc(ctx->arena, (new_cap + 1) * sizeof(const char*));
const char** new_qns = (const char**)cbm_arena_alloc(ctx->arena, (new_cap + 1) * sizeof(const char*));
if (!new_names || !new_qns) return;
if (ctx->import_local_names && ctx->import_count > 0) {
memcpy(new_names, ctx->import_local_names, ctx->import_count * sizeof(const char*));
memcpy(new_qns, ctx->import_package_qns, ctx->import_count * sizeof(const char*));
}
ctx->import_local_names = new_names;
ctx->import_package_qns = new_qns;
}
ctx->import_local_names[ctx->import_count] = cbm_arena_strdup(ctx->arena, local_name);
ctx->import_package_qns[ctx->import_count] = cbm_arena_strdup(ctx->arena, pkg_qn);
ctx->import_count++;
}
// --- Helper: get node text ---
static char* lsp_node_text(GoLSPContext* ctx, TSNode node) {
return cbm_node_text(ctx->arena, node, ctx->source);
}
// --- Helper: resolve import alias to package QN ---
static const char* resolve_import(GoLSPContext* ctx, const char* local_name) {
for (int i = 0; i < ctx->import_count; i++) {
if (strcmp(ctx->import_local_names[i], local_name) == 0) {
return ctx->import_package_qns[i];
}
}
return NULL;
}
// --- Helper: check if name is a Go builtin ---
static bool is_go_builtin_func(const char* name) {
static const char* builtins[] = {
"make", "new", "append", "len", "cap", "delete",
"close", "copy", "panic", "recover", "print", "println",
"complex", "real", "imag", "min", "max", "clear",
NULL
};
for (const char** b = builtins; *b; b++) {
if (strcmp(name, *b) == 0) return true;
}
return false;
}
// --- Helper: check if name is a Go builtin type ---
static const CBMType* resolve_builtin_type(GoLSPContext* ctx, const char* name) {
static const char* builtin_types[] = {
"int", "int8", "int16", "int32", "int64",
"uint", "uint8", "uint16", "uint32", "uint64",
"float32", "float64", "complex64", "complex128",
"string", "bool", "byte", "rune", "error",
"uintptr", "any",
NULL
};
for (const char** b = builtin_types; *b; b++) {
if (strcmp(name, *b) == 0) {
return cbm_type_builtin(ctx->arena, name);
}
}
return NULL;
}
// --- go_parse_type_node: AST type node -> CBMType ---
const CBMType* go_parse_type_node(GoLSPContext* ctx, TSNode node) {
if (ts_node_is_null(node)) return cbm_type_unknown();
const char* kind = ts_node_type(node);
// type_identifier: simple named type
if (strcmp(kind, "type_identifier") == 0) {
char* name = lsp_node_text(ctx, node);
if (!name) return cbm_type_unknown();
const CBMType* builtin = resolve_builtin_type(ctx, name);
if (builtin) return builtin;
// Resolve as local type: package_qn.TypeName
return cbm_type_named(ctx->arena,
cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->package_qn, name));
}
// qualified_type: pkg.Type
if (strcmp(kind, "qualified_type") == 0) {
TSNode pkg_node = ts_node_child_by_field_name(node, "package", 7);
TSNode name_node = ts_node_child_by_field_name(node, "name", 4);
if (!ts_node_is_null(pkg_node) && !ts_node_is_null(name_node)) {
char* pkg = lsp_node_text(ctx, pkg_node);
char* name = lsp_node_text(ctx, name_node);
const char* pkg_qn = resolve_import(ctx, pkg);
if (pkg_qn) {
return cbm_type_named(ctx->arena,
cbm_arena_sprintf(ctx->arena, "%s.%s", pkg_qn, name));
}
}
return cbm_type_unknown();
}
// pointer_type: *T
if (strcmp(kind, "pointer_type") == 0) {
uint32_t nc = ts_node_named_child_count(node);
if (nc > 0) {
return cbm_type_pointer(ctx->arena,
go_parse_type_node(ctx, ts_node_named_child(node, nc - 1)));
}
return cbm_type_unknown();
}
// slice_type: []T
if (strcmp(kind, "slice_type") == 0) {
TSNode elem = ts_node_child_by_field_name(node, "element", 7);
if (ts_node_is_null(elem) && ts_node_named_child_count(node) > 0) {
elem = ts_node_named_child(node, ts_node_named_child_count(node) - 1);
}
return cbm_type_slice(ctx->arena, go_parse_type_node(ctx, elem));
}
// array_type: [N]T — treat as slice for our purposes
if (strcmp(kind, "array_type") == 0) {
TSNode elem = ts_node_child_by_field_name(node, "element", 7);
if (ts_node_is_null(elem) && ts_node_named_child_count(node) > 0) {
elem = ts_node_named_child(node, ts_node_named_child_count(node) - 1);
}
return cbm_type_slice(ctx->arena, go_parse_type_node(ctx, elem));
}
// map_type: map[K]V
if (strcmp(kind, "map_type") == 0) {
TSNode key = ts_node_child_by_field_name(node, "key", 3);
TSNode value = ts_node_child_by_field_name(node, "value", 5);
return cbm_type_map(ctx->arena,
go_parse_type_node(ctx, key),
go_parse_type_node(ctx, value));
}
// channel_type: chan T
if (strcmp(kind, "channel_type") == 0) {
TSNode value = ts_node_child_by_field_name(node, "value", 5);
if (ts_node_is_null(value) && ts_node_named_child_count(node) > 0) {
value = ts_node_named_child(node, ts_node_named_child_count(node) - 1);
}
// Determine direction from text
char* text = lsp_node_text(ctx, node);
int dir = 0;
if (text) {
if (strncmp(text, "chan<-", 6) == 0 || strncmp(text, "chan <-", 7) == 0) dir = 1;
else if (strncmp(text, "<-chan", 6) == 0 || strncmp(text, "<- chan", 7) == 0) dir = 2;
}
return cbm_type_channel(ctx->arena, go_parse_type_node(ctx, value), dir);
}
// function_type: func(...)...
if (strcmp(kind, "function_type") == 0) {
return cbm_type_func(ctx->arena, NULL, NULL, NULL); // simplified
}
// interface_type
if (strcmp(kind, "interface_type") == 0) {
CBMType* t = (CBMType*)cbm_arena_alloc(ctx->arena, sizeof(CBMType));
memset(t, 0, sizeof(CBMType));
t->kind = CBM_TYPE_INTERFACE;
return t;
}
// struct_type
if (strcmp(kind, "struct_type") == 0) {
CBMType* t = (CBMType*)cbm_arena_alloc(ctx->arena, sizeof(CBMType));
memset(t, 0, sizeof(CBMType));
t->kind = CBM_TYPE_STRUCT;
return t;
}
// parenthesized_type: (T)
if (strcmp(kind, "parenthesized_type") == 0 && ts_node_named_child_count(node) > 0) {
return go_parse_type_node(ctx, ts_node_named_child(node, 0));
}
// type_elem: wrapper in type_arguments, unwrap to inner type
if (strcmp(kind, "type_elem") == 0 && ts_node_named_child_count(node) > 0) {
return go_parse_type_node(ctx, ts_node_named_child(node, 0));
}
// generic_type: Type[T1, T2] — return as named without generic args for now
if (strcmp(kind, "generic_type") == 0) {
TSNode type_node = ts_node_child_by_field_name(node, "type", 4);
if (!ts_node_is_null(type_node)) {
return go_parse_type_node(ctx, type_node);
}
}
// parameter_list used as result type (multi-return)
if (strcmp(kind, "parameter_list") == 0) {
int count = 0;
const CBMType* elems[16];
uint32_t nc = ts_node_child_count(node);
for (uint32_t i = 0; i < nc && count < 16; i++) {
TSNode child = ts_node_child(node, i);
if (ts_node_is_null(child) || !ts_node_is_named(child)) continue;
const char* ck = ts_node_type(child);
if (strcmp(ck, "parameter_declaration") == 0) {
TSNode tn = ts_node_child_by_field_name(child, "type", 4);
if (!ts_node_is_null(tn)) {
elems[count++] = go_parse_type_node(ctx, tn);
}
} else {
elems[count++] = go_parse_type_node(ctx, child);
}
}
if (count == 1) return elems[0];
if (count > 1) return cbm_type_tuple(ctx->arena, elems, count);
}
return cbm_type_unknown();
}
// --- Implicit generics: type unification ---
// Unify a parameter type pattern (containing TYPE_PARAM) against a concrete argument type.
// Fills inferred[i] with the concrete type bound to type_param_names[i].
static void go_unify_type(const CBMType* param_type, const CBMType* arg_type,
const char** type_param_names, const CBMType** inferred, int param_count) {
if (!param_type || !arg_type || cbm_type_is_unknown(arg_type)) return;
if (param_type->kind == CBM_TYPE_TYPE_PARAM) {
for (int i = 0; i < param_count; i++) {
if (strcmp(param_type->data.type_param.name, type_param_names[i]) == 0) {
if (!inferred[i]) inferred[i] = arg_type;
break;
}
}
return;
}
// Structural matching — recurse into composite types
if (param_type->kind == CBM_TYPE_SLICE && arg_type->kind == CBM_TYPE_SLICE) {
go_unify_type(param_type->data.slice.elem, arg_type->data.slice.elem,
type_param_names, inferred, param_count);
}
if (param_type->kind == CBM_TYPE_POINTER && arg_type->kind == CBM_TYPE_POINTER) {
go_unify_type(param_type->data.pointer.elem, arg_type->data.pointer.elem,
type_param_names, inferred, param_count);
}
if (param_type->kind == CBM_TYPE_MAP && arg_type->kind == CBM_TYPE_MAP) {
go_unify_type(param_type->data.map.key, arg_type->data.map.key,
type_param_names, inferred, param_count);
go_unify_type(param_type->data.map.value, arg_type->data.map.value,
type_param_names, inferred, param_count);
}
if (param_type->kind == CBM_TYPE_CHANNEL && arg_type->kind == CBM_TYPE_CHANNEL) {
go_unify_type(param_type->data.channel.elem, arg_type->data.channel.elem,
type_param_names, inferred, param_count);
}
if (param_type->kind == CBM_TYPE_FUNC && arg_type->kind == CBM_TYPE_FUNC) {
// Match param types
if (param_type->data.func.param_types && arg_type->data.func.param_types) {
for (int i = 0; param_type->data.func.param_types[i] && arg_type->data.func.param_types[i]; i++) {
go_unify_type(param_type->data.func.param_types[i], arg_type->data.func.param_types[i],
type_param_names, inferred, param_count);
}
}
// Match return types
if (param_type->data.func.return_types && arg_type->data.func.return_types) {
for (int i = 0; param_type->data.func.return_types[i] && arg_type->data.func.return_types[i]; i++) {
go_unify_type(param_type->data.func.return_types[i], arg_type->data.func.return_types[i],
type_param_names, inferred, param_count);
}
}
}
}
// --- go_eval_expr_type: recursive expression type evaluator ---
const CBMType* go_eval_expr_type(GoLSPContext* ctx, TSNode node) {
if (ts_node_is_null(node)) return cbm_type_unknown();
const char* kind = ts_node_type(node);
// --- Identifier: scope lookup ---
if (strcmp(kind, "identifier") == 0) {
char* name = lsp_node_text(ctx, node);
if (!name) return cbm_type_unknown();
// Check scope first
const CBMType* t = cbm_scope_lookup(ctx->current_scope, name);
if (!cbm_type_is_unknown(t)) return t;
// Check if it's a package-level function
const CBMRegisteredFunc* f = cbm_registry_lookup_symbol(ctx->registry, ctx->package_qn, name);
if (f && f->signature) return f->signature;
// Check if it's a builtin type (for type conversions like string(x), int(x))
const CBMType* bt = resolve_builtin_type(ctx, name);
if (bt) return cbm_type_named(ctx->arena, name);
// Check if it's a registered type (for type conversions like MyType(x))
const char* type_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->package_qn, name);
const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, type_qn);
if (rt) return cbm_type_named(ctx->arena, type_qn);
return cbm_type_unknown();
}
// --- Selector expression: a.B ---
if (strcmp(kind, "selector_expression") == 0) {
TSNode operand = ts_node_child_by_field_name(node, "operand", 7);
TSNode field = ts_node_child_by_field_name(node, "field", 5);
if (ts_node_is_null(operand) || ts_node_is_null(field)) return cbm_type_unknown();
char* field_name = lsp_node_text(ctx, field);
if (!field_name) return cbm_type_unknown();
// Check if operand is an import alias (pkg.Symbol)
if (strcmp(ts_node_type(operand), "identifier") == 0) {
char* pkg_name = lsp_node_text(ctx, operand);
if (pkg_name) {
const char* pkg_qn = resolve_import(ctx, pkg_name);
if (pkg_qn) {
// Look up pkg.Symbol as a function or type
const CBMRegisteredFunc* f = cbm_registry_lookup_symbol(ctx->registry, pkg_qn, field_name);
if (f && f->signature) return f->signature;
// Check if it's a type
char* type_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", pkg_qn, field_name);
const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, type_qn);
if (rt) return cbm_type_named(ctx->arena, type_qn);
return cbm_type_unknown();
}
}
}
// Evaluate operand type
const CBMType* recv_type = go_eval_expr_type(ctx, operand);
if (cbm_type_is_unknown(recv_type)) return cbm_type_unknown();
// Auto-deref pointers for method calls
const CBMType* base_type = recv_type;
if (base_type->kind == CBM_TYPE_POINTER) {
base_type = cbm_type_deref(base_type);
}
if (base_type->kind == CBM_TYPE_NAMED) {
const char* type_qn = base_type->data.named.qualified_name;
// Look up method/field (methods recurse through embeddings)
const CBMRegisteredFunc* method = go_lookup_field_or_method(ctx, type_qn, field_name);
if (method && method->signature) return method->signature;
// Check struct fields (with embedded field promotion)
const CBMType* field_type = go_lookup_field(ctx, type_qn, field_name, 0);
if (field_type && !cbm_type_is_unknown(field_type)) return field_type;
}
return cbm_type_unknown();
}
// --- Call expression: f(...) ---
if (strcmp(kind, "call_expression") == 0) {
TSNode func_node = ts_node_child_by_field_name(node, "function", 8);
TSNode args_node = ts_node_child_by_field_name(node, "arguments", 9);
if (ts_node_is_null(func_node)) return cbm_type_unknown();
// Check for builtin calls
if (strcmp(ts_node_type(func_node), "identifier") == 0) {
char* name = lsp_node_text(ctx, func_node);
if (name && is_go_builtin_func(name)) {
return go_eval_builtin_call(ctx, name, args_node);
}
}
// Evaluate function type
const CBMType* func_type = go_eval_expr_type(ctx, func_node);
// If it's a FUNC type, return its return type
if (func_type && func_type->kind == CBM_TYPE_FUNC &&
func_type->data.func.return_types && func_type->data.func.return_types[0]) {
// Check for explicit type arguments: call_expression has type_arguments field
// Go tree-sitter: Func[T1, T2](args) → call_expression { function, type_arguments, arguments }
TSNode targs_node = ts_node_child_by_field_name(node, "type_arguments", 14);
if (!ts_node_is_null(targs_node)) {
// Look up the registered function to get type_param_names
const CBMRegisteredFunc* rfunc = NULL;
char* func_name = lsp_node_text(ctx, func_node);
if (func_name) {
const char* func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s",
ctx->package_qn, func_name);
rfunc = cbm_registry_lookup_func(ctx->registry, func_qn);
// If not found as local, try via import (selector_expression: pkg.Func)
if (!rfunc && strcmp(ts_node_type(func_node), "selector_expression") == 0) {
TSNode operand = ts_node_child_by_field_name(func_node, "operand", 7);
TSNode field = ts_node_child_by_field_name(func_node, "field", 5);
if (!ts_node_is_null(operand) && !ts_node_is_null(field)) {
char* pkg = lsp_node_text(ctx, operand);
char* fn = lsp_node_text(ctx, field);
if (pkg && fn) {
const char* pkg_qn = resolve_import(ctx, pkg);
if (pkg_qn) {
rfunc = cbm_registry_lookup_symbol(ctx->registry, pkg_qn, fn);
}
}
}
}
}
if (rfunc && rfunc->type_param_names) {
// Parse type arguments from AST
const CBMType* type_args[16];
int targ_count = 0;
uint32_t ta_nc = ts_node_child_count(targs_node);
for (uint32_t ti = 0; ti < ta_nc && targ_count < 15; ti++) {
TSNode targ = ts_node_child(targs_node, ti);
if (ts_node_is_null(targ) || !ts_node_is_named(targ)) continue;
type_args[targ_count++] = go_parse_type_node(ctx, targ);
}
// Count type params
int param_count = 0;
while (rfunc->type_param_names[param_count]) param_count++;
if (targ_count > 0 && targ_count == param_count) {
const CBMType** targ_arr = (const CBMType**)cbm_arena_alloc(
ctx->arena, (targ_count + 1) * sizeof(const CBMType*));
for (int ti = 0; ti < targ_count; ti++) targ_arr[ti] = type_args[ti];
targ_arr[targ_count] = NULL;
// Substitute type params in return type(s)
int ret_count = 0;
while (func_type->data.func.return_types[ret_count]) ret_count++;
if (ret_count == 1) {
return cbm_type_substitute(ctx->arena,
func_type->data.func.return_types[0],
rfunc->type_param_names, targ_arr);
}
// Multi-return: substitute all
const CBMType** new_rets = (const CBMType**)cbm_arena_alloc(
ctx->arena, (ret_count + 1) * sizeof(const CBMType*));
for (int ri = 0; ri < ret_count; ri++) {
new_rets[ri] = cbm_type_substitute(ctx->arena,
func_type->data.func.return_types[ri],
rfunc->type_param_names, targ_arr);
}
new_rets[ret_count] = NULL;
return cbm_type_tuple(ctx->arena, new_rets, ret_count);
}
}
}
// Implicit generics: infer type args from argument types
if (ts_node_is_null(targs_node)) {
// Look up registered function to check for type_param_names
const CBMRegisteredFunc* rfunc = NULL;
char* func_name = lsp_node_text(ctx, func_node);
if (func_name) {
const char* func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s",
ctx->package_qn, func_name);
rfunc = cbm_registry_lookup_func(ctx->registry, func_qn);
if (!rfunc && strcmp(ts_node_type(func_node), "selector_expression") == 0) {
TSNode operand = ts_node_child_by_field_name(func_node, "operand", 7);
TSNode field = ts_node_child_by_field_name(func_node, "field", 5);
if (!ts_node_is_null(operand) && !ts_node_is_null(field)) {
char* pkg = lsp_node_text(ctx, operand);
char* fn = lsp_node_text(ctx, field);
if (pkg && fn) {
const char* pkg_qn = resolve_import(ctx, pkg);
if (pkg_qn)
rfunc = cbm_registry_lookup_symbol(ctx->registry, pkg_qn, fn);
}
}
}
}
if (rfunc && rfunc->type_param_names && func_type->data.func.param_types) {
int tpc = 0;
while (rfunc->type_param_names[tpc]) tpc++;
// Check if any return type uses a type param
bool has_type_param = false;
int ret_count = 0;
while (func_type->data.func.return_types[ret_count]) ret_count++;
for (int ri = 0; ri < ret_count && !has_type_param; ri++) {
// Quick check: walk return type tree for TYPE_PARAM nodes
const CBMType* rt = func_type->data.func.return_types[ri];
if (rt->kind == CBM_TYPE_TYPE_PARAM) has_type_param = true;
else if (rt->kind == CBM_TYPE_SLICE && rt->data.slice.elem &&
rt->data.slice.elem->kind == CBM_TYPE_TYPE_PARAM) has_type_param = true;
else if (rt->kind == CBM_TYPE_POINTER && rt->data.pointer.elem &&
rt->data.pointer.elem->kind == CBM_TYPE_TYPE_PARAM) has_type_param = true;
else if (rt->kind == CBM_TYPE_MAP) {
if (rt->data.map.key && rt->data.map.key->kind == CBM_TYPE_TYPE_PARAM) has_type_param = true;
if (rt->data.map.value && rt->data.map.value->kind == CBM_TYPE_TYPE_PARAM) has_type_param = true;
}
}
if (has_type_param && tpc > 0 && tpc <= 16) {
// Evaluate argument types and unify
const CBMType* inferred[16] = {0};
if (!ts_node_is_null(args_node)) {
uint32_t argc = ts_node_named_child_count(args_node);
int pi = 0;
for (uint32_t ai = 0; ai < argc && func_type->data.func.param_types[pi]; ai++) {
TSNode arg = ts_node_named_child(args_node, ai);
if (ts_node_is_null(arg)) continue;
const CBMType* arg_type = go_eval_expr_type(ctx, arg);
go_unify_type(func_type->data.func.param_types[pi],
arg_type, rfunc->type_param_names, inferred, tpc);
pi++;
}
}
// Check if all type params were inferred
bool all_inferred = true;
for (int i = 0; i < tpc; i++) {
if (!inferred[i]) { all_inferred = false; break; }
}
if (all_inferred) {
const CBMType** targ_arr = (const CBMType**)cbm_arena_alloc(
ctx->arena, (tpc + 1) * sizeof(const CBMType*));
for (int i = 0; i < tpc; i++) targ_arr[i] = inferred[i];
targ_arr[tpc] = NULL;
if (ret_count == 1) {
return cbm_type_substitute(ctx->arena,
func_type->data.func.return_types[0],
rfunc->type_param_names, targ_arr);
}
const CBMType** new_rets = (const CBMType**)cbm_arena_alloc(
ctx->arena, (ret_count + 1) * sizeof(const CBMType*));
for (int ri = 0; ri < ret_count; ri++) {
new_rets[ri] = cbm_type_substitute(ctx->arena,
func_type->data.func.return_types[ri],
rfunc->type_param_names, targ_arr);
}
new_rets[ret_count] = NULL;
return cbm_type_tuple(ctx->arena, new_rets, ret_count);
}
}
}
}
// No type arguments or no substitution needed — return as-is
if (!func_type->data.func.return_types[1]) {
return func_type->data.func.return_types[0];
}
int count = 0;
while (func_type->data.func.return_types[count]) count++;
if (count > 1) {
return cbm_type_tuple(ctx->arena, func_type->data.func.return_types, count);
}
}
// Type conversion: Type(expr) — if func_node resolves to a named type
if (func_type && func_type->kind == CBM_TYPE_NAMED) {
return func_type;
}
// Type conversion with composite type syntax: []byte(s), map[K]V(x), etc.
// The function node is a type node (slice_type, map_type, etc.)
{
const char* fk = ts_node_type(func_node);
if (strcmp(fk, "slice_type") == 0 || strcmp(fk, "array_type") == 0 ||
strcmp(fk, "map_type") == 0 || strcmp(fk, "pointer_type") == 0 ||
strcmp(fk, "channel_type") == 0) {
return go_parse_type_node(ctx, func_node);
}
}
return cbm_type_unknown();
}
// --- Composite literal: Type{...} ---
if (strcmp(kind, "composite_literal") == 0) {
TSNode type_node = ts_node_child_by_field_name(node, "type", 4);
if (!ts_node_is_null(type_node)) {
return go_parse_type_node(ctx, type_node);
}
return cbm_type_unknown();
}
// --- Unary expression: &x, *x, <-ch, !x ---
if (strcmp(kind, "unary_expression") == 0) {
TSNode operand = ts_node_child_by_field_name(node, "operand", 7);
if (ts_node_is_null(operand)) return cbm_type_unknown();
// Get operator
for (uint32_t i = 0; i < ts_node_child_count(node); i++) {
TSNode child = ts_node_child(node, i);
if (!ts_node_is_named(child)) {
char* op = lsp_node_text(ctx, child);
if (!op) continue;
if (strcmp(op, "&") == 0) {
return cbm_type_pointer(ctx->arena, go_eval_expr_type(ctx, operand));
}
if (strcmp(op, "*") == 0) {
return cbm_type_deref(go_eval_expr_type(ctx, operand));
}
if (strcmp(op, "<-") == 0) {
const CBMType* ch_type = go_eval_expr_type(ctx, operand);
if (ch_type && ch_type->kind == CBM_TYPE_CHANNEL) {
return ch_type->data.channel.elem;
}
return cbm_type_unknown();
}
if (strcmp(op, "!") == 0) {
return cbm_type_builtin(ctx->arena, "bool");
}
break;
}
}
return cbm_type_unknown();
}
// --- Index expression: a[i] ---
if (strcmp(kind, "index_expression") == 0) {
TSNode operand = ts_node_child_by_field_name(node, "operand", 7);
if (ts_node_is_null(operand)) return cbm_type_unknown();
const CBMType* op_type = go_eval_expr_type(ctx, operand);
if (!op_type) return cbm_type_unknown();
if (op_type->kind == CBM_TYPE_MAP) return op_type->data.map.value;
if (op_type->kind == CBM_TYPE_SLICE) return op_type->data.slice.elem;
return cbm_type_unknown();
}
// --- Type assertion: x.(Type) ---
if (strcmp(kind, "type_assertion_expression") == 0) {
TSNode type_node = ts_node_child_by_field_name(node, "type", 4);
if (!ts_node_is_null(type_node)) {
return go_parse_type_node(ctx, type_node);
}
return cbm_type_unknown();
}
// --- Parenthesized expression: (x) ---
if (strcmp(kind, "parenthesized_expression") == 0 && ts_node_named_child_count(node) > 0) {
return go_eval_expr_type(ctx, ts_node_named_child(node, 0));
}
// --- Binary expression ---
if (strcmp(kind, "binary_expression") == 0) {
// For comparisons, return bool
// For arithmetic, return left operand type
TSNode left = ts_node_child_by_field_name(node, "left", 4);
for (uint32_t i = 0; i < ts_node_child_count(node); i++) {
TSNode child = ts_node_child(node, i);
if (!ts_node_is_named(child)) {
char* op = lsp_node_text(ctx, child);
if (op && (strcmp(op, "==") == 0 || strcmp(op, "!=") == 0 ||
strcmp(op, "<") == 0 || strcmp(op, ">") == 0 ||
strcmp(op, "<=") == 0 || strcmp(op, ">=") == 0 ||
strcmp(op, "&&") == 0 || strcmp(op, "||") == 0)) {
return cbm_type_builtin(ctx->arena, "bool");
}
break;
}
}
if (!ts_node_is_null(left)) return go_eval_expr_type(ctx, left);
return cbm_type_unknown();
}
// --- Slice expression: a[low:high] ---
if (strcmp(kind, "slice_expression") == 0) {
TSNode operand = ts_node_child_by_field_name(node, "operand", 7);
if (!ts_node_is_null(operand)) return go_eval_expr_type(ctx, operand);
return cbm_type_unknown();
}
// --- Literals ---
if (strcmp(kind, "interpreted_string_literal") == 0 ||
strcmp(kind, "raw_string_literal") == 0) {
return cbm_type_builtin(ctx->arena, "string");
}
if (strcmp(kind, "int_literal") == 0) {
return cbm_type_builtin(ctx->arena, "int");
}
if (strcmp(kind, "float_literal") == 0) {
return cbm_type_builtin(ctx->arena, "float64");
}
if (strcmp(kind, "true") == 0 || strcmp(kind, "false") == 0) {
return cbm_type_builtin(ctx->arena, "bool");
}
if (strcmp(kind, "nil") == 0) {
return cbm_type_unknown(); // nil has no concrete type
}
// --- Func literal (closure) ---
if (strcmp(kind, "func_literal") == 0) {
// Process the closure body to resolve calls with captured scope
TSNode body = ts_node_child_by_field_name(node, "body", 4);
if (!ts_node_is_null(body)) {
// Push child scope (inherits all outer bindings via parent chain)
CBMScope* saved = ctx->current_scope;
ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope);
// Bind closure parameters
TSNode params = ts_node_child_by_field_name(node, "parameters", 10);
if (!ts_node_is_null(params)) {
uint32_t nc = ts_node_child_count(params);
for (uint32_t i = 0; i < nc; i++) {
TSNode param = ts_node_child(params, i);
if (ts_node_is_null(param) || !ts_node_is_named(param)) continue;
if (strcmp(ts_node_type(param), "parameter_declaration") != 0) continue;
TSNode type_node = ts_node_child_by_field_name(param, "type", 4);
const CBMType* pt = go_parse_type_node(ctx, type_node);
uint32_t pnc = ts_node_child_count(param);
for (uint32_t j = 0; j < pnc; j++) {
TSNode ch = ts_node_child(param, j);
if (!ts_node_is_null(ch) && ts_node_is_named(ch) &&
strcmp(ts_node_type(ch), "identifier") == 0) {
char* pname = lsp_node_text(ctx, ch);
if (pname && strcmp(pname, "_") != 0)
cbm_scope_bind(ctx->current_scope, pname, pt);
}
}
}
}
// Walk closure body to resolve calls
resolve_calls_in_node(ctx, body);
ctx->current_scope = saved;
}
// Build full FUNC type with param/return types from AST
const CBMType* pt_arr[16];
int pt_count = 0;
TSNode params2 = ts_node_child_by_field_name(node, "parameters", 10);
if (!ts_node_is_null(params2)) {
uint32_t nc2 = ts_node_child_count(params2);
for (uint32_t i = 0; i < nc2 && pt_count < 15; i++) {
TSNode p = ts_node_child(params2, i);
if (ts_node_is_null(p) || !ts_node_is_named(p)) continue;
if (strcmp(ts_node_type(p), "parameter_declaration") != 0) continue;
TSNode pt = ts_node_child_by_field_name(p, "type", 4);
if (!ts_node_is_null(pt))
pt_arr[pt_count++] = go_parse_type_node(ctx, pt);
}
}
pt_arr[pt_count] = NULL;
const CBMType* rt_arr[16];
int rt_count = 0;
TSNode result = ts_node_child_by_field_name(node, "result", 6);
if (!ts_node_is_null(result)) {
if (strcmp(ts_node_type(result), "parameter_list") == 0) {
uint32_t rnc = ts_node_child_count(result);
for (uint32_t i = 0; i < rnc && rt_count < 15; i++) {
TSNode rc = ts_node_child(result, i);
if (ts_node_is_null(rc) || !ts_node_is_named(rc)) continue;
TSNode rt = ts_node_child_by_field_name(rc, "type", 4);
if (ts_node_is_null(rt)) rt = rc;
rt_arr[rt_count++] = go_parse_type_node(ctx, rt);
}
} else {
rt_arr[rt_count++] = go_parse_type_node(ctx, result);
}
}
rt_arr[rt_count] = NULL;
return cbm_type_func(ctx->arena, NULL,
pt_count > 0 ? (const CBMType**)pt_arr : NULL,
rt_count > 0 ? (const CBMType**)rt_arr : NULL);
}
return cbm_type_unknown();
}
// --- go_eval_builtin_call ---
const CBMType* go_eval_builtin_call(GoLSPContext* ctx, const char* name, TSNode args) {
// make(Type, ...) -> Type
if (strcmp(name, "make") == 0 && !ts_node_is_null(args)) {
uint32_t nc = ts_node_named_child_count(args);
if (nc > 0) {
TSNode first_arg = ts_node_named_child(args, 0);
return go_parse_type_node(ctx, first_arg);
}
}
// new(Type) -> *Type
if (strcmp(name, "new") == 0 && !ts_node_is_null(args)) {
uint32_t nc = ts_node_named_child_count(args);
if (nc > 0) {
TSNode first_arg = ts_node_named_child(args, 0);
return cbm_type_pointer(ctx->arena, go_parse_type_node(ctx, first_arg));
}
}
// append(slice, ...) -> same slice type
if (strcmp(name, "append") == 0 && !ts_node_is_null(args)) {
uint32_t nc = ts_node_named_child_count(args);
if (nc > 0) {
return go_eval_expr_type(ctx, ts_node_named_child(args, 0));
}
}
// len, cap -> int
if (strcmp(name, "len") == 0 || strcmp(name, "cap") == 0) {
return cbm_type_builtin(ctx->arena, "int");
}
// delete -> void (no return)
if (strcmp(name, "delete") == 0) {
return cbm_type_unknown();
}
return cbm_type_unknown();
}
// --- go_lookup_field: struct field lookup with embedding recursion ---
static const CBMType* go_lookup_field(GoLSPContext* ctx,
const char* type_qn, const char* field_name, int depth) {
if (!type_qn || !field_name || depth > 5) return NULL;
const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, type_qn);
if (!rt) return NULL;
// Follow alias chain
if (rt->alias_of) return go_lookup_field(ctx, rt->alias_of, field_name, depth + 1);
// Direct field lookup
if (rt->field_names) {
for (int i = 0; rt->field_names[i]; i++) {
if (strcmp(rt->field_names[i], field_name) == 0 && rt->field_types && rt->field_types[i]) {
return rt->field_types[i];
}
}
}
// Promoted fields from embedded types
if (rt->embedded_types) {
for (int i = 0; rt->embedded_types[i]; i++) {
const CBMType* f = go_lookup_field(ctx, rt->embedded_types[i], field_name, depth + 1);
if (f) return f;
}
}
return NULL;
}
// --- go_lookup_field_or_method: method sets + embedding ---
static const CBMRegisteredFunc* go_lookup_field_or_method_depth(GoLSPContext* ctx,
const char* type_qn, const char* member_name, int depth) {
if (!type_qn || !member_name) return NULL;
if (depth > CBM_LSP_MAX_LOOKUP_DEPTH) return NULL;
// Direct method lookup
const CBMRegisteredFunc* f = cbm_registry_lookup_method(ctx->registry, type_qn, member_name);
if (f) return f;
const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, type_qn);
if (rt) {
// Follow type alias chain
if (rt->alias_of) {
f = go_lookup_field_or_method_depth(ctx, rt->alias_of, member_name, depth + 1);
if (f) return f;
}
// Check embedded types (promoted methods)
if (rt->embedded_types) {
for (int i = 0; rt->embedded_types[i]; i++) {
f = go_lookup_field_or_method_depth(ctx, rt->embedded_types[i], member_name, depth + 1);
if (f) return f;
}
}
}
return NULL;
}
const CBMRegisteredFunc* go_lookup_field_or_method(GoLSPContext* ctx,
const char* type_qn, const char* member_name) {
return go_lookup_field_or_method_depth(ctx, type_qn, member_name, 0);
}
// --- go_process_statement: bind variables from statements ---
void go_process_statement(GoLSPContext* ctx, TSNode node) {
if (ts_node_is_null(node)) return;
const char* kind = ts_node_type(node);
// short_var_declaration: a, b := expr
if (strcmp(kind, "short_var_declaration") == 0) {
TSNode left = ts_node_child_by_field_name(node, "left", 4);
TSNode right = ts_node_child_by_field_name(node, "right", 5);
if (ts_node_is_null(left) || ts_node_is_null(right)) return;
const CBMType* rhs_type = NULL;
// Check if RHS is an expression_list (multiple values)
if (strcmp(ts_node_type(right), "expression_list") == 0) {
uint32_t rhs_count = ts_node_named_child_count(right);
if (rhs_count == 1) {
// Single expression that might return a tuple (multi-return)
rhs_type = go_eval_expr_type(ctx, ts_node_named_child(right, 0));
}
} else {
rhs_type = go_eval_expr_type(ctx, right);
}
// Bind left-hand side variables
if (strcmp(ts_node_type(left), "expression_list") == 0) {
uint32_t lhs_count = ts_node_named_child_count(left);
for (uint32_t i = 0; i < lhs_count; i++) {
TSNode lhs_var = ts_node_named_child(left, i);
if (strcmp(ts_node_type(lhs_var), "identifier") != 0) continue;
char* var_name = lsp_node_text(ctx, lhs_var);
if (!var_name || strcmp(var_name, "_") == 0) continue;
const CBMType* var_type = cbm_type_unknown();
if (rhs_type) {
if (rhs_type->kind == CBM_TYPE_TUPLE && (int)i < rhs_type->data.tuple.count) {
var_type = rhs_type->data.tuple.elems[i];
} else if (i == 0) {
var_type = rhs_type;
}
}
cbm_scope_bind(ctx->current_scope, var_name, var_type);
}
} else if (strcmp(ts_node_type(left), "identifier") == 0) {
char* var_name = lsp_node_text(ctx, left);
if (var_name && strcmp(var_name, "_") != 0 && rhs_type) {
cbm_scope_bind(ctx->current_scope, var_name, rhs_type);
}
}
return;
}
// var_spec: var x Type = expr OR var a, b, c Type = expr
if (strcmp(kind, "var_spec") == 0) {
TSNode type_node = ts_node_child_by_field_name(node, "type", 4);
TSNode value_node = ts_node_child_by_field_name(node, "value", 5);
const CBMType* var_type = cbm_type_unknown();
if (!ts_node_is_null(type_node)) {
var_type = go_parse_type_node(ctx, type_node);
} else if (!ts_node_is_null(value_node)) {
if (strcmp(ts_node_type(value_node), "expression_list") == 0 &&
ts_node_named_child_count(value_node) > 0) {
var_type = go_eval_expr_type(ctx, ts_node_named_child(value_node, 0));
} else {
var_type = go_eval_expr_type(ctx, value_node);
}
}
// Bind all name identifiers (handles: var a, b, c int)
uint32_t vnc = ts_node_child_count(node);
for (uint32_t vi = 0; vi < vnc; vi++) {
TSNode ch = ts_node_child(node, vi);
if (ts_node_is_null(ch) || !ts_node_is_named(ch)) continue;
if (strcmp(ts_node_type(ch), "identifier") == 0) {
char* var_name = lsp_node_text(ctx, ch);
if (var_name && strcmp(var_name, "_") != 0) {
cbm_scope_bind(ctx->current_scope, var_name, var_type);
}
}
}
return;
}
// const_spec: const x Type = expr OR const x = expr
if (strcmp(kind, "const_spec") == 0) {
TSNode name_node = ts_node_child_by_field_name(node, "name", 4);
TSNode type_node = ts_node_child_by_field_name(node, "type", 4);
TSNode value_node = ts_node_child_by_field_name(node, "value", 5);
if (ts_node_is_null(name_node)) return;
const CBMType* const_type = cbm_type_unknown();
if (!ts_node_is_null(type_node)) {
const_type = go_parse_type_node(ctx, type_node);
} else if (!ts_node_is_null(value_node)) {
// For expression_list values, evaluate first element
if (strcmp(ts_node_type(value_node), "expression_list") == 0 &&
ts_node_named_child_count(value_node) > 0) {
const_type = go_eval_expr_type(ctx, ts_node_named_child(value_node, 0));
} else {
const_type = go_eval_expr_type(ctx, value_node);
}
}
if (strcmp(ts_node_type(name_node), "identifier") == 0) {
char* name = lsp_node_text(ctx, name_node);
if (name && strcmp(name, "_") != 0)
cbm_scope_bind(ctx->current_scope, name, const_type);
}
return;
}
// range_clause: for k, v := range container
if (strcmp(kind, "range_clause") == 0) {
TSNode left = ts_node_child_by_field_name(node, "left", 4);
TSNode right = ts_node_child_by_field_name(node, "right", 5);
if (ts_node_is_null(right)) return;
const CBMType* container_type = go_eval_expr_type(ctx, right);
// Determine key and value types based on container type
const CBMType* key_type = cbm_type_unknown();
const CBMType* val_type = cbm_type_unknown();
if (container_type) {
switch (container_type->kind) {
case CBM_TYPE_SLICE:
key_type = cbm_type_builtin(ctx->arena, "int");
val_type = container_type->data.slice.elem;
break;
case CBM_TYPE_MAP:
key_type = container_type->data.map.key;
val_type = container_type->data.map.value;
break;
case CBM_TYPE_CHANNEL:
val_type = container_type->data.channel.elem;
break;
default:
if (container_type->kind == CBM_TYPE_BUILTIN &&
strcmp(container_type->data.builtin.name, "string") == 0) {
key_type = cbm_type_builtin(ctx->arena, "int");
val_type = cbm_type_builtin(ctx->arena, "rune");
}
break;
}
}
if (!ts_node_is_null(left) && strcmp(ts_node_type(left), "expression_list") == 0) {
uint32_t lhs_count = ts_node_named_child_count(left);
for (uint32_t i = 0; i < lhs_count; i++) {
TSNode var_node = ts_node_named_child(left, i);
if (strcmp(ts_node_type(var_node), "identifier") != 0) continue;
char* var_name = lsp_node_text(ctx, var_node);
if (!var_name || strcmp(var_name, "_") == 0) continue;
cbm_scope_bind(ctx->current_scope, var_name, i == 0 ? key_type : val_type);
}
}
return;
}
// type_switch_statement is handled in resolve_calls_in_node, not here
// (needs per-case scope narrowing, not just variable binding)
}
// --- Emit a resolved call ---
static void emit_resolved_call(GoLSPContext* 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);
}
// Emit a diagnostic for an unresolved call (confidence 0.0).
static void emit_unresolved_call(GoLSPContext* ctx, const char* expr_text, const char* reason) {
if (!ctx->resolved_calls || !ctx->enclosing_func_qn) return;
CBMResolvedCall rc;
rc.caller_qn = ctx->enclosing_func_qn;
rc.callee_qn = expr_text ? expr_text : "?";
rc.strategy = "lsp_unresolved";
rc.confidence = 0.0f;
rc.reason = reason;
cbm_resolvedcall_push(ctx->resolved_calls, ctx->arena, rc);
}
// --- Walk call expressions and resolve them ---
static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {
if (ts_node_is_null(node)) return;
const char* kind = ts_node_type(node);
// Process statements to build scope
go_process_statement(ctx, node);
// Resolve call expressions
if (strcmp(kind, "call_expression") == 0) {
TSNode func_node = ts_node_child_by_field_name(node, "function", 8);
if (!ts_node_is_null(func_node)) {
const char* fk = ts_node_type(func_node);
// selector_expression: obj.Method() or pkg.Func()
if (strcmp(fk, "selector_expression") == 0) {
TSNode operand = ts_node_child_by_field_name(func_node, "operand", 7);
TSNode field = ts_node_child_by_field_name(func_node, "field", 5);
if (!ts_node_is_null(operand) && !ts_node_is_null(field)) {
char* field_name = lsp_node_text(ctx, field);
// Check if operand is a package import
if (strcmp(ts_node_type(operand), "identifier") == 0) {
char* pkg_name = lsp_node_text(ctx, operand);
if (pkg_name) {
const char* pkg_qn = resolve_import(ctx, pkg_name);
if (pkg_qn && field_name) {
const CBMRegisteredFunc* f = cbm_registry_lookup_symbol(ctx->registry, pkg_qn, field_name);
if (f) {
emit_resolved_call(ctx, f->qualified_name, "lsp_direct", 0.95f);
goto recurse;
}
// Package found but symbol not in registry
emit_unresolved_call(ctx,
cbm_arena_sprintf(ctx->arena, "%s.%s", pkg_name, field_name),
"symbol_not_in_registry");
goto recurse;
}
}
}
// Type-based method dispatch
if (field_name) {
const CBMType* recv_type = go_eval_expr_type(ctx, operand);
const CBMType* base = recv_type;
if (base && base->kind == CBM_TYPE_POINTER) base = cbm_type_deref(base);
if (base && base->kind == CBM_TYPE_NAMED) {
const CBMRegisteredFunc* method = go_lookup_field_or_method(ctx,
base->data.named.qualified_name, field_name);
if (method) {
const char* strategy = "lsp_type_dispatch";
if (method->receiver_type &&
strcmp(method->receiver_type, base->data.named.qualified_name) != 0) {
strategy = "lsp_embed_dispatch";
}
emit_resolved_call(ctx, method->qualified_name, strategy, 0.95f);
goto recurse;
}
}
// Interface dispatch: NAMED type that is an interface, or bare INTERFACE type
if (base && field_name) {
bool is_iface = (base->kind == CBM_TYPE_INTERFACE);
const char* iface_qn = NULL;
if (!is_iface && base->kind == CBM_TYPE_NAMED) {
const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry,
base->data.named.qualified_name);
if (rt && rt->is_interface) {
is_iface = true;
iface_qn = base->data.named.qualified_name;
}
}
if (is_iface) {
// Try interface satisfaction: find concrete types implementing this interface
const CBMRegisteredType* iface_rt = iface_qn ?
cbm_registry_lookup_type(ctx->registry, iface_qn) : NULL;
if (iface_rt && iface_rt->method_names && iface_rt->method_names[0]) {
// Count interface methods
int iface_mcount = 0;
while (iface_rt->method_names[iface_mcount]) iface_mcount++;
// Scan all registered types for satisfaction
const char* sole_impl_qn = NULL;
int impl_count = 0;
// Skip stdlib types when interface is from a project package
bool iface_is_project = iface_qn && strchr(iface_qn, '/') != NULL;
for (int ti = 0; ti < ctx->registry->type_count && impl_count < 2; ti++) {
const CBMRegisteredType* cand = &ctx->registry->types[ti];
if (cand->is_interface) continue;
if (!cand->qualified_name) continue;
if (cand->alias_of) continue;
// For project interfaces, skip stdlib candidates (no '/' in QN)
if (iface_is_project && !strchr(cand->qualified_name, '/')) continue;
// Check if candidate has all interface methods
bool satisfies = true;
for (int mi = 0; mi < iface_mcount; mi++) {
if (!cbm_registry_lookup_method(ctx->registry,
cand->qualified_name, iface_rt->method_names[mi])) {
satisfies = false;
break;
}
}
if (satisfies) {
sole_impl_qn = cand->qualified_name;
impl_count++;
}
}
if (impl_count == 1 && sole_impl_qn) {
// Single implementer: resolve to concrete method
const CBMRegisteredFunc* concrete_method =
cbm_registry_lookup_method(ctx->registry, sole_impl_qn, field_name);
if (concrete_method) {
// Sole-implementer interface dispatch is an unambiguous
// resolution (exactly one concrete method); rank it at least
// as high as a direct type dispatch (0.95) so the concrete
// `Type.method` wins over the interface-method type_dispatch
// for the same call site.
emit_resolved_call(ctx, concrete_method->qualified_name,
"lsp_interface_resolve", 0.95f);
goto recurse;
}
}
}
// Fallback: generic interface dispatch
emit_resolved_call(ctx,
cbm_arena_sprintf(ctx->arena, "%s.%s",
iface_qn ? iface_qn : "interface", field_name),
"lsp_interface_dispatch", 0.85f);
goto recurse;
}
}
// Type resolved to NAMED but neither method nor interface matched
if (base && base->kind == CBM_TYPE_NAMED) {
emit_unresolved_call(ctx,
cbm_arena_sprintf(ctx->arena, "%s.%s",
base->data.named.qualified_name, field_name),
"method_not_found");
} else if (cbm_type_is_unknown(recv_type)) {
char* operand_text = lsp_node_text(ctx, operand);
emit_unresolved_call(ctx,
cbm_arena_sprintf(ctx->arena, "%s.%s",
operand_text ? operand_text : "?", field_name),
"unknown_receiver_type");
}
}
}
}
// Direct function call: FuncName()
if (strcmp(fk, "identifier") == 0) {
char* name = lsp_node_text(ctx, func_node);
if (name && !is_go_builtin_func(name)) {
// Package-local function
const CBMRegisteredFunc* f = cbm_registry_lookup_symbol(ctx->registry, ctx->package_qn, name);
if (f) {
emit_resolved_call(ctx, f->qualified_name, "lsp_direct", 0.95f);
} else {
emit_unresolved_call(ctx, name, "function_not_in_registry");
}
}
}
}
}
recurse:;
// Push scope for blocks and statements that introduce variables
bool push_scope = (strcmp(kind, "block") == 0 ||
strcmp(kind, "if_statement") == 0 ||
strcmp(kind, "for_statement") == 0 ||
strcmp(kind, "expression_switch_statement") == 0);
if (push_scope) {
ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope);
}
// Process initializer field for if/for/switch statements.
// Go patterns: if err := f(); ..., for i := 0; ..., switch v := x; v { ... }
if (strcmp(kind, "if_statement") == 0 ||
strcmp(kind, "for_statement") == 0 ||
strcmp(kind, "expression_switch_statement") == 0) {
TSNode init = ts_node_child_by_field_name(node, "initializer", 11);
if (!ts_node_is_null(init)) {
go_process_statement(ctx, init);
}
}
// Process for_statement range_clause before recursing into body
if (strcmp(kind, "for_statement") == 0) {
for (uint32_t i = 0; i < ts_node_child_count(node); i++) {
TSNode child = ts_node_child(node, i);
if (!ts_node_is_null(child) && ts_node_is_named(child) &&
strcmp(ts_node_type(child), "range_clause") == 0) {
go_process_statement(ctx, child);
break;
}
}
}
// Type switch: switch a := expr.(type) { case *T: a.Method() }
// Go tree-sitter structure: type_switch_statement has flat children:
// expression_list (contains var name "a")
// identifier (operand "animal")
// type_case (case clause, may repeat)
if (strcmp(kind, "type_switch_statement") == 0) {
const char* switch_var = NULL;
// Pass 1: find switch variable and operand type
uint32_t nc2 = ts_node_child_count(node);
bool found_assign = false;
for (uint32_t i = 0; i < nc2; i++) {
TSNode child = ts_node_child(node, i);
if (ts_node_is_null(child)) continue;
const char* ck = ts_node_type(child);
// expression_list before := is the LHS (variable name)
if (!found_assign && strcmp(ck, "expression_list") == 0) {
TSNode var_node = ts_node_named_child(child, 0);
if (!ts_node_is_null(var_node) && strcmp(ts_node_type(var_node), "identifier") == 0)
switch_var = lsp_node_text(ctx, var_node);
}
// := operator marks the assignment
if (!ts_node_is_named(child)) {
char* tok = lsp_node_text(ctx, child);
if (tok && strcmp(tok, ":=") == 0) found_assign = true;
}
// identifier after := is the operand being type-switched
if (found_assign && strcmp(ck, "identifier") == 0) {
(void)go_eval_expr_type(ctx, child);
}
}
// Pass 2: process each type_case with narrowed scope
for (uint32_t i = 0; i < nc2; i++) {
TSNode child = ts_node_child(node, i);
if (ts_node_is_null(child) || !ts_node_is_named(child)) continue;
if (strcmp(ts_node_type(child), "type_case") != 0) continue;
CBMScope* saved = ctx->current_scope;
ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope);
// Find case type and bind switch variable
uint32_t cc_count = ts_node_child_count(child);
for (uint32_t j = 0; j < cc_count; j++) {
TSNode cc_child = ts_node_child(child, j);
if (ts_node_is_null(cc_child) || !ts_node_is_named(cc_child)) continue;
const char* cc_kind = ts_node_type(cc_child);
if (strcmp(cc_kind, "type_identifier") == 0 ||
strcmp(cc_kind, "qualified_type") == 0 ||
strcmp(cc_kind, "pointer_type") == 0 ||
strcmp(cc_kind, "slice_type") == 0) {
if (switch_var) {
cbm_scope_bind(ctx->current_scope, switch_var,
go_parse_type_node(ctx, cc_child));
}
break;
}
}
// Recurse into case body statements
for (uint32_t j = 0; j < cc_count; j++) {
TSNode cc_child = ts_node_child(child, j);
if (ts_node_is_null(cc_child) || !ts_node_is_named(cc_child)) continue;
const char* cc_kind = ts_node_type(cc_child);
// Skip type nodes, process everything else (expression_statement, etc.)
if (strcmp(cc_kind, "type_identifier") == 0 ||
strcmp(cc_kind, "qualified_type") == 0 ||
strcmp(cc_kind, "pointer_type") == 0 ||
strcmp(cc_kind, "slice_type") == 0) continue;
resolve_calls_in_node(ctx, cc_child);
}
ctx->current_scope = saved;
}
if (push_scope) ctx->current_scope = cbm_scope_pop(ctx->current_scope);
return;
}
// Select statement: each communication_case gets its own scope
// case msg := <-ch: → receive_statement has left (vars) + unary_expression (<-ch)
if (strcmp(kind, "select_statement") == 0) {
uint32_t nc3 = ts_node_child_count(node);
for (uint32_t i = 0; i < nc3; i++) {
TSNode child = ts_node_child(node, i);
if (ts_node_is_null(child) || !ts_node_is_named(child)) continue;
const char* ck3 = ts_node_type(child);
if (strcmp(ck3, "communication_case") == 0 || strcmp(ck3, "default_case") == 0) {
CBMScope* saved = ctx->current_scope;
ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope);
// Process children: receive_statement binds vars, then recurse body
uint32_t cc_count = ts_node_child_count(child);
for (uint32_t j = 0; j < cc_count; j++) {
TSNode cc_child = ts_node_child(child, j);
if (ts_node_is_null(cc_child) || !ts_node_is_named(cc_child)) continue;
const char* cc_kind = ts_node_type(cc_child);
if (strcmp(cc_kind, "receive_statement") == 0) {
// receive_statement: left := <-right
// tree-sitter Go: right field is the channel expression (after <-)
// The receive value type is the channel's element type
TSNode left = ts_node_child_by_field_name(cc_child, "left", 4);
TSNode right = ts_node_child_by_field_name(cc_child, "right", 5);
if (!ts_node_is_null(right)) {
const CBMType* right_type = go_eval_expr_type(ctx, right);
// right_type might be a channel (if right is the channel)
// or already the elem type (if right is a <-ch unary expr)
const CBMType* recv_type = right_type;
if (right_type && right_type->kind == CBM_TYPE_CHANNEL) {
recv_type = right_type->data.channel.elem;
}
if (!ts_node_is_null(left)) {
if (strcmp(ts_node_type(left), "expression_list") == 0) {
uint32_t lhs_count = ts_node_named_child_count(left);
for (uint32_t k = 0; k < lhs_count; k++) {
TSNode var_node = ts_node_named_child(left, k);
if (strcmp(ts_node_type(var_node), "identifier") != 0) continue;
char* var_name = lsp_node_text(ctx, var_node);
if (!var_name || strcmp(var_name, "_") == 0) continue;
if (k == 0) {
cbm_scope_bind(ctx->current_scope, var_name, recv_type);
} else {
// second var is the ok bool
cbm_scope_bind(ctx->current_scope, var_name,
cbm_type_builtin(ctx->arena, "bool"));
}
}
} else if (strcmp(ts_node_type(left), "identifier") == 0) {
char* var_name = lsp_node_text(ctx, left);
if (var_name && strcmp(var_name, "_") != 0) {
cbm_scope_bind(ctx->current_scope, var_name, recv_type);
}
}
}
}
} else {
// Body statements
resolve_calls_in_node(ctx, cc_child);
}
}
ctx->current_scope = saved;
}
}
if (push_scope) ctx->current_scope = cbm_scope_pop(ctx->current_scope);
return;
}
// Recurse into children via a cursor (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)) {
do {
resolve_calls_in_node(ctx, ts_tree_cursor_current_node(&cursor));
} while (ts_tree_cursor_goto_next_sibling(&cursor));
}
ts_tree_cursor_delete(&cursor);
}
if (push_scope) {
ctx->current_scope = cbm_scope_pop(ctx->current_scope);
}
}
// --- Process a single function body ---
static void process_function(GoLSPContext* ctx, TSNode func_node) {
// Set enclosing function QN
TSNode name_node = ts_node_child_by_field_name(func_node, "name", 4);
if (ts_node_is_null(name_node)) return;
char* func_name = lsp_node_text(ctx, name_node);
if (!func_name || !func_name[0]) return;
// For methods, the enclosing-function QN must include the receiver type
// (package.Type.Method), matching how the textual extractor and the
// registry qualify the method. Building it as package.Method (no receiver)
// here made the LSP-resolved call's caller_qn disagree with the textual
// call's enclosing_func_qn, so cbm_pipeline_find_lsp_resolution never
// joined them — every call inside a method body silently lost its
// type-aware LSP strategy. Derive the bare receiver type name the same way
// the receiver binding below does.
char* recv_type_name = NULL;
{
TSNode recv0 = ts_node_child_by_field_name(func_node, "receiver", 8);
if (!ts_node_is_null(recv0)) {
uint32_t rnc0 = ts_node_child_count(recv0);
for (uint32_t i = 0; i < rnc0 && !recv_type_name; i++) {
TSNode rp = ts_node_child(recv0, i);
if (ts_node_is_null(rp) || !ts_node_is_named(rp)) continue;
if (strcmp(ts_node_type(rp), "parameter_declaration") != 0) continue;
TSNode rtype = ts_node_child_by_field_name(rp, "type", 4);
if (ts_node_is_null(rtype)) continue;
// Unwrap a pointer receiver (*Type) to the bare type identifier.
const char* rtk = ts_node_type(rtype);
if (strcmp(rtk, "pointer_type") == 0 && ts_node_named_child_count(rtype) > 0) {
rtype = ts_node_named_child(rtype, 0);
}
char* tn = lsp_node_text(ctx, rtype);
if (tn && tn[0]) recv_type_name = tn;
}
}
}
if (recv_type_name) {
ctx->enclosing_func_qn =
cbm_arena_sprintf(ctx->arena, "%s.%s.%s", ctx->package_qn, recv_type_name, func_name);
} else {
ctx->enclosing_func_qn = cbm_arena_sprintf(ctx->arena, "%s.%s", ctx->package_qn, func_name);
}
// Push function scope
CBMScope* saved_scope = ctx->current_scope;
ctx->current_scope = cbm_scope_push(ctx->arena, ctx->current_scope);
// Bind parameters into scope (including variadic)
TSNode params = ts_node_child_by_field_name(func_node, "parameters", 10);
if (!ts_node_is_null(params)) {
uint32_t nc = ts_node_child_count(params);
for (uint32_t i = 0; i < nc; i++) {
TSNode param = ts_node_child(params, i);
if (ts_node_is_null(param) || !ts_node_is_named(param)) continue;
const char* pk = ts_node_type(param);
bool is_variadic = (strcmp(pk, "variadic_parameter_declaration") == 0);
if (!is_variadic && strcmp(pk, "parameter_declaration") != 0) continue;
// Get type
TSNode type_node = ts_node_child_by_field_name(param, "type", 4);
const CBMType* param_type = go_parse_type_node(ctx, type_node);
// Variadic: ...T is []T in the function body
if (is_variadic && param_type) {
param_type = cbm_type_slice(ctx->arena, param_type);
}
// Get name(s) — Go allows multiple names per declaration: a, b int
uint32_t pnc = ts_node_child_count(param);
for (uint32_t j = 0; j < pnc; j++) {
TSNode child = ts_node_child(param, j);
if (ts_node_is_null(child) || !ts_node_is_named(child)) continue;
if (strcmp(ts_node_type(child), "identifier") == 0) {
char* pname = lsp_node_text(ctx, child);
if (pname && strcmp(pname, "_") != 0) {
cbm_scope_bind(ctx->current_scope, pname, param_type);
}
}
}
}
}
// Bind named return values into scope
TSNode result_node = ts_node_child_by_field_name(func_node, "result", 6);
if (!ts_node_is_null(result_node)) {
// result can be a parameter_list (named returns) or a simple type
const char* rk = ts_node_type(result_node);
if (strcmp(rk, "parameter_list") == 0) {
uint32_t rnc = ts_node_child_count(result_node);
for (uint32_t i = 0; i < rnc; i++) {
TSNode rparam = ts_node_child(result_node, i);
if (ts_node_is_null(rparam) || !ts_node_is_named(rparam)) continue;
if (strcmp(ts_node_type(rparam), "parameter_declaration") != 0) continue;
TSNode rtype = ts_node_child_by_field_name(rparam, "type", 4);
const CBMType* ret_type = go_parse_type_node(ctx, rtype);
uint32_t rpnc = ts_node_child_count(rparam);
for (uint32_t j = 0; j < rpnc; j++) {
TSNode child = ts_node_child(rparam, j);
if (ts_node_is_null(child) || !ts_node_is_named(child)) continue;
if (strcmp(ts_node_type(child), "identifier") == 0) {
char* rname = lsp_node_text(ctx, child);
if (rname && strcmp(rname, "_") != 0) {
cbm_scope_bind(ctx->current_scope, rname, ret_type);
}
}
}
}
}
}
// Bind receiver for methods
TSNode recv = ts_node_child_by_field_name(func_node, "receiver", 8);
if (!ts_node_is_null(recv)) {
// receiver is a parameter_list with one parameter_declaration
uint32_t rnc = ts_node_child_count(recv);
for (uint32_t i = 0; i < rnc; i++) {
TSNode rp = ts_node_child(recv, i);
if (ts_node_is_null(rp) || !ts_node_is_named(rp)) continue;
if (strcmp(ts_node_type(rp), "parameter_declaration") != 0) continue;
TSNode rtype = ts_node_child_by_field_name(rp, "type", 4);
const CBMType* recv_type = go_parse_type_node(ctx, rtype);
// Find receiver name
uint32_t rpnc = ts_node_child_count(rp);
for (uint32_t j = 0; j < rpnc; j++) {
TSNode rc = ts_node_child(rp, j);
if (!ts_node_is_null(rc) && ts_node_is_named(rc) &&
strcmp(ts_node_type(rc), "identifier") == 0) {
char* rname = lsp_node_text(ctx, rc);
if (rname && strcmp(rname, "_") != 0) {
cbm_scope_bind(ctx->current_scope, rname, recv_type);
}
break;
}
}
}
}
// Walk function body
TSNode body = ts_node_child_by_field_name(func_node, "body", 4);
if (!ts_node_is_null(body)) {
resolve_calls_in_node(ctx, body);
}
// Restore scope
ctx->current_scope = saved_scope;
}
// --- Process entire file ---
void go_lsp_process_file(GoLSPContext* ctx, TSNode root) {
if (ts_node_is_null(root)) return;
// 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);
// Pass 1: Bind package-level var/const declarations into root scope.
// Must happen before processing functions so all globals are visible.
for (uint32_t i = 0; i < kn; i++) {
TSNode child = kids[i];
const char* kind = ts_node_type(child);
if (strcmp(kind, "var_declaration") == 0 || strcmp(kind, "const_declaration") == 0) {
uint32_t vnc = ts_node_child_count(child);
for (uint32_t j = 0; j < vnc; j++) {
TSNode spec = ts_node_child(child, j);
if (ts_node_is_null(spec) || !ts_node_is_named(spec)) continue;
go_process_statement(ctx, spec);
}
}
}
// Pass 2: Process function/method bodies
for (uint32_t i = 0; i < kn; i++) {
TSNode child = kids[i];
const char* kind = ts_node_type(child);
if (strcmp(kind, "function_declaration") == 0 ||
strcmp(kind, "method_declaration") == 0) {
process_function(ctx, child);
}
}
}
// --- Helper: parse Go return type text into CBMType ---
const CBMType* cbm_parse_return_type_text(CBMArena* a, const char* text, const char* module_qn) {
if (!text || !text[0]) return cbm_type_unknown();
// Pointer: *Foo
if (text[0] == '*') {
return cbm_type_pointer(a, cbm_parse_return_type_text(a, text + 1, module_qn));
}
// Slice: []Foo
if (text[0] == '[' && text[1] == ']') {
return cbm_type_slice(a, cbm_parse_return_type_text(a, text + 2, module_qn));
}
// Builtin types
static const char* builtins[] = {
"int","int8","int16","int32","int64",
"uint","uint8","uint16","uint32","uint64",
"float32","float64","string","bool","byte","rune","error",
"any","uintptr",
NULL
};
for (const char** b = builtins; *b; b++) {
if (strcmp(text, *b) == 0) return cbm_type_builtin(a, text);
}
// Named type: assume local to module
return cbm_type_named(a, cbm_arena_sprintf(a, "%s.%s", module_qn, text));
}
// --- Entry point: build registry from file defs + run LSP ---
void cbm_run_go_lsp(CBMArena* arena, CBMFileResult* result,
const char* source, int source_len, TSNode root) {
CBMTypeRegistry reg;
cbm_registry_init(&reg, arena);
// Register Go stdlib types/functions
cbm_go_stdlib_register(&reg, arena);
const char* module_qn = result->module_qn;
// Phase 1: Register all types and functions from the file's own definitions
for (int i = 0; i < result->defs.count; i++) {
CBMDefinition* d = &result->defs.items[i];
if (!d->qualified_name || !d->name) continue;
// Register every type-like container (Class/Struct/Type/Interface/Enum/
// Trait). Struct included so a Go `type T struct {...}` (now labelled
// "Struct") is registered as a type and its methods/embedding resolve.
if (cbm_label_is_type_like(d->label)) {
CBMRegisteredType rt;
memset(&rt, 0, sizeof(rt));
rt.qualified_name = d->qualified_name;
rt.short_name = d->name;
rt.is_interface = d->label && strcmp(d->label, "Interface") == 0;
// Populate embedded_types from base_classes (Go struct embedding)
if (d->base_classes) {
int bc_count = 0;
while (d->base_classes[bc_count]) bc_count++;
if (bc_count > 0) {
const char** embedded = (const char**)cbm_arena_alloc(arena, (bc_count + 1) * sizeof(const char*));
for (int j = 0; j < bc_count; j++) {
const char* bc = d->base_classes[j];
// Strip pointer prefix for embedded *Type
while (bc[0] == '*') bc++;
// Qualify the embedded type name
embedded[j] = cbm_arena_sprintf(arena, "%s.%s", module_qn, bc);
}
embedded[bc_count] = NULL;
rt.embedded_types = embedded;
}
}
cbm_registry_add_type(&reg, rt);
}
// Register Function/Method nodes
if (d->label && (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;
// Build FUNC type from return_types
const CBMType** ret_types = NULL;
if (d->return_types) {
int count = 0;
while (d->return_types[count]) count++;
if (count > 0) {
ret_types = (const CBMType**)cbm_arena_alloc(arena, (count + 1) * sizeof(const CBMType*));
for (int j = 0; j < count; j++) {
ret_types[j] = cbm_parse_return_type_text(arena, d->return_types[j], module_qn);
}
ret_types[count] = NULL;
}
} else if (d->return_type && d->return_type[0]) {
// Fallback: single return_type string
ret_types = (const CBMType**)cbm_arena_alloc(arena, 2 * sizeof(const CBMType*));
ret_types[0] = cbm_parse_return_type_text(arena, d->return_type, module_qn);
ret_types[1] = NULL;
}
// Build param types from param_types array
const CBMType** param_types_arr = NULL;
if (d->param_types) {
int count = 0;
while (d->param_types[count]) count++;
if (count > 0) {
param_types_arr = (const CBMType**)cbm_arena_alloc(arena, (count + 1) * sizeof(const CBMType*));
for (int j = 0; j < count; j++) {
param_types_arr[j] = cbm_parse_return_type_text(arena, d->param_types[j], module_qn);
}
param_types_arr[count] = NULL;
}
}
rf.signature = cbm_type_func(arena, d->param_names, param_types_arr, ret_types);
// For methods, extract receiver type from receiver text
if (strcmp(d->label, "Method") == 0 && d->receiver && d->receiver[0]) {
// Receiver format: "(name *Type)" or "(name Type)"
// Extract type name: skip parens, skip first identifier, strip *
const char* r = d->receiver;
while (*r == '(' || *r == ' ') r++;
// Skip receiver name
while (*r && *r != ' ' && *r != '*') r++;
while (*r == ' ' || *r == '*') r++;
// Now r points to the type name
const char* end = r;
while (*end && *end != ')' && *end != ' ') end++;
if (end > r) {
char* type_name = cbm_arena_strndup(arena, r, end - r);
rf.receiver_type = cbm_arena_sprintf(arena, "%s.%s", module_qn, type_name);
// Also register this method under the type
const CBMRegisteredType* existing = cbm_registry_lookup_type(&reg, rf.receiver_type);
if (!existing) {
// Auto-create the type entry
CBMRegisteredType auto_type;
memset(&auto_type, 0, sizeof(auto_type));
auto_type.qualified_name = rf.receiver_type;
auto_type.short_name = type_name;
cbm_registry_add_type(&reg, auto_type);
}
}
}
cbm_registry_add_func(&reg, rf);
}
}
// Phase 1b: Scan AST for struct definitions to populate embedded_types
{
// Collect top-level children once (O(n)); ts_node_child(root,i) is O(i) → O(n²).
uint32_t rkn = 0;
TSNode* rkids = cbm_lsp_collect_children(arena, root, &rkn);
for (uint32_t i = 0; i < rkn; i++) {
TSNode top = rkids[i];
const char* tk = ts_node_type(top);
// type_declaration contains type_spec children
if (strcmp(tk, "type_declaration") != 0) continue;
uint32_t td_nc = ts_node_child_count(top);
for (uint32_t j = 0; j < td_nc; j++) {
TSNode spec = ts_node_child(top, j);
if (ts_node_is_null(spec) || !ts_node_is_named(spec)) continue;
const char* spec_kind = ts_node_type(spec);
// type_alias: type Foo = Bar
if (strcmp(spec_kind, "type_alias") == 0) {
TSNode alias_name = ts_node_child_by_field_name(spec, "name", 4);
TSNode alias_type = ts_node_child_by_field_name(spec, "type", 4);
if (!ts_node_is_null(alias_name) && !ts_node_is_null(alias_type)) {
char* aname = cbm_node_text(arena, alias_name, source);
char* atarget = cbm_node_text(arena, alias_type, source);
if (aname && aname[0] && atarget && atarget[0]) {
const char* alias_type_qn = cbm_arena_sprintf(arena, "%s.%s", module_qn, aname);
const char* alias_target_qn = cbm_arena_sprintf(arena, "%s.%s", module_qn, atarget);
bool found_a = false;
for (int ti = 0; ti < reg.type_count; ti++) {
if (reg.types[ti].qualified_name &&
strcmp(reg.types[ti].qualified_name, alias_type_qn) == 0) {
reg.types[ti].alias_of = alias_target_qn;
found_a = true;
break;
}
}
if (!found_a) {
CBMRegisteredType alias_rt;
memset(&alias_rt, 0, sizeof(alias_rt));
alias_rt.qualified_name = alias_type_qn;
alias_rt.short_name = aname;
alias_rt.alias_of = alias_target_qn;
cbm_registry_add_type(&reg, alias_rt);
}
}
}
continue;
}
if (strcmp(spec_kind, "type_spec") != 0) continue;
TSNode name_node = ts_node_child_by_field_name(spec, "name", 4);
TSNode type_node = ts_node_child_by_field_name(spec, "type", 4);
if (ts_node_is_null(name_node) || ts_node_is_null(type_node)) continue;
char* type_name = cbm_node_text(arena,name_node, source);
if (!type_name || !type_name[0]) continue;
const char* type_qn = cbm_arena_sprintf(arena, "%s.%s", module_qn, type_name);
// Interface type: extract method names for satisfaction checking
if (strcmp(ts_node_type(type_node), "interface_type") == 0) {
const char* iface_methods[64];
int iface_method_count = 0;
uint32_t inl_nc = ts_node_named_child_count(type_node);
for (uint32_t k = 0; k < inl_nc && iface_method_count < 63; k++) {
TSNode child = ts_node_named_child(type_node, k);
if (ts_node_is_null(child)) continue;
const char* ck = ts_node_type(child);
// method_spec: MethodName(params) returns
if (strcmp(ck, "method_spec") == 0 || strcmp(ck, "method_elem") == 0) {
TSNode mname = ts_node_child_by_field_name(child, "name", 4);
if (!ts_node_is_null(mname)) {
char* mn = cbm_node_text(arena, mname, source);
if (mn && mn[0]) {
iface_methods[iface_method_count++] = mn;
}
}
}
}
if (iface_method_count > 0) {
for (int ti = 0; ti < reg.type_count; ti++) {
if (!reg.types[ti].qualified_name ||
strcmp(reg.types[ti].qualified_name, type_qn) != 0) continue;
const char** names = (const char**)cbm_arena_alloc(arena,
(iface_method_count + 1) * sizeof(const char*));
for (int mi = 0; mi < iface_method_count; mi++) {
names[mi] = iface_methods[mi];
}
names[iface_method_count] = NULL;
reg.types[ti].method_names = names;
break;
}
}
continue;
}
if (strcmp(ts_node_type(type_node), "struct_type") != 0) continue;
// Find field_declaration_list inside struct_type
TSNode field_list = ts_node_child_by_field_name(type_node, "body", 4);
if (ts_node_is_null(field_list)) {
// Some grammars use first named child
if (ts_node_named_child_count(type_node) > 0)
field_list = ts_node_named_child(type_node, 0);
}
if (ts_node_is_null(field_list)) continue;
// Scan field_declarations for embeds and named fields
const char* embeds[16];
int embed_count = 0;
const char* fld_names[64];
const CBMType* fld_types[64];
int fld_count = 0;
// Create a temporary LSP context for parsing field types
GoLSPContext tmp_ctx;
memset(&tmp_ctx, 0, sizeof(tmp_ctx));
tmp_ctx.arena = arena;
tmp_ctx.source = source;
tmp_ctx.source_len = (int)strlen(source);
tmp_ctx.registry = &reg;
tmp_ctx.package_qn = module_qn;
uint32_t fl_nc = ts_node_child_count(field_list);
for (uint32_t k = 0; k < fl_nc; k++) {
TSNode field = ts_node_child(field_list, k);
if (ts_node_is_null(field) || !ts_node_is_named(field)) continue;
if (strcmp(ts_node_type(field), "field_declaration") != 0) continue;
TSNode fname = ts_node_child_by_field_name(field, "name", 4);
TSNode ftype = ts_node_child_by_field_name(field, "type", 4);
if (ts_node_is_null(fname) && !ts_node_is_null(ftype)) {
// Embedded field: has type but no name
if (embed_count < 15) {
char* embed_text = cbm_node_text(arena, ftype, source);
if (embed_text && embed_text[0]) {
const char* et = embed_text;
while (*et == '*') et++;
embeds[embed_count++] = cbm_arena_sprintf(arena, "%s.%s", module_qn, et);
}
}
} else if (!ts_node_is_null(fname) && !ts_node_is_null(ftype) && fld_count < 63) {
// Named field: name + type
char* fn = cbm_node_text(arena, fname, source);
if (fn && fn[0]) {
fld_names[fld_count] = fn;
fld_types[fld_count] = go_parse_type_node(&tmp_ctx, ftype);
fld_count++;
}
}
}
// Find the registered type and update it
for (int ti = 0; ti < reg.type_count; ti++) {
if (!reg.types[ti].qualified_name ||
strcmp(reg.types[ti].qualified_name, type_qn) != 0) continue;
if (embed_count > 0) {
const char** arr = (const char**)cbm_arena_alloc(arena,
(embed_count + 1) * sizeof(const char*));
for (int ei = 0; ei < embed_count; ei++) arr[ei] = embeds[ei];
arr[embed_count] = NULL;
reg.types[ti].embedded_types = arr;
}
if (fld_count > 0) {
const char** names = (const char**)cbm_arena_alloc(arena,
(fld_count + 1) * sizeof(const char*));
const CBMType** types = (const CBMType**)cbm_arena_alloc(arena,
(fld_count + 1) * sizeof(const CBMType*));
for (int fi = 0; fi < fld_count; fi++) {
names[fi] = fld_names[fi];
types[fi] = fld_types[fi];
}
names[fld_count] = NULL;
types[fld_count] = NULL;
reg.types[ti].field_names = names;
reg.types[ti].field_types = types;
}
break;
}
}
}
}
// Phase 1c: Extract type parameters from generic function declarations
extract_type_params_from_ast(arena, &reg, root, source, module_qn);
// Phase 2: Build LSP context and run
GoLSPContext lsp_ctx;
go_lsp_init(&lsp_ctx, arena, source, source_len, &reg, module_qn, &result->resolved_calls);
// Add imports
for (int i = 0; i < result->imports.count; i++) {
CBMImport* imp = &result->imports.items[i];
if (imp->local_name && imp->module_path) {
go_lsp_add_import(&lsp_ctx, imp->local_name, imp->module_path);
}
}
// Process the file
go_lsp_process_file(&lsp_ctx, root);
}
// --- Cross-file LSP: parse source, build registry from defs, run LSP ---
// Helper: split "|"-separated string into array of CBMType*.
static const CBMType** split_pipe_types(CBMArena* a, const char* text, const char* module_qn) {
if (!text || !text[0]) return NULL;
// Count separators
int count = 1;
for (const char* p = text; *p; p++) {
if (*p == '|') count++;
}
const CBMType** arr = (const CBMType**)cbm_arena_alloc(a, (count + 1) * sizeof(const CBMType*));
if (!arr) return NULL;
// Split and parse each type
char* buf = cbm_arena_strdup(a, text);
int idx = 0;
char* start = buf;
for (char* p = buf; ; p++) {
if (*p == '|' || *p == '\0') {
char save = *p;
*p = '\0';
if (start[0]) {
arr[idx++] = cbm_parse_return_type_text(a, start, module_qn);
}
if (save == '\0') break;
start = p + 1;
}
}
arr[idx] = NULL;
return idx > 0 ? arr : NULL;
}
// Helper: split "|"-separated string into array of const char*.
static const char** split_pipe_strings(CBMArena* a, const char* text) {
if (!text || !text[0]) return NULL;
int count = 1;
for (const char* p = text; *p; p++) {
if (*p == '|') count++;
}
const char** arr = (const char**)cbm_arena_alloc(a, (count + 1) * sizeof(const char*));
if (!arr) return NULL;
char* buf = cbm_arena_strdup(a, text);
int idx = 0;
char* start = buf;
for (char* p = buf; ; p++) {
if (*p == '|' || *p == '\0') {
char save = *p;
*p = '\0';
if (start[0]) {
arr[idx++] = cbm_arena_strdup(a, start);
}
if (save == '\0') break;
start = p + 1;
}
}
arr[idx] = NULL;
return idx > 0 ? arr : NULL;
}
// Helper: parse "|"-separated "name:type" field definitions and populate a registered type.
// Format: "Binder:Binder|Name:string|Count:int"
// type_text is resolved relative to def_module_qn.
static void parse_field_defs_into_type(CBMArena* arena, CBMTypeRegistry* reg,
const char* type_qn, const char* field_defs, const char* def_module_qn) {
if (!field_defs || !field_defs[0] || !type_qn) return;
// Count fields
int count = 1;
for (const char* p = field_defs; *p; p++) {
if (*p == '|') count++;
}
if (count > 63) count = 63;
const char** names = (const char**)cbm_arena_alloc(arena, (count + 1) * sizeof(const char*));
const CBMType** types = (const CBMType**)cbm_arena_alloc(arena, (count + 1) * sizeof(const CBMType*));
if (!names || !types) return;
char* buf = cbm_arena_strdup(arena, field_defs);
int idx = 0;
char* start = buf;
for (char* p = buf; ; p++) {
if (*p == '|' || *p == '\0') {
char save = *p;
*p = '\0';
// Parse "name:type"
char* colon = NULL;
for (char* q = start; *q; q++) {
if (*q == ':') { colon = q; break; }
}
if (colon && idx < count) {
*colon = '\0';
names[idx] = cbm_arena_strdup(arena, start);
types[idx] = cbm_parse_return_type_text(arena, colon + 1, def_module_qn);
idx++;
}
if (save == '\0') break;
start = p + 1;
}
}
names[idx] = NULL;
types[idx] = NULL;
if (idx > 0) {
// Find the registered type and update field info
for (int ti = 0; ti < reg->type_count; ti++) {
if (reg->types[ti].qualified_name &&
strcmp(reg->types[ti].qualified_name, type_qn) == 0) {
reg->types[ti].field_names = names;
reg->types[ti].field_types = types;
break;
}
}
}
}
// --- Phase 1c: Extract type parameters from generic function declarations ---
// Helper: parse a tree-sitter type AST node with type param awareness.
// Like go_parse_type_node but checks type_identifier against type_param_names first,
// and works without a full GoLSPContext (uses raw arena/source/module_qn).
static const CBMType* parse_type_node_with_params(CBMArena* arena, TSNode node,
const char* source, const char* module_qn, const char** type_param_names) {
if (ts_node_is_null(node)) return cbm_type_unknown();
const char* kind = ts_node_type(node);
// type_identifier: check type params first, then resolve as named type
if (strcmp(kind, "type_identifier") == 0) {
char* name = cbm_node_text(arena, node, source);
if (!name) return cbm_type_unknown();
// Check type param names
if (type_param_names) {
for (int i = 0; type_param_names[i]; i++) {
if (strcmp(name, type_param_names[i]) == 0) {
return cbm_type_type_param(arena, name);
}
}
}
// Builtin types
if (strcmp(name, "int") == 0 || strcmp(name, "string") == 0 ||
strcmp(name, "bool") == 0 || strcmp(name, "float64") == 0 ||
strcmp(name, "float32") == 0 || strcmp(name, "byte") == 0 ||
strcmp(name, "rune") == 0 || strcmp(name, "error") == 0 ||
strcmp(name, "any") == 0 || strcmp(name, "int8") == 0 ||
strcmp(name, "int16") == 0 || strcmp(name, "int32") == 0 ||
strcmp(name, "int64") == 0 || strcmp(name, "uint") == 0 ||
strcmp(name, "uint8") == 0 || strcmp(name, "uint16") == 0 ||
strcmp(name, "uint32") == 0 || strcmp(name, "uint64") == 0 ||
strcmp(name, "uintptr") == 0) {
return cbm_type_builtin(arena, name);
}
return cbm_type_named(arena, cbm_arena_sprintf(arena, "%s.%s", module_qn, name));
}
// type_elem: wrapper in type_arguments
if (strcmp(kind, "type_elem") == 0 && ts_node_named_child_count(node) > 0) {
return parse_type_node_with_params(arena, ts_node_named_child(node, 0),
source, module_qn, type_param_names);
}
// qualified_type: pkg.Type
if (strcmp(kind, "qualified_type") == 0) {
TSNode pkg_node = ts_node_child_by_field_name(node, "package", 7);
TSNode name_node = ts_node_child_by_field_name(node, "name", 4);
if (!ts_node_is_null(pkg_node) && !ts_node_is_null(name_node)) {
char* pkg = cbm_node_text(arena, pkg_node, source);
char* name = cbm_node_text(arena, name_node, source);
if (pkg && name) {
return cbm_type_named(arena, cbm_arena_sprintf(arena, "%s.%s", pkg, name));
}
}
return cbm_type_unknown();
}
// pointer_type: *T
if (strcmp(kind, "pointer_type") == 0) {
uint32_t nc = ts_node_named_child_count(node);
if (nc > 0)
return cbm_type_pointer(arena, parse_type_node_with_params(arena,
ts_node_named_child(node, nc - 1), source, module_qn, type_param_names));
return cbm_type_unknown();
}
// slice_type: []T
if (strcmp(kind, "slice_type") == 0) {
TSNode elem = ts_node_child_by_field_name(node, "element", 7);
if (ts_node_is_null(elem) && ts_node_named_child_count(node) > 0)
elem = ts_node_named_child(node, ts_node_named_child_count(node) - 1);
return cbm_type_slice(arena, parse_type_node_with_params(arena,
elem, source, module_qn, type_param_names));
}
// array_type: [N]T
if (strcmp(kind, "array_type") == 0) {
TSNode elem = ts_node_child_by_field_name(node, "element", 7);
if (ts_node_is_null(elem) && ts_node_named_child_count(node) > 0)
elem = ts_node_named_child(node, ts_node_named_child_count(node) - 1);
return cbm_type_slice(arena, parse_type_node_with_params(arena,
elem, source, module_qn, type_param_names));
}
// map_type: map[K]V
if (strcmp(kind, "map_type") == 0) {
TSNode key = ts_node_child_by_field_name(node, "key", 3);
TSNode value = ts_node_child_by_field_name(node, "value", 5);
return cbm_type_map(arena,
parse_type_node_with_params(arena, key, source, module_qn, type_param_names),
parse_type_node_with_params(arena, value, source, module_qn, type_param_names));
}
// channel_type: chan T
if (strcmp(kind, "channel_type") == 0) {
TSNode value = ts_node_child_by_field_name(node, "value", 5);
if (ts_node_is_null(value) && ts_node_named_child_count(node) > 0)
value = ts_node_named_child(node, ts_node_named_child_count(node) - 1);
char* text = cbm_node_text(arena, node, source);
int dir = 0;
if (text) {
if (strncmp(text, "chan<-", 6) == 0 || strncmp(text, "chan <-", 7) == 0) dir = 1;
else if (strncmp(text, "<-chan", 6) == 0 || strncmp(text, "<- chan", 7) == 0) dir = 2;
}
return cbm_type_channel(arena, parse_type_node_with_params(arena,
value, source, module_qn, type_param_names), dir);
}
// function_type: func(T) R — full parsing with type param awareness
if (strcmp(kind, "function_type") == 0) {
TSNode params_node = ts_node_child_by_field_name(node, "parameters", 10);
TSNode result_node = ts_node_child_by_field_name(node, "result", 6);
const CBMType* param_types_arr[16];
int pc = 0;
if (!ts_node_is_null(params_node)) {
uint32_t pnc = ts_node_child_count(params_node);
for (uint32_t i = 0; i < pnc && pc < 15; i++) {
TSNode child = ts_node_child(params_node, i);
if (ts_node_is_null(child) || !ts_node_is_named(child)) continue;
if (strcmp(ts_node_type(child), "parameter_declaration") == 0) {
TSNode pt = ts_node_child_by_field_name(child, "type", 4);
if (!ts_node_is_null(pt))
param_types_arr[pc++] = parse_type_node_with_params(arena,
pt, source, module_qn, type_param_names);
}
}
}
param_types_arr[pc] = NULL;
const CBMType* ret_types_arr[16];
int rc = 0;
if (!ts_node_is_null(result_node)) {
if (strcmp(ts_node_type(result_node), "parameter_list") == 0) {
uint32_t rnc = ts_node_child_count(result_node);
for (uint32_t i = 0; i < rnc && rc < 15; i++) {
TSNode child = ts_node_child(result_node, i);
if (ts_node_is_null(child) || !ts_node_is_named(child)) continue;
TSNode rt = ts_node_child_by_field_name(child, "type", 4);
if (ts_node_is_null(rt)) rt = child;
ret_types_arr[rc++] = parse_type_node_with_params(arena,
rt, source, module_qn, type_param_names);
}
} else {
ret_types_arr[rc++] = parse_type_node_with_params(arena,
result_node, source, module_qn, type_param_names);
}
}
ret_types_arr[rc] = NULL;
const CBMType** pt = pc > 0 ? (const CBMType**)param_types_arr : NULL;
const CBMType** rt = rc > 0 ? (const CBMType**)ret_types_arr : NULL;
return cbm_type_func(arena, NULL, pt, rt);
}
// interface_type
if (strcmp(kind, "interface_type") == 0) {
CBMType* t = (CBMType*)cbm_arena_alloc(arena, sizeof(CBMType));
memset(t, 0, sizeof(CBMType));
t->kind = CBM_TYPE_INTERFACE;
return t;
}
// parenthesized_type: (T)
if (strcmp(kind, "parenthesized_type") == 0 && ts_node_named_child_count(node) > 0) {
return parse_type_node_with_params(arena, ts_node_named_child(node, 0),
source, module_qn, type_param_names);
}
// generic_type: Type[T1, T2]
if (strcmp(kind, "generic_type") == 0) {
TSNode type_node = ts_node_child_by_field_name(node, "type", 4);
if (!ts_node_is_null(type_node))
return parse_type_node_with_params(arena, type_node, source, module_qn, type_param_names);
}
return cbm_type_unknown();
}
// Scan AST for generic function declarations and set type_param_names + re-parse
// return types on matching registered functions.
static void extract_type_params_from_ast(CBMArena* arena, CBMTypeRegistry* reg,
TSNode root, const char* source, const char* module_qn) {
// Collect top-level children once (O(n)); ts_node_child(root,i) is O(i) → O(n²).
uint32_t rkn = 0;
TSNode* rkids = cbm_lsp_collect_children(arena, root, &rkn);
for (uint32_t i = 0; i < rkn; i++) {
TSNode top = rkids[i];
if (strcmp(ts_node_type(top), "function_declaration") != 0) continue;
// Check for type_parameter_list (field name: "type_parameters", 15 chars)
TSNode tp_list = ts_node_child_by_field_name(top, "type_parameters", 15);
if (ts_node_is_null(tp_list)) continue;
// Get function name
TSNode name_node = ts_node_child_by_field_name(top, "name", 4);
if (ts_node_is_null(name_node)) continue;
char* func_name = cbm_node_text(arena, name_node, source);
if (!func_name || !func_name[0]) continue;
// Extract type param names from type_parameter_declaration children
const char* params[16];
int param_count = 0;
uint32_t tp_nc = ts_node_child_count(tp_list);
for (uint32_t j = 0; j < tp_nc && param_count < 15; j++) {
TSNode child = ts_node_child(tp_list, j);
if (ts_node_is_null(child) || !ts_node_is_named(child)) continue;
if (strcmp(ts_node_type(child), "type_parameter_declaration") != 0) continue;
TSNode pname = ts_node_child_by_field_name(child, "name", 4);
if (ts_node_is_null(pname)) {
// Fallback: first type_identifier child
uint32_t cc = ts_node_child_count(child);
for (uint32_t k = 0; k < cc; k++) {
TSNode c = ts_node_child(child, k);
if (!ts_node_is_null(c) && ts_node_is_named(c) &&
strcmp(ts_node_type(c), "type_identifier") == 0) {
pname = c;
break;
}
}
}
if (!ts_node_is_null(pname)) {
char* pn = cbm_node_text(arena, pname, source);
if (pn && pn[0]) {
params[param_count++] = cbm_arena_strdup(arena, pn);
}
}
}
if (param_count == 0) continue;
params[param_count] = NULL;
// Build arena-allocated type_param_names array
const char** tp_names = (const char**)cbm_arena_alloc(arena, (param_count + 1) * sizeof(const char*));
for (int j = 0; j <= param_count; j++) tp_names[j] = params[j];
// Find the matching registered function and set type_param_names
const char* func_qn = cbm_arena_sprintf(arena, "%s.%s", module_qn, func_name);
for (int fi = 0; fi < reg->func_count; fi++) {
if (reg->funcs[fi].qualified_name &&
strcmp(reg->funcs[fi].qualified_name, func_qn) == 0) {
reg->funcs[fi].type_param_names = tp_names;
// Re-parse return types with type param awareness
if (reg->funcs[fi].signature && reg->funcs[fi].signature->kind == CBM_TYPE_FUNC &&
reg->funcs[fi].signature->data.func.return_types) {
const CBMType** old_rets = reg->funcs[fi].signature->data.func.return_types;
int ret_count = 0;
while (old_rets[ret_count]) ret_count++;
// Check if any return type is a NAMED that matches a type param
bool needs_reparse = false;
for (int ri = 0; ri < ret_count && !needs_reparse; ri++) {
const CBMType* check = old_rets[ri];
// Unwrap slice/pointer to get inner named type
if (check->kind == CBM_TYPE_SLICE && check->data.slice.elem)
check = check->data.slice.elem;
else if (check->kind == CBM_TYPE_POINTER && check->data.pointer.elem)
check = check->data.pointer.elem;
if (check->kind == CBM_TYPE_NAMED) {
const char* qn = check->data.named.qualified_name;
const char* dot = strrchr(qn, '.');
const char* short_name = dot ? dot + 1 : qn;
for (int pi = 0; pi < param_count; pi++) {
if (strcmp(short_name, tp_names[pi]) == 0) {
needs_reparse = true;
break;
}
}
}
}
if (needs_reparse) {
TSNode result_node = ts_node_child_by_field_name(top, "result", 6);
if (!ts_node_is_null(result_node)) {
const CBMType** new_rets = (const CBMType**)cbm_arena_alloc(arena,
(ret_count + 1) * sizeof(const CBMType*));
if (strcmp(ts_node_type(result_node), "parameter_list") == 0) {
int idx = 0;
uint32_t rnc = ts_node_child_count(result_node);
for (uint32_t ri = 0; ri < rnc && idx < ret_count; ri++) {
TSNode rchild = ts_node_child(result_node, ri);
if (ts_node_is_null(rchild) || !ts_node_is_named(rchild)) continue;
TSNode rtype = ts_node_child_by_field_name(rchild, "type", 4);
if (ts_node_is_null(rtype)) rtype = rchild;
new_rets[idx++] = parse_type_node_with_params(arena,
rtype, source, module_qn, tp_names);
}
new_rets[idx] = NULL;
} else {
new_rets[0] = parse_type_node_with_params(arena,
result_node, source, module_qn, tp_names);
new_rets[1] = NULL;
}
CBMType* new_sig = (CBMType*)cbm_arena_alloc(arena, sizeof(CBMType));
*new_sig = *(CBMType*)reg->funcs[fi].signature;
new_sig->data.func.return_types = new_rets;
reg->funcs[fi].signature = new_sig;
}
}
}
// Also re-parse param types with TYPE_PARAM awareness (for implicit generics)
if (reg->funcs[fi].signature && reg->funcs[fi].signature->kind == CBM_TYPE_FUNC &&
reg->funcs[fi].signature->data.func.param_types) {
const CBMType** old_params = reg->funcs[fi].signature->data.func.param_types;
int pc = 0;
while (old_params[pc]) pc++;
// Re-parse from AST parameter list
TSNode params_node = ts_node_child_by_field_name(top, "parameters", 10);
if (!ts_node_is_null(params_node)) {
const CBMType** new_params = (const CBMType**)cbm_arena_alloc(arena,
(pc + 1) * sizeof(const CBMType*));
int idx = 0;
uint32_t pnc = ts_node_child_count(params_node);
for (uint32_t pi = 0; pi < pnc && idx < pc; pi++) {
TSNode pchild = ts_node_child(params_node, pi);
if (ts_node_is_null(pchild) || !ts_node_is_named(pchild)) continue;
if (strcmp(ts_node_type(pchild), "parameter_declaration") != 0) continue;
TSNode ptype = ts_node_child_by_field_name(pchild, "type", 4);
if (ts_node_is_null(ptype)) continue;
new_params[idx++] = parse_type_node_with_params(arena,
ptype, source, module_qn, tp_names);
}
new_params[idx] = NULL;
if (idx > 0) {
CBMType* sig = (CBMType*)reg->funcs[fi].signature;
if (sig != reg->funcs[fi].signature) {
// Already rebuilt — update in place
((CBMType*)reg->funcs[fi].signature)->data.func.param_types = new_params;
} else {
CBMType* new_sig = (CBMType*)cbm_arena_alloc(arena, sizeof(CBMType));
*new_sig = *sig;
new_sig->data.func.param_types = new_params;
reg->funcs[fi].signature = new_sig;
}
}
}
}
break;
}
}
}
}
// Forward: tree-sitter Go language (from lang_specs.c)
extern const TSLanguage* tree_sitter_go(void);
void cbm_run_go_lsp_cross(
CBMArena* arena,
const char* source, int source_len,
const char* module_qn,
CBMLSPDef* defs, int def_count,
const char** import_names, const char** import_qns, int import_count,
TSTree* cached_tree,
CBMResolvedCallArray* out)
{
if (!source || source_len <= 0) return;
// 1. Use cached tree if available, otherwise parse fresh
TSParser* parser = NULL;
TSTree* tree = cached_tree;
bool owns_tree = false;
if (!tree) {
parser = ts_parser_new();
if (!parser) return;
ts_parser_set_language(parser, tree_sitter_go());
tree = ts_parser_parse_string(parser, NULL, source, source_len);
owns_tree = true;
if (!tree) { ts_parser_delete(parser); return; }
}
TSNode root = ts_tree_root_node(tree);
// 2. Build registry
CBMTypeRegistry reg;
cbm_registry_init(&reg, arena);
cbm_go_stdlib_register(&reg, arena);
// Register all defs (file-local + cross-file).
// Perf: borrow strings from defs[] directly — they live in the
// caller's arena which outlives this call, so cbm_arena_strdup is
// wasted work. On kubernetes (~110k defs × 11k files × 5 strdups
// ~= 6B mallocs) this alone saves ~90s of resolve wall time.
for (int i = 0; i < def_count; i++) {
CBMLSPDef* d = &defs[i];
if (!d->qualified_name || !d->short_name || !d->label) continue;
const char* def_mod = d->def_module_qn ? d->def_module_qn : module_qn;
// Every type-like container (Type/Class/Struct/Interface/Enum/Trait).
// Struct included so Go structs (now labelled "Struct") register as types.
if (cbm_label_is_type_like(d->label)) {
CBMRegisteredType rt;
memset(&rt, 0, sizeof(rt));
rt.qualified_name = d->qualified_name; // borrowed
rt.short_name = d->short_name; // borrowed
rt.is_interface = d->is_interface || strcmp(d->label, "Interface") == 0;
rt.embedded_types = split_pipe_strings(arena, d->embedded_types);
// Set method_names for interfaces from "|"-separated string
if (rt.is_interface && d->method_names_str && d->method_names_str[0]) {
rt.method_names = split_pipe_strings(arena, d->method_names_str);
}
cbm_registry_add_type(&reg, rt);
// Populate struct field types from field_defs
if (d->field_defs && d->field_defs[0]) {
parse_field_defs_into_type(arena, &reg, rt.qualified_name, d->field_defs, def_mod);
}
}
// Function/Method
if (strcmp(d->label, "Function") == 0 || strcmp(d->label, "Method") == 0) {
CBMRegisteredFunc rf;
memset(&rf, 0, sizeof(rf));
rf.qualified_name = d->qualified_name; // borrowed
rf.short_name = d->short_name; // borrowed
// Build FUNC type from return_types text
const CBMType** ret_types = split_pipe_types(arena, d->return_types, def_mod);
rf.signature = cbm_type_func(arena, NULL, NULL, ret_types);
// Method receiver
if (strcmp(d->label, "Method") == 0 && d->receiver_type && d->receiver_type[0]) {
rf.receiver_type = d->receiver_type; // borrowed
// Auto-create type entry if not exists
if (!cbm_registry_lookup_type(&reg, rf.receiver_type)) {
CBMRegisteredType auto_type;
memset(&auto_type, 0, sizeof(auto_type));
auto_type.qualified_name = rf.receiver_type;
const char* dot = strrchr(d->receiver_type, '.');
auto_type.short_name = dot ? dot + 1 : rf.receiver_type; // borrowed substring
cbm_registry_add_type(&reg, auto_type);
}
}
cbm_registry_add_func(&reg, rf);
}
}
// 3. Phase 1b: Scan AST for struct definitions to populate embedded_types
{
// Collect top-level children once (O(n)); ts_node_child(root,i) is O(i) → O(n²).
uint32_t rkn = 0;
TSNode* rkids = cbm_lsp_collect_children(arena, root, &rkn);
for (uint32_t i = 0; i < rkn; i++) {
TSNode top = rkids[i];
if (strcmp(ts_node_type(top), "type_declaration") != 0) continue;
uint32_t td_nc = ts_node_child_count(top);
for (uint32_t j = 0; j < td_nc; j++) {
TSNode spec = ts_node_child(top, j);
if (ts_node_is_null(spec) || !ts_node_is_named(spec)) continue;
const char* spec_kind2 = ts_node_type(spec);
// type_alias: type Foo = Bar
if (strcmp(spec_kind2, "type_alias") == 0) {
TSNode alias_name = ts_node_child_by_field_name(spec, "name", 4);
TSNode alias_type = ts_node_child_by_field_name(spec, "type", 4);
if (!ts_node_is_null(alias_name) && !ts_node_is_null(alias_type)) {
char* aname = cbm_node_text(arena, alias_name, source);
char* atarget = cbm_node_text(arena, alias_type, source);
if (aname && aname[0] && atarget && atarget[0]) {
const char* alias_type_qn = cbm_arena_sprintf(arena, "%s.%s", module_qn, aname);
const char* alias_target_qn = cbm_arena_sprintf(arena, "%s.%s", module_qn, atarget);
bool found_a = false;
for (int ti = 0; ti < reg.type_count; ti++) {
if (reg.types[ti].qualified_name &&
strcmp(reg.types[ti].qualified_name, alias_type_qn) == 0) {
reg.types[ti].alias_of = alias_target_qn;
found_a = true;
break;
}
}
if (!found_a) {
CBMRegisteredType alias_rt;
memset(&alias_rt, 0, sizeof(alias_rt));
alias_rt.qualified_name = alias_type_qn;
alias_rt.short_name = aname;
alias_rt.alias_of = alias_target_qn;
cbm_registry_add_type(&reg, alias_rt);
}
}
}
continue;
}
if (strcmp(spec_kind2, "type_spec") != 0) continue;
TSNode name_node = ts_node_child_by_field_name(spec, "name", 4);
TSNode type_node = ts_node_child_by_field_name(spec, "type", 4);
if (ts_node_is_null(name_node) || ts_node_is_null(type_node)) continue;
if (strcmp(ts_node_type(type_node), "struct_type") != 0) continue;
char* type_name = cbm_node_text(arena, name_node, source);
if (!type_name || !type_name[0]) continue;
const char* type_qn = cbm_arena_sprintf(arena, "%s.%s", module_qn, type_name);
TSNode field_list = ts_node_child_by_field_name(type_node, "body", 4);
if (ts_node_is_null(field_list)) {
if (ts_node_named_child_count(type_node) > 0)
field_list = ts_node_named_child(type_node, 0);
}
if (ts_node_is_null(field_list)) continue;
const char* embeds[16];
int embed_count = 0;
const char* fld_names[64];
const CBMType* fld_types[64];
int fld_count = 0;
GoLSPContext tmp_ctx;
memset(&tmp_ctx, 0, sizeof(tmp_ctx));
tmp_ctx.arena = arena;
tmp_ctx.source = source;
tmp_ctx.source_len = source_len;
tmp_ctx.registry = &reg;
tmp_ctx.package_qn = module_qn;
tmp_ctx.import_local_names = import_names;
tmp_ctx.import_package_qns = import_qns;
tmp_ctx.import_count = import_count;
uint32_t fl_nc = ts_node_child_count(field_list);
for (uint32_t k = 0; k < fl_nc; k++) {
TSNode field = ts_node_child(field_list, k);
if (ts_node_is_null(field) || !ts_node_is_named(field)) continue;
if (strcmp(ts_node_type(field), "field_declaration") != 0) continue;
TSNode fname = ts_node_child_by_field_name(field, "name", 4);
TSNode ftype = ts_node_child_by_field_name(field, "type", 4);
if (ts_node_is_null(fname) && !ts_node_is_null(ftype)) {
if (embed_count < 15) {
char* embed_text = cbm_node_text(arena, ftype, source);
if (embed_text && embed_text[0]) {
const char* et = embed_text;
while (*et == '*') et++;
embeds[embed_count++] = cbm_arena_sprintf(arena, "%s.%s", module_qn, et);
}
}
} else if (!ts_node_is_null(fname) && !ts_node_is_null(ftype) && fld_count < 63) {
char* fn = cbm_node_text(arena, fname, source);
if (fn && fn[0]) {
fld_names[fld_count] = fn;
fld_types[fld_count] = go_parse_type_node(&tmp_ctx, ftype);
fld_count++;
}
}
}
for (int ti = 0; ti < reg.type_count; ti++) {
if (!reg.types[ti].qualified_name ||
strcmp(reg.types[ti].qualified_name, type_qn) != 0) continue;
if (embed_count > 0) {
const char** arr = (const char**)cbm_arena_alloc(arena,
(embed_count + 1) * sizeof(const char*));
for (int ei = 0; ei < embed_count; ei++) arr[ei] = embeds[ei];
arr[embed_count] = NULL;
reg.types[ti].embedded_types = arr;
}
if (fld_count > 0) {
const char** names = (const char**)cbm_arena_alloc(arena,
(fld_count + 1) * sizeof(const char*));
const CBMType** types = (const CBMType**)cbm_arena_alloc(arena,
(fld_count + 1) * sizeof(const CBMType*));
for (int fi = 0; fi < fld_count; fi++) {
names[fi] = fld_names[fi];
types[fi] = fld_types[fi];
}
names[fld_count] = NULL;
types[fld_count] = NULL;
reg.types[ti].field_names = names;
reg.types[ti].field_types = types;
}
break;
}
}
}
}
// 3b. Phase 1c: Extract type params from generic function declarations
extract_type_params_from_ast(arena, &reg, root, source, module_qn);
// 3c. Finalize registry — builds hash buckets for O(1) lookups in
// cbm_registry_lookup_method / lookup_type. Without this, every
// lookup falls through to the O(N) linear scan in type_registry.c,
// turning the resolution pass below into an O(N·C·F) cost per file
// (kubernetes: ~64B strcmps observed = 35min). Must come AFTER all
// mutations (Phase 1b/1c above) and BEFORE the LSP context init.
cbm_registry_finalize(&reg);
// 4. Build LSP context and run
GoLSPContext ctx;
go_lsp_init(&ctx, arena, source, source_len, &reg, module_qn, out);
for (int i = 0; i < import_count; i++) {
if (import_names[i] && import_qns[i]) {
go_lsp_add_import(&ctx, import_names[i], import_qns[i]);
}
}
go_lsp_process_file(&ctx, root);
// 5. Cleanup — only free if we allocated
if (owns_tree) {
ts_tree_delete(tree);
if (parser) ts_parser_delete(parser);
}
}
/* ── Tier 2: pre-built per-language registry (gopls package summary pattern) ──
*
* cbm_go_build_cross_registry runs the registry build ONCE per project (in
* pipeline.c, before the parallel resolve workers fire). The returned
* registry is finalized (O(1) lookups) and shared READ-ONLY across all
* workers in the resolve phase.
*
* cbm_run_go_lsp_cross_with_registry is the per-file entrypoint used by
* the resolve worker. It SKIPS the registry build (uses the pre-built reg)
* AND skips Phase 1b/1c AST scans (those mutate the registry, which would
* race with other workers reading the shared reg). Accuracy trade-off:
* file-local type aliases and struct embeddings discovered ONLY by Phase
* 1b (and not already in defs[] embedded_types/field_defs strings) are
* missed. In practice the per-file LSP during extract already captures
* those via the def strings. */
CBMTypeRegistry* cbm_go_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_go_stdlib_register(reg, arena);
for (int i = 0; i < def_count; i++) {
CBMLSPDef* d = &defs[i];
if (!d->qualified_name || !d->short_name || !d->label) continue;
/* Filter to Go defs only — the all_defs[] array is mixed-language. */
if (d->lang != CBM_LANG_GO) continue;
/* In the pre-built path every def carries its own def_module_qn
* (set by cbm_pxc_collect_all_defs). There is no caller module to
* fall back to — this registry is project-wide, not per-file. */
const char* def_mod = d->def_module_qn ? d->def_module_qn : "";
// Every type-like container (Type/Class/Struct/Interface/Enum/Trait).
// Struct included so Go structs (now labelled "Struct") register as types.
if (cbm_label_is_type_like(d->label)) {
CBMRegisteredType rt;
memset(&rt, 0, sizeof(rt));
rt.qualified_name = d->qualified_name; /* borrowed */
rt.short_name = d->short_name;
rt.is_interface = d->is_interface || strcmp(d->label, "Interface") == 0;
rt.embedded_types = split_pipe_strings(arena, d->embedded_types);
if (rt.is_interface && d->method_names_str && d->method_names_str[0]) {
rt.method_names = split_pipe_strings(arena, d->method_names_str);
}
cbm_registry_add_type(reg, rt);
if (d->field_defs && d->field_defs[0]) {
parse_field_defs_into_type(arena, reg, rt.qualified_name,
d->field_defs, def_mod);
}
}
if (strcmp(d->label, "Function") == 0 || strcmp(d->label, "Method") == 0) {
CBMRegisteredFunc rf;
memset(&rf, 0, sizeof(rf));
rf.qualified_name = d->qualified_name; /* borrowed */
rf.short_name = d->short_name;
const CBMType** ret_types = split_pipe_types(arena, d->return_types, def_mod);
rf.signature = cbm_type_func(arena, NULL, NULL, ret_types);
if (strcmp(d->label, "Method") == 0 && d->receiver_type && d->receiver_type[0]) {
rf.receiver_type = d->receiver_type;
if (!cbm_registry_lookup_type(reg, rf.receiver_type)) {
CBMRegisteredType auto_type;
memset(&auto_type, 0, sizeof(auto_type));
auto_type.qualified_name = rf.receiver_type;
const char* dot = strrchr(d->receiver_type, '.');
auto_type.short_name = dot ? dot + 1 : rf.receiver_type;
cbm_registry_add_type(reg, auto_type);
}
}
cbm_registry_add_func(reg, rf);
}
}
cbm_registry_finalize(reg);
reg->read_only = true; /* seal: shared Tier-2 registry is read-only during resolve */
return reg;
}
void cbm_run_go_lsp_cross_with_registry(
CBMArena* arena,
const char* source, int source_len,
const char* module_qn,
CBMTypeRegistry* reg,
const char** import_names, const char** import_qns, int import_count,
TSTree* cached_tree,
CBMResolvedCallArray* out) {
if (!source || source_len <= 0 || !reg) return;
TSParser* parser = NULL;
TSTree* tree = cached_tree;
bool owns_tree = false;
if (!tree) {
parser = ts_parser_new();
if (!parser) return;
ts_parser_set_language(parser, tree_sitter_go());
tree = ts_parser_parse_string(parser, NULL, source, source_len);
owns_tree = true;
if (!tree) { ts_parser_delete(parser); return; }
}
TSNode root = ts_tree_root_node(tree);
/* Phase 1b/1c (per-file AST mutations on registry) DELIBERATELY SKIPPED —
* shared registry must stay immutable across parallel workers. */
GoLSPContext ctx;
go_lsp_init(&ctx, arena, source, source_len, reg, module_qn, out);
for (int i = 0; i < import_count; i++) {
if (import_names[i] && import_qns[i]) {
go_lsp_add_import(&ctx, import_names[i], import_qns[i]);
}
}
go_lsp_process_file(&ctx, root);
if (owns_tree) {
ts_tree_delete(tree);
if (parser) ts_parser_delete(parser);
}
}
/* ── Tier 3: AST-walk-free metadata-driven cross-file resolver ────
*
* KEY INSIGHT: the per-file LSP during extract ALREADY emits one
* CBMResolvedCall per call site, with strategy="lsp_unresolved" for
* calls it couldn't resolve. For those unresolved entries:
*
* * "symbol_not_in_registry" (go_lsp.c:1142) → callee_qn is
* "pkg_name.field_name" (literal local import alias)
* * "method_not_found" (go_lsp.c:1242) → callee_qn is
* "<FullyQualifiedReceiverTypeQN>.<MethodName>" — receiver type
* was ALREADY inferred by per-file LSP, just the method wasn't
* in the per-file registry (cross-file)
* * "function_not_in_registry" (go_lsp.c:1266) → callee_qn is
* the bare function name (likely a same-package symbol we
* missed, or a cross-file package function used unqualified)
* * "unknown_receiver_type" (go_lsp.c:1248) → per-file LSP
* couldn't infer the receiver type. Cross-LSP can't either
* without re-walking — leave unresolved.
*
* So cross-LSP work reduces to:
* for each unresolved entry → look up callee_qn (or pkg.x via
* import map) in the global registry → emit resolved version
* on top. NO TREE-SITTER PARSE. NO AST WALK. Just hash lookups.
*
* This is what the user meant: walk the AST ONCE (during extract),
* capture everything cross-LSP needs as metadata, and let cross-LSP
* be a pure lookup pass. */
int cbm_go_fast_resolve_qualified_calls(
CBMFileResult* result,
CBMTypeRegistry* reg,
const char** import_names, const char** import_qns, int import_count) {
if (!result || !reg) return 0;
/* Snapshot the count: we append to resolved_calls inside the loop,
* but only want to scan the original unresolved entries. */
int initial_count = result->resolved_calls.count;
int newly_resolved = 0;
for (int i = 0; i < initial_count; i++) {
const CBMResolvedCall* uc = &result->resolved_calls.items[i];
if (!uc->strategy || !uc->callee_qn || !uc->caller_qn) continue;
if (strcmp(uc->strategy, "lsp_unresolved") != 0) continue;
/* Case 1: callee_qn is already a fully-qualified type/pkg
* symbol that per-file LSP couldn't find in its local registry
* but the global registry has. (method_not_found with NAMED
* receiver, or any other case where callee_qn looks like a
* full QN). Direct lookup. */
const CBMRegisteredFunc* f = cbm_registry_lookup_func(reg, uc->callee_qn);
/* Case 2: callee_qn is "local_pkg_alias.suffix" — the alias
* needs translating through the file's import map to get the
* real module QN. */
if (!f) {
const char* dot = strchr(uc->callee_qn, '.');
if (dot) {
size_t prefix_len = (size_t)(dot - uc->callee_qn);
if (prefix_len > 0 && prefix_len < 256) {
char prefix[256];
memcpy(prefix, uc->callee_qn, prefix_len);
prefix[prefix_len] = '\0';
const char* suffix = dot + 1;
for (int j = 0; j < import_count; j++) {
if (import_names[j] && import_qns[j] &&
strcmp(prefix, import_names[j]) == 0) {
char fq[1024];
int n = snprintf(fq, sizeof(fq), "%s.%s",
import_qns[j], suffix);
if (n > 0 && n < (int)sizeof(fq)) {
f = cbm_registry_lookup_func(reg, fq);
}
break;
}
}
}
}
}
if (!f) continue;
/* Emit a resolved entry. cbm_pipeline_find_lsp_resolution
* picks the highest-confidence match, so the unresolved entry
* stays (harmless duplicate) but our resolved entry wins. */
CBMResolvedCall rc;
rc.caller_qn = uc->caller_qn;
rc.callee_qn = f->qualified_name; /* borrowed from pipeline arena */
rc.strategy = "lsp_strategy_cross_file";
rc.confidence = 0.92f;
rc.reason = NULL;
cbm_resolvedcall_push(&result->resolved_calls, &result->arena, rc);
newly_resolved++;
}
/* Return value mirrors the old contract (count of items still
* needing the slow path) but is always 0 now — caller should
* unconditionally skip the slow path for Go. */
(void)newly_resolved;
return 0;
}
// --- Batch cross-file LSP ---
void cbm_batch_go_lsp_cross(
CBMArena* arena,
CBMBatchGoLSPFile* files, int file_count,
CBMResolvedCallArray* out)
{
if (!files || file_count <= 0 || !out) return;
for (int f = 0; f < file_count; f++) {
CBMBatchGoLSPFile* file = &files[f];
memset(&out[f], 0, sizeof(CBMResolvedCallArray));
if (!file->source || file->source_len <= 0 || file->def_count <= 0) continue;
// Per-file arena: registry + temp data freed after each file
CBMArena file_arena;
cbm_arena_init(&file_arena);
CBMResolvedCallArray file_out;
memset(&file_out, 0, sizeof(file_out));
// Delegate to existing per-file function
cbm_run_go_lsp_cross(
&file_arena,
file->source, file->source_len,
file->module_qn,
file->defs, file->def_count,
file->import_names, file->import_qns, file->import_count,
file->cached_tree,
&file_out);
// Copy results to output arena (must outlive per-file arena)
if (file_out.count > 0) {
out[f].count = file_out.count;
out[f].items = (CBMResolvedCall*)cbm_arena_alloc(arena,
file_out.count * sizeof(CBMResolvedCall));
for (int j = 0; j < file_out.count; j++) {
CBMResolvedCall* src = &file_out.items[j];
CBMResolvedCall* dst = &out[f].items[j];
dst->caller_qn = src->caller_qn ? cbm_arena_strdup(arena, src->caller_qn) : NULL;
dst->callee_qn = src->callee_qn ? cbm_arena_strdup(arena, src->callee_qn) : NULL;
dst->strategy = src->strategy ? cbm_arena_strdup(arena, src->strategy) : NULL;
dst->confidence = src->confidence;
dst->reason = src->reason ? cbm_arena_strdup(arena, src->reason) : NULL;
}
}
cbm_arena_destroy(&file_arena);
}
}