ffi M1.2 A.2a: objcDefinedStateStructType helper

Builds (and interns) the hidden sx-state struct type for an
sx-defined '#objc_class'. Layout:

    __<ClassName>State {
        user_field_0,
        user_field_1,
        ...
    }

This struct is what the runtime's '__sx_state' ivar points at —
separate from the Obj-C object itself, which stays opaque. The
sx method bodies will operate on '*__SxFooState' (after '*Self'
substitution in A.2b) so 'self.field' resolves to a plain struct
field access — A.3's 'free if types align' premise.

M1.2 A.5 will prepend '__sx_allocator: Allocator' so dealloc can
free through the per-instance allocator. Field-by-name access
stays correct across the future repositioning.

Methods / '#extends' / '#implements' members are ignored — only
'.field' contributes. Three unit tests pin: typical-field case,
empty-class case, mixed-member case.

Dead code at this commit — helper isn't called yet. A.2b (body
lowering with '*Self' substitution) wires it in. 170 example
tests + zig build test green.
This commit is contained in:
agra
2026-05-25 21:51:07 +03:00
parent 6cc016cd4f
commit 7b98b3ae78
2 changed files with 142 additions and 0 deletions

View File

@@ -4672,6 +4672,49 @@ pub const Lowering = struct {
return self.module.types.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } });
}
/// Build (and cache) the hidden sx-state struct type for an sx-defined
/// `#objc_class`. The state struct is what the runtime's `__sx_state`
/// ivar points at — separate from the Obj-C object itself, which stays
/// opaque. Layout (M1.2 A.2):
///
/// __<ClassName>State {
/// user_field_0,
/// user_field_1,
/// ...
/// }
///
/// M1.2 A.5 will prepend `__sx_allocator: Allocator` so `-dealloc`
/// can free through the per-instance allocator and method bodies can
/// access `self.allocator`. For A.2 the struct holds only the
/// user-declared fields — sufficient for the body lowering +
/// `self.field` access work in A.2/A.3. Field-by-name resolution
/// stays correct across the future repositioning.
///
/// Foreign-class members other than `.field` are ignored here —
/// methods / `#extends` / `#implements` don't contribute to the
/// state layout.
fn objcDefinedStateStructType(self: *Lowering, fcd: *const ast.ForeignClassDecl) TypeId {
const state_name = std.fmt.allocPrint(self.alloc, "__{s}State", .{fcd.name}) catch unreachable;
const name_id = self.module.types.internString(state_name);
if (self.module.types.findByName(name_id)) |existing| return existing;
var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
for (fcd.members) |m| {
switch (m) {
.field => |f| {
const f_name_id = self.module.types.internString(f.name);
const f_ty = self.resolveType(f.field_type);
fields.append(self.alloc, .{ .name = f_name_id, .ty = f_ty }) catch unreachable;
},
else => {},
}
}
return self.module.types.intern(.{ .@"struct" = .{
.name = name_id,
.fields = fields.toOwnedSlice(self.alloc) catch unreachable,
} });
}
/// 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