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}