ffi M1.2 A.1: objcTypeEncodingFromSignature helper + encoding table

Derives Apple's runtime type-encoding string from an IR method
signature. Called by class_addMethod(cls, sel, imp, types) when
M1.2 A.4+ synthesise IMPs for sx-defined classes.

Layout: <ret> @ : <param0> <param1> ...   — @ is the receiver,
: is _cmd. Caller passes user-declared params AFTER stripping
'self: *Self'.

Encoding table:
  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
  @=foreign Obj-C class ptr        #=Class  :=SEL
  *=[*]u8 (C string)               ^v=any other ptr

bool (sx i1) maps to 'B' (C99 _Bool); s8 to 'c' (Apple's BOOL).
Foreign-class pointers detected via foreign_class_map lookup on
the pointee struct name. Other pointers fall to ^v — encoding is
metadata, not ABI, so conservative is safe.

Struct / slice / closure / etc. BAIL via diagnostic
(ObjcEncodingUnsupported) rather than silently mis-encoding, per
CLAUDE.md rejected-patterns rule. Future passes will widen the
table as new shapes show up in real IMPs.

Dead code at this commit — helper isn't called yet. Three unit
tests in src/ir/lower.test.zig pin the primitive / pointer /
Obj-C-class-pointer encodings before A.2 wires the helper in.

170 example tests + zig build test green.
This commit is contained in:
agra
2026-05-25 21:43:53 +03:00
parent 61a2593020
commit 6cc016cd4f
2 changed files with 213 additions and 0 deletions

View File

@@ -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);
}