mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-11 03:38:07 +00:00
fix(zig): type locals through try/catch/orelse, return types and payload captures
F6 of the gitnexus-check review. Three gaps in the value flow that types a local receiver, all measured on Lightpanda: 1. `@type-binding.call-return` needed the `call_expression` as the DIRECT value child, so `const p = try Page.init(…)` (2,551 sites), `… catch return` (410) and `… orelse return` typed nothing. The rule is now one keyword-gated declaration match; `emitZigScopeCaptures` unwraps `try`, `catch`, `orelse` and parentheses (`zigUnwrapValue`) and decides what the value types (`zigCallReturnTypeOf`): a module-level receiver still names the type (`Counter.init()` → Counter, Rust `Foo::new()`); a free call binds the callee name (`makeThing`); a member call on a fn-LOCAL receiver (parameter / local / payload — Zig forbids shadowing, so "declared in the fn" is exact) binds the compound `node.asElement()` the shared resolver walks to the method's return type — instead of typing `el` as `Node`. A TitleCase callee (`List(u8)`) is a type constructor and binds nothing. 2. No `@type-binding.return` existed. `fn make() !*Thing` now binds `make ↦ Thing` in the enclosing scope (Module for free fns, the container's Class scope for methods, where the compound resolver reads it). Builtins, `type`, `@TypeOf(…)` and comptime type parameters (`?*T`) bind nothing; `@This()` / `Self` returns name the container. `normalizeZigTypeName` now strips the error union BEFORE the payload's sigils, so `Allocator.Error!*Page` → `Page` (it used to leave `*Page`). 3. Payload captures had no binding at all. `populateZigRangeBindings` (registered as `populateRangeBindings`) types `for (items) |it| / |*it|`, `for (items, 0..) |it, i|`, `if (opt) |v|`, `if (call()) |v|`, `while (it.next()) |x|` from the SUBJECT's written type minus one layer (`[]T` element, `?T` payload) — declining when the layer is not visible (`ArrayList(T)`) — and the same projection for `const t = items[i]` / `opt.?` / `ptr.*`. `catch |err|` and `switch` prongs are skipped. Corpus (Lightpanda, gate before → after): CALLS 15838 → 17853; `ident.m() bound=local-try/catch/orelse` 11/1298 → 584/1298; `local-call` 159/1282 → 398/1282; `payload` 82/1085 → 233/1085; `other-recv:call_expression` 5/991 → 457/991; `local-other` 271 → 364; `self.m()` 100 %, `free f()` 97.5 %, `ns.f()` 83.4 % unchanged; nothing down. Not covered: `const t = ns.f()` (a namespace fn's return type across files — Case 3 has no path from a namespace head to a callable's return binding), and expression receivers (`items[i].run()`, `o.?.run()`).
This commit is contained in:
parent
a46bce7681
commit
bb51ca342c
11 changed files with 1090 additions and 16 deletions
|
|
@ -288,8 +288,10 @@ function collectZigThisAliases(
|
|||
function rewriteZigThisAlias(
|
||||
typeNode: SyntaxNode,
|
||||
aliases: ReadonlyMap<number, { readonly alias: string; readonly container: string }>,
|
||||
// The text to rewrite: the node's own, unless an earlier step (F6's value
|
||||
// inference) already replaced the capture text.
|
||||
text: string = typeNode.text,
|
||||
): string | undefined {
|
||||
const text = typeNode.text;
|
||||
const nominal = /[A-Za-z_@][\w.]*(?:\([^)]*\))?\s*$/.exec(text)?.[0]?.trim();
|
||||
if (nominal === undefined || nominal.length === 0) return undefined;
|
||||
const bare = nominal.replace(/\(.*\)$/, '');
|
||||
|
|
@ -370,6 +372,252 @@ const ZIG_CALLABLE_CAPTURE_OPTIONS = {
|
|||
},
|
||||
} as const;
|
||||
|
||||
// ─── F6: value-inferred and return-type bindings ─────────────────────────────
|
||||
|
||||
/** Strip the value wrappers Zig puts around a call — `try f()`, `f() catch
|
||||
* …`, `f() orelse …`, `(f())` — down to the wrapped expression. Rust's
|
||||
* `.await` unwrapping, one level richer. */
|
||||
export function zigUnwrapValue(value: SyntaxNode): SyntaxNode {
|
||||
let cur = value;
|
||||
for (;;) {
|
||||
if (cur.type === 'try_expression' || cur.type === 'parenthesized_expression') {
|
||||
const inner = cur.namedChildren.find(
|
||||
(c): c is SyntaxNode => c !== null && c.type !== 'comment',
|
||||
);
|
||||
if (inner === undefined) return cur;
|
||||
cur = inner;
|
||||
} else if (cur.type === 'catch_expression') {
|
||||
// `<operand> catch [|err|] <handler>` — the operand is the first child.
|
||||
const inner = cur.namedChild(0);
|
||||
if (inner === null) return cur;
|
||||
cur = inner;
|
||||
} else if (cur.type === 'binary_expression') {
|
||||
if (cur.childForFieldName('operator')?.type !== 'orelse') return cur;
|
||||
const left = cur.childForFieldName('left');
|
||||
if (left === null) return cur;
|
||||
cur = left;
|
||||
} else {
|
||||
return cur;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Zig style: types are TitleCase, functions camelCase, namespaces
|
||||
* snake_case. A TitleCase callee (`List(u8)`, `std.ArrayList(T)`) is a type
|
||||
* constructor — its call yields a TYPE, not a value, and is a type alias
|
||||
* (F7), not a call-return binding. */
|
||||
function isZigTitleCase(name: string): boolean {
|
||||
return /^[A-Z]/.test(name);
|
||||
}
|
||||
|
||||
/** The leftmost identifier of a `field_expression` chain (`self.parser` →
|
||||
* `self`), the identifier itself, or null when the chain roots in anything
|
||||
* else (a call, an index, a builtin). */
|
||||
function zigChainHead(node: SyntaxNode): SyntaxNode | null {
|
||||
let cur: SyntaxNode | null = node;
|
||||
while (cur !== null && cur.type === 'field_expression') cur = cur.childForFieldName('object');
|
||||
return cur !== null && cur.type === 'identifier' ? cur : null;
|
||||
}
|
||||
|
||||
/** Names bound INSIDE `fn` (parameters, `const`/`var` locals, payload
|
||||
* captures), lazily computed once per function node. Zig forbids shadowing,
|
||||
* so a name declared anywhere in the function is a value local everywhere in
|
||||
* it, and a name NOT declared in it is a module-level (or imported) name.
|
||||
* That is the whole distinction `zigCallReturnTypeOf` needs: a call on a
|
||||
* local receiver (`node.asElement()`) yields the METHOD's return type, a
|
||||
* call on a module-level receiver (`Counter.init()`, `mod.Counter.init()`)
|
||||
* names the receiver's type. */
|
||||
function zigFunctionLocalNames(fn: SyntaxNode, cache: Map<number, Set<string>>): Set<string> {
|
||||
const cached = cache.get(fn.id);
|
||||
if (cached !== undefined) return cached;
|
||||
const names = new Set<string>();
|
||||
const visit = (node: SyntaxNode): void => {
|
||||
if (node.type === 'parameter') {
|
||||
const name = node.childForFieldName('name');
|
||||
if (name !== null) names.add(name.text);
|
||||
} else if (node.type === 'variable_declaration' && isZigKeywordDeclaration(node)) {
|
||||
const first = node.namedChild(0);
|
||||
if (first?.type === 'identifier') names.add(first.text);
|
||||
} else if (node.type === 'payload') {
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const c = node.namedChild(i);
|
||||
if (c?.type === 'identifier') names.add(c.text);
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const c = node.namedChild(i);
|
||||
if (c !== null) visit(c);
|
||||
}
|
||||
};
|
||||
visit(fn);
|
||||
cache.set(fn.id, names);
|
||||
return names;
|
||||
}
|
||||
|
||||
/** The nearest enclosing `function_declaration` / `test_declaration`, or
|
||||
* null at module (container) level. */
|
||||
function zigEnclosingFunction(node: SyntaxNode): SyntaxNode | null {
|
||||
let cur = node.parent;
|
||||
while (cur !== null && cur !== undefined) {
|
||||
if (cur.type === 'function_declaration' || cur.type === 'test_declaration') return cur;
|
||||
cur = cur.parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** For `const t = <value>;`, the type source the value names, or undefined
|
||||
* when the value types nothing on its own:
|
||||
* - `Counter.init()` / `mod.Counter.init()` / `List(u8).init()` (receiver
|
||||
* is a module-level name) → `{ type: 'Counter' }` — the receiver names
|
||||
* the type (Rust `Foo::new()`); a value receiver simply finds no
|
||||
* container later and declines.
|
||||
* - `node.asElement()` / `self.parser.next()` (receiver head is a fn-local
|
||||
* param / local / payload) → `{ type: 'node.asElement()', memberCall:
|
||||
* true }` — the compound shape the shared resolver walks through the
|
||||
* receiver's class scope to the method's `@type-binding.return`.
|
||||
* - `makeThing()` (free call) → `{ type: 'makeThing' }` — chains to the
|
||||
* fn's return binding.
|
||||
* - a `struct_initializer` under a wrapper (`try Thing{…}`) → its type,
|
||||
* `structLiteral: true` (the direct shape has its own query rules).
|
||||
* - `.init()` decl literals, TitleCase callees (type constructors —
|
||||
* aliases, F7), identifiers, field chains, literals → undefined.
|
||||
* Wrappers (`try`, `catch`, `orelse`, parens) are unwrapped first. */
|
||||
export function zigCallReturnTypeOf(
|
||||
value: SyntaxNode,
|
||||
localNamesCache: Map<number, Set<string>>,
|
||||
):
|
||||
| { readonly type: string; readonly memberCall?: true; readonly structLiteral?: true }
|
||||
| undefined {
|
||||
const inner = zigUnwrapValue(value);
|
||||
if (inner.type === 'struct_initializer') {
|
||||
if (inner.id === value.id) return undefined; // direct: the constructor rules own it
|
||||
const typeNode = inner.namedChild(0);
|
||||
if (
|
||||
typeNode === null ||
|
||||
!(
|
||||
typeNode.type === 'identifier' ||
|
||||
typeNode.type === 'field_expression' ||
|
||||
typeNode.type === 'call_expression'
|
||||
)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return { type: typeNode.text, structLiteral: true };
|
||||
}
|
||||
if (inner.type !== 'call_expression') return undefined;
|
||||
const callee = inner.childForFieldName('function');
|
||||
if (callee === null) return undefined;
|
||||
if (callee.type === 'identifier') {
|
||||
return isZigTitleCase(callee.text) ? undefined : { type: callee.text };
|
||||
}
|
||||
if (callee.type !== 'field_expression') return undefined;
|
||||
const object = callee.childForFieldName('object');
|
||||
const member = callee.childForFieldName('member');
|
||||
if (object === null || member === null) return undefined; // `.init()` decl literal
|
||||
if (isZigTitleCase(member.text)) return undefined;
|
||||
const head = zigChainHead(object);
|
||||
if (head !== null) {
|
||||
const fn = zigEnclosingFunction(value);
|
||||
if (fn !== null && zigFunctionLocalNames(fn, localNamesCache).has(head.text)) {
|
||||
return { type: `${object.text}.${member.text}()`, memberCall: true };
|
||||
}
|
||||
}
|
||||
return { type: object.text };
|
||||
}
|
||||
|
||||
/** Return-type node → the nominal type node under its sigils (`!*Thing` →
|
||||
* `Thing`), following error-union `ok`, pointer / nullable / slice / array
|
||||
* wrappers. Undefined when it bottoms out in nothing nominal. */
|
||||
function zigNominalTypeNode(typeNode: SyntaxNode): SyntaxNode | undefined {
|
||||
let cur: SyntaxNode | null | undefined = typeNode;
|
||||
for (let guard = 0; cur !== null && cur !== undefined && guard < 16; guard++) {
|
||||
switch (cur.type) {
|
||||
case 'error_union_type':
|
||||
cur = cur.childForFieldName('ok');
|
||||
continue;
|
||||
case 'pointer_type':
|
||||
case 'nullable_type':
|
||||
case 'slice_type':
|
||||
case 'array_type':
|
||||
// The element type is the LAST named child (arrays carry the length
|
||||
// first, pointers/slices may carry a sentinel or `const`).
|
||||
cur = cur.namedChildren
|
||||
.filter((c): c is SyntaxNode => c !== null && c.type !== 'comment')
|
||||
.pop();
|
||||
continue;
|
||||
default:
|
||||
return cur;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Should `fn name() <type>` bind `name ↦ <type>`? Only when the return
|
||||
* type is nominal: a container name, a qualified name, a generic
|
||||
* instantiation, or `@This()`. Builtins (`void`, `u8`, `bool`, `anyerror`),
|
||||
* `type` (a generic type constructor — its container binding is the type),
|
||||
* and `@TypeOf(…)` bind nothing: `List ↦ type` would hijack the
|
||||
* `List(u8){}` constructor chain, and `f ↦ void` is noise. */
|
||||
export function zigReturnTypeIsNominal(typeNode: SyntaxNode): boolean {
|
||||
const nominal = zigNominalTypeNode(typeNode);
|
||||
if (nominal === undefined) return false;
|
||||
switch (nominal.type) {
|
||||
case 'identifier':
|
||||
// `fn is(self: *Node, comptime T: type) ?*T` — `T` is the CALLER's
|
||||
// choice, not a type; binding `is ↦ T` would type every `node.is(X)`
|
||||
// as whatever class happens to be named `T`.
|
||||
return !zigIsComptimeTypeParameter(typeNode, nominal.text);
|
||||
case 'field_expression':
|
||||
case 'call_expression':
|
||||
return true;
|
||||
case 'builtin_function':
|
||||
return nominal.namedChild(0)?.text === '@This';
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Is `name` a `comptime <name>: type` parameter of the function whose
|
||||
* return type `returnTypeNode` is, or of any function enclosing it (the
|
||||
* `T` of a generic type constructor is visible to every method of the
|
||||
* returned container: `fn get(self) T`)? */
|
||||
function zigIsComptimeTypeParameter(returnTypeNode: SyntaxNode, name: string): boolean {
|
||||
let fn: SyntaxNode | null | undefined = returnTypeNode.parent;
|
||||
while (fn !== null && fn !== undefined) {
|
||||
if (fn.type === 'function_declaration') {
|
||||
const params = fn.namedChildren.find((c) => c?.type === 'parameters');
|
||||
for (let i = 0; params !== undefined && params !== null && i < params.namedChildCount; i++) {
|
||||
const p = params.namedChild(i);
|
||||
if (p?.type !== 'parameter') continue;
|
||||
if (
|
||||
p.childForFieldName('name')?.text === name &&
|
||||
p.childForFieldName('type')?.text === 'type'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
fn = fn.parent;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** The container a `@This()` in `typeNode`'s position names: the innermost
|
||||
* enclosing container's binding name, or the file-struct's name at file
|
||||
* level. Undefined outside any named container. */
|
||||
function zigThisTargetFor(
|
||||
typeNode: SyntaxNode,
|
||||
fileStructName: string | undefined,
|
||||
): string | undefined {
|
||||
let cur = typeNode.parent;
|
||||
while (cur !== null && cur !== undefined) {
|
||||
if (ZIG_CONTAINER_TYPES.has(cur.type)) return zigContainerName(cur);
|
||||
if (cur.type === 'source_file') return fileStructName;
|
||||
cur = cur.parent;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function emitZigScopeCaptures(
|
||||
sourceText: string,
|
||||
_filePath: string,
|
||||
|
|
@ -455,6 +703,8 @@ 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);
|
||||
// F6: fn-local names per function node, for `zigCallReturnTypeOf`.
|
||||
const fnLocalNames = new Map<number, Set<string>>();
|
||||
|
||||
for (const m of rawMatches) {
|
||||
const grouped: Record<string, Capture> = {};
|
||||
|
|
@ -606,6 +856,57 @@ export function emitZigScopeCaptures(
|
|||
const aliasBinding = nodeMap['@type-binding.alias'];
|
||||
if (aliasBinding !== undefined && aliasDeclIds.has(aliasBinding.id)) continue;
|
||||
|
||||
// F6 — value-inferred bindings: `const t = [try] <call>` (see
|
||||
// `zigCallReturnTypeOf`). The query pins every keyword declaration; here
|
||||
// the value decides whether it types anything, and how. The value capture
|
||||
// is scaffolding and never leaves this function.
|
||||
const valueNode = nodeMap['@type-binding.value'];
|
||||
if (valueNode !== undefined && nodeMap['@type-binding.call-return'] !== undefined) {
|
||||
delete grouped['@type-binding.value'];
|
||||
// `const x: T;` (extern) — the trailing child is the type, not a value.
|
||||
if (valueNode.id === nodeMap['@type-binding.call-return'].childForFieldName('type')?.id) {
|
||||
continue;
|
||||
}
|
||||
const inferred = zigCallReturnTypeOf(valueNode, fnLocalNames);
|
||||
if (inferred === undefined) continue;
|
||||
grouped['@type-binding.type'] = syntheticCapture(
|
||||
'@type-binding.type',
|
||||
valueNode,
|
||||
inferred.type,
|
||||
);
|
||||
nodeMap['@type-binding.type'] = valueNode;
|
||||
if (inferred.structLiteral === true) {
|
||||
const anchor = grouped['@type-binding.call-return']!;
|
||||
delete grouped['@type-binding.call-return'];
|
||||
grouped['@type-binding.constructor'] = { ...anchor, name: '@type-binding.constructor' };
|
||||
} else if (inferred.memberCall === true) {
|
||||
grouped['@type-binding.member-call-return'] = syntheticCapture(
|
||||
'@type-binding.member-call-return',
|
||||
nodeMap['@type-binding.name']!,
|
||||
'true',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// F6 — return-type bindings: `fn make() !*Thing` binds `make ↦ Thing`.
|
||||
// Only nominal returns bind (see `zigReturnTypeIsNominal`); a bare
|
||||
// `@This()` return names the enclosing container by name (the alias
|
||||
// rewrite below handles the `Self` spelling of the same thing).
|
||||
const returnAnchor = nodeMap['@type-binding.return'];
|
||||
if (returnAnchor !== undefined) {
|
||||
const retType = nodeMap['@type-binding.type'];
|
||||
if (retType === undefined || !zigReturnTypeIsNominal(retType)) continue;
|
||||
if (retType.text.includes('@This()')) {
|
||||
const target = zigThisTargetFor(retType, fileStructName);
|
||||
if (target === undefined) continue;
|
||||
grouped['@type-binding.type'] = syntheticCapture(
|
||||
'@type-binding.type',
|
||||
retType,
|
||||
retType.text.replace('@This()', target),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// `@This()` aliases in type position — `self: *Self` in a container that
|
||||
// declares `const Self = @This();`, or `self: *SigHandler` in
|
||||
// `Sighandler.zig` (`const SigHandler = @This();`) — name the enclosing
|
||||
|
|
@ -614,7 +915,11 @@ export function emitZigScopeCaptures(
|
|||
// rust/captures.ts). Only the nominal part is rewritten; sigils stay.
|
||||
const typeNode = nodeMap['@type-binding.type'];
|
||||
if (typeNode !== undefined && thisAliases.size > 0) {
|
||||
const rewritten = rewriteZigThisAlias(typeNode, thisAliases);
|
||||
const rewritten = rewriteZigThisAlias(
|
||||
typeNode,
|
||||
thisAliases,
|
||||
grouped['@type-binding.type']?.text,
|
||||
);
|
||||
if (rewritten !== undefined) {
|
||||
grouped['@type-binding.type'] = syntheticCapture('@type-binding.type', typeNode, rewritten);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,18 @@ export {
|
|||
zigContainerName,
|
||||
zigFileStructName,
|
||||
zigImportRootOf,
|
||||
zigCallReturnTypeOf,
|
||||
zigReturnTypeIsNominal,
|
||||
zigTypeConstructorOf,
|
||||
zigUnwrapValue,
|
||||
ZIG_CONTAINER_TYPES,
|
||||
} from './captures.js';
|
||||
export {
|
||||
populateZigRangeBindings,
|
||||
zigElementSpelling,
|
||||
zigOptionalPayloadSpelling,
|
||||
zigPointeeSpelling,
|
||||
} from './range-binding.js';
|
||||
export { interpretZigImport, interpretZigTypeBinding, normalizeZigTypeName } from './interpret.js';
|
||||
export {
|
||||
expandZigWildcardNames,
|
||||
|
|
|
|||
|
|
@ -90,14 +90,16 @@ export function interpretZigImport(captures: CaptureMatch): ParsedImport | null
|
|||
*/
|
||||
export function normalizeZigTypeName(text: string): string {
|
||||
let t = text.trim();
|
||||
// Error union first: the payload of `E!*T` / `!?T` carries its own sigils
|
||||
// (F6: `Allocator.Error!*Page` is the shape of every fallible constructor).
|
||||
const bang = t.lastIndexOf('!');
|
||||
if (bang !== -1) t = t.slice(bang + 1).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();
|
||||
// Generic instantiation `List(u8)` / `std.ArrayList(u8)` → the type
|
||||
// constructor `List` / `std.ArrayList`: Zig spells a generic type as a
|
||||
// call, and the container def is registered under the function's name.
|
||||
|
|
@ -129,7 +131,22 @@ export function interpretZigTypeBinding(captures: CaptureMatch): ParsedTypeBindi
|
|||
// `var c = Counter.init();` — the call's receiver names the type when it
|
||||
// is a container (`Counter`, `mod.Counter`, `List(u8)`); a value receiver
|
||||
// (`std.mem`, `self.items`) simply finds no container and declines.
|
||||
// `const t = makeThing();` — the callee name, chained to its return
|
||||
// binding by the shared resolver.
|
||||
source = 'constructor-inferred';
|
||||
// `const el = node.asElement();` on a fn-LOCAL receiver (F6): the type is
|
||||
// the METHOD's return type, spelled as the compound `node.asElement()` the
|
||||
// shared resolver walks (receiver binding → class scope → the method's
|
||||
// `@type-binding.return`). Kept verbatim: `normalizeZigTypeName` would
|
||||
// read the `()` as a generic instantiation and strip it.
|
||||
if (captures['@type-binding.member-call-return'] !== undefined) {
|
||||
return { boundName: name, rawTypeName: type.trim(), source };
|
||||
}
|
||||
} else if (captures['@type-binding.return'] !== undefined) {
|
||||
// `fn make() !*Thing` — `make ↦ Thing`, in the enclosing scope, so a
|
||||
// call site chains through it. Error unions / pointers / optionals are
|
||||
// stripped by `normalizeZigTypeName` (`Allocator.Error!*Page` → `Page`).
|
||||
source = 'return-annotation';
|
||||
} else if (captures['@type-binding.annotation'] !== undefined) {
|
||||
// `var x: T = undefined;` / `const x: T = .init(…);` — the declared type.
|
||||
source = 'annotation';
|
||||
|
|
|
|||
|
|
@ -258,22 +258,32 @@ const ZIG_SCOPE_QUERY = `
|
|||
. (identifier) @type-binding.name
|
||||
type: (_) @type-binding.type) @type-binding.annotation
|
||||
|
||||
;; Type bindings — call-return inference: var c = Counter.init(); (Rust's
|
||||
;; \`let x = Foo::new()\` twin). The receiver of the call is the type when it
|
||||
;; names a container (\`Counter\`, \`mod.Counter\`, \`List(u8)\`); when it is a
|
||||
;; value (\`std.mem\`, \`self.items\`) the lookup finds no container and
|
||||
;; declines, exactly as Rust's does. Keyword-gated: \`_ = e.top();\` is an
|
||||
;; assignment (same node type), not a binding of \`_\`.
|
||||
;; Type bindings — value inference (F6): const t = <value>; where <value> is a
|
||||
;; call (\`Counter.init()\`, \`makeThing()\`, \`node.asElement()\`), possibly
|
||||
;; wrapped in \`try\` / \`catch …\` / \`… orelse …\` / parentheses — the shape of
|
||||
;; nearly every Zig constructor call (\`const p = try Page.init(…)\`). The
|
||||
;; query only pins the declaration; \`emitZigScopeCaptures\` unwraps the
|
||||
;; wrappers and rewrites \`@type-binding.type\` to the type source (see
|
||||
;; \`zigCallReturnTypeOf\`). Keyword-gated: \`_ = e.top();\` is an assignment
|
||||
;; (same node type), not a binding of \`_\`. Rust's twin is
|
||||
;; \`let x = Foo::new()\` / \`let x = foo().await\`.
|
||||
(variable_declaration
|
||||
"const" . (identifier) @type-binding.name
|
||||
(call_expression
|
||||
function: (field_expression
|
||||
object: (_) @type-binding.type)) .) @type-binding.call-return
|
||||
(_) @type-binding.value .) @type-binding.call-return
|
||||
(variable_declaration
|
||||
"var" . (identifier) @type-binding.name
|
||||
(call_expression
|
||||
function: (field_expression
|
||||
object: (_) @type-binding.type)) .) @type-binding.call-return
|
||||
(_) @type-binding.value .) @type-binding.call-return
|
||||
|
||||
;; Type bindings — return-type annotation (F6): \`fn make() !*Thing\` binds
|
||||
;; \`make ↦ Thing\` in the enclosing scope (Module for free fns, the container's
|
||||
;; Class scope for methods — that is where the compound resolver reads a
|
||||
;; method's return type for \`node.asElement()\`), so \`const t = makeThing()\`
|
||||
;; chains to \`Thing\`. Rust: \`(function_item … return_type:) @type-binding.return\`.
|
||||
;; \`emitZigScopeCaptures\` drops builtin / \`type\` returns (a \`List ↦ type\`
|
||||
;; binding would hijack the \`List(u8){}\` constructor chain).
|
||||
(function_declaration
|
||||
name: (identifier) @type-binding.name
|
||||
type: (_) @type-binding.type) @type-binding.return
|
||||
|
||||
;; Type bindings — aliases (F5 field-access aliases + F7 type aliases):
|
||||
;; \`const page = self.page;\` (F5: the RHS path is kept verbatim as the
|
||||
|
|
|
|||
370
gitnexus/src/core/ingestion/languages/zig/range-binding.ts
Normal file
370
gitnexus/src/core/ingestion/languages/zig/range-binding.ts
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
/**
|
||||
* Zig payload captures (F6): `for (items) |it|`, `if (opt) |v|`,
|
||||
* `while (it.next()) |x|`. The captured name has no annotation anywhere — its
|
||||
* type is the SUBJECT's type minus one layer (slice/array element, optional
|
||||
* payload) — so the tree-sitter query cannot bind it; this post-finalize hook
|
||||
* reads the subject's binding from the finished scope model and injects the
|
||||
* payload's. Rust/Go do the same (`rust/range-binding.ts`,
|
||||
* `go/range-binding.ts`).
|
||||
*
|
||||
* Deliberately narrow and honest: a payload binds only when the subject's
|
||||
* WRITTEN type visibly has the layer the construct removes — `[]T` / `[N]T` /
|
||||
* `[*]T` / `*[N]T` for `for`, `?T` (after any `E!`) for `if` / `while`. A
|
||||
* subject typed `std.ArrayList(T)`, `anytype`, or anything unresolved binds
|
||||
* nothing rather than guessing. `catch |err|` (an error, never a container
|
||||
* receiver) and `switch` prongs are skipped. The same one-layer projection
|
||||
* types `const t = items[i];` / `opt.?` / `ptr.*` (`bindProjectedLocal`).
|
||||
*
|
||||
* Runs after `propagateImportedReturnTypes`, so return bindings hoisted from
|
||||
* other files are visible when a subject is a call.
|
||||
*/
|
||||
|
||||
import type { ParsedFile, Scope, ScopeId, TypeRef } from 'gitnexus-shared';
|
||||
import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
|
||||
import { getZigParser } from './query.js';
|
||||
import { getTreeSitterBufferSize } from '../../constants.js';
|
||||
import { parseSourceSafe, ParseTimeoutError } from '../../../tree-sitter/safe-parse.js';
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
import { logger } from '../../../logger.js';
|
||||
import {
|
||||
findClassBindingInScope,
|
||||
findReceiverTypeBinding,
|
||||
isClassLike,
|
||||
} from '../../scope-resolution/scope/walkers.js';
|
||||
import { isZigKeywordDeclaration, zigUnwrapValue } from './captures.js';
|
||||
import { normalizeZigTypeName } from './interpret.js';
|
||||
|
||||
type ZigTree = ReturnType<ReturnType<typeof getZigParser>['parse']>;
|
||||
|
||||
const PAYLOAD_HOSTS: ReadonlySet<string> = new Set([
|
||||
'for_statement',
|
||||
'for_expression',
|
||||
'if_statement',
|
||||
'if_expression',
|
||||
'while_statement',
|
||||
'while_expression',
|
||||
]);
|
||||
|
||||
export function populateZigRangeBindings(
|
||||
parsedFiles: readonly ParsedFile[],
|
||||
indexes: ScopeResolutionIndexes,
|
||||
ctx: {
|
||||
readonly fileContents: ReadonlyMap<string, string>;
|
||||
readonly treeCache?: { get(filePath: string): unknown };
|
||||
},
|
||||
): void {
|
||||
const parser = getZigParser();
|
||||
|
||||
// Class def nodeId → its Class scope (whose typeBindings hold the members'
|
||||
// field types and the methods' return types). Same derivation as
|
||||
// `buildWorkspaceResolutionIndex`, which this hook does not receive.
|
||||
const classScopeByDefId = new Map<string, Scope>();
|
||||
for (const parsed of parsedFiles) {
|
||||
for (const scope of parsed.scopes) {
|
||||
if (scope.kind !== 'Class') continue;
|
||||
const cd = scope.ownedDefs.find((d) => isClassLike(d.type));
|
||||
if (cd !== undefined) classScopeByDefId.set(cd.nodeId, scope);
|
||||
}
|
||||
}
|
||||
|
||||
for (const parsed of parsedFiles) {
|
||||
const sourceText = ctx.fileContents.get(parsed.filePath);
|
||||
if (sourceText === undefined) continue;
|
||||
let tree = ctx.treeCache?.get(parsed.filePath) as ZigTree | undefined;
|
||||
if (tree === undefined) {
|
||||
try {
|
||||
tree = parseSourceSafe(parser, sourceText, undefined, {
|
||||
bufferSize: getTreeSitterBufferSize(sourceText),
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof ParseTimeoutError) {
|
||||
logger.warn(
|
||||
{ file: parsed.filePath },
|
||||
'zig range-binding: parse timed out, skipping file',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
const scopes = parsed.scopes;
|
||||
if (scopes.length === 0) continue;
|
||||
const resolver = new ZigSubjectTypeResolver(scopes, indexes, classScopeByDefId);
|
||||
|
||||
// Pre-order: an outer payload is bound before an inner construct reads it
|
||||
// (`for (pages) |p| { if (p.frame) |f| … }`).
|
||||
const visit = (node: SyntaxNode): void => {
|
||||
if (PAYLOAD_HOSTS.has(node.type)) bindPayloads(node, resolver);
|
||||
else if (node.type === 'variable_declaration') bindProjectedLocal(node, resolver);
|
||||
for (let i = 0; i < node.namedChildCount; i++) {
|
||||
const c = node.namedChild(i);
|
||||
if (c !== null) visit(c);
|
||||
}
|
||||
};
|
||||
visit(tree.rootNode);
|
||||
}
|
||||
}
|
||||
|
||||
/** Bind the payload identifiers of one `for` / `if` / `while` node. */
|
||||
function bindPayloads(node: SyntaxNode, resolver: ZigSubjectTypeResolver): void {
|
||||
const named = node.namedChildren.filter(
|
||||
(c): c is SyntaxNode => c !== null && c.type !== 'comment',
|
||||
);
|
||||
// The payload right after the subject(s); a trailing `else |err|` payload
|
||||
// belongs to the else_clause and is not a child here.
|
||||
const payload = named.find((c) => c.type === 'payload');
|
||||
if (payload === undefined) return;
|
||||
const isFor = node.type === 'for_statement' || node.type === 'for_expression';
|
||||
// A `for` lists its subjects BEFORE the payload; the body follows it.
|
||||
const subjects: SyntaxNode[] = isFor
|
||||
? named.slice(0, named.indexOf(payload)).filter((c) => c.type !== 'block_label')
|
||||
: [node.childForFieldName('condition')].filter((c): c is SyntaxNode => c !== null);
|
||||
if (subjects.length === 0) return;
|
||||
// For a `for` the payload names pair positionally with the subjects
|
||||
// (`for (items, 0..) |it, i|`); `if` / `while` capture one value.
|
||||
const captured = payload.namedChildren.filter(
|
||||
(c): c is SyntaxNode => c !== null && c.type === 'identifier',
|
||||
);
|
||||
const host = payloadHostScope(node, resolver);
|
||||
if (host === undefined) return;
|
||||
for (let i = 0; i < captured.length && i < subjects.length; i++) {
|
||||
const name = captured[i]!.text;
|
||||
if (name === '_') continue;
|
||||
const subject = subjects[i]!;
|
||||
if (subject.type === 'range_expression') continue; // `0..` — an index
|
||||
const spelling = resolver.spellingOf(subject);
|
||||
if (spelling === undefined) continue;
|
||||
const element = isFor ? zigElementSpelling(spelling) : zigOptionalPayloadSpelling(spelling);
|
||||
if (element === undefined) continue;
|
||||
const rawName = normalizeZigTypeName(element);
|
||||
if (rawName.length === 0 || rawName.startsWith('@')) continue;
|
||||
const existing = host.typeBindings.get(name);
|
||||
// Zig forbids shadowing, so an existing binding of this name in the host
|
||||
// can only be a sibling construct's payload (bare-expression bodies share
|
||||
// the enclosing scope). Two different types for one name: decline both
|
||||
// rather than let the last one type the first one's body.
|
||||
if (existing !== undefined) {
|
||||
if (existing.rawName !== rawName) {
|
||||
(host.typeBindings as Map<string, TypeRef>).delete(name);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const ref: TypeRef =
|
||||
element === rawName
|
||||
? { rawName, declaredAtScope: host.id, source: 'annotation' }
|
||||
: { rawName, declaredSpelling: element, declaredAtScope: host.id, source: 'annotation' };
|
||||
(host.typeBindings as Map<string, TypeRef>).set(name, ref);
|
||||
}
|
||||
}
|
||||
|
||||
/** `const t = items[i];` / `const t = opt.?;` / `const t = ptr.*;` — a local
|
||||
* bound to ONE LAYER under a typed subject: the element of a slice/array
|
||||
* (not a `[a..b]` re-slice), the payload of an optional, the pointee of a
|
||||
* pointer. Same honesty rule as the payloads: the subject's WRITTEN type
|
||||
* must show the layer. Skipped when the name is already typed (an
|
||||
* annotation, or a capture-time inference). */
|
||||
function bindProjectedLocal(decl: SyntaxNode, resolver: ZigSubjectTypeResolver): void {
|
||||
if (!isZigKeywordDeclaration(decl)) return;
|
||||
const named = decl.namedChildren.filter(
|
||||
(c): c is SyntaxNode => c !== null && c.type !== 'comment',
|
||||
);
|
||||
if (named.length < 2 || named[0]!.type !== 'identifier') return;
|
||||
const value = named[named.length - 1]!;
|
||||
if (value.id === decl.childForFieldName('type')?.id) return; // `const x: T;`
|
||||
const inner = zigUnwrapValue(value);
|
||||
let subject: SyntaxNode | null;
|
||||
let project: (spelling: string) => string | undefined;
|
||||
switch (inner.type) {
|
||||
case 'index_expression':
|
||||
if (inner.childForFieldName('index')?.type === 'range_expression') return; // `a[0..n]`
|
||||
subject = inner.childForFieldName('object');
|
||||
project = zigElementSpelling;
|
||||
break;
|
||||
case 'null_coercion_expression':
|
||||
subject = inner.namedChild(0);
|
||||
project = zigOptionalPayloadSpelling;
|
||||
break;
|
||||
case 'dereference_expression':
|
||||
subject = inner.namedChild(0);
|
||||
project = zigPointeeSpelling;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if (subject === null) return;
|
||||
const name = named[0]!.text;
|
||||
const host = resolver.scopeAt(decl.startPosition.row + 1, decl.startPosition.column, false);
|
||||
if (host === undefined || host.typeBindings.has(name)) return;
|
||||
const spelling = resolver.spellingOf(subject);
|
||||
if (spelling === undefined) return;
|
||||
const projected = project(spelling);
|
||||
if (projected === undefined) return;
|
||||
const rawName = normalizeZigTypeName(projected);
|
||||
if (rawName.length === 0 || rawName.startsWith('@')) return;
|
||||
const ref: TypeRef =
|
||||
projected === rawName
|
||||
? { rawName, declaredAtScope: host.id, source: 'assignment-inferred' }
|
||||
: {
|
||||
rawName,
|
||||
declaredSpelling: projected,
|
||||
declaredAtScope: host.id,
|
||||
source: 'assignment-inferred',
|
||||
};
|
||||
(host.typeBindings as Map<string, TypeRef>).set(name, ref);
|
||||
}
|
||||
|
||||
/** `*T` / `*const T` / `E!*T` → `T`; undefined when not visibly a pointer. */
|
||||
export function zigPointeeSpelling(spelling: string): string | undefined {
|
||||
let t = spelling.trim();
|
||||
const bang = t.lastIndexOf('!');
|
||||
if (bang !== -1) t = t.slice(bang + 1).trim();
|
||||
const m = /^\*(const\s+)?/.exec(t);
|
||||
if (m === null) return undefined;
|
||||
const rest = t.slice(m[0].length).trim();
|
||||
return rest.length > 0 ? rest : undefined;
|
||||
}
|
||||
|
||||
function body(node: SyntaxNode): SyntaxNode | null {
|
||||
return node.childForFieldName('body');
|
||||
}
|
||||
|
||||
/** The scope the payload name lives in: the body block's own scope when the
|
||||
* body is a block (`|t| { … }`), otherwise the innermost scope enclosing the
|
||||
* construct (a bare-expression body: `for (items) |t| t.run();`). */
|
||||
function payloadHostScope(node: SyntaxNode, resolver: ZigSubjectTypeResolver): Scope | undefined {
|
||||
// for/if/while_statement carry `body:`; in the *_expression forms the
|
||||
// body is the named child right after the payload.
|
||||
let bodyNode = body(node);
|
||||
if (bodyNode === null) {
|
||||
const named = node.namedChildren.filter((c): c is SyntaxNode => c !== null);
|
||||
const at = named.findIndex((c) => c.type === 'payload');
|
||||
bodyNode = at === -1 ? null : (named[at + 1] ?? null);
|
||||
}
|
||||
let block: SyntaxNode | null = bodyNode;
|
||||
if (block?.type === 'block_expression')
|
||||
block = block.namedChildren.find((c) => c?.type === 'block') ?? null;
|
||||
if (block?.type === 'labeled_statement')
|
||||
block = block.namedChildren.find((c) => c?.type === 'block') ?? null;
|
||||
if (block?.type === 'block') {
|
||||
const exact = resolver.scopeAt(block.startPosition.row + 1, block.startPosition.column, true);
|
||||
if (exact !== undefined) return exact;
|
||||
}
|
||||
return resolver.scopeAt(node.startPosition.row + 1, node.startPosition.column, false);
|
||||
}
|
||||
|
||||
/** `[]T` / `[]const T` / `[N]T` / `[*]T` / `[*:0]T` / `*[N]T` / `*const []T`
|
||||
* → `T` (with `T`'s own sigils kept: `[]*Thing` → `*Thing`). Undefined when
|
||||
* the written type has no slice/array layer to remove. */
|
||||
export function zigElementSpelling(spelling: string): string | undefined {
|
||||
let t = spelling.trim();
|
||||
const bang = t.lastIndexOf('!');
|
||||
if (bang !== -1) t = t.slice(bang + 1).trim();
|
||||
// A pointer TO an array/slice iterates the pointee.
|
||||
t = t.replace(/^\*(const\s+)?(?=\[)/, '');
|
||||
const m = /^\[[^\]]*\]\s*(const\s+)?/.exec(t);
|
||||
if (m === null) return undefined;
|
||||
const rest = t.slice(m[0].length).trim();
|
||||
return rest.length > 0 ? rest : undefined;
|
||||
}
|
||||
|
||||
/** `?T` / `?*T` / `E!?T` → `T` (sigils of `T` kept). Undefined when the
|
||||
* written type is not visibly optional. */
|
||||
export function zigOptionalPayloadSpelling(spelling: string): string | undefined {
|
||||
let t = spelling.trim();
|
||||
const bang = t.lastIndexOf('!');
|
||||
if (bang !== -1) t = t.slice(bang + 1).trim();
|
||||
if (!t.startsWith('?')) return undefined;
|
||||
const rest = t.slice(1).trim();
|
||||
return rest.length > 0 ? rest : undefined;
|
||||
}
|
||||
|
||||
/** Resolves a subject expression to the WRITTEN type spelling of its binding
|
||||
* (`declaredSpelling` when the capture layer reduced it, else `rawName`),
|
||||
* through the finished scope model of one file. */
|
||||
class ZigSubjectTypeResolver {
|
||||
constructor(
|
||||
private readonly scopes: readonly Scope[],
|
||||
private readonly indexes: ScopeResolutionIndexes,
|
||||
private readonly classScopeByDefId: ReadonlyMap<string, Scope>,
|
||||
) {}
|
||||
|
||||
/** Innermost scope containing (line, col); with `exact`, only a scope
|
||||
* STARTING there (a block's own scope). */
|
||||
scopeAt(line: number, col: number, exact: boolean): Scope | undefined {
|
||||
let best: Scope | undefined;
|
||||
for (const s of this.scopes) {
|
||||
const r = s.range;
|
||||
if (exact) {
|
||||
if (r.startLine === line && r.startCol === col && s.kind === 'Block') return s;
|
||||
continue;
|
||||
}
|
||||
const startsBefore = r.startLine < line || (r.startLine === line && r.startCol <= col);
|
||||
const endsAfter = r.endLine > line || (r.endLine === line && r.endCol >= col);
|
||||
if (!startsBefore || !endsAfter) continue;
|
||||
if (best === undefined || contains(best.range, r)) best = s;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
spellingOf(subject: SyntaxNode): string | undefined {
|
||||
const ref = this.typeRefOf(subject);
|
||||
return ref === undefined ? undefined : (ref.declaredSpelling ?? ref.rawName);
|
||||
}
|
||||
|
||||
private typeRefOf(subjectRaw: SyntaxNode): TypeRef | undefined {
|
||||
const subject = zigUnwrapValue(subjectRaw);
|
||||
const scope = this.scopeAt(subject.startPosition.row + 1, subject.startPosition.column, false);
|
||||
if (scope === undefined) return undefined;
|
||||
switch (subject.type) {
|
||||
case 'identifier':
|
||||
return findReceiverTypeBinding(scope.id, subject.text, this.indexes);
|
||||
case 'field_expression': {
|
||||
// `self.items` / `page.session` — the member's binding on the
|
||||
// object's class scope (field type, or a method's return type).
|
||||
const object = subject.childForFieldName('object');
|
||||
const member = subject.childForFieldName('member');
|
||||
if (object === null || member === null) return undefined;
|
||||
return this.memberRefOf(object, member.text, scope.id);
|
||||
}
|
||||
case 'call_expression': {
|
||||
const callee = subject.childForFieldName('function');
|
||||
if (callee === null) return undefined;
|
||||
if (callee.type === 'identifier') {
|
||||
// A free call: the fn's return binding, in the scope chain.
|
||||
return findReceiverTypeBinding(scope.id, callee.text, this.indexes);
|
||||
}
|
||||
if (callee.type !== 'field_expression') return undefined;
|
||||
const object = callee.childForFieldName('object');
|
||||
const member = callee.childForFieldName('member');
|
||||
if (object === null || member === null) return undefined;
|
||||
return this.memberRefOf(object, member.text, scope.id);
|
||||
}
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** The binding of `member` on the class the `object` expression is typed
|
||||
* by — recursively through field chains (`self.a.b`). */
|
||||
private memberRefOf(object: SyntaxNode, member: string, scopeId: ScopeId): TypeRef | undefined {
|
||||
const objectRef = this.typeRefOf(object);
|
||||
if (objectRef === undefined) return undefined;
|
||||
// A compound rawName (`node.asElement()`, F6's member-call shape) needs
|
||||
// the shared compound resolver; not walked here.
|
||||
if (objectRef.rawName.includes('(')) return undefined;
|
||||
const classDef = findClassBindingInScope(scopeId, objectRef.rawName, this.indexes);
|
||||
if (classDef === undefined) return undefined;
|
||||
const classScope = this.classScopeByDefId.get(classDef.nodeId);
|
||||
return classScope?.typeBindings.get(member);
|
||||
}
|
||||
}
|
||||
|
||||
function contains(outer: Scope['range'], inner: Scope['range']): boolean {
|
||||
const startsBefore =
|
||||
outer.startLine < inner.startLine ||
|
||||
(outer.startLine === inner.startLine && outer.startCol <= inner.startCol);
|
||||
const endsAfter =
|
||||
outer.endLine > inner.endLine ||
|
||||
(outer.endLine === inner.endLine && outer.endCol >= inner.endCol);
|
||||
return startsBefore && endsAfter;
|
||||
}
|
||||
|
|
@ -18,6 +18,7 @@ import { loadZigBuildConfig, type ZigBuildZonConfig } from '../../language-confi
|
|||
import { resolveZigImportInternal } from '../../import-resolvers/zig.js';
|
||||
import { zigProvider } from '../zig.js';
|
||||
import { expandZigWildcardNames, zigArityCompatibility, zigMergeBindings } from './index.js';
|
||||
import { populateZigRangeBindings } from './range-binding.js';
|
||||
|
||||
export const zigScopeResolver: ScopeResolver = {
|
||||
language: SupportedLanguages.Zig,
|
||||
|
|
@ -46,6 +47,10 @@ export const zigScopeResolver: ScopeResolver = {
|
|||
|
||||
populateOwners: (parsed: ParsedFile) => populateClassOwnedMembers(parsed),
|
||||
|
||||
// Payload captures — `for (items) |it|`, `if (opt) |v|`, `while (it.next())
|
||||
// |x|` — typed from the subject's binding after finalize (F6).
|
||||
populateRangeBindings: populateZigRangeBindings,
|
||||
|
||||
// Zig has no `super`.
|
||||
isSuperReceiver: () => false,
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ pub fn bump(self: *Page) void {
|
|||
self.count += 1;
|
||||
_ = self.getArena();
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
|
||||
// Field-typed receivers (F5): `session: *Session` types the member, so a
|
||||
// call through it dispatches into Session.zig — directly (`self.session.name()`)
|
||||
|
|
@ -33,3 +34,24 @@ pub fn sessionLabel(self: *Page) []const u8 {
|
|||
const s = self.session;
|
||||
return s.name();
|
||||
}
|
||||
|
||||
||||||| parent of fb83c9d9 (fix(zig): type locals through try/catch/orelse, return types and payload captures)
|
||||
|
||||
=======
|
||||
|
||||
// F6 — value flow: a fallible constructor, member calls whose RETURN types
|
||||
// type the local, optionals for `if` / `while` payloads.
|
||||
pub fn make(session: *Session) !*Page {
|
||||
_ = session;
|
||||
return error.Nope;
|
||||
}
|
||||
|
||||
pub fn getSession(self: *Page) *Session {
|
||||
return self.session;
|
||||
}
|
||||
|
||||
pub fn maybeSession(self: *Page) ?*Session {
|
||||
return self.session;
|
||||
}
|
||||
|
||||
>>>>>>> fb83c9d9 (fix(zig): type locals through try/catch/orelse, return types and payload captures)
|
||||
|
|
@ -11,3 +11,9 @@ pub fn findFrame(self: *Session, page: *Page) u32 {
|
|||
// Method call on a parameter typed by ANOTHER file-struct.
|
||||
return page.getArena();
|
||||
}
|
||||
|
||||
// F6 — an iterator-shaped method: `while (s.next()) |p| p.bump()`.
|
||||
pub fn next(self: *Session) ?*Page {
|
||||
_ = self;
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
109
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/flow.zig
vendored
Normal file
109
gitnexus/test/fixtures/lang-resolution/zig-filestruct/src/flow.zig
vendored
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
// F6 — the value-flow shapes that bind a local WITHOUT an annotation. Each fn
|
||||
// is one shape; the CALLS edge out of it (`… → bump`, `… → name`) only exists
|
||||
// when the local was typed. Lightpanda: `const page = try Page.init(…)`
|
||||
// (2,551 `try T.f()` sites), `for (self.frames) |*f|`, `if (self.doc) |doc|`,
|
||||
// `while (it.next()) |node|`, `const el = node.asElement()`.
|
||||
const Page = @import("Page.zig");
|
||||
const Session = @import("Session.zig");
|
||||
|
||||
// `try` / `catch` / `orelse` / parens around a constructor call — the
|
||||
// receiver names the type exactly as in the bare `Page.init(…)` shape.
|
||||
fn viaTry(s: *Session) !void {
|
||||
const p = try Page.make(s);
|
||||
p.bump();
|
||||
}
|
||||
fn viaCatch(s: *Session) void {
|
||||
const p = Page.make(s) catch return;
|
||||
p.bump();
|
||||
}
|
||||
fn viaOrelse(p: *Page) void {
|
||||
const s = p.maybeSession() orelse return;
|
||||
_ = s.name();
|
||||
}
|
||||
fn viaParens(s: *Session) !void {
|
||||
const p = (try Page.make(s));
|
||||
p.bump();
|
||||
}
|
||||
fn viaTryLiteral(s: *Session) !void {
|
||||
const p = try Page{ .session = s };
|
||||
p.bump();
|
||||
}
|
||||
|
||||
// A free call: the callee's RETURN type types the local.
|
||||
fn viaFreeCall() void {
|
||||
const p = makeLocal();
|
||||
p.bump();
|
||||
}
|
||||
fn makeLocal() Page {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// A member call on a fn-local receiver: the METHOD's return type, not the
|
||||
// receiver's — `s` is a Session, not a Page.
|
||||
fn viaMemberReturn(p: *Page) void {
|
||||
const s = p.getSession();
|
||||
_ = s.name();
|
||||
}
|
||||
|
||||
// The same on `self`, inside a container (`const el = self.asElement()`).
|
||||
const Runner = struct {
|
||||
page: *Page,
|
||||
pub fn current(self: *Runner) *Page {
|
||||
return self.page;
|
||||
}
|
||||
pub fn go(self: *Runner) void {
|
||||
const p = self.current();
|
||||
p.bump();
|
||||
}
|
||||
};
|
||||
|
||||
// Payload captures.
|
||||
fn forSlice(pages: []Page) void {
|
||||
for (pages) |*p| p.bump();
|
||||
for (pages) |p| {
|
||||
_ = p.getArena();
|
||||
}
|
||||
}
|
||||
fn forIndexed(pages: []Page) void {
|
||||
for (pages, 0..) |p, i| {
|
||||
_ = i;
|
||||
p.bump();
|
||||
}
|
||||
}
|
||||
fn ifOptional(o: ?*Page) void {
|
||||
if (o) |p| p.bump();
|
||||
}
|
||||
fn ifCallOptional(p: *Page) void {
|
||||
if (p.maybeSession()) |s| {
|
||||
_ = s.name();
|
||||
}
|
||||
}
|
||||
fn whileNext(s: *Session) void {
|
||||
while (s.next()) |p| p.bump();
|
||||
}
|
||||
|
||||
// One-layer projections bound to a local.
|
||||
fn viaIndex(pages: []Page) void {
|
||||
const p = pages[0];
|
||||
p.bump();
|
||||
}
|
||||
fn viaUnwrap(o: ?Page) void {
|
||||
const p = o.?;
|
||||
p.bump();
|
||||
}
|
||||
fn viaDeref(ptr: *Page) void {
|
||||
const p = ptr.*;
|
||||
p.bump();
|
||||
}
|
||||
|
||||
// Guards: nothing typed here, no edge may appear.
|
||||
fn viaTypeConstructor() void {
|
||||
// A TitleCase callee is a type constructor (an alias, F7), not a value.
|
||||
const L = List(u8);
|
||||
_ = L;
|
||||
}
|
||||
fn List(comptime T: type) type {
|
||||
return struct {
|
||||
x: T,
|
||||
};
|
||||
}
|
||||
|
|
@ -552,4 +552,59 @@ describe.skipIf(!zigAvailable)('Zig type aliases (zig-filestruct fixture, aliase
|
|||
// resolves `main`'s discards to anything.
|
||||
expect(calls.filter((c) => c.startsWith('main → '))).toEqual([]);
|
||||
});
|
||||
|
||||
// F6 — locals bound WITHOUT an annotation (src/flow.zig). Before this, the
|
||||
// call-return rule needed the `call_expression` as the DIRECT value child,
|
||||
// so `const p = try Page.make(s)` (the shape of 2,551 Lightpanda sites),
|
||||
// `… catch return`, `… orelse return` typed nothing; no fn had a
|
||||
// return-type binding, so `const t = makeThing()` / `const el =
|
||||
// node.asElement()` typed nothing; and payload captures (`for … |*p|`,
|
||||
// `if (o) |p|`, `while (it.next()) |p|`) had no binding at all. Every
|
||||
// edge below is a method call on such a local.
|
||||
it('types a local through try / catch / orelse / parens around a constructor call', () => {
|
||||
const calls = edgeSet(getRelationships(result, 'CALLS'));
|
||||
expect(calls).toContain('viaTry → bump');
|
||||
expect(calls).toContain('viaCatch → bump');
|
||||
expect(calls).toContain('viaOrelse → name');
|
||||
expect(calls).toContain('viaParens → bump');
|
||||
// `try Page{ .session = s }` — a wrapped struct literal
|
||||
expect(calls).toContain('viaTryLiteral → bump');
|
||||
});
|
||||
|
||||
it('types a local from the callee’s RETURN type: free call, member call on a local, member call on self', () => {
|
||||
const calls = edgeSet(getRelationships(result, 'CALLS'));
|
||||
// `const p = makeLocal();` — `fn makeLocal() Page`
|
||||
expect(calls).toContain('viaFreeCall → bump');
|
||||
// `const s = p.getSession();` — `p: *Page`, `fn getSession(self: *Page) *Session`.
|
||||
// The RECEIVER is a Page: had the old "receiver names the type" rule
|
||||
// applied to a value receiver, `s` would be a Page and `s.name()` would
|
||||
// find no method (or the wrong one).
|
||||
expect(calls).toContain('viaMemberReturn → name');
|
||||
// `const p = self.current();` inside `Runner`
|
||||
expect(calls).toContain('go → bump');
|
||||
// A TitleCase callee (`const L = List(u8)`) is a type constructor: the free
|
||||
// call itself resolves, and NOTHING else — `List ↦ type` must not bind.
|
||||
expect(calls.filter((c) => c.startsWith('viaTypeConstructor →'))).toEqual([
|
||||
'viaTypeConstructor → List',
|
||||
]);
|
||||
});
|
||||
|
||||
it('types payload captures from the subject: for |*p| / |p|, for (…, 0..) |p, i|, if (o) |p|, if (call) |s|, while (it.next()) |p|', () => {
|
||||
const calls = edgeSet(getRelationships(result, 'CALLS'));
|
||||
expect(calls).toContain('forSlice → bump');
|
||||
expect(calls).toContain('forSlice → getArena');
|
||||
expect(calls).toContain('forIndexed → bump');
|
||||
expect(calls).toContain('ifOptional → bump');
|
||||
// `if (p.maybeSession()) |s|` — the payload of the METHOD's `?*Session`
|
||||
expect(calls).toContain('ifCallOptional → name');
|
||||
// `while (s.next()) |p|` — `fn next(self: *Session) ?*Page`
|
||||
expect(calls).toContain('whileNext → bump');
|
||||
});
|
||||
|
||||
it('types one-layer projections bound to a local: items[i], opt.?, ptr.*', () => {
|
||||
const calls = edgeSet(getRelationships(result, 'CALLS'));
|
||||
expect(calls).toContain('viaIndex → bump');
|
||||
expect(calls).toContain('viaUnwrap → bump');
|
||||
expect(calls).toContain('viaDeref → bump');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,6 +31,11 @@ import {
|
|||
interpretZigTypeBinding,
|
||||
normalizeZigTypeName,
|
||||
} from '../../src/core/ingestion/languages/zig/interpret.js';
|
||||
import {
|
||||
zigElementSpelling,
|
||||
zigOptionalPayloadSpelling,
|
||||
zigPointeeSpelling,
|
||||
} from '../../src/core/ingestion/languages/zig/range-binding.js';
|
||||
import { createFieldExtractor } from '../../src/core/ingestion/field-extractors/generic.js';
|
||||
import { zigFieldConfig } from '../../src/core/ingestion/field-extractors/configs/zig.js';
|
||||
import { zigProvider } from '../../src/core/ingestion/languages/zig.js';
|
||||
|
|
@ -1242,3 +1247,164 @@ pub fn run(self: *Holder) void {
|
|||
});
|
||||
},
|
||||
);
|
||||
|
||||
describeZig('Zig value-inferred and return-type bindings (F6)', () => {
|
||||
const bindingsOf = (src: string, file = 'x.zig') =>
|
||||
emitZigScopeCaptures(src, file)
|
||||
.filter(
|
||||
(m) => m['@type-binding.name'] !== undefined && m['@type-binding.parameter'] === undefined,
|
||||
)
|
||||
.map((m) => ({
|
||||
...interpretZigTypeBinding(m),
|
||||
kind: Object.keys(m).find(
|
||||
(k) =>
|
||||
k !== '@type-binding.name' &&
|
||||
k !== '@type-binding.type' &&
|
||||
k !== '@type-binding.member-call-return',
|
||||
),
|
||||
}));
|
||||
const cr = (boundName: string, rawTypeName: string) => ({
|
||||
boundName,
|
||||
rawTypeName,
|
||||
source: 'constructor-inferred',
|
||||
kind: '@type-binding.call-return',
|
||||
});
|
||||
const ret = (boundName: string, rawTypeName: string) => ({
|
||||
boundName,
|
||||
rawTypeName,
|
||||
source: 'return-annotation',
|
||||
kind: '@type-binding.return',
|
||||
});
|
||||
|
||||
it('unwraps try / catch / orelse / parens around a constructor call — the receiver still names the type', () => {
|
||||
// Before: the call had to be the DIRECT value child, so every one of
|
||||
// these (2,551 `try T.f()` sites in Lightpanda) typed nothing.
|
||||
const src = `
|
||||
fn f() !void {
|
||||
const a = try Thing.make();
|
||||
const b = Thing.make() catch return;
|
||||
const c = Thing.maybe() orelse return;
|
||||
const d = (Thing.make());
|
||||
const e = try Thing.make() catch |err| return err;
|
||||
var g = try mod.Thing.make();
|
||||
}
|
||||
`;
|
||||
expect(bindingsOf(src)).toEqual([
|
||||
cr('a', 'Thing'),
|
||||
cr('b', 'Thing'),
|
||||
cr('c', 'Thing'),
|
||||
cr('d', 'Thing'),
|
||||
cr('e', 'Thing'),
|
||||
cr('g', 'mod.Thing'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('a wrapped struct literal (`try Thing{…}`) is a constructor binding', () => {
|
||||
expect(bindingsOf('fn f() !void { const t = try Thing{ .a = 1 }; }')).toEqual([
|
||||
{
|
||||
boundName: 't',
|
||||
rawTypeName: 'Thing',
|
||||
source: 'constructor-inferred',
|
||||
kind: '@type-binding.constructor',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('a free call binds the callee name (chained to its return binding); a member call on a fn-LOCAL receiver binds the compound `recv.method()`', () => {
|
||||
const src = `
|
||||
fn f(node: *Node) void {
|
||||
const t = makeThing();
|
||||
const el = try node.asElement();
|
||||
const p = self.parser.next();
|
||||
for (items) |it| { const n = it.next(); }
|
||||
}
|
||||
const R = struct { pub fn go(self: *R) void { const q = self.current(); } };
|
||||
`;
|
||||
expect(bindingsOf(src)).toEqual([
|
||||
cr('t', 'makeThing'),
|
||||
// `node` is a parameter: the type is asElement's RETURN, walked by the
|
||||
// shared compound resolver — NOT `Node` (the old rule typed el as its
|
||||
// receiver, a confidently wrong owner for `el.m()`).
|
||||
cr('el', 'node.asElement()'),
|
||||
// `self` is NOT a local of `f` (no such parameter): module-level
|
||||
// receiver semantics — the receiver text is the type (declines later).
|
||||
cr('p', 'self.parser'),
|
||||
// a payload capture is a local too
|
||||
cr('n', 'it.next()'),
|
||||
cr('q', 'self.current()'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('emits nothing for a TitleCase callee (type constructor), a decl literal, an identifier or a field chain', () => {
|
||||
const src = `
|
||||
fn f() void {
|
||||
const B = util.List(u8);
|
||||
const L = List(u8);
|
||||
const X = Foo.Bar();
|
||||
const d = .init();
|
||||
const y = other;
|
||||
const z = a.b;
|
||||
const n = 42;
|
||||
}
|
||||
`;
|
||||
expect(bindingsOf(src)).toEqual([]);
|
||||
});
|
||||
|
||||
it('binds a fn name to its NOMINAL return type; builtins, `type`, @TypeOf and comptime type parameters bind nothing', () => {
|
||||
const src = `
|
||||
pub const Iter = struct {
|
||||
const Self = @This();
|
||||
pub fn next(self: *Iter) ?Thing { _ = self; return null; }
|
||||
pub fn me(self: *Iter) *Self { return self; }
|
||||
pub fn mine() @This() { return .{}; }
|
||||
pub fn count(self: *Iter) usize { _ = self; return 0; }
|
||||
pub fn is(self: *Iter, comptime T: type) ?*T { _ = self; return null; }
|
||||
pub fn Wrap(comptime T: type) type { return struct { pub fn get(self: *@This()) T { _ = self; } }; }
|
||||
};
|
||||
pub fn makeErr() std.mem.Allocator.Error!*Thing { return .{}; }
|
||||
pub fn make0() void {}
|
||||
pub fn make1() []const u8 {}
|
||||
pub fn make2() @TypeOf(x) {}
|
||||
pub fn make3() !List(u8) {}
|
||||
`;
|
||||
expect(bindingsOf(src)).toEqual([
|
||||
ret('next', 'Thing'),
|
||||
// `*Self` and `@This()` both name the enclosing container
|
||||
ret('me', 'Iter'),
|
||||
ret('mine', 'Iter'),
|
||||
ret('makeErr', 'Thing'),
|
||||
ret('make3', 'List'),
|
||||
]);
|
||||
});
|
||||
|
||||
it('a file-struct’s `@This()` return names the file stem', () => {
|
||||
const src = `
|
||||
count: u32 = 0,
|
||||
pub fn init() @This() { return .{}; }
|
||||
`;
|
||||
expect(bindingsOf(src, 'src/Page.zig')).toEqual([ret('init', 'Page')]);
|
||||
});
|
||||
|
||||
it('normalizes an error-union payload’s own sigils (`Allocator.Error!*Page` → `Page`)', () => {
|
||||
expect(normalizeZigTypeName('Allocator.Error!*Page')).toBe('Page');
|
||||
expect(normalizeZigTypeName('!?*const Page')).toBe('Page');
|
||||
expect(normalizeZigTypeName('anyerror![]Thing')).toBe('Thing');
|
||||
});
|
||||
|
||||
it('projects one layer off a written type for payloads and projections', () => {
|
||||
expect(zigElementSpelling('[]Thing')).toBe('Thing');
|
||||
expect(zigElementSpelling('[]const *Thing')).toBe('*Thing');
|
||||
expect(zigElementSpelling('[4]Thing')).toBe('Thing');
|
||||
expect(zigElementSpelling('[*:0]const u8')).toBe('u8');
|
||||
expect(zigElementSpelling('*const []Thing')).toBe('Thing');
|
||||
expect(zigElementSpelling('![]Thing')).toBe('Thing');
|
||||
// not visibly iterable — declines instead of guessing
|
||||
expect(zigElementSpelling('std.ArrayList(Thing)')).toBeUndefined();
|
||||
expect(zigElementSpelling('Thing')).toBeUndefined();
|
||||
expect(zigOptionalPayloadSpelling('?*Thing')).toBe('*Thing');
|
||||
expect(zigOptionalPayloadSpelling('!?Thing')).toBe('Thing');
|
||||
expect(zigOptionalPayloadSpelling('*Thing')).toBeUndefined();
|
||||
expect(zigPointeeSpelling('*const Thing')).toBe('Thing');
|
||||
expect(zigPointeeSpelling('Thing')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue