Skip to main content

shape_jit/ffi/object/
closure.rs

1// Heap allocation audit (PR-9 V8 Gap Closure):
2//   Category A (NaN-boxed returns): 1 site
3//     jit_box(HK_CLOSURE, ...) — jit_make_closure
4//   Category B (intermediate/consumed): 1 site
5//     JITClosure::new() allocates captures via Box — consumed by jit_box
6//   Category C (heap islands): 0 sites
7//!
8//! Closure Creation
9//!
10//! Functions for creating closures with captured values.
11
12use super::super::super::context::{JITClosure, JITContext};
13use crate::ffi::jit_kinds::*;
14use crate::ffi::value_ffi::*;
15
16// ============================================================================
17// Closure Creation
18// ============================================================================
19
20/// Create a closure with captured values from the stack.
21///
22/// Supports unlimited captures via heap-allocated capture array.
23///
24/// # Deprecation (Closure-spec Phase H1/H5)
25///
26/// Phase H1 introduces `MirToIR::emit_heap_closure` which inlines the
27/// allocation + `TypedClosureHeader` init directly in Cranelift IR. Phase H2
28/// makes `emit_heap_closure` the unconditional default for escaping
29/// closures — this FFI is no longer called from that opcode's lowering and
30/// exists only to service the legacy non-layout fallback in the unified
31/// `MakeClosure` opcode. Phase H5 merged `MakeClosureHeap` into
32/// `MakeClosure` (the escape flag now lives in the operand variant
33/// `ClosureAlloc { escapes }`); a follow-up phase can delete this FFI once
34/// all closure functions are guaranteed to have a registered
35/// `ClosureLayout`.
36#[deprecated(
37    note = "Closure-spec Phase H2: `emit_heap_closure` + `jit_finalize_heap_closure` \
38            is now the unconditional path for escaping closures. This FFI remains \
39            only for residual non-layout fallback paths; a follow-up phase deletes it."
40)]
41#[inline(always)]
42pub extern "C" fn jit_make_closure(
43    ctx: *mut JITContext,
44    function_id: u16,
45    captures_count: u16,
46) -> u64 {
47    unsafe {
48        if ctx.is_null() {
49            return box_function(function_id);
50        }
51
52        let ctx_ref = &mut *ctx;
53        let count = captures_count as usize;
54
55        // Check stack bounds
56        if ctx_ref.stack_ptr < count || ctx_ref.stack_ptr > 512 {
57            return box_function(function_id);
58        }
59
60        // Pop captured values from stack
61        let mut captures = Vec::with_capacity(count);
62        for _ in 0..count {
63            ctx_ref.stack_ptr -= 1;
64            captures.push(ctx_ref.stack[ctx_ref.stack_ptr]);
65        }
66        captures.reverse(); // Restore original order
67
68        // Create closure struct with dynamic captures
69        let closure = JITClosure::new(function_id, &captures);
70        unified_box(HK_CLOSURE, *closure)
71    }
72}
73
74// ============================================================================
75// Closure-spec Phase H2: TypedClosureHeader finalizer
76// ============================================================================
77
78/// Closure-spec Phase H2 → §14.6 (H6.5): wrap an H1-allocated
79/// `TypedClosureHeader` block into a NaN-boxed `Arc<HeapValue::ClosureRaw>`
80/// bits value.
81///
82/// Phase H1 (`MirToIR::emit_heap_closure`) allocates the block and writes
83/// captures at their `ClosureLayout::heap_capture_offset(i)` offsets.
84/// Pre-H6.5 this FFI then rebuilt an `Arc<HeapValue::Closure { function_id,
85/// upvalues }>` by copying every capture into a `Vec<Upvalue>` — a hot-path
86/// allocation that dominated `arr.map(|x| x + n)` profiles. H6.5 deletes
87/// that rebuild: the raw block is already the canonical representation of
88/// the closure. We simply hand ownership of the `*const TypedClosureHeader`
89/// (and one refcount share, allocated by `emit_heap_closure`) to a fresh
90/// `OwnedClosureBlock` and wrap it in `HeapValue::ClosureRaw`. Downstream
91/// dispatch paths go through the `VmClosureHandle` shim, which transparently
92/// reads captures out of the raw block via `read_capture_as_value_bits`.
93///
94/// The `function_id` and `captures_count` FFI arguments are kept for the
95/// Cranelift-level signature stability — the authoritative values live in
96/// the block's header (`function_id` at offset 8) and the layout
97/// (`capture_count()`). The function asserts the two agree in debug builds.
98///
99/// # Safety
100///
101/// - `header_ptr` must be a live `TypedClosureHeader` block allocated by
102///   `jit_v2_alloc_struct` with `kind = HEAP_KIND_V2_CLOSURE` and a capture
103///   area matching the `layout_ptr` argument.
104/// - `layout_ptr` must point to a live `ClosureLayout` whose lifetime
105///   dominates this call. Programs own `Arc<ClosureLayout>`s in
106///   `BytecodeProgram.closure_function_layouts`; `emit_heap_closure`
107///   materialises the raw address via `Arc::as_ptr`, so we reconstruct the
108///   Arc below with `Arc::increment_strong_count` + `Arc::from_raw` to
109///   acquire a counted share for the new `OwnedClosureBlock`.
110/// - `captures_count` must equal `(*layout_ptr).capture_count()`.
111/// - This function takes ownership of the `TypedClosureHeader` block: the
112///   caller must not release the raw pointer after the call.
113/// - Heap-typed captures (`heap_capture_mask` bits) in the block own one
114///   refcount share apiece (emit_heap_closure emits `atomic_rmw add … 1`
115///   for each). Those shares stay with the block and release automatically
116///   via `release_typed_closure` when `OwnedClosureBlock::Drop` runs.
117#[unsafe(no_mangle)]
118pub unsafe extern "C" fn jit_finalize_heap_closure(
119    header_ptr: *mut u8,
120    _function_id: u32,
121    captures_count: u32,
122    layout_ptr: *const shape_value::v2::closure_layout::ClosureLayout,
123) -> u64 {
124    use shape_value::heap_value::HeapValue;
125    use shape_value::v2::closure_layout::ClosureLayout;
126    use shape_value::v2::closure_raw::OwnedClosureBlock;
127    use std::sync::Arc;
128
129    unsafe {
130        if header_ptr.is_null() || layout_ptr.is_null() {
131            // Safety valve: refuse to construct an invalid closure. Per
132            // ADR-006 §2.7.5 the JIT-FFI carries raw `u64` plus a parallel
133            // `NativeKind` companion stamped at JIT compile time from the
134            // call signature; the kind for this entry-point is
135            // `NativeKind::Ptr(HeapKind::Closure)` and a null payload (raw
136            // 0u64) is the carrier-level miss. Callers must not deref the
137            // return as a function — this is a codegen bug if it ever fires.
138            return 0u64;
139        }
140
141        let layout_ref: &ClosureLayout = &*layout_ptr;
142        let count = captures_count as usize;
143        debug_assert_eq!(
144            count,
145            layout_ref.capture_count(),
146            "jit_finalize_heap_closure: captures_count {} != layout.capture_count() {}",
147            count,
148            layout_ref.capture_count()
149        );
150        let _ = count; // kept for the assert in release builds
151
152        // Acquire a counted share of the `Arc<ClosureLayout>` so the owning
153        // block keeps the layout alive on its own. `emit_heap_closure`
154        // passed in `Arc::as_ptr(&layout)` which is a raw pointer into a
155        // program-lifetime Arc; we bump its refcount once, then reconstruct
156        // the share via `Arc::from_raw` (matching `increment_strong_count`
157        // pairs with exactly one `Arc::from_raw` drop).
158        Arc::increment_strong_count(layout_ptr);
159        let layout_arc: Arc<ClosureLayout> = Arc::from_raw(layout_ptr);
160
161        // SAFETY: `header_ptr` was freshly-allocated with refcount=1 by
162        // `emit_heap_closure`; that share transfers to the new
163        // `OwnedClosureBlock` (its Drop calls `release_typed_closure`). Heap
164        // captures retain their own shares as emitted by H1's
165        // `atomic_rmw add 1` loop — those stay with the block.
166        let owned = OwnedClosureBlock::from_raw(header_ptr as *const u8, layout_arc);
167
168        // Wrap in the H6.5 `HeapValue::ClosureRaw` variant. Per ADR-006
169        // §2.7.5 / W7 closure-share carrier audit (commit `5fa4b19`,
170        // 2026-05-09): closure share carrier is `Arc<HeapValue>`, returned
171        // here as raw `Arc::into_raw(Arc::new(HeapValue::ClosureRaw(owned)))
172        // as u64`. The companion `NativeKind::Ptr(HeapKind::Closure)` is
173        // stamped at the JIT call signature; the runtime-tier
174        // `clone_with_kind` / `drop_with_kind` dispatch tables retain /
175        // release `Arc<HeapValue>` per W7-closure-retain.
176        Arc::into_raw(Arc::new(HeapValue::ClosureRaw(owned))) as u64
177    }
178}
179
180// ============================================================================
181// Per-NativeKind::Ptr(HeapKind::Closure) kinded retain / release
182// ============================================================================
183//
184// W15.2-LANG-4 jit-filter-predicate close (2026-05-18). The closure
185// callee slot's strict-typed carrier is `Arc::into_raw(Arc<HeapValue::
186// ClosureRaw>) as u64` per `jit_finalize_heap_closure` above and per
187// ADR-006 §2.7.11 / Q12. Refcount discipline mirrors the VM-side
188// `clone_with_kind` / `drop_with_kind` `HeapKind::Closure` arms in
189// `crates/shape-vm/src/executor/vm_impl/stack.rs:351 / :697` —
190// `Arc::increment_strong_count::<HeapValue>` retain,
191// `Arc::decrement_strong_count::<HeapValue>` release.
192//
193// The legacy `jit_arc_retain` / `jit_arc_release` operate on the W11
194// `UnifiedValue<T>` HeapHeader refcount at offset 4, which would
195// scribble on the inner `HeapValue` payload of an
196// `Arc::into_raw(Arc<HeapValue>)` carrier (whose refcount lives at
197// offset -16 per Rust Arc contract). Same defection-shape Round 7A's
198// `arc_result_retain` / `arc_option_retain` resolved at the
199// Result/Option Arc-carrier site, and Round 12 T2/T3's
200// `arc_string_retain` resolved at the String Arc-carrier site.
201
202/// Retain (clone) an `Arc<HeapValue>` strong-count share for a
203/// `NativeKind::Ptr(HeapKind::Closure)` slot. Bumps the standard Rust
204/// Arc refcount at offset -16 of the `Arc::into_raw` pointer.
205///
206/// SAFETY: `bits` must be `Arc::into_raw(Arc<HeapValue>) as u64` whose
207/// payload is the `HeapValue::ClosureRaw(OwnedClosureBlock)` variant
208/// (the `jit_finalize_heap_closure` return shape and the runtime-tier
209/// `KindedSlot { kind: Ptr(HeapKind::Closure), .. }` carrier). Null
210/// (raw 0u64) is silently no-op'd (mirror of the `String` / `Result` /
211/// `Option` Arc-carrier null-bits safety convention).
212#[unsafe(no_mangle)]
213pub extern "C" fn jit_arc_closure_retain(bits: u64) {
214    use shape_value::heap_value::HeapValue;
215    use std::sync::Arc;
216
217    if bits == 0 {
218        return;
219    }
220    // W15.2-LANG-4 dual-carrier dispatch (2026-05-18). The
221    // `NativeKind::Ptr(HeapKind::Closure)` slot's bit-shape is dual at
222    // the JIT carrier tier per the §2.7.5 closure-zero-captures
223    // optimization paths in the bytecode/JIT closure emit:
224    //
225    //   (a) `Arc::into_raw(Arc<HeapValue::ClosureRaw(block)>)` — the
226    //       canonical §2.7.11/Q12 heap-closure shape returned by
227    //       `jit_finalize_heap_closure` (escaping closure with or
228    //       without captures via `emit_heap_closure`).
229    //
230    //   (b) `box_function(fn_id)` — the JIT-internal NaN-box
231    //       function-ref shape emitted when the closure compiler
232    //       optimizes a no-capture, non-escaping closure to a bare
233    //       function reference. Tag bits in the high half of the u64
234    //       mark this shape per `value_ffi.rs::TAG_FUNCTION_BITS`.
235    //
236    // The slot's declared type is `(args) -> ret` / `Function(_)` per
237    // `concrete_type_from_annotation`; both runtime shapes share the
238    // same semantic type. Refcount discipline differs: (a) bumps the
239    // `Arc<HeapValue>` strong count at offset -16; (b) is a no-op (a
240    // bare function reference has no heap state). Mirror of the
241    // §2.7.11/Q12 `jit_call_value` dispatch shell which already
242    // discriminates on the same bit-shape predicate before any deref.
243    if crate::ffi::value_ffi::is_inline_function(bits) {
244        // No-op: bare function reference has no heap state to retain.
245        return;
246    }
247    // SAFETY: per the §2.7.11/Q12 Closure carrier contract the
248    // remaining bits shape is `Arc::into_raw(Arc<HeapValue>) as u64`
249    // (post-`jit_finalize_heap_closure`); `Arc::increment_strong_count
250    // ::<HeapValue>` operates on the Arc control block at offset -16
251    // — identical to the runtime-tier `HeapKind::Closure` arm in
252    // `executor/vm_impl/stack.rs:352`.
253    unsafe {
254        Arc::increment_strong_count(bits as *const HeapValue);
255    }
256}
257
258/// Release an `Arc<HeapValue>` strong-count share for a
259/// `NativeKind::Ptr(HeapKind::Closure)` slot. Mirror of
260/// `jit_arc_closure_retain` — uses
261/// `Arc::decrement_strong_count::<HeapValue>` per Rust Arc contract.
262/// Reaching refcount zero runs `HeapValue::Drop` (which dispatches the
263/// `ClosureRaw` arm and retires the `OwnedClosureBlock`'s typed-closure
264/// header refcount via the block's own `Drop`).
265///
266/// SAFETY: same as `jit_arc_closure_retain`. Null is silently no-op'd.
267#[unsafe(no_mangle)]
268pub extern "C" fn jit_arc_closure_release(bits: u64) {
269    use shape_value::heap_value::HeapValue;
270    use std::sync::Arc;
271
272    if bits == 0 {
273        return;
274    }
275    // W15.2-LANG-4 dual-carrier dispatch (2026-05-18). Mirror of
276    // `jit_arc_closure_retain` — see that fn's docstring for the
277    // dual-shape (Arc<HeapValue> vs `box_function(fn_id)` NaN-box)
278    // rationale. Bare function reference has no heap state; only the
279    // Arc-shape requires decrement.
280    if crate::ffi::value_ffi::is_inline_function(bits) {
281        return;
282    }
283    // SAFETY: see fn docs. Mirror of `executor/vm_impl/stack.rs:697`
284    // `HeapKind::Closure` arm in `drop_with_kind`.
285    unsafe {
286        Arc::decrement_strong_count(bits as *const HeapValue);
287    }
288}
289
290// ============================================================================
291// Track A.1D: OwnedMutable capture cell allocator
292// ============================================================================
293
294/// Allocate a heap cell for an `OwnedMutable` closure capture.
295///
296/// The closure's capture slot for a `CaptureKind::OwnedMutable` capture must
297/// hold a `*mut ValueWord` pointer — a raw Box allocation that the closure
298/// exclusively owns. `op_make_closure` (interpreter) and
299/// `MirToIR::emit_heap_closure` (JIT) both call this shim to materialise a
300/// fresh cell from the capture's initial `ValueWord` bits.
301///
302/// Rust's `Box` has a stable layout for `Sized` types under the current
303/// allocator and uses the system allocator for `u64`-sized allocations, so
304/// the pointer returned here can be reclaimed via `Box::from_raw` —
305/// `release_typed_closure` (A.1A) does exactly that for every bit set in
306/// `ClosureLayout::owned_mutable_capture_mask`.
307///
308/// # Safety invariants
309///
310/// - This function is the **sole** allocator for OwnedMutable cells. The
311///   pointer it returns is owned by the closure block it gets installed
312///   into; the block releases it via `Box::from_raw` when the closure's
313///   refcount hits zero (see `release_typed_closure` in
314///   `shape-value/src/v2/closure_raw.rs`).
315/// - The caller (JIT codegen or the interpreter's `op_make_closure`) must
316///   write the returned pointer into the capture's `Ptr` slot and must NOT
317///   drop the closure block between allocation and the pointer write —
318///   otherwise the pointer leaks. This matches the interpreter's
319///   `Box::into_raw(Box::new(initial))` pattern introduced in A.1B.
320/// - `initial` is a raw `ValueWord` bit pattern. If those bits encode a
321///   heap-refcounted pointer, the caller must ensure the appropriate
322///   refcount share was already taken for the capture slot — this FFI
323///   does not retain or release heap refs.
324#[unsafe(no_mangle)]
325pub unsafe extern "C" fn jit_alloc_owned_mut_cell(initial: u64) -> *mut u64 {
326    Box::into_raw(Box::new(initial))
327}
328
329// ============================================================================
330// Track A.1E: Shared capture FFI helpers
331// ============================================================================
332
333/// Retain a Shared capture's `Arc<SharedCell>` strong share.
334///
335/// The closure's capture slot for a `CaptureKind::Shared` capture holds
336/// a `*const SharedCell` obtained via `Arc::into_raw` on an outer-scope
337/// `Arc<SharedCell>`. At closure-allocation time, the outer slot already
338/// owns one strong share; the closure needs its own share. Matches the
339/// interpreter's `op_make_closure` Shared branch (`control_flow/mod.rs`)
340/// which calls `Arc::<SharedCell>::increment_strong_count(cell_ptr)` on
341/// the capture pointer before writing it into the closure's Ptr slot.
342///
343/// The JIT emits a call to this helper from
344/// `MirToIR::emit_heap_closure`'s Shared branch. The helper returns the
345/// same pointer so the store-back site can chain: `store(retain(ptr),
346/// closure + off)`.
347///
348/// # Safety
349///
350/// - `ptr` must be a non-null `*const SharedCell` obtained from a live
351///   `Arc<SharedCell>`. `Arc::increment_strong_count` has the same
352///   safety contract: the pointer must have come from `Arc::into_raw`
353///   (or another `Arc::as_ptr`) on a valid `Arc<SharedCell>` and the
354///   Arc must still have at least one strong share live.
355/// - The caller must install the returned pointer into a capture Ptr
356///   slot that `release_typed_closure` will reclaim (via
357///   `Arc::from_raw`) on closure drop, balancing this increment.
358#[unsafe(no_mangle)]
359pub unsafe extern "C" fn jit_arc_shared_retain(ptr: u64) -> u64 {
360    use shape_value::v2::closure_layout::SharedCell;
361    use std::sync::Arc;
362    if ptr == 0 {
363        // cell-identity #1: a zero pointer indicates the operand's root
364        // slot was not flagged as a `SharedCow` local by the MirToIR
365        // side-table — i.e. `initialize_shared_local_slots` never
366        // installed an Arc<SharedCell> for this slot. Previously this
367        // would segfault inside `Arc::increment_strong_count(null)`.
368        // Return 0 so the caller stores a null pointer and the
369        // downstream dispatch path can report a clean error rather
370        // than corrupting memory.
371        tracing::debug!(
372            target: "shape_jit",
373            "jit-shared-cell retain null (no-op)",
374        );
375        return 0;
376    }
377    unsafe {
378        Arc::<SharedCell>::increment_strong_count(ptr as *const SharedCell);
379    }
380    tracing::debug!(
381        target: "shape_jit",
382        ptr,
383        "jit-shared-cell retain",
384    );
385    ptr
386}
387
388/// Contended lock-slow-path helper for Shared capture reads/writes.
389///
390/// Called by the JIT when the inline CAS lock (state byte 0→1) fails.
391/// Spins on the state byte, matching the interpreter's
392/// `SharedCell::lock_contended` implementation. Closure-capture
393/// contention is rare in practice, so a spin-wait is acceptable.
394///
395/// # Safety
396///
397/// - `ptr` must be a live `*const SharedCell` whose state byte lives at
398///   offset `SHARED_CELL_STATE_OFFSET` (0). Callers reach this helper
399///   only after a failing inline CAS against the same state byte, so
400///   the layout contract is inherited from the caller.
401/// - On return, the lock state byte is `1` (locked) with `Acquire`
402///   ordering. The caller must eventually pair this with a matching
403///   release (via the inline unlock CAS or
404///   `jit_shared_unlock_contended`).
405#[unsafe(no_mangle)]
406pub unsafe extern "C" fn jit_shared_lock_contended(ptr: u64) {
407    use shape_value::v2::closure_layout::SharedCell;
408    if ptr == 0 {
409        return;
410    }
411    // SAFETY: see function SAFETY docs. Reborrowing `&SharedCell` for
412    // the duration of the spinlock is sound as long as the Arc strong
413    // share owning the allocation outlives this call — which the
414    // closure's capture slot guarantees (slot release is keyed on the
415    // closure's refcount hitting zero, which cannot race with a JIT'd
416    // body's lock acquire on the same slot).
417    let cell: &SharedCell = unsafe { &*(ptr as *const SharedCell) };
418    cell.lock_contended();
419}
420
421/// Contended unlock-slow-path helper for Shared capture reads/writes.
422///
423/// In the current hand-rolled-spinlock design, unlock is always a
424/// single `state.store(0, Release)` — there is no actual "slow path"
425/// because we don't park threads. This helper is provided for
426/// ABI-compatibility with the JIT's branch structure (the inline CAS
427/// could fail in a future implementation that adds a PARKED_BIT) and
428/// simply performs the release store.
429///
430/// # Safety
431///
432/// Same contract as `jit_shared_lock_contended`. Caller must currently
433/// hold the lock.
434#[unsafe(no_mangle)]
435pub unsafe extern "C" fn jit_shared_unlock_contended(ptr: u64) {
436    use shape_value::v2::closure_layout::SharedCell;
437    if ptr == 0 {
438        return;
439    }
440    // SAFETY: see `jit_shared_lock_contended`. Unlock with release
441    // ordering so the JIT-body's writes become visible to the next
442    // acquirer.
443    let cell: &SharedCell = unsafe { &*(ptr as *const SharedCell) };
444    unsafe { cell.unlock() };
445}
446
447// ============================================================================
448// Session 1 Commit 3: Outer-scope Shared-cell lifecycle helpers
449// ============================================================================
450//
451// These FFIs are the JIT counterparts of the interpreter handlers
452// `op_alloc_shared_local` and `op_drop_shared_local` (see
453// `shape-vm/src/executor/variables/mod.rs`). They allocate / release
454// exactly one `Arc<SharedCell>` strong share per outer-scope `var`
455// binding that escapes into a closure.
456//
457// Relationship to the A.1E Shared-capture FFIs:
458//
459//   * `jit_alloc_shared_cell`   — outer-scope allocation. Creates a
460//                                  fresh `Arc<SharedCell>` with the
461//                                  initial `ValueWord` bits and hands
462//                                  out one strong share to the caller.
463//                                  Mirrors `op_alloc_shared_local`.
464//   * `jit_arc_shared_retain`   — closure-capture retain (A.1E). Bumps
465//                                  the strong count by 1 for a closure
466//                                  taking a share of the outer cell.
467//   * `jit_arc_shared_release`  — outer-scope release. Consumes exactly
468//                                  one strong share. Mirrors
469//                                  `op_drop_shared_local`.
470//
471// Together they form a balanced lifecycle: each `AllocSharedLocal`
472// produces exactly one `Release`, and each `ClosureCapture` produces
473// exactly one `Retain`, which is balanced by the
474// `release_typed_closure` walk when the closure drops.
475
476/// Allocate a fresh `Arc<SharedCell>` from `initial_bits` and return
477/// the raw pointer bits of the strong share.
478///
479/// The returned pointer is owned by the caller's slot; it MUST be
480/// released via `jit_arc_shared_release` exactly once when the slot
481/// exits scope. `ValueWord::from_bits(initial_bits)` seeds the cell's
482/// inner payload; subsequent reads/writes go through the lock-gated
483/// pointer-deref lowering in `mir_compiler/places.rs`.
484///
485/// # Safety
486///
487/// - `initial_bits` is a raw `ValueWord` bit pattern. If the bits
488///   encode a heap-refcounted pointer, the caller must ensure the
489///   appropriate refcount share was already taken — this FFI does not
490///   retain or release heap refs on the payload.
491/// - The returned pointer is 8-byte aligned (Arc + repr(C) SharedCell)
492///   and non-null (Arc::new never returns null).
493/// - The returned pointer is the sole strong share owned by the
494///   caller's slot; `jit_arc_shared_release` is the sole releaser.
495///   Additional shares (one per capturing closure) are minted via
496///   `jit_arc_shared_retain` and balanced by `release_typed_closure`
497///   on closure drop.
498#[unsafe(no_mangle)]
499pub unsafe extern "C" fn jit_alloc_shared_cell(_initial_bits: u64) -> u64 {
500    // SURFACE (W10 jit-playbook §5 / ADR-006 §2.7.8 / Q10):
501    // `SharedCell::new(value, kind)` requires the cell's
502    // `NativeKind` companion at construction (cell-storage parallel
503    // kind track per ADR-006 §2.7.8); the FFI signature here only
504    // carries `initial_bits`, with no source for the kind. Per
505    // §2.7.8 #4 the correct response is surface-and-stop, never a
506    // Bool-default fallback.
507    //
508    // The strict-typing rebuild widens this entry to
509    // `jit_alloc_shared_cell(initial_bits: u64, kind: i32 /* NativeKind */)`
510    // with the kind sourced from the JIT-emitted `AllocSharedLocal`
511    // call signature per §2.7.5; the bytecode-side companion is
512    // already kinded (`shape-vm/src/executor/variables/mod.rs:1510`
513    // builds `SharedCell::new(value_bits, value_kind)`).
514    //
515    // Until the JIT lowering threads a kind through the call site
516    // (W11 / deeper Phase-2c), this entry-point fails loudly so
517    // callers reach this error at the JIT-emitted FFI boundary
518    // rather than silently allocating a kind-less cell.
519    todo!(
520        "phase-2c §2.7.8/Q10 / W10 jit-playbook §5: SharedCell kind \
521         companion — jit_alloc_shared_cell needs a NativeKind \
522         parameter per ADR-006 §2.7.8 (cell parallel-kind track). \
523         The bytecode-side AllocSharedLocal already threads \
524         value_kind (`shape-vm/src/executor/variables/mod.rs:1510`); \
525         the JIT lowering for the same opcode must thread the \
526         matching kind through the FFI signature per §2.7.5."
527    )
528}
529
530/// Release exactly one strong share of an `Arc<SharedCell>` at
531/// `ptr`. `ptr == 0` is a no-op, matching the interpreter's
532/// `op_drop_shared_local` null-pointer guard (the slot is overwritten
533/// with 0 after drop, so re-drops are silent).
534///
535/// # Safety
536///
537/// - `ptr` must be either null or a pointer previously returned by
538///   `jit_alloc_shared_cell` (or any other `Arc::into_raw`/`as_ptr`
539///   on a live `Arc<SharedCell>`) that has NOT yet been released.
540///   Double-release is UB (use-after-free on the second call).
541/// - `Arc::from_raw` reconstructs the strong share and the subsequent
542///   `drop` performs one atomic decrement. If this was the last
543///   strong share, the allocation is freed.
544#[unsafe(no_mangle)]
545pub unsafe extern "C" fn jit_arc_shared_release(ptr: u64) {
546    use shape_value::v2::closure_layout::SharedCell;
547    use std::sync::Arc;
548    if ptr == 0 {
549        return;
550    }
551    tracing::debug!(
552        target: "shape_jit",
553        ptr,
554        "jit-shared-cell release",
555    );
556    // SAFETY: the caller contract (see SAFETY docs above) guarantees
557    // `ptr` is a live Arc-from-raw pointer. Reconstructing the Arc
558    // and dropping it releases exactly one strong share.
559    unsafe {
560        drop(Arc::<SharedCell>::from_raw(ptr as *const SharedCell));
561    }
562}
563
564// ============================================================================
565// Wave C.1: Per-FieldKind closure-cell FFI wrappers (D1 native ABI)
566// ============================================================================
567//
568// These wrappers thread the Wave-B per-FieldKind helpers
569// (`shape_value::v2::closure_raw::{alloc,read,write}_owned_mutable_<kind>`
570// and `read_shared_<kind>` / `write_shared_<kind>`) through the JIT FFI
571// surface as 33 + 22 = 55 distinct symbols.
572//
573// ABI contract (locked in Wave A):
574//   * Cell pointers travel as `i64` (raw `*mut T` bits) across the FFI
575//     boundary.
576//   * 8-byte payloads (i64/u64/f64/Ptr) use their native Cranelift type
577//     (I64 / F64).
578//   * 4-byte payloads (i32/u32) use Cranelift `I32`.
579//   * Sub-32 payloads (i16/u16/i8/u8/bool) are widened to `i32` at the FFI
580//     boundary because Cranelift on SystemV does not have a `bool` or `i8`
581//     parameter class — these are passed in i32 registers with the high
582//     bits zero/sign-extended. The wrappers below truncate on entry and
583//     widen on return.
584//
585// The legacy `jit_alloc_owned_mut_cell` / `jit_arc_shared_*` helpers above
586// remain in place for now; Wave G handles the cleanup after C.2 ports the
587// Cranelift codegen sites.
588
589// --- OwnedMutable: i64 -------------------------------------------------------
590
591#[unsafe(no_mangle)]
592pub unsafe extern "C" fn jit_alloc_owned_mut_cell_i64(initial: i64) -> i64 {
593    shape_value::v2::closure_raw::alloc_owned_mutable_i64(initial) as i64
594}
595
596#[unsafe(no_mangle)]
597pub unsafe extern "C" fn jit_read_owned_mut_cell_i64(ptr: i64) -> i64 {
598    unsafe { shape_value::v2::closure_raw::read_owned_mutable_i64(ptr as *mut i64) }
599}
600
601#[unsafe(no_mangle)]
602pub unsafe extern "C" fn jit_write_owned_mut_cell_i64(ptr: i64, value: i64) {
603    unsafe { shape_value::v2::closure_raw::write_owned_mutable_i64(ptr as *mut i64, value) };
604}
605
606// --- OwnedMutable: u64 -------------------------------------------------------
607
608#[unsafe(no_mangle)]
609pub unsafe extern "C" fn jit_alloc_owned_mut_cell_u64(initial: i64) -> i64 {
610    shape_value::v2::closure_raw::alloc_owned_mutable_u64(initial as u64) as i64
611}
612
613#[unsafe(no_mangle)]
614pub unsafe extern "C" fn jit_read_owned_mut_cell_u64(ptr: i64) -> i64 {
615    unsafe { shape_value::v2::closure_raw::read_owned_mutable_u64(ptr as *mut u64) as i64 }
616}
617
618#[unsafe(no_mangle)]
619pub unsafe extern "C" fn jit_write_owned_mut_cell_u64(ptr: i64, value: i64) {
620    unsafe {
621        shape_value::v2::closure_raw::write_owned_mutable_u64(ptr as *mut u64, value as u64)
622    };
623}
624
625// --- OwnedMutable: f64 -------------------------------------------------------
626
627#[unsafe(no_mangle)]
628pub unsafe extern "C" fn jit_alloc_owned_mut_cell_f64(initial: f64) -> i64 {
629    shape_value::v2::closure_raw::alloc_owned_mutable_f64(initial) as i64
630}
631
632#[unsafe(no_mangle)]
633pub unsafe extern "C" fn jit_read_owned_mut_cell_f64(ptr: i64) -> f64 {
634    unsafe { shape_value::v2::closure_raw::read_owned_mutable_f64(ptr as *mut f64) }
635}
636
637#[unsafe(no_mangle)]
638pub unsafe extern "C" fn jit_write_owned_mut_cell_f64(ptr: i64, value: f64) {
639    unsafe { shape_value::v2::closure_raw::write_owned_mutable_f64(ptr as *mut f64, value) };
640}
641
642// --- OwnedMutable: i32 -------------------------------------------------------
643
644#[unsafe(no_mangle)]
645pub unsafe extern "C" fn jit_alloc_owned_mut_cell_i32(initial: i32) -> i64 {
646    shape_value::v2::closure_raw::alloc_owned_mutable_i32(initial) as i64
647}
648
649#[unsafe(no_mangle)]
650pub unsafe extern "C" fn jit_read_owned_mut_cell_i32(ptr: i64) -> i32 {
651    unsafe { shape_value::v2::closure_raw::read_owned_mutable_i32(ptr as *mut i32) }
652}
653
654#[unsafe(no_mangle)]
655pub unsafe extern "C" fn jit_write_owned_mut_cell_i32(ptr: i64, value: i32) {
656    unsafe { shape_value::v2::closure_raw::write_owned_mutable_i32(ptr as *mut i32, value) };
657}
658
659// --- OwnedMutable: u32 -------------------------------------------------------
660
661#[unsafe(no_mangle)]
662pub unsafe extern "C" fn jit_alloc_owned_mut_cell_u32(initial: i32) -> i64 {
663    shape_value::v2::closure_raw::alloc_owned_mutable_u32(initial as u32) as i64
664}
665
666#[unsafe(no_mangle)]
667pub unsafe extern "C" fn jit_read_owned_mut_cell_u32(ptr: i64) -> i32 {
668    unsafe { shape_value::v2::closure_raw::read_owned_mutable_u32(ptr as *mut u32) as i32 }
669}
670
671#[unsafe(no_mangle)]
672pub unsafe extern "C" fn jit_write_owned_mut_cell_u32(ptr: i64, value: i32) {
673    unsafe {
674        shape_value::v2::closure_raw::write_owned_mutable_u32(ptr as *mut u32, value as u32)
675    };
676}
677
678// --- OwnedMutable: i16 -------------------------------------------------------
679
680#[unsafe(no_mangle)]
681pub unsafe extern "C" fn jit_alloc_owned_mut_cell_i16(initial: i32) -> i64 {
682    shape_value::v2::closure_raw::alloc_owned_mutable_i16(initial as i16) as i64
683}
684
685#[unsafe(no_mangle)]
686pub unsafe extern "C" fn jit_read_owned_mut_cell_i16(ptr: i64) -> i32 {
687    unsafe { shape_value::v2::closure_raw::read_owned_mutable_i16(ptr as *mut i16) as i32 }
688}
689
690#[unsafe(no_mangle)]
691pub unsafe extern "C" fn jit_write_owned_mut_cell_i16(ptr: i64, value: i32) {
692    unsafe {
693        shape_value::v2::closure_raw::write_owned_mutable_i16(ptr as *mut i16, value as i16)
694    };
695}
696
697// --- OwnedMutable: u16 -------------------------------------------------------
698
699#[unsafe(no_mangle)]
700pub unsafe extern "C" fn jit_alloc_owned_mut_cell_u16(initial: i32) -> i64 {
701    shape_value::v2::closure_raw::alloc_owned_mutable_u16(initial as u16) as i64
702}
703
704#[unsafe(no_mangle)]
705pub unsafe extern "C" fn jit_read_owned_mut_cell_u16(ptr: i64) -> i32 {
706    unsafe { shape_value::v2::closure_raw::read_owned_mutable_u16(ptr as *mut u16) as i32 }
707}
708
709#[unsafe(no_mangle)]
710pub unsafe extern "C" fn jit_write_owned_mut_cell_u16(ptr: i64, value: i32) {
711    unsafe {
712        shape_value::v2::closure_raw::write_owned_mutable_u16(ptr as *mut u16, value as u16)
713    };
714}
715
716// --- OwnedMutable: i8 --------------------------------------------------------
717
718#[unsafe(no_mangle)]
719pub unsafe extern "C" fn jit_alloc_owned_mut_cell_i8(initial: i32) -> i64 {
720    shape_value::v2::closure_raw::alloc_owned_mutable_i8(initial as i8) as i64
721}
722
723#[unsafe(no_mangle)]
724pub unsafe extern "C" fn jit_read_owned_mut_cell_i8(ptr: i64) -> i32 {
725    unsafe { shape_value::v2::closure_raw::read_owned_mutable_i8(ptr as *mut i8) as i32 }
726}
727
728#[unsafe(no_mangle)]
729pub unsafe extern "C" fn jit_write_owned_mut_cell_i8(ptr: i64, value: i32) {
730    unsafe {
731        shape_value::v2::closure_raw::write_owned_mutable_i8(ptr as *mut i8, value as i8)
732    };
733}
734
735// --- OwnedMutable: u8 --------------------------------------------------------
736
737#[unsafe(no_mangle)]
738pub unsafe extern "C" fn jit_alloc_owned_mut_cell_u8(initial: i32) -> i64 {
739    shape_value::v2::closure_raw::alloc_owned_mutable_u8(initial as u8) as i64
740}
741
742#[unsafe(no_mangle)]
743pub unsafe extern "C" fn jit_read_owned_mut_cell_u8(ptr: i64) -> i32 {
744    unsafe { shape_value::v2::closure_raw::read_owned_mutable_u8(ptr as *mut u8) as i32 }
745}
746
747#[unsafe(no_mangle)]
748pub unsafe extern "C" fn jit_write_owned_mut_cell_u8(ptr: i64, value: i32) {
749    unsafe {
750        shape_value::v2::closure_raw::write_owned_mutable_u8(ptr as *mut u8, value as u8)
751    };
752}
753
754// --- OwnedMutable: bool ------------------------------------------------------
755
756#[unsafe(no_mangle)]
757pub unsafe extern "C" fn jit_alloc_owned_mut_cell_bool(initial: i32) -> i64 {
758    shape_value::v2::closure_raw::alloc_owned_mutable_bool(initial != 0) as i64
759}
760
761#[unsafe(no_mangle)]
762pub unsafe extern "C" fn jit_read_owned_mut_cell_bool(ptr: i64) -> i32 {
763    unsafe { shape_value::v2::closure_raw::read_owned_mutable_bool(ptr as *mut bool) as i32 }
764}
765
766#[unsafe(no_mangle)]
767pub unsafe extern "C" fn jit_write_owned_mut_cell_bool(ptr: i64, value: i32) {
768    unsafe {
769        shape_value::v2::closure_raw::write_owned_mutable_bool(ptr as *mut bool, value != 0)
770    };
771}
772
773// --- OwnedMutable: ptr (8-byte ValueWord-bits payload) -----------------------
774
775#[unsafe(no_mangle)]
776pub unsafe extern "C" fn jit_alloc_owned_mut_cell_ptr(initial: i64) -> i64 {
777    shape_value::v2::closure_raw::alloc_owned_mutable_ptr(initial as u64) as i64
778}
779
780#[unsafe(no_mangle)]
781pub unsafe extern "C" fn jit_read_owned_mut_cell_ptr(ptr: i64) -> i64 {
782    unsafe { shape_value::v2::closure_raw::read_owned_mutable_ptr(ptr as *mut u64) as i64 }
783}
784
785#[unsafe(no_mangle)]
786pub unsafe extern "C" fn jit_write_owned_mut_cell_ptr(ptr: i64, value: i64) {
787    unsafe {
788        shape_value::v2::closure_raw::write_owned_mutable_ptr(ptr as *mut u64, value as u64)
789    };
790}
791
792// --- Shared: i64 -------------------------------------------------------------
793
794#[unsafe(no_mangle)]
795pub unsafe extern "C" fn jit_read_shared_cell_i64(cell_ptr: i64) -> i64 {
796    use shape_value::v2::closure_layout::SharedCell;
797    unsafe { shape_value::v2::closure_raw::read_shared_i64(cell_ptr as *const SharedCell) }
798}
799
800#[unsafe(no_mangle)]
801pub unsafe extern "C" fn jit_write_shared_cell_i64(cell_ptr: i64, value: i64) {
802    use shape_value::v2::closure_layout::SharedCell;
803    unsafe {
804        shape_value::v2::closure_raw::write_shared_i64(cell_ptr as *const SharedCell, value)
805    };
806}
807
808// --- Shared: u64 -------------------------------------------------------------
809
810#[unsafe(no_mangle)]
811pub unsafe extern "C" fn jit_read_shared_cell_u64(cell_ptr: i64) -> i64 {
812    use shape_value::v2::closure_layout::SharedCell;
813    unsafe {
814        shape_value::v2::closure_raw::read_shared_u64(cell_ptr as *const SharedCell) as i64
815    }
816}
817
818#[unsafe(no_mangle)]
819pub unsafe extern "C" fn jit_write_shared_cell_u64(cell_ptr: i64, value: i64) {
820    use shape_value::v2::closure_layout::SharedCell;
821    unsafe {
822        shape_value::v2::closure_raw::write_shared_u64(
823            cell_ptr as *const SharedCell,
824            value as u64,
825        )
826    };
827}
828
829// --- Shared: f64 -------------------------------------------------------------
830
831#[unsafe(no_mangle)]
832pub unsafe extern "C" fn jit_read_shared_cell_f64(cell_ptr: i64) -> f64 {
833    use shape_value::v2::closure_layout::SharedCell;
834    unsafe { shape_value::v2::closure_raw::read_shared_f64(cell_ptr as *const SharedCell) }
835}
836
837#[unsafe(no_mangle)]
838pub unsafe extern "C" fn jit_write_shared_cell_f64(cell_ptr: i64, value: f64) {
839    use shape_value::v2::closure_layout::SharedCell;
840    unsafe {
841        shape_value::v2::closure_raw::write_shared_f64(cell_ptr as *const SharedCell, value)
842    };
843}
844
845// --- Shared: i32 -------------------------------------------------------------
846
847#[unsafe(no_mangle)]
848pub unsafe extern "C" fn jit_read_shared_cell_i32(cell_ptr: i64) -> i32 {
849    use shape_value::v2::closure_layout::SharedCell;
850    unsafe { shape_value::v2::closure_raw::read_shared_i32(cell_ptr as *const SharedCell) }
851}
852
853#[unsafe(no_mangle)]
854pub unsafe extern "C" fn jit_write_shared_cell_i32(cell_ptr: i64, value: i32) {
855    use shape_value::v2::closure_layout::SharedCell;
856    unsafe {
857        shape_value::v2::closure_raw::write_shared_i32(cell_ptr as *const SharedCell, value)
858    };
859}
860
861// --- Shared: u32 -------------------------------------------------------------
862
863#[unsafe(no_mangle)]
864pub unsafe extern "C" fn jit_read_shared_cell_u32(cell_ptr: i64) -> i32 {
865    use shape_value::v2::closure_layout::SharedCell;
866    unsafe {
867        shape_value::v2::closure_raw::read_shared_u32(cell_ptr as *const SharedCell) as i32
868    }
869}
870
871#[unsafe(no_mangle)]
872pub unsafe extern "C" fn jit_write_shared_cell_u32(cell_ptr: i64, value: i32) {
873    use shape_value::v2::closure_layout::SharedCell;
874    unsafe {
875        shape_value::v2::closure_raw::write_shared_u32(
876            cell_ptr as *const SharedCell,
877            value as u32,
878        )
879    };
880}
881
882// --- Shared: i16 -------------------------------------------------------------
883
884#[unsafe(no_mangle)]
885pub unsafe extern "C" fn jit_read_shared_cell_i16(cell_ptr: i64) -> i32 {
886    use shape_value::v2::closure_layout::SharedCell;
887    unsafe {
888        shape_value::v2::closure_raw::read_shared_i16(cell_ptr as *const SharedCell) as i32
889    }
890}
891
892#[unsafe(no_mangle)]
893pub unsafe extern "C" fn jit_write_shared_cell_i16(cell_ptr: i64, value: i32) {
894    use shape_value::v2::closure_layout::SharedCell;
895    unsafe {
896        shape_value::v2::closure_raw::write_shared_i16(
897            cell_ptr as *const SharedCell,
898            value as i16,
899        )
900    };
901}
902
903// --- Shared: u16 -------------------------------------------------------------
904
905#[unsafe(no_mangle)]
906pub unsafe extern "C" fn jit_read_shared_cell_u16(cell_ptr: i64) -> i32 {
907    use shape_value::v2::closure_layout::SharedCell;
908    unsafe {
909        shape_value::v2::closure_raw::read_shared_u16(cell_ptr as *const SharedCell) as i32
910    }
911}
912
913#[unsafe(no_mangle)]
914pub unsafe extern "C" fn jit_write_shared_cell_u16(cell_ptr: i64, value: i32) {
915    use shape_value::v2::closure_layout::SharedCell;
916    unsafe {
917        shape_value::v2::closure_raw::write_shared_u16(
918            cell_ptr as *const SharedCell,
919            value as u16,
920        )
921    };
922}
923
924// --- Shared: i8 --------------------------------------------------------------
925
926#[unsafe(no_mangle)]
927pub unsafe extern "C" fn jit_read_shared_cell_i8(cell_ptr: i64) -> i32 {
928    use shape_value::v2::closure_layout::SharedCell;
929    unsafe {
930        shape_value::v2::closure_raw::read_shared_i8(cell_ptr as *const SharedCell) as i32
931    }
932}
933
934#[unsafe(no_mangle)]
935pub unsafe extern "C" fn jit_write_shared_cell_i8(cell_ptr: i64, value: i32) {
936    use shape_value::v2::closure_layout::SharedCell;
937    unsafe {
938        shape_value::v2::closure_raw::write_shared_i8(
939            cell_ptr as *const SharedCell,
940            value as i8,
941        )
942    };
943}
944
945// --- Shared: u8 --------------------------------------------------------------
946
947#[unsafe(no_mangle)]
948pub unsafe extern "C" fn jit_read_shared_cell_u8(cell_ptr: i64) -> i32 {
949    use shape_value::v2::closure_layout::SharedCell;
950    unsafe {
951        shape_value::v2::closure_raw::read_shared_u8(cell_ptr as *const SharedCell) as i32
952    }
953}
954
955#[unsafe(no_mangle)]
956pub unsafe extern "C" fn jit_write_shared_cell_u8(cell_ptr: i64, value: i32) {
957    use shape_value::v2::closure_layout::SharedCell;
958    unsafe {
959        shape_value::v2::closure_raw::write_shared_u8(
960            cell_ptr as *const SharedCell,
961            value as u8,
962        )
963    };
964}
965
966// --- Shared: bool ------------------------------------------------------------
967
968#[unsafe(no_mangle)]
969pub unsafe extern "C" fn jit_read_shared_cell_bool(cell_ptr: i64) -> i32 {
970    use shape_value::v2::closure_layout::SharedCell;
971    unsafe {
972        shape_value::v2::closure_raw::read_shared_bool(cell_ptr as *const SharedCell) as i32
973    }
974}
975
976#[unsafe(no_mangle)]
977pub unsafe extern "C" fn jit_write_shared_cell_bool(cell_ptr: i64, value: i32) {
978    use shape_value::v2::closure_layout::SharedCell;
979    unsafe {
980        shape_value::v2::closure_raw::write_shared_bool(
981            cell_ptr as *const SharedCell,
982            value != 0,
983        )
984    };
985}
986
987// --- Shared: ptr (8-byte ValueWord-bits payload) -----------------------------
988
989#[unsafe(no_mangle)]
990pub unsafe extern "C" fn jit_read_shared_cell_ptr(cell_ptr: i64) -> i64 {
991    use shape_value::v2::closure_layout::SharedCell;
992    unsafe {
993        shape_value::v2::closure_raw::read_shared_ptr(cell_ptr as *const SharedCell) as i64
994    }
995}
996
997#[unsafe(no_mangle)]
998pub unsafe extern "C" fn jit_write_shared_cell_ptr(cell_ptr: i64, value: i64) {
999    use shape_value::v2::closure_layout::SharedCell;
1000    unsafe {
1001        shape_value::v2::closure_raw::write_shared_ptr(
1002            cell_ptr as *const SharedCell,
1003            value as u64,
1004        )
1005    };
1006}
1007
1008// W11: gated out — body uses deleted `shape_value::ValueWord` /
1009// `ValueWordExt` (removed by the strict-typing bulldozer; see
1010// `crates/shape-value/src/native_kind.rs:103-107` and Forbidden Patterns
1011// in `CLAUDE.md`). The kinded-FFI replacement (`KindedSlot`-based shared-
1012// cell lifecycle helpers) is part of the §2.7.4 Phase 2c FFI rebuild.
1013#[cfg(any())]
1014#[cfg(test)]
1015mod a1e_shared_ffi_tests {
1016    //! Track A.1E unit tests for the Shared capture FFI helpers.
1017    //!
1018    //! These are direct FFI tests that manipulate `Arc<SharedCell>` by
1019    //! hand and verify the refcount bookkeeping matches the interpreter's
1020    //! `op_make_closure` Shared branch contract.
1021    use super::*;
1022    use shape_value::v2::closure_layout::SharedCell;
1023    use shape_value::{ValueWord, ValueWordExt};
1024    use std::sync::Arc;
1025
1026    #[test]
1027    fn a1e_ffi_arc_shared_retain_increments_strong_count() {
1028        // Allocate an Arc<SharedCell> and take its raw pointer. Initial
1029        // strong count = 1 (the cloned observer share below takes count
1030        // to 2 — our baseline).
1031        let arc: Arc<SharedCell> = Arc::new(SharedCell::new(ValueWord::from_i64(1234)));
1032        let observer = Arc::clone(&arc);
1033        assert_eq!(Arc::strong_count(&observer), 2);
1034
1035        // Take one raw share via Arc::into_raw (this is what the outer
1036        // slot's AllocSharedLocal did; we simulate it here).
1037        let raw_slot_share = Arc::into_raw(Arc::clone(&arc));
1038        assert_eq!(Arc::strong_count(&observer), 3);
1039
1040        // Call the FFI retain — mirrors `op_make_closure`'s
1041        // `Arc::increment_strong_count` on the capture pointer.
1042        let returned = unsafe { jit_arc_shared_retain(raw_slot_share as u64) };
1043        assert_eq!(returned, raw_slot_share as u64, "helper returns the pointer");
1044        assert_eq!(
1045            Arc::strong_count(&observer),
1046            4,
1047            "retain must bump the strong count by one"
1048        );
1049
1050        // Unwind: release the two shares taken via `Arc::into_raw` /
1051        // `increment_strong_count` by reconstructing Arcs and dropping.
1052        unsafe {
1053            Arc::<SharedCell>::from_raw(raw_slot_share);
1054            Arc::<SharedCell>::from_raw(raw_slot_share);
1055        }
1056        assert_eq!(Arc::strong_count(&observer), 2);
1057        drop(arc);
1058        assert_eq!(Arc::strong_count(&observer), 1);
1059    }
1060
1061    #[test]
1062    fn a1e_ffi_shared_lock_unlock_contended_roundtrip() {
1063        // Lock / unlock roundtrip via the FFI slow-path helpers. No
1064        // contention — these helpers are still correct on uncontended
1065        // cells.
1066        let cell = Box::new(SharedCell::new(ValueWord::from_i64(42)));
1067        let ptr = Box::into_raw(cell);
1068        unsafe {
1069            jit_shared_lock_contended(ptr as u64);
1070            // While locked, the state byte must read 1.
1071            let state = (*ptr)
1072                .state
1073                .load(std::sync::atomic::Ordering::Relaxed);
1074            assert_eq!(state, 1, "lock helper must leave state byte = 1");
1075            jit_shared_unlock_contended(ptr as u64);
1076            let state = (*ptr)
1077                .state
1078                .load(std::sync::atomic::Ordering::Relaxed);
1079            assert_eq!(state, 0, "unlock helper must leave state byte = 0");
1080            drop(Box::from_raw(ptr));
1081        }
1082    }
1083
1084    #[test]
1085    fn a1e_ffi_shared_helpers_handle_null_ptr_safely() {
1086        // Null pointers should be no-ops, not crashes. The JIT guards
1087        // against codegen bugs by emitting a branch on null; this is a
1088        // defense-in-depth test.
1089        unsafe {
1090            jit_shared_lock_contended(0);
1091            jit_shared_unlock_contended(0);
1092        }
1093    }
1094}
1095
1096#[cfg(test)]
1097mod a1d_owned_mutable_cell_tests {
1098    //! Track A.1D unit tests for `jit_alloc_owned_mut_cell`.
1099    //!
1100    //! The FFI helper is the sole allocator for `CaptureKind::OwnedMutable`
1101    //! cells. These tests verify:
1102    //! - The returned pointer deref yields the exact `initial` bits.
1103    //! - Multiple allocations are distinct and independently owned.
1104    //! - The pointer layout matches `Box::<u64>::into_raw`, so
1105    //!   `Box::from_raw` reclaims without UB.
1106    use super::*;
1107
1108    #[test]
1109    fn a1d_ffi_alloc_owned_mut_cell_roundtrip() {
1110        let initial: u64 = 42;
1111        let ptr = unsafe { jit_alloc_owned_mut_cell(initial) };
1112        assert!(!ptr.is_null(), "allocator must return a non-null pointer");
1113        let read = unsafe { *ptr };
1114        assert_eq!(read, initial, "deref of fresh cell must yield the initial bits");
1115        // Reclaim via Box::from_raw — matching `release_typed_closure`'s path.
1116        let _boxed: Box<u64> = unsafe { Box::from_raw(ptr) };
1117    }
1118
1119    #[test]
1120    fn a1d_ffi_alloc_owned_mut_cell_independent_cells() {
1121        let a = unsafe { jit_alloc_owned_mut_cell(10) };
1122        let b = unsafe { jit_alloc_owned_mut_cell(20) };
1123        assert_ne!(a, b, "distinct allocations must yield distinct pointers");
1124        // Writes through one pointer must not bleed into the other.
1125        unsafe {
1126            std::ptr::write(a, 999);
1127            assert_eq!(*a, 999);
1128            assert_eq!(*b, 20);
1129        }
1130        unsafe {
1131            let _ = Box::from_raw(a);
1132            let _ = Box::from_raw(b);
1133        }
1134    }
1135
1136    #[test]
1137    fn a1d_ffi_alloc_owned_mut_cell_store_then_read() {
1138        // Simulate Load/Store semantics: the interpreter's
1139        // `op_store_owned_mutable_capture` writes through the pointer with
1140        // `std::ptr::write`, and `op_load_owned_mutable_capture` reads with
1141        // `std::ptr::read`. This mirrors that usage pattern on the FFI
1142        // helper's output.
1143        let ptr = unsafe { jit_alloc_owned_mut_cell(0) };
1144        for new_bits in [7u64, 13, 99, u64::MAX, 0] {
1145            unsafe { std::ptr::write(ptr, new_bits) };
1146            let out = unsafe { std::ptr::read(ptr) };
1147            assert_eq!(out, new_bits);
1148        }
1149        unsafe {
1150            let _ = Box::from_raw(ptr);
1151        }
1152    }
1153}
1154
1155// W11: gated out — body uses deleted `shape_value::ValueWord` /
1156// `tag_bits` API. Kinded-FFI replacement deferred to §2.7.4 Phase 2c.
1157#[cfg(any())]
1158#[cfg(test)]
1159mod phase_h2_finalizer_tests {
1160    //! Closure-spec Phase H2 (updated for §14.6 / H6.5) unit tests for
1161    //! `jit_finalize_heap_closure`.
1162    //!
1163    //! These exercise the finalizer directly with manually-constructed
1164    //! `TypedClosureHeader` blocks (matching what `emit_heap_closure` emits
1165    //! in Cranelift) to verify:
1166    //! - The finalizer hands the raw block off to an `OwnedClosureBlock`
1167    //!   without rebuilding captures into a `Vec<Upvalue>`.
1168    //! - The resulting `HeapValue::ClosureRaw` reads captures back through
1169    //!   the `VmClosureHandle` shim at their typed widths.
1170    //! - The `TypedClosureHeader` block's refcount is owned by the returned
1171    //!   ValueWord; dropping the value releases the block and, for heap-
1172    //!   typed captures, the corresponding capture share.
1173    //! - The NaN-boxed return value decodes back to `HeapValue::ClosureRaw`
1174    //!   via `as_heap_ref`.
1175    //!
1176    //! See `docs/v2-closure-specialization.md` §13 H2 and §14.6 (H6.5).
1177    use super::*;
1178    use shape_value::heap_value::HeapValue;
1179    use shape_value::v2::closure_layout::{
1180        CaptureKind, ClosureLayout, HEAP_CLOSURE_HEADER_SIZE, TypedClosureHeader,
1181    };
1182    use shape_value::v2::concrete_type::ConcreteType;
1183    use shape_value::v2::heap_header::{HEAP_KIND_V2_CLOSURE, HeapHeader};
1184    use shape_value::{ValueWord, ValueWordExt};
1185    use std::sync::Arc;
1186
1187    // Test-local helper: immutable-only layout (all captures tagged
1188    // `CaptureKind::Immutable`). Matches the pre-A.1A signature.
1189    fn immutable_layout(types: &[ConcreteType]) -> ClosureLayout {
1190        let kinds = vec![CaptureKind::Immutable; types.len()];
1191        ClosureLayout::from_capture_types(types, &kinds)
1192    }
1193
1194    /// Allocate a TypedClosureHeader block with the given layout and return a
1195    /// zero-initialized raw pointer (HeapHeader fields are written, captures
1196    /// area is zeroed).
1197    unsafe fn alloc_typed_closure_for_test(
1198        layout: &ClosureLayout,
1199        function_id: u16,
1200        type_id: u32,
1201    ) -> *mut u8 {
1202        let size = layout.total_heap_size();
1203        let align = 8;
1204        let alloc_layout =
1205            std::alloc::Layout::from_size_align(size, align).expect("valid layout");
1206        let ptr = unsafe { std::alloc::alloc_zeroed(alloc_layout) };
1207        assert!(!ptr.is_null(), "alloc_zeroed returned null");
1208        unsafe {
1209            std::ptr::write(ptr as *mut HeapHeader, HeapHeader::new(HEAP_KIND_V2_CLOSURE));
1210            let header = ptr as *mut TypedClosureHeader;
1211            (*header).function_id = function_id as u32;
1212            (*header).type_id = type_id;
1213        }
1214        ptr
1215    }
1216
1217    /// Helper: assert that a ValueWord-bits value decodes to a
1218    /// `HeapValue::ClosureRaw` whose `VmClosureHandle` matches the given
1219    /// `function_id` and capture count. Returns the handle for further
1220    /// capture reads.
1221    fn assert_closure_raw(bits: u64, expected_fid: u16, expected_caps: usize) -> (u16, usize) {
1222        // Clone the bits to get an owned ValueWord that still holds the
1223        // Arc share; the test's `drop_bits_via_raw(bits)` releases the
1224        // original share at the end of the scope.
1225        let vw = unsafe { ValueWord::clone_from_bits(bits) };
1226        let hv = vw.as_heap_ref().expect("finalizer should produce a heap value");
1227        assert!(
1228            matches!(hv, HeapValue::ClosureRaw(..)),
1229            "expected ClosureRaw variant, got {:?}",
1230            hv.type_name()
1231        );
1232        let handle = hv.as_closure_handle().expect("closure handle");
1233        assert_eq!(handle.function_id() as u16, expected_fid);
1234        assert_eq!(handle.capture_count(), expected_caps);
1235        let out = (handle.function_id() as u16, handle.capture_count());
1236        drop(vw);
1237        out
1238    }
1239
1240    #[test]
1241    fn finalizer_empty_captures() {
1242        // Zero-capture closure: header only, captures area is empty.
1243        let layout = Arc::new(immutable_layout(&[]));
1244        let ptr = unsafe { alloc_typed_closure_for_test(&layout, 42, 0) };
1245        let bits = unsafe {
1246            jit_finalize_heap_closure(ptr, 42, 0, Arc::as_ptr(&layout))
1247        };
1248        assert_closure_raw(bits, 42, 0);
1249        // Final drop releases the owning share — ClosureRaw's OwnedClosureBlock
1250        // Drop routes through `release_typed_closure` and frees the block.
1251        unsafe { drop_bits_via_raw(bits) };
1252    }
1253
1254    /// Drop a NaN-boxed value's refcount share. Used in tests.
1255    unsafe fn drop_bits_via_raw(bits: u64) {
1256        let vw = unsafe { ValueWord::from_raw_bits(bits) };
1257        drop(vw);
1258    }
1259
1260    #[test]
1261    fn finalizer_single_i64_capture() {
1262        // Single I64 capture written at offset 16 (HEAP_CLOSURE_HEADER_SIZE).
1263        let layout = Arc::new(immutable_layout(&[ConcreteType::I64]));
1264        let ptr = unsafe { alloc_typed_closure_for_test(&layout, 7, 0) };
1265        // Write the capture value at the typed offset.
1266        unsafe {
1267            let off = layout.heap_capture_offset(0);
1268            assert_eq!(off, HEAP_CLOSURE_HEADER_SIZE);
1269            // Store the raw bits of from_i64(123) as u64 — this is what the
1270            // JIT's coerce_for_capture_store would do for an I64 capture
1271            // (widen to I64 with the ValueWord bit pattern).
1272            let raw = ValueWord::from_i64(123).into_raw_bits();
1273            std::ptr::write(ptr.add(off) as *mut u64, raw);
1274        }
1275        let bits = unsafe {
1276            jit_finalize_heap_closure(ptr, 7, 1, Arc::as_ptr(&layout))
1277        };
1278        let vw = unsafe { ValueWord::clone_from_bits(bits) };
1279        let hv = vw.as_heap_ref().expect("should be heap value");
1280        let handle = hv.as_closure_handle().expect("closure handle");
1281        assert!(matches!(hv, HeapValue::ClosureRaw(..)));
1282        assert_eq!(handle.function_id() as u16, 7);
1283        assert_eq!(handle.capture_count(), 1);
1284        assert_eq!(handle.capture_as_value(0).as_i64(), Some(123));
1285        drop(vw);
1286        unsafe { drop_bits_via_raw(bits) };
1287    }
1288
1289    #[test]
1290    fn finalizer_single_f64_capture() {
1291        let layout = Arc::new(immutable_layout(&[ConcreteType::F64]));
1292        let ptr = unsafe { alloc_typed_closure_for_test(&layout, 9, 0) };
1293        unsafe {
1294            let off = layout.heap_capture_offset(0);
1295            // F64 captures are stored as native f64 (not NaN-boxed).
1296            std::ptr::write(ptr.add(off) as *mut f64, 3.14);
1297        }
1298        let bits = unsafe {
1299            jit_finalize_heap_closure(ptr, 9, 1, Arc::as_ptr(&layout))
1300        };
1301        let vw = unsafe { ValueWord::clone_from_bits(bits) };
1302        let hv = vw.as_heap_ref().expect("heap");
1303        assert!(matches!(hv, HeapValue::ClosureRaw(..)));
1304        let handle = hv.as_closure_handle().expect("handle");
1305        assert_eq!(handle.function_id() as u16, 9);
1306        assert_eq!(handle.capture_count(), 1);
1307        assert_eq!(handle.capture_as_value(0).as_f64(), Some(3.14));
1308        drop(vw);
1309        unsafe { drop_bits_via_raw(bits) };
1310    }
1311
1312    #[test]
1313    fn finalizer_bool_capture() {
1314        let layout = Arc::new(immutable_layout(&[ConcreteType::Bool]));
1315        let ptr = unsafe { alloc_typed_closure_for_test(&layout, 1, 0) };
1316        unsafe {
1317            let off = layout.heap_capture_offset(0);
1318            std::ptr::write(ptr.add(off) as *mut u8, 1u8);
1319        }
1320        let bits = unsafe {
1321            jit_finalize_heap_closure(ptr, 1, 1, Arc::as_ptr(&layout))
1322        };
1323        let vw = unsafe { ValueWord::clone_from_bits(bits) };
1324        let hv = vw.as_heap_ref().expect("heap");
1325        assert!(matches!(hv, HeapValue::ClosureRaw(..)));
1326        let handle = hv.as_closure_handle().expect("handle");
1327        assert_eq!(handle.capture_as_value(0).as_bool(), Some(true));
1328        drop(vw);
1329        unsafe { drop_bits_via_raw(bits) };
1330    }
1331
1332    #[test]
1333    fn finalizer_i32_capture_zero_extended() {
1334        let layout = Arc::new(immutable_layout(&[ConcreteType::I32]));
1335        let ptr = unsafe { alloc_typed_closure_for_test(&layout, 2, 0) };
1336        unsafe {
1337            let off = layout.heap_capture_offset(0);
1338            std::ptr::write(ptr.add(off) as *mut i32, -12345);
1339        }
1340        let bits = unsafe {
1341            jit_finalize_heap_closure(ptr, 2, 1, Arc::as_ptr(&layout))
1342        };
1343        let vw = unsafe { ValueWord::clone_from_bits(bits) };
1344        let hv = vw.as_heap_ref().expect("heap");
1345        assert!(matches!(hv, HeapValue::ClosureRaw(..)));
1346        let handle = hv.as_closure_handle().expect("handle");
1347        assert_eq!(handle.capture_as_value(0).as_i64(), Some(-12345));
1348        drop(vw);
1349        unsafe { drop_bits_via_raw(bits) };
1350    }
1351
1352    #[test]
1353    fn finalizer_mixed_f64_i32_captures() {
1354        // Two typed captures at distinct offsets.
1355        let layout = Arc::new(immutable_layout(&[
1356            ConcreteType::F64,
1357            ConcreteType::I32,
1358        ]));
1359        // F64 @ 16, I32 @ 24 (8-aligned after F64)
1360        assert_eq!(layout.heap_capture_offset(0), 16);
1361        assert_eq!(layout.heap_capture_offset(1), 24);
1362        let ptr = unsafe { alloc_typed_closure_for_test(&layout, 100, 0) };
1363        unsafe {
1364            std::ptr::write(ptr.add(16) as *mut f64, 2.71);
1365            std::ptr::write(ptr.add(24) as *mut i32, 99);
1366        }
1367        let bits = unsafe {
1368            jit_finalize_heap_closure(ptr, 100, 2, Arc::as_ptr(&layout))
1369        };
1370        let vw = unsafe { ValueWord::clone_from_bits(bits) };
1371        let hv = vw.as_heap_ref().expect("heap");
1372        assert!(matches!(hv, HeapValue::ClosureRaw(..)));
1373        let handle = hv.as_closure_handle().expect("handle");
1374        assert_eq!(handle.function_id() as u16, 100);
1375        assert_eq!(handle.capture_count(), 2);
1376        assert_eq!(handle.capture_as_value(0).as_f64(), Some(2.71));
1377        assert_eq!(handle.capture_as_value(1).as_i64(), Some(99));
1378        drop(vw);
1379        unsafe { drop_bits_via_raw(bits) };
1380    }
1381
1382    #[test]
1383    fn finalizer_heap_typed_string_capture_preserves_refcount() {
1384        // A string capture: the JIT's emit_heap_closure would have emitted
1385        // one atomic retain on the string's HeapHeader. Under H6.5 that
1386        // retained share stays with the block — `OwnedClosureBlock::Drop`
1387        // releases it when the ClosureRaw value's refcount hits zero via
1388        // `release_typed_closure`'s heap_capture_mask walk.
1389        let layout = Arc::new(immutable_layout(&[ConcreteType::String]));
1390        let ptr = unsafe { alloc_typed_closure_for_test(&layout, 55, 0) };
1391        // Allocate a string ValueWord (refcount = 1 initially).
1392        let s = ValueWord::from_string(Arc::new("hello".to_string()));
1393        let s_bits = s.into_raw_bits();
1394        // Simulate the emit_heap_closure retain + store:
1395        // store the bits at the heap capture offset, then retain.
1396        unsafe {
1397            let off = layout.heap_capture_offset(0);
1398            std::ptr::write(ptr.add(off) as *mut u64, s_bits);
1399            // The JIT retains via `atomic_rmw add [cap_ptr + 0], 1`. We simulate
1400            // this by cloning the ValueWord bits (which bumps the Arc refcount).
1401            let _retained = ValueWord::clone_from_bits(s_bits);
1402            std::mem::forget(_retained);
1403        }
1404        // Before finalizer: refcount should be 2 (original + retained for closure).
1405        let bits = unsafe {
1406            jit_finalize_heap_closure(ptr, 55, 1, Arc::as_ptr(&layout))
1407        };
1408        let vw = unsafe { ValueWord::clone_from_bits(bits) };
1409        let hv = vw.as_heap_ref().expect("heap");
1410        assert!(matches!(hv, HeapValue::ClosureRaw(..)));
1411        let handle = hv.as_closure_handle().expect("handle");
1412        // Widen the captured string bits through the shim — this calls
1413        // `read_capture_as_value_bits` (Ptr kind → verbatim 8-byte read).
1414        let captured_bits = handle.capture_as_value(0).into_raw_bits();
1415        let captured = unsafe { ValueWord::clone_from_bits(captured_bits) };
1416        let captured_str = captured.as_heap_ref().and_then(|h| match h {
1417            HeapValue::String(s) => Some(s.as_str().to_string()),
1418            _ => None,
1419        });
1420        assert_eq!(captured_str.as_deref(), Some("hello"));
1421        drop(captured);
1422        drop(vw);
1423        unsafe { drop_bits_via_raw(bits) };
1424        // Drop the original reference (released via its own ValueWord's Drop
1425        // when we reconstruct it).
1426        let _orig = unsafe { ValueWord::from_raw_bits(s_bits) };
1427        // If refcounts are balanced, this drop takes the last reference.
1428        drop(_orig);
1429    }
1430
1431    #[test]
1432    fn finalizer_multi_capture_layout_offsets() {
1433        // Exercise the plan's multi-capture example: (I64, F64, String).
1434        // Expected offsets: 16, 24, 32.
1435        let layout = Arc::new(immutable_layout(&[
1436            ConcreteType::I64,
1437            ConcreteType::F64,
1438            ConcreteType::String,
1439        ]));
1440        assert_eq!(layout.heap_capture_offset(0), 16);
1441        assert_eq!(layout.heap_capture_offset(1), 24);
1442        assert_eq!(layout.heap_capture_offset(2), 32);
1443        assert_eq!(layout.heap_capture_mask, 0b100);
1444    }
1445
1446    #[test]
1447    fn finalizer_preserves_function_id_from_header() {
1448        // The authoritative function_id is the one stored IN the header — the
1449        // FFI argument is ignored in favour of the in-block value.
1450        let layout = Arc::new(immutable_layout(&[]));
1451        let ptr = unsafe { alloc_typed_closure_for_test(&layout, 777, 0) };
1452        // Pass a different function_id via the FFI argument; finalizer must
1453        // still return a closure with function_id = 777.
1454        let bits = unsafe {
1455            jit_finalize_heap_closure(ptr, 555, 0, Arc::as_ptr(&layout))
1456        };
1457        let vw = unsafe { ValueWord::clone_from_bits(bits) };
1458        let hv = vw.as_heap_ref().expect("heap");
1459        assert!(matches!(hv, HeapValue::ClosureRaw(..)));
1460        let handle = hv.as_closure_handle().expect("handle");
1461        assert_eq!(handle.function_id() as u16, 777);
1462        drop(vw);
1463        unsafe { drop_bits_via_raw(bits) };
1464    }
1465
1466    #[test]
1467    fn finalizer_null_header_returns_none_tag() {
1468        // A null header is a codegen bug; finalizer returns TAG_NONE (as a
1469        // safety valve) rather than dereferencing null.
1470        let layout = Arc::new(immutable_layout(&[]));
1471        let bits = unsafe {
1472            jit_finalize_heap_closure(
1473                std::ptr::null_mut(),
1474                0,
1475                0,
1476                Arc::as_ptr(&layout),
1477            )
1478        };
1479        // Should not be a HeapValue::Closure.
1480        let vw = unsafe { ValueWord::from_raw_bits(bits) };
1481        assert!(vw.as_heap_ref().is_none(), "null-input must not decode as heap");
1482    }
1483
1484    #[test]
1485    fn finalizer_null_layout_returns_none_tag() {
1486        let layout = Arc::new(immutable_layout(&[]));
1487        let ptr = unsafe { alloc_typed_closure_for_test(&layout, 0, 0) };
1488        let bits = unsafe {
1489            jit_finalize_heap_closure(ptr, 0, 0, std::ptr::null())
1490        };
1491        let vw = unsafe { ValueWord::from_raw_bits(bits) };
1492        assert!(vw.as_heap_ref().is_none());
1493        // The header is leaked in this test (finalizer's safety valve
1494        // returns without dealloc). Free manually.
1495        unsafe {
1496            let size = layout.total_heap_size();
1497            let dl = std::alloc::Layout::from_size_align_unchecked(size, 8);
1498            std::alloc::dealloc(ptr, dl);
1499        }
1500    }
1501
1502    #[test]
1503    fn finalizer_layout_total_size_matches_alloc_shim_contract() {
1504        // Regression: the finalizer deallocates using
1505        // `Layout::from_size_align(layout.total_heap_size(), 8)`. This must
1506        // match `jit_v2_alloc_struct`'s allocation layout (size from the
1507        // compile-time ClosureLayout::total_heap_size(), align=8). A
1508        // mismatch would cause UB on dealloc.
1509        for types in [
1510            vec![],
1511            vec![ConcreteType::I64],
1512            vec![ConcreteType::F64, ConcreteType::I32],
1513            vec![ConcreteType::String, ConcreteType::F64, ConcreteType::Bool],
1514        ] {
1515            let layout = immutable_layout(&types);
1516            assert!(layout.total_heap_size() >= 16);
1517            assert_eq!(layout.total_heap_size() % 8, 0);
1518        }
1519    }
1520
1521    #[test]
1522    fn finalizer_interpreter_baseline_is_callable() {
1523        // Closure spec H6.5: both the JIT finalizer AND
1524        // `op_make_closure` (when a layout is available and captures are
1525        // immutable) produce a `HeapValue::ClosureRaw`. The VM dispatch
1526        // reads both backings through the same `VmClosureHandle` shim —
1527        // this is a structural regression test verifying the finalizer's
1528        // output is `ClosureRaw` and is readable via the shim.
1529        let layout = Arc::new(immutable_layout(&[ConcreteType::I64]));
1530        let ptr = unsafe { alloc_typed_closure_for_test(&layout, 3, 0) };
1531        unsafe {
1532            let off = layout.heap_capture_offset(0);
1533            std::ptr::write(
1534                ptr.add(off) as *mut u64,
1535                ValueWord::from_i64(7).into_raw_bits(),
1536            );
1537        }
1538        let bits = unsafe {
1539            jit_finalize_heap_closure(ptr, 3, 1, Arc::as_ptr(&layout))
1540        };
1541        let vw = unsafe { ValueWord::clone_from_bits(bits) };
1542        let is_closure_raw = matches!(
1543            vw.as_heap_ref(),
1544            Some(HeapValue::ClosureRaw(..))
1545        );
1546        assert!(
1547            is_closure_raw,
1548            "finalizer must produce HeapValue::ClosureRaw for H6.5 dispatch"
1549        );
1550        // Shim-backed read must surface the capture.
1551        let handle = vw.as_heap_ref().unwrap().as_closure_handle().unwrap();
1552        assert_eq!(handle.function_id() as u16, 3);
1553        assert_eq!(handle.capture_as_value(0).as_i64(), Some(7));
1554        drop(vw);
1555        unsafe { drop_bits_via_raw(bits) };
1556    }
1557
1558    #[test]
1559    fn finalizer_arc_strong_count_after_explicit_release() {
1560        // Structural refcount lifecycle test (H6.5-native).
1561        //
1562        // `emit_heap_closure` allocates the TypedClosureHeader with
1563        // refcount=1, writes captures, and retains each heap-typed
1564        // capture. The finalizer transfers the block's own share to a
1565        // fresh `OwnedClosureBlock` (embedded in
1566        // `HeapValue::ClosureRaw`). Dropping the ValueWord drops the
1567        // outer Arc<HeapValue> → drops OwnedClosureBlock →
1568        // `release_typed_closure` → decrements the outer-Arc share on
1569        // each heap-typed capture, then deallocates the block.
1570        //
1571        // The refcount observable via the ValueWord path is
1572        // `Arc<HeapValue>::strong_count` — the inner `Arc<String>` is
1573        // unrelated to the shared-heap retain protocol. Track the
1574        // outer count by pulling a dedicated ValueWord share
1575        // alongside the capture slot.
1576        let layout = Arc::new(immutable_layout(&[ConcreteType::String]));
1577        let ptr = unsafe { alloc_typed_closure_for_test(&layout, 71, 0) };
1578
1579        // Build a unique (non-interned) outer Arc<HeapValue::String> by
1580        // boxing a long string. The `from_string` interning path keys on
1581        // string content — 256+ bytes of repeated text skip the cache.
1582        let long_payload = "lifecycle".repeat(32);
1583        let original_vw = ValueWord::from_string(Arc::new(long_payload));
1584        let s_bits = original_vw.into_raw_bits();
1585
1586        // Acquire an independent Arc<HeapValue> "observer" share via
1587        // increment_strong_count + from_raw. This share is kept through
1588        // the test so `Arc::strong_count` reflects the live count; we
1589        // drop it at the very end.
1590        let outer_ptr = {
1591            let payload = shape_value::tag_bits::get_payload(s_bits);
1592            let masked = payload & shape_value::tag_bits::HEAP_PTR_MASK;
1593            masked as *const HeapValue
1594        };
1595        // +1 observer share on top of s_bits' own share.
1596        unsafe { Arc::increment_strong_count(outer_ptr); }
1597        let observer: Arc<HeapValue> = unsafe { Arc::from_raw(outer_ptr) };
1598        // observer share + s_bits share = 2 live shares.
1599        assert_eq!(Arc::strong_count(&observer), 2);
1600
1601        // Simulate emit_heap_closure: store the capture bits and retain
1602        // one extra share for the block (JIT `atomic_rmw add 1`).
1603        unsafe {
1604            let off = layout.heap_capture_offset(0);
1605            std::ptr::write(ptr.add(off) as *mut u64, s_bits);
1606            let _retained = ValueWord::clone_from_bits(s_bits);
1607            std::mem::forget(_retained);
1608        }
1609        // observer + s_bits + block = 3 shares.
1610        assert_eq!(Arc::strong_count(&observer), 3);
1611
1612        // Finalize — produces the ClosureRaw ValueWord.
1613        let bits = unsafe {
1614            jit_finalize_heap_closure(ptr, 71, 1, Arc::as_ptr(&layout))
1615        };
1616        // Still 3 shares — finalizer just wraps the block pointer.
1617        assert_eq!(Arc::strong_count(&observer), 3);
1618
1619        // ValueWord is a `u64` alias — it has no `Drop` impl. Release
1620        // the outer `Arc<HeapValue::ClosureRaw>` share by extracting the
1621        // payload pointer and calling `Arc::decrement_strong_count`. That
1622        // drops the HeapValue, which drops OwnedClosureBlock, which
1623        // calls `release_typed_closure` → block refcount 1→0 → heap-
1624        // capture mask walk → outer String Arc count 3→2.
1625        unsafe {
1626            let payload = shape_value::tag_bits::get_payload(bits);
1627            let block_ptr = (payload & shape_value::tag_bits::HEAP_PTR_MASK)
1628                as *const HeapValue;
1629            Arc::decrement_strong_count(block_ptr);
1630        }
1631        assert_eq!(Arc::strong_count(&observer), 2);
1632
1633        // Drop the original outer share via `s_bits`. ValueWord has no
1634        // Drop impl, so release the Arc share by hand.
1635        unsafe { Arc::decrement_strong_count(outer_ptr); }
1636        assert_eq!(Arc::strong_count(&observer), 1);
1637        drop(observer);
1638    }
1639}
1640
1641// W11: gated out — body uses deleted `shape_value::ValueWord` /
1642// `ValueWordExt` API. Kinded-FFI replacement deferred to §2.7.4 Phase 2c.
1643#[cfg(any())]
1644#[cfg(test)]
1645mod session_1_shared_local_lifecycle_tests {
1646    //! Session 1 Commit 3 unit tests for
1647    //! `jit_alloc_shared_cell` / `jit_arc_shared_release`.
1648    //!
1649    //! These helpers are the JIT-side counterparts of the interpreter
1650    //! handlers `op_alloc_shared_local` / `op_drop_shared_local`. The
1651    //! tests pin:
1652    //!   * alloc produces a non-null 8-byte aligned `*const SharedCell`
1653    //!     with the expected initial ValueWord bits;
1654    //!   * release consumes exactly one strong share and (when that was
1655    //!     the last share) frees the allocation;
1656    //!   * alloc + retain + release balances the refcount bookkeeping
1657    //!     exactly as the outer-scope lifecycle contract requires.
1658    use super::*;
1659    use shape_value::v2::closure_layout::{SharedCell, SHARED_CELL_VALUE_OFFSET};
1660    use shape_value::{ValueWord, ValueWordExt};
1661    use std::sync::Arc;
1662
1663    #[test]
1664    fn session1_ffi_alloc_shared_cell_roundtrip() {
1665        // Allocate a fresh shared cell from a well-formed ValueWord bit
1666        // pattern and verify the payload is readable at
1667        // `SHARED_CELL_VALUE_OFFSET` via a plain pointer dereference
1668        // (matching how the JIT's inline lock-gated path indexes the
1669        // payload).
1670        let initial = ValueWord::from_i64(1234).into_raw_bits();
1671        let ptr = unsafe { jit_alloc_shared_cell(initial) };
1672        assert_ne!(ptr, 0, "alloc must return a non-null pointer");
1673        assert_eq!(ptr % 8, 0, "SharedCell is 8-byte aligned");
1674
1675        // Reborrow the pointer to inspect the payload (matches JIT read).
1676        let cell: &SharedCell = unsafe { &*(ptr as *const SharedCell) };
1677        // Initial state: unlocked (state byte = 0).
1678        assert_eq!(
1679            cell.state.load(std::sync::atomic::Ordering::Relaxed),
1680            0,
1681            "freshly-allocated cell must be unlocked"
1682        );
1683        // Payload at offset 8 matches initial bits.
1684        let payload = unsafe {
1685            std::ptr::read((ptr as *const u8).add(SHARED_CELL_VALUE_OFFSET as usize)
1686                as *const u64)
1687        };
1688        assert_eq!(payload, initial, "payload must equal initial_bits");
1689
1690        // Release the sole strong share — the allocation is freed.
1691        unsafe { jit_arc_shared_release(ptr) };
1692    }
1693
1694    #[test]
1695    fn session1_ffi_alloc_shared_cell_independent_allocations() {
1696        // Two allocations must produce distinct pointers, each
1697        // holding their own initial payload.
1698        let a = unsafe { jit_alloc_shared_cell(ValueWord::from_i64(10).into_raw_bits()) };
1699        let b = unsafe { jit_alloc_shared_cell(ValueWord::from_i64(20).into_raw_bits()) };
1700        assert_ne!(a, 0);
1701        assert_ne!(b, 0);
1702        assert_ne!(a, b, "independent allocations must yield distinct pointers");
1703        unsafe {
1704            jit_arc_shared_release(a);
1705            jit_arc_shared_release(b);
1706        }
1707    }
1708
1709    #[test]
1710    fn session1_ffi_arc_shared_release_null_is_noop() {
1711        // `jit_arc_shared_release(0)` mirrors the interpreter's
1712        // null-pointer guard in `op_drop_shared_local` and must be a
1713        // silent no-op (defense-in-depth against codegen bugs).
1714        unsafe { jit_arc_shared_release(0) };
1715    }
1716
1717    #[test]
1718    fn session1_ffi_alloc_retain_release_strong_count_balanced() {
1719        // Full outer-scope + closure-capture lifecycle: alloc produces
1720        // one share, retain bumps to 2, the outer release takes it
1721        // back to 1, the capture release takes it to 0 and frees.
1722        let initial = ValueWord::from_i64(7).into_raw_bits();
1723        let ptr = unsafe { jit_alloc_shared_cell(initial) };
1724        // Observer: take an extra share to probe the refcount.
1725        let arc_observer: Arc<SharedCell> = unsafe {
1726            Arc::increment_strong_count(ptr as *const SharedCell);
1727            Arc::from_raw(ptr as *const SharedCell)
1728        };
1729        // observer + alloc = 2 strong shares.
1730        assert_eq!(Arc::strong_count(&arc_observer), 2);
1731
1732        // Simulate `ClosureCapture` operand path: retain one more share.
1733        let _retained = unsafe { jit_arc_shared_retain(ptr) };
1734        assert_eq!(Arc::strong_count(&arc_observer), 3);
1735
1736        // Outer-scope release (slot's share).
1737        unsafe { jit_arc_shared_release(ptr) };
1738        assert_eq!(Arc::strong_count(&arc_observer), 2);
1739
1740        // Capture release.
1741        unsafe { jit_arc_shared_release(ptr) };
1742        assert_eq!(Arc::strong_count(&arc_observer), 1);
1743
1744        // Last share is the observer — drop it to free.
1745        drop(arc_observer);
1746    }
1747
1748    #[test]
1749    fn session1_ffi_shared_cell_value_roundtrip_via_lock_helpers() {
1750        // Alloc, lock-gated write via FFI helpers (mirroring the JIT's
1751        // inline lock path with contended fallback), locked-gated read
1752        // returns the written bits.
1753        let ptr = unsafe {
1754            jit_alloc_shared_cell(ValueWord::from_i64(100).into_raw_bits())
1755        };
1756        unsafe {
1757            // Take the lock via the contended helper (always safe even
1758            // when uncontended).
1759            jit_shared_lock_contended(ptr);
1760            // Write a new value at offset 8.
1761            std::ptr::write(
1762                (ptr as *mut u8).add(SHARED_CELL_VALUE_OFFSET as usize) as *mut u64,
1763                ValueWord::from_i64(500).into_raw_bits(),
1764            );
1765            jit_shared_unlock_contended(ptr);
1766
1767            // Read back.
1768            jit_shared_lock_contended(ptr);
1769            let v = std::ptr::read(
1770                (ptr as *const u8).add(SHARED_CELL_VALUE_OFFSET as usize) as *const u64,
1771            );
1772            jit_shared_unlock_contended(ptr);
1773            assert_eq!(
1774                v,
1775                ValueWord::from_i64(500).into_raw_bits(),
1776                "locked write must be visible to locked read on the same cell"
1777            );
1778
1779            jit_arc_shared_release(ptr);
1780        }
1781    }
1782}