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:
19
src/core.zig
19
src/core.zig
@@ -39,6 +39,11 @@ pub const Compilation = struct {
|
||||
/// `imports.buildDeclTable` in parallel with the import facts. Borrowed by
|
||||
/// `ProgramIndex.decl_table`.
|
||||
decl_table: imports.DeclTable,
|
||||
/// The owning resolution pass's output (Fork C S2.1a), built by
|
||||
/// `resolveProgram` before lowering and borrowed by
|
||||
/// `ProgramIndex.resolved_program`. ADDITIVE / PARALLEL / UNCONSUMED — nothing
|
||||
/// in lowering reads it yet, so generated output is byte-identical.
|
||||
resolved_program: ?ir.resolver.ResolvedProgram = null,
|
||||
ir_emitter: ?ir.LLVMEmitter = null,
|
||||
/// Lowered IR module, kept alive past `generateCode` so post-link
|
||||
/// callbacks can re-enter the interpreter to invoke sx functions
|
||||
@@ -80,6 +85,7 @@ pub const Compilation = struct {
|
||||
m.deinit();
|
||||
self.allocator.destroy(m);
|
||||
}
|
||||
if (self.resolved_program) |*rp| rp.deinit();
|
||||
self.diagnostics.deinit();
|
||||
}
|
||||
|
||||
@@ -295,6 +301,18 @@ pub const Compilation = struct {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Run the owning resolution pass (Fork C S2.1a) over the resolved root,
|
||||
/// storing its `ResolvedProgram` on `self` and lending a borrowed pointer to
|
||||
/// `index`. Slotted after the `program_index` import facts are wired and
|
||||
/// before `lowerRoot`. ADDITIVE / PARALLEL / UNCONSUMED — nothing in lowering
|
||||
/// reads the result yet, so this changes no generated byte; it is the clean
|
||||
/// pass seam future consumers (S3) cut over to.
|
||||
fn resolveProgram(self: *Compilation, index: *ir.ProgramIndex, root: *const Node) void {
|
||||
if (self.resolved_program) |*rp| rp.deinit();
|
||||
self.resolved_program = ir.resolver.resolve(root, index, self.file_path, self.allocator);
|
||||
index.resolved_program = &self.resolved_program.?;
|
||||
}
|
||||
|
||||
/// Lower the parsed AST to the sx IR module (shadow pipeline).
|
||||
pub fn lowerToIR(self: *Compilation) !ir.Module {
|
||||
const root = self.resolved_root orelse self.root orelse return ir.Module.init(self.allocator);
|
||||
@@ -314,6 +332,7 @@ pub const Compilation = struct {
|
||||
lowering.program_index.module_decls = &self.module_decls;
|
||||
lowering.program_index.namespace_edges = &self.namespace_edges;
|
||||
lowering.program_index.decl_table = &self.decl_table;
|
||||
self.resolveProgram(&lowering.program_index, root);
|
||||
lowering.lowerRoot(root);
|
||||
if (self.diagnostics.hasErrors()) return error.CompileError;
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ const types = @import("types.zig");
|
||||
const inst = @import("inst.zig");
|
||||
const errors = @import("../errors.zig");
|
||||
const type_resolver = @import("type_resolver.zig");
|
||||
const resolver = @import("resolver.zig");
|
||||
|
||||
const Node = ast.Node;
|
||||
const TypeId = types.TypeId;
|
||||
@@ -627,6 +628,11 @@ pub const ProgramIndex = struct {
|
||||
/// in parallel with the import facts. Borrowed view; nothing in lowering
|
||||
/// consumes it for selection yet (additive — S4 makes it the fact-store key).
|
||||
decl_table: ?*imports.DeclTable = null,
|
||||
/// The owning resolution pass's output (Fork C S2.1a), built by
|
||||
/// `resolver.resolve` and owned by `Compilation`. Borrowed view; ADDITIVE /
|
||||
/// PARALLEL / UNCONSUMED — nothing in lowering reads it yet (lowering still
|
||||
/// uses the old selectors), so generated output is byte-identical.
|
||||
resolved_program: ?*resolver.ResolvedProgram = null,
|
||||
|
||||
// ── Declaration maps ──
|
||||
/// Function name → AST decl.
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -170,3 +170,548 @@ fn containsAuthor(list: []const RawAuthor, b: RawAuthor) bool {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── The owning resolution pass (Fork C S2.1a) ───────────────────────────────
|
||||
//
|
||||
// `resolve` turns this module from a raw author-collection FACADE into the
|
||||
// OWNING pass: ONE exhaustive recursive walk of the resolved AST that populates a
|
||||
// `ResolvedProgram` — node-keyed side tables binding each user spelling to its RAW
|
||||
// author identity. ADDITIVE / PARALLEL / UNCONSUMED: lowering still reads the old
|
||||
// selectors, so this changes no generated byte. The walk switches over EVERY
|
||||
// `ast.Node.Data` kind with NO `else` arm, so a newly added node kind is a compile
|
||||
// error here rather than a silently unvisited subtree (it structurally cannot be
|
||||
// "half-populated").
|
||||
//
|
||||
// S2.1a populates the three BARE-NAME domains (type / value-const / callable) via
|
||||
// `collectVisibleAuthors`, and records generic-param references ($T, ..$Ts,
|
||||
// $pack[i]) SYMBOLICALLY (template/pack ids, never TypeIds). The remaining seven
|
||||
// tables are DECLARED but stay empty until S2.1b (namespace-qualified + the three
|
||||
// head domains) and S2.1c (foreign-class / struct-const / UFCS) populate them.
|
||||
|
||||
/// A symbolic id for one enclosing generic TYPE/VALUE param (`$T`, `$N`), assigned
|
||||
/// by the pass and indexing `ResolvedProgram.template_params`. Process-local.
|
||||
pub const TemplateParamId = enum(u32) { _ };
|
||||
|
||||
/// A symbolic id for one enclosing type pack (`..$Ts`, referenced as `$Ts` /
|
||||
/// `$Ts[i]`), assigned by the pass and indexing `ResolvedProgram.pack_params`.
|
||||
pub const PackParamId = enum(u32) { _ };
|
||||
|
||||
/// One generic param, identified symbolically — NOT a TypeId (the concrete binding
|
||||
/// is instantiation-time, owned by S2.2+).
|
||||
pub const TemplateParamInfo = struct {
|
||||
id: TemplateParamId,
|
||||
name: []const u8,
|
||||
/// The decl node that introduced the param (fn / struct / lambda / protocol /
|
||||
/// impl) — the param's identity is its address, this is its scope owner.
|
||||
owner: *const ast.Node,
|
||||
/// `$N: u32` (value) vs `$T: Type` (type), read off the param's constraint.
|
||||
is_value: bool,
|
||||
};
|
||||
|
||||
/// One type pack, identified symbolically.
|
||||
pub const PackParamInfo = struct {
|
||||
id: PackParamId,
|
||||
name: []const u8,
|
||||
owner: *const ast.Node,
|
||||
};
|
||||
|
||||
/// A reference to an enclosing pack — the whole pack (`$Ts`) or one element
|
||||
/// (`$Ts[i]`). Symbolic.
|
||||
pub const PackRef = struct {
|
||||
id: PackParamId,
|
||||
/// `$Ts[i]` literal index, or null for a whole-pack reference (`$Ts`).
|
||||
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.
|
||||
pub const ResolvedRef = union(enum) {
|
||||
authors: AuthorSet,
|
||||
template: TemplateParamId,
|
||||
pack: PackRef,
|
||||
};
|
||||
|
||||
/// Node-keyed side table: an AST reference node → its `ResolvedRef`. Keyed by node
|
||||
/// IDENTITY (the `*const ast.Node` pointer), so two textually-identical spellings
|
||||
/// at different sites are distinct entries.
|
||||
pub const NodeRefTable = std.AutoHashMap(*const ast.Node, ResolvedRef);
|
||||
|
||||
/// The output of the owning resolution pass: ten node-keyed side tables (one per
|
||||
/// reference domain) plus the symbolic template/pack registries. OWNS every
|
||||
/// allocation it holds — the maps, the registry lists, and each `AuthorSet.flat`
|
||||
/// slice the collector returned — and frees them in `deinit`. Owned by
|
||||
/// `Compilation`; borrowed by `ProgramIndex.resolved_program`.
|
||||
pub const ResolvedProgram = struct {
|
||||
alloc: std.mem.Allocator,
|
||||
|
||||
// ── bare-name domains (S2.1a populates these) ──
|
||||
type_refs: NodeRefTable,
|
||||
value_refs: NodeRefTable,
|
||||
callable_refs: NodeRefTable,
|
||||
// ── namespace-qualified + head domains (S2.1b) ──
|
||||
namespace_refs: NodeRefTable,
|
||||
generic_struct_heads: NodeRefTable,
|
||||
type_fn_heads: NodeRefTable,
|
||||
protocol_heads: NodeRefTable,
|
||||
// ── foreign-class / struct-const / UFCS (S2.1c) ──
|
||||
foreign_class_refs: NodeRefTable,
|
||||
struct_const_refs: NodeRefTable,
|
||||
ufcs_refs: NodeRefTable,
|
||||
|
||||
// ── symbolic generic-param registries ──
|
||||
template_params: std.ArrayList(TemplateParamInfo) = .empty,
|
||||
template_by_ptr: std.AutoHashMap(usize, TemplateParamId),
|
||||
pack_params: std.ArrayList(PackParamInfo) = .empty,
|
||||
pack_by_ptr: std.AutoHashMap(usize, PackParamId),
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) ResolvedProgram {
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.type_refs = NodeRefTable.init(alloc),
|
||||
.value_refs = NodeRefTable.init(alloc),
|
||||
.callable_refs = NodeRefTable.init(alloc),
|
||||
.namespace_refs = NodeRefTable.init(alloc),
|
||||
.generic_struct_heads = NodeRefTable.init(alloc),
|
||||
.type_fn_heads = NodeRefTable.init(alloc),
|
||||
.protocol_heads = NodeRefTable.init(alloc),
|
||||
.foreign_class_refs = NodeRefTable.init(alloc),
|
||||
.struct_const_refs = NodeRefTable.init(alloc),
|
||||
.ufcs_refs = NodeRefTable.init(alloc),
|
||||
.template_by_ptr = std.AutoHashMap(usize, TemplateParamId).init(alloc),
|
||||
.pack_by_ptr = std.AutoHashMap(usize, PackParamId).init(alloc),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *ResolvedProgram) void {
|
||||
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),
|
||||
.template, .pack => {},
|
||||
};
|
||||
t.deinit();
|
||||
}
|
||||
self.template_params.deinit(self.alloc);
|
||||
self.template_by_ptr.deinit();
|
||||
self.pack_params.deinit(self.alloc);
|
||||
self.pack_by_ptr.deinit();
|
||||
}
|
||||
|
||||
fn allTables(self: *ResolvedProgram) [10]*NodeRefTable {
|
||||
return .{
|
||||
&self.type_refs, &self.value_refs,
|
||||
&self.callable_refs, &self.namespace_refs,
|
||||
&self.generic_struct_heads, &self.type_fn_heads,
|
||||
&self.protocol_heads, &self.foreign_class_refs,
|
||||
&self.struct_const_refs, &self.ufcs_refs,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// Run the owning resolution pass over `root` (the resolved program root), using
|
||||
/// `index`'s borrowed import facts (`module_decls` / `flat_import_graph`) for
|
||||
/// author collection. `main_file` is the ambient-source fallback for nodes that
|
||||
/// carry no `source_file` stamp. Returns a fully-owned `ResolvedProgram` (the
|
||||
/// caller stores it and calls `deinit`). One pass, no AST mutation, no diagnostics
|
||||
/// — parallel/unconsumed, so generated output is unaffected.
|
||||
pub fn resolve(
|
||||
root: *const ast.Node,
|
||||
index: *ProgramIndex,
|
||||
main_file: []const u8,
|
||||
alloc: std.mem.Allocator,
|
||||
) ResolvedProgram {
|
||||
var pass = ResolvePass{
|
||||
.res = Resolver.init(index, alloc),
|
||||
.out = ResolvedProgram.init(alloc),
|
||||
};
|
||||
pass.visit(root, .{ .source = main_file, .scope = null });
|
||||
return pass.out;
|
||||
}
|
||||
|
||||
/// One frame of generic params introduced by an enclosing decl (fn / struct /
|
||||
/// lambda / protocol / impl). Lives on the Zig call stack (no allocation), chained
|
||||
/// to its parent — a reference resolves a name against the NEAREST enclosing frame.
|
||||
const Frame = struct {
|
||||
params: []const ast.StructTypeParam,
|
||||
owner: *const ast.Node,
|
||||
parent: ?*const Frame,
|
||||
};
|
||||
|
||||
/// Ambient walk context: the querying module's source path (`collectVisibleAuthors`'s
|
||||
/// `from`) and the enclosing generic-param scope.
|
||||
const Ctx = struct {
|
||||
source: []const u8,
|
||||
scope: ?*const Frame,
|
||||
};
|
||||
|
||||
/// A resolved generic-param reference: the matched param (its address is its
|
||||
/// identity) plus the scope owner that declared it.
|
||||
const GenericMatch = struct {
|
||||
param: *const ast.StructTypeParam,
|
||||
owner: *const ast.Node,
|
||||
};
|
||||
|
||||
/// `$N: u32` is a value param; `$T: Type` (or a variadic / non-type constraint) is
|
||||
/// a type param. Read off the param's constraint type-expr name.
|
||||
fn paramIsValue(p: ast.StructTypeParam) bool {
|
||||
if (p.is_variadic) return false;
|
||||
return switch (p.constraint.data) {
|
||||
.type_expr => |te| !std.mem.eql(u8, te.name, "Type"),
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
/// Nearest enclosing generic param named `name`, or null when the name is not a
|
||||
/// generic in scope (→ it is an ordinary bare-name reference).
|
||||
fn lookupGeneric(scope: ?*const Frame, name: []const u8) ?GenericMatch {
|
||||
var cur = scope;
|
||||
while (cur) |f| : (cur = f.parent) {
|
||||
for (f.params) |*p| {
|
||||
if (std.mem.eql(u8, p.name, name)) return .{ .param = p, .owner = f.owner };
|
||||
}
|
||||
}
|
||||
return 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 {
|
||||
res: Resolver,
|
||||
out: ResolvedProgram,
|
||||
|
||||
/// Visit ONE node, then recurse into its children. A stamped
|
||||
/// `node.source_file` (top-level decls, and cross-module fn bodies whose bare
|
||||
/// names must resolve in their DEFINING module) overrides the ambient source
|
||||
/// for this subtree; an unstamped node inherits its parent's.
|
||||
fn visit(self: *ResolvePass, node: *const ast.Node, ctx: Ctx) void {
|
||||
const here = Ctx{ .source = node.source_file orelse ctx.source, .scope = ctx.scope };
|
||||
switch (node.data) {
|
||||
// ── declarations that open a generic-param scope ──
|
||||
.fn_decl => |*fd| {
|
||||
var frame = Frame{ .params = fd.type_params, .owner = node, .parent = here.scope };
|
||||
const inner = Ctx{ .source = here.source, .scope = &frame };
|
||||
for (fd.params) |p| {
|
||||
self.visit(p.type_expr, inner);
|
||||
if (p.default_expr) |d| self.visit(d, inner);
|
||||
}
|
||||
if (fd.return_type) |rt| self.visit(rt, inner);
|
||||
self.visit(fd.body, inner);
|
||||
},
|
||||
.lambda => |*l| {
|
||||
var frame = Frame{ .params = l.type_params, .owner = node, .parent = here.scope };
|
||||
const inner = Ctx{ .source = here.source, .scope = &frame };
|
||||
for (l.params) |p| {
|
||||
self.visit(p.type_expr, inner);
|
||||
if (p.default_expr) |d| self.visit(d, inner);
|
||||
}
|
||||
if (l.return_type) |rt| self.visit(rt, inner);
|
||||
self.visit(l.body, inner);
|
||||
},
|
||||
.struct_decl => |*sd| {
|
||||
var frame = Frame{ .params = sd.type_params, .owner = node, .parent = here.scope };
|
||||
const inner = Ctx{ .source = here.source, .scope = &frame };
|
||||
self.visitAll(sd.field_types, inner);
|
||||
self.visitAllOpt(sd.field_defaults, inner);
|
||||
self.visitAll(sd.methods, inner);
|
||||
self.visitAll(sd.constants, inner);
|
||||
},
|
||||
.protocol_decl => |*pd| {
|
||||
var frame = Frame{ .params = pd.type_params, .owner = node, .parent = here.scope };
|
||||
const inner = Ctx{ .source = here.source, .scope = &frame };
|
||||
for (pd.methods) |m| {
|
||||
self.visitAll(m.params, inner);
|
||||
if (m.return_type) |rt| self.visit(rt, inner);
|
||||
if (m.default_body) |b| self.visit(b, inner);
|
||||
}
|
||||
},
|
||||
.impl_block => |*ib| {
|
||||
var frame = Frame{ .params = ib.target_type_params, .owner = node, .parent = here.scope };
|
||||
const inner = Ctx{ .source = here.source, .scope = &frame };
|
||||
if (ib.target_type_expr) |tt| self.visit(tt, inner);
|
||||
self.visitAll(ib.protocol_type_args, inner);
|
||||
self.visitAll(ib.methods, inner);
|
||||
},
|
||||
.foreign_class_decl => |*fc| {
|
||||
for (fc.members) |m| switch (m) {
|
||||
.method => |meth| {
|
||||
self.visitAll(meth.params, here);
|
||||
if (meth.return_type) |rt| self.visit(rt, here);
|
||||
if (meth.body) |b| self.visit(b, here);
|
||||
},
|
||||
.field => |fld| self.visit(fld.field_type, here),
|
||||
.extends, .implements => {},
|
||||
};
|
||||
},
|
||||
|
||||
// ── the three bare-name domains + symbolic generic refs ──
|
||||
.type_expr => self.classifyType(node, here),
|
||||
.identifier => self.classifyValue(node, here),
|
||||
.call => |*c| {
|
||||
if (c.callee.data == .identifier) {
|
||||
// bare-name callable HEAD — recorded here, not re-walked as a
|
||||
// value ref.
|
||||
self.recordAuthors(&self.out.callable_refs, c.callee, c.callee.data.identifier.name, here.source);
|
||||
} else {
|
||||
self.visit(c.callee, here);
|
||||
}
|
||||
self.visitAll(c.args, here);
|
||||
},
|
||||
.field_access => |*fa| {
|
||||
// namespace-qualified / struct-const / UFCS receivers are
|
||||
// S2.1b/c — a BARE identifier receiver is left unclassified here;
|
||||
// a compound receiver is recursed so its inner refs are collected.
|
||||
if (fa.object.data != .identifier) self.visit(fa.object, here);
|
||||
},
|
||||
.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),
|
||||
.parameterized_type_expr => |*p| {
|
||||
// the head (generic-struct / type-fn / protocol) is S2.1b; the
|
||||
// type args are ordinary references, collected now.
|
||||
self.visitAll(p.args, here);
|
||||
},
|
||||
|
||||
// ── structural recursion (no classification of their own) ──
|
||||
.root => |*r| {
|
||||
// each top-level decl carries its own ambient source stamp.
|
||||
self.visitAll(r.decls, here);
|
||||
},
|
||||
.block => |*b| self.visitAll(b.stmts, here),
|
||||
.binary_op => |*b| {
|
||||
self.visit(b.lhs, here);
|
||||
self.visit(b.rhs, here);
|
||||
},
|
||||
.chained_comparison => |*c| self.visitAll(c.operands, here),
|
||||
.unary_op => |*u| self.visit(u.operand, here),
|
||||
.if_expr => |*e| {
|
||||
self.visit(e.condition, here);
|
||||
self.visit(e.then_branch, here);
|
||||
if (e.else_branch) |b| self.visit(b, here);
|
||||
},
|
||||
.match_expr => |*e| {
|
||||
self.visit(e.subject, here);
|
||||
for (e.arms) |arm| {
|
||||
if (arm.pattern) |pat| self.visit(pat, here);
|
||||
self.visit(arm.body, here);
|
||||
}
|
||||
},
|
||||
.match_arm => |*arm| {
|
||||
if (arm.pattern) |pat| self.visit(pat, here);
|
||||
self.visit(arm.body, here);
|
||||
},
|
||||
.const_decl => |*cd| {
|
||||
if (cd.type_annotation) |ta| self.visit(ta, here);
|
||||
self.visit(cd.value, here);
|
||||
},
|
||||
.var_decl => |*vd| {
|
||||
if (vd.type_annotation) |ta| self.visit(ta, here);
|
||||
if (vd.value) |v| self.visit(v, here);
|
||||
},
|
||||
.assignment => |*a| {
|
||||
self.visit(a.target, here);
|
||||
self.visit(a.value, here);
|
||||
},
|
||||
.multi_assign => |*m| {
|
||||
self.visitAll(m.targets, here);
|
||||
self.visitAll(m.values, here);
|
||||
},
|
||||
.destructure_decl => |*d| self.visit(d.value, here),
|
||||
.enum_decl => |*ed| {
|
||||
self.visitAllOpt(ed.variant_types, here);
|
||||
self.visitAllOpt(ed.variant_values, here);
|
||||
if (ed.backing_type) |bt| self.visit(bt, here);
|
||||
},
|
||||
.union_decl => |*ud| self.visitAll(ud.field_types, here),
|
||||
.struct_literal => |*sl| {
|
||||
if (sl.type_expr) |te| self.visit(te, here);
|
||||
for (sl.field_inits) |fi| self.visit(fi.value, here);
|
||||
if (sl.init_block) |ib| self.visit(ib, here);
|
||||
},
|
||||
.param => |*p| {
|
||||
self.visit(p.type_expr, here);
|
||||
if (p.default_expr) |d| self.visit(d, here);
|
||||
},
|
||||
.defer_stmt => |*d| self.visit(d.expr, here),
|
||||
.push_stmt => |*p| {
|
||||
self.visit(p.context_expr, here);
|
||||
self.visit(p.body, here);
|
||||
},
|
||||
.comptime_expr => |*c| self.visit(c.expr, here),
|
||||
.insert_expr => |*i| self.visit(i.expr, here),
|
||||
.return_stmt => |*r| if (r.value) |v| self.visit(v, here),
|
||||
.array_type_expr => |*a| {
|
||||
self.visit(a.length, here);
|
||||
self.visit(a.element_type, here);
|
||||
},
|
||||
.slice_type_expr => |*s| self.visit(s.element_type, here),
|
||||
.array_literal => |*a| {
|
||||
if (a.type_expr) |te| self.visit(te, here);
|
||||
self.visitAll(a.elements, here);
|
||||
},
|
||||
.index_expr => |*i| {
|
||||
self.visit(i.object, here);
|
||||
self.visit(i.index, here);
|
||||
},
|
||||
.slice_expr => |*s| {
|
||||
self.visit(s.object, here);
|
||||
if (s.start) |st| self.visit(st, here);
|
||||
if (s.end) |en| self.visit(en, here);
|
||||
},
|
||||
.pointer_type_expr => |*p| self.visit(p.pointee_type, here),
|
||||
.many_pointer_type_expr => |*p| self.visit(p.element_type, here),
|
||||
.optional_type_expr => |*o| self.visit(o.inner_type, here),
|
||||
.raise_stmt => |*r| self.visit(r.tag, here),
|
||||
.try_expr => |*t| self.visit(t.operand, here),
|
||||
.catch_expr => |*c| {
|
||||
self.visit(c.operand, here);
|
||||
self.visit(c.body, here);
|
||||
},
|
||||
.onfail_stmt => |*o| self.visit(o.body, here),
|
||||
.force_unwrap => |*f| self.visit(f.operand, here),
|
||||
.null_coalesce => |*n| {
|
||||
self.visit(n.lhs, here);
|
||||
self.visit(n.rhs, here);
|
||||
},
|
||||
.deref_expr => |*d| self.visit(d.operand, here),
|
||||
.while_expr => |*w| {
|
||||
self.visit(w.condition, here);
|
||||
self.visit(w.body, here);
|
||||
},
|
||||
.for_expr => |*f| {
|
||||
self.visit(f.iterable, here);
|
||||
if (f.range_end) |re| self.visit(re, here);
|
||||
self.visit(f.body, here);
|
||||
},
|
||||
.spread_expr => |*s| self.visit(s.operand, here),
|
||||
.function_type_expr => |*ft| {
|
||||
self.visitAll(ft.param_types, here);
|
||||
if (ft.return_type) |rt| self.visit(rt, here);
|
||||
},
|
||||
.closure_type_expr => |*ct| {
|
||||
self.visitAll(ct.param_types, here);
|
||||
if (ct.return_type) |rt| self.visit(rt, here);
|
||||
},
|
||||
.tuple_type_expr => |*tt| self.visitAll(tt.field_types, here),
|
||||
.tuple_literal => |*tl| {
|
||||
for (tl.elements) |el| self.visit(el.value, here);
|
||||
},
|
||||
.ffi_intrinsic_call => |*f| {
|
||||
self.visit(f.return_type, here);
|
||||
self.visitAll(f.args, here);
|
||||
},
|
||||
.jni_env_block => |*j| {
|
||||
self.visit(j.env, here);
|
||||
self.visit(j.body, here);
|
||||
},
|
||||
|
||||
// ── leaves: no child node, no bare-name reference of their own ──
|
||||
// `namespace_decl` is a leaf HERE: its members belong to another
|
||||
// module and are reached via `collectNamespaceAuthors` in S2.1b, not
|
||||
// re-walked through the importing root.
|
||||
.int_literal,
|
||||
.float_literal,
|
||||
.bool_literal,
|
||||
.string_literal,
|
||||
.enum_literal,
|
||||
.caller_location,
|
||||
.null_literal,
|
||||
.break_expr,
|
||||
.continue_expr,
|
||||
.undef_literal,
|
||||
.inferred_type,
|
||||
.builtin_expr,
|
||||
.compiler_expr,
|
||||
.import_decl,
|
||||
.namespace_decl,
|
||||
.error_set_decl,
|
||||
.error_type_expr,
|
||||
.foreign_expr,
|
||||
.library_decl,
|
||||
.framework_decl,
|
||||
.ufcs_alias,
|
||||
.c_import_decl,
|
||||
=> {},
|
||||
}
|
||||
}
|
||||
|
||||
fn visitAll(self: *ResolvePass, nodes: anytype, ctx: Ctx) void {
|
||||
for (nodes) |n| self.visit(n, ctx);
|
||||
}
|
||||
|
||||
fn visitAllOpt(self: *ResolvePass, nodes: anytype, ctx: Ctx) void {
|
||||
for (nodes) |n| if (n) |nn| self.visit(nn, ctx);
|
||||
}
|
||||
|
||||
/// A type-position reference: a generic param in scope → symbolic template ref;
|
||||
/// otherwise a user type, collected RAW. Builtins / undeclared names collect to
|
||||
/// an empty set and are simply not recorded.
|
||||
fn classifyType(self: *ResolvePass, node: *const ast.Node, ctx: Ctx) void {
|
||||
const te = node.data.type_expr;
|
||||
if (!te.is_raw) {
|
||||
if (lookupGeneric(ctx.scope, te.name)) |m| {
|
||||
self.recordTemplate(&self.out.type_refs, node, m);
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.recordAuthors(&self.out.type_refs, node, te.name, ctx.source);
|
||||
}
|
||||
|
||||
/// A value-position identifier: a generic value/type param in scope (shadowing)
|
||||
/// → symbolic template ref; otherwise a module value/const, collected RAW.
|
||||
fn classifyValue(self: *ResolvePass, node: *const ast.Node, ctx: Ctx) void {
|
||||
const id = node.data.identifier;
|
||||
if (!id.is_raw) {
|
||||
if (lookupGeneric(ctx.scope, id.name)) |m| {
|
||||
self.recordTemplate(&self.out.value_refs, node, m);
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.recordAuthors(&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.
|
||||
fn recordAuthors(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;
|
||||
table.put(node, .{ .authors = set }) catch @panic("resolve: OOM");
|
||||
}
|
||||
|
||||
fn recordTemplate(self: *ResolvePass, table: *NodeRefTable, node: *const ast.Node, m: GenericMatch) void {
|
||||
table.put(node, .{ .template = self.internTemplate(m) }) catch @panic("resolve: OOM");
|
||||
}
|
||||
|
||||
fn recordPack(self: *ResolvePass, table: *NodeRefTable, node: *const ast.Node, name: []const u8, index: ?u32, scope: ?*const Frame) void {
|
||||
const m = lookupGeneric(scope, name) orelse return;
|
||||
table.put(node, .{ .pack = .{ .id = self.internPack(m), .index = index } }) catch @panic("resolve: OOM");
|
||||
}
|
||||
|
||||
fn internTemplate(self: *ResolvePass, m: GenericMatch) TemplateParamId {
|
||||
const key = @intFromPtr(m.param);
|
||||
if (self.out.template_by_ptr.get(key)) |id| return id;
|
||||
const id: TemplateParamId = @enumFromInt(@as(u32, @intCast(self.out.template_params.items.len)));
|
||||
self.out.template_params.append(self.out.alloc, .{
|
||||
.id = id,
|
||||
.name = m.param.name,
|
||||
.owner = m.owner,
|
||||
.is_value = paramIsValue(m.param.*),
|
||||
}) catch @panic("resolve: OOM");
|
||||
self.out.template_by_ptr.put(key, id) catch @panic("resolve: OOM");
|
||||
return id;
|
||||
}
|
||||
|
||||
fn internPack(self: *ResolvePass, m: GenericMatch) PackParamId {
|
||||
const key = @intFromPtr(m.param);
|
||||
if (self.out.pack_by_ptr.get(key)) |id| return id;
|
||||
const id: PackParamId = @enumFromInt(@as(u32, @intCast(self.out.pack_params.items.len)));
|
||||
self.out.pack_params.append(self.out.alloc, .{
|
||||
.id = id,
|
||||
.name = m.param.name,
|
||||
.owner = m.owner,
|
||||
}) catch @panic("resolve: OOM");
|
||||
self.out.pack_by_ptr.put(key, id) catch @panic("resolve: OOM");
|
||||
return id;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user