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