diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 0a4d099..5fa7e49 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -12256,8 +12256,39 @@ pub const Lowering = struct { get_args[1] = ivar_handle; const state_ptr = self.builder.emit(.{ .call = .{ .callee = get_ivar_fid, .args = get_args } }, ptr_void); - // GEP to the field, load. 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.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();