feat(stdlib/S2.1c): foreign-class + struct-const + UFCS — final 3 domains on the owning pass [additive]

On the S2.1a owning traversal, populate the last three ResolvedProgram side
tables, closing planspec S2.1's full-population acceptance (all ten domains):

  - foreign_class_refs: a bare reference whose collected author is a
    foreign_class_decl is routed here (its own domain) instead of the bare
    type/value/callable table.
  - struct_const_refs: a Type.CONST field access whose base resolves to a
    struct author carrying that const member (mirrors lowering's struct_const_map).
  - ufcs_refs: a ufcs_alias decl (alias -> target author) plus its UFCS-rewrite
    call sites (alias(args), incl. pipe-desugared), via a global traversal-ordered
    alias map mirroring lowering's flat ufcs_alias_map.

Still PARALLEL / UNCONSUMED / RAW: lowering reads the OLD selectors, no consumer
cut over, ResolvedRef stays raw. Byte-identical vs baseline (540 examples).

Population proof extended: a new resolver.test fixture exercises all ten domains
at once and asserts each side table non-empty + node-keyed for the three new ones.
This commit is contained in:
agra
2026-06-09 14:32:27 +03:00
parent 2301b60bb4
commit 59681e0a09
2 changed files with 263 additions and 20 deletions

View File

@@ -703,3 +703,145 @@ test "resolver: resolve — namespace-qualified + generic-struct/type-fn/protoco
try std.testing.expectEqual(@as(u32, 0), rp.struct_const_refs.count());
try std.testing.expectEqual(@as(u32, 0), rp.ufcs_refs.count());
}
// ── the owning resolution pass — S2.1c final three domains + FULL population ──
// S2.1c closes the set on the SAME traversal, still RAW / PARALLEL / UNCONSUMED.
// (1) A bare reference whose author is a `foreign_class_decl` (`o: Obj`) is routed
// into foreign_class_refs — NOT type_refs. (2) A `Type.CONST` field access whose
// base resolves to a struct carrying that const member (`Phys.GRAVITY`) fills
// struct_const_refs, keyed by the field_access node. (3) A UFCS alias
// (`plus :: ufcs adder`) is keyed by its decl node AND its rewrite call site
// (`plus(1, 2)`) is keyed by the callee, both resolving to the target's author.
// This fixture exercises ALL TEN domains at once, proving the full-population
// acceptance: every ResolvedProgram side table is non-empty and node-keyed.
test "resolver: resolve — S2.1c foreign-class/struct-const/UFCS + all ten domains populated" {
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 = "lib.sx", .data = "helper :: () -> s64 { 5 }\n" });
try tmp.dir.writeFile(io, .{ .sub_path = "ns.sx", .data = "helper_fn :: () -> s64 { 7 }\n" });
try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data =
\\#import "lib.sx";
\\g :: #import "ns.sx";
\\Obj :: #foreign #objc_class("Obj") {
\\ alloc :: () -> *Obj;
\\}
\\Phys :: struct { mass: s64; GRAVITY :: 10; }
\\Box :: struct($T: Type) { value: T }
\\Make :: ($T: Type) -> Type { return [3]T; }
\\Cmp :: protocol(T: Type) { get :: () -> T; }
\\LIMIT :: 5;
\\adder :: (a: s64, b: s64) -> s64 { a + b }
\\plus :: ufcs adder;
\\use_phys :: (p: Phys) -> s64 { p.mass }
\\use_obj :: (o: Obj) -> s64 { 0 }
\\use_box :: (b: Box(s64)) -> s64 { 0 }
\\use_make :: (m: Make(s64)) -> s64 { 0 }
\\use_cmp :: (c: Cmp(s64)) -> s64 { 0 }
\\read_const :: () -> s64 { Phys.GRAVITY }
\\read_ns :: () -> s64 { g.helper_fn() }
\\call_alias :: () -> s64 { plus(1, 2) }
\\main :: () -> s32 {
\\ n := helper();
\\ m := LIMIT;
\\ _ = n;
\\ _ = m;
\\ return 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});
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();
// (0) FULL population: every one of the ten domains is non-empty.
try std.testing.expect(rp.type_refs.count() > 0);
try std.testing.expect(rp.value_refs.count() > 0);
try std.testing.expect(rp.callable_refs.count() > 0);
try std.testing.expect(rp.namespace_refs.count() > 0);
try std.testing.expect(rp.generic_struct_heads.count() > 0);
try std.testing.expect(rp.type_fn_heads.count() > 0);
try std.testing.expect(rp.protocol_heads.count() > 0);
try std.testing.expect(rp.foreign_class_refs.count() > 0);
try std.testing.expect(rp.struct_const_refs.count() > 0);
try std.testing.expect(rp.ufcs_refs.count() > 0);
const struct_tag = std.meta.Tag(resolver.RawDeclRef).struct_decl;
const fn_tag = std.meta.Tag(resolver.RawDeclRef).fn_decl;
const foreign_tag = std.meta.Tag(resolver.RawDeclRef).foreign_class_decl;
// (1) Foreign-class ref: `o: Obj` is keyed by the exact param type-expr node in
// foreign_class_refs, resolves RAW to the foreign_class_decl author — and is
// NOT mis-recorded into the bare type_refs table.
const use_obj = findFn(prog.root, "use_obj") orelse return error.MissingFn;
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(rp.type_refs.get(obj_te) == null);
// (2) Struct constant: `Phys.GRAVITY` is keyed by its field_access node in
// struct_const_refs and resolves RAW to the owning struct author.
var saw_const = false;
var sit = rp.struct_const_refs.iterator();
while (sit.next()) |e| {
const k = e.key_ptr.*;
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));
saw_const = true;
}
}
try std.testing.expect(saw_const);
// (3a) UFCS alias decl: the `plus :: ufcs adder` node is keyed in ufcs_refs and
// resolves RAW to the target fn (`adder`) author.
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));
// (3b) UFCS rewrite site: the `plus(1, 2)` callee identifier is keyed in
// ufcs_refs (NOT callable_refs) and resolves to the SAME target author.
var saw_site = false;
var uit = rp.ufcs_refs.iterator();
while (uit.next()) |e| {
const k = e.key_ptr.*;
if (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));
saw_site = true;
}
}
try std.testing.expect(saw_site);
// A UFCS-rewritten callee is NOT also recorded as an ordinary callable head.
var cit = rp.callable_refs.iterator();
while (cit.next()) |e| {
try std.testing.expect(!std.mem.eql(u8, e.key_ptr.*.data.identifier.name, "plus"));
}
}