mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
fix(zig): give function-local and anonymous containers an identity (F8)
`const R = struct {…}` declared inside a fn (Lightpanda's reflection.zig
has ~20, one per builder) all collapsed onto one `Struct:<file>:R` with one
`R.get`; anonymous containers (`std.sort.pdq(…, struct { fn lessThan … }
.lessThan)`, `const byte_size = struct { fn it … }.it;`, `?struct { min,
max }` field types) had no identity at all, so their fns were OWNERLESS
Methods (`Method:<file>:lessThan#3`) that collided across a file.
`zigContainerName` now yields the graph IDENTITY on both phases:
- function-local named: `<enclosing callable>$<name>` — `Reflect.string$R`
(Java local-class `$` chain; `populateClassOwnedMembers` leaves it whole);
- anonymous: `<host>$<ordinal>` — `build$1`, `Outer$1`, `Page$1`
(javac's `Outer$1` numbering per host, in source order);
- a `test` host is keyed `test@L<line>` (its string does not survive the
class extractor's qualified-name normalization).
`zigContainerBindingName` keeps the spelling code writes (`R`) for scope
bindings and `@This()` alias rewrites (`@declaration.binding-name`).
Structure phase: bare `(struct|enum|union|opaque_declaration)` rules mint the
local/anonymous nodes via the class extractor; `shouldSkipDefinitionCapture`
keeps exactly one rule per container (`zigContainerAnchor`); a new
grammar-neutral `resolveContainerTypeOwner` provider hook lets the shared
owner walk name a container from context, so `Method:<file>:Reflect.string$R
.get#0` and its HAS_METHOD source agree by construction. Scope phase: the
wrapper group splits name/binding-name for locals and anonymous containers
get synthesized `@declaration.<kind>` defs (`is-synthetic`).
Lightpanda: ownerless Methods 14 → 0, fns without a node 55 → 0, ownerless
Properties 276 → 5, HAS_METHOD 8003 → 8074, HAS_PROPERTY 7275 → 7403,
CALLS 15838 → 15857, Struct 1905 → 2199; no resolution bucket dropped.
This commit is contained in:
parent
9f958557d6
commit
4ec68c5f82
12 changed files with 833 additions and 23 deletions
|
|
@ -4,6 +4,7 @@ import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
|||
import {
|
||||
isZigFileStruct,
|
||||
ZIG_CONTAINER_TYPES,
|
||||
zigContainerLabel,
|
||||
zigContainerName,
|
||||
} from '../../languages/zig/captures.js';
|
||||
|
||||
|
|
@ -12,11 +13,16 @@ import {
|
|||
*
|
||||
* const Point = struct { ... };
|
||||
* pub fn List(comptime T: type) type { return struct { ... }; }
|
||||
* fn build() void { const R = struct { ... }; sort(struct { fn lt … }.lt); }
|
||||
*
|
||||
* The binding name is the first identifier child of the parent
|
||||
* variable_declaration, or the generic type constructor's name —
|
||||
* `zigContainerName` is the single source shared with the field/method
|
||||
* extractors so owner ids and node ids agree by construction.
|
||||
* The identity is the binding name (first identifier of the parent
|
||||
* variable_declaration), the generic type constructor's name, or — for a
|
||||
* FUNCTION-LOCAL or ANONYMOUS container — a synthesized `host$Name` /
|
||||
* `host$N` (F8). `zigContainerName` is the single source shared with the
|
||||
* field/method extractors and the owner walk, so owner ids and node ids
|
||||
* agree by construction. Which of the (up to three) ZIG_QUERIES rules that
|
||||
* match one container gets to mint it is decided by the provider's
|
||||
* `shouldSkipDefinitionCapture` (`isZigRedundantContainerCapture`).
|
||||
*/
|
||||
const extractZigContainerName = (node: SyntaxNode, filePath?: string): string | undefined =>
|
||||
zigContainerName(node, filePath);
|
||||
|
|
@ -25,13 +31,7 @@ const extractZigContainerType = (node: SyntaxNode): ClassLikeNodeLabel | undefin
|
|||
// The file itself, when it declares top-level fields (file-struct); a
|
||||
// namespace-only file is not a type — `extract` then yields no symbol.
|
||||
if (node.type === 'source_file') return isZigFileStruct(node) ? 'Struct' : undefined;
|
||||
if (node.type === 'struct_declaration') return 'Struct';
|
||||
if (node.type === 'enum_declaration') return 'Enum';
|
||||
if (node.type === 'union_declaration') return 'Union';
|
||||
// `opaque {}` is a fieldless container that may own methods — Struct is the
|
||||
// closest class-like label (rationale in ZIG_QUERIES).
|
||||
if (node.type === 'opaque_declaration') return 'Struct';
|
||||
return undefined;
|
||||
return zigContainerLabel(node);
|
||||
};
|
||||
|
||||
export const zigClassConfig: ClassExtractionConfig = {
|
||||
|
|
|
|||
|
|
@ -174,6 +174,26 @@ interface LanguageProviderConfig {
|
|||
filePath: string,
|
||||
) => { readonly name: string; readonly label: NodeLabel } | null;
|
||||
|
||||
/**
|
||||
* The type a CONTAINER node declares, when the language names it from its
|
||||
* context rather than from a name child of the node — a binding wrapper,
|
||||
* an enclosing callable, an ordinal among anonymous siblings (Zig:
|
||||
* `const T = struct {…}` is `T`; a function-local `const R = struct {…}`
|
||||
* inside `fn string` is `string$R`; `struct { fn lessThan … }.lessThan`
|
||||
* passed to a sort is `<fn>$1`).
|
||||
*
|
||||
* Consulted by the enclosing-owner walk for every `CLASS_CONTAINER_TYPES`
|
||||
* node it meets (after `resolveEnclosingOwner` remapping), BEFORE the
|
||||
* generic name-child derivation; return `null` to fall back to it. The name
|
||||
* must be the one the class-like node is minted under
|
||||
* (`<label>:<file>:<name>`), so a member's owner id and the node id agree
|
||||
* by construction.
|
||||
* Default: undefined (containers are named by the generic derivation). */
|
||||
readonly resolveContainerTypeOwner?: (
|
||||
container: SyntaxNode,
|
||||
filePath: string,
|
||||
) => { readonly name: string; readonly label: NodeLabel } | null;
|
||||
|
||||
// ── Enclosing function resolution ───────────────────────────────
|
||||
/** Resolve the enclosing function name + label from an AST ancestor node
|
||||
* that is NOT a standard FUNCTION_NODE_TYPE. For languages where the
|
||||
|
|
|
|||
|
|
@ -53,11 +53,15 @@ import {
|
|||
interpretZigTypeBinding,
|
||||
isZigContainerMethod,
|
||||
isZigFileStruct,
|
||||
isZigRedundantContainerCapture,
|
||||
isZigTypeShadowingBinding,
|
||||
zigArityCompatibility,
|
||||
zigContainerLabel,
|
||||
zigContainerName,
|
||||
zigFileStructName,
|
||||
zigBindingScopeFor,
|
||||
zigReceiverBinding,
|
||||
ZIG_CONTAINER_TYPES,
|
||||
} from './zig/index.js';
|
||||
|
||||
export const zigProvider = defineLanguage({
|
||||
|
|
@ -79,16 +83,40 @@ export const zigProvider = defineLanguage({
|
|||
variableExtractor: createVariableExtractor(zigVariableConfig),
|
||||
// A `const`/`var` whose value is a container or an `@import` is the
|
||||
// Struct/Enum/Union node or the import binding, not a Const beside it.
|
||||
// Up to three ZIG_QUERIES rules match one container (wrapper, type
|
||||
// constructor, bare container — F8); `zigContainerAnchor` names the one
|
||||
// that mints it and the others are dropped here.
|
||||
shouldSkipDefinitionCapture: (captureMap, defaultLabel) => {
|
||||
if (defaultLabel !== 'Const' && defaultLabel !== 'Variable') return false;
|
||||
const decl = captureMap['definition.const'] ?? captureMap['definition.variable'];
|
||||
return decl !== undefined && isZigTypeShadowingBinding(decl);
|
||||
if (defaultLabel === 'Const' || defaultLabel === 'Variable') {
|
||||
const decl = captureMap['definition.const'] ?? captureMap['definition.variable'];
|
||||
return decl !== undefined && isZigTypeShadowingBinding(decl);
|
||||
}
|
||||
if (defaultLabel === 'Struct' || defaultLabel === 'Enum' || defaultLabel === 'Union') {
|
||||
const decl =
|
||||
captureMap['definition.struct'] ??
|
||||
captureMap['definition.enum'] ??
|
||||
captureMap['definition.union'];
|
||||
return decl !== undefined && isZigRedundantContainerCapture(decl, captureMap['name']);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
// A file whose top level declares fields IS a struct named after the file
|
||||
// (`Page.zig` → `Page`): its top-level fns/fields are members of that
|
||||
// Struct. Files without fields are namespaces and own nothing.
|
||||
resolveFileTypeOwner: (root, filePath) =>
|
||||
isZigFileStruct(root) ? { name: zigFileStructName(filePath), label: 'Struct' } : null,
|
||||
// Every container's identity comes from `zigContainerName` — the binding
|
||||
// name for `const T = struct {…}` at file/container level, the fn name for
|
||||
// a generic type constructor, and (F8) `string$R` / `build$1` for
|
||||
// function-local and anonymous containers, which no name child spells. The
|
||||
// class extractor names the node from the same function, so a member's
|
||||
// owner id (`Method:<file>:string$R.get`) and the node id agree.
|
||||
resolveContainerTypeOwner: (container, filePath) => {
|
||||
if (!ZIG_CONTAINER_TYPES.has(container.type)) return null;
|
||||
const name = zigContainerName(container, filePath);
|
||||
const label = zigContainerLabel(container);
|
||||
return name !== undefined && label !== undefined ? { name, label } : null;
|
||||
},
|
||||
labelOverride: (functionNode, defaultLabel) => {
|
||||
if (defaultLabel !== 'Function') return defaultLabel;
|
||||
if (isZigContainerMethod(functionNode)) return 'Method';
|
||||
|
|
|
|||
|
|
@ -143,16 +143,24 @@ export function isZigPublishingImport(declNode: SyntaxNode): boolean {
|
|||
return declNode.parent?.type === 'source_file' && hasZigPubKeyword(declNode);
|
||||
}
|
||||
|
||||
/** The binding name of a Zig container node, or undefined for a truly
|
||||
* anonymous one. Two shapes carry a name:
|
||||
/** The BINDING name of a Zig container node — the spelling code uses to
|
||||
* refer to it — or undefined for a truly anonymous one. Three shapes carry
|
||||
* one:
|
||||
* - a file-struct: the file stem (`Page.zig` → `Page`);
|
||||
* - `const Point = struct {…}` — the first identifier of the wrapping
|
||||
* `variable_declaration`;
|
||||
* - `pub fn List(comptime T: type) type { return struct {…}; }` — the
|
||||
* generic type constructor. Zig has no other spelling for a generic
|
||||
* type, and every reader calls the returned container `List`, so the
|
||||
* enclosing function's name IS the type name (`ArrayList(u8)`).
|
||||
* Single source for the class/field/method extractor configs. */
|
||||
export function zigContainerName(containerNode: SyntaxNode, filePath?: string): string | undefined {
|
||||
* This is what scope-side NAME BINDINGS use (`R.get()`, `self: *R`, a
|
||||
* `const Self = @This();` rewrite). The graph IDENTITY of a container is
|
||||
* `zigContainerName`, which equals the binding name except for the
|
||||
* function-local and anonymous shapes (F8). */
|
||||
export function zigContainerBindingName(
|
||||
containerNode: SyntaxNode,
|
||||
filePath?: string,
|
||||
): string | undefined {
|
||||
if (containerNode.type === 'source_file') {
|
||||
return filePath !== undefined && isZigFileStruct(containerNode)
|
||||
? zigFileStructName(filePath)
|
||||
|
|
@ -170,6 +178,210 @@ export function zigContainerName(containerNode: SyntaxNode, filePath?: string):
|
|||
return zigTypeConstructorOf(containerNode)?.childForFieldName('name')?.text;
|
||||
}
|
||||
|
||||
/** The graph IDENTITY of a Zig container node — the name its class-like
|
||||
* node, its members' owner segment (`Method:<file>:<name>.<fn>`) and the
|
||||
* scope-side def all carry. Single source for the class/field/method
|
||||
* extractor configs, the provider's `resolveContainerTypeOwner` hook (the
|
||||
* structure phase's owner walk) and `emitZigScopeCaptures`, so the two
|
||||
* phases agree by construction.
|
||||
*
|
||||
* - file-struct / `const T = struct {…}` at file or container level /
|
||||
* generic type constructor: the binding name (`zigContainerBindingName`).
|
||||
* - FUNCTION-LOCAL named container (F8) — `const R = struct {…}` inside a
|
||||
* `fn` or `test` body: `<enclosing callable>$<name>`, e.g. `string$R`,
|
||||
* `Reflect.string$R`. Zig code declares such helper containers per
|
||||
* builder function (Lightpanda's `reflection.zig` has ~20 `const R =
|
||||
* struct { fn get… fn set… }`), and by binding name alone they all
|
||||
* collapsed onto ONE `Struct:<file>:R` with one `R.get`. Java's local
|
||||
* classes are keyed the same way (`Outer$1Local`); the `$` is what tells
|
||||
* the shared `populateClassOwnedMembers` the name is already complete.
|
||||
* Two same-named locals in sibling blocks of ONE callable still share
|
||||
* an identity (Zig code does not do that; javac's ordinal would).
|
||||
* - ANONYMOUS container (F8) — `struct { fn lessThan(…) … }.lessThan`
|
||||
* passed to `std.sort.pdq`, `const cmp = struct {…}.lt;`, a field or
|
||||
* parameter typed `struct { min: u32 }`: `<host>$<ordinal>` where the
|
||||
* host is the enclosing callable (`build$1`), else the enclosing
|
||||
* container (`Outer$1`), else the file stem (`util$1`), and the ordinal
|
||||
* counts the anonymous containers of that host in source order —
|
||||
* javac's `Outer$1` rule (`synthesizeJavaTypeIdentity`). Without an
|
||||
* identity their fns were OWNERLESS Methods (`Method:<file>:lessThan`),
|
||||
* and same-named ones in one file collided on a single node.
|
||||
* Deterministic in the tree alone, so any two walks over one source agree. */
|
||||
export function zigContainerName(containerNode: SyntaxNode, filePath?: string): string | undefined {
|
||||
const binding = zigContainerBindingName(containerNode, filePath);
|
||||
if (containerNode.type === 'source_file') return binding;
|
||||
if (!ZIG_CONTAINER_TYPES.has(containerNode.type)) return undefined;
|
||||
const host = zigIdentityHost(containerNode);
|
||||
if (binding !== undefined) {
|
||||
if (containerNode.parent?.type !== 'variable_declaration' || host === null) return binding;
|
||||
if (!isZigCallableNode(host)) return binding; // file / container level: unchanged
|
||||
return `${zigCallableQualifiedName(host, filePath)}$${binding}`;
|
||||
}
|
||||
const ordinal = zigAnonymousContainerOrdinal(containerNode);
|
||||
const prefix = host === null ? undefined : zigAnonymousHostPrefix(host, filePath);
|
||||
return `${prefix ?? ''}$${ordinal}`;
|
||||
}
|
||||
|
||||
/** The label the graph gives a container node (`opaque {}` is a fieldless
|
||||
* container that may own methods — Struct, see ZIG_QUERIES). */
|
||||
export function zigContainerLabel(
|
||||
containerNode: SyntaxNode,
|
||||
): 'Struct' | 'Enum' | 'Union' | undefined {
|
||||
switch (containerNode.type) {
|
||||
case 'struct_declaration':
|
||||
case 'opaque_declaration':
|
||||
return 'Struct';
|
||||
case 'enum_declaration':
|
||||
return 'Enum';
|
||||
case 'union_declaration':
|
||||
return 'Union';
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Which ZIG_QUERIES rule mints a container's graph node (F8):
|
||||
* - 'wrapper': `const T = struct {…}` at file or container level — the
|
||||
* `variable_declaration … @definition.struct` rule (name from `@name`);
|
||||
* - 'constructor': the container a generic type constructor returns —
|
||||
* the `fn … type { return struct {…}; }` rule;
|
||||
* - 'container': everything the bare `(struct_declaration)
|
||||
* @definition.struct` rule owns — FUNCTION-LOCAL named containers (their
|
||||
* identity `string$R` is not a capture, so the class extractor names
|
||||
* them via `zigContainerName`) and ANONYMOUS containers.
|
||||
* The provider's `shouldSkipClassCapture` drops the other rules' matches for
|
||||
* the same node so each container is minted exactly once. */
|
||||
export function zigContainerAnchor(
|
||||
containerNode: SyntaxNode,
|
||||
): 'wrapper' | 'constructor' | 'container' | 'file' {
|
||||
if (containerNode.type === 'source_file') return 'file';
|
||||
if (zigTypeConstructorOf(containerNode) !== null) return 'constructor';
|
||||
if (
|
||||
containerNode.parent?.type === 'variable_declaration' &&
|
||||
zigContainerBindingName(containerNode) !== undefined
|
||||
) {
|
||||
const host = zigIdentityHost(containerNode);
|
||||
return host !== null && isZigCallableNode(host) ? 'container' : 'wrapper';
|
||||
}
|
||||
return 'container';
|
||||
}
|
||||
|
||||
/** Is this `@definition.<container>` match of ZIG_QUERIES REDUNDANT — i.e.
|
||||
* minted by another rule for the same container (F8)? `definitionNode` is
|
||||
* the match anchor (the wrapper `variable_declaration`, or the container
|
||||
* node for the type-constructor and bare-container rules), `nameNode` its
|
||||
* `@name` capture (the type-constructor rule captures the fn's name, the
|
||||
* bare rule captures none). Wired through the provider's
|
||||
* `shouldSkipDefinitionCapture`, which the definition phase consults for
|
||||
* EVERY label — `shouldSkipClassCapture` would not see `Union`. */
|
||||
export function isZigRedundantContainerCapture(
|
||||
definitionNode: SyntaxNode,
|
||||
nameNode: SyntaxNode | undefined,
|
||||
): boolean {
|
||||
let container: SyntaxNode | undefined;
|
||||
if (ZIG_CONTAINER_TYPES.has(definitionNode.type)) {
|
||||
container = definitionNode;
|
||||
} else if (definitionNode.type === 'variable_declaration') {
|
||||
for (let i = 0; i < definitionNode.namedChildCount; i++) {
|
||||
const child = definitionNode.namedChild(i);
|
||||
if (child !== null && ZIG_CONTAINER_TYPES.has(child.type)) {
|
||||
container = child;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (container === undefined) return false;
|
||||
const anchor = zigContainerAnchor(container);
|
||||
if (definitionNode.type === 'variable_declaration') return anchor !== 'wrapper';
|
||||
return nameNode !== undefined ? anchor !== 'constructor' : anchor !== 'container';
|
||||
}
|
||||
|
||||
/** A callable body in Zig: a `fn` or a `test` block. */
|
||||
function isZigCallableNode(node: SyntaxNode): boolean {
|
||||
return node.type === 'function_declaration' || node.type === 'test_declaration';
|
||||
}
|
||||
|
||||
/** The nearest ancestor that hosts identities — a callable, a container or
|
||||
* the file — starting from `node`'s parent. */
|
||||
function zigIdentityHost(node: SyntaxNode): SyntaxNode | null {
|
||||
let cur = node.parent;
|
||||
while (cur !== null && cur !== undefined) {
|
||||
if (isZigCallableNode(cur) || ZIG_CONTAINER_TYPES.has(cur.type) || cur.type === 'source_file') {
|
||||
return cur;
|
||||
}
|
||||
cur = cur.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** A callable's qualified name as the structure phase spells it: its owner
|
||||
* container's identity, a dot, its own name (`Reflect.string`, `Page.init`,
|
||||
* `helper` in a namespace file). A `test` block is keyed by its LINE
|
||||
* (`test@L<line>`, 1-based; a test is a top-level declaration, so the line
|
||||
* identifies it), not by its string: the class extractor's
|
||||
* `buildQualifiedName` normalizes a graph node's `qualifiedName`
|
||||
* (whitespace stripped, `::` → `.`), so an identity carrying `"Function:
|
||||
* requested termination …"` would key the node under one spelling and the
|
||||
* scope def under another, and `State{}` in that test would resolve to
|
||||
* nothing. No `:` either (`row:col` would do), so an id keeps a single
|
||||
* `label:file:name` colon structure for anything that splits on it. */
|
||||
export function zigCallableQualifiedName(fnNode: SyntaxNode, filePath?: string): string {
|
||||
const own =
|
||||
fnNode.type === 'test_declaration'
|
||||
? `test@L${fnNode.startPosition.row + 1}`
|
||||
: (fnNode.childForFieldName('name')?.text ?? `fn@L${fnNode.startPosition.row + 1}`);
|
||||
const host = zigIdentityHost(fnNode);
|
||||
if (host === null) return own;
|
||||
const hostName = ZIG_CONTAINER_TYPES.has(host.type)
|
||||
? zigContainerName(host, filePath)
|
||||
: host.type === 'source_file'
|
||||
? zigContainerBindingName(host, filePath) // file-struct stem; nothing for a namespace file
|
||||
: zigCallableQualifiedName(host, filePath);
|
||||
return hostName === undefined ? own : `${hostName}.${own}`;
|
||||
}
|
||||
|
||||
/** The `<host>` part of an anonymous container's identity. */
|
||||
function zigAnonymousHostPrefix(host: SyntaxNode, filePath?: string): string | undefined {
|
||||
if (isZigCallableNode(host)) return zigCallableQualifiedName(host, filePath);
|
||||
if (ZIG_CONTAINER_TYPES.has(host.type)) return zigContainerName(host, filePath);
|
||||
// File level: the file stem — a Zig file is itself a struct, so this is
|
||||
// javac's `Outer$1` with the file as `Outer` (`Page$1`, `util$1`).
|
||||
return filePath === undefined ? undefined : zigFileStructName(filePath);
|
||||
}
|
||||
|
||||
/** 1-based ordinal of an anonymous container among the anonymous containers
|
||||
* sharing its identity host, in source order. Computed once per tree and
|
||||
* memoized on the tree object (a node without `.tree` recomputes from its
|
||||
* root); position-derived, so two parses of one source agree. */
|
||||
const zigAnonymousOrdinalMemo = new WeakMap<object, Map<number, number>>();
|
||||
function zigAnonymousContainerOrdinal(containerNode: SyntaxNode): number {
|
||||
const tree = (containerNode as { tree?: object }).tree;
|
||||
let table = tree === undefined ? undefined : zigAnonymousOrdinalMemo.get(tree);
|
||||
if (table === undefined) {
|
||||
let root: SyntaxNode = containerNode;
|
||||
while (root.parent !== null && root.parent !== undefined) root = root.parent;
|
||||
const built = new Map<number, number>();
|
||||
const perHost = new Map<number, number>();
|
||||
const visit = (node: SyntaxNode): void => {
|
||||
if (ZIG_CONTAINER_TYPES.has(node.type) && zigContainerBindingName(node) === undefined) {
|
||||
const host = zigIdentityHost(node);
|
||||
const key = host === null ? -1 : host.id;
|
||||
const next = (perHost.get(key) ?? 0) + 1;
|
||||
perHost.set(key, next);
|
||||
built.set(node.id, next);
|
||||
}
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child !== null) visit(child);
|
||||
}
|
||||
};
|
||||
visit(root);
|
||||
if (tree !== undefined) zigAnonymousOrdinalMemo.set(tree, built);
|
||||
table = built;
|
||||
}
|
||||
return table.get(containerNode.id) ?? 0;
|
||||
}
|
||||
|
||||
/** For a container that is the direct `return` value of a function whose
|
||||
* return type is `type` — `fn List(comptime T: type) type { return struct
|
||||
* {…}; }` — the function_declaration; null for any other placement. Only the
|
||||
|
|
@ -242,10 +454,15 @@ function republishMarker(stmt: SyntaxNode): Record<string, Capture> {
|
|||
* returns a map from the DECLARING container node id to `[aliasName,
|
||||
* containerName]`. The file-level alias maps to the file-struct name (only
|
||||
* when the file is a file-struct — a namespace-only file's `const js =
|
||||
* @This();` stays a Const and is not a type to rewrite to). */
|
||||
* @This();` stays a Const and is not a type to rewrite to). The container
|
||||
* side is the name the SCOPE BINDS: the binding name (`R` for a function-
|
||||
* local `const R = struct {…}`, whose identity `string$R` is not a binding),
|
||||
* or the synthetic identity of an anonymous container, which is bound under
|
||||
* that very name (F8). */
|
||||
function collectZigThisAliases(
|
||||
root: SyntaxNode,
|
||||
fileStructName: string | undefined,
|
||||
filePath: string,
|
||||
): Map<number, { readonly alias: string; readonly container: string }> {
|
||||
const out = new Map<number, { readonly alias: string; readonly container: string }>();
|
||||
const visit = (node: SyntaxNode): void => {
|
||||
|
|
@ -265,7 +482,7 @@ function collectZigThisAliases(
|
|||
out.set(owner.id, { alias: named[0]!.text, container: fileStructName });
|
||||
}
|
||||
} else if (owner !== null && ZIG_CONTAINER_TYPES.has(owner.type)) {
|
||||
const containerName = zigContainerName(owner);
|
||||
const containerName = zigContainerBindingName(owner) ?? zigContainerName(owner, filePath);
|
||||
if (containerName !== undefined) {
|
||||
out.set(owner.id, { alias: named[0]!.text, container: containerName });
|
||||
}
|
||||
|
|
@ -711,7 +928,7 @@ export function emitZigScopeCaptures(
|
|||
}
|
||||
// `@This()` aliases: alias name ↦ the container it names (file stem for the
|
||||
// file-struct, binding name for `const Self = @This();` inside a container).
|
||||
const thisAliases = collectZigThisAliases(root, fileStructName);
|
||||
const thisAliases = collectZigThisAliases(root, fileStructName, _filePath);
|
||||
// F6: fn-local names per function node, for `zigCallReturnTypeOf`.
|
||||
const fnLocalNames = new Map<number, Set<string>>();
|
||||
|
||||
|
|
@ -971,6 +1188,23 @@ export function emitZigScopeCaptures(
|
|||
'true',
|
||||
);
|
||||
}
|
||||
// F8 — FUNCTION-LOCAL named container (`const R = struct {…}` inside a
|
||||
// fn body): the def's qualified name is its identity (`string$R`,
|
||||
// matching the graph node the structure phase mints via
|
||||
// `zigContainerName`), while the scope still binds the spelling code
|
||||
// uses (`R.get()`, `self: *R`) — the Java local-class split
|
||||
// (`@declaration.binding-name`, java/captures.ts).
|
||||
if (
|
||||
containerAnchor !== undefined &&
|
||||
nameNode !== undefined &&
|
||||
zigContainerAnchor(containerAnchor) === 'container'
|
||||
) {
|
||||
const identity = zigContainerName(containerAnchor, _filePath);
|
||||
if (identity !== undefined && identity !== nameNode.text) {
|
||||
grouped['@declaration.binding-name'] = grouped['@declaration.name']!;
|
||||
grouped['@declaration.name'] = syntheticCapture('@declaration.name', nameNode, identity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Relabel container-nested fns Function → Method (provider labelOverride
|
||||
|
|
@ -1097,7 +1331,49 @@ export function emitZigScopeCaptures(
|
|||
}
|
||||
}
|
||||
|
||||
// F8 — ANONYMOUS containers (`std.sort.pdq(…, struct { fn lessThan … }
|
||||
// .lessThan)`, `const cmp = struct { fn lt … }.lt;`, `clamp: ?struct { min:
|
||||
// u32 }`): no query rule binds them, so their Class scope owned no def and
|
||||
// their fns were ownerless, colliding Methods. Synthesize the def the
|
||||
// structure phase mints (`zigContainerName` → `build$1`), anchored on the
|
||||
// container node so it shares the `@scope.class` range: the def joins that
|
||||
// scope's ownedDefs (`populateClassOwnedMembers` then qualifies its members
|
||||
// `build$1.lessThan` and stamps their ownerId) and the name auto-hoists to
|
||||
// the enclosing scope, where a `const Self = @This();` rewrite can find it.
|
||||
out.push(...synthesizeZigAnonymousContainerDeclarations(root, _filePath));
|
||||
|
||||
out.push(...synthesizeCallableFlowCaptures(tree.rootNode, ZIG_CALLABLE_CAPTURE_OPTIONS));
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/** One `@declaration.<kind>` group per anonymous container in the tree —
|
||||
* see the call site in `emitZigScopeCaptures` (F8). Named after
|
||||
* `synthesizeJavaAnonymousClassDeclarations`, which does the same for
|
||||
* `new Runnable() {…}` bodies. */
|
||||
function synthesizeZigAnonymousContainerDeclarations(
|
||||
root: SyntaxNode,
|
||||
filePath: string,
|
||||
): CaptureMatch[] {
|
||||
const out: CaptureMatch[] = [];
|
||||
const visit = (node: SyntaxNode): void => {
|
||||
if (ZIG_CONTAINER_TYPES.has(node.type) && zigContainerBindingName(node) === undefined) {
|
||||
const identity = zigContainerName(node, filePath);
|
||||
const label = zigContainerLabel(node);
|
||||
if (identity !== undefined && label !== undefined) {
|
||||
const tag = `@declaration.${label.toLowerCase()}`;
|
||||
out.push({
|
||||
[tag]: nodeToCapture(tag, node),
|
||||
'@declaration.name': syntheticCapture('@declaration.name', node, identity),
|
||||
'@declaration.is-synthetic': syntheticCapture('@declaration.is-synthetic', node, 'true'),
|
||||
});
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const child = node.namedChild(i);
|
||||
if (child !== null) visit(child);
|
||||
}
|
||||
};
|
||||
visit(root);
|
||||
return out;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,12 @@ export {
|
|||
isZigFileStruct,
|
||||
isZigFileThisAlias,
|
||||
isZigKeywordDeclaration,
|
||||
isZigRedundantContainerCapture,
|
||||
isZigTypeShadowingBinding,
|
||||
zigCallableQualifiedName,
|
||||
zigContainerAnchor,
|
||||
zigContainerBindingName,
|
||||
zigContainerLabel,
|
||||
zigContainerName,
|
||||
zigFileStructName,
|
||||
zigImportRootOf,
|
||||
|
|
|
|||
|
|
@ -2518,6 +2518,21 @@ export const ZIG_QUERIES = `
|
|||
(enum_declaration) @definition.enum))))
|
||||
(#eq? @_ret "type"))
|
||||
|
||||
; Function-local and anonymous containers (F8): \`fn string() { const R =
|
||||
; struct { fn get … }; }\` (Lightpanda's reflection.zig declares one \`R\` per
|
||||
; builder fn), \`std.sort.pdq(T, items, {}, struct { fn lessThan … }.lessThan)\`,
|
||||
; \`const cmp = struct { fn lt … }.lt;\`, a field typed \`?struct { min: u32 }\`.
|
||||
; No name child spells their identity, so these rules match EVERY container
|
||||
; and the class extractor names the node from \`zigContainerName\` (\`string$R\`,
|
||||
; \`build$1\`) — the same function the owner walk uses for their fns, which
|
||||
; were ownerless, colliding Methods before. The bound shapes above match too;
|
||||
; the provider's \`shouldSkipDefinitionCapture\` keeps exactly one rule per
|
||||
; container (\`zigContainerAnchor\`).
|
||||
(struct_declaration) @definition.struct
|
||||
(enum_declaration) @definition.enum
|
||||
(union_declaration) @definition.union
|
||||
(opaque_declaration) @definition.struct
|
||||
|
||||
; Container fields (struct fields, enum variants, union variants) — all are
|
||||
; \`container_field\` in the grammar and all become Property (C labels its
|
||||
; enumerators Const; Rust captures no variants; Zig's own vocabulary is
|
||||
|
|
|
|||
|
|
@ -906,6 +906,18 @@ export const findEnclosingClassInfo = (
|
|||
root: SyntaxNode,
|
||||
filePath: string,
|
||||
) => { readonly name: string; readonly label: NodeLabel } | null,
|
||||
/**
|
||||
* Optional: the type a CONTAINER node declares
|
||||
* (`LanguageProvider.resolveContainerTypeOwner`). Consulted for every
|
||||
* `CLASS_CONTAINER_TYPES` node the walk meets, before the generic name-child
|
||||
* derivation, for languages whose containers are named from context (a
|
||||
* binding wrapper, an enclosing callable, an anonymous ordinal). Null falls
|
||||
* through to the generic derivation.
|
||||
*/
|
||||
resolveContainerTypeOwner?: (
|
||||
container: SyntaxNode,
|
||||
filePath: string,
|
||||
) => { readonly name: string; readonly label: NodeLabel } | null,
|
||||
): EnclosingClassInfo | null => {
|
||||
let current = node.parent;
|
||||
let iterations = 0;
|
||||
|
|
@ -1003,6 +1015,19 @@ export const findEnclosingClassInfo = (
|
|||
}
|
||||
}
|
||||
|
||||
// A container the PROVIDER names from context (binding wrapper,
|
||||
// enclosing callable, anonymous ordinal — Zig). The name is what the
|
||||
// class-like node is minted under, so owner id == node id.
|
||||
if (resolveContainerTypeOwner !== undefined) {
|
||||
const containerOwner = resolveContainerTypeOwner(current, filePath);
|
||||
if (containerOwner !== null) {
|
||||
return {
|
||||
classId: generateId(containerOwner.label, `${filePath}:${containerOwner.name}`),
|
||||
className: containerOwner.name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Rust impl_item: for `impl Trait for Struct {}`, pick the type after `for`
|
||||
// NOTE: This impl_item ownership logic is mirrored in
|
||||
// method-extractors/configs/rust.ts (extractOwnerName, metadata only).
|
||||
|
|
|
|||
|
|
@ -942,6 +942,7 @@ const callableOwnQualifiedName = (
|
|||
provider.resolveEnclosingOwner,
|
||||
undefined,
|
||||
provider.resolveFileTypeOwner,
|
||||
provider.resolveContainerTypeOwner,
|
||||
)
|
||||
: null;
|
||||
const owner = prefix ?? classInfo?.className;
|
||||
|
|
@ -991,6 +992,7 @@ const findEnclosingFunctionId = (
|
|||
provider.resolveEnclosingOwner,
|
||||
undefined,
|
||||
provider.resolveFileTypeOwner,
|
||||
provider.resolveContainerTypeOwner,
|
||||
);
|
||||
const encLang = getLanguageFromFilename(filePath);
|
||||
const standaloneMethodInfo =
|
||||
|
|
@ -1079,6 +1081,7 @@ const findEnclosingFunctionId = (
|
|||
provider.resolveEnclosingOwner,
|
||||
undefined,
|
||||
provider.resolveFileTypeOwner,
|
||||
provider.resolveContainerTypeOwner,
|
||||
);
|
||||
// Same nesting rule as the generic branch above (#2699). Anchored on
|
||||
// `sigNode`-equivalent (`current.previousSibling ?? current`) so Dart,
|
||||
|
|
@ -1144,6 +1147,7 @@ const cachedFindEnclosingClassInfo = (
|
|||
resolveEnclosingOwner?: (node: SyntaxNode) => SyntaxNode | null,
|
||||
getQualifiedOwnerName?: (node: SyntaxNode, simpleName: string) => string | null,
|
||||
resolveFileTypeOwner?: LanguageProvider['resolveFileTypeOwner'],
|
||||
resolveContainerTypeOwner?: LanguageProvider['resolveContainerTypeOwner'],
|
||||
): EnclosingClassInfo | null => {
|
||||
const cached = classIdCache.get(node);
|
||||
if (cached !== undefined) return cached;
|
||||
|
|
@ -1154,6 +1158,7 @@ const cachedFindEnclosingClassInfo = (
|
|||
resolveEnclosingOwner,
|
||||
getQualifiedOwnerName,
|
||||
resolveFileTypeOwner,
|
||||
resolveContainerTypeOwner,
|
||||
);
|
||||
classIdCache.set(node, result);
|
||||
return result;
|
||||
|
|
@ -2000,6 +2005,7 @@ const processFileGroup = (
|
|||
provider.resolveEnclosingOwner,
|
||||
propGetQualifiedOwnerName,
|
||||
provider.resolveFileTypeOwner,
|
||||
provider.resolveContainerTypeOwner,
|
||||
);
|
||||
const propEnclosingClassId =
|
||||
propEnclosingInfo?.qualifiedClassId ?? propEnclosingInfo?.classId ?? null;
|
||||
|
|
@ -2407,6 +2413,7 @@ const processFileGroup = (
|
|||
provider.resolveEnclosingOwner,
|
||||
getQualifiedOwnerName,
|
||||
provider.resolveFileTypeOwner,
|
||||
provider.resolveContainerTypeOwner,
|
||||
)
|
||||
: null;
|
||||
const enclosingClassId =
|
||||
|
|
|
|||
50
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/Sorter.zig
vendored
Normal file
50
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/Sorter.zig
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// A file-struct hosting ANONYMOUS containers (F8):
|
||||
// - `std.sort.pdq(T, items, {}, struct { fn lessThan … }.lessThan)` — the
|
||||
// comparator idiom (Lightpanda's ImportMap.zig has three in one file, all
|
||||
// named `lessThan`: one ownerless `Method:…:lessThan` node for all three);
|
||||
// - `const byteSize = struct { fn it … }.it;` — build.zig's shape;
|
||||
// - a field typed `?struct { min: u32, max: u32 }`.
|
||||
// Each gets an identity (`Sorter.sortBoth$1`, `Sorter.sortBoth$2`,
|
||||
// `Sorter$1`, `Sorter$2`) so its fns are Methods WITH an owner and never
|
||||
// collide.
|
||||
const std = @import("std");
|
||||
const Sorter = @This();
|
||||
|
||||
items: []u32,
|
||||
bounds: ?struct { min: u32, max: u32 } = null,
|
||||
|
||||
const byteSize = struct {
|
||||
fn it(n: u32) bool {
|
||||
return before(n, n);
|
||||
}
|
||||
}.it;
|
||||
|
||||
pub fn sortBoth(self: *Sorter) void {
|
||||
std.sort.pdq(u32, self.items, {}, struct {
|
||||
fn lessThan(_: void, a: u32, b: u32) bool {
|
||||
return before(a, b);
|
||||
}
|
||||
}.lessThan);
|
||||
std.sort.pdq(u32, self.items, {}, struct {
|
||||
fn lessThan(_: void, a: u32, b: u32) bool {
|
||||
return before(b, a);
|
||||
}
|
||||
}.lessThan);
|
||||
}
|
||||
|
||||
fn before(a: u32, b: u32) bool {
|
||||
return a < b;
|
||||
}
|
||||
|
||||
// A container local to a TEST block (Function.zig / HttpClient.zig tests
|
||||
// declare a `const State = struct {…}` per test): host is the quoted test name.
|
||||
test "Sorter: local state" {
|
||||
const State = struct {
|
||||
n: u32 = 0,
|
||||
fn kill(self: *@This()) void {
|
||||
self.n = if (before(self.n, 1)) 1 else 0;
|
||||
}
|
||||
};
|
||||
var state = State{};
|
||||
state.kill();
|
||||
}
|
||||
62
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/reflect.zig
vendored
Normal file
62
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/reflect.zig
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
// Lightpanda's `webapi/element/reflection.zig` shape (F8): a generic type
|
||||
// constructor whose builder fns each declare a FUNCTION-LOCAL helper container
|
||||
// named `R`. By binding name alone every `R` collapsed onto ONE `Struct:…:R`
|
||||
// with one `R.get` / one `R.set`; each must be its own type
|
||||
// (`Reflect.string$R`, `Reflect.url$R`) owning its own methods.
|
||||
const std = @import("std");
|
||||
|
||||
pub const Accessor = struct {
|
||||
get: *const anyopaque,
|
||||
set: *const anyopaque,
|
||||
};
|
||||
|
||||
pub fn Reflect(comptime T: type) type {
|
||||
return struct {
|
||||
pub fn string(comptime attr: []const u8) Accessor {
|
||||
const R = struct {
|
||||
fn get(self: *const T) []const u8 {
|
||||
return readAttr(self, attr);
|
||||
}
|
||||
fn set(self: *T, value: []const u8) void {
|
||||
writeAttr(self, attr, value);
|
||||
}
|
||||
};
|
||||
return Accessor{ .get = R.get, .set = R.set };
|
||||
}
|
||||
|
||||
pub fn url(comptime attr: []const u8) Accessor {
|
||||
const R = struct {
|
||||
// A `@This()` alias inside a function-local container names
|
||||
// THAT container (`R`), not the enclosing type constructor:
|
||||
// `self: *const Self` below must dispatch `self.get()` to
|
||||
// `Reflect.url$R.get`.
|
||||
const Self = @This();
|
||||
fn get(self: *const T) []const u8 {
|
||||
return normalize(readAttr(self, attr));
|
||||
}
|
||||
fn set(self: *T, value: []const u8) void {
|
||||
writeAttr(self, attr, value);
|
||||
}
|
||||
fn check(self: *const Self) []const u8 {
|
||||
return self.get();
|
||||
}
|
||||
};
|
||||
return Accessor{ .get = R.get, .set = R.set };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
fn readAttr(self: anytype, attr: []const u8) []const u8 {
|
||||
_ = self;
|
||||
return attr;
|
||||
}
|
||||
|
||||
fn writeAttr(self: anytype, attr: []const u8, value: []const u8) void {
|
||||
_ = self;
|
||||
_ = attr;
|
||||
_ = value;
|
||||
}
|
||||
|
||||
fn normalize(s: []const u8) []const u8 {
|
||||
return s;
|
||||
}
|
||||
|
|
@ -608,3 +608,134 @@ describe.skipIf(!zigAvailable)('Zig type aliases (zig-filestruct fixture, aliase
|
|||
expect(calls).toContain('viaDeref → bump');
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!zigAvailable)('Zig function-local and anonymous containers (F8)', () => {
|
||||
// reflect.zig mirrors Lightpanda's reflection.zig: a generic type
|
||||
// constructor whose builder fns each declare `const R = struct { fn get…
|
||||
// fn set… }`. Sorter.zig hosts the anonymous shapes: two `std.sort.pdq(…,
|
||||
// struct { fn lessThan … }.lessThan)` comparators in one fn (ImportMap.zig
|
||||
// has three), `const byteSize = struct { fn it … }.it;` (build.zig), a
|
||||
// field typed `?struct { min, max }`, and a test-local `const State`.
|
||||
// Before this modelling every `R` was one `Struct:…:R` with one `R.get`,
|
||||
// and every comparator's `lessThan` was an OWNERLESS `Method:<file>:lessThan`
|
||||
// — the corpus gate counted 14 ownerless Methods and 55 fns without a node.
|
||||
let result: PipelineResult;
|
||||
|
||||
beforeAll(async () => {
|
||||
result = await runPipelineFromRepo(path.join(FIXTURES, 'zig-filestruct'), () => {});
|
||||
}, 60000);
|
||||
|
||||
const idsIn = (label: string, file: string): string[] => {
|
||||
const ids: string[] = [];
|
||||
result.graph.forEachNode((n) => {
|
||||
if (n.label === label && String(n.properties.filePath).endsWith(file)) ids.push(n.id);
|
||||
});
|
||||
return ids.sort();
|
||||
};
|
||||
|
||||
it('keys a function-local `const R = struct` by its enclosing callable, so two builders own two `R.get`s', () => {
|
||||
expect(idsIn('Struct', 'reflect.zig')).toEqual([
|
||||
'Struct:src/reflect.zig:Accessor',
|
||||
'Struct:src/reflect.zig:Reflect',
|
||||
'Struct:src/reflect.zig:Reflect.string$R',
|
||||
'Struct:src/reflect.zig:Reflect.url$R',
|
||||
]);
|
||||
// Both `get`s exist, under distinct owner-qualified ids…
|
||||
expect(result.graph.getNode('Method:src/reflect.zig:Reflect.string$R.get#0')).toBeDefined();
|
||||
expect(result.graph.getNode('Method:src/reflect.zig:Reflect.url$R.get#0')).toBeDefined();
|
||||
// …and nothing is left under the bare binding name.
|
||||
expect(result.graph.getNode('Struct:src/reflect.zig:R')).toBeUndefined();
|
||||
expect(result.graph.getNode('Method:src/reflect.zig:R.get#0')).toBeUndefined();
|
||||
|
||||
const hasMethod = getRelationships(result, 'HAS_METHOD').map(
|
||||
(e) => `${e.rel.sourceId} → ${e.rel.targetId}`,
|
||||
);
|
||||
expect(hasMethod).toContain(
|
||||
'Struct:src/reflect.zig:Reflect.string$R → Method:src/reflect.zig:Reflect.string$R.get#0',
|
||||
);
|
||||
expect(hasMethod).toContain(
|
||||
'Struct:src/reflect.zig:Reflect.url$R → Method:src/reflect.zig:Reflect.url$R.get#0',
|
||||
);
|
||||
});
|
||||
|
||||
it('gives anonymous containers a host + ordinal identity, so no Method is ownerless and same-named fns never collide', () => {
|
||||
expect(idsIn('Struct', 'Sorter.zig')).toEqual([
|
||||
'Struct:src/Sorter.zig:Sorter',
|
||||
'Struct:src/Sorter.zig:Sorter$1', // `bounds: ?struct { min, max }`
|
||||
'Struct:src/Sorter.zig:Sorter$2', // `const byteSize = struct { fn it }.it`
|
||||
'Struct:src/Sorter.zig:Sorter.sortBoth$1', // first comparator
|
||||
'Struct:src/Sorter.zig:Sorter.sortBoth$2', // second comparator
|
||||
'Struct:src/Sorter.zig:Sorter.test@L41$State', // test-local `const State`
|
||||
]);
|
||||
// Two `lessThan`s in one file → two nodes, each owned by its own Struct.
|
||||
expect(
|
||||
result.graph.getNode('Method:src/Sorter.zig:Sorter.sortBoth$1.lessThan#3'),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
result.graph.getNode('Method:src/Sorter.zig:Sorter.sortBoth$2.lessThan#3'),
|
||||
).toBeDefined();
|
||||
expect(result.graph.getNode('Method:src/Sorter.zig:Sorter$2.it#1')).toBeDefined();
|
||||
const hasMethod = getRelationships(result, 'HAS_METHOD').map(
|
||||
(e) => `${e.rel.sourceId} → ${e.rel.targetId}`,
|
||||
);
|
||||
expect(hasMethod).toContain(
|
||||
'Struct:src/Sorter.zig:Sorter.sortBoth$1 → Method:src/Sorter.zig:Sorter.sortBoth$1.lessThan#3',
|
||||
);
|
||||
expect(hasMethod).toContain(
|
||||
'Struct:src/Sorter.zig:Sorter.sortBoth$2 → Method:src/Sorter.zig:Sorter.sortBoth$2.lessThan#3',
|
||||
);
|
||||
expect(hasMethod).toContain(
|
||||
'Struct:src/Sorter.zig:Sorter$2 → Method:src/Sorter.zig:Sorter$2.it#1',
|
||||
);
|
||||
// The anonymous field type owns its fields (they were `Sorter.min` before).
|
||||
const hasProp = getRelationships(result, 'HAS_PROPERTY').map(
|
||||
(e) => `${e.rel.sourceId} → ${e.rel.targetId}`,
|
||||
);
|
||||
expect(hasProp).toContain(
|
||||
'Struct:src/Sorter.zig:Sorter$1 → Property:src/Sorter.zig:Sorter$1.min',
|
||||
);
|
||||
// No ownerless Method anywhere in the fixture: every Method id is `<owner>.<name>#N`.
|
||||
const ownerless: string[] = [];
|
||||
result.graph.forEachNode((n) => {
|
||||
if (n.label === 'Method' && /^Method:[^:]+:[^.]+#\d+$/.test(n.id)) ownerless.push(n.id);
|
||||
});
|
||||
expect(ownerless).toEqual([]);
|
||||
});
|
||||
|
||||
it('attributes calls inside such containers to the right node, on both ends', () => {
|
||||
const calls = getRelationships(result, 'CALLS').map(
|
||||
(e) => `${e.rel.sourceId} → ${e.rel.targetId}`,
|
||||
);
|
||||
// From inside each `R.get` — the caller is THAT builder's `R.get`.
|
||||
expect(calls).toContain(
|
||||
'Method:src/reflect.zig:Reflect.string$R.get#0 → Function:src/reflect.zig:readAttr',
|
||||
);
|
||||
expect(calls).toContain(
|
||||
'Method:src/reflect.zig:Reflect.url$R.get#0 → Function:src/reflect.zig:normalize',
|
||||
);
|
||||
// A `const Self = @This();` inside a local container still names THAT
|
||||
// container: `check(self: *const Self)` dispatches `self.get()` to `url$R.get`.
|
||||
expect(calls).toContain(
|
||||
'Method:src/reflect.zig:Reflect.url$R.check#0 → Method:src/reflect.zig:Reflect.url$R.get#0',
|
||||
);
|
||||
// From inside each anonymous comparator (and the test-local State).
|
||||
expect(calls).toContain(
|
||||
'Method:src/Sorter.zig:Sorter.sortBoth$1.lessThan#3 → Method:src/Sorter.zig:Sorter.before#2',
|
||||
);
|
||||
expect(calls).toContain(
|
||||
'Method:src/Sorter.zig:Sorter.sortBoth$2.lessThan#3 → Method:src/Sorter.zig:Sorter.before#2',
|
||||
);
|
||||
expect(calls).toContain(
|
||||
'Method:src/Sorter.zig:Sorter.test@L41$State.kill#0 → Method:src/Sorter.zig:Sorter.before#2',
|
||||
);
|
||||
// INTO a test-local container: `State{}` and `state.kill()` from the test
|
||||
// resolve to the qualified Struct / Method (the qualified key must survive
|
||||
// the class extractor's whitespace normalization — a test-string host would not).
|
||||
expect(calls).toContain(
|
||||
'Function:src/Sorter.zig:Sorter."Sorter: local state" → Struct:src/Sorter.zig:Sorter.test@L41$State',
|
||||
);
|
||||
expect(calls).toContain(
|
||||
'Function:src/Sorter.zig:Sorter."Sorter: local state" → Method:src/Sorter.zig:Sorter.test@L41$State.kill#0',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -22,7 +22,11 @@ import {
|
|||
isZigContainerOrImportBinding,
|
||||
isZigFileStruct,
|
||||
isZigKeywordDeclaration,
|
||||
isZigRedundantContainerCapture,
|
||||
isZigTypeShadowingBinding,
|
||||
zigCallableQualifiedName,
|
||||
zigContainerAnchor,
|
||||
zigContainerBindingName,
|
||||
zigContainerName,
|
||||
zigFileStructName,
|
||||
} from '../../src/core/ingestion/languages/zig/captures.js';
|
||||
|
|
@ -41,6 +45,7 @@ import { zigFieldConfig } from '../../src/core/ingestion/field-extractors/config
|
|||
import { zigProvider } from '../../src/core/ingestion/languages/zig.js';
|
||||
import { createSemanticModel } from '../../src/core/ingestion/model/semantic-model.js';
|
||||
import { extract as extractScopes } from '../../src/core/ingestion/scope-extractor.js';
|
||||
import { populateClassOwnedMembers } from '../../src/core/ingestion/scope-resolution/scope/walkers.js';
|
||||
|
||||
const _require = createRequire(import.meta.url);
|
||||
let Zig: unknown = null;
|
||||
|
|
@ -786,7 +791,7 @@ fn notAType(comptime T: type) u32 {
|
|||
const Plain = struct { a: u8 };
|
||||
`;
|
||||
|
||||
it('names the container returned by a fn returning `type` after that fn; other anonymous containers stay nameless', () => {
|
||||
it('names the container returned by a fn returning `type` after that fn; an anonymous container is keyed by its host (F8)', () => {
|
||||
const root = parse(src).rootNode;
|
||||
const structs: SyntaxNode[] = [];
|
||||
const walk = (n: SyntaxNode): void => {
|
||||
|
|
@ -794,7 +799,10 @@ const Plain = struct { a: u8 };
|
|||
n.children.forEach(walk);
|
||||
};
|
||||
walk(root);
|
||||
expect(structs.map((n) => zigContainerName(n))).toEqual(['Stack', undefined, 'Plain']);
|
||||
// `struct { … }.x()` inside `notAType` has no binding name; its identity
|
||||
// is `<enclosing fn>$<ordinal>` so its fn `x` is a Method WITH an owner.
|
||||
expect(structs.map((n) => zigContainerBindingName(n))).toEqual(['Stack', undefined, 'Plain']);
|
||||
expect(structs.map((n) => zigContainerName(n))).toEqual(['Stack', 'notAType$1', 'Plain']);
|
||||
});
|
||||
|
||||
it('the method and field extractors own the generic container’s members under the fn name', () => {
|
||||
|
|
@ -1414,3 +1422,186 @@ pub fn init() @This() { return .{}; }
|
|||
expect(zigPointeeSpelling('Thing')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describeZig('Zig function-local and anonymous containers (F8)', () => {
|
||||
// Lightpanda shapes: reflection.zig's per-builder `const R = struct {…}`,
|
||||
// ImportMap.zig's `std.sort.pdq(…, struct { fn lessThan … }.lessThan)`
|
||||
// (three in one file), build.zig's `const byte_size = struct { fn it … }.it;`,
|
||||
// and a test-local `const State = struct {…}`.
|
||||
const src = `
|
||||
const Page = @This();
|
||||
count: u32 = 0,
|
||||
pub fn Reflect(comptime T: type) type {
|
||||
return struct {
|
||||
pub fn string() void {
|
||||
const R = struct {
|
||||
fn get(self: *const T) u8 { _ = self; return 1; }
|
||||
};
|
||||
_ = R.get;
|
||||
}
|
||||
pub fn url() void {
|
||||
const R = struct {
|
||||
fn get(self: *const T) u8 { _ = self; return 2; }
|
||||
};
|
||||
_ = R.get;
|
||||
}
|
||||
};
|
||||
}
|
||||
pub fn sortBoth(self: *Page) void {
|
||||
_ = self;
|
||||
sort(struct { fn lessThan(a: u32, b: u32) bool { return a < b; } }.lessThan);
|
||||
sort(struct { fn lessThan(a: u32, b: u32) bool { return a > b; } }.lessThan);
|
||||
}
|
||||
const byte_size = struct { fn it(n: u32) u32 { return n; } }.it;
|
||||
test "Page: local state" {
|
||||
const State = struct { fn kill(self: *@This()) void { _ = self; } };
|
||||
var s = State{};
|
||||
s.kill();
|
||||
}
|
||||
`;
|
||||
const containers = (): SyntaxNode[] => {
|
||||
const out: SyntaxNode[] = [];
|
||||
const walk = (n: SyntaxNode): void => {
|
||||
if (n.type === 'struct_declaration') out.push(n);
|
||||
n.children.forEach(walk);
|
||||
};
|
||||
walk(parse(src).rootNode);
|
||||
return out;
|
||||
};
|
||||
const findFn = (n: SyntaxNode, name: string): SyntaxNode | undefined =>
|
||||
n.type === 'function_declaration' && n.childForFieldName('name')?.text === name
|
||||
? n
|
||||
: n.children.map((c) => findFn(c, name)).find((x) => x !== undefined);
|
||||
|
||||
it('keys a function-local container by its enclosing callable and an anonymous one by host + ordinal', () => {
|
||||
// Before F8 the two `R` collapsed onto one `Struct:<file>:R` (one `R.get`
|
||||
// for ~20 builders in reflection.zig) and the anonymous comparators had
|
||||
// no identity at all — their `lessThan`s were ownerless, colliding Methods.
|
||||
expect(containers().map((n) => zigContainerName(n, 'src/Page.zig'))).toEqual([
|
||||
'Reflect',
|
||||
'Reflect.string$R',
|
||||
'Reflect.url$R',
|
||||
'Page.sortBoth$1',
|
||||
'Page.sortBoth$2',
|
||||
'Page$1',
|
||||
// A test block is keyed by its line: its string is not stable under
|
||||
// the class extractor's qualified-name normalization (whitespace).
|
||||
'Page.test@L26$State',
|
||||
]);
|
||||
// The BINDING name is what code writes — the scope binds `R`, not `string$R`.
|
||||
expect(containers().map((n) => zigContainerBindingName(n))).toEqual([
|
||||
'Reflect',
|
||||
'R',
|
||||
'R',
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
'State',
|
||||
]);
|
||||
// Which ZIG_QUERIES rule mints each: only file/container-level bindings
|
||||
// stay on the `const T = struct` wrapper rule.
|
||||
expect(containers().map((n) => zigContainerAnchor(n))).toEqual([
|
||||
'constructor',
|
||||
'container',
|
||||
'container',
|
||||
'container',
|
||||
'container',
|
||||
'container',
|
||||
'container',
|
||||
]);
|
||||
const root = parse(src).rootNode;
|
||||
expect(zigCallableQualifiedName(findFn(root, 'get')!, 'src/Page.zig')).toBe(
|
||||
'Reflect.string$R.get',
|
||||
);
|
||||
expect(zigCallableQualifiedName(findFn(root, 'sortBoth')!, 'src/Page.zig')).toBe(
|
||||
'Page.sortBoth',
|
||||
);
|
||||
// A namespace file (no fields) prefixes nothing for the file itself.
|
||||
const nsRoot = parse('fn build() void { const R = struct {}; _ = R; }').rootNode;
|
||||
expect(
|
||||
zigContainerName(
|
||||
findFn(nsRoot, 'build')!.descendantsOfType('struct_declaration')[0]!,
|
||||
'build.zig',
|
||||
),
|
||||
).toBe('build$R');
|
||||
});
|
||||
|
||||
it('the redundancy predicate keeps exactly one structure-phase rule per container', () => {
|
||||
const [ctor, localR, , anon] = containers();
|
||||
// Type constructor: its own rule (anchored on the container, fn `@name`)
|
||||
// is kept; the bare rule's match (no name) is redundant.
|
||||
const ctorFn = findFn(parse(src).rootNode, 'Reflect')!;
|
||||
expect(isZigRedundantContainerCapture(ctor!, ctorFn.childForFieldName('name')!)).toBe(false);
|
||||
expect(isZigRedundantContainerCapture(ctor!, undefined)).toBe(true);
|
||||
// Function-local `const R = struct`: the wrapper rule is redundant, the
|
||||
// bare rule owns it (it is the one that names it `string$R`).
|
||||
const wrapper = localR!.parent!;
|
||||
expect(isZigRedundantContainerCapture(wrapper, wrapper.namedChild(0)!)).toBe(true);
|
||||
expect(isZigRedundantContainerCapture(localR!, undefined)).toBe(false);
|
||||
// Anonymous: only the bare rule matches, and it is kept.
|
||||
expect(isZigRedundantContainerCapture(anon!, undefined)).toBe(false);
|
||||
// File-level `const Plain = struct {}` stays on the wrapper rule.
|
||||
const plainWrapper = parse('const Plain = struct { a: u8 };').rootNode.namedChild(0)!;
|
||||
const plain = plainWrapper.namedChild(1)!;
|
||||
expect(isZigRedundantContainerCapture(plainWrapper, plainWrapper.namedChild(0)!)).toBe(false);
|
||||
expect(isZigRedundantContainerCapture(plain, undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it('scope captures: a local container def is qualified but binds its own name; anonymous ones get distinct synthetic defs', () => {
|
||||
const caps = emitZigScopeCaptures(src, 'src/Page.zig');
|
||||
const structDefs = caps
|
||||
.filter((m) => m['@declaration.struct'] !== undefined)
|
||||
.map((m) => [
|
||||
m['@declaration.name']?.text,
|
||||
m['@declaration.binding-name']?.text,
|
||||
m['@declaration.is-synthetic']?.text,
|
||||
]);
|
||||
expect(structDefs).toContainEqual(['Reflect.string$R', 'R', undefined]);
|
||||
expect(structDefs).toContainEqual(['Reflect.url$R', 'R', undefined]);
|
||||
expect(structDefs).toContainEqual(['Page.test@L26$State', 'State', undefined]);
|
||||
// Two same-shaped `struct { fn lessThan }` comparators → two identities.
|
||||
expect(structDefs).toContainEqual(['Page.sortBoth$1', undefined, 'true']);
|
||||
expect(structDefs).toContainEqual(['Page.sortBoth$2', undefined, 'true']);
|
||||
expect(structDefs).toContainEqual(['Page$1', undefined, 'true']);
|
||||
// No def is left under the bare binding name.
|
||||
expect(structDefs.map((d) => d[0])).not.toContain('R');
|
||||
|
||||
// Through the scope extractor: the members are owned by DISTINCT class
|
||||
// defs and their qualified names carry the identity — the graph node id
|
||||
// the structure phase mints (`Method:<file>:Reflect.string$R.get`).
|
||||
const parsed = extractScopes(caps, 'src/Page.zig', zigProvider);
|
||||
populateClassOwnedMembers(parsed);
|
||||
const gets = parsed.localDefs.filter(
|
||||
(d) => d.type === 'Method' && d.qualifiedName?.endsWith('.get'),
|
||||
);
|
||||
expect(gets.map((d) => d.qualifiedName).sort()).toEqual([
|
||||
'Reflect.string$R.get',
|
||||
'Reflect.url$R.get',
|
||||
]);
|
||||
expect(new Set(gets.map((d) => d.ownerId)).size).toBe(2);
|
||||
const lts = parsed.localDefs.filter(
|
||||
(d) => d.type === 'Method' && d.qualifiedName?.endsWith('.lessThan'),
|
||||
);
|
||||
expect(lts.map((d) => d.qualifiedName).sort()).toEqual([
|
||||
'Page.sortBoth$1.lessThan',
|
||||
'Page.sortBoth$2.lessThan',
|
||||
]);
|
||||
expect(lts.every((d) => d.ownerId !== undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it('a `@This()` alias inside a local container rewrites to the BINDING name (`R`), which is what the scope binds', () => {
|
||||
const localAliasSrc = `
|
||||
pub fn string() void {
|
||||
const R = struct {
|
||||
const Self = @This();
|
||||
fn get(self: *const Self) u8 { _ = self; return 1; }
|
||||
};
|
||||
_ = R;
|
||||
}
|
||||
`;
|
||||
const selfParam = emitZigScopeCaptures(localAliasSrc, 'src/r.zig').find(
|
||||
(m) => m['@type-binding.parameter'] !== undefined && m['@type-binding.name']?.text === 'self',
|
||||
);
|
||||
expect(selfParam?.['@type-binding.type']?.text).toBe('*const R');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue