Skip to main content

shape_value/v2/
closure_layout.rs

1//! Typed closure layout for v2 runtime.
2//!
3//! A `TypedClosure` parallels `TypedStruct`: it has an 8-byte `HeapHeader`
4//! followed by a `function_id: u32` / `type_id: u32` pair, then a compact
5//! C-style capture area with compile-time-known offsets.
6//!
7//! ## Memory layout
8//!
9//! ```text
10//! Heap variant (escaping closure):
11//!   Offset  Size  Field
12//!   ------  ----  -----
13//!     0       8   HeapHeader
14//!     8       4   function_id (u32)
15//!    12       4   type_id (u32, ClosureTypeId.0)
16//!    16+      ..  captures[] (C-laid-out per ClosureLayout)
17//!
18//! Stack variant (non-escaping closure, Cranelift StackSlot):
19//!   Offset  Size  Field
20//!   ------  ----  -----
21//!     0       4   function_id (u32)
22//!     4       4   type_id (u32, ClosureTypeId.0)
23//!     8+      ..  captures[]
24//! ```
25//!
26//! Captures start 8-byte aligned in both variants (HeapHeader and the
27//! function_id+type_id pair are both 8 bytes). The relative offset of each
28//! capture inside the captures area is the same for both variants — only
29//! the preceding header differs.
30//!
31//! ## Keying
32//!
33//! `ClosureTypeId`s are minted per **capture signature** (`Vec<ConcreteType>`),
34//! not per closure literal. The closure body is carried separately by
35//! `function_id`. Two literals with identical captures (e.g. two `|x| x + 1`
36//! expressions with no captures) share `ClosureTypeId(0)`. See
37//! `docs/v2-closure-specialization.md` §1.2.
38
39use super::concrete_type::{ClosureTypeId, ConcreteType};
40use super::struct_layout::{FieldInfo, FieldKind};
41use crate::heap_value::{
42    // V3-S5 ckpt-4 (2026-05-15): `TypedArrayData` import deleted — the
43    // enum was retired at ckpt-1 per W12-typed-array-data-deletion-audit
44    // §3.5 + ADR-006 §2.7.24 Q25.A SUPERSEDED. The
45    // `HeapKind::TypedArray` arm at line ~378 (drop dispatch) is
46    // 4-table-lockstep territory deferred to V3-S5 ckpt-5 — it stays
47    // cascade-broken at this checkpoint per multi-session chain step 2
48    // (broken state OK on feature branch).
49    AtomicData, ChannelData, DequeData, HashMapData, HashSetData, HeapKind, HeapValue,
50    IoHandleData, LazyData, MatrixData, MatrixSliceData, MutexData, NativeViewData,
51    PriorityQueueData, RangeData, TableViewData, TaskGroupData, TemporalData,
52    TraitObjectStorage, TypedObjectStorage,
53};
54use crate::native_kind::NativeKind;
55use std::collections::HashMap;
56use std::sync::Arc;
57
58/// Interior-mutable cell backing a `CaptureKind::Shared` capture.
59///
60/// A `Shared` capture slot stores `*const SharedCell` — a raw pointer
61/// obtained via `Arc::into_raw` on an `Arc<SharedCell>`. Each live slot
62/// holds exactly one strong-count share; closure Drop reclaims it with
63/// `Arc::from_raw(ptr).drop()`.
64///
65/// # ⚠ JIT-coupled ABI: payload offset is part of the contract
66///
67/// The 8-byte payload sits at offset 8 (`SHARED_CELL_VALUE_OFFSET`). The
68/// JIT in `crates/shape-jit/src/mir_compiler/places.rs` and the inline
69/// lock/unlock lowering in `shape-jit::ffi::object::closure` both read
70/// offset 8 directly via Cranelift codegen with this constant baked in.
71/// Changing the layout requires updating the JIT in lockstep — the
72/// `const _: () = { ... }` static assertion below catches a drifting
73/// definition at compile time, but a mismatch in the JIT's hardcoded
74/// constants would still need a manual audit. Per-FieldKind read/write
75/// helpers in `closure_raw.rs::read_shared_*` / `write_shared_*`
76/// reinterpret the 8-byte payload through narrower `FieldKind` widths
77/// for sub-8-byte scalar inner types but never change the physical
78/// offset.
79///
80/// # ABI and layout (Track A.1E)
81///
82/// Pre-A.1E this was a `parking_lot::Mutex<ValueWord>` type alias. The
83/// JIT Cranelift inline lock/unlock lowering in A.1E reads the lock
84/// state byte and the value payload at **hard-coded byte offsets**
85/// (state @ 0, value @ 8), so the cell is redefined as an explicit
86/// `#[repr(C)]` struct with a hand-rolled spinlock. This gives the JIT
87/// full ABI control without depending on parking_lot's (non-repr-C)
88/// internal layout. The interpreter continues to use the `.lock()`
89/// API, which returns a guard that supports `*guard = ...` and
90/// `let bits = *guard;` — so interpreter code paths stay unchanged.
91///
92/// ## Layout invariants (load-bearing for JIT)
93///
94/// - Offset 0: `AtomicU8` state. `0` = unlocked, `1` = locked. All other
95///   bit patterns are reserved — the JIT CAS is `0 → 1` for lock and
96///   `1 → 0` for unlock.
97/// - Offsets 1..=7: padding. Must be zero on construction but not read.
98/// - Offset 8: `ValueWord` payload (u64 bit pattern).
99/// - Trailing fields after offset 16: kind tracking (added by ADR-006
100///   §2.7.8 / Q10). NOT read by the JIT — JIT only touches state @ 0
101///   and value @ 8 via the `SHARED_CELL_*_OFFSET` constants below.
102///
103/// ## ADR-006 §2.7.8 / Q10 — parallel-kind invariant extended to cells
104///
105/// Cell-storage structs that hold raw heap-pointer bits grow a parallel
106/// `NativeKind` companion alongside their raw payload (per ADR-006
107/// §2.7.8 / Q10). For `SharedCell` the payload is single-slot
108/// (`UnsafeCell<u64>`), so the companion is a single `kind: NativeKind`
109/// field set at construction (`SharedCell::new(value, kind)`) and read
110/// at drop (`Drop for SharedCell`). The drop dispatch mirrors
111/// `KindedSlot::drop` in `kinded_slot.rs:274` — same retire-the-Arc
112/// matrix, same forbidden alternatives (no `vw_drop`, no `is_heap` probe,
113/// no Bool-default fallback). Construction sites must source the kind
114/// at the same call where the bits are sourced — see ADR-006 §2.7.8 for
115/// the binding rules.
116///
117/// ## Contention
118///
119/// The JIT's inline fast path is a single CAS from 0→1 for lock and
120/// 1→0 for unlock. On failure it calls the `jit_shared_lock_contended`
121/// / `jit_shared_unlock_contended` FFI helpers. The interpreter's
122/// `.lock()` method runs the same acquire-loop. Closure-capture
123/// contention is rare so a simple `spin_loop`-based wait is sufficient
124/// — no parking behaviour is preserved from the old parking_lot-based
125/// implementation.
126///
127/// Memory ordering: lock acquire is `Acquire`, lock release is `Release`,
128/// matching the standard `Mutex` contract.
129#[repr(C)]
130pub struct SharedCell {
131    /// Lock state byte at offset 0. `0` = unlocked, `1` = locked.
132    pub state: std::sync::atomic::AtomicU8,
133    /// Padding to align `value` to offset 8. Not read.
134    _pad: [u8; 7],
135    /// Value payload. Read/written only while the lock is held.
136    pub value: std::cell::UnsafeCell<u64>,
137    /// Per-cell `NativeKind` companion, set at construction and read at
138    /// drop (ADR-006 §2.7.8 / Q10). When `kind` selects a heap-bearing
139    /// arm, `value`'s bits are the result of `Arc::into_raw::<T>` for
140    /// the matching `T`, and `Drop` retires exactly one strong-count
141    /// share. For inline-scalar kinds (Int*, UInt*, Float64, Bool, ...)
142    /// drop is a no-op. Lockstep invariant: `kind` MUST stay in sync
143    /// with `value` — every write to `value` from a different kind goes
144    /// through `Drop` + `new()` (i.e. replace the whole cell), never
145    /// in-place reassignment of `value` alone. Mid-life kind changes
146    /// are forbidden.
147    ///
148    /// Located AFTER `value` so the JIT-baked offsets
149    /// (`SHARED_CELL_VALUE_OFFSET = 8`, `SHARED_CELL_STATE_OFFSET = 0`)
150    /// stay stable. The JIT reads only state and value; it does not
151    /// touch this field.
152    kind: NativeKind,
153}
154
155// SAFETY: SharedCell provides interior mutability guarded by its own
156// atomic state byte, matching the `Mutex<T: Send>: Send + Sync` contract.
157// ValueWord is a `u64` alias, trivially Send + Sync.
158unsafe impl Send for SharedCell {}
159unsafe impl Sync for SharedCell {}
160
161const _: () = {
162    // Load-bearing for the JIT Cranelift lowering: the state byte MUST be
163    // at offset 0 and the value at offset 8. If these layout assumptions
164    // ever drift, the JIT's inline CAS on the state byte and the
165    // `load/store.i64 [ptr + 8]` on the value would touch the wrong
166    // bytes. The JIT reads these offsets as compile-time constants
167    // (`SHARED_CELL_STATE_OFFSET` / `SHARED_CELL_VALUE_OFFSET` in
168    // `shape-jit::ffi::object::closure`), so a mismatch surfaces as a
169    // hard build error here, not a runtime miscompile.
170    //
171    // The total struct size grew from 16 to 24 bytes when the §2.7.8 / Q10
172    // `kind: NativeKind` companion field landed (added AFTER `value` so
173    // the JIT-baked offsets are unaffected). The JIT does not read total
174    // size — only the two offset constants below — so the size delta is
175    // safe.
176    assert!(std::mem::align_of::<SharedCell>() == 8);
177    assert!(std::mem::offset_of!(SharedCell, state) == 0);
178    assert!(std::mem::offset_of!(SharedCell, value) == 8);
179};
180
181/// Byte offset of the lock state byte within [`SharedCell`]. The JIT's
182/// inline lock CAS targets this offset as a compile-time constant.
183pub const SHARED_CELL_STATE_OFFSET: i32 = 0;
184
185/// Byte offset of the value payload within [`SharedCell`]. The JIT's
186/// inline load/store targets this offset as a compile-time constant.
187pub const SHARED_CELL_VALUE_OFFSET: i32 = 8;
188
189const _: () = {
190    // Tie the public JIT-facing `SHARED_CELL_VALUE_OFFSET` constant to the
191    // actual struct field offset. If `SharedCell` is ever re-laid-out
192    // (e.g. by adding a field before `value`, or changing the padding)
193    // this assertion fires before the JIT can miscompile — and the
194    // narrower-`FieldKind` payload helpers in `closure_raw.rs::read_shared_*`
195    // / `write_shared_*` rely on the same constant for their reads.
196    assert!(SHARED_CELL_VALUE_OFFSET as usize == std::mem::offset_of!(SharedCell, value));
197    assert!(SHARED_CELL_STATE_OFFSET as usize == std::mem::offset_of!(SharedCell, state));
198};
199
200/// Locked state byte value.
201pub const SHARED_CELL_LOCKED: u8 = 1;
202/// Unlocked state byte value.
203pub const SHARED_CELL_UNLOCKED: u8 = 0;
204
205impl SharedCell {
206    /// Construct a new unlocked cell holding `value` with the matching
207    /// `NativeKind` companion (ADR-006 §2.7.8 / Q10).
208    ///
209    /// `kind` MUST classify `value`'s bits at construction. When `kind`
210    /// selects a heap-bearing arm (e.g. `NativeKind::String`,
211    /// `NativeKind::Ptr(_)`), `value` MUST be the result of
212    /// `Arc::into_raw::<T>` for the matching `T` and the caller transfers
213    /// exactly one strong-count share into the cell. `Drop` retires that
214    /// share when the last `Arc<SharedCell>` share is released. For
215    /// inline-scalar kinds the bits are the raw scalar value and `Drop`
216    /// is a no-op for the value field.
217    ///
218    /// Mid-life kind changes are forbidden: every write that changes the
219    /// kind must replace the whole cell (drop + reconstruct), never
220    /// reassign `value` alone — the lockstep invariant matches the
221    /// stack-side §2.7.7 rule.
222    #[inline]
223    pub fn new(value: u64, kind: NativeKind) -> Self {
224        Self {
225            state: std::sync::atomic::AtomicU8::new(SHARED_CELL_UNLOCKED),
226            _pad: [0; 7],
227            value: std::cell::UnsafeCell::new(value),
228            kind,
229        }
230    }
231
232    /// Read the cell's `NativeKind` companion.
233    ///
234    /// Set once at construction; never changes during the cell's lifetime
235    /// (ADR-006 §2.7.8 / Q10 lockstep invariant). Callers that need to
236    /// drop the cell's value through `KindedSlot` / `drop_with_kind`
237    /// dispatch read this and pass it alongside the value bits.
238    #[inline]
239    pub fn kind(&self) -> NativeKind {
240        self.kind
241    }
242
243    /// Acquire the lock, blocking (spinning) until the state byte
244    /// transitions from `0` to `1`. Returns a RAII guard that unlocks
245    /// on Drop.
246    ///
247    /// Memory ordering: `Acquire` on the successful CAS, so all writes
248    /// protected by the lock on the previous owner are visible here.
249    #[inline]
250    pub fn lock(&self) -> SharedCellGuard<'_> {
251        use std::sync::atomic::Ordering;
252        // Uncontended fast path: single CAS 0→1.
253        if self
254            .state
255            .compare_exchange(
256                SHARED_CELL_UNLOCKED,
257                SHARED_CELL_LOCKED,
258                Ordering::Acquire,
259                Ordering::Relaxed,
260            )
261            .is_ok()
262        {
263            return SharedCellGuard { cell: self };
264        }
265        // Contended slow path: spin-wait.
266        self.lock_contended();
267        SharedCellGuard { cell: self }
268    }
269
270    /// Spin-wait on the state byte until it becomes `0` and we
271    /// successfully flip it to `1`. Uses `spin_loop` hints to ease the
272    /// CPU during the busy-wait. Closure-capture contention is rare in
273    /// practice so the simplicity of a spinlock is acceptable.
274    ///
275    /// `pub` so the JIT's `jit_shared_lock_contended` FFI helper can
276    /// call it directly on a `&SharedCell` reborrowed from the raw
277    /// pointer bits stored in a capture slot. The lock transitions from
278    /// `0` → `1` with `Acquire` ordering and does NOT return a guard —
279    /// the JIT-emitted body is responsible for the matching unlock.
280    #[cold]
281    #[inline(never)]
282    pub fn lock_contended(&self) {
283        use std::sync::atomic::Ordering;
284        loop {
285            // Spin-wait for the state byte to show unlocked. Use a
286            // relaxed load in the inner spin (the CAS below does the
287            // acquire ordering on success).
288            while self.state.load(Ordering::Relaxed) != SHARED_CELL_UNLOCKED {
289                std::hint::spin_loop();
290            }
291            if self
292                .state
293                .compare_exchange_weak(
294                    SHARED_CELL_UNLOCKED,
295                    SHARED_CELL_LOCKED,
296                    Ordering::Acquire,
297                    Ordering::Relaxed,
298                )
299                .is_ok()
300            {
301                return;
302            }
303        }
304    }
305
306    /// Release the lock. Only the current lock holder may call this.
307    ///
308    /// # Safety
309    ///
310    /// The caller must currently hold the lock (state == 1). Callers
311    /// other than `SharedCellGuard::drop` must guarantee this manually;
312    /// the normal path is to let the guard go out of scope.
313    ///
314    /// `pub` so the JIT's `jit_shared_unlock_contended` FFI helper can
315    /// call it on a `&SharedCell` reborrowed from a capture slot.
316    #[inline]
317    pub unsafe fn unlock(&self) {
318        use std::sync::atomic::Ordering;
319        self.state.store(SHARED_CELL_UNLOCKED, Ordering::Release);
320    }
321}
322
323/// Retire the inner `value` share when the cell itself is dropped
324/// (ADR-006 §2.7.8 / Q10 — "set at construction, read at drop").
325///
326/// The cell is wrapped in `Arc<SharedCell>`; this `Drop` fires only when
327/// the last `Arc` share retires. At that point the `value` slot's bits
328/// must release whatever resource the `kind` companion classifies them
329/// as. This mirrors `KindedSlot::drop` in `kinded_slot.rs:274` exactly —
330/// same Arc-decrement matrix, same forbidden alternatives:
331///
332/// - **No `vw_drop(bits)`** (forbidden #8 per CLAUDE.md / ADR-006 §2.7.7):
333///   the dispatch is on `self.kind`, not on tag bits.
334/// - **No `is_heap()` / "drop only if heap-shaped" probe** (forbidden #7):
335///   the kind already encodes the discriminator; inline-scalar arms fall
336///   through to a no-op without probing.
337/// - **No Bool-default fallback** (§2.7.7 #9): the kind is always
338///   concrete — set at construction, never `Unknown`/`Pending`/`Dynamic`
339///   (those `NativeKind` variants are deleted).
340impl Drop for SharedCell {
341    fn drop(&mut self) {
342        // SAFETY: we hold the cell exclusively (last Arc share is
343        // retiring), so we can read the `UnsafeCell<u64>` payload
344        // without acquiring the spinlock — no other thread can touch it.
345        let bits = unsafe { *self.value.get() };
346        if bits == 0 {
347            return;
348        }
349        // SAFETY: per the construction-side contract on `SharedCell::new`,
350        // when `self.kind` selects a heap-bearing arm the `bits` are the
351        // result of `Arc::into_raw::<T>` for the matching `T`. The cell
352        // owned exactly one strong-count share for the value's lifetime;
353        // we retire it here. For inline-scalar kinds the bits are a raw
354        // scalar value and drop is a no-op.
355        unsafe {
356            match self.kind {
357                NativeKind::String => {
358                    Arc::decrement_strong_count(bits as *const String);
359                }
360                // Wave 2 Agent B (ADR-006 §2.7.5 amendment, 2026-05-14):
361                // A `SharedCell` whose `kind` companion is
362                // `NativeKind::StringV2` / `NativeKind::DecimalV2` carries
363                // `ptr as u64` where `ptr: *const StringObj` / `*const
364                // DecimalObj`. Retire one refcount share at cell drop via
365                // `release_elem` (HeapElement trait — calls `v2_release`
366                // against the HeapHeader at offset 0; on refcount=0 the
367                // carrier-side `drop` deallocates the `repr(C)` struct).
368                NativeKind::StringV2 => {
369                    use crate::v2::heap_element::HeapElement;
370                    crate::v2::string_obj::StringObj::release_elem(
371                        bits as *const crate::v2::string_obj::StringObj,
372                    );
373                }
374                NativeKind::DecimalV2 => {
375                    use crate::v2::heap_element::HeapElement;
376                    crate::v2::decimal_obj::DecimalObj::release_elem(
377                        bits as *const crate::v2::decimal_obj::DecimalObj,
378                    );
379                }
380                NativeKind::Ptr(hk) => match hk {
381                    HeapKind::String => {
382                        Arc::decrement_strong_count(bits as *const String);
383                    }
384                    // r5c-2-β-δ-(α) (2026-05-20): `HeapKind::TypedArray`
385                    // dispatch arm RE-INSTATED — the V3-S5 ckpt-5-prime
386                    // "no live slot bits carry this kind" claim was
387                    // empirically false (a `var`-shared `Array<T>` flows a
388                    // v2-raw `*mut TypedArray<T>` carrier through a
389                    // `SharedCell`). `release_v2_typed_array` retires one
390                    // refcount share and, on the last share, frees via the
391                    // stamped-element-type `drop_array` / `drop_array_heap`.
392                    // Mirror of the `TypedObject` release arm below
393                    // (4-table lockstep, ADR-006 §2.3 / §2.7.7).
394                    HeapKind::TypedArray => {
395                        crate::v2::typed_array::release_v2_typed_array(bits as *mut u8);
396                    }
397                    // Wave 2 Agent D4 ckpt-2 (ADR-006 §2.3 / §2.7.5
398                    // amendment, 2026-05-14): a `SharedCell` whose
399                    // single-slot payload is a
400                    // `NativeKind::Ptr(HeapKind::TypedObject)` carries
401                    // `ptr as u64` where `ptr: *const TypedObjectStorage`
402                    // (v2-raw carrier per Agent D1's `_new` /
403                    // `impl HeapElement for TypedObjectStorage`). Retire
404                    // one refcount share at cell drop via `release_elem`
405                    // (HeapElement trait — calls `v2_release` against the
406                    // HeapHeader at offset 0; on refcount=0 the
407                    // carrier-side `_drop` runs the per-field heap-mask
408                    // walk and deallocates). Mirror of the §2.7.5 StringV2
409                    // / DecimalV2 release arms above.
410                    HeapKind::TypedObject => {
411                        use crate::v2::heap_element::HeapElement;
412                        TypedObjectStorage::release_elem(
413                            bits as *const TypedObjectStorage,
414                        );
415                    }
416                    HeapKind::HashMap => {
417                        // Wave 2 Round 3b C2-joint ckpt-2 (2026-05-14):
418                        // bits are `Arc::into_raw(Arc<HashMapKindedRef>)`;
419                        // release dispatches outer Arc decrement → enum
420                        // Drop chains to per-V `Arc<HashMapData<V>>` release.
421                        Arc::decrement_strong_count(
422                            bits as *const crate::heap_value::HashMapKindedRef,
423                        );
424                    }
425                    // Wave 13 W13-hashset-rebuild (ADR-006 §2.7.15 / Q16,
426                    // 2026-05-10): mirror of the HashMap arm. A
427                    // SharedCell whose single-slot payload is a
428                    // `NativeKind::Ptr(HeapKind::HashSet)` carries
429                    // `Arc::into_raw(Arc<HashSetData>) as u64`. Retire
430                    // one `Arc<HashSetData>` strong-count share at cell
431                    // drop. Same dispatch shape as HashMap (HashSet is
432                    // a HashMap sibling per §2.7.15).
433                    HeapKind::HashSet => {
434                        Arc::decrement_strong_count(bits as *const HashSetData);
435                    }
436                    // Wave 15 W15-deque (ADR-006 §2.7.19 / Q20,
437                    // 2026-05-10): mirror of the HashSet arm. A
438                    // SharedCell whose single-slot payload is a
439                    // `NativeKind::Ptr(HeapKind::Deque)` carries
440                    // `Arc::into_raw(Arc<DequeData>) as u64`. Retire
441                    // one `Arc<DequeData>` strong-count share at cell
442                    // drop. Deque is a HashSet sibling per §2.7.19.
443                    HeapKind::Deque => {
444                        Arc::decrement_strong_count(bits as *const DequeData);
445                    }
446                    // Wave 15 W15-channel-rebuild (ADR-006 §2.7.20 / Q21,
447                    // 2026-05-10): mirror of the HashSet arm. A
448                    // `SharedCell` whose single-slot payload is a
449                    // `NativeKind::Ptr(HeapKind::Channel)` carries
450                    // `Arc::into_raw(Arc<ChannelData>) as u64`. Retire
451                    // one `Arc<ChannelData>` strong-count share at cell
452                    // drop. The Channel is the first concurrency
453                    // primitive to flow through the §2.7.8 / Q10
454                    // cell-storage parallel-kind track.
455                    HeapKind::Channel => {
456                        Arc::decrement_strong_count(bits as *const ChannelData);
457                    }
458                    // W17-concurrency (ADR-006 §2.7.25, 2026-05-11):
459                    // Mutex / Atomic / Lazy mirror the Channel arm at
460                    // the §2.7.8 / Q10 cell-storage parallel-kind
461                    // track. A `SharedCell` whose single-slot payload
462                    // is a `NativeKind::Ptr(HeapKind::Mutex/Atomic/Lazy)`
463                    // carries `Arc::into_raw(Arc<MutexData/AtomicData/
464                    // LazyData>) as u64`. Retire one strong-count
465                    // share at cell drop. Same dispatch shape as
466                    // Channel (concurrency primitives, full HeapValue
467                    // arm per §2.7.25).
468                    HeapKind::Mutex => {
469                        Arc::decrement_strong_count(bits as *const MutexData);
470                    }
471                    HeapKind::Atomic => {
472                        Arc::decrement_strong_count(bits as *const AtomicData);
473                    }
474                    HeapKind::Lazy => {
475                        Arc::decrement_strong_count(bits as *const LazyData);
476                    }
477                    // W17-trait-object-storage (ADR-006 §2.7.24 / Q25.C,
478                    // 2026-05-11): a `SharedCell` whose single-slot
479                    // payload is a `NativeKind::Ptr(HeapKind::TraitObject)`
480                    // carries `Arc::into_raw(Arc<TraitObjectStorage>)
481                    // as u64`. Retire one strong-count share at cell
482                    // drop — auto-derived `TraitObjectStorage::Drop`
483                    // releases the inner value + vtable Arcs at
484                    // refcount=0.
485                    // Wave 2 Agent D4 ckpt-2 (ADR-006 §2.7.24 / Q25.C.5 +
486                    // E close 2026-05-14): TraitObject release via
487                    // `HeapElement::release_elem` + carrier-side `_drop`
488                    // (per Agent E's `impl HeapElement for
489                    // TraitObjectStorage`). Mirror of the TypedObject
490                    // arm above.
491                    HeapKind::TraitObject => {
492                        use crate::v2::heap_element::HeapElement;
493                        TraitObjectStorage::release_elem(
494                            bits as *const TraitObjectStorage,
495                        );
496                    }
497                    HeapKind::Decimal => {
498                        Arc::decrement_strong_count(bits as *const rust_decimal::Decimal);
499                    }
500                    HeapKind::BigInt => {
501                        Arc::decrement_strong_count(bits as *const i64);
502                    }
503                    HeapKind::DataTable => {
504                        Arc::decrement_strong_count(bits as *const crate::datatable::DataTable);
505                    }
506                    HeapKind::IoHandle => {
507                        Arc::decrement_strong_count(bits as *const IoHandleData);
508                    }
509                    HeapKind::NativeView => {
510                        Arc::decrement_strong_count(bits as *const NativeViewData);
511                    }
512                    HeapKind::Content => {
513                        Arc::decrement_strong_count(bits as *const crate::content::ContentNode);
514                    }
515                    HeapKind::Instant => {
516                        Arc::decrement_strong_count(bits as *const std::time::Instant);
517                    }
518                    HeapKind::Temporal => {
519                        Arc::decrement_strong_count(bits as *const TemporalData);
520                    }
521                    HeapKind::TableView => {
522                        Arc::decrement_strong_count(bits as *const TableViewData);
523                    }
524                    HeapKind::TaskGroup => {
525                        Arc::decrement_strong_count(bits as *const TaskGroupData);
526                    }
527                    // Wave-γ G-heap-filter-expr (ADR-006 §2.3 / §2.7.6 / Q8
528                    // amendment): FilterExpr cells own one
529                    // `Arc::into_raw(Arc<FilterNode>)` strong-count share.
530                    // Pre-amendment the FilterExpr branch reused
531                    // `HeapKind::NativeView` as its kind label and dispatched
532                    // here as `Arc<NativeViewData>` — wrong-type retain/release
533                    // (Wave-α D-raw-helpers `a27c0e4` surfaced the gap).
534                    HeapKind::FilterExpr => {
535                        Arc::decrement_strong_count(bits as *const crate::value::FilterNode);
536                    }
537                    // Wave 8 W8-T26 (ADR-006 §2.7.13 / Q14, 2026-05-10):
538                    // a `SharedCell` whose single-slot payload is a
539                    // `NativeKind::Ptr(HeapKind::Reference)` carries
540                    // `Arc::into_raw(Arc<RefTarget>) as u64` directly
541                    // (mirror of FilterExpr's pure-discriminator-style
542                    // dispatch — NOT a `Box<HeapValue>` wrap). Retire one
543                    // `Arc<RefTarget>` strong-count share at cell drop.
544                    HeapKind::Reference => {
545                        Arc::decrement_strong_count(bits as *const crate::reference::RefTarget);
546                    }
547                    // W13-iterator-state (ADR-006 §2.7.16 / Q17,
548                    // 2026-05-10): a `SharedCell` whose single-slot
549                    // payload is a
550                    // `NativeKind::Ptr(HeapKind::Iterator)` carries
551                    // `Arc::into_raw(Arc<IteratorState>) as u64`
552                    // directly (mirror of FilterExpr / Reference's
553                    // typed-Arc dispatch — NOT a `Box<HeapValue>`
554                    // wrap). Retire one `Arc<IteratorState>`
555                    // strong-count share at cell drop.
556                    HeapKind::Iterator => {
557                        Arc::decrement_strong_count(
558                            bits as *const crate::iterator_state::IteratorState,
559                        );
560                    }
561                    // Wave 15 W15-priority-queue (ADR-006 §2.7.18 / Q19,
562                    // 2026-05-10): mirror of the HashSet arm. A
563                    // SharedCell whose single-slot payload is a
564                    // `NativeKind::Ptr(HeapKind::PriorityQueue)` carries
565                    // `Arc::into_raw(Arc<PriorityQueueData>) as u64`.
566                    // Retire one `Arc<PriorityQueueData>` strong-count
567                    // share at cell drop. Same dispatch shape as
568                    // HashSet (PriorityQueue is a HashSet sibling per
569                    // §2.7.18).
570                    HeapKind::PriorityQueue => {
571                        Arc::decrement_strong_count(bits as *const PriorityQueueData);
572                    }
573                    // W15-range (ADR-006 §2.7.23 / Q24, 2026-05-10): a
574                    // `SharedCell` whose single-slot payload is a
575                    // `NativeKind::Ptr(HeapKind::Range)` carries
576                    // `Arc::into_raw(Arc<RangeData>) as u64` directly
577                    // (typed-Arc shape, mirror of HashMap / HashSet /
578                    // Iterator). Retire one `Arc<RangeData>`
579                    // strong-count share at cell drop.
580                    HeapKind::Range => {
581                        Arc::decrement_strong_count(bits as *const RangeData);
582                    }
583                    // Wave 14 W14-variant-codegen (ADR-006 §2.7.17 / Q18,
584                    // 2026-05-10): a `SharedCell` whose single-slot
585                    // payload is `NativeKind::Ptr(HeapKind::Result)` /
586                    // `NativeKind::Ptr(HeapKind::Option)` carries
587                    // `Arc::into_raw(Arc<ResultData>) as u64` /
588                    // `Arc::into_raw(Arc<OptionData>) as u64` directly
589                    // (mirror of Iterator typed-Arc dispatch). Retire
590                    // one matching strong-count share at cell drop.
591                    HeapKind::Result => {
592                        Arc::decrement_strong_count(
593                            bits as *const crate::heap_value::ResultData,
594                        );
595                    }
596                    HeapKind::Option => {
597                        Arc::decrement_strong_count(
598                            bits as *const crate::heap_value::OptionData,
599                        );
600                    }
601                    // Char: inline-scalar payload (codepoint bits, not an
602                    // `Arc<T>`). Drop is a no-op; non-zero bits are valid.
603                    HeapKind::Char => {
604                        // No-op: inline-scalar payload.
605                    }
606                    // Round 2.5b W7-closure-retain-parallel (ADR-006
607                    // §2.7.11 / Q12, 2026-05-09 — lockstep with vm-tier
608                    // Round 2.5 close `5fa4b19`): a `SharedCell` whose
609                    // single-slot payload is a
610                    // `NativeKind::Ptr(HeapKind::Closure)` carries
611                    // `Arc::into_raw(Arc<HeapValue>) as u64` pointing
612                    // to a `HeapValue::ClosureRaw(OwnedClosureBlock)`
613                    // arm — the share carrier at the slot tier is the
614                    // outer `Arc<HeapValue>`. Round 2 close (`06cdfce`)
615                    // committed to this slot-bits shape via
616                    // `callee.slot.as_heap_value()` →
617                    // `HeapValue::ClosureRaw(block)`. Same dispatch
618                    // shape as the `HeapKind::FilterExpr` §2.7.9
619                    // amendment (one variant, one matching `Arc<T>`
620                    // retire at the slot tier).
621                    HeapKind::Closure => {
622                        Arc::decrement_strong_count(bits as *const HeapValue);
623                    }
624                    // `Ptr(HeapKind::Future)` carries the future-id u64
625                    // directly in `bits` (inline scalar — no `Arc<T>`
626                    // payload). See `async_ops/mod.rs` §"Wave 6.5 /
627                    // E-async migration" docstring. Same shape as
628                    // `HeapKind::Char`.
629                    HeapKind::Future => {
630                        // No-op: future-id inline scalar.
631                    }
632                    // W17-comptime-vm-dispatch (ADR-006 §2.7.26, 2026-05-12):
633                    // module-fn-id inline scalar payload — no `Arc<T>`,
634                    // no heap state. Same shape as `HeapKind::Future` /
635                    // `HeapKind::Char`. A SharedCell carrying a
636                    // ModuleFn-labeled inner payload retires no
637                    // refcount share.
638                    HeapKind::ModuleFn => {
639                        // No-op: module-fn-id inline scalar.
640                    }
641                    // Wave 8 W8-T25 (ADR-006 §2.7.12 / Q13 amendment,
642                    // 2026-05-10): a `SharedCell` whose `kind` companion
643                    // is `NativeKind::Ptr(HeapKind::SharedCell)` carries
644                    // an inner `Arc::into_raw(Arc<SharedCell>) as u64`
645                    // pointer — the closure-capture shape where one
646                    // shared-mutable variable is itself captured shared
647                    // into another closure (the inner SharedCell wraps
648                    // an outer SharedCell cell-pointer). Retires one
649                    // `Arc<SharedCell>` strong-count share. Same dispatch
650                    // shape as the `HeapKind::FilterExpr` §2.7.9 amendment
651                    // (one variant, one matching `Arc<T>` retire at the
652                    // cell-storage tier).
653                    HeapKind::SharedCell => {
654                        Arc::decrement_strong_count(bits as *const SharedCell);
655                    }
656                    // ADR-006 §2.7.22 amendment (Round 18 S3, 2026-05-13):
657                    // a `SharedCell` whose `kind` companion is
658                    // `NativeKind::Ptr(HeapKind::Matrix)` /
659                    // `NativeKind::Ptr(HeapKind::MatrixSlice)` carries
660                    // `Arc::into_raw(Arc<MatrixData>) as u64` /
661                    // `Arc::into_raw(Arc<MatrixSliceData>) as u64` directly
662                    // (typed-Arc pure-discriminator dispatch, mirror of
663                    // §2.7.9 FilterExpr / §2.7.13 Reference). Retire one
664                    // matching strong-count share at cell drop.
665                    HeapKind::Matrix => {
666                        Arc::decrement_strong_count(bits as *const MatrixData);
667                    }
668                    HeapKind::MatrixSlice => {
669                        Arc::decrement_strong_count(bits as *const MatrixSliceData);
670                    }
671                    // `HeapKind::NativeScalar` has no kinded `Arc<T>`
672                    // carrier yet — the redesign is the phase-2c
673                    // surface tracked in ADR-006 §2.7.4. When the
674                    // kinded NativeScalar carrier lands, this arm
675                    // wires its release per the chosen share carrier
676                    // (per the playbook's surface-and-stop discipline
677                    // — no Bool-default fallback). Until then, a
678                    // non-zero pointer with this kind is a
679                    // construction-side bug.
680                    HeapKind::NativeScalar => {
681                        debug_assert!(
682                            false,
683                            "SharedCell::drop: NativeScalar kinded carrier pending \
684                             phase-2c kinded redesign (ADR-006 §2.7.4)"
685                        );
686                    }
687                },
688                // Inline-scalar kinds: nothing to decrement. Bits are a
689                // raw value, not a pointer.
690                NativeKind::Float64
691                | NativeKind::NullableFloat64
692                | NativeKind::Int8
693                | NativeKind::NullableInt8
694                | NativeKind::UInt8
695                | NativeKind::NullableUInt8
696                | NativeKind::Int16
697                | NativeKind::NullableInt16
698                | NativeKind::UInt16
699                | NativeKind::NullableUInt16
700                | NativeKind::Int32
701                | NativeKind::NullableInt32
702                | NativeKind::UInt32
703                | NativeKind::NullableUInt32
704                | NativeKind::Int64
705                | NativeKind::NullableInt64
706                | NativeKind::UInt64
707                | NativeKind::NullableUInt64
708                | NativeKind::IntSize
709                | NativeKind::NullableIntSize
710                | NativeKind::UIntSize
711                | NativeKind::NullableUIntSize
712                | NativeKind::Bool
713                // Round 19 S1.5 W12-nativekind-scalar-additions
714                // (2026-05-14): Float32 + Char are inline 4-byte scalars
715                // per ADR-006 §2.7.5 amendment. A `SharedCell` whose
716                // `kind` companion is one of these stores raw f32 bit
717                // pattern / `c as u32` codepoint bits zero-extended into
718                // the low 32 bits of the 8-byte cell. No `Arc<T>`
719                // payload, no refcount work at cell drop.
720                | NativeKind::Float32
721                | NativeKind::Char
722                // R5b-2-bool-null-sentinel-cluster (ADR-006 §2.7 +
723                // §2.7.5 + §2.7.7/Q9, 2026-05-19): `NativeKind::Null`
724                // is a non-parametric absence-of-value sentinel — no
725                // Arc<T> payload, no refcount work at cell drop.
726                | NativeKind::Null => {}
727            }
728        }
729    }
730}
731
732/// RAII guard returned by [`SharedCell::lock`]. Releases the lock on
733/// Drop. Dereffs to the inner `ValueWord`.
734pub struct SharedCellGuard<'a> {
735    cell: &'a SharedCell,
736}
737
738impl<'a> std::ops::Deref for SharedCellGuard<'a> {
739    type Target = u64;
740    #[inline]
741    fn deref(&self) -> &u64 {
742        // SAFETY: holding the guard implies the lock is held, so we
743        // have exclusive access to the UnsafeCell payload.
744        unsafe { &*self.cell.value.get() }
745    }
746}
747
748impl<'a> std::ops::DerefMut for SharedCellGuard<'a> {
749    #[inline]
750    fn deref_mut(&mut self) -> &mut u64 {
751        // SAFETY: see `deref`.
752        unsafe { &mut *self.cell.value.get() }
753    }
754}
755
756impl<'a> Drop for SharedCellGuard<'a> {
757    #[inline]
758    fn drop(&mut self) {
759        // SAFETY: we hold the lock (guard construction acquired it);
760        // `unlock` transitions state 1→0 via a `Release` store.
761        unsafe { self.cell.unlock() };
762    }
763}
764
765impl std::fmt::Debug for SharedCell {
766    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
767        f.debug_struct("SharedCell").finish_non_exhaustive()
768    }
769}
770
771/// Storage discipline for a closure capture.
772///
773/// Each capture index i has exactly one `CaptureKind`. The three kinds
774/// are mutually exclusive and map to three mutually-exclusive bitmasks
775/// on `ClosureLayout` (`heap_capture_mask`, `owned_mutable_capture_mask`,
776/// `shared_capture_mask`).
777///
778/// - **`Immutable`** — `let` by-move/copy captures. The slot's width
779///   follows `capture_types[i]` via [`FieldKind`]; reads and writes go
780///   through [`super::closure_raw::read_capture_as_value_bits`] and
781///   [`super::closure_raw::write_capture_typed`] as today. If the
782///   underlying field kind is `Ptr`, the slot owns one heap-refcount
783///   share (participates in `heap_capture_mask`).
784/// - **`OwnedMutable`** — `let mut` by-move captures. The 8-byte slot
785///   holds `*mut ValueWord` obtained from `Box::into_raw(Box::new(...))`.
786///   Exactly one closure owns the box; Drop reclaims it with
787///   `Box::from_raw`. The interior `ValueWord` can itself carry heap
788///   refcount shares — those must be dropped before the box is freed.
789/// - **`Shared`** — `var` captures shared across nested closures. The
790///   8-byte slot holds `*const SharedCell` obtained from
791///   `Arc::into_raw(Arc::new(Mutex::new(...)))`. Each slot counts as one
792///   `Arc` strong share; reads/writes take the parking_lot mutex.
793#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
794pub enum CaptureKind {
795    /// `let` binding: value in slot, width per `FieldKind`.
796    Immutable,
797    /// `let mut` binding: Ptr slot holds `*mut ValueWord` (Box cell).
798    OwnedMutable,
799    /// `var` binding: Ptr slot holds `*const SharedCell`
800    /// (`Arc<parking_lot::Mutex<ValueWord>>` via `Arc::into_raw`).
801    Shared,
802}
803
804/// Byte size of the heap closure header: `HeapHeader (8) + function_id (4) + type_id (4)`.
805pub const HEAP_CLOSURE_HEADER_SIZE: usize = 16;
806
807/// Byte size of the stack closure header: `function_id (4) + type_id (4)`.
808pub const STACK_CLOSURE_HEADER_SIZE: usize = 8;
809
810/// Heap-allocated closure. The `HeapHeader` is at offset 0; captures follow
811/// the `function_id`/`type_id` pair at offset 16.
812///
813/// This is a layout marker used by JIT/VM codegen — captures are not declared
814/// as Rust fields because their number and types are only known per
815/// `ClosureTypeId`.
816#[repr(C)]
817pub struct TypedClosureHeader {
818    pub header: super::heap_header::HeapHeader, // offset 0, 8 bytes
819    pub function_id: u32,                       // offset 8, 4 bytes
820    pub type_id: u32,                           // offset 12, 4 bytes
821                                                // captures follow starting at offset 16
822}
823
824/// Stack-allocated closure. No `HeapHeader`; captures follow the
825/// `function_id`/`type_id` pair at offset 8.
826#[repr(C)]
827pub struct StackClosure {
828    pub function_id: u32, // offset 0, 4 bytes
829    pub type_id: u32,     // offset 4, 4 bytes
830                          // captures follow starting at offset 8
831}
832
833const _: () = {
834    assert!(std::mem::size_of::<StackClosure>() == 8);
835    assert!(std::mem::size_of::<TypedClosureHeader>() == 16);
836};
837
838/// Computed layout for a closure's captures.
839///
840/// Offsets in `captures` are relative to the **captures area start** (i.e.
841/// offset 0 = first byte after the header). Use [`ClosureLayout::heap_capture_offset`]
842/// or [`ClosureLayout::stack_capture_offset`] for absolute offsets from the
843/// corresponding closure base pointer.
844///
845/// # ADR-006 §2.7.8 / Q10 — per-capture `NativeKind` companion
846///
847/// `capture_native_kinds` extends the §2.7.7 stack-side parallel-`Vec<NativeKind>`
848/// invariant to closure cell storage. Each entry is the `NativeKind` interpretation
849/// of capture slot `i`'s 8-byte raw payload — set at construction (lockstep with
850/// `capture_types[i]` and `capture_kinds[i]`), read at access/teardown so that
851/// drop dispatch routes through `drop_with_kind(bits, kind)` (the canonical
852/// `KindedSlot::drop` table) instead of the deleted ValueWord-shape
853/// `vw_drop(bits)` (forbidden #8 per §2.7.7) or the also-deleted
854/// `Arc<HeapValue>` blanket decrement.
855///
856/// **Index invariant:** `capture_types.len() == capture_native_kinds.len() ==
857/// capture_kinds.len() == captures.len()` at every observable boundary.
858///
859/// **Storage location.** Per ADR-006 §2.7.8 / Q10, the kinds live in the layout
860/// descriptor (constant per `ClosureTypeId`), NOT in the per-instance raw
861/// closure block. The block's fixed-offset C-shaped byte buffer is unchanged —
862/// JIT FFI offsets (`SHARED_CELL_VALUE_OFFSET`, `HEAP_CLOSURE_HEADER_SIZE`,
863/// per-capture `heap_capture_offset(i)`) are preserved. The kind track is a
864/// pure side-table on the layout, identical in shape to the §2.7.8 ADR
865/// example for `ClosureCell { bits, kinds }` but specialised to the
866/// existing `OwnedClosureBlock` raw-byte form: bits live in the block at
867/// `layout.heap_capture_offset(i)`, kinds live in `layout.capture_native_kinds[i]`.
868#[derive(Debug, Clone)]
869pub struct ClosureLayout {
870    /// The `ConcreteType` of each capture, in declaration order. Also the
871    /// registry key for this layout.
872    pub capture_types: Vec<ConcreteType>,
873    /// Per-capture field info. `offset` is relative to the captures area start.
874    pub captures: Vec<FieldInfo>,
875    /// Per-capture storage discipline. `capture_kinds[i]` corresponds to
876    /// `captures[i]` and determines which of the three mutually-exclusive
877    /// masks below (if any) has bit `i` set.
878    pub capture_kinds: Vec<CaptureKind>,
879    /// Per-capture `NativeKind` companion (ADR-006 §2.7.8 / Q10). Entry `i`
880    /// is the kind interpretation of capture slot `i`'s raw 8-byte payload
881    /// in the closure block. Lockstep with `capture_types` / `capture_kinds`
882    /// at every observable boundary. Read at access/teardown by drop glue
883    /// — the cell-store `drop_with_kind(bits, kind)` dispatch reads this
884    /// per-capture entry to route to the matching `Arc<T>::decrement` arm.
885    ///
886    /// The default constructor [`ClosureLayout::from_capture_types`] derives
887    /// this list from `capture_types` via [`native_kind_from_concrete_type`].
888    /// The explicit constructor
889    /// [`ClosureLayout::from_capture_types_with_native_kinds`] accepts a
890    /// caller-supplied list when the kind is finer-grained than what
891    /// `ConcreteType` can express (e.g. `NativeKind::Ptr(HeapKind::TypedArray)`
892    /// vs the generic `Ptr` field kind).
893    pub capture_native_kinds: Vec<NativeKind>,
894    /// Bitmap: bit N = capture N is a heap-refcounted pointer (`Ptr`) held
895    /// directly in the slot (i.e. `CaptureKind::Immutable` over a `Ptr`
896    /// field kind). Used by Drop glue to call `release_raw_value_bits` on
897    /// the slot contents.
898    pub heap_capture_mask: u64,
899    /// Bitmap: bit N = capture N is `CaptureKind::OwnedMutable`. The slot
900    /// holds `*mut ValueWord` (from `Box::into_raw`); Drop reclaims via
901    /// `Box::from_raw`, which also releases any heap refcount share held
902    /// inside the boxed `ValueWord`.
903    pub owned_mutable_capture_mask: u64,
904    /// Bitmap: bit N = capture N is `CaptureKind::Shared`. The slot holds
905    /// `*const SharedCell` (from `Arc::into_raw`); Drop reclaims via
906    /// `Arc::from_raw`, which decrements the strong count by one.
907    pub shared_capture_mask: u64,
908    /// Size in bytes of the captures area (rounded up to 8-byte alignment).
909    /// Does NOT include the header.
910    pub captures_size: usize,
911    /// Alignment of the captures area (always 8 in practice).
912    pub captures_align: usize,
913}
914
915/// Map a `ConcreteType` to the matching `NativeKind` for closure-capture
916/// kind tracking (ADR-006 §2.7.8 / Q10).
917///
918/// This is the default derivation used by [`ClosureLayout::from_capture_types`].
919/// Callers that need a finer-grained kind (e.g. distinguishing
920/// `Ptr(HeapKind::TypedArray)` from `Ptr(HeapKind::TypedObject)` when both
921/// would map through `ConcreteType::Pointer(_)`) should use
922/// [`ClosureLayout::from_capture_types_with_native_kinds`] and pass the
923/// explicit per-capture kinds.
924///
925/// The mapping is total and post-proof per §2.7.5.1 — every `ConcreteType`
926/// resolves to a concrete `NativeKind`. There is NO `NativeKind::Unknown` /
927/// `Pending` / `Dynamic` fallback (those variants are deleted from the enum)
928/// and there is NO Bool-default fallback (forbidden #9 per §2.7.7).
929pub fn native_kind_from_concrete_type(ty: &ConcreteType) -> NativeKind {
930    match ty {
931        ConcreteType::F64 => NativeKind::Float64,
932        ConcreteType::I64 => NativeKind::Int64,
933        ConcreteType::I32 => NativeKind::Int32,
934        ConcreteType::I16 => NativeKind::Int16,
935        ConcreteType::I8 => NativeKind::Int8,
936        ConcreteType::U64 => NativeKind::UInt64,
937        ConcreteType::U32 => NativeKind::UInt32,
938        ConcreteType::U16 => NativeKind::UInt16,
939        ConcreteType::U8 => NativeKind::UInt8,
940        ConcreteType::Bool => NativeKind::Bool,
941        ConcreteType::String => NativeKind::String,
942        ConcreteType::Array(_) => NativeKind::Ptr(HeapKind::TypedArray),
943        ConcreteType::HashMap(_, _) => NativeKind::Ptr(HeapKind::HashMap),
944        ConcreteType::Struct(_) => NativeKind::Ptr(HeapKind::TypedObject),
945        ConcreteType::Enum(_) => NativeKind::Ptr(HeapKind::TypedObject),
946        ConcreteType::Closure(_) => NativeKind::Ptr(HeapKind::Closure),
947        ConcreteType::Function(_) => NativeKind::Ptr(HeapKind::Closure),
948        ConcreteType::Pointer(_) => NativeKind::Ptr(HeapKind::NativeView),
949        ConcreteType::Tuple(_) => NativeKind::Ptr(HeapKind::TypedObject),
950        ConcreteType::Decimal => NativeKind::Ptr(HeapKind::Decimal),
951        ConcreteType::BigInt => NativeKind::Ptr(HeapKind::BigInt),
952        ConcreteType::DateTime => NativeKind::Ptr(HeapKind::Temporal),
953        // `Option<T>` / `Result<T, E>` are heap-typed wrappers in the v2
954        // runtime; the Ptr-side payload is the underlying typed object.
955        ConcreteType::Option(_) => NativeKind::Ptr(HeapKind::TypedObject),
956        ConcreteType::Result(_, _) => NativeKind::Ptr(HeapKind::TypedObject),
957        // ── Phase 3 cluster-0 Round 11-trinity 11E (2026-05-13) ─────────
958        // Collection / concurrency carriers from §2.7.15 / §2.7.17 /
959        // §2.7.18 / §2.7.20 / §2.7.25. Each ConcreteType arm maps to its
960        // own dedicated `HeapKind` ordinal — the kind label drives
961        // refcount discipline through `clone_with_kind` / `drop_with_kind`
962        // (§2.7.7 / §2.7.8) which dispatch each ordinal to the matching
963        // `Arc::increment/decrement_strong_count::<XData>`. A
964        // `Ptr(HeapKind::TypedObject)`-labeled slot would route through
965        // the wrong `Arc<TypedObjectStorage>` retain/release on these
966        // carriers — the same wrong-carrier defect Round 9's
967        // `retain_func_for_place` / `release_func_for_place` 8-arm
968        // extension specifically corrects.
969        ConcreteType::HashSet(_) => NativeKind::Ptr(HeapKind::HashSet),
970        ConcreteType::Deque(_) => NativeKind::Ptr(HeapKind::Deque),
971        ConcreteType::PriorityQueue => NativeKind::Ptr(HeapKind::PriorityQueue),
972        ConcreteType::Channel(_) => NativeKind::Ptr(HeapKind::Channel),
973        ConcreteType::Mutex(_) => NativeKind::Ptr(HeapKind::Mutex),
974        ConcreteType::Atomic => NativeKind::Ptr(HeapKind::Atomic),
975        ConcreteType::Lazy(_) => NativeKind::Ptr(HeapKind::Lazy),
976        // ── Round 19 S1.5 W12-nativekind-scalar-additions ──────────
977        // (2026-05-14) — ADR-006 §2.7.5 amendment adds F32 + Char as
978        // 4-byte scalar concrete types. Each maps to its matching
979        // scalar `NativeKind` variant per the §Q8 carrier-API bound.
980        ConcreteType::F32 => NativeKind::Float32,
981        ConcreteType::Char => NativeKind::Char,
982        // `Void` captures are not a well-formed bytecode shape — a void
983        // value has no bits to capture. Reaching this arm signals a
984        // construction-side bug upstream. We refuse to map `Void` to a
985        // sentinel kind (a Bool-default fallback would be forbidden #9
986        // per §2.7.7) and panic instead so the construction-side
987        // discipline holds.
988        ConcreteType::Void => panic!(
989            "ClosureLayout: ConcreteType::Void is not a well-formed capture type \
990             (ADR-006 §2.7.8 / Q10 — kinds must be concrete at construction; \
991             no Bool-default fallback)"
992        ),
993    }
994}
995
996impl ClosureLayout {
997    /// Build a layout from parallel lists of capture types and storage
998    /// kinds.
999    ///
1000    /// Captures are laid out in declaration order with natural alignment
1001    /// padding, starting from offset 0 of the captures area. The total size
1002    /// is rounded up to 8 bytes so the whole closure object is 8-aligned.
1003    ///
1004    /// For `CaptureKind::OwnedMutable` / `CaptureKind::Shared` the slot is
1005    /// always emitted as a `FieldKind::Ptr` (8-byte pointer), regardless of
1006    /// the underlying `ConcreteType` — the slot holds the raw
1007    /// `*mut ValueWord` (Box) or `*const SharedCell` (Arc), not the value
1008    /// directly. Only `CaptureKind::Immutable` honours the natural width of
1009    /// `capture_types[i]`.
1010    ///
1011    /// # Invariants on the emitted masks
1012    ///
1013    /// The three per-index masks are **mutually exclusive**: for any index
1014    /// `i`, at most one of `heap_capture_mask`, `owned_mutable_capture_mask`,
1015    /// `shared_capture_mask` has bit `i` set. `release_typed_closure`
1016    /// relies on this to avoid double-releases.
1017    ///
1018    /// # Panics
1019    ///
1020    /// - If `capture_types.len() != kinds.len()`.
1021    /// - If `capture_types.len() > 64` (mask-width limit).
1022    /// - If any capture type is `ConcreteType::Void` (not a well-formed
1023    ///   capture per §2.7.8 / Q10 — see [`native_kind_from_concrete_type`]).
1024    ///
1025    /// `capture_native_kinds` is derived from `capture_types` via
1026    /// [`native_kind_from_concrete_type`]. Use
1027    /// [`ClosureLayout::from_capture_types_with_native_kinds`] when the
1028    /// caller has a finer-grained kind in hand (e.g. distinguishing
1029    /// `Ptr(HeapKind::TypedArray)` vs `Ptr(HeapKind::TypedObject)` for two
1030    /// `ConcreteType::Pointer(_)` captures).
1031    pub fn from_capture_types(capture_types: &[ConcreteType], kinds: &[CaptureKind]) -> Self {
1032        let native_kinds: Vec<NativeKind> = capture_types
1033            .iter()
1034            .map(native_kind_from_concrete_type)
1035            .collect();
1036        Self::from_capture_types_with_native_kinds(capture_types, kinds, &native_kinds)
1037    }
1038
1039    /// Build a layout from parallel lists of capture types, storage kinds,
1040    /// and explicit per-capture `NativeKind`s (ADR-006 §2.7.8 / Q10).
1041    ///
1042    /// This is the explicit-kinds entry point. The default
1043    /// [`ClosureLayout::from_capture_types`] derives the kinds via
1044    /// [`native_kind_from_concrete_type`]; use this when the caller knows a
1045    /// finer-grained kind (e.g. specific `HeapKind` discriminator for a
1046    /// `ConcreteType::Pointer(_)` capture) or wants to pin the kind track
1047    /// to an authoritative source (e.g. `FrameDescriptor.slots[binding_idx]`
1048    /// per §2.7.8's debug cross-check).
1049    ///
1050    /// # Panics
1051    ///
1052    /// - If `capture_types.len() != kinds.len()` or
1053    ///   `capture_types.len() != native_kinds.len()`.
1054    /// - If `capture_types.len() > 64` (mask-width limit).
1055    pub fn from_capture_types_with_native_kinds(
1056        capture_types: &[ConcreteType],
1057        kinds: &[CaptureKind],
1058        native_kinds: &[NativeKind],
1059    ) -> Self {
1060        assert_eq!(
1061            capture_types.len(),
1062            kinds.len(),
1063            "from_capture_types_with_native_kinds: capture_types ({}) and kinds ({}) must have equal length",
1064            capture_types.len(),
1065            kinds.len()
1066        );
1067        assert_eq!(
1068            capture_types.len(),
1069            native_kinds.len(),
1070            "from_capture_types_with_native_kinds: capture_types ({}) and native_kinds ({}) must have equal length \
1071             (ADR-006 §2.7.8 / Q10 — lockstep parallel-`Vec<NativeKind>` invariant)",
1072            capture_types.len(),
1073            native_kinds.len()
1074        );
1075        if capture_types.len() > 64 {
1076            panic!(
1077                "closure has {} captures; capture masks are limited to 64 captures",
1078                capture_types.len()
1079            );
1080        }
1081
1082        let mut current_offset: usize = 0;
1083        let mut captures = Vec::with_capacity(capture_types.len());
1084        let mut heap_mask: u64 = 0;
1085        let mut owned_mutable_mask: u64 = 0;
1086        let mut shared_mask: u64 = 0;
1087        let mut max_align: usize = 1;
1088
1089        for (i, (ty, capture_kind)) in capture_types.iter().zip(kinds.iter()).enumerate() {
1090            // Field kind emission: OwnedMutable and Shared are ALWAYS Ptr
1091            // slots regardless of the declared type — the slot stores a
1092            // raw pointer (Box cell or Arc cell), not the value.
1093            let kind = match capture_kind {
1094                CaptureKind::Immutable => ty.to_field_kind(),
1095                CaptureKind::OwnedMutable | CaptureKind::Shared => FieldKind::Ptr,
1096            };
1097            let align = kind.alignment();
1098            let size = kind.size();
1099            current_offset = (current_offset + align - 1) & !(align - 1);
1100            captures.push(FieldInfo {
1101                name: format!("capture_{i}"),
1102                kind,
1103                offset: current_offset,
1104                size,
1105            });
1106            match capture_kind {
1107                CaptureKind::Immutable => {
1108                    if kind == FieldKind::Ptr {
1109                        heap_mask |= 1u64 << i;
1110                    }
1111                }
1112                CaptureKind::OwnedMutable => {
1113                    owned_mutable_mask |= 1u64 << i;
1114                }
1115                CaptureKind::Shared => {
1116                    shared_mask |= 1u64 << i;
1117                }
1118            }
1119            if align > max_align {
1120                max_align = align;
1121            }
1122            current_offset += size;
1123        }
1124
1125        // SAFETY of the three masks: by construction each index is assigned
1126        // to exactly one `CaptureKind` branch above, so the three mask bits
1127        // at any index `i` are mutually exclusive. `release_typed_closure`
1128        // relies on this invariant for correctness.
1129        debug_assert_eq!(
1130            heap_mask & owned_mutable_mask,
1131            0,
1132            "heap/owned_mutable masks overlap"
1133        );
1134        debug_assert_eq!(heap_mask & shared_mask, 0, "heap/shared masks overlap");
1135        debug_assert_eq!(
1136            owned_mutable_mask & shared_mask,
1137            0,
1138            "owned_mutable/shared masks overlap"
1139        );
1140
1141        let captures_align = if capture_types.is_empty() {
1142            8
1143        } else {
1144            max_align.max(8)
1145        };
1146        let captures_size = (current_offset + captures_align - 1) & !(captures_align - 1);
1147
1148        ClosureLayout {
1149            capture_types: capture_types.to_vec(),
1150            captures,
1151            capture_kinds: kinds.to_vec(),
1152            // ADR-006 §2.7.8 / Q10: the per-capture `NativeKind` companion
1153            // is stored in the layout descriptor (constant per
1154            // `ClosureTypeId`), not in the per-instance raw closure block.
1155            // Lockstep with `capture_types` / `capture_kinds` by the
1156            // length-equality assertions above.
1157            capture_native_kinds: native_kinds.to_vec(),
1158            heap_capture_mask: heap_mask,
1159            owned_mutable_capture_mask: owned_mutable_mask,
1160            shared_capture_mask: shared_mask,
1161            captures_size,
1162            captures_align,
1163        }
1164    }
1165
1166    /// Number of captures.
1167    #[inline]
1168    pub fn capture_count(&self) -> usize {
1169        self.captures.len()
1170    }
1171
1172    /// Offset of capture `i` from the captures area start (not from the
1173    /// heap / stack base pointer).
1174    #[inline]
1175    pub fn capture_offset(&self, i: usize) -> usize {
1176        self.captures[i].offset
1177    }
1178
1179    /// `FieldKind` of capture `i`.
1180    #[inline]
1181    pub fn capture_kind(&self, i: usize) -> FieldKind {
1182        self.captures[i].kind
1183    }
1184
1185    /// Interior `FieldKind` of capture `i` — the type stored *inside* the
1186    /// box/cell, not the slot kind.
1187    ///
1188    /// For `Immutable` captures this returns the same value as
1189    /// [`capture_kind`](Self::capture_kind): the slot directly holds a value
1190    /// of the declared type.
1191    ///
1192    /// For `OwnedMutable` and `Shared` captures the slot kind is always
1193    /// `FieldKind::Ptr` (the slot stores `*mut T` / `*const SharedCell`),
1194    /// so `capture_kind` would lose the underlying type. This method
1195    /// returns the interior type by consulting `capture_types[i]` directly.
1196    /// Drop glue uses this to reconstruct the typed `Box<T>` for an
1197    /// `OwnedMutable` cell.
1198    #[inline]
1199    pub fn capture_inner_kind(&self, i: usize) -> FieldKind {
1200        self.capture_types[i].to_field_kind()
1201    }
1202
1203    /// Absolute offset of capture `i` from the start of a heap-allocated
1204    /// `TypedClosureHeader` (i.e. add 16 for the header).
1205    #[inline]
1206    pub fn heap_capture_offset(&self, i: usize) -> usize {
1207        HEAP_CLOSURE_HEADER_SIZE + self.captures[i].offset
1208    }
1209
1210    /// Absolute offset of capture `i` from the start of a `StackClosure`
1211    /// (i.e. add 8 for the function_id/type_id pair).
1212    #[inline]
1213    pub fn stack_capture_offset(&self, i: usize) -> usize {
1214        STACK_CLOSURE_HEADER_SIZE + self.captures[i].offset
1215    }
1216
1217    /// Total size of a heap-allocated closure with this layout:
1218    /// `HeapHeader + function_id + type_id + captures`.
1219    #[inline]
1220    pub fn total_heap_size(&self) -> usize {
1221        HEAP_CLOSURE_HEADER_SIZE + self.captures_size
1222    }
1223
1224    /// Total size of a stack-allocated closure with this layout:
1225    /// `function_id + type_id + captures`.
1226    #[inline]
1227    pub fn total_stack_size(&self) -> usize {
1228        STACK_CLOSURE_HEADER_SIZE + self.captures_size
1229    }
1230
1231    /// Whether capture `i` is a heap-refcounted pointer (slot-owned Arc
1232    /// share on an immutable `Ptr` capture).
1233    #[inline]
1234    pub fn is_heap_capture(&self, i: usize) -> bool {
1235        self.heap_capture_mask & (1u64 << i) != 0
1236    }
1237
1238    /// Whether capture `i` is `CaptureKind::OwnedMutable` — slot holds
1239    /// `*mut ValueWord` and must be `Box::from_raw`'d on drop.
1240    #[inline]
1241    pub fn is_owned_mutable_capture(&self, i: usize) -> bool {
1242        self.owned_mutable_capture_mask & (1u64 << i) != 0
1243    }
1244
1245    /// Whether capture `i` is `CaptureKind::Shared` — slot holds
1246    /// `*const SharedCell` and must be `Arc::from_raw`'d on drop.
1247    #[inline]
1248    pub fn is_shared_capture(&self, i: usize) -> bool {
1249        self.shared_capture_mask & (1u64 << i) != 0
1250    }
1251
1252    /// Storage discipline for capture `i`.
1253    #[inline]
1254    pub fn capture_storage_kind(&self, i: usize) -> CaptureKind {
1255        self.capture_kinds[i]
1256    }
1257
1258    /// `NativeKind` of capture `i`'s raw 8-byte payload (ADR-006 §2.7.8 /
1259    /// Q10). Used by drop glue to dispatch through `drop_with_kind(bits, kind)`
1260    /// — the canonical `KindedSlot::Drop` table — rather than the deleted
1261    /// `vw_drop` / `Arc<HeapValue>` blanket-decrement shapes.
1262    ///
1263    /// For `Immutable` captures the kind classifies the slot's payload
1264    /// directly (e.g. `Float64` for an `f64` capture, `String` for an
1265    /// `Arc<String>` capture, `Ptr(HeapKind::TypedArray)` for an
1266    /// `Arc<TypedArrayData>` capture).
1267    ///
1268    /// For `OwnedMutable` and `Shared` captures the slot stores a raw
1269    /// `*mut T` (Box) or `*const SharedCell` (Arc) cell pointer — the
1270    /// kind classifies the **interior** payload of that cell (the same
1271    /// shape `capture_inner_kind` returns at the FieldKind level, but
1272    /// resolved to `NativeKind` for kind-aware drop dispatch). The
1273    /// per-Arc / per-Box drop helper (`drop_owned_mutable_capture` /
1274    /// `drop_shared_capture`) consumes this to release the inner share
1275    /// before reclaiming the cell allocation itself.
1276    #[inline]
1277    pub fn capture_native_kind(&self, i: usize) -> NativeKind {
1278        self.capture_native_kinds[i]
1279    }
1280}
1281
1282/// Registry of closure capture layouts, keyed on capture signature AND
1283/// per-capture kind.
1284///
1285/// Track A.1C.2: the registry key is `(capture_types, capture_kinds)`.
1286/// Two closures with identical capture types but different kinds (e.g.
1287/// one captures a `let` and another captures a `var` of the same type)
1288/// MUST NOT share a layout — the masks, release glue, and code emission
1289/// differ. The legacy `intern(capture_types)` entry point defaults all
1290/// kinds to `Immutable` and is the common case; the new
1291/// `intern_with_kinds` variant keys on the kind vector as well.
1292#[derive(Debug, Default, Clone)]
1293pub struct ClosureRegistry {
1294    layouts: Vec<ClosureLayout>,
1295    /// (capture_types, capture_kinds) → ClosureTypeId
1296    signature_to_id: HashMap<(Vec<ConcreteType>, Vec<CaptureKind>), ClosureTypeId>,
1297}
1298
1299impl ClosureRegistry {
1300    /// Create an empty registry.
1301    pub fn new() -> Self {
1302        Self::default()
1303    }
1304
1305    /// Intern a capture signature with every capture defaulted to
1306    /// `CaptureKind::Immutable`. Returns an existing id if the
1307    /// (types, all-Immutable kinds) key is present.
1308    pub fn intern(&mut self, capture_types: Vec<ConcreteType>) -> ClosureTypeId {
1309        let kinds = vec![CaptureKind::Immutable; capture_types.len()];
1310        self.intern_with_kinds(capture_types, kinds)
1311    }
1312
1313    /// Intern a capture signature with explicit per-capture kinds.
1314    /// Two closures with identical types but different kinds get
1315    /// distinct `ClosureTypeId`s.
1316    pub fn intern_with_kinds(
1317        &mut self,
1318        capture_types: Vec<ConcreteType>,
1319        capture_kinds: Vec<CaptureKind>,
1320    ) -> ClosureTypeId {
1321        assert_eq!(
1322            capture_types.len(),
1323            capture_kinds.len(),
1324            "intern_with_kinds: types and kinds must match in length",
1325        );
1326        let key = (capture_types, capture_kinds);
1327        if let Some(&id) = self.signature_to_id.get(&key) {
1328            return id;
1329        }
1330        let id = ClosureTypeId(self.layouts.len() as u32);
1331        let layout = ClosureLayout::from_capture_types(&key.0, &key.1);
1332        self.layouts.push(layout);
1333        self.signature_to_id.insert(key, id);
1334        id
1335    }
1336
1337    /// Get the layout for a previously interned `ClosureTypeId`.
1338    pub fn get(&self, id: ClosureTypeId) -> Option<&ClosureLayout> {
1339        self.layouts.get(id.0 as usize)
1340    }
1341
1342    /// Number of distinct capture signatures interned.
1343    pub fn len(&self) -> usize {
1344        self.layouts.len()
1345    }
1346
1347    /// Whether the registry is empty.
1348    pub fn is_empty(&self) -> bool {
1349        self.layouts.is_empty()
1350    }
1351
1352    /// Iterate over all `(ClosureTypeId, ClosureLayout)` pairs.
1353    pub fn iter(&self) -> impl Iterator<Item = (ClosureTypeId, &ClosureLayout)> {
1354        self.layouts
1355            .iter()
1356            .enumerate()
1357            .map(|(i, l)| (ClosureTypeId(i as u32), l))
1358    }
1359
1360    /// Look up a `ClosureTypeId` by capture signature (all-Immutable
1361    /// kinds) without interning. Returns `None` if not seen before.
1362    pub fn lookup(&self, capture_types: &[ConcreteType]) -> Option<ClosureTypeId> {
1363        let kinds = vec![CaptureKind::Immutable; capture_types.len()];
1364        self.signature_to_id
1365            .get(&(capture_types.to_vec(), kinds))
1366            .copied()
1367    }
1368}
1369
1370#[cfg(test)]
1371mod tests {
1372    use super::*;
1373    use crate::v2::concrete_type::{ConcreteType, StructLayoutId};
1374
1375    // Test-local helper: constructs a layout with every capture marked
1376    // `Immutable`. Mirrors the pre-A.1A constructor signature so the
1377    // existing layout-geometry tests stay concise.
1378    fn immutable_layout(types: &[ConcreteType]) -> ClosureLayout {
1379        let kinds = vec![CaptureKind::Immutable; types.len()];
1380        ClosureLayout::from_capture_types(types, &kinds)
1381    }
1382
1383    // ---- ClosureLayout layout tests ----
1384
1385    #[test]
1386    fn test_empty_captures() {
1387        let layout = immutable_layout(&[]);
1388        assert_eq!(layout.capture_count(), 0);
1389        assert_eq!(layout.captures_size, 0);
1390        assert_eq!(layout.captures_align, 8);
1391        assert_eq!(layout.heap_capture_mask, 0);
1392        assert_eq!(layout.total_heap_size(), 16);
1393        assert_eq!(layout.total_stack_size(), 8);
1394    }
1395
1396    #[test]
1397    fn test_single_f64_capture() {
1398        let layout = immutable_layout(&[ConcreteType::F64]);
1399        assert_eq!(layout.capture_count(), 1);
1400        assert_eq!(layout.capture_offset(0), 0);
1401        assert_eq!(layout.capture_kind(0), FieldKind::F64);
1402        assert_eq!(layout.heap_capture_offset(0), 16);
1403        assert_eq!(layout.stack_capture_offset(0), 8);
1404        assert_eq!(layout.captures_size, 8);
1405        assert_eq!(layout.heap_capture_mask, 0);
1406        assert_eq!(layout.total_heap_size(), 24);
1407        assert_eq!(layout.total_stack_size(), 16);
1408    }
1409
1410    #[test]
1411    fn test_two_f64_captures() {
1412        let layout = immutable_layout(&[ConcreteType::F64, ConcreteType::F64]);
1413        assert_eq!(layout.capture_count(), 2);
1414        assert_eq!(layout.capture_offset(0), 0);
1415        assert_eq!(layout.capture_offset(1), 8);
1416        assert_eq!(layout.captures_size, 16);
1417        assert_eq!(layout.heap_capture_mask, 0);
1418        assert_eq!(layout.total_heap_size(), 32);
1419        assert_eq!(layout.total_stack_size(), 24);
1420    }
1421
1422    #[test]
1423    fn test_single_i64_capture() {
1424        let layout = immutable_layout(&[ConcreteType::I64]);
1425        assert_eq!(layout.capture_offset(0), 0);
1426        assert_eq!(layout.capture_kind(0), FieldKind::I64);
1427        assert_eq!(layout.captures_size, 8);
1428        assert_eq!(layout.total_heap_size(), 24);
1429        assert_eq!(layout.total_stack_size(), 16);
1430    }
1431
1432    #[test]
1433    fn test_mixed_f64_i32_ptr() {
1434        // (F64, I32, String) — String is a heap pointer.
1435        // f64 @ 0  (size 8)
1436        // i32 @ 8  (size 4)
1437        // ptr @ 16 (needs 8-align from offset 12, pad to 16; size 8)
1438        // captures_size = 24
1439        let layout =
1440            immutable_layout(&[ConcreteType::F64, ConcreteType::I32, ConcreteType::String]);
1441        assert_eq!(layout.capture_count(), 3);
1442        assert_eq!(layout.capture_offset(0), 0);
1443        assert_eq!(layout.capture_offset(1), 8);
1444        assert_eq!(layout.capture_offset(2), 16);
1445        assert_eq!(layout.capture_kind(0), FieldKind::F64);
1446        assert_eq!(layout.capture_kind(1), FieldKind::I32);
1447        assert_eq!(layout.capture_kind(2), FieldKind::Ptr);
1448        assert_eq!(layout.captures_size, 24);
1449        assert_eq!(layout.heap_capture_mask, 0b100);
1450        assert!(layout.is_heap_capture(2));
1451        assert!(!layout.is_heap_capture(0));
1452        assert!(!layout.is_heap_capture(1));
1453        assert_eq!(layout.total_heap_size(), 40);
1454        assert_eq!(layout.total_stack_size(), 32);
1455    }
1456
1457    #[test]
1458    fn test_single_heap_typed_capture_string() {
1459        // Single String (Ptr) capture: captures area = 8 bytes, mask bit 0 set.
1460        let layout = immutable_layout(&[ConcreteType::String]);
1461        assert_eq!(layout.capture_offset(0), 0);
1462        assert_eq!(layout.capture_kind(0), FieldKind::Ptr);
1463        assert_eq!(layout.captures_size, 8);
1464        assert_eq!(layout.heap_capture_mask, 0b1);
1465        assert!(layout.is_heap_capture(0));
1466        assert_eq!(layout.total_heap_size(), 24);
1467        assert_eq!(layout.total_stack_size(), 16);
1468    }
1469
1470    #[test]
1471    fn test_array_capture_is_heap() {
1472        // Array<int> is a heap-typed pointer.
1473        let arr = ConcreteType::Array(Box::new(ConcreteType::I64));
1474        let layout = immutable_layout(&[arr]);
1475        assert_eq!(layout.capture_kind(0), FieldKind::Ptr);
1476        assert_eq!(layout.heap_capture_mask, 0b1);
1477    }
1478
1479    #[test]
1480    fn test_struct_capture_is_heap() {
1481        let s = ConcreteType::placeholder_struct(StructLayoutId(42));
1482        let layout = immutable_layout(&[s]);
1483        assert_eq!(layout.capture_kind(0), FieldKind::Ptr);
1484        assert_eq!(layout.heap_capture_mask, 0b1);
1485    }
1486
1487    #[test]
1488    fn test_small_field_packing() {
1489        // (Bool, I8, I16, I32) — small fields pack tightly.
1490        // bool @ 0 (size 1)
1491        // i8   @ 1 (size 1)
1492        // i16  @ 2 (size 2)  — 2 is already 2-aligned
1493        // i32  @ 4 (size 4)  — 4 is 4-aligned
1494        // captures_size = 8 (rounded up to 8)
1495        let layout = immutable_layout(&[
1496            ConcreteType::Bool,
1497            ConcreteType::I8,
1498            ConcreteType::I16,
1499            ConcreteType::I32,
1500        ]);
1501        assert_eq!(layout.capture_offset(0), 0);
1502        assert_eq!(layout.capture_offset(1), 1);
1503        assert_eq!(layout.capture_offset(2), 2);
1504        assert_eq!(layout.capture_offset(3), 4);
1505        assert_eq!(layout.captures_size, 8);
1506        assert_eq!(layout.heap_capture_mask, 0);
1507    }
1508
1509    #[test]
1510    fn test_heap_mask_positions() {
1511        // (I32, String, F64, Array<F64>) → Ptr at positions 1 and 3.
1512        let arr = ConcreteType::Array(Box::new(ConcreteType::F64));
1513        let layout = immutable_layout(&[
1514            ConcreteType::I32,
1515            ConcreteType::String,
1516            ConcreteType::F64,
1517            arr,
1518        ]);
1519        assert_eq!(layout.heap_capture_mask, 0b1010);
1520        assert!(!layout.is_heap_capture(0));
1521        assert!(layout.is_heap_capture(1));
1522        assert!(!layout.is_heap_capture(2));
1523        assert!(layout.is_heap_capture(3));
1524    }
1525
1526    #[test]
1527    fn test_offsets_relative_and_absolute_agree() {
1528        let layout =
1529            immutable_layout(&[ConcreteType::F64, ConcreteType::I64, ConcreteType::String]);
1530        for i in 0..layout.capture_count() {
1531            assert_eq!(layout.heap_capture_offset(i), 16 + layout.capture_offset(i));
1532            assert_eq!(layout.stack_capture_offset(i), 8 + layout.capture_offset(i));
1533        }
1534    }
1535
1536    #[test]
1537    fn test_size_rounded_up_for_trailing_small_field() {
1538        // Single Bool: 1 byte, rounded up to 8.
1539        let layout = immutable_layout(&[ConcreteType::Bool]);
1540        assert_eq!(layout.captures_size, 8);
1541        assert_eq!(layout.total_heap_size(), 24);
1542        assert_eq!(layout.total_stack_size(), 16);
1543    }
1544
1545    // ---- ClosureRegistry tests ----
1546
1547    #[test]
1548    fn test_registry_empty() {
1549        let r = ClosureRegistry::new();
1550        assert_eq!(r.len(), 0);
1551        assert!(r.is_empty());
1552    }
1553
1554    #[test]
1555    fn test_registry_same_signature_returns_same_id() {
1556        let mut r = ClosureRegistry::new();
1557        let id_a = r.intern(vec![ConcreteType::I64]);
1558        let id_b = r.intern(vec![ConcreteType::I64]);
1559        assert_eq!(id_a, id_b);
1560        assert_eq!(r.len(), 1);
1561    }
1562
1563    #[test]
1564    fn test_registry_different_signatures_returns_different_ids() {
1565        let mut r = ClosureRegistry::new();
1566        let id_empty = r.intern(vec![]);
1567        let id_i64 = r.intern(vec![ConcreteType::I64]);
1568        let id_f64 = r.intern(vec![ConcreteType::F64]);
1569        let id_i64_f64 = r.intern(vec![ConcreteType::I64, ConcreteType::F64]);
1570        let id_f64_i64 = r.intern(vec![ConcreteType::F64, ConcreteType::I64]);
1571
1572        assert_ne!(id_empty, id_i64);
1573        assert_ne!(id_i64, id_f64);
1574        assert_ne!(id_i64_f64, id_f64_i64, "order matters in the signature");
1575        assert_eq!(r.len(), 5);
1576    }
1577
1578    #[test]
1579    fn test_registry_roundtrip_and_layout_retrieval() {
1580        let mut r = ClosureRegistry::new();
1581        let id = r.intern(vec![ConcreteType::F64, ConcreteType::String]);
1582        let layout = r.get(id).expect("layout should exist");
1583        assert_eq!(layout.capture_count(), 2);
1584        assert_eq!(layout.capture_kind(0), FieldKind::F64);
1585        assert_eq!(layout.capture_kind(1), FieldKind::Ptr);
1586        assert_eq!(layout.heap_capture_mask, 0b10);
1587    }
1588
1589    #[test]
1590    fn test_registry_lookup_without_intern() {
1591        let mut r = ClosureRegistry::new();
1592        assert_eq!(r.lookup(&[ConcreteType::I64]), None);
1593        let id = r.intern(vec![ConcreteType::I64]);
1594        assert_eq!(r.lookup(&[ConcreteType::I64]), Some(id));
1595        assert_eq!(r.lookup(&[ConcreteType::F64]), None);
1596    }
1597
1598    #[test]
1599    fn test_registry_iter() {
1600        let mut r = ClosureRegistry::new();
1601        r.intern(vec![]);
1602        r.intern(vec![ConcreteType::I64]);
1603        r.intern(vec![ConcreteType::F64]);
1604        let collected: Vec<_> = r.iter().collect();
1605        assert_eq!(collected.len(), 3);
1606        assert_eq!(collected[0].0, ClosureTypeId(0));
1607        assert_eq!(collected[1].0, ClosureTypeId(1));
1608        assert_eq!(collected[2].0, ClosureTypeId(2));
1609    }
1610
1611    #[test]
1612    fn test_registry_ids_are_sequential_from_zero() {
1613        let mut r = ClosureRegistry::new();
1614        let a = r.intern(vec![ConcreteType::I64]);
1615        let b = r.intern(vec![ConcreteType::F64]);
1616        let c = r.intern(vec![ConcreteType::Bool]);
1617        assert_eq!(a, ClosureTypeId(0));
1618        assert_eq!(b, ClosureTypeId(1));
1619        assert_eq!(c, ClosureTypeId(2));
1620    }
1621
1622    #[test]
1623    fn test_registry_nested_types_are_distinct() {
1624        let mut r = ClosureRegistry::new();
1625        let arr_i64 = ConcreteType::Array(Box::new(ConcreteType::I64));
1626        let arr_f64 = ConcreteType::Array(Box::new(ConcreteType::F64));
1627        let id1 = r.intern(vec![arr_i64]);
1628        let id2 = r.intern(vec![arr_f64]);
1629        assert_ne!(id1, id2);
1630    }
1631
1632    // ---- Compile-time size / repr checks ----
1633
1634    #[test]
1635    fn test_sizeof_stack_closure_is_8() {
1636        assert_eq!(std::mem::size_of::<StackClosure>(), 8);
1637    }
1638
1639    #[test]
1640    fn test_sizeof_typed_closure_header_is_16() {
1641        assert_eq!(std::mem::size_of::<TypedClosureHeader>(), 16);
1642    }
1643
1644    #[test]
1645    fn test_header_constants() {
1646        assert_eq!(HEAP_CLOSURE_HEADER_SIZE, 16);
1647        assert_eq!(STACK_CLOSURE_HEADER_SIZE, 8);
1648    }
1649
1650    // ---- capture_inner_kind tests ----
1651
1652    #[test]
1653    fn capture_inner_kind_immutable_matches_capture_kind() {
1654        // Immutable captures: slot kind == interior kind for all types.
1655        let kinds = vec![
1656            CaptureKind::Immutable,
1657            CaptureKind::Immutable,
1658            CaptureKind::Immutable,
1659        ];
1660        let layout = ClosureLayout::from_capture_types(
1661            &[ConcreteType::I64, ConcreteType::F64, ConcreteType::String],
1662            &kinds,
1663        );
1664        assert_eq!(layout.capture_kind(0), FieldKind::I64);
1665        assert_eq!(layout.capture_inner_kind(0), FieldKind::I64);
1666        assert_eq!(layout.capture_kind(1), FieldKind::F64);
1667        assert_eq!(layout.capture_inner_kind(1), FieldKind::F64);
1668        // String is a heap-typed Ptr in both views.
1669        assert_eq!(layout.capture_kind(2), FieldKind::Ptr);
1670        assert_eq!(layout.capture_inner_kind(2), FieldKind::Ptr);
1671    }
1672
1673    #[test]
1674    fn capture_inner_kind_owned_mutable_returns_interior() {
1675        // OwnedMutable<i64>: slot kind is Ptr (Box<i64> *mut), interior is I64.
1676        let kinds = vec![CaptureKind::OwnedMutable];
1677        let layout = ClosureLayout::from_capture_types(&[ConcreteType::I64], &kinds);
1678        assert_eq!(layout.capture_kind(0), FieldKind::Ptr);
1679        assert_eq!(layout.capture_inner_kind(0), FieldKind::I64);
1680    }
1681
1682    #[test]
1683    fn capture_inner_kind_owned_mutable_f64() {
1684        let kinds = vec![CaptureKind::OwnedMutable];
1685        let layout = ClosureLayout::from_capture_types(&[ConcreteType::F64], &kinds);
1686        assert_eq!(layout.capture_kind(0), FieldKind::Ptr);
1687        assert_eq!(layout.capture_inner_kind(0), FieldKind::F64);
1688    }
1689
1690    #[test]
1691    fn capture_inner_kind_shared_returns_interior() {
1692        // Shared<bool>: slot kind is Ptr (*const SharedCell), interior is Bool.
1693        let kinds = vec![CaptureKind::Shared];
1694        let layout = ClosureLayout::from_capture_types(&[ConcreteType::Bool], &kinds);
1695        assert_eq!(layout.capture_kind(0), FieldKind::Ptr);
1696        assert_eq!(layout.capture_inner_kind(0), FieldKind::Bool);
1697    }
1698
1699    #[test]
1700    fn capture_inner_kind_owned_mutable_ptr() {
1701        // OwnedMutable<String>: slot kind is Ptr, interior is also Ptr
1702        // (the box contains a heap pointer that itself owns a refcount).
1703        let kinds = vec![CaptureKind::OwnedMutable];
1704        let layout = ClosureLayout::from_capture_types(&[ConcreteType::String], &kinds);
1705        assert_eq!(layout.capture_kind(0), FieldKind::Ptr);
1706        assert_eq!(layout.capture_inner_kind(0), FieldKind::Ptr);
1707    }
1708
1709    // ---- ADR-006 §2.7.8 / Q10 — capture_native_kinds tests ----
1710
1711    #[test]
1712    fn capture_native_kinds_inline_scalars() {
1713        // Inline-scalar `ConcreteType`s map to their matching inline
1714        // `NativeKind` (lockstep with `capture_types`).
1715        let layout = immutable_layout(&[
1716            ConcreteType::F64,
1717            ConcreteType::I64,
1718            ConcreteType::I32,
1719            ConcreteType::Bool,
1720        ]);
1721        assert_eq!(layout.capture_native_kinds.len(), 4);
1722        assert_eq!(layout.capture_native_kind(0), NativeKind::Float64);
1723        assert_eq!(layout.capture_native_kind(1), NativeKind::Int64);
1724        assert_eq!(layout.capture_native_kind(2), NativeKind::Int32);
1725        assert_eq!(layout.capture_native_kind(3), NativeKind::Bool);
1726    }
1727
1728    #[test]
1729    fn capture_native_kinds_string() {
1730        // String captures map to NativeKind::String — the special-cased
1731        // most-common heap shape per ADR-005 §2.
1732        let layout = immutable_layout(&[ConcreteType::String]);
1733        assert_eq!(layout.capture_native_kind(0), NativeKind::String);
1734    }
1735
1736    #[test]
1737    fn capture_native_kinds_typed_array() {
1738        // Array<T> captures map to NativeKind::Ptr(HeapKind::TypedArray)
1739        // (the underlying storage is `Arc<TypedArrayData>`).
1740        let arr = ConcreteType::Array(Box::new(ConcreteType::F64));
1741        let layout = immutable_layout(&[arr]);
1742        assert_eq!(
1743            layout.capture_native_kind(0),
1744            NativeKind::Ptr(HeapKind::TypedArray)
1745        );
1746    }
1747
1748    #[test]
1749    fn capture_native_kinds_struct() {
1750        // Struct captures map to NativeKind::Ptr(HeapKind::TypedObject).
1751        let s = ConcreteType::placeholder_struct(StructLayoutId(7));
1752        let layout = immutable_layout(&[s]);
1753        assert_eq!(
1754            layout.capture_native_kind(0),
1755            NativeKind::Ptr(HeapKind::TypedObject)
1756        );
1757    }
1758
1759    #[test]
1760    fn capture_native_kinds_lockstep_with_capture_types() {
1761        // The §2.7.8 / Q10 lockstep invariant: every constructed layout
1762        // satisfies `capture_types.len() == capture_native_kinds.len() ==
1763        // capture_kinds.len()`.
1764        let layout = immutable_layout(&[
1765            ConcreteType::F64,
1766            ConcreteType::String,
1767            ConcreteType::I32,
1768        ]);
1769        assert_eq!(
1770            layout.capture_types.len(),
1771            layout.capture_native_kinds.len()
1772        );
1773        assert_eq!(layout.capture_types.len(), layout.capture_kinds.len());
1774        assert_eq!(layout.capture_types.len(), layout.captures.len());
1775    }
1776
1777    #[test]
1778    fn capture_native_kinds_from_explicit_constructor() {
1779        // The explicit-kinds constructor lets the caller pin the kind
1780        // track to a finer-grained source than ConcreteType can express
1781        // (e.g. specifying HeapKind::HashMap for a generic Pointer).
1782        let types = vec![ConcreteType::Pointer(Box::new(ConcreteType::Void))];
1783        let kinds = vec![CaptureKind::Immutable];
1784        let native_kinds = vec![NativeKind::Ptr(HeapKind::HashMap)];
1785        let layout = ClosureLayout::from_capture_types_with_native_kinds(
1786            &types,
1787            &kinds,
1788            &native_kinds,
1789        );
1790        assert_eq!(
1791            layout.capture_native_kind(0),
1792            NativeKind::Ptr(HeapKind::HashMap)
1793        );
1794        // Geometry from the underlying ConcreteType is unchanged — it's
1795        // the kind track alone that the explicit constructor overrides.
1796        assert_eq!(layout.capture_kind(0), FieldKind::Ptr);
1797    }
1798
1799    #[test]
1800    #[should_panic(expected = "must have equal length")]
1801    fn capture_native_kinds_explicit_constructor_length_mismatch_panics() {
1802        // Passing mismatched-length slices violates the §2.7.8 / Q10
1803        // lockstep invariant — the constructor MUST panic, not silently
1804        // truncate or pad.
1805        let types = vec![ConcreteType::F64, ConcreteType::I64];
1806        let kinds = vec![CaptureKind::Immutable, CaptureKind::Immutable];
1807        let native_kinds = vec![NativeKind::Float64]; // wrong length
1808        let _ = ClosureLayout::from_capture_types_with_native_kinds(
1809            &types,
1810            &kinds,
1811            &native_kinds,
1812        );
1813    }
1814
1815    #[test]
1816    fn native_kind_from_concrete_type_inline_scalars() {
1817        // Round-trip every inline-scalar ConcreteType through the
1818        // mapping helper.
1819        assert_eq!(
1820            native_kind_from_concrete_type(&ConcreteType::F64),
1821            NativeKind::Float64
1822        );
1823        assert_eq!(
1824            native_kind_from_concrete_type(&ConcreteType::I64),
1825            NativeKind::Int64
1826        );
1827        assert_eq!(
1828            native_kind_from_concrete_type(&ConcreteType::I32),
1829            NativeKind::Int32
1830        );
1831        assert_eq!(
1832            native_kind_from_concrete_type(&ConcreteType::I16),
1833            NativeKind::Int16
1834        );
1835        assert_eq!(
1836            native_kind_from_concrete_type(&ConcreteType::I8),
1837            NativeKind::Int8
1838        );
1839        assert_eq!(
1840            native_kind_from_concrete_type(&ConcreteType::U64),
1841            NativeKind::UInt64
1842        );
1843        assert_eq!(
1844            native_kind_from_concrete_type(&ConcreteType::U32),
1845            NativeKind::UInt32
1846        );
1847        assert_eq!(
1848            native_kind_from_concrete_type(&ConcreteType::U16),
1849            NativeKind::UInt16
1850        );
1851        assert_eq!(
1852            native_kind_from_concrete_type(&ConcreteType::U8),
1853            NativeKind::UInt8
1854        );
1855        assert_eq!(
1856            native_kind_from_concrete_type(&ConcreteType::Bool),
1857            NativeKind::Bool
1858        );
1859    }
1860
1861    #[test]
1862    fn native_kind_from_concrete_type_heap_arms() {
1863        // Heap ConcreteType arms map to their matching Ptr(HeapKind)
1864        // discriminator (or NativeKind::String for the ADR-005 §2 special
1865        // case).
1866        assert_eq!(
1867            native_kind_from_concrete_type(&ConcreteType::String),
1868            NativeKind::String
1869        );
1870        assert_eq!(
1871            native_kind_from_concrete_type(&ConcreteType::Decimal),
1872            NativeKind::Ptr(HeapKind::Decimal)
1873        );
1874        assert_eq!(
1875            native_kind_from_concrete_type(&ConcreteType::BigInt),
1876            NativeKind::Ptr(HeapKind::BigInt)
1877        );
1878        assert_eq!(
1879            native_kind_from_concrete_type(&ConcreteType::DateTime),
1880            NativeKind::Ptr(HeapKind::Temporal)
1881        );
1882    }
1883
1884    #[test]
1885    #[should_panic(expected = "Void is not a well-formed capture type")]
1886    fn native_kind_from_concrete_type_void_panics() {
1887        // Top-level ConcreteType::Void in a capture slot is malformed —
1888        // the helper refuses to map it to a sentinel kind (a Bool-default
1889        // fallback would be forbidden #9 per §2.7.7).
1890        let _ = native_kind_from_concrete_type(&ConcreteType::Void);
1891    }
1892}