From f4faef97dde415e5ee56b50d28bdecac38269f2a Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 26 May 2026 23:02:08 +0300 Subject: [PATCH] ffi M4.B setter: emit ARC ops in sx-defined property setters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit emitObjcDefinedPropertySetter now dispatches on objcPropertyKind to emit the right runtime ops per Apple's ARC contract: - assign → bare store (primitives, explicitly opted-out object slots). - strong → load old; objc_retain(new); store new; objc_release(old). Apple's runtime treats release(NULL) as a safe no-op, so no explicit null-check on the old value. - weak → objc_storeWeak(field_addr, val) — handles first-store (init) and re-store (destroy + init) atomically. Registers the slot with libobjc's side-table; the runtime auto-nils it when the target deallocates. - copy → [val copy] (sends `copy` selector — returns retained per the NSCopying contract); load old; store the copied instance; release old. Side-effect on the weak path: even with the bare-load getter still in place (loaded directly from the slot), weak reads work because Apple's runtime side-table-nils the slot at target dealloc. The getter improvement via objc_loadWeakRetained is the next commit and is needed for race-safe reads (between load and use, the target could deinit on another thread); for the single-threaded test scenarios the bare load is sufficient. ffi-objc-arc-02-strong-property advances from "child dealloc'd at midpoint" to "unbalanced; alloc=2 dealloc=1" — strong setter now retains, but the M4.B-dealloc cleanup hasn't landed so the child held by the property isn't released when the parent deallocates. Final commit (M4.B dealloc) closes the loop. ffi-objc-arc-03-weak-property turns fully green: storeWeak + auto-nil side-table do the work. 189/189 example tests pass; chess on iOS-sim green. --- src/ir/lower.zig | 67 ++++++++++++++++++- .../ffi-objc-arc-02-strong-property.txt | 2 +- .../ffi-objc-arc-03-weak-property.exit | 2 +- .../ffi-objc-arc-03-weak-property.txt | 2 +- 4 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/ir/lower.zig b/src/ir/lower.zig index fe9c402..0a4d099 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -12315,7 +12315,72 @@ pub const Lowering = struct { 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); - self.builder.store(field_addr, val_ref); + + // M4.B setter — emit ARC ops based on the property's modifier kind. + const kind = self.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(); } diff --git a/tests/expected/ffi-objc-arc-02-strong-property.txt b/tests/expected/ffi-objc-arc-02-strong-property.txt index ff29713..114d680 100644 --- a/tests/expected/ffi-objc-arc-02-strong-property.txt +++ b/tests/expected/ffi-objc-arc-02-strong-property.txt @@ -1 +1 @@ -FAIL: child dealloc'd at midpoint (strong setter not retaining); delta=1 +FAIL: unbalanced; alloc=2 dealloc=1 diff --git a/tests/expected/ffi-objc-arc-03-weak-property.exit b/tests/expected/ffi-objc-arc-03-weak-property.exit index d00491f..573541a 100644 --- a/tests/expected/ffi-objc-arc-03-weak-property.exit +++ b/tests/expected/ffi-objc-arc-03-weak-property.exit @@ -1 +1 @@ -1 +0 diff --git a/tests/expected/ffi-objc-arc-03-weak-property.txt b/tests/expected/ffi-objc-arc-03-weak-property.txt index 73a890f..a5e3b0c 100644 --- a/tests/expected/ffi-objc-arc-03-weak-property.txt +++ b/tests/expected/ffi-objc-arc-03-weak-property.txt @@ -1 +1 @@ -FAIL: weak property didn't auto-nil after target dealloc +weak property: ok