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