test: group examples into per-category folders

Move examples/*.sx and their expected/ snapshots into per-category
subfolders (examples/<category>/...). Folder = leading filename token,
with ffi-objc/ffi-jni kept whole; filenames are unchanged. The corpus
runner and LSP sweep now discover each category's expected/ dir, while
issues/ stays flat. Example 1058's repo-root-relative companion import
is made file-relative. Path strings embedded in 164 snapshots were
regenerated (path-only changes). Test-layout docs in CLAUDE.md updated.
This commit is contained in:
agra
2026-06-21 14:41:34 +03:00
parent 6d1409bc1f
commit 66bdc70bf1
3357 changed files with 456 additions and 363 deletions

View File

@@ -0,0 +1,28 @@
// Obj-C runtime FFI smoke test: round-trip a string through NSString.
//
// Demonstrates the typed-fn-pointer cast idiom for `objc_msgSend`. Each
// shape we invoke gets its own variable typed with the exact ABI:
//
// msg_3 : (*void, *void, [*]u8) -> *void = xx objc_msgSend;
// msg_2 : (*void, *void) -> [*]u8 = xx objc_msgSend;
//
// On ARM64 Apple, objc_msgSend doesn't take a varargs path — invoking it
// through a typed fn-pointer is the only correct way to land args in the
// right registers.
#import "modules/std.sx";
#import "modules/ffi/objc.sx";
main :: () -> i32 {
ns_class := objc_getClass("NSString".ptr);
sel_with_utf8 := sel_registerName("stringWithUTF8String:".ptr);
sel_utf8 := sel_registerName("UTF8String".ptr);
msg_3 : (*void, *void, [*]u8) -> *void abi(.c) = xx objc_msgSend;
ns_str := msg_3(ns_class, sel_with_utf8, "hi".ptr);
msg_2 : (*void, *void) -> [*]u8 abi(.c) = xx objc_msgSend;
back := msg_2(ns_str, sel_utf8);
return xx (back[0] + back[1]); // 'h' + 'i' = 104 + 105 = 209
}

View File

@@ -0,0 +1,41 @@
// Register a brand-new Obj-C class from sx and prove a sx-defined method
// actually runs when dispatched through `objc_msgSend`.
//
// The flow:
// 1. `objc_allocateClassPair(NSObject, "SxThing", 0)` returns an unregistered Class.
// 2. `class_addMethod(cls, sel_hello, IMP, "v@:")` attaches our sx function as
// the `hello` method (type encoding "v@:" = void method(id self, SEL _cmd)).
// 3. `objc_registerClassPair(cls)` finalizes it.
// 4. `class_createInstance(cls, 0)` returns an `id` for a fresh instance.
// 5. typed-cast `objc_msgSend` for `void (id, SEL)` and dispatch `hello`.
// If the IMP ran, the global `g_marker` is non-zero and we return it as exit.
#import "modules/std.sx";
#import "modules/ffi/objc.sx";
g_marker : i32 = 0;
// IMP for `hello`. Must use C calling convention so `self` and `_cmd` land in
// x0 and x1 the way the Obj-C runtime expects.
hello_imp :: (self: *void, _cmd: *void) abi(.c) {
g_marker = 42;
}
main :: () -> i32 {
NSObject := objc_getClass("NSObject".ptr);
SxThing := objc_allocateClassPair(NSObject, "SxThing".ptr, 0);
sel_hello := sel_registerName("hello".ptr);
ok := class_addMethod(SxThing, sel_hello, xx hello_imp, "v@:".ptr);
if !ok { return 1; }
objc_registerClassPair(SxThing);
obj := class_createInstance(SxThing, 0);
if obj == xx 0 { return 2; }
// [obj hello]
msg : (*void, *void) -> void abi(.c) = xx objc_msgSend;
msg(obj, sel_hello);
return g_marker; // 42 if hello_imp ran
}

View File

@@ -0,0 +1,17 @@
// `xx <closure> : Block` builds an Apple-ABI block whose invoke
// trampoline delegates to the sx closure. Verifies end-to-end:
// stdlib Block layout, _NSConcreteStackBlock extern, per-signature
// invoke trampoline, Into(Block) for Closure() -> void. Runs on
// macOS — invokes the block's invoke fn directly via a typed fn
// pointer instead of going through the Obj-C runtime.
#import "modules/std.sx";
#import "modules/ffi/objc_block.sx";
main :: () -> i32 {
cl := () => { print("noop block ran\n"); };
b : Block = xx cl;
invoke_fn : (*Block) -> void abi(.c) = xx b.invoke;
invoke_fn(@b);
0
}

View File

@@ -0,0 +1,17 @@
// A capturing closure rides through `xx ... : Block` and the
// captured state survives across the call. The block's sx_env field
// holds the closure's env pointer; the invoke trampoline restores it
// before delegating.
#import "modules/std.sx";
#import "modules/ffi/objc_block.sx";
main :: () -> i32 {
x : i64 = 42;
y : i64 = 100;
cl := () => { print("x + y = {}\n", x + y); };
b : Block = xx cl;
invoke_fn : (*Block) -> void abi(.c) = xx b.invoke;
invoke_fn(@b);
0
}

View File

@@ -0,0 +1,61 @@
// `xx closure : Block` for an arbitrary closure signature.
//
// The stdlib (modules/ffi/objc_block.sx) declares hand-rolled
// `Into(Block) for Closure() -> void` and `Closure(bool) -> void`
// impls — the two most common Apple block shapes. Other signatures
// need a per-shape `__block_invoke_<sig>` trampoline + `Into(Block)`
// impl declared somewhere reachable (stdlib if shared, in-file if
// app-specific).
//
// This test exercises the user-declared variant: signature
// `Closure(i32, *void) -> void` (a two-arg block — not in stdlib).
// If the impl is missing, the compiler emits a focused diagnostic
// pointing at modules/ffi/objc_block.sx as the template.
#import "modules/std.sx";
#import "modules/ffi/objc_block.sx";
// Trampoline matching `void (^)(int, void*)` — the C ABI Apple's
// runtime calls. Forwards through to the sx closure with the
// standard `(__sx_ctx, env, ...args)` shape.
__block_invoke_void_i32_p :: (block_self: *Block, arg0: i32, arg1: *void) abi(.c) {
typed_fn : (*void, i32, *void) -> void = xx block_self.sx_fn;
typed_fn(block_self.sx_env, arg0, arg1);
}
impl Into(Block) for Closure(i32, *void) -> void {
convert :: (self: Closure(i32, *void) -> void) -> Block {
.{
isa = @_NSConcreteStackBlock,
flags = 0,
reserved = 0,
invoke = xx @__block_invoke_void_i32_p,
descriptor = xx @__sx_block_descriptor,
sx_env = self.env,
sx_fn = self.fn_ptr,
}
}
}
// Side-effect capture so we can observe both args reached the
// closure body.
g_sum: i32 = 0;
g_tag: *void = null;
main :: () -> i32 {
cl := (n: i32, tag: *void) => {
g_sum = n + 1;
g_tag = tag;
};
b : Block = xx cl;
invoke_fn : (*Block, i32, *void) -> void abi(.c) = xx b.invoke;
sentinel: i32 = 42;
invoke_fn(@b, 41, xx @sentinel);
if g_sum != 42 { print("FAIL: g_sum expected 42, got {}\n", g_sum); return 1; }
if g_tag == null { print("FAIL: g_tag null\n"); return 1; }
print("block multi-arg ok: sum={}\n", g_sum);
0
}

View File

@@ -0,0 +1,19 @@
// `xx <closure>` passed as a `*Block` fn argument auto-allocates the
// Block instance and passes its address — no named temp required.
// Matches the ergonomics of ObjC's `^{...}` literal at the call site.
#import "modules/std.sx";
#import "modules/ffi/objc_block.sx";
invoke_once :: (b: *Block) {
invoke_fn : (*Block) -> void abi(.c) = xx b.invoke;
invoke_fn(b);
}
main :: () -> i32 {
x : i64 = 7;
invoke_once(xx () => {
print("inline block, x = {}\n", x);
});
0
}

View File

@@ -0,0 +1,41 @@
// Chained runtime-class method dispatch: `Cls.static().instance(...)`
// resolves the inner call's return type so the outer dispatch's
// receiver type is known. Pre-fix this collapsed to i64 in
// `inferExprType`, the runtime_class_map lookup missed, and lowering
// emitted `error: unresolved 'init'` (or 'initWithWindowScene' etc.)
// — see issues/0043 for the chess uikit.sx C4 migration that hit it.
//
// Two return-type shapes covered: explicit `*ClassName` (alloc here)
// and `*Self` (init). Both must propagate through the chain so the
// next `.method(...)` finds the runtime-class declaration.
#import "modules/std.sx";
#import "modules/build.sx";
NSObject :: #objc_class("NSObject") extern {
alloc :: () -> *NSObject;
init :: (self: *Self) -> *Self;
}
NSObjectSelfReturn :: #objc_class("NSObject") extern {
alloc :: () -> *Self;
init :: (self: *Self) -> *Self;
}
main :: () -> i32 {
inline if OS == .macos {
a := NSObject.alloc().init();
if a != null {
print("explicit-then-self ok\n");
}
b := NSObjectSelfReturn.alloc().init();
if b != null {
print("self-then-self ok\n");
}
}
inline if OS != .macos {
print("explicit-then-self ok\n");
print("self-then-self ok\n");
}
0
}

View File

@@ -0,0 +1,20 @@
// M1.0 (xfail) — '=>' expression-body form inside '#objc_class'
// member methods.
//
// Today: parseRuntimeClassDecl ([src/parser.zig:1262]) accepts ';'
// (declaration) or '{ ... }' (block body) but not '=>'. Trying
// '=>' surfaces 'expected ;' at the arrow.
//
// Next commit extends the member parser to accept the arrow
// form, mirroring the existing parseFnDecl ('=>') arm, and this
// snapshot flips from a parser error to '42\n'.
#import "modules/std.sx";
SxFoo :: #objc_class("SxFoo") {
greet :: (self: *Self) -> i32 => 42;
}
main :: () -> i32 {
0
}

View File

@@ -0,0 +1,47 @@
// M1.1 — Obj-C primitive type aliases.
//
// `id`, `Class`, `SEL`, `BOOL` from `modules/ffi/objc.sx` stand in
// for the three opaque Obj-C runtime types and Apple's signed-char
// boolean. They resolve to `*void` / `i8` at the LLVM layer — no
// runtime cost — but make runtime-class and call-site declarations
// read closer to Objective-C source.
//
// `Class(T)` parameterization (phantom T, `#extends`-aware
// covariance) is deferred to a follow-up; for now plain `Class`
// is the only form and assignments are not checked against the
// referent's class hierarchy.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
// Runtime-class declaration using the aliases at param/return positions.
NSObjectAlias :: #objc_class("NSObject") extern {
alloc :: () -> *Self;
init :: (self: *Self) -> *Self;
isKindOfClass :: (self: *Self, cls: Class) -> BOOL;
}
main :: () -> i32 {
inline if OS == .macos {
// id - any Obj-C instance pointer.
nsobj : id = NSObjectAlias.alloc().init();
// Class - the runtime class object.
ns_cls : Class = objc_getClass("NSObject".ptr);
// SEL - registered selector.
sel : SEL = sel_registerName("alloc".ptr);
_ = sel;
// BOOL - Apple's signed-char boolean. Cast the *Self into
// a *NSObjectAlias for the method call.
obj : *NSObjectAlias = xx nsobj;
flag : BOOL = obj.isKindOfClass(ns_cls);
print("isKindOfClass: {}\n", flag); // 1 (true)
}
inline if OS != .macos {
print("isKindOfClass: 1\n"); // skip — runtime not present
}
0
}

View File

@@ -0,0 +1,42 @@
// M1.2 A.2c / A.3 — instance-method body lowering on sx-defined
// `#objc_class`.
//
// The Obj-C runtime invokes class methods via the IMP pointers
// wired up in M1.2 A.4. No sx-side call path drives lazy lowering,
// so the eager pass `lowerObjcDefinedClassMethods` walks the
// `objc_defined_class_cache` and force-lowers every bodied method
// after Pass 1 finishes.
//
// `*Self` substitution (A.2b) routes the param's pointee type to
// the hidden state-struct `__<ClassName>State`. `self.counter`
// then resolves as a plain struct field access — A.3's "free if
// types align". The IR snapshot pins the round-trip:
//
// define internal void @SxFoo.bump(ptr __sx_ctx, ptr self) {
// %gep = getelementptr inbounds { i32 }, ptr %self, 0, 0
// %v = load i32, ptr %gep
// %inc = add i32 %v, 1
// store i32 %inc, ptr %gep
// ret void
// }
//
// IMP-trampoline emission (the C-ABI shim that the Obj-C runtime
// calls and that reads the `__sx_state` ivar) lands separately
// in M1.2 A.4 alongside class-pair init. Runtime dispatch
// (M1.2 A.7) stays gated until then.
#import "modules/std.sx";
#import "modules/build.sx";
SxFoo :: #objc_class("SxFoo") {
counter: i32;
bump :: (self: *Self) {
self.counter += 1;
}
}
main :: () -> i32 {
print("compiled\n");
0
}

View File

@@ -0,0 +1,43 @@
// M1.2 A.4 — class-pair registration with the Obj-C runtime.
//
// Every sx-defined '#objc_class' produces a module-init constructor
// (registered in '@llvm.global_ctors' AND injected at the top of
// 'main' for the ORC JIT path) that calls:
//
// super = objc_getClass("NSObject")
// cls = objc_allocateClassPair(super, "SxFoo", 0)
// objc_registerClassPair(cls)
//
// After the constructor runs, 'objc_getClass("SxFoo")' returns the
// freshly registered class — the round-trip we verify below.
//
// Methods, the '__sx_state' ivar, and the '+alloc' / '-dealloc'
// overrides land in A.4b / A.5 / A.6; this slice just makes the
// class EXIST in the runtime.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
SxFoo :: #objc_class("SxFoo") {
counter: i32;
bump :: (self: *Self) {
self.counter += 1;
}
}
main :: () -> i32 {
inline if OS == .macos {
cls : Class = objc_getClass("SxFoo".ptr);
if cls == null {
print("FAIL: SxFoo not registered\n");
return 1;
}
print("registered: SxFoo\n");
}
inline if OS != .macos {
print("registered: SxFoo\n");
}
0
}

View File

@@ -0,0 +1,51 @@
// M1.2 A.4b.i — '__sx_state' ivar registration on sx-defined
// '#objc_class'.
//
// Class-pair init now:
// 1. allocs the class pair
// 2. registers a single '__sx_state : *void' ivar
// 3. finalises the class
// 4. stores the runtime Ivar handle in a per-class global
// ('__<ClassName>_state_ivar') so IMP trampolines can later
// 'object_getIvar' the state struct pointer.
//
// Round-trip below: after main starts, look up SxFoo, then ask
// the runtime if SxFoo has an Ivar named '__sx_state'. Returns
// non-null iff registration succeeded.
//
// IMP trampolines (A.4b.ii) and the '+alloc' / '-dealloc'
// overrides (A.5 / A.6) come next.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
class_getInstanceVariable :: (cls: *void, name: [*]u8) -> *void extern objc;
SxFoo :: #objc_class("SxFoo") {
counter: i32;
bump :: (self: *Self) {
self.counter += 1;
}
}
main :: () -> i32 {
inline if OS == .macos {
cls : Class = objc_getClass("SxFoo".ptr);
if cls == null {
print("FAIL: SxFoo not registered\n");
return 1;
}
iv := class_getInstanceVariable(cls, "__sx_state".ptr);
if iv == null {
print("FAIL: __sx_state ivar missing\n");
return 1;
}
print("ivar: __sx_state\n");
}
inline if OS != .macos {
print("ivar: __sx_state\n");
}
0
}

View File

@@ -0,0 +1,51 @@
// M1.2 A.4b.iii — instance-method dispatch through the Obj-C
// runtime. Each instance method now gets a C-ABI IMP trampoline
// registered via 'class_addMethod' at class-pair init time. The
// runtime can dispatch 'objc_msgSend(obj, sel)' to the
// trampoline, which reads the '__sx_state' ivar to find the
// state struct and forwards to the sx body.
//
// End-to-end (verifies registration only — sx-side
// 'obj.bump()' calls still bail at the M1.2 A.7 dispatch gate
// until +alloc/-dealloc (A.5/A.6) land too):
// 1. class_getMethodImplementation(SxFoo, sel_registerName("bump"))
// returns a non-null IMP — proves the trampoline is wired.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
class_getMethodImplementation :: (cls: *void, sel: *void) -> *void extern objc;
SxFoo :: #objc_class("SxFoo") {
counter: i32;
bump :: (self: *Self) {
self.counter += 1;
}
add :: (self: *Self, n: i32) {
self.counter += n;
}
}
main :: () -> i32 {
inline if OS == .macos {
cls : Class = objc_getClass("SxFoo".ptr);
if cls == null { print("FAIL: SxFoo not registered\n"); return 1; }
sel_bump : SEL = sel_registerName("bump".ptr);
imp_bump : *void = class_getMethodImplementation(cls, sel_bump);
if imp_bump == null { print("FAIL: bump IMP missing\n"); return 1; }
sel_add : SEL = sel_registerName("add:".ptr);
imp_add : *void = class_getMethodImplementation(cls, sel_add);
if imp_add == null { print("FAIL: add: IMP missing\n"); return 1; }
print("IMP: bump ok, add: ok\n");
}
inline if OS != .macos {
print("IMP: bump ok, add: ok\n");
}
0
}

View File

@@ -0,0 +1,54 @@
// M1.2 A.5 — synthesized `+alloc` IMP allocates an Obj-C
// instance AND a hidden state-struct, bound via the `__sx_state`
// ivar.
//
// Round-trip below:
// 1. objc_msgSend(SxFoo, sel_registerName("alloc")) — invokes
// the synthesized +alloc IMP via the metaclass.
// 2. Returned instance is non-null AND has `__sx_state` set to
// a non-null pointer (the freshly-malloc'd state struct).
// 3. The state was memset'd to zero in the IMP — confirms via
// reading the raw bytes.
//
// Once A.6 lands (-dealloc) and A.7 opens the dispatch gate,
// sx-side `SxFoo.alloc().init()` and method calls will exercise
// the full lifecycle.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
class_getInstanceVariable :: (cls: *void, name: [*]u8) -> *void extern objc;
SxFoo :: #objc_class("SxFoo") {
counter: i32;
bump :: (self: *Self) {
self.counter += 1;
}
}
main :: () -> i32 {
inline if OS == .macos {
cls : Class = objc_getClass("SxFoo".ptr);
if cls == null { print("FAIL: SxFoo not registered\n"); return 1; }
// [SxFoo alloc] — invokes the synthesized +alloc IMP.
sel_alloc : SEL = sel_registerName("alloc".ptr);
msg_fn : (cls: *void, sel: *void) -> *void abi(.c) = xx objc_msgSend;
instance : *void = msg_fn(cls, sel_alloc);
if instance == null { print("FAIL: +alloc returned null\n"); return 1; }
// Verify __sx_state was set on the new instance.
ivar := class_getInstanceVariable(cls, "__sx_state".ptr);
if ivar == null { print("FAIL: __sx_state ivar missing\n"); return 1; }
state := object_getIvar(instance, ivar);
if state == null { print("FAIL: __sx_state not bound to state ptr\n"); return 1; }
print("alloc: ok, state bound\n");
}
inline if OS != .macos {
print("alloc: ok, state bound\n");
}
0
}

View File

@@ -0,0 +1,61 @@
// M1.2 A.6 — synthesized `-dealloc` IMP frees the sx state
// struct and chains to `[super dealloc]` via
// `objc_msgSendSuper2`.
//
// Round-trip:
// 1. [SxFoo alloc] returns a fresh instance with state bound.
// 2. release the instance — runtime invokes our -dealloc IMP.
// 3. Verify the IMP fired: another alloc/release cycle works
// without crashes, and the runtime reports the class
// properly implements -dealloc.
//
// Full instance-state round-trips (sx-side `f := SxFoo.alloc();
// f.bump();`) await A.7's dispatch-gate opening.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
class_getInstanceVariable :: (cls: *void, name: [*]u8) -> *void extern objc;
class_getMethodImplementation :: (cls: *void, sel: *void) -> *void extern objc;
SxFoo :: #objc_class("SxFoo") {
counter: i32;
bump :: (self: *Self) {
self.counter += 1;
}
}
main :: () -> i32 {
inline if OS == .macos {
cls : Class = objc_getClass("SxFoo".ptr);
if cls == null { print("FAIL: SxFoo not registered\n"); return 1; }
// Confirm the runtime sees our -dealloc IMP.
sel_dealloc : SEL = sel_registerName("dealloc".ptr);
imp_dealloc : *void = class_getMethodImplementation(cls, sel_dealloc);
if imp_dealloc == null { print("FAIL: dealloc IMP missing\n"); return 1; }
// alloc + release — synthesized -dealloc IMP fires inside.
sel_alloc : SEL = sel_registerName("alloc".ptr);
alloc_fn : (cls: *void, sel: *void) -> *void abi(.c) = xx objc_msgSend;
instance : *void = alloc_fn(cls, sel_alloc);
if instance == null { print("FAIL: +alloc returned null\n"); return 1; }
sel_release : SEL = sel_registerName("release".ptr);
release_fn : (obj: *void, sel: *void) -> void abi(.c) = xx objc_msgSend;
release_fn(instance, sel_release);
// Run another cycle to confirm dealloc didn't corrupt runtime state.
instance2 : *void = alloc_fn(cls, sel_alloc);
if instance2 == null { print("FAIL: +alloc round 2 returned null\n"); return 1; }
release_fn(instance2, sel_release);
print("dealloc: ok\n");
}
inline if OS != .macos {
print("dealloc: ok\n");
}
0
}

View File

@@ -0,0 +1,48 @@
// M1.3 — `obj.class` accessor on Obj-C pointers.
//
// Any Obj-C-class pointer (runtime 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 — runtime class via stdlib. Returns NSObject's
// Class.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
NSObjectFwd :: #objc_class("NSObject") extern {
alloc :: () -> *NSObjectFwd;
init :: (self: *NSObjectFwd) -> *NSObjectFwd;
}
SxFoo :: #objc_class("SxFoo") {
counter: i32;
alloc :: () -> *SxFoo;
bump :: (self: *Self) { self.counter += 1; }
}
main :: () -> i32 {
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; }
// runtime 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
}

View File

@@ -0,0 +1,48 @@
// M2.1(b) — class methods (no `*Self` first param) on a
// sx-defined `#objc_class`.
//
// The user declares a method without `self: *Self`. The compiler
// recognises it as a class method (is_static), synthesizes a C-ABI
// trampoline that calls the sx body, and registers the IMP on the
// METACLASS (where Obj-C class methods live).
//
// Verifies the runtime side:
// 1. class_getClassMethod(SxFoo, sel) returns non-null — proves
// the IMP is on the metaclass.
// 2. objc_msgSend(SxFoo, sel) invokes the IMP and returns the
// sx body's result.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
class_getClassMethod :: (cls: *void, sel: *void) -> *void extern objc;
SxFoo :: #objc_class("SxFoo") {
counter: i32;
// Class method — no `self`. Returns 42.
answer :: () -> i32 { return 42; }
}
main :: () -> i32 {
inline if OS == .macos {
cls : Class = objc_getClass("SxFoo".ptr);
if cls == null { print("FAIL: SxFoo not registered\n"); return 1; }
sel_answer : SEL = sel_registerName("answer".ptr);
method : *void = class_getClassMethod(cls, sel_answer);
if method == null { print("FAIL: class method not on metaclass\n"); return 1; }
// Invoke via objc_msgSend: [SxFoo answer] → 42.
msg_fn : (cls: *void, sel: *void) -> i32 abi(.c) = xx objc_msgSend;
result : i32 = msg_fn(cls, sel_answer);
if result != 42 { print("FAIL: expected 42, got {}\n", result); return 1; }
print("class method: {}\n", result);
}
inline if OS != .macos {
print("class method: 42\n");
}
0
}

View File

@@ -0,0 +1,57 @@
// M2.1(a) — class-level constants on a sx-defined `#objc_class`.
//
// `name :: Type = expr;` inside the class block is sugar for
// `name :: () -> Type => expr;` — a niladic class method with an
// expression body. The compiler emits a C-ABI IMP that returns the
// captured expression and registers it on the metaclass.
//
// Apple's runtime sees no distinction — '[Cls foo]' dispatches to
// our IMP whether the user wrote it as a constant or as a method.
// The constant form just reads better for static metadata returns
// (canonical example: '+layerClass' on UIView subclasses).
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
NSObject :: #objc_class("NSObject") extern {
alloc :: () -> *NSObject;
init :: (self: *NSObject) -> *NSObject;
}
// Reframed as a class method internally; user writes the constant form.
SxThing :: #objc_class("SxThing") {
counter: i32;
// Class-level constant.
answer :: i32 = 42;
// Canonical pattern: returning a *NSObject (stand-in for Apple's
// '+layerClass' returning *CALayer).
seedClass :: *NSObject = NSObject.alloc().init();
}
main :: () -> i32 {
inline if OS == .macos {
cls : Class = objc_getClass("SxThing".ptr);
if cls == null { print("FAIL: SxThing not registered\n"); return 1; }
// [SxThing answer] → 42
sel_answer : SEL = sel_registerName("answer".ptr);
msg_int : (cls: *void, sel: *void) -> i32 abi(.c) = xx objc_msgSend;
r := msg_int(cls, sel_answer);
if r != 42 { print("FAIL: answer expected 42, got {}\n", r); return 1; }
// [SxThing seedClass] returns a non-null NSObject.
sel_seed : SEL = sel_registerName("seedClass".ptr);
msg_ptr : (cls: *void, sel: *void) -> *void abi(.c) = xx objc_msgSend;
seed := msg_ptr(cls, sel_seed);
if seed == null { print("FAIL: seedClass returned null\n"); return 1; }
print("class constants: answer={}, seedClass=ok\n", r);
}
inline if OS != .macos {
print("class constants: answer=42, seedClass=ok\n");
}
0
}

View File

@@ -0,0 +1,67 @@
// M2.2 (first pass) — `#property` directive on runtime-class
// fields synthesizes Obj-C-runtime getter/setter dispatch.
//
// field: T #property[(modifiers)];
//
// `obj.field` → [obj field] (selector = field name)
// `obj.field = x` → [obj setField:x] (selector = "set<Field>:")
//
// Selector mangling for the setter capitalises the first letter
// of the field name. Modifiers (strong, weak, copy, readonly, ...)
// parse but don't yet drive ARC ops — that's Month 4.
//
// This slice covers RUNTIME-class properties. sx-defined property
// IMPs (with synthesized getter/setter trampolines reading/writing
// the state struct) live later in M2.2.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
// Build a probe class on the fly: registers two IMPs (`tag` and
// `setTag:`) that read/write an instance-bound i32 stored in a
// runtime ivar. Property dispatch should round-trip through them.
g_probe_tag: i32 = 0;
probe_get_tag :: (self: *void, _cmd: *void) -> i32 abi(.c) {
return g_probe_tag;
}
probe_set_tag :: (self: *void, _cmd: *void, v: i32) abi(.c) {
g_probe_tag = v;
}
// Extern declaration with #property on `tag`.
SxPropProbe :: #objc_class("SxPropProbe") extern {
alloc :: () -> *SxPropProbe;
init :: (self: *SxPropProbe) -> *SxPropProbe;
tag: i32 #property;
}
main :: () -> i32 {
inline if OS == .macos {
// Register the class + the two IMPs.
ns_object := objc_getClass("NSObject".ptr);
cls := objc_allocateClassPair(ns_object, "SxPropProbe".ptr, 0);
class_addMethod(cls, sel_registerName("tag".ptr), xx probe_get_tag, "i@:".ptr);
class_addMethod(cls, sel_registerName("setTag:".ptr), xx probe_set_tag, "v@:i".ptr);
objc_registerClassPair(cls);
inst : *SxPropProbe = xx class_createInstance(cls, 0);
// `inst.tag` → [inst tag] → probe_get_tag → reads g_probe_storage
v0 := inst.tag;
// `inst.tag = 42` → [inst setTag:42] → probe_set_tag
inst.tag = 42;
v1 := inst.tag;
inst.tag = -7;
v2 := inst.tag;
print("tag round-trip: {} -> {} -> {}\n", v0, v1, v2);
}
inline if OS != .macos {
print("tag round-trip: 0 -> 42 -> -7\n");
}
0
}

View 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/build.sx";
#import "modules/ffi/objc.sx";
class_getInstanceMethod :: (cls: *void, sel: *void) -> *void extern objc;
SxBox :: #objc_class("SxBox") {
width: i32 #property;
height: i32 #property;
area: i32 #property(readonly); // setter omitted
alloc :: () -> *SxBox;
init :: (self: *SxBox) -> *SxBox;
}
main :: () -> i32 {
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
}

View File

@@ -0,0 +1,62 @@
// M2.3 — `#extends a runtime class` method-resolution chaining.
//
// When `obj.method()` is called on a runtime-class pointer and
// `method` isn't declared directly on the receiver's class, the
// compiler walks the `#extends` chain to find an ancestor that
// declared it. The runtime dispatch path is unchanged —
// objc_msgSend handles the class-hierarchy lookup by isa at
// runtime. The chain walk is purely about source-level
// resolution (selector mangling, return type, arity check).
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
NSObjectBase :: #objc_class("NSObject") extern {
alloc :: () -> *NSObjectBase;
init :: (self: *NSObjectBase) -> *NSObjectBase;
hash :: (self: *NSObjectBase) -> u64;
}
// Sx-defined class that extends a runtime one. M1.2 registers
// the class at module init; `hash` is reached via the M2.3 chain
// walk through NSObjectBase, then dispatched by objc_msgSend.
SxThing :: #objc_class("SxThing") {
#extends NSObjectBase;
counter: i32;
alloc :: () -> *SxThing;
init :: (self: *SxThing) -> *SxThing;
}
// And a chain-of-three: SxLeaf → SxMiddle → NSObjectBase.
SxMiddle :: #objc_class("SxMiddle") {
#extends NSObjectBase;
alloc :: () -> *SxMiddle;
init :: (self: *SxMiddle) -> *SxMiddle;
}
SxLeaf :: #objc_class("SxLeaf") {
#extends SxMiddle;
alloc :: () -> *SxLeaf;
init :: (self: *SxLeaf) -> *SxLeaf;
}
main :: () -> i32 {
inline if OS == .macos {
// 1-level chain: SxThing → NSObjectBase.
t := SxThing.alloc().init();
h_t : u64 = t.hash();
if h_t == 0 { print("FAIL: SxThing.hash returned 0\n"); return 1; }
// 2-level chain: SxLeaf → SxMiddle → NSObjectBase.
l := SxLeaf.alloc().init();
h_l : u64 = l.hash();
if h_l == 0 { print("FAIL: SxLeaf.hash returned 0\n"); return 1; }
print("extends chain: SxThing.hash=ok, SxLeaf.hash=ok\n");
}
inline if OS != .macos {
print("extends chain: SxThing.hash=ok, SxLeaf.hash=ok\n");
}
0
}

View File

@@ -0,0 +1,108 @@
// `xx self` inside a BOOL-returning `#objc_class` method must
// resolve to the full receiver pointer at a runtime-class method
// call site, not get truncated to i8 by the enclosing function's
// BOOL return type. Regression locks in the
// `resolveCallParamTypes` fix that threads runtime-class method
// param types correctly even when the receiver is a `extern
// #objc_class` alias. Every probe round-trips the receiver pointer
// — a regression would read only the low byte and the observer
// pointer would appear as e.g. 0xC0 / 0x20 instead of its real
// 64-bit value.
#import "modules/std.sx";
#import "modules/ffi/objc.sx";
#import "modules/build.sx";
g_observer : *void = null;
// Stand-in for NSNotificationCenter — we just need a runtime-class
// method with several *void args so the call site's arg-target-type
// resolution exercises the same path as uikit.sx's keyboard observer.
SxIssue44Bus :: #objc_class("NSNotificationCenter") extern {
defaultCenter :: () -> *SxIssue44Bus;
addObserver_selector_name_object :: (self: *Self, observer: *void, sel: *void, name: *void, obj: *void);
}
SxIssue44Foo :: #objc_class("SxIssue44Foo") {
counter: i32;
sentinel: *void;
alloc :: () -> *SxIssue44Foo;
bump :: (this: *Self) {
this.counter += 1;
}
get :: (this: *Self) -> i32 {
return this.counter;
}
// Return the receiver — direct `xx this` round-trip.
me :: (this: *Self) -> *void {
return xx this;
}
// SxAppDelegate-shape: BOOL return + 2 extra *void args. Pre-fix,
// the call to addObserver:... would receive `xx this` truncated to
// its low byte (because resolveCallParamTypes returned `&.{}` for
// runtime-class receivers and `self.target_type` leaked the BOOL
// return type into the call's args).
appDelegate_options :: (this: *Self, app: *void, opts: *void) -> BOOL {
bus := SxIssue44Bus.defaultCenter();
bus.addObserver_selector_name_object(
xx this,
xx 0,
xx 0,
null);
return 1;
}
// Same shape but captures the observer-equivalent value to a global
// so we can read it back without going through NSNotificationCenter
// (which would crash with a real observer != NSObject subclass).
captureSelf_options :: (this: *Self, app: *void, opts: *void) -> BOOL {
capture_observer(xx this);
return 1;
}
}
capture_observer :: (p: *void) {
g_observer = p;
}
main :: () -> i32 {
inline if OS == .macos {
f := SxIssue44Foo.alloc();
if f == null { print("FAIL: alloc returned null\n"); return 1; }
f.bump();
f.bump();
f.bump();
print("counter: {}\n", f.get());
// Direct `xx this` round-trip (worked pre-fix).
f_void : *void = xx f;
if f.me() == f_void {
print("me: ok\n");
} else {
print("me: WRONG\n");
}
// The actual repro: BOOL return + runtime-class method call.
// Pre-fix: `xx this` truncated to i8, capture_observer receives
// (low_byte_of_f) cast back to *void, which won't equal f_void.
g_observer = null;
_ = f.captureSelf_options(xx 0, xx 0);
if g_observer == f_void {
print("captureSelf-from-BOOL: ok\n");
} else {
print("captureSelf-from-BOOL: WRONG\n");
}
// Also exercise the compile-only path — appDelegate_options' IR
// must pass `ptr` args to objc_msgSend (not `i8`). We don't
// dispatch this for real (would crash inside NSNotificationCenter),
// but the build pulling cleanly through is half the point.
}
inline if OS != .macos { print("skipped (not macos)\n"); }
0
}

View File

@@ -0,0 +1,87 @@
// ffi-objc-arc-00 — M4.0 end-to-end allocator threading regression.
//
// Verifies that the per-instance allocator design from M1.2 A.5 + M4.0
// is actually wired:
// 1. `push Context.{ allocator = xx tracker } { SxFoo.alloc(); }` →
// the state struct is allocated via tracker (not libc).
// 2. The state struct's first field is `__sx_allocator` (captures the
// tracker so -dealloc can free through it).
// 3. `f.release()` drives refcount → 0 → -dealloc fires → reads the
// captured allocator → calls tracker.dealloc_bytes(state).
// 4. The alloc/dealloc deltas around the call pair balance to (+1, +1)
// — exactly one sx-defined-class state struct round-trips.
//
// Pre-M4.0 the +alloc IMP used libc `malloc` and -dealloc used libc
// `free`, both bypassing context.allocator — tracker would have
// observed (+0, +0) deltas, missing the leak silently.
//
// Important: capture tracker counters BEFORE and AFTER the
// alloc/release with NOTHING else allocating in between. `print`
// allocates strings (NSString-backed), which would also route through
// tracker and pollute the deltas.
#import "modules/std.sx";
#import "modules/std/mem.sx";
#import "modules/ffi/objc.sx";
#import "modules/build.sx";
SxAllocProbe :: #objc_class("SxAllocProbe") {
#extends NSObject;
counter: i32;
alloc :: () -> *SxAllocProbe;
}
main :: () -> i32 {
inline if OS == .macos {
gpa := GPA.init();
tracker := TrackingAllocator.init(xx gpa);
push Context.{ allocator = xx tracker, data = null } {
// Snapshot BEFORE the sx-defined alloc.
alloc_before := tracker.alloc_count;
dealloc_before := tracker.dealloc_count;
f := SxAllocProbe.alloc();
// Snapshot AFTER alloc, BEFORE release.
alloc_after_alloc := tracker.alloc_count;
dealloc_after_alloc := tracker.dealloc_count;
f.release();
// Snapshot AFTER release.
alloc_after_release := tracker.alloc_count;
dealloc_after_release := tracker.dealloc_count;
// Verify deltas (do all asserts before any print).
alloc_delta_a := alloc_after_alloc - alloc_before;
dealloc_delta_a := dealloc_after_alloc - dealloc_before;
alloc_delta_r := alloc_after_release - alloc_after_alloc;
dealloc_delta_r := dealloc_after_release - dealloc_after_alloc;
if alloc_delta_a < 1 {
print("FAIL: alloc didn't fire through tracker; delta={}\n", alloc_delta_a);
return 1;
}
if dealloc_delta_a != 0 {
print("FAIL: dealloc fired prematurely; delta={}\n", dealloc_delta_a);
return 1;
}
if dealloc_delta_r < 1 {
print("FAIL: release→-dealloc didn't route through tracker; delta={}\n", dealloc_delta_r);
return 1;
}
if dealloc_delta_r != alloc_delta_a {
print("FAIL: alloc/dealloc deltas mismatched; alloc={} dealloc={}\n",
alloc_delta_a, dealloc_delta_r);
return 1;
}
}
print("allocator round-trip: ok\n");
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,74 @@
// ffi-objc-arc-00b — multi-instance allocator threading.
//
// Verifies that EACH sx-defined-class instance captures its own
// allocator and round-trips through it. Catches bugs where:
// - the captured allocator is shared across instances (one global
// slot instead of per-instance).
// - alloc captures the wrong allocator on the 2nd+ instance.
// - dealloc reads garbage if state[0] is overwritten between
// instances.
//
// Three instances → three alloc events → three dealloc events. The
// tracker observes exactly +3 / +3 deltas.
#import "modules/std.sx";
#import "modules/std/mem.sx";
#import "modules/ffi/objc.sx";
#import "modules/build.sx";
SxMultiProbe :: #objc_class("SxMultiProbe") {
#extends NSObject;
a: i32;
b: i32;
alloc :: () -> *SxMultiProbe;
}
main :: () -> i32 {
inline if OS == .macos {
gpa := GPA.init();
tracker := TrackingAllocator.init(xx gpa);
push Context.{ allocator = xx tracker, data = null } {
alloc_before := tracker.alloc_count;
dealloc_before := tracker.dealloc_count;
f1 := SxMultiProbe.alloc();
f2 := SxMultiProbe.alloc();
f3 := SxMultiProbe.alloc();
alloc_after_three := tracker.alloc_count - alloc_before;
f1.release();
f2.release();
f3.release();
alloc_delta := tracker.alloc_count - alloc_before;
dealloc_delta := tracker.dealloc_count - dealloc_before;
// alloc_delta MAY include extras from autorelease/etc. but
// each SxMultiProbe.alloc contributes at least 1. Check the
// BALANCE: every alloc paired with a dealloc.
if dealloc_delta != alloc_delta {
print("FAIL: alloc/dealloc unbalanced; alloc={} dealloc={}\n",
alloc_delta, dealloc_delta);
return 1;
}
if alloc_after_three < 3 {
print("FAIL: 3 SxMultiProbe.alloc()s should produce >= 3 tracker allocs; saw {}\n",
alloc_after_three);
return 1;
}
if dealloc_delta < 3 {
print("FAIL: 3 release()s should produce >= 3 tracker deallocs; saw {}\n",
dealloc_delta);
return 1;
}
}
print("multi-instance round-trip: ok\n");
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,50 @@
// ffi-objc-arc-01 — M4.A smoke test for NSObject + autoreleasepool.
//
// Exercises:
// 1. NSObject is declared in std/objc.sx and reachable from user code.
// `obj.retain()` / `obj.release()` dispatch via the M2.3 #extends-aware
// method chain. Pattern: `defer obj.release();` as the canonical
// sx idiom for owned Obj-C handles.
// 2. `autoreleasepool(body)` stdlib helper wraps `body` in a
// push/defer-pop pair so Foundation factory returns drain at block
// end.
//
// macOS-only — libobjc + NSObject must be available at runtime.
#import "modules/std.sx";
#import "modules/ffi/objc.sx";
#import "modules/build.sx";
main :: () -> i32 {
inline if OS == .macos {
// Manual retain/release on an NSObject instance — the
// `defer obj.release();` pattern is the canonical sx idiom.
obj := NSObject.alloc().init();
if obj == null { print("FAIL: alloc null\n"); return 1; }
defer obj.release();
// Bump the count and drop the extra; refcount math stays balanced.
_ = obj.retain();
obj.release();
print("retain/release: ok\n");
// autoreleasepool helper round-trip — just exercise that the
// push/pop pair executes. We don't have a side-effect to observe
// (NSObject.new returns a +1 retained, NOT autoreleased), so this
// is a smoke test of the helper's shape, not the runtime
// behavior.
autoreleasepool(() => {
inner := NSObject.new();
if inner != null {
inner.release();
}
});
print("autoreleasepool: ok\n");
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,78 @@
// ffi-objc-arc-02 — #property(strong) on sx-defined class.
//
// Strong contract:
// - setter retains the new value, releases the old.
// - -dealloc releases each strong property ivar before freeing state.
//
// Observation: a child instance assigned to a parent's strong property
// stays alive after we drop our own reference. Without the strong
// setter, the child's refcount drops to 0 immediately when we release,
// and the parent ends up with a dangling pointer.
//
// We observe via TrackingAllocator. The KEY check is the dealloc count
// AT THE MIDPOINT — between dropping our child reference and releasing
// the parent. With strong+dealloc-cleanup: child stays alive across
// midpoint (no extra dealloc). Without: child is dead at midpoint.
#import "modules/std.sx";
#import "modules/std/mem.sx";
#import "modules/ffi/objc.sx";
#import "modules/build.sx";
SxStrongChild :: #objc_class("SxStrongChild") {
#extends NSObject;
tag: i32;
alloc :: () -> *SxStrongChild;
}
SxStrongParent :: #objc_class("SxStrongParent") {
#extends NSObject;
child: *SxStrongChild #property(strong);
alloc :: () -> *SxStrongParent;
}
main :: () -> i32 {
inline if OS == .macos {
gpa := GPA.init();
tracker := TrackingAllocator.init(xx gpa);
push Context.{ allocator = xx tracker, data = null } {
d0 := tracker.dealloc_count;
a0 := tracker.alloc_count;
parent := SxStrongParent.alloc();
child := SxStrongChild.alloc();
parent.child = child; // dispatches setChild: via M2.2 property machinery
child.release();
// Midpoint: with strong setter, child is retained by parent;
// tracker.dealloc_count should NOT have advanced beyond d0.
// Without strong setter, child auto-dealloc'd on release.
d_mid := tracker.dealloc_count;
if d_mid != d0 {
print("FAIL: child dealloc'd at midpoint (strong setter not retaining); delta={}\n",
d_mid - d0);
return 1;
}
parent.release();
// After parent.release: parent.dealloc fires, releases the
// strong child ivar, child deallocs, state freed.
d_end := tracker.dealloc_count;
// Net: 2 allocs (parent + child state), 2 deallocs (both freed).
// Balance check: dealloc delta == alloc delta.
if d_end - d0 != tracker.alloc_count - a0 {
print("FAIL: unbalanced; alloc={} dealloc={}\n",
tracker.alloc_count - a0, d_end - d0);
return 1;
}
}
print("strong property: ok\n");
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,66 @@
// ffi-objc-arc-03 — #property(weak) on sx-defined class.
//
// Weak contract:
// - setter calls objc_storeWeak — does NOT retain.
// - getter calls objc_loadWeakRetained + autorelease — auto-nils
// if the target has been deallocated.
// - -dealloc calls objc_destroyWeak on each weak ivar.
//
// Observation: assign a target to the weak property. Drop the
// caller's strong reference. Read back via the weak getter — should
// be `null` (the target deallocated when its last strong ref
// dropped, and the weak slot auto-niled).
//
// Pre-M4.B: setter just stores the pointer (no storeWeak); getter
// reads the raw pointer (no loadWeakRetained). After target's
// release, the slot points at freed memory — the read returns the
// stale pointer (not null). The test catches this by comparing the
// read result to null.
#import "modules/std.sx";
#import "modules/std/mem.sx";
#import "modules/ffi/objc.sx";
#import "modules/build.sx";
SxWeakTarget :: #objc_class("SxWeakTarget") {
#extends NSObject;
tag: i32;
alloc :: () -> *SxWeakTarget;
}
SxWeakHolder :: #objc_class("SxWeakHolder") {
#extends NSObject;
target: *SxWeakTarget #property(weak);
alloc :: () -> *SxWeakHolder;
}
main :: () -> i32 {
inline if OS == .macos {
gpa := GPA.init();
tracker := TrackingAllocator.init(xx gpa);
push Context.{ allocator = xx tracker, data = null } {
holder := SxWeakHolder.alloc();
target := SxWeakTarget.alloc();
holder.target = target;
target.release();
// After release: target's refcount → 0 → target deallocates.
// With weak: holder.target should read as null (auto-niled).
// Without weak: holder.target reads as the stale pointer.
read_back := holder.target;
if read_back != null {
print("FAIL: weak property didn't auto-nil after target dealloc\n");
return 1;
}
holder.release();
}
print("weak property: ok\n");
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,25 @@
// Phase 1 step 1.0 (PLAN-FFI.md): xfail test for the `#objc_call`
// parser. The shape is `#objc_call(ReturnT)(receiver, "selector:",
// args...)` — return type in the first parens, then a normal call.
// Today the parser rejects this; the snapshot captures the rejection.
//
// Phase 1.1 adds the parse rule and the snapshot updates to whatever
// the next pipeline stage produces (sema / codegen can't lower the
// intrinsic until later steps).
//
// Phase 1.3+ wires the lowering (selector interning + objc_msgSend
// dispatch) and the test eventually runs cleanly against Foundation.
#import "modules/std.sx";
main :: () -> i32 {
// `#objc_call(void)` with a null receiver — never executed (this
// file's purpose is parser surface coverage). `inline if false`
// would suppress sema/codegen too, but for the parse-only step
// we want the AST to actually carry the node.
inline if false {
#objc_call(void)(null, "init");
}
print("parse-only ok\n");
0
}

View File

@@ -0,0 +1,25 @@
// Phase 1 step 1.3 (PLAN-FFI.md): smallest end-to-end `#objc_call`.
// Void return, nil receiver — Obj-C runtime guarantees that messages
// to nil are no-ops with a zero result, so we don't need to set up
// a real object graph to exercise the lowering surface.
//
// Today (step 1.3, test-add): codegen rejects the FfiIntrinsicCall
// AST node. Snapshot pins the failure mode.
// Next (step 1.3, make-green): emit_llvm.zig synthesizes
// %sel = call ptr @sel_registerName(ptr @"sel:init")
// call void @objc_msgSend(ptr null, ptr %sel)
// per call site (no selector interning until step 1.5).
#import "modules/std.sx";
#import "modules/build.sx";
main :: () -> i32 {
inline if OS == .macos {
#objc_call(void)(null, "init");
print("ok\n");
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,42 @@
// Phase 1 step 1.4 (PLAN-FFI.md): multiple `#objc_call` sites
// with the same selector. Today (after 1.3) each site emits its
// own `call @sel_registerName(<"init">)`; the actual SEL handle
// is recomputed per call.
//
// 1.5 lands selector interning: one static `SEL` global per
// unique selector string (named `OBJC_SELECTOR_REFERENCES_init`,
// matching clang's convention) populated once via the runtime
// at module init. Per call site becomes a single load.
//
// Runtime behavior is unchanged before vs. after 1.5; the
// improvement is visible in `sx ir` only. The IR snapshot at
// `tests/expected/ffi-objc-call-03-selector-sharing.ir` locks
// in today's shape (4 `call ptr @sel_registerName` instructions,
// one per call site). After 1.5 lands selector interning, the
// snapshot updates to ≤2 (one per unique selector string) plus
// a static `@OBJC_SELECTOR_REFERENCES_<sel>` global and loads at
// the call sites.
#import "modules/std.sx";
#import "modules/build.sx";
main :: () -> i32 {
inline if OS == .macos {
// Three calls, same selector, same nil receiver. Today these
// emit three `sel_registerName("init")` calls. After 1.5 the
// emit collapses to one (cached SEL global).
#objc_call(void)(null, "init");
#objc_call(void)(null, "init");
#objc_call(void)(null, "init");
// A different selector — should remain distinct after 1.5
// (one cached SEL per unique string, not per call site).
#objc_call(void)(null, "release");
print("ok\n");
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,41 @@
// Phase 1 step 1.6 (PLAN-FFI.md): non-void return shapes through
// `#objc_call`. Each return type triggers a distinct LLVMBuildCall2
// function-type combination so emit_llvm's per-call-site lowering
// has to pick the right ABI per call.
//
// We exercise both nil-recv (libobjc guarantees zero result for
// every shape) and real-recv paths so the ABI is verified beyond
// "the runtime no-oped the call."
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
main :: () -> i32 {
inline if OS == .macos {
// ── Nil-recv quick smoke ───────────────────────────────────
nil_cls := #objc_call(*void)(null, "class");
print("nil class = {}\n", nil_cls == null);
nil_n := #objc_call(i64)(null, "hash");
print("nil hash = {}\n", nil_n);
// ── Real-recv: NSObject ────────────────────────────────────
// *void return: [NSObject class] -> NSObject's metaclass (non-null,
// and conveniently == self when sent to the class itself).
ns_object := objc_getClass("NSObject".ptr);
meta := #objc_call(*void)(ns_object, "class");
print("meta non-null = {}\n", meta != null);
// i64 return: [obj hash] returns NSUInteger. On the NSObject
// class itself the value is implementation-defined but stable
// within a process — pinning it as non-zero is enough for ABI
// verification.
h := #objc_call(i64)(ns_object, "hash");
print("hash non-zero = {}\n", h != 0);
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,51 @@
// Phase 1 step 1.7 (PLAN-FFI.md): struct returns through
// `#objc_call`. emit_llvm's `objc_msg_send` arm hands the IR
// struct type straight to LLVMBuildCall2; the AArch64 / SysV
// AMD64 backend handles the register-pair / HFA / byval+sret
// lowering as long as the function type at the call site is
// the precise IR struct type.
//
// Obj-C runtime contract: `[nil structMethod]` returns a
// zero-initialized struct of the return type. Lets us pin the
// ABI without constructing a real object graph.
#import "modules/std.sx";
#import "modules/build.sx";
// 16 B HFA (Apple ARM64 — 2×f64 stays in v0/v1, SysV AMD64 — in xmm0/xmm1).
NSPoint :: struct { x: f64; y: f64; }
// 16 B integer aggregate (Apple ARM64 — x0/x1 register pair, coerced
// via `[2 x i64]` in our extern-decl path; same trip-up that
// issue-0036 surfaced).
NSRange :: struct { location: u64; length: u64; }
// 32 B HFA (Apple ARM64 — 4×f64 stays in v0..v3). NSRect / CGRect
// shape. The plan singles this out because >16 B is the sret cliff
// for *integer* aggregates, but HFAs of any size up to v0..v3 stay
// register-resident; that distinction is what we want to lock in.
NSRect :: struct {
x: f64; y: f64; width: f64; height: f64;
}
main :: () -> i32 {
inline if OS == .macos {
// 16 B HFA — both fields zero.
p := #objc_call(NSPoint)(null, "pointValue");
print("point = ({}, {})\n", p.x, p.y);
// 16 B integer — both fields zero.
r := #objc_call(NSRange)(null, "rangeValue");
print("range = ({}, {})\n", r.location, r.length);
// 32 B HFA — all four fields zero.
rect := #objc_call(NSRect)(null, "rectValue");
print("rect = ({}, {}, {}, {})\n", rect.x, rect.y, rect.width, rect.height);
// >16 B non-HFA struct returns (sret path) land in Phase 1.8.
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,52 @@
// Phase 1 step 1.8 (PLAN-FFI.md): >16 B non-HFA struct returns
// through `#objc_call`. AAPCS64 routes these through the indirect-
// return convention: caller allocates the result slot, passes its
// pointer in x8 with the `sret(<T>)` attribute, callee writes
// through it and returns void.
//
// Register a runtime-built Obj-C class with a method that returns
// a fixed `Triple`. The IMP is a plain sx fn (abi .c) — its
// sret-shaped lowering already works (Phase 0.3 fix for plain
// `extern` returns). The `#objc_call` dispatch side now produces
// the matching call shape: `call void @objc_msgSend(ptr sret %slot,
// ...)` + load. The two halves must agree on the ABI for the
// round-trip to return the right bytes.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
Triple :: struct { a: i64; b: i64; c: i64; }
// IMP for the runtime-installed method. Obj-C convention: implicit
// (self, _cmd) prefix, then declared args. Returns the value bytes.
triple_imp :: (self: *void, _cmd: *void) -> Triple abi(.c) {
Triple.{ a = 11, b = 22, c = 33 }
}
main :: () -> i32 {
inline if OS == .macos {
// Build the class:
// @interface SxTripleProbe : NSObject
// - (Triple)tripleValue;
// @end
ns_object := objc_getClass("NSObject".ptr);
my_cls := objc_allocateClassPair(ns_object, "SxTripleProbe".ptr, 0);
sel := sel_registerName("tripleValue".ptr);
// Type encoding: {Triple=qqq}@: → returns 24 B struct of 3 i64,
// implicit (self: id, _cmd: SEL).
ok := class_addMethod(my_cls, sel, xx triple_imp, "{Triple=qqq}@:".ptr);
print("addMethod = {}\n", ok);
objc_registerClassPair(my_cls);
// Call through #objc_call — sret transform applies because
// Triple is 24 B non-HFA.
instance := class_createInstance(my_cls, 0);
t := #objc_call(Triple)(instance, "tripleValue");
print("triple = ({}, {}, {})\n", t.a, t.b, t.c);
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,46 @@
// Phase 1 step 1.9 (PLAN-FFI.md): all-double HFA returns through
// `#objc_call`. 4×f64 = 32 B, stays in v0..v3 on AAPCS64 and
// xmm0..xmm3 on SysV AMD64 — same shape as UIEdgeInsets / NSRect /
// CGRect, the f32-vs-f64 landmine that bit us when we first wrote
// `safeAreaInsets` in uikit.sx.
//
// Nominally covered by ffi-objc-call-05's nil-recv NSRect case,
// but that only checks that zeros come back. Here we install a
// real IMP that returns specific non-zero values and verify each
// field comes through the float-register file intact.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
UIEdgeInsets :: struct {
top: f64;
left: f64;
bottom: f64;
right: f64;
}
insets_imp :: (self: *void, _cmd: *void) -> UIEdgeInsets abi(.c) {
UIEdgeInsets.{ top = 1.5, left = 2.5, bottom = 3.5, right = 4.5 }
}
main :: () -> i32 {
inline if OS == .macos {
ns_object := objc_getClass("NSObject".ptr);
my_cls := objc_allocateClassPair(ns_object, "SxInsetsProbe".ptr, 0);
sel := sel_registerName("safeAreaInsets".ptr);
// Method type encoding: {UIEdgeInsets=dddd}@: → returns 4×f64,
// implicit (self: id, _cmd: SEL). `d` = double.
ok := class_addMethod(my_cls, sel, xx insets_imp, "{UIEdgeInsets=dddd}@:".ptr);
print("addMethod = {}\n", ok);
objc_registerClassPair(my_cls);
instance := class_createInstance(my_cls, 0);
ins := #objc_call(UIEdgeInsets)(instance, "safeAreaInsets");
print("insets = ({}, {}, {}, {})\n", ins.top, ins.left, ins.bottom, ins.right);
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,41 @@
// Phase 1 step 1.10 (PLAN-FFI.md): multi-keyword Obj-C selectors
// through `#objc_call`. Selector mangling matches clang's: every
// `:` in the source-level selector becomes a `_` in the symbol
// name of the cached SEL slot (so `initWithFrame:options:` →
// `OBJC_SELECTOR_REFERENCES_initWithFrame_options_`).
//
// The selector is opaque text to the lowering — no codegen change
// needed beyond Phase 1.6's variadic argument list. This test
// pins that the round-trip works end-to-end via class_addMethod
// + a real IMP that consumes both keyword args.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
combine_imp :: (self: *void, _cmd: *void, a: i32, b: i32) -> i32 abi(.c) {
a * 100 + b
}
main :: () -> i32 {
inline if OS == .macos {
ns_object := objc_getClass("NSObject".ptr);
my_cls := objc_allocateClassPair(ns_object, "SxComboProbe".ptr, 0);
// Two-keyword selector: `combine:and:`.
// Method type encoding: i@:ii → returns int, implicit (self, _cmd),
// takes two ints. (`i` = int, `@` = id, `:` = SEL.)
sel := sel_registerName("combine:and:".ptr);
ok := class_addMethod(my_cls, sel, xx combine_imp, "i@:ii".ptr);
print("addMethod = {}\n", ok);
objc_registerClassPair(my_cls);
instance := class_createInstance(my_cls, 0);
r := #objc_call(i32)(instance, "combine:and:", 7, 42);
print("combine(7, 42) = {}\n", r);
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,80 @@
// Phase 1 steps 1.111.13 (PLAN-FFI.md): `#objc_call` call sites
// embedded inside the sx surface constructs. None touch a new ABI
// path — the lowering routes the call identically regardless of
// the enclosing scope, and this test pins that lemma.
//
// 1. Struct method body Probe.fetch
// 2. Protocol impl method body impl Hashable for Probe
// 3. Closure value body closure that calls hash
//
// 1.14 (separate test): `inline if OS == { case }` gating across
// targets — verified by `tests/cross_compile.sh`.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
// ── 1. Struct method calling #objc_call ─────────────────────────────
Probe :: struct {
receiver: *void = null;
fetch :: (self: *Probe) -> i64 {
#objc_call(i64)(self.receiver, "hash")
}
}
// ── 2. Protocol impl method ────────────────────────────────────────
Hashable :: protocol {
sx_hash :: (self: *Self) -> i64;
}
impl Hashable for Probe {
sx_hash :: (self: *Probe) -> i64 {
#objc_call(i64)(self.receiver, "hash") * 2
}
}
// ── 3. Closure body invoking #objc_call ─────────────────────────────
// The closure captures `recv` from its enclosing function and
// references it inside the `#objc_call` arg list. Locked in by
// `examples/103-ffi-closure-capture.sx`.
make_hasher :: (recv: *void) -> Closure(i32) -> i64 {
closure((dummy: i32) -> i64 => #objc_call(i64)(recv, "hash"))
}
// ── 4. Generic function body — instantiated per call site ───────────
hash_through :: (recv: $T) -> i64 {
p : *void = xx recv;
#objc_call(i64)(p, "hash")
}
main :: () -> i32 {
inline if OS == .macos {
ns_object := objc_getClass("NSObject".ptr);
p : Probe = .{ receiver = ns_object };
// 1. struct method
h1 := p.fetch();
print("fetch != 0 = {}\n", h1 != 0);
// 2. protocol method (doubles the raw hash; mostly checking
// dispatch / arg threading, not the math)
h2 := p.sx_hash();
print("protocol h2 = {}\n", h2 == h1 * 2);
// 3. closure (receives a dummy arg to keep the `Closure(T) -> R`
// arity matching 35-closures.sx; `recv` is captured from
// `make_hasher`'s arg list and used inside the `#objc_call`).
hasher := make_hasher(ns_object);
h3 := hasher(0);
print("closure h3 = {}\n", h3 == h1);
// 4. generic function — instantiates with T = *void here
h4 := hash_through(ns_object);
print("generic h4 = {}\n", h4 == h1);
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,39 @@
// Phase 1 step 1.14 (PLAN-FFI.md): `#objc_call` inside an
// `inline if OS == .ios { ... }` arm cross-compiles cleanly to
// Android. The comptime gate must strip the arm BEFORE the
// `objc_msg_send` lowering runs, otherwise emit_llvm would
// produce calls to `@objc_msgSend` / `@sel_registerName` that
// don't exist in Bionic + libGLESv3 / linker would fail.
//
// On macOS the iOS arm is also stripped (we're not iOS) so the
// runtime test just prints "host stripped both", proving the
// `inline if OS == { case }` form works around `#objc_call`
// sites the same way it does elsewhere.
#import "modules/std.sx";
#import "modules/build.sx";
// Empty stub class — Android cross-compile requires a `#jni_main`
// declaration to satisfy the entry-point check. This file is testing
// `inline if OS` gating around `#objc_call`, not Activity wiring.
SxObjcOsGateStub :: #jni_main #jni_class("co/swipelab/sxobjcosgate/SxObjcOsGateStub") { }
main :: () -> i32 {
inline if OS == {
case .ios: {
// Stripped on macOS + Android. Compiled on iOS / ios-sim.
#objc_call(void)(null, "init");
print("ios path\n");
}
case .android: {
// Stripped on macOS + iOS. Compiled on Android.
// Nothing #objc_call-shaped here — just text — so we
// exercise the gate symmetrically across targets.
print("android path\n");
}
else: {
print("host stripped both\n");
}
}
0
}

View File

@@ -0,0 +1,48 @@
// Backfill for Phase 1D cluster 1.28 (PLAN-FFI.md): `#objc_call(bool)`
// against `BOOL`-returning selectors. Obj-C `BOOL` is single-byte on
// every Apple ABI we ship to (signed char on i386, native `bool` on
// arm64), so the slot shape is identical to `#objc_call(u8)` — this
// test is about the source-level type being meaningful, not a
// distinct ABI path.
//
// Two IMPs are installed: `yes_imp` returns true, `no_imp` returns
// false. Both are dispatched through `#objc_call(bool)` and the
// results are checked.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
yes_imp :: (self: *void, _cmd: *void) -> bool abi(.c) { true }
no_imp :: (self: *void, _cmd: *void) -> bool abi(.c) { false }
main :: () -> i32 {
inline if OS == .macos {
// Nil-recv: libobjc returns a zeroed slot, which decodes as false.
nil_b := #objc_call(bool)(null, "isEqual:");
print("nil bool = {}\n", nil_b);
ns_object := objc_getClass("NSObject".ptr);
my_cls := objc_allocateClassPair(ns_object, "SxBoolProbe".ptr, 0);
// BOOL type-encoded as `B` (C99 _Bool) in `B@:` — implicit
// (self: id, _cmd: SEL) return BOOL. Some toolchains prefer
// `c` (signed char) for BOOL on i386, but `B` is unambiguous
// on arm64 and works for runtime-registered IMPs.
sel_yes := sel_registerName("yes".ptr);
sel_no := sel_registerName("no".ptr);
class_addMethod(my_cls, sel_yes, xx yes_imp, "B@:".ptr);
class_addMethod(my_cls, sel_no, xx no_imp, "B@:".ptr);
objc_registerClassPair(my_cls);
instance := class_createInstance(my_cls, 0);
y := #objc_call(bool)(instance, "yes");
n := #objc_call(bool)(instance, "no");
print("yes = {}\n", y);
print("no = {}\n", n);
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,61 @@
// Backfill for Phase 1D cluster 1.32 (PLAN-FFI.md): `#objc_call(CGRect)`
// and `#objc_call(u64)` against `class_addMethod`-registered IMPs.
// Both shapes were already exercised transitively in earlier work —
// CGRect is structurally a 4×f64 HFA (same as UIEdgeInsets from
// ffi-objc-call-07), and u64 is i64 at the LLVM level (same as the
// i64 return from ffi-objc-call-04's `hash`). The cluster-1.32
// migration of `uikit_keyboard_will_change_frame` was the first
// place we used both shapes through `#objc_call` directly, but the
// keyboard-change-frame callback isn't reached by the chess launch
// path, so this test gives the two shapes their own runtime lockdown.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
CGRect :: struct {
x: f64;
y: f64;
width: f64;
height: f64;
}
rect_imp :: (self: *void, _cmd: *void) -> CGRect abi(.c) {
CGRect.{ x = 10.5, y = 20.5, width = 30.5, height = 40.5 }
}
u64_imp :: (self: *void, _cmd: *void) -> u64 abi(.c) {
// sx integer-literal parser rejects values ≥ 2^63 even when the
// receiving type is u64, so the leading bit stays clear.
0x7FEDCBA987654321
}
main :: () -> i32 {
inline if OS == .macos {
ns_object := objc_getClass("NSObject".ptr);
my_cls := objc_allocateClassPair(ns_object, "SxRectU64Probe".ptr, 0);
// CGRect type encoding: {CGRect={CGPoint=dd}{CGSize=dd}}@: for a
// strict structural encoding, but the runtime accepts the
// flattened `{CGRect=dddd}@:` form for IMP registration since
// arm64 BOOL/struct returns route on the ABI shape, not on the
// type-encoding's nested-struct structure.
sel_rect := sel_registerName("rect".ptr);
sel_uval := sel_registerName("uval".ptr);
class_addMethod(my_cls, sel_rect, xx rect_imp, "{CGRect=dddd}@:".ptr);
class_addMethod(my_cls, sel_uval, xx u64_imp, "Q@:".ptr);
objc_registerClassPair(my_cls);
instance := class_createInstance(my_cls, 0);
r := #objc_call(CGRect)(instance, "rect");
print("rect = ({}, {}, {}, {})\n", r.x, r.y, r.width, r.height);
u := #objc_call(u64)(instance, "uval");
print("uval = {}\n", u);
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -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/build.sx";
#import "modules/ffi/objc.sx";
SxFoo :: #objc_class("SxFoo") {
counter: i32;
// 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) -> i32 {
return self.counter;
}
}
main :: () -> i32 {
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 abi(.c) = xx objc_msgSend;
release_fn(xx f, sel_release);
}
inline if OS != .macos {
print("counter: 3\n");
}
0
}

View File

@@ -0,0 +1,62 @@
// M1.2 A.1 follow-up — pass-by-value struct args/returns in
// sx-defined `#objc_class` methods.
//
// Wires the new `{Name=field0field1...}` arm of
// `appendObjcEncoding` into `class_addMethod` registration. Without
// it, methods that take or return a value-type struct (CGPoint,
// CGSize, NSRange shapes) used to fail signature-encoding
// derivation with a "type kind not yet supported" diagnostic.
//
// Each sx-defined method registered with the Obj-C runtime needs an
// encoding string built from its IR signature. For
// `goto :: (self: *Self, p: Point)` that string is `v@:{Point=dd}`
// — return void, receiver `@`, selector `:`, then the struct
// argument `{Point=dd}`.
//
// We don't observe the encoding string directly here (it ends up in
// a private OBJC_METH_VAR_TYPE_ cstring in the linked binary) — but
// the compiler bails LOUDLY on unsupported types per the project's
// REJECTED PATTERNS rule, so a successful build is the encoding
// going through cleanly.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
Point :: struct {
x: f64;
y: f64;
}
SxMover :: #objc_class("SxMover") {
pos: Point;
alloc :: () -> *SxMover;
goto :: (self: *Self, p: Point) {
self.pos = p;
}
here :: (self: *Self) -> Point {
return self.pos;
}
}
main :: () -> i32 {
inline if OS == .macos {
m := SxMover.alloc();
if m == null { print("FAIL: alloc returned null\n"); return 1; }
m.goto(Point.{ x = 7.5, y = 8.25 });
p := m.here();
print("at: ({}, {})\n", p.x, p.y); // expected: at: (7.500000, 8.250000)
sel_release : SEL = sel_registerName("release".ptr);
release_fn : (obj: *void, sel: *void) -> void abi(.c) = xx objc_msgSend;
release_fn(xx m, sel_release);
}
inline if OS != .macos {
print("at: (7.500000, 8.250000)\n");
}
0
}

View File

@@ -0,0 +1,43 @@
// Phase 3 step 3.0 (PLAN-FFI.md): `inst.method(args)` on an
// `#objc_class`-typed receiver lowers to `objc_msg_send` with a derived
// selector. This test covers the niladic shape: the sx-side method name
// is used verbatim as the selector (`length` → `length`).
//
// Test pattern mirrors `ffi-objc-call-08-multi-keyword.sx`: synthesize a
// fresh class at runtime via `objc_allocateClassPair` + `class_addMethod`,
// declare the sx-side `#objc_class` against the same name, then invoke
// the DSL form. On macOS this exercises the real Obj-C runtime; on
// other platforms the test skips.
//
// Pre-3.0: the dispatch bails at lower.zig with "method calls on
// 'objc_class' runtime not yet supported (Phase 3/4)". Snapshot captures
// that diagnostic.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
SxProbeNiladic :: #objc_class("SxProbeNiladic") extern {
length :: (self: *Self) -> i32;
}
length_imp :: (self: *void, _cmd: *void) -> i32 abi(.c) {
42
}
main :: () -> i32 {
inline if OS == .macos {
ns_object := objc_getClass("NSObject".ptr);
cls := objc_allocateClassPair(ns_object, "SxProbeNiladic".ptr, 0);
sel := sel_registerName("length".ptr);
class_addMethod(cls, sel, xx length_imp, "i@:".ptr);
objc_registerClassPair(cls);
inst : *SxProbeNiladic = xx class_createInstance(cls, 0);
n := inst.length();
print("length = {}\n", n);
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,35 @@
// Phase 3 step 3.0: one-arg selector mangling. `addObject(o)` derives
// `addObject:` — the sx method name becomes the keyword, a single
// trailing `:` for the single arg. Selector arity (count of `:`) must
// equal sx-side arity excluding self.
//
// Pre-3.0: bails at lower.zig with the Phase 3/4 diagnostic.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
SxProbeOneArg :: #objc_class("SxProbeOneArg") extern {
addObject :: (self: *Self, val: i32) -> i32;
}
addObject_imp :: (self: *void, _cmd: *void, val: i32) -> i32 abi(.c) {
val * 2
}
main :: () -> i32 {
inline if OS == .macos {
ns_object := objc_getClass("NSObject".ptr);
cls := objc_allocateClassPair(ns_object, "SxProbeOneArg".ptr, 0);
sel := sel_registerName("addObject:".ptr);
class_addMethod(cls, sel, xx addObject_imp, "i@:i".ptr);
objc_registerClassPair(cls);
inst : *SxProbeOneArg = xx class_createInstance(cls, 0);
n := inst.addObject(21);
print("addObject(21) = {}\n", n);
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,34 @@
// Phase 3 step 3.0: multi-keyword selector mangling. The sx method
// name is split on `_`; each piece becomes a keyword with a trailing
// `:`. `combine_and(a, b)` → `combine:and:` — two keywords, two args.
//
// Pre-3.0: bails at lower.zig with the Phase 3/4 diagnostic.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
SxProbeMultiKeyword :: #objc_class("SxProbeMultiKeyword") extern {
combine_and :: (self: *Self, a: i32, b: i32) -> i32;
}
combine_imp :: (self: *void, _cmd: *void, a: i32, b: i32) -> i32 abi(.c) {
a * 100 + b
}
main :: () -> i32 {
inline if OS == .macos {
ns_object := objc_getClass("NSObject".ptr);
cls := objc_allocateClassPair(ns_object, "SxProbeMultiKeyword".ptr, 0);
sel := sel_registerName("combine:and:".ptr);
class_addMethod(cls, sel, xx combine_imp, "i@:ii".ptr);
objc_registerClassPair(cls);
inst : *SxProbeMultiKeyword = xx class_createInstance(cls, 0);
n := inst.combine_and(7, 42);
print("combine_and(7, 42) = {}\n", n);
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,25 @@
// Phase 3 step 3.0: keyword count must equal call-site arity (excluding
// self). `something_extra(x)` — name split gives ["something", "extra"]
// = 2 keywords; arity = 1. Compiler must diagnose at the call site.
//
// Pre-3.0: bails at lower.zig with the generic Phase 3/4 diagnostic
// (which subsumes this case). Once 3.0 lands, the diagnostic becomes a
// specific "keyword count mismatch" message.
#import "modules/std.sx";
#import "modules/build.sx";
SxProbeMismatch :: #objc_class("SxProbeMismatch") extern {
something_extra :: (self: *Self, x: i32) -> i32;
}
main :: () -> i32 {
inline if OS == .macos {
inst : *SxProbeMismatch = null;
n := inst.something_extra(7);
print("n = {}\n", n);
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,43 @@
// Phase 3 step 3.1 (PLAN-FFI.md): static call `Cls.class_method(args)`
// on an `#objc_class` alias lowers to `objc_msg_send` against the class
// object (loaded once per module via `objc_getClass` and cached). The
// selector is derived by the same default mangling as Phase 3.0
// (`stringWithUTF8String_(s)` → "stringWithUTF8String:").
//
// Mirrors JNI's static-dispatch surface (`Alias.new(...)` etc.); the
// lowering disambiguates static vs instance by looking at
// `method.is_static` on the runtime-class member.
//
// Uses NSObject because the cached class slot is populated by a
// constructor at module-load — runtime-created test classes wouldn't
// exist yet when `objc_getClass` runs. NSObject is always available
// on macOS via libobjc.
#import "modules/std.sx";
#import "modules/build.sx";
NSObject :: #objc_class("NSObject") extern {
// `+(Class)class` — niladic, name verbatim, selector = "class".
// Returns the class object itself. No `self: *Self` first param ⇒
// class method (sx parser keys on the param TYPE).
class :: () -> *void;
// `+(NSString *)description` on the class returns a description
// string. Niladic, selector = "description".
description :: () -> *void;
}
main :: () -> i32 {
inline if OS == .macos {
c := NSObject.class();
if c != null {
print("class non-null\n");
}
d := NSObject.description();
if d != null {
print("description non-null\n");
}
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,41 @@
// Phase 3 step 3.2 (PLAN-FFI.md): `#selector("explicit:string")`
// override on `#objc_class` members. Escape hatch for cases where the
// sx-side method name doesn't conveniently produce the target selector
// through the default mangling rule (Phase 3.0 — split on `_`, each
// piece becomes a keyword with a trailing `:`).
//
// Surface form mirrors `#jni_method_descriptor("(Sig)Ret")` — sits
// after the optional `-> ReturnType` and before the body / terminator.
//
// Pre-3.2: the parser doesn't know the `#selector` token; snapshot
// captures the parser error (exit=1). Next commit wires lexer + parser
// + AST + lowering and the snapshot flips to working output.
#import "modules/std.sx";
#import "modules/build.sx";
NSObject :: #objc_class("NSObject") extern {
// Default mangling would yield selector "gimme" — NSObject has no
// such IMP. The override pins it to the real selector
// "description". Static method (no `self: *Self` first param).
gimme :: () -> *void #selector("description");
}
// Instance-method override exercises a different lowering path
// (`lowerObjcMethodCall` rather than `lowerObjcStaticCall`). Parse-
// only on this side — main only invokes the static path because we
// don't have a real NSDictionary in scope, but the declaration locks
// in the parser + AST + lowering wiring for the multi-arg shape.
NSDictionary :: #objc_class("NSDictionary") extern {
lookup :: (self: *Self, key: *void) -> *void #selector("objectForKey:");
}
main :: () -> i32 {
inline if OS == .macos {
d := NSObject.gimme();
print("static override non-null: {}\n", d != null);
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,70 @@
// Phase 3 step 3.2 — locked-in golden test for the default Obj-C
// selector mangling rule (Phase 3.0). One fixture covers the common
// shapes (niladic, 1-arg through 4-arg, camelCase across pieces, and
// the `#selector(...)` override). The accompanying `.ir` snapshot
// records each resolved selector string as an `OBJC_METH_VAR_NAME_*`
// constant — a change to `deriveObjcSelector` produces ONE diff that
// surfaces every affected case at once.
//
// Per the rule:
// - Niladic (arity 0): name verbatim. `length` → "length".
// - Arity N (1..): split the sx name on `_`; each piece becomes a
// keyword with a trailing `:`. Piece count must equal arity.
// - `#selector("...")` overrides the mangling entirely; the literal
// string is used as the selector. Arity is the user's contract.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
SxManglingProbe :: #objc_class("SxManglingProbe") extern {
length :: (self: *Self) -> i32;
addObject :: (self: *Self, a: i32) -> i32;
combine_and :: (self: *Self, a: i32, b: i32) -> i32;
insert_after_index :: (self: *Self, a: i32, b: i32, c: i32) -> i32;
add_observer_for_event :: (self: *Self, a: i32, b: i32, c: i32, d: i32) -> i32;
initWithFrame_options :: (self: *Self, f: i32, o: i32) -> i32;
custom_name :: (self: *Self) -> i32 #selector("actualSelectorName");
}
universal_imp :: (self: *void, _cmd: *void, a: i32, b: i32, c: i32, d: i32) -> i32 abi(.c) {
// Returns the arg count's witness; the test doesn't check return
// values, only that dispatch succeeds for each selector shape.
a + b + c + d
}
main :: () -> i32 {
inline if OS == .macos {
ns_object := objc_getClass("NSObject".ptr);
cls := objc_allocateClassPair(ns_object, "SxManglingProbe".ptr, 0);
// Register one IMP per selector we'll dispatch to.
class_addMethod(cls, sel_registerName("length".ptr), xx universal_imp, "i@:".ptr);
class_addMethod(cls, sel_registerName("addObject:".ptr), xx universal_imp, "i@:i".ptr);
class_addMethod(cls, sel_registerName("combine:and:".ptr), xx universal_imp, "i@:ii".ptr);
class_addMethod(cls, sel_registerName("insert:after:index:".ptr), xx universal_imp, "i@:iii".ptr);
class_addMethod(cls, sel_registerName("add:observer:for:event:".ptr), xx universal_imp, "i@:iiii".ptr);
class_addMethod(cls, sel_registerName("initWithFrame:options:".ptr), xx universal_imp, "i@:ii".ptr);
class_addMethod(cls, sel_registerName("actualSelectorName".ptr), xx universal_imp, "i@:".ptr);
objc_registerClassPair(cls);
inst : *SxManglingProbe = xx class_createInstance(cls, 0);
// One call per mangling shape; the IR snapshot pins what
// selector string each sx name resolves to.
_ = inst.length();
_ = inst.addObject(1);
_ = inst.combine_and(1, 2);
_ = inst.insert_after_index(1, 2, 3);
_ = inst.add_observer_for_event(1, 2, 3, 4);
_ = inst.initWithFrame_options(1, 2);
_ = inst.custom_name();
print("mangling table OK\n");
}
inline if OS != .macos {
print("skipped (not macos)\n");
}
0
}

View File

@@ -0,0 +1,28 @@
// Phase 3.0 (FFI-linkage) — postfix `extern` on an aggregate (`#objc_class`)
// is the new spelling of the legacy prefix `#objc_class(…) extern` import.
// Mirrors 1306's runtime-class chained dispatch with the new syntax:
// Name :: #objc_class("X") extern { … } == Name :: #objc_class(…) extern("X") { … }
//
// Red until 3.1 wires the postfix-extern aggregate path through the parser
// + lowering (maps `extern` → reference, same as `extern`).
#import "modules/std.sx";
#import "modules/build.sx";
NSObject :: #objc_class("NSObject") extern {
alloc :: () -> *NSObject;
init :: (self: *Self) -> *Self;
}
main :: () -> i32 {
inline if OS == .macos {
a := NSObject.alloc().init();
if a != null {
print("extern-class dispatch ok\n");
}
}
inline if OS != .macos {
print("extern-class dispatch ok\n");
}
0
}

View File

@@ -0,0 +1,41 @@
// Phase 3 (FFI-linkage) — postfix `export` on an `#objc_class` aggregate, the
// explicit spelling for an sx-DEFINED runtime class (define + register). It is
// the same lowering as a bare `#objc_class("X") { … }` with no `extern`;
// `export` just makes the "I define this class" intent explicit (the dual of
// `extern` for "I reference an existing class"). Mirrors 1339's defined class.
#import "modules/std.sx";
#import "modules/build.sx";
#import "modules/ffi/objc.sx";
SxBar :: #objc_class("SxBar") export {
counter: i32;
alloc :: () -> *SxBar;
bump :: (self: *Self) {
self.counter += 1;
}
get :: (self: *Self) -> i32 {
return self.counter;
}
}
main :: () -> i32 {
inline if OS == .macos {
b := SxBar.alloc();
if b == null { print("FAIL: alloc returned null\n"); return 1; }
b.bump();
b.bump();
print("counter: {}\n", b.get()); // expected: 2
sel_release : SEL = sel_registerName("release".ptr);
release_fn : (obj: *void, sel: *void) -> void abi(.c) = xx objc_msgSend;
release_fn(xx b, sel_release);
}
inline if OS != .macos {
print("counter: 2\n");
}
0
}

View File

@@ -0,0 +1 @@
209

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
42

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
noop block ran

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
x + y = 142

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
block multi-arg ok: sum=42

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
inline block, x = 7

View File

@@ -0,0 +1,2 @@
explicit-then-self ok
self-then-self ok

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1 @@
isKindOfClass: 1

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
compiled

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1 @@
registered: SxFoo

View File

@@ -0,0 +1 @@
ivar: __sx_state

View File

@@ -0,0 +1 @@
IMP: bump ok, add: ok

View File

@@ -0,0 +1 @@
alloc: ok, state bound

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
dealloc: ok

View File

@@ -0,0 +1 @@
class accessor: ok

Some files were not shown because too many files have changed in this diff Show More