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:
491
src/ir/lower.zig
491
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: `<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
|
||||
|
||||
Reference in New Issue
Block a user