Files
sx/src/ir/resolver.zig
2026-06-09 13:05:23 +03:00

742 lines
32 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! The unified sx name/type resolver — the shared author-collection layer.
//!
//! A read-only facade over the borrowed Phase A import facts on a
//! `*ProgramIndex` (`module_decls` / `namespace_edges`) and the existing
//! `import_graph` / `flat_import_graph` views. It OWNS nothing import-derived;
//! those maps live in `imports.zig`/`core.zig` and are borrowed here.
//!
//! Two collectors sit on top of these facts (R5 §1 #1):
//! - `collectVisibleAuthors` — own author the flat-import edge walk. THE one
//! graph-walk; the permanent flat-import F-series root.
//! - `collectNamespaceAuthors` — a single already-selected namespace target's
//! members. NO graph walk.
//!
//! Both are RAW and verdict-free: they return who authors a name, not which
//! author wins. Per-domain selectors (Phase C+) decide eligibility. Nothing
//! routes resolution through these collectors yet.
//!
//! Falsifiable invariant (R5 §1 #1): there is EXACTLY ONE iterator over
//! `flat_import_graph`/`import_graph` in this file — inside
//! `collectVisibleAuthors`. `collectNamespaceAuthors` iterates one
//! `NamespaceTarget.own_decls` slice and touches no graph. This is what keeps
//! 0102 (callable) and 0105 (type) the SAME cross-module edge-walk.
const std = @import("std");
const ast = @import("../ast.zig");
const imports = @import("../imports.zig");
const program_index = @import("program_index.zig");
const ProgramIndex = program_index.ProgramIndex;
// ── Raw-fact aliases (defined in imports.zig by buildImportFacts, Phase A) ──
pub const RawDeclRef = imports.RawDeclRef;
pub const RawAuthor = imports.RawAuthor;
pub const NamespaceTarget = imports.NamespaceTarget;
/// Author multiplicity for ONE name as seen from ONE querying module: the
/// own-module author (tier-2) plus the distinct flat-import authors (tier-3),
/// diamond-deduped by author identity. RAW — no verdict, no domain, no pick.
pub const AuthorSet = struct {
/// The author declared in the querying module itself, if any.
own: ?RawAuthor,
/// Distinct flat-import authors. Diamond imports of the SAME author (same
/// AST node reached over two edges, e.g. a directory aggregate and one of
/// its member files) collapse to a single entry. Always disjoint from `own`.
flat: []const RawAuthor,
/// own + flat, counted by author identity. `flat` is already deduped and
/// disjoint from `own`, so this is a plain sum.
pub fn distinctCount(self: AuthorSet) usize {
return (if (self.own != null) @as(usize, 1) else 0) + self.flat.len;
}
};
/// How a name's cross-module visibility is computed. The author collector and
/// the lowering-side visibility predicate (`Lowering.isVisible`) both switch on
/// this single vocabulary.
pub const VisibilityMode = enum {
/// own scope `flat_import_graph`. The PERMANENT core for bare-name lookup
/// under flat imports (Agra constraint) — never a transitional path.
user_bare_flat,
/// `user_bare_flat` plus the foreign-C gate (today's `isCImportVisible`):
/// only C-import `fn_decl`s without a `library_ref` are policed; everything
/// else is unconditionally visible.
c_import_bare,
/// own scope the TRANSITIVE import relation (specs.md:793-801). Owned by
/// `ProtocolResolver.findVisibleImpls`; the single-hop author collector
/// never serves it.
impl_transitive,
/// Registration / lazy lowering: falls open (visible), emits no user
/// diagnostic, performs no graph walk.
lowering_internal,
};
/// Read-only facade over the borrowed import facts. `alloc` backs the
/// `AuthorSet.flat` slices the collectors return (the caller owns + frees them).
pub const Resolver = struct {
index: *ProgramIndex,
alloc: std.mem.Allocator,
pub fn init(index: *ProgramIndex, alloc: std.mem.Allocator) Resolver {
return .{ .index = index, .alloc = alloc };
}
/// THE single graph-walk in this file (falsifiable invariant, R5 §1 #1):
/// the own author declared in `from` the flat-import authors reachable
/// over the edge set `vis` chooses. RAW — selectors decide eligibility, not
/// this. `from` is the querying module's source path.
///
/// Edge set by mode: `flat_import_graph` for `user_bare_flat`/
/// `c_import_bare`. `impl_transitive` (a transitive closure owned by
/// `findVisibleImpls`) and `lowering_internal` (no graph walk) are not
/// single-hop author walks — reaching them here is a wiring bug, so we trip
/// loudly.
pub fn collectVisibleAuthors(
self: *Resolver,
name: []const u8,
from: []const u8,
vis: VisibilityMode,
) AuthorSet {
const decls = self.index.module_decls orelse return .{ .own = null, .flat = &.{} };
const own: ?RawAuthor = blk: {
const mod = decls.get(from) orelse break :blk null;
const ref = mod.names.get(name) orelse break :blk null;
break :blk .{ .raw = ref, .source = mod.source };
};
const graph = (switch (vis) {
.user_bare_flat, .c_import_bare => self.index.flat_import_graph,
// findVisibleImpls owns transitive visibility; lowering_internal
// performs no graph walk. Neither selects a single-hop edge set.
.impl_transitive, .lowering_internal => @panic(
"collectVisibleAuthors: vis mode performs no single-hop author walk",
),
}) orelse return .{ .own = own, .flat = &.{} };
const direct = graph.get(from) orelse return .{ .own = own, .flat = &.{} };
var flat = std.ArrayList(RawAuthor).empty;
var it = direct.iterator(); // ← the one graph iterator (invariant)
while (it.next()) |kv| {
const dep = decls.get(kv.key_ptr.*) orelse continue;
const ref = dep.names.get(name) orelse continue;
const cand = RawAuthor{ .raw = ref, .source = dep.source };
if (sameAuthor(own, cand)) continue; // keep flat disjoint from own
if (containsAuthor(flat.items, cand)) continue; // diamond dedup
flat.append(self.alloc, cand) catch @panic("collectVisibleAuthors: OOM");
}
return .{
.own = own,
.flat = flat.toOwnedSlice(self.alloc) catch @panic("collectVisibleAuthors: OOM"),
};
}
/// Container collector for ONE already-selected namespace target. Iterates
/// the target's `own_decls` and touches NO import graph (R5 §1 #1). A
/// namespace's `own_decls` is name-deduped, so a name has at most one author
/// here — returned as `own`, sourced to the target's module path.
pub fn collectNamespaceAuthors(
self: *Resolver,
target: NamespaceTarget,
name: []const u8,
) AuthorSet {
_ = self;
for (target.own_decls) |decl| {
const dn = decl.data.declName() orelse continue;
if (!std.mem.eql(u8, dn, name)) continue;
const ref = imports.rawDeclRefOf(decl) orelse continue;
return .{ .own = .{ .raw = ref, .source = target.target_module_path }, .flat = &.{} };
}
return .{ .own = null, .flat = &.{} };
}
};
/// Author identity is the AST node pointer the `RawDeclRef` wraps; every variant
/// holds a pointer, so a single `inline else` extracts it.
fn authorNodePtr(ref: RawDeclRef) usize {
return switch (ref) {
inline else => |p| @intFromPtr(p),
};
}
fn sameAuthor(a: ?RawAuthor, b: RawAuthor) bool {
const aa = a orelse return false;
return authorNodePtr(aa.raw) == authorNodePtr(b.raw);
}
fn containsAuthor(list: []const RawAuthor, b: RawAuthor) bool {
for (list) |x| {
if (authorNodePtr(x.raw) == authorNodePtr(b.raw)) return true;
}
return false;
}
// ── The owning resolution pass (Fork C S2.1a) ───────────────────────────────
//
// `resolve` turns this module from a raw author-collection FACADE into the
// OWNING pass: ONE exhaustive recursive walk of the resolved AST that populates a
// `ResolvedProgram` — node-keyed side tables binding each user spelling to its RAW
// author identity. ADDITIVE / PARALLEL / UNCONSUMED: lowering still reads the old
// selectors, so this changes no generated byte. The walk switches over EVERY
// `ast.Node.Data` kind with NO `else` arm, so a newly added node kind is a compile
// error here rather than a silently unvisited subtree (it structurally cannot be
// "half-populated").
//
// S2.1a populates the three BARE-NAME domains (type / value-const / callable) via
// `collectVisibleAuthors`, and records generic-param references ($T, ..$Ts,
// $pack[i]) SYMBOLICALLY (template/pack ids, never TypeIds). The remaining seven
// tables are DECLARED but stay empty until S2.1b (namespace-qualified + the three
// head domains) and S2.1c (foreign-class / struct-const / UFCS) populate them.
/// A symbolic id for one enclosing generic TYPE/VALUE param (`$T`, `$N`), assigned
/// by the pass and indexing `ResolvedProgram.template_params`. Process-local.
pub const TemplateParamId = enum(u32) { _ };
/// A symbolic id for one enclosing type pack (`..$Ts`, referenced as `$Ts` /
/// `$Ts[i]`), assigned by the pass and indexing `ResolvedProgram.pack_params`.
pub const PackParamId = enum(u32) { _ };
/// One generic param, identified symbolically — NOT a TypeId (the concrete binding
/// is instantiation-time, owned by S2.2+).
pub const TemplateParamInfo = struct {
id: TemplateParamId,
name: []const u8,
/// The decl node that introduced the param (fn / struct / lambda / protocol /
/// impl) — the param's identity is its address, this is its scope owner.
owner: *const ast.Node,
/// `$N: u32` (value) vs `$T: Type` (type), read off the param's constraint.
is_value: bool,
};
/// One type pack, identified symbolically.
pub const PackParamInfo = struct {
id: PackParamId,
name: []const u8,
owner: *const ast.Node,
};
/// A reference to an enclosing pack — the whole pack (`$Ts`) or one element
/// (`$Ts[i]`). Symbolic.
pub const PackRef = struct {
id: PackParamId,
/// `$Ts[i]` literal index, or null for a whole-pack reference (`$Ts`).
index: ?u32 = null,
};
/// What ONE reference site resolves to — the S2.1 RAW form. `authors` carries the
/// collected author identity (own flat, diamond-deduped) with NO verdict:
/// own-wins / direct-flat ambiguity selection is S2.2. `template` / `pack` are
/// symbolic generic-param references.
pub const ResolvedRef = union(enum) {
authors: AuthorSet,
template: TemplateParamId,
pack: PackRef,
};
/// Node-keyed side table: an AST reference node → its `ResolvedRef`. Keyed by node
/// IDENTITY (the `*const ast.Node` pointer), so two textually-identical spellings
/// at different sites are distinct entries.
pub const NodeRefTable = std.AutoHashMap(*const ast.Node, ResolvedRef);
/// The output of the owning resolution pass: ten node-keyed side tables (one per
/// reference domain) plus the symbolic template/pack registries. OWNS every
/// allocation it holds — the maps, the registry lists, and each `AuthorSet.flat`
/// slice the collector returned — and frees them in `deinit`. Owned by
/// `Compilation`; borrowed by `ProgramIndex.resolved_program`.
pub const ResolvedProgram = struct {
alloc: std.mem.Allocator,
// ── bare-name domains (S2.1a populates these) ──
type_refs: NodeRefTable,
value_refs: NodeRefTable,
callable_refs: NodeRefTable,
// ── namespace-qualified + head domains (S2.1b) ──
namespace_refs: NodeRefTable,
generic_struct_heads: NodeRefTable,
type_fn_heads: NodeRefTable,
protocol_heads: NodeRefTable,
// ── foreign-class / struct-const / UFCS (S2.1c) ──
foreign_class_refs: NodeRefTable,
struct_const_refs: NodeRefTable,
ufcs_refs: NodeRefTable,
// ── symbolic generic-param registries ──
template_params: std.ArrayList(TemplateParamInfo) = .empty,
template_by_ptr: std.AutoHashMap(usize, TemplateParamId),
pack_params: std.ArrayList(PackParamInfo) = .empty,
pack_by_ptr: std.AutoHashMap(usize, PackParamId),
pub fn init(alloc: std.mem.Allocator) ResolvedProgram {
return .{
.alloc = alloc,
.type_refs = NodeRefTable.init(alloc),
.value_refs = NodeRefTable.init(alloc),
.callable_refs = NodeRefTable.init(alloc),
.namespace_refs = NodeRefTable.init(alloc),
.generic_struct_heads = NodeRefTable.init(alloc),
.type_fn_heads = NodeRefTable.init(alloc),
.protocol_heads = NodeRefTable.init(alloc),
.foreign_class_refs = NodeRefTable.init(alloc),
.struct_const_refs = NodeRefTable.init(alloc),
.ufcs_refs = NodeRefTable.init(alloc),
.template_by_ptr = std.AutoHashMap(usize, TemplateParamId).init(alloc),
.pack_by_ptr = std.AutoHashMap(usize, PackParamId).init(alloc),
};
}
pub fn deinit(self: *ResolvedProgram) void {
for (self.allTables()) |t| {
var it = t.valueIterator();
while (it.next()) |ref| switch (ref.*) {
.authors => |a| if (a.flat.len > 0) self.alloc.free(a.flat),
.template, .pack => {},
};
t.deinit();
}
self.template_params.deinit(self.alloc);
self.template_by_ptr.deinit();
self.pack_params.deinit(self.alloc);
self.pack_by_ptr.deinit();
}
fn allTables(self: *ResolvedProgram) [10]*NodeRefTable {
return .{
&self.type_refs, &self.value_refs,
&self.callable_refs, &self.namespace_refs,
&self.generic_struct_heads, &self.type_fn_heads,
&self.protocol_heads, &self.foreign_class_refs,
&self.struct_const_refs, &self.ufcs_refs,
};
}
};
/// Run the owning resolution pass over `root` (the resolved program root), using
/// `index`'s borrowed import facts (`module_decls` / `flat_import_graph`) for
/// author collection. `main_file` is the ambient-source fallback for nodes that
/// carry no `source_file` stamp. Returns a fully-owned `ResolvedProgram` (the
/// caller stores it and calls `deinit`). One pass, no AST mutation, no diagnostics
/// — parallel/unconsumed, so generated output is unaffected.
pub fn resolve(
root: *const ast.Node,
index: *ProgramIndex,
main_file: []const u8,
alloc: std.mem.Allocator,
) ResolvedProgram {
var pass = ResolvePass{
.res = Resolver.init(index, alloc),
.out = ResolvedProgram.init(alloc),
};
pass.visit(root, .{ .source = main_file, .scope = null });
return pass.out;
}
/// One frame of generic params introduced by an enclosing decl (fn / struct /
/// lambda / protocol / impl). Lives on the Zig call stack (no allocation), chained
/// to its parent — a reference resolves a name against the NEAREST enclosing frame.
const Frame = struct {
params: []const ast.StructTypeParam,
owner: *const ast.Node,
parent: ?*const Frame,
};
/// Ambient walk context: the querying module's source path (`collectVisibleAuthors`'s
/// `from`) and the enclosing generic-param scope.
const Ctx = struct {
source: []const u8,
scope: ?*const Frame,
};
/// A resolved generic-param reference: the matched param (its address is its
/// identity) plus the scope owner that declared it.
const GenericMatch = struct {
param: *const ast.StructTypeParam,
owner: *const ast.Node,
};
/// `$N: u32` is a value param; `$T: Type` (or a variadic / non-type constraint) is
/// a type param. Read off the param's constraint type-expr name.
fn paramIsValue(p: ast.StructTypeParam) bool {
if (p.is_variadic) return false;
return switch (p.constraint.data) {
.type_expr => |te| !std.mem.eql(u8, te.name, "Type"),
else => false,
};
}
/// Nearest enclosing generic param named `name`, or null when the name is not a
/// generic in scope (→ it is an ordinary bare-name reference).
fn lookupGeneric(scope: ?*const Frame, name: []const u8) ?GenericMatch {
var cur = scope;
while (cur) |f| : (cur = f.parent) {
for (f.params) |*p| {
if (std.mem.eql(u8, p.name, name)) return .{ .param = p, .owner = f.owner };
}
}
return null;
}
/// The single owning traversal. Holds the author collector + the `ResolvedProgram`
/// it populates; threads `Ctx` (ambient source + generic scope) down the tree.
const ResolvePass = struct {
res: Resolver,
out: ResolvedProgram,
/// Visit ONE node, then recurse into its children. A stamped
/// `node.source_file` (top-level decls, and cross-module fn bodies whose bare
/// names must resolve in their DEFINING module) overrides the ambient source
/// for this subtree; an unstamped node inherits its parent's.
fn visit(self: *ResolvePass, node: *const ast.Node, ctx: Ctx) void {
const here = Ctx{ .source = node.source_file orelse ctx.source, .scope = ctx.scope };
switch (node.data) {
// ── declarations that open a generic-param scope ──
.fn_decl => |*fd| {
var frame = Frame{ .params = fd.type_params, .owner = node, .parent = here.scope };
const inner = Ctx{ .source = here.source, .scope = &frame };
self.visitTypeParamConstraints(fd.type_params, inner);
for (fd.params) |p| {
self.visit(p.type_expr, inner);
if (p.default_expr) |d| self.visit(d, inner);
}
if (fd.return_type) |rt| self.visit(rt, inner);
self.visit(fd.body, inner);
},
.lambda => |*l| {
var frame = Frame{ .params = l.type_params, .owner = node, .parent = here.scope };
const inner = Ctx{ .source = here.source, .scope = &frame };
self.visitTypeParamConstraints(l.type_params, inner);
for (l.params) |p| {
self.visit(p.type_expr, inner);
if (p.default_expr) |d| self.visit(d, inner);
}
if (l.return_type) |rt| self.visit(rt, inner);
self.visit(l.body, inner);
},
.struct_decl => |*sd| {
var frame = Frame{ .params = sd.type_params, .owner = node, .parent = here.scope };
const inner = Ctx{ .source = here.source, .scope = &frame };
self.visitTypeParamConstraints(sd.type_params, inner);
self.visitAll(sd.field_types, inner);
self.visitAllOpt(sd.field_defaults, inner);
self.visitAll(sd.methods, inner);
self.visitAll(sd.constants, inner);
},
.protocol_decl => |*pd| {
var frame = Frame{ .params = pd.type_params, .owner = node, .parent = here.scope };
const inner = Ctx{ .source = here.source, .scope = &frame };
self.visitTypeParamConstraints(pd.type_params, inner);
for (pd.methods) |m| {
self.visitAll(m.params, inner);
if (m.return_type) |rt| self.visit(rt, inner);
if (m.default_body) |b| self.visit(b, inner);
}
},
.impl_block => |*ib| {
var frame = Frame{ .params = ib.target_type_params, .owner = node, .parent = here.scope };
const inner = Ctx{ .source = here.source, .scope = &frame };
self.visitTypeParamConstraints(ib.target_type_params, inner);
if (ib.target_type_expr) |tt| self.visit(tt, inner);
self.visitAll(ib.protocol_type_args, inner);
self.visitAll(ib.methods, inner);
},
.foreign_class_decl => |*fc| {
for (fc.members) |m| switch (m) {
.method => |meth| {
self.visitAll(meth.params, here);
if (meth.return_type) |rt| self.visit(rt, here);
if (meth.body) |b| self.visit(b, here);
},
.field => |fld| self.visit(fld.field_type, here),
.extends, .implements => {},
};
},
// ── the three bare-name domains + symbolic generic refs ──
.type_expr => self.classifyType(node, here),
.identifier => self.classifyValue(node, here),
.call => |*c| {
if (c.callee.data == .identifier) {
// bare-name callable HEAD — recorded here, not re-walked as a
// value ref.
self.recordAuthors(&self.out.callable_refs, c.callee, c.callee.data.identifier.name, here.source);
} else {
self.visit(c.callee, here);
}
self.visitAll(c.args, here);
},
.field_access => |*fa| {
// namespace-qualified / struct-const / UFCS receivers are
// S2.1b/c — a BARE identifier receiver is left unclassified here;
// a compound receiver is recursed so its inner refs are collected.
if (fa.object.data != .identifier) self.visit(fa.object, here);
},
.pack_index_type_expr => |*p| self.recordPack(&self.out.type_refs, node, p.pack_name, p.index, here.scope),
.comptime_pack_ref => |*p| self.recordPack(&self.out.value_refs, node, p.pack_name, null, here.scope),
.error_type_expr => |*e| {
if (e.name) |name| self.recordAuthors(&self.out.type_refs, node, name, here.source);
},
.parameterized_type_expr => |*p| {
// the head (generic-struct / type-fn / protocol) is S2.1b; the
// type args are ordinary references, collected now.
self.visitAll(p.args, here);
},
// ── structural recursion (no classification of their own) ──
.root => |*r| {
// each top-level decl carries its own ambient source stamp.
self.visitAll(r.decls, here);
},
.block => |*b| self.visitAll(b.stmts, here),
.binary_op => |*b| {
self.visit(b.lhs, here);
self.visit(b.rhs, here);
},
.chained_comparison => |*c| self.visitAll(c.operands, here),
.unary_op => |*u| self.visit(u.operand, here),
.if_expr => |*e| {
self.visit(e.condition, here);
self.visit(e.then_branch, here);
if (e.else_branch) |b| self.visit(b, here);
},
.match_expr => |*e| {
self.visit(e.subject, here);
for (e.arms) |arm| {
if (arm.pattern) |pat| self.visit(pat, here);
self.visit(arm.body, here);
}
},
.match_arm => |*arm| {
if (arm.pattern) |pat| self.visit(pat, here);
self.visit(arm.body, here);
},
.const_decl => |*cd| {
if (cd.type_annotation) |ta| self.visit(ta, here);
self.visit(cd.value, here);
},
.var_decl => |*vd| {
if (vd.type_annotation) |ta| self.visit(ta, here);
if (vd.value) |v| self.visit(v, here);
},
.assignment => |*a| {
self.visit(a.target, here);
self.visit(a.value, here);
},
.multi_assign => |*m| {
self.visitAll(m.targets, here);
self.visitAll(m.values, here);
},
.destructure_decl => |*d| self.visit(d.value, here),
.enum_decl => |*ed| {
self.visitAllOpt(ed.variant_types, here);
self.visitAllOpt(ed.variant_values, here);
if (ed.backing_type) |bt| self.visit(bt, here);
},
.union_decl => |*ud| self.visitAll(ud.field_types, here),
.struct_literal => |*sl| {
if (sl.type_expr) |te| self.visit(te, here);
for (sl.field_inits) |fi| self.visit(fi.value, here);
if (sl.init_block) |ib| self.visit(ib, here);
},
.param => |*p| {
self.visit(p.type_expr, here);
if (p.default_expr) |d| self.visit(d, here);
},
.defer_stmt => |*d| self.visit(d.expr, here),
.push_stmt => |*p| {
self.visit(p.context_expr, here);
self.visit(p.body, here);
},
.comptime_expr => |*c| self.visit(c.expr, here),
.insert_expr => |*i| self.visit(i.expr, here),
.return_stmt => |*r| if (r.value) |v| self.visit(v, here),
.array_type_expr => |*a| {
self.visit(a.length, here);
self.visit(a.element_type, here);
},
.slice_type_expr => |*s| self.visit(s.element_type, here),
.array_literal => |*a| {
if (a.type_expr) |te| self.visit(te, here);
self.visitAll(a.elements, here);
},
.index_expr => |*i| {
self.visit(i.object, here);
self.visit(i.index, here);
},
.slice_expr => |*s| {
self.visit(s.object, here);
if (s.start) |st| self.visit(st, here);
if (s.end) |en| self.visit(en, here);
},
.pointer_type_expr => |*p| self.visit(p.pointee_type, here),
.many_pointer_type_expr => |*p| self.visit(p.element_type, here),
.optional_type_expr => |*o| self.visit(o.inner_type, here),
.raise_stmt => |*r| self.visit(r.tag, here),
.try_expr => |*t| self.visit(t.operand, here),
.catch_expr => |*c| {
self.visit(c.operand, here);
self.visit(c.body, here);
},
.onfail_stmt => |*o| self.visit(o.body, here),
.force_unwrap => |*f| self.visit(f.operand, here),
.null_coalesce => |*n| {
self.visit(n.lhs, here);
self.visit(n.rhs, here);
},
.deref_expr => |*d| self.visit(d.operand, here),
.while_expr => |*w| {
self.visit(w.condition, here);
self.visit(w.body, here);
},
.for_expr => |*f| {
self.visit(f.iterable, here);
if (f.range_end) |re| self.visit(re, here);
self.visit(f.body, here);
},
.spread_expr => |*s| self.visit(s.operand, here),
.function_type_expr => |*ft| {
self.visitAll(ft.param_types, here);
if (ft.return_type) |rt| self.visit(rt, here);
},
.closure_type_expr => |*ct| {
self.visitAll(ct.param_types, here);
if (ct.return_type) |rt| self.visit(rt, here);
},
.tuple_type_expr => |*tt| self.visitAll(tt.field_types, here),
.tuple_literal => |*tl| {
for (tl.elements) |el| self.visit(el.value, here);
},
.ffi_intrinsic_call => |*f| {
self.visit(f.return_type, here);
self.visitAll(f.args, here);
},
.jni_env_block => |*j| {
self.visit(j.env, here);
self.visit(j.body, here);
},
// ── leaves: no child node, no bare-name reference of their own ──
// `namespace_decl` is a leaf HERE: its members belong to another
// module and are reached via `collectNamespaceAuthors` in S2.1b, not
// re-walked through the importing root.
.int_literal,
.float_literal,
.bool_literal,
.string_literal,
.enum_literal,
.caller_location,
.null_literal,
.break_expr,
.continue_expr,
.undef_literal,
.inferred_type,
.builtin_expr,
.compiler_expr,
.import_decl,
.namespace_decl,
.error_set_decl,
.foreign_expr,
.library_decl,
.framework_decl,
.ufcs_alias,
.c_import_decl,
=> {},
}
}
fn visitAll(self: *ResolvePass, nodes: anytype, ctx: Ctx) void {
for (nodes) |n| self.visit(n, ctx);
}
fn visitAllOpt(self: *ResolvePass, nodes: anytype, ctx: Ctx) void {
for (nodes) |n| if (n) |nn| self.visit(nn, ctx);
}
fn visitTypeParamConstraints(self: *ResolvePass, params: []const ast.StructTypeParam, ctx: Ctx) void {
for (params) |p| self.visit(p.constraint, ctx);
}
/// A type-position reference: a generic param in scope → symbolic template ref;
/// otherwise a user type, collected RAW. Builtins / undeclared names collect to
/// an empty set and are simply not recorded.
fn classifyType(self: *ResolvePass, node: *const ast.Node, ctx: Ctx) void {
const te = node.data.type_expr;
if (!te.is_raw) {
if (lookupGeneric(ctx.scope, te.name)) |m| {
self.recordTemplate(&self.out.type_refs, node, m);
return;
}
}
self.recordAuthors(&self.out.type_refs, node, te.name, ctx.source);
}
/// A value-position identifier: a generic value/type param in scope (shadowing)
/// → symbolic template ref; otherwise a module value/const, collected RAW.
fn classifyValue(self: *ResolvePass, node: *const ast.Node, ctx: Ctx) void {
const id = node.data.identifier;
if (!id.is_raw) {
if (lookupGeneric(ctx.scope, id.name)) |m| {
self.recordTemplate(&self.out.value_refs, node, m);
return;
}
}
self.recordAuthors(&self.out.value_refs, node, id.name, ctx.source);
}
/// RAW author collection for a bare name. Only recorded when the name has ≥1
/// visible author (own or flat); a builtin / local / undeclared spelling has
/// none and is omitted — this is what keeps the tables to genuine authors.
fn recordAuthors(self: *ResolvePass, table: *NodeRefTable, node: *const ast.Node, name: []const u8, from: []const u8) void {
const set = self.res.collectVisibleAuthors(name, from, .user_bare_flat);
if (set.distinctCount() == 0) return;
self.replaceRef(table, node, .{ .authors = set });
}
fn recordTemplate(self: *ResolvePass, table: *NodeRefTable, node: *const ast.Node, m: GenericMatch) void {
self.replaceRef(table, node, .{ .template = self.internTemplate(m) });
}
fn recordPack(self: *ResolvePass, table: *NodeRefTable, node: *const ast.Node, name: []const u8, index: ?u32, scope: ?*const Frame) void {
const m = lookupGeneric(scope, name) orelse return;
self.replaceRef(table, node, .{ .pack = .{ .id = self.internPack(m), .index = index } });
}
fn replaceRef(self: *ResolvePass, table: *NodeRefTable, node: *const ast.Node, ref: ResolvedRef) void {
const entry = table.getOrPut(node) catch @panic("resolve: OOM");
if (entry.found_existing) self.releaseRef(entry.value_ptr.*);
entry.value_ptr.* = ref;
}
fn releaseRef(self: *ResolvePass, ref: ResolvedRef) void {
switch (ref) {
.authors => |a| if (a.flat.len > 0) self.out.alloc.free(a.flat),
.template, .pack => {},
}
}
fn internTemplate(self: *ResolvePass, m: GenericMatch) TemplateParamId {
const key = @intFromPtr(m.param);
if (self.out.template_by_ptr.get(key)) |id| return id;
const id: TemplateParamId = @enumFromInt(@as(u32, @intCast(self.out.template_params.items.len)));
self.out.template_params.append(self.out.alloc, .{
.id = id,
.name = m.param.name,
.owner = m.owner,
.is_value = paramIsValue(m.param.*),
}) catch @panic("resolve: OOM");
self.out.template_by_ptr.put(key, id) catch @panic("resolve: OOM");
return id;
}
fn internPack(self: *ResolvePass, m: GenericMatch) PackParamId {
const key = @intFromPtr(m.param);
if (self.out.pack_by_ptr.get(key)) |id| return id;
const id: PackParamId = @enumFromInt(@as(u32, @intCast(self.out.pack_params.items.len)));
self.out.pack_params.append(self.out.alloc, .{
.id = id,
.name = m.param.name,
.owner = m.owner,
}) catch @panic("resolve: OOM");
self.out.pack_by_ptr.put(key, id) catch @panic("resolve: OOM");
return id;
}
};