diff --git a/examples/96-objc-block-multi-arg.sx b/examples/96-objc-block-multi-arg.sx index bcce063..335c9ea 100644 --- a/examples/96-objc-block-multi-arg.sx +++ b/examples/96-objc-block-multi-arg.sx @@ -1,20 +1,44 @@ -// M5.A — `xx closure : Block` for an arbitrary closure signature. +// `xx closure : Block` for an arbitrary closure signature. // -// Pre-M5.A: the stdlib hand-rolled `Into(Block) for Closure(s32, *void) -> s32` -// didn't exist — this code wouldn't compile. Only `Closure() -> void` -// and `Closure(bool) -> void` shapes were supported. +// The stdlib (modules/std/objc_block.sx) declares hand-rolled +// `Into(Block) for Closure() -> void` and `Closure(bool) -> void` +// impls — the two most common Apple block shapes. Other signatures +// need a per-shape `__block_invoke_` trampoline + `Into(Block)` +// impl declared somewhere reachable (stdlib if shared, in-file if +// app-specific). // -// Post-M5.A: the compiler synthesises `__block_invoke_i_i_p` for this -// signature on the fly. The block's invoke trampoline forwards -// `(__sx_default_context, sx_env, arg0, arg1)` to the captured closure -// and returns the s32 result. +// This test exercises the user-declared variant: signature +// `Closure(s32, *void) -> void` (a two-arg block — not in stdlib). +// If the impl is missing, the compiler emits a focused diagnostic +// pointing at modules/std/objc_block.sx as the template. #import "modules/std.sx"; #import "modules/std/objc_block.sx"; +// Trampoline matching `void (^)(int, void*)` — the C ABI Apple's +// runtime calls. Forwards through to the sx closure with the +// standard `(__sx_ctx, env, ...args)` shape. +__block_invoke_void_s32_p :: (block_self: *Block, arg0: s32, arg1: *void) callconv(.c) { + typed_fn : (*void, s32, *void) -> void = xx block_self.sx_fn; + typed_fn(block_self.sx_env, arg0, arg1); +} + +impl Into(Block) for Closure(s32, *void) -> void { + convert :: (self: Closure(s32, *void) -> void) -> Block { + .{ + isa = @_NSConcreteStackBlock, + flags = 0, + reserved = 0, + invoke = xx @__block_invoke_void_s32_p, + descriptor = xx @__sx_block_descriptor, + sx_env = self.env, + sx_fn = self.fn_ptr, + }; + } +} + // Side-effect capture so we can observe both args reached the -// closure body, even though void-returning trampolines are the -// well-tested shape. +// closure body. g_sum: s32 = 0; g_tag: *void = null; diff --git a/library/modules/std/objc_block.sx b/library/modules/std/objc_block.sx index b9e5c78..09d73cb 100644 --- a/library/modules/std/objc_block.sx +++ b/library/modules/std/objc_block.sx @@ -52,17 +52,58 @@ __sx_block_descriptor : BlockDescriptor = .{ size = 48, }; -// M5.A — `xx closure : Block` casts are handled by the compiler. -// For every closure signature seen at a cast site, the compiler -// synthesises: -// 1. A C-ABI trampoline `__block_invoke_` matching Apple's -// `__block_literal.invoke` calling convention. -// 2. Inline Block-struct construction at the cast site, with -// `invoke` pointing at the synthesised trampoline and -// `sx_env`/`sx_fn` taken from the closure value. +// Per-signature invoke trampolines. Each one reads sx_env + sx_fn from +// its block_self argument and tail-calls the closure through a typed +// fn-ptr cast. One per Apple block signature we support. // -// User-facing surface: `xx my_closure : Block` for ANY closure -// signature. No per-signature stdlib boilerplate. The shared -// infrastructure above (`Block`, `BlockDescriptor`, -// `_NSConcreteStackBlock`, `__sx_block_descriptor`) is referenced -// by the synthesised code; users only need to `#import` this module. +// Adding a new signature: write a `__block_invoke_` trampoline +// matching the closure's calling convention and an +// `impl Into(Block) for Closure()` that points its `invoke` +// field at the trampoline. The `xx closure : Block` cast finds the +// impl via `Into` protocol dispatch. +// +// Signature: `void (^)(void)` — no args, no return. The single most +// common Apple block shape (UIView animation bodies, dispatch_async, etc). +__block_invoke_void :: (block_self: *Block) callconv(.c) { + // `sx_fn` is the closure trampoline — an sx-side function with the + // implicit __sx_ctx at slot 0 and env at slot 1. We're a callconv(.c) + // entry, so the call site needs ctx prepended; the typed fn-pointer + // type stays default-conv to enable that. + typed_fn : (*void) -> void = xx block_self.sx_fn; + typed_fn(block_self.sx_env); +} + +impl Into(Block) for Closure() -> void { + convert :: (self: Closure() -> void) -> Block { + .{ + isa = @_NSConcreteStackBlock, + flags = 0, + reserved = 0, + invoke = xx @__block_invoke_void, + descriptor = xx @__sx_block_descriptor, + sx_env = self.env, + sx_fn = self.fn_ptr, + }; + } +} + +// Signature: `void (^)(BOOL)` — UIView animation completion handlers and +// similar one-arg-bool callbacks. +__block_invoke_bool :: (block_self: *Block, arg0: bool) callconv(.c) { + typed_fn : (*void, bool) -> void = xx block_self.sx_fn; + typed_fn(block_self.sx_env, arg0); +} + +impl Into(Block) for Closure(bool) -> void { + convert :: (self: Closure(bool) -> void) -> Block { + .{ + isa = @_NSConcreteStackBlock, + flags = 0, + reserved = 0, + invoke = xx @__block_invoke_bool, + descriptor = xx @__sx_block_descriptor, + sx_env = self.env, + sx_fn = self.fn_ptr, + }; + } +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 0df6a1c..c1a133b 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -145,7 +145,6 @@ 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, @@ -11053,246 +11052,21 @@ 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; + /// Detect the `xx closure : Block` cast pattern so `tryUserConversion` + /// can emit a focused diagnostic when no `Into(Block) for Closure(...)` + /// impl is reachable. Replaces what was briefly a compiler-synthesised + /// trampoline path with a "declare an impl" requirement — the stdlib + /// covers common signatures (see modules/std/objc_block.sx), users + /// add their own for unusual ones. + fn isClosureToBlockCast(self: *Lowering, src_ty: TypeId, dst_ty: TypeId) bool { + if (src_ty.isBuiltin()) return false; 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; + if (src_info != .closure) return false; + if (dst_ty.isBuiltin()) return false; 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', - }; + if (dst_info != .@"struct") return false; + const block_name = self.module.types.internString("Block"); + return dst_info.@"struct".name == block_name; } /// Look up `Into(dst_ty)` impl for `src_ty` and, if found, monomorphise @@ -11300,14 +11074,6 @@ pub const Lowering = struct { /// 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)) { @@ -11335,8 +11101,36 @@ 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 return null; - if (entries.items.len == 0) return null; + 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; + } + return null; + }; + if (entries.items.len == 0) { + 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; + } + return null; + } // Filter by import visibility: only impls in modules that the current // file transitively imports (or the current file itself) are reachable.