ffi M2.2 (sx-defined): property getter/setter IMPs
Properties on sx-defined #objc_class declarations now synthesize
getter (always) and setter (unless 'readonly') IMPs that GEP into
the hidden state struct and load / store the corresponding field.
The state struct already holds every user-declared field
(objcDefinedStateStructType), so no new layout work — the IMPs
just dispatch a struct_gep + load/store through the __sx_state
ivar.
For each '#property' field on a sx-defined class:
Getter '__<Cls>_<field>_imp(self, _cmd) -> T':
state = object_getIvar(self, load(__<Cls>_state_ivar))
return state.<field>
Setter '__<Cls>_set<Field>_imp(self, _cmd, val) -> void':
state = object_getIvar(self, load(__<Cls>_state_ivar))
state.<field> = val
Both IMPs land in the cache's methods slice (mirroring the
method-IMP wiring from M1.2 A.4b.iii) so emit_llvm's
class_addMethod loop registers them on the class without
special-casing. Selector mangling:
getter: <field> (e.g. 'width')
setter: set<Field>: (e.g. 'setWidth:')
Type encoding derived from the field's resolved IR TypeId.
'readonly' (the only modifier honored in this slice) skips the
setter emission AND the corresponding method entry — so the
runtime reports the selector as absent. Other modifiers
(strong, weak, copy, assign) parse fine but stay no-ops until
M4.2 wires up ARC ops in the setter body.
152-objc-property-sx-defined.sx round-trips on macOS:
b.width = 10; b.height = 7;
read back through getter IMPs.
area is readonly — class_getInstanceMethod(SxBox, sel(setArea:))
returns NULL, confirming the setter is absent.
182 example tests pass (+1). zig build test green.
This commit is contained in:
72
examples/152-objc-property-sx-defined.sx
Normal file
72
examples/152-objc-property-sx-defined.sx
Normal file
@@ -0,0 +1,72 @@
|
||||
// M2.2 second pass — `#property` on sx-defined `#objc_class`
|
||||
// synthesizes getter/setter IMPs that read/write the hidden
|
||||
// state struct.
|
||||
//
|
||||
// User writes:
|
||||
// field: T #property[(modifiers)];
|
||||
//
|
||||
// Compiler emits:
|
||||
// __<Cls>_<field>_imp(self, _cmd) -> T // load state.field
|
||||
// __<Cls>_set<Field>_imp(self, _cmd, v) -> void // store state.field
|
||||
// (setter skipped for `readonly`.)
|
||||
//
|
||||
// Both register on the class via class_addMethod with auto-derived
|
||||
// selectors and type encodings.
|
||||
//
|
||||
// Verifies dispatch through the real Obj-C runtime by direct
|
||||
// objc_msgSend — round-trips a primitive property and confirms
|
||||
// `readonly` skips setter registration.
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/compiler.sx";
|
||||
#import "modules/std/objc.sx";
|
||||
|
||||
class_getInstanceMethod :: (cls: *void, sel: *void) -> *void #foreign objc;
|
||||
|
||||
SxBox :: #objc_class("SxBox") {
|
||||
width: s32 #property;
|
||||
height: s32 #property;
|
||||
area: s32 #property(readonly); // setter omitted
|
||||
|
||||
alloc :: () -> *SxBox;
|
||||
init :: (self: *SxBox) -> *SxBox;
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
inline if OS == .macos {
|
||||
b := SxBox.alloc().init();
|
||||
|
||||
// width / height round-trip through synthesized IMPs.
|
||||
b.width = 10;
|
||||
b.height = 7;
|
||||
w := b.width;
|
||||
h := b.height;
|
||||
if w != 10 or h != 7 {
|
||||
print("FAIL: width/height round-trip\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `area` is readonly — getter registered, setter NOT.
|
||||
// area starts at 0 (state zero-init); read works:
|
||||
a := b.area;
|
||||
if a != 0 {
|
||||
print("FAIL: area expected 0, got {}\n", a);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Confirm the setter selector is absent on the class.
|
||||
cls : Class = objc_getClass("SxBox".ptr);
|
||||
sel_set_area : SEL = sel_registerName("setArea:".ptr);
|
||||
m := class_getInstanceMethod(cls, sel_set_area);
|
||||
if m != null {
|
||||
print("FAIL: setArea: should not be registered (readonly)\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
print("property: w={} h={} area={}\n", w, h, a);
|
||||
}
|
||||
inline if OS != .macos {
|
||||
print("property: w=10 h=7 area=0\n");
|
||||
}
|
||||
0;
|
||||
}
|
||||
240
src/ir/lower.zig
240
src/ir/lower.zig
@@ -11709,16 +11709,244 @@ pub const Lowering = struct {
|
||||
self.emitObjcDefinedClassAllocImp(fcd);
|
||||
self.emitObjcDefinedClassDeallocImp(fcd);
|
||||
for (fcd.members) |m| {
|
||||
const method = switch (m) {
|
||||
.method => |md| md,
|
||||
else => continue,
|
||||
};
|
||||
if (method.body == null) continue;
|
||||
self.emitObjcDefinedClassImp(fcd, method);
|
||||
switch (m) {
|
||||
.method => |method| {
|
||||
if (method.body == null) continue;
|
||||
self.emitObjcDefinedClassImp(fcd, method);
|
||||
},
|
||||
.field => |field| {
|
||||
// M2.2 second pass — sx-defined property fields
|
||||
// synthesize getter (+ setter unless `readonly`)
|
||||
// IMPs that GEP into the state struct.
|
||||
if (field.is_property) {
|
||||
self.emitObjcDefinedClassPropertyImps(fcd, field);
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// IMPs just dispatch a load/store through the `__sx_state` ivar.
|
||||
///
|
||||
/// Getter IMP: `__<Cls>_<field>_imp(self, _cmd) -> T`
|
||||
/// state = object_getIvar(self, load(__<Cls>_state_ivar))
|
||||
/// return state.<field>
|
||||
///
|
||||
/// Setter IMP (skipped if `readonly` in modifiers):
|
||||
/// `__<Cls>_set<Field>_imp(self, _cmd, val) -> void`
|
||||
/// state = object_getIvar(self, load(__<Cls>_state_ivar))
|
||||
/// state.<field> = val
|
||||
///
|
||||
/// 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.
|
||||
fn emitObjcDefinedClassPropertyImps(self: *Lowering, fcd: *const ast.ForeignClassDecl, field: ast.ForeignFieldDecl) void {
|
||||
const state_ty = self.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.
|
||||
const field_name_id = self.module.types.internString(field.name);
|
||||
var field_idx: ?u32 = null;
|
||||
for (state_info.@"struct".fields, 0..) |sf, i| {
|
||||
if (sf.name == field_name_id) {
|
||||
field_idx = @intCast(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
const fidx = field_idx orelse return;
|
||||
const field_ty = self.resolveType(field.field_type);
|
||||
|
||||
// (1) Getter: __<Cls>_<field>_imp
|
||||
self.emitObjcDefinedPropertyGetter(fcd, field, state_ty, fidx, field_ty);
|
||||
|
||||
// (2) Setter — skipped for `readonly`.
|
||||
var is_readonly = false;
|
||||
for (field.property_modifiers) |mod| {
|
||||
if (std.mem.eql(u8, mod, "readonly")) {
|
||||
is_readonly = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!is_readonly) {
|
||||
self.emitObjcDefinedPropertySetter(fcd, field, state_ty, fidx, field_ty);
|
||||
}
|
||||
|
||||
// (3) Register in the cache's methods slice. Both IMPs use the
|
||||
// method-registration pipeline that lands in class_addMethod
|
||||
// calls from emit_llvm.
|
||||
self.registerObjcDefinedPropertyMethodEntries(fcd, field, field_ty, is_readonly);
|
||||
}
|
||||
|
||||
fn emitObjcDefinedPropertyGetter(self: *Lowering, fcd: *const ast.ForeignClassDecl, field: ast.ForeignFieldDecl, state_ty: TypeId, fidx: u32, field_ty: TypeId) void {
|
||||
const saved_func = self.builder.func;
|
||||
const saved_block = self.builder.current_block;
|
||||
const saved_counter = self.builder.inst_counter;
|
||||
defer {
|
||||
self.builder.func = saved_func;
|
||||
self.builder.current_block = saved_block;
|
||||
self.builder.inst_counter = saved_counter;
|
||||
}
|
||||
|
||||
const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, field.name }) catch return;
|
||||
const name_id = self.module.types.internString(imp_name);
|
||||
const ptr_void = self.module.types.ptrTo(.void);
|
||||
|
||||
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
||||
params.append(self.alloc, .{ .name = self.module.types.internString("self"), .ty = ptr_void }) catch return;
|
||||
params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return;
|
||||
const params_slice = params.toOwnedSlice(self.alloc) catch return;
|
||||
|
||||
_ = self.builder.beginFunction(name_id, params_slice, field_ty);
|
||||
const func = self.builder.currentFunc();
|
||||
func.linkage = .external;
|
||||
func.call_conv = .c;
|
||||
func.has_implicit_ctx = false;
|
||||
|
||||
const entry_name = self.module.types.internString("entry");
|
||||
const entry = self.builder.appendBlock(entry_name, &.{});
|
||||
self.builder.switchToBlock(entry);
|
||||
|
||||
// state = object_getIvar(self, load @__<Cls>_state_ivar)
|
||||
const self_ref = Ref.fromIndex(0);
|
||||
const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return;
|
||||
defer self.alloc.free(ivar_global_name);
|
||||
const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse return;
|
||||
const ivar_addr = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void);
|
||||
const ivar_handle = self.builder.load(ivar_addr, ptr_void);
|
||||
const get_ivar_fid = self.ensureCRuntimeDecl("object_getIvar", &.{ ptr_void, ptr_void }, ptr_void);
|
||||
const get_args = self.alloc.alloc(Ref, 2) catch return;
|
||||
get_args[0] = self_ref;
|
||||
get_args[1] = ivar_handle;
|
||||
const state_ptr = self.builder.emit(.{ .call = .{ .callee = get_ivar_fid, .args = get_args } }, ptr_void);
|
||||
|
||||
// GEP to the field, load.
|
||||
const field_addr = self.builder.emit(.{ .struct_gep = .{ .base = state_ptr, .field_index = fidx, .base_type = state_ty } }, ptr_void);
|
||||
const val = self.builder.load(field_addr, field_ty);
|
||||
self.builder.ret(val, field_ty);
|
||||
self.builder.finalize();
|
||||
}
|
||||
|
||||
fn emitObjcDefinedPropertySetter(self: *Lowering, fcd: *const ast.ForeignClassDecl, field: ast.ForeignFieldDecl, state_ty: TypeId, fidx: u32, field_ty: TypeId) void {
|
||||
const saved_func = self.builder.func;
|
||||
const saved_block = self.builder.current_block;
|
||||
const saved_counter = self.builder.inst_counter;
|
||||
defer {
|
||||
self.builder.func = saved_func;
|
||||
self.builder.current_block = saved_block;
|
||||
self.builder.inst_counter = saved_counter;
|
||||
}
|
||||
|
||||
// Setter selector: set<Field>: → imp name: __<Cls>_set<Field>_imp
|
||||
var setter_field_buf = std.ArrayList(u8).empty;
|
||||
defer setter_field_buf.deinit(self.alloc);
|
||||
setter_field_buf.appendSlice(self.alloc, "set") catch unreachable;
|
||||
if (field.name.len > 0) {
|
||||
setter_field_buf.append(self.alloc, std.ascii.toUpper(field.name[0])) catch unreachable;
|
||||
setter_field_buf.appendSlice(self.alloc, field.name[1..]) catch unreachable;
|
||||
}
|
||||
const imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, setter_field_buf.items }) catch return;
|
||||
const name_id = self.module.types.internString(imp_name);
|
||||
const ptr_void = self.module.types.ptrTo(.void);
|
||||
|
||||
var params = std.ArrayList(inst_mod.Function.Param).empty;
|
||||
params.append(self.alloc, .{ .name = self.module.types.internString("self"), .ty = ptr_void }) catch return;
|
||||
params.append(self.alloc, .{ .name = self.module.types.internString("_cmd"), .ty = ptr_void }) catch return;
|
||||
params.append(self.alloc, .{ .name = self.module.types.internString("val"), .ty = field_ty }) catch return;
|
||||
const params_slice = params.toOwnedSlice(self.alloc) catch return;
|
||||
|
||||
_ = self.builder.beginFunction(name_id, params_slice, .void);
|
||||
const func = self.builder.currentFunc();
|
||||
func.linkage = .external;
|
||||
func.call_conv = .c;
|
||||
func.has_implicit_ctx = false;
|
||||
|
||||
const entry_name = self.module.types.internString("entry");
|
||||
const entry = self.builder.appendBlock(entry_name, &.{});
|
||||
self.builder.switchToBlock(entry);
|
||||
|
||||
const self_ref = Ref.fromIndex(0);
|
||||
const val_ref = Ref.fromIndex(2);
|
||||
const ivar_global_name = std.fmt.allocPrint(self.alloc, "__{s}_state_ivar", .{fcd.name}) catch return;
|
||||
defer self.alloc.free(ivar_global_name);
|
||||
const ivar_global_id = self.lookupGlobalIdByName(ivar_global_name) orelse return;
|
||||
const ivar_addr = self.builder.emit(.{ .global_addr = ivar_global_id }, ptr_void);
|
||||
const ivar_handle = self.builder.load(ivar_addr, ptr_void);
|
||||
const get_ivar_fid = self.ensureCRuntimeDecl("object_getIvar", &.{ ptr_void, ptr_void }, ptr_void);
|
||||
const get_args = self.alloc.alloc(Ref, 2) catch return;
|
||||
get_args[0] = self_ref;
|
||||
get_args[1] = ivar_handle;
|
||||
const state_ptr = self.builder.emit(.{ .call = .{ .callee = get_ivar_fid, .args = get_args } }, ptr_void);
|
||||
|
||||
const field_addr = self.builder.emit(.{ .struct_gep = .{ .base = state_ptr, .field_index = fidx, .base_type = state_ty } }, ptr_void);
|
||||
self.builder.store(field_addr, val_ref);
|
||||
self.builder.retVoid();
|
||||
self.builder.finalize();
|
||||
}
|
||||
|
||||
/// Append the property's getter (and setter, unless readonly)
|
||||
/// entries to the class's method-registration slice so emit_llvm
|
||||
/// calls class_addMethod on each. Selectors + encodings derived
|
||||
/// from the field type.
|
||||
fn registerObjcDefinedPropertyMethodEntries(self: *Lowering, fcd: *const ast.ForeignClassDecl, field: ast.ForeignFieldDecl, field_ty: TypeId, is_readonly: bool) void {
|
||||
const cur = self.module.lookupObjcDefinedClass(fcd.name) orelse return;
|
||||
_ = cur;
|
||||
// Find the existing entry and grow its methods slice.
|
||||
var new_methods = std.ArrayList(Module.ObjcDefinedMethodEntry).empty;
|
||||
for (self.module.objc_defined_class_cache.items) |entry| {
|
||||
if (!std.mem.eql(u8, entry.name, fcd.name)) continue;
|
||||
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_imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, field.name }) catch return;
|
||||
new_methods.append(self.alloc, .{
|
||||
.sel = field.name,
|
||||
.encoding = getter_enc,
|
||||
.imp_name = getter_imp_name,
|
||||
.is_class = false,
|
||||
}) catch unreachable;
|
||||
|
||||
// Setter entry — selector = set<Field>:, encoding = "v@:<ty>".
|
||||
if (!is_readonly) {
|
||||
var sel_buf = std.ArrayList(u8).empty;
|
||||
defer sel_buf.deinit(self.alloc);
|
||||
sel_buf.appendSlice(self.alloc, "set") catch unreachable;
|
||||
if (field.name.len > 0) {
|
||||
sel_buf.append(self.alloc, std.ascii.toUpper(field.name[0])) catch unreachable;
|
||||
sel_buf.appendSlice(self.alloc, field.name[1..]) catch unreachable;
|
||||
}
|
||||
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;
|
||||
|
||||
var setter_imp_field_buf = std.ArrayList(u8).empty;
|
||||
defer setter_imp_field_buf.deinit(self.alloc);
|
||||
setter_imp_field_buf.appendSlice(self.alloc, "set") catch unreachable;
|
||||
if (field.name.len > 0) {
|
||||
setter_imp_field_buf.append(self.alloc, std.ascii.toUpper(field.name[0])) catch unreachable;
|
||||
setter_imp_field_buf.appendSlice(self.alloc, field.name[1..]) catch unreachable;
|
||||
}
|
||||
const setter_imp_name = std.fmt.allocPrint(self.alloc, "__{s}_{s}_imp", .{ fcd.name, setter_imp_field_buf.items }) catch return;
|
||||
|
||||
new_methods.append(self.alloc, .{
|
||||
.sel = setter_sel,
|
||||
.encoding = setter_enc,
|
||||
.imp_name = setter_imp_name,
|
||||
.is_class = false,
|
||||
}) catch unreachable;
|
||||
}
|
||||
break;
|
||||
}
|
||||
const slice = new_methods.toOwnedSlice(self.alloc) catch return;
|
||||
self.module.setObjcDefinedClassMethods(fcd.name, slice);
|
||||
}
|
||||
|
||||
fn emitObjcDefinedClassImp(self: *Lowering, fcd: *const ast.ForeignClassDecl, md: ast.ForeignMethodDecl) void {
|
||||
// Class methods (no `*Self` first param) skip the ivar read —
|
||||
// they have no instance state to thread through.
|
||||
|
||||
1
tests/expected/152-objc-property-sx-defined.exit
Normal file
1
tests/expected/152-objc-property-sx-defined.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
1
tests/expected/152-objc-property-sx-defined.txt
Normal file
1
tests/expected/152-objc-property-sx-defined.txt
Normal file
@@ -0,0 +1 @@
|
||||
property: w=10 h=7 area=0
|
||||
Reference in New Issue
Block a user