diff --git a/tests/unit/test_drop_in_place.jam b/tests/unit/test_drop_in_place.jam new file mode 100644 index 0000000..cac66c1 --- /dev/null +++ b/tests/unit/test_drop_in_place.jam @@ -0,0 +1,106 @@ +// Tests for the `@dropInPlace(ptr)` compiler intrinsic — the analog of +// Rust's `core::ptr::drop_in_place::(*mut T)`. The intrinsic +// synthesizes the drop sequence for T at the call site: +// - If T has a registered `cfn drop`, emit a Call to T.drop(ptr). +// - If T is a struct with droppable fields, recurse into each field. +// - Otherwise (primitive, no-drop type), emit nothing. +// +// We verify by side-effect: a `Sentinel` struct's `cfn drop` bumps a +// counter visible through a borrowed pointer. After `@dropInPlace` runs +// on a constructed value, the counter increments by exactly one per +// expected drop invocation. + +const { assert } = import("test"); +const { Vec } = import("std/collections"); +const { Box } = import("std/box"); + +// Sentinel — its cfn drop increments the u32 at `counter`. Used to +// instrument tests so we can read drop firing as a number. +const Sentinel = struct { + counter: *mut[] u32, + + cfn drop(self: mut Self) { + self.counter[0] = self.counter[0] + 1; + } +}; + +// Holder — a struct with a Sentinel field but no drop of its own. Used +// to test that @dropInPlace recurses into struct fields and fires their +// drops. +const Holder = struct { + s: Sentinel, +}; + +// Two-field holder — verifies declaration-order traversal and that +// non-droppable fields (u32) are skipped without affecting the others. +const TwoHolder = struct { + first: Sentinel, + pad: u32, + second: Sentinel, +}; + +// Heap-counter helper: a one-slot u32 buffer used by all tests to +// observe drop firings. We allocate via Vec(u32).filled(0, 1) so the +// counter lives until the surrounding tfn returns (Vec auto-drops via +// the cfn drop the drop_registry tracks for stack-local `var` bindings). +fn newCounter() Vec(u32) { + return Vec(u32).filled(0, 1); +} + +// Direct invocation: `@dropInPlace(&s)` on a struct with its own cfn drop +// should fire that drop exactly once. +tfn directDrop() { + var counter: Vec(u32) = newCounter(); + var s: Sentinel = Sentinel { counter: counter.ptr }; + @dropInPlace(&s); + assert(counter[0], 1); +} + +// `@dropInPlace(&h)` on a Holder (no own drop) should recurse into the +// Sentinel field and fire its drop. +tfn structFieldDrop() { + var counter: Vec(u32) = newCounter(); + var h: Holder = Holder { s: Sentinel { counter: counter.ptr } }; + @dropInPlace(&h); + assert(counter[0], 1); +} + +// Two Sentinel fields with a non-drop u32 in between — verifies both +// fire and the u32 is correctly skipped. +tfn twoFieldsDrop() { + var counter: Vec(u32) = newCounter(); + var h: TwoHolder = TwoHolder { + first: Sentinel { counter: counter.ptr }, + pad: 42, + second: Sentinel { counter: counter.ptr }, + }; + @dropInPlace(&h); + assert(counter[0], 2); +} + +// Repeated invocation on the same pointer: drop is a side-effect, so +// calling @dropInPlace twice fires drop twice. (Caller is responsible +// for not invoking @dropInPlace twice on real owning code — this just +// confirms the intrinsic emits one call per invocation, no caching.) +tfn repeatedInvocation() { + var counter: Vec(u32) = newCounter(); + var s: Sentinel = Sentinel { counter: counter.ptr }; + @dropInPlace(&s); + @dropInPlace(&s); + assert(counter[0], 2); +} + +// Pointer through a raw `*mut[] Sentinel` — fires drop on the indexed +// element exactly once. We go through `v.ptr` rather than `&v[0]` +// because Vec's `cfn at` is value-shaped, so `&v[0]` doesn't yield an +// lvalue. `v.ptr[i]` IS lvalue-indexable. +tfn vecElementDrop() { + var counter: Vec(u32) = newCounter(); + var v: Vec(Sentinel) = Vec(Sentinel).withCapacity(2); + v.push(Sentinel { counter: counter.ptr }); + v.push(Sentinel { counter: counter.ptr }); + @dropInPlace(&v.ptr[0]); + assert(counter[0], 1); + @dropInPlace(&v.ptr[1]); + assert(counter[0], 2); +} diff --git a/tests/unit/test_return_move.jam b/tests/unit/test_return_move.jam new file mode 100644 index 0000000..c396093 --- /dev/null +++ b/tests/unit/test_return_move.jam @@ -0,0 +1,152 @@ +// Tests for return-as-move semantics on drop-bearing types. +// +// When a function has `var v: T = ...; return v;` where T has a `cfn drop`, +// the local `v`'s drop is SUPPRESSED at function exit — ownership has +// been transferred to the caller's binding. Without this, every helper +// that constructs a drop-bearing value via a named local and returns it +// would double-free: the local drops at function exit, then the caller's +// binding holds the same (now-freed) pointer. +// +// Verification mirrors test_drop_in_place.jam: a Sentinel struct's +// `cfn drop` increments a heap counter, and tests assert the counter +// reaches exactly the expected value (one drop per owning scope). + +const { assert } = import("test"); +const { Vec } = import("std/collections"); + +const Sentinel = struct { + counter: *mut[] u32, + + cfn drop(self: mut Self) { + self.counter[0] = self.counter[0] + 1; + } +}; + +// Baseline: a helper that constructs a Sentinel into a `var` and +// returns the var. With return-move the local's drop is suppressed +// and the caller's binding becomes the sole owner. +fn makeSentinel(c: *mut[] u32) Sentinel { + var s: Sentinel = Sentinel { counter: c }; + return s; +} + +// Helper variant that returns the result of another call — no `var` +// binding, no return-move analysis needed. Confirms the simple +// "return temporary" path isn't perturbed. +fn forwardSentinel(c: *mut[] u32) Sentinel { + return makeSentinel(c); +} + +// Conditional return: each path's analysis should be independent. +// Both branches return the same tracked var; both should suppress +// its drop. The other-branch flow doesn't matter because only one +// path executes. +fn conditionalReturn(c: *mut[] u32, take: u8) Sentinel { + var s: Sentinel = Sentinel { counter: c }; + if (take != 0) { + return s; + } + return s; +} + +// Early-return path doesn't return the local — local should still +// drop normally on the early-return because no move analysis fires. +// The late-return DOES move the local out. +fn earlyOther(c: *mut[] u32, takeEarly: u8) Sentinel { + var s: Sentinel = Sentinel { counter: c }; + if (takeEarly != 0) { + // Return a freshly-constructed Sentinel; `s` is not the + // returned expression, so its drop should fire here. + return Sentinel { counter: c }; + } + return s; +} + +// Helper: owns the returned Sentinel for one scope so we can observe +// its drop firing on this fn's exit. Returning into a `var` here AND +// from the tfn would otherwise leak the drop firing into the test's +// scope and the assertions would run before the drops landed. +fn dropAfterMakeSentinel(c: *mut[] u32) { + var got: Sentinel = makeSentinel(c); + // No assertion inside this fn (the counter is only readable in + // the caller's scope), but the fn's exit fires got.drop exactly + // once — caller can then observe the count. +} + +fn dropAfterForwardSentinel(c: *mut[] u32) { + var got: Sentinel = forwardSentinel(c); +} + +fn dropAfterConditional(c: *mut[] u32, take: u8) { + var got: Sentinel = conditionalReturn(c, take); +} + +fn dropAfterEarlyOther(c: *mut[] u32, takeEarly: u8) { + var got: Sentinel = earlyOther(c, takeEarly); +} + +// Heap counter helper — single-slot Vec(u32) so it lives until the +// surrounding tfn returns. Vec auto-drops the backing on tfn exit. +fn newCounter() Vec(u32) { + return Vec(u32).filled(0, 1); +} + +// Single return-move: the local doesn't fire its drop; the caller's +// binding does on its scope's exit. Net count = 1. +tfn returnMoveBasic() { + var c: Vec(u32) = newCounter(); + dropAfterMakeSentinel(c.ptr); + assert(c[0], 1); +} + +// Two-step return-through-helper: the inner helper moves its var to +// the outer helper's return path, which moves it to the caller. One +// net drop. +tfn returnMoveChained() { + var c: Vec(u32) = newCounter(); + dropAfterForwardSentinel(c.ptr); + assert(c[0], 1); +} + +// Conditional-return (both branches return the same local): either +// path suppresses the local's drop. Caller's binding is sole owner. +// One net drop. +tfn returnMoveConditional() { + var c: Vec(u32) = newCounter(); + dropAfterConditional(c.ptr, 1); + assert(c[0], 1); +} + +// Early-return path through earlyOther returns a fresh Sentinel; the +// local `s` is NOT the returned expression so its drop must fire +// before the early return. Two net drops total (local + caller's). +tfn returnMoveEarlyOther() { + var c: Vec(u32) = newCounter(); + dropAfterEarlyOther(c.ptr, 1); + assert(c[0], 2); +} + +// Late-return path through earlyOther moves the local; only the +// caller's binding fires drop. One net drop. +tfn returnMoveLatePath() { + var c: Vec(u32) = newCounter(); + dropAfterEarlyOther(c.ptr, 0); + assert(c[0], 1); +} + +// Negative: a helper that returns a NEW value (not a tracked local). +// No tracked local exists in this helper, so there's nothing to move. +// Caller owns the result. One net drop. +fn makeFresh(c: *mut[] u32) Sentinel { + return Sentinel { counter: c }; +} + +fn dropAfterMakeFresh(c: *mut[] u32) { + var got: Sentinel = makeFresh(c); +} + +tfn returnFreshNoMove() { + var c: Vec(u32) = newCounter(); + dropAfterMakeFresh(c.ptr); + assert(c[0], 1); +}