ffi M5.A revert: drop compiler synthesis, require explicit Into(Block) impls
Reconsidered the M5.A.2 cleanup. The compiler-synthesised trampoline
path was hidden behaviour — a user reading their code couldn't tell
how `xx my_closure : Block` worked without reading lower.zig. That's
exactly the kind of magic sx's design has been pushing against.
New design (strict mode):
1. Stdlib's modules/std/objc_block.sx hand-rolls
`__block_invoke_void` + `Into(Block) for Closure() -> void` and
the same pair for `Closure(bool) -> void` (restored from M5.A.2).
These are readable reference implementations of the bridge ABI.
2. The compiler intercept fires NO synthesis — instead, when
`tryUserConversion` can't find a reachable `Into(Block)` impl for
the closure's signature, it emits a focused diagnostic:
"no `Into(Block) for <Closure-sig>` impl — add a per-signature
`__block_invoke_<sig>` trampoline + Into impl alongside the
existing ones in modules/std/objc_block.sx, or declare it in
your own code"
3. Per-signature declarations live in stdlib (for common signatures)
or in user code (for app-specific ones). 96-objc-block-multi-arg
now demonstrates the user-side pattern in-file — it declares its
own `__block_invoke_void_s32_p` + `Into(Block) for Closure(s32,
*void) -> void` impl alongside its main().
Net effect:
- Every block bridge is source-visible. No hidden compiler magic.
- Users see exactly how the Apple ABI shape is constructed in sx
source — stdlib serves as the reference implementation.
- Compiler enforces the discipline: missing impl → clear diagnostic
pointing at the template.
- Coverage for arbitrary signatures requires conscious user opt-in,
not silent fallthrough.
Removed from lower.zig: `tryClosureToBlockConversion`,
`emitBlockInvokeTrampoline`, `mangleClosureSigForBlock`,
`mangleTypeForBlock`, and the `block_invoke_trampolines` dedup
state field. Net: the synthesis machinery is gone; only the
detection helper `isClosureToBlockCast` remains, used by the
diagnostic.
190/190 example tests pass; chess on iOS-sim green.
This commit is contained in:
@@ -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_<sig>` 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;
|
||||
|
||||
|
||||
@@ -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_<sig>` 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_<sig>` trampoline
|
||||
// matching the closure's calling convention and an
|
||||
// `impl Into(Block) for Closure(<sig>)` 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
292
src/ir/lower.zig
292
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_<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,
|
||||
@@ -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_<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;
|
||||
/// 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_<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',
|
||||
};
|
||||
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(<sig>)` 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_<sig>` 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_<sig>` 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.
|
||||
|
||||
Reference in New Issue
Block a user