Surface rename of the signed integer family: s1..s64 become i1..i64
(u1..u64, usize, isize unchanged). 'string' keeps the s-prefix arm in
name classification; width parsing moves to the i-prefix arm next to
isize.
Internal TypeId tags follow the surface (.s8/.s16/.s32/.s64 ->
.i8/.i16/.i32/.i64), as do mono-key mangle fragments (ptr_i64,
tu_i64_bool) and all display/diagnostic formatting (i{d}).
Migrated in the same sweep: stdlib + examples + issue repros + FFI C
companions (shared symbol names like ffi_id_i64), expected
stdout/stderr/ir snapshots, specs.md, readme.md, CLAUDE.md/AGENTS.md,
implementation_plan.md, docs/, issue writeups. Vendored stb_image and
historical flow state left untouched.
zig build test: 426/426; examples suite: 595/595.
429 lines
20 KiB
Zig
429 lines
20 KiB
Zig
const std = @import("std");
|
|
const ast = @import("../ast.zig");
|
|
const lower = @import("lower.zig");
|
|
const types = @import("types.zig");
|
|
|
|
const Lowering = lower.Lowering;
|
|
const TypeId = types.TypeId;
|
|
|
|
/// Tracks struct TypeIds currently being emitted so a struct field of
|
|
/// `*Self` (or a transitive pointee that cycles back) emits the
|
|
/// abbreviated `{Name}` form instead of recursing forever. Bounded to
|
|
/// `cap` — well above any realistic Obj-C struct nesting depth.
|
|
const ObjcEncodingStack = struct {
|
|
const cap = 16;
|
|
items: [cap]TypeId = undefined,
|
|
len: u8 = 0,
|
|
|
|
fn push(self: *ObjcEncodingStack, tid: TypeId) bool {
|
|
if (self.len >= cap) return false;
|
|
self.items[self.len] = tid;
|
|
self.len += 1;
|
|
return true;
|
|
}
|
|
|
|
fn pop(self: *ObjcEncodingStack) void {
|
|
std.debug.assert(self.len > 0);
|
|
self.len -= 1;
|
|
}
|
|
|
|
fn contains(self: *const ObjcEncodingStack, tid: TypeId) bool {
|
|
var i: usize = 0;
|
|
while (i < self.len) : (i += 1) {
|
|
if (self.items[i] == tid) return true;
|
|
}
|
|
return false;
|
|
}
|
|
};
|
|
|
|
/// `assign` is the default for primitives (direct store, no ARC ops);
|
|
/// `strong` is the default for pointer-to-object types (retain on
|
|
/// assign, release on dealloc); `weak` and `copy` are explicit. The
|
|
/// helper rejects ambiguous combinations loudly per the silent-error
|
|
/// budget — `*void` requires explicit modifier, `weak` requires an
|
|
/// object-pointer slot.
|
|
const ObjcPropertyKind = enum {
|
|
assign, // primitives or explicitly opted-out object slots
|
|
strong, // default for *<ObjC-class> — retain on assign, release on dealloc
|
|
weak, // objc_storeWeak / objc_loadWeakRetained — auto-nilling
|
|
copy, // [val copy] on assign — for immutable-wanting String/Array slots
|
|
|
|
pub fn isObject(k: ObjcPropertyKind) bool {
|
|
return k == .strong or k == .weak or k == .copy;
|
|
}
|
|
};
|
|
|
|
/// Pure Obj-C decision helpers (architecture phase A6.1), extracted from
|
|
/// `Lowering`. A `*Lowering` facade (Principle 5, like `ErrorAnalysis`/
|
|
/// `CoercionResolver`): selector derivation, type-encoding-string derivation,
|
|
/// ARC property-kind classification, Obj-C class-pointer recognition, and
|
|
/// hidden-state-struct planning. No IR is emitted here — the emission-heavy IMP
|
|
/// builders / `lowerObjc*Call` dispatch stay in `Lowering` (PLAN-ARCH A6.1
|
|
/// step 6). Reads `self.l.{alloc, module, program_index, diagnostics}` and the
|
|
/// `self.l.resolveType` resolver.
|
|
pub const ObjcLowering = struct {
|
|
l: *Lowering,
|
|
|
|
pub fn deriveObjcSelector(self: ObjcLowering, 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| {
|
|
if (ch == ':') colons += 1;
|
|
}
|
|
return .{ .sel = sel, .keyword_count = colons, .is_override = true };
|
|
}
|
|
if (arity == 0) {
|
|
return .{ .sel = method.name, .keyword_count = 0, .is_override = false };
|
|
}
|
|
// Each `_` in the sx name becomes a `:` (one-byte-for-one), plus
|
|
// one trailing `:` regardless of how many pieces. Piece count
|
|
// = (number of `_`) + 1.
|
|
var pieces: usize = 1;
|
|
for (method.name) |ch| {
|
|
if (ch == '_') pieces += 1;
|
|
}
|
|
const out = self.l.alloc.alloc(u8, method.name.len + 1) catch unreachable;
|
|
for (method.name, 0..) |ch, i| {
|
|
out[i] = if (ch == '_') ':' else ch;
|
|
}
|
|
out[method.name.len] = ':';
|
|
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=i8/BOOL s=i16 i=i32 q=i64
|
|
/// 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. Pass-by-value
|
|
/// structs encode as `{Name=field0field1...}`; nested structs
|
|
/// recurse with cycle-break via `ObjcEncodingStack`. Tagged-union /
|
|
/// array / vector / function 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.l.alloc`.
|
|
pub fn objcTypeEncodingFromSignature(
|
|
self: ObjcLowering,
|
|
return_ty: TypeId,
|
|
param_tys: []const TypeId,
|
|
span: ?ast.Span,
|
|
) ![]const u8 {
|
|
var out = std.ArrayList(u8).empty;
|
|
errdefer out.deinit(self.l.alloc);
|
|
|
|
var stack: ObjcEncodingStack = .{};
|
|
try self.appendObjcEncoding(&out, return_ty, span, &stack);
|
|
try out.append(self.l.alloc, '@'); // self
|
|
try out.append(self.l.alloc, ':'); // _cmd
|
|
for (param_tys) |pty| {
|
|
try self.appendObjcEncoding(&out, pty, span, &stack);
|
|
}
|
|
|
|
return try out.toOwnedSlice(self.l.alloc);
|
|
}
|
|
|
|
fn appendObjcEncoding(
|
|
self: ObjcLowering,
|
|
out: *std.ArrayList(u8),
|
|
ty: TypeId,
|
|
span: ?ast.Span,
|
|
stack: *ObjcEncodingStack,
|
|
) !void {
|
|
const info = self.l.module.types.get(ty);
|
|
switch (info) {
|
|
.void => try out.append(self.l.alloc, 'v'),
|
|
.bool => try out.append(self.l.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.l.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.l.alloc, ch);
|
|
},
|
|
.f32 => try out.append(self.l.alloc, 'f'),
|
|
.f64 => try out.append(self.l.alloc, 'd'),
|
|
// sx-target arm64 — pointer-sized aliases match i64/u64.
|
|
.isize => try out.append(self.l.alloc, 'q'),
|
|
.usize => try out.append(self.l.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.l.module.types.get(p.pointee);
|
|
const is_objc_obj = blk: {
|
|
if (pointee_info != .@"struct") break :blk false;
|
|
const name = self.l.module.types.getString(pointee_info.@"struct".name);
|
|
break :blk self.l.program_index.foreign_class_map.get(name) != null;
|
|
};
|
|
if (is_objc_obj) {
|
|
try out.append(self.l.alloc, '@');
|
|
} else {
|
|
try out.appendSlice(self.l.alloc, "^v");
|
|
}
|
|
},
|
|
.many_pointer => |mp| {
|
|
// `[*]u8` is the canonical C-string carrier — encode as `*`.
|
|
// Other element types fall to generic `^v`.
|
|
const el = self.l.module.types.get(mp.element);
|
|
if (el == .unsigned and el.unsigned == 8) {
|
|
try out.append(self.l.alloc, '*');
|
|
} else {
|
|
try out.appendSlice(self.l.alloc, "^v");
|
|
}
|
|
},
|
|
.optional => |o| {
|
|
// sx's `?T` is a nullable T. At the Obj-C ABI boundary
|
|
// nullability is just "this pointer may be null" — the
|
|
// wire-level encoding is the same as T. Unwrap and
|
|
// recurse. (Same goes for `?*UIView` etc. — the
|
|
// underlying pointer kind drives the encoding char.)
|
|
return self.appendObjcEncoding(out, o.child, span, stack);
|
|
},
|
|
.@"struct" => |s| {
|
|
// Pass-by-value struct argument or return: Apple's
|
|
// encoding is `{Name=field0field1...}`. A struct
|
|
// already on the encoding stack (i.e. transitively
|
|
// referenced through a struct field — extremely rare
|
|
// since sx structs don't recurse by value) gets the
|
|
// abbreviated `{Name}` form. Recursion through
|
|
// POINTERS is fine because `.pointer` collapses to
|
|
// `^v` regardless of pointee shape.
|
|
const name = self.l.module.types.getString(s.name);
|
|
try out.append(self.l.alloc, '{');
|
|
try out.appendSlice(self.l.alloc, name);
|
|
if (stack.contains(ty)) {
|
|
try out.append(self.l.alloc, '}');
|
|
return;
|
|
}
|
|
if (!stack.push(ty)) {
|
|
return self.bailObjcEncoding(span, "Obj-C struct encoding nested deeper than supported", ObjcEncodingStack.cap);
|
|
}
|
|
defer stack.pop();
|
|
try out.append(self.l.alloc, '=');
|
|
for (s.fields) |f| {
|
|
try self.appendObjcEncoding(out, f.ty, span, stack);
|
|
}
|
|
try out.append(self.l.alloc, '}');
|
|
},
|
|
else => return self.bailObjcEncoding(span, "type kind not yet supported by Obj-C encoding", @intFromEnum(std.meta.activeTag(info))),
|
|
}
|
|
}
|
|
|
|
fn bailObjcEncoding(self: ObjcLowering, span: ?ast.Span, reason: []const u8, detail: anytype) anyerror {
|
|
if (self.l.diagnostics) |d| {
|
|
d.addFmt(.err, span, "cannot derive Obj-C type encoding: {s} (detail={any})", .{ reason, detail });
|
|
}
|
|
return error.ObjcEncodingUnsupported;
|
|
}
|
|
|
|
/// Build (and cache) the hidden sx-state struct type for an sx-defined
|
|
/// `#objc_class`. The state struct is what the runtime's `__sx_state`
|
|
/// ivar points at — separate from the Obj-C object itself, which stays
|
|
/// opaque. Layout (M1.2 A.2):
|
|
///
|
|
/// __<ClassName>State {
|
|
/// user_field_0,
|
|
/// user_field_1,
|
|
/// ...
|
|
/// }
|
|
///
|
|
/// M1.2 A.5 will prepend `__sx_allocator: Allocator` so `-dealloc`
|
|
/// can free through the per-instance allocator and method bodies can
|
|
/// access `self.allocator`. For A.2 the struct holds only the
|
|
/// user-declared fields — sufficient for the body lowering +
|
|
/// `self.field` access work in A.2/A.3. Field-by-name resolution
|
|
/// stays correct across the future repositioning.
|
|
///
|
|
/// Foreign-class members other than `.field` are ignored here —
|
|
/// methods / `#extends` / `#implements` don't contribute to the
|
|
/// state layout.
|
|
pub fn objcDefinedStateStructType(self: ObjcLowering, fcd: *const ast.ForeignClassDecl) TypeId {
|
|
const state_name = std.fmt.allocPrint(self.l.alloc, "__{s}State", .{fcd.name}) catch unreachable;
|
|
defer self.l.alloc.free(state_name); // internString copies; the temp isn't needed after.
|
|
const name_id = self.l.module.types.internString(state_name);
|
|
if (self.l.module.types.findByName(name_id)) |existing| return existing;
|
|
|
|
// The interned struct's `fields` slice lives for the module's lifetime;
|
|
// allocate it (and the building ArrayList) in the module arena so it's
|
|
// freed at module deinit rather than leaking through `self.l.alloc`.
|
|
const field_alloc = self.l.module.slice_arena.allocator();
|
|
var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty;
|
|
// M4.0: prepend __sx_allocator at field index 0 — captured at +alloc
|
|
// time, read at -dealloc time to free the state struct through the
|
|
// same allocator. Lookup by name (the existing by-name resolution in
|
|
// emitObjcDefinedClassPropertyImps + lookupObjcDefinedStateFieldOnPointer)
|
|
// naturally finds user fields at their post-shift indices.
|
|
if (self.objcStateAllocatorType()) |allocator_ty| {
|
|
fields.append(field_alloc, .{
|
|
.name = self.l.module.types.internString("__sx_allocator"),
|
|
.ty = allocator_ty,
|
|
}) catch unreachable;
|
|
}
|
|
for (fcd.members) |m| {
|
|
switch (m) {
|
|
.field => |f| {
|
|
const f_name_id = self.l.module.types.internString(f.name);
|
|
const f_ty = self.l.resolveType(f.field_type);
|
|
fields.append(field_alloc, .{ .name = f_name_id, .ty = f_ty }) catch unreachable;
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
return self.l.module.types.intern(.{ .@"struct" = .{
|
|
.name = name_id,
|
|
.fields = fields.toOwnedSlice(field_alloc) catch unreachable,
|
|
} });
|
|
}
|
|
|
|
/// Return the `Allocator` protocol TypeId (the value-shape used in
|
|
/// Context.allocator). Falls back to null if Context isn't registered
|
|
/// yet (early-init paths); callers omit the field in that case.
|
|
fn objcStateAllocatorType(self: ObjcLowering) ?TypeId {
|
|
const ctx_name = self.l.module.types.internString("Context");
|
|
const ctx_ty = self.l.module.types.findByName(ctx_name) orelse return null;
|
|
const ctx_info = self.l.module.types.get(ctx_ty);
|
|
if (ctx_info != .@"struct" or ctx_info.@"struct".fields.len < 1) return null;
|
|
return ctx_info.@"struct".fields[0].ty;
|
|
}
|
|
|
|
pub fn isObjcClassPointer(self: ObjcLowering, ty: TypeId) bool {
|
|
if (ty.isBuiltin()) return false;
|
|
const ptr_info = self.l.module.types.get(ty);
|
|
if (ptr_info != .pointer) return false;
|
|
const pointee_info = self.l.module.types.get(ptr_info.pointer.pointee);
|
|
if (pointee_info != .@"struct") return false;
|
|
const struct_name = self.l.module.types.getString(pointee_info.@"struct".name);
|
|
const fcd = self.l.program_index.foreign_class_map.get(struct_name) orelse return false;
|
|
return fcd.runtime == .objc_class or fcd.runtime == .objc_protocol;
|
|
}
|
|
|
|
/// Resolve a `#property(...)` field's ARC kind. Loud at compile time
|
|
/// for known footguns (per the silent-error budget in the plan):
|
|
/// - unknown modifier name (typo) → diagnostic
|
|
/// - `weak` on a non-object field type → diagnostic
|
|
/// - `strong` (explicit or defaulted) on `*void` (ambiguous: Obj-C
|
|
/// object vs raw memory) → require explicit modifier
|
|
pub fn objcPropertyKind(self: ObjcLowering, field: ast.ForeignFieldDecl) ObjcPropertyKind {
|
|
// Survey the modifier list.
|
|
var has_strong = false;
|
|
var has_weak = false;
|
|
var has_copy = false;
|
|
var has_assign = false;
|
|
for (field.property_modifiers) |mod| {
|
|
if (std.mem.eql(u8, mod, "strong")) has_strong = true
|
|
else if (std.mem.eql(u8, mod, "weak")) has_weak = true
|
|
else if (std.mem.eql(u8, mod, "copy")) has_copy = true
|
|
else if (std.mem.eql(u8, mod, "assign")) has_assign = true
|
|
else if (std.mem.eql(u8, mod, "readonly")) {
|
|
// Orthogonal to ARC kind — no-op here.
|
|
}
|
|
else if (std.mem.eql(u8, mod, "nonatomic") or std.mem.eql(u8, mod, "atomic")) {
|
|
// Atomicity — recorded for the property attribute string;
|
|
// doesn't affect the ARC kind.
|
|
}
|
|
else if (std.mem.startsWith(u8, mod, "getter(") or std.mem.startsWith(u8, mod, "setter(")) {
|
|
// Selector overrides — handled elsewhere.
|
|
}
|
|
else {
|
|
if (self.l.diagnostics) |d| {
|
|
const span = ast.Span{ .start = 0, .end = 0 };
|
|
d.addFmt(.err, span, "unknown #property modifier '{s}' on field '{s}' — expected one of: strong, weak, copy, assign, readonly, nonatomic, atomic, getter(\"...\"), setter(\"...\")", .{ mod, field.name });
|
|
}
|
|
}
|
|
}
|
|
|
|
// Mutually-exclusive ARC modifiers — at most one.
|
|
const explicit_count: u32 =
|
|
(@as(u32, if (has_strong) 1 else 0)) +
|
|
(@as(u32, if (has_weak) 1 else 0)) +
|
|
(@as(u32, if (has_copy) 1 else 0)) +
|
|
(@as(u32, if (has_assign) 1 else 0));
|
|
if (explicit_count > 1) {
|
|
if (self.l.diagnostics) |d| {
|
|
const span = ast.Span{ .start = 0, .end = 0 };
|
|
d.addFmt(.err, span, "conflicting #property modifiers on field '{s}' — strong/weak/copy/assign are mutually exclusive", .{field.name});
|
|
}
|
|
}
|
|
|
|
// Resolve the field's type to decide defaults + validate.
|
|
const field_ty = self.l.resolveType(field.field_type);
|
|
const is_pointer = !field_ty.isBuiltin() and self.l.module.types.get(field_ty) == .pointer;
|
|
const is_object_ptr = is_pointer and blk: {
|
|
const pointee = self.l.module.types.get(field_ty).pointer.pointee;
|
|
// `*void` is NOT considered an object pointer — ambiguous.
|
|
if (pointee == .void) break :blk false;
|
|
// `*T` where T is a foreign-class struct (Obj-C class).
|
|
if (pointee.isBuiltin()) break :blk false;
|
|
const pointee_info = self.l.module.types.get(pointee);
|
|
if (pointee_info != .@"struct") break :blk false;
|
|
const struct_name = self.l.module.types.getString(pointee_info.@"struct".name);
|
|
const fcd = self.l.program_index.foreign_class_map.get(struct_name) orelse break :blk false;
|
|
break :blk fcd.runtime == .objc_class or fcd.runtime == .objc_protocol;
|
|
};
|
|
|
|
// `weak` requires an object pointer — `weak i32` is meaningless and
|
|
// would invoke objc_storeWeak on a non-object slot.
|
|
if (has_weak and !is_object_ptr) {
|
|
if (self.l.diagnostics) |d| {
|
|
const span = ast.Span{ .start = 0, .end = 0 };
|
|
d.addFmt(.err, span, "#property(weak) on field '{s}' requires a pointer-to-Obj-C-class type; got '{s}'", .{ field.name, self.l.module.types.typeName(field_ty) });
|
|
}
|
|
}
|
|
|
|
// `copy` requires an object pointer — `copy i32` makes no sense.
|
|
if (has_copy and !is_object_ptr) {
|
|
if (self.l.diagnostics) |d| {
|
|
const span = ast.Span{ .start = 0, .end = 0 };
|
|
d.addFmt(.err, span, "#property(copy) on field '{s}' requires a pointer-to-Obj-C-class type (typically NSString or NSArray)", .{field.name});
|
|
}
|
|
}
|
|
|
|
// `*void` is ambiguous (Obj-C object vs raw memory): require explicit
|
|
// modifier so the user opts into ARC semantics consciously.
|
|
if (is_pointer) {
|
|
const pointee = self.l.module.types.get(field_ty).pointer.pointee;
|
|
if (pointee == .void and explicit_count == 0) {
|
|
if (self.l.diagnostics) |d| {
|
|
const span = ast.Span{ .start = 0, .end = 0 };
|
|
d.addFmt(.err, span, "#property on field '{s}' of type '*void' is ambiguous — specify `#property(strong|weak|copy|assign)` explicitly (Obj-C object vs raw memory)", .{field.name});
|
|
}
|
|
return .assign; // assume safe default to keep compilation going
|
|
}
|
|
}
|
|
|
|
// Apply explicit modifier or default.
|
|
if (has_weak) return .weak;
|
|
if (has_copy) return .copy;
|
|
if (has_strong) return .strong;
|
|
if (has_assign) return .assign;
|
|
// Default: object pointers → strong; everything else → assign.
|
|
return if (is_object_ptr) .strong else .assign;
|
|
}
|
|
};
|