feat(stdlib/S2.2): selection verdicts in the resolver [additive]

Move the author-SELECTION SEMANTICS (the verdicts) into the resolver. Every
`.authors` ResolvedRef now carries the verdict 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
collected author set (`eligibleKind`). This folds the per-kind selection 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 (E6c/d/e) as resolver behavior.

Template/pack grammar stays carried as `.template` / `.pack` refs — NO
`sig_registration_mode`.

ADDITIVE / PARALLEL / UNCONSUMED: lowering still reads the old selectors, so the
verdict changes no generated byte. No file outside resolver.zig reads
ResolvedRef, so byte-identity is structural. ResolvedRef.authors is wrapped into
{ set, verdict } (the RAW set is preserved; the verdict filters).

Resolver unit tests prove the verdicts on real Phase A facts: the five bare-type
outcomes incl. the type-vs-value filter, and the resolver-target classes the old
selectors get WRONG — 0811 error-set / 0821 protocol-head / 0829 generic-struct
all → ambiguous (two flat authors, none own) and → own-wins (own author present).
The resolver-target corpus stays xfail in run_examples (unconsumed until S3.9);
verdicts asserted via the harness, not by flipping goldens.

Gate: zig build && zig build test (430) && tests/run_examples.sh (540 byte-identical),
all exit 0; tests/resolver-target 18 xfail unchanged.
This commit is contained in:
agra
2026-06-09 15:29:17 +03:00
parent 42acf93401
commit a8d57521ac
2 changed files with 493 additions and 77 deletions

View File

@@ -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).?);
}
}