diff --git a/src/ir/resolver.test.zig b/src/ir/resolver.test.zig index 884d9ec..5f2c2da 100644 --- a/src/ir/resolver.test.zig +++ b/src/ir/resolver.test.zig @@ -550,6 +550,8 @@ test "resolver: resolve — generic constraints and named error sets are type re // generic struct (`Box(s64)`) → generic_struct_heads, a type-function // (`Make(s64)`) → type_fn_heads, a parameterized protocol used as a value type // (`Cmp(s64)`) → protocol_heads — each keyed by its parameterized_type_expr node. +// The same head bins are also proved for parser-folded namespace-qualified heads +// (`g.NsBox(s64)` / `g.NsMake(s64)` / `g.NsCmp(s64)`). test "resolver: resolve — namespace-qualified + generic-struct/type-fn/protocol heads" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); @@ -561,6 +563,9 @@ test "resolver: resolve — namespace-qualified + generic-struct/type-fn/protoco try tmp.dir.writeFile(io, .{ .sub_path = "lib.sx", .data = \\helper_fn :: () -> s64 { 7 } + \\NsBox :: struct($T: Type) { value: T } + \\NsMake :: ($T: Type) -> Type { return [3]T; } + \\NsCmp :: protocol(T: Type) { get :: () -> T; } \\ }); try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data = @@ -571,6 +576,9 @@ test "resolver: resolve — namespace-qualified + generic-struct/type-fn/protoco \\use_box :: (b: Box(s64)) -> s64 { return 0; } \\use_make :: (m: Make(s64)) -> s64 { return 0; } \\use_cmp :: (c: Cmp(s64)) -> s64 { return 0; } + \\use_ns_box :: (b: g.NsBox(s64)) -> s64 { return 0; } + \\use_ns_make :: (m: g.NsMake(s64)) -> s64 { return 0; } + \\use_ns_cmp :: (c: g.NsCmp(s64)) -> s64 { return 0; } \\read_ns :: () -> s64 { return g.helper_fn(); } \\main :: () -> s32 { return 0; } \\ @@ -579,6 +587,7 @@ test "resolver: resolve — namespace-qualified + generic-struct/type-fn/protoco 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}); + const lib_path = try std.fmt.allocPrint(alloc, "{s}/lib.sx", .{absdir}); var prog = try buildResolved(alloc, io, absdir, main_path); @@ -649,6 +658,46 @@ test "resolver: resolve — namespace-qualified + generic-struct/type-fn/protoco try std.testing.expect(cmp_ref.authors.own != null); try std.testing.expectEqual(protocol_tag, std.meta.activeTag(cmp_ref.authors.own.?.raw)); + // (4b) Namespace-qualified parameterized heads are folded by the parser into + // a single parameterized_type_expr name (`g.NsBox`), not a field_access. + // They still resolve through namespace_edges and are keyed in the head + // tables with RAW authors sourced from lib.sx. + const use_ns_box = findFn(prog.root, "use_ns_box") orelse return error.MissingFn; + const ns_box_head = use_ns_box.data.fn_decl.params[0].type_expr; + try std.testing.expect(ns_box_head.data == .parameterized_type_expr); + try std.testing.expectEqualStrings("g.NsBox", ns_box_head.data.parameterized_type_expr.name); + const ns_box_ref = rp.generic_struct_heads.get(ns_box_head) orelse return error.NsBoxHeadNotKeyed; + try std.testing.expect(ns_box_ref == .authors); + try std.testing.expect(ns_box_ref.authors.own != null); + try std.testing.expectEqual(struct_tag, std.meta.activeTag(ns_box_ref.authors.own.?.raw)); + try std.testing.expectEqualStrings(lib_path, ns_box_ref.authors.own.?.source); + try std.testing.expect(rp.type_fn_heads.get(ns_box_head) == null); + try std.testing.expect(rp.protocol_heads.get(ns_box_head) == null); + + const use_ns_make = findFn(prog.root, "use_ns_make") orelse return error.MissingFn; + const ns_make_head = use_ns_make.data.fn_decl.params[0].type_expr; + try std.testing.expect(ns_make_head.data == .parameterized_type_expr); + try std.testing.expectEqualStrings("g.NsMake", ns_make_head.data.parameterized_type_expr.name); + const ns_make_ref = rp.type_fn_heads.get(ns_make_head) orelse return error.NsMakeHeadNotKeyed; + try std.testing.expect(ns_make_ref == .authors); + try std.testing.expect(ns_make_ref.authors.own != null); + try std.testing.expectEqual(fn_tag, std.meta.activeTag(ns_make_ref.authors.own.?.raw)); + try std.testing.expectEqualStrings(lib_path, ns_make_ref.authors.own.?.source); + try std.testing.expect(rp.generic_struct_heads.get(ns_make_head) == null); + try std.testing.expect(rp.protocol_heads.get(ns_make_head) == null); + + const use_ns_cmp = findFn(prog.root, "use_ns_cmp") orelse return error.MissingFn; + const ns_cmp_head = use_ns_cmp.data.fn_decl.params[0].type_expr; + try std.testing.expect(ns_cmp_head.data == .parameterized_type_expr); + try std.testing.expectEqualStrings("g.NsCmp", ns_cmp_head.data.parameterized_type_expr.name); + const ns_cmp_ref = rp.protocol_heads.get(ns_cmp_head) orelse return error.NsCmpHeadNotKeyed; + try std.testing.expect(ns_cmp_ref == .authors); + try std.testing.expect(ns_cmp_ref.authors.own != null); + try std.testing.expectEqual(protocol_tag, std.meta.activeTag(ns_cmp_ref.authors.own.?.raw)); + try std.testing.expectEqualStrings(lib_path, ns_cmp_ref.authors.own.?.source); + try std.testing.expect(rp.generic_struct_heads.get(ns_cmp_head) == null); + try std.testing.expect(rp.type_fn_heads.get(ns_cmp_head) == null); + // (5) The three S2.1c domains remain UNTOUCHED by S2.1b. try std.testing.expectEqual(@as(u32, 0), rp.foreign_class_refs.count()); try std.testing.expectEqual(@as(u32, 0), rp.struct_const_refs.count()); diff --git a/src/ir/resolver.zig b/src/ir/resolver.zig index b904b9d..7cd35fe 100644 --- a/src/ir/resolver.zig +++ b/src/ir/resolver.zig @@ -752,10 +752,38 @@ const ResolvePass = struct { /// 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;