From e6cbb60d8f01250bdda6f9d0c1f4cb38bb03846f Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 2 Jun 2026 22:10:40 +0300 Subject: [PATCH] refactor(ir): move protocol/impl registration into ProtocolResolver (A4.2 registration increment) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 -> . 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. --- src/ir/lower.zig | 354 +---------------------------------- src/ir/protocols.test.zig | 79 ++++++++ src/ir/protocols.zig | 377 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 454 insertions(+), 356 deletions(-) diff --git a/src/ir/lower.zig b/src/ir/lower.zig index f298ed1..1928ae3 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -250,7 +250,7 @@ pub const Lowering = struct { /// One impl block for a parameterised protocol (e.g. `impl Into(Block) for Closure() -> void`). /// Stored in `param_impl_map` keyed by (protocol_name, target_args_mangled, source_mangled). /// `defining_module` enables import-scoped visibility + cross-module duplicate diagnostics. - const ParamImplEntry = struct { + pub const ParamImplEntry = struct { methods: []const *const ast.FnDecl, source_ty: TypeId, target_args: []const TypeId, @@ -272,7 +272,7 @@ pub const Lowering = struct { /// (e.g. "args") and the source's return type binds to `ret_var_name` /// (e.g. "R") when the impl's return is generic. `ret_var_name == null` /// means the return type is concrete and must match exactly. - const PackParamImplEntry = struct { + pub const PackParamImplEntry = struct { methods: []const *const ast.FnDecl, source_pack_ty: TypeId, target_args: []const TypeId, @@ -968,7 +968,7 @@ pub const Lowering = struct { self.registerProtocolDecl(&decl.data.protocol_decl); }, .impl_block => { - self.registerImplBlock(&decl.data.impl_block, is_imported, decl); + self.protocolResolver().registerImplBlock(&decl.data.impl_block, is_imported, decl); }, .foreign_class_decl => { self.registerForeignClassDecl(&decl.data.foreign_class_decl); @@ -1201,7 +1201,7 @@ pub const Lowering = struct { self.registerProtocolDecl(&decl.data.protocol_decl); }, .impl_block => { - self.registerImplBlock(&decl.data.impl_block, is_imported, decl); + self.protocolResolver().registerImplBlock(&decl.data.impl_block, is_imported, decl); }, .foreign_class_decl => { self.registerForeignClassDecl(&decl.data.foreign_class_decl); @@ -1477,7 +1477,7 @@ pub const Lowering = struct { } /// Declare a function as an extern stub (signature only, no body). - fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8) void { + pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8) void { // Skip generic templates — they're monomorphized on demand, not declared as extern if (fd.type_params.len > 0) return; @@ -12972,117 +12972,11 @@ pub const Lowering = struct { /// Inline protocols: { ctx: *void, method1: *void, method2: *void, ... } /// Non-inline protocols: { ctx: *void, __vtable: *void } /// Also stores protocol info for dispatch and vtable struct type for vtable protocols. + /// Register a protocol declaration. Thin delegation to the canonical owner + /// (`ProtocolResolver`, `protocols.zig`); kept on `Lowering` as a `pub` + /// entry point because the scan pass + several unit tests reach it here. pub fn registerProtocolDecl(self: *Lowering, pd: *const ast.ProtocolDecl) void { - // Decision 4 soft-convention warning: a type-arg and a method (the - // "runtime accessor" namespace — protocols have no fields) sharing a - // name is allowed, but `..pack.` then resolves by *position* - // rather than by precedence, which surprises readers. Alert at decl. - for (pd.type_params) |tp| { - for (pd.methods) |m| { - if (std.mem.eql(u8, tp.name, m.name)) { - if (self.diagnostics) |diags| { - diags.addFmt(.warn, null, "protocol '{s}' declares type-arg and method both named '{s}'; `..pack.{s}` resolves by position (type-arg in type position, method in value position)", .{ pd.name, tp.name, tp.name }); - } - } - } - } - - // Parameterised protocols are compile-time-only — no vtable, no boxed - // instance struct. Methods reference unbound type params (e.g. - // `convert :: () -> Target`) that only get a concrete TypeId per - // (Source, Target) pair at xx resolution time. Stash the AST so - // `param_impl_map` lookup can resolve method signatures lazily. - if (pd.type_params.len > 0) { - self.program_index.protocol_ast_map.put(pd.name, pd) catch {}; - return; - } - - const table = &self.module.types; - const name_id = table.internString(pd.name); - - var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; - - // First field: ctx: *void - const void_ptr_ty = table.ptrTo(.void); - fields.append(self.alloc, .{ - .name = table.internString("ctx"), - .ty = void_ptr_ty, - }) catch unreachable; - - if (pd.is_inline) { - // One fn-ptr field per protocol method - for (pd.methods) |method| { - fields.append(self.alloc, .{ - .name = table.internString(method.name), - .ty = void_ptr_ty, // fn ptrs are opaque pointers - }) catch unreachable; - } - } else { - // Vtable pointer - fields.append(self.alloc, .{ - .name = table.internString("__vtable"), - .ty = void_ptr_ty, - }) catch unreachable; - } - - const struct_info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items, .is_protocol = true } }; - const id = if (table.findByName(name_id)) |existing| existing else table.intern(struct_info); - table.update(id, struct_info); - - // Build protocol method info for dispatch - var method_infos = std.ArrayList(ProtocolMethodInfo).empty; - for (pd.methods) |method| { - var ptypes = std.ArrayList(TypeId).empty; - for (method.params) |p| { - // Self → *void for protocol context; everything else - // goes through `resolveAstType`, threaded with the canonical - // alias map (`ProgramIndex.type_alias_map`). - const pty = blk: { - if (p.data == .type_expr and std.mem.eql(u8, p.data.type_expr.name, "Self")) { - break :blk void_ptr_ty; - } - break :blk type_bridge.resolveAstType(p, table, &self.program_index.type_alias_map); - }; - ptypes.append(self.alloc, pty) catch unreachable; - } - var ret_is_self = false; - const ret = if (method.return_type) |rt| blk: { - if (rt.data == .type_expr and std.mem.eql(u8, rt.data.type_expr.name, "Self")) { - ret_is_self = true; - break :blk void_ptr_ty; - } - break :blk type_bridge.resolveAstType(rt, table, &self.program_index.type_alias_map); - } else .void; - method_infos.append(self.alloc, .{ - .name = method.name, - .param_types = self.alloc.dupe(TypeId, ptypes.items) catch unreachable, - .ret_type = ret, - .ret_is_self = ret_is_self, - }) catch unreachable; - } - self.program_index.protocol_decl_map.put(pd.name, .{ - .name = pd.name, - .is_inline = pd.is_inline, - .methods = self.alloc.dupe(ProtocolMethodInfo, method_infos.items) catch unreachable, - }) catch {}; - self.program_index.protocol_ast_map.put(pd.name, pd) catch {}; - - // For vtable protocols, create the vtable struct type - if (!pd.is_inline) { - var vtable_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; - for (pd.methods) |method| { - vtable_fields.append(self.alloc, .{ - .name = table.internString(method.name), - .ty = void_ptr_ty, - }) catch unreachable; - } - var vtable_name_buf: [128]u8 = undefined; - const vtable_name = std.fmt.bufPrint(&vtable_name_buf, "__{s}__Vtable", .{pd.name}) catch "__Vtable"; - const vtable_name_id = table.internString(vtable_name); - const vtable_info: types.TypeInfo = .{ .@"struct" = .{ .name = vtable_name_id, .fields = vtable_fields.items } }; - const vtable_ty = table.intern(vtable_info); - self.protocol_vtable_type_map.put(pd.name, vtable_ty) catch {}; - } + return self.protocolResolver().registerProtocolDecl(pd); } /// Instantiate a parameterized protocol as a runtime VALUE type: @@ -13545,236 +13439,6 @@ pub const Lowering = struct { } } - /// Register an impl block: register its methods as TypeName.method in fn_ast_map. - fn registerImplBlock(self: *Lowering, ib: *const ast.ImplBlock, is_imported: bool, decl: *const Node) void { - // Parameterised-protocol impl (e.g. `impl Into(Block) for Closure() -> void`): - // record into `param_impl_map` for compile-time resolution by `lowerXX`. - // Methods are NOT registered in fn_ast_map — they're monomorphised lazily - // per (Source, Target) pair at the xx call site. - if (ib.protocol_type_args.len > 0) { - self.registerParamImpl(ib, decl, is_imported); - return; - } - // Collect explicitly implemented method names - var impl_methods = std.StringHashMap(void).init(self.alloc); - defer impl_methods.deinit(); - for (ib.methods) |method_node| { - if (method_node.data == .fn_decl) { - const method_fd = &method_node.data.fn_decl; - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ ib.target_type, method_fd.name }) catch continue; - self.program_index.fn_ast_map.put(qualified, method_fd) catch {}; - self.program_index.import_flags.put(qualified, is_imported) catch {}; - self.declareFunction(method_fd, qualified); - impl_methods.put(method_fd.name, {}) catch {}; - } - } - // Synthesize default methods from protocol declaration - if (self.program_index.protocol_ast_map.get(ib.protocol_name)) |pd| { - for (pd.methods) |method| { - if (method.default_body != null and !impl_methods.contains(method.name)) { - // Create a synthesized fn_decl for the default method - const synth_fd = self.synthesizeDefaultMethod(method, ib.target_type); - const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ ib.target_type, method.name }) catch continue; - self.program_index.fn_ast_map.put(qualified, synth_fd) catch {}; - self.program_index.import_flags.put(qualified, is_imported) catch {}; - self.declareFunction(synth_fd, qualified); - } - } - } - } - - /// Register a parameterised-protocol impl into `param_impl_map`. - /// Resolves the protocol's type args + the source type, mangles them, and - /// stashes the impl's method fn_decls for later monomorphisation by - /// `lowerXX`. Same-module duplicate impls produce a diagnostic here; - /// cross-module duplicates are detected at the xx resolution site. - /// - /// Pack-shaped sources (`Closure(..$args) -> $R`, detected via - /// `pack_start != null`) are additionally registered into - /// `param_impl_pack_map` keyed without the source suffix — the matching - /// site walks that map to bind packs against any concrete closure shape. - fn registerParamImpl(self: *Lowering, ib: *const ast.ImplBlock, decl: *const Node, is_imported: bool) void { - const table = &self.module.types; - - // Resolve the protocol's type-arg list to concrete TypeIds. - var arg_tys = std.ArrayList(TypeId).empty; - for (ib.protocol_type_args) |arg_node| { - const t = type_bridge.resolveAstType(arg_node, table, &self.program_index.type_alias_map); - arg_tys.append(self.alloc, t) catch return; - } - - // Resolve the source type. Parser stores it on `target_type_expr` for - // parameterised impls (back-compat `target_type` string is kept for - // simple cases but the canonical form is the TypeExpr). - const src_ty: TypeId = if (ib.target_type_expr) |te| - type_bridge.resolveAstType(te, table, &self.program_index.type_alias_map) - else if (ib.target_type.len > 0) - type_bridge.resolveAstType(&.{ .span = decl.span, .data = .{ .type_expr = .{ .name = ib.target_type } } }, table, &self.program_index.type_alias_map) - else - return; - - // Mangle into the lookup key. - var key_buf = std.ArrayList(u8).empty; - key_buf.appendSlice(self.alloc, ib.protocol_name) catch return; - for (arg_tys.items) |t| { - key_buf.append(self.alloc, 0) catch return; - key_buf.appendSlice(self.alloc, self.mangleTypeName(t)) catch return; - } - const pack_key_len = key_buf.items.len; // proto + args, no src — used for pack map - key_buf.append(self.alloc, 0) catch return; - key_buf.appendSlice(self.alloc, self.mangleTypeName(src_ty)) catch return; - const key = key_buf.items; - - // Collect method fn_decl pointers. - var methods = std.ArrayList(*const ast.FnDecl).empty; - for (ib.methods) |method_node| { - if (method_node.data == .fn_decl) { - methods.append(self.alloc, &method_node.data.fn_decl) catch {}; - } - } - - const defining_module: []const u8 = self.current_source_file orelse ""; - const entry: ParamImplEntry = .{ - .methods = self.alloc.dupe(*const ast.FnDecl, methods.items) catch return, - .source_ty = src_ty, - .target_args = self.alloc.dupe(TypeId, arg_tys.items) catch return, - .defining_module = defining_module, - .span = decl.span, - }; - - const gop = self.param_impl_map.getOrPut(key) catch return; - if (!gop.found_existing) { - gop.value_ptr.* = std.ArrayList(ParamImplEntry).empty; - } else { - // Same-file duplicate is an immediate error. Cross-file overlaps - // are deferred to the xx resolution site (Phase 5) so the impl - // surface can be richer than any one file's view. - for (gop.value_ptr.items) |existing| { - if (std.mem.eql(u8, existing.defining_module, defining_module)) { - if (self.diagnostics) |diags| { - diags.addFmt(.err, decl.span, "duplicate impl '{s}' for source '{s}' in {s}", .{ - ib.protocol_name, self.mangleTypeName(src_ty), defining_module, - }); - } - return; - } - } - } - gop.value_ptr.append(self.alloc, entry) catch return; - - // Concrete-struct source: also register the impl's methods as - // `.` in fn_ast_map so UFCS resolves them (e.g. - // `xs[i].get()` on a pack element). For a concrete impl like - // `impl Box(s64) for IntCell`, the method is already fully concrete — - // nothing to monomorphize, unlike generic/pack sources (which stay - // lazy in param_impl_map and are handled below). - { - const si = table.get(src_ty); - if (!src_ty.isBuiltin() and si == .@"struct") { - const src_name = self.formatTypeName(src_ty); - // A generic-struct source (`impl VL($R) for Combined($R, ..$Ts)`) - // registers each method as a TEMPLATE only: its signature - // references unbound type params (`-> $R`), so declaring it as a - // standalone function would emit garbage (an unresolved return - // type). Concrete instances are monomorphized per-erasure by - // createProtocolThunk via this same fn_ast_map entry. - const is_generic_src = self.program_index.struct_template_map.contains(src_name); - for (methods.items) |mfd| { - const q = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ src_name, mfd.name }) catch continue; - if (self.program_index.fn_ast_map.contains(q)) continue; // first impl wins - self.program_index.fn_ast_map.put(q, mfd) catch {}; - self.program_index.import_flags.put(q, is_imported) catch {}; - if (!is_generic_src) self.declareFunction(mfd, q); - } - } - } - - // Pack-shaped source: also register in the pack map. The source - // closure carries `pack_start` set; matching binds the source's - // tail param types to the pack-name and the source's return to - // the impl's return-type-var (when the return is generic). - const src_info = table.get(src_ty); - if (src_info == .closure and src_info.closure.pack_start != null) { - const target_expr_node = ib.target_type_expr orelse return; - if (target_expr_node.data != .closure_type_expr) return; - const ct = target_expr_node.data.closure_type_expr; - const pack_var = ct.pack_name orelse return; - // Extract the return-type-var name if the impl's return is generic. - // `Closure(...) -> $R` parses with the return-type node carrying - // `is_generic = true`. Concrete returns leave it null. - var ret_var: ?[]const u8 = null; - if (ct.return_type) |rt| { - if (rt.data == .type_expr and rt.data.type_expr.is_generic) { - ret_var = rt.data.type_expr.name; - } - } - const pack_entry: PackParamImplEntry = .{ - .methods = self.alloc.dupe(*const ast.FnDecl, methods.items) catch return, - .source_pack_ty = src_ty, - .target_args = self.alloc.dupe(TypeId, arg_tys.items) catch return, - .defining_module = defining_module, - .span = decl.span, - .pack_var_name = self.alloc.dupe(u8, pack_var) catch return, - .ret_var_name = if (ret_var) |rv| (self.alloc.dupe(u8, rv) catch return) else null, - }; - const pack_key = key_buf.items[0..pack_key_len]; - const pack_key_owned = self.alloc.dupe(u8, pack_key) catch return; - const pgop = self.param_impl_pack_map.getOrPut(pack_key_owned) catch return; - if (!pgop.found_existing) { - pgop.value_ptr.* = std.ArrayList(PackParamImplEntry).empty; - } else { - for (pgop.value_ptr.items) |existing| { - if (std.mem.eql(u8, existing.defining_module, defining_module)) { - if (self.diagnostics) |diags| { - diags.addFmt(.err, decl.span, "duplicate pack impl '{s}' for source '{s}' in {s}", .{ - ib.protocol_name, self.mangleTypeName(src_ty), defining_module, - }); - } - return; - } - } - } - pgop.value_ptr.append(self.alloc, pack_entry) catch return; - } - } - - /// Synthesize a fn_decl from a protocol default method for a concrete type. - fn synthesizeDefaultMethod(self: *Lowering, method: ast.ProtocolMethodDecl, target_type: []const u8) *const ast.FnDecl { - // Build parameter list: self: *TargetType, then the protocol method params - var params_list = std.ArrayList(ast.Param).empty; - defer params_list.deinit(self.alloc); - - // Add self parameter: self: *TargetType - const self_type_node = self.alloc.create(ast.Node) catch unreachable; - const pointee_node = self.alloc.create(ast.Node) catch unreachable; - pointee_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = target_type } } }; - self_type_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .pointer_type_expr = .{ - .pointee_type = pointee_node, - } } }; - params_list.append(self.alloc, .{ - .name = "self", - .name_span = .{ .start = 0, .end = 0 }, - .type_expr = self_type_node, - }) catch unreachable; - - // Add remaining params from the protocol method - for (method.params, method.param_names) |pty, pname| { - params_list.append(self.alloc, .{ - .name = pname, - .name_span = .{ .start = 0, .end = 0 }, - .type_expr = pty, - }) catch unreachable; - } - - const fd = self.alloc.create(ast.FnDecl) catch unreachable; - fd.* = .{ - .name = method.name, - .params = self.alloc.dupe(ast.Param, params_list.items) catch unreachable, - .body = method.default_body.?, - .return_type = method.return_type, - }; - return fd; - } // ── Protocol dispatch ────────────────────────────────────────── diff --git a/src/ir/protocols.test.zig b/src/ir/protocols.test.zig index abe4a59..3b26ed5 100644 --- a/src/ir/protocols.test.zig +++ b/src/ir/protocols.test.zig @@ -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 . 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); +} diff --git a/src/ir/protocols.zig b/src/ir/protocols.zig index 51ca63e..5b914d9 100644 --- a/src/ir/protocols.zig +++ b/src/ir/protocols.zig @@ -1,26 +1,36 @@ const std = @import("std"); +const ast = @import("../ast.zig"); const types = @import("types.zig"); +const type_bridge = @import("type_bridge.zig"); const lower = @import("lower.zig"); const program_index_mod = @import("program_index.zig"); +const Node = ast.Node; const TypeId = types.TypeId; const Lowering = lower.Lowering; const ProtocolDeclInfo = program_index_mod.ProtocolDeclInfo; +const ProtocolMethodInfo = program_index_mod.ProtocolMethodInfo; -/// Protocol / impl LOOKUP (architecture phase A4.2, first increment), extracted -/// from `Lowering`. Owns the read-only conformance queries: -/// - `getProtocolInfo` — is a type a registered protocol, and its method table, -/// - `hasImplPlain` — has a (protocol, type) pair had its thunks materialized, -/// - `packArgConformsTo` — does a type conform to a protocol at the -/// impl-declaration level (for protocol-pack `..xs: P` elements). +/// Protocol / impl LOOKUP + REGISTRATION (architecture phase A4.2), extracted +/// from `Lowering`. Owns: +/// - read-only conformance queries: `getProtocolInfo` (is a type a registered +/// protocol + its method table), `hasImplPlain` (have a (protocol, type) +/// pair's thunks been materialized), `packArgConformsTo` (impl-declaration +/// conformance for protocol-pack `..xs: P` elements), +/// - registration: `registerProtocolDecl` (protocol struct + method table + +/// vtable type), `registerImplBlock` / `registerParamImpl` (populate the +/// impl maps + the `0410`/`0411`/`0412` visibility/duplicate diagnostics), +/// and the default-method synthesis they use. /// /// A `*Lowering` facade (Principle 5, like `GenericResolver` / `CallResolver`): -/// these read the protocol/impl registries (`protocol_decl_map` / +/// it reads/writes the protocol/impl registries (`protocol_decl_map` / /// `protocol_ast_map` in `ProgramIndex`; `protocol_thunk_map` / `param_impl_map` -/// on `Lowering`) plus the type table, so it borrows `*Lowering` rather than -/// re-threading every map. Registration (`register*`) and IR emission -/// (`createProtocolThunk` / `buildProtocolValue` / `tryUserConversion`) stay in -/// `Lowering` for the later A4.2 increments — this step moves only pure lookup. +/// / `param_impl_pack_map` / `protocol_vtable_type_map` on `Lowering`) plus the +/// type table, so it borrows `*Lowering` rather than re-threading every map. +/// IR EMISSION stays in `Lowering` for the later A4.2 increment — registration +/// calls `self.l.declareFunction` (the emission primitive) but the thunk/value +/// builders (`createProtocolThunk` / `buildProtocolValue` / `tryUserConversion`) +/// are NOT moved here. pub const ProtocolResolver = struct { l: *Lowering, @@ -82,4 +92,349 @@ pub const ProtocolResolver = struct { } return true; } + + // ── Registration ──────────────────────────────────────────────────── + + pub fn registerProtocolDecl(self: ProtocolResolver, pd: *const ast.ProtocolDecl) void { + // Decision 4 soft-convention warning: a type-arg and a method (the + // "runtime accessor" namespace — protocols have no fields) sharing a + // name is allowed, but `..pack.` then resolves by *position* + // rather than by precedence, which surprises readers. Alert at decl. + for (pd.type_params) |tp| { + for (pd.methods) |m| { + if (std.mem.eql(u8, tp.name, m.name)) { + if (self.l.diagnostics) |diags| { + diags.addFmt(.warn, null, "protocol '{s}' declares type-arg and method both named '{s}'; `..pack.{s}` resolves by position (type-arg in type position, method in value position)", .{ pd.name, tp.name, tp.name }); + } + } + } + } + + // Parameterised protocols are compile-time-only — no vtable, no boxed + // instance struct. Methods reference unbound type params (e.g. + // `convert :: () -> Target`) that only get a concrete TypeId per + // (Source, Target) pair at xx resolution time. Stash the AST so + // `param_impl_map` lookup can resolve method signatures lazily. + if (pd.type_params.len > 0) { + self.l.program_index.protocol_ast_map.put(pd.name, pd) catch {}; + return; + } + + const table = &self.l.module.types; + const name_id = table.internString(pd.name); + + var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; + + // First field: ctx: *void + const void_ptr_ty = table.ptrTo(.void); + fields.append(self.l.alloc, .{ + .name = table.internString("ctx"), + .ty = void_ptr_ty, + }) catch unreachable; + + if (pd.is_inline) { + // One fn-ptr field per protocol method + for (pd.methods) |method| { + fields.append(self.l.alloc, .{ + .name = table.internString(method.name), + .ty = void_ptr_ty, // fn ptrs are opaque pointers + }) catch unreachable; + } + } else { + // Vtable pointer + fields.append(self.l.alloc, .{ + .name = table.internString("__vtable"), + .ty = void_ptr_ty, + }) catch unreachable; + } + + const struct_info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items, .is_protocol = true } }; + const id = if (table.findByName(name_id)) |existing| existing else table.intern(struct_info); + table.update(id, struct_info); + + // Build protocol method info for dispatch + var method_infos = std.ArrayList(ProtocolMethodInfo).empty; + for (pd.methods) |method| { + var ptypes = std.ArrayList(TypeId).empty; + for (method.params) |p| { + // Self → *void for protocol context; everything else + // goes through `resolveAstType`, threaded with the canonical + // alias map (`ProgramIndex.type_alias_map`). + const pty = blk: { + if (p.data == .type_expr and std.mem.eql(u8, p.data.type_expr.name, "Self")) { + break :blk void_ptr_ty; + } + break :blk type_bridge.resolveAstType(p, table, &self.l.program_index.type_alias_map); + }; + ptypes.append(self.l.alloc, pty) catch unreachable; + } + var ret_is_self = false; + const ret = if (method.return_type) |rt| blk: { + if (rt.data == .type_expr and std.mem.eql(u8, rt.data.type_expr.name, "Self")) { + ret_is_self = true; + break :blk void_ptr_ty; + } + break :blk type_bridge.resolveAstType(rt, table, &self.l.program_index.type_alias_map); + } else .void; + method_infos.append(self.l.alloc, .{ + .name = method.name, + .param_types = self.l.alloc.dupe(TypeId, ptypes.items) catch unreachable, + .ret_type = ret, + .ret_is_self = ret_is_self, + }) catch unreachable; + } + self.l.program_index.protocol_decl_map.put(pd.name, .{ + .name = pd.name, + .is_inline = pd.is_inline, + .methods = self.l.alloc.dupe(ProtocolMethodInfo, method_infos.items) catch unreachable, + }) catch {}; + self.l.program_index.protocol_ast_map.put(pd.name, pd) catch {}; + + // For vtable protocols, create the vtable struct type + if (!pd.is_inline) { + var vtable_fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; + for (pd.methods) |method| { + vtable_fields.append(self.l.alloc, .{ + .name = table.internString(method.name), + .ty = void_ptr_ty, + }) catch unreachable; + } + var vtable_name_buf: [128]u8 = undefined; + const vtable_name = std.fmt.bufPrint(&vtable_name_buf, "__{s}__Vtable", .{pd.name}) catch "__Vtable"; + const vtable_name_id = table.internString(vtable_name); + const vtable_info: types.TypeInfo = .{ .@"struct" = .{ .name = vtable_name_id, .fields = vtable_fields.items } }; + const vtable_ty = table.intern(vtable_info); + self.l.protocol_vtable_type_map.put(pd.name, vtable_ty) catch {}; + } + } + + pub fn registerImplBlock(self: ProtocolResolver, ib: *const ast.ImplBlock, is_imported: bool, decl: *const Node) void { + // Parameterised-protocol impl (e.g. `impl Into(Block) for Closure() -> void`): + // record into `param_impl_map` for compile-time resolution by `lowerXX`. + // Methods are NOT registered in fn_ast_map — they're monomorphised lazily + // per (Source, Target) pair at the xx call site. + if (ib.protocol_type_args.len > 0) { + self.registerParamImpl(ib, decl, is_imported); + return; + } + // Collect explicitly implemented method names + var impl_methods = std.StringHashMap(void).init(self.l.alloc); + defer impl_methods.deinit(); + for (ib.methods) |method_node| { + if (method_node.data == .fn_decl) { + const method_fd = &method_node.data.fn_decl; + const qualified = std.fmt.allocPrint(self.l.alloc, "{s}.{s}", .{ ib.target_type, method_fd.name }) catch continue; + self.l.program_index.fn_ast_map.put(qualified, method_fd) catch {}; + self.l.program_index.import_flags.put(qualified, is_imported) catch {}; + self.l.declareFunction(method_fd, qualified); + impl_methods.put(method_fd.name, {}) catch {}; + } + } + // Synthesize default methods from protocol declaration + if (self.l.program_index.protocol_ast_map.get(ib.protocol_name)) |pd| { + for (pd.methods) |method| { + if (method.default_body != null and !impl_methods.contains(method.name)) { + // Create a synthesized fn_decl for the default method + const synth_fd = self.synthesizeDefaultMethod(method, ib.target_type); + const qualified = std.fmt.allocPrint(self.l.alloc, "{s}.{s}", .{ ib.target_type, method.name }) catch continue; + self.l.program_index.fn_ast_map.put(qualified, synth_fd) catch {}; + self.l.program_index.import_flags.put(qualified, is_imported) catch {}; + self.l.declareFunction(synth_fd, qualified); + } + } + } + } + + /// Register a parameterised-protocol impl into `param_impl_map`. + /// Resolves the protocol's type args + the source type, mangles them, and + /// stashes the impl's method fn_decls for later monomorphisation by + /// `lowerXX`. Same-module duplicate impls produce a diagnostic here; + /// cross-module duplicates are detected at the xx resolution site. + /// + /// Pack-shaped sources (`Closure(..$args) -> $R`, detected via + /// `pack_start != null`) are additionally registered into + /// `param_impl_pack_map` keyed without the source suffix — the matching + /// site walks that map to bind packs against any concrete closure shape. + pub fn registerParamImpl(self: ProtocolResolver, ib: *const ast.ImplBlock, decl: *const Node, is_imported: bool) void { + const table = &self.l.module.types; + + // Resolve the protocol's type-arg list to concrete TypeIds. + var arg_tys = std.ArrayList(TypeId).empty; + for (ib.protocol_type_args) |arg_node| { + const t = type_bridge.resolveAstType(arg_node, table, &self.l.program_index.type_alias_map); + arg_tys.append(self.l.alloc, t) catch return; + } + + // Resolve the source type. Parser stores it on `target_type_expr` for + // parameterised impls (back-compat `target_type` string is kept for + // simple cases but the canonical form is the TypeExpr). + const src_ty: TypeId = if (ib.target_type_expr) |te| + type_bridge.resolveAstType(te, table, &self.l.program_index.type_alias_map) + else if (ib.target_type.len > 0) + type_bridge.resolveAstType(&.{ .span = decl.span, .data = .{ .type_expr = .{ .name = ib.target_type } } }, table, &self.l.program_index.type_alias_map) + else + return; + + // Mangle into the lookup key. + var key_buf = std.ArrayList(u8).empty; + key_buf.appendSlice(self.l.alloc, ib.protocol_name) catch return; + for (arg_tys.items) |t| { + key_buf.append(self.l.alloc, 0) catch return; + key_buf.appendSlice(self.l.alloc, self.l.mangleTypeName(t)) catch return; + } + const pack_key_len = key_buf.items.len; // proto + args, no src — used for pack map + key_buf.append(self.l.alloc, 0) catch return; + key_buf.appendSlice(self.l.alloc, self.l.mangleTypeName(src_ty)) catch return; + const key = key_buf.items; + + // Collect method fn_decl pointers. + var methods = std.ArrayList(*const ast.FnDecl).empty; + for (ib.methods) |method_node| { + if (method_node.data == .fn_decl) { + methods.append(self.l.alloc, &method_node.data.fn_decl) catch {}; + } + } + + const defining_module: []const u8 = self.l.current_source_file orelse ""; + const entry: Lowering.ParamImplEntry = .{ + .methods = self.l.alloc.dupe(*const ast.FnDecl, methods.items) catch return, + .source_ty = src_ty, + .target_args = self.l.alloc.dupe(TypeId, arg_tys.items) catch return, + .defining_module = defining_module, + .span = decl.span, + }; + + const gop = self.l.param_impl_map.getOrPut(key) catch return; + if (!gop.found_existing) { + gop.value_ptr.* = std.ArrayList(Lowering.ParamImplEntry).empty; + } else { + // Same-file duplicate is an immediate error. Cross-file overlaps + // are deferred to the xx resolution site (Phase 5) so the impl + // surface can be richer than any one file's view. + for (gop.value_ptr.items) |existing| { + if (std.mem.eql(u8, existing.defining_module, defining_module)) { + if (self.l.diagnostics) |diags| { + diags.addFmt(.err, decl.span, "duplicate impl '{s}' for source '{s}' in {s}", .{ + ib.protocol_name, self.l.mangleTypeName(src_ty), defining_module, + }); + } + return; + } + } + } + gop.value_ptr.append(self.l.alloc, entry) catch return; + + // Concrete-struct source: also register the impl's methods as + // `.` in fn_ast_map so UFCS resolves them (e.g. + // `xs[i].get()` on a pack element). For a concrete impl like + // `impl Box(s64) for IntCell`, the method is already fully concrete — + // nothing to monomorphize, unlike generic/pack sources (which stay + // lazy in param_impl_map and are handled below). + { + const si = table.get(src_ty); + if (!src_ty.isBuiltin() and si == .@"struct") { + const src_name = self.l.formatTypeName(src_ty); + // A generic-struct source (`impl VL($R) for Combined($R, ..$Ts)`) + // registers each method as a TEMPLATE only: its signature + // references unbound type params (`-> $R`), so declaring it as a + // standalone function would emit garbage (an unresolved return + // type). Concrete instances are monomorphized per-erasure by + // createProtocolThunk via this same fn_ast_map entry. + const is_generic_src = self.l.program_index.struct_template_map.contains(src_name); + for (methods.items) |mfd| { + const q = std.fmt.allocPrint(self.l.alloc, "{s}.{s}", .{ src_name, mfd.name }) catch continue; + if (self.l.program_index.fn_ast_map.contains(q)) continue; // first impl wins + self.l.program_index.fn_ast_map.put(q, mfd) catch {}; + self.l.program_index.import_flags.put(q, is_imported) catch {}; + if (!is_generic_src) self.l.declareFunction(mfd, q); + } + } + } + + // Pack-shaped source: also register in the pack map. The source + // closure carries `pack_start` set; matching binds the source's + // tail param types to the pack-name and the source's return to + // the impl's return-type-var (when the return is generic). + const src_info = table.get(src_ty); + if (src_info == .closure and src_info.closure.pack_start != null) { + const target_expr_node = ib.target_type_expr orelse return; + if (target_expr_node.data != .closure_type_expr) return; + const ct = target_expr_node.data.closure_type_expr; + const pack_var = ct.pack_name orelse return; + // Extract the return-type-var name if the impl's return is generic. + // `Closure(...) -> $R` parses with the return-type node carrying + // `is_generic = true`. Concrete returns leave it null. + var ret_var: ?[]const u8 = null; + if (ct.return_type) |rt| { + if (rt.data == .type_expr and rt.data.type_expr.is_generic) { + ret_var = rt.data.type_expr.name; + } + } + const pack_entry: Lowering.PackParamImplEntry = .{ + .methods = self.l.alloc.dupe(*const ast.FnDecl, methods.items) catch return, + .source_pack_ty = src_ty, + .target_args = self.l.alloc.dupe(TypeId, arg_tys.items) catch return, + .defining_module = defining_module, + .span = decl.span, + .pack_var_name = self.l.alloc.dupe(u8, pack_var) catch return, + .ret_var_name = if (ret_var) |rv| (self.l.alloc.dupe(u8, rv) catch return) else null, + }; + const pack_key = key_buf.items[0..pack_key_len]; + const pack_key_owned = self.l.alloc.dupe(u8, pack_key) catch return; + const pgop = self.l.param_impl_pack_map.getOrPut(pack_key_owned) catch return; + if (!pgop.found_existing) { + pgop.value_ptr.* = std.ArrayList(Lowering.PackParamImplEntry).empty; + } else { + for (pgop.value_ptr.items) |existing| { + if (std.mem.eql(u8, existing.defining_module, defining_module)) { + if (self.l.diagnostics) |diags| { + diags.addFmt(.err, decl.span, "duplicate pack impl '{s}' for source '{s}' in {s}", .{ + ib.protocol_name, self.l.mangleTypeName(src_ty), defining_module, + }); + } + return; + } + } + } + pgop.value_ptr.append(self.l.alloc, pack_entry) catch return; + } + } + + /// Synthesize a fn_decl from a protocol default method for a concrete type. + fn synthesizeDefaultMethod(self: ProtocolResolver, method: ast.ProtocolMethodDecl, target_type: []const u8) *const ast.FnDecl { + // Build parameter list: self: *TargetType, then the protocol method params + var params_list = std.ArrayList(ast.Param).empty; + defer params_list.deinit(self.l.alloc); + + // Add self parameter: self: *TargetType + const self_type_node = self.l.alloc.create(ast.Node) catch unreachable; + const pointee_node = self.l.alloc.create(ast.Node) catch unreachable; + pointee_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = target_type } } }; + self_type_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .pointer_type_expr = .{ + .pointee_type = pointee_node, + } } }; + params_list.append(self.l.alloc, .{ + .name = "self", + .name_span = .{ .start = 0, .end = 0 }, + .type_expr = self_type_node, + }) catch unreachable; + + // Add remaining params from the protocol method + for (method.params, method.param_names) |pty, pname| { + params_list.append(self.l.alloc, .{ + .name = pname, + .name_span = .{ .start = 0, .end = 0 }, + .type_expr = pty, + }) catch unreachable; + } + + const fd = self.l.alloc.create(ast.FnDecl) catch unreachable; + fd.* = .{ + .name = method.name, + .params = self.l.alloc.dupe(ast.Param, params_list.items) catch unreachable, + .body = method.default_body.?, + .return_type = method.return_type, + }; + return fd; + } };