fix(graph): precise Struct label for Rust/Swift/D/Go structs (Option B)

Struct definitions were labelled 'Class', conflating a distinct kind. Emit
the precise 'Struct' label for Rust (struct_item), Swift/D (struct_declaration)
and Go (type_spec struct_type); C/C++/Obj-C struct_specifier stays 'Class'.

To avoid regressing type resolution, a struct must still behave as a type
everywhere a class does. Added a centralized predicate cbm_label_is_type_like()
(Class/Struct/Interface/Enum/Type/Trait) and routed every type-like consumer
through it: registry seeding (pass_definitions/pass_parallel/pipeline_incremental
kept in sync), class-resolution (pass_semantic/pass_parallel resolve_as_class),
Go interface satisfaction (collects Struct nodes), the cross-file LSP label map,
and the Go/Rust LSP type registrars. Class-only sites (protobuf services, K8s
kinds, the C-family LSPs) are left unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
Martin Vogel
2026-06-27 13:54:51 +02:00
parent d97dee0239
commit 554b171cf7
13 changed files with 130 additions and 55 deletions
+13
View File
@@ -598,4 +598,17 @@ void cbm_extract_unified(CBMExtractCtx *ctx);
// K8s / Kustomize semantic extractor (called when language is CBM_LANG_K8S or CBM_LANG_KUSTOMIZE).
void cbm_extract_k8s(CBMExtractCtx *ctx);
// --- Label predicates ---
// True when `label` names a TYPE-LIKE container definition — a node that can own
// methods/fields, be a base/embedded type, satisfy/declare an interface, and be a
// target of name→type resolution. The canonical set is:
// Class, Struct, Interface, Enum, Type, Trait.
// Single source of truth for every type-resolution / registry-seeding /
// INHERITS·IMPLEMENTS / LSP-type-registrar consumer, so adding a new type-like
// label (e.g. "Struct" for Rust/Go/Swift/D structs) updates them all at once
// instead of scattering `|| strcmp(label,"Struct")==0` across the tree.
// `label` may be NULL (returns false). Defined in helpers.c.
bool cbm_label_is_type_like(const char *label);
#endif // CBM_H
+21 -4
View File
@@ -3502,8 +3502,8 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
// Sway/WGSL: label struct defs as "Struct" and Sway `abi` blocks as
// "Interface". Scoped to these grammar-only languages so established
// struct-as-"Class" labeling (Rust/C++/Go/Cap'n Proto …) and the
// downstream type/IMPLEMENTS resolvers that depend on it are unaffected.
// struct-as-"Class" labeling (C++/Cap'n Proto …) and the downstream
// type/IMPLEMENTS resolvers that depend on it are unaffected.
if (ctx->language == CBM_LANG_SWAY || ctx->language == CBM_LANG_WGSL) {
if (strcmp(kind, "struct_item") == 0 || strcmp(kind, "struct_declaration") == 0) {
label = "Struct";
@@ -3511,6 +3511,20 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
label = "Interface";
}
}
// Rust/Swift/D: a struct is a distinct kind from a class — emit the precise
// "Struct" label rather than collapsing it to "Class". Scoped to these three
// grammar/LSP languages whose struct node is `struct_item` (Rust) or
// `struct_declaration` (Swift, D). C/C++/Obj-C keep `struct_specifier` →
// "Class" (a C++ struct is class-like). "Struct" is a type-like container:
// every type-resolution / registry / IMPLEMENTS / LSP-registrar consumer
// routes through cbm_label_is_type_like(), so a struct still resolves as a
// type for its methods, fields, inheritance and impls.
if (ctx->language == CBM_LANG_RUST || ctx->language == CBM_LANG_SWIFT ||
ctx->language == CBM_LANG_DLANG) {
if (strcmp(kind, "struct_item") == 0 || strcmp(kind, "struct_declaration") == 0) {
label = "Struct";
}
}
// F#: a `type_definition` that has a primary constructor (`type Foo(...) =`)
// or an `inherit` clause is an OOP class, not a plain type alias. Label it
// "Class" so it is registered as a resolvable inheritance target (the graph
@@ -3525,7 +3539,10 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
}
}
// Go type_spec: check inner type for interface/struct
// Go type_spec: check inner type for interface/struct. A Go `type T struct
// {...}` is a struct → emit the precise "Struct" label (a type-like container;
// its methods/fields/embedding resolve through cbm_label_is_type_like(), and
// cbm_pipeline_implements_go() collects Struct nodes too).
if (strcmp(kind, "type_spec") == 0) {
TSNode type_inner = ts_node_child_by_field_name(node, TS_FIELD("type"));
if (!ts_node_is_null(type_inner)) {
@@ -3533,7 +3550,7 @@ static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec
if (strcmp(inner_kind, "interface_type") == 0) {
label = "Interface";
} else if (strcmp(inner_kind, "struct_type") == 0) {
label = "Class";
label = "Struct";
}
}
}
+12
View File
@@ -155,6 +155,18 @@ static const char *puppet_keywords[] = {
"and", "or", "in", "node", "class", "define", "inherits", "default",
"return", NULL};
// True when `label` names a type-like container definition (see cbm.h). Single
// source of truth for the type-resolution / registry / IMPLEMENTS / LSP-type
// consumers — adding a label here updates them all.
bool cbm_label_is_type_like(const char *label) {
if (!label) {
return false;
}
return strcmp(label, "Class") == 0 || strcmp(label, "Struct") == 0 ||
strcmp(label, "Interface") == 0 || strcmp(label, "Enum") == 0 ||
strcmp(label, "Type") == 0 || strcmp(label, "Trait") == 0;
}
bool cbm_is_keyword(const char *name, CBMLanguage lang) {
if (!name || !name[0]) {
return true;
+10 -8
View File
@@ -1713,9 +1713,10 @@ void cbm_run_go_lsp(CBMArena* arena, CBMFileResult* result,
CBMDefinition* d = &result->defs.items[i];
if (!d->qualified_name || !d->name) continue;
// Register Class/Type nodes
if (d->label && (strcmp(d->label, "Class") == 0 || strcmp(d->label, "Type") == 0 ||
strcmp(d->label, "Interface") == 0)) {
// 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;
@@ -2534,9 +2535,9 @@ void cbm_run_go_lsp_cross(
const char* def_mod = d->def_module_qn ? d->def_module_qn : module_qn;
// Type/Interface/Class
if (strcmp(d->label, "Type") == 0 || strcmp(d->label, "Class") == 0 ||
strcmp(d->label, "Interface") == 0) {
// 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
@@ -2787,8 +2788,9 @@ CBMTypeRegistry* cbm_go_build_cross_registry(
* fall back to — this registry is project-wide, not per-file. */
const char* def_mod = d->def_module_qn ? d->def_module_qn : "";
if (strcmp(d->label, "Type") == 0 || strcmp(d->label, "Class") == 0 ||
strcmp(d->label, "Interface") == 0) {
// 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 */
+11 -5
View File
@@ -4530,8 +4530,10 @@ static void rust_build_registry_from_defs(CBMArena *arena, CBMTypeRegistry *reg,
if (!d->qualified_name || !d->name)
continue;
if (d->label && (strcmp(d->label, "Class") == 0 || strcmp(d->label, "Type") == 0 ||
strcmp(d->label, "Interface") == 0 || strcmp(d->label, "Trait") == 0)) {
// Every type-like container (Class/Struct/Type/Interface/Trait/Enum).
// Struct included so a Rust `struct Foo` (now labelled "Struct") registers
// as a type and its `impl Foo` methods/fields resolve.
if (cbm_label_is_type_like(d->label)) {
CBMRegisteredType rt;
memset(&rt, 0, sizeof(rt));
rt.qualified_name = d->qualified_name;
@@ -4839,7 +4841,10 @@ static void rust_build_registry_from_defs(CBMArena *arena, CBMTypeRegistry *reg,
CBMDefinition *d = &result->defs.items[i];
if (!d->qualified_name || !d->name)
continue;
if (!d->label || (strcmp(d->label, "Class") != 0 && strcmp(d->label, "Type") != 0))
/* `#[derive(...)]` rides on type-like defs — most often a struct or
* enum (now labelled "Struct"/"Enum"), also type aliases. Accept the
* whole type-like set so a derive on a struct is not dropped. */
if (!cbm_label_is_type_like(d->label))
continue;
if (!d->decorators)
continue;
@@ -5151,8 +5156,9 @@ void cbm_run_rust_lsp_cross(CBMArena *arena, const char *source, int source_len,
continue;
const char *def_mod = d->def_module_qn ? d->def_module_qn : module_qn;
if (strcmp(d->label, "Type") == 0 || strcmp(d->label, "Class") == 0 ||
strcmp(d->label, "Interface") == 0 || strcmp(d->label, "Trait") == 0) {
// Every type-like container (Type/Class/Struct/Interface/Trait/Enum).
// Struct included so Rust structs (now labelled "Struct") register here.
if (cbm_label_is_type_like(d->label)) {
CBMRegisteredType rt;
memset(&rt, 0, sizeof(rt));
rt.qualified_name = cbm_arena_strdup(arena, d->qualified_name);
+4 -2
View File
@@ -105,7 +105,7 @@ static int collect_config_entries(const cbm_gbuf_node_t *const *vars, int var_co
return n;
}
/* Collect code nodes (Function/Variable/Class) not from config files. */
/* Collect code nodes (Function/Variable/Class/Struct) not from config files. */
typedef struct {
int64_t node_id;
char normalized[CBM_SZ_256];
@@ -113,7 +113,9 @@ typedef struct {
static int collect_code_entries(cbm_gbuf_t *gb, code_entry_t *out, int max_out) {
int n = 0;
static const char *labels[] = {"Function", "Variable", "Class", NULL};
/* "Struct" alongside "Class": a config key may name a Go/Rust/Swift/D struct
* type, which is now labelled "Struct" — keep it linkable. */
static const char *labels[] = {"Function", "Variable", "Class", "Struct", NULL};
for (int li = 0; labels[li] && n < max_out; li++) {
const cbm_gbuf_node_t **nodes = NULL;
+10 -7
View File
@@ -295,15 +295,18 @@ static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const
int64_t node_id = cbm_gbuf_upsert_node(
ctx->gbuf, def->label ? def->label : "Function", def->name, def->qualified_name,
def->file_path ? def->file_path : rel, (int)def->start_line, (int)def->end_line, props);
/* Register callable symbols + Interface. Interface must be in the registry
* so C#/Java `class Foo : IBar` / `class Foo implements IBar` can resolve
* `IBar` to an INHERITS edge target during the enrichment phase.
* Variable/Field defs are also registered so pass_usages.c can resolve
* READS/WRITES accesses (rw->var_name) to a Variable/Field node QN. */
/* Register callable symbols + every type-like container (Class/Struct/
* Interface/Enum/Type/Trait). Type-like defs must be in the registry so
* `class Foo : IBar` (INHERITS), `impl Trait for S` (IMPLEMENTS), and method/
* field resolution can reach them — Struct included so Rust/Go/Swift/D structs
* resolve as type targets just as a Class did. Variable/Field defs are also
* registered so pass_usages.c can resolve READS/WRITES accesses (rw->var_name)
* to a Variable/Field node QN.
* KEEP IN SYNC with pass_parallel.c and pipeline_incremental.c's seed sets. */
if (node_id > 0 && def->label &&
(strcmp(def->label, "Function") == 0 || strcmp(def->label, "Method") == 0 ||
strcmp(def->label, "Class") == 0 || strcmp(def->label, "Interface") == 0 ||
strcmp(def->label, "Variable") == 0 || strcmp(def->label, "Field") == 0)) {
cbm_label_is_type_like(def->label) || strcmp(def->label, "Variable") == 0 ||
strcmp(def->label, "Field") == 0)) {
cbm_registry_add(ctx->registry, def->name, def->qualified_name, def->label);
}
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__");
+5 -2
View File
@@ -292,8 +292,11 @@ static void free_tagged_nodes(tagged_node_t *nodes, int count) {
/* Phase 1: Collect decorated nodes and count word frequency. */
static int collect_decorated_nodes(cbm_gbuf_t *gbuf, tagged_node_t **out_nodes,
CBMHashTable *word_counts) {
static const char *labels[] = {"Function", "Method", "Class"};
static const int nlabels = 3;
/* "Struct" alongside "Class" so Go/Rust/Swift/D struct names keep
* contributing to / receiving auto-tags as they did when structs were
* labelled "Class". */
static const char *labels[] = {"Function", "Method", "Class", "Struct"};
static const int nlabels = 4;
tagged_node_t *nodes = NULL;
int node_count = 0;
int node_cap = 0;
+7 -7
View File
@@ -82,16 +82,16 @@ static char *pxc_read_file(const char *path, int *out_len) {
return buf;
}
/* Map a CBMDefinition.label to a CBMLSPDef.label. Per-language LSP
* registrars only care about Class/Interface/Trait/Enum/Type/Protocol/
* Function/Method — variables, modules, decorators, etc. are skipped. */
/* Map a CBMDefinition.label to a CBMLSPDef.label. Per-language LSP registrars
* only care about type-like containers (Class/Struct/Interface/Trait/Enum/Type)
* plus Protocol/Function/Method — variables, modules, decorators, etc. are
* skipped. Struct passes through so Rust/Go struct type-registration via the
* cross-file LSP path is not dropped. */
static const char *pxc_map_label(const char *label) {
if (!label)
return NULL;
if (strcmp(label, "Class") == 0 || strcmp(label, "Interface") == 0 ||
strcmp(label, "Trait") == 0 || strcmp(label, "Enum") == 0 || strcmp(label, "Type") == 0 ||
strcmp(label, "Protocol") == 0 || strcmp(label, "Function") == 0 ||
strcmp(label, "Method") == 0) {
if (cbm_label_is_type_like(label) || strcmp(label, "Protocol") == 0 ||
strcmp(label, "Function") == 0 || strcmp(label, "Method") == 0) {
return label;
}
return NULL;
+12 -9
View File
@@ -410,12 +410,12 @@ static const char *resolve_as_class(const cbm_registry_t *reg, const char *name,
if (!res.qualified_name || res.qualified_name[0] == '\0') {
return NULL;
}
/* Accept any type-like container (Class/Struct/Interface/Enum/Type/Trait):
* base classes, Rust `impl Trait for S` struct receivers, and Go struct
* embedding all resolve through here. Struct included so the struct receiver
* of an IMPLEMENTS edge is not dropped. */
const char *label = cbm_registry_label_of(reg, res.qualified_name);
if (!label) {
return NULL;
}
if (strcmp(label, "Class") != 0 && strcmp(label, "Interface") != 0 &&
strcmp(label, "Type") != 0 && strcmp(label, "Enum") != 0) {
if (!cbm_label_is_type_like(label)) {
return NULL;
}
return res.qualified_name;
@@ -822,11 +822,14 @@ static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *d
if (!def->name || !def->qualified_name || !def->label) {
return 0;
}
/* Register callable symbols + Interface — see pass_definitions.c for rationale.
* Variable/Field defs are registered too so READS/WRITES can resolve. */
/* Register callable symbols + every type-like container (Class/Struct/
* Interface/Enum/Type/Trait) — see pass_definitions.c for rationale. Struct
* included so Rust/Go/Swift/D structs resolve as type targets. Variable/Field
* defs are registered too so READS/WRITES can resolve.
* KEEP IN SYNC with pass_definitions.c and pipeline_incremental.c. */
if (strcmp(def->label, "Function") == 0 || strcmp(def->label, "Method") == 0 ||
strcmp(def->label, "Class") == 0 || strcmp(def->label, "Interface") == 0 ||
strcmp(def->label, "Variable") == 0 || strcmp(def->label, "Field") == 0) {
cbm_label_is_type_like(def->label) || strcmp(def->label, "Variable") == 0 ||
strcmp(def->label, "Field") == 0) {
cbm_registry_add(ctx->registry, def->name, def->qualified_name, def->label);
(*reg_entries)++;
}
+17 -9
View File
@@ -167,13 +167,12 @@ static const char *resolve_as_class(const cbm_registry_t *reg, const char *name,
return NULL;
}
/* Verify it's a Class, Interface, or Type */
/* Verify it's a type-like container (Class/Struct/Interface/Enum/Type/Trait):
* a base/embedded type, impl receiver, or inheritance target must resolve to
* one of these. Struct included so Rust/Go/Swift/D `impl Trait for S` and Go
* struct embedding resolve. */
const char *label = cbm_registry_label_of(reg, res.qualified_name);
if (!label) {
return NULL;
}
if (strcmp(label, "Class") != 0 && strcmp(label, "Interface") != 0 &&
strcmp(label, "Type") != 0 && strcmp(label, "Enum") != 0) {
if (!cbm_label_is_type_like(label)) {
return NULL;
}
return res.qualified_name;
@@ -301,11 +300,16 @@ int cbm_pipeline_implements_go(cbm_pipeline_ctx_t *ctx) {
return 0;
}
/* Find all Class nodes */
/* Find candidate concrete types. In Go the type that satisfies an interface
* is a struct (now labelled "Struct") or a named type (labelled "Class"); both
* sets are checked. Each call returns a borrowed internal array (no free). */
const cbm_gbuf_node_t **classes = NULL;
int class_count = 0;
cbm_gbuf_find_by_label(ctx->gbuf, "Class", &classes, &class_count);
if (class_count == 0) {
const cbm_gbuf_node_t **structs = NULL;
int struct_count = 0;
cbm_gbuf_find_by_label(ctx->gbuf, "Struct", &structs, &struct_count);
if (class_count == 0 && struct_count == 0) {
return 0;
}
@@ -337,7 +341,11 @@ int cbm_pipeline_implements_go(cbm_pipeline_ctx_t *ctx) {
continue;
}
/* Check each Class node for method-set satisfaction */
/* Check each concrete-type node (Struct + Class) for method-set
* satisfaction. */
for (int c = 0; c < struct_count; c++) {
edge_count += check_go_class_implements(ctx, structs[c], iface, imethods, im_count);
}
for (int c = 0; c < class_count; c++) {
edge_count += check_go_class_implements(ctx, classes[c], iface, imethods, im_count);
}
+6 -2
View File
@@ -509,9 +509,13 @@ static void persist_hashes(cbm_store_t *store, const char *project, cbm_file_inf
* resolve to the same-named Module node instead of the Class node. Only
* callable / declared symbols belong in the registry. */
static bool incr_label_is_registry_symbol(const char *label) {
/* Mirror pass_definitions.c / pass_parallel.c registry seeding EXACTLY:
* callables + every type-like container (Class/Struct/Interface/Enum/Type/
* Trait) + Variable/Field. Struct included so an incremental re-resolve seeds
* the same struct type nodes a full reindex would. */
return label && (strcmp(label, "Function") == 0 || strcmp(label, "Method") == 0 ||
strcmp(label, "Class") == 0 || strcmp(label, "Interface") == 0 ||
strcmp(label, "Variable") == 0 || strcmp(label, "Field") == 0);
cbm_label_is_type_like(label) || strcmp(label, "Variable") == 0 ||
strcmp(label, "Field") == 0);
}
/* Callback for cbm_gbuf_foreach_node: seed the registry with the existing
+2
View File
@@ -85,6 +85,8 @@ static float size_for_label(const char *label) {
return 8.0f;
if (strcmp(label, "Class") == 0)
return 6.0f;
if (strcmp(label, "Struct") == 0)
return 6.0f;
if (strcmp(label, "Interface") == 0)
return 6.0f;
if (strcmp(label, "Function") == 0)