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:
@@ -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).?);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 :: <type>`,
|
||||
// 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 => {},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user