refactor(ir): factor protocol/impl planning into ProtocolResolver (A4.2 planning increment)
Factor the lookup/planning half of the protocol emission functions into protocols.zig, keeping IR emission in Lowering (PLAN-ARCH A4.2 final increment): - protocolMethodInfos(proto) — the dispatch method table = which methods getOrCreateThunks must thunk. getOrCreateThunks now does PLANNING via this + EMISSION (createProtocolThunk loop) in Lowering. - findVisibleImpls(entries, out) — moved verbatim (pure BFS over the import graph; the cross-module visibility selection behind the 0410 path). tryUserConversion calls it via the resolver. - matchPackImpl(src_ty, pack_key) -> ?PackImplMatch — the pure pack-impl matching loop (prefix + return match) + convert-method find, returning the matched entry + convert fd + src params/ret. tryPackImplMatch consumes it; the binding + monomorphise + call emission stays in Lowering. Emission untouched: createProtocolThunk, buildProtocolValue, and the monomorphise+call tails of tryUserConversion / tryPackImplMatch remain in Lowering. The reentrancy guard, key-build, and the Into no-visible / duplicate / recursive diagnostics stay in tryUserConversion (byte-for-byte). lower.zig net -94 lines. No new pub exposure (uses the existing ParamImplEntry / PackParamImplEntry / formatTypeName surface). protocols.test.zig +3: protocolMethodInfos (method table + null-for-unknown, no silent empty default); findVisibleImpls (falls open with no graph; filters to here + transitive imports); matchPackImpl (selects on prefix+return; null for non-closure source / unknown key). zig build, zig build test, tests/run_examples.sh (357/0) all green — no .ir churn; the 0410/0411/0412 diagnostics are byte-for-byte preserved.
This commit is contained in:
118
src/ir/lower.zig
118
src/ir/lower.zig
@@ -11422,47 +11422,6 @@ pub const Lowering = struct {
|
||||
return self.genericResolver().mangleTypeName(ty);
|
||||
}
|
||||
|
||||
/// Collect impl entries visible from `current_source_file` — defined in
|
||||
/// the current file or in any module the current file transitively
|
||||
/// imports. Falls open (returns all entries) when the source-file
|
||||
/// context or import graph isn't wired (e.g. comptime callers).
|
||||
fn findVisibleImpls(self: *Lowering, entries: []const ParamImplEntry, out: *std.ArrayList(ParamImplEntry)) void {
|
||||
const here = self.current_source_file orelse {
|
||||
out.appendSlice(self.alloc, entries) catch {};
|
||||
return;
|
||||
};
|
||||
const graph = self.program_index.import_graph orelse {
|
||||
out.appendSlice(self.alloc, entries) catch {};
|
||||
return;
|
||||
};
|
||||
|
||||
// BFS over the import graph to compute the visible set.
|
||||
var visible = std.StringHashMap(void).init(self.alloc);
|
||||
defer visible.deinit();
|
||||
visible.put(here, {}) catch {};
|
||||
var queue = std.ArrayList([]const u8).empty;
|
||||
defer queue.deinit(self.alloc);
|
||||
queue.append(self.alloc, here) catch {};
|
||||
var head: usize = 0;
|
||||
while (head < queue.items.len) : (head += 1) {
|
||||
const node = queue.items[head];
|
||||
const direct = graph.get(node) orelse continue;
|
||||
var it = direct.iterator();
|
||||
while (it.next()) |kv| {
|
||||
const next = kv.key_ptr.*;
|
||||
if (visible.contains(next)) continue;
|
||||
visible.put(next, {}) catch {};
|
||||
queue.append(self.alloc, next) catch {};
|
||||
}
|
||||
}
|
||||
|
||||
for (entries) |e| {
|
||||
if (visible.contains(e.defining_module)) {
|
||||
out.append(self.alloc, e) catch {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve type category names (like "int", "struct", "float") to matching TypeId tag values.
|
||||
/// Returns a list of TypeId index values that match the category.
|
||||
fn resolveTypeCategoryTags(self: *Lowering, name: []const u8) []const u64 {
|
||||
@@ -13462,11 +13421,13 @@ pub const Lowering = struct {
|
||||
const key = std.fmt.allocPrint(self.alloc, "{s}\x00{s}", .{ proto_name, concrete_type_name }) catch return &.{};
|
||||
if (self.protocol_thunk_map.get(key)) |thunks| return thunks;
|
||||
|
||||
const pd = self.program_index.protocol_decl_map.get(proto_name) orelse return &.{};
|
||||
// PLANNING: which methods need a thunk (owned by the registry).
|
||||
const methods = self.protocolResolver().protocolMethodInfos(proto_name) orelse return &.{};
|
||||
var thunk_ids = std.ArrayList(FuncId).empty;
|
||||
defer thunk_ids.deinit(self.alloc);
|
||||
|
||||
for (pd.methods) |method| {
|
||||
// EMISSION: materialize one thunk per method (stays in Lowering).
|
||||
for (methods) |method| {
|
||||
const thunk_id = self.createProtocolThunk(proto_name, concrete_type_name, method);
|
||||
thunk_ids.append(self.alloc, thunk_id) catch unreachable;
|
||||
}
|
||||
@@ -14018,69 +13979,14 @@ pub const Lowering = struct {
|
||||
guard_key: u64,
|
||||
) ?Ref {
|
||||
_ = operand_node;
|
||||
const pack_entries = self.param_impl_pack_map.get(pack_key) orelse return null;
|
||||
if (pack_entries.items.len == 0) return null;
|
||||
// PLANNING: select the matching pack impl + its `convert` (registry).
|
||||
const match = self.protocolResolver().matchPackImpl(src_ty, pack_key) orelse return null;
|
||||
const entry = match.entry;
|
||||
const fd = match.convert_fd;
|
||||
const src_params = match.src_params;
|
||||
const src_ret = match.src_ret;
|
||||
const table = &self.module.types;
|
||||
// Source must itself be a closure/function the pack can match.
|
||||
const src_info = table.get(src_ty);
|
||||
if (src_info != .closure and src_info != .function) return null;
|
||||
|
||||
const src_params: []const TypeId = switch (src_info) {
|
||||
.closure => |c| c.params,
|
||||
.function => |f| f.params,
|
||||
else => unreachable,
|
||||
};
|
||||
const src_ret: TypeId = switch (src_info) {
|
||||
.closure => |c| c.ret,
|
||||
.function => |f| f.ret,
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
// Find pack impls whose fixed prefix matches src's leading params.
|
||||
var matched_idx: ?usize = null;
|
||||
for (pack_entries.items, 0..) |entry, i| {
|
||||
const ent_info = table.get(entry.source_pack_ty);
|
||||
// Pack impls always wear a closure (resolveClosureType routes
|
||||
// both Closure and the future Fn pack forms through
|
||||
// closureTypePack); a function-typed pack impl is not produced
|
||||
// by current parser shapes.
|
||||
if (ent_info != .closure) continue;
|
||||
const ent_ci = ent_info.closure;
|
||||
const pack_start = ent_ci.pack_start orelse continue;
|
||||
// Fixed prefix must fit within the source's params.
|
||||
if (pack_start > src_params.len) continue;
|
||||
var prefix_ok = true;
|
||||
var i_fix: u32 = 0;
|
||||
while (i_fix < pack_start) : (i_fix += 1) {
|
||||
if (ent_ci.params[i_fix] != src_params[i_fix]) {
|
||||
prefix_ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!prefix_ok) continue;
|
||||
// Return type: if the impl's return is a generic var
|
||||
// (ret_var_name set), any source return binds; otherwise it
|
||||
// must equal the source's return exactly.
|
||||
if (entry.ret_var_name == null and ent_ci.ret != src_ret) continue;
|
||||
// First match wins for v1; concrete-wins-over-pack already
|
||||
// happened by the caller checking concrete first. Multiple
|
||||
// overlapping pack impls would be a separate diagnostic
|
||||
// (deferred — same module duplicates are caught at registration).
|
||||
matched_idx = i;
|
||||
break;
|
||||
}
|
||||
const idx = matched_idx orelse return null;
|
||||
const entry = pack_entries.items[idx];
|
||||
|
||||
// Find the `convert` method.
|
||||
var convert_fd: ?*const ast.FnDecl = null;
|
||||
for (entry.methods) |m| {
|
||||
if (std.mem.eql(u8, m.name, "convert")) {
|
||||
convert_fd = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const fd = convert_fd orelse return null;
|
||||
// EMISSION: bind the pack tail + ret-var, monomorphise, call (Lowering).
|
||||
|
||||
// Build bindings. Target → dst_ty (already in the protocol's type
|
||||
// params), pack-var → src tail TypeIds, ret-var (when generic) →
|
||||
@@ -14188,7 +14094,7 @@ pub const Lowering = struct {
|
||||
// Falls open when import_graph isn't wired (e.g. comptime callers).
|
||||
var visible_impls = std.ArrayList(ParamImplEntry).empty;
|
||||
defer visible_impls.deinit(self.alloc);
|
||||
self.findVisibleImpls(entries.items, &visible_impls);
|
||||
self.protocolResolver().findVisibleImpls(entries.items, &visible_impls);
|
||||
|
||||
if (visible_impls.items.len == 0) {
|
||||
if (self.diagnostics) |diags| {
|
||||
|
||||
@@ -184,3 +184,101 @@ test "protocols: registerParamImpl flags a same-file duplicate impl" {
|
||||
}
|
||||
try std.testing.expect(dup_reported);
|
||||
}
|
||||
|
||||
// ── Planning (lookup-only; emission stays in Lowering) ───────────────
|
||||
|
||||
test "protocols: protocolMethodInfos lists the methods to materialize thunks for" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
defer module.deinit();
|
||||
var l = Lowering.init(&module);
|
||||
const pr = ProtocolResolver{ .l = &l };
|
||||
|
||||
const methods = [_]ast.ProtocolMethodDecl{ protoMethodReq("draw"), protoMethodReq("area") };
|
||||
const pd = ast.ProtocolDecl{ .name = "Drawable", .methods = &methods };
|
||||
l.registerProtocolDecl(&pd);
|
||||
|
||||
// The registry knows exactly which methods getOrCreateThunks must thunk.
|
||||
const infos = pr.protocolMethodInfos("Drawable").?;
|
||||
try std.testing.expectEqual(@as(usize, 2), infos.len);
|
||||
try std.testing.expectEqualStrings("draw", infos[0].name);
|
||||
try std.testing.expectEqualStrings("area", infos[1].name);
|
||||
// Unknown protocol → null (no silent empty-table default).
|
||||
try std.testing.expect(pr.protocolMethodInfos("Nope") == null);
|
||||
}
|
||||
|
||||
test "protocols: findVisibleImpls filters by transitive import visibility" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
defer module.deinit();
|
||||
var l = Lowering.init(&module);
|
||||
const pr = ProtocolResolver{ .l = &l };
|
||||
|
||||
const here_entry: Lowering.ParamImplEntry = .{ .methods = &.{}, .source_ty = .s64, .target_args = &.{}, .defining_module = "a.sx", .span = .{ .start = 0, .end = 0 } };
|
||||
const other_entry: Lowering.ParamImplEntry = .{ .methods = &.{}, .source_ty = .s64, .target_args = &.{}, .defining_module = "b.sx", .span = .{ .start = 0, .end = 0 } };
|
||||
const entries = [_]Lowering.ParamImplEntry{ here_entry, other_entry };
|
||||
|
||||
// No source-file context → falls open (all entries visible).
|
||||
{
|
||||
var out = std.ArrayList(Lowering.ParamImplEntry).empty;
|
||||
defer out.deinit(alloc);
|
||||
pr.findVisibleImpls(&entries, &out);
|
||||
try std.testing.expectEqual(@as(usize, 2), out.items.len);
|
||||
}
|
||||
|
||||
// From `a.sx`, which imports nothing: only the `a.sx` impl is visible.
|
||||
{
|
||||
var graph = std.StringHashMap(std.StringHashMap(void)).init(alloc);
|
||||
graph.put("a.sx", std.StringHashMap(void).init(alloc)) catch unreachable;
|
||||
l.program_index.import_graph = &graph;
|
||||
l.current_source_file = "a.sx";
|
||||
var out = std.ArrayList(Lowering.ParamImplEntry).empty;
|
||||
defer out.deinit(alloc);
|
||||
pr.findVisibleImpls(&entries, &out);
|
||||
try std.testing.expectEqual(@as(usize, 1), out.items.len);
|
||||
try std.testing.expectEqualStrings("a.sx", out.items[0].defining_module);
|
||||
}
|
||||
}
|
||||
|
||||
test "protocols: matchPackImpl selects a pack impl whose prefix + return match" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
defer module.deinit();
|
||||
var l = Lowering.init(&module);
|
||||
const pr = ProtocolResolver{ .l = &l };
|
||||
|
||||
// A pack impl source `Closure(..$args) -> void` (pack_start = 0).
|
||||
const pack_src = module.types.closureTypePack(&.{}, .void, 0);
|
||||
const convert = ast.FnDecl{ .name = "convert", .params = &.{}, .return_type = null, .body = emptyBody(alloc) };
|
||||
const conv_methods = [_]*const ast.FnDecl{&convert};
|
||||
const pack_entry: Lowering.PackParamImplEntry = .{
|
||||
.methods = &conv_methods,
|
||||
.source_pack_ty = pack_src,
|
||||
.target_args = &.{},
|
||||
.defining_module = "test.sx",
|
||||
.span = .{ .start = 0, .end = 0 },
|
||||
.pack_var_name = "args",
|
||||
.ret_var_name = null,
|
||||
};
|
||||
var list = std.ArrayList(Lowering.PackParamImplEntry).empty;
|
||||
list.append(alloc, pack_entry) catch unreachable;
|
||||
const pack_key = "Into\x00Block";
|
||||
l.param_impl_pack_map.put(pack_key, list) catch unreachable;
|
||||
|
||||
// A concrete `Closure() -> void` source matches (no fixed prefix, void ret).
|
||||
const src = module.types.closureType(&.{}, .void);
|
||||
const m = pr.matchPackImpl(src, pack_key).?;
|
||||
try std.testing.expectEqualStrings("convert", m.convert_fd.name);
|
||||
try std.testing.expectEqual(@as(usize, 0), m.src_params.len);
|
||||
try std.testing.expectEqual(TypeId.void, m.src_ret);
|
||||
|
||||
// A non-closure source does not match; an unknown key does not match.
|
||||
try std.testing.expect(pr.matchPackImpl(.s64, pack_key) == null);
|
||||
try std.testing.expect(pr.matchPackImpl(src, "Into\x00Nope") == null);
|
||||
}
|
||||
|
||||
@@ -93,6 +93,138 @@ pub const ProtocolResolver = struct {
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── Thunk / impl PLANNING (lookup only; emission stays in Lowering) ──
|
||||
|
||||
/// The dispatch method table for protocol `proto_name` — i.e. exactly which
|
||||
/// methods `getOrCreateThunks` must materialize a thunk for. Null if the
|
||||
/// name isn't a registered (non-parameterised) protocol.
|
||||
pub fn protocolMethodInfos(self: ProtocolResolver, proto_name: []const u8) ?[]const ProtocolMethodInfo {
|
||||
const pd = self.l.program_index.protocol_decl_map.get(proto_name) orelse return null;
|
||||
return pd.methods;
|
||||
}
|
||||
|
||||
/// Filter parameterised-impl `entries` to those reachable from the current
|
||||
/// source file (the file itself + everything it transitively imports). The
|
||||
/// cross-module visibility selection behind the `0410` path. Falls open
|
||||
/// (all entries) when the source-file context or import graph isn't wired
|
||||
/// (e.g. comptime callers). Appends the visible subset to `out`.
|
||||
pub fn findVisibleImpls(self: ProtocolResolver, entries: []const Lowering.ParamImplEntry, out: *std.ArrayList(Lowering.ParamImplEntry)) void {
|
||||
const here = self.l.current_source_file orelse {
|
||||
out.appendSlice(self.l.alloc, entries) catch {};
|
||||
return;
|
||||
};
|
||||
const graph = self.l.program_index.import_graph orelse {
|
||||
out.appendSlice(self.l.alloc, entries) catch {};
|
||||
return;
|
||||
};
|
||||
|
||||
// BFS over the import graph to compute the visible set.
|
||||
var visible = std.StringHashMap(void).init(self.l.alloc);
|
||||
defer visible.deinit();
|
||||
visible.put(here, {}) catch {};
|
||||
var queue = std.ArrayList([]const u8).empty;
|
||||
defer queue.deinit(self.l.alloc);
|
||||
queue.append(self.l.alloc, here) catch {};
|
||||
var head: usize = 0;
|
||||
while (head < queue.items.len) : (head += 1) {
|
||||
const node = queue.items[head];
|
||||
const direct = graph.get(node) orelse continue;
|
||||
var it = direct.iterator();
|
||||
while (it.next()) |kv| {
|
||||
const next = kv.key_ptr.*;
|
||||
if (visible.contains(next)) continue;
|
||||
visible.put(next, {}) catch {};
|
||||
queue.append(self.l.alloc, next) catch {};
|
||||
}
|
||||
}
|
||||
|
||||
for (entries) |e| {
|
||||
if (visible.contains(e.defining_module)) {
|
||||
out.append(self.l.alloc, e) catch {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A pack-impl selected for a concrete source closure/function: the matched
|
||||
/// entry plus its `convert` method. Pure SELECTION — binding + monomorphise
|
||||
/// + emission stay in `Lowering.tryPackImplMatch`.
|
||||
pub const PackImplMatch = struct {
|
||||
entry: Lowering.PackParamImplEntry,
|
||||
convert_fd: *const ast.FnDecl,
|
||||
/// The source closure/function's param + return types — the binding
|
||||
/// step (in `Lowering`) reads these to bind the pack-var tail + ret-var.
|
||||
src_params: []const TypeId,
|
||||
src_ret: TypeId,
|
||||
};
|
||||
|
||||
/// Among the pack impls under `pack_key`, find the first whose fixed prefix
|
||||
/// matches `src_ty`'s leading params (and whose return matches, unless the
|
||||
/// impl's return is a generic var). Returns the matched entry + its
|
||||
/// `convert` method, or null when nothing matches. No emission.
|
||||
pub fn matchPackImpl(self: ProtocolResolver, src_ty: TypeId, pack_key: []const u8) ?PackImplMatch {
|
||||
const pack_entries = self.l.param_impl_pack_map.get(pack_key) orelse return null;
|
||||
if (pack_entries.items.len == 0) return null;
|
||||
const table = &self.l.module.types;
|
||||
// Source must itself be a closure/function the pack can match.
|
||||
const src_info = table.get(src_ty);
|
||||
if (src_info != .closure and src_info != .function) return null;
|
||||
|
||||
const src_params: []const TypeId = switch (src_info) {
|
||||
.closure => |c| c.params,
|
||||
.function => |f| f.params,
|
||||
else => unreachable,
|
||||
};
|
||||
const src_ret: TypeId = switch (src_info) {
|
||||
.closure => |c| c.ret,
|
||||
.function => |f| f.ret,
|
||||
else => unreachable,
|
||||
};
|
||||
|
||||
// Find pack impls whose fixed prefix matches src's leading params.
|
||||
var matched_idx: ?usize = null;
|
||||
for (pack_entries.items, 0..) |entry, i| {
|
||||
const ent_info = table.get(entry.source_pack_ty);
|
||||
// Pack impls always wear a closure (resolveClosureType routes
|
||||
// both Closure and the future Fn pack forms through
|
||||
// closureTypePack); a function-typed pack impl is not produced
|
||||
// by current parser shapes.
|
||||
if (ent_info != .closure) continue;
|
||||
const ent_ci = ent_info.closure;
|
||||
const pack_start = ent_ci.pack_start orelse continue;
|
||||
// Fixed prefix must fit within the source's params.
|
||||
if (pack_start > src_params.len) continue;
|
||||
var prefix_ok = true;
|
||||
var i_fix: u32 = 0;
|
||||
while (i_fix < pack_start) : (i_fix += 1) {
|
||||
if (ent_ci.params[i_fix] != src_params[i_fix]) {
|
||||
prefix_ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!prefix_ok) continue;
|
||||
// Return type: if the impl's return is a generic var
|
||||
// (ret_var_name set), any source return binds; otherwise it
|
||||
// must equal the source's return exactly.
|
||||
if (entry.ret_var_name == null and ent_ci.ret != src_ret) continue;
|
||||
// First match wins for v1; concrete-wins-over-pack already
|
||||
// happened by the caller checking concrete first. Multiple
|
||||
// overlapping pack impls would be a separate diagnostic
|
||||
// (deferred — same module duplicates are caught at registration).
|
||||
matched_idx = i;
|
||||
break;
|
||||
}
|
||||
const idx = matched_idx orelse return null;
|
||||
const entry = pack_entries.items[idx];
|
||||
|
||||
// Find the `convert` method.
|
||||
for (entry.methods) |m| {
|
||||
if (std.mem.eql(u8, m.name, "convert")) {
|
||||
return .{ .entry = entry, .convert_fd = m, .src_params = src_params, .src_ret = src_ret };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Registration ────────────────────────────────────────────────────
|
||||
|
||||
pub fn registerProtocolDecl(self: ProtocolResolver, pd: *const ast.ProtocolDecl) void {
|
||||
|
||||
Reference in New Issue
Block a user