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:
@@ -295,6 +295,105 @@ test "lower: objcTypeEncodingFromSignature emits pointer shapes" {
|
||||
try std.testing.expectEqualStrings("v@:^v", e3);
|
||||
}
|
||||
|
||||
// M1.2 A.2 — sx-defined #objc_class state struct construction.
|
||||
test "lower: objcDefinedStateStructType collects user-declared fields" {
|
||||
const alloc = std.testing.allocator;
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
defer module.deinit();
|
||||
var lowering = Lowering.init(&module);
|
||||
|
||||
// Synthesize a #objc_class("SxFoo") { counter: s32; ticks: s64; } AST.
|
||||
const span = ast.Span{ .start = 0, .end = 0 };
|
||||
const counter_type = try alloc.create(Node);
|
||||
defer alloc.destroy(counter_type);
|
||||
counter_type.* = .{ .span = span, .data = .{ .type_expr = .{ .name = "s32", .is_generic = false } } };
|
||||
const ticks_type = try alloc.create(Node);
|
||||
defer alloc.destroy(ticks_type);
|
||||
ticks_type.* = .{ .span = span, .data = .{ .type_expr = .{ .name = "s64", .is_generic = false } } };
|
||||
|
||||
const members = [_]ast.ForeignClassMember{
|
||||
.{ .field = .{ .name = "counter", .field_type = counter_type } },
|
||||
.{ .field = .{ .name = "ticks", .field_type = ticks_type } },
|
||||
};
|
||||
const fcd = ast.ForeignClassDecl{
|
||||
.name = "SxFoo",
|
||||
.foreign_path = "SxFoo",
|
||||
.runtime = .objc_class,
|
||||
.members = &members,
|
||||
.is_foreign = false,
|
||||
.is_main = false,
|
||||
};
|
||||
|
||||
const state_ty = lowering.objcDefinedStateStructType(&fcd);
|
||||
const info = module.types.get(state_ty);
|
||||
try std.testing.expectEqual(@as(std.meta.Tag(@TypeOf(info)), .@"struct"), std.meta.activeTag(info));
|
||||
|
||||
const s = info.@"struct";
|
||||
try std.testing.expectEqualStrings("__SxFooState", module.types.getString(s.name));
|
||||
try std.testing.expectEqual(@as(usize, 2), s.fields.len);
|
||||
try std.testing.expectEqualStrings("counter", module.types.getString(s.fields[0].name));
|
||||
try std.testing.expectEqual(TypeId.s32, s.fields[0].ty);
|
||||
try std.testing.expectEqualStrings("ticks", module.types.getString(s.fields[1].name));
|
||||
try std.testing.expectEqual(TypeId.s64, s.fields[1].ty);
|
||||
|
||||
// Idempotency: a second call returns the same TypeId (cache hit on name).
|
||||
const state_ty2 = lowering.objcDefinedStateStructType(&fcd);
|
||||
try std.testing.expectEqual(state_ty, state_ty2);
|
||||
}
|
||||
|
||||
test "lower: objcDefinedStateStructType handles empty field set" {
|
||||
const alloc = std.testing.allocator;
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
defer module.deinit();
|
||||
var lowering = Lowering.init(&module);
|
||||
|
||||
const fcd = ast.ForeignClassDecl{
|
||||
.name = "SxEmpty",
|
||||
.foreign_path = "SxEmpty",
|
||||
.runtime = .objc_class,
|
||||
.members = &.{},
|
||||
.is_foreign = false,
|
||||
.is_main = false,
|
||||
};
|
||||
|
||||
const state_ty = lowering.objcDefinedStateStructType(&fcd);
|
||||
const info = module.types.get(state_ty);
|
||||
try std.testing.expectEqualStrings("__SxEmptyState", module.types.getString(info.@"struct".name));
|
||||
try std.testing.expectEqual(@as(usize, 0), info.@"struct".fields.len);
|
||||
}
|
||||
|
||||
test "lower: objcDefinedStateStructType skips non-field members" {
|
||||
const alloc = std.testing.allocator;
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
defer module.deinit();
|
||||
var lowering = Lowering.init(&module);
|
||||
|
||||
// Mix in #extends and method members — only `.field` contributes.
|
||||
const span = ast.Span{ .start = 0, .end = 0 };
|
||||
const counter_type = try alloc.create(Node);
|
||||
defer alloc.destroy(counter_type);
|
||||
counter_type.* = .{ .span = span, .data = .{ .type_expr = .{ .name = "s32", .is_generic = false } } };
|
||||
|
||||
const members = [_]ast.ForeignClassMember{
|
||||
.{ .extends = "NSObject" },
|
||||
.{ .field = .{ .name = "counter", .field_type = counter_type } },
|
||||
.{ .implements = "UIApplicationDelegate" },
|
||||
};
|
||||
const fcd = ast.ForeignClassDecl{
|
||||
.name = "SxMixed",
|
||||
.foreign_path = "SxMixed",
|
||||
.runtime = .objc_class,
|
||||
.members = &members,
|
||||
.is_foreign = false,
|
||||
.is_main = false,
|
||||
};
|
||||
|
||||
const state_ty = lowering.objcDefinedStateStructType(&fcd);
|
||||
const info = module.types.get(state_ty);
|
||||
try std.testing.expectEqual(@as(usize, 1), info.@"struct".fields.len);
|
||||
try std.testing.expectEqualStrings("counter", module.types.getString(info.@"struct".fields[0].name));
|
||||
}
|
||||
|
||||
test "lower: objcTypeEncodingFromSignature emits @ for Obj-C class pointers" {
|
||||
const alloc = std.testing.allocator;
|
||||
var module = ir_mod.Module.init(alloc);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user