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:
28
examples/ffi-objc/1300-ffi-objc-roundtrip.sx
Normal file
28
examples/ffi-objc/1300-ffi-objc-roundtrip.sx
Normal 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
|
||||
}
|
||||
41
examples/ffi-objc/1301-ffi-objc-class.sx
Normal file
41
examples/ffi-objc/1301-ffi-objc-class.sx
Normal 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
|
||||
}
|
||||
17
examples/ffi-objc/1302-ffi-objc-block-noop.sx
Normal file
17
examples/ffi-objc/1302-ffi-objc-block-noop.sx
Normal 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
|
||||
}
|
||||
17
examples/ffi-objc/1303-ffi-objc-block-capture.sx
Normal file
17
examples/ffi-objc/1303-ffi-objc-block-capture.sx
Normal 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
|
||||
}
|
||||
61
examples/ffi-objc/1304-ffi-objc-block-multi-arg.sx
Normal file
61
examples/ffi-objc/1304-ffi-objc-block-multi-arg.sx
Normal 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
|
||||
}
|
||||
19
examples/ffi-objc/1305-ffi-objc-block-inline.sx
Normal file
19
examples/ffi-objc/1305-ffi-objc-block-inline.sx
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
47
examples/ffi-objc/1308-ffi-objc-type-aliases.sx
Normal file
47
examples/ffi-objc/1308-ffi-objc-type-aliases.sx
Normal 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
|
||||
}
|
||||
42
examples/ffi-objc/1309-ffi-objc-class-method-lowering.sx
Normal file
42
examples/ffi-objc/1309-ffi-objc-class-method-lowering.sx
Normal 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
|
||||
}
|
||||
43
examples/ffi-objc/1310-ffi-objc-class-registration.sx
Normal file
43
examples/ffi-objc/1310-ffi-objc-class-registration.sx
Normal 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
|
||||
}
|
||||
51
examples/ffi-objc/1311-ffi-objc-class-ivar-registration.sx
Normal file
51
examples/ffi-objc/1311-ffi-objc-class-ivar-registration.sx
Normal 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
|
||||
}
|
||||
51
examples/ffi-objc/1312-ffi-objc-class-method-dispatch.sx
Normal file
51
examples/ffi-objc/1312-ffi-objc-class-method-dispatch.sx
Normal 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
|
||||
}
|
||||
54
examples/ffi-objc/1313-ffi-objc-class-alloc-roundtrip.sx
Normal file
54
examples/ffi-objc/1313-ffi-objc-class-alloc-roundtrip.sx
Normal 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
|
||||
}
|
||||
61
examples/ffi-objc/1314-ffi-objc-class-dealloc-roundtrip.sx
Normal file
61
examples/ffi-objc/1314-ffi-objc-class-dealloc-roundtrip.sx
Normal 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
|
||||
}
|
||||
48
examples/ffi-objc/1315-ffi-objc-self-class-accessor.sx
Normal file
48
examples/ffi-objc/1315-ffi-objc-self-class-accessor.sx
Normal 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
|
||||
}
|
||||
48
examples/ffi-objc/1316-ffi-objc-class-method-static-imp.sx
Normal file
48
examples/ffi-objc/1316-ffi-objc-class-method-static-imp.sx
Normal 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
|
||||
}
|
||||
57
examples/ffi-objc/1317-ffi-objc-class-level-constant.sx
Normal file
57
examples/ffi-objc/1317-ffi-objc-class-level-constant.sx
Normal 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
|
||||
}
|
||||
67
examples/ffi-objc/1318-ffi-objc-property-extern-class.sx
Normal file
67
examples/ffi-objc/1318-ffi-objc-property-extern-class.sx
Normal 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
|
||||
}
|
||||
72
examples/ffi-objc/1319-ffi-objc-property-sx-defined.sx
Normal file
72
examples/ffi-objc/1319-ffi-objc-property-sx-defined.sx
Normal file
@@ -0,0 +1,72 @@
|
||||
// M2.2 second pass — `#property` on sx-defined `#objc_class`
|
||||
// synthesizes getter/setter IMPs that read/write the hidden
|
||||
// state struct.
|
||||
//
|
||||
// User writes:
|
||||
// field: T #property[(modifiers)];
|
||||
//
|
||||
// Compiler emits:
|
||||
// __<Cls>_<field>_imp(self, _cmd) -> T // load state.field
|
||||
// __<Cls>_set<Field>_imp(self, _cmd, v) -> void // store state.field
|
||||
// (setter skipped for `readonly`.)
|
||||
//
|
||||
// Both register on the class via class_addMethod with auto-derived
|
||||
// selectors and type encodings.
|
||||
//
|
||||
// Verifies dispatch through the real Obj-C runtime by direct
|
||||
// objc_msgSend — round-trips a primitive property and confirms
|
||||
// `readonly` skips setter registration.
|
||||
|
||||
#import "modules/std.sx";
|
||||
#import "modules/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
|
||||
}
|
||||
62
examples/ffi-objc/1320-ffi-objc-extends-chain.sx
Normal file
62
examples/ffi-objc/1320-ffi-objc-extends-chain.sx
Normal 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
|
||||
}
|
||||
108
examples/ffi-objc/1321-ffi-objc-defined-class-method-self.sx
Normal file
108
examples/ffi-objc/1321-ffi-objc-defined-class-method-self.sx
Normal 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
|
||||
}
|
||||
87
examples/ffi-objc/1322-ffi-objc-arc-00-allocator-thread.sx
Normal file
87
examples/ffi-objc/1322-ffi-objc-arc-00-allocator-thread.sx
Normal 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
|
||||
}
|
||||
74
examples/ffi-objc/1323-ffi-objc-arc-00b-multi-instance.sx
Normal file
74
examples/ffi-objc/1323-ffi-objc-arc-00b-multi-instance.sx
Normal 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
|
||||
}
|
||||
50
examples/ffi-objc/1324-ffi-objc-arc-01-autoreleasepool.sx
Normal file
50
examples/ffi-objc/1324-ffi-objc-arc-01-autoreleasepool.sx
Normal 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
|
||||
}
|
||||
78
examples/ffi-objc/1325-ffi-objc-arc-02-strong-property.sx
Normal file
78
examples/ffi-objc/1325-ffi-objc-arc-02-strong-property.sx
Normal 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
|
||||
}
|
||||
66
examples/ffi-objc/1326-ffi-objc-arc-03-weak-property.sx
Normal file
66
examples/ffi-objc/1326-ffi-objc-arc-03-weak-property.sx
Normal 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
|
||||
}
|
||||
25
examples/ffi-objc/1327-ffi-objc-call-01-parse.sx
Normal file
25
examples/ffi-objc/1327-ffi-objc-call-01-parse.sx
Normal 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
|
||||
}
|
||||
25
examples/ffi-objc/1328-ffi-objc-call-02-void-return.sx
Normal file
25
examples/ffi-objc/1328-ffi-objc-call-02-void-return.sx
Normal 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
|
||||
}
|
||||
42
examples/ffi-objc/1329-ffi-objc-call-03-selector-sharing.sx
Normal file
42
examples/ffi-objc/1329-ffi-objc-call-03-selector-sharing.sx
Normal 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
|
||||
}
|
||||
41
examples/ffi-objc/1330-ffi-objc-call-04-primitive-returns.sx
Normal file
41
examples/ffi-objc/1330-ffi-objc-call-04-primitive-returns.sx
Normal 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
|
||||
}
|
||||
51
examples/ffi-objc/1331-ffi-objc-call-05-struct-returns.sx
Normal file
51
examples/ffi-objc/1331-ffi-objc-call-05-struct-returns.sx
Normal 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
|
||||
}
|
||||
52
examples/ffi-objc/1332-ffi-objc-call-06-sret-return.sx
Normal file
52
examples/ffi-objc/1332-ffi-objc-call-06-sret-return.sx
Normal 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
|
||||
}
|
||||
46
examples/ffi-objc/1333-ffi-objc-call-07-fp-hfa-return.sx
Normal file
46
examples/ffi-objc/1333-ffi-objc-call-07-fp-hfa-return.sx
Normal 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
|
||||
}
|
||||
41
examples/ffi-objc/1334-ffi-objc-call-08-multi-keyword.sx
Normal file
41
examples/ffi-objc/1334-ffi-objc-call-08-multi-keyword.sx
Normal 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
|
||||
}
|
||||
80
examples/ffi-objc/1335-ffi-objc-call-09-in-construct.sx
Normal file
80
examples/ffi-objc/1335-ffi-objc-call-09-in-construct.sx
Normal file
@@ -0,0 +1,80 @@
|
||||
// Phase 1 steps 1.11–1.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
|
||||
}
|
||||
39
examples/ffi-objc/1336-ffi-objc-call-10-os-gate.sx
Normal file
39
examples/ffi-objc/1336-ffi-objc-call-10-os-gate.sx
Normal 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
|
||||
}
|
||||
48
examples/ffi-objc/1337-ffi-objc-call-11-bool-return.sx
Normal file
48
examples/ffi-objc/1337-ffi-objc-call-11-bool-return.sx
Normal 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
|
||||
}
|
||||
61
examples/ffi-objc/1338-ffi-objc-call-12-rect-u64-returns.sx
Normal file
61
examples/ffi-objc/1338-ffi-objc-call-12-rect-u64-returns.sx
Normal 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
|
||||
}
|
||||
61
examples/ffi-objc/1339-ffi-objc-defined-class-01-instance.sx
Normal file
61
examples/ffi-objc/1339-ffi-objc-defined-class-01-instance.sx
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
43
examples/ffi-objc/1341-ffi-objc-dsl-01-niladic.sx
Normal file
43
examples/ffi-objc/1341-ffi-objc-dsl-01-niladic.sx
Normal 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
|
||||
}
|
||||
35
examples/ffi-objc/1342-ffi-objc-dsl-02-one-arg.sx
Normal file
35
examples/ffi-objc/1342-ffi-objc-dsl-02-one-arg.sx
Normal 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
|
||||
}
|
||||
34
examples/ffi-objc/1343-ffi-objc-dsl-03-multi-keyword.sx
Normal file
34
examples/ffi-objc/1343-ffi-objc-dsl-03-multi-keyword.sx
Normal 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
|
||||
}
|
||||
25
examples/ffi-objc/1344-ffi-objc-dsl-04-mismatch.sx
Normal file
25
examples/ffi-objc/1344-ffi-objc-dsl-04-mismatch.sx
Normal 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
|
||||
}
|
||||
43
examples/ffi-objc/1345-ffi-objc-dsl-05-static.sx
Normal file
43
examples/ffi-objc/1345-ffi-objc-dsl-05-static.sx
Normal 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
|
||||
}
|
||||
41
examples/ffi-objc/1346-ffi-objc-dsl-06-selector-override.sx
Normal file
41
examples/ffi-objc/1346-ffi-objc-dsl-06-selector-override.sx
Normal 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
|
||||
}
|
||||
70
examples/ffi-objc/1347-ffi-objc-dsl-07-mangling-table.sx
Normal file
70
examples/ffi-objc/1347-ffi-objc-dsl-07-mangling-table.sx
Normal 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
|
||||
}
|
||||
28
examples/ffi-objc/1348-ffi-objc-extern-class.sx
Normal file
28
examples/ffi-objc/1348-ffi-objc-extern-class.sx
Normal 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
|
||||
}
|
||||
41
examples/ffi-objc/1349-ffi-objc-export-class.sx
Normal file
41
examples/ffi-objc/1349-ffi-objc-export-class.sx
Normal 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
|
||||
}
|
||||
1
examples/ffi-objc/expected/1300-ffi-objc-roundtrip.exit
Normal file
1
examples/ffi-objc/expected/1300-ffi-objc-roundtrip.exit
Normal file
@@ -0,0 +1 @@
|
||||
209
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
1
examples/ffi-objc/expected/1301-ffi-objc-class.exit
Normal file
1
examples/ffi-objc/expected/1301-ffi-objc-class.exit
Normal file
@@ -0,0 +1 @@
|
||||
42
|
||||
1
examples/ffi-objc/expected/1301-ffi-objc-class.stderr
Normal file
1
examples/ffi-objc/expected/1301-ffi-objc-class.stderr
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
examples/ffi-objc/expected/1301-ffi-objc-class.stdout
Normal file
1
examples/ffi-objc/expected/1301-ffi-objc-class.stdout
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
examples/ffi-objc/expected/1302-ffi-objc-block-noop.exit
Normal file
1
examples/ffi-objc/expected/1302-ffi-objc-block-noop.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
noop block ran
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
x + y = 142
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
block multi-arg ok: sum=42
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
inline block, x = 7
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
explicit-then-self ok
|
||||
self-then-self ok
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
isKindOfClass: 1
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
16236
examples/ffi-objc/expected/1309-ffi-objc-class-method-lowering.ir
Normal file
16236
examples/ffi-objc/expected/1309-ffi-objc-class-method-lowering.ir
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
compiled
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
registered: SxFoo
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ivar: __sx_state
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
IMP: bump ok, add: ok
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
alloc: ok, state bound
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
16675
examples/ffi-objc/expected/1314-ffi-objc-class-dealloc-roundtrip.ir
Normal file
16675
examples/ffi-objc/expected/1314-ffi-objc-class-dealloc-roundtrip.ir
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
dealloc: ok
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
class accessor: ok
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user