diff --git a/src/ir/lower.test.zig b/src/ir/lower.test.zig index 0f6766f..84f852b 100644 --- a/src/ir/lower.test.zig +++ b/src/ir/lower.test.zig @@ -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); diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 2350e81..5aecc7f 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -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): + /// + /// __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