diff --git a/src/ir/resolver.test.zig b/src/ir/resolver.test.zig index b78815a..a00e2fa 100644 --- a/src/ir/resolver.test.zig +++ b/src/ir/resolver.test.zig @@ -353,6 +353,18 @@ fn findDecl(root: *const ast.Node, name: []const u8, comptime kind: std.meta.Tag return null; } +fn firstBareCallCallee(func: *const ast.Node, name: []const u8) ?*const ast.Node { + if (func.data != .fn_decl) return null; + const body = func.data.fn_decl.body; + if (body.data != .block) return null; + for (body.data.block.stmts) |stmt| { + if (stmt.data != .call) continue; + const callee = stmt.data.call.callee; + if (callee.data == .identifier and std.mem.eql(u8, callee.data.identifier.name, name)) return callee; + } + return null; +} + fn expectTypeRefOwnTag( rp: *const resolver.ResolvedProgram, node: *const ast.Node, @@ -711,8 +723,9 @@ test "resolver: resolve — namespace-qualified + generic-struct/type-fn/protoco // 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. +// (`plus :: ufcs adder`) is keyed by its decl node AND its rewrite call sites +// (including one before the alias decl) are 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" { @@ -738,6 +751,7 @@ test "resolver: resolve — S2.1c foreign-class/struct-const/UFCS + all ten doma \\Cmp :: protocol(T: Type) { get :: () -> T; } \\LIMIT :: 5; \\adder :: (a: s64, b: s64) -> s64 { a + b } + \\call_alias_before :: () -> s64 { plus(3, 4) } \\plus :: ufcs adder; \\use_phys :: (p: Phys) -> s64 { p.mass } \\use_obj :: (o: Obj) -> s64 { 0 } @@ -746,7 +760,7 @@ test "resolver: resolve — S2.1c foreign-class/struct-const/UFCS + all ten doma \\use_cmp :: (c: Cmp(s64)) -> s64 { 0 } \\read_const :: () -> s64 { Phys.GRAVITY } \\read_ns :: () -> s64 { g.helper_fn() } - \\call_alias :: () -> s64 { plus(1, 2) } + \\call_alias_after :: () -> s64 { plus(1, 2) } \\main :: () -> s32 { \\ n := helper(); \\ m := LIMIT; @@ -824,20 +838,29 @@ test "resolver: resolve — S2.1c foreign-class/struct-const/UFCS + all ten doma 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; + // (3b) UFCS rewrite sites: both the forward `plus(3, 4)` site and the later + // `plus(1, 2)` site are keyed in ufcs_refs (NOT callable_refs) and + // resolve to the SAME target author. + const before_alias = findFn(prog.root, "call_alias_before") orelse return error.MissingBeforeAliasFn; + const before_alias_callee = firstBareCallCallee(before_alias, "plus") orelse return error.MissingBeforeAliasCallee; + const before_ref = rp.ufcs_refs.get(before_alias_callee) orelse return error.BeforeAliasUfcsNotKeyed; + try std.testing.expect(before_ref == .authors); + try std.testing.expect(before_ref.authors.own != null); + try std.testing.expectEqual(fn_tag, std.meta.activeTag(before_ref.authors.own.?.raw)); + try std.testing.expect(rp.callable_refs.get(before_alias_callee) == null); + + var saw_after_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")) { + if (k != before_alias_callee and 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; + saw_after_site = true; } } - try std.testing.expect(saw_site); + try std.testing.expect(saw_after_site); // A UFCS-rewritten callee is NOT also recorded as an ordinary callable head. var cit = rp.callable_refs.iterator(); diff --git a/src/ir/resolver.zig b/src/ir/resolver.zig index 56713b7..c55c8bb 100644 --- a/src/ir/resolver.zig +++ b/src/ir/resolver.zig @@ -334,6 +334,7 @@ pub fn resolve( .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; } @@ -352,6 +353,10 @@ const Frame = struct { 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 @@ -467,7 +472,11 @@ const ResolvePass = struct { /// 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 }; + 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| { @@ -585,7 +594,8 @@ const ResolvePass = struct { // ── structural recursion (no classification of their own) ── .root => |*r| { // each top-level decl carries its own ambient source stamp. - self.visitAll(r.decls, here); + 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| { @@ -746,12 +756,37 @@ const ResolvePass = struct { // 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"); + 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); }