From fcbd7a42353c4e7b24b56f2ee2bdbc9b15cc37b0 Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 26 May 2026 23:10:00 +0300 Subject: [PATCH] ffi M4.B dealloc: release strong/copy property ivars + destroyWeak weak MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit emitObjcDefinedClassDeallocImp now walks the class's #property fields BEFORE freeing the state struct. For each: - assign → no-op (primitives, no ARC traffic). - strong → val = load field; objc_release(val). - copy → same as strong (the stored value is a +1 retained copy produced by the setter's [val copy]; we release it here). - weak → objc_destroyWeak(&field) — unregisters the slot from libobjc's side-table so the runtime stops tracking it. Order matters: property releases happen BEFORE freeing the state struct (which would invalidate the pointers we need to read), which happens BEFORE [super dealloc] (which eventually frees the Obj-C instance's own memory). The full sequence is now: %state = object_getIvar(self, __sx_state_ivar) // M4.B (this commit): for each strong/copy property P: val = load struct_gep(state, P.idx); objc_release(val) for each weak property P: objc_destroyWeak(struct_gep(state, P.idx)) // M4.0c (already shipped): allocator = load struct_gep(state, 0) allocator.dealloc(state) object_setIvar(self, ivar, null) // M1.2 A.6: [super dealloc] // → objc_msgSendSuper2 ffi-objc-arc-02-strong-property now passes: child held by parent's strong property gets released when parent deallocates, refcount → 0, child deallocates, both states freed via tracker. Balanced 2/2. 189/189 example tests pass; chess on iOS-sim green. M4 complete. --- src/ir/lower.zig | 65 ++++++++++++++++++- .../ffi-objc-arc-02-strong-property.exit | 2 +- .../ffi-objc-arc-02-strong-property.txt | 2 +- 3 files changed, 65 insertions(+), 4 deletions(-) diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 5fa7e49..85a227e 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -12904,7 +12904,69 @@ pub const Lowering = struct { get_args[1] = ivar_handle; const state = self.builder.emit(.{ .call = .{ .callee = get_ivar_fid, .args = get_args } }, ptr_void); - // (2) Free state through the captured allocator (M4.0a + M4.0b): + // (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.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.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 @@ -12925,7 +12987,6 @@ pub const Lowering = struct { } const allocator_ty = ctx_info.@"struct".fields[0].ty; - const state_struct_ty = self.objcDefinedStateStructType(fcd); const state_alloc_addr = self.builder.emit(.{ .struct_gep = .{ .base = state, .field_index = 0, diff --git a/tests/expected/ffi-objc-arc-02-strong-property.exit b/tests/expected/ffi-objc-arc-02-strong-property.exit index d00491f..573541a 100644 --- a/tests/expected/ffi-objc-arc-02-strong-property.exit +++ b/tests/expected/ffi-objc-arc-02-strong-property.exit @@ -1 +1 @@ -1 +0 diff --git a/tests/expected/ffi-objc-arc-02-strong-property.txt b/tests/expected/ffi-objc-arc-02-strong-property.txt index 114d680..7f951a0 100644 --- a/tests/expected/ffi-objc-arc-02-strong-property.txt +++ b/tests/expected/ffi-objc-arc-02-strong-property.txt @@ -1 +1 @@ -FAIL: unbalanced; alloc=2 dealloc=1 +strong property: ok