Skip to content
Draft
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
4 changes: 4 additions & 0 deletions internal/cbm/cbm.c
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,10 @@ CBMFileResult *cbm_extract_file_ex(const char *source, int source_len, CBMLangua
.root = root,
.macro_table = macro_table,
.return_type_table = return_type_table,
/* #1911: per-file Go build constraint, folded into func/method QNs by
* the def and scope builders so build-tag twin files stop colliding. */
.go_build_tau =
language == CBM_LANG_GO ? cbm_go_build_tau(a, source, source_len, rel_path) : NULL,
};

// Run extractors: defs + imports use separate walks (unique recursion patterns),
Expand Down
13 changes: 13 additions & 0 deletions internal/cbm/cbm.h
Original file line number Diff line number Diff line change
Expand Up @@ -599,8 +599,21 @@ typedef struct {
* class-body variable def records which class declares it (parent_class)
* without changing its module-level qualified name. NULL elsewhere. */
const char *var_parent_class;
/* #1911: the Go file's build constraint τ (compacted //go:build expression,
* else the GOOS/GOARCH filename suffix), or NULL for an unconstrained
* file. Folded into func/method QNs by extract_defs.c and mirrored by the
* scope builder in extract_unified.c so build-tag twin files stop
* colliding in the graph upsert. Set once in cbm_extract_file. */
const char *go_build_tau;
} CBMExtractCtx;

/* #1911: resolve a Go file's build constraint τ — the compacted //go:build
* expression when present (constraint lines precede the package clause), else
* the official GOOS/GOARCH filename suffix (name_GOOS.go, name_GOARCH.go,
* name_GOOS_GOARCH.go, each optionally followed by _test) — or NULL for an
* unconstrained file. Defined in helpers.c. */
const char *cbm_go_build_tau(CBMArena *a, const char *source, int source_len, const char *rel_path);

// --- Public API ---

// Bind third-party allocators (tree-sitter, sqlite3) to mimalloc as
Expand Down
10 changes: 10 additions & 0 deletions internal/cbm/extract_defs.c
Original file line number Diff line number Diff line change
Expand Up @@ -3778,6 +3778,16 @@ static void extract_func_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
def.is_test = rust_def_is_test(def.decorators);
}

// Go: same move for build-constrained twin files (//go:build lines,
// GOOS/GOARCH filename suffixes) — fold the per-file constraint τ into
// func/method QNs so both variants survive the upsert (#1911). Types and
// vars stay plain, keeping parent_class / DEFINES_METHOD joins intact.
// MUST mirror go_tau_scope_qn in extract_unified.c exactly, or body calls
// in constrained files detach to the File node.
if (ctx->language == CBM_LANG_GO && ctx->go_build_tau) {
def.qualified_name = cbm_arena_sprintf(a, "%s#%s", def.qualified_name, ctx->go_build_tau);
}

// C++/CUDA: GoogleTest macros are test functions (#1266).
if (is_gtest) {
def.is_test = true;
Expand Down
15 changes: 13 additions & 2 deletions internal/cbm/extract_unified.c
Original file line number Diff line number Diff line change
Expand Up @@ -920,6 +920,17 @@ static const char *compute_func_qn(CBMExtractCtx *ctx, TSNode node, const CBMLan
ctx->language);
}

/* #1911: fold the per-file Go build constraint into the scope QN. MUST mirror
* the def-side formula in extract_defs.c exactly — the two produce the same
* string for the same function, or body calls in build-constrained files
* detach to the File node. */
static const char *go_tau_scope_qn(CBMExtractCtx *ctx, const char *fqn) {
if (!fqn || ctx->language != CBM_LANG_GO || !ctx->go_build_tau) {
return fqn;
}
return cbm_arena_sprintf(ctx->arena, "%s#%s", fqn, ctx->go_build_tau);
}

// Compute class QN for scope tracking.
static const char *compute_class_qn(CBMExtractCtx *ctx, TSNode node, const WalkState *state) {
if (ctx->language == CBM_LANG_OBJECTSCRIPT_UDL) {
Expand Down Expand Up @@ -2069,7 +2080,7 @@ static bool push_pre_node_scope(CBMExtractCtx *ctx, TSNode node, const CBMLangSp
if (ts_node_is_null(label)) {
return false;
}
const char *fqn = compute_func_qn(ctx, label, spec, state);
const char *fqn = go_tau_scope_qn(ctx, compute_func_qn(ctx, label, spec, state));
if (!fqn) {
return false;
}
Expand Down Expand Up @@ -2199,7 +2210,7 @@ static void push_boundary_scopes(CBMExtractCtx *ctx, TSNode node, const CBMLangS
}
}
if (!skip_nested) {
const char *fqn = compute_func_qn(ctx, node, spec, state);
const char *fqn = go_tau_scope_qn(ctx, compute_func_qn(ctx, node, spec, state));
if (fqn && push_function_scope(state, depth, fqn, node)) {
const char *node_kind = ts_node_type(node);
bool split_signature = (ctx->language == CBM_LANG_DART &&
Expand Down
105 changes: 105 additions & 0 deletions internal/cbm/helpers.c
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,111 @@ bool cbm_is_keyword(const char *name, CBMLanguage lang) {
return false;
}

/* Official GOOS / GOARCH tokens recognized as build-constraint filename
* suffixes (go/build's lists; hurd and legacy nacl included for completeness). */
static const char *const GO_TAU_GOOS[] = {
"aix", "android", "darwin", "dragonfly", "freebsd", "hurd", "illumos", "ios", "js", "linux",
"nacl", "netbsd", "openbsd", "plan9", "solaris", "wasip1", "windows", "zos", NULL};
static const char *const GO_TAU_GOARCH[] = {
"386", "amd64", "amd64p32", "arm", "arm64", "loong64", "mips",
"mipsle", "mips64", "mips64le", "ppc", "ppc64", "ppc64le", "riscv",
"riscv64", "s390", "s390x", "sparc", "sparc64", "wasm", NULL};

static bool go_tau_token(const char *s, size_t len, const char *const *table) {
for (int i = 0; table[i]; i++) {
if (strlen(table[i]) == len && strncmp(s, table[i], len) == 0) {
return true;
}
}
return false;
}

const char *cbm_go_build_tau(CBMArena *a, const char *source, int source_len,
const char *rel_path) {
/* A //go:build line wins over the filename suffix (both may be present,
* and the directive is the authoritative constraint since Go 1.17). */
if (source && source_len > 0) {
static const char kPrefix[] = "//go:build";
const char *p = source;
const char *end = source + source_len;
while (p < end) {
const char *nl = memchr(p, '\n', (size_t)(end - p));
size_t linelen = nl ? (size_t)(nl - p) : (size_t)(end - p);
if (linelen >= SLEN("package ") && strncmp(p, "package ", SLEN("package ")) == 0) {
break; /* constraints cannot appear after the package clause */
}
if (linelen > SLEN(kPrefix) && strncmp(p, kPrefix, SLEN(kPrefix)) == 0 &&
(p[SLEN(kPrefix)] == ' ' || p[SLEN(kPrefix)] == '\t')) {
/* Compact the expression: drop whitespace and CR so the QN
* suffix stays readable and stable (the #495 Rust move). */
char buf[CBM_SZ_256];
size_t bi = 0;
for (size_t i = SLEN(kPrefix); i < linelen && bi + SKIP_ONE < sizeof(buf); i++) {
char c = p[i];
if (c == ' ' || c == '\t' || c == '\r') {
continue;
}
buf[bi++] = c;
}
buf[bi] = '\0';
return bi > 0 ? cbm_arena_sprintf(a, "%s", buf) : NULL;
}
if (!nl) {
break;
}
p = nl + SKIP_ONE;
}
}

/* GOOS/GOARCH filename suffix: name_GOOS.go, name_GOARCH.go,
* name_GOOS_GOARCH.go, each optionally followed by _test. */
if (!rel_path || !rel_path[0]) {
return NULL;
}
const char *base = rel_path;
for (const char *pb = rel_path; *pb; pb++) {
if (*pb == '/' || *pb == '\\') {
base = pb + SKIP_ONE;
}
}
size_t blen = strlen(base);
if (blen <= SLEN(".go") || strcmp(base + blen - SLEN(".go"), ".go") != 0) {
return NULL;
}
size_t stem_len = blen - SLEN(".go");
if (stem_len > SLEN("_test") &&
strncmp(base + stem_len - SLEN("_test"), "_test", SLEN("_test")) == 0) {
stem_len -= SLEN("_test");
}
/* Walk the last two '_'-separated segments. */
size_t last = stem_len;
while (last > 0 && base[last - SKIP_ONE] != '_') {
last--;
}
if (last == 0) {
return NULL; /* no '_' — unconstrained */
}
const char *seg2 = base + last;
size_t seg2_len = stem_len - last;
size_t prev_end = last - SKIP_ONE; /* the '_' before seg2 */
if (go_tau_token(seg2, seg2_len, GO_TAU_GOARCH)) {
size_t prev = prev_end;
while (prev > 0 && base[prev - SKIP_ONE] != '_') {
prev--;
}
const char *seg1 = base + prev;
size_t seg1_len = prev_end - prev;
if (prev_end > 0 && go_tau_token(seg1, seg1_len, GO_TAU_GOOS)) {
return cbm_arena_sprintf(a, "%.*s_%.*s", (int)seg1_len, seg1, (int)seg2_len, seg2);
}
return cbm_arena_sprintf(a, "%.*s", (int)seg2_len, seg2);
}
if (go_tau_token(seg2, seg2_len, GO_TAU_GOOS)) {
return cbm_arena_sprintf(a, "%.*s", (int)seg2_len, seg2);
}
return NULL;
}

// Builtins that appear in the keyword set above (so they are suppressed as bare
// usages) but for which we mint a real graph node and an LSP resolution, so a
// CALL to them must still be extracted. MUST stay in sync with kPyBuiltinNodes
Expand Down
30 changes: 29 additions & 1 deletion internal/cbm/lsp/type_registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,30 @@ const CBMRegisteredFunc *cbm_registry_lookup_method_aliased(const CBMTypeRegistr
return NULL;
}

/* #495/#1911: a build-constrained twin carries a `#`-suffixed QN
* (`pkg.name#unix`, Rust cfg / Go //go:build), so the exact `pkg.name` key
* misses it. A SOLE suffixed variant is still an exact symbol — return it.
* Several variants are genuinely ambiguous without the caller's own build
* configuration — fail closed and let the multi-candidate registry path
* handle the call at its honest confidence. */
static const CBMRegisteredFunc *lookup_func_sole_tau_variant(const CBMTypeRegistry *reg,
const char *qn, size_t qn_len) {
const CBMRegisteredFunc *sole = NULL;
for (; reg; reg = reg->fallback) {
for (int i = 0; i < reg->func_count; i++) {
const char *cand = reg->funcs[i].qualified_name;
if (!cand || strncmp(cand, qn, qn_len) != 0 || cand[qn_len] != '#') {
continue;
}
if (sole) {
return NULL; /* two constrained twins — ambiguous */
}
sole = &reg->funcs[i];
}
}
return sole;
}

const CBMRegisteredFunc *cbm_registry_lookup_symbol(const CBMTypeRegistry *reg,
const char *package_qn, const char *name) {
if (!reg || !package_qn || !name)
Expand All @@ -671,7 +695,11 @@ const CBMRegisteredFunc *cbm_registry_lookup_symbol(const CBMTypeRegistry *reg,
memcpy(buf + pkg_len + 1, name, name_len);
buf[total_len] = '\0';

return cbm_registry_lookup_func(reg, buf);
const CBMRegisteredFunc *r = cbm_registry_lookup_func(reg, buf);
if (r) {
return r;
}
return lookup_func_sole_tau_variant(reg, buf, total_len);
}

// Count parameters in a FUNC signature.
Expand Down
14 changes: 12 additions & 2 deletions src/pipeline/lsp_resolve.h
Original file line number Diff line number Diff line change
Expand Up @@ -337,8 +337,18 @@ static inline bool cbm_pipeline_invocation_leaf_matches(const CBMResolvedCall *r
}
const char *resolved_leaf = cbm_lsp_bare_segment(resolved->callee_qn);
const char *call_leaf = cbm_lsp_bare_segment(call->callee_name);
if (resolved_leaf && call_leaf && strcmp(resolved_leaf, call_leaf) == 0) {
return true;
if (resolved_leaf && call_leaf) {
if (strcmp(resolved_leaf, call_leaf) == 0) {
return true;
}
/* #495/#1911: a build-constrained twin QN carries a `#τ` suffix
* (`FlushDisk#linux`); its callable leaf is the part before the '#'
* (identifiers cannot contain one). */
size_t call_leaf_len = strlen(call_leaf);
if (strncmp(resolved_leaf, call_leaf, call_leaf_len) == 0 &&
resolved_leaf[call_leaf_len] == '#') {
return true;
}
}

/* Destructors intentionally join by their exact delete-expression
Expand Down
17 changes: 16 additions & 1 deletion src/pipeline/registry.c
Original file line number Diff line number Diff line change
Expand Up @@ -590,8 +590,23 @@ void cbm_registry_add(cbm_registry_t *r, const char *name, const char *qualified
const char *owned_qn = cbm_ht_get_key(r->exact, qualified_name);

/* Index by simple name.
* No array dedup needed: exact-map check above guarantees uniqueness. */
* No array dedup needed: exact-map check above guarantees uniqueness.
* #495/#1911: a build-constrained twin QN carries a `#τ` suffix
* (`pkg.Flush#linux`, Rust cfg / Go //go:build); the SIMPLE name is the
* part before it — identifiers cannot contain '#' — or callers looking up
* `Flush` would never find the constrained definition. */
const char *simple = simple_name(qualified_name);
char simple_buf[CBM_SZ_256];
const char *hash = strchr(simple, '#');
if (hash) {
size_t n = (size_t)(hash - simple);
if (n == 0 || n >= sizeof(simple_buf)) {
return; /* degenerate `#`-leaf — nothing callable to index */
}
memcpy(simple_buf, simple, n);
simple_buf[n] = '\0';
simple = simple_buf;
}
qn_array_t *arr = cbm_ht_get(r->by_name, simple);
if (!arr) {
arr = calloc(CBM_ALLOC_ONE, sizeof(qn_array_t));
Expand Down
71 changes: 71 additions & 0 deletions tests/test_extraction.c
Original file line number Diff line number Diff line change
Expand Up @@ -3015,6 +3015,76 @@ TEST(go_imports) {
PASS();
}

/* #1911: Go build-constrained twin files (//go:build lines, GOOS/GOARCH
* filename suffixes) legally define the same symbols; fold the per-file
* constraint τ into func/method QNs (#495's Rust cfg `#`-suffix move) so the
* twins stop colliding in the graph upsert. Types/vars stay plain — the
* callable surface is what the call graph needs, and plain type QNs keep
* parent_class/DEFINES_METHOD joins working. */
static int def_qn_has_suffix(CBMFileResult *r, const char *name, const char *suffix) {
for (int i = 0; i < r->defs.count; i++) {
const CBMDefinition *d = &r->defs.items[i];
if (!d->name || strcmp(d->name, name) != 0 || !d->qualified_name) {
continue;
}
size_t qlen = strlen(d->qualified_name);
size_t slen = strlen(suffix);
if (qlen >= slen && strcmp(d->qualified_name + (qlen - slen), suffix) == 0) {
return 1;
}
}
return 0;
}

TEST(extract_go_buildtag_tau_in_func_qns) {
/* //go:build expression wins and is compacted into the suffix. */
CBMFileResult *r = extract("//go:build linux && amd64\n\n"
"package mirror\n\n"
"type porter struct{ n int }\n\n"
"func MirrorConfig(path string) string { return path }\n\n"
"func (p *porter) Flush() {}\n",
CBM_LANG_GO, "t", "mirror_impl.go");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
ASSERT_TRUE(def_qn_has_suffix(r, "MirrorConfig", "#linux&&amd64"));
ASSERT_TRUE(def_qn_has_suffix(r, "Flush", "#linux&&amd64"));
/* The type stays plain so DEFINES_METHOD / parent_class joins keep working. */
ASSERT_FALSE(def_qn_has_suffix(r, "porter", "#linux&&amd64"));
cbm_free_result(r);

/* GOOS/GOARCH filename suffix when no //go:build line is present. */
r = extract("package mirror\n\n"
"func MirrorConfig(path string) string { return \"\" }\n",
CBM_LANG_GO, "t", "mirror_windows_amd64.go");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
ASSERT_TRUE(def_qn_has_suffix(r, "MirrorConfig", "#windows_amd64"));
cbm_free_result(r);

/* _test suffix is stripped before the GOOS check. */
r = extract("package mirror\n\n"
"func helperLinux() int { return 1 }\n",
CBM_LANG_GO, "t", "mirror_linux_test.go");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
ASSERT_TRUE(def_qn_has_suffix(r, "helperLinux", "#linux"));
cbm_free_result(r);

/* Unconstrained file → no τ anywhere. */
r = extract("package mirror\n\n"
"func Plain() int { return 1 }\n",
CBM_LANG_GO, "t", "mirror.go");
ASSERT_NOT_NULL(r);
ASSERT_FALSE(r->has_error);
for (int i = 0; i < r->defs.count; i++) {
if (r->defs.items[i].qualified_name) {
ASSERT_TRUE(strchr(r->defs.items[i].qualified_name, '#') == NULL);
}
}
cbm_free_result(r);
PASS();
}

TEST(java_imports) {
CBMFileResult *r = extract(
"import java.util.List;\nimport java.util.ArrayList;\nimport static java.lang.Math.PI;\n"
Expand Down Expand Up @@ -6699,6 +6769,7 @@ SUITE(extraction) {
RUN_TEST(python_imports);
RUN_TEST(js_imports);
RUN_TEST(go_imports);
RUN_TEST(extract_go_buildtag_tau_in_func_qns);
RUN_TEST(java_imports);
RUN_TEST(rust_imports);
RUN_TEST(c_imports);
Expand Down
Loading
Loading