diff --git a/src/ir/lower.zig b/src/ir/lower.zig index c1a133b..854af42 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -136,6 +136,16 @@ pub const Lowering = struct { protocol_vtable_type_map: std.StringHashMap(TypeId) = std.StringHashMap(TypeId).init(std.heap.page_allocator), // protocol name → vtable struct TypeId protocol_vtable_global_map: std.StringHashMap(inst_mod.GlobalId) = std.StringHashMap(inst_mod.GlobalId).init(std.heap.page_allocator), // "Proto\x00Type" → vtable GlobalId param_impl_map: std.StringHashMap(std.ArrayList(ParamImplEntry)) = std.StringHashMap(std.ArrayList(ParamImplEntry)).init(std.heap.page_allocator), // "Proto\x00\x00" → impl entries (parameterised protocols only; list lets Phase 4/5 detect cross-module overlap) + /// Pack-variadic impl entries — separate map keyed by `"Proto\x00"` + /// (NO source suffix) so a single impl `Closure(..$args) -> $R` can be + /// matched against many concrete source shapes. Concrete impls in + /// `param_impl_map` win when both match (specificity rule). + param_impl_pack_map: std.StringHashMap(std.ArrayList(PackParamImplEntry)) = std.StringHashMap(std.ArrayList(PackParamImplEntry)).init(std.heap.page_allocator), + /// Active pack bindings during monomorphisation. Mirrors `type_bindings` + /// but for variadic pack names: `args → [T1, T2, ...]`. Read by + /// `resolveTypeWithBindings` on closure_type_expr to substitute + /// `Closure(..$args) -> $R` into a concrete closure type. + pack_bindings: ?std.StringHashMap([]const TypeId) = null, struct_const_map: std.StringHashMap(StructConstInfo) = std.StringHashMap(StructConstInfo).init(std.heap.page_allocator), // "Struct.CONST" → value info module_const_map: std.StringHashMap(ModuleConstInfo) = std.StringHashMap(ModuleConstInfo).init(std.heap.page_allocator), // module-level value constants (e.g. AF_INET :s32: 2) foreign_name_map: std.StringHashMap([]const u8) = std.StringHashMap([]const u8).init(std.heap.page_allocator), // sx name → C name for #foreign renames @@ -188,6 +198,22 @@ pub const Lowering = struct { span: ast.Span, }; + /// Pack-variadic impl entry — `impl Proto(Args...) for Closure(Prefix..., ..$pack) -> $ret`. + /// Matches any concrete closure source whose first `prefix_len` param types + /// equal `source_pack_ty`'s fixed prefix; the tail binds to `pack_var_name` + /// (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 { + methods: []const *const ast.FnDecl, + source_pack_ty: TypeId, + target_args: []const TypeId, + defining_module: []const u8, + span: ast.Span, + pack_var_name: []const u8, + ret_var_name: ?[]const u8, + }; + /// Owned copy of a generic struct template (AST pointers are copied/interned to survive imports) const StructTemplate = struct { name: []const u8, @@ -9043,6 +9069,9 @@ pub const Lowering = struct { // Handle List(T), Vector(N, T) etc. as type constructor calls return self.resolveTypeCallWithBindings(&cl); }, + .closure_type_expr => |ct| { + return self.resolveClosureTypeWithBindings(&ct); + }, else => {}, } } @@ -9077,6 +9106,9 @@ pub const Lowering = struct { const len: u32 = if (at.length.data == .int_literal) @intCast(at.length.data.int_literal.value) else 0; return self.module.types.arrayOf(elem, len); }, + .closure_type_expr => |ct| { + return self.resolveClosureTypeWithBindings(&ct); + }, else => {}, } // Check type aliases before falling through to type_bridge @@ -9086,6 +9118,36 @@ pub const Lowering = struct { return type_bridge.resolveAstType(node, &self.module.types); } + /// Resolve a `Closure(...)` type expression with the active type/pack + /// bindings applied. Pack-shaped closure exprs (`Closure(Prefix..., ..$pack)`) + /// substitute `pack` from `self.pack_bindings`, producing a concrete + /// closure type — used when monomorphising a pack-variadic impl body + /// against a concrete source signature. + fn resolveClosureTypeWithBindings(self: *Lowering, ct: *const ast.ClosureTypeExpr) TypeId { + var param_ids = std.ArrayList(TypeId).empty; + defer param_ids.deinit(self.alloc); + for (ct.param_types) |pt| { + param_ids.append(self.alloc, self.resolveTypeWithBindings(pt)) catch return .void; + } + if (ct.pack_name) |pn| { + if (self.pack_bindings) |pb| { + if (pb.get(pn)) |pack_tys| { + for (pack_tys) |t| param_ids.append(self.alloc, t) catch return .void; + // Fully bound — emit a concrete closure type, no pack_start. + const ret_ty = if (ct.return_type) |rt| self.resolveTypeWithBindings(rt) else .void; + return self.module.types.closureType(param_ids.items, ret_ty); + } + } + // Pack name in scope but no binding — preserve the pack-shape + // so downstream code can still see it's variadic. (Hit during + // impl-block parsing before any concrete monomorphisation.) + const ret_ty = if (ct.return_type) |rt| self.resolveTypeWithBindings(rt) else .void; + return self.module.types.closureTypePack(param_ids.items, ret_ty, @intCast(param_ids.items.len)); + } + const ret_ty = if (ct.return_type) |rt| self.resolveTypeWithBindings(rt) else .void; + return self.module.types.closureType(param_ids.items, ret_ty); + } + /// Resolve a .call node that represents a type constructor (e.g., List(T), Vector(N, T)). fn resolveTypeCallWithBindings(self: *Lowering, cl: *const ast.Call) TypeId { const callee_name: []const u8 = switch (cl.callee.data) { @@ -10050,6 +10112,11 @@ pub const Lowering = struct { /// 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) void { const table = &self.module.types; @@ -10077,6 +10144,7 @@ pub const Lowering = struct { 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; @@ -10117,6 +10185,54 @@ pub const Lowering = struct { } } gop.value_ptr.append(self.alloc, entry) catch return; + + // 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. @@ -11069,6 +11185,133 @@ pub const Lowering = struct { return dst_info.@"struct".name == block_name; } + /// Pack-variadic impl matching. Walks `param_impl_pack_map[pack_key]` + /// and returns a call ref when a single pack impl matches `src_ty`'s + /// shape (concrete src closure / fn with the same fixed prefix as + /// the impl's source pack closure). Binds the pack-var to the source's + /// tail param types and the return-var (when generic) to the source's + /// return type, then monomorphises the convert method. + /// Returns null if no pack impls registered for this (proto, dst) or + /// none of them match `src_ty`'s shape. + fn tryPackImplMatch( + self: *Lowering, + operand: Ref, + operand_node: *const Node, + src_ty: TypeId, + dst_ty: TypeId, + proto_name: []const u8, + pack_key: []const u8, + 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; + 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; + + // Build bindings. Target → dst_ty (already in the protocol's type + // params), pack-var → src tail TypeIds, ret-var (when generic) → + // src ret. + const ent_pack_start = table.get(entry.source_pack_ty).closure.pack_start.?; + const tail = src_params[ent_pack_start..]; + const tail_owned = self.alloc.dupe(TypeId, tail) catch return null; + + var bindings = std.StringHashMap(TypeId).init(self.alloc); + defer bindings.deinit(); + const pd = self.protocol_ast_map.get(proto_name) orelse return null; + bindings.put(pd.type_params[0].name, dst_ty) catch return null; + if (entry.ret_var_name) |rv| bindings.put(rv, src_ret) catch return null; + + var pack_bindings = std.StringHashMap([]const TypeId).init(self.alloc); + defer pack_bindings.deinit(); + pack_bindings.put(entry.pack_var_name, tail_owned) catch return null; + + // Mangled name keyed on the CONCRETE source so distinct shapes + // monomorphise separately. Same scheme as the concrete path: + // ".convert__". + const mangled = std.fmt.allocPrint(self.alloc, "{s}.convert__{s}", .{ + self.mangleTypeName(src_ty), self.mangleTypeName(dst_ty), + }) catch return null; + + self.xx_reentrancy.put(guard_key, {}) catch {}; + defer _ = self.xx_reentrancy.remove(guard_key); + + if (!self.lowered_functions.contains(mangled)) { + const saved_pack = self.pack_bindings; + self.pack_bindings = pack_bindings; + defer self.pack_bindings = saved_pack; + self.monomorphizeFunction(fd, mangled, &bindings); + } + + const fid = self.resolveFuncByName(mangled) orelse return null; + const func = &self.module.functions.items[@intFromEnum(fid)]; + const ret_ty = func.ret; + const params = func.params; + var single = [_]Ref{operand}; + const final_args = self.prependCtxIfNeeded(func, single[0..]); + self.coerceCallArgs(final_args, params); + return self.builder.call(fid, final_args, ret_ty); + } + /// Look up `Into(dst_ty)` impl for `src_ty` and, if found, monomorphise /// the impl's `convert` method and emit a direct call. Returns null when /// no impl matches (caller falls back to the built-in result, which is @@ -11101,25 +11344,18 @@ pub const Lowering = struct { key_buf.appendSlice(self.alloc, self.mangleTypeName(src_ty)) catch return null; const key = key_buf.items; - const entries = self.param_impl_map.get(key) orelse { - // M5.A — focused diagnostic for the closure→Block case. When - // a user writes `xx cl : Block` for a closure signature that - // has no `Into(Block) for Closure()` impl in stdlib or - // user code, the generic "no Into impl" path returns silently - // and the cast becomes a no-op. Emit a hint pointing at the - // missing impl pattern so they know what to add. - if (self.isClosureToBlockCast(src_ty, dst_ty)) { - if (self.diagnostics) |diags| { - const saved = diags.current_source_file; - diags.current_source_file = operand_node.source_file orelse self.current_source_file; - defer diags.current_source_file = saved; - diags.addFmt(.err, operand_node.span, "no `Into(Block) for {s}` impl — add a per-signature `__block_invoke_` trampoline + Into impl alongside the existing ones in modules/std/objc_block.sx, or declare it in your own code", .{self.mangleTypeName(src_ty)}); - } - return operand; + // Pack-only key (proto + dst) — used if the concrete lookup misses. + // Same prefix as the concrete key, minus the `\x00` tail. + const dst_mangled_len = self.mangleTypeName(dst_ty).len; + const pack_key = key_buf.items[0 .. proto_name.len + 1 + dst_mangled_len]; + + const entries_opt = self.param_impl_map.get(key); + const has_concrete = entries_opt != null and entries_opt.?.items.len > 0; + if (!has_concrete) { + // Concrete miss — try the pack map before emitting a diagnostic. + if (self.tryPackImplMatch(operand, operand_node, src_ty, dst_ty, proto_name, pack_key, guard_key)) |result| { + return result; } - return null; - }; - if (entries.items.len == 0) { if (self.isClosureToBlockCast(src_ty, dst_ty)) { if (self.diagnostics) |diags| { const saved = diags.current_source_file; @@ -11131,6 +11367,7 @@ pub const Lowering = struct { } return null; } + const entries = entries_opt.?; // Filter by import visibility: only impls in modules that the current // file transitively imports (or the current file itself) are reachable. diff --git a/tests/expected/155-pack-impl-match.exit b/tests/expected/155-pack-impl-match.exit index d00491f..573541a 100644 --- a/tests/expected/155-pack-impl-match.exit +++ b/tests/expected/155-pack-impl-match.exit @@ -1 +1 @@ -1 +0 diff --git a/tests/expected/155-pack-impl-match.txt b/tests/expected/155-pack-impl-match.txt index 47b309a..48d6f98 100644 --- a/tests/expected/155-pack-impl-match.txt +++ b/tests/expected/155-pack-impl-match.txt @@ -1 +1 @@ -/Users/agra/projects/sx/examples/155-pack-impl-match.sx:43:21: error: no `Into(Block) for cl_s32_bool__bool` impl — add a per-signature `__block_invoke_` trampoline + Into impl alongside the existing ones in modules/std/objc_block.sx, or declare it in your own code +pack impl match ok