From 137285f33dbda112646576b4d477b51fd6011534 Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 2 Jun 2026 22:23:01 +0300 Subject: [PATCH] refactor(ir): factor protocol/impl planning into ProtocolResolver (A4.2 planning increment) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/ir/lower.zig | 118 ++++------------------------------ src/ir/protocols.test.zig | 98 ++++++++++++++++++++++++++++ src/ir/protocols.zig | 132 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 242 insertions(+), 106 deletions(-) diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 1928ae30..fe0008a3 100644 --- a/src/ir/lower.zig +++ b/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| { diff --git a/src/ir/protocols.test.zig b/src/ir/protocols.test.zig index 3b26ed5d..9c511c15 100644 --- a/src/ir/protocols.test.zig +++ b/src/ir/protocols.test.zig @@ -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); +} diff --git a/src/ir/protocols.zig b/src/ir/protocols.zig index 5b914d9d..d85cc1d4 100644 --- a/src/ir/protocols.zig +++ b/src/ir/protocols.zig @@ -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 {