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:
@@ -127,7 +127,7 @@ pub const ExprTyper = struct {
|
|||||||
}
|
}
|
||||||
// M1.3 — `obj.class` on an Obj-C-class pointer returns Class (*void).
|
// M1.3 — `obj.class` on an Obj-C-class pointer returns Class (*void).
|
||||||
if (std.mem.eql(u8, fa.field, "class")) {
|
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);
|
return self.l.module.types.ptrTo(.void);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
428
src/ir/ffi_objc.zig
Normal file
428
src/ir/ffi_objc.zig
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -14,6 +14,7 @@ pub const protocols = @import("protocols.zig");
|
|||||||
pub const conversions = @import("conversions.zig");
|
pub const conversions = @import("conversions.zig");
|
||||||
pub const error_analysis = @import("error_analysis.zig");
|
pub const error_analysis = @import("error_analysis.zig");
|
||||||
pub const error_flow = @import("error_flow.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 semantic_diagnostics = @import("semantic_diagnostics.zig");
|
||||||
|
|
||||||
pub const TypeId = types.TypeId;
|
pub const TypeId = types.TypeId;
|
||||||
@@ -54,6 +55,7 @@ pub const CoercionResolver = conversions.CoercionResolver;
|
|||||||
pub const CoercionPlan = conversions.CoercionResolver.CoercionPlan;
|
pub const CoercionPlan = conversions.CoercionResolver.CoercionPlan;
|
||||||
pub const ErrorAnalysis = error_analysis.ErrorAnalysis;
|
pub const ErrorAnalysis = error_analysis.ErrorAnalysis;
|
||||||
pub const ErrorFlow = error_flow.ErrorFlow;
|
pub const ErrorFlow = error_flow.ErrorFlow;
|
||||||
|
pub const ObjcLowering = ffi_objc.ObjcLowering;
|
||||||
pub const ErrorFacts = error_analysis.ErrorFacts;
|
pub const ErrorFacts = error_analysis.ErrorFacts;
|
||||||
|
|
||||||
pub const compiler_hooks = @import("compiler_hooks.zig");
|
pub const compiler_hooks = @import("compiler_hooks.zig");
|
||||||
|
|||||||
@@ -296,42 +296,42 @@ test "lower: objcTypeEncodingFromSignature emits primitive shapes" {
|
|||||||
var lowering = Lowering.init(&module);
|
var lowering = Lowering.init(&module);
|
||||||
|
|
||||||
// Niladic void method: -(void)greet → "v@:"
|
// 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);
|
defer alloc.free(e1);
|
||||||
try std.testing.expectEqualStrings("v@:", e1);
|
try std.testing.expectEqualStrings("v@:", e1);
|
||||||
|
|
||||||
// Returns s32, takes s32: -(int)add:(int)x → "i@:i"
|
// 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);
|
defer alloc.free(e2);
|
||||||
try std.testing.expectEqualStrings("i@:i", e2);
|
try std.testing.expectEqualStrings("i@:i", e2);
|
||||||
|
|
||||||
// s64 return, two s64 args: "q@:qq"
|
// 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);
|
defer alloc.free(e3);
|
||||||
try std.testing.expectEqualStrings("q@:qq", e3);
|
try std.testing.expectEqualStrings("q@:qq", e3);
|
||||||
|
|
||||||
// BOOL return (s8): "c@:"
|
// BOOL return (s8): "c@:"
|
||||||
const e4 = try lowering.objcTypeEncodingFromSignature(.s8, &.{}, null);
|
const e4 = try lowering.objc().objcTypeEncodingFromSignature(.s8, &.{}, null);
|
||||||
defer alloc.free(e4);
|
defer alloc.free(e4);
|
||||||
try std.testing.expectEqualStrings("c@:", e4);
|
try std.testing.expectEqualStrings("c@:", e4);
|
||||||
|
|
||||||
// Float/double: "f@:d"
|
// 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);
|
defer alloc.free(e5);
|
||||||
try std.testing.expectEqualStrings("f@:d", e5);
|
try std.testing.expectEqualStrings("f@:d", e5);
|
||||||
|
|
||||||
// bool (i1) is `B` — distinct from BOOL (`c`).
|
// 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);
|
defer alloc.free(e6);
|
||||||
try std.testing.expectEqualStrings("B@:B", e6);
|
try std.testing.expectEqualStrings("B@:B", e6);
|
||||||
|
|
||||||
// usize / isize on the 64-bit target.
|
// 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);
|
defer alloc.free(e7);
|
||||||
try std.testing.expectEqualStrings("Q@:q", e7);
|
try std.testing.expectEqualStrings("Q@:q", e7);
|
||||||
|
|
||||||
// Unsigned variants u8/u16/u32/u64.
|
// 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);
|
defer alloc.free(e8);
|
||||||
try std.testing.expectEqualStrings("I@:CSQ", e8);
|
try std.testing.expectEqualStrings("I@:CSQ", e8);
|
||||||
}
|
}
|
||||||
@@ -344,19 +344,19 @@ test "lower: objcTypeEncodingFromSignature emits pointer shapes" {
|
|||||||
|
|
||||||
// Generic `*void` → `^v`.
|
// Generic `*void` → `^v`.
|
||||||
const void_ptr = module.types.ptrTo(.void);
|
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);
|
defer alloc.free(e1);
|
||||||
try std.testing.expectEqualStrings("^v@:^v", e1);
|
try std.testing.expectEqualStrings("^v@:^v", e1);
|
||||||
|
|
||||||
// `[*]u8` C-string carrier → `*`.
|
// `[*]u8` C-string carrier → `*`.
|
||||||
const u8_many = module.types.intern(.{ .many_pointer = .{ .element = .u8 } });
|
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);
|
defer alloc.free(e2);
|
||||||
try std.testing.expectEqualStrings("v@:*", e2);
|
try std.testing.expectEqualStrings("v@:*", e2);
|
||||||
|
|
||||||
// `[*]s32` (non-u8 many-pointer) → `^v`.
|
// `[*]s32` (non-u8 many-pointer) → `^v`.
|
||||||
const s32_many = module.types.intern(.{ .many_pointer = .{ .element = .s32 } });
|
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);
|
defer alloc.free(e3);
|
||||||
try std.testing.expectEqualStrings("v@:^v", e3);
|
try std.testing.expectEqualStrings("v@:^v", e3);
|
||||||
}
|
}
|
||||||
@@ -390,7 +390,7 @@ test "lower: objcDefinedStateStructType collects user-declared fields" {
|
|||||||
.is_main = false,
|
.is_main = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const state_ty = lowering.objcDefinedStateStructType(&fcd);
|
const state_ty = lowering.objc().objcDefinedStateStructType(&fcd);
|
||||||
const info = module.types.get(state_ty);
|
const info = module.types.get(state_ty);
|
||||||
try std.testing.expectEqual(@as(std.meta.Tag(@TypeOf(info)), .@"struct"), std.meta.activeTag(info));
|
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);
|
try std.testing.expectEqual(TypeId.s64, s.fields[1].ty);
|
||||||
|
|
||||||
// Idempotency: a second call returns the same TypeId (cache hit on name).
|
// 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);
|
try std.testing.expectEqual(state_ty, state_ty2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,7 +422,7 @@ test "lower: objcDefinedStateStructType handles empty field set" {
|
|||||||
.is_main = false,
|
.is_main = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const state_ty = lowering.objcDefinedStateStructType(&fcd);
|
const state_ty = lowering.objc().objcDefinedStateStructType(&fcd);
|
||||||
const info = module.types.get(state_ty);
|
const info = module.types.get(state_ty);
|
||||||
try std.testing.expectEqualStrings("__SxEmptyState", module.types.getString(info.@"struct".name));
|
try std.testing.expectEqualStrings("__SxEmptyState", module.types.getString(info.@"struct".name));
|
||||||
try std.testing.expectEqual(@as(usize, 0), info.@"struct".fields.len);
|
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,
|
.is_main = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const state_ty = lowering.objcDefinedStateStructType(&fcd);
|
const state_ty = lowering.objc().objcDefinedStateStructType(&fcd);
|
||||||
const info = module.types.get(state_ty);
|
const info = module.types.get(state_ty);
|
||||||
try std.testing.expectEqual(@as(usize, 1), info.@"struct".fields.len);
|
try std.testing.expectEqual(@as(usize, 1), info.@"struct".fields.len);
|
||||||
try std.testing.expectEqualStrings("counter", module.types.getString(info.@"struct".fields[0].name));
|
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);
|
try lowering.program_index.foreign_class_map.put("NSString", &ns_fcd);
|
||||||
|
|
||||||
// Return *NSString, no args: "@@:"
|
// 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);
|
defer alloc.free(e1);
|
||||||
try std.testing.expectEqualStrings("@@:", e1);
|
try std.testing.expectEqualStrings("@@:", e1);
|
||||||
|
|
||||||
// Return *NSString, take *NSString: "@@:@"
|
// 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);
|
defer alloc.free(e2);
|
||||||
try std.testing.expectEqualStrings("@@:@", 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.
|
// `?s64 -> ?*NSString` collapses to `q -> @` at the Obj-C boundary.
|
||||||
const opt_s64 = module.types.optionalOf(.s64);
|
const opt_s64 = module.types.optionalOf(.s64);
|
||||||
const opt_ns = module.types.optionalOf(ns_ptr);
|
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);
|
defer alloc.free(e1);
|
||||||
try std.testing.expectEqualStrings("@@:q", e1);
|
try std.testing.expectEqualStrings("@@:q", e1);
|
||||||
|
|
||||||
// Nested optional unwrap (`??f64`) — same as `f64` at the wire.
|
// Nested optional unwrap (`??f64`) — same as `f64` at the wire.
|
||||||
const opt_f64 = module.types.optionalOf(.f64);
|
const opt_f64 = module.types.optionalOf(.f64);
|
||||||
const opt_opt_f64 = module.types.optionalOf(opt_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);
|
defer alloc.free(e2);
|
||||||
try std.testing.expectEqualStrings("v@:d", 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 } });
|
const cgpoint = module.types.intern(.{ .@"struct" = .{ .name = cgpoint_name, .fields = &cgpoint_fields } });
|
||||||
|
|
||||||
// `-(void)setOrigin:(CGPoint)p` → `v@:{CGPoint=dd}`
|
// `-(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);
|
defer alloc.free(e1);
|
||||||
try std.testing.expectEqualStrings("v@:{CGPoint=dd}", e1);
|
try std.testing.expectEqualStrings("v@:{CGPoint=dd}", e1);
|
||||||
|
|
||||||
// `-(CGPoint)origin` → `{CGPoint=dd}@:`
|
// `-(CGPoint)origin` → `{CGPoint=dd}@:`
|
||||||
const e2 = try lowering.objcTypeEncodingFromSignature(cgpoint, &.{}, null);
|
const e2 = try lowering.objc().objcTypeEncodingFromSignature(cgpoint, &.{}, null);
|
||||||
defer alloc.free(e2);
|
defer alloc.free(e2);
|
||||||
try std.testing.expectEqualStrings("{CGPoint=dd}@:", 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 },
|
.{ .name = len_name, .ty = .u64 },
|
||||||
};
|
};
|
||||||
const nsrange = module.types.intern(.{ .@"struct" = .{ .name = nsrange_name, .fields = &nsrange_fields } });
|
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);
|
defer alloc.free(e3);
|
||||||
try std.testing.expectEqualStrings("{_NSRange=QQ}@:{_NSRange=QQ}q", 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 } });
|
const cgrect = module.types.intern(.{ .@"struct" = .{ .name = cgrect_name, .fields = &cgrect_fields } });
|
||||||
|
|
||||||
// `-(CGRect)frame` → `{CGRect={CGPoint=dd}{CGSize=dd}}@:`
|
// `-(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);
|
defer alloc.free(e1);
|
||||||
try std.testing.expectEqualStrings("{CGRect={CGPoint=dd}{CGSize=dd}}@:", e1);
|
try std.testing.expectEqualStrings("{CGRect={CGPoint=dd}{CGSize=dd}}@:", e1);
|
||||||
|
|
||||||
// `-(void)setFrame:(CGRect)f` round-trip.
|
// `-(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);
|
defer alloc.free(e2);
|
||||||
try std.testing.expectEqualStrings("v@:{CGRect={CGPoint=dd}{CGSize=dd}}", 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);
|
var lowering = Lowering.init(&module);
|
||||||
|
|
||||||
// arity 0 → bare name, no colons, not an override.
|
// 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.expectEqualStrings("count", niladic.sel);
|
||||||
try std.testing.expectEqual(@as(usize, 0), niladic.keyword_count);
|
try std.testing.expectEqual(@as(usize, 0), niladic.keyword_count);
|
||||||
try std.testing.expectEqual(false, niladic.is_override);
|
try std.testing.expectEqual(false, niladic.is_override);
|
||||||
|
|
||||||
// arity ≥ 1, no `_` → single trailing colon, one keyword.
|
// 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);
|
defer alloc.free(single.sel);
|
||||||
try std.testing.expectEqualStrings("setValue:", single.sel);
|
try std.testing.expectEqualStrings("setValue:", single.sel);
|
||||||
try std.testing.expectEqual(@as(usize, 1), single.keyword_count);
|
try std.testing.expectEqual(@as(usize, 1), single.keyword_count);
|
||||||
try std.testing.expectEqual(false, single.is_override);
|
try std.testing.expectEqual(false, single.is_override);
|
||||||
|
|
||||||
// each `_` → `:`, plus a trailing `:`; piece count = (#`_`) + 1.
|
// 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);
|
defer alloc.free(multi.sel);
|
||||||
try std.testing.expectEqualStrings("setValue:forKey:", multi.sel);
|
try std.testing.expectEqualStrings("setValue:forKey:", multi.sel);
|
||||||
try std.testing.expectEqual(@as(usize, 2), multi.keyword_count);
|
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.
|
// `#selector(...)` override: used verbatim, keyword_count = #colons.
|
||||||
var m = objcMethod("init_with_frame_style");
|
var m = objcMethod("init_with_frame_style");
|
||||||
m.selector_override = "initWithFrame: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.expectEqualStrings("initWithFrame:style:", overridden.sel);
|
||||||
try std.testing.expectEqual(@as(usize, 2), overridden.keyword_count);
|
try std.testing.expectEqual(@as(usize, 2), overridden.keyword_count);
|
||||||
try std.testing.expectEqual(true, overridden.is_override);
|
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,
|
.is_main = false,
|
||||||
};
|
};
|
||||||
try lowering.program_index.foreign_class_map.put("NSString", &ns_fcd);
|
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
|
// *NSCopying where NSCopying is a registered Obj-C *protocol* → also true
|
||||||
// (the predicate accepts .objc_class OR .objc_protocol).
|
// (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,
|
.is_main = false,
|
||||||
};
|
};
|
||||||
try lowering.program_index.foreign_class_map.put("NSCopying", &proto_fcd);
|
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.
|
// *Plain where Plain is a non-foreign struct → false.
|
||||||
const plain_name = module.types.internString("Plain");
|
const plain_name = module.types.internString("Plain");
|
||||||
const plain_struct = module.types.intern(.{ .@"struct" = .{ .name = plain_name, .fields = &.{} } });
|
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).
|
// *void and a builtin scalar → false (not object pointers).
|
||||||
try std.testing.expect(!lowering.isObjcClassPointer(module.types.ptrTo(.void)));
|
try std.testing.expect(!lowering.objc().isObjcClassPointer(module.types.ptrTo(.void)));
|
||||||
try std.testing.expect(!lowering.isObjcClassPointer(.s32));
|
try std.testing.expect(!lowering.objc().isObjcClassPointer(.s32));
|
||||||
}
|
}
|
||||||
|
|
||||||
test "lower: objcPropertyKind defaults + explicit ARC modifiers" {
|
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).
|
// Primitive field, no modifiers → assign (the non-object default).
|
||||||
const prim = ast.ForeignFieldDecl{ .name = "count", .field_type = typeKeyword(alloc, "s32"), .is_property = true };
|
const prim = ast.ForeignFieldDecl{ .name = "count", .field_type = typeKeyword(alloc, "s32"), .is_property = true };
|
||||||
defer alloc.destroy(prim.field_type);
|
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).
|
// Object-pointer field, no modifiers → strong (the object default).
|
||||||
const obj_ty = typeKeyword(alloc, "*NSString");
|
const obj_ty = typeKeyword(alloc, "*NSString");
|
||||||
defer alloc.destroy(obj_ty);
|
defer alloc.destroy(obj_ty);
|
||||||
const obj_default = ast.ForeignFieldDecl{ .name = "title", .field_type = obj_ty, .is_property = true };
|
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
|
// Protocol-pointer field → also strong by default (same object-pointer
|
||||||
// predicate accepts .objc_protocol).
|
// predicate accepts .objc_protocol).
|
||||||
@@ -752,17 +752,17 @@ test "lower: objcPropertyKind defaults + explicit ARC modifiers" {
|
|||||||
const proto_ty = typeKeyword(alloc, "*NSCoding");
|
const proto_ty = typeKeyword(alloc, "*NSCoding");
|
||||||
defer alloc.destroy(proto_ty);
|
defer alloc.destroy(proto_ty);
|
||||||
const proto_default = ast.ForeignFieldDecl{ .name = "coder", .field_type = proto_ty, .is_property = true };
|
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.
|
// Explicit modifiers on an object pointer win over the default.
|
||||||
const weak_mods = [_][]const u8{"weak"};
|
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"};
|
const copy_mods = [_][]const u8{"copy"};
|
||||||
try std.testing.expect(lowering.objcPropertyKind(.{ .name = "name", .field_type = obj_ty, .is_property = true, .property_modifiers = ©_mods }) == .copy);
|
try std.testing.expect(lowering.objc().objcPropertyKind(.{ .name = "name", .field_type = obj_ty, .is_property = true, .property_modifiers = ©_mods }) == .copy);
|
||||||
|
|
||||||
const assign_mods = [_][]const u8{"assign"};
|
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) ────────────
|
// ── Pack projection name resolution (Feature 1, Step 2.2) ────────────
|
||||||
|
|||||||
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 CoercionResolver = @import("conversions.zig").CoercionResolver;
|
||||||
const ErrorAnalysis = @import("error_analysis.zig").ErrorAnalysis;
|
const ErrorAnalysis = @import("error_analysis.zig").ErrorAnalysis;
|
||||||
const ErrorFlow = @import("error_flow.zig").ErrorFlow;
|
const ErrorFlow = @import("error_flow.zig").ErrorFlow;
|
||||||
|
const ObjcLowering = @import("ffi_objc.zig").ObjcLowering;
|
||||||
const semantic_diagnostics = @import("semantic_diagnostics.zig");
|
const semantic_diagnostics = @import("semantic_diagnostics.zig");
|
||||||
|
|
||||||
const TypeId = types.TypeId;
|
const TypeId = types.TypeId;
|
||||||
@@ -4714,7 +4715,7 @@ pub const Lowering = struct {
|
|||||||
// typed Class(T) parameterization is M1.1.b).
|
// typed Class(T) parameterization is M1.1.b).
|
||||||
if (std.mem.eql(u8, fa.field, "class")) {
|
if (std.mem.eql(u8, fa.field, "class")) {
|
||||||
const expr_ty = self.inferExprType(fa.object);
|
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 obj_ref = self.lowerExpr(fa.object);
|
||||||
const ptr_void = self.module.types.ptrTo(.void);
|
const ptr_void = self.module.types.ptrTo(.void);
|
||||||
const get_class_fid = self.ensureCRuntimeDecl("object_getClass", &.{ptr_void}, ptr_void);
|
const get_class_fid = self.ensureCRuntimeDecl("object_getClass", &.{ptr_void}, ptr_void);
|
||||||
@@ -6020,224 +6021,10 @@ pub const Lowering = struct {
|
|||||||
} }, ret_ty);
|
} }, ret_ty);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve the Obj-C selector for a foreign-class method, honoring
|
// Pure Obj-C decision helpers (selector derivation, type-encoding, ARC
|
||||||
/// any `#selector("...")` override on the declaration. When an
|
// property-kind, class-pointer recognition, state-struct planning) live in
|
||||||
/// override is present the selector string is the user's literal;
|
// `ffi_objc.zig` (`ObjcLowering`, a `*Lowering` facade). Reached via
|
||||||
/// `keyword_count` is the `:` count in the literal (so callers can
|
// `self.objc()`. Emission-heavy IMP builders + `lowerObjc*Call` stay here.
|
||||||
/// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve a foreign-class member type, substituting `Self` (and `*Self`)
|
/// Resolve a foreign-class member type, substituting `Self` (and `*Self`)
|
||||||
/// with the foreign class's own struct type. Without this substitution
|
/// 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 = &.{} } });
|
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`
|
/// Lower `inst.method(args)` on an `#objc_class` / `#objc_protocol`
|
||||||
/// receiver. The selector is derived by `deriveObjcSelector`; arity
|
/// receiver. The selector is derived by `deriveObjcSelector`; arity
|
||||||
/// is validated against the keyword count produced by the mangling
|
/// is validated against the keyword count produced by the mangling
|
||||||
@@ -6359,7 +6076,7 @@ pub const Lowering = struct {
|
|||||||
span: ast.Span,
|
span: ast.Span,
|
||||||
) Ref {
|
) Ref {
|
||||||
const arity = method_args.len;
|
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
|
// Arity validation: the keyword count (number of `:` in the
|
||||||
// selector) must equal the number of args passed at the call
|
// selector) must equal the number of args passed at the call
|
||||||
@@ -6422,7 +6139,7 @@ pub const Lowering = struct {
|
|||||||
span: ast.Span,
|
span: ast.Span,
|
||||||
) Ref {
|
) Ref {
|
||||||
const arity = method_args.len;
|
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 (arity > 0 and derived.keyword_count != arity) {
|
||||||
if (self.diagnostics) |d| {
|
if (self.diagnostics) |d| {
|
||||||
@@ -12851,7 +12568,7 @@ pub const Lowering = struct {
|
|||||||
// (skipped); class methods have no self in the AST.
|
// (skipped); class methods have no self in the AST.
|
||||||
const user_param_start: usize = if (method.is_static) 0 else 1;
|
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 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;
|
const ret_ty: TypeId = if (method.return_type) |rt| self.resolveType(rt) else .void;
|
||||||
var arg_tys = std.ArrayList(TypeId).empty;
|
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;
|
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;
|
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 };
|
return .{ .l = self };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn objc(self: *Lowering) ObjcLowering {
|
||||||
|
return .{ .l = self };
|
||||||
|
}
|
||||||
|
|
||||||
/// Lower the `xx` operator (type coercion).
|
/// Lower the `xx` operator (type coercion).
|
||||||
/// Uses self.target_type for context when available. Handles:
|
/// Uses self.target_type for context when available. Handles:
|
||||||
/// - Any → concrete type: unbox_any
|
/// - Any → concrete type: unbox_any
|
||||||
@@ -15463,21 +15184,6 @@ pub const Lowering = struct {
|
|||||||
self.emitObjcDefinedClassImps();
|
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
|
/// 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
|
/// and that class (or any of its `#extends` ancestors) declares a
|
||||||
/// `#property` field with the given name, return the
|
/// `#property` field with the given name, return the
|
||||||
@@ -15571,7 +15277,7 @@ pub const Lowering = struct {
|
|||||||
.field => |f| {
|
.field => |f| {
|
||||||
if (std.mem.eql(u8, f.name, field_name)) {
|
if (std.mem.eql(u8, f.name, field_name)) {
|
||||||
if (f.is_property) return null;
|
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);
|
const state_info = self.module.types.get(state_ty);
|
||||||
if (state_info != .@"struct") return null;
|
if (state_info != .@"struct") return null;
|
||||||
const fname_id = self.module.types.internString(f.name);
|
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
|
/// M2.2 second pass — emit synthesized getter/setter IMPs for a
|
||||||
/// property field on a sx-defined `#objc_class`. The state struct
|
/// property field on a sx-defined `#objc_class`. The state struct
|
||||||
/// already holds the field (via objcDefinedStateStructType); the
|
/// 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
|
/// Both IMPs land in the cache's methods slice with appropriate
|
||||||
/// selectors + encodings; emit_llvm's class_addMethod loop wires
|
/// selectors + encodings; emit_llvm's class_addMethod loop wires
|
||||||
/// them up like any other instance method.
|
/// 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 {
|
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);
|
const state_info = self.module.types.get(state_ty);
|
||||||
if (state_info != .@"struct") return;
|
if (state_info != .@"struct") return;
|
||||||
// Find the field's index in the state struct.
|
// 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.
|
// diagnostics for typos, weak-on-non-object, ambiguous *void, etc.
|
||||||
// For now the setter/getter still emit bare load/store; subsequent
|
// For now the setter/getter still emit bare load/store; subsequent
|
||||||
// M4.B commits wire the actual ARC ops keyed on this kind.
|
// M4.B commits wire the actual ARC ops keyed on this kind.
|
||||||
_ = self.objcPropertyKind(field);
|
_ = self.objc().objcPropertyKind(field);
|
||||||
|
|
||||||
// (1) Getter: __<Cls>_<field>_imp
|
// (1) Getter: __<Cls>_<field>_imp
|
||||||
self.emitObjcDefinedPropertyGetter(fcd, field, state_ty, fidx, field_ty);
|
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
|
// objc_autorelease for race-safe reads. The bare-load path
|
||||||
// (strong/copy/assign) is the common case and reads the slot
|
// (strong/copy/assign) is the common case and reads the slot
|
||||||
// directly.
|
// directly.
|
||||||
const kind = self.objcPropertyKind(field);
|
const kind = self.objc().objcPropertyKind(field);
|
||||||
if (kind == .weak) {
|
if (kind == .weak) {
|
||||||
self.ensureArcRuntimeDecls();
|
self.ensureArcRuntimeDecls();
|
||||||
const load_weak_fid = self.ensureCRuntimeDecl("objc_loadWeakRetained", &.{ptr_void}, ptr_void);
|
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);
|
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.
|
// 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) {
|
switch (kind) {
|
||||||
.assign => {
|
.assign => {
|
||||||
// Primitives or explicit assign: bare store, no ARC.
|
// 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;
|
for (entry.methods) |m| new_methods.append(self.alloc, m) catch unreachable;
|
||||||
|
|
||||||
// Getter entry — selector = field name, encoding = "<ret>@:".
|
// 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;
|
const getter_imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, field.name }) catch return;
|
||||||
new_methods.append(self.alloc, .{
|
new_methods.append(self.alloc, .{
|
||||||
.sel = field.name,
|
.sel = field.name,
|
||||||
@@ -16181,7 +15766,7 @@ pub const Lowering = struct {
|
|||||||
sel_buf.append(self.alloc, ':') catch unreachable;
|
sel_buf.append(self.alloc, ':') catch unreachable;
|
||||||
const setter_sel = self.alloc.dupe(u8, sel_buf.items) catch return;
|
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;
|
var setter_imp_field_buf = std.ArrayList(u8).empty;
|
||||||
defer setter_imp_field_buf.deinit(self.alloc);
|
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);
|
const instance = self.builder.emit(.{ .call = .{ .callee = create_fid, .args = create_args } }, ptr_void);
|
||||||
|
|
||||||
// STATE_SIZE = max(typeSizeBytes(__<Cls>State), 1).
|
// 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 raw_size = self.module.types.typeSizeBytes(state_struct_ty);
|
||||||
const state_size: u64 = if (raw_size == 0) 1 else @intCast(raw_size);
|
const state_size: u64 = if (raw_size == 0) 1 else @intCast(raw_size);
|
||||||
const size_const = self.builder.constInt(@intCast(state_size), .u64);
|
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
|
// (which would invalidate the pointers we need to read). Property
|
||||||
// metadata is re-derived from `fcd.members`; the state struct is
|
// metadata is re-derived from `fcd.members`; the state struct is
|
||||||
// already interned via objcDefinedStateStructType.
|
// 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);
|
const state_info_check = self.module.types.get(state_struct_ty);
|
||||||
if (state_info_check == .@"struct") {
|
if (state_info_check == .@"struct") {
|
||||||
const state_fields = state_info_check.@"struct".fields;
|
const state_fields = state_info_check.@"struct".fields;
|
||||||
@@ -16658,7 +16243,7 @@ pub const Lowering = struct {
|
|||||||
}
|
}
|
||||||
const fidx = pfidx orelse continue;
|
const fidx = pfidx orelse continue;
|
||||||
const field_ty = self.resolveType(f.field_type);
|
const field_ty = self.resolveType(f.field_type);
|
||||||
const kind = self.objcPropertyKind(f);
|
const kind = self.objc().objcPropertyKind(f);
|
||||||
|
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
.assign => {}, // no ARC ops
|
.assign => {}, // no ARC ops
|
||||||
|
|||||||
Reference in New Issue
Block a user