diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 85a227e..0df6a1c 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -145,6 +145,7 @@ pub const Lowering = struct { comptime_constants: std.StringHashMap(ComptimeValue) = std.StringHashMap(ComptimeValue).init(std.heap.page_allocator), // compile-time known constants (e.g. OS, ARCH) diagnostics: ?*errors.DiagnosticList = null, // error reporting with source locations xx_reentrancy: std.AutoHashMap(u64, void) = std.AutoHashMap(u64, void).init(std.heap.page_allocator), // (src_ty, dst_ty) pairs currently being resolved through user-space Into; prevents infinite monomorphisation when a convert body re-enters the same xx + block_invoke_trampolines: std.StringHashMap(FuncId) = std.StringHashMap(FuncId).init(std.heap.page_allocator), // M5.A — dedup compiler-synthesised `__block_invoke_` trampolines per mangled closure signature; one entry per Closure(...) type seen at an `xx cl : Block` cast site pub const ComptimeValue = union(enum) { int_val: i64, @@ -11052,11 +11053,261 @@ pub const Lowering = struct { return result; } + /// M5.A — try the closure→Block bridge before the user-space Into + /// fallback. If `src_ty` is `Closure(...)` and `dst_ty` is `Block`, + /// synthesise a `__block_invoke_` trampoline (deduped per + /// signature) and emit IR constructing the Block struct inline. + /// Returns the Block value, or null if the cast doesn't match this + /// pattern (caller falls back to user-space Into). + fn tryClosureToBlockConversion(self: *Lowering, operand: Ref, src_ty: TypeId, dst_ty: TypeId) ?Ref { + // Source must be Closure(...). + if (src_ty.isBuiltin()) return null; + const src_info = self.module.types.get(src_ty); + if (src_info != .closure) return null; + // Destination must be the `Block` struct (declared in std/objc_block.sx). + if (dst_ty.isBuiltin()) return null; + const dst_info = self.module.types.get(dst_ty); + if (dst_info != .@"struct") return null; + const block_name_id = self.module.types.internString("Block"); + if (dst_info.@"struct".name != block_name_id) return null; + + const ptr_void = self.module.types.ptrTo(.void); + + // Emit (or look up) the trampoline for this signature. + const trampoline_fid = self.emitBlockInvokeTrampoline(src_ty) orelse return null; + + // Look up the shared block infrastructure declared in std/objc_block.sx. + const isa_gid = self.global_names.get("_NSConcreteStackBlock") orelse { + if (self.diagnostics) |d| { + d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "xx Closure : Block requires `#import \"modules/std/objc_block.sx\";` (missing `_NSConcreteStackBlock` extern)", .{}); + } + return null; + }; + const descriptor_gid = self.global_names.get("__sx_block_descriptor") orelse { + if (self.diagnostics) |d| { + d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "xx Closure : Block requires `#import \"modules/std/objc_block.sx\";` (missing `__sx_block_descriptor` global)", .{}); + } + return null; + }; + + // Build the Block struct fields. + const isa_addr = self.builder.emit(.{ .global_addr = isa_gid.id }, ptr_void); + const descriptor_addr = self.builder.emit(.{ .global_addr = descriptor_gid.id }, ptr_void); + const invoke_addr = self.builder.emit(.{ .func_ref = trampoline_fid }, ptr_void); + + // Extract the closure's fn_ptr (field 0) and env (field 1). + const closure_fn_ptr = self.builder.structGet(operand, 0, ptr_void); + const closure_env = self.builder.structGet(operand, 1, ptr_void); + + // Construct Block { isa, flags=0, reserved=0, invoke, descriptor, sx_env, sx_fn }. + var fields = std.ArrayList(Ref).empty; + defer fields.deinit(self.alloc); + fields.append(self.alloc, isa_addr) catch return null; + fields.append(self.alloc, self.builder.constInt(0, .s32)) catch return null; // flags + fields.append(self.alloc, self.builder.constInt(0, .s32)) catch return null; // reserved + fields.append(self.alloc, invoke_addr) catch return null; + fields.append(self.alloc, descriptor_addr) catch return null; + fields.append(self.alloc, closure_env) catch return null; // sx_env + fields.append(self.alloc, closure_fn_ptr) catch return null; // sx_fn + + const owned_fields = self.alloc.dupe(Ref, fields.items) catch return null; + return self.builder.emit(.{ .struct_init = .{ .fields = owned_fields } }, dst_ty); + } + + /// Synthesise (and cache) the C-ABI trampoline that bridges Apple's + /// `__block_literal.invoke` calling convention to the sx closure + /// stored in `block_self.sx_fn` + `block_self.sx_env`. One per + /// unique closure signature. + /// + /// Signature for a `Closure(A, B) -> R`: + /// `__block_invoke_(block_self: *Block, a: A, b: B) -> R callconv(.c)` + /// Body: + /// sx_fn = block_self.sx_fn + /// sx_env = block_self.sx_env + /// ret sx_fn(__sx_default_context, sx_env, a, b) // matches sx closure ABI + fn emitBlockInvokeTrampoline(self: *Lowering, closure_ty: TypeId) ?FuncId { + const closure_info = self.module.types.get(closure_ty); + if (closure_info != .closure) return null; + const cinfo = closure_info.closure; + + const mangled = self.mangleClosureSigForBlock(cinfo) orelse return null; + defer self.alloc.free(mangled); + + if (self.block_invoke_trampolines.get(mangled)) |fid| return fid; + + const ptr_void = self.module.types.ptrTo(.void); + + // The trampoline name owned by the module's intern pool. + const tramp_name = std.fmt.allocPrint(self.alloc, "__block_invoke_{s}", .{mangled}) catch return null; + const tramp_name_id = self.module.types.internString(tramp_name); + // Hold the mangled string across the cache map's lifetime. + const mangled_owned = self.alloc.dupe(u8, mangled) catch return null; + + const block_ty_name = self.module.types.internString("Block"); + const block_ty = self.module.types.findByName(block_ty_name) orelse { + if (self.diagnostics) |d| { + d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitBlockInvokeTrampoline: `Block` struct not in module — `#import \"modules/std/objc_block.sx\";` required", .{}); + } + self.alloc.free(mangled_owned); + return null; + }; + const block_ptr_ty = self.module.types.ptrTo(block_ty); + + // Save+restore builder state — emitting a new function mid-pass. + const saved_func = self.builder.func; + const saved_block = self.builder.current_block; + const saved_counter = self.builder.inst_counter; + defer { + self.builder.func = saved_func; + self.builder.current_block = saved_block; + self.builder.inst_counter = saved_counter; + } + + // Build the trampoline's param list: (block_self: *Block, user_args...). + var params = std.ArrayList(inst_mod.Function.Param).empty; + params.append(self.alloc, .{ + .name = self.module.types.internString("block_self"), + .ty = block_ptr_ty, + }) catch { + self.alloc.free(mangled_owned); + return null; + }; + for (cinfo.params, 0..) |pty, i| { + var nbuf: [32]u8 = undefined; + const pname = std.fmt.bufPrint(&nbuf, "arg{d}", .{i}) catch "arg"; + params.append(self.alloc, .{ + .name = self.module.types.internString(pname), + .ty = pty, + }) catch { + self.alloc.free(mangled_owned); + return null; + }; + } + const params_slice = params.toOwnedSlice(self.alloc) catch { + self.alloc.free(mangled_owned); + return null; + }; + + const fid = self.builder.beginFunction(tramp_name_id, params_slice, cinfo.ret); + const func = self.builder.currentFunc(); + func.linkage = .external; + func.call_conv = .c; + func.has_implicit_ctx = false; + + const entry_name = self.module.types.internString("entry"); + const entry = self.builder.appendBlock(entry_name, &.{}); + self.builder.switchToBlock(entry); + + // Load block_self struct, extract sx_env (field 5) + sx_fn (field 6). + const block_self_ref = Ref.fromIndex(0); + const block_val = self.builder.load(block_self_ref, block_ty); + const sx_env = self.builder.structGet(block_val, 5, ptr_void); + const sx_fn = self.builder.structGet(block_val, 6, ptr_void); + + // Call sx_fn(__sx_default_context, sx_env, user_args...) — matches + // the sx closure ABI (ctx prepended to env + user args). + const default_ctx_gi = self.global_names.get("__sx_default_context") orelse { + if (self.diagnostics) |d| { + d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitBlockInvokeTrampoline: __sx_default_context not in module (compiler bug)", .{}); + } + self.alloc.free(mangled_owned); + return null; + }; + const default_ctx_addr = self.builder.emit(.{ .global_addr = default_ctx_gi.id }, ptr_void); + + const num_user_args = cinfo.params.len; + const total_args = 2 + num_user_args; // ctx + env + user + const call_args = self.alloc.alloc(Ref, total_args) catch { + self.alloc.free(mangled_owned); + return null; + }; + call_args[0] = default_ctx_addr; + call_args[1] = sx_env; + var i: usize = 0; + while (i < num_user_args) : (i += 1) { + // Params start at slot 1 (block_self is slot 0); user args + // are slots 1..num_user_args. + call_args[2 + i] = Ref.fromIndex(@intCast(1 + i)); + } + + const result = self.builder.emit(.{ .call_indirect = .{ + .callee = sx_fn, + .args = call_args, + } }, cinfo.ret); + + if (cinfo.ret == .void) { + self.builder.retVoid(); + } else { + self.builder.ret(result, cinfo.ret); + } + self.builder.finalize(); + + self.block_invoke_trampolines.put(mangled_owned, fid) catch { + self.alloc.free(mangled_owned); + return null; + }; + return fid; + } + + /// Compact mangling for a closure signature's `__block_invoke_` + /// trampoline name. Keys: `v` void, `b` bool, `i` s32, `q` s64, `f` + /// f32, `d` f64, `p` pointer/aggregate (everything that lowers to a + /// machine word). Multi-arg signatures are underscore-joined; return + /// comes first. + fn mangleClosureSigForBlock(self: *Lowering, cinfo: types.TypeInfo.ClosureInfo) ?[]u8 { + var buf = std.ArrayList(u8).empty; + defer buf.deinit(self.alloc); + buf.append(self.alloc, mangleTypeForBlock(cinfo.ret, &self.module.types)) catch return null; + for (cinfo.params) |pty| { + buf.append(self.alloc, '_') catch return null; + buf.append(self.alloc, mangleTypeForBlock(pty, &self.module.types)) catch return null; + } + return self.alloc.dupe(u8, buf.items) catch null; + } + + fn mangleTypeForBlock(ty: TypeId, tbl: *const types.TypeTable) u8 { + if (ty == .void) return 'v'; + if (ty == .bool) return 'b'; + if (ty == .f32) return 'f'; + if (ty == .f64) return 'd'; + const info = tbl.get(ty); + return switch (info) { + .signed => |w| switch (w) { + 1, 8 => 'c', + 16 => 's', + 32 => 'i', + 64 => 'q', + else => 'i', + }, + .unsigned => |w| switch (w) { + 1, 8 => 'C', + 16 => 'S', + 32 => 'I', + 64 => 'Q', + else => 'I', + }, + // Everything else lowers to a machine word at the C ABI. + // 'p' covers pointers, many-pointers, struct ptrs, closures, + // and small structs that pass in a register. Aggregates + // larger than the register cutoff would need a different + // calling convention — out of M5.A scope. + else => 'p', + }; + } + /// 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 /// the unchanged operand — Phase 3 emits no diagnostic for v0). fn tryUserConversion(self: *Lowering, operand: Ref, operand_node: *const Node, src_ty: TypeId, dst_ty: TypeId) ?Ref { + // M5.A: compiler-synthesised closure→Block bridge. Runs BEFORE + // the user-space Into lookup so the synthesised trampoline takes + // over from the hand-rolled stdlib impls. Falls through (returns + // null in caller chain) when src isn't Closure or dst isn't Block. + if (self.tryClosureToBlockConversion(operand, src_ty, dst_ty)) |converted| { + return converted; + } + // Reentrancy guard — pack (src, dst) into a u64. const guard_key: u64 = (@as(u64, src_ty.index()) << 32) | @as(u64, dst_ty.index()); if (self.xx_reentrancy.contains(guard_key)) {