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:
@@ -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);
|
||||
}
|
||||
|
||||
108
src/ir/lower.zig
108
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: `<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
|
||||
|
||||
Reference in New Issue
Block a user