fix(go-lsp): split the raw Go result list, keep a single result raw

The first D1 change fed the Go cross-file registrar the extractor's
return_types. Those are cleaned names for type-reference edges:
clean_type_name strips `[]` and `*`. A cross-file single result `[]T`
therefore reached the registrar as `T`. `xs := pkg.Generate(); xs[0].M()`
lost its typing and fell back to a name guess. On prometheus the base
binds `sample[0].T()` to chunks.Sample.T, but the first D1 change bound
it to tsdb.T.

pass_lsp_cross.c now leaves a single result's raw text alone, as before
D1; the registrar already parses `*T` and `[]T`. Only a result LIST
`(...)` is split, at its top-level commas, into raw element types:
- `(*T, error)` -> `*T|error`;
- the named `(t *T, err error)` -> `*T|error`;
- the grouped `(a, b int)` -> `int|int`.
`chan`, `func`, `map`, `struct` and `interface` never count as result
names. A list that does not split cleanly keeps its raw text.

Tests: the D1 fixture gains three call sites, each with two same-named
decoys:
- a single slice result indexed (`xs[0].TS()`);
- a tuple with a slice element;
- grouped named results.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-10-19 03:29:05 +02:00
parent 9129c48f48
commit bec015461c
2 changed files with 214 additions and 12 deletions
+168 -7
View File
@@ -166,6 +166,163 @@ static const char *pxc_join_pipe(CBMArena *arena, const char *const *items) {
return buf;
}
/* ── Go result lists for the cross-file registrar ─────────────────
*
* The Go registrar reads a def's return_types as '|'-separated type texts and
* parses each piece on its own (go_lsp.c split_pipe_types ->
* cbm_parse_return_type_text: `*T`, `[]T`, builtins, named types). A single
* result already is one such text. A result LIST arrives as its raw source,
* `(*T, error)` or the named `(t *T, err error)`, which parses as nothing, so
* pxc_go_result_list splits it into its element types. The extractor's
* return_types are no substitute: clean_type_name strips `[]` and `*` for
* type-reference edges, and `[]T` read as `T` mistypes `xs[0].M()`. */
enum { PXC_GO_MAX_RESULTS = 16 };
typedef struct {
const char *text;
size_t len;
} pxc_span_t;
static bool pxc_is_space(char c) {
return c == ' ' || c == '\t' || c == '\n' || c == '\r';
}
static bool pxc_is_ident_char(char c) {
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
}
static int pxc_go_depth(char c, int depth) {
if (c == '(' || c == '[' || c == '{') {
return depth + 1;
}
if ((c == ')' || c == ']' || c == '}') && depth > 0) {
return depth - 1;
}
return depth;
}
static pxc_span_t pxc_trim(const char *s, size_t n) {
while (n > 0 && pxc_is_space(*s)) {
s++;
n--;
}
while (n > 0 && pxc_is_space(s[n - 1])) {
n--;
}
return (pxc_span_t){s, n};
}
/* Split `(a, b)` at its top-level commas into trimmed elements. A trailing
* comma (a multi-line list) leaves no element. Returns the element count, or
* -1 for text that is no result list or holds an empty element. */
static int pxc_go_split_results(const char *raw, pxc_span_t *out) {
size_t n = strlen(raw);
if (n < 2 || raw[0] != '(' || raw[n - 1] != ')') {
return -1;
}
const char *s = raw + 1;
size_t len = n - 2;
int count = 0;
int depth = 0;
size_t start = 0;
for (size_t i = 0; i <= len; i++) {
bool end = i == len;
if (!end && (s[i] != ',' || depth != 0)) {
depth = pxc_go_depth(s[i], depth);
continue;
}
pxc_span_t e = pxc_trim(s + start, i - start);
if (e.len == 0 && !(end && count > 0)) {
return -1;
}
if (e.len > 0) {
if (count == PXC_GO_MAX_RESULTS) {
return -1;
}
out[count++] = e;
}
start = i + 1;
}
return count;
}
/* Length of the result NAME that starts an element (`err` in `err error`),
* 0 when the element is a bare type. A type keyword is never a name: `chan T`
* and `func() T` are types. *lone is set when the whole element is one
* identifier: a type in `(T, error)`, a grouped name in `(a, b int)`. */
static size_t pxc_go_result_name(pxc_span_t e, bool *lone) {
size_t k = 0;
while (k < e.len && pxc_is_ident_char(e.text[k])) {
k++;
}
*lone = k > 0 && k == e.len;
if (k == 0 || k == e.len || !pxc_is_space(e.text[k])) {
return 0;
}
static const char *const type_keywords[] = {"chan", "func", "map", "struct", "interface", NULL};
for (int i = 0; type_keywords[i]; i++) {
if (strlen(type_keywords[i]) == k && memcmp(e.text, type_keywords[i], k) == 0) {
return 0;
}
}
return k;
}
/* Named results: each lone name takes the type of the next `name type`
* element, as Go groups them. Returns count, or -1 when the list is not a
* well-formed named list. */
static int pxc_go_named_types(CBMArena *arena, const pxc_span_t *el, int count,
const char **types) {
int pending = 0;
for (int i = 0; i < count; i++) {
bool lone = false;
size_t k = pxc_go_result_name(el[i], &lone);
if (k == 0) {
if (!lone) {
return -1;
}
pending++;
continue;
}
pxc_span_t t = pxc_trim(el[i].text + k, el[i].len - k);
const char *type = cbm_arena_sprintf(arena, "%.*s", (int)t.len, t.text);
for (int j = i - pending; j <= i; j++) {
types[j] = type;
}
pending = 0;
}
return pending == 0 ? count : -1;
}
/* `(*T, error)` -> `*T|error`; `(t *T, err error)` -> `*T|error`;
* `(a, b int)` -> `int|int`. NULL when raw is no result list (a single
* result) or does not split cleanly: the caller keeps the raw text then. */
static const char *pxc_go_result_list(CBMArena *arena, const char *raw) {
pxc_span_t el[PXC_GO_MAX_RESULTS];
int count = pxc_go_split_results(raw, el);
if (count <= 0) {
return NULL;
}
bool named = false;
for (int i = 0; i < count && !named; i++) {
bool lone = false;
named = pxc_go_result_name(el[i], &lone) > 0;
}
const char *types[PXC_GO_MAX_RESULTS + 1];
if (named) {
if (pxc_go_named_types(arena, el, count, types) < 0) {
return NULL;
}
} else {
for (int i = 0; i < count; i++) {
types[i] = cbm_arena_sprintf(arena, "%.*s", (int)el[i].len, el[i].text);
}
}
types[count] = NULL;
return pxc_join_pipe(arena, types);
}
/* ── Cross-file base-class QN resolution ──────────────────────────
*
* CBMDefinition.base_classes carries the SOURCE SPELLING of each base
@@ -514,13 +671,17 @@ static int pxc_build_lsp_def(CBMArena *arena, const CBMDefinition *src, const ch
}
dst->is_interface = (strcmp(label, "Interface") == 0 || strcmp(label, "Protocol") == 0);
/* The return-type string. The Go registrar splits it on '|' (go_lsp.c
* split_pipe_types), so Go gets the PARSED result list, joined: `(*T,
* error)` arrives as `*T|error` and a named result `(t *T)` as `*T`. The
* raw result text it used to get did not parse, so every variable bound
* by a cross-file multi-value or named-result call (`t, err := pkg.New()`)
* stayed untyped (F-G8 D1). Other languages keep the single raw string. */
if (lang == CBM_LANG_GO && src->return_types && src->return_types[0]) {
dst->return_types = pxc_join_pipe(arena, src->return_types);
* split_pipe_types), so a Go result LIST arrives split into its element
* types: `(*T, error)` as `*T|error`, a named `(t *T)` as `*T`. The raw
* list text did not parse, so every variable bound by a cross-file
* multi-value or named-result call (`t, err := pkg.New()`) stayed untyped
* (F-G8 D1). A single result, and every other language, keeps the raw
* text the registrar already parses (`[]T` stays a slice). */
const char *go_list = (lang == CBM_LANG_GO && src->return_type)
? pxc_go_result_list(arena, src->return_type)
: NULL;
if (go_list) {
dst->return_types = go_list;
} else {
dst->return_types = src->return_type ? cbm_arena_strdup(arena, src->return_type) : NULL;
}
+46 -5
View File
@@ -8822,9 +8822,11 @@ static void write_go_c_ref_guard_fixture(const char *tmp, int pad_files) {
/* F-G8 D1: a variable bound by a CROSS-file multi-value or named-result call
* is typed. The cross-file registrar received the raw result text
* `(*Encoder, error)`, which did not parse, so `e, err := enc.NewEncoder()`
* left e untyped and e.EncodeTuple() fell to a name guess. Every target
* method has two same-named decoys, so a guess cannot pass. pad_files > 0
* forces the parallel resolver. */
* left e untyped and e.EncodeTuple() fell to a name guess. A single result
* keeps its raw text: `[]Sample` must stay a slice so that `xs[0].TS()` is
* typed (the extractor's cleaned return_types read it as `Sample`). Every
* target method has two same-named decoys, so a guess cannot pass.
* pad_files > 0 forces the parallel resolver. */
static void write_go_multi_return_fixture(const char *tmp, int pad_files) {
write_temp_file(tmp, "go.mod", "module example.com/fx\n\ngo 1.22\n");
write_temp_file(tmp, "enc/enc.go",
@@ -8836,9 +8838,25 @@ static void write_go_multi_return_fixture(const char *tmp, int pad_files) {
"\n"
"func NewNamed() (e *Encoder) { e = &Encoder{}; return }\n"
"\n"
"func NewPair() (a, b *Encoder) { return &Encoder{}, &Encoder{} }\n"
"\n"
"func (e *Encoder) EncodeTuple() int { return e.n }\n"
"\n"
"func (e *Encoder) EncodeNamed() int { return e.n }\n");
"func (e *Encoder) EncodeNamed() int { return e.n }\n"
"\n"
"func (e *Encoder) EncodeGrouped() int { return e.n }\n");
write_temp_file(tmp, "samples/samples.go",
"package samples\n"
"\n"
"type Sample interface{ TS() int64 }\n"
"\n"
"type sample struct{}\n"
"\n"
"func (sample) TS() int64 { return 0 }\n"
"\n"
"func Generate() []Sample { return []Sample{sample{}} }\n"
"\n"
"func List() ([]Sample, error) { return []Sample{sample{}}, nil }\n");
write_temp_file(tmp, "diag/diag.go",
"package diag\n"
"\n"
@@ -8865,8 +8883,24 @@ static void write_go_multi_return_fixture(const char *tmp, int pad_files) {
"import (\n"
"\t\"example.com/fx/diag\"\n"
"\t\"example.com/fx/enc\"\n"
"\t\"example.com/fx/samples\"\n"
")\n"
"\n"
"func UseIndex() int64 {\n"
"\txs := samples.Generate()\n"
"\treturn xs[0].TS()\n"
"}\n"
"\n"
"func UseTupleSlice() int64 {\n"
"\tys, _ := samples.List()\n"
"\treturn ys[0].TS()\n"
"}\n"
"\n"
"func UseGrouped() int {\n"
"\tx, _ := enc.NewPair()\n"
"\treturn x.EncodeGrouped()\n"
"}\n"
"\n"
"func UseTuple() int {\n"
"\te, err := enc.NewEncoder()\n"
"\t_ = err\n"
@@ -8901,7 +8935,11 @@ static void write_go_multi_return_fixture(const char *tmp, int pad_files) {
"func (D1) LowWatermarkField() int { return 0 }\n"
"func (D2) LowWatermarkField() int { return 0 }\n"
"func (D1) HasErrorsSlice() bool { return false }\n"
"func (D2) HasErrorsSlice() bool { return false }\n");
"func (D2) HasErrorsSlice() bool { return false }\n"
"func (D1) EncodeGrouped() int { return 0 }\n"
"func (D2) EncodeGrouped() int { return 0 }\n"
"func (D1) TS() int64 { return 0 }\n"
"func (D2) TS() int64 { return 0 }\n");
for (int i = 0; i < pad_files; i++) {
char name[64];
char body[128];
@@ -8951,6 +8989,9 @@ static int go_multi_return_case(int pad_files) {
{"UseNamed", "EncodeNamed", "enc/enc.go"}, /* named result (e *Encoder) */
{"UseField", "LowWatermarkField", "app/types.go"}, /* h, _ := newHead(); h.iso.X() */
{"UseSlice", "HasErrorsSlice", "diag/diag.go"}, /* _, d := diag.Run() */
{"UseGrouped", "EncodeGrouped", "enc/enc.go"}, /* (a, b *Encoder) */
{"UseIndex", "TS", "samples/samples.go"}, /* single []Sample, xs[0] */
{"UseTupleSlice", "TS", "samples/samples.go"}, /* ([]Sample, error) */
};
for (size_t i = 0; i < sizeof(want) / sizeof(want[0]); i++) {
int right = named_edge_to_file_count(s, project, "CALLS", want[i].caller, want[i].method,