fix(stdlib/S2.1b): namespace-qualified parameterized heads [additive]

This commit is contained in:
agra
2026-06-09 14:01:23 +03:00
parent 6b41d113f2
commit 1f10036a1a
2 changed files with 77 additions and 0 deletions

View File

@@ -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());

View File

@@ -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;