refactor(ir): move protocol/impl registration into ProtocolResolver (A4.2 registration increment)

Move the registration functions behind the protocols.zig facade, per PLAN-ARCH
A4.2 ("then registration", keeping IR emission in Lowering):
- registerProtocolDecl (protocol struct + dispatch method table + vtable type),
- registerImplBlock (concrete impl -> <Target>.<method> in fn_ast_map + default-
  method synthesis),
- registerParamImpl (parameterised impl -> param_impl_map / param_impl_pack_map
  + the same-file duplicate diagnostic),
- synthesizeDefaultMethod (facade-private; its only caller moved too).

Moved verbatim with self. -> self.l. facade rewrites. Emission stays in
Lowering: the registry calls self.l.declareFunction (the extern-stub primitive)
but the thunk/value builders (createProtocolThunk / buildProtocolValue /
tryUserConversion / getOrCreateThunks) are NOT moved.

Lowering keeps registerProtocolDecl as a thin pub wrapper (scan pass + 7
unit-test callers); registerImplBlock / registerParamImpl /
synthesizeDefaultMethod deleted (no fallback), the 2 scan call sites routed
through protocolResolver(). New pub: declareFunction (8 callers, emission infra),
ParamImplEntry / PackParamImplEntry (the registry constructs them; stay as
Lowering nested types). State maps remain on Lowering; the facade reads/writes
self.l.* (migrate once planning lands).

protocols.test.zig +2: registerImplBlock records Circle.draw in fn_ast_map (and
packArgConformsTo then sees it); registerParamImpl flags a same-file duplicate
impl Into(s64) for IntCell (the 0412-class, unit level).

zig build, zig build test, tests/run_examples.sh (357/0) all green — no .ir
churn; the 0410/0411/0412 rejection diagnostics are byte-for-byte preserved.
This commit is contained in:
agra
2026-06-02 22:10:40 +03:00
parent 81d332dfb0
commit e6cbb60d8f
3 changed files with 454 additions and 356 deletions

View File

@@ -7,6 +7,7 @@
const std = @import("std");
const ast = @import("../ast.zig");
const Node = ast.Node;
const errors = @import("../errors.zig");
const ir_mod = @import("ir.zig");
const TypeId = ir_mod.TypeId;
@@ -19,6 +20,18 @@ fn protoMethodReq(name: []const u8) ast.ProtocolMethodDecl {
return .{ .name = name, .params = &.{}, .param_names = &.{}, .return_type = null, .default_body = null };
}
fn mk(alloc: std.mem.Allocator, data: ast.Node.Data) *Node {
const n = alloc.create(Node) catch unreachable;
n.* = .{ .span = .{ .start = 0, .end = 0 }, .data = data };
return n;
}
fn typeExpr(alloc: std.mem.Allocator, name: []const u8) *Node {
return mk(alloc, .{ .type_expr = .{ .name = name, .is_generic = false } });
}
fn emptyBody(alloc: std.mem.Allocator) *Node {
return mk(alloc, .{ .block = .{ .stmts = &.{} } });
}
test "protocols: getProtocolInfo resolves registered protocol structs only" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
@@ -105,3 +118,69 @@ test "protocols: packArgConformsTo at the impl-declaration level (non-parameteri
// An unregistered protocol name conforms to nothing.
try std.testing.expect(!pr.packArgConformsTo("Nope", circle));
}
test "protocols: registerImplBlock records <Target>.<method> in fn_ast_map" {
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 };
// Drawable :: protocol { draw :: (); } + impl Drawable for Circle { draw :: (){} }
const proto_methods = [_]ast.ProtocolMethodDecl{protoMethodReq("draw")};
const pd = ast.ProtocolDecl{ .name = "Drawable", .methods = &proto_methods };
l.registerProtocolDecl(&pd);
const draw_node = mk(alloc, .{ .fn_decl = .{ .name = "draw", .params = &.{}, .return_type = null, .body = emptyBody(alloc) } });
const methods = [_]*Node{draw_node};
const ib = ast.ImplBlock{ .protocol_name = "Drawable", .target_type = "Circle", .methods = &methods };
const decl = mk(alloc, .{ .impl_block = ib });
// Not registered before; the non-parameterised impl registers `Circle.draw`.
try std.testing.expect(!l.program_index.fn_ast_map.contains("Circle.draw"));
pr.registerImplBlock(&ib, false, decl);
try std.testing.expect(l.program_index.fn_ast_map.contains("Circle.draw"));
// And it now conforms (same fn_ast_map entry packArgConformsTo checks).
const circle = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("Circle"), .fields = &.{} } });
try std.testing.expect(pr.packArgConformsTo("Drawable", circle));
}
test "protocols: registerParamImpl flags a same-file duplicate impl" {
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 diags = errors.DiagnosticList.init(alloc, "", "test.sx");
defer diags.deinit();
var l = Lowering.init(&module);
l.diagnostics = &diags;
l.current_source_file = "test.sx"; // both impls share a defining module
const pr = ProtocolResolver{ .l = &l };
_ = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("IntCell"), .fields = &.{} } });
// impl Into(s64) for IntCell { ... } — a parameterised-protocol impl.
const args = [_]*Node{typeExpr(alloc, "s64")};
const conv = mk(alloc, .{ .fn_decl = .{ .name = "convert", .params = &.{}, .return_type = null, .body = emptyBody(alloc) } });
const methods = [_]*Node{conv};
const ib = ast.ImplBlock{
.protocol_name = "Into",
.target_type = "IntCell",
.methods = &methods,
.protocol_type_args = &args,
};
const decl = mk(alloc, .{ .impl_block = ib });
// First registration is fine; the second (same key, same module) is a dup.
pr.registerImplBlock(&ib, false, decl);
pr.registerImplBlock(&ib, false, decl);
var dup_reported = false;
for (diags.items.items) |d| {
if (d.level == .err and std.mem.indexOf(u8, d.message, "duplicate impl 'Into'") != null) dup_reported = true;
}
try std.testing.expect(dup_reported);
}