mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
feat(zig): scope-resolution hooks — IMPORTS and CALLS edges (Ring 3)
Implements the registry-primary scope-resolution path for Zig, the
prerequisite for cross-file edges since the legacy DAG removal. Adds the
standard per-language stack under languages/zig/:
- query.ts: scope query (containers as Class scopes, blocks, functions),
declarations (container anchors placed on the container node itself so
the def lands in its own Class scope and the name binding auto-hoists
to the parent — populateClassOwnedMembers needs the class-like def
among the class scope's ownedDefs), @import statements (#eq?-gated
builtin), parameter/constructor type bindings, and call/constructor
reference sites. The grammar is required lazily (optionalDependency).
- captures.ts: emitZigScopeCaptures — groups query matches, drops the
plain-variable group for container/import bindings (their dedicated
rules bind the name), and relabels container-nested fns
@declaration.function → @declaration.method (labelOverride parity).
- interpret.ts: namespace-kind imports (const x = @import("…")) and
type bindings — self-parameter convention marks the receiver, Zig
sigils (*, ?, [], error unions, const) stripped from type names while
dotted qualifiers (mod.T) are preserved for Case-3 namespace-prefix
receiver dispatch.
- simple-hooks.ts: parameter bindings stay function-local (Go
rationale), local-over-import merge precedence, bounds-check arity
(always 'unknown' today — no synthesized arity metadata).
- scope-resolver.ts: emit-side wiring; build.zig.zon threads through
loadResolutionConfig into the same resolveZigImportInternal the legacy
resolver config wraps. fieldFallbackOnMethodLookup off (statically
typed). Registered in SCOPE_RESOLVERS.
The resolvers integration test un-skips the import-edge case and gains
CALLS assertions: free call (main → helper) and receiver-bound method
dispatch through a namespace-qualified constructor
(var p = pioneer.Pioneer{…}; p.tick() → main → tick).
This commit is contained in:
parent
4dd87b16ad
commit
81eccec36e
10 changed files with 447 additions and 16 deletions
|
|
@ -31,7 +31,7 @@ const ZIG_STDLIB_NAMES = new Set(['std', 'builtin', 'root']);
|
|||
export function resolveZigImportInternal(
|
||||
currentFile: string,
|
||||
importPath: string,
|
||||
allFiles: Set<string>,
|
||||
allFiles: ReadonlySet<string>,
|
||||
buildZon?: ZigBuildZonConfig | null,
|
||||
): string | null {
|
||||
// Stdlib / compiler builtin / root — not resolvable from source files alone.
|
||||
|
|
@ -68,10 +68,7 @@ export function resolveZigImportInternal(
|
|||
if (normalized !== null) {
|
||||
// Conventional Zig layout: <pkg_root>/src/<name>.zig (matches the
|
||||
// package's primary module name) or <pkg_root>/src/main.zig.
|
||||
const candidates = [
|
||||
`${normalized}/src/${importPath}.zig`,
|
||||
`${normalized}/src/main.zig`,
|
||||
];
|
||||
const candidates = [`${normalized}/src/${importPath}.zig`, `${normalized}/src/main.zig`];
|
||||
for (const c of candidates) {
|
||||
if (allFiles.has(c)) return c;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,13 @@
|
|||
* `@import("std")` and external packages are deliberately external.
|
||||
* - namedBindingExtractor: omitted — `const Foo = @import("x").Foo` is a
|
||||
* const declaration, not import-statement syntax.
|
||||
* - scope-resolution hooks (emitScopeCaptures, interpretImport, …) are
|
||||
* omitted — Zig is classified `experimental` and uses the generic
|
||||
* fallback resolution path.
|
||||
* - scope-resolution hooks (Ring 3): `emitScopeCaptures` walks the file via
|
||||
* `zig/query.ts` (containers as Class scopes, container-nested fns
|
||||
* relabeled @declaration.method, plain-variable groups filtered for
|
||||
* container/import bindings); `interpretImport` maps
|
||||
* `const x = @import("…")` to a namespace import; receiver dispatch
|
||||
* rides the `self`-parameter convention. The emit-side wiring lives in
|
||||
* `zig/scope-resolver.ts` (SCOPE_RESOLVERS registry).
|
||||
*/
|
||||
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
|
|
@ -34,6 +38,14 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
|
|||
import { zigVariableConfig } from '../variable-extractors/configs/zig.js';
|
||||
import { zigTypeConfig } from '../type-extractors/zig.js';
|
||||
import type { SyntaxNode } from '../utils/ast-helpers.js';
|
||||
import {
|
||||
emitZigScopeCaptures,
|
||||
interpretZigImport,
|
||||
interpretZigTypeBinding,
|
||||
zigArityCompatibility,
|
||||
zigBindingScopeFor,
|
||||
zigReceiverBinding,
|
||||
} from './zig/index.js';
|
||||
|
||||
const ZIG_CONTAINER_TYPES = new Set(['struct_declaration', 'enum_declaration', 'union_declaration']);
|
||||
|
||||
|
|
@ -75,4 +87,14 @@ export const zigProvider = defineLanguage({
|
|||
if (isZigContainerMethod(functionNode)) return 'Method';
|
||||
return defaultLabel;
|
||||
},
|
||||
|
||||
// ── RFC #909 Ring 3: scope-based resolution hooks ──
|
||||
emitScopeCaptures: emitZigScopeCaptures,
|
||||
interpretImport: interpretZigImport,
|
||||
interpretTypeBinding: interpretZigTypeBinding,
|
||||
bindingScopeFor: zigBindingScopeFor,
|
||||
receiverBinding: zigReceiverBinding,
|
||||
// Provider contract is (def, callsite); the ScopeResolver contract is
|
||||
// (callsite, def) — same function, adapted argument order.
|
||||
arityCompatibility: (def, callsite) => zigArityCompatibility(callsite, def),
|
||||
});
|
||||
|
|
|
|||
88
gitnexus/src/core/ingestion/languages/zig/captures.ts
Normal file
88
gitnexus/src/core/ingestion/languages/zig/captures.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import type { Capture, CaptureMatch } from 'gitnexus-shared';
|
||||
import { nodeToCapture, type SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
import { getZigParser, getZigScopeQuery } from './query.js';
|
||||
import { getTreeSitterBufferSize } from '../../constants.js';
|
||||
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
||||
|
||||
const ZIG_CONTAINER_TYPES = new Set([
|
||||
'struct_declaration',
|
||||
'enum_declaration',
|
||||
'union_declaration',
|
||||
]);
|
||||
|
||||
/** Is this variable_declaration a container binding (`const T = struct {…}`)
|
||||
* or an import binding (`const x = @import("…")`)? Those groups are emitted
|
||||
* by their dedicated query rules; the plain @declaration.variable match for
|
||||
* the same node must be dropped so the name binds exactly once. */
|
||||
function isContainerOrImportBinding(declNode: SyntaxNode): boolean {
|
||||
for (let i = 0; i < declNode.namedChildCount; i++) {
|
||||
const child = declNode.namedChild(i);
|
||||
if (child === null) continue;
|
||||
if (ZIG_CONTAINER_TYPES.has(child.type)) return true;
|
||||
if (child.type === 'builtin_function') {
|
||||
const builtin = child.namedChild(0);
|
||||
if (builtin?.type === 'builtin_identifier' && builtin.text === '@import') return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** A `fn` nested in a struct/enum/union container is a method — mirror the
|
||||
* provider's `labelOverride` so scope-side defs carry the same label the
|
||||
* worker gives the graph node. */
|
||||
function isContainerMethod(fnNode: SyntaxNode): boolean {
|
||||
let ancestor = fnNode.parent;
|
||||
while (ancestor) {
|
||||
if (ZIG_CONTAINER_TYPES.has(ancestor.type)) return true;
|
||||
ancestor = ancestor.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function emitZigScopeCaptures(
|
||||
sourceText: string,
|
||||
_filePath: string,
|
||||
cachedTree?: unknown,
|
||||
): readonly CaptureMatch[] {
|
||||
let tree = cachedTree as ReturnType<ReturnType<typeof getZigParser>['parse']> | undefined;
|
||||
if (tree === undefined) {
|
||||
tree = parseSourceSafe(getZigParser(), sourceText, undefined, {
|
||||
bufferSize: getTreeSitterBufferSize(sourceText),
|
||||
});
|
||||
}
|
||||
|
||||
const rawMatches = getZigScopeQuery().matches(tree.rootNode);
|
||||
const out: CaptureMatch[] = [];
|
||||
|
||||
for (const m of rawMatches) {
|
||||
const grouped: Record<string, Capture> = {};
|
||||
const nodeMap: Record<string, SyntaxNode> = {};
|
||||
for (const c of m.captures) {
|
||||
const tag = '@' + c.name;
|
||||
if (tag.startsWith('@_')) continue; // skip anonymous predicate captures
|
||||
grouped[tag] = nodeToCapture(tag, c.node);
|
||||
nodeMap[tag] = c.node;
|
||||
}
|
||||
if (Object.keys(grouped).length === 0) continue;
|
||||
|
||||
// Drop the plain-variable group for container/import bindings — their
|
||||
// dedicated rules already bind the name (as Struct/Enum/Union or import).
|
||||
const variableAnchor = nodeMap['@declaration.variable'];
|
||||
if (variableAnchor !== undefined && isContainerOrImportBinding(variableAnchor)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Relabel container-nested fns Function → Method (provider labelOverride
|
||||
// parity). The anchor capture name carries the kind, so rebuild it.
|
||||
const fnAnchor = nodeMap['@declaration.function'];
|
||||
if (fnAnchor !== undefined && isContainerMethod(fnAnchor)) {
|
||||
const fnCapture = grouped['@declaration.function']!;
|
||||
delete grouped['@declaration.function'];
|
||||
grouped['@declaration.method'] = { ...fnCapture, name: '@declaration.method' };
|
||||
}
|
||||
|
||||
out.push(grouped);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
8
gitnexus/src/core/ingestion/languages/zig/index.ts
Normal file
8
gitnexus/src/core/ingestion/languages/zig/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export { emitZigScopeCaptures } from './captures.js';
|
||||
export { interpretZigImport, interpretZigTypeBinding, normalizeZigTypeName } from './interpret.js';
|
||||
export {
|
||||
zigArityCompatibility,
|
||||
zigBindingScopeFor,
|
||||
zigMergeBindings,
|
||||
zigReceiverBinding,
|
||||
} from './simple-hooks.js';
|
||||
55
gitnexus/src/core/ingestion/languages/zig/interpret.ts
Normal file
55
gitnexus/src/core/ingestion/languages/zig/interpret.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import type { CaptureMatch, ParsedImport, ParsedTypeBinding, TypeRef } from 'gitnexus-shared';
|
||||
|
||||
const stripQuotes = (s: string): string => s.replace(/^["']|["']$/g, '');
|
||||
|
||||
/**
|
||||
* `const std = @import("std");` binds the imported module to a const handle
|
||||
* accessed via qualified syntax — a namespace import (closest peers: Python
|
||||
* `import numpy`, Go `import "pkg/bar"`). The local name and imported name
|
||||
* are always the same identifier; Zig has no rename syntax at the import
|
||||
* site (renames are ordinary const aliases handled as variable bindings).
|
||||
*/
|
||||
export function interpretZigImport(captures: CaptureMatch): ParsedImport | null {
|
||||
const name = captures['@import.name']?.text;
|
||||
const source = captures['@import.source']?.text;
|
||||
if (name === undefined || source === undefined) return null;
|
||||
const targetRaw = stripQuotes(source);
|
||||
if (targetRaw.length === 0) return null;
|
||||
return { kind: 'namespace', localName: name, importedName: name, targetRaw };
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip Zig type sigils that wrap the nominal type: pointers (`*T`, `[*]T`),
|
||||
* optionals (`?T`), error unions (`!T` / `E!T`), slices (`[]T`), arrays
|
||||
* (`[N]T`), and `const` qualifiers. Keeps the bare type name so registry
|
||||
* lookup matches the container declaration.
|
||||
*/
|
||||
export function normalizeZigTypeName(text: string): string {
|
||||
let t = text.trim();
|
||||
let previous: string;
|
||||
do {
|
||||
previous = t;
|
||||
t = t.replace(/^(\*|\?|\[\*?c?\]|\[[^\]]*\])\s*/, '');
|
||||
t = t.replace(/^const\s+/, '');
|
||||
} while (t !== previous);
|
||||
const bang = t.lastIndexOf('!');
|
||||
if (bang !== -1) t = t.slice(bang + 1).trim();
|
||||
return t;
|
||||
}
|
||||
|
||||
export function interpretZigTypeBinding(captures: CaptureMatch): ParsedTypeBinding | null {
|
||||
const name = captures['@type-binding.name']?.text;
|
||||
const type = captures['@type-binding.type']?.text;
|
||||
if (name === undefined || type === undefined) return null;
|
||||
|
||||
let source: TypeRef['source'] = 'annotation';
|
||||
if (captures['@type-binding.parameter'] !== undefined) {
|
||||
// Zig has no implicit receiver keyword; the convention is a first
|
||||
// parameter named `self`. Mark it so `receiverBinding` finds it.
|
||||
source = name === 'self' ? 'self' : 'parameter-annotation';
|
||||
} else if (captures['@type-binding.constructor'] !== undefined) {
|
||||
source = 'constructor-inferred';
|
||||
}
|
||||
|
||||
return { boundName: name, rawTypeName: normalizeZigTypeName(type), source };
|
||||
}
|
||||
126
gitnexus/src/core/ingestion/languages/zig/query.ts
Normal file
126
gitnexus/src/core/ingestion/languages/zig/query.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import Parser from 'tree-sitter';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
const _require = createRequire(import.meta.url);
|
||||
|
||||
/**
|
||||
* Zig scope-resolution query (RFC #909 Ring 3).
|
||||
*
|
||||
* The grammar is an optionalDependency (`@tree-sitter-grammars/tree-sitter-zig`),
|
||||
* so the language module is required lazily and `getZigParser` /
|
||||
* `getZigScopeQuery` throw only when actually invoked without the grammar
|
||||
* installed. That is safe: the parse pipeline filters `.zig` files through
|
||||
* `parser-loader.isLanguageAvailable` before any scope extraction runs.
|
||||
*
|
||||
* Zig specifics encoded here:
|
||||
* - Containers (struct/enum/union) are anonymous nodes bound by the
|
||||
* enclosing `variable_declaration`; declarations capture the binding
|
||||
* identifier from the wrapper.
|
||||
* - `@import` is a builtin call, not import-statement syntax; the
|
||||
* `#eq?` predicate keeps other builtins (@sizeOf, @as, …) out.
|
||||
* - A plain `(variable_declaration (identifier))` rule would also match
|
||||
* container and import bindings — `emitZigScopeCaptures` filters those
|
||||
* groups out so a name binds exactly once.
|
||||
*/
|
||||
const ZIG_SCOPE_QUERY = `
|
||||
;; Scopes
|
||||
(source_file) @scope.module
|
||||
(struct_declaration) @scope.class
|
||||
(enum_declaration) @scope.class
|
||||
(union_declaration) @scope.class
|
||||
(function_declaration) @scope.function
|
||||
(block) @scope.block
|
||||
|
||||
;; Declarations — functions (relabeled @declaration.method inside containers
|
||||
;; by emitZigScopeCaptures, mirroring the provider's labelOverride)
|
||||
(function_declaration
|
||||
name: (identifier) @declaration.name) @declaration.function
|
||||
|
||||
;; Declarations — containers. The binding name lives on the wrapper
|
||||
;; variable_declaration, but the ANCHOR is the container node itself so its
|
||||
;; range equals the @scope.class range: the extractor then attaches the def
|
||||
;; to the class scope (walkers.populateClassOwnedMembers expects the
|
||||
;; class-like def among the class scope's ownedDefs) and auto-hoists the
|
||||
;; name binding to the parent scope.
|
||||
(variable_declaration
|
||||
(identifier) @declaration.name
|
||||
(struct_declaration) @declaration.struct)
|
||||
(variable_declaration
|
||||
(identifier) @declaration.name
|
||||
(enum_declaration) @declaration.enum)
|
||||
(variable_declaration
|
||||
(identifier) @declaration.name
|
||||
(union_declaration) @declaration.union)
|
||||
|
||||
;; Declarations — container fields (struct fields, enum/union variants)
|
||||
(container_field
|
||||
name: (identifier) @declaration.name) @declaration.field
|
||||
|
||||
;; Declarations — const/var bindings (import/container groups filtered in TS)
|
||||
(variable_declaration
|
||||
(identifier) @declaration.name) @declaration.variable
|
||||
|
||||
;; Imports — const x = @import("...")
|
||||
(variable_declaration
|
||||
(identifier) @import.name
|
||||
(builtin_function
|
||||
(builtin_identifier) @_builtin
|
||||
(arguments (string) @import.source))
|
||||
(#eq? @_builtin "@import")) @import.statement
|
||||
|
||||
;; Type bindings — parameter annotations (incl. self: *T receivers)
|
||||
(parameter
|
||||
name: (identifier) @type-binding.name
|
||||
type: (_) @type-binding.type) @type-binding.parameter
|
||||
|
||||
;; Type bindings — constructor inference: const p = T{ ... }
|
||||
(variable_declaration
|
||||
(identifier) @type-binding.name
|
||||
(struct_initializer
|
||||
(identifier) @type-binding.type)) @type-binding.constructor
|
||||
|
||||
;; Type bindings — qualified constructor: const p = mod.T{ ... }. The whole
|
||||
;; field_expression is captured so the dotted text "mod.T" survives —
|
||||
;; receiver dispatch resolves the namespace prefix through the import
|
||||
;; binding (emitReceiverBoundCalls Case 3).
|
||||
(variable_declaration
|
||||
(identifier) @type-binding.name
|
||||
(struct_initializer
|
||||
(field_expression) @type-binding.type)) @type-binding.constructor
|
||||
|
||||
;; References — free calls: foo(...)
|
||||
(call_expression
|
||||
function: (identifier) @reference.name) @reference.call.free
|
||||
|
||||
;; References — member calls: obj.method(...) / mod.fn(...)
|
||||
(call_expression
|
||||
function: (field_expression
|
||||
object: (_) @reference.receiver
|
||||
member: (identifier) @reference.name)) @reference.call.member
|
||||
|
||||
;; References — constructor uses: T{ ... }
|
||||
(struct_initializer
|
||||
(identifier) @reference.name) @reference.call.constructor
|
||||
`;
|
||||
|
||||
let _parser: Parser | null = null;
|
||||
let _query: Parser.Query | null = null;
|
||||
|
||||
function getZigLanguage(): Parameters<Parser['setLanguage']>[0] {
|
||||
return _require('@tree-sitter-grammars/tree-sitter-zig');
|
||||
}
|
||||
|
||||
export function getZigParser(): Parser {
|
||||
if (_parser === null) {
|
||||
_parser = new Parser();
|
||||
_parser.setLanguage(getZigLanguage());
|
||||
}
|
||||
return _parser;
|
||||
}
|
||||
|
||||
export function getZigScopeQuery(): Parser.Query {
|
||||
if (_query === null) {
|
||||
_query = new Parser.Query(getZigLanguage(), ZIG_SCOPE_QUERY);
|
||||
}
|
||||
return _query;
|
||||
}
|
||||
50
gitnexus/src/core/ingestion/languages/zig/scope-resolver.ts
Normal file
50
gitnexus/src/core/ingestion/languages/zig/scope-resolver.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* Zig `ScopeResolver` registered in `SCOPE_RESOLVERS` and consumed by the
|
||||
* generic `runScopeResolution` orchestrator.
|
||||
*
|
||||
* Thin wiring: Zig has no inheritance (default MRO linearization over an
|
||||
* empty heritage set), no `super`, and is statically typed (field-fallback
|
||||
* heuristic off per the contract guidance). Import resolution reuses the
|
||||
* same `resolveZigImportInternal` the legacy import-resolver config wraps,
|
||||
* with `build.zig.zon` `.path` deps threaded through `loadResolutionConfig`.
|
||||
*/
|
||||
|
||||
import type { ParsedFile } from 'gitnexus-shared';
|
||||
import { SupportedLanguages } from 'gitnexus-shared';
|
||||
import { buildMro, defaultLinearize } from '../../scope-resolution/passes/mro.js';
|
||||
import { populateClassOwnedMembers } from '../../scope-resolution/scope/walkers.js';
|
||||
import type { ScopeResolver } from '../../scope-resolution/contract/scope-resolver.js';
|
||||
import { loadZigBuildZon, type ZigBuildZonConfig } from '../../language-config.js';
|
||||
import { resolveZigImportInternal } from '../../import-resolvers/zig.js';
|
||||
import { zigProvider } from '../zig.js';
|
||||
import { zigArityCompatibility, zigMergeBindings } from './index.js';
|
||||
|
||||
export const zigScopeResolver: ScopeResolver = {
|
||||
language: SupportedLanguages.Zig,
|
||||
languageProvider: zigProvider,
|
||||
importEdgeReason: 'zig-scope: import',
|
||||
|
||||
loadResolutionConfig: (repoPath: string) => loadZigBuildZon(repoPath),
|
||||
|
||||
resolveImportTarget: (targetRaw, fromFile, allFilePaths, resolutionConfig) =>
|
||||
resolveZigImportInternal(
|
||||
fromFile,
|
||||
targetRaw,
|
||||
allFilePaths,
|
||||
(resolutionConfig as ZigBuildZonConfig | null | undefined) ?? null,
|
||||
),
|
||||
|
||||
mergeBindings: zigMergeBindings,
|
||||
arityCompatibility: zigArityCompatibility,
|
||||
|
||||
buildMro: (graph, parsedFiles, nodeLookup) =>
|
||||
buildMro(graph, parsedFiles, nodeLookup, defaultLinearize),
|
||||
|
||||
populateOwners: (parsed: ParsedFile) => populateClassOwnedMembers(parsed),
|
||||
|
||||
// Zig has no `super`.
|
||||
isSuperReceiver: () => false,
|
||||
|
||||
// Statically typed — the field-fallback heuristic over-connects.
|
||||
fieldFallbackOnMethodLookup: false,
|
||||
};
|
||||
77
gitnexus/src/core/ingestion/languages/zig/simple-hooks.ts
Normal file
77
gitnexus/src/core/ingestion/languages/zig/simple-hooks.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import type {
|
||||
BindingRef,
|
||||
Callsite,
|
||||
CaptureMatch,
|
||||
Scope,
|
||||
ScopeId,
|
||||
ScopeTree,
|
||||
SymbolDefinition,
|
||||
TypeRef,
|
||||
} from 'gitnexus-shared';
|
||||
|
||||
/** Keep parameter (incl. `self`) typeBindings in the function scope —
|
||||
* hoisting them to Module would pollute other functions' receiver
|
||||
* resolution (same rationale as `goBindingScopeFor`). */
|
||||
export function zigBindingScopeFor(
|
||||
decl: CaptureMatch,
|
||||
innermost: Scope,
|
||||
_tree: ScopeTree,
|
||||
): ScopeId | null {
|
||||
if (decl['@type-binding.parameter'] !== undefined) {
|
||||
return innermost.id;
|
||||
}
|
||||
return null; // default auto-hoist for other bindings
|
||||
}
|
||||
|
||||
/** Zig's receiver convention is a first parameter named `self`; the
|
||||
* `self`-sourced typeBinding on the function scope carries its type. */
|
||||
export function zigReceiverBinding(functionScope: Scope): TypeRef | null {
|
||||
if (functionScope.kind !== 'Function') return null;
|
||||
for (const binding of functionScope.typeBindings.values()) {
|
||||
if (binding.source === 'self') return binding;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const TIER: Record<BindingRef['origin'], number> = {
|
||||
local: 0,
|
||||
namespace: 1,
|
||||
import: 2,
|
||||
reexport: 3,
|
||||
wildcard: 4,
|
||||
};
|
||||
|
||||
/** Local declarations shadow imports; deterministic order within a tier. */
|
||||
export function zigMergeBindings(
|
||||
existing: readonly BindingRef[],
|
||||
incoming: readonly BindingRef[],
|
||||
_scopeId: string,
|
||||
): BindingRef[] {
|
||||
const seen = new Set<string>();
|
||||
return [...existing, ...incoming]
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(TIER[a.origin] ?? 99) - (TIER[b.origin] ?? 99) || a.def.nodeId.localeCompare(b.def.nodeId),
|
||||
)
|
||||
.filter((binding) => {
|
||||
if (seen.has(binding.def.nodeId)) return false;
|
||||
seen.add(binding.def.nodeId);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/** Zig has no overloading; without synthesized arity metadata on
|
||||
* declarations the comparison is always 'unknown' — kept as a real
|
||||
* bounds check so it turns on if arity captures are added later. */
|
||||
export function zigArityCompatibility(
|
||||
callsite: Callsite,
|
||||
def: SymbolDefinition,
|
||||
): 'compatible' | 'unknown' | 'incompatible' {
|
||||
const max = def.parameterCount;
|
||||
const min = def.requiredParameterCount;
|
||||
if (max === undefined && min === undefined) return 'unknown';
|
||||
if (!Number.isFinite(callsite.arity) || callsite.arity < 0) return 'unknown';
|
||||
if (min !== undefined && callsite.arity < min) return 'incompatible';
|
||||
if (max !== undefined && callsite.arity > max) return 'incompatible';
|
||||
return 'compatible';
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@ import { cobolScopeResolver } from '../../languages/cobol/scope-resolver.js';
|
|||
import { swiftScopeResolver } from '../../languages/swift/scope-resolver.js';
|
||||
import { dartScopeResolver } from '../../languages/dart/scope-resolver.js';
|
||||
import { vueScopeResolver } from '../../languages/vue/scope-resolver.js';
|
||||
import { zigScopeResolver } from '../../languages/zig/scope-resolver.js';
|
||||
|
||||
/** Map of `SupportedLanguages` → `ScopeResolver`. The scope-resolution phase
|
||||
* iterates this map directly — every registered resolver runs. This is the
|
||||
|
|
@ -51,4 +52,5 @@ export const SCOPE_RESOLVERS: ReadonlyMap<SupportedLanguages, ScopeResolver> = n
|
|||
[SupportedLanguages.Swift, swiftScopeResolver],
|
||||
[SupportedLanguages.Dart, dartScopeResolver],
|
||||
[SupportedLanguages.Vue, vueScopeResolver],
|
||||
[SupportedLanguages.Zig, zigScopeResolver],
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import path from 'path';
|
||||
import {
|
||||
edgeSet,
|
||||
FIXTURES,
|
||||
getNodesByLabel,
|
||||
getRelationships,
|
||||
|
|
@ -41,17 +42,22 @@ describe('Zig basic resolution', () => {
|
|||
expect(methods).toContain('reset');
|
||||
});
|
||||
|
||||
// IMPORTS (and CALLS) edges are produced by the scope-resolution pipeline,
|
||||
// which requires the provider to implement `emitScopeCaptures` /
|
||||
// `interpretImport`. Zig is classified `experimental` and does not provide
|
||||
// those hooks yet — the import RESOLVER itself (relative paths +
|
||||
// build.zig.zon, see test/unit/zig-import-resolver.test.ts) is wired into
|
||||
// the resolver factory and becomes live the moment the hooks land.
|
||||
// Un-skip when Zig gains scope-resolution hooks.
|
||||
it.skip('resolves the relative @import("./pioneer.zig") to pioneer.zig', () => {
|
||||
it('resolves the relative @import("./pioneer.zig") to pioneer.zig', () => {
|
||||
const imports = getRelationships(result, 'IMPORTS');
|
||||
const internal = imports.filter((e) => e.targetFilePath.endsWith('pioneer.zig'));
|
||||
expect(internal.length).toBeGreaterThan(0);
|
||||
expect(internal[0].sourceFilePath).toContain('main.zig');
|
||||
});
|
||||
|
||||
it('emits a CALLS edge for the free call main → helper', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
expect(edgeSet(calls)).toContain('main → helper');
|
||||
});
|
||||
|
||||
it('emits a CALLS edge for the receiver-bound method call main → tick', () => {
|
||||
const calls = getRelationships(result, 'CALLS');
|
||||
// `var p = pioneer.Pioneer{…}; p.tick()` — constructor-inferred receiver
|
||||
// type through the namespace import, dispatched onto Pioneer.tick.
|
||||
expect(edgeSet(calls)).toContain('main → tick');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue