diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 4172694..be0eedf 100644 --- a/src/ir/lower.zig +++ b/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 @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 { diff --git a/tests/expected/ffi-objc-call-02-void-return.exit b/tests/expected/ffi-objc-call-02-void-return.exit index d00491f..573541a 100644 --- a/tests/expected/ffi-objc-call-02-void-return.exit +++ b/tests/expected/ffi-objc-call-02-void-return.exit @@ -1 +1 @@ -1 +0 diff --git a/tests/expected/ffi-objc-call-02-void-return.txt b/tests/expected/ffi-objc-call-02-void-return.txt index 2214600..9766475 100644 --- a/tests/expected/ffi-objc-call-02-void-return.txt +++ b/tests/expected/ffi-objc-call-02-void-return.txt @@ -1 +1 @@ -/Users/agra/projects/sx/examples/ffi-objc-call-02-void-return.sx:18:9: error: unresolved: 'unknown_expr' +ok