refactor(ir): extract ObjcLowering (ffi_objc.zig) for pure Obj-C decision helpers (A6.1 step 2)

Move the pure Obj-C decision helpers out of lower.zig into src/ir/ffi_objc.zig
behind an ObjcLowering *Lowering facade (Principle 5, like the A4/A5 resolvers).
Behavior-preserving relocation — the only non-self.l rewrites are facade
plumbing.

Moved verbatim (self. -> self.l. for Lowering members):
- deriveObjcSelector (selector derivation)
- objcTypeEncodingFromSignature + appendObjcEncoding + bailObjcEncoding +
  the ObjcEncodingStack type
- objcPropertyKind + the ObjcPropertyKind enum
- isObjcClassPointer
- objcDefinedStateStructType + objcStateAllocatorType

Emission-heavy code stays in lower.zig per PLAN A6.1 step 6: emitObjc* IMP
builders, lowerObjc*Call, registerObjc*, declareObjc*, the lookupObjc* property/
state lookups, and the Self-substitution resolvers.

- Call sites rerouted through a new objc() accessor: 15 in lower.zig, 1 in
  expr_typer.zig, 39 in lower.test.zig (the A6.1 scaffolding tests now drive the
  facade). No Lowering wrappers kept. Barrel-wired ffi_objc + ObjcLowering.
- No new visibility widening beyond sub-step 1's two pubs — the facade reads
  self.l.{alloc,module,program_index,diagnostics} (fields) + the already-pub
  resolveType. lower.zig -478 (->16615); ffi_objc.zig 428.
- Doc-only re-home: the property-IMP getter/setter comment was attached (a
  pre-existing artifact) to the moving ObjcPropertyKind enum, two decls away from
  its real subject emitObjcDefinedClassPropertyImps (which had no doc). Re-homed
  it there so the move neither orphans a `///` block (Zig errors on a dangling doc
  comment) nor misattributes it to ensureArcRuntimeDecls.

Gate: zig build, zig build test, bash tests/run_examples.sh -> 361/0
(48 13xx Obj-C examples + 4 Obj-C .ir snapshots green, no churn).
This commit is contained in:
agra
2026-06-03 08:00:42 +03:00
parent b5119e8587
commit 9bde1dd590
5 changed files with 508 additions and 493 deletions

View File

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

428
src/ir/ffi_objc.zig Normal file
View File

@@ -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 *<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=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):
///
/// __<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 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;
}
};

View File

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

View File

@@ -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 = &copy_mods }) == .copy);
try std.testing.expect(lowering.objc().objcPropertyKind(.{ .name = "name", .field_type = obj_ty, .is_property = true, .property_modifiers = &copy_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) ────────────

View File

@@ -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: `<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. 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):
///
/// __<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: *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 *<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;
}
};
/// 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: __<Cls>_<field>_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 = "<ret>@:".
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(__<Cls>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