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);
|
||||
|
||||
Reference in New Issue
Block a user