diff --git a/src/ir/resolver.test.zig b/src/ir/resolver.test.zig index 5f2c2da..b78815a 100644 --- a/src/ir/resolver.test.zig +++ b/src/ir/resolver.test.zig @@ -703,3 +703,145 @@ test "resolver: resolve — namespace-qualified + generic-struct/type-fn/protoco try std.testing.expectEqual(@as(u32, 0), rp.struct_const_refs.count()); try std.testing.expectEqual(@as(u32, 0), rp.ufcs_refs.count()); } + +// ── the owning resolution pass — S2.1c final three domains + FULL population ── + +// S2.1c closes the set on the SAME traversal, still RAW / PARALLEL / UNCONSUMED. +// (1) A bare reference whose author is a `foreign_class_decl` (`o: Obj`) is routed +// into foreign_class_refs — NOT type_refs. (2) A `Type.CONST` field access whose +// base resolves to a struct carrying that const member (`Phys.GRAVITY`) fills +// struct_const_refs, keyed by the field_access node. (3) A UFCS alias +// (`plus :: ufcs adder`) is keyed by its decl node AND its rewrite call site +// (`plus(1, 2)`) is keyed by the callee, both resolving to the target's author. +// This fixture exercises ALL TEN domains at once, proving the full-population +// acceptance: every ResolvedProgram side table is non-empty and node-keyed. +test "resolver: resolve — S2.1c foreign-class/struct-const/UFCS + all ten domains populated" { + 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 = "ns.sx", .data = "helper_fn :: () -> s64 { 7 }\n" }); + try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data = + \\#import "lib.sx"; + \\g :: #import "ns.sx"; + \\Obj :: #foreign #objc_class("Obj") { + \\ alloc :: () -> *Obj; + \\} + \\Phys :: struct { mass: s64; GRAVITY :: 10; } + \\Box :: struct($T: Type) { value: T } + \\Make :: ($T: Type) -> Type { return [3]T; } + \\Cmp :: protocol(T: Type) { get :: () -> T; } + \\LIMIT :: 5; + \\adder :: (a: s64, b: s64) -> s64 { a + b } + \\plus :: ufcs adder; + \\use_phys :: (p: Phys) -> s64 { p.mass } + \\use_obj :: (o: Obj) -> s64 { 0 } + \\use_box :: (b: Box(s64)) -> s64 { 0 } + \\use_make :: (m: Make(s64)) -> s64 { 0 } + \\use_cmp :: (c: Cmp(s64)) -> s64 { 0 } + \\read_const :: () -> s64 { Phys.GRAVITY } + \\read_ns :: () -> s64 { g.helper_fn() } + \\call_alias :: () -> s64 { plus(1, 2) } + \\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.namespace_edges = &prog.ns_edges; + 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(); + + // (0) FULL population: every one of the ten domains is 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); + try std.testing.expect(rp.namespace_refs.count() > 0); + try std.testing.expect(rp.generic_struct_heads.count() > 0); + try std.testing.expect(rp.type_fn_heads.count() > 0); + try std.testing.expect(rp.protocol_heads.count() > 0); + try std.testing.expect(rp.foreign_class_refs.count() > 0); + try std.testing.expect(rp.struct_const_refs.count() > 0); + try std.testing.expect(rp.ufcs_refs.count() > 0); + + const struct_tag = std.meta.Tag(resolver.RawDeclRef).struct_decl; + const fn_tag = std.meta.Tag(resolver.RawDeclRef).fn_decl; + const foreign_tag = std.meta.Tag(resolver.RawDeclRef).foreign_class_decl; + + // (1) Foreign-class ref: `o: Obj` is keyed by the exact param type-expr node in + // foreign_class_refs, resolves RAW to the foreign_class_decl author — and is + // NOT mis-recorded into the bare type_refs table. + const use_obj = findFn(prog.root, "use_obj") orelse return error.MissingFn; + const obj_te = use_obj.data.fn_decl.params[0].type_expr; + const obj_ref = rp.foreign_class_refs.get(obj_te) orelse return error.ObjNotKeyed; + try std.testing.expect(obj_ref == .authors); + try std.testing.expect(obj_ref.authors.own != null); + try std.testing.expectEqual(foreign_tag, std.meta.activeTag(obj_ref.authors.own.?.raw)); + try std.testing.expect(rp.type_refs.get(obj_te) == null); + + // (2) Struct constant: `Phys.GRAVITY` is keyed by its field_access node in + // struct_const_refs and resolves RAW to the owning struct author. + var saw_const = false; + var sit = rp.struct_const_refs.iterator(); + while (sit.next()) |e| { + const k = e.key_ptr.*; + try std.testing.expect(k.data == .field_access); // struct-const keys the access node + if (std.mem.eql(u8, k.data.field_access.field, "GRAVITY")) { + try std.testing.expect(e.value_ptr.* == .authors); + try std.testing.expect(e.value_ptr.authors.own != null); + try std.testing.expectEqual(struct_tag, std.meta.activeTag(e.value_ptr.authors.own.?.raw)); + saw_const = true; + } + } + try std.testing.expect(saw_const); + + // (3a) UFCS alias decl: the `plus :: ufcs adder` node is keyed in ufcs_refs and + // resolves RAW to the target fn (`adder`) author. + const plus_decl = findDecl(prog.root, "plus", .ufcs_alias) orelse return error.MissingUfcsAlias; + const plus_ref = rp.ufcs_refs.get(plus_decl) orelse return error.UfcsAliasNotKeyed; + try std.testing.expect(plus_ref == .authors); + try std.testing.expect(plus_ref.authors.own != null); + try std.testing.expectEqual(fn_tag, std.meta.activeTag(plus_ref.authors.own.?.raw)); + + // (3b) UFCS rewrite site: the `plus(1, 2)` callee identifier is keyed in + // ufcs_refs (NOT callable_refs) and resolves to the SAME target author. + var saw_site = false; + var uit = rp.ufcs_refs.iterator(); + while (uit.next()) |e| { + const k = e.key_ptr.*; + if (k.data == .identifier and std.mem.eql(u8, k.data.identifier.name, "plus")) { + try std.testing.expect(e.value_ptr.* == .authors); + try std.testing.expect(e.value_ptr.authors.own != null); + try std.testing.expectEqual(fn_tag, std.meta.activeTag(e.value_ptr.authors.own.?.raw)); + saw_site = true; + } + } + try std.testing.expect(saw_site); + + // A UFCS-rewritten callee is NOT also recorded as an ordinary callable head. + var cit = rp.callable_refs.iterator(); + while (cit.next()) |e| { + try std.testing.expect(!std.mem.eql(u8, e.key_ptr.*.data.identifier.name, "plus")); + } +} diff --git a/src/ir/resolver.zig b/src/ir/resolver.zig index 7cd35fe..56713b7 100644 --- a/src/ir/resolver.zig +++ b/src/ir/resolver.zig @@ -187,8 +187,12 @@ fn containsAuthor(list: []const RawAuthor, b: RawAuthor) bool { // $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. The remaining -// three tables (foreign-class / struct-const / UFCS) stay empty until S2.1c. +// 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. @@ -327,7 +331,9 @@ pub fn resolve( 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.visit(root, .{ .source = main_file, .scope = null }); return pass.out; } @@ -405,11 +411,56 @@ fn classifyHeadKind(raw: RawDeclRef, gs: *bool, tf: *bool, pr: *bool) void { } } +/// 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 @@ -485,23 +536,35 @@ const ResolvePass = struct { .identifier => self.classifyValue(node, here), .call => |*c| { if (c.callee.data == .identifier) { - // bare-name callable HEAD — recorded here, not re-walked as a + 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. - self.recordAuthors(&self.out.callable_refs, c.callee, c.callee.data.identifier.name, here.source); + 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 alias is a namespace import edge of the + // `alias.member` whose base is a namespace import edge of the // ambient source resolves via `collectNamespaceAuthors` into the - // namespace-qualified table (S2.1b). A non-alias bare receiver - // (struct-const / UFCS / local value) stays unclassified — S2.1c — - // and is not walked as a value ref; a compound receiver is recursed - // so its inner refs are collected. + // 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) { - self.classifyNamespaceQualified(node, fa.object.data.identifier.name, fa.field, here.source); + 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); } @@ -675,9 +738,17 @@ const ResolvePass = struct { .foreign_expr, .library_decl, .framework_decl, - .ufcs_alias, .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| { + self.ufcs_aliases.put(ua.name, ua.target) catch @panic("resolve: OOM"); + self.recordAuthorsInto(&self.out.ufcs_refs, node, ua.target, here.source); + }, } } @@ -722,8 +793,20 @@ const ResolvePass = struct { /// 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. + /// 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 }); @@ -731,16 +814,34 @@ const ResolvePass = struct { /// `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. A base that - /// is not a namespace alias (struct-const / UFCS / local value — S2.1c) records - /// nothing here. - fn classifyNamespaceQualified(self: *ResolvePass, node: *const ast.Node, alias: []const u8, member: []const u8, from: []const u8) void { - const edges = self.res.index.namespace_edges orelse return; - const aliases = edges.get(from) orelse return; - const target = aliases.get(alias) orelse return; + /// (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; + 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