diff --git a/src/ir/resolver.test.zig b/src/ir/resolver.test.zig index a00e2fa..e254603 100644 --- a/src/ir/resolver.test.zig +++ b/src/ir/resolver.test.zig @@ -372,8 +372,8 @@ fn expectTypeRefOwnTag( ) !void { const ref = rp.type_refs.get(node) orelse return error.MissingTypeRef; try std.testing.expect(ref == .authors); - try std.testing.expect(ref.authors.own != null); - try std.testing.expectEqual(expected, std.meta.activeTag(ref.authors.own.?.raw)); + try std.testing.expect(ref.authors.set.own != null); + try std.testing.expectEqual(expected, std.meta.activeTag(ref.authors.set.own.?.raw)); } // The pass populates the three bare-name domains over REAL Phase A facts: a type @@ -435,10 +435,10 @@ test "resolver: resolve — bare-name domains populated, keyed by node, symbolic const point_te = use_point.data.fn_decl.params[0].type_expr; const point_ref = rp.type_refs.get(point_te) orelse return error.PointNotKeyed; try std.testing.expect(point_ref == .authors); - try std.testing.expect(point_ref.authors.own != null); + try std.testing.expect(point_ref.authors.set.own != null); try std.testing.expectEqual( std.meta.Tag(resolver.RawDeclRef).struct_decl, - std.meta.activeTag(point_ref.authors.own.?.raw), + std.meta.activeTag(point_ref.authors.set.own.?.raw), ); // (3) A value/const reference (`LIMIT`) collected RAW to its own author, and a @@ -449,7 +449,7 @@ test "resolver: resolve — bare-name domains populated, keyed by node, symbolic const k = e.key_ptr.*; if (k.data == .identifier and std.mem.eql(u8, k.data.identifier.name, "LIMIT")) { try std.testing.expect(e.value_ptr.* == .authors); - try std.testing.expect(e.value_ptr.authors.own != null); + try std.testing.expect(e.value_ptr.authors.set.own != null); saw_limit = true; } } @@ -462,7 +462,7 @@ test "resolver: resolve — bare-name domains populated, keyed by node, symbolic try std.testing.expect(k.data == .identifier); // callable heads key bare-name callees if (std.mem.eql(u8, k.data.identifier.name, "helper")) { try std.testing.expect(e.value_ptr.* == .authors); - try std.testing.expect(e.value_ptr.authors.flat.len == 1); // authored only in lib.sx + try std.testing.expect(e.value_ptr.authors.set.flat.len == 1); // authored only in lib.sx saw_helper = true; } } @@ -632,8 +632,8 @@ test "resolver: resolve — namespace-qualified + generic-struct/type-fn/protoco try std.testing.expect(k.data == .field_access); // namespace refs key the access node if (std.mem.eql(u8, k.data.field_access.field, "helper_fn")) { 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)); + try std.testing.expect(e.value_ptr.authors.set.own != null); + try std.testing.expectEqual(fn_tag, std.meta.activeTag(e.value_ptr.authors.set.own.?.raw)); saw_ns = true; } } @@ -646,8 +646,8 @@ test "resolver: resolve — namespace-qualified + generic-struct/type-fn/protoco try std.testing.expect(box_head.data == .parameterized_type_expr); const box_ref = rp.generic_struct_heads.get(box_head) orelse return error.BoxHeadNotKeyed; try std.testing.expect(box_ref == .authors); - try std.testing.expect(box_ref.authors.own != null); - try std.testing.expectEqual(struct_tag, std.meta.activeTag(box_ref.authors.own.?.raw)); + try std.testing.expect(box_ref.authors.set.own != null); + try std.testing.expectEqual(struct_tag, std.meta.activeTag(box_ref.authors.set.own.?.raw)); // and it is NOT mis-binned into the other head tables. try std.testing.expect(rp.type_fn_heads.get(box_head) == null); try std.testing.expect(rp.protocol_heads.get(box_head) == null); @@ -658,8 +658,8 @@ test "resolver: resolve — namespace-qualified + generic-struct/type-fn/protoco try std.testing.expect(make_head.data == .parameterized_type_expr); const make_ref = rp.type_fn_heads.get(make_head) orelse return error.MakeHeadNotKeyed; try std.testing.expect(make_ref == .authors); - try std.testing.expect(make_ref.authors.own != null); - try std.testing.expectEqual(fn_tag, std.meta.activeTag(make_ref.authors.own.?.raw)); + try std.testing.expect(make_ref.authors.set.own != null); + try std.testing.expectEqual(fn_tag, std.meta.activeTag(make_ref.authors.set.own.?.raw)); // (4) Protocol head: the `Cmp(s64)` value-type param keyed in protocol_heads. const use_cmp = findFn(prog.root, "use_cmp") orelse return error.MissingFn; @@ -667,8 +667,8 @@ test "resolver: resolve — namespace-qualified + generic-struct/type-fn/protoco try std.testing.expect(cmp_head.data == .parameterized_type_expr); const cmp_ref = rp.protocol_heads.get(cmp_head) orelse return error.CmpHeadNotKeyed; try std.testing.expect(cmp_ref == .authors); - try std.testing.expect(cmp_ref.authors.own != null); - try std.testing.expectEqual(protocol_tag, std.meta.activeTag(cmp_ref.authors.own.?.raw)); + try std.testing.expect(cmp_ref.authors.set.own != null); + try std.testing.expectEqual(protocol_tag, std.meta.activeTag(cmp_ref.authors.set.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. @@ -680,9 +680,9 @@ test "resolver: resolve — namespace-qualified + generic-struct/type-fn/protoco 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(ns_box_ref.authors.set.own != null); + try std.testing.expectEqual(struct_tag, std.meta.activeTag(ns_box_ref.authors.set.own.?.raw)); + try std.testing.expectEqualStrings(lib_path, ns_box_ref.authors.set.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); @@ -692,9 +692,9 @@ test "resolver: resolve — namespace-qualified + generic-struct/type-fn/protoco 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(ns_make_ref.authors.set.own != null); + try std.testing.expectEqual(fn_tag, std.meta.activeTag(ns_make_ref.authors.set.own.?.raw)); + try std.testing.expectEqualStrings(lib_path, ns_make_ref.authors.set.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); @@ -704,9 +704,9 @@ test "resolver: resolve — namespace-qualified + generic-struct/type-fn/protoco 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(ns_cmp_ref.authors.set.own != null); + try std.testing.expectEqual(protocol_tag, std.meta.activeTag(ns_cmp_ref.authors.set.own.?.raw)); + try std.testing.expectEqualStrings(lib_path, ns_cmp_ref.authors.set.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); @@ -810,8 +810,8 @@ test "resolver: resolve — S2.1c foreign-class/struct-const/UFCS + all ten doma 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(obj_ref.authors.set.own != null); + try std.testing.expectEqual(foreign_tag, std.meta.activeTag(obj_ref.authors.set.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 @@ -823,8 +823,8 @@ test "resolver: resolve — S2.1c foreign-class/struct-const/UFCS + all ten doma 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)); + try std.testing.expect(e.value_ptr.authors.set.own != null); + try std.testing.expectEqual(struct_tag, std.meta.activeTag(e.value_ptr.authors.set.own.?.raw)); saw_const = true; } } @@ -835,8 +835,8 @@ test "resolver: resolve — S2.1c foreign-class/struct-const/UFCS + all ten doma 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)); + try std.testing.expect(plus_ref.authors.set.own != null); + try std.testing.expectEqual(fn_tag, std.meta.activeTag(plus_ref.authors.set.own.?.raw)); // (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 @@ -845,8 +845,8 @@ test "resolver: resolve — S2.1c foreign-class/struct-const/UFCS + all ten doma 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(before_ref.authors.set.own != null); + try std.testing.expectEqual(fn_tag, std.meta.activeTag(before_ref.authors.set.own.?.raw)); try std.testing.expect(rp.callable_refs.get(before_alias_callee) == null); var saw_after_site = false; @@ -855,8 +855,8 @@ test "resolver: resolve — S2.1c foreign-class/struct-const/UFCS + all ten doma const k = e.key_ptr.*; 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)); + try std.testing.expect(e.value_ptr.authors.set.own != null); + try std.testing.expectEqual(fn_tag, std.meta.activeTag(e.value_ptr.authors.set.own.?.raw)); saw_after_site = true; } } @@ -868,3 +868,233 @@ test "resolver: resolve — S2.1c foreign-class/struct-const/UFCS + all ten doma try std.testing.expect(!std.mem.eql(u8, e.key_ptr.*.data.identifier.name, "plus")); } } + +// ── the verdict layer (S2.2) ─────────────────────────────────────────────── + +/// The verdict on a `.authors` resolved ref, or null for a template/pack ref. +fn refVerdict(ref: resolver.ResolvedRef) ?resolver.Verdict { + return switch (ref) { + .authors => |a| a.verdict, + .template, .pack => null, + }; +} + +/// The first flat author of `set` whose raw decl is `kind`, or null — lets a test +/// pick the domain-eligible author out of an UNFILTERED same-name set. +fn flatAuthorOfKind(set: resolver.AuthorSet, comptime kind: std.meta.Tag(resolver.RawDeclRef)) ?resolver.RawAuthor { + for (set.flat) |fa| { + if (std.meta.activeTag(fa.raw) == kind) return fa; + } + return null; +} + +/// The first parameter's type-expr node of the top-level fn `name`. +fn paramTypeNode(root: *const ast.Node, name: []const u8) ?*const ast.Node { + const fnd = findFn(root, name) orelse return null; + if (fnd.data.fn_decl.params.len == 0) return null; + return fnd.data.fn_decl.params[0].type_expr; +} + +/// The verdict of the first-param TYPE reference of fn `name` from `rp.type_refs`. +fn typeParamVerdict(rp: *const resolver.ResolvedProgram, root: *const ast.Node, name: []const u8) ?resolver.Verdict { + const node = paramTypeNode(root, name) orelse return null; + return refVerdict(rp.type_refs.get(node) orelse return null); +} + +// S2.2 attaches the selection verdict to every `.authors` ref, computed over the +// DOMAIN-ELIGIBLE subset of the collected author set. This proves all five outcomes +// on the BARE-TYPE domain over real Phase A facts: own_wins (own author), +// single (one flat type author), ambiguous (≥2 flat type authors), domain_filtered +// (the only visible same-name author is a VALUE — the type-vs-value filter excludes +// it), and not_visible (authored as a type only over a namespace edge). The same +// fixture proves the type-vs-value filter from the VALUE side: a name authored as a +// struct AND a value-const resolves to the STRUCT in type position and the CONST in +// value position. +test "resolver: verdicts — own-wins / single / ambiguous / domain-filtered / not-visible" { + 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 = "lib1.sx", .data = "Widget :: struct { x: s64 }\n" }); + try tmp.dir.writeFile(io, .{ .sub_path = "onlyval.sx", .data = "OnlyVal :: 7;\n" }); + try tmp.dir.writeFile(io, .{ .sub_path = "p.sx", .data = "Secret :: struct { s: s64 }\n" }); + try tmp.dir.writeFile(io, .{ .sub_path = "a.sx", .data = "Dup :: struct { d: s64 }\nThing :: struct { t: s64 }\n" }); + try tmp.dir.writeFile(io, .{ .sub_path = "b.sx", .data = "Dup :: struct { d: s64 }\nThing :: 99;\n" }); + try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data = + \\#import "lib1.sx"; + \\#import "onlyval.sx"; + \\#import "a.sx"; + \\#import "b.sx"; + \\g :: #import "p.sx"; + \\Gadget :: struct { y: s64 } + \\use_own :: (x: Gadget) -> s64 { 0 } + \\use_single :: (x: Widget) -> s64 { 0 } + \\use_ambig :: (x: Dup) -> s64 { 0 } + \\use_filtered :: (x: OnlyVal) -> s64 { 0 } + \\use_notvis :: (x: Secret) -> s64 { 0 } + \\use_thing :: (x: Thing) -> s64 { 0 } + \\read_thing :: () -> s64 { Thing } + \\main :: () -> s32 { 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}); + const a_path = try std.fmt.allocPrint(alloc, "{s}/a.sx", .{absdir}); + const b_path = try std.fmt.allocPrint(alloc, "{s}/b.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(); + + // The five bare-TYPE verdicts. + try std.testing.expectEqual(resolver.Verdict.own_wins, typeParamVerdict(&rp, prog.root, "use_own").?); + try std.testing.expectEqual(resolver.Verdict.single, typeParamVerdict(&rp, prog.root, "use_single").?); + try std.testing.expectEqual(resolver.Verdict.ambiguous, typeParamVerdict(&rp, prog.root, "use_ambig").?); + try std.testing.expectEqual(resolver.Verdict.domain_filtered, typeParamVerdict(&rp, prog.root, "use_filtered").?); + try std.testing.expectEqual(resolver.Verdict.not_visible, typeParamVerdict(&rp, prog.root, "use_notvis").?); + + // Type-vs-value filter: `Thing` (struct in a.sx, value-const in b.sx). The RAW + // set keeps BOTH same-name flat authors; the verdict is computed over the + // domain-eligible subset only — in TYPE position just the struct counts → single, + // and the eligible author is the struct, sourced to a.sx. + const thing_type = paramTypeNode(prog.root, "use_thing").?; + const thing_type_ref = rp.type_refs.get(thing_type) orelse return error.ThingTypeNotKeyed; + try std.testing.expectEqual(resolver.Verdict.single, refVerdict(thing_type_ref).?); + try std.testing.expectEqual(@as(usize, 2), thing_type_ref.authors.set.flat.len); // unfiltered + const thing_struct = flatAuthorOfKind(thing_type_ref.authors.set, .struct_decl) orelse return error.NoThingStruct; + try std.testing.expectEqualStrings(a_path, thing_struct.source); + + // In VALUE position only the const is eligible → single, and the eligible author + // is the const, sourced to b.sx — the same two-author set, the other domain. + var saw_thing_value = false; + var vit = rp.value_refs.iterator(); + while (vit.next()) |e| { + const k = e.key_ptr.*; + if (k.data == .identifier and std.mem.eql(u8, k.data.identifier.name, "Thing")) { + try std.testing.expectEqual(resolver.Verdict.single, refVerdict(e.value_ptr.*).?); + const thing_const = flatAuthorOfKind(e.value_ptr.authors.set, .const_decl) orelse return error.NoThingConst; + try std.testing.expectEqualStrings(b_path, thing_const.source); + saw_thing_value = true; + } + } + try std.testing.expect(saw_thing_value); +} + +// The acceptance proof: querying the resolver produces the TARGET verdicts for the +// resolver-target corpus (which the OLD per-kind selectors get WRONG on this base) — +// 0811-class error-set, 0821-class protocol head, and 0829-class generic-struct +// (concrete `*Box` prefix) all → AMBIGUOUS when two flat modules author them and the +// querying module authors none; all → OWN_WINS when the querying module authors its +// own. This is the uniform verdict the old error-set / protocol / param-impl +// selectors fail to compute (they silently pick a global last-wins author). The +// corpus stays xfail in run_examples (lowering does not consume these until S3.9); +// the verdicts are asserted HERE via the resolver test harness, not by flipping any +// golden. +test "resolver: verdicts — resolver-target corpus (0811/0821/0829 → ambiguous; own-wins)" { + 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(); + + const author_module = "IoErr :: error { Disk, Net }\nCmp :: protocol(T: Type) { get :: () -> T; }\nBox :: struct($T: Type) { value: T }\n"; + try tmp.dir.writeFile(io, .{ .sub_path = "a.sx", .data = author_module }); + try tmp.dir.writeFile(io, .{ .sub_path = "b.sx", .data = author_module }); + // Querying module authors NONE itself → every bare reference is ambiguous. + try tmp.dir.writeFile(io, .{ .sub_path = "ambig.sx", .data = + \\#import "a.sx"; + \\#import "b.sx"; + \\fail :: (e: IoErr) -> s32 { 0 } + \\use_cmp :: (c: Cmp(s64)) -> s64 { 0 } + \\use_box :: (b: Box(s64)) -> s64 { 0 } + \\main :: () -> s32 { 0 } + \\ + }); + // Querying module authors its OWN IoErr / Cmp / Box → own-wins, even against the + // two same-name flat imports. + try tmp.dir.writeFile(io, .{ .sub_path = "ownwins.sx", .data = + \\#import "a.sx"; + \\#import "b.sx"; + \\IoErr :: error { Disk, Net } + \\Cmp :: protocol(T: Type) { get :: () -> T; } + \\Box :: struct($T: Type) { value: T } + \\fail :: (e: IoErr) -> s32 { 0 } + \\use_cmp :: (c: Cmp(s64)) -> s64 { 0 } + \\use_box :: (b: Box(s64)) -> s64 { 0 } + \\main :: () -> s32 { 0 } + \\ + }); + + var dirbuf: [4096]u8 = undefined; + const absdir = dirbuf[0..try tmp.dir.realPath(io, &dirbuf)]; + + // (1) AMBIGUOUS — the 08xx SILENT-RESOLVE target. + { + const ambig_path = try std.fmt.allocPrint(alloc, "{s}/ambig.sx", .{absdir}); + var prog = try buildResolved(alloc, io, absdir, ambig_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, ambig_path, alloc); + defer rp.deinit(); + + // 0811-class: bare error-set type reference → ambiguous. + try std.testing.expectEqual(resolver.Verdict.ambiguous, typeParamVerdict(&rp, prog.root, "fail").?); + + // 0821-class: parameterized protocol head → ambiguous (protocol_heads). + const cmp_head = paramTypeNode(prog.root, "use_cmp").?; + try std.testing.expect(cmp_head.data == .parameterized_type_expr); + const cmp_ref = rp.protocol_heads.get(cmp_head) orelse return error.CmpHeadNotKeyed; + try std.testing.expectEqual(resolver.Verdict.ambiguous, refVerdict(cmp_ref).?); + + // 0829-class: concrete generic-struct head (`Box(..)`) → ambiguous + // (generic_struct_heads). + const box_head = paramTypeNode(prog.root, "use_box").?; + try std.testing.expect(box_head.data == .parameterized_type_expr); + const box_ref = rp.generic_struct_heads.get(box_head) orelse return error.BoxHeadNotKeyed; + try std.testing.expectEqual(resolver.Verdict.ambiguous, refVerdict(box_ref).?); + } + + // (2) OWN-WINS — the 08xx OWN-WINS target. + { + const own_path = try std.fmt.allocPrint(alloc, "{s}/ownwins.sx", .{absdir}); + var prog = try buildResolved(alloc, io, absdir, own_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, own_path, alloc); + defer rp.deinit(); + + try std.testing.expectEqual(resolver.Verdict.own_wins, typeParamVerdict(&rp, prog.root, "fail").?); + + const cmp_head = paramTypeNode(prog.root, "use_cmp").?; + const cmp_ref = rp.protocol_heads.get(cmp_head) orelse return error.CmpHeadNotKeyed; + try std.testing.expectEqual(resolver.Verdict.own_wins, refVerdict(cmp_ref).?); + + const box_head = paramTypeNode(prog.root, "use_box").?; + const box_ref = rp.generic_struct_heads.get(box_head) orelse return error.BoxHeadNotKeyed; + try std.testing.expectEqual(resolver.Verdict.own_wins, refVerdict(box_ref).?); + } +} diff --git a/src/ir/resolver.zig b/src/ir/resolver.zig index c55c8bb..d94c5c8 100644 --- a/src/ir/resolver.zig +++ b/src/ir/resolver.zig @@ -192,7 +192,19 @@ fn containsAuthor(list: []const RawAuthor, b: RawAuthor) bool { // 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. +// ten domains are now populated — still PARALLEL / UNCONSUMED. +// +// S2.2 adds the VERDICT layer: every `.authors` ref now carries the selection +// outcome the resolver COMPUTES above the collector — own-wins / single-flat-visible +// / ≥2-ambiguous / not-visible / type-vs-value domain-filtered — evaluated over the +// DOMAIN-ELIGIBLE subset of the author set (`eligibleKind`). This folds the per-kind +// selection semantics the old lower-side selectors carried (`selectNominalLeaf` / +// `flatTypeAuthorCount` / `selectModuleConst` / `selectPlainCallableAuthor` / +// `selectGenericStructHead` / `headTypeGate` / `headFnLeak`) into ONE uniform +// computation, closing the protocol / error-set / foreign per-kind surfaces as +// resolver behavior. The template/pack grammar is the `.template` / `.pack` refs +// above — NO `sig_registration_mode`. STILL ADDITIVE / PARALLEL / UNCONSUMED: +// lowering reads the old selectors, so the verdict changes no generated byte. /// A symbolic id for one enclosing generic TYPE/VALUE param (`$T`, `$N`), assigned /// by the pass and indexing `ResolvedProgram.template_params`. Process-local. @@ -229,12 +241,45 @@ pub const PackRef = struct { index: ?u32 = null, }; -/// What ONE reference site resolves to — the S2.1 RAW form. `authors` carries the -/// collected author identity (own ∪ flat, diamond-deduped) with NO verdict: -/// own-wins / direct-flat ambiguity selection is S2.2. `template` / `pack` are -/// symbolic generic-param references. +/// The selection verdict computed (S2.2) above a reference's collected author set +/// — the own-wins / single-flat-visible / ≥2-ambiguous layer the S0 ledger places +/// "above the collectors, producing ResolvedRef". Evaluated over the DOMAIN-ELIGIBLE +/// subset of the author set (so a same-name VALUE never decides a TYPE reference — +/// the type-vs-value `domain_filtered` outcome). ADDITIVE / PARALLEL / UNCONSUMED: +/// lowering still reads the old selectors, so producing these changes no byte. +pub const Verdict = enum { + /// The querying module's OWN author is eligible — it wins outright, regardless + /// of how many same-name flat authors exist. + own_wins, + /// Exactly ONE eligible flat-visible author, no own — the byte-identical + /// single-author path. + single, + /// ≥2 distinct eligible flat-visible authors, no own — a genuine collision the + /// source cannot disambiguate (the LOUD diagnostic at lowering / S3). + ambiguous, + /// No eligible author is flat-visible, but the name IS authored for this domain + /// in some module — reachable only over a namespace edge ⇒ a not-visible leak. + not_visible, + /// Visible same-name author(s) exist but NONE is eligible for this domain (a + /// same-name VALUE for a TYPE reference, etc.) and the name is authored for this + /// domain nowhere — the type-vs-value filter excluded every visible candidate. + domain_filtered, +}; + +/// A collected author set paired with the verdict the resolver computed over it. +/// `set` is the RAW collection (own ∪ flat, diamond-deduped — the S2.1 form, owned +/// here); `verdict` is the S2.2 selection outcome over its domain-eligible subset. +pub const ResolvedAuthors = struct { + set: AuthorSet, + verdict: Verdict, +}; + +/// What ONE reference site resolves to. `authors` carries the collected author +/// identity plus its computed verdict; `template` / `pack` are symbolic generic-param +/// references (no author, no verdict — the template grammar the resolver carries as +/// refs instead of a mutable `sig_registration_mode`). pub const ResolvedRef = union(enum) { - authors: AuthorSet, + authors: ResolvedAuthors, template: TemplateParamId, pack: PackRef, }; @@ -294,7 +339,7 @@ pub const ResolvedProgram = struct { for (self.allTables()) |t| { var it = t.valueIterator(); while (it.next()) |ref| switch (ref.*) { - .authors => |a| if (a.flat.len > 0) self.alloc.free(a.flat), + .authors => |a| if (a.set.flat.len > 0) self.alloc.free(a.set.flat), .template, .pack => {}, }; t.deinit(); @@ -455,6 +500,85 @@ fn authorSetHasStructConst(set: AuthorSet, field: []const u8) bool { return false; } +/// The `*StructDecl` a raw author wraps (bare or `Name :: struct(...)` const), or +/// null when the author is not a struct. Mirrors lowering's `structDeclOfRaw`. +fn structDeclOf(raw: RawDeclRef) ?*const ast.StructDecl { + return switch (raw) { + .struct_decl => |sd| sd, + .const_decl => |cd| if (cd.value.data == .struct_decl) &cd.value.data.struct_decl else null, + else => null, + }; +} + +/// The `*FnDecl` a raw author wraps (bare or `Name :: fn(...)` const), or null when +/// the author is not a function. Mirrors lowering's `fnDeclOfRaw`. +fn fnDeclOf(raw: RawDeclRef) ?*const ast.FnDecl { + return switch (raw) { + .fn_decl => |fd| fd, + .const_decl => |cd| if (cd.value.data == .fn_decl) &cd.value.data.fn_decl else null, + else => null, + }; +} + +/// A PLAIN free function — no type params, an ordinary (non-`#foreign`/`#builtin`/ +/// `#compiler`) body — the only callable kind the bare-call verdict counts. Mirrors +/// lowering's `isPlainFreeFn`. +fn isPlainFreeFnDecl(fd: *const ast.FnDecl) bool { + if (fd.type_params.len > 0) return false; + return switch (fd.body.data) { + .foreign_expr, .builtin_expr, .compiler_expr => false, + else => true, + }; +} + +/// The reference domains a verdict is computed over. Each carries its own +/// eligibility filter (`eligibleKind`), so the own-wins / ambiguity count surveys +/// only the authors that can actually decide THIS kind of reference — a same-name +/// value never decides a type, a non-generic struct never authors a generic head. +const Domain = enum { + bare_type, + value_const, + callable, + generic_struct_head, + type_fn_head, + protocol_head, + foreign_class, + struct_const, + namespace_member, + ufcs, +}; + +/// Whether `raw` is an author ELIGIBLE to decide a reference in `domain` — the +/// type-vs-value domain filter applied BEFORE the own-wins / ambiguity count. +/// `field` is the accessed member name (struct-const domain only; ignored +/// elsewhere). Mirrors the per-kind author predicates the old lowering selectors +/// gate on (`isNamedTypeKind`, `isPlainFreeFn`, `typeFnAuthor`, the `classifyHeadKind` +/// struct/fn `type_params.len > 0` test, `structHasConstMember`). +fn eligibleKind(domain: Domain, raw: RawDeclRef, field: ?[]const u8) bool { + return switch (domain) { + // Foreign classes are routed to their own domain before the type verdict, so + // a bare TYPE author is a non-foreign named type. A type ALIAS (`Name :: `, + // a `const_decl`) is recognised by lowering via the E0 source-keyed alias cache, + // which the resolver does not yet carry — alias authorship folds in when the + // alias facts move into the resolver (a later S2/S4 refinement), not here. + .bare_type => switch (raw) { + .struct_decl, .enum_decl, .union_decl, .error_set_decl, .protocol_decl => true, + else => false, + }, + .value_const => raw == .const_decl, + .callable => if (fnDeclOf(raw)) |fd| isPlainFreeFnDecl(fd) else false, + .generic_struct_head => if (structDeclOf(raw)) |sd| sd.type_params.len > 0 else false, + .type_fn_head => if (fnDeclOf(raw)) |fd| fd.type_params.len > 0 else false, + .protocol_head => raw == .protocol_decl, + .foreign_class => raw == .foreign_class_decl, + .struct_const => structHasConstMember(raw, field orelse return false), + // A namespace member is already selected against ONE namespace target, so any + // kind the member declares is the unambiguous author. + .namespace_member => true, + .ufcs => fnDeclOf(raw) != null, + }; +} + /// The single owning traversal. Holds the author collector + the `ResolvedProgram` /// it populates; threads `Ctx` (ambient source + generic scope) down the tree. const ResolvePass = struct { @@ -554,7 +678,7 @@ const ResolvePass = struct { 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); + self.recordAuthors(.callable, &self.out.callable_refs, c.callee, cname, here.source); } } else { self.visit(c.callee, here); @@ -581,7 +705,7 @@ const ResolvePass = struct { .pack_index_type_expr => |*p| self.recordPack(&self.out.type_refs, node, p.pack_name, p.index, here.scope), .comptime_pack_ref => |*p| self.recordPack(&self.out.value_refs, node, p.pack_name, null, here.scope), .error_type_expr => |*e| { - if (e.name) |name| self.recordAuthors(&self.out.type_refs, node, name, here.source); + if (e.name) |name| self.recordAuthors(.bare_type, &self.out.type_refs, node, name, here.source); }, .parameterized_type_expr => |*p| { // the head (`Name(args)`) is binned by its resolved author's decl @@ -810,7 +934,7 @@ const ResolvePass = struct { return; } } - self.recordAuthors(&self.out.type_refs, node, te.name, ctx.source); + self.recordAuthors(.bare_type, &self.out.type_refs, node, te.name, ctx.source); } /// A value-position identifier: a generic value/type param in scope (shadowing) @@ -823,28 +947,79 @@ const ResolvePass = struct { return; } } - self.recordAuthors(&self.out.value_refs, node, id.name, ctx.source); + self.recordAuthors(.value_const, &self.out.value_refs, node, id.name, ctx.source); } - /// 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. 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 { + /// Collect a bare name's authors AND compute its `domain` verdict. A name whose + /// author is a `foreign_class_decl` is routed to `foreign_class_refs` (its own + /// S2.1c domain, with the foreign-class verdict) instead of the passed + /// type/value/callable table. Records own-wins / single / ambiguous when an + /// eligible author is visible, and `not_visible` when the name is authored for + /// this domain only over a namespace edge; a builtin / local / undeclared + /// spelling (no visible author and none authored anywhere) is dropped, exactly + /// as S2.1 dropped the empty set. + fn recordAuthors(self: *ResolvePass, domain: Domain, 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 }); + const foreign = authorSetIsForeignClass(set); + const dom: Domain = if (foreign) .foreign_class else domain; + const dest = if (foreign) &self.out.foreign_class_refs else table; + const verdict = self.verdictOver(dom, name, set, null); + // Nothing visible AND not a domain author anywhere → a builtin / local / + // undeclared spelling, never a reference of this domain — drop it (the S2.1 + // empty-set behavior). An empty set owns no `flat` slice to free. + if (verdict == .domain_filtered and set.distinctCount() == 0) return; + self.replaceRef(dest, node, .{ .authors = .{ .set = set, .verdict = verdict } }); } - /// 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). + /// Collect a target name's authors into an explicit table with the `.ufcs` + /// verdict and 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). The target is always present, so an empty set is + /// simply not recorded (no not-visible leak path here). 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 }); + const verdict = self.verdictOver(.ufcs, name, set, null); + self.replaceRef(table, node, .{ .authors = .{ .set = set, .verdict = verdict } }); + } + + /// The verdict over a collected author set for `domain`: own-wins when the + /// querying module's own author is eligible; ≥2 distinct eligible flat authors → + /// ambiguous; exactly one → single; none eligible but authored for this domain + /// in some (non-flat-visible) module → not_visible; otherwise domain_filtered + /// (visible same-name authors of the wrong kind, or nothing authored anywhere). + /// `field` is the accessed member (struct-const domain only). + fn verdictOver(self: *ResolvePass, domain: Domain, name: []const u8, set: AuthorSet, field: ?[]const u8) Verdict { + if (set.own) |o| { + if (eligibleKind(domain, o.raw, field)) return .own_wins; + } + var n: usize = 0; + for (set.flat) |fa| { + if (eligibleKind(domain, fa.raw, field)) { + n += 1; + if (n >= 2) return .ambiguous; + } + } + if (n == 1) return .single; + if (self.authoredAsDomainAnywhere(domain, name, field)) return .not_visible; + return .domain_filtered; + } + + /// TRUE iff `name` is authored for `domain` in ANY module's raw facts — the + /// not-visible leak detector. Reached only with zero eligible flat-visible + /// authors, so a hit means the author is reachable only over a namespace edge + /// (had it been a flat edge it would already be in the surveyed set). Mirrors + /// lowering's `nameAuthoredAsTypeAnywhere`, generalized over every domain via + /// `eligibleKind`. + fn authoredAsDomainAnywhere(self: *ResolvePass, domain: Domain, name: []const u8, field: ?[]const u8) bool { + const decls = self.res.index.module_decls orelse return false; + var it = decls.valueIterator(); + while (it.next()) |m| { + if (m.names.get(name)) |ref| { + if (eligibleKind(domain, ref, field)) return true; + } + } + return false; } /// `alias.member`: when `alias` is a namespace import edge of `from`, resolve @@ -858,7 +1033,8 @@ const ResolvePass = struct { const target = aliases.get(alias) orelse return false; const set = self.res.collectNamespaceAuthors(target, member); if (set.distinctCount() == 0) return false; - self.replaceRef(&self.out.namespace_refs, node, .{ .authors = set }); + const verdict = self.verdictOver(.namespace_member, member, set, null); + self.replaceRef(&self.out.namespace_refs, node, .{ .authors = .{ .set = set, .verdict = verdict } }); return true; } @@ -875,22 +1051,24 @@ const ResolvePass = struct { if (set.flat.len > 0) self.out.alloc.free(set.flat); return false; } - self.replaceRef(&self.out.struct_const_refs, node, .{ .authors = set }); + const verdict = self.verdictOver(.struct_const, base, set, member); + self.replaceRef(&self.out.struct_const_refs, node, .{ .authors = .{ .set = set, .verdict = verdict } }); return true; } /// A parameterized head (`Name(args)`) binned by its resolved author's decl /// kind: a generic struct (struct with type params) → `generic_struct_heads`; /// a type-function (fn / const-wrapped fn with type params) → `type_fn_heads`; a - /// protocol → `protocol_heads`. RAW — the whole author set is recorded with no - /// winner picked, so a name authored as more than one head kind across modules - /// lands a distinct entry in every matching table. A head naming a generic param - /// in scope is symbolic (not an author); a name with no user author (builtins - /// like `Vector`, undeclared) or only non-head authors is omitted. + /// protocol → `protocol_heads`. The whole author set is recorded in every + /// matching table WITH that head kind's verdict (own-wins / single / ambiguous), + /// so a name authored as more than one head kind across modules lands a distinct + /// verdict-bearing entry per table. A head naming a generic param 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); + self.classifyHeadSet(node, name, set); return; } @@ -898,7 +1076,7 @@ const ResolvePass = struct { const set = self.res.collectVisibleAuthors(name, ctx.source, .user_bare_flat); if (set.distinctCount() == 0) return; - self.classifyHeadSet(node, set); + self.classifyHeadSet(node, name, set); } /// `alias.Member(args)` reaches this pass as one `parameterized_type_expr` @@ -919,25 +1097,28 @@ const ResolvePass = struct { return set; } - fn classifyHeadSet(self: *ResolvePass, node: *const ast.Node, set: AuthorSet) void { + /// One head table plus the verdict domain whose eligibility it counts. + const HeadBin = struct { table: *NodeRefTable, domain: Domain }; + + fn classifyHeadSet(self: *ResolvePass, node: *const ast.Node, name: []const u8, set: AuthorSet) void { var gs = false; var tf = false; var pr = false; if (set.own) |a| classifyHeadKind(a.raw, &gs, &tf, &pr); for (set.flat) |a| classifyHeadKind(a.raw, &gs, &tf, &pr); - var tables: [3]*NodeRefTable = undefined; + var bins: [3]HeadBin = undefined; var n: usize = 0; if (gs) { - tables[n] = &self.out.generic_struct_heads; + bins[n] = .{ .table = &self.out.generic_struct_heads, .domain = .generic_struct_head }; n += 1; } if (tf) { - tables[n] = &self.out.type_fn_heads; + bins[n] = .{ .table = &self.out.type_fn_heads, .domain = .type_fn_head }; n += 1; } if (pr) { - tables[n] = &self.out.protocol_heads; + bins[n] = .{ .table = &self.out.protocol_heads, .domain = .protocol_head }; n += 1; } if (n == 0) { @@ -946,13 +1127,18 @@ const ResolvePass = struct { if (set.flat.len > 0) self.out.alloc.free(set.flat); return; } - // each table OWNS its `AuthorSet.flat`; give the first match the collected - // slice and a fresh copy to every subsequent table so `deinit` frees each - // exactly once. - self.replaceRef(tables[0], node, .{ .authors = set }); + // each table OWNS its `AuthorSet.flat`; give the first bin the collected + // slice and a fresh copy to every subsequent bin so `deinit` frees each + // exactly once. The verdict is computed PER head kind — `Box` authored as a + // generic struct in one module and a type-fn in another lands an own-wins / + // single / ambiguous verdict independently in each table. + const v0 = self.verdictOver(bins[0].domain, name, set, null); + self.replaceRef(bins[0].table, node, .{ .authors = .{ .set = set, .verdict = v0 } }); var i: usize = 1; while (i < n) : (i += 1) { - self.replaceRef(tables[i], node, .{ .authors = self.dupAuthorSet(set) }); + const dup = self.dupAuthorSet(set); + const vi = self.verdictOver(bins[i].domain, name, dup, null); + self.replaceRef(bins[i].table, node, .{ .authors = .{ .set = dup, .verdict = vi } }); } } @@ -987,7 +1173,7 @@ const ResolvePass = struct { fn releaseRef(self: *ResolvePass, ref: ResolvedRef) void { switch (ref) { - .authors => |a| if (a.flat.len > 0) self.out.alloc.free(a.flat), + .authors => |a| if (a.set.flat.len > 0) self.out.alloc.free(a.set.flat), .template, .pack => {}, } }