ffi M4.B dealloc: release strong/copy property ivars + destroyWeak weak

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.
This commit is contained in:
agra
2026-05-26 23:10:00 +03:00
parent c88a293cf4
commit fcbd7a4235
3 changed files with 65 additions and 4 deletions

View File

@@ -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,