diff --git a/gitnexus/src/core/ingestion/languages/rust/query.ts b/gitnexus/src/core/ingestion/languages/rust/query.ts index f2ef4f4a0..ba20a436c 100644 --- a/gitnexus/src/core/ingestion/languages/rust/query.ts +++ b/gitnexus/src/core/ingestion/languages/rust/query.ts @@ -36,14 +36,14 @@ const RUST_SCOPE_QUERY = ` type_parameters: (type_parameters)? @declaration.type-parameters) @declaration.enum ;; Declarations — union -;; Deliberately tagged @declaration.struct (→ Struct label), NOT a -;; @declaration.union: every registry-primary resolution gate — -;; isLinkableLabel (node-lookup.ts), CALLABLE_OR_TYPE_LIKE -;; (finalize-algorithm.ts), ClassLikeNodeLabel (class-types.ts) — includes -;; Struct but EXCLUDES Union, so a Union-labeled node would be an -;; unresolvable orphan. A Rust union is a type whose literal is a real -;; constructor, so Struct is both the resolvable and the semantically -;; honest label here. #1934 F71. +;; Tagged @declaration.struct (→ Struct label), NOT @declaration.union. +;; Historically forced: the registry-primary gates (isLinkableLabel, +;; CALLABLE_OR_TYPE_LIKE, ClassLikeNodeLabel) excluded Union, so a +;; Union-labeled node was an unresolvable orphan (#1934 F71). Zig support +;; widened all three for its union(enum) containers, so the label is now +;; resolvable — Struct is kept here on the semantic argument alone (a Rust +;; union is a type whose literal is a real constructor) and to leave the +;; graph node ids of existing Rust indexes unchanged. (union_item name: (type_identifier) @declaration.name type_parameters: (type_parameters)? @declaration.type-parameters) @declaration.struct diff --git a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts index 5099508e3..fb42f954b 100644 --- a/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts +++ b/gitnexus/src/core/ingestion/scope-resolution/graph-bridge/node-lookup.ts @@ -273,6 +273,15 @@ export const LINKABLE_LABELS: ReadonlySet = new Set([ // are unreachable and label-agnostic fallback can alias it to a same-named // Constructor or Method (#2801). 'Record', + // Union is linkable because Zig wires `union` / `union(enum)` as a member + // container: the definition phase emits HAS_METHOD / HAS_PROPERTY edges FROM + // the Union node (`union_declaration` in MEMBER_OWNER_NODE_TYPES) and the + // scope side dispatches methods on union receivers (`main → isEnergy` in + // test/integration/resolvers/zig.test.ts). Without this entry the schema + // never declares a `FROM Union` pair and `analyze` aborts on the first Zig + // repo that declares a union (reproduced on the zig-basic fixture itself). + // Also lets `Tag{ .energy = 5 }` constructor references bridge to the node. + 'Union', // Trait nodes are linkable so MRO builders can bridge PHP/Rust trait // defs between scope-resolution DefIds and the graph's node ids. // IMPLEMENTS edges from classes to traits are otherwise invisible to @@ -288,8 +297,8 @@ export const LINKABLE_LABELS: ReadonlySet = new Set([ // // Covers every language that spells an alias this way — TypeScript, Kotlin, // Dart and Rust all emit `@declaration.type_alias`. The remaining - // `CLASS_KINDS` entries (Typedef, Record, Union, Delegate, Annotation, - // Template) plausibly have the same gap, but nothing exercises them today + // `CLASS_KINDS` entries (Typedef, Delegate, Annotation, Template) plausibly + // have the same gap, but nothing exercises them today // and adding labels no test covers is how this list drifts out of sync with // what it claims. 'TypeAlias', diff --git a/gitnexus/src/core/lbug/schema.ts b/gitnexus/src/core/lbug/schema.ts index 11471f2e6..5722d0c75 100644 --- a/gitnexus/src/core/lbug/schema.ts +++ b/gitnexus/src/core/lbug/schema.ts @@ -420,14 +420,14 @@ const ATTACHMENT_TARGET_LABELS: readonly NodeTableName[] = [ ]; /** - * The 69 pairs NEITHER rule above generates — everything left after the two + * The 66 pairs NEITHER rule above generates — everything left after the two * cross products are subtracted. Carried by CONTAINMENT, inheritance, imports * and DI: a container label crossed with a contained label. No predicate * describes that surface (any container can hold any definition). * * What survives here is characteristic, not arbitrary. Almost all of it is a * TARGET no rule reaches — `CodeElement`, `Impl`, `Namespace`, `Template`, - * `Typedef`, `Union`, `Static`, `Section`, `Folder` are in neither + * `Typedef`, `Static`, `Section`, `Folder` are in neither * `SCOPE_BRIDGE_TARGET_LABELS` nor {@link ATTACHMENT_TARGET_LABELS} — plus the * `Impl|*` and `Template|*` member rows (Rust `impl`/`trait` bodies, C++ * templates), the two `Route|Process` / `Tool|Process` entry points whose @@ -455,7 +455,7 @@ const ATTACHMENT_TARGET_LABELS: readonly NodeTableName[] = [ * from it and so moves with any change to it. * * Folding this remainder into a third cross product - * (`DEFINITION_ANCHOR_LABELS × {CodeElement, Section, Typedef, Union, + * (`DEFINITION_ANCHOR_LABELS × {CodeElement, Section, Typedef, * Namespace, Impl, TypeAlias, Static, Template}`) would take the table to 641 * pairs and leave only ~29 lines here. `bench/schema-pairs` measures 641 at * 1.22–1.43× on the historical reference box; current production's 461 pairs @@ -472,7 +472,6 @@ const ATTACHMENT_TARGET_LABELS: readonly NodeTableName[] = [ export const STRUCTURAL_PAIR_DDL = ` FROM File TO Folder, FROM File TO CodeElement, FROM File TO \`Typedef\`, - FROM File TO \`Union\`, FROM File TO \`Namespace\`, FROM File TO \`Impl\`, FROM File TO \`Static\`, @@ -484,11 +483,9 @@ export const STRUCTURAL_PAIR_DDL = ` FROM File TO Folder, FROM Function TO \`Namespace\`, FROM Function TO \`Impl\`, FROM Function TO \`Typedef\`, - FROM Function TO \`Union\`, FROM Function TO CodeElement, FROM Class TO \`Template\`, FROM Class TO \`Impl\`, - FROM Class TO \`Union\`, FROM Class TO \`Namespace\`, FROM Class TO \`Typedef\`, FROM Class TO CodeElement, diff --git a/gitnexus/test/fixtures/lang-resolution/zig-idioms/build.zig b/gitnexus/test/fixtures/lang-resolution/zig-idioms/build.zig new file mode 100644 index 000000000..e8cd1de4a --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/zig-idioms/build.zig @@ -0,0 +1,7 @@ +const std = @import("std"); +pub fn build(b: *std.Build) void { + const geo = b.dependency("geo", .{}); + const exe = b.addExecutable(.{ .name = "idioms", .root_source_file = b.path("src/main.zig") }); + exe.root_module.addImport("geo", geo.module("geo")); + b.installArtifact(exe); +} diff --git a/gitnexus/test/fixtures/lang-resolution/zig-idioms/build.zig.zon b/gitnexus/test/fixtures/lang-resolution/zig-idioms/build.zig.zon new file mode 100644 index 000000000..893a67db8 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/zig-idioms/build.zig.zon @@ -0,0 +1,17 @@ +.{ + .name = .idioms, + .version = "0.1.0", + .fingerprint = 0x9a2f1c4e7b3d5061, + .minimum_zig_version = "0.14.0", + .dependencies = .{ + // Library laid out by `zig init` ≥ 0.12: src/root.zig, declared in its build.zig. + .geo = .{ .path = "libs/geo" }, + // Older convention: src/.zig, no addModule in build.zig. + .oldlib = .{ .path = "libs/oldlib" }, + .remote = .{ + .url = "https://example.invalid/remote.tar.gz", + .hash = "1220deadbeef", + }, + }, + .paths = .{ "build.zig", "build.zig.zon", "src" }, +} diff --git a/gitnexus/test/fixtures/lang-resolution/zig-idioms/libs/geo/build.zig b/gitnexus/test/fixtures/lang-resolution/zig-idioms/libs/geo/build.zig new file mode 100644 index 000000000..4bb312416 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/zig-idioms/libs/geo/build.zig @@ -0,0 +1,4 @@ +const std = @import("std"); +pub fn build(b: *std.Build) void { + _ = b.addModule("geo", .{ .root_source_file = b.path("src/root.zig") }); +} diff --git a/gitnexus/test/fixtures/lang-resolution/zig-idioms/libs/geo/src/root.zig b/gitnexus/test/fixtures/lang-resolution/zig-idioms/libs/geo/src/root.zig new file mode 100644 index 000000000..c113a647b --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/zig-idioms/libs/geo/src/root.zig @@ -0,0 +1,9 @@ +pub fn area(w: u32, h: u32) u32 { + return w * h; +} +pub const Point = struct { + x: i32 = 0, + pub fn shift(self: *Point, dx: i32) void { + self.x += dx; + } +}; diff --git a/gitnexus/test/fixtures/lang-resolution/zig-idioms/libs/oldlib/src/oldlib.zig b/gitnexus/test/fixtures/lang-resolution/zig-idioms/libs/oldlib/src/oldlib.zig new file mode 100644 index 000000000..bbec66c9a --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/zig-idioms/libs/oldlib/src/oldlib.zig @@ -0,0 +1 @@ +pub fn legacy() void {} diff --git a/gitnexus/test/fixtures/lang-resolution/zig-idioms/src/counter.zig b/gitnexus/test/fixtures/lang-resolution/zig-idioms/src/counter.zig new file mode 100644 index 000000000..557f01b8c --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/zig-idioms/src/counter.zig @@ -0,0 +1,48 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +pub const VERSION: []const u8 = "1.0"; +pub var global_count: u32 = 0; +pub const Err = error{ Oops, Bad }; + +pub const Counter = struct { + const Self = @This(); + count: u32 = 0, + + pub fn init() Self { + return .{}; + } + pub fn incr(self: *Self) void { + self.count += 1; + } + pub fn get(self: *const @This()) u32 { + return self.count; + } + pub fn twice(self: *Self) void { + self.incr(); + self.incr(); + } +}; + +/// Generic type constructor — Zig's only spelling of a generic type. +pub fn Stack(comptime T: type) type { + return struct { + const Self = @This(); + items: []T = &.{}, + + pub fn init() Self { + return .{}; + } + pub fn push(self: *Self, v: T) void { + _ = self; + _ = v; + } + pub fn top(self: Self) ?T { + _ = self; + return null; + } + pub fn clear(self: *Self) void { + self.items = &.{}; + } + }; +} diff --git a/gitnexus/test/fixtures/lang-resolution/zig-idioms/src/main.zig b/gitnexus/test/fixtures/lang-resolution/zig-idioms/src/main.zig new file mode 100644 index 000000000..52c58bb43 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/zig-idioms/src/main.zig @@ -0,0 +1,45 @@ +const std = @import("std"); +const counter = @import("counter.zig"); +// Alias of a namespace member — the most common way to bring a type into scope. +const Counter = counter.Counter; +// Single-member import straight off @import. +const Stack = @import("counter.zig").Stack; +// build.zig.zon path deps: geo declares src/root.zig in its build.zig, +// oldlib has no build.zig and relies on the src/.zig convention. +const geo = @import("geo"); +const oldlib = @import("oldlib"); +// Removed from the language in 0.15, still everywhere in 0.11–0.14 code. +pub usingnamespace @import("mixin.zig"); + +pub fn main() void { + // call-return inference: the receiver of the constructor call names the type + var a = Counter.init(); + a.incr(); + // annotation-only binding — `= undefined` and 0.14+ decl literals `.init` / `.empty` + var b: Counter = undefined; + b.twice(); + const c: Counter = .init(); + _ = c.get(); + // struct-literal constructor through the alias + const d = Counter{}; + _ = d.get(); + // generic instantiation: literal, static call on the instantiation, annotation + var s = Stack(u8){}; + s.push(1); + var t = Stack(u8).init(); + _ = t.top(); + const u: Stack(u16) = .init(); + u.clear(); + // path deps + _ = geo.area(2, 3); + var p = geo.Point{}; + p.shift(1); + oldlib.legacy(); + // statement assignments share the variable_declaration node type with + // declarations in tree-sitter-zig 1.1.2 — none of these is a binding. + counter.global_count = 5; + counter.global_count += 1; + _ = counter.VERSION; + a = Counter.init(); + a.incr(); +} diff --git a/gitnexus/test/fixtures/lang-resolution/zig-idioms/src/mixin.zig b/gitnexus/test/fixtures/lang-resolution/zig-idioms/src/mixin.zig new file mode 100644 index 000000000..1bb968a0d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/zig-idioms/src/mixin.zig @@ -0,0 +1 @@ +pub fn mixed() void {} diff --git a/gitnexus/test/integration/structural-pair-coverage.test.ts b/gitnexus/test/integration/structural-pair-coverage.test.ts index ffa76cee0..cfa1171b9 100644 --- a/gitnexus/test/integration/structural-pair-coverage.test.ts +++ b/gitnexus/test/integration/structural-pair-coverage.test.ts @@ -36,6 +36,8 @@ import { FIXTURES, runPipelineFromRepo } from './resolvers/helpers.js'; import { RELATION_SCHEMA } from '../../src/core/lbug/schema.js'; import { parseRelationSchemaPairs, relPairKeyFor } from '../../src/core/lbug/rel-pair-routing.js'; import { DIST_WORKER_URL, distWorkerExists } from '../helpers/worker-parse.js'; +import { isLanguageAvailable } from '../../src/core/tree-sitter/parser-loader.js'; +import { SupportedLanguages } from '../../src/config/supported-languages.js'; vi.setConfig({ testTimeout: 180_000 }); @@ -156,6 +158,29 @@ const NON_BRIDGE_CORPUS = [ }, ] as const satisfies readonly CorpusEntry[]; +/** + * Same contract, for fixtures whose grammar is an npm optionalDependency and + * may be absent on the runner. Gated per language rather than per case so a + * missing grammar SKIPS (the pipeline drops the files by contract, and an + * empty emit would otherwise fail every sentinel for a reason that has + * nothing to do with the schema). + */ +const OPTIONAL_GRAMMAR_CORPUS = [ + { + // Zig `union(enum)` is a member container: `union_declaration` sits in + // MEMBER_OWNER_NODE_TYPES, so the definition phase emits HAS_PROPERTY / + // HAS_METHOD FROM a `Union` node. Shipped once with `Union` off the + // generated grid — every resolver test passed on the in-memory graph while + // a real `analyze` of THIS fixture aborted at `assertDeclaredPair` + // (`Union|Property`). `Union` is now in `LINKABLE_LABELS`; take it out and + // both sentinels vanish from the DDL and this fails. + fixture: 'zig-basic', + language: SupportedLanguages.Zig, + emitter: 'zig union HAS_PROPERTY / HAS_METHOD', + sentinels: ['Union|Property', 'Union|Method'], + }, +] as const satisfies readonly (CorpusEntry & { readonly language: SupportedLanguages })[]; + /* * NOTE — why this suite runs its own pipelines instead of reusing the resolver * suites' graphs (measured, not assumed): @@ -222,4 +247,16 @@ describeIfWorkerBuilt('RELATION_SCHEMA covers the non-bridge emitters', () => { }).toEqual({ undeclaredPairs: [], missingSentinelPairs: [] }); }, ); + + it.concurrent.each(OPTIONAL_GRAMMAR_CORPUS)( + '$fixture emits only declared FROM/TO pairs, and still reaches $emitter', + async ({ fixture, language, sentinels }, ctx) => { + if (!isLanguageAvailable(language)) ctx.skip(); + const emitted = await pairsEmittedBy(fixture); + expect({ + undeclaredPairs: [...emitted].filter((pair) => !DECLARED.has(pair)).sort(), + missingSentinelPairs: sentinels.filter((pair) => !emitted.has(pair)), + }).toEqual({ undeclaredPairs: [], missingSentinelPairs: [] }); + }, + ); });