fix(stdlib/S2.1b): namespace-qualified parameterized heads [additive]
This commit is contained in:
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user