Files
sx/src/ir/resolver.zig

1022 lines
46 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). S2.1b adds the
// namespace-qualified table (`alias.member` resolved via `collectNamespaceAuthors`)
// and the three HEAD tables (generic-struct / type-fn / protocol), binned by the
// resolved author's decl kind at `parameterized_type_expr` heads. S2.1c closes the
// set with the final three: a bare reference whose author is a `foreign_class_decl`
// is routed to `foreign_class_refs`; a `Type.CONST` field access whose base author
// is a struct carrying that const member fills `struct_const_refs`; and a UFCS
// alias (`alias :: ufcs target`) plus its rewrite call sites fill `ufcs_refs`. All
// ten domains are now populated — still PARALLEL / UNCONSUMED / RAW.
/// 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),
.ufcs_aliases = std.StringHashMap([]const u8).init(alloc),
};
defer pass.ufcs_aliases.deinit();
pass.seedTopLevelUfcsAliases(root);
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,
/// True only while visiting declarations that were already covered by the
/// top-level UFCS alias pre-scan. Their decl nodes still get recorded into
/// `ufcs_refs`, but the alias map keeps the scanDecls-style final state.
preseeded_decl: bool = false,
};
/// 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;
}
/// Bin ONE raw author by the head kind it can author: a struct with type params (a
/// generic-struct head), a fn / const-wrapped fn with type params (a type-function
/// head), or a protocol. The `type_params.len > 0` gate is the head test — a
/// non-generic struct or a zero-type-param fn authors no head kind and sets
/// nothing. The `const_decl` arm unwraps a `Name :: struct/fn(...)` const exactly
/// as `structDeclOfRaw` / `fnDeclOfRaw` do.
fn classifyHeadKind(raw: RawDeclRef, gs: *bool, tf: *bool, pr: *bool) void {
switch (raw) {
.struct_decl => |sd| if (sd.type_params.len > 0) {
gs.* = true;
},
.fn_decl => |fd| if (fd.type_params.len > 0) {
tf.* = true;
},
.const_decl => |cd| switch (cd.value.data) {
.struct_decl => |*sd| if (sd.type_params.len > 0) {
gs.* = true;
},
.fn_decl => |*fd| if (fd.type_params.len > 0) {
tf.* = true;
},
else => {},
},
.protocol_decl => pr.* = true,
else => {},
}
}
/// True when an author set resolves to a `foreign_class_decl` — the own author
/// decides when present, else any flat author. Such a reference is routed to
/// `foreign_class_refs` (its own domain) instead of the bare type/value table.
fn authorSetIsForeignClass(set: AuthorSet) bool {
if (set.own) |a| return std.meta.activeTag(a.raw) == .foreign_class_decl;
for (set.flat) |a| {
if (std.meta.activeTag(a.raw) == .foreign_class_decl) return true;
}
return false;
}
/// A struct author carrying a `const_decl` member named `field` — the RAW shape
/// `Type.CONST` field access resolves to. Mirrors the lowering `struct_const_map`
/// domain, which is struct-level constants only; enums / other decls carry no
/// const members, so only `struct_decl` matches.
fn structHasConstMember(raw: RawDeclRef, field: []const u8) bool {
return switch (raw) {
.struct_decl => |sd| blk: {
for (sd.constants) |c| {
if (c.data == .const_decl and std.mem.eql(u8, c.data.const_decl.name, field))
break :blk true;
}
break :blk false;
},
else => false,
};
}
/// Any author in the set (own or flat) is a struct with a const member `field`.
fn authorSetHasStructConst(set: AuthorSet, field: []const u8) bool {
if (set.own) |a| {
if (structHasConstMember(a.raw, field)) return true;
}
for (set.flat) |a| {
if (structHasConstMember(a.raw, field)) return true;
}
return false;
}
/// 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,
/// `alias name → target name` for every `ufcs_alias` seen so far on the walk.
/// Global (not block-scoped) and populated in traversal order, mirroring
/// lowering's flat `ufcs_alias_map`; lets a UFCS-rewrite call site resolve to
/// the alias target's author. Scratch — freed when `resolve` returns, NOT part
/// of the owned `ResolvedProgram`.
ufcs_aliases: std.StringHashMap([]const u8),
/// 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,
.preseeded_decl = ctx.preseeded_decl,
};
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) {
const cname = c.callee.data.identifier.name;
// a UFCS-alias callee (`alias(args)`, incl. the parser's
// pipe-desugared `x |> alias()`) resolves to the alias TARGET's
// author → ufcs_refs (S2.1c). Any other bare callee is an
// ordinary callable HEAD — recorded here, not re-walked as a
// value ref.
if (self.ufcs_aliases.get(cname)) |target| {
self.recordAuthorsInto(&self.out.ufcs_refs, c.callee, target, here.source);
} else {
self.recordAuthors(&self.out.callable_refs, c.callee, cname, here.source);
}
} else {
self.visit(c.callee, here);
}
self.visitAll(c.args, here);
},
.field_access => |*fa| {
// `alias.member` whose base is a namespace import edge of the
// ambient source resolves via `collectNamespaceAuthors` into the
// namespace-qualified table (S2.1b). Otherwise `Type.CONST` — a base
// resolving to a struct author that carries the named const member —
// fills `struct_const_refs` (S2.1c). A receiver that is neither (a
// local value / instance field access) records nothing and is not
// walked as a value ref; a compound receiver is recursed so its
// inner refs are collected.
if (fa.object.data == .identifier) {
const base = fa.object.data.identifier.name;
if (!self.classifyNamespaceQualified(node, base, fa.field, here.source))
_ = self.classifyStructConst(node, base, fa.field, here.source);
} else {
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 (`Name(args)`) is binned by its resolved author's decl
// kind into the generic-struct / type-fn / protocol head tables
// (S2.1b); the type args are ordinary references, collected now.
self.classifyHead(node, p.name, p.is_raw, here);
self.visitAll(p.args, here);
},
// ── structural recursion (no classification of their own) ──
.root => |*r| {
// each top-level decl carries its own ambient source stamp.
const decl_ctx = Ctx{ .source = here.source, .scope = here.scope, .preseeded_decl = true };
self.visitAll(r.decls, decl_ctx);
},
.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,
.c_import_decl,
=> {},
// `alias :: ufcs target` — record the alias→target binding (the
// target's RAW author) keyed by the decl node, and remember the alias
// name so its rewrite call sites resolve to the same target. The map is
// global / traversal-ordered, mirroring lowering's flat `ufcs_alias_map`.
.ufcs_alias => |ua| {
if (!here.preseeded_decl) {
self.ufcs_aliases.put(ua.name, ua.target) catch @panic("resolve: OOM");
}
self.recordAuthorsInto(&self.out.ufcs_refs, node, ua.target, here.source);
},
}
}
/// Mirror lowering's declaration pre-scan for UFCS aliases: top-level roots
/// and namespace declaration lists are scanned before function bodies are
/// visited, so a call can resolve through an alias declared later in the file.
/// Function/lambda/block bodies are intentionally not entered here; local
/// aliases keep normal statement-order behavior on the owning walk.
fn seedTopLevelUfcsAliases(self: *ResolvePass, node: *const ast.Node) void {
switch (node.data) {
.root => |*r| self.seedTopLevelUfcsAliasDecls(r.decls),
.namespace_decl => |*ns| self.seedTopLevelUfcsAliasDecls(ns.decls),
else => {},
}
}
fn seedTopLevelUfcsAliasDecls(self: *ResolvePass, decls: []const *ast.Node) void {
for (decls) |decl| switch (decl.data) {
.ufcs_alias => |ua| {
self.ufcs_aliases.put(ua.name, ua.target) catch @panic("resolve: OOM");
},
.namespace_decl => self.seedTopLevelUfcsAliases(decl),
else => {},
};
}
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. A
/// name whose author is a `foreign_class_decl` is routed to `foreign_class_refs`
/// (its own S2.1c domain) instead of the passed bare type/value/callable table.
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;
const dest = if (authorSetIsForeignClass(set)) &self.out.foreign_class_refs else table;
self.replaceRef(dest, node, .{ .authors = set });
}
/// RAW author collection into an explicit table, with NO foreign-class routing —
/// the destination domain is already chosen by the caller (UFCS rewrite sites
/// and alias decls, whose target is always a free function).
fn recordAuthorsInto(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 });
}
/// `alias.member`: when `alias` is a namespace import edge of `from`, resolve
/// `member` against that already-selected target via `collectNamespaceAuthors`
/// (NO graph walk) and record it into the namespace-qualified table. Returns
/// whether it recorded — a base that is not a namespace alias records nothing
/// here and lets the caller try the struct-const path.
fn classifyNamespaceQualified(self: *ResolvePass, node: *const ast.Node, alias: []const u8, member: []const u8, from: []const u8) bool {
const edges = self.res.index.namespace_edges orelse return false;
const aliases = edges.get(from) orelse return false;
const target = aliases.get(alias) orelse return false;
const set = self.res.collectNamespaceAuthors(target, member);
if (set.distinctCount() == 0) return false;
self.replaceRef(&self.out.namespace_refs, node, .{ .authors = set });
return true;
}
/// `Type.CONST`: when `base` resolves to a struct author carrying a const member
/// named `member`, record the base's RAW author set into `struct_const_refs`
/// (keyed by the field-access node) — the owning-type identity the constant
/// lives on. A base with authors but no matching const member, or no author at
/// all (a local value receiver), records nothing and releases its allocation.
/// Returns whether it recorded.
fn classifyStructConst(self: *ResolvePass, node: *const ast.Node, base: []const u8, member: []const u8, from: []const u8) bool {
const set = self.res.collectVisibleAuthors(base, from, .user_bare_flat);
if (set.distinctCount() == 0) return false;
if (!authorSetHasStructConst(set, member)) {
if (set.flat.len > 0) self.out.alloc.free(set.flat);
return false;
}
self.replaceRef(&self.out.struct_const_refs, node, .{ .authors = set });
return true;
}
/// A parameterized head (`Name(args)`) binned by its resolved author's decl
/// kind: a generic struct (struct with type params) → `generic_struct_heads`;
/// a type-function (fn / const-wrapped fn with type params) → `type_fn_heads`; a
/// protocol → `protocol_heads`. RAW — the whole author set is recorded with no
/// winner picked, so a name authored as more than one head kind across modules
/// lands a distinct entry in every matching table. A head naming a generic param
/// in scope is symbolic (not an author); a name with no user author (builtins
/// like `Vector`, undeclared) or only non-head authors is omitted.
fn classifyHead(self: *ResolvePass, node: *const ast.Node, name: []const u8, is_raw: bool, ctx: Ctx) void {
if (std.mem.indexOfScalar(u8, name, '.')) |first_dot| {
const set = self.collectQualifiedHeadAuthors(name, first_dot, ctx.source) orelse return;
self.classifyHeadSet(node, set);
return;
}
if (!is_raw and lookupGeneric(ctx.scope, name) != null) return;
const set = self.res.collectVisibleAuthors(name, ctx.source, .user_bare_flat);
if (set.distinctCount() == 0) return;
self.classifyHeadSet(node, set);
}
/// `alias.Member(args)` reaches this pass as one `parameterized_type_expr`
/// named `alias.Member`. Split like the old lowering path: alias before the
/// first dot, member after the last dot, then collect from the namespace
/// target's own declarations only.
fn collectQualifiedHeadAuthors(self: *ResolvePass, name: []const u8, first_dot: usize, from: []const u8) ?AuthorSet {
const alias = name[0..first_dot];
const last_dot = std.mem.lastIndexOfScalar(u8, name, '.') orelse first_dot;
const member = name[last_dot + 1 ..];
if (alias.len == 0 or member.len == 0) return null;
const edges = self.res.index.namespace_edges orelse return null;
const aliases = edges.get(from) orelse return null;
const target = aliases.get(alias) orelse return null;
const set = self.res.collectNamespaceAuthors(target, member);
if (set.distinctCount() == 0) return null;
return set;
}
fn classifyHeadSet(self: *ResolvePass, node: *const ast.Node, set: AuthorSet) void {
var gs = false;
var tf = false;
var pr = false;
if (set.own) |a| classifyHeadKind(a.raw, &gs, &tf, &pr);
for (set.flat) |a| classifyHeadKind(a.raw, &gs, &tf, &pr);
var tables: [3]*NodeRefTable = undefined;
var n: usize = 0;
if (gs) {
tables[n] = &self.out.generic_struct_heads;
n += 1;
}
if (tf) {
tables[n] = &self.out.type_fn_heads;
n += 1;
}
if (pr) {
tables[n] = &self.out.protocol_heads;
n += 1;
}
if (n == 0) {
// an author exists but is not a head kind (e.g. a non-generic struct or
// a zero-type-param fn) — own this set's allocation, then drop it.
if (set.flat.len > 0) self.out.alloc.free(set.flat);
return;
}
// each table OWNS its `AuthorSet.flat`; give the first match the collected
// slice and a fresh copy to every subsequent table so `deinit` frees each
// exactly once.
self.replaceRef(tables[0], node, .{ .authors = set });
var i: usize = 1;
while (i < n) : (i += 1) {
self.replaceRef(tables[i], node, .{ .authors = self.dupAuthorSet(set) });
}
}
/// A shallow copy of an `AuthorSet` with its own freshly-allocated `flat` slice
/// (the `RawAuthor` elements are borrowed AST pointers + source strings, so the
/// copy is shallow). Lets one head reference be recorded into several head
/// tables without aliasing the owned slice.
fn dupAuthorSet(self: *ResolvePass, set: AuthorSet) AuthorSet {
return .{
.own = set.own,
.flat = if (set.flat.len > 0)
(self.out.alloc.dupe(RawAuthor, set.flat) catch @panic("resolve: OOM"))
else
&.{},
};
}
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;
}
};