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}