//! 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; } };