ffi 1.3 make-green: #objc_call(void)(recv, "sel:") codegen
100/100 regression tests pass; ffi-objc-call-02-void-return flips
from xfail (codegen rejection) to passing ("ok").
Lowering for `#objc_call(void)(recv, "selector:")` lands in
lower.zig as `lowerFfiIntrinsicCall`:
%sel = call ptr @sel_registerName(<"selector:">)
%call = call ptr @objc_msgSend(<recv>, %sel)
Two extern decls (`sel_registerName(*u8) -> *void` and
`objc_msgSend(*void, *void) -> *void`) are declared lazily and
cached on the Lowering instance via `objc_msg_send_fid` /
`sel_register_name_fid`, so multiple call sites share one
declaration each.
Phase 1.3 deliberately keeps scope tight: only `void` return + just
(recv, selector) arity is wired. Non-void returns + variadic arity
fall through with a diagnostic and are owned by subsequent phase-1
steps (1.6 primitive returns; 1.7..1.9 struct shapes; 1.10 multi-
keyword selectors).
Selector resolution is still per-call-site `sel_registerName` —
the planned 1.5 interning turns the per-call hashtable lookup into
a single static-global load. Chess Android + iOS-sim builds clean
— no regression on the existing typed-`objc_msgSend`-cast pattern.
This commit is contained in:
113
src/ir/lower.zig
113
src/ir/lower.zig
@@ -95,6 +95,8 @@ pub const Lowering = struct {
|
||||
module_scopes: ?*std.StringHashMap(std.StringHashMap(void)) = null, // per-module visible names (from import resolution)
|
||||
import_graph: ?*std.StringHashMap(std.StringHashMap(void)) = null, // module path → set of directly imported paths (used by param_impl_map visibility filter)
|
||||
current_source_file: ?[]const u8 = null, // source file of function currently being lowered
|
||||
objc_msg_send_fid: ?FuncId = null, // lazily-declared `objc_msgSend` extern (for #objc_call lowering)
|
||||
sel_register_name_fid: ?FuncId = null, // lazily-declared `sel_registerName` extern
|
||||
type_bindings: ?std.StringHashMap(TypeId) = null, // generic type param bindings ($T → concrete TypeId)
|
||||
current_match_tags: ?[]const u64 = null, // type tags for current match arm (for runtime dispatch)
|
||||
force_block_value: bool = false, // set by lowerBlockValue to extract if-else values
|
||||
@@ -1753,6 +1755,7 @@ pub const Lowering = struct {
|
||||
.break_expr => self.lowerBreak(),
|
||||
.continue_expr => self.lowerContinue(),
|
||||
.call => |c| self.lowerCall(&c),
|
||||
.ffi_intrinsic_call => |fic| self.lowerFfiIntrinsicCall(&fic),
|
||||
.field_access => |fa| self.lowerFieldAccess(&fa, node.span),
|
||||
.struct_literal => |sl| self.lowerStructLiteral(&sl),
|
||||
.array_literal => |al| self.lowerArrayLiteral(&al),
|
||||
@@ -3729,6 +3732,116 @@ pub const Lowering = struct {
|
||||
return .s64;
|
||||
}
|
||||
|
||||
// ── FFI intrinsics (#objc_call / #jni_call / #jni_static_call) ─
|
||||
|
||||
/// Lazily declare `sel_registerName(name: *u8) -> *void` as an extern.
|
||||
/// Cached per Lowering instance so multiple `#objc_call` sites share
|
||||
/// one declaration.
|
||||
fn getSelRegisterNameFid(self: *Lowering) FuncId {
|
||||
if (self.sel_register_name_fid) |fid| return fid;
|
||||
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
||||
const name_str = self.module.types.internString("name");
|
||||
const ptr_ty = self.module.types.ptrTo(.u8);
|
||||
params.append(self.alloc, .{ .name = name_str, .ty = ptr_ty }) catch unreachable;
|
||||
const fn_name = self.module.types.internString("sel_registerName");
|
||||
const ret_ty = self.module.types.ptrTo(.void);
|
||||
const fid = self.builder.declareExtern(fn_name, params.toOwnedSlice(self.alloc) catch unreachable, ret_ty);
|
||||
const func = self.module.getFunctionMut(fid);
|
||||
func.call_conv = .c;
|
||||
self.sel_register_name_fid = fid;
|
||||
return fid;
|
||||
}
|
||||
|
||||
/// Lazily declare `objc_msgSend(recv: *void, sel: *void) -> *void`.
|
||||
/// Cast at the call site by the LLVM lowering (the `coerceArg` /
|
||||
/// type-equivalence path). For Phase 1.3 the only return shape
|
||||
/// exercised is void; the *void return is discarded.
|
||||
fn getObjcMsgSendFid(self: *Lowering) FuncId {
|
||||
if (self.objc_msg_send_fid) |fid| return fid;
|
||||
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
||||
const recv_str = self.module.types.internString("recv");
|
||||
const sel_str = self.module.types.internString("sel");
|
||||
const vptr = self.module.types.ptrTo(.void);
|
||||
params.append(self.alloc, .{ .name = recv_str, .ty = vptr }) catch unreachable;
|
||||
params.append(self.alloc, .{ .name = sel_str, .ty = vptr }) catch unreachable;
|
||||
const fn_name = self.module.types.internString("objc_msgSend");
|
||||
const fid = self.builder.declareExtern(fn_name, params.toOwnedSlice(self.alloc) catch unreachable, vptr);
|
||||
const func = self.module.getFunctionMut(fid);
|
||||
func.call_conv = .c;
|
||||
self.objc_msg_send_fid = fid;
|
||||
return fid;
|
||||
}
|
||||
|
||||
/// Lower `#objc_call(T)(recv, "sel:", args...)` to:
|
||||
/// %sel = call ptr @sel_registerName(<"sel:">)
|
||||
/// %ret = call <ABI(T)> @objc_msgSend(recv, %sel, args...)
|
||||
/// For Phase 1.3 only the (void return, no extra args) form is
|
||||
/// fully wired. Extra arities + non-void returns will land in
|
||||
/// subsequent phase-1 steps.
|
||||
fn lowerFfiIntrinsicCall(self: *Lowering, fic: *const ast.FfiIntrinsicCall) Ref {
|
||||
if (fic.kind != .objc_call) {
|
||||
if (self.diagnostics) |d| {
|
||||
d.add(.err, "#jni_call / #jni_static_call lowering not implemented yet (Phase 1.15+)", null);
|
||||
}
|
||||
return Ref.none;
|
||||
}
|
||||
|
||||
if (fic.args.len < 2) {
|
||||
if (self.diagnostics) |d| {
|
||||
d.add(.err, "#objc_call requires at least a receiver and a selector", null);
|
||||
}
|
||||
return Ref.none;
|
||||
}
|
||||
|
||||
// Resolve the return type from the syntactic slot.
|
||||
const ret_ty = self.resolveType(fic.return_type);
|
||||
|
||||
// For Phase 1.3 the only supported return-type / arity combo is
|
||||
// (void, recv + selector). Anything else falls through to undef
|
||||
// for now — the next phase-1 steps fill these in one shape at
|
||||
// a time.
|
||||
if (ret_ty != .void or fic.args.len != 2) {
|
||||
if (self.diagnostics) |d| {
|
||||
d.add(.err, "#objc_call: only `void` return + (recv, selector) is lowered today; non-void / arg-bearing arities land in later phase-1 steps", null);
|
||||
}
|
||||
return Ref.none;
|
||||
}
|
||||
|
||||
// Receiver expression.
|
||||
const recv = self.lowerExpr(fic.args[0]);
|
||||
|
||||
// Selector must be a literal string at parse time so we can
|
||||
// intern it (Phase 1.5 will cache the SEL too). For Phase 1.3
|
||||
// we accept any expression that lowers to a string Ref.
|
||||
const sel_arg_node = fic.args[1];
|
||||
const sel_ref = blk: {
|
||||
if (sel_arg_node.data == .string_literal) {
|
||||
const raw = sel_arg_node.data.string_literal.raw;
|
||||
break :blk self.builder.constString(self.module.types.internString(raw));
|
||||
}
|
||||
break :blk self.lowerExpr(sel_arg_node);
|
||||
};
|
||||
|
||||
// Resolve selector via the runtime — per call site for now.
|
||||
const sel_fid = self.getSelRegisterNameFid();
|
||||
var sel_args = std.ArrayList(Ref).empty;
|
||||
sel_args.append(self.alloc, sel_ref) catch unreachable;
|
||||
const sel_owned = sel_args.toOwnedSlice(self.alloc) catch unreachable;
|
||||
const sel = self.builder.emit(.{ .call = .{ .callee = sel_fid, .args = sel_owned } }, self.module.types.ptrTo(.void));
|
||||
|
||||
// Dispatch through objc_msgSend.
|
||||
const msg_fid = self.getObjcMsgSendFid();
|
||||
var call_args = std.ArrayList(Ref).empty;
|
||||
call_args.append(self.alloc, recv) catch unreachable;
|
||||
call_args.append(self.alloc, sel) catch unreachable;
|
||||
const owned = call_args.toOwnedSlice(self.alloc) catch unreachable;
|
||||
// Result type is `*void` here (objc_msgSend's declared shape).
|
||||
// For `void` user-facing returns we just discard the Ref —
|
||||
// the IR keeps the side-effecting call instruction either way.
|
||||
_ = self.builder.emit(.{ .call = .{ .callee = msg_fid, .args = owned } }, self.module.types.ptrTo(.void));
|
||||
return Ref.none;
|
||||
}
|
||||
|
||||
// ── Calls ───────────────────────────────────────────────────────
|
||||
|
||||
fn lowerCall(self: *Lowering, c: *const ast.Call) Ref {
|
||||
|
||||
@@ -1 +1 @@
|
||||
1
|
||||
0
|
||||
|
||||
@@ -1 +1 @@
|
||||
/Users/agra/projects/sx/examples/ffi-objc-call-02-void-return.sx:18:9: error: unresolved: 'unknown_expr'
|
||||
ok
|
||||
|
||||
Reference in New Issue
Block a user