Files
sx/src/ir/lower/objc_class.zig
agra 811a280517 refactor(ffi-linkage): Phase 9.3 — purge 'foreign' from comments (src caps + examples + docs)
src/: ~21 capital-Foreign comments the case-sensitive verify grep missed
(Foreign-class→Runtime-class, Foreign path→Runtime path, Foreign decls→Extern decls,
FOREIGN function→extern function) across calls/inst/ffi_objc/jni_descriptor/emit_llvm/
c_import/lower.*/ops. src 'foreign' now = ONLY the hash_foreign token + 4 rejection
messages (9.0-delete targets). examples/*.sx comments → extern/runtime-class (1219
stdout regen; KEPT 1176). docs/inline-asm-design + debugger purged. Comments only —
no build impact. 9.0 ratified: DELETE hash_foreign token next.
2026-06-15 10:52:56 +03:00

1297 lines
61 KiB
Zig

const std = @import("std");
const ast = @import("../../ast.zig");
const Node = ast.Node;
const types = @import("../types.zig");
const inst_mod = @import("../inst.zig");
const mod_mod = @import("../module.zig");
const TypeId = types.TypeId;
const Ref = inst_mod.Ref;
const FuncId = inst_mod.FuncId;
const Function = inst_mod.Function;
const Module = mod_mod.Module;
const lower = @import("../lower.zig");
const Lowering = lower.Lowering;
/// Emit a C-ABI exported function for every bodied method on a
/// `#jni_main #jni_class("...")` declaration. The symbol name follows
/// JNI's name-mangling convention so Android's JNI runtime can resolve
/// `private native sx_<method>(...)` (declared in the bundled
/// classes.dex by `jni_java_emit`) without an explicit `RegisterNatives`
/// call — i.e. `Java_<pkg-mangled>_<Class>_sx_1<method-mangled>`.
///
/// Param ABI: prepended `(env: *void, self: *void)` (JNIEnv* + jobject
/// receiver), followed by the user-declared params with pointer types
/// type-erased to `*void` (JNI carries jobjects, not sx-typed handles —
/// future work can keep richer typing inside the body when needed).
/// Eagerly lower bodied instance methods on every sx-defined
/// `#objc_class`. The Obj-C runtime invokes these via the IMP
/// pointers wired up in M1.2 A.4 — no sx-side call path triggers
/// lazy lowering, so we walk the cache and force-lower here.
/// `lowerFunction` sets `current_runtime_class` automatically based
/// on the qualified name, so `*Self` substitutions in the body
/// resolve correctly (M1.2 A.2b). After the bodies are lowered,
/// `emitObjcDefinedClassImps` wraps each with a C-ABI trampoline
/// (M1.2 A.4b.ii).
pub fn lowerObjcDefinedClassMethods(self: *Lowering) void {
for (self.module.objc_defined_class_cache.items) |entry| {
const fcd = entry.decl;
for (fcd.members) |m| {
const method = switch (m) {
.method => |md| md,
else => continue,
};
if (method.body == null) continue;
const qualified = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fcd.name, method.name }) catch continue;
self.lazyLowerFunction(qualified);
}
}
// Now the bodies are lowered — emit the C-ABI IMP trampolines
// that bridge `objc_msgSend` invocations to them.
self.emitObjcDefinedClassImps();
}
/// If `obj_expr` is typed as a pointer to a runtime Obj-C class
/// and that class (or any of its `#extends` ancestors) declares a
/// `#property` field with the given name, return the
/// `RuntimeFieldDecl`. M2.2 + M2.3.
pub fn lookupObjcPropertyOnPointer(self: *Lowering, obj_expr: *const ast.Node, field_name: []const u8) ?ast.RuntimeFieldDecl {
const obj_ty = self.inferExprType(obj_expr);
if (obj_ty.isBuiltin()) return null;
const ptr_info = self.module.types.get(obj_ty);
if (ptr_info != .pointer) return null;
const pointee_info = self.module.types.get(ptr_info.pointer.pointee);
if (pointee_info != .@"struct") return null;
const struct_name = self.module.types.getString(pointee_info.@"struct".name);
const fcd = self.program_index.runtime_class_map.get(struct_name) orelse return null;
if (fcd.runtime != .objc_class and fcd.runtime != .objc_protocol) return null;
return self.findRuntimePropertyInChain(fcd, field_name);
}
/// Walk the `#extends` chain looking for a method by name. M2.3.
/// Returns the owning fcd + the method decl, or null if no ancestor
/// declares it. Depth-capped at 16 to break accidental cycles
/// (real Obj-C class chains rarely exceed 6 levels).
pub fn findRuntimeMethodInChain(self: *Lowering, fcd: *const ast.RuntimeClassDecl, method_name: []const u8) ?struct { fcd: *const ast.RuntimeClassDecl, method: ast.RuntimeMethodDecl } {
var current: *const ast.RuntimeClassDecl = fcd;
var depth: u32 = 0;
while (depth < 16) : (depth += 1) {
for (current.members) |m| switch (m) {
.method => |md| if (std.mem.eql(u8, md.name, method_name)) return .{ .fcd = current, .method = md },
else => {},
};
// Not on this level — follow `#extends ParentName`.
const parent = blk: {
for (current.members) |m| switch (m) {
.extends => |p| break :blk p,
else => {},
};
break :blk null;
} orelse return null;
current = self.program_index.runtime_class_map.get(parent) orelse return null;
}
return null;
}
/// Walk the `#extends` chain looking for a `#property` field by
/// name. M2.3 companion to findRuntimeMethodInChain.
pub fn findRuntimePropertyInChain(self: *Lowering, fcd: *const ast.RuntimeClassDecl, field_name: []const u8) ?ast.RuntimeFieldDecl {
var current: *const ast.RuntimeClassDecl = fcd;
var depth: u32 = 0;
while (depth < 16) : (depth += 1) {
for (current.members) |m| switch (m) {
.field => |f| if (f.is_property and std.mem.eql(u8, f.name, field_name)) return f,
else => {},
};
const parent = blk: {
for (current.members) |m| switch (m) {
.extends => |p| break :blk p,
else => {},
};
break :blk null;
} orelse return null;
current = self.program_index.runtime_class_map.get(parent) orelse return null;
}
return null;
}
const ObjcDefinedStateField = struct {
field_ty: TypeId,
state_ty: TypeId,
field_idx: u32,
fcd: *const ast.RuntimeClassDecl,
};
/// State-field-access info: if obj_expr is *<sx-defined-class>
/// and `field_name` is in the state struct (not a property),
/// returns the field's TypeId, the state struct's TypeId, and
/// the field's index. M1.2 A.3 supports.
pub fn lookupObjcDefinedStateFieldOnPointer(self: *Lowering, obj_expr: *const ast.Node, field_name: []const u8) ?ObjcDefinedStateField {
const obj_ty = self.inferExprType(obj_expr);
if (obj_ty.isBuiltin()) return null;
const ptr_info = self.module.types.get(obj_ty);
if (ptr_info != .pointer) return null;
const pointee_info = self.module.types.get(ptr_info.pointer.pointee);
if (pointee_info != .@"struct") return null;
const struct_name = self.module.types.getString(pointee_info.@"struct".name);
const fcd = self.program_index.runtime_class_map.get(struct_name) orelse return null;
// Only sx-defined Obj-C classes have a state struct. Extern (referenced)
// runtime classes' fields are purely declaration metadata (no state).
if (fcd.is_extern or fcd.runtime != .objc_class) return null;
// Skip property fields — those dispatch via the M2.2 getter/setter
// path. Plain instance fields take the ivar+gep path.
for (fcd.members) |m| switch (m) {
.field => |f| {
if (std.mem.eql(u8, f.name, field_name)) {
if (f.is_property) return null;
const state_ty = self.objc().objcDefinedStateStructType(fcd);
const state_info = self.module.types.get(state_ty);
if (state_info != .@"struct") return null;
const fname_id = self.module.types.internString(f.name);
for (state_info.@"struct".fields, 0..) |sf, idx| {
if (sf.name == fname_id) {
return .{
.field_ty = sf.ty,
.state_ty = state_ty,
.field_idx = @intCast(idx),
.fcd = fcd,
};
}
}
return null;
}
},
else => {},
};
return null;
}
/// Lower a read of `self.field` (or `obj.field`) on a sx-defined
/// Obj-C class: `state = object_getIvar(self, load(ivar_global))`
/// then `struct_gep(state, idx)` + load. M1.2 A.3 — the runtime
/// hop through the hidden ivar.
pub fn lowerObjcDefinedStateFieldRead(
self: *Lowering,
obj_expr: *const ast.Node,
info: ObjcDefinedStateField,
) Ref {
const obj_ref = self.lowerExpr(obj_expr);
const state_ptr = self.lowerObjcDefinedStateForObj(obj_ref, info.fcd) orelse return Ref.none;
const ptr_void = self.module.types.ptrTo(.void);
const field_addr = self.builder.emit(.{ .struct_gep = .{
.base = state_ptr,
.field_index = info.field_idx,
.base_type = info.state_ty,
} }, ptr_void);
return self.builder.load(field_addr, info.field_ty);
}
/// `state = object_getIvar(obj, load(__<Cls>_state_ivar))`. Shared
/// helper for state-field read + write (M1.2 A.3).
pub fn lowerObjcDefinedStateForObj(self: *Lowering, obj_ref: Ref, fcd: *const ast.RuntimeClassDecl) ?Ref {
const ptr_void = self.module.types.ptrTo(.void);
const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return null;
defer self.alloc.free(ivar_global_name);
const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse return null;
const ivar_addr = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void);
const ivar_handle = self.builder.load(ivar_addr, ptr_void);
const get_ivar_fid = self.ensureCRuntimeDecl("object_getIvar", &.{ ptr_void, ptr_void }, ptr_void);
const args = self.alloc.alloc(Ref, 2) catch return null;
args[0] = obj_ref;
args[1] = ivar_handle;
return self.builder.emit(.{ .call = .{ .callee = get_ivar_fid, .args = args } }, ptr_void);
}
/// Lower `obj.field` for an Obj-C `#property` field as
/// `objc_msg_send(obj, sel_<fieldName>)`. M2.2 — getter side.
/// The setter side lives in the assignment-statement lowering.
pub fn lowerObjcPropertyGetter(self: *Lowering, obj_expr: *const ast.Node, field: ast.RuntimeFieldDecl, _: []const u8, _: ast.Span) Ref {
const obj_ref = self.lowerExpr(obj_expr);
const ret_ty = self.resolveType(field.field_type);
const vptr_ty = self.module.types.ptrTo(.void);
// The selector for a property getter is the field name verbatim
// (Obj-C convention; the override hook is for niche cases like
// `isHidden` and lands with M2.2's modifier handling).
const sel_slot_gid = self.internObjcSelector(field.name);
const slot_ptr = self.builder.emit(.{ .global_addr = sel_slot_gid }, self.module.types.ptrTo(vptr_ty));
const sel = self.builder.emit(.{ .load = .{ .operand = slot_ptr } }, vptr_ty);
return self.builder.emit(.{ .objc_msg_send = .{
.recv = obj_ref,
.sel = sel,
.args = &.{},
} }, ret_ty);
}
/// Lower `obj.field = val` for an Obj-C `#property` field as
/// `objc_msg_send(obj, sel_set<Field>:, val)`. M2.2 — setter side.
/// Selector: prepend "set", capitalize the first letter of the
/// field name, append ":". `backgroundColor` → `setBackgroundColor:`.
pub fn lowerObjcPropertySetter(self: *Lowering, obj_expr: *const ast.Node, field: ast.RuntimeFieldDecl, val: Ref) void {
const obj_ref = self.lowerExpr(obj_expr);
const vptr_ty = self.module.types.ptrTo(.void);
// Build the setter selector.
var sel_buf = std.ArrayList(u8).empty;
defer sel_buf.deinit(self.alloc);
sel_buf.appendSlice(self.alloc, "set") catch unreachable;
if (field.name.len > 0) {
sel_buf.append(self.alloc, std.ascii.toUpper(field.name[0])) catch unreachable;
sel_buf.appendSlice(self.alloc, field.name[1..]) catch unreachable;
}
sel_buf.append(self.alloc, ':') catch unreachable;
const sel_str = self.alloc.dupe(u8, sel_buf.items) catch unreachable;
const sel_slot_gid = self.internObjcSelector(sel_str);
const slot_ptr = self.builder.emit(.{ .global_addr = sel_slot_gid }, self.module.types.ptrTo(vptr_ty));
const sel = self.builder.emit(.{ .load = .{ .operand = slot_ptr } }, vptr_ty);
const args = self.alloc.alloc(Ref, 1) catch unreachable;
args[0] = val;
_ = self.builder.emit(.{ .objc_msg_send = .{
.recv = obj_ref,
.sel = sel,
.args = args,
} }, .void);
}
/// Get a FuncId for an external C-callconv function. If a function
/// with this exported name already exists in the module (e.g.
/// declared by stdlib `extern` decl), return it; otherwise
/// declare it fresh with the given signature.
///
/// One helper instead of a `get<Name>Fid` per runtime function —
/// avoids per-function cache fields and per-function boilerplate.
pub fn ensureCRuntimeDecl(self: *Lowering, name: []const u8, param_tys: []const TypeId, ret_ty: TypeId) FuncId {
const name_id = self.module.types.internString(name);
for (self.module.functions.items, 0..) |f, i| {
if (f.name == name_id) return FuncId.fromIndex(@intCast(i));
}
var params = std.ArrayList(inst_mod.Function.Param).empty;
for (param_tys, 0..) |pty, i| {
// Param names don't matter at the LLVM ABI boundary —
// synthesize generic ones (`a0`, `a1`, ...) so we don't
// need a parallel name list per call site.
const synth = std.fmt.allocPrint(self.alloc, "a{d}", .{i}) catch unreachable;
params.append(self.alloc, .{
.name = self.module.types.internString(synth),
.ty = pty,
}) catch unreachable;
}
const fid = self.builder.declareExtern(name_id, params.toOwnedSlice(self.alloc) catch unreachable, ret_ty);
self.module.getFunctionMut(fid).call_conv = .c;
return fid;
}
/// For each bodied instance method on a sx-defined `#objc_class`,
/// emit a C-ABI IMP trampoline that the Obj-C runtime calls (after
/// the dispatch path from `objc_msgSend`). The trampoline:
/// 1. Loads the cached ivar handle from `@__<Cls>_state_ivar`.
/// 2. Calls `object_getIvar(obj, ivar)` to get the `*<Cls>State`
/// state pointer.
/// 3. Calls the sx body `@<Cls>.<method>(__sx_default_context,
/// state, ...user_args)` (default sx-callconv).
/// 4. Returns the result (or `ret void`).
///
/// IMP name: `__<ClassName>_<methodName>_imp`. emit_llvm's
/// constructor (A.4b.ii companion) registers this via
/// `class_addMethod` with a derived selector + type encoding.
pub fn emitObjcDefinedClassImps(self: *Lowering) void {
for (self.module.objc_defined_class_cache.items) |entry| {
const fcd = entry.decl;
// Pin to the class's defining module (E4) so the IMP trampolines'
// method-signature types (`-> BOOL`, param types) resolve where they
// are visible, not at whatever lowering site triggered emission.
const saved_src = self.current_source_file;
defer self.setCurrentSourceFile(saved_src);
if (fcd.source_file) |src| self.setCurrentSourceFile(src);
// Synthesize +alloc (M1.2 A.5) and -dealloc (M1.2 A.6). emit_llvm
// registers +alloc on the metaclass and -dealloc on the class
// itself after objc_registerClassPair.
self.emitObjcDefinedClassAllocImp(fcd);
self.emitObjcDefinedClassDeallocImp(fcd);
for (fcd.members) |m| {
switch (m) {
.method => |method| {
if (method.body == null) continue;
self.emitObjcDefinedClassImp(fcd, method);
},
.field => |field| {
// M2.2 second pass — sx-defined property fields
// synthesize getter (+ setter unless `readonly`)
// IMPs that GEP into the state struct.
if (field.is_property) {
self.emitObjcDefinedClassPropertyImps(fcd, field);
}
},
else => {},
}
}
}
}
/// Lazily declare libobjc's ARC runtime helpers. Idempotent — uses
/// `ensureCRuntimeDecl` which skips already-declared symbols. Called
/// from the property setter/getter and -dealloc emission paths when
/// they need to emit a retain/release/storeWeak/etc.
pub fn ensureArcRuntimeDecls(self: *Lowering) void {
const ptr_void = self.module.types.ptrTo(.void);
_ = self.ensureCRuntimeDecl("objc_retain", &.{ptr_void}, ptr_void);
_ = self.ensureCRuntimeDecl("objc_release", &.{ptr_void}, .void);
_ = self.ensureCRuntimeDecl("objc_storeWeak", &.{ ptr_void, ptr_void }, ptr_void);
_ = self.ensureCRuntimeDecl("objc_loadWeakRetained", &.{ptr_void}, ptr_void);
_ = self.ensureCRuntimeDecl("objc_initWeak", &.{ ptr_void, ptr_void }, ptr_void);
_ = self.ensureCRuntimeDecl("objc_destroyWeak", &.{ptr_void}, .void);
}
/// M2.2 second pass — emit synthesized getter/setter IMPs for a
/// property field on a sx-defined `#objc_class`. The state struct
/// already holds the field (via objcDefinedStateStructType); the
/// IMPs just dispatch a load/store through the `__sx_state` ivar.
///
/// Getter IMP: `__<Cls>_<field>_imp(self, _cmd) -> T`
/// state = object_getIvar(self, load(__<Cls>_state_ivar))
/// return state.<field>
///
/// Setter IMP (skipped if `readonly` in modifiers):
/// `__<Cls>_set<Field>_imp(self, _cmd, val) -> void`
/// state = object_getIvar(self, load(__<Cls>_state_ivar))
/// state.<field> = val
///
/// Both IMPs land in the cache's methods slice with appropriate
/// selectors + encodings; emit_llvm's class_addMethod loop wires
/// them up like any other instance method.
pub fn emitObjcDefinedClassPropertyImps(self: *Lowering, fcd: *const ast.RuntimeClassDecl, field: ast.RuntimeFieldDecl) void {
const state_ty = self.objc().objcDefinedStateStructType(fcd);
const state_info = self.module.types.get(state_ty);
if (state_info != .@"struct") return;
// Find the field's index in the state struct.
const field_name_id = self.module.types.internString(field.name);
var field_idx: ?u32 = null;
for (state_info.@"struct".fields, 0..) |sf, i| {
if (sf.name == field_name_id) {
field_idx = @intCast(i);
break;
}
}
const fidx = field_idx orelse return;
const field_ty = self.resolveType(field.field_type);
// M4.B: validate modifiers + resolve ARC kind. Side-effect: emits
// diagnostics for typos, weak-on-non-object, ambiguous *void, etc.
// For now the setter/getter still emit bare load/store; subsequent
// M4.B commits wire the actual ARC ops keyed on this kind.
_ = self.objc().objcPropertyKind(field);
// (1) Getter: __<Cls>_<field>_imp
self.emitObjcDefinedPropertyGetter(fcd, field, state_ty, fidx, field_ty);
// (2) Setter — skipped for `readonly`.
var is_readonly = false;
for (field.property_modifiers) |mod| {
if (std.mem.eql(u8, mod, "readonly")) {
is_readonly = true;
break;
}
}
if (!is_readonly) {
self.emitObjcDefinedPropertySetter(fcd, field, state_ty, fidx, field_ty);
}
// (3) Register in the cache's methods slice. Both IMPs use the
// method-registration pipeline that lands in class_addMethod
// calls from emit_llvm.
self.registerObjcDefinedPropertyMethodEntries(fcd, field, field_ty, is_readonly);
}
pub fn emitObjcDefinedPropertyGetter(self: *Lowering, fcd: *const ast.RuntimeClassDecl, field: ast.RuntimeFieldDecl, state_ty: TypeId, fidx: u32, field_ty: TypeId) void {
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;
}
const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, field.name }) catch return;
const name_id = self.module.types.internString(imp_name);
const ptr_void = self.module.types.ptrTo(.void);
var params = std.ArrayList(inst_mod.Function.Param).empty;
params.append(self.alloc, .{ .name = self.module.types.internString("self"), .ty = ptr_void }) catch return;
params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return;
const params_slice = params.toOwnedSlice(self.alloc) catch return;
_ = self.builder.beginFunction(name_id, params_slice, field_ty);
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);
// state = object_getIvar(self, load @__<Cls>_state_ivar)
const self_ref = Ref.fromIndex(0);
const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return;
defer self.alloc.free(ivar_global_name);
const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse return;
const ivar_addr = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void);
const ivar_handle = self.builder.load(ivar_addr, ptr_void);
const get_ivar_fid = self.ensureCRuntimeDecl("object_getIvar", &.{ ptr_void, ptr_void }, ptr_void);
const get_args = self.alloc.alloc(Ref, 2) catch return;
get_args[0] = self_ref;
get_args[1] = ivar_handle;
const state_ptr = self.builder.emit(.{ .call = .{ .callee = get_ivar_fid, .args = get_args } }, ptr_void);
const field_addr = self.builder.emit(.{ .struct_gep = .{ .base = state_ptr, .field_index = fidx, .base_type = state_ty } }, ptr_void);
// M4.B getter — weak fields go through objc_loadWeakRetained +
// objc_autorelease for race-safe reads. The bare-load path
// (strong/copy/assign) is the common case and reads the slot
// directly.
const kind = self.objc().objcPropertyKind(field);
if (kind == .weak) {
self.ensureArcRuntimeDecls();
const load_weak_fid = self.ensureCRuntimeDecl("objc_loadWeakRetained", &.{ptr_void}, ptr_void);
const autorelease_fid = self.ensureCRuntimeDecl("objc_autorelease", &.{ptr_void}, ptr_void);
// retained = objc_loadWeakRetained(field_addr)
// - atomic upgrade-to-strong via libobjc's side-table; if the
// target deinitialised, returns null. The caller gets a
// +1 retained reference (or null).
const load_args = self.alloc.alloc(Ref, 1) catch return;
load_args[0] = field_addr;
const retained = self.builder.emit(.{ .call = .{ .callee = load_weak_fid, .args = load_args } }, ptr_void);
// autoreleased = objc_autorelease(retained)
// - drops it into the current pool so the caller doesn't need
// to manually release. Returns the same pointer (typed).
const ar_args = self.alloc.alloc(Ref, 1) catch return;
ar_args[0] = retained;
const autoreleased = self.builder.emit(.{ .call = .{ .callee = autorelease_fid, .args = ar_args } }, ptr_void);
self.builder.ret(autoreleased, field_ty);
self.builder.finalize();
return;
}
// strong / copy / assign — bare load.
const val = self.builder.load(field_addr, field_ty);
self.builder.ret(val, field_ty);
self.builder.finalize();
}
pub fn emitObjcDefinedPropertySetter(self: *Lowering, fcd: *const ast.RuntimeClassDecl, field: ast.RuntimeFieldDecl, state_ty: TypeId, fidx: u32, field_ty: TypeId) void {
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;
}
// Setter selector: set<Field>: → imp name: __<Cls>_set<Field>_imp
var setter_field_buf = std.ArrayList(u8).empty;
defer setter_field_buf.deinit(self.alloc);
setter_field_buf.appendSlice(self.alloc, "set") catch unreachable;
if (field.name.len > 0) {
setter_field_buf.append(self.alloc, std.ascii.toUpper(field.name[0])) catch unreachable;
setter_field_buf.appendSlice(self.alloc, field.name[1..]) catch unreachable;
}
const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, setter_field_buf.items }) catch return;
const name_id = self.module.types.internString(imp_name);
const ptr_void = self.module.types.ptrTo(.void);
var params = std.ArrayList(inst_mod.Function.Param).empty;
params.append(self.alloc, .{ .name = self.module.types.internString("self"), .ty = ptr_void }) catch return;
params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return;
params.append(self.alloc, .{ .name = self.module.types.internString("val"), .ty = field_ty }) catch return;
const params_slice = params.toOwnedSlice(self.alloc) catch return;
_ = self.builder.beginFunction(name_id, params_slice, .void);
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);
const self_ref = Ref.fromIndex(0);
const val_ref = Ref.fromIndex(2);
const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return;
defer self.alloc.free(ivar_global_name);
const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse return;
const ivar_addr = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void);
const ivar_handle = self.builder.load(ivar_addr, ptr_void);
const get_ivar_fid = self.ensureCRuntimeDecl("object_getIvar", &.{ ptr_void, ptr_void }, ptr_void);
const get_args = self.alloc.alloc(Ref, 2) catch return;
get_args[0] = self_ref;
get_args[1] = ivar_handle;
const state_ptr = self.builder.emit(.{ .call = .{ .callee = get_ivar_fid, .args = get_args } }, ptr_void);
const field_addr = self.builder.emit(.{ .struct_gep = .{ .base = state_ptr, .field_index = fidx, .base_type = state_ty } }, ptr_void);
// M4.B setter — emit ARC ops based on the property's modifier kind.
const kind = self.objc().objcPropertyKind(field);
switch (kind) {
.assign => {
// Primitives or explicit assign: bare store, no ARC.
self.builder.store(field_addr, val_ref);
},
.strong => {
// Retain new, release old. Order matters: retain first
// (in case val == old, we don't release before retain).
self.ensureArcRuntimeDecls();
const retain_fid = self.ensureCRuntimeDecl("objc_retain", &.{ptr_void}, ptr_void);
const release_fid = self.ensureCRuntimeDecl("objc_release", &.{ptr_void}, .void);
// old = load field_addr
const old_val = self.builder.load(field_addr, field_ty);
// new = objc_retain(val)
const retain_args = self.alloc.alloc(Ref, 1) catch return;
retain_args[0] = val_ref;
_ = self.builder.emit(.{ .call = .{ .callee = retain_fid, .args = retain_args } }, ptr_void);
// store field_addr, val
self.builder.store(field_addr, val_ref);
// objc_release(old) — Apple's runtime treats release(NULL) as a no-op,
// so we skip an explicit null-check (saves a branch on every assign).
const release_args = self.alloc.alloc(Ref, 1) catch return;
release_args[0] = old_val;
_ = self.builder.emit(.{ .call = .{ .callee = release_fid, .args = release_args } }, .void);
},
.weak => {
// objc_storeWeak(field_addr, val) handles first-store
// (init) and re-store (destroy old + init new) atomically.
self.ensureArcRuntimeDecls();
const store_weak_fid = self.ensureCRuntimeDecl("objc_storeWeak", &.{ ptr_void, ptr_void }, ptr_void);
const store_args = self.alloc.alloc(Ref, 2) catch return;
store_args[0] = field_addr;
store_args[1] = val_ref;
_ = self.builder.emit(.{ .call = .{ .callee = store_weak_fid, .args = store_args } }, ptr_void);
},
.copy => {
// copy = objc_msgSend(val, sel_copy) — returns retained
// (NSCopying contract).
// Release old, then store the copy.
self.ensureArcRuntimeDecls();
const release_fid = self.ensureCRuntimeDecl("objc_release", &.{ptr_void}, .void);
// Load + cache the `copy` selector slot.
const sel_copy_gid = self.internObjcSelector("copy");
const sel_slot_ptr = self.builder.emit(.{ .global_addr = sel_copy_gid }, self.module.types.ptrTo(ptr_void));
const sel_copy = self.builder.emit(.{ .load = .{ .operand = sel_slot_ptr } }, ptr_void);
// copy = [val copy]
const copy_args = self.alloc.alloc(Ref, 0) catch return;
const copied = self.builder.emit(.{ .objc_msg_send = .{
.recv = val_ref,
.sel = sel_copy,
.args = copy_args,
} }, ptr_void);
const old_val = self.builder.load(field_addr, field_ty);
self.builder.store(field_addr, copied);
const release_args = self.alloc.alloc(Ref, 1) catch return;
release_args[0] = old_val;
_ = self.builder.emit(.{ .call = .{ .callee = release_fid, .args = release_args } }, .void);
},
}
self.builder.retVoid();
self.builder.finalize();
}
/// Append the property's getter (and setter, unless readonly)
/// entries to the class's method-registration slice so emit_llvm
/// calls class_addMethod on each. Selectors + encodings derived
/// from the field type.
pub fn registerObjcDefinedPropertyMethodEntries(self: *Lowering, fcd: *const ast.RuntimeClassDecl, field: ast.RuntimeFieldDecl, field_ty: TypeId, is_readonly: bool) void {
const cur = self.module.lookupObjcDefinedClass(fcd.name) orelse return;
_ = cur;
// Find the existing entry and grow its methods slice.
var new_methods = std.ArrayList(Module.ObjcDefinedMethodEntry).empty;
for (self.module.objc_defined_class_cache.items) |entry| {
if (!std.mem.eql(u8, entry.name, fcd.name)) continue;
for (entry.methods) |m| new_methods.append(self.alloc, m) catch unreachable;
// Getter entry — selector = field name, encoding = "<ret>@:".
const getter_enc = self.objc().objcTypeEncodingFromSignature(field_ty, &.{}, null) catch return;
const getter_imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, field.name }) catch return;
new_methods.append(self.alloc, .{
.sel = field.name,
.encoding = getter_enc,
.imp_name = getter_imp_name,
.is_class = false,
}) catch unreachable;
// Setter entry — selector = set<Field>:, encoding = "v@:<ty>".
if (!is_readonly) {
var sel_buf = std.ArrayList(u8).empty;
defer sel_buf.deinit(self.alloc);
sel_buf.appendSlice(self.alloc, "set") catch unreachable;
if (field.name.len > 0) {
sel_buf.append(self.alloc, std.ascii.toUpper(field.name[0])) catch unreachable;
sel_buf.appendSlice(self.alloc, field.name[1..]) catch unreachable;
}
sel_buf.append(self.alloc, ':') catch unreachable;
const setter_sel = self.alloc.dupe(u8, sel_buf.items) catch return;
const setter_enc = self.objc().objcTypeEncodingFromSignature(.void, &.{field_ty}, null) catch return;
var setter_imp_field_buf = std.ArrayList(u8).empty;
defer setter_imp_field_buf.deinit(self.alloc);
setter_imp_field_buf.appendSlice(self.alloc, "set") catch unreachable;
if (field.name.len > 0) {
setter_imp_field_buf.append(self.alloc, std.ascii.toUpper(field.name[0])) catch unreachable;
setter_imp_field_buf.appendSlice(self.alloc, field.name[1..]) catch unreachable;
}
const setter_imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, setter_imp_field_buf.items }) catch return;
new_methods.append(self.alloc, .{
.sel = setter_sel,
.encoding = setter_enc,
.imp_name = setter_imp_name,
.is_class = false,
}) catch unreachable;
}
break;
}
const slice = new_methods.toOwnedSlice(self.alloc) catch return;
self.module.setObjcDefinedClassMethods(fcd.name, slice);
}
pub fn emitObjcDefinedClassImp(self: *Lowering, fcd: *const ast.RuntimeClassDecl, md: ast.RuntimeMethodDecl) void {
// Class methods (no `*Self` first param) skip the ivar read —
// they have no instance state to thread through.
if (md.is_static) {
self.emitObjcDefinedClassStaticImp(fcd, md);
return;
}
// Save+restore builder state — we're switching into a new fn
// mid-pass and need to restore for the next emit_llvm steps.
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;
}
const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, md.name }) catch return;
const name_id = self.module.types.internString(imp_name);
const ptr_void = self.module.types.ptrTo(.void);
// C-ABI signature: (obj: *void, _cmd: *void, ...user_args) -> ret.
// User params skip index 0 (which is *Self).
var params = std.ArrayList(inst_mod.Function.Param).empty;
params.append(self.alloc, .{ .name = self.module.types.internString("obj"), .ty = ptr_void }) catch return;
params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return;
// Set current_runtime_class so *Self in user-param resolution
// resolves to *<Cls>State (M1.2 A.2b). Save+restore.
const saved_fc = self.current_runtime_class;
self.current_runtime_class = fcd;
defer self.current_runtime_class = saved_fc;
const param_start: usize = 1;
for (md.params[param_start..], 0..) |p_node, i| {
// User params are reflected at the C-ABI boundary AS-IS —
// the runtime trampoline forwards them through to the body.
// *Self here would be a programming error (only the implicit
// self at index 0 is *Self), but we use resolveType to handle
// pointer types correctly.
const pty = self.resolveType(p_node);
params.append(self.alloc, .{
.name = self.module.types.internString(md.param_names[param_start + i]),
.ty = pty,
}) catch return;
}
const ret_ty: TypeId = if (md.return_type) |rt| self.resolveType(rt) else .void;
const params_slice = params.toOwnedSlice(self.alloc) catch return;
_ = self.builder.beginFunction(name_id, params_slice, ret_ty);
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);
// Pass the Obj-C receiver pointer through to the sx body as
// `self`. The body's `self: *Self` type resolves to the
// runtime-class stub (the opaque Obj-C type), matching Apple's
// Obj-C semantics where `self` IS the object. `self.field`
// access on a sx-defined class is rewritten by lowerFieldAccess
// to go through `object_getIvar(self, __sx_state_ivar)` and
// a struct_gep on the state struct — see M1.2 A.3.
const obj_ref = Ref.fromIndex(0);
// Call sx body `@<Cls>.<method>(default_ctx, self, ...user_args)`.
const body_name = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fcd.name, md.name }) catch return;
defer self.alloc.free(body_name);
const body_fid = self.resolveFuncByName(body_name) orelse return;
const ctx_ref: ?Ref = blk: {
if (!self.implicit_ctx_enabled) break :blk null;
const dctx_gi = self.program_index.global_names.get("__sx_default_context") orelse break :blk null;
break :blk self.builder.emit(.{ .global_addr = dctx_gi.id }, ptr_void);
};
// Build arg list: [ctx?] + self + user_args.
const num_user_args = params_slice.len - 2; // minus obj + _cmd
const num_call_args = (if (ctx_ref != null) @as(usize, 1) else 0) + 1 + num_user_args;
const call_args = self.alloc.alloc(Ref, num_call_args) catch return;
var idx: usize = 0;
if (ctx_ref) |c_ref| {
call_args[idx] = c_ref;
idx += 1;
}
call_args[idx] = obj_ref;
idx += 1;
var ip: usize = 2;
while (ip < params_slice.len) : (ip += 1) {
call_args[idx] = Ref.fromIndex(@intCast(ip));
idx += 1;
}
const call_ref = self.builder.emit(.{ .call = .{
.callee = body_fid,
.args = call_args,
} }, ret_ty);
// (4) Return.
if (ret_ty == .void) {
self.builder.retVoid();
} else {
self.builder.ret(call_ref, ret_ty);
}
self.builder.finalize();
}
/// Synthesize the `+alloc` IMP for an sx-defined `#objc_class`.
/// Class method registered on the metaclass — when `[SxFoo alloc]`
/// runs from Apple's runtime (Info.plist principal class,
/// NSCoder unarchive, UIKit reflection), this IMP fires.
///
/// C-ABI: `(cls: id, _cmd: SEL) -> id`. No implicit ctx.
///
/// Body (M4.0):
/// %instance = class_createInstance(cls, 0)
/// %ctx_addr = &__sx_default_context
/// %state = ctx_addr.allocator.alloc(STATE_SIZE)
/// memset(state, 0, STATE_SIZE)
/// state[0] = allocator ← capture for -dealloc
/// object_setIvar(instance, __sx_state_ivar, state)
/// ret instance
///
/// Sx-side `Cls.alloc()` is intercepted at the call site (see
/// `lowerObjcStaticCall`) and emits the same sequence inline with
/// `current_ctx_ref` as the ctx — so `push Context.{ allocator = ... }`
/// flows through to per-instance allocator capture without going via
/// the IMP.
pub fn emitObjcDefinedClassAllocImp(self: *Lowering, fcd: *const ast.RuntimeClassDecl) void {
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;
}
const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_alloc_imp", .{fcd.name}) catch return;
const name_id = self.module.types.internString(imp_name);
const ptr_void = self.module.types.ptrTo(.void);
var params = std.ArrayList(inst_mod.Function.Param).empty;
params.append(self.alloc, .{ .name = self.module.types.internString("cls"), .ty = ptr_void }) catch return;
params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return;
const params_slice = params.toOwnedSlice(self.alloc) catch return;
_ = self.builder.beginFunction(name_id, params_slice, ptr_void);
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);
// ctx_addr = &__sx_default_context — IMP runs in Apple's runtime
// context, no implicit sx ctx to inherit, so use the process-wide
// default allocator. Sx-side callers bypass this IMP entirely
// (compiler intercepts Cls.alloc()) and use their own
// `context.allocator`.
const default_ctx_gi = self.program_index.global_names.get("__sx_default_context") orelse {
if (self.diagnostics) |d| {
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedClassAllocImp: __sx_default_context global missing for class '{s}' (compiler bug — scan pass did not register the default context)", .{fcd.name});
}
return;
};
const ctx_addr = self.builder.emit(.{ .global_addr = default_ctx_gi.id }, ptr_void);
const cls_ref = Ref.fromIndex(0);
const instance = self.emitObjcDefinedAllocAndInit(fcd, cls_ref, ctx_addr) orelse return;
self.builder.ret(instance, ptr_void);
self.builder.finalize();
}
/// Shared inline sequence: allocate Obj-C instance + sx state struct,
/// capture the allocator, bind to the `__sx_state` ivar. Used by both
/// the `+alloc` IMP (ctx_addr = &__sx_default_context) and the sx-side
/// `Cls.alloc()` interception (ctx_addr = current_ctx_ref).
///
/// Returns the new instance pointer, or `null` if a required global is
/// missing (compiler bug — should be impossible after scan pass).
pub fn emitObjcDefinedAllocAndInit(
self: *Lowering,
fcd: *const ast.RuntimeClassDecl,
cls_ref: Ref,
ctx_addr: Ref,
) ?Ref {
const ptr_void = self.module.types.ptrTo(.void);
// (1) instance = class_createInstance(cls, 0)
const create_fid = self.ensureCRuntimeDecl("class_createInstance", &.{ ptr_void, .u64 }, ptr_void);
const create_args = self.alloc.alloc(Ref, 2) catch return null;
create_args[0] = cls_ref;
create_args[1] = self.builder.constInt(0, .u64);
const instance = self.builder.emit(.{ .call = .{ .callee = create_fid, .args = create_args } }, ptr_void);
// STATE_SIZE = max(typeSizeBytes(__<Cls>State), 1).
const state_struct_ty = self.objc().objcDefinedStateStructType(fcd);
const raw_size = self.module.types.typeSizeBytes(state_struct_ty);
const state_size: u64 = if (raw_size == 0) 1 else @intCast(raw_size);
const size_const = self.builder.constInt(@intCast(state_size), .u64);
// (2) Dispatch through Context.allocator at ctx_addr:
// allocator = (*ctx_addr).field[0]
// state = allocator.alloc(size) (via inline-protocol fn-ptr)
const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse {
if (self.diagnostics) |d| {
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedAllocAndInit: Context type not found in module for class '{s}' (compiler bug)", .{fcd.name});
}
return null;
};
const ctx_info = self.module.types.get(ctx_ty);
if (ctx_info != .@"struct" or ctx_info.@"struct".fields.len < 1) {
if (self.diagnostics) |d| {
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedAllocAndInit: Context has unexpected shape for class '{s}' (compiler bug)", .{fcd.name});
}
return null;
}
const allocator_ty = ctx_info.@"struct".fields[0].ty;
const ctx_val = self.builder.load(ctx_addr, ctx_ty);
const allocator = self.builder.structGet(ctx_val, 0, allocator_ty);
const alloc_ctx = self.builder.structGet(allocator, 0, ptr_void);
const alloc_fn_ptr = self.builder.structGet(allocator, 1, ptr_void);
const call_args = self.alloc.dupe(Ref, &.{ ctx_addr, alloc_ctx, size_const }) catch return null;
const state = self.builder.emit(.{ .call_indirect = .{
.callee = alloc_fn_ptr,
.args = call_args,
} }, ptr_void);
// (3) memset(state, 0, STATE_SIZE) — zero everything including the
// allocator slot; the next store re-writes the allocator slot.
const memset_fid = self.ensureCRuntimeDecl("memset", &.{ ptr_void, .i32, .u64 }, ptr_void);
const memset_args = self.alloc.alloc(Ref, 3) catch return null;
memset_args[0] = state;
memset_args[1] = self.builder.constInt(0, .i32);
memset_args[2] = size_const;
_ = self.builder.emit(.{ .call = .{ .callee = memset_fid, .args = memset_args } }, ptr_void);
// (4) Capture allocator at state[0] — `-dealloc` reads it back.
const state_alloc_addr = self.builder.emit(.{ .struct_gep = .{
.base = state,
.field_index = 0,
.base_type = state_struct_ty,
} }, ptr_void);
self.builder.store(state_alloc_addr, allocator);
// (5) object_setIvar(instance, load(@__<Cls>_state_ivar), state)
const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return null;
defer self.alloc.free(ivar_global_name);
const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse {
if (self.diagnostics) |d| {
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedAllocAndInit: ivar global '{s}' missing (scan-pass bug)", .{ivar_global_name});
}
return null;
};
const ivar_addr_v = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void);
const ivar_handle = self.builder.load(ivar_addr_v, ptr_void);
const set_ivar_fid = self.ensureCRuntimeDecl("object_setIvar", &.{ ptr_void, ptr_void, ptr_void }, .void);
const set_args = self.alloc.alloc(Ref, 3) catch return null;
set_args[0] = instance;
set_args[1] = ivar_handle;
set_args[2] = state;
_ = self.builder.emit(.{ .call = .{ .callee = set_ivar_fid, .args = set_args } }, .void);
return instance;
}
/// Emit a C-ABI IMP trampoline for a CLASS method (no `*Self`
/// first param) on a sx-defined `#objc_class`. M2.1(b).
/// Registered on the metaclass by emit_llvm.
///
/// C-ABI: `(cls: Class, _cmd: SEL, ...user_args) -> ret`
///
/// Body:
/// call @<Cls>.<method>(__sx_default_context, ...user_args)
/// ret <result>
///
/// No ivar read — class methods have no per-instance state.
pub fn emitObjcDefinedClassStaticImp(self: *Lowering, fcd: *const ast.RuntimeClassDecl, md: ast.RuntimeMethodDecl) void {
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;
}
const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, md.name }) catch return;
const name_id = self.module.types.internString(imp_name);
const ptr_void = self.module.types.ptrTo(.void);
var params = std.ArrayList(inst_mod.Function.Param).empty;
params.append(self.alloc, .{ .name = self.module.types.internString("cls"), .ty = ptr_void }) catch return;
params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return;
// current_runtime_class lets `*Self` (if it appears in
// user-arg types — rare for class methods) resolve to the
// state-struct type. Save+restore.
const saved_fc = self.current_runtime_class;
self.current_runtime_class = fcd;
defer self.current_runtime_class = saved_fc;
for (md.params, 0..) |p_node, i| {
const pty = self.resolveType(p_node);
params.append(self.alloc, .{
.name = self.module.types.internString(md.param_names[i]),
.ty = pty,
}) catch return;
}
const ret_ty: TypeId = if (md.return_type) |rt| self.resolveType(rt) else .void;
const params_slice = params.toOwnedSlice(self.alloc) catch return;
_ = self.builder.beginFunction(name_id, params_slice, ret_ty);
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);
// Call @<Cls>.<method>(default_ctx, ...user_args).
const body_name = std.fmt.allocPrint(self.alloc, "{s}.{s}", .{ fcd.name, md.name }) catch return;
defer self.alloc.free(body_name);
const body_fid = self.resolveFuncByName(body_name) orelse return;
const ctx_ref: ?Ref = blk: {
if (!self.implicit_ctx_enabled) break :blk null;
const dctx_gi = self.program_index.global_names.get("__sx_default_context") orelse break :blk null;
break :blk self.builder.emit(.{ .global_addr = dctx_gi.id }, ptr_void);
};
const num_user_args = params_slice.len - 2; // minus cls + _cmd
const num_call_args = (if (ctx_ref != null) @as(usize, 1) else 0) + num_user_args;
const call_args = self.alloc.alloc(Ref, num_call_args) catch return;
var idx: usize = 0;
if (ctx_ref) |c_ref| {
call_args[idx] = c_ref;
idx += 1;
}
var ip: usize = 2;
while (ip < params_slice.len) : (ip += 1) {
call_args[idx] = Ref.fromIndex(@intCast(ip));
idx += 1;
}
const call_ref = self.builder.emit(.{ .call = .{
.callee = body_fid,
.args = call_args,
} }, ret_ty);
if (ret_ty == .void) self.builder.retVoid() else self.builder.ret(call_ref, ret_ty);
self.builder.finalize();
}
/// Synthesize the `-dealloc` IMP for an sx-defined `#objc_class`.
/// Runs when the Obj-C runtime drops the last retain on an instance.
///
/// C-ABI: `(self: id, _cmd: SEL) -> void`. No implicit sx ctx.
///
/// Body (M4.0c):
/// %state = object_getIvar(self, load @__<Cls>_state_ivar)
/// %allocator = load struct_gep(state, 0) ← __sx_allocator (M4.0a)
/// allocator.dealloc(state) ← via inline-protocol fn-ptr
/// object_setIvar(self, ivar, null)
/// [super dealloc] // objc_msgSendSuper2(&super, sel_dealloc)
/// ret void
///
/// The state struct's first field is the allocator captured at
/// +alloc time (M4.0a + M4.0b). Reading it back lets -dealloc free
/// through the same allocator the instance was constructed with —
/// the per-instance allocator design from M1.2 A.5, now realised.
pub fn emitObjcDefinedClassDeallocImp(self: *Lowering, fcd: *const ast.RuntimeClassDecl) void {
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;
}
const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_dealloc_imp", .{fcd.name}) catch return;
const name_id = self.module.types.internString(imp_name);
const ptr_void = self.module.types.ptrTo(.void);
var params = std.ArrayList(inst_mod.Function.Param).empty;
params.append(self.alloc, .{ .name = self.module.types.internString("self"), .ty = ptr_void }) catch return;
params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return;
const params_slice = params.toOwnedSlice(self.alloc) catch return;
_ = self.builder.beginFunction(name_id, params_slice, .void);
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);
const self_ref = Ref.fromIndex(0);
// (1) state = object_getIvar(self, load @__<Cls>_state_ivar)
const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return;
defer self.alloc.free(ivar_global_name);
const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse return;
const ivar_addr = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void);
const ivar_handle = self.builder.load(ivar_addr, ptr_void);
const get_ivar_fid = self.ensureCRuntimeDecl("object_getIvar", &.{ ptr_void, ptr_void }, ptr_void);
const get_args = self.alloc.alloc(Ref, 2) catch return;
get_args[0] = self_ref;
get_args[1] = ivar_handle;
const state = self.builder.emit(.{ .call = .{ .callee = get_ivar_fid, .args = get_args } }, ptr_void);
// (2) M4.B dealloc — release strong/copy property ivars and
// destroyWeak weak property ivars BEFORE freeing the state struct
// (which would invalidate the pointers we need to read). Property
// metadata is re-derived from `fcd.members`; the state struct is
// already interned via objcDefinedStateStructType.
const state_struct_ty = self.objc().objcDefinedStateStructType(fcd);
const state_info_check = self.module.types.get(state_struct_ty);
if (state_info_check == .@"struct") {
const state_fields = state_info_check.@"struct".fields;
for (fcd.members) |m| switch (m) {
.field => |f| {
if (!f.is_property) continue;
// Find the field index in the state struct (by name —
// M4.0a's prepended __sx_allocator shifted user fields).
const field_name_id = self.module.types.internString(f.name);
var pfidx: ?u32 = null;
for (state_fields, 0..) |sf, i| {
if (sf.name == field_name_id) {
pfidx = @intCast(i);
break;
}
}
const fidx = pfidx orelse continue;
const field_ty = self.resolveType(f.field_type);
const kind = self.objc().objcPropertyKind(f);
switch (kind) {
.assign => {}, // no ARC ops
.strong, .copy => {
// val = load field; objc_release(val) — release(NULL) is a no-op.
self.ensureArcRuntimeDecls();
const release_fid = self.ensureCRuntimeDecl("objc_release", &.{ptr_void}, .void);
const field_addr = self.builder.emit(.{ .struct_gep = .{
.base = state,
.field_index = fidx,
.base_type = state_struct_ty,
} }, ptr_void);
const val = self.builder.load(field_addr, field_ty);
const args = self.alloc.alloc(Ref, 1) catch continue;
args[0] = val;
_ = self.builder.emit(.{ .call = .{ .callee = release_fid, .args = args } }, .void);
},
.weak => {
// objc_destroyWeak(&field) — unregisters the slot
// from libobjc's side-table.
self.ensureArcRuntimeDecls();
const destroy_weak_fid = self.ensureCRuntimeDecl("objc_destroyWeak", &.{ptr_void}, .void);
const field_addr = self.builder.emit(.{ .struct_gep = .{
.base = state,
.field_index = fidx,
.base_type = state_struct_ty,
} }, ptr_void);
const args = self.alloc.alloc(Ref, 1) catch continue;
args[0] = field_addr;
_ = self.builder.emit(.{ .call = .{ .callee = destroy_weak_fid, .args = args } }, .void);
},
}
},
else => {},
};
}
// (3) Free state through the captured allocator (M4.0a + M4.0b):
// allocator = load struct_gep(state, 0) ← __sx_allocator field
// allocator.dealloc(state) ← inline-protocol fn-ptr at field 2
// Compare to the old `free(state)` — that ignored the per-instance
// allocator and went straight to libc. Now `push Context.{ allocator = arena }`
// round-trips correctly: arena.alloc on construction, arena.dealloc here.
const ctx_ty = self.module.types.findByName(self.module.types.internString("Context")) orelse {
if (self.diagnostics) |d| {
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedClassDeallocImp: Context type not found for class '{s}' (compiler bug)", .{fcd.name});
}
return;
};
const ctx_info = self.module.types.get(ctx_ty);
if (ctx_info != .@"struct" or ctx_info.@"struct".fields.len < 1) {
if (self.diagnostics) |d| {
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedClassDeallocImp: Context has unexpected shape for class '{s}'", .{fcd.name});
}
return;
}
const allocator_ty = ctx_info.@"struct".fields[0].ty;
const state_alloc_addr = self.builder.emit(.{ .struct_gep = .{
.base = state,
.field_index = 0,
.base_type = state_struct_ty,
} }, ptr_void);
const allocator = self.builder.load(state_alloc_addr, allocator_ty);
// Default-context address for the implicit __sx_ctx the dealloc
// fn-ptr takes as its first arg (the dealloc body might allocate
// internally; default GPA is the safe baseline).
const default_ctx_gi = self.program_index.global_names.get("__sx_default_context") orelse {
if (self.diagnostics) |d| {
d.addFmt(.err, ast.Span{ .start = 0, .end = 0 }, "emitObjcDefinedClassDeallocImp: __sx_default_context global missing for class '{s}'", .{fcd.name});
}
return;
};
const default_ctx_addr = self.builder.emit(.{ .global_addr = default_ctx_gi.id }, ptr_void);
const alloc_ctx = self.builder.structGet(allocator, 0, ptr_void);
const dealloc_fn_ptr = self.builder.structGet(allocator, 2, ptr_void);
const dealloc_args = self.alloc.dupe(Ref, &.{ default_ctx_addr, alloc_ctx, state }) catch return;
_ = self.builder.emit(.{ .call_indirect = .{
.callee = dealloc_fn_ptr,
.args = dealloc_args,
} }, .void);
// (3) object_setIvar(self, ivar, null)
const set_ivar_fid = self.ensureCRuntimeDecl("object_setIvar", &.{ ptr_void, ptr_void, ptr_void }, .void);
const null_ptr = self.builder.constInt(0, ptr_void);
const set_args = self.alloc.alloc(Ref, 3) catch return;
set_args[0] = self_ref;
set_args[1] = ivar_handle;
set_args[2] = null_ptr;
_ = self.builder.emit(.{ .call = .{ .callee = set_ivar_fid, .args = set_args } }, .void);
// (4) [super dealloc]
//
// objc_super = struct { receiver: id, super_class: Class }
const super_struct_ty = self.module.types.intern(.{ .@"struct" = .{
.name = self.module.types.internString("__sx_objc_super"),
.fields = blk: {
var f = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
f.append(self.alloc, .{ .name = self.module.types.internString("receiver"), .ty = ptr_void }) catch unreachable;
f.append(self.alloc, .{ .name = self.module.types.internString("super_class"), .ty = ptr_void }) catch unreachable;
break :blk f.toOwnedSlice(self.alloc) catch unreachable;
},
} });
const super_alloca = self.builder.alloca(super_struct_ty);
// store receiver
const recv_gep = self.builder.emit(.{ .struct_gep = .{ .base = super_alloca, .field_index = 0, .base_type = super_struct_ty } }, ptr_void);
self.builder.store(recv_gep, self_ref);
// store super_class = load @__<Cls>_class
const class_global_name = std.fmt.allocPrint(self.alloc, "__{s}_class", .{fcd.name}) catch return;
defer self.alloc.free(class_global_name);
const class_global_id = self.lookupGlobalIdByName(class_global_name) orelse return;
const class_addr = self.builder.emit(.{ .global_addr = class_global_id }, ptr_void);
const class_val = self.builder.load(class_addr, ptr_void);
const cls_gep = self.builder.emit(.{ .struct_gep = .{ .base = super_alloca, .field_index = 1, .base_type = super_struct_ty } }, ptr_void);
self.builder.store(cls_gep, class_val);
// sel_dealloc = sel_registerName("dealloc")
const sel_reg_fid = self.ensureCRuntimeDecl("sel_registerName", &.{ptr_void}, ptr_void);
const sel_str_gid = self.internStringConstantGlobal("dealloc");
const sel_str_addr = self.builder.emit(.{ .global_addr = sel_str_gid }, ptr_void);
const sel_args = self.alloc.alloc(Ref, 1) catch return;
sel_args[0] = sel_str_addr;
const sel_dealloc = self.builder.emit(.{ .call = .{ .callee = sel_reg_fid, .args = sel_args } }, ptr_void);
// objc_msgSendSuper2(&super, sel_dealloc)
const send_super_fid = self.ensureCRuntimeDecl("objc_msgSendSuper2", &.{ ptr_void, ptr_void }, .void);
const send_args = self.alloc.alloc(Ref, 2) catch return;
send_args[0] = super_alloca;
send_args[1] = sel_dealloc;
_ = self.builder.emit(.{ .call = .{ .callee = send_super_fid, .args = send_args } }, .void);
self.builder.retVoid();
self.builder.finalize();
}
/// Intern a C-string constant as a `[N:0]u8` global and return
/// its GlobalId. Used by IMP trampolines that need to pass a
/// literal string to runtime helpers (e.g. selector names).
pub fn internStringConstantGlobal(self: *Lowering, s: []const u8) inst_mod.GlobalId {
const z = self.alloc.allocSentinel(u8, s.len, 0) catch unreachable;
@memcpy(z[0..s.len], s);
const arr_ty = self.module.types.arrayOf(.u8, @intCast(s.len + 1));
const slot_name = std.fmt.allocPrint(self.alloc, "__sx_objc_cstr_{s}", .{s}) catch unreachable;
const name_id = self.module.types.internString(slot_name);
if (self.lookupGlobalIdByName(slot_name)) |existing| {
self.alloc.free(z);
return existing;
}
var bytes_vec = std.ArrayList(inst_mod.ConstantValue).empty;
for (z[0 .. s.len + 1]) |b| {
bytes_vec.append(self.alloc, .{ .int = b }) catch unreachable;
}
const init_val: inst_mod.ConstantValue = .{ .aggregate = bytes_vec.toOwnedSlice(self.alloc) catch unreachable };
return self.module.addGlobal(.{
.name = name_id,
.ty = arr_ty,
.init_val = init_val,
.is_extern = false,
.is_const = true,
});
}
/// Linear scan over module globals for a given name. Used for
/// looking up the per-class ivar handle global from inside IMP
/// trampoline emission.
pub fn lookupGlobalIdByName(self: *Lowering, name: []const u8) ?inst_mod.GlobalId {
const name_id = self.module.types.internString(name);
for (self.module.globals.items, 0..) |g, i| {
if (g.name == name_id) return inst_mod.GlobalId.fromIndex(@intCast(i));
}
return null;
}