diff --git a/src/ir/lower.test.zig b/src/ir/lower.test.zig index 1ceaf87..0f6766f 100644 --- a/src/ir/lower.test.zig +++ b/src/ir/lower.test.zig @@ -221,3 +221,108 @@ test "lower: while loop generates header/body/exit blocks" { try std.testing.expect(std.mem.indexOf(u8, output, "while.exit") != null); try std.testing.expect(std.mem.indexOf(u8, output, "cond_br") != null); } + +// M1.2 A.1 — Obj-C type-encoding helper. +test "lower: objcTypeEncodingFromSignature emits primitive shapes" { + const alloc = std.testing.allocator; + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var lowering = Lowering.init(&module); + + // Niladic void method: -(void)greet → "v@:" + const e1 = try lowering.objcTypeEncodingFromSignature(.void, &.{}, null); + defer alloc.free(e1); + try std.testing.expectEqualStrings("v@:", e1); + + // Returns s32, takes s32: -(int)add:(int)x → "i@:i" + const e2 = try lowering.objcTypeEncodingFromSignature(.s32, &.{.s32}, null); + defer alloc.free(e2); + try std.testing.expectEqualStrings("i@:i", e2); + + // s64 return, two s64 args: "q@:qq" + const e3 = try lowering.objcTypeEncodingFromSignature(.s64, &.{ .s64, .s64 }, null); + defer alloc.free(e3); + try std.testing.expectEqualStrings("q@:qq", e3); + + // BOOL return (s8): "c@:" + const e4 = try lowering.objcTypeEncodingFromSignature(.s8, &.{}, null); + defer alloc.free(e4); + try std.testing.expectEqualStrings("c@:", e4); + + // Float/double: "f@:d" + const e5 = try lowering.objcTypeEncodingFromSignature(.f32, &.{.f64}, null); + defer alloc.free(e5); + try std.testing.expectEqualStrings("f@:d", e5); + + // bool (i1) is `B` — distinct from BOOL (`c`). + const e6 = try lowering.objcTypeEncodingFromSignature(.bool, &.{.bool}, null); + defer alloc.free(e6); + try std.testing.expectEqualStrings("B@:B", e6); + + // usize / isize on the 64-bit target. + const e7 = try lowering.objcTypeEncodingFromSignature(.usize, &.{.isize}, null); + defer alloc.free(e7); + try std.testing.expectEqualStrings("Q@:q", e7); + + // Unsigned variants u8/u16/u32/u64. + const e8 = try lowering.objcTypeEncodingFromSignature(.u32, &.{ .u8, .u16, .u64 }, null); + defer alloc.free(e8); + try std.testing.expectEqualStrings("I@:CSQ", e8); +} + +test "lower: objcTypeEncodingFromSignature emits pointer shapes" { + const alloc = std.testing.allocator; + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var lowering = Lowering.init(&module); + + // Generic `*void` → `^v`. + const void_ptr = module.types.ptrTo(.void); + const e1 = try lowering.objcTypeEncodingFromSignature(void_ptr, &.{void_ptr}, null); + defer alloc.free(e1); + try std.testing.expectEqualStrings("^v@:^v", e1); + + // `[*]u8` C-string carrier → `*`. + const u8_many = module.types.intern(.{ .many_pointer = .{ .element = .u8 } }); + const e2 = try lowering.objcTypeEncodingFromSignature(.void, &.{u8_many}, null); + defer alloc.free(e2); + try std.testing.expectEqualStrings("v@:*", e2); + + // `[*]s32` (non-u8 many-pointer) → `^v`. + const s32_many = module.types.intern(.{ .many_pointer = .{ .element = .s32 } }); + const e3 = try lowering.objcTypeEncodingFromSignature(.void, &.{s32_many}, null); + defer alloc.free(e3); + try std.testing.expectEqualStrings("v@:^v", e3); +} + +test "lower: objcTypeEncodingFromSignature emits @ for Obj-C class pointers" { + const alloc = std.testing.allocator; + var module = ir_mod.Module.init(alloc); + defer module.deinit(); + var lowering = Lowering.init(&module); + + // Synthesize a foreign Obj-C class entry so the encoder recognises + // `*NSString` as an object pointer. + 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.foreign_class_map.put("NSString", &ns_fcd); + + // Return *NSString, no args: "@@:" + const e1 = try lowering.objcTypeEncodingFromSignature(ns_ptr, &.{}, null); + defer alloc.free(e1); + try std.testing.expectEqualStrings("@@:", e1); + + // Return *NSString, take *NSString: "@@:@" + const e2 = try lowering.objcTypeEncodingFromSignature(ns_ptr, &.{ns_ptr}, null); + defer alloc.free(e2); + try std.testing.expectEqualStrings("@@:@", e2); +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 91cc93d..2350e81 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -4528,6 +4528,114 @@ pub const Lowering = struct { return .{ .sel = out, .keyword_count = pieces, .is_override = false }; } + /// Derive an Obj-C type-encoding string for a synthesized IMP + /// signature (M1.2 A.1). Apple's runtime accepts these strings on + /// `class_addMethod(cls, sel, imp, types)`; the encoding tells the + /// runtime the IMP's argument layout for KVC, NSCoder, and reflective + /// dispatch. + /// + /// Layout: ` @ : ...`. The `@` slot is the + /// receiver (self); `:` is `_cmd`. Caller passes user-declared params + /// AFTER stripping `self`. + /// + /// Single-character encodings (the common case): + /// v=void B=bool c=s8/BOOL s=s16 i=s32 q=s64 + /// C=u8 S=u16 I=u32 Q=u64 f=f32 d=f64 + /// @=id #=Class :=SEL *=C string ^v=void* / generic ptr + /// + /// Foreign-class pointers (`*UIView` etc.) encode as `@` (object + /// pointer). Other pointers fall to `^v` — the encoding is metadata, + /// not ABI, so being conservative here is safe. Struct returns and + /// other complex shapes BAIL loudly via diagnostics rather than + /// silently mis-encoding (per CLAUDE.md rejected-patterns rule). + /// + /// Returns an allocator-owned slice; caller frees via `self.alloc`. + fn objcTypeEncodingFromSignature( + self: *Lowering, + return_ty: TypeId, + param_tys: []const TypeId, + span: ?ast.Span, + ) ![]const u8 { + var out = std.ArrayList(u8).empty; + errdefer out.deinit(self.alloc); + + try self.appendObjcEncoding(&out, return_ty, span); + try out.append(self.alloc, '@'); // self + try out.append(self.alloc, ':'); // _cmd + for (param_tys) |pty| { + try self.appendObjcEncoding(&out, pty, span); + } + + return try out.toOwnedSlice(self.alloc); + } + + fn appendObjcEncoding(self: *Lowering, out: *std.ArrayList(u8), ty: TypeId, span: ?ast.Span) !void { + const info = self.module.types.get(ty); + switch (info) { + .void => try out.append(self.alloc, 'v'), + .bool => try out.append(self.alloc, 'B'), + .signed => |bits| { + const ch: u8 = switch (bits) { + 8 => 'c', + 16 => 's', + 32 => 'i', + 64 => 'q', + else => return self.bailObjcEncoding(span, "signed integer with non-standard bit width", bits), + }; + try out.append(self.alloc, ch); + }, + .unsigned => |bits| { + const ch: u8 = switch (bits) { + 8 => 'C', + 16 => 'S', + 32 => 'I', + 64 => 'Q', + else => return self.bailObjcEncoding(span, "unsigned integer with non-standard bit width", bits), + }; + try out.append(self.alloc, ch); + }, + .f32 => try out.append(self.alloc, 'f'), + .f64 => try out.append(self.alloc, 'd'), + // sx-target arm64 — pointer-sized aliases match s64/u64. + .isize => try out.append(self.alloc, 'q'), + .usize => try out.append(self.alloc, 'Q'), + .pointer => |p| { + // Pointer to a foreign Obj-C class (or sx-defined #objc_class) + // encodes as `@`. Anything else falls to `^v` — generic + // pointer; the runtime treats it as opaque. + const pointee_info = self.module.types.get(p.pointee); + const is_objc_obj = blk: { + if (pointee_info != .@"struct") break :blk false; + const name = self.module.types.getString(pointee_info.@"struct".name); + break :blk self.foreign_class_map.get(name) != null; + }; + if (is_objc_obj) { + try out.append(self.alloc, '@'); + } else { + try out.appendSlice(self.alloc, "^v"); + } + }, + .many_pointer => |mp| { + // `[*]u8` is the canonical C-string carrier — encode as `*`. + // Other element types fall to generic `^v`. + const el = self.module.types.get(mp.element); + if (el == .unsigned and el.unsigned == 8) { + try out.append(self.alloc, '*'); + } else { + try out.appendSlice(self.alloc, "^v"); + } + }, + else => return self.bailObjcEncoding(span, "type kind not yet supported by Obj-C encoding", @intFromEnum(std.meta.activeTag(info))), + } + } + + fn bailObjcEncoding(self: *Lowering, span: ?ast.Span, reason: []const u8, detail: anytype) anyerror { + if (self.diagnostics) |d| { + d.addFmt(.err, span, "cannot derive Obj-C type encoding: {s} (detail={any})", .{ reason, detail }); + } + return error.ObjcEncodingUnsupported; + } + /// Resolve a foreign-class member type, substituting `Self` (and `*Self`) /// with the foreign class's own struct type. Without this substitution /// chained calls like `Cls.alloc().init()` see the inner result as a