ffi M4.0a: prepend __sx_allocator to sx-defined-class state struct

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.
This commit is contained in:
agra
2026-05-26 22:07:56 +03:00
parent 9fbc24a602
commit 8d7164f45f
2 changed files with 25 additions and 3 deletions

View File

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

View File

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