ffi M5.A: compiler-synthesised __block_invoke_<sig> trampolines
`xx closure : Block` casts now bypass the user-space Into(Block)
protocol path entirely. The compiler intercepts in
`tryUserConversion` BEFORE the Into lookup, detects when src is
`Closure(...)` and dst is `Block`, and emits:
1. A C-ABI trampoline `__block_invoke_<sig>` (deduped per closure
signature via `block_invoke_trampolines` map). Body matches the
existing hand-rolled `__block_invoke_void` exactly: load
block_self struct, extract sx_env (field 5) + sx_fn (field 6),
call sx_fn(__sx_default_context, sx_env, ...user_args), return.
2. Inline Block-struct construction at the cast site:
`Block { isa = &_NSConcreteStackBlock, flags=0, reserved=0,
invoke = &__block_invoke_<sig>,
descriptor = &__sx_block_descriptor,
sx_env = closure.env, sx_fn = closure.fn_ptr }`
Signature mangling: compact codes — `v` void, `b` bool, `i` s32,
`q` s64, `f` f32, `d` f64, `c/C/s/S/I/Q` for other ints, `p` for
pointers/aggregates that lower to a machine word. Return first,
then params underscore-joined. `Closure() -> void` mangles to `v`;
`Closure(bool) -> void` mangles to `v_b`.
Loud failures at the cast site:
- `Block` struct missing → "requires #import \"modules/std/objc_block.sx\";"
- `_NSConcreteStackBlock` extern missing → same diagnostic.
- `__sx_block_descriptor` global missing → same.
- `__sx_default_context` missing inside the trampoline emitter →
compiler-bug diagnostic (the scan pass should always register it).
The existing hand-rolled stdlib impls (`__block_invoke_void`,
`__block_invoke_bool`, the two `Into(Block) for Closure(...)`
impls) are now redundant — the compiler-synthesised trampoline
takes over via the intercept. Next commit (M5.A.2) removes them.
95-objc-block-noop continues to pass; IR shows `__block_invoke_v`
(the synthesised name) replacing the hand-rolled
`__block_invoke_void` at the cast site. 189/189 example tests
pass; chess on iOS-sim green.
This commit is contained in:
251
src/ir/lower.zig
251
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_<sig>` 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_<sig>` 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_<sig>(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_<sig>`
|
||||
/// 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)) {
|
||||
|
||||
Reference in New Issue
Block a user