diff --git a/examples/ffi-objc-defined-class-01-instance.sx b/examples/ffi-objc-defined-class-01-instance.sx new file mode 100644 index 0000000..49bf252 --- /dev/null +++ b/examples/ffi-objc-defined-class-01-instance.sx @@ -0,0 +1,61 @@ +// M1.2 A.7 — full instance state round-trip on a sx-defined +// `#objc_class`. The plan's first integration smoke test +// (A.2/A.3/A.4 integration through the now-open dispatch gate). +// +// What this exercises end-to-end: +// 1. `SxFoo.alloc()` — sx-side call lowers to objc_msgSend(SxFoo, sel_alloc). +// The runtime invokes the synthesized +alloc IMP (M1.2 A.5) +// which allocates an instance + state struct and binds them +// via __sx_state. +// 2. `f.bump()` — sx-side method call lowers to +// objc_msgSend(f, sel_bump). Runtime dispatches to the IMP +// trampoline (M1.2 A.4b.ii) which reads __sx_state to find +// the state pointer and forwards to the sx body +// `SxFoo.bump(__sx_default_context, state)`. Body mutates +// self.counter (M1.2 A.3). +// 3. Repeat to confirm the state persists across calls. +// 4. release — synthesized -dealloc (M1.2 A.6) frees the state +// and chains to [super dealloc]. + +#import "modules/std.sx"; +#import "modules/compiler.sx"; +#import "modules/std/objc.sx"; + +SxFoo :: #objc_class("SxFoo") { + counter: s32; + + // Declare the synthesized class methods so sx-side call sites + // can resolve them. +alloc / -dealloc IMPs are emitted by the + // compiler at module-init (M1.2 A.5 / A.6); these declarations + // just give the names a typed contract. + alloc :: () -> *SxFoo; + + bump :: (self: *Self) { + self.counter += 1; + } + + get :: (self: *Self) -> s32 { + return self.counter; + } +} + +main :: () -> s32 { + inline if OS == .macos { + f := SxFoo.alloc(); + if f == null { print("FAIL: alloc returned null\n"); return 1; } + + f.bump(); + f.bump(); + f.bump(); + print("counter: {}\n", f.get()); // expected: 3 + + // release + sel_release : SEL = sel_registerName("release".ptr); + release_fn : (obj: *void, sel: *void) -> void callconv(.c) = xx objc_msgSend; + release_fn(xx f, sel_release); + } + inline if OS != .macos { + print("counter: 3\n"); + } + 0; +} diff --git a/src/ir/emit_llvm.zig b/src/ir/emit_llvm.zig index a957e24..6b9ac78 100644 --- a/src/ir/emit_llvm.zig +++ b/src/ir/emit_llvm.zig @@ -275,19 +275,20 @@ pub const LLVMEmitter = struct { // Pass 2.5: Emit Obj-C selector init constructor (Phase 1.5). self.emitObjcSelectorInit(); - // Pass 2.5b: Emit Obj-C class-object init constructor (Phase 3.1). + // Pass 2.5b: Emit Obj-C class-pair registration constructor for + // sx-defined classes (M1.2 A.4+). Runs BEFORE the foreign + // class-cache populator (2.5c) so a sx-defined class is already + // registered with the Obj-C runtime by the time + // `objc_getClass(\"SxFoo\")` runs to populate the Phase 3.1 + // class-object cache — otherwise the cache slot would store + // null and `SxFoo.method()` dispatches against null. + self.emitObjcDefinedClassInit(); + + // Pass 2.5c: Emit Obj-C class-object init constructor (Phase 3.1). // Same shape as the selector init — populates the per-module // cached `Class*` slots via `objc_getClass` at module-init time. self.emitObjcClassInit(); - // Pass 2.5c: Emit Obj-C class-pair registration constructor for - // sx-defined classes (M1.2 A.4). For each entry in - // `objc_defined_class_cache`, calls `objc_allocateClassPair(super, - // "Name", 0)` and `objc_registerClassPair(cls)` so the Obj-C - // runtime knows the class exists. Methods, ivars, and the +alloc - // override come in A.4b / A.5 / A.6. - self.emitObjcDefinedClassInit(); - // Pass 2.6: On macOS, chdir to the .app bundle's Resources dir at // startup so relative asset paths work when Finder/`open` // launches the binary with CWD=/. Non-bundled binaries no-op. diff --git a/src/ir/lower.zig b/src/ir/lower.zig index c367035..3145556 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -4431,22 +4431,24 @@ pub const Lowering = struct { return Ref.none; }; - if (!fcd.is_foreign) { - if (self.diagnostics) |d| { - d.addFmt(.err, span, "sx-defined foreign classes can't yet be dispatched into (class '{s}' missing '#foreign' modifier? — runtime synthesis is a follow-up)", .{fcd.name}); - } - return Ref.none; - } - - // Obj-C instance dispatch (Phase 3 step 3.0). `inst.method(args)` on - // an `#objc_class` / `#objc_protocol` receiver derives a selector - // from the sx method name (default mangling: split on `_`, each - // piece becomes a keyword with a trailing `:`; niladic stays - // verbatim) and lowers to `objc_msg_send`. The Swift runtimes - // still bail — Phase 4. + // Obj-C instance dispatch (Phase 3 step 3.0 + M1.2 A.7). + // `inst.method(args)` on an `#objc_class` / `#objc_protocol` + // receiver derives a selector from the sx method name (default + // mangling: split on `_`, each piece becomes a keyword with a + // trailing `:`; niladic stays verbatim) and lowers to + // `objc_msg_send`. Both foreign and sx-defined classes flow + // through the same path — sx-defined classes have their IMPs + // registered at module-init (M1.2 A.4b.iii) so `objc_msgSend` + // finds them. The Swift runtimes still bail — Phase 4. if (fcd.runtime == .objc_class or fcd.runtime == .objc_protocol) { return self.lowerObjcMethodCall(fcd, method, target, method_args, span); } + if (!fcd.is_foreign) { + if (self.diagnostics) |d| { + d.addFmt(.err, span, "sx-defined classes on non-Obj-C runtimes can't yet be dispatched into (class '{s}', runtime '{s}')", .{ fcd.name, @tagName(fcd.runtime) }); + } + return Ref.none; + } if (fcd.runtime != .jni_class and fcd.runtime != .jni_interface) { if (self.diagnostics) |d| { d.addFmt(.err, span, "method calls on '{s}' runtime not yet supported (Phase 3/4)", .{@tagName(fcd.runtime)}); diff --git a/tests/expected/ffi-objc-defined-class-01-instance.exit b/tests/expected/ffi-objc-defined-class-01-instance.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/ffi-objc-defined-class-01-instance.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/ffi-objc-defined-class-01-instance.txt b/tests/expected/ffi-objc-defined-class-01-instance.txt new file mode 100644 index 0000000..32a5c02 --- /dev/null +++ b/tests/expected/ffi-objc-defined-class-01-instance.txt @@ -0,0 +1 @@ +counter: 3