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

@@ -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);
}