From 0ac5ba2ccd6c7f4a06eaf0b58fc3d31d5c7755ca Mon Sep 17 00:00:00 2001 From: agra Date: Mon, 25 May 2026 23:33:52 +0300 Subject: [PATCH] ffi M1.3: obj.class accessor on Obj-C-class pointers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a special case to lowerFieldAccess: when the field is literally 'class' and the receiver is a pointer to an Obj-C (or Obj-C protocol) foreign-class struct, emit 'object_getClass(obj)' instead of falling through to struct GEP. Returns 'Class' (the M1.1 first-pass alias for *void; parameterized Class(T) covariance is deferred to M1.1.b). f := SxFoo.alloc(); cls := f.class; // → object_getClass(f) cls == objc_getClass("SxFoo".ptr); // ok New helper isObjcClassPointer(ty) detects 'ptr -> struct in foreign_class_map under .objc_class / .objc_protocol'. The check fires BEFORE the auto-deref so the runtime call sees the opaque Obj-C pointer rather than the load'd struct stub. 148-objc-self-class-accessor.sx exercises both shapes end-to-end against the macOS runtime: sx-defined class (SxFoo) and foreign class (NSObject). Round-trips against objc_getClass(name). 178 example tests pass. zig build test green. This effectively closes Month 1 — M1.0, M1.1 (first pass), M1.2, M1.3 all done. Remaining: M1.1.b (Class(T) covariance + instancetype), then Month 2 (declarative sugar). --- examples/148-objc-self-class-accessor.sx | 48 +++++++++++++++++++ src/ir/lower.zig | 31 ++++++++++++ .../148-objc-self-class-accessor.exit | 1 + .../expected/148-objc-self-class-accessor.txt | 1 + 4 files changed, 81 insertions(+) create mode 100644 examples/148-objc-self-class-accessor.sx create mode 100644 tests/expected/148-objc-self-class-accessor.exit create mode 100644 tests/expected/148-objc-self-class-accessor.txt diff --git a/examples/148-objc-self-class-accessor.sx b/examples/148-objc-self-class-accessor.sx new file mode 100644 index 0000000..c883b6c --- /dev/null +++ b/examples/148-objc-self-class-accessor.sx @@ -0,0 +1,48 @@ +// M1.3 — `obj.class` accessor on Obj-C pointers. +// +// Any Obj-C-class pointer (foreign or sx-defined) can be probed +// for its runtime class object via `obj.class`. Lowers to +// `object_getClass(obj)`. Returns `Class` (alias for *void — +// parameterized `Class(T)` covariance is M1.1.b). +// +// Verifies both shapes: +// 1. (*SxFoo).class — sx-defined class. Returns the SxFoo Class. +// 2. (*NSObject).class — foreign class via stdlib. Returns NSObject's +// Class. + +#import "modules/std.sx"; +#import "modules/compiler.sx"; +#import "modules/std/objc.sx"; + +NSObjectFwd :: #foreign #objc_class("NSObject") { + alloc :: () -> *NSObjectFwd; + init :: (self: *NSObjectFwd) -> *NSObjectFwd; +} + +SxFoo :: #objc_class("SxFoo") { + counter: s32; + alloc :: () -> *SxFoo; + bump :: (self: *Self) { self.counter += 1; } +} + +main :: () -> s32 { + inline if OS == .macos { + // sx-defined class round-trip. + f := SxFoo.alloc(); + cls_f : Class = f.class; + expected_f : Class = objc_getClass("SxFoo".ptr); + if cls_f != expected_f { print("FAIL: SxFoo.class mismatch\n"); return 1; } + + // foreign class round-trip. + nso := NSObjectFwd.alloc().init(); + cls_n : Class = nso.class; + expected_n : Class = objc_getClass("NSObject".ptr); + if cls_n != expected_n { print("FAIL: NSObject.class mismatch\n"); return 1; } + + print("class accessor: ok\n"); + } + inline if OS != .macos { + print("class accessor: ok\n"); + } + 0; +} diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 3145556..b57e067 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -3576,6 +3576,22 @@ pub const Lowering = struct { } } + // M1.3 — `obj.class` on any Obj-C-class pointer lowers to + // `object_getClass(obj)`. Sugar; the receiver is opaque so + // we don't auto-deref. Returns `Class` (alias for *void; + // typed Class(T) parameterization is M1.1.b). + if (std.mem.eql(u8, fa.field, "class")) { + const expr_ty = self.inferExprType(fa.object); + if (self.isObjcClassPointer(expr_ty)) { + const obj_ref = self.lowerExpr(fa.object); + const ptr_void = self.module.types.ptrTo(.void); + const get_class_fid = self.ensureCRuntimeDecl("object_getClass", &.{ptr_void}, ptr_void); + const args = self.alloc.alloc(Ref, 1) catch unreachable; + args[0] = obj_ref; + return self.builder.emit(.{ .call = .{ .callee = get_class_fid, .args = args } }, ptr_void); + } + } + var obj = self.lowerExpr(fa.object); var obj_ty = self.inferExprType(fa.object); @@ -11530,6 +11546,21 @@ pub const Lowering = struct { self.emitObjcDefinedClassImps(); } + /// True if `ty` is a pointer to a struct whose name is registered + /// in `foreign_class_map` under an Obj-C runtime. Used by the + /// `obj.class` accessor (M1.3) to decide whether to lower the + /// field access as a struct GEP or as `object_getClass(obj)`. + 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.foreign_class_map.get(struct_name) orelse return false; + return fcd.runtime == .objc_class or fcd.runtime == .objc_protocol; + } + /// Get a FuncId for an external C-callconv function. If a function /// with this exported name already exists in the module (e.g. /// declared by stdlib `#foreign` decl), return it; otherwise diff --git a/tests/expected/148-objc-self-class-accessor.exit b/tests/expected/148-objc-self-class-accessor.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/148-objc-self-class-accessor.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/148-objc-self-class-accessor.txt b/tests/expected/148-objc-self-class-accessor.txt new file mode 100644 index 0000000..aca0e74 --- /dev/null +++ b/tests/expected/148-objc-self-class-accessor.txt @@ -0,0 +1 @@ +class accessor: ok