From 8d7164f45f4fe968dcff1d69880f7d822d3a6f61 Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 26 May 2026 22:07:56 +0300 Subject: [PATCH] ffi M4.0a: prepend __sx_allocator to sx-defined-class state struct MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit State struct for an sx-defined `#objc_class` now leads with an Allocator field at index 0 — captured at +alloc time, read by -dealloc to free the state through the same allocator. User fields shift to index 1+; the existing by-name lookups in emitObjcDefinedClassPropertyImps + lookupObjcDefinedStateFieldOnPointer naturally resolve them at the new indices. This step is the layout change only; the +alloc IMP still mallocs (M4.0b will rewrite it to thread context.allocator through), and -dealloc still uses free() (M4.0c). The field is allocated but uninitialised; nobody reads it yet. Storage type comes from `Context.fields[0].ty` via the new `objcStateAllocatorType` helper — same Allocator value-shape the implicit context machinery has used all along. If Context isn't registered (early-init paths), the helper falls back to omitting the field rather than synthesising a half-broken layout. IR snapshot for 142-objc-class-method-lowering updated to reflect the new struct shape and the +24-byte state allocation. Chess on iOS-sim green; 184/184 example tests pass. --- src/ir/lower.zig | 22 +++++++++++++++++++ .../142-objc-class-method-lowering.ir | 6 ++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 76cbfe8..a5fcbf6 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -4795,6 +4795,17 @@ pub const Lowering = struct { if (self.module.types.findByName(name_id)) |existing| return existing; var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; + // M4.0: prepend __sx_allocator at field index 0 — captured at +alloc + // time, read at -dealloc time to free the state struct through the + // same allocator. Lookup by name (the existing by-name resolution in + // emitObjcDefinedClassPropertyImps + lookupObjcDefinedStateFieldOnPointer) + // naturally finds user fields at their post-shift indices. + if (self.objcStateAllocatorType()) |allocator_ty| { + fields.append(self.alloc, .{ + .name = self.module.types.internString("__sx_allocator"), + .ty = allocator_ty, + }) catch unreachable; + } for (fcd.members) |m| { switch (m) { .field => |f| { @@ -4811,6 +4822,17 @@ pub const Lowering = struct { } }); } + /// Return the `Allocator` protocol TypeId (the value-shape used in + /// Context.allocator). Falls back to null if Context isn't registered + /// yet (early-init paths); callers omit the field in that case. + fn objcStateAllocatorType(self: *Lowering) ?TypeId { + const ctx_name = self.module.types.internString("Context"); + const ctx_ty = self.module.types.findByName(ctx_name) orelse return null; + const ctx_info = self.module.types.get(ctx_ty); + if (ctx_info != .@"struct" or ctx_info.@"struct".fields.len < 1) return null; + return ctx_info.@"struct".fields[0].ty; + } + /// Lower `inst.method(args)` on an `#objc_class` / `#objc_protocol` /// receiver. The selector is derived by `deriveObjcSelector`; arity /// is validated against the keyword count produced by the mangling diff --git a/tests/expected/142-objc-class-method-lowering.ir b/tests/expected/142-objc-class-method-lowering.ir index 0668b5b..ed6e159 100644 --- a/tests/expected/142-objc-class-method-lowering.ir +++ b/tests/expected/142-objc-class-method-lowering.ir @@ -740,7 +740,7 @@ entry: %load = load ptr, ptr %alloca, align 8 %loadN = load ptr, ptr @__SxFoo_state_ivar, align 8 %call = call ptr @object_getIvar(ptr %load, ptr %loadN) - %gep = getelementptr inbounds { i32 }, ptr %call, i32 0, i32 0 + %gep = getelementptr inbounds { { ptr, ptr, ptr }, i32 }, ptr %call, i32 0, i32 1 %loadN = load i32, ptr %gep, align 4 %add = add i32 %loadN, 1 store i32 %add, ptr %gep, align 4 @@ -800,8 +800,8 @@ declare ptr @object_getIvar(ptr, ptr) #0 define ptr @__SxFoo_alloc_imp(ptr %0, ptr %1) #0 { entry: %call = call ptr @class_createInstance(ptr %0, i64 0) - %callN = call ptr @malloc(i64 4) - %callN = call ptr @memset(ptr %callN, i32 0, i64 4) + %callN = call ptr @malloc(i64 32) + %callN = call ptr @memset(ptr %callN, i32 0, i64 32) %load = load ptr, ptr @__SxFoo_state_ivar, align 8 call void @object_setIvar(ptr %call, ptr %load, ptr %callN) ret ptr %call