feat(stdlib/S2.1a): resolver.zig owning pass + ResolvedProgram scaffold + 3 bare-name domains [additive]

Turn src/ir/resolver.zig from a raw author-collection facade into the OWNING
resolution pass: one exhaustive recursive AST walk (exhaustive switch over
ast.Node.Data with NO else arm, so a new node kind is a compile error here
rather than a silently unvisited subtree) populating a ResolvedProgram.

- ResolvedProgram: all 10 node-keyed side tables declared as
  AutoHashMap(*const ast.Node, ResolvedRef) + symbolic TemplateParamId/
  PackParamId registries. ResolvedRef is the S2.1 RAW form — collected author
  identity (AuthorSet, own ∪ flat), NO verdict (own-wins/ambiguity is S2.2).
- Populate the 3 bare-name domains (type / value-const / callable heads) via
  collectVisibleAuthors(.user_bare_flat); record $T / ..$Ts / $pack[i] as
  SYMBOLIC template/pack refs, never TypeIds. The 7 head/qualified/foreign
  domains stay declared-but-empty (S2.1b/c own them).
- Slot via Compilation.resolveProgram() after the program_index facts are
  wired and before lowerRoot; ResolvedProgram owned on Compilation, borrowed
  *ResolvedProgram lent to ProgramIndex (lowerToIR signature unchanged).
- Population proof unit test over real Phase A facts: the 3 tables are
  non-empty, keyed by node identity, and carry symbolic template/pack refs.

ADDITIVE / PARALLEL / UNCONSUMED: lowering still reads the OLD selectors, so
single-author output is byte-identical. Gate green: zig build; zig build test
(425/425, LSP smoke 574 files no crash); run_examples (540 passed, 0 failed,
byte-identical incl. FFI 12xx-14xx + 1615 ios-sim); resolver-target (18 xfail
unchanged).
This commit is contained in:
agra
2026-06-09 12:29:27 +03:00
parent b29037b257
commit b46ad8b7a7
4 changed files with 753 additions and 0 deletions

View File

@@ -279,3 +279,186 @@ test "resolver: visibility edge-walk — own + flat visible; namespaced-only onl
try std.testing.expect(lower.nameVisibleOverEdges(null, &flat, "main", "secret"));
try std.testing.expect(lower.nameVisibleOverEdges(&scopes, null, "main", "secret"));
}
// ── the owning resolution pass — ResolvedProgram population proof (S2.1a) ──
/// Parse + resolve imports + build the raw facts AND the resolved root the pass
/// walks (built from `mod.decls`, exactly as `core.zig` does). `alloc` must be an
/// arena that outlives the views.
const Resolved = struct {
root: *ast.Node,
decls: imports.ModuleDecls,
flat_import_graph: Graph,
import_graph: Graph,
};
fn buildResolved(alloc: std.mem.Allocator, io: std.Io, absdir: []const u8, main_path: []const u8) !Resolved {
const main_bytes = try std.Io.Dir.readFileAlloc(.cwd(), io, main_path, alloc, .limited(1 << 20));
const main_source = try alloc.dupeZ(u8, main_bytes);
var p = parser.Parser.init(alloc, main_source);
const parsed = p.parse() catch return error.ParseFailed;
var diags = errors.DiagnosticList.init(alloc, main_source, main_path);
var chain = std.StringHashMap(void).init(alloc);
var cache = imports.ModuleCache.init(alloc);
var import_graph = Graph.init(alloc);
var flat_import_graph = Graph.init(alloc);
const stdlib_paths = [_][]const u8{};
const mod = try imports.resolveImports(
alloc,
io,
parsed,
absdir,
main_path,
&chain,
&cache,
null,
&diags,
&stdlib_paths,
&import_graph,
&flat_import_graph,
.{},
);
const facts = try imports.buildImportFacts(alloc, main_path, mod, &cache);
const root = try alloc.create(ast.Node);
root.* = .{ .span = parsed.span, .data = .{ .root = .{ .decls = mod.decls } } };
return .{
.root = root,
.decls = facts.decls,
.flat_import_graph = flat_import_graph,
.import_graph = import_graph,
};
}
/// Find the top-level `fn_decl` named `name` in the resolved root, or null.
fn findFn(root: *const ast.Node, name: []const u8) ?*const ast.Node {
for (root.data.root.decls) |d| {
if (d.data == .fn_decl and std.mem.eql(u8, d.data.fn_decl.name, name)) return d;
}
return null;
}
// The pass populates the three bare-name domains over REAL Phase A facts: a type
// reference (own struct author), a value/const reference (own const), and a
// callable head (flat-imported author). It keys every entry by NODE IDENTITY, and
// records generic-param references SYMBOLICALLY — a `$T` template ref and a
// `$args[0]` pack ref — never as a collected author. The seven unpopulated domains
// stay empty (S2.1b/c own them).
test "resolver: resolve — bare-name domains populated, keyed by node, symbolic generic refs" {
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 = "main.sx", .data =
\\#import "lib.sx";
\\Point :: struct { x: s64 }
\\LIMIT :: 10;
\\identity :: (x: $T) -> T { x }
\\third :: (..$args) -> $args[0] => args[0];
\\use_point :: (p: Point) -> s64 { p.x }
\\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.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();
// (1) The three bare-name domains are 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);
// (2) Keyed by NODE IDENTITY: the `Point` type reference is keyed by the exact
// `use_point` param type-expr node, and resolves RAW to the own struct.
const use_point = findFn(prog.root, "use_point") orelse return error.MissingFn;
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.expectEqual(
std.meta.Tag(resolver.RawDeclRef).struct_decl,
std.meta.activeTag(point_ref.authors.own.?.raw),
);
// (3) A value/const reference (`LIMIT`) collected RAW to its own author, and a
// callable head (`helper`) collected RAW to its FLAT-imported author.
var saw_limit = 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, "LIMIT")) {
try std.testing.expect(e.value_ptr.* == .authors);
try std.testing.expect(e.value_ptr.authors.own != null);
saw_limit = true;
}
}
try std.testing.expect(saw_limit);
var saw_helper = false;
var cit = rp.callable_refs.iterator();
while (cit.next()) |e| {
const k = e.key_ptr.*;
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
saw_helper = true;
}
}
try std.testing.expect(saw_helper);
// (4) Generic-param references are SYMBOLIC, not authors: a `$T` template ref
// and a `$args[0]` pack ref, each backed by a registry entry.
try std.testing.expect(rp.template_params.items.len > 0);
try std.testing.expect(rp.pack_params.items.len > 0);
var saw_template = false;
var tit = rp.type_refs.iterator();
while (tit.next()) |e| {
if (e.value_ptr.* == .template) saw_template = true;
}
try std.testing.expect(saw_template);
// The `$args[0]` return type is a pack ref keyed by its pack-index node.
const third = findFn(prog.root, "third") orelse return error.MissingFn;
const ret_pack = third.data.fn_decl.return_type orelse return error.NoReturnType;
const pack_ref = rp.type_refs.get(ret_pack) orelse return error.PackNotKeyed;
try std.testing.expect(pack_ref == .pack);
try std.testing.expectEqual(@as(?u32, 0), pack_ref.pack.index);
// (5) The seven domains S2.1b/c own stay EMPTY — S2.1a is parallel/unconsumed.
try std.testing.expectEqual(@as(u32, 0), rp.namespace_refs.count());
try std.testing.expectEqual(@as(u32, 0), rp.generic_struct_heads.count());
try std.testing.expectEqual(@as(u32, 0), rp.type_fn_heads.count());
try std.testing.expectEqual(@as(u32, 0), rp.protocol_heads.count());
try std.testing.expectEqual(@as(u32, 0), rp.foreign_class_refs.count());
try std.testing.expectEqual(@as(u32, 0), rp.struct_const_refs.count());
try std.testing.expectEqual(@as(u32, 0), rp.ufcs_refs.count());
}