Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -6660,6 +6660,18 @@ static void extract_class_fields(CBMExtractCtx *ctx, TSNode class_node, const ch
return;
}

// Go: struct_type wraps fields in a field_declaration_list whose named
// children are the actual field_declaration nodes. The generic scan below
// matches direct children of `body` only, so unwrap the list — otherwise
// every Go struct registers with zero fields and cross-package field-chain
// calls (h.svc.Handle) can never resolve.
if (ctx->language == CBM_LANG_GO && strcmp(ts_node_type(body), "struct_type") == 0) {
TSNode fdl = cbm_find_child_by_kind(body, "field_declaration_list");
if (!ts_node_is_null(fdl)) {
body = fdl;
}
}

CBMArena *a = ctx->arena;
uint32_t count = ts_node_named_child_count(body);
for (uint32_t i = 0; i < count; i++) {
Expand Down
183 changes: 143 additions & 40 deletions internal/cbm/lsp/go_lsp.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdatomic.h>

/* CBM_DISPATCH_TRACE diagnostics sink. The Go resolve worker's stderr is
* swallowed by the daemon supervisor, so dispatch traces route to a file
* instead: CBM_TRACE_FILE, default /tmp/cbm_dispatch_trace.txt (append). */
static FILE *cbm_dispatch_trace_sink(void) {
static FILE *sink = NULL;
static _Atomic int tried = 0;
if (atomic_exchange_explicit(&tried, 1, memory_order_relaxed) == 0) {
const char *d = getenv("CBM_DISPATCH_TRACE");
if (d && d[0]) {
const char *path = getenv("CBM_TRACE_FILE");
if (!path || !path[0]) path = "/tmp/cbm_dispatch_trace.txt";
sink = fopen(path, "a");
}
}
return sink;
}

// Forward declarations
static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node);
Expand Down Expand Up @@ -866,11 +884,53 @@ const CBMType* go_eval_builtin_call(GoLSPContext* ctx, const char* name, TSNode

// --- go_lookup_field: struct field lookup with embedding recursion ---

// --- Import-alias re-qualification ------------------------------------
//
// parse_field_defs_into_type qualifies struct field type texts as
// "<def_module>.<text>". When the author wrote the text through an import
// alias ("Svc:svc.Svc" in module test.main), that yields a QN
// ("test.main.svc.Svc") that exists nowhere in the project-wide registry —
// the real QN is "<import_qn>.Svc" and only the calling file's import map
// can say so. On an exact-QN miss this rewrites the alias segment through
// the file's imports; returns NULL when no alias segment is involved.

static const char *go_requalify_via_imports(GoLSPContext *ctx, const char *type_qn) {
if (!ctx || !type_qn || !type_qn[0] || ctx->import_count <= 0) return NULL;
for (int j = 0; j < ctx->import_count; j++) {
const char *alias = ctx->import_local_names[j];
const char *alias_qn = ctx->import_package_qns[j];
if (!alias || !alias[0] || !alias_qn || strchr(alias, '.')) continue;
size_t alias_len = strlen(alias);
/* Last occurrence of a "<dot>alias<dot>" segment in type_qn. */
const char *hit = NULL;
for (const char *p = type_qn;;) {
p = strstr(p, ".");
if (!p) break;
p++;
if (strncmp(p, alias, alias_len) == 0 && p[alias_len] == '.') {
hit = p;
p += alias_len;
}
}
if (hit) {
const char *rest = hit + alias_len + 1; /* past "<alias>." */
return cbm_arena_sprintf(ctx->arena, "%s.%s", alias_qn, rest);
}
}
return NULL;
}

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 && depth == 0) {
/* Import-alias re-qualification: field texts from cross-package defs
* may embed an alias segment only this file's import map resolves. */
const char* alt_qn = go_requalify_via_imports(ctx, type_qn);
if (alt_qn) rt = cbm_registry_lookup_type(ctx->registry, alt_qn);
}
if (!rt) return NULL;

// Follow alias chain
Expand Down Expand Up @@ -907,6 +967,18 @@ static const CBMRegisteredFunc* go_lookup_field_or_method_depth(GoLSPContext* ct
const CBMRegisteredFunc* f = cbm_registry_lookup_method(ctx->registry, type_qn, member_name);
if (f) return f;

/* Import-alias re-qualification fallback: NAMED receivers built from
* cross-package field type texts can carry a "<module>.svc.Svc" QN;
* retry the method set on the import-resolved QN (see
* go_requalify_via_imports). */
if (depth == 0) {
const char* alt_qn = go_requalify_via_imports(ctx, type_qn);
if (alt_qn) {
f = go_lookup_field_or_method_depth(ctx, alt_qn, member_name, depth + 1);
if (f) return f;
}
}

const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry, type_qn);
if (rt) {
// Follow type alias chain
Expand Down Expand Up @@ -1430,22 +1502,55 @@ static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {
if (base && base->kind == CBM_TYPE_POINTER) base = cbm_type_deref(base);

if (base && base->kind == CBM_TYPE_NAMED) {
const char *recv_qn = base->data.named.qualified_name;
const CBMRegisteredType *receiver_type = cbm_registry_lookup_type(
ctx->registry, base->data.named.qualified_name);
ctx->registry, recv_qn);
const char *alt_qn = NULL;
/* Re-qualify NAMED receivers that embed an import
* alias segment (cross-package field type texts) —
* the real type only exists under the import QN. */
if (!receiver_type) {
alt_qn = go_requalify_via_imports(ctx, recv_qn);
if (alt_qn) {
receiver_type = cbm_registry_lookup_type(ctx->registry, alt_qn);
if (receiver_type) recv_qn = alt_qn;
}
}
{
static _Atomic int g_diag_once = 0;
FILE *tf = cbm_dispatch_trace_sink();
if (tf && atomic_fetch_add_explicit(&g_diag_once, 1,
memory_order_relaxed) < 200000) {
fprintf(tf, "[DISPATCH] pkg=%s call=%s recv_qn=%s recv_hit=%d alt=%s reg_has_type_total=%d\n",
ctx->package_qn ? ctx->package_qn : "?", field_name ? field_name : "?",
base->data.named.qualified_name ? base->data.named.qualified_name : "?",
receiver_type != NULL, alt_qn ? alt_qn : "-", ctx->registry->type_count);
}
}
/* Registered interface receivers must reach the
* interface-resolution branch below. Their semantic
* method registrations are signatures, not concrete
* dispatch targets. */
if (!receiver_type || !receiver_type->is_interface) {
const CBMRegisteredFunc *method = go_lookup_field_or_method(
ctx, base->data.named.qualified_name, field_name);
ctx, recv_qn, field_name);
if (method) {
const char *strategy = "lsp_type_dispatch";
if (method->receiver_type &&
strcmp(method->receiver_type,
base->data.named.qualified_name) != 0) {
recv_qn) != 0) {
strategy = "lsp_embed_dispatch";
}
{
FILE *tf = cbm_dispatch_trace_sink();
if (tf) {
fprintf(tf, "[DISPATCH-OK] pkg=%s call=%s target=%s strat=%s recv=%s\n",
ctx->package_qn ? ctx->package_qn : "?",
field_name ? field_name : "?",
method->qualified_name ? method->qualified_name : "?",
strategy, recv_qn ? recv_qn : "?");
}
}
emit_resolved_call(ctx, method->qualified_name, strategy, 0.95f,
node);
goto recurse;
Expand All @@ -1460,9 +1565,15 @@ static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {
if (!is_iface && base->kind == CBM_TYPE_NAMED) {
const CBMRegisteredType* rt = cbm_registry_lookup_type(ctx->registry,
base->data.named.qualified_name);
if (!rt) {
const char* alt_qn = go_requalify_via_imports(
ctx, base->data.named.qualified_name);
if (alt_qn)
rt = cbm_registry_lookup_type(ctx->registry, alt_qn);
}
if (rt && rt->is_interface) {
is_iface = true;
iface_qn = base->data.named.qualified_name;
iface_qn = rt->qualified_name;
}
}
if (is_iface) {
Expand Down Expand Up @@ -1533,12 +1644,31 @@ static void resolve_calls_in_node_inner(GoLSPContext* ctx, TSNode node) {

// Type resolved to NAMED but neither method nor interface matched
if (base && base->kind == CBM_TYPE_NAMED) {
{
FILE *tf = cbm_dispatch_trace_sink();
if (tf) {
fprintf(tf, "[DISPATCH-FAIL] pkg=%s call=%s.%s qn=%s type_total=%d\n",
ctx->package_qn ? ctx->package_qn : "?",
base->data.named.qualified_name ? base->data.named.qualified_name : "?",
field_name ? field_name : "?", "method_not_found",
ctx->registry->type_count);
}
}
emit_unresolved_call(ctx,
cbm_arena_sprintf(ctx->arena, "%s.%s",
base->data.named.qualified_name,
field_name),
"method_not_found", node);
} else if (cbm_type_is_unknown(recv_type)) {
{
FILE *tf = cbm_dispatch_trace_sink();
if (tf) {
fprintf(tf, "[DISPATCH-FAIL] pkg=%s call=%s qn=%s type_total=%d\n",
ctx->package_qn ? ctx->package_qn : "?",
field_name ? field_name : "?", "unknown_receiver_type",
ctx->registry->type_count);
}
}
char* operand_text = lsp_node_text(ctx, operand);
emit_unresolved_call(
ctx,
Expand Down Expand Up @@ -1787,42 +1917,15 @@ static void process_function(GoLSPContext* ctx, TSNode func_node) {
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);
}
// Enclosing-function QN must be the BARE package.Func form (no receiver
// type segment). The textual call events (extract_unified.c) source calls
// as package_qn.func_name — methods included — and the defs pass creates
// the graph Method node under the same QN, so any other form breaks the
// caller-QN join in cbm_pipeline_find_lsp_resolution and the LSP-resolved
// call silently falls back to the registry short-name resolver. The
// receiver type still reaches the registry via the def's parent_class /
// method->receiver_type; it just does not appear in the caller QN.
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;
Expand Down
Loading
Loading