diff --git a/src/ir/lower.test.zig b/src/ir/lower.test.zig index 5cba51e..6afaa59 100644 --- a/src/ir/lower.test.zig +++ b/src/ir/lower.test.zig @@ -616,6 +616,121 @@ test "lower: objcTypeEncodingFromSignature emits nested structs (CGRect)" { try std.testing.expectEqualStrings("v@:{CGRect={CGPoint=dd}{CGSize=dd}}", e2); } +// ── A6.1 scaffolding: pure Obj-C decision helpers ─────────────────── +// Lock selector derivation, property-kind classification, and Obj-C +// class-pointer recognition before they move to `ffi_objc.zig`. + +fn objcMethod(name: []const u8) ast.ForeignMethodDecl { + return .{ .name = name, .params = &.{}, .param_names = &.{}, .return_type = null }; +} + +test "lower: deriveObjcSelector — niladic / keyword / multi-keyword / override" { + const alloc = std.testing.allocator; + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var lowering = Lowering.init(&module); + + // arity 0 → bare name, no colons, not an override. + const niladic = lowering.deriveObjcSelector(objcMethod("count"), 0); + try std.testing.expectEqualStrings("count", niladic.sel); + try std.testing.expectEqual(@as(usize, 0), niladic.keyword_count); + try std.testing.expectEqual(false, niladic.is_override); + + // arity ≥ 1, no `_` → single trailing colon, one keyword. + const single = lowering.deriveObjcSelector(objcMethod("setValue"), 1); + defer alloc.free(single.sel); + try std.testing.expectEqualStrings("setValue:", single.sel); + try std.testing.expectEqual(@as(usize, 1), single.keyword_count); + try std.testing.expectEqual(false, single.is_override); + + // each `_` → `:`, plus a trailing `:`; piece count = (#`_`) + 1. + const multi = lowering.deriveObjcSelector(objcMethod("setValue_forKey"), 2); + defer alloc.free(multi.sel); + try std.testing.expectEqualStrings("setValue:forKey:", multi.sel); + try std.testing.expectEqual(@as(usize, 2), multi.keyword_count); + try std.testing.expectEqual(false, multi.is_override); + + // `#selector(...)` override: used verbatim, keyword_count = #colons. + var m = objcMethod("init_with_frame_style"); + m.selector_override = "initWithFrame:style:"; + const overridden = lowering.deriveObjcSelector(m, 2); + try std.testing.expectEqualStrings("initWithFrame:style:", overridden.sel); + try std.testing.expectEqual(@as(usize, 2), overridden.keyword_count); + try std.testing.expectEqual(true, overridden.is_override); +} + +test "lower: isObjcClassPointer recognises pointer-to-foreign-Obj-C-class" { + const alloc = std.testing.allocator; + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var lowering = Lowering.init(&module); + + // *NSString where NSString is a registered Obj-C class → true. + const ns_name = module.types.internString("NSString"); + const ns_struct = module.types.intern(.{ .@"struct" = .{ .name = ns_name, .fields = &.{} } }); + const ns_ptr = module.types.ptrTo(ns_struct); + var ns_fcd = ast.ForeignClassDecl{ + .name = "NSString", + .foreign_path = "NSString", + .runtime = .objc_class, + .members = &.{}, + .is_foreign = true, + .is_main = false, + }; + try lowering.program_index.foreign_class_map.put("NSString", &ns_fcd); + try std.testing.expect(lowering.isObjcClassPointer(ns_ptr)); + + // *Plain where Plain is a non-foreign struct → false. + const plain_name = module.types.internString("Plain"); + const plain_struct = module.types.intern(.{ .@"struct" = .{ .name = plain_name, .fields = &.{} } }); + try std.testing.expect(!lowering.isObjcClassPointer(module.types.ptrTo(plain_struct))); + + // *void and a builtin scalar → false (not object pointers). + try std.testing.expect(!lowering.isObjcClassPointer(module.types.ptrTo(.void))); + try std.testing.expect(!lowering.isObjcClassPointer(.s32)); +} + +test "lower: objcPropertyKind defaults + explicit ARC modifiers" { + const alloc = std.testing.allocator; + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var lowering = Lowering.init(&module); + + // Register NSString so `*NSString` resolves to an object pointer. + const ns_name = module.types.internString("NSString"); + _ = module.types.intern(.{ .@"struct" = .{ .name = ns_name, .fields = &.{} } }); + var ns_fcd = ast.ForeignClassDecl{ + .name = "NSString", + .foreign_path = "NSString", + .runtime = .objc_class, + .members = &.{}, + .is_foreign = true, + .is_main = false, + }; + try lowering.program_index.foreign_class_map.put("NSString", &ns_fcd); + + // Primitive field, no modifiers → assign (the non-object default). + const prim = ast.ForeignFieldDecl{ .name = "count", .field_type = typeKeyword(alloc, "s32"), .is_property = true }; + defer alloc.destroy(prim.field_type); + try std.testing.expect(lowering.objcPropertyKind(prim) == .assign); + + // Object-pointer field, no modifiers → strong (the object default). + const obj_ty = typeKeyword(alloc, "*NSString"); + defer alloc.destroy(obj_ty); + const obj_default = ast.ForeignFieldDecl{ .name = "title", .field_type = obj_ty, .is_property = true }; + try std.testing.expect(lowering.objcPropertyKind(obj_default) == .strong); + + // Explicit modifiers on an object pointer win over the default. + const weak_mods = [_][]const u8{"weak"}; + try std.testing.expect(lowering.objcPropertyKind(.{ .name = "delegate", .field_type = obj_ty, .is_property = true, .property_modifiers = &weak_mods }) == .weak); + + const copy_mods = [_][]const u8{"copy"}; + try std.testing.expect(lowering.objcPropertyKind(.{ .name = "name", .field_type = obj_ty, .is_property = true, .property_modifiers = ©_mods }) == .copy); + + const assign_mods = [_][]const u8{"assign"}; + try std.testing.expect(lowering.objcPropertyKind(.{ .name = "raw", .field_type = obj_ty, .is_property = true, .property_modifiers = &assign_mods }) == .assign); +} + // ── Pack projection name resolution (Feature 1, Step 2.2) ──────────── const errors = @import("../errors.zig"); diff --git a/src/ir/lower.zig b/src/ir/lower.zig index ada1672..395ba6e 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -6031,7 +6031,7 @@ pub const Lowering = struct { /// - arity ≥ 1: split the sx name on `_`; each piece becomes a /// keyword with a trailing `:` (`addObject` → `addObject:`, /// `combine_and` → `combine:and:`). - fn deriveObjcSelector(self: *Lowering, method: ast.ForeignMethodDecl, arity: usize) struct { sel: []const u8, keyword_count: usize, is_override: bool } { + pub fn deriveObjcSelector(self: *Lowering, method: ast.ForeignMethodDecl, arity: usize) struct { sel: []const u8, keyword_count: usize, is_override: bool } { if (method.selector_override) |sel| { var colons: usize = 0; for (sel) |ch| { @@ -15790,7 +15790,7 @@ pub const Lowering = struct { /// - `weak` on a non-object field type → diagnostic /// - `strong` (explicit or defaulted) on `*void` (ambiguous: Obj-C /// object vs raw memory) → require explicit modifier - fn objcPropertyKind(self: *Lowering, field: ast.ForeignFieldDecl) ObjcPropertyKind { + pub fn objcPropertyKind(self: *Lowering, field: ast.ForeignFieldDecl) ObjcPropertyKind { // Survey the modifier list. var has_strong = false; var has_weak = false;