diff --git a/src/ir/expr_typer.zig b/src/ir/expr_typer.zig index 59b6bd8..ee05d26 100644 --- a/src/ir/expr_typer.zig +++ b/src/ir/expr_typer.zig @@ -127,7 +127,7 @@ pub const ExprTyper = struct { } // M1.3 — `obj.class` on an Obj-C-class pointer returns Class (*void). if (std.mem.eql(u8, fa.field, "class")) { - if (self.l.isObjcClassPointer(self.l.inferExprType(fa.object))) { + if (self.l.objc().isObjcClassPointer(self.l.inferExprType(fa.object))) { return self.l.module.types.ptrTo(.void); } } diff --git a/src/ir/ffi_objc.zig b/src/ir/ffi_objc.zig new file mode 100644 index 0000000..808a00a --- /dev/null +++ b/src/ir/ffi_objc.zig @@ -0,0 +1,428 @@ +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 * — 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: ` @ : ...`. 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. 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 s64/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): + /// + /// __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 s32` 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 s32` 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; + } +}; diff --git a/src/ir/ir.zig b/src/ir/ir.zig index f4a3773..144f77d 100644 --- a/src/ir/ir.zig +++ b/src/ir/ir.zig @@ -14,6 +14,7 @@ pub const protocols = @import("protocols.zig"); pub const conversions = @import("conversions.zig"); pub const error_analysis = @import("error_analysis.zig"); pub const error_flow = @import("error_flow.zig"); +pub const ffi_objc = @import("ffi_objc.zig"); pub const semantic_diagnostics = @import("semantic_diagnostics.zig"); pub const TypeId = types.TypeId; @@ -54,6 +55,7 @@ pub const CoercionResolver = conversions.CoercionResolver; pub const CoercionPlan = conversions.CoercionResolver.CoercionPlan; pub const ErrorAnalysis = error_analysis.ErrorAnalysis; pub const ErrorFlow = error_flow.ErrorFlow; +pub const ObjcLowering = ffi_objc.ObjcLowering; pub const ErrorFacts = error_analysis.ErrorFacts; pub const compiler_hooks = @import("compiler_hooks.zig"); diff --git a/src/ir/lower.test.zig b/src/ir/lower.test.zig index ecad5d1..f957eb8 100644 --- a/src/ir/lower.test.zig +++ b/src/ir/lower.test.zig @@ -296,42 +296,42 @@ test "lower: objcTypeEncodingFromSignature emits primitive shapes" { var lowering = Lowering.init(&module); // Niladic void method: -(void)greet → "v@:" - const e1 = try lowering.objcTypeEncodingFromSignature(.void, &.{}, null); + const e1 = try lowering.objc().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); + const e2 = try lowering.objc().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); + const e3 = try lowering.objc().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); + const e4 = try lowering.objc().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); + const e5 = try lowering.objc().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); + const e6 = try lowering.objc().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); + const e7 = try lowering.objc().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); + const e8 = try lowering.objc().objcTypeEncodingFromSignature(.u32, &.{ .u8, .u16, .u64 }, null); defer alloc.free(e8); try std.testing.expectEqualStrings("I@:CSQ", e8); } @@ -344,19 +344,19 @@ test "lower: objcTypeEncodingFromSignature emits pointer shapes" { // Generic `*void` → `^v`. const void_ptr = module.types.ptrTo(.void); - const e1 = try lowering.objcTypeEncodingFromSignature(void_ptr, &.{void_ptr}, null); + const e1 = try lowering.objc().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); + const e2 = try lowering.objc().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); + const e3 = try lowering.objc().objcTypeEncodingFromSignature(.void, &.{s32_many}, null); defer alloc.free(e3); try std.testing.expectEqualStrings("v@:^v", e3); } @@ -390,7 +390,7 @@ test "lower: objcDefinedStateStructType collects user-declared fields" { .is_main = false, }; - const state_ty = lowering.objcDefinedStateStructType(&fcd); + const state_ty = lowering.objc().objcDefinedStateStructType(&fcd); const info = module.types.get(state_ty); try std.testing.expectEqual(@as(std.meta.Tag(@TypeOf(info)), .@"struct"), std.meta.activeTag(info)); @@ -403,7 +403,7 @@ test "lower: objcDefinedStateStructType collects user-declared fields" { try std.testing.expectEqual(TypeId.s64, s.fields[1].ty); // Idempotency: a second call returns the same TypeId (cache hit on name). - const state_ty2 = lowering.objcDefinedStateStructType(&fcd); + const state_ty2 = lowering.objc().objcDefinedStateStructType(&fcd); try std.testing.expectEqual(state_ty, state_ty2); } @@ -422,7 +422,7 @@ test "lower: objcDefinedStateStructType handles empty field set" { .is_main = false, }; - const state_ty = lowering.objcDefinedStateStructType(&fcd); + const state_ty = lowering.objc().objcDefinedStateStructType(&fcd); const info = module.types.get(state_ty); try std.testing.expectEqualStrings("__SxEmptyState", module.types.getString(info.@"struct".name)); try std.testing.expectEqual(@as(usize, 0), info.@"struct".fields.len); @@ -454,7 +454,7 @@ test "lower: objcDefinedStateStructType skips non-field members" { .is_main = false, }; - const state_ty = lowering.objcDefinedStateStructType(&fcd); + const state_ty = lowering.objc().objcDefinedStateStructType(&fcd); const info = module.types.get(state_ty); try std.testing.expectEqual(@as(usize, 1), info.@"struct".fields.len); try std.testing.expectEqualStrings("counter", module.types.getString(info.@"struct".fields[0].name)); @@ -482,12 +482,12 @@ test "lower: objcTypeEncodingFromSignature emits @ for Obj-C class pointers" { try lowering.program_index.foreign_class_map.put("NSString", &ns_fcd); // Return *NSString, no args: "@@:" - const e1 = try lowering.objcTypeEncodingFromSignature(ns_ptr, &.{}, null); + const e1 = try lowering.objc().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); + const e2 = try lowering.objc().objcTypeEncodingFromSignature(ns_ptr, &.{ns_ptr}, null); defer alloc.free(e2); try std.testing.expectEqualStrings("@@:@", e2); } @@ -515,14 +515,14 @@ test "lower: objcTypeEncodingFromSignature unwraps optional to wire type" { // `?s64 -> ?*NSString` collapses to `q -> @` at the Obj-C boundary. const opt_s64 = module.types.optionalOf(.s64); const opt_ns = module.types.optionalOf(ns_ptr); - const e1 = try lowering.objcTypeEncodingFromSignature(opt_ns, &.{opt_s64}, null); + const e1 = try lowering.objc().objcTypeEncodingFromSignature(opt_ns, &.{opt_s64}, null); defer alloc.free(e1); try std.testing.expectEqualStrings("@@:q", e1); // Nested optional unwrap (`??f64`) — same as `f64` at the wire. const opt_f64 = module.types.optionalOf(.f64); const opt_opt_f64 = module.types.optionalOf(opt_f64); - const e2 = try lowering.objcTypeEncodingFromSignature(.void, &.{opt_opt_f64}, null); + const e2 = try lowering.objc().objcTypeEncodingFromSignature(.void, &.{opt_opt_f64}, null); defer alloc.free(e2); try std.testing.expectEqualStrings("v@:d", e2); } @@ -544,12 +544,12 @@ test "lower: objcTypeEncodingFromSignature emits structs as {Name=fields...}" { const cgpoint = module.types.intern(.{ .@"struct" = .{ .name = cgpoint_name, .fields = &cgpoint_fields } }); // `-(void)setOrigin:(CGPoint)p` → `v@:{CGPoint=dd}` - const e1 = try lowering.objcTypeEncodingFromSignature(.void, &.{cgpoint}, null); + const e1 = try lowering.objc().objcTypeEncodingFromSignature(.void, &.{cgpoint}, null); defer alloc.free(e1); try std.testing.expectEqualStrings("v@:{CGPoint=dd}", e1); // `-(CGPoint)origin` → `{CGPoint=dd}@:` - const e2 = try lowering.objcTypeEncodingFromSignature(cgpoint, &.{}, null); + const e2 = try lowering.objc().objcTypeEncodingFromSignature(cgpoint, &.{}, null); defer alloc.free(e2); try std.testing.expectEqualStrings("{CGPoint=dd}@:", e2); @@ -564,7 +564,7 @@ test "lower: objcTypeEncodingFromSignature emits structs as {Name=fields...}" { .{ .name = len_name, .ty = .u64 }, }; const nsrange = module.types.intern(.{ .@"struct" = .{ .name = nsrange_name, .fields = &nsrange_fields } }); - const e3 = try lowering.objcTypeEncodingFromSignature(nsrange, &.{ nsrange, .s64 }, null); + const e3 = try lowering.objc().objcTypeEncodingFromSignature(nsrange, &.{ nsrange, .s64 }, null); defer alloc.free(e3); try std.testing.expectEqualStrings("{_NSRange=QQ}@:{_NSRange=QQ}q", e3); } @@ -606,12 +606,12 @@ test "lower: objcTypeEncodingFromSignature emits nested structs (CGRect)" { const cgrect = module.types.intern(.{ .@"struct" = .{ .name = cgrect_name, .fields = &cgrect_fields } }); // `-(CGRect)frame` → `{CGRect={CGPoint=dd}{CGSize=dd}}@:` - const e1 = try lowering.objcTypeEncodingFromSignature(cgrect, &.{}, null); + const e1 = try lowering.objc().objcTypeEncodingFromSignature(cgrect, &.{}, null); defer alloc.free(e1); try std.testing.expectEqualStrings("{CGRect={CGPoint=dd}{CGSize=dd}}@:", e1); // `-(void)setFrame:(CGRect)f` round-trip. - const e2 = try lowering.objcTypeEncodingFromSignature(.void, &.{cgrect}, null); + const e2 = try lowering.objc().objcTypeEncodingFromSignature(.void, &.{cgrect}, null); defer alloc.free(e2); try std.testing.expectEqualStrings("v@:{CGRect={CGPoint=dd}{CGSize=dd}}", e2); } @@ -631,20 +631,20 @@ test "lower: deriveObjcSelector — niladic / keyword / multi-keyword / override var lowering = Lowering.init(&module); // arity 0 → bare name, no colons, not an override. - const niladic = lowering.deriveObjcSelector(objcMethod("count"), 0); + const niladic = lowering.objc().deriveObjcSelector(objcMethod("count"), 0); try std.testing.expectEqualStrings("count", niladic.sel); try std.testing.expectEqual(@as(usize, 0), niladic.keyword_count); try std.testing.expectEqual(false, niladic.is_override); // arity ≥ 1, no `_` → single trailing colon, one keyword. - const single = lowering.deriveObjcSelector(objcMethod("setValue"), 1); + const single = lowering.objc().deriveObjcSelector(objcMethod("setValue"), 1); defer alloc.free(single.sel); try std.testing.expectEqualStrings("setValue:", single.sel); try std.testing.expectEqual(@as(usize, 1), single.keyword_count); try std.testing.expectEqual(false, single.is_override); // each `_` → `:`, plus a trailing `:`; piece count = (#`_`) + 1. - const multi = lowering.deriveObjcSelector(objcMethod("setValue_forKey"), 2); + const multi = lowering.objc().deriveObjcSelector(objcMethod("setValue_forKey"), 2); defer alloc.free(multi.sel); try std.testing.expectEqualStrings("setValue:forKey:", multi.sel); try std.testing.expectEqual(@as(usize, 2), multi.keyword_count); @@ -653,7 +653,7 @@ test "lower: deriveObjcSelector — niladic / keyword / multi-keyword / override // `#selector(...)` override: used verbatim, keyword_count = #colons. var m = objcMethod("init_with_frame_style"); m.selector_override = "initWithFrame:style:"; - const overridden = lowering.deriveObjcSelector(m, 2); + const overridden = lowering.objc().deriveObjcSelector(m, 2); try std.testing.expectEqualStrings("initWithFrame:style:", overridden.sel); try std.testing.expectEqual(@as(usize, 2), overridden.keyword_count); try std.testing.expectEqual(true, overridden.is_override); @@ -678,7 +678,7 @@ test "lower: isObjcClassPointer recognises pointer-to-foreign-Obj-C-class" { .is_main = false, }; try lowering.program_index.foreign_class_map.put("NSString", &ns_fcd); - try std.testing.expect(lowering.isObjcClassPointer(ns_ptr)); + try std.testing.expect(lowering.objc().isObjcClassPointer(ns_ptr)); // *NSCopying where NSCopying is a registered Obj-C *protocol* → also true // (the predicate accepts .objc_class OR .objc_protocol). @@ -694,16 +694,16 @@ test "lower: isObjcClassPointer recognises pointer-to-foreign-Obj-C-class" { .is_main = false, }; try lowering.program_index.foreign_class_map.put("NSCopying", &proto_fcd); - try std.testing.expect(lowering.isObjcClassPointer(proto_ptr)); + try std.testing.expect(lowering.objc().isObjcClassPointer(proto_ptr)); // *Plain where Plain is a non-foreign struct → false. const plain_name = module.types.internString("Plain"); const plain_struct = module.types.intern(.{ .@"struct" = .{ .name = plain_name, .fields = &.{} } }); - try std.testing.expect(!lowering.isObjcClassPointer(module.types.ptrTo(plain_struct))); + try std.testing.expect(!lowering.objc().isObjcClassPointer(module.types.ptrTo(plain_struct))); // *void and a builtin scalar → false (not object pointers). - try std.testing.expect(!lowering.isObjcClassPointer(module.types.ptrTo(.void))); - try std.testing.expect(!lowering.isObjcClassPointer(.s32)); + try std.testing.expect(!lowering.objc().isObjcClassPointer(module.types.ptrTo(.void))); + try std.testing.expect(!lowering.objc().isObjcClassPointer(.s32)); } test "lower: objcPropertyKind defaults + explicit ARC modifiers" { @@ -728,13 +728,13 @@ test "lower: objcPropertyKind defaults + explicit ARC modifiers" { // Primitive field, no modifiers → assign (the non-object default). const prim = ast.ForeignFieldDecl{ .name = "count", .field_type = typeKeyword(alloc, "s32"), .is_property = true }; defer alloc.destroy(prim.field_type); - try std.testing.expect(lowering.objcPropertyKind(prim) == .assign); + try std.testing.expect(lowering.objc().objcPropertyKind(prim) == .assign); // Object-pointer field, no modifiers → strong (the object default). const obj_ty = typeKeyword(alloc, "*NSString"); defer alloc.destroy(obj_ty); const obj_default = ast.ForeignFieldDecl{ .name = "title", .field_type = obj_ty, .is_property = true }; - try std.testing.expect(lowering.objcPropertyKind(obj_default) == .strong); + try std.testing.expect(lowering.objc().objcPropertyKind(obj_default) == .strong); // Protocol-pointer field → also strong by default (same object-pointer // predicate accepts .objc_protocol). @@ -752,17 +752,17 @@ test "lower: objcPropertyKind defaults + explicit ARC modifiers" { const proto_ty = typeKeyword(alloc, "*NSCoding"); defer alloc.destroy(proto_ty); const proto_default = ast.ForeignFieldDecl{ .name = "coder", .field_type = proto_ty, .is_property = true }; - try std.testing.expect(lowering.objcPropertyKind(proto_default) == .strong); + try std.testing.expect(lowering.objc().objcPropertyKind(proto_default) == .strong); // Explicit modifiers on an object pointer win over the default. const weak_mods = [_][]const u8{"weak"}; - try std.testing.expect(lowering.objcPropertyKind(.{ .name = "delegate", .field_type = obj_ty, .is_property = true, .property_modifiers = &weak_mods }) == .weak); + try std.testing.expect(lowering.objc().objcPropertyKind(.{ .name = "delegate", .field_type = obj_ty, .is_property = true, .property_modifiers = &weak_mods }) == .weak); const copy_mods = [_][]const u8{"copy"}; - try std.testing.expect(lowering.objcPropertyKind(.{ .name = "name", .field_type = obj_ty, .is_property = true, .property_modifiers = ©_mods }) == .copy); + try std.testing.expect(lowering.objc().objcPropertyKind(.{ .name = "name", .field_type = obj_ty, .is_property = true, .property_modifiers = ©_mods }) == .copy); const assign_mods = [_][]const u8{"assign"}; - try std.testing.expect(lowering.objcPropertyKind(.{ .name = "raw", .field_type = obj_ty, .is_property = true, .property_modifiers = &assign_mods }) == .assign); + try std.testing.expect(lowering.objc().objcPropertyKind(.{ .name = "raw", .field_type = obj_ty, .is_property = true, .property_modifiers = &assign_mods }) == .assign); } // ── Pack projection name resolution (Feature 1, Step 2.2) ──────────── diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 395ba6e..2573880 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -29,6 +29,7 @@ const ProtocolResolver = @import("protocols.zig").ProtocolResolver; const CoercionResolver = @import("conversions.zig").CoercionResolver; const ErrorAnalysis = @import("error_analysis.zig").ErrorAnalysis; const ErrorFlow = @import("error_flow.zig").ErrorFlow; +const ObjcLowering = @import("ffi_objc.zig").ObjcLowering; const semantic_diagnostics = @import("semantic_diagnostics.zig"); const TypeId = types.TypeId; @@ -4714,7 +4715,7 @@ pub const Lowering = struct { // typed Class(T) parameterization is M1.1.b). if (std.mem.eql(u8, fa.field, "class")) { const expr_ty = self.inferExprType(fa.object); - if (self.isObjcClassPointer(expr_ty)) { + if (self.objc().isObjcClassPointer(expr_ty)) { const obj_ref = self.lowerExpr(fa.object); const ptr_void = self.module.types.ptrTo(.void); const get_class_fid = self.ensureCRuntimeDecl("object_getClass", &.{ptr_void}, ptr_void); @@ -6020,224 +6021,10 @@ pub const Lowering = struct { } }, ret_ty); } - /// Resolve the Obj-C selector for a foreign-class method, honoring - /// any `#selector("...")` override on the declaration. When an - /// override is present the selector string is the user's literal; - /// `keyword_count` is the `:` count in the literal (so callers can - /// still cross-check arity, downgrading the diagnostic to a - /// warning). When no override exists, the default mangling rule - /// runs: - /// - niladic: name verbatim (`length` → `length`). - /// - arity ≥ 1: split the sx name on `_`; each piece becomes a - /// keyword with a trailing `:` (`addObject` → `addObject:`, - /// `combine_and` → `combine:and:`). - pub fn deriveObjcSelector(self: *Lowering, 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.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: ` @ : ...`. 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. 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.alloc`. - pub 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); - - var stack: ObjcEncodingStack = .{}; - try self.appendObjcEncoding(&out, return_ty, span, &stack); - try out.append(self.alloc, '@'); // self - try out.append(self.alloc, ':'); // _cmd - for (param_tys) |pty| { - try self.appendObjcEncoding(&out, pty, span, &stack); - } - - return try out.toOwnedSlice(self.alloc); - } - - /// 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; - } - }; - - fn appendObjcEncoding( - self: *Lowering, - out: *std.ArrayList(u8), - ty: TypeId, - span: ?ast.Span, - stack: *ObjcEncodingStack, - ) !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.program_index.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"); - } - }, - .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.module.types.getString(s.name); - try out.append(self.alloc, '{'); - try out.appendSlice(self.alloc, name); - if (stack.contains(ty)) { - try out.append(self.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.alloc, '='); - for (s.fields) |f| { - try self.appendObjcEncoding(out, f.ty, span, stack); - } - try out.append(self.alloc, '}'); - }, - 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; - } + // Pure Obj-C decision helpers (selector derivation, type-encoding, ARC + // property-kind, class-pointer recognition, state-struct planning) live in + // `ffi_objc.zig` (`ObjcLowering`, a `*Lowering` facade). Reached via + // `self.objc()`. Emission-heavy IMP builders + `lowerObjc*Call` stay here. /// Resolve a foreign-class member type, substituting `Self` (and `*Self`) /// with the foreign class's own struct type. Without this substitution @@ -6275,76 +6062,6 @@ pub const Lowering = struct { return self.module.types.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } }); } - /// 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): - /// - /// __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: *Lowering, fcd: *const ast.ForeignClassDecl) TypeId { - const state_name = std.fmt.allocPrint(self.alloc, "__{s}State", .{fcd.name}) catch unreachable; - defer self.alloc.free(state_name); // internString copies; the temp isn't needed after. - const name_id = self.module.types.internString(state_name); - if (self.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.alloc`. - const field_alloc = self.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.module.types.internString("__sx_allocator"), - .ty = allocator_ty, - }) catch unreachable; - } - for (fcd.members) |m| { - switch (m) { - .field => |f| { - const f_name_id = self.module.types.internString(f.name); - const f_ty = self.resolveType(f.field_type); - fields.append(field_alloc, .{ .name = f_name_id, .ty = f_ty }) catch unreachable; - }, - else => {}, - } - } - return self.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: *Lowering) ?TypeId { - const ctx_name = self.module.types.internString("Context"); - const ctx_ty = self.module.types.findByName(ctx_name) orelse return null; - const ctx_info = self.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; - } - /// Lower `inst.method(args)` on an `#objc_class` / `#objc_protocol` /// receiver. The selector is derived by `deriveObjcSelector`; arity /// is validated against the keyword count produced by the mangling @@ -6359,7 +6076,7 @@ pub const Lowering = struct { span: ast.Span, ) Ref { const arity = method_args.len; - const derived = self.deriveObjcSelector(method, arity); + const derived = self.objc().deriveObjcSelector(method, arity); // Arity validation: the keyword count (number of `:` in the // selector) must equal the number of args passed at the call @@ -6422,7 +6139,7 @@ pub const Lowering = struct { span: ast.Span, ) Ref { const arity = method_args.len; - const derived = self.deriveObjcSelector(method, arity); + const derived = self.objc().deriveObjcSelector(method, arity); if (arity > 0 and derived.keyword_count != arity) { if (self.diagnostics) |d| { @@ -12851,7 +12568,7 @@ pub const Lowering = struct { // (skipped); class methods have no self in the AST. const user_param_start: usize = if (method.is_static) 0 else 1; const user_arg_count = if (method.params.len > user_param_start) method.params.len - user_param_start else 0; - const sel_info = self.deriveObjcSelector(method, user_arg_count); + const sel_info = self.objc().deriveObjcSelector(method, user_arg_count); const ret_ty: TypeId = if (method.return_type) |rt| self.resolveType(rt) else .void; var arg_tys = std.ArrayList(TypeId).empty; @@ -12861,7 +12578,7 @@ pub const Lowering = struct { arg_tys.append(self.alloc, self.resolveType(p_node)) catch unreachable; } } - const encoding = self.objcTypeEncodingFromSignature(ret_ty, arg_tys.items, null) catch continue; + const encoding = self.objc().objcTypeEncodingFromSignature(ret_ty, arg_tys.items, null) catch continue; const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, method.name }) catch continue; @@ -13482,6 +13199,10 @@ pub const Lowering = struct { return .{ .l = self }; } + pub fn objc(self: *Lowering) ObjcLowering { + return .{ .l = self }; + } + /// Lower the `xx` operator (type coercion). /// Uses self.target_type for context when available. Handles: /// - Any → concrete type: unbox_any @@ -15463,21 +15184,6 @@ pub const Lowering = struct { self.emitObjcDefinedClassImps(); } - /// True if `ty` is a pointer to a struct whose name is registered - /// in `foreign_class_map` under an Obj-C runtime. Used by the - /// `obj.class` accessor (M1.3) to decide whether to lower the - /// field access as a struct GEP or as `object_getClass(obj)`. - pub fn isObjcClassPointer(self: *Lowering, ty: TypeId) bool { - if (ty.isBuiltin()) return false; - const ptr_info = self.module.types.get(ty); - if (ptr_info != .pointer) return false; - const pointee_info = self.module.types.get(ptr_info.pointer.pointee); - if (pointee_info != .@"struct") return false; - const struct_name = self.module.types.getString(pointee_info.@"struct".name); - const fcd = self.program_index.foreign_class_map.get(struct_name) orelse return false; - return fcd.runtime == .objc_class or fcd.runtime == .objc_protocol; - } - /// If `obj_expr` is typed as a pointer to a foreign Obj-C class /// and that class (or any of its `#extends` ancestors) declares a /// `#property` field with the given name, return the @@ -15571,7 +15277,7 @@ pub const Lowering = struct { .field => |f| { if (std.mem.eql(u8, f.name, field_name)) { if (f.is_property) return null; - const state_ty = self.objcDefinedStateStructType(fcd); + const state_ty = self.objc().objcDefinedStateStructType(fcd); const state_info = self.module.types.get(state_ty); if (state_info != .@"struct") return null; const fname_id = self.module.types.internString(f.name); @@ -15749,6 +15455,20 @@ pub const Lowering = struct { } } + /// Lazily declare libobjc's ARC runtime helpers. Idempotent — uses + /// `ensureCRuntimeDecl` which skips already-declared symbols. Called + /// from the property setter/getter and -dealloc emission paths when + /// they need to emit a retain/release/storeWeak/etc. + fn ensureArcRuntimeDecls(self: *Lowering) void { + const ptr_void = self.module.types.ptrTo(.void); + _ = self.ensureCRuntimeDecl("objc_retain", &.{ptr_void}, ptr_void); + _ = self.ensureCRuntimeDecl("objc_release", &.{ptr_void}, .void); + _ = self.ensureCRuntimeDecl("objc_storeWeak", &.{ ptr_void, ptr_void }, ptr_void); + _ = self.ensureCRuntimeDecl("objc_loadWeakRetained", &.{ptr_void}, ptr_void); + _ = self.ensureCRuntimeDecl("objc_initWeak", &.{ ptr_void, ptr_void }, ptr_void); + _ = self.ensureCRuntimeDecl("objc_destroyWeak", &.{ptr_void}, .void); + } + /// M2.2 second pass — emit synthesized getter/setter IMPs for a /// property field on a sx-defined `#objc_class`. The state struct /// already holds the field (via objcDefinedStateStructType); the @@ -15766,143 +15486,8 @@ pub const Lowering = struct { /// Both IMPs land in the cache's methods slice with appropriate /// selectors + encodings; emit_llvm's class_addMethod loop wires /// them up like any other instance method. - /// M4.B — interpretation of `#property(...)` modifiers for ARC. - /// `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 * — 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; - } - }; - - /// 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: *Lowering, 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.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.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.resolveType(field.field_type); - const is_pointer = !field_ty.isBuiltin() and self.module.types.get(field_ty) == .pointer; - const is_object_ptr = is_pointer and blk: { - const pointee = self.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.module.types.get(pointee); - if (pointee_info != .@"struct") break :blk false; - const struct_name = self.module.types.getString(pointee_info.@"struct".name); - const fcd = self.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 s32` is meaningless and - // would invoke objc_storeWeak on a non-object slot. - if (has_weak and !is_object_ptr) { - if (self.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.module.types.typeName(field_ty) }); - } - } - - // `copy` requires an object pointer — `copy s32` makes no sense. - if (has_copy and !is_object_ptr) { - if (self.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.module.types.get(field_ty).pointer.pointee; - if (pointee == .void and explicit_count == 0) { - if (self.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; - } - - /// Lazily declare libobjc's ARC runtime helpers. Idempotent — uses - /// `ensureCRuntimeDecl` which skips already-declared symbols. Called - /// from the property setter/getter and -dealloc emission paths when - /// they need to emit a retain/release/storeWeak/etc. - fn ensureArcRuntimeDecls(self: *Lowering) void { - const ptr_void = self.module.types.ptrTo(.void); - _ = self.ensureCRuntimeDecl("objc_retain", &.{ptr_void}, ptr_void); - _ = self.ensureCRuntimeDecl("objc_release", &.{ptr_void}, .void); - _ = self.ensureCRuntimeDecl("objc_storeWeak", &.{ ptr_void, ptr_void }, ptr_void); - _ = self.ensureCRuntimeDecl("objc_loadWeakRetained", &.{ptr_void}, ptr_void); - _ = self.ensureCRuntimeDecl("objc_initWeak", &.{ ptr_void, ptr_void }, ptr_void); - _ = self.ensureCRuntimeDecl("objc_destroyWeak", &.{ptr_void}, .void); - } - fn emitObjcDefinedClassPropertyImps(self: *Lowering, fcd: *const ast.ForeignClassDecl, field: ast.ForeignFieldDecl) void { - const state_ty = self.objcDefinedStateStructType(fcd); + const state_ty = self.objc().objcDefinedStateStructType(fcd); const state_info = self.module.types.get(state_ty); if (state_info != .@"struct") return; // Find the field's index in the state struct. @@ -15921,7 +15506,7 @@ pub const Lowering = struct { // diagnostics for typos, weak-on-non-object, ambiguous *void, etc. // For now the setter/getter still emit bare load/store; subsequent // M4.B commits wire the actual ARC ops keyed on this kind. - _ = self.objcPropertyKind(field); + _ = self.objc().objcPropertyKind(field); // (1) Getter: ____imp self.emitObjcDefinedPropertyGetter(fcd, field, state_ty, fidx, field_ty); @@ -15992,7 +15577,7 @@ pub const Lowering = struct { // objc_autorelease for race-safe reads. The bare-load path // (strong/copy/assign) is the common case and reads the slot // directly. - const kind = self.objcPropertyKind(field); + const kind = self.objc().objcPropertyKind(field); if (kind == .weak) { self.ensureArcRuntimeDecls(); const load_weak_fid = self.ensureCRuntimeDecl("objc_loadWeakRetained", &.{ptr_void}, ptr_void); @@ -16078,7 +15663,7 @@ pub const Lowering = struct { const field_addr = self.builder.emit(.{ .struct_gep = .{ .base = state_ptr, .field_index = fidx, .base_type = state_ty } }, ptr_void); // M4.B setter — emit ARC ops based on the property's modifier kind. - const kind = self.objcPropertyKind(field); + const kind = self.objc().objcPropertyKind(field); switch (kind) { .assign => { // Primitives or explicit assign: bare store, no ARC. @@ -16160,7 +15745,7 @@ pub const Lowering = struct { for (entry.methods) |m| new_methods.append(self.alloc, m) catch unreachable; // Getter entry — selector = field name, encoding = "@:". - const getter_enc = self.objcTypeEncodingFromSignature(field_ty, &.{}, null) catch return; + const getter_enc = self.objc().objcTypeEncodingFromSignature(field_ty, &.{}, null) catch return; const getter_imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, field.name }) catch return; new_methods.append(self.alloc, .{ .sel = field.name, @@ -16181,7 +15766,7 @@ pub const Lowering = struct { sel_buf.append(self.alloc, ':') catch unreachable; const setter_sel = self.alloc.dupe(u8, sel_buf.items) catch return; - const setter_enc = self.objcTypeEncodingFromSignature(.void, &.{field_ty}, null) catch return; + const setter_enc = self.objc().objcTypeEncodingFromSignature(.void, &.{field_ty}, null) catch return; var setter_imp_field_buf = std.ArrayList(u8).empty; defer setter_imp_field_buf.deinit(self.alloc); @@ -16412,7 +15997,7 @@ pub const Lowering = struct { const instance = self.builder.emit(.{ .call = .{ .callee = create_fid, .args = create_args } }, ptr_void); // STATE_SIZE = max(typeSizeBytes(__State), 1). - const state_struct_ty = self.objcDefinedStateStructType(fcd); + const state_struct_ty = self.objc().objcDefinedStateStructType(fcd); const raw_size = self.module.types.typeSizeBytes(state_struct_ty); const state_size: u64 = if (raw_size == 0) 1 else @intCast(raw_size); const size_const = self.builder.constInt(@intCast(state_size), .u64); @@ -16639,7 +16224,7 @@ pub const Lowering = struct { // (which would invalidate the pointers we need to read). Property // metadata is re-derived from `fcd.members`; the state struct is // already interned via objcDefinedStateStructType. - const state_struct_ty = self.objcDefinedStateStructType(fcd); + const state_struct_ty = self.objc().objcDefinedStateStructType(fcd); const state_info_check = self.module.types.get(state_struct_ty); if (state_info_check == .@"struct") { const state_fields = state_info_check.@"struct".fields; @@ -16658,7 +16243,7 @@ pub const Lowering = struct { } const fidx = pfidx orelse continue; const field_ty = self.resolveType(f.field_type); - const kind = self.objcPropertyKind(f); + const kind = self.objc().objcPropertyKind(f); switch (kind) { .assign => {}, // no ARC ops