diff --git a/src/core.zig b/src/core.zig index b104b73..08f34f2 100644 --- a/src/core.zig +++ b/src/core.zig @@ -39,6 +39,11 @@ pub const Compilation = struct { /// `imports.buildDeclTable` in parallel with the import facts. Borrowed by /// `ProgramIndex.decl_table`. decl_table: imports.DeclTable, + /// The owning resolution pass's output (Fork C S2.1a), built by + /// `resolveProgram` before lowering and borrowed by + /// `ProgramIndex.resolved_program`. ADDITIVE / PARALLEL / UNCONSUMED — nothing + /// in lowering reads it yet, so generated output is byte-identical. + resolved_program: ?ir.resolver.ResolvedProgram = null, ir_emitter: ?ir.LLVMEmitter = null, /// Lowered IR module, kept alive past `generateCode` so post-link /// callbacks can re-enter the interpreter to invoke sx functions @@ -80,6 +85,7 @@ pub const Compilation = struct { m.deinit(); self.allocator.destroy(m); } + if (self.resolved_program) |*rp| rp.deinit(); self.diagnostics.deinit(); } @@ -295,6 +301,18 @@ pub const Compilation = struct { return null; } + /// Run the owning resolution pass (Fork C S2.1a) over the resolved root, + /// storing its `ResolvedProgram` on `self` and lending a borrowed pointer to + /// `index`. Slotted after the `program_index` import facts are wired and + /// before `lowerRoot`. ADDITIVE / PARALLEL / UNCONSUMED — nothing in lowering + /// reads the result yet, so this changes no generated byte; it is the clean + /// pass seam future consumers (S3) cut over to. + fn resolveProgram(self: *Compilation, index: *ir.ProgramIndex, root: *const Node) void { + if (self.resolved_program) |*rp| rp.deinit(); + self.resolved_program = ir.resolver.resolve(root, index, self.file_path, self.allocator); + index.resolved_program = &self.resolved_program.?; + } + /// Lower the parsed AST to the sx IR module (shadow pipeline). pub fn lowerToIR(self: *Compilation) !ir.Module { const root = self.resolved_root orelse self.root orelse return ir.Module.init(self.allocator); @@ -314,6 +332,7 @@ pub const Compilation = struct { lowering.program_index.module_decls = &self.module_decls; lowering.program_index.namespace_edges = &self.namespace_edges; lowering.program_index.decl_table = &self.decl_table; + self.resolveProgram(&lowering.program_index, root); lowering.lowerRoot(root); if (self.diagnostics.hasErrors()) return error.CompileError; diff --git a/src/ir/program_index.zig b/src/ir/program_index.zig index 3e55f64..b2e8930 100644 --- a/src/ir/program_index.zig +++ b/src/ir/program_index.zig @@ -5,6 +5,7 @@ const types = @import("types.zig"); const inst = @import("inst.zig"); const errors = @import("../errors.zig"); const type_resolver = @import("type_resolver.zig"); +const resolver = @import("resolver.zig"); const Node = ast.Node; const TypeId = types.TypeId; @@ -627,6 +628,11 @@ pub const ProgramIndex = struct { /// in parallel with the import facts. Borrowed view; nothing in lowering /// consumes it for selection yet (additive — S4 makes it the fact-store key). decl_table: ?*imports.DeclTable = null, + /// The owning resolution pass's output (Fork C S2.1a), built by + /// `resolver.resolve` and owned by `Compilation`. Borrowed view; ADDITIVE / + /// PARALLEL / UNCONSUMED — nothing in lowering reads it yet (lowering still + /// uses the old selectors), so generated output is byte-identical. + resolved_program: ?*resolver.ResolvedProgram = null, // ── Declaration maps ── /// Function name → AST decl. diff --git a/src/ir/resolver.test.zig b/src/ir/resolver.test.zig index 48354aa..3e18a55 100644 --- a/src/ir/resolver.test.zig +++ b/src/ir/resolver.test.zig @@ -279,3 +279,186 @@ test "resolver: visibility edge-walk — own + flat visible; namespaced-only onl try std.testing.expect(lower.nameVisibleOverEdges(null, &flat, "main", "secret")); try std.testing.expect(lower.nameVisibleOverEdges(&scopes, null, "main", "secret")); } + +// ── the owning resolution pass — ResolvedProgram population proof (S2.1a) ── + +/// Parse + resolve imports + build the raw facts AND the resolved root the pass +/// walks (built from `mod.decls`, exactly as `core.zig` does). `alloc` must be an +/// arena that outlives the views. +const Resolved = struct { + root: *ast.Node, + decls: imports.ModuleDecls, + flat_import_graph: Graph, + import_graph: Graph, +}; + +fn buildResolved(alloc: std.mem.Allocator, io: std.Io, absdir: []const u8, main_path: []const u8) !Resolved { + const main_bytes = try std.Io.Dir.readFileAlloc(.cwd(), io, main_path, alloc, .limited(1 << 20)); + const main_source = try alloc.dupeZ(u8, main_bytes); + var p = parser.Parser.init(alloc, main_source); + const parsed = p.parse() catch return error.ParseFailed; + + var diags = errors.DiagnosticList.init(alloc, main_source, main_path); + var chain = std.StringHashMap(void).init(alloc); + var cache = imports.ModuleCache.init(alloc); + var import_graph = Graph.init(alloc); + var flat_import_graph = Graph.init(alloc); + const stdlib_paths = [_][]const u8{}; + + const mod = try imports.resolveImports( + alloc, + io, + parsed, + absdir, + main_path, + &chain, + &cache, + null, + &diags, + &stdlib_paths, + &import_graph, + &flat_import_graph, + .{}, + ); + + const facts = try imports.buildImportFacts(alloc, main_path, mod, &cache); + + const root = try alloc.create(ast.Node); + root.* = .{ .span = parsed.span, .data = .{ .root = .{ .decls = mod.decls } } }; + + return .{ + .root = root, + .decls = facts.decls, + .flat_import_graph = flat_import_graph, + .import_graph = import_graph, + }; +} + +/// Find the top-level `fn_decl` named `name` in the resolved root, or null. +fn findFn(root: *const ast.Node, name: []const u8) ?*const ast.Node { + for (root.data.root.decls) |d| { + if (d.data == .fn_decl and std.mem.eql(u8, d.data.fn_decl.name, name)) return d; + } + return null; +} + +// The pass populates the three bare-name domains over REAL Phase A facts: a type +// reference (own struct author), a value/const reference (own const), and a +// callable head (flat-imported author). It keys every entry by NODE IDENTITY, and +// records generic-param references SYMBOLICALLY — a `$T` template ref and a +// `$args[0]` pack ref — never as a collected author. The seven unpopulated domains +// stay empty (S2.1b/c own them). +test "resolver: resolve — bare-name domains populated, keyed by node, symbolic generic refs" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + const io = testIo(); + + var tmp = std.testing.tmpDir(.{}); + defer tmp.cleanup(); + + try tmp.dir.writeFile(io, .{ .sub_path = "lib.sx", .data = "helper :: () -> s64 { 5 }\n" }); + try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data = + \\#import "lib.sx"; + \\Point :: struct { x: s64 } + \\LIMIT :: 10; + \\identity :: (x: $T) -> T { x } + \\third :: (..$args) -> $args[0] => args[0]; + \\use_point :: (p: Point) -> s64 { p.x } + \\main :: () -> s32 { + \\ n := helper(); + \\ m := LIMIT; + \\ _ = n; + \\ _ = m; + \\ return 0; + \\} + \\ + }); + + var dirbuf: [4096]u8 = undefined; + const absdir = dirbuf[0..try tmp.dir.realPath(io, &dirbuf)]; + const main_path = try std.fmt.allocPrint(alloc, "{s}/main.sx", .{absdir}); + + var prog = try buildResolved(alloc, io, absdir, main_path); + + var idx = ProgramIndex.init(alloc); + defer idx.deinit(); + idx.module_decls = &prog.decls; + idx.flat_import_graph = &prog.flat_import_graph; + idx.import_graph = &prog.import_graph; + + var rp = resolver.resolve(prog.root, &idx, main_path, alloc); + defer rp.deinit(); + + // (1) The three bare-name domains are NON-EMPTY. + try std.testing.expect(rp.type_refs.count() > 0); + try std.testing.expect(rp.value_refs.count() > 0); + try std.testing.expect(rp.callable_refs.count() > 0); + + // (2) Keyed by NODE IDENTITY: the `Point` type reference is keyed by the exact + // `use_point` param type-expr node, and resolves RAW to the own struct. + const use_point = findFn(prog.root, "use_point") orelse return error.MissingFn; + const point_te = use_point.data.fn_decl.params[0].type_expr; + const point_ref = rp.type_refs.get(point_te) orelse return error.PointNotKeyed; + try std.testing.expect(point_ref == .authors); + try std.testing.expect(point_ref.authors.own != null); + try std.testing.expectEqual( + std.meta.Tag(resolver.RawDeclRef).struct_decl, + std.meta.activeTag(point_ref.authors.own.?.raw), + ); + + // (3) A value/const reference (`LIMIT`) collected RAW to its own author, and a + // callable head (`helper`) collected RAW to its FLAT-imported author. + var saw_limit = false; + var vit = rp.value_refs.iterator(); + while (vit.next()) |e| { + const k = e.key_ptr.*; + if (k.data == .identifier and std.mem.eql(u8, k.data.identifier.name, "LIMIT")) { + try std.testing.expect(e.value_ptr.* == .authors); + try std.testing.expect(e.value_ptr.authors.own != null); + saw_limit = true; + } + } + try std.testing.expect(saw_limit); + + var saw_helper = false; + var cit = rp.callable_refs.iterator(); + while (cit.next()) |e| { + const k = e.key_ptr.*; + try std.testing.expect(k.data == .identifier); // callable heads key bare-name callees + if (std.mem.eql(u8, k.data.identifier.name, "helper")) { + try std.testing.expect(e.value_ptr.* == .authors); + try std.testing.expect(e.value_ptr.authors.flat.len == 1); // authored only in lib.sx + saw_helper = true; + } + } + try std.testing.expect(saw_helper); + + // (4) Generic-param references are SYMBOLIC, not authors: a `$T` template ref + // and a `$args[0]` pack ref, each backed by a registry entry. + try std.testing.expect(rp.template_params.items.len > 0); + try std.testing.expect(rp.pack_params.items.len > 0); + + var saw_template = false; + var tit = rp.type_refs.iterator(); + while (tit.next()) |e| { + if (e.value_ptr.* == .template) saw_template = true; + } + try std.testing.expect(saw_template); + + // The `$args[0]` return type is a pack ref keyed by its pack-index node. + const third = findFn(prog.root, "third") orelse return error.MissingFn; + const ret_pack = third.data.fn_decl.return_type orelse return error.NoReturnType; + const pack_ref = rp.type_refs.get(ret_pack) orelse return error.PackNotKeyed; + try std.testing.expect(pack_ref == .pack); + try std.testing.expectEqual(@as(?u32, 0), pack_ref.pack.index); + + // (5) The seven domains S2.1b/c own stay EMPTY — S2.1a is parallel/unconsumed. + try std.testing.expectEqual(@as(u32, 0), rp.namespace_refs.count()); + try std.testing.expectEqual(@as(u32, 0), rp.generic_struct_heads.count()); + try std.testing.expectEqual(@as(u32, 0), rp.type_fn_heads.count()); + try std.testing.expectEqual(@as(u32, 0), rp.protocol_heads.count()); + try std.testing.expectEqual(@as(u32, 0), rp.foreign_class_refs.count()); + try std.testing.expectEqual(@as(u32, 0), rp.struct_const_refs.count()); + try std.testing.expectEqual(@as(u32, 0), rp.ufcs_refs.count()); +} diff --git a/src/ir/resolver.zig b/src/ir/resolver.zig index 994f01f..7040ffe 100644 --- a/src/ir/resolver.zig +++ b/src/ir/resolver.zig @@ -170,3 +170,548 @@ fn containsAuthor(list: []const RawAuthor, b: RawAuthor) bool { } 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 }; + 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 }; + 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.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 }; + 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 }; + 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), + .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, + .error_type_expr, + .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); + } + + /// 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; + table.put(node, .{ .authors = set }) catch @panic("resolve: OOM"); + } + + fn recordTemplate(self: *ResolvePass, table: *NodeRefTable, node: *const ast.Node, m: GenericMatch) void { + table.put(node, .{ .template = self.internTemplate(m) }) catch @panic("resolve: OOM"); + } + + 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; + table.put(node, .{ .pack = .{ .id = self.internPack(m), .index = index } }) catch @panic("resolve: OOM"); + } + + 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; + } +};