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

View File

@@ -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: `<ret> @ : <param0> <param1> ...`. 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