Skip to content
9 changes: 8 additions & 1 deletion gitnexus-shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ export type {
FieldRegistry,
FieldLookupOptions,
} from './scope-resolution/registries/field-registry.js';
export { buildMacroRegistry } from './scope-resolution/registries/macro-registry.js';
export type { MacroRegistry } from './scope-resolution/registries/macro-registry.js';
export { lookupCore } from './scope-resolution/registries/lookup-core.js';
export type { CoreLookupParams } from './scope-resolution/registries/lookup-core.js';
export { lookupQualified } from './scope-resolution/registries/lookup-qualified.js';
Expand All @@ -127,7 +129,12 @@ export {
CONFIDENCE_EPSILON,
} from './scope-resolution/registries/tie-breaks.js';
export type { TieBreakKey } from './scope-resolution/registries/tie-breaks.js';
export { CLASS_KINDS, METHOD_KINDS, FIELD_KINDS } from './scope-resolution/registries/context.js';
export {
CLASS_KINDS,
METHOD_KINDS,
FIELD_KINDS,
MACRO_KINDS,
} from './scope-resolution/registries/context.js';
export type {
RegistryContext,
RegistryProviders,
Expand Down
7 changes: 6 additions & 1 deletion gitnexus-shared/src/scope-resolution/reference-site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,12 @@ export type ReferenceKind =
| 'write'
| 'type-reference'
| 'inherits'
| 'import-use';
| 'import-use'
// A macro invocation (`log!(...)` / `vec![...]`). Resolved against
// `Macro`-labeled definitions ONLY (see `MacroRegistry`) so a macro
// never aliases a same-named free function — macros and functions are
// disjoint namespaces. Emitted as a `USES` edge, not `CALLS`.
| 'macro';

/**
* How a call site binds its target. Informs `Registry.lookup` Step 2
Expand Down
7 changes: 7 additions & 0 deletions gitnexus-shared/src/scope-resolution/registries/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,10 @@ export const FIELD_KINDS: readonly NodeLabel[] = Object.freeze([
'Const',
'Static',
]);

// Macros occupy a namespace disjoint from functions/methods: a `log!`
// invocation must resolve ONLY to a `macro_rules! log` definition, never
// to a same-named `fn log`. `MACRO_KINDS` is therefore a singleton
// (`['Macro']`) and is NOT merged into METHOD_KINDS — keeping the two
// keyspaces separate is what prevents the cross-namespace false-edge.
export const MACRO_KINDS: readonly NodeLabel[] = Object.freeze(['Macro']);
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* `MacroRegistry` — scope-aware lookup for macro definitions
* (`macro_rules!` in Rust; `#define` in C/C++) referenced from a macro
* invocation site.
*
* Thin wrapper over `lookupCore`, specialized for the macro namespace:
*
* - `acceptedKinds` = `MACRO_KINDS` (`['Macro']` only). Crucially this
* does NOT include `Function`/`Method`, so a `log!(…)` invocation can
* never resolve to a same-named free function `fn log` — macros and
* functions are disjoint namespaces (the false-`CALLS`-edge class the
* #1934 review flagged).
* - `useReceiverTypeBinding` is **false** — a macro invocation has no
* receiver; resolution is name-through-the-lexical-chain + the global
* qualified fallback, exactly like `ClassRegistry`.
* - Arity is not applied — macros are variadic by nature.
*/

import type { Resolution, ScopeId } from '../types.js';
import { lookupCore, type CoreLookupParams } from './lookup-core.js';
import { MACRO_KINDS, type RegistryContext } from './context.js';

export interface MacroRegistry {
/**
* Look up a macro definition by simple or scoped name anchored at
* `scope`. Returns a confidence-ranked `Resolution[]`; consume `[0]`
* for the best answer.
*/
lookup(name: string, scope: ScopeId): readonly Resolution[];
}

export function buildMacroRegistry(ctx: RegistryContext): MacroRegistry {
const params: CoreLookupParams = {
acceptedKinds: MACRO_KINDS,
useReceiverTypeBinding: false,
ownerScopedContributor: null,
};
return {
lookup(name: string, scope: ScopeId) {
return lookupCore(name, scope, params, ctx);
},
};
}
2 changes: 1 addition & 1 deletion gitnexus-shared/src/scope-resolution/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,7 @@ export interface Reference {
readonly toDef: DefId;
/** Location of the reference in source. */
readonly atRange: Range;
readonly kind: 'call' | 'read' | 'write' | 'type-reference' | 'inherits' | 'import-use';
readonly kind: 'call' | 'read' | 'write' | 'type-reference' | 'inherits' | 'import-use' | 'macro';
readonly confidence: number;
readonly evidence: readonly ResolutionEvidence[];
}
Expand Down
6 changes: 3 additions & 3 deletions gitnexus/bench/scope-capture/baselines.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@
"scaling_budget": 1.5
},
"rust": {
"fingerprint": "3c4b8e0a707299cc5db0af2528c72a99457859104589a7ef3cd1f377da01793e",
"fingerprint": "a5fdff2cf427504e33e66d0221b3ad62739c64bd0898e1dafedc15dbbe347b4d",
"scaling_budget": 1.5,
"_rebaselined": "#1956 tri-review U1: rust-qualified-trait fixture (scoped + generic-of-scoped impl trait paths); bareTypeIdentifier now resolves scoped_type_identifier bases by their name: tail (additive, no existing-fixture drift); linear (~1.04).",
"_note": "#1975: + rust-scoped-impl fixture (impl a::Inner / b::Inner inherent scoped impls). Pure fixture-corpus drift — the fix is the legacy @definition.impl scoped arm + findEnclosingClassInfo inherent-impl scoped target, NOT the rust scope-extractor; existing fixtures' captures byte-identical. fixture_count 120->121."
"_rebaselined": "#1956 tri-review U1: rust-qualified-trait fixture (scoped + generic-of-scoped impl trait paths); bareTypeIdentifier now resolves scoped_type_identifier bases by their name: tail (additive, no existing-fixture drift); linear (~1.04). #1975: + rust-scoped-impl fixture (impl a::Inner / b::Inner inherent scoped impls) — legacy @definition.impl scoped arm + findEnclosingClassInfo inherent-impl scoped target; rust scope-extractor captures byte-identical.",
"_note": "PR #1934: F66/F68 let-binding pattern narrowing; F71 union (Struct-labeled, now materialized via legacy @definition.struct + resolvable); F72 macro FULLY WIRED — @declaration.macro/@reference.macro + MacroRegistry → USES edges to Macro nodes (never a same-named fn). + rust-macro / rust-union fixtures and merged with origin/main #1975 rust-scoped-impl; fingerprint re-baselined (scaling ~0.99, fixture_count 126)."
},
"php": {
"fingerprint": "f9c8eaf6d1084f9b95a9fb97ccce5e618a24d936c85fb8af4b96c73a560f7a7f",
Expand Down
10 changes: 8 additions & 2 deletions gitnexus/src/core/ingestion/emit-references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,14 @@ function buildRelationship(

/**
* Map a `Reference.kind` to an existing `RelationshipType`. Read/write
* both fold into `ACCESSES`; `type-reference` + `import-use` both fold
* into `USES`. This keeps the graph schema additive — no new
* both fold into `ACCESSES`; `type-reference`, `import-use`, and `macro`
* all fold into `USES`. This keeps the graph schema additive — no new
* RelationshipType values are introduced by this module.
*
* `macro` folds into `USES` (not `CALLS`) deliberately: a macro
* invocation targets a `Macro` node, not a callable function, so keeping
* it out of the `CALLS` keyspace preserves the invariant that `CALLS`
* edges denote function/method dispatch.
*/
function mapKindToType(kind: Reference['kind']): RelationshipType {
switch (kind) {
Expand All @@ -282,6 +287,7 @@ function mapKindToType(kind: Reference['kind']): RelationshipType {
return 'INHERITS';
case 'type-reference':
case 'import-use':
case 'macro':
return 'USES';
}
}
Expand Down
40 changes: 40 additions & 0 deletions gitnexus/src/core/ingestion/languages/rust/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const RUST_SCOPE_QUERY = `
(trait_item) @scope.class
(impl_item) @scope.class
(enum_item) @scope.class
(union_item) @scope.class
(function_item) @scope.function
(closure_expression) @scope.function
(block) @scope.block
Expand All @@ -30,6 +31,26 @@ const RUST_SCOPE_QUERY = `
(enum_item
name: (type_identifier) @declaration.name) @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.
(union_item
name: (type_identifier) @declaration.name) @declaration.struct

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Union is labeled Struct, not Union — intentional/benign, but undocumented & untested. @declaration.struct yields normalizeNodeLabel('struct')='Struct'. This is actually the correct pragmatic choice: every registry-primary resolution gate — isLinkableLabel (node-lookup.ts:165-189), CALLABLE_OR_TYPE_LIKE (finalize-algorithm.ts:785-803), ClassLikeNodeLabel (class-types.ts:4-7) — includes Struct but excludes Union, so a @declaration.union node would be an unresolvable orphan. So this is not a bug. But (a) no comment explains the deliberate downgrade, and (b) no pipeline-level test asserts union.rs resolves to a Struct named MyUnion.

Fix: add a one-line rationale comment here, plus a pipeline-level union-resolution test.

[reproduced + code-read; Codex + correctness/adversarial]

@magyargergo magyargergo Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed, and it went deeper than docs: the union had no graph node at all — the legacy RUST_QUERIES never captured union_item, so the @declaration.struct scope capture had nothing to resolve to. Added (union_item name: (type_identifier) @name) @definition.struct to materialize it, plus the rationale comment (Struct chosen because every registry gate includes Struct, excludes Union). New pipeline test rust.test.ts › Rust union resolution asserts union.rs resolves to a Struct named MyUnion and that MyUnion { .. } emits a CALLS edge to it — runs on both parity halves. (49d2940)


;; Declarations — macro (macro_rules! foo { ... })
;; Captured as @declaration.macro → Macro label. A macro invocation
;; (@reference.macro, below) resolves to this definition via MacroRegistry,
;; whose acceptedKinds is ['Macro'] ONLY — so an invoked macro never binds
;; to a same-named free function (log!() is not fn log). #1934 F72.
(macro_definition
name: (identifier) @declaration.name) @declaration.macro

;; Declarations — function (top-level or inside mod)
(function_item
name: (identifier) @declaration.name) @declaration.function
Expand All @@ -40,6 +61,10 @@ const RUST_SCOPE_QUERY = `
type: (_) @declaration.field-type) @declaration.field

;; Declarations — variables (let bindings)
;; Uses pattern:(identifier) — works for let x and let mut x (mutable_specifier
;; is a sibling, not a wrapper). Destructuring patterns like let (a, b) use
;; tuple_pattern etc. which pattern:(identifier) intentionally does not match;
;; capturing them with (_) would produce "(a, b)" as the name, which is useless.
(let_declaration
pattern: (identifier) @declaration.name) @declaration.variable

Expand Down Expand Up @@ -109,9 +134,24 @@ const RUST_SCOPE_QUERY = `
name: (identifier) @reference.name)) @reference.call.free

;; References — constructor calls (struct literal)
;; Covers bare names (Foo {}), scoped (foo::bar::Baz {}), and turbofish
;; (Foo::<T> {}) — the name: field resolves to the trailing identifier
;; in all cases through tree-sitter-rust's grammar.
(struct_expression
name: (_) @reference.name) @reference.call.constructor

;; References — macro invocations (disjoint namespace from functions)
;; Resolved via MacroRegistry → Macro defs only (never fn of the same name).
(macro_invocation
macro: (identifier) @reference.name) @reference.macro

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2 · headline] F72 macro coverage is a silent no-op. This @reference.macro capture fires at the query layer, but the scope-extractor drops it: referenceKindFromAnchor (scope-extractor.ts:1027-1049) has no macro case so it returns undefined, and pass5CollectReferences (scope-extractor.ts:990-991) does if (kind === undefined) continue;. ReferenceKind (gitnexus-shared/src/scope-resolution/reference-site.ts:34-40) has no macro member, and nothing in src consumes @reference.macro. Net: 0 ReferenceSites / 0 graph edges for any macro invocation — macros are as invisible after this PR as before. Not a regression, but F72 ships inert.

Fix: add 'macro' to ReferenceKind + a referenceKindFromAnchor branch + a consumer, or document it as capture-only and drop the F72 claim from the title.

[reproduced via tsx + code-read; corroborated by Codex (gpt-5.5/xhigh) + correctness/adversarial/risk lanes]

@magyargergo magyargergo Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved by fully wiring F72. Added a 'macro' ReferenceKind + a MacroRegistry: referenceKindFromAnchor now maps @reference.macro'macro', resolve-references routes those sites through the macro registry, the edge mappers map 'macro'USES, and isLinkableLabel makes Macro linkable so the registry def bridges to the legacy @definition.macro graph node. Net: println!() / vec![] / log::info!() now produce real USES edges to their Macro defs. (49d2940)


;; Scoped macro invocation (log::info!(…)) — capture the tail identifier,
;; mirroring the scoped free-call pattern above, so the resolved name is
;; the tail (info), not the full path (log::info).
(macro_invocation
macro: (scoped_identifier
name: (identifier) @reference.name)) @reference.macro

;; References — field reads
(field_expression
value: (_) @reference.receiver
Expand Down
23 changes: 22 additions & 1 deletion gitnexus/src/core/ingestion/resolve-references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,14 @@
import {
buildClassRegistry,
buildFieldRegistry,
buildMacroRegistry,
buildMethodRegistry,
CLASS_KINDS,
FIELD_KINDS,
METHOD_KINDS,
type ClassRegistry,
type FieldRegistry,
type MacroRegistry,
type MethodRegistry,
type Reference,
type ReferenceIndex,
Expand Down Expand Up @@ -102,6 +104,7 @@ export function resolveReferenceSites(input: ResolveReferencesInput): ResolveRef
const classRegistry = buildClassRegistry(ctx);
const methodRegistry = buildMethodRegistry(ctx);
const fieldRegistry = buildFieldRegistry(ctx);
const macroRegistry = buildMacroRegistry(ctx);

// bySourceScope is the canonical index; byTargetDef is derived from it.
const bySourceScope = new Map<ScopeId, Reference[]>();
Expand All @@ -114,7 +117,13 @@ export function resolveReferenceSites(input: ResolveReferencesInput): ResolveRef
for (const site of scopes.referenceSites) {
sitesProcessed++;

const resolutions = lookupForSite(site, classRegistry, methodRegistry, fieldRegistry);
const resolutions = lookupForSite(
site,
classRegistry,
methodRegistry,
fieldRegistry,
macroRegistry,
);
if (resolutions.length === 0) {
unresolved++;
continue;
Expand Down Expand Up @@ -165,6 +174,12 @@ export function resolveReferenceSites(input: ResolveReferencesInput): ResolveRef
* | `type-reference` | ClassRegistry | CLASS_KINDS |
* | `read`/`write` | FieldRegistry | FIELD_KINDS |
* | `import-use` | tiered fallback | METHOD ∪ CLASS ∪ FIELD |
* | `macro` | MacroRegistry | MACRO_KINDS (`Macro` only) |
*
* `macro` has its own single-kind registry so a macro invocation
* (`log!(…)`) resolves ONLY to a `macro_rules! log` definition and never
* to a same-named free function — macros and functions are disjoint
* namespaces (the false-`CALLS`-edge class flagged in the #1934 review).
*
* `import-use` doesn't have a single registry — the imported name might
* be a class, a function, or a constant. Try each in priority order and
Expand All @@ -177,6 +192,7 @@ function lookupForSite(
classRegistry: ClassRegistry,
methodRegistry: MethodRegistry,
fieldRegistry: FieldRegistry,
macroRegistry: MacroRegistry,
): readonly Resolution[] {
switch (site.kind) {
case 'call': {
Expand Down Expand Up @@ -213,6 +229,11 @@ function lookupForSite(
if (methodHits.length > 0) return methodHits;
return fieldRegistry.lookup(site.name, site.inScope);
}
case 'macro': {
// Macro-only namespace: resolves against `Macro`-labeled defs, never
// functions. No receiver, no arity — see `MacroRegistry`.
return macroRegistry.lookup(site.name, site.inScope);
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions gitnexus/src/core/ingestion/scope-extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,8 @@ function normalizeNodeLabel(kindStr: string): SymbolDefinition['type'] | undefin
return 'Annotation';
case 'namespace':
return 'Namespace';
case 'macro':
return 'Macro';
default:
return undefined;
}
Expand Down Expand Up @@ -1044,6 +1046,8 @@ function referenceKindFromAnchor(name: string): ReferenceKind | undefined {
case 'import_use':
case 'import-use':
return 'import-use';
case 'macro':
return 'macro';
default:
return undefined;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export function mapReferenceKindToEdgeType(
return 'EXTENDS';
case 'type-reference':
return 'USES';
// Macro invocations resolve to a `Macro` node (never a function), so
// they emit `USES` — kept out of the `CALLS` keyspace which denotes
// function/method dispatch (#1934 review).
case 'macro':
return 'USES';
case 'import-use':
return undefined;
default:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ export function isLinkableLabel(label: NodeLabel): boolean {
// `Variable` def for `export const fooService = {...}` to the canonical
// `Const:filePath:name` graph node id, against which object-literal
// method symbols register their `ownerId` (PR #1718 / issue #1358).
label === 'Const'
label === 'Const' ||
// Macro nodes are linkable so a macro invocation (`log!(…)`) resolved
// via `MacroRegistry` can bridge its scope-resolution `Macro` def to
// the legacy `@definition.macro` graph node and emit the `USES` edge
// (Rust #1934 F72; also covers C/C++ `#define` macro defs).
label === 'Macro'
);
}
5 changes: 5 additions & 0 deletions gitnexus/src/core/ingestion/tree-sitter-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1215,6 +1215,11 @@ export const RUST_QUERIES = `
(function_item name: (identifier) @name) @definition.function
(function_signature_item name: (identifier) @name) @definition.function
(struct_item name: (type_identifier) @name) @definition.struct
; A union is materialized as a Struct node (same rationale as the
; scope-resolution @declaration.struct in languages/rust/query.ts: every
; registry-primary resolution gate includes Struct but excludes Union, so a
; Union-labeled node would be an unresolvable orphan). #1934 F71.
(union_item name: (type_identifier) @name) @definition.struct
(enum_item name: (type_identifier) @name) @definition.enum
(trait_item name: (type_identifier) @name) @definition.trait
(impl_item type: (type_identifier) @name !trait) @definition.impl
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// F72 — macro invocations
fn use_macros() {
println!("hello");
let v = vec![1, 2, 3];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// F66/F68 — let binding with various pattern shapes
fn pattern_shapes() {
let x = 1; // bare identifier
let mut y = 2; // identifier with mut
let (a, b) = (1, 2); // tuple pattern
let Some(val) = Some(3); // tuple struct pattern
let Foo { field } = Foo { field: 1 }; // struct pattern
let ref z = 4; // ref pattern
let n @ 1..=10 = 5; // captured pattern
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// F71 — union declaration
union MyUnion {
x: i32,
y: f64,
}
21 changes: 21 additions & 0 deletions gitnexus/test/fixtures/lang-resolution/rust-macro/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// F72 — a macro invocation resolves to its `macro_rules!` definition (a
// Macro node) via a USES edge, and NEVER to a same-named free function.
// Macros and functions are disjoint namespaces.

macro_rules! greet {
($name:expr) => {
let _ = $name;
};
}

// Same simple name as the macro, on purpose: proves the macro invocation
// does not bind to this function (no false CALLS edge) and the function
// call does not bind to the macro.
fn greet() -> u32 {
0
}

fn run() {
greet!("world"); // macro invocation -> USES edge to Macro greet
let _ = greet(); // function call -> CALLS edge to Function greet
}
12 changes: 12 additions & 0 deletions gitnexus/test/fixtures/lang-resolution/rust-union/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// F71 — a `union` is captured as a Struct-labeled node (every
// registry-primary resolution gate includes Struct but excludes Union),
// and it is resolvable: the union literal is a real type constructor.

union MyUnion {
int_val: i32,
float_val: f64,
}

fn make() -> MyUnion {
MyUnion { int_val: 5 } // constructor -> CALLS edge to the Struct MyUnion
}
Loading
Loading