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                    // V3-S5 ckpt-5-prime (2026-05-15): `HeapKind::TypedArray`
385                    // dispatch arm RETIRED per W12 audit §3.6 + handover §0
386                    // 4-table lockstep rule (SharedCell::drop table). Mirror
387                    // of the drop_with_kind / clone_with_kind retirements in
388                    // `heap_value.rs` + `kinded_slot.rs`. Ordinal 8 vacated;
389                    // no SharedCell single-slot payload carries this kind
390                    // post-V3-S5 ckpt-4. Refusal #1 binding.
391                    HeapKind::TypedArray => {
392                        unreachable!(
393                            "HeapKind::TypedArray ordinal 8 is vacated per W12 audit §3.6 \
394                             (SharedCell::drop); no live slot bits carry this kind \
395                             post-V3-S5 ckpt-4 (v2-raw *mut TypedArray<T> carriers per ADR-006 \
396                             §2.7.24 Q25.A SUPERSEDED)"
397                        );
398                    }
399                    // Wave 2 Agent D4 ckpt-2 (ADR-006 §2.3 / §2.7.5
400                    // amendment, 2026-05-14): a `SharedCell` whose
401                    // single-slot payload is a
402                    // `NativeKind::Ptr(HeapKind::TypedObject)` carries
403                    // `ptr as u64` where `ptr: *const TypedObjectStorage`
404                    // (v2-raw carrier per Agent D1's `_new` /
405                    // `impl HeapElement for TypedObjectStorage`). Retire
406                    // one refcount share at cell drop via `release_elem`
407                    // (HeapElement trait — calls `v2_release` against the
408                    // HeapHeader at offset 0; on refcount=0 the
409                    // carrier-side `_drop` runs the per-field heap-mask
410                    // walk and deallocates). Mirror of the §2.7.5 StringV2
411                    // / DecimalV2 release arms above.
412                    HeapKind::TypedObject => {
413                        use crate::v2::heap_element::HeapElement;
414                        TypedObjectStorage::release_elem(
415                            bits as *const TypedObjectStorage,
416                        );
417                    }
418                    HeapKind::HashMap => {
419                        // Wave 2 Round 3b C2-joint ckpt-2 (2026-05-14):
420                        // bits are `Arc::into_raw(Arc<HashMapKindedRef>)`;
421                        // release dispatches outer Arc decrement → enum
422                        // Drop chains to per-V `Arc<HashMapData<V>>` release.
423                        Arc::decrement_strong_count(
424                            bits as *const crate::heap_value::HashMapKindedRef,
425                        );
426                    }
427                    // Wave 13 W13-hashset-rebuild (ADR-006 §2.7.15 / Q16,
428                    // 2026-05-10): mirror of the HashMap arm. A
429                    // SharedCell whose single-slot payload is a
430                    // `NativeKind::Ptr(HeapKind::HashSet)` carries
431                    // `Arc::into_raw(Arc<HashSetData>) as u64`. Retire
432                    // one `Arc<HashSetData>` strong-count share at cell
433                    // drop. Same dispatch shape as HashMap (HashSet is
434                    // a HashMap sibling per §2.7.15).
435                    HeapKind::HashSet => {
436                        Arc::decrement_strong_count(bits as *const HashSetData);
437                    }
438                    // Wave 15 W15-deque (ADR-006 §2.7.19 / Q20,
439                    // 2026-05-10): mirror of the HashSet arm. A
440                    // SharedCell whose single-slot payload is a
441                    // `NativeKind::Ptr(HeapKind::Deque)` carries
442                    // `Arc::into_raw(Arc<DequeData>) as u64`. Retire
443                    // one `Arc<DequeData>` strong-count share at cell
444                    // drop. Deque is a HashSet sibling per §2.7.19.
445                    HeapKind::Deque => {
446                        Arc::decrement_strong_count(bits as *const DequeData);
447                    }
448                    // Wave 15 W15-channel-rebuild (ADR-006 §2.7.20 / Q21,
449                    // 2026-05-10): mirror of the HashSet arm. A
450                    // `SharedCell` whose single-slot payload is a
451                    // `NativeKind::Ptr(HeapKind::Channel)` carries
452                    // `Arc::into_raw(Arc<ChannelData>) as u64`. Retire
453                    // one `Arc<ChannelData>` strong-count share at cell
454                    // drop. The Channel is the first concurrency
455                    // primitive to flow through the §2.7.8 / Q10
456                    // cell-storage parallel-kind track.
457                    HeapKind::Channel => {
458                        Arc::decrement_strong_count(bits as *const ChannelData);
459                    }
460                    // W17-concurrency (ADR-006 §2.7.25, 2026-05-11):
461                    // Mutex / Atomic / Lazy mirror the Channel arm at
462                    // the §2.7.8 / Q10 cell-storage parallel-kind
463                    // track. A `SharedCell` whose single-slot payload
464                    // is a `NativeKind::Ptr(HeapKind::Mutex/Atomic/Lazy)`
465                    // carries `Arc::into_raw(Arc<MutexData/AtomicData/
466                    // LazyData>) as u64`. Retire one strong-count
467                    // share at cell drop. Same dispatch shape as
468                    // Channel (concurrency primitives, full HeapValue
469                    // arm per §2.7.25).
470                    HeapKind::Mutex => {
471                        Arc::decrement_strong_count(bits as *const MutexData);
472                    }
473                    HeapKind::Atomic => {
474                        Arc::decrement_strong_count(bits as *const AtomicData);
475                    }
476                    HeapKind::Lazy => {
477                        Arc::decrement_strong_count(bits as *const LazyData);
478                    }
479                    // W17-trait-object-storage (ADR-006 §2.7.24 / Q25.C,
480                    // 2026-05-11): a `SharedCell` whose single-slot
481                    // payload is a `NativeKind::Ptr(HeapKind::TraitObject)`
482                    // carries `Arc::into_raw(Arc<TraitObjectStorage>)
483                    // as u64`. Retire one strong-count share at cell
484                    // drop — auto-derived `TraitObjectStorage::Drop`
485                    // releases the inner value + vtable Arcs at
486                    // refcount=0.
487                    // Wave 2 Agent D4 ckpt-2 (ADR-006 §2.7.24 / Q25.C.5 +
488                    // E close 2026-05-14): TraitObject release via
489                    // `HeapElement::release_elem` + carrier-side `_drop`
490                    // (per Agent E's `impl HeapElement for
491                    // TraitObjectStorage`). Mirror of the TypedObject
492                    // arm above.
493                    HeapKind::TraitObject => {
494                        use crate::v2::heap_element::HeapElement;
495                        TraitObjectStorage::release_elem(
496                            bits as *const TraitObjectStorage,
497                        );
498                    }
499                    HeapKind::Decimal => {
500                        Arc::decrement_strong_count(bits as *const rust_decimal::Decimal);
501                    }
502                    HeapKind::BigInt => {
503                        Arc::decrement_strong_count(bits as *const i64);
504                    }
505                    HeapKind::DataTable => {
506                        Arc::decrement_strong_count(bits as *const crate::datatable::DataTable);
507                    }
508                    HeapKind::IoHandle => {
509                        Arc::decrement_strong_count(bits as *const IoHandleData);
510                    }
511                    HeapKind::NativeView => {
512                        Arc::decrement_strong_count(bits as *const NativeViewData);
513                    }
514                    HeapKind::Content => {
515                        Arc::decrement_strong_count(bits as *const crate::content::ContentNode);
516                    }
517                    HeapKind::Instant => {
518                        Arc::decrement_strong_count(bits as *const std::time::Instant);
519                    }
520                    HeapKind::Temporal => {
521                        Arc::decrement_strong_count(bits as *const TemporalData);
522                    }
523                    HeapKind::TableView => {
524                        Arc::decrement_strong_count(bits as *const TableViewData);
525                    }
526                    HeapKind::TaskGroup => {
527                        Arc::decrement_strong_count(bits as *const TaskGroupData);
528                    }
529                    // Wave-γ G-heap-filter-expr (ADR-006 §2.3 / §2.7.6 / Q8
530                    // amendment): FilterExpr cells own one
531                    // `Arc::into_raw(Arc<FilterNode>)` strong-count share.
532                    // Pre-amendment the FilterExpr branch reused
533                    // `HeapKind::NativeView` as its kind label and dispatched
534                    // here as `Arc<NativeViewData>` — wrong-type retain/release
535                    // (Wave-α D-raw-helpers `a27c0e4` surfaced the gap).
536                    HeapKind::FilterExpr => {
537                        Arc::decrement_strong_count(bits as *const crate::value::FilterNode);
538                    }
539                    // Wave 8 W8-T26 (ADR-006 §2.7.13 / Q14, 2026-05-10):
540                    // a `SharedCell` whose single-slot payload is a
541                    // `NativeKind::Ptr(HeapKind::Reference)` carries
542                    // `Arc::into_raw(Arc<RefTarget>) as u64` directly
543                    // (mirror of FilterExpr's pure-discriminator-style
544                    // dispatch — NOT a `Box<HeapValue>` wrap). Retire one
545                    // `Arc<RefTarget>` strong-count share at cell drop.
546                    HeapKind::Reference => {
547                        Arc::decrement_strong_count(bits as *const crate::reference::RefTarget);
548                    }
549                    // W13-iterator-state (ADR-006 §2.7.16 / Q17,
550                    // 2026-05-10): a `SharedCell` whose single-slot
551                    // payload is a
552                    // `NativeKind::Ptr(HeapKind::Iterator)` carries
553                    // `Arc::into_raw(Arc<IteratorState>) as u64`
554                    // directly (mirror of FilterExpr / Reference's
555                    // typed-Arc dispatch — NOT a `Box<HeapValue>`
556                    // wrap). Retire one `Arc<IteratorState>`
557                    // strong-count share at cell drop.
558                    HeapKind::Iterator => {
559                        Arc::decrement_strong_count(
560                            bits as *const crate::iterator_state::IteratorState,
561                        );
562                    }
563                    // Wave 15 W15-priority-queue (ADR-006 §2.7.18 / Q19,
564                    // 2026-05-10): mirror of the HashSet arm. A
565                    // SharedCell whose single-slot payload is a
566                    // `NativeKind::Ptr(HeapKind::PriorityQueue)` carries
567                    // `Arc::into_raw(Arc<PriorityQueueData>) as u64`.
568                    // Retire one `Arc<PriorityQueueData>` strong-count
569                    // share at cell drop. Same dispatch shape as
570                    // HashSet (PriorityQueue is a HashSet sibling per
571                    // §2.7.18).
572                    HeapKind::PriorityQueue => {
573                        Arc::decrement_strong_count(bits as *const PriorityQueueData);
574                    }
575                    // W15-range (ADR-006 §2.7.23 / Q24, 2026-05-10): a
576                    // `SharedCell` whose single-slot payload is a
577                    // `NativeKind::Ptr(HeapKind::Range)` carries
578                    // `Arc::into_raw(Arc<RangeData>) as u64` directly
579                    // (typed-Arc shape, mirror of HashMap / HashSet /
580                    // Iterator). Retire one `Arc<RangeData>`
581                    // strong-count share at cell drop.
582                    HeapKind::Range => {
583                        Arc::decrement_strong_count(bits as *const RangeData);
584                    }
585                    // Wave 14 W14-variant-codegen (ADR-006 §2.7.17 / Q18,
586                    // 2026-05-10): a `SharedCell` whose single-slot
587                    // payload is `NativeKind::Ptr(HeapKind::Result)` /
588                    // `NativeKind::Ptr(HeapKind::Option)` carries
589                    // `Arc::into_raw(Arc<ResultData>) as u64` /
590                    // `Arc::into_raw(Arc<OptionData>) as u64` directly
591                    // (mirror of Iterator typed-Arc dispatch). Retire
592                    // one matching strong-count share at cell drop.
593                    HeapKind::Result => {
594                        Arc::decrement_strong_count(
595                            bits as *const crate::heap_value::ResultData,
596                        );
597                    }
598                    HeapKind::Option => {
599                        Arc::decrement_strong_count(
600                            bits as *const crate::heap_value::OptionData,
601                        );
602                    }
603                    // Char: inline-scalar payload (codepoint bits, not an
604                    // `Arc<T>`). Drop is a no-op; non-zero bits are valid.
605                    HeapKind::Char => {
606                        // No-op: inline-scalar payload.
607                    }
608                    // Round 2.5b W7-closure-retain-parallel (ADR-006
609                    // §2.7.11 / Q12, 2026-05-09 — lockstep with vm-tier
610                    // Round 2.5 close `5fa4b19`): a `SharedCell` whose
611                    // single-slot payload is a
612                    // `NativeKind::Ptr(HeapKind::Closure)` carries
613                    // `Arc::into_raw(Arc<HeapValue>) as u64` pointing
614                    // to a `HeapValue::ClosureRaw(OwnedClosureBlock)`
615                    // arm — the share carrier at the slot tier is the
616                    // outer `Arc<HeapValue>`. Round 2 close (`06cdfce`)
617                    // committed to this slot-bits shape via
618                    // `callee.slot.as_heap_value()` →
619                    // `HeapValue::ClosureRaw(block)`. Same dispatch
620                    // shape as the `HeapKind::FilterExpr` §2.7.9
621                    // amendment (one variant, one matching `Arc<T>`
622                    // retire at the slot tier).
623                    HeapKind::Closure => {
624                        Arc::decrement_strong_count(bits as *const HeapValue);
625                    }
626                    // `Ptr(HeapKind::Future)` carries the future-id u64
627                    // directly in `bits` (inline scalar — no `Arc<T>`
628                    // payload). See `async_ops/mod.rs` §"Wave 6.5 /
629                    // E-async migration" docstring. Same shape as
630                    // `HeapKind::Char`.
631                    HeapKind::Future => {
632                        // No-op: future-id inline scalar.
633                    }
634                    // W17-comptime-vm-dispatch (ADR-006 §2.7.26, 2026-05-12):
635                    // module-fn-id inline scalar payload — no `Arc<T>`,
636                    // no heap state. Same shape as `HeapKind::Future` /
637                    // `HeapKind::Char`. A SharedCell carrying a
638                    // ModuleFn-labeled inner payload retires no
639                    // refcount share.
640                    HeapKind::ModuleFn => {
641                        // No-op: module-fn-id inline scalar.
642                    }
643                    // Wave 8 W8-T25 (ADR-006 §2.7.12 / Q13 amendment,
644                    // 2026-05-10): a `SharedCell` whose `kind` companion
645                    // is `NativeKind::Ptr(HeapKind::SharedCell)` carries
646                    // an inner `Arc::into_raw(Arc<SharedCell>) as u64`
647                    // pointer — the closure-capture shape where one
648                    // shared-mutable variable is itself captured shared
649                    // into another closure (the inner SharedCell wraps
650                    // an outer SharedCell cell-pointer). Retires one
651                    // `Arc<SharedCell>` strong-count share. Same dispatch
652                    // shape as the `HeapKind::FilterExpr` §2.7.9 amendment
653                    // (one variant, one matching `Arc<T>` retire at the
654                    // cell-storage tier).
655                    HeapKind::SharedCell => {
656                        Arc::decrement_strong_count(bits as *const SharedCell);
657                    }
658                    // ADR-006 §2.7.22 amendment (Round 18 S3, 2026-05-13):
659                    // a `SharedCell` whose `kind` companion is
660                    // `NativeKind::Ptr(HeapKind::Matrix)` /
661                    // `NativeKind::Ptr(HeapKind::MatrixSlice)` carries
662                    // `Arc::into_raw(Arc<MatrixData>) as u64` /
663                    // `Arc::into_raw(Arc<MatrixSliceData>) as u64` directly
664                    // (typed-Arc pure-discriminator dispatch, mirror of
665                    // §2.7.9 FilterExpr / §2.7.13 Reference). Retire one
666                    // matching strong-count share at cell drop.
667                    HeapKind::Matrix => {
668                        Arc::decrement_strong_count(bits as *const MatrixData);
669                    }
670                    HeapKind::MatrixSlice => {
671                        Arc::decrement_strong_count(bits as *const MatrixSliceData);
672                    }
673                    // `HeapKind::NativeScalar` has no kinded `Arc<T>`
674                    // carrier yet — the redesign is the phase-2c
675                    // surface tracked in ADR-006 §2.7.4. When the
676                    // kinded NativeScalar carrier lands, this arm
677                    // wires its release per the chosen share carrier
678                    // (per the playbook's surface-and-stop discipline
679                    // — no Bool-default fallback). Until then, a
680                    // non-zero pointer with this kind is a
681                    // construction-side bug.
682                    HeapKind::NativeScalar => {
683                        debug_assert!(
684                            false,
685                            "SharedCell::drop: NativeScalar kinded carrier pending \
686                             phase-2c kinded redesign (ADR-006 §2.7.4)"
687                        );
688                    }
689                },
690                // Inline-scalar kinds: nothing to decrement. Bits are a
691                // raw value, not a pointer.
692                NativeKind::Float64
693                | NativeKind::NullableFloat64
694                | NativeKind::Int8
695                | NativeKind::NullableInt8
696                | NativeKind::UInt8
697                | NativeKind::NullableUInt8
698                | NativeKind::Int16
699                | NativeKind::NullableInt16
700                | NativeKind::UInt16
701                | NativeKind::NullableUInt16
702                | NativeKind::Int32
703                | NativeKind::NullableInt32
704                | NativeKind::UInt32
705                | NativeKind::NullableUInt32
706                | NativeKind::Int64
707                | NativeKind::NullableInt64
708                | NativeKind::UInt64
709                | NativeKind::NullableUInt64
710                | NativeKind::IntSize
711                | NativeKind::NullableIntSize
712                | NativeKind::UIntSize
713                | NativeKind::NullableUIntSize
714                | NativeKind::Bool
715                // Round 19 S1.5 W12-nativekind-scalar-additions
716                // (2026-05-14): Float32 + Char are inline 4-byte scalars
717                // per ADR-006 §2.7.5 amendment. A `SharedCell` whose
718                // `kind` companion is one of these stores raw f32 bit
719                // pattern / `c as u32` codepoint bits zero-extended into
720                // the low 32 bits of the 8-byte cell. No `Arc<T>`
721                // payload, no refcount work at cell drop.
722                | NativeKind::Float32
723                | NativeKind::Char => {}
724            }
725        }
726    }
727}
728
729/// RAII guard returned by [`SharedCell::lock`]. Releases the lock on
730/// Drop. Dereffs to the inner `ValueWord`.
731pub struct SharedCellGuard<'a> {
732    cell: &'a SharedCell,
733}
734
735impl<'a> std::ops::Deref for SharedCellGuard<'a> {
736    type Target = u64;
737    #[inline]
738    fn deref(&self) -> &u64 {
739        // SAFETY: holding the guard implies the lock is held, so we
740        // have exclusive access to the UnsafeCell payload.
741        unsafe { &*self.cell.value.get() }
742    }
743}
744
745impl<'a> std::ops::DerefMut for SharedCellGuard<'a> {
746    #[inline]
747    fn deref_mut(&mut self) -> &mut u64 {
748        // SAFETY: see `deref`.
749        unsafe { &mut *self.cell.value.get() }
750    }
751}
752
753impl<'a> Drop for SharedCellGuard<'a> {
754    #[inline]
755    fn drop(&mut self) {
756        // SAFETY: we hold the lock (guard construction acquired it);
757        // `unlock` transitions state 1→0 via a `Release` store.
758        unsafe { self.cell.unlock() };
759    }
760}
761
762impl std::fmt::Debug for SharedCell {
763    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
764        f.debug_struct("SharedCell").finish_non_exhaustive()
765    }
766}
767
768/// Storage discipline for a closure capture.
769///
770/// Each capture index i has exactly one `CaptureKind`. The three kinds
771/// are mutually exclusive and map to three mutually-exclusive bitmasks
772/// on `ClosureLayout` (`heap_capture_mask`, `owned_mutable_capture_mask`,
773/// `shared_capture_mask`).
774///
775/// - **`Immutable`** — `let` by-move/copy captures. The slot's width
776///   follows `capture_types[i]` via [`FieldKind`]; reads and writes go
777///   through [`super::closure_raw::read_capture_as_value_bits`] and
778///   [`super::closure_raw::write_capture_typed`] as today. If the
779///   underlying field kind is `Ptr`, the slot owns one heap-refcount
780///   share (participates in `heap_capture_mask`).
781/// - **`OwnedMutable`** — `let mut` by-move captures. The 8-byte slot
782///   holds `*mut ValueWord` obtained from `Box::into_raw(Box::new(...))`.
783///   Exactly one closure owns the box; Drop reclaims it with
784///   `Box::from_raw`. The interior `ValueWord` can itself carry heap
785///   refcount shares — those must be dropped before the box is freed.
786/// - **`Shared`** — `var` captures shared across nested closures. The
787///   8-byte slot holds `*const SharedCell` obtained from
788///   `Arc::into_raw(Arc::new(Mutex::new(...)))`. Each slot counts as one
789///   `Arc` strong share; reads/writes take the parking_lot mutex.
790#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
791pub enum CaptureKind {
792    /// `let` binding: value in slot, width per `FieldKind`.
793    Immutable,
794    /// `let mut` binding: Ptr slot holds `*mut ValueWord` (Box cell).
795    OwnedMutable,
796    /// `var` binding: Ptr slot holds `*const SharedCell`
797    /// (`Arc<parking_lot::Mutex<ValueWord>>` via `Arc::into_raw`).
798    Shared,
799}
800
801/// Byte size of the heap closure header: `HeapHeader (8) + function_id (4) + type_id (4)`.
802pub const HEAP_CLOSURE_HEADER_SIZE: usize = 16;
803
804/// Byte size of the stack closure header: `function_id (4) + type_id (4)`.
805pub const STACK_CLOSURE_HEADER_SIZE: usize = 8;
806
807/// Heap-allocated closure. The `HeapHeader` is at offset 0; captures follow
808/// the `function_id`/`type_id` pair at offset 16.
809///
810/// This is a layout marker used by JIT/VM codegen — captures are not declared
811/// as Rust fields because their number and types are only known per
812/// `ClosureTypeId`.
813#[repr(C)]
814pub struct TypedClosureHeader {
815    pub header: super::heap_header::HeapHeader, // offset 0, 8 bytes
816    pub function_id: u32,                       // offset 8, 4 bytes
817    pub type_id: u32,                           // offset 12, 4 bytes
818                                                // captures follow starting at offset 16
819}
820
821/// Stack-allocated closure. No `HeapHeader`; captures follow the
822/// `function_id`/`type_id` pair at offset 8.
823#[repr(C)]
824pub struct StackClosure {
825    pub function_id: u32, // offset 0, 4 bytes
826    pub type_id: u32,     // offset 4, 4 bytes
827                          // captures follow starting at offset 8
828}
829
830const _: () = {
831    assert!(std::mem::size_of::<StackClosure>() == 8);
832    assert!(std::mem::size_of::<TypedClosureHeader>() == 16);
833};
834
835/// Computed layout for a closure's captures.
836///
837/// Offsets in `captures` are relative to the **captures area start** (i.e.
838/// offset 0 = first byte after the header). Use [`ClosureLayout::heap_capture_offset`]
839/// or [`ClosureLayout::stack_capture_offset`] for absolute offsets from the
840/// corresponding closure base pointer.
841///
842/// # ADR-006 §2.7.8 / Q10 — per-capture `NativeKind` companion
843///
844/// `capture_native_kinds` extends the §2.7.7 stack-side parallel-`Vec<NativeKind>`
845/// invariant to closure cell storage. Each entry is the `NativeKind` interpretation
846/// of capture slot `i`'s 8-byte raw payload — set at construction (lockstep with
847/// `capture_types[i]` and `capture_kinds[i]`), read at access/teardown so that
848/// drop dispatch routes through `drop_with_kind(bits, kind)` (the canonical
849/// `KindedSlot::drop` table) instead of the deleted ValueWord-shape
850/// `vw_drop(bits)` (forbidden #8 per §2.7.7) or the also-deleted
851/// `Arc<HeapValue>` blanket decrement.
852///
853/// **Index invariant:** `capture_types.len() == capture_native_kinds.len() ==
854/// capture_kinds.len() == captures.len()` at every observable boundary.
855///
856/// **Storage location.** Per ADR-006 §2.7.8 / Q10, the kinds live in the layout
857/// descriptor (constant per `ClosureTypeId`), NOT in the per-instance raw
858/// closure block. The block's fixed-offset C-shaped byte buffer is unchanged —
859/// JIT FFI offsets (`SHARED_CELL_VALUE_OFFSET`, `HEAP_CLOSURE_HEADER_SIZE`,
860/// per-capture `heap_capture_offset(i)`) are preserved. The kind track is a
861/// pure side-table on the layout, identical in shape to the §2.7.8 ADR
862/// example for `ClosureCell { bits, kinds }` but specialised to the
863/// existing `OwnedClosureBlock` raw-byte form: bits live in the block at
864/// `layout.heap_capture_offset(i)`, kinds live in `layout.capture_native_kinds[i]`.
865#[derive(Debug, Clone)]
866pub struct ClosureLayout {
867    /// The `ConcreteType` of each capture, in declaration order. Also the
868    /// registry key for this layout.
869    pub capture_types: Vec<ConcreteType>,
870    /// Per-capture field info. `offset` is relative to the captures area start.
871    pub captures: Vec<FieldInfo>,
872    /// Per-capture storage discipline. `capture_kinds[i]` corresponds to
873    /// `captures[i]` and determines which of the three mutually-exclusive
874    /// masks below (if any) has bit `i` set.
875    pub capture_kinds: Vec<CaptureKind>,
876    /// Per-capture `NativeKind` companion (ADR-006 §2.7.8 / Q10). Entry `i`
877    /// is the kind interpretation of capture slot `i`'s raw 8-byte payload
878    /// in the closure block. Lockstep with `capture_types` / `capture_kinds`
879    /// at every observable boundary. Read at access/teardown by drop glue
880    /// — the cell-store `drop_with_kind(bits, kind)` dispatch reads this
881    /// per-capture entry to route to the matching `Arc<T>::decrement` arm.
882    ///
883    /// The default constructor [`ClosureLayout::from_capture_types`] derives
884    /// this list from `capture_types` via [`native_kind_from_concrete_type`].
885    /// The explicit constructor
886    /// [`ClosureLayout::from_capture_types_with_native_kinds`] accepts a
887    /// caller-supplied list when the kind is finer-grained than what
888    /// `ConcreteType` can express (e.g. `NativeKind::Ptr(HeapKind::TypedArray)`
889    /// vs the generic `Ptr` field kind).
890    pub capture_native_kinds: Vec<NativeKind>,
891    /// Bitmap: bit N = capture N is a heap-refcounted pointer (`Ptr`) held
892    /// directly in the slot (i.e. `CaptureKind::Immutable` over a `Ptr`
893    /// field kind). Used by Drop glue to call `release_raw_value_bits` on
894    /// the slot contents.
895    pub heap_capture_mask: u64,
896    /// Bitmap: bit N = capture N is `CaptureKind::OwnedMutable`. The slot
897    /// holds `*mut ValueWord` (from `Box::into_raw`); Drop reclaims via
898    /// `Box::from_raw`, which also releases any heap refcount share held
899    /// inside the boxed `ValueWord`.
900    pub owned_mutable_capture_mask: u64,
901    /// Bitmap: bit N = capture N is `CaptureKind::Shared`. The slot holds
902    /// `*const SharedCell` (from `Arc::into_raw`); Drop reclaims via
903    /// `Arc::from_raw`, which decrements the strong count by one.
904    pub shared_capture_mask: u64,
905    /// Size in bytes of the captures area (rounded up to 8-byte alignment).
906    /// Does NOT include the header.
907    pub captures_size: usize,
908    /// Alignment of the captures area (always 8 in practice).
909    pub captures_align: usize,
910}
911
912/// Map a `ConcreteType` to the matching `NativeKind` for closure-capture
913/// kind tracking (ADR-006 §2.7.8 / Q10).
914///
915/// This is the default derivation used by [`ClosureLayout::from_capture_types`].
916/// Callers that need a finer-grained kind (e.g. distinguishing
917/// `Ptr(HeapKind::TypedArray)` from `Ptr(HeapKind::TypedObject)` when both
918/// would map through `ConcreteType::Pointer(_)`) should use
919/// [`ClosureLayout::from_capture_types_with_native_kinds`] and pass the
920/// explicit per-capture kinds.
921///
922/// The mapping is total and post-proof per §2.7.5.1 — every `ConcreteType`
923/// resolves to a concrete `NativeKind`. There is NO `NativeKind::Unknown` /
924/// `Pending` / `Dynamic` fallback (those variants are deleted from the enum)
925/// and there is NO Bool-default fallback (forbidden #9 per §2.7.7).
926pub fn native_kind_from_concrete_type(ty: &ConcreteType) -> NativeKind {
927    match ty {
928        ConcreteType::F64 => NativeKind::Float64,
929        ConcreteType::I64 => NativeKind::Int64,
930        ConcreteType::I32 => NativeKind::Int32,
931        ConcreteType::I16 => NativeKind::Int16,
932        ConcreteType::I8 => NativeKind::Int8,
933        ConcreteType::U64 => NativeKind::UInt64,
934        ConcreteType::U32 => NativeKind::UInt32,
935        ConcreteType::U16 => NativeKind::UInt16,
936        ConcreteType::U8 => NativeKind::UInt8,
937        ConcreteType::Bool => NativeKind::Bool,
938        ConcreteType::String => NativeKind::String,
939        ConcreteType::Array(_) => NativeKind::Ptr(HeapKind::TypedArray),
940        ConcreteType::HashMap(_, _) => NativeKind::Ptr(HeapKind::HashMap),
941        ConcreteType::Struct(_) => NativeKind::Ptr(HeapKind::TypedObject),
942        ConcreteType::Enum(_) => NativeKind::Ptr(HeapKind::TypedObject),
943        ConcreteType::Closure(_) => NativeKind::Ptr(HeapKind::Closure),
944        ConcreteType::Function(_) => NativeKind::Ptr(HeapKind::Closure),
945        ConcreteType::Pointer(_) => NativeKind::Ptr(HeapKind::NativeView),
946        ConcreteType::Tuple(_) => NativeKind::Ptr(HeapKind::TypedObject),
947        ConcreteType::Decimal => NativeKind::Ptr(HeapKind::Decimal),
948        ConcreteType::BigInt => NativeKind::Ptr(HeapKind::BigInt),
949        ConcreteType::DateTime => NativeKind::Ptr(HeapKind::Temporal),
950        // `Option<T>` / `Result<T, E>` are heap-typed wrappers in the v2
951        // runtime; the Ptr-side payload is the underlying typed object.
952        ConcreteType::Option(_) => NativeKind::Ptr(HeapKind::TypedObject),
953        ConcreteType::Result(_, _) => NativeKind::Ptr(HeapKind::TypedObject),
954        // ── Phase 3 cluster-0 Round 11-trinity 11E (2026-05-13) ─────────
955        // Collection / concurrency carriers from §2.7.15 / §2.7.17 /
956        // §2.7.18 / §2.7.20 / §2.7.25. Each ConcreteType arm maps to its
957        // own dedicated `HeapKind` ordinal — the kind label drives
958        // refcount discipline through `clone_with_kind` / `drop_with_kind`
959        // (§2.7.7 / §2.7.8) which dispatch each ordinal to the matching
960        // `Arc::increment/decrement_strong_count::<XData>`. A
961        // `Ptr(HeapKind::TypedObject)`-labeled slot would route through
962        // the wrong `Arc<TypedObjectStorage>` retain/release on these
963        // carriers — the same wrong-carrier defect Round 9's
964        // `retain_func_for_place` / `release_func_for_place` 8-arm
965        // extension specifically corrects.
966        ConcreteType::HashSet(_) => NativeKind::Ptr(HeapKind::HashSet),
967        ConcreteType::Deque(_) => NativeKind::Ptr(HeapKind::Deque),
968        ConcreteType::PriorityQueue => NativeKind::Ptr(HeapKind::PriorityQueue),
969        ConcreteType::Channel(_) => NativeKind::Ptr(HeapKind::Channel),
970        ConcreteType::Mutex(_) => NativeKind::Ptr(HeapKind::Mutex),
971        ConcreteType::Atomic => NativeKind::Ptr(HeapKind::Atomic),
972        ConcreteType::Lazy(_) => NativeKind::Ptr(HeapKind::Lazy),
973        // ── Round 19 S1.5 W12-nativekind-scalar-additions ──────────
974        // (2026-05-14) — ADR-006 §2.7.5 amendment adds F32 + Char as
975        // 4-byte scalar concrete types. Each maps to its matching
976        // scalar `NativeKind` variant per the §Q8 carrier-API bound.
977        ConcreteType::F32 => NativeKind::Float32,
978        ConcreteType::Char => NativeKind::Char,
979        // `Void` captures are not a well-formed bytecode shape — a void
980        // value has no bits to capture. Reaching this arm signals a
981        // construction-side bug upstream. We refuse to map `Void` to a
982        // sentinel kind (a Bool-default fallback would be forbidden #9
983        // per §2.7.7) and panic instead so the construction-side
984        // discipline holds.
985        ConcreteType::Void => panic!(
986            "ClosureLayout: ConcreteType::Void is not a well-formed capture type \
987             (ADR-006 §2.7.8 / Q10 — kinds must be concrete at construction; \
988             no Bool-default fallback)"
989        ),
990    }
991}
992
993impl ClosureLayout {
994    /// Build a layout from parallel lists of capture types and storage
995    /// kinds.
996    ///
997    /// Captures are laid out in declaration order with natural alignment
998    /// padding, starting from offset 0 of the captures area. The total size
999    /// is rounded up to 8 bytes so the whole closure object is 8-aligned.
1000    ///
1001    /// For `CaptureKind::OwnedMutable` / `CaptureKind::Shared` the slot is
1002    /// always emitted as a `FieldKind::Ptr` (8-byte pointer), regardless of
1003    /// the underlying `ConcreteType` — the slot holds the raw
1004    /// `*mut ValueWord` (Box) or `*const SharedCell` (Arc), not the value
1005    /// directly. Only `CaptureKind::Immutable` honours the natural width of
1006    /// `capture_types[i]`.
1007    ///
1008    /// # Invariants on the emitted masks
1009    ///
1010    /// The three per-index masks are **mutually exclusive**: for any index
1011    /// `i`, at most one of `heap_capture_mask`, `owned_mutable_capture_mask`,
1012    /// `shared_capture_mask` has bit `i` set. `release_typed_closure`
1013    /// relies on this to avoid double-releases.
1014    ///
1015    /// # Panics
1016    ///
1017    /// - If `capture_types.len() != kinds.len()`.
1018    /// - If `capture_types.len() > 64` (mask-width limit).
1019    /// - If any capture type is `ConcreteType::Void` (not a well-formed
1020    ///   capture per §2.7.8 / Q10 — see [`native_kind_from_concrete_type`]).
1021    ///
1022    /// `capture_native_kinds` is derived from `capture_types` via
1023    /// [`native_kind_from_concrete_type`]. Use
1024    /// [`ClosureLayout::from_capture_types_with_native_kinds`] when the
1025    /// caller has a finer-grained kind in hand (e.g. distinguishing
1026    /// `Ptr(HeapKind::TypedArray)` vs `Ptr(HeapKind::TypedObject)` for two
1027    /// `ConcreteType::Pointer(_)` captures).
1028    pub fn from_capture_types(capture_types: &[ConcreteType], kinds: &[CaptureKind]) -> Self {
1029        let native_kinds: Vec<NativeKind> = capture_types
1030            .iter()
1031            .map(native_kind_from_concrete_type)
1032            .collect();
1033        Self::from_capture_types_with_native_kinds(capture_types, kinds, &native_kinds)
1034    }
1035
1036    /// Build a layout from parallel lists of capture types, storage kinds,
1037    /// and explicit per-capture `NativeKind`s (ADR-006 §2.7.8 / Q10).
1038    ///
1039    /// This is the explicit-kinds entry point. The default
1040    /// [`ClosureLayout::from_capture_types`] derives the kinds via
1041    /// [`native_kind_from_concrete_type`]; use this when the caller knows a
1042    /// finer-grained kind (e.g. specific `HeapKind` discriminator for a
1043    /// `ConcreteType::Pointer(_)` capture) or wants to pin the kind track
1044    /// to an authoritative source (e.g. `FrameDescriptor.slots[binding_idx]`
1045    /// per §2.7.8's debug cross-check).
1046    ///
1047    /// # Panics
1048    ///
1049    /// - If `capture_types.len() != kinds.len()` or
1050    ///   `capture_types.len() != native_kinds.len()`.
1051    /// - If `capture_types.len() > 64` (mask-width limit).
1052    pub fn from_capture_types_with_native_kinds(
1053        capture_types: &[ConcreteType],
1054        kinds: &[CaptureKind],
1055        native_kinds: &[NativeKind],
1056    ) -> Self {
1057        assert_eq!(
1058            capture_types.len(),
1059            kinds.len(),
1060            "from_capture_types_with_native_kinds: capture_types ({}) and kinds ({}) must have equal length",
1061            capture_types.len(),
1062            kinds.len()
1063        );
1064        assert_eq!(
1065            capture_types.len(),
1066            native_kinds.len(),
1067            "from_capture_types_with_native_kinds: capture_types ({}) and native_kinds ({}) must have equal length \
1068             (ADR-006 §2.7.8 / Q10 — lockstep parallel-`Vec<NativeKind>` invariant)",
1069            capture_types.len(),
1070            native_kinds.len()
1071        );
1072        if capture_types.len() > 64 {
1073            panic!(
1074                "closure has {} captures; capture masks are limited to 64 captures",
1075                capture_types.len()
1076            );
1077        }
1078
1079        let mut current_offset: usize = 0;
1080        let mut captures = Vec::with_capacity(capture_types.len());
1081        let mut heap_mask: u64 = 0;
1082        let mut owned_mutable_mask: u64 = 0;
1083        let mut shared_mask: u64 = 0;
1084        let mut max_align: usize = 1;
1085
1086        for (i, (ty, capture_kind)) in capture_types.iter().zip(kinds.iter()).enumerate() {
1087            // Field kind emission: OwnedMutable and Shared are ALWAYS Ptr
1088            // slots regardless of the declared type — the slot stores a
1089            // raw pointer (Box cell or Arc cell), not the value.
1090            let kind = match capture_kind {
1091                CaptureKind::Immutable => ty.to_field_kind(),
1092                CaptureKind::OwnedMutable | CaptureKind::Shared => FieldKind::Ptr,
1093            };
1094            let align = kind.alignment();
1095            let size = kind.size();
1096            current_offset = (current_offset + align - 1) & !(align - 1);
1097            captures.push(FieldInfo {
1098                name: format!("capture_{i}"),
1099                kind,
1100                offset: current_offset,
1101                size,
1102            });
1103            match capture_kind {
1104                CaptureKind::Immutable => {
1105                    if kind == FieldKind::Ptr {
1106                        heap_mask |= 1u64 << i;
1107                    }
1108                }
1109                CaptureKind::OwnedMutable => {
1110                    owned_mutable_mask |= 1u64 << i;
1111                }
1112                CaptureKind::Shared => {
1113                    shared_mask |= 1u64 << i;
1114                }
1115            }
1116            if align > max_align {
1117                max_align = align;
1118            }
1119            current_offset += size;
1120        }
1121
1122        // SAFETY of the three masks: by construction each index is assigned
1123        // to exactly one `CaptureKind` branch above, so the three mask bits
1124        // at any index `i` are mutually exclusive. `release_typed_closure`
1125        // relies on this invariant for correctness.
1126        debug_assert_eq!(
1127            heap_mask & owned_mutable_mask,
1128            0,
1129            "heap/owned_mutable masks overlap"
1130        );
1131        debug_assert_eq!(heap_mask & shared_mask, 0, "heap/shared masks overlap");
1132        debug_assert_eq!(
1133            owned_mutable_mask & shared_mask,
1134            0,
1135            "owned_mutable/shared masks overlap"
1136        );
1137
1138        let captures_align = if capture_types.is_empty() {
1139            8
1140        } else {
1141            max_align.max(8)
1142        };
1143        let captures_size = (current_offset + captures_align - 1) & !(captures_align - 1);
1144
1145        ClosureLayout {
1146            capture_types: capture_types.to_vec(),
1147            captures,
1148            capture_kinds: kinds.to_vec(),
1149            // ADR-006 §2.7.8 / Q10: the per-capture `NativeKind` companion
1150            // is stored in the layout descriptor (constant per
1151            // `ClosureTypeId`), not in the per-instance raw closure block.
1152            // Lockstep with `capture_types` / `capture_kinds` by the
1153            // length-equality assertions above.
1154            capture_native_kinds: native_kinds.to_vec(),
1155            heap_capture_mask: heap_mask,
1156            owned_mutable_capture_mask: owned_mutable_mask,
1157            shared_capture_mask: shared_mask,
1158            captures_size,
1159            captures_align,
1160        }
1161    }
1162
1163    /// Number of captures.
1164    #[inline]
1165    pub fn capture_count(&self) -> usize {
1166        self.captures.len()
1167    }
1168
1169    /// Offset of capture `i` from the captures area start (not from the
1170    /// heap / stack base pointer).
1171    #[inline]
1172    pub fn capture_offset(&self, i: usize) -> usize {
1173        self.captures[i].offset
1174    }
1175
1176    /// `FieldKind` of capture `i`.
1177    #[inline]
1178    pub fn capture_kind(&self, i: usize) -> FieldKind {
1179        self.captures[i].kind
1180    }
1181
1182    /// Interior `FieldKind` of capture `i` — the type stored *inside* the
1183    /// box/cell, not the slot kind.
1184    ///
1185    /// For `Immutable` captures this returns the same value as
1186    /// [`capture_kind`](Self::capture_kind): the slot directly holds a value
1187    /// of the declared type.
1188    ///
1189    /// For `OwnedMutable` and `Shared` captures the slot kind is always
1190    /// `FieldKind::Ptr` (the slot stores `*mut T` / `*const SharedCell`),
1191    /// so `capture_kind` would lose the underlying type. This method
1192    /// returns the interior type by consulting `capture_types[i]` directly.
1193    /// Drop glue uses this to reconstruct the typed `Box<T>` for an
1194    /// `OwnedMutable` cell.
1195    #[inline]
1196    pub fn capture_inner_kind(&self, i: usize) -> FieldKind {
1197        self.capture_types[i].to_field_kind()
1198    }
1199
1200    /// Absolute offset of capture `i` from the start of a heap-allocated
1201    /// `TypedClosureHeader` (i.e. add 16 for the header).
1202    #[inline]
1203    pub fn heap_capture_offset(&self, i: usize) -> usize {
1204        HEAP_CLOSURE_HEADER_SIZE + self.captures[i].offset
1205    }
1206
1207    /// Absolute offset of capture `i` from the start of a `StackClosure`
1208    /// (i.e. add 8 for the function_id/type_id pair).
1209    #[inline]
1210    pub fn stack_capture_offset(&self, i: usize) -> usize {
1211        STACK_CLOSURE_HEADER_SIZE + self.captures[i].offset
1212    }
1213
1214    /// Total size of a heap-allocated closure with this layout:
1215    /// `HeapHeader + function_id + type_id + captures`.
1216    #[inline]
1217    pub fn total_heap_size(&self) -> usize {
1218        HEAP_CLOSURE_HEADER_SIZE + self.captures_size
1219    }
1220
1221    /// Total size of a stack-allocated closure with this layout:
1222    /// `function_id + type_id + captures`.
1223    #[inline]
1224    pub fn total_stack_size(&self) -> usize {
1225        STACK_CLOSURE_HEADER_SIZE + self.captures_size
1226    }
1227
1228    /// Whether capture `i` is a heap-refcounted pointer (slot-owned Arc
1229    /// share on an immutable `Ptr` capture).
1230    #[inline]
1231    pub fn is_heap_capture(&self, i: usize) -> bool {
1232        self.heap_capture_mask & (1u64 << i) != 0
1233    }
1234
1235    /// Whether capture `i` is `CaptureKind::OwnedMutable` — slot holds
1236    /// `*mut ValueWord` and must be `Box::from_raw`'d on drop.
1237    #[inline]
1238    pub fn is_owned_mutable_capture(&self, i: usize) -> bool {
1239        self.owned_mutable_capture_mask & (1u64 << i) != 0
1240    }
1241
1242    /// Whether capture `i` is `CaptureKind::Shared` — slot holds
1243    /// `*const SharedCell` and must be `Arc::from_raw`'d on drop.
1244    #[inline]
1245    pub fn is_shared_capture(&self, i: usize) -> bool {
1246        self.shared_capture_mask & (1u64 << i) != 0
1247    }
1248
1249    /// Storage discipline for capture `i`.
1250    #[inline]
1251    pub fn capture_storage_kind(&self, i: usize) -> CaptureKind {
1252        self.capture_kinds[i]
1253    }
1254
1255    /// `NativeKind` of capture `i`'s raw 8-byte payload (ADR-006 §2.7.8 /
1256    /// Q10). Used by drop glue to dispatch through `drop_with_kind(bits, kind)`
1257    /// — the canonical `KindedSlot::Drop` table — rather than the deleted
1258    /// `vw_drop` / `Arc<HeapValue>` blanket-decrement shapes.
1259    ///
1260    /// For `Immutable` captures the kind classifies the slot's payload
1261    /// directly (e.g. `Float64` for an `f64` capture, `String` for an
1262    /// `Arc<String>` capture, `Ptr(HeapKind::TypedArray)` for an
1263    /// `Arc<TypedArrayData>` capture).
1264    ///
1265    /// For `OwnedMutable` and `Shared` captures the slot stores a raw
1266    /// `*mut T` (Box) or `*const SharedCell` (Arc) cell pointer — the
1267    /// kind classifies the **interior** payload of that cell (the same
1268    /// shape `capture_inner_kind` returns at the FieldKind level, but
1269    /// resolved to `NativeKind` for kind-aware drop dispatch). The
1270    /// per-Arc / per-Box drop helper (`drop_owned_mutable_capture` /
1271    /// `drop_shared_capture`) consumes this to release the inner share
1272    /// before reclaiming the cell allocation itself.
1273    #[inline]
1274    pub fn capture_native_kind(&self, i: usize) -> NativeKind {
1275        self.capture_native_kinds[i]
1276    }
1277}
1278
1279/// Registry of closure capture layouts, keyed on capture signature AND
1280/// per-capture kind.
1281///
1282/// Track A.1C.2: the registry key is `(capture_types, capture_kinds)`.
1283/// Two closures with identical capture types but different kinds (e.g.
1284/// one captures a `let` and another captures a `var` of the same type)
1285/// MUST NOT share a layout — the masks, release glue, and code emission
1286/// differ. The legacy `intern(capture_types)` entry point defaults all
1287/// kinds to `Immutable` and is the common case; the new
1288/// `intern_with_kinds` variant keys on the kind vector as well.
1289#[derive(Debug, Default, Clone)]
1290pub struct ClosureRegistry {
1291    layouts: Vec<ClosureLayout>,
1292    /// (capture_types, capture_kinds) → ClosureTypeId
1293    signature_to_id: HashMap<(Vec<ConcreteType>, Vec<CaptureKind>), ClosureTypeId>,
1294}
1295
1296impl ClosureRegistry {
1297    /// Create an empty registry.
1298    pub fn new() -> Self {
1299        Self::default()
1300    }
1301
1302    /// Intern a capture signature with every capture defaulted to
1303    /// `CaptureKind::Immutable`. Returns an existing id if the
1304    /// (types, all-Immutable kinds) key is present.
1305    pub fn intern(&mut self, capture_types: Vec<ConcreteType>) -> ClosureTypeId {
1306        let kinds = vec![CaptureKind::Immutable; capture_types.len()];
1307        self.intern_with_kinds(capture_types, kinds)
1308    }
1309
1310    /// Intern a capture signature with explicit per-capture kinds.
1311    /// Two closures with identical types but different kinds get
1312    /// distinct `ClosureTypeId`s.
1313    pub fn intern_with_kinds(
1314        &mut self,
1315        capture_types: Vec<ConcreteType>,
1316        capture_kinds: Vec<CaptureKind>,
1317    ) -> ClosureTypeId {
1318        assert_eq!(
1319            capture_types.len(),
1320            capture_kinds.len(),
1321            "intern_with_kinds: types and kinds must match in length",
1322        );
1323        let key = (capture_types, capture_kinds);
1324        if let Some(&id) = self.signature_to_id.get(&key) {
1325            return id;
1326        }
1327        let id = ClosureTypeId(self.layouts.len() as u32);
1328        let layout = ClosureLayout::from_capture_types(&key.0, &key.1);
1329        self.layouts.push(layout);
1330        self.signature_to_id.insert(key, id);
1331        id
1332    }
1333
1334    /// Get the layout for a previously interned `ClosureTypeId`.
1335    pub fn get(&self, id: ClosureTypeId) -> Option<&ClosureLayout> {
1336        self.layouts.get(id.0 as usize)
1337    }
1338
1339    /// Number of distinct capture signatures interned.
1340    pub fn len(&self) -> usize {
1341        self.layouts.len()
1342    }
1343
1344    /// Whether the registry is empty.
1345    pub fn is_empty(&self) -> bool {
1346        self.layouts.is_empty()
1347    }
1348
1349    /// Iterate over all `(ClosureTypeId, ClosureLayout)` pairs.
1350    pub fn iter(&self) -> impl Iterator<Item = (ClosureTypeId, &ClosureLayout)> {
1351        self.layouts
1352            .iter()
1353            .enumerate()
1354            .map(|(i, l)| (ClosureTypeId(i as u32), l))
1355    }
1356
1357    /// Look up a `ClosureTypeId` by capture signature (all-Immutable
1358    /// kinds) without interning. Returns `None` if not seen before.
1359    pub fn lookup(&self, capture_types: &[ConcreteType]) -> Option<ClosureTypeId> {
1360        let kinds = vec![CaptureKind::Immutable; capture_types.len()];
1361        self.signature_to_id
1362            .get(&(capture_types.to_vec(), kinds))
1363            .copied()
1364    }
1365}
1366
1367#[cfg(test)]
1368mod tests {
1369    use super::*;
1370    use crate::v2::concrete_type::{ConcreteType, StructLayoutId};
1371
1372    // Test-local helper: constructs a layout with every capture marked
1373    // `Immutable`. Mirrors the pre-A.1A constructor signature so the
1374    // existing layout-geometry tests stay concise.
1375    fn immutable_layout(types: &[ConcreteType]) -> ClosureLayout {
1376        let kinds = vec![CaptureKind::Immutable; types.len()];
1377        ClosureLayout::from_capture_types(types, &kinds)
1378    }
1379
1380    // ---- ClosureLayout layout tests ----
1381
1382    #[test]
1383    fn test_empty_captures() {
1384        let layout = immutable_layout(&[]);
1385        assert_eq!(layout.capture_count(), 0);
1386        assert_eq!(layout.captures_size, 0);
1387        assert_eq!(layout.captures_align, 8);
1388        assert_eq!(layout.heap_capture_mask, 0);
1389        assert_eq!(layout.total_heap_size(), 16);
1390        assert_eq!(layout.total_stack_size(), 8);
1391    }
1392
1393    #[test]
1394    fn test_single_f64_capture() {
1395        let layout = immutable_layout(&[ConcreteType::F64]);
1396        assert_eq!(layout.capture_count(), 1);
1397        assert_eq!(layout.capture_offset(0), 0);
1398        assert_eq!(layout.capture_kind(0), FieldKind::F64);
1399        assert_eq!(layout.heap_capture_offset(0), 16);
1400        assert_eq!(layout.stack_capture_offset(0), 8);
1401        assert_eq!(layout.captures_size, 8);
1402        assert_eq!(layout.heap_capture_mask, 0);
1403        assert_eq!(layout.total_heap_size(), 24);
1404        assert_eq!(layout.total_stack_size(), 16);
1405    }
1406
1407    #[test]
1408    fn test_two_f64_captures() {
1409        let layout = immutable_layout(&[ConcreteType::F64, ConcreteType::F64]);
1410        assert_eq!(layout.capture_count(), 2);
1411        assert_eq!(layout.capture_offset(0), 0);
1412        assert_eq!(layout.capture_offset(1), 8);
1413        assert_eq!(layout.captures_size, 16);
1414        assert_eq!(layout.heap_capture_mask, 0);
1415        assert_eq!(layout.total_heap_size(), 32);
1416        assert_eq!(layout.total_stack_size(), 24);
1417    }
1418
1419    #[test]
1420    fn test_single_i64_capture() {
1421        let layout = immutable_layout(&[ConcreteType::I64]);
1422        assert_eq!(layout.capture_offset(0), 0);
1423        assert_eq!(layout.capture_kind(0), FieldKind::I64);
1424        assert_eq!(layout.captures_size, 8);
1425        assert_eq!(layout.total_heap_size(), 24);
1426        assert_eq!(layout.total_stack_size(), 16);
1427    }
1428
1429    #[test]
1430    fn test_mixed_f64_i32_ptr() {
1431        // (F64, I32, String) — String is a heap pointer.
1432        // f64 @ 0  (size 8)
1433        // i32 @ 8  (size 4)
1434        // ptr @ 16 (needs 8-align from offset 12, pad to 16; size 8)
1435        // captures_size = 24
1436        let layout =
1437            immutable_layout(&[ConcreteType::F64, ConcreteType::I32, ConcreteType::String]);
1438        assert_eq!(layout.capture_count(), 3);
1439        assert_eq!(layout.capture_offset(0), 0);
1440        assert_eq!(layout.capture_offset(1), 8);
1441        assert_eq!(layout.capture_offset(2), 16);
1442        assert_eq!(layout.capture_kind(0), FieldKind::F64);
1443        assert_eq!(layout.capture_kind(1), FieldKind::I32);
1444        assert_eq!(layout.capture_kind(2), FieldKind::Ptr);
1445        assert_eq!(layout.captures_size, 24);
1446        assert_eq!(layout.heap_capture_mask, 0b100);
1447        assert!(layout.is_heap_capture(2));
1448        assert!(!layout.is_heap_capture(0));
1449        assert!(!layout.is_heap_capture(1));
1450        assert_eq!(layout.total_heap_size(), 40);
1451        assert_eq!(layout.total_stack_size(), 32);
1452    }
1453
1454    #[test]
1455    fn test_single_heap_typed_capture_string() {
1456        // Single String (Ptr) capture: captures area = 8 bytes, mask bit 0 set.
1457        let layout = immutable_layout(&[ConcreteType::String]);
1458        assert_eq!(layout.capture_offset(0), 0);
1459        assert_eq!(layout.capture_kind(0), FieldKind::Ptr);
1460        assert_eq!(layout.captures_size, 8);
1461        assert_eq!(layout.heap_capture_mask, 0b1);
1462        assert!(layout.is_heap_capture(0));
1463        assert_eq!(layout.total_heap_size(), 24);
1464        assert_eq!(layout.total_stack_size(), 16);
1465    }
1466
1467    #[test]
1468    fn test_array_capture_is_heap() {
1469        // Array<int> is a heap-typed pointer.
1470        let arr = ConcreteType::Array(Box::new(ConcreteType::I64));
1471        let layout = immutable_layout(&[arr]);
1472        assert_eq!(layout.capture_kind(0), FieldKind::Ptr);
1473        assert_eq!(layout.heap_capture_mask, 0b1);
1474    }
1475
1476    #[test]
1477    fn test_struct_capture_is_heap() {
1478        let s = ConcreteType::Struct(StructLayoutId(42));
1479        let layout = immutable_layout(&[s]);
1480        assert_eq!(layout.capture_kind(0), FieldKind::Ptr);
1481        assert_eq!(layout.heap_capture_mask, 0b1);
1482    }
1483
1484    #[test]
1485    fn test_small_field_packing() {
1486        // (Bool, I8, I16, I32) — small fields pack tightly.
1487        // bool @ 0 (size 1)
1488        // i8   @ 1 (size 1)
1489        // i16  @ 2 (size 2)  — 2 is already 2-aligned
1490        // i32  @ 4 (size 4)  — 4 is 4-aligned
1491        // captures_size = 8 (rounded up to 8)
1492        let layout = immutable_layout(&[
1493            ConcreteType::Bool,
1494            ConcreteType::I8,
1495            ConcreteType::I16,
1496            ConcreteType::I32,
1497        ]);
1498        assert_eq!(layout.capture_offset(0), 0);
1499        assert_eq!(layout.capture_offset(1), 1);
1500        assert_eq!(layout.capture_offset(2), 2);
1501        assert_eq!(layout.capture_offset(3), 4);
1502        assert_eq!(layout.captures_size, 8);
1503        assert_eq!(layout.heap_capture_mask, 0);
1504    }
1505
1506    #[test]
1507    fn test_heap_mask_positions() {
1508        // (I32, String, F64, Array<F64>) → Ptr at positions 1 and 3.
1509        let arr = ConcreteType::Array(Box::new(ConcreteType::F64));
1510        let layout = immutable_layout(&[
1511            ConcreteType::I32,
1512            ConcreteType::String,
1513            ConcreteType::F64,
1514            arr,
1515        ]);
1516        assert_eq!(layout.heap_capture_mask, 0b1010);
1517        assert!(!layout.is_heap_capture(0));
1518        assert!(layout.is_heap_capture(1));
1519        assert!(!layout.is_heap_capture(2));
1520        assert!(layout.is_heap_capture(3));
1521    }
1522
1523    #[test]
1524    fn test_offsets_relative_and_absolute_agree() {
1525        let layout =
1526            immutable_layout(&[ConcreteType::F64, ConcreteType::I64, ConcreteType::String]);
1527        for i in 0..layout.capture_count() {
1528            assert_eq!(layout.heap_capture_offset(i), 16 + layout.capture_offset(i));
1529            assert_eq!(layout.stack_capture_offset(i), 8 + layout.capture_offset(i));
1530        }
1531    }
1532
1533    #[test]
1534    fn test_size_rounded_up_for_trailing_small_field() {
1535        // Single Bool: 1 byte, rounded up to 8.
1536        let layout = immutable_layout(&[ConcreteType::Bool]);
1537        assert_eq!(layout.captures_size, 8);
1538        assert_eq!(layout.total_heap_size(), 24);
1539        assert_eq!(layout.total_stack_size(), 16);
1540    }
1541
1542    // ---- ClosureRegistry tests ----
1543
1544    #[test]
1545    fn test_registry_empty() {
1546        let r = ClosureRegistry::new();
1547        assert_eq!(r.len(), 0);
1548        assert!(r.is_empty());
1549    }
1550
1551    #[test]
1552    fn test_registry_same_signature_returns_same_id() {
1553        let mut r = ClosureRegistry::new();
1554        let id_a = r.intern(vec![ConcreteType::I64]);
1555        let id_b = r.intern(vec![ConcreteType::I64]);
1556        assert_eq!(id_a, id_b);
1557        assert_eq!(r.len(), 1);
1558    }
1559
1560    #[test]
1561    fn test_registry_different_signatures_returns_different_ids() {
1562        let mut r = ClosureRegistry::new();
1563        let id_empty = r.intern(vec![]);
1564        let id_i64 = r.intern(vec![ConcreteType::I64]);
1565        let id_f64 = r.intern(vec![ConcreteType::F64]);
1566        let id_i64_f64 = r.intern(vec![ConcreteType::I64, ConcreteType::F64]);
1567        let id_f64_i64 = r.intern(vec![ConcreteType::F64, ConcreteType::I64]);
1568
1569        assert_ne!(id_empty, id_i64);
1570        assert_ne!(id_i64, id_f64);
1571        assert_ne!(id_i64_f64, id_f64_i64, "order matters in the signature");
1572        assert_eq!(r.len(), 5);
1573    }
1574
1575    #[test]
1576    fn test_registry_roundtrip_and_layout_retrieval() {
1577        let mut r = ClosureRegistry::new();
1578        let id = r.intern(vec![ConcreteType::F64, ConcreteType::String]);
1579        let layout = r.get(id).expect("layout should exist");
1580        assert_eq!(layout.capture_count(), 2);
1581        assert_eq!(layout.capture_kind(0), FieldKind::F64);
1582        assert_eq!(layout.capture_kind(1), FieldKind::Ptr);
1583        assert_eq!(layout.heap_capture_mask, 0b10);
1584    }
1585
1586    #[test]
1587    fn test_registry_lookup_without_intern() {
1588        let mut r = ClosureRegistry::new();
1589        assert_eq!(r.lookup(&[ConcreteType::I64]), None);
1590        let id = r.intern(vec![ConcreteType::I64]);
1591        assert_eq!(r.lookup(&[ConcreteType::I64]), Some(id));
1592        assert_eq!(r.lookup(&[ConcreteType::F64]), None);
1593    }
1594
1595    #[test]
1596    fn test_registry_iter() {
1597        let mut r = ClosureRegistry::new();
1598        r.intern(vec![]);
1599        r.intern(vec![ConcreteType::I64]);
1600        r.intern(vec![ConcreteType::F64]);
1601        let collected: Vec<_> = r.iter().collect();
1602        assert_eq!(collected.len(), 3);
1603        assert_eq!(collected[0].0, ClosureTypeId(0));
1604        assert_eq!(collected[1].0, ClosureTypeId(1));
1605        assert_eq!(collected[2].0, ClosureTypeId(2));
1606    }
1607
1608    #[test]
1609    fn test_registry_ids_are_sequential_from_zero() {
1610        let mut r = ClosureRegistry::new();
1611        let a = r.intern(vec![ConcreteType::I64]);
1612        let b = r.intern(vec![ConcreteType::F64]);
1613        let c = r.intern(vec![ConcreteType::Bool]);
1614        assert_eq!(a, ClosureTypeId(0));
1615        assert_eq!(b, ClosureTypeId(1));
1616        assert_eq!(c, ClosureTypeId(2));
1617    }
1618
1619    #[test]
1620    fn test_registry_nested_types_are_distinct() {
1621        let mut r = ClosureRegistry::new();
1622        let arr_i64 = ConcreteType::Array(Box::new(ConcreteType::I64));
1623        let arr_f64 = ConcreteType::Array(Box::new(ConcreteType::F64));
1624        let id1 = r.intern(vec![arr_i64]);
1625        let id2 = r.intern(vec![arr_f64]);
1626        assert_ne!(id1, id2);
1627    }
1628
1629    // ---- Compile-time size / repr checks ----
1630
1631    #[test]
1632    fn test_sizeof_stack_closure_is_8() {
1633        assert_eq!(std::mem::size_of::<StackClosure>(), 8);
1634    }
1635
1636    #[test]
1637    fn test_sizeof_typed_closure_header_is_16() {
1638        assert_eq!(std::mem::size_of::<TypedClosureHeader>(), 16);
1639    }
1640
1641    #[test]
1642    fn test_header_constants() {
1643        assert_eq!(HEAP_CLOSURE_HEADER_SIZE, 16);
1644        assert_eq!(STACK_CLOSURE_HEADER_SIZE, 8);
1645    }
1646
1647    // ---- capture_inner_kind tests ----
1648
1649    #[test]
1650    fn capture_inner_kind_immutable_matches_capture_kind() {
1651        // Immutable captures: slot kind == interior kind for all types.
1652        let kinds = vec![
1653            CaptureKind::Immutable,
1654            CaptureKind::Immutable,
1655            CaptureKind::Immutable,
1656        ];
1657        let layout = ClosureLayout::from_capture_types(
1658            &[ConcreteType::I64, ConcreteType::F64, ConcreteType::String],
1659            &kinds,
1660        );
1661        assert_eq!(layout.capture_kind(0), FieldKind::I64);
1662        assert_eq!(layout.capture_inner_kind(0), FieldKind::I64);
1663        assert_eq!(layout.capture_kind(1), FieldKind::F64);
1664        assert_eq!(layout.capture_inner_kind(1), FieldKind::F64);
1665        // String is a heap-typed Ptr in both views.
1666        assert_eq!(layout.capture_kind(2), FieldKind::Ptr);
1667        assert_eq!(layout.capture_inner_kind(2), FieldKind::Ptr);
1668    }
1669
1670    #[test]
1671    fn capture_inner_kind_owned_mutable_returns_interior() {
1672        // OwnedMutable<i64>: slot kind is Ptr (Box<i64> *mut), interior is I64.
1673        let kinds = vec![CaptureKind::OwnedMutable];
1674        let layout = ClosureLayout::from_capture_types(&[ConcreteType::I64], &kinds);
1675        assert_eq!(layout.capture_kind(0), FieldKind::Ptr);
1676        assert_eq!(layout.capture_inner_kind(0), FieldKind::I64);
1677    }
1678
1679    #[test]
1680    fn capture_inner_kind_owned_mutable_f64() {
1681        let kinds = vec![CaptureKind::OwnedMutable];
1682        let layout = ClosureLayout::from_capture_types(&[ConcreteType::F64], &kinds);
1683        assert_eq!(layout.capture_kind(0), FieldKind::Ptr);
1684        assert_eq!(layout.capture_inner_kind(0), FieldKind::F64);
1685    }
1686
1687    #[test]
1688    fn capture_inner_kind_shared_returns_interior() {
1689        // Shared<bool>: slot kind is Ptr (*const SharedCell), interior is Bool.
1690        let kinds = vec![CaptureKind::Shared];
1691        let layout = ClosureLayout::from_capture_types(&[ConcreteType::Bool], &kinds);
1692        assert_eq!(layout.capture_kind(0), FieldKind::Ptr);
1693        assert_eq!(layout.capture_inner_kind(0), FieldKind::Bool);
1694    }
1695
1696    #[test]
1697    fn capture_inner_kind_owned_mutable_ptr() {
1698        // OwnedMutable<String>: slot kind is Ptr, interior is also Ptr
1699        // (the box contains a heap pointer that itself owns a refcount).
1700        let kinds = vec![CaptureKind::OwnedMutable];
1701        let layout = ClosureLayout::from_capture_types(&[ConcreteType::String], &kinds);
1702        assert_eq!(layout.capture_kind(0), FieldKind::Ptr);
1703        assert_eq!(layout.capture_inner_kind(0), FieldKind::Ptr);
1704    }
1705
1706    // ---- ADR-006 §2.7.8 / Q10 — capture_native_kinds tests ----
1707
1708    #[test]
1709    fn capture_native_kinds_inline_scalars() {
1710        // Inline-scalar `ConcreteType`s map to their matching inline
1711        // `NativeKind` (lockstep with `capture_types`).
1712        let layout = immutable_layout(&[
1713            ConcreteType::F64,
1714            ConcreteType::I64,
1715            ConcreteType::I32,
1716            ConcreteType::Bool,
1717        ]);
1718        assert_eq!(layout.capture_native_kinds.len(), 4);
1719        assert_eq!(layout.capture_native_kind(0), NativeKind::Float64);
1720        assert_eq!(layout.capture_native_kind(1), NativeKind::Int64);
1721        assert_eq!(layout.capture_native_kind(2), NativeKind::Int32);
1722        assert_eq!(layout.capture_native_kind(3), NativeKind::Bool);
1723    }
1724
1725    #[test]
1726    fn capture_native_kinds_string() {
1727        // String captures map to NativeKind::String — the special-cased
1728        // most-common heap shape per ADR-005 §2.
1729        let layout = immutable_layout(&[ConcreteType::String]);
1730        assert_eq!(layout.capture_native_kind(0), NativeKind::String);
1731    }
1732
1733    #[test]
1734    fn capture_native_kinds_typed_array() {
1735        // Array<T> captures map to NativeKind::Ptr(HeapKind::TypedArray)
1736        // (the underlying storage is `Arc<TypedArrayData>`).
1737        let arr = ConcreteType::Array(Box::new(ConcreteType::F64));
1738        let layout = immutable_layout(&[arr]);
1739        assert_eq!(
1740            layout.capture_native_kind(0),
1741            NativeKind::Ptr(HeapKind::TypedArray)
1742        );
1743    }
1744
1745    #[test]
1746    fn capture_native_kinds_struct() {
1747        // Struct captures map to NativeKind::Ptr(HeapKind::TypedObject).
1748        let s = ConcreteType::Struct(StructLayoutId(7));
1749        let layout = immutable_layout(&[s]);
1750        assert_eq!(
1751            layout.capture_native_kind(0),
1752            NativeKind::Ptr(HeapKind::TypedObject)
1753        );
1754    }
1755
1756    #[test]
1757    fn capture_native_kinds_lockstep_with_capture_types() {
1758        // The §2.7.8 / Q10 lockstep invariant: every constructed layout
1759        // satisfies `capture_types.len() == capture_native_kinds.len() ==
1760        // capture_kinds.len()`.
1761        let layout = immutable_layout(&[
1762            ConcreteType::F64,
1763            ConcreteType::String,
1764            ConcreteType::I32,
1765        ]);
1766        assert_eq!(
1767            layout.capture_types.len(),
1768            layout.capture_native_kinds.len()
1769        );
1770        assert_eq!(layout.capture_types.len(), layout.capture_kinds.len());
1771        assert_eq!(layout.capture_types.len(), layout.captures.len());
1772    }
1773
1774    #[test]
1775    fn capture_native_kinds_from_explicit_constructor() {
1776        // The explicit-kinds constructor lets the caller pin the kind
1777        // track to a finer-grained source than ConcreteType can express
1778        // (e.g. specifying HeapKind::HashMap for a generic Pointer).
1779        let types = vec![ConcreteType::Pointer(Box::new(ConcreteType::Void))];
1780        let kinds = vec![CaptureKind::Immutable];
1781        let native_kinds = vec![NativeKind::Ptr(HeapKind::HashMap)];
1782        let layout = ClosureLayout::from_capture_types_with_native_kinds(
1783            &types,
1784            &kinds,
1785            &native_kinds,
1786        );
1787        assert_eq!(
1788            layout.capture_native_kind(0),
1789            NativeKind::Ptr(HeapKind::HashMap)
1790        );
1791        // Geometry from the underlying ConcreteType is unchanged — it's
1792        // the kind track alone that the explicit constructor overrides.
1793        assert_eq!(layout.capture_kind(0), FieldKind::Ptr);
1794    }
1795
1796    #[test]
1797    #[should_panic(expected = "must have equal length")]
1798    fn capture_native_kinds_explicit_constructor_length_mismatch_panics() {
1799        // Passing mismatched-length slices violates the §2.7.8 / Q10
1800        // lockstep invariant — the constructor MUST panic, not silently
1801        // truncate or pad.
1802        let types = vec![ConcreteType::F64, ConcreteType::I64];
1803        let kinds = vec![CaptureKind::Immutable, CaptureKind::Immutable];
1804        let native_kinds = vec![NativeKind::Float64]; // wrong length
1805        let _ = ClosureLayout::from_capture_types_with_native_kinds(
1806            &types,
1807            &kinds,
1808            &native_kinds,
1809        );
1810    }
1811
1812    #[test]
1813    fn native_kind_from_concrete_type_inline_scalars() {
1814        // Round-trip every inline-scalar ConcreteType through the
1815        // mapping helper.
1816        assert_eq!(
1817            native_kind_from_concrete_type(&ConcreteType::F64),
1818            NativeKind::Float64
1819        );
1820        assert_eq!(
1821            native_kind_from_concrete_type(&ConcreteType::I64),
1822            NativeKind::Int64
1823        );
1824        assert_eq!(
1825            native_kind_from_concrete_type(&ConcreteType::I32),
1826            NativeKind::Int32
1827        );
1828        assert_eq!(
1829            native_kind_from_concrete_type(&ConcreteType::I16),
1830            NativeKind::Int16
1831        );
1832        assert_eq!(
1833            native_kind_from_concrete_type(&ConcreteType::I8),
1834            NativeKind::Int8
1835        );
1836        assert_eq!(
1837            native_kind_from_concrete_type(&ConcreteType::U64),
1838            NativeKind::UInt64
1839        );
1840        assert_eq!(
1841            native_kind_from_concrete_type(&ConcreteType::U32),
1842            NativeKind::UInt32
1843        );
1844        assert_eq!(
1845            native_kind_from_concrete_type(&ConcreteType::U16),
1846            NativeKind::UInt16
1847        );
1848        assert_eq!(
1849            native_kind_from_concrete_type(&ConcreteType::U8),
1850            NativeKind::UInt8
1851        );
1852        assert_eq!(
1853            native_kind_from_concrete_type(&ConcreteType::Bool),
1854            NativeKind::Bool
1855        );
1856    }
1857
1858    #[test]
1859    fn native_kind_from_concrete_type_heap_arms() {
1860        // Heap ConcreteType arms map to their matching Ptr(HeapKind)
1861        // discriminator (or NativeKind::String for the ADR-005 §2 special
1862        // case).
1863        assert_eq!(
1864            native_kind_from_concrete_type(&ConcreteType::String),
1865            NativeKind::String
1866        );
1867        assert_eq!(
1868            native_kind_from_concrete_type(&ConcreteType::Decimal),
1869            NativeKind::Ptr(HeapKind::Decimal)
1870        );
1871        assert_eq!(
1872            native_kind_from_concrete_type(&ConcreteType::BigInt),
1873            NativeKind::Ptr(HeapKind::BigInt)
1874        );
1875        assert_eq!(
1876            native_kind_from_concrete_type(&ConcreteType::DateTime),
1877            NativeKind::Ptr(HeapKind::Temporal)
1878        );
1879    }
1880
1881    #[test]
1882    #[should_panic(expected = "Void is not a well-formed capture type")]
1883    fn native_kind_from_concrete_type_void_panics() {
1884        // Top-level ConcreteType::Void in a capture slot is malformed —
1885        // the helper refuses to map it to a sentinel kind (a Bool-default
1886        // fallback would be forbidden #9 per §2.7.7).
1887        let _ = native_kind_from_concrete_type(&ConcreteType::Void);
1888    }
1889}