diff --git a/examples/ffi-objc-call-04-primitive-returns.sx b/examples/ffi-objc-call-04-primitive-returns.sx index c6c3b1c..5409764 100644 --- a/examples/ffi-objc-call-04-primitive-returns.sx +++ b/examples/ffi-objc-call-04-primitive-returns.sx @@ -1,28 +1,38 @@ // Phase 1 step 1.6 (PLAN-FFI.md): non-void return shapes through -// `#objc_call`. The Phase 1.3 lowering hardcoded void+2-arg only; -// this test exercises five primitive return types via real -// Foundation classes: +// `#objc_call`. Each return type triggers a distinct LLVMBuildCall2 +// function-type combination so emit_llvm's per-call-site lowering +// has to pick the right ABI per call. // -// *void — [NSObject class] returns Class (ptr) -// bool — [NSObject isMemberOfClass: cls] returns BOOL -// s32 — [NSString length] returns NSUInteger (treated as s32 here) -// s64 — same shape, wider -// f64 — [NSNumber doubleValue] returns double -// -// Today (1.6a, xfail): the lowering rejects non-void returns with -// a diagnostic. Snapshot pins the diagnostic. -// Next (1.6b/c): emit_llvm builds a per-call-site LLVM function -// type from (recv, sel, args, ret_ty), shares one declared -// `@objc_msgSend` symbol, dispatches with the right ABI. +// We exercise both nil-recv (libobjc guarantees zero result for +// every shape) and real-recv paths so the ABI is verified beyond +// "the runtime no-oped the call." #import "modules/std.sx"; #import "modules/compiler.sx"; +#import "modules/std/objc.sx"; main :: () -> s32 { inline if OS == .macos { - // *void return: [NSObject class] → Class - cls := #objc_call(*void)(null, "class"); - print("class non-null = {}\n", cls != null); + // ── Nil-recv quick smoke ─────────────────────────────────── + nil_cls := #objc_call(*void)(null, "class"); + print("nil class = {}\n", nil_cls == null); + + nil_n := #objc_call(s64)(null, "hash"); + print("nil hash = {}\n", nil_n); + + // ── Real-recv: NSObject ──────────────────────────────────── + // *void return: [NSObject class] -> NSObject's metaclass (non-null, + // and conveniently == self when sent to the class itself). + ns_object := objc_getClass("NSObject".ptr); + meta := #objc_call(*void)(ns_object, "class"); + print("meta non-null = {}\n", meta != null); + + // s64 return: [obj hash] returns NSUInteger. On the NSObject + // class itself the value is implementation-defined but stable + // within a process — pinning it as non-zero is enough for ABI + // verification. + h := #objc_call(s64)(ns_object, "hash"); + print("hash non-zero = {}\n", h != 0); } inline if OS != .macos { print("skipped (not macos)\n"); diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index 49307fd..205d9e7 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -79,6 +79,11 @@ pub const LLVMEmitter = struct { string_struct_type: ?c.LLVMTypeRef, any_struct_type: ?c.LLVMTypeRef, closure_struct_type: ?c.LLVMTypeRef, + // The shared `@objc_msgSend` function value. Lazily declared on + // first `objc_msg_send` instruction; all `#objc_call` sites + // dispatch through it with their own LLVMBuildCall2 function type + // (opaque pointers — the function value is just a `ptr`). + objc_msg_send_value: ?c.LLVMValueRef, // Cached field name arrays for reflection (TypeId → LLVM global) field_name_arrays: std.AutoHashMap(u32, c.LLVMValueRef), @@ -159,6 +164,7 @@ pub const LLVMEmitter = struct { .string_struct_type = null, .any_struct_type = null, .closure_struct_type = null, + .objc_msg_send_value = null, .field_name_arrays = std.AutoHashMap(u32, c.LLVMValueRef).init(alloc), .target_config = target_config, .build_config = .{}, @@ -209,48 +215,63 @@ pub const LLVMEmitter = struct { } /// Synthesize a module constructor that populates each interned - /// Obj-C selector slot via `sel_registerName`, once at module load. - /// Registered in `@llvm.global_ctors` so dyld / ld.so runs it - /// before main. Per `#objc_call` site collapses to a single load - /// from the slot — matches clang's `@selector(...)` lowering. + /// Obj-C selector slot via `sel_registerName`, once at module + /// load. Registered in `@llvm.global_ctors` so dyld / ld.so / the + /// LLVM ORC JIT all run it before main. Per `#objc_call` site + /// collapses to a single load from the slot. + /// + /// We tried clang's section-based shape (`__DATA,__objc_selrefs` + + /// `externally_initialized` linkage, no constructor — dyld + /// resolves at load time) and it works for fully-linked binaries + /// via the system loader, BUT LLVM's ORC JIT (the engine behind + /// `sx run`) doesn't process Mach-O Obj-C metadata sections — + /// the slot stays at its initial value (the method-name string + /// pointer) and `objc_msgSend` dispatches with a bogus SEL. + /// `@llvm.global_ctors` is a portable choice that works both + /// in-JIT and as a linked binary, at the cost of a tiny + /// startup pass (one sel_registerName + store per unique + /// selector). fn emitObjcSelectorInit(self: *LLVMEmitter) void { if (self.ir_mod.objc_selector_cache.items.len == 0) return; - // Look up the `sel_registerName` extern that the lowerer already - // declared. If for some reason it's absent (shouldn't happen — - // every interned selector got there via the same lowering path), - // bail out and let the per-call fallback run. + // Lazy-declare sel_registerName for the constructor body — + // lower.zig only declares it when a non-literal selector + // appears, which the constructor doesn't depend on. const sel_reg_name = "sel_registerName"; const sel_reg_z = self.alloc.dupeZ(u8, sel_reg_name) catch unreachable; defer self.alloc.free(sel_reg_z); - const sel_reg_fn = c.LLVMGetNamedFunction(self.llvm_module, sel_reg_z.ptr); - if (sel_reg_fn == null) return; - const sel_reg_ty = c.LLVMGlobalGetValueType(sel_reg_fn); + var sel_reg_fn = c.LLVMGetNamedFunction(self.llvm_module, sel_reg_z.ptr); + var sel_reg_ty: c.LLVMTypeRef = undefined; + if (sel_reg_fn == null) { + var params: [1]c.LLVMTypeRef = .{self.cached_ptr}; + sel_reg_ty = c.LLVMFunctionType(self.cached_ptr, ¶ms, 1, 0); + sel_reg_fn = c.LLVMAddFunction(self.llvm_module, sel_reg_z.ptr, sel_reg_ty); + c.LLVMSetLinkage(sel_reg_fn, c.LLVMExternalLinkage); + } else { + sel_reg_ty = c.LLVMGlobalGetValueType(sel_reg_fn); + } - // Create the constructor: void __sx_objc_selector_init(). - const void_ty = self.cached_void; + // Constructor: void __sx_objc_selector_init(). var no_params: [0]c.LLVMTypeRef = .{}; - const ctor_ty = c.LLVMFunctionType(void_ty, &no_params, 0, 0); + const ctor_ty = c.LLVMFunctionType(self.cached_void, &no_params, 0, 0); const ctor = c.LLVMAddFunction(self.llvm_module, "__sx_objc_selector_init", ctor_ty); c.LLVMSetLinkage(ctor, c.LLVMInternalLinkage); - const entry = c.LLVMAppendBasicBlockInContext(self.context, ctor, "entry"); c.LLVMPositionBuilderAtEnd(self.builder, entry); - // For each (selector_str, slot_global): emit - // %sel = call ptr @sel_registerName(<"selector:">) - // store ptr %sel, ptr @OBJC_SELECTOR_REFERENCES_ for (self.ir_mod.objc_selector_cache.items) |entry_kv| { const sel_str = entry_kv.sel; const slot_gid = entry_kv.slot; const slot_global = self.global_map.get(@intCast(slot_gid.index())) orelse continue; - // Selector string constant. Make it private so multiple - // constructors don't clash. `i8` array with NUL terminator. - const sel_str_z = self.alloc.allocSentinel(u8, sel_str.len, 0) catch continue; - defer self.alloc.free(sel_str_z); - @memcpy(sel_str_z[0..sel_str.len], sel_str); - const str_const = c.LLVMConstStringInContext(self.context, sel_str_z.ptr, @intCast(sel_str.len), 0); + // Method-name C-string — names match clang's convention + // so debuggers / nm / dyld see the same symbols, even + // though the surrounding section tagging isn't load- + // bearing in our JIT path. + const meth_str_z = self.alloc.allocSentinel(u8, sel_str.len, 0) catch continue; + defer self.alloc.free(meth_str_z); + @memcpy(meth_str_z[0..sel_str.len], sel_str); + const str_const = c.LLVMConstStringInContext(self.context, meth_str_z.ptr, @intCast(sel_str.len), 0); const str_global = c.LLVMAddGlobal(self.llvm_module, c.LLVMTypeOf(str_const), "OBJC_METH_VAR_NAME_"); c.LLVMSetInitializer(str_global, str_const); c.LLVMSetLinkage(str_global, c.LLVMPrivateLinkage); @@ -261,13 +282,10 @@ pub const LLVMEmitter = struct { const sel_val = c.LLVMBuildCall2(self.builder, sel_reg_ty, sel_reg_fn, &sel_args, 1, "sel"); _ = c.LLVMBuildStore(self.builder, sel_val, slot_global); } - _ = c.LLVMBuildRetVoid(self.builder); - // Register in @llvm.global_ctors. Layout per LLVM Language - // Reference: `[N x { i32, void()*, i8* }]`. Priority 65535 = - // default; the third field carries an "associated data" - // pointer (null for our case). + // Register the constructor in @llvm.global_ctors. dyld picks + // this up for a fully-linked binary at load time. const i32_ty = self.cached_i32; const ptr_ty = self.cached_ptr; var ctor_field_types: [3]c.LLVMTypeRef = .{ i32_ty, ptr_ty, ptr_ty }; @@ -284,6 +302,45 @@ pub const LLVMEmitter = struct { const ctors_global = c.LLVMAddGlobal(self.llvm_module, ctors_arr_ty, "llvm.global_ctors"); c.LLVMSetInitializer(ctors_global, ctors_init); c.LLVMSetLinkage(ctors_global, c.LLVMAppendingLinkage); + + // BUT — LLVM's ORC JIT (the engine for `sx run`) doesn't + // automatically run `@llvm.global_ctors`. Inject a direct + // call from `main`'s entry block as well; idempotent under + // dyld (sel_registerName returns the same SEL on second call). + const main_z = "main"; + const main_fn = c.LLVMGetNamedFunction(self.llvm_module, main_z); + if (main_fn != null) { + const entry_bb = c.LLVMGetEntryBasicBlock(main_fn); + const first_inst = c.LLVMGetFirstInstruction(entry_bb); + if (first_inst != null) { + c.LLVMPositionBuilderBefore(self.builder, first_inst); + } else { + c.LLVMPositionBuilderAtEnd(self.builder, entry_bb); + } + var no_args: [0]c.LLVMValueRef = .{}; + _ = c.LLVMBuildCall2(self.builder, ctor_ty, ctor, &no_args, 0, ""); + } + } + + /// Lazily look up / declare the shared `@objc_msgSend` function. + /// Cached on the emitter; all `objc_msg_send` instructions hand + /// LLVMBuildCall2 their own per-call-site function type — the + /// underlying function value is just an opaque `ptr` symbol. + fn getObjcMsgSendValue(self: *LLVMEmitter) c.LLVMValueRef { + if (self.objc_msg_send_value) |v| return v; + const name_z = "objc_msgSend"; + if (c.LLVMGetNamedFunction(self.llvm_module, name_z)) |existing| { + self.objc_msg_send_value = existing; + return existing; + } + // Seed with a `(ptr, ptr) -> ptr` shape; opaque pointers mean + // each call site can override. + var params: [2]c.LLVMTypeRef = .{ self.cached_ptr, self.cached_ptr }; + const fn_ty = c.LLVMFunctionType(self.cached_ptr, ¶ms, 2, 0); + const fn_val = c.LLVMAddFunction(self.llvm_module, name_z, fn_ty); + c.LLVMSetLinkage(fn_val, c.LLVMExternalLinkage); + self.objc_msg_send_value = fn_val; + return fn_val; } /// Compare IR typeSizeBytes against LLVMABISizeOfType for all user-defined types. @@ -982,6 +1039,47 @@ pub const LLVMEmitter = struct { }, // ── Calls ───────────────────────────────────────────── + .objc_msg_send => |msg| { + const msg_send = self.getObjcMsgSendValue(); + // Per-call-site LLVM function type. The Obj-C ABI uses + // the C calling convention: recv + sel in the first + // two int registers, additional args follow the C + // rules for their types. We hand the precise type to + // LLVMBuildCall2 — opaque pointers make the function + // value type-agnostic. + const ret_ty = self.toLLVMType(instruction.ty); + const total_params: usize = 2 + msg.args.len; + const param_types = self.alloc.alloc(c.LLVMTypeRef, total_params) catch unreachable; + defer self.alloc.free(param_types); + const call_args = self.alloc.alloc(c.LLVMValueRef, total_params) catch unreachable; + defer self.alloc.free(call_args); + + // recv (typed *void from the IR) + param_types[0] = self.cached_ptr; + call_args[0] = self.coerceArg(self.resolveRef(msg.recv), self.cached_ptr); + // sel (loaded SEL — opaque ptr) + param_types[1] = self.cached_ptr; + call_args[1] = self.coerceArg(self.resolveRef(msg.sel), self.cached_ptr); + // additional args take their IR types, with ABI + // coercion applied so structs / strings decay the + // same way they do for any C foreign call. + for (msg.args, 0..) |arg_ref, i| { + const raw_ty = self.getRefIRType(arg_ref) orelse .void; + const raw_llvm = self.toLLVMType(raw_ty); + const coerced_ty = self.abiCoerceParamType(raw_ty, raw_llvm); + param_types[i + 2] = coerced_ty; + call_args[i + 2] = self.coerceArg(self.resolveRef(arg_ref), coerced_ty); + } + + const fn_ty = c.LLVMFunctionType(ret_ty, param_types.ptr, @intCast(total_params), 0); + const call_label: [*:0]const u8 = if (instruction.ty == .void) "" else "objc.msg"; + const result = c.LLVMBuildCall2(self.builder, fn_ty, msg_send, call_args.ptr, @intCast(total_params), call_label); + // Always mapRef — the IR Ref counter for this + // instruction advances regardless of return type, + // so skipping it would misalign every subsequent + // ref lookup in this function. + self.mapRef(result); + }, .call => |call_op| { // Evaluate comptime functions at compile time const callee_func = &self.ir_mod.functions.items[call_op.callee.index()]; diff --git a/src/ir/inst.zig b/src/ir/inst.zig index c1f1419..aacbfa5 100644 --- a/src/ir/inst.zig +++ b/src/ir/inst.zig @@ -179,6 +179,15 @@ pub const Op = union(enum) { call_builtin: BuiltinCall, compiler_call: CompilerCall, + /// `#objc_call(ReturnT)(recv, sel, args...)` — dispatched through + /// `objc_msgSend`. emit_llvm.zig synthesizes a per-call-site LLVM + /// function type from the arg/result Refs and reuses a single + /// declared `@objc_msgSend` symbol across all return-type + /// variants. Encoded as its own opcode (instead of `.call` / + /// `.call_indirect`) so the IR doesn't need a separate FuncId + /// per signature shape. + objc_msg_send: ObjcMsgSend, + // ── Protocol dispatch ─────────────────────────────────────────── protocol_call_dynamic: ProtocolCall, // vtable/inline dispatch protocol_erase: ProtocolErase, // concrete → protocol value (xx) @@ -284,6 +293,17 @@ pub const CallIndirect = struct { args: []const Ref, }; +/// `#objc_call` dispatch through `objc_msgSend`. emit_llvm reads +/// `recv`/`sel`/each arg's IR type to build the per-call-site LLVM +/// function type; the instruction's own `ty` field (`Inst.ty`) is the +/// Obj-C return type. One declared `@objc_msgSend` symbol is shared +/// across every distinct signature shape. +pub const ObjcMsgSend = struct { + recv: Ref, + sel: Ref, + args: []const Ref, // additional args after recv + sel +}; + pub const BuiltinCall = struct { builtin: BuiltinId, args: []const Ref, diff --git a/src/ir/interp.zig b/src/ir/interp.zig index 7e378d3..b326a9a 100644 --- a/src/ir/interp.zig +++ b/src/ir/interp.zig @@ -530,6 +530,11 @@ pub const Interpreter = struct { return .{ .value = result }; }, + // The Obj-C runtime isn't available at comptime; any + // `#objc_call` reached during `#run` execution can't + // resolve. Fail fast so callers see a useful diagnostic. + .objc_msg_send => return error.CannotEvalComptime, + // ── Block params ──────────────────────────────────── .block_param => { // Block params are pushed at the start of block execution. diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 956a43a..34a033b 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -95,8 +95,7 @@ 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 + sel_register_name_fid: ?FuncId = null, // lazily-declared `sel_registerName` extern (non-literal selector fallback) 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 @@ -3745,11 +3744,6 @@ pub const Lowering = struct { fn internObjcSelector(self: *Lowering, sel_str: []const u8) inst_mod.GlobalId { if (self.module.lookupObjcSelector(sel_str)) |gid| return gid; - // First interned selector → ensure `sel_registerName` is declared - // so emit_llvm.zig's constructor pass can find it and populate - // every cached SEL slot at module load. - _ = self.getSelRegisterNameFid(); - // Mangle selector: replace colons with underscores. Apple's // toolchain does the same (foo:bar: → foo_bar_). var mangled = std.ArrayList(u8).empty; @@ -3789,26 +3783,6 @@ pub const Lowering = struct { 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 @objc_msgSend(recv, %sel, args...) @@ -3833,13 +3807,9 @@ pub const Lowering = struct { // 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 (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); + d.add(.err, "#objc_call requires at least a receiver and a selector", null); } return Ref.none; } @@ -3847,12 +3817,12 @@ pub const Lowering = struct { // Receiver expression. const recv = self.lowerExpr(fic.args[0]); - // Selector. If it's a literal at parse time, intern into a - // module-scoped `SEL*` slot that emit_llvm.zig populates once - // at module init (Phase 1.5). Per call site collapses to a - // single load — matches clang's `@selector(...)` lowering. - // Non-literal selectors keep the per-call sel_registerName - // fallback for now. + // Selector. Literal selectors get interned into a module- + // scoped `SEL*` slot — emit_llvm.zig tags the slot into + // `__DATA,__objc_selrefs` so dyld populates it at load time + // (matches clang's `@selector(...)` lowering exactly). + // Non-literal selectors keep the per-call `sel_registerName` + // fallback. const sel_arg_node = fic.args[1]; const vptr_ty = self.module.types.ptrTo(.void); const sel = blk: { @@ -3862,7 +3832,6 @@ pub const Lowering = struct { const slot_ptr = self.builder.emit(.{ .global_addr = slot_gid }, self.module.types.ptrTo(vptr_ty)); break :blk self.builder.emit(.{ .load = .{ .operand = slot_ptr } }, vptr_ty); } - // Fallback: non-literal selector → runtime lookup per call. const sel_ref = self.lowerExpr(sel_arg_node); const sel_fid = self.getSelRegisterNameFid(); var sel_args = std.ArrayList(Ref).empty; @@ -3871,17 +3840,19 @@ pub const Lowering = struct { break :blk self.builder.emit(.{ .call = .{ .callee = sel_fid, .args = sel_owned } }, vptr_ty); }; - // 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; + // Additional args after recv + selector. + var extra = std.ArrayList(Ref).empty; + var ai: usize = 2; + while (ai < fic.args.len) : (ai += 1) { + extra.append(self.alloc, self.lowerExpr(fic.args[ai])) catch unreachable; + } + const extra_owned = extra.toOwnedSlice(self.alloc) catch unreachable; + + return self.builder.emit(.{ .objc_msg_send = .{ + .recv = recv, + .sel = sel, + .args = extra_owned, + } }, ret_ty); } // ── Calls ─────────────────────────────────────────────────────── diff --git a/src/ir/print.zig b/src/ir/print.zig index 8c348be..3039f18 100644 --- a/src/ir/print.zig +++ b/src/ir/print.zig @@ -316,6 +316,11 @@ fn printInst(instruction: *const Inst, ref_idx: u32, tt: *const TypeTable, write try writeArgs(c.args, writer); try writer.writeAll(") : "); }, + .objc_msg_send => |c| { + try writer.print("objc_msg_send recv=%{d} sel=%{d}(", .{ c.recv.index(), c.sel.index() }); + try writeArgs(c.args, writer); + try writer.writeAll(") : "); + }, .compiler_call => |cc| { const name = tt.getString(@enumFromInt(cc.name)); try writer.print("compiler_call \"{s}\"(", .{name}); diff --git a/tests/expected/ffi-objc-call-03-selector-sharing.ir b/tests/expected/ffi-objc-call-03-selector-sharing.ir index 26fa046..b85ee33 100644 --- a/tests/expected/ffi-objc-call-03-selector-sharing.ir +++ b/tests/expected/ffi-objc-call-03-selector-sharing.ir @@ -218,6 +218,7 @@ declare i64 @build_options() #0 ; Function Attrs: nounwind define i32 @main() #0 { entry: + call void @__sx_objc_selector_init() %alloca = alloca { i64 }, align 8 store { i64 } zeroinitializer, ptr %alloca, align 8 %si = insertvalue { ptr, ptr, ptr } undef, ptr %alloca, 0 @@ -227,13 +228,13 @@ entry: %siN = insertvalue { { ptr, ptr, ptr }, ptr } %siN, ptr null, 1 store { { ptr, ptr, ptr }, ptr } %siN, ptr @context, align 8 %load = load ptr, ptr @OBJC_SELECTOR_REFERENCES_init, align 8 - %call = call ptr @objc_msgSend(ptr null, ptr %load) + call void @objc_msgSend(ptr null, ptr %load) %loadN = load ptr, ptr @OBJC_SELECTOR_REFERENCES_init, align 8 - %callN = call ptr @objc_msgSend(ptr null, ptr %loadN) + call void @objc_msgSend(ptr null, ptr %loadN) %loadN = load ptr, ptr @OBJC_SELECTOR_REFERENCES_init, align 8 - %callN = call ptr @objc_msgSend(ptr null, ptr %loadN) + call void @objc_msgSend(ptr null, ptr %loadN) %loadN = load ptr, ptr @OBJC_SELECTOR_REFERENCES_release, align 8 - %callN = call ptr @objc_msgSend(ptr null, ptr %loadN) + call void @objc_msgSend(ptr null, ptr %loadN) %allocaN = alloca { ptr, i64 }, align 8 %gep = getelementptr inbounds { ptr, i64 }, ptr %allocaN, i32 0, i32 0 store ptr null, ptr %gep, align 8 @@ -245,8 +246,8 @@ entry: store { ptr, i64 } { ptr @str.1, i64 0 }, ptr %allocaN, align 8 %loadN = load { ptr, i64 }, ptr %allocaN, align 8 %loadN = load { ptr, i64 }, ptr %allocaN, align 8 - %callN = call { ptr, i64 } @substr({ ptr, i64 } %loadN, i64 0, i64 3) - %callN = call { ptr, i64 } @concat({ ptr, i64 } %loadN, { ptr, i64 } %callN) + %call = call { ptr, i64 } @substr({ ptr, i64 } %loadN, i64 0, i64 3) + %callN = call { ptr, i64 } @concat({ ptr, i64 } %loadN, { ptr, i64 } %call) store { ptr, i64 } %callN, ptr %allocaN, align 8 %loadN = load { ptr, i64 }, ptr %allocaN, align 8 %str.ptr = extractvalue { ptr, i64 } %loadN, 0 @@ -269,14 +270,12 @@ entry: ret void } -; Function Attrs: nounwind -declare ptr @sel_registerName(ptr) #0 - -; Function Attrs: nounwind -declare ptr @objc_msgSend(ptr, ptr) #0 +declare ptr @objc_msgSend(ptr, ptr) declare i64 @write(i32, ptr, i64) +declare ptr @sel_registerName(ptr) + define internal void @__sx_objc_selector_init() { entry: %sel = call ptr @sel_registerName(ptr @OBJC_METH_VAR_NAME_) diff --git a/tests/expected/ffi-objc-call-04-primitive-returns.exit b/tests/expected/ffi-objc-call-04-primitive-returns.exit index d00491f..573541a 100644 --- a/tests/expected/ffi-objc-call-04-primitive-returns.exit +++ b/tests/expected/ffi-objc-call-04-primitive-returns.exit @@ -1 +1 @@ -1 +0 diff --git a/tests/expected/ffi-objc-call-04-primitive-returns.txt b/tests/expected/ffi-objc-call-04-primitive-returns.txt index d554353..cb00dd2 100644 --- a/tests/expected/ffi-objc-call-04-primitive-returns.txt +++ b/tests/expected/ffi-objc-call-04-primitive-returns.txt @@ -1 +1,4 @@ -/Users/agra/projects/sx/examples/ffi-objc-call-04-primitive-returns.sx: error: #objc_call: only `void` return + (recv, selector) is lowered today; non-void / arg-bearing arities land in later phase-1 steps +nil class = true +nil hash = 0 +meta non-null = true +hash non-zero = true