diff --git a/examples/152-objc-property-sx-defined.sx b/examples/152-objc-property-sx-defined.sx new file mode 100644 index 0000000..7fd0e21 --- /dev/null +++ b/examples/152-objc-property-sx-defined.sx @@ -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: +// ____imp(self, _cmd) -> T // load state.field +// ___set_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; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 50fbddb..62d8818 100644 --- a/src/ir/lower.zig +++ b/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: `____imp(self, _cmd) -> T` + /// state = object_getIvar(self, load(___state_ivar)) + /// return state. + /// + /// Setter IMP (skipped if `readonly` in modifiers): + /// `___set_imp(self, _cmd, val) -> void` + /// state = object_getIvar(self, load(___state_ivar)) + /// state. = 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: ____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 @___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: → imp name: ___set_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 = "@:". + 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:, encoding = "v@:". + 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. diff --git a/tests/expected/152-objc-property-sx-defined.exit b/tests/expected/152-objc-property-sx-defined.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/152-objc-property-sx-defined.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/152-objc-property-sx-defined.txt b/tests/expected/152-objc-property-sx-defined.txt new file mode 100644 index 0000000..dd9c4aa --- /dev/null +++ b/tests/expected/152-objc-property-sx-defined.txt @@ -0,0 +1 @@ +property: w=10 h=7 area=0