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:
agra
2026-06-02 22:23:01 +03:00
parent e6cbb60d8f
commit 137285f33d
3 changed files with 242 additions and 106 deletions

View File

@@ -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| {