diff --git a/examples/ffi-objc-arc-00-allocator-thread.sx b/examples/ffi-objc-arc-00-allocator-thread.sx new file mode 100644 index 0000000..4a6346a --- /dev/null +++ b/examples/ffi-objc-arc-00-allocator-thread.sx @@ -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(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/allocators.sx"; +#import "modules/std/objc.sx"; +#import "modules/compiler.sx"; + +SxAllocProbe :: #objc_class("SxAllocProbe") { + #extends NSObject; + counter: s32; + alloc :: () -> *SxAllocProbe; +} + +main :: () -> s32 { + 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; +} diff --git a/examples/ffi-objc-arc-00b-multi-instance.sx b/examples/ffi-objc-arc-00b-multi-instance.sx new file mode 100644 index 0000000..6f87d7a --- /dev/null +++ b/examples/ffi-objc-arc-00b-multi-instance.sx @@ -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/allocators.sx"; +#import "modules/std/objc.sx"; +#import "modules/compiler.sx"; + +SxMultiProbe :: #objc_class("SxMultiProbe") { + #extends NSObject; + a: s32; + b: s32; + alloc :: () -> *SxMultiProbe; +} + +main :: () -> s32 { + 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; +} diff --git a/tests/expected/ffi-objc-arc-00-allocator-thread.exit b/tests/expected/ffi-objc-arc-00-allocator-thread.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/ffi-objc-arc-00-allocator-thread.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/ffi-objc-arc-00-allocator-thread.txt b/tests/expected/ffi-objc-arc-00-allocator-thread.txt new file mode 100644 index 0000000..41718ef --- /dev/null +++ b/tests/expected/ffi-objc-arc-00-allocator-thread.txt @@ -0,0 +1 @@ +allocator round-trip: ok diff --git a/tests/expected/ffi-objc-arc-00b-multi-instance.exit b/tests/expected/ffi-objc-arc-00b-multi-instance.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/tests/expected/ffi-objc-arc-00b-multi-instance.exit @@ -0,0 +1 @@ +0 diff --git a/tests/expected/ffi-objc-arc-00b-multi-instance.txt b/tests/expected/ffi-objc-arc-00b-multi-instance.txt new file mode 100644 index 0000000..3378066 --- /dev/null +++ b/tests/expected/ffi-objc-arc-00b-multi-instance.txt @@ -0,0 +1 @@ +multi-instance round-trip: ok