From 554b171cf7bd978adece34ab8af2a5cd6d16c66c Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Sat, 27 Jun 2026 13:54:51 +0200 Subject: [PATCH] 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 --- internal/cbm/cbm.h | 13 +++++++++++++ internal/cbm/extract_defs.c | 25 +++++++++++++++++++++---- internal/cbm/helpers.c | 12 ++++++++++++ internal/cbm/lsp/go_lsp.c | 18 ++++++++++-------- internal/cbm/lsp/rust_lsp.c | 16 +++++++++++----- src/pipeline/pass_configlink.c | 6 ++++-- src/pipeline/pass_definitions.c | 17 ++++++++++------- src/pipeline/pass_enrichment.c | 7 +++++-- src/pipeline/pass_lsp_cross.c | 14 +++++++------- src/pipeline/pass_parallel.c | 21 ++++++++++++--------- src/pipeline/pass_semantic.c | 26 +++++++++++++++++--------- src/pipeline/pipeline_incremental.c | 8 ++++++-- src/ui/layout3d.c | 2 ++ 13 files changed, 130 insertions(+), 55 deletions(-) diff --git a/internal/cbm/cbm.h b/internal/cbm/cbm.h index 39ddb96b0..b40305af9 100644 --- a/internal/cbm/cbm.h +++ b/internal/cbm/cbm.h @@ -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 diff --git a/internal/cbm/extract_defs.c b/internal/cbm/extract_defs.c index dfca3b4c4..b6f101841 100644 --- a/internal/cbm/extract_defs.c +++ b/internal/cbm/extract_defs.c @@ -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"; } } } diff --git a/internal/cbm/helpers.c b/internal/cbm/helpers.c index 1d89f4485..2235f8371 100644 --- a/internal/cbm/helpers.c +++ b/internal/cbm/helpers.c @@ -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; diff --git a/internal/cbm/lsp/go_lsp.c b/internal/cbm/lsp/go_lsp.c index c5ca2513a..798dcf0cc 100644 --- a/internal/cbm/lsp/go_lsp.c +++ b/internal/cbm/lsp/go_lsp.c @@ -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 */ diff --git a/internal/cbm/lsp/rust_lsp.c b/internal/cbm/lsp/rust_lsp.c index 4ef4bdf7b..03da7a132 100644 --- a/internal/cbm/lsp/rust_lsp.c +++ b/internal/cbm/lsp/rust_lsp.c @@ -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); diff --git a/src/pipeline/pass_configlink.c b/src/pipeline/pass_configlink.c index af5a260ac..341847c40 100644 --- a/src/pipeline/pass_configlink.c +++ b/src/pipeline/pass_configlink.c @@ -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; diff --git a/src/pipeline/pass_definitions.c b/src/pipeline/pass_definitions.c index 676f1b169..f0816068c 100644 --- a/src/pipeline/pass_definitions.c +++ b/src/pipeline/pass_definitions.c @@ -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__"); diff --git a/src/pipeline/pass_enrichment.c b/src/pipeline/pass_enrichment.c index d842e507c..bf3e4210a 100644 --- a/src/pipeline/pass_enrichment.c +++ b/src/pipeline/pass_enrichment.c @@ -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; diff --git a/src/pipeline/pass_lsp_cross.c b/src/pipeline/pass_lsp_cross.c index a279956d6..d86c819d3 100644 --- a/src/pipeline/pass_lsp_cross.c +++ b/src/pipeline/pass_lsp_cross.c @@ -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; diff --git a/src/pipeline/pass_parallel.c b/src/pipeline/pass_parallel.c index 61d9faac6..ade59e95d 100644 --- a/src/pipeline/pass_parallel.c +++ b/src/pipeline/pass_parallel.c @@ -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)++; } diff --git a/src/pipeline/pass_semantic.c b/src/pipeline/pass_semantic.c index a2a5493b0..affeb9561 100644 --- a/src/pipeline/pass_semantic.c +++ b/src/pipeline/pass_semantic.c @@ -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); } diff --git a/src/pipeline/pipeline_incremental.c b/src/pipeline/pipeline_incremental.c index a1cc44820..e5d1b4c9f 100644 --- a/src/pipeline/pipeline_incremental.c +++ b/src/pipeline/pipeline_incremental.c @@ -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 diff --git a/src/ui/layout3d.c b/src/ui/layout3d.c index 5758a3334..a0c93ba35 100644 --- a/src/ui/layout3d.c +++ b/src/ui/layout3d.c @@ -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)