shape_value/heap_value.rs
1//! Heap-allocated value types reachable through `HeapValue`.
2//!
3//! After the strict-typing Phase-2 bulldozer (option C — heterogeneous
4//! collections / dynamic single-value wrappers excised), `HeapValue` carries
5//! only typed payloads:
6//!
7//! - typed primitives (string, decimal, bigint, char, future-id),
8//! - typed handles (datatable, content, instant, io-handle, native scalars),
9//! - typed object slots (`TypedObject` with `Box<[ValueSlot]>`),
10//! - the typed-closure-raw block (`ClosureRaw`),
11//! - typed temporal data (`TemporalData`),
12//! - typed table views (`TableViewData`).
13//!
14//! V3-S5 ckpt-1..ckpt-4 (2026-05-15): the inline `TypedArrayData` enum +
15//! the outer `HeapValue::TypedArray(Arc<TypedArrayData>)` arm +
16//! `TypedBuffer<T>` / `AlignedTypedBuffer` wrapper layer were retired
17//! wholesale per W12-typed-array-data-deletion-audit §3.5 + §B + ADR-006
18//! §2.7.24 Q25.A SUPERSEDED. The canonical replacement is the v2-raw
19//! `TypedArray<T>` flat struct at `crate::v2::typed_array::TypedArray<T>`
20//! (per `docs/runtime-v2-spec.md`). The `HeapKind::TypedArray = 8`
21//! ordinal is vacated; do not reuse.
22//!
23//! Variants that previously held `ValueWord` (the deleted dynamic word) —
24//! `Some`/`Ok`/`Err`/`Range`/`TraitObject`/`FunctionRef`,
25//! `HashMap`/`Set`/`Deque`/`PriorityQueue`, `Iterator`/`Generator`/
26//! `ProjectedRef`, `Concurrency` (Mutex/Atomic/Lazy/Channel), `Rare`,
27//! `Enum`, `Array` (heterogeneous-element), `HostClosure` — were removed
28//! together with their `*Data` structs. The corresponding `HeapKind`
29//! ordinals are preserved (annotated "(removed)" in `heap_variants.rs`)
30//! and await monomorphized typed replacements per `docs/runtime-v2-spec.md`.
31
32use crate::aligned_vec::AlignedVec;
33use std::fmt;
34use std::sync::Arc;
35
36// ── Matrix storage (carries `HeapKind::Matrix` and `HeapKind::MatrixSlice`) ──
37//
38// ADR-006 §2.7.22 amendment (Round 18 S3 W12-matrix-floatslice-heapkind-exit,
39// 2026-05-13): Matrix is a single Matrix value (NOT a buffer-of-Matrix), and
40// exits the `TypedArrayData` carrier hierarchy. `HeapKind::Matrix = 34` +
41// `HeapValue::Matrix(Arc<MatrixData>)`; FloatSlice projection becomes
42// `HeapKind::MatrixSlice = 35` + `HeapValue::MatrixSlice(Arc<MatrixSliceData>)`.
43// The prior §2.7.22 Q23 ruling (Matrix lives under `HeapKind::TypedArray` via
44// `TypedArrayData::Matrix`) is superseded — see §2.7.22 amendment text.
45
46/// Flat, SIMD-aligned matrix storage (row-major order).
47#[derive(Debug, Clone)]
48pub struct MatrixData {
49 pub data: AlignedVec<f64>,
50 pub rows: u32,
51 pub cols: u32,
52}
53
54impl MatrixData {
55 /// Create a zero-initialized matrix.
56 pub fn new(rows: u32, cols: u32) -> Self {
57 let len = (rows as usize) * (cols as usize);
58 let mut data = AlignedVec::with_capacity(len);
59 for _ in 0..len {
60 data.push(0.0);
61 }
62 Self { data, rows, cols }
63 }
64
65 /// Create from a flat data buffer.
66 pub fn from_flat(data: AlignedVec<f64>, rows: u32, cols: u32) -> Self {
67 debug_assert_eq!(data.len(), (rows as usize) * (cols as usize));
68 Self { data, rows, cols }
69 }
70
71 /// Get element at (row, col).
72 #[inline]
73 pub fn get(&self, row: u32, col: u32) -> f64 {
74 self.data[(row as usize) * (self.cols as usize) + (col as usize)]
75 }
76
77 /// Set element at (row, col).
78 #[inline]
79 pub fn set(&mut self, row: u32, col: u32, val: f64) {
80 self.data[(row as usize) * (self.cols as usize) + (col as usize)] = val;
81 }
82
83 /// Get a row slice.
84 #[inline]
85 pub fn row_slice(&self, row: u32) -> &[f64] {
86 let start = (row as usize) * (self.cols as usize);
87 &self.data[start..start + self.cols as usize]
88 }
89
90 /// Get shape as (rows, cols).
91 #[inline]
92 pub fn shape(&self) -> (u32, u32) {
93 (self.rows, self.cols)
94 }
95
96 /// Get a row's data as a slice (alias for `row_slice`).
97 #[inline]
98 pub fn row_data(&self, row: u32) -> &[f64] {
99 self.row_slice(row)
100 }
101}
102
103/// Row/column projection into a parent `MatrixData` (`{ parent, offset, len }`).
104///
105/// ADR-006 §2.7.22 amendment (Round 18 S3 W12-matrix-floatslice-heapkind-exit,
106/// 2026-05-13): FloatSlice exits the `TypedArrayData` carrier hierarchy as a
107/// category-error. It is a projection-into-a-Matrix, not a buffer of floats.
108/// The carrier is `Arc<MatrixSliceData>` with kind `Ptr(HeapKind::MatrixSlice)`,
109/// constructed by `Matrix.row(i)` / `Matrix.col(i)` projection methods.
110///
111/// Aliasing semantics: the projection shares the parent Matrix's buffer
112/// (mutating through the projection writes through to the parent), preserved
113/// from the pre-amendment `TypedArrayData::FloatSlice` shape. The `parent`
114/// Arc retains one strong-count share for the projection's lifetime.
115#[derive(Debug, Clone)]
116pub struct MatrixSliceData {
117 pub parent: Arc<MatrixData>,
118 pub offset: u32,
119 pub len: u32,
120}
121
122impl MatrixSliceData {
123 /// Construct a projection into a parent matrix.
124 #[inline]
125 pub fn new(parent: Arc<MatrixData>, offset: u32, len: u32) -> Self {
126 Self { parent, offset, len }
127 }
128
129 /// Borrow the underlying slice into the parent's flat data buffer.
130 #[inline]
131 pub fn as_slice(&self) -> &[f64] {
132 let off = self.offset as usize;
133 let n = self.len as usize;
134 &self.parent.data.as_slice()[off..off + n]
135 }
136}
137
138// ── NativeScalar — width-preserving native ABI scalars ──────────────────────
139
140/// Native ABI-width scalars used by C ABI / `extern C fn` boundaries.
141///
142/// These values preserve their ABI width across VM boundaries so C wrappers
143/// can avoid lossy `i64` normalization.
144#[derive(Debug, Clone, Copy, PartialEq)]
145pub enum NativeScalar {
146 I8(i8),
147 U8(u8),
148 I16(i16),
149 U16(u16),
150 I32(i32),
151 I64(i64),
152 U32(u32),
153 U64(u64),
154 Isize(isize),
155 Usize(usize),
156 Ptr(usize),
157 F32(f32),
158}
159
160impl NativeScalar {
161 #[inline]
162 pub fn type_name(&self) -> &'static str {
163 match self {
164 NativeScalar::I8(_) => "i8",
165 NativeScalar::U8(_) => "u8",
166 NativeScalar::I16(_) => "i16",
167 NativeScalar::U16(_) => "u16",
168 NativeScalar::I32(_) => "i32",
169 NativeScalar::I64(_) => "i64",
170 NativeScalar::U32(_) => "u32",
171 NativeScalar::U64(_) => "u64",
172 NativeScalar::Isize(_) => "isize",
173 NativeScalar::Usize(_) => "usize",
174 NativeScalar::Ptr(_) => "ptr",
175 NativeScalar::F32(_) => "f32",
176 }
177 }
178
179 #[inline]
180 pub fn is_truthy(&self) -> bool {
181 match self {
182 NativeScalar::I8(v) => *v != 0,
183 NativeScalar::U8(v) => *v != 0,
184 NativeScalar::I16(v) => *v != 0,
185 NativeScalar::U16(v) => *v != 0,
186 NativeScalar::I32(v) => *v != 0,
187 NativeScalar::I64(v) => *v != 0,
188 NativeScalar::U32(v) => *v != 0,
189 NativeScalar::U64(v) => *v != 0,
190 NativeScalar::Isize(v) => *v != 0,
191 NativeScalar::Usize(v) => *v != 0,
192 NativeScalar::Ptr(v) => *v != 0,
193 NativeScalar::F32(v) => *v != 0.0 && !v.is_nan(),
194 }
195 }
196
197 #[inline]
198 pub fn as_i64(&self) -> Option<i64> {
199 match self {
200 NativeScalar::I8(v) => Some(*v as i64),
201 NativeScalar::U8(v) => Some(*v as i64),
202 NativeScalar::I16(v) => Some(*v as i64),
203 NativeScalar::U16(v) => Some(*v as i64),
204 NativeScalar::I32(v) => Some(*v as i64),
205 NativeScalar::I64(v) => Some(*v),
206 NativeScalar::U32(v) => Some(*v as i64),
207 NativeScalar::U64(v) => i64::try_from(*v).ok(),
208 NativeScalar::Isize(v) => i64::try_from(*v).ok(),
209 NativeScalar::Usize(v) => i64::try_from(*v).ok(),
210 NativeScalar::Ptr(v) => i64::try_from(*v).ok(),
211 NativeScalar::F32(_) => None,
212 }
213 }
214
215 #[inline]
216 pub fn as_u64(&self) -> Option<u64> {
217 match self {
218 NativeScalar::U8(v) => Some(*v as u64),
219 NativeScalar::U16(v) => Some(*v as u64),
220 NativeScalar::U32(v) => Some(*v as u64),
221 NativeScalar::U64(v) => Some(*v),
222 NativeScalar::Usize(v) => Some(*v as u64),
223 NativeScalar::Ptr(v) => Some(*v as u64),
224 NativeScalar::I8(v) if *v >= 0 => Some(*v as u64),
225 NativeScalar::I16(v) if *v >= 0 => Some(*v as u64),
226 NativeScalar::I32(v) if *v >= 0 => Some(*v as u64),
227 NativeScalar::I64(v) if *v >= 0 => Some(*v as u64),
228 NativeScalar::Isize(v) if *v >= 0 => Some(*v as u64),
229 _ => None,
230 }
231 }
232
233 #[inline]
234 pub fn as_i128(&self) -> Option<i128> {
235 match self {
236 NativeScalar::I8(v) => Some(*v as i128),
237 NativeScalar::U8(v) => Some(*v as i128),
238 NativeScalar::I16(v) => Some(*v as i128),
239 NativeScalar::U16(v) => Some(*v as i128),
240 NativeScalar::I32(v) => Some(*v as i128),
241 NativeScalar::U32(v) => Some(*v as i128),
242 NativeScalar::I64(v) => Some(*v as i128),
243 NativeScalar::U64(v) => Some(*v as i128),
244 NativeScalar::Isize(v) => Some(*v as i128),
245 NativeScalar::Usize(v) => Some(*v as i128),
246 NativeScalar::Ptr(v) => Some(*v as i128),
247 NativeScalar::F32(_) => None,
248 }
249 }
250
251 #[inline]
252 pub fn as_f64(&self) -> f64 {
253 match self {
254 NativeScalar::I8(v) => *v as f64,
255 NativeScalar::U8(v) => *v as f64,
256 NativeScalar::I16(v) => *v as f64,
257 NativeScalar::U16(v) => *v as f64,
258 NativeScalar::I32(v) => *v as f64,
259 NativeScalar::I64(v) => *v as f64,
260 NativeScalar::U32(v) => *v as f64,
261 NativeScalar::U64(v) => *v as f64,
262 NativeScalar::Isize(v) => *v as f64,
263 NativeScalar::Usize(v) => *v as f64,
264 NativeScalar::Ptr(v) => *v as f64,
265 NativeScalar::F32(v) => *v as f64,
266 }
267 }
268}
269
270impl std::fmt::Display for NativeScalar {
271 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272 match self {
273 NativeScalar::I8(v) => write!(f, "{v}"),
274 NativeScalar::U8(v) => write!(f, "{v}"),
275 NativeScalar::I16(v) => write!(f, "{v}"),
276 NativeScalar::U16(v) => write!(f, "{v}"),
277 NativeScalar::I32(v) => write!(f, "{v}"),
278 NativeScalar::I64(v) => write!(f, "{v}"),
279 NativeScalar::U32(v) => write!(f, "{v}"),
280 NativeScalar::U64(v) => write!(f, "{v}"),
281 NativeScalar::Isize(v) => write!(f, "{v}"),
282 NativeScalar::Usize(v) => write!(f, "{v}"),
283 NativeScalar::Ptr(v) => write!(f, "0x{v:x}"),
284 NativeScalar::F32(v) => write!(f, "{v}"),
285 }
286 }
287}
288
289// ── Native type layouts (used by C ABI native views) ─────────────────────────
290
291/// Field layout metadata for `type C` structs.
292#[derive(Debug, Clone)]
293pub struct NativeLayoutField {
294 pub name: String,
295 pub c_type: String,
296 pub offset: u32,
297 pub size: u32,
298 pub align: u32,
299}
300
301/// Runtime layout descriptor for one native type.
302#[derive(Debug, Clone)]
303pub struct NativeTypeLayout {
304 pub name: String,
305 pub abi: String,
306 pub size: u32,
307 pub align: u32,
308 pub fields: Vec<NativeLayoutField>,
309}
310
311impl NativeTypeLayout {
312 #[inline]
313 pub fn field(&self, name: &str) -> Option<&NativeLayoutField> {
314 self.fields.iter().find(|field| field.name == name)
315 }
316}
317
318/// Pointer-backed zero-copy view into native memory.
319#[derive(Debug, Clone)]
320pub struct NativeViewData {
321 pub ptr: usize,
322 pub layout: Arc<NativeTypeLayout>,
323 pub mutable: bool,
324}
325
326// ── I/O handles ──────────────────────────────────────────────────────────────
327
328/// I/O handle kind discriminant.
329#[derive(Debug, Clone, Copy, PartialEq, Eq)]
330#[repr(u8)]
331pub enum IoHandleKind {
332 File = 0,
333 TcpStream = 1,
334 TcpListener = 2,
335 UdpSocket = 3,
336 ChildProcess = 4,
337 PipeReader = 5,
338 PipeWriter = 6,
339 Custom = 7,
340}
341
342/// The underlying OS resource wrapped by an IoHandle.
343pub enum IoResource {
344 File(std::fs::File),
345 TcpStream(std::net::TcpStream),
346 TcpListener(std::net::TcpListener),
347 UdpSocket(std::net::UdpSocket),
348 ChildProcess(std::process::Child),
349 PipeReader(std::process::ChildStdout),
350 PipeWriter(std::process::ChildStdin),
351 PipeReaderErr(std::process::ChildStderr),
352 /// Type-erased resource for custom I/O handles (e.g. memoized transports).
353 Custom(Box<dyn std::any::Any + Send>),
354}
355
356impl std::fmt::Debug for IoResource {
357 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358 match self {
359 IoResource::File(_) => write!(f, "File(...)"),
360 IoResource::TcpStream(_) => write!(f, "TcpStream(...)"),
361 IoResource::TcpListener(_) => write!(f, "TcpListener(...)"),
362 IoResource::UdpSocket(_) => write!(f, "UdpSocket(...)"),
363 IoResource::ChildProcess(_) => write!(f, "ChildProcess(...)"),
364 IoResource::PipeReader(_) => write!(f, "PipeReader(...)"),
365 IoResource::PipeWriter(_) => write!(f, "PipeWriter(...)"),
366 IoResource::PipeReaderErr(_) => write!(f, "PipeReaderErr(...)"),
367 IoResource::Custom(_) => write!(f, "Custom(...)"),
368 }
369 }
370}
371
372/// Data for IoHandle variant (Arc-wrapped at the HeapValue level to keep
373/// HeapValue small and to enable cluster #2 marshal `FromSlot for
374/// Arc<IoHandleData>`).
375///
376/// Wraps an OS resource (file, socket, process) in an Arc<Mutex<Option<IoResource>>>
377/// so it can be shared and closed. The `Option` is `None` after close().
378/// Rust's `Drop` closes the underlying resource if not already closed.
379///
380/// Storage: `HeapValue::IoHandle(Arc<IoHandleData>)`. The variant Arc is
381/// the marshal-layer's typed handle (per cluster #2 option γ in
382/// `docs/defections.md` 2026-05-06); the inner `Arc<Mutex<...>>` is the
383/// shared resource lock. Cloning the variant is one atomic op.
384#[derive(Clone)]
385pub struct IoHandleData {
386 pub kind: IoHandleKind,
387 pub resource: Arc<std::sync::Mutex<Option<IoResource>>>,
388 pub path: String,
389 pub mode: String,
390}
391
392impl std::fmt::Debug for IoHandleData {
393 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
394 f.debug_struct("IoHandleData")
395 .field("kind", &self.kind)
396 .field("path", &self.path)
397 .field("mode", &self.mode)
398 .field(
399 "open",
400 &self.resource.lock().map(|g| g.is_some()).unwrap_or(false),
401 )
402 .finish()
403 }
404}
405
406impl IoHandleData {
407 /// Create a new file handle.
408 pub fn new_file(file: std::fs::File, path: String, mode: String) -> Self {
409 Self {
410 kind: IoHandleKind::File,
411 resource: Arc::new(std::sync::Mutex::new(Some(IoResource::File(file)))),
412 path,
413 mode,
414 }
415 }
416
417 /// Create a new TCP stream handle.
418 pub fn new_tcp_stream(stream: std::net::TcpStream, addr: String) -> Self {
419 Self {
420 kind: IoHandleKind::TcpStream,
421 resource: Arc::new(std::sync::Mutex::new(Some(IoResource::TcpStream(stream)))),
422 path: addr,
423 mode: "rw".to_string(),
424 }
425 }
426
427 /// Create a new TCP listener handle.
428 pub fn new_tcp_listener(listener: std::net::TcpListener, addr: String) -> Self {
429 Self {
430 kind: IoHandleKind::TcpListener,
431 resource: Arc::new(std::sync::Mutex::new(Some(IoResource::TcpListener(
432 listener,
433 )))),
434 path: addr,
435 mode: "listen".to_string(),
436 }
437 }
438
439 /// Create a new UDP socket handle.
440 pub fn new_udp_socket(socket: std::net::UdpSocket, addr: String) -> Self {
441 Self {
442 kind: IoHandleKind::UdpSocket,
443 resource: Arc::new(std::sync::Mutex::new(Some(IoResource::UdpSocket(socket)))),
444 path: addr,
445 mode: "rw".to_string(),
446 }
447 }
448
449 /// Create a handle wrapping a spawned child process.
450 pub fn new_child_process(child: std::process::Child, cmd: String) -> Self {
451 Self {
452 kind: IoHandleKind::ChildProcess,
453 resource: Arc::new(std::sync::Mutex::new(Some(IoResource::ChildProcess(child)))),
454 path: cmd,
455 mode: "process".to_string(),
456 }
457 }
458
459 /// Create a handle wrapping a child stdout pipe.
460 pub fn new_pipe_reader(stdout: std::process::ChildStdout, label: String) -> Self {
461 Self {
462 kind: IoHandleKind::PipeReader,
463 resource: Arc::new(std::sync::Mutex::new(Some(IoResource::PipeReader(stdout)))),
464 path: label,
465 mode: "r".to_string(),
466 }
467 }
468
469 /// Create a handle wrapping a child stdin pipe.
470 pub fn new_pipe_writer(stdin: std::process::ChildStdin, label: String) -> Self {
471 Self {
472 kind: IoHandleKind::PipeWriter,
473 resource: Arc::new(std::sync::Mutex::new(Some(IoResource::PipeWriter(stdin)))),
474 path: label,
475 mode: "w".to_string(),
476 }
477 }
478
479 /// Create a handle wrapping a child stderr pipe.
480 pub fn new_pipe_reader_err(stderr: std::process::ChildStderr, label: String) -> Self {
481 Self {
482 kind: IoHandleKind::PipeReader,
483 resource: Arc::new(std::sync::Mutex::new(Some(IoResource::PipeReaderErr(
484 stderr,
485 )))),
486 path: label,
487 mode: "r".to_string(),
488 }
489 }
490
491 /// Create a handle wrapping a custom type-erased resource.
492 pub fn new_custom(resource: Box<dyn std::any::Any + Send>, label: String) -> Self {
493 Self {
494 kind: IoHandleKind::Custom,
495 resource: Arc::new(std::sync::Mutex::new(Some(IoResource::Custom(resource)))),
496 path: label,
497 mode: "custom".to_string(),
498 }
499 }
500
501 /// Check if the handle is still open.
502 pub fn is_open(&self) -> bool {
503 self.resource.lock().map(|g| g.is_some()).unwrap_or(false)
504 }
505
506 /// Close the handle, returning true if it was open.
507 pub fn close(&self) -> bool {
508 if let Ok(mut guard) = self.resource.lock() {
509 guard.take().is_some()
510 } else {
511 false
512 }
513 }
514}
515
516// ── TraitObjectStorage now lives at lines ~1790+ (W17-trait-object-storage merge) ──
517// The placeholder shape from W17-typed-carrier-bundle-A checkpoint 1 is
518// superseded by the real `TraitObjectStorage { value: Arc<TypedObjectStorage>,
519// vtable: Arc<VTable> }` from W17-trait-object-storage's close commit
520// (`e58218c`). 4-table lockstep landed there; HeapKind::TraitObject = 29
521// is the assigned ordinal.
522
523/// Owning newtype around `*const TypedObjectStorage` carrying one
524/// v2-raw refcount share on the pointed-to allocation's HeapHeader.
525///
526/// **Wave 2 Round 4 D4 ckpt-final (2026-05-14):** redesigned to own its
527/// share. Previously a trivially-Copy transparent newtype; that shape
528/// leaks element refcounts when the enclosing `Vec<TypedObjectPtr>`
529/// drops because trivial bit-copy Drop never calls `release_elem`. Now
530/// the wrapper:
531/// - Owns one v2-raw HeapHeader-at-offset-0 refcount share.
532/// - `Clone` bumps the refcount via `v2_retain`.
533/// - `Drop` retires the share via `TypedObjectStorage::release_elem`.
534/// - `Default` is the null pointer (no refcount share owed).
535///
536/// `#[repr(transparent)]` so the in-memory layout is identical to
537/// `*const TypedObjectStorage` — zero ABI cost vs the raw pointer; the
538/// wrapper exists only to localize the manual Send/Sync impl + the
539/// Drop/Clone refcount discipline (Rust disables auto-Send/Sync for ALL
540/// instantiations of a generic struct as soon as ANY manual impl exists,
541/// so per-T newtypes are the canonical workaround for raw-ptr inner
542/// elements in generic buffers).
543///
544/// Used as the inner element type of:
545/// - `HashMapValueBuf::TypedObject(Arc<TypedBuffer<TypedObjectPtr>>)`
546/// - `TypedArrayData::TypedObject(Arc<TypedBuffer<TypedObjectPtr>>)`
547///
548/// Construction-side contract: callers transfer one strong-count share
549/// on the v2-raw HeapHeader to the new `TypedObjectPtr`. Reads via
550/// `as_ptr()` return the underlying pointer without bumping refcount.
551#[repr(transparent)]
552#[derive(Debug, PartialEq, Eq, Hash)]
553pub struct TypedObjectPtr(pub *const TypedObjectStorage);
554
555// SAFETY: `*const TypedObjectStorage` is `!Send + !Sync` by default. The
556// wrapper is safe to share across threads because:
557// (1) `TypedObjectStorage` itself is `Send + Sync` (Box<[ValueSlot]> +
558// `Arc<[NativeKind]>` + POD fields; ValueSlot wraps `u64`).
559// (2) The HeapHeader-based refcount uses atomic ops (`v2_retain` /
560// `v2_release` in `v2/refcount.rs`).
561// (3) Aliasing safety is the same as `Arc<TypedObjectStorage>` — multiple
562// threads can hold their own retain shares concurrently.
563unsafe impl Send for TypedObjectPtr {}
564unsafe impl Sync for TypedObjectPtr {}
565
566impl Default for TypedObjectPtr {
567 /// Null pointer default — used by `TypedBuffer::<TypedObjectPtr>::push_null`
568 /// and similar default-requiring construction sites. Callers must
569 /// not dereference a default-constructed `TypedObjectPtr`. No
570 /// refcount share is owed for a null wrapper; Drop on a null
571 /// pointer is a no-op.
572 #[inline]
573 fn default() -> Self {
574 Self(std::ptr::null())
575 }
576}
577
578impl Clone for TypedObjectPtr {
579 /// v2-raw refcount bump via `v2_retain` on the pointed-to
580 /// HeapHeader. The clone owns its own share, retired at its own
581 /// `Drop`.
582 #[inline]
583 fn clone(&self) -> Self {
584 if !self.0.is_null() {
585 // SAFETY: per the construction-side contract, `self.0` points
586 // to a live `TypedObjectStorage` allocated via `_new` (or a
587 // legacy Arc-allocated one whose embedded HeapHeader is
588 // unused but still bumpable safely — atomic increment is
589 // sound on any aligned u32 within the legitimate allocation).
590 unsafe { crate::v2::refcount::v2_retain(&(*self.0).header) };
591 }
592 Self(self.0)
593 }
594}
595
596impl Drop for TypedObjectPtr {
597 /// Retire the owned share via `TypedObjectStorage::release_elem`
598 /// (HeapElement trait — calls `v2_release` and, on refcount=0,
599 /// runs `_drop` to dealloc the allocation + retire heap-mask
600 /// shares). No-op on null wrappers.
601 #[inline]
602 fn drop(&mut self) {
603 if !self.0.is_null() {
604 use crate::v2::heap_element::HeapElement;
605 // SAFETY: per the construction-side contract this carrier owns
606 // one share on the HeapHeader-at-offset-0 refcount.
607 unsafe { TypedObjectStorage::release_elem(self.0) };
608 }
609 }
610}
611
612impl TypedObjectPtr {
613 /// Construct from a raw pointer obtained via `TypedObjectStorage::_new`.
614 /// The caller transfers one strong-count share to the wrapper.
615 #[inline]
616 pub fn new(ptr: *const TypedObjectStorage) -> Self {
617 Self(ptr)
618 }
619
620 /// Recover the underlying raw pointer. Does NOT bump refcount;
621 /// the returned pointer is borrowed for the wrapper's lifetime.
622 #[inline]
623 pub fn as_ptr(&self) -> *const TypedObjectStorage {
624 self.0
625 }
626
627 /// Whether the pointer is null. Construction-side contract permits
628 /// null only for default-initialized cells.
629 #[inline]
630 pub fn is_null(&self) -> bool {
631 self.0.is_null()
632 }
633
634 /// Consume the wrapper without running Drop, returning the raw
635 /// pointer. The caller takes over the one refcount share. Mirror
636 /// of `Arc::into_raw`.
637 #[inline]
638 pub fn into_raw(self) -> *const TypedObjectStorage {
639 let ptr = self.0;
640 std::mem::forget(self);
641 ptr
642 }
643}
644
645// Deref to `&TypedObjectStorage` so consumer sites can read fields
646// (`s.slots`, `s.schema_id`, etc.) without manual unsafe deref. The
647// wrapper owns one refcount share for its lifetime, so the pointed-to
648// storage is live while the wrapper is in scope.
649//
650// Wave 2 Round 4 D4 ckpt-final-prime² (2026-05-14): added to support the
651// HeapValue::TypedObject(TypedObjectPtr) variant signature flip with
652// minimal consumer-cascade churn.
653impl std::ops::Deref for TypedObjectPtr {
654 type Target = TypedObjectStorage;
655 #[inline]
656 fn deref(&self) -> &TypedObjectStorage {
657 // SAFETY: per the construction-side contract, `self.0` is non-null
658 // for any `TypedObjectPtr` constructed via `new(_new(...))` or
659 // cloned from such. The default-constructed null pointer must not
660 // be dereferenced — callers reading fields are expected to hold a
661 // wrapper that owns a real share. Debug builds catch the null case
662 // via the assert; release builds UB on deref of null (mirroring
663 // `Arc<T>::deref` semantics for a null Arc — not constructable
664 // without `unsafe`). Length-zero storage is fine.
665 debug_assert!(
666 !self.0.is_null(),
667 "TypedObjectPtr::deref on null pointer (default-constructed wrapper \
668 must not be dereferenced)"
669 );
670 unsafe { &*self.0 }
671 }
672}
673
674// ── TraitObjectPtr (Wave 2 Round 4 D4 ckpt-final-prime², 2026-05-14) ────────
675
676/// Owning newtype around `*const TraitObjectStorage` carrying one
677/// v2-raw refcount share on the pointed-to allocation's HeapHeader.
678///
679/// **Wave 2 Round 4 D4 ckpt-final-prime² (2026-05-14):** mirrors the
680/// `TypedObjectPtr` precedent (above) for `TraitObjectStorage`. Carrier
681/// shape used as both:
682/// - `HeapValue::TraitObject(TraitObjectPtr)` variant payload
683/// - Future `TypedArrayData::TraitObject` element type (if/when the
684/// §Q25.A monomorphic specialization for trait-object-element arrays
685/// lands; not under this ckpt's scope)
686///
687/// `#[repr(transparent)]` so the in-memory layout is identical to
688/// `*const TraitObjectStorage` — zero ABI cost vs the raw pointer; the
689/// wrapper exists only to localize the manual Send/Sync impl + the
690/// Drop/Clone refcount discipline. Same auto-trait suppression rule
691/// applies as for TypedObjectPtr — per-T newtype is the canonical
692/// workaround for raw-ptr inner elements in HeapValue variant payloads
693/// without disabling Rust's auto-derived Send/Sync/Clone/Drop on the
694/// enclosing `HeapValue` enum.
695///
696/// Construction-side contract: callers transfer one strong-count share
697/// on the v2-raw HeapHeader (initialized to 1 via `TraitObjectStorage::_new`)
698/// to the new `TraitObjectPtr`. Reads via `as_ptr()` return the
699/// underlying pointer without bumping refcount.
700#[repr(transparent)]
701#[derive(Debug, PartialEq, Eq, Hash)]
702pub struct TraitObjectPtr(pub *const TraitObjectStorage);
703
704// SAFETY: same argument as TypedObjectPtr's Send/Sync impls — the
705// underlying storage is Send + Sync (manually unsafe impl'd on
706// TraitObjectStorage), the HeapHeader-based refcount uses atomic ops
707// (`v2_retain` / `v2_release` in `v2/refcount.rs`), and aliasing safety
708// matches `Arc<TraitObjectStorage>` — multiple threads can hold their
709// own retain shares concurrently.
710unsafe impl Send for TraitObjectPtr {}
711unsafe impl Sync for TraitObjectPtr {}
712
713impl Default for TraitObjectPtr {
714 /// Null pointer default — used by container types that need a
715 /// `Default` impl. Callers must not dereference a default-constructed
716 /// `TraitObjectPtr`. No refcount share is owed; Drop on a null
717 /// pointer is a no-op.
718 #[inline]
719 fn default() -> Self {
720 Self(std::ptr::null())
721 }
722}
723
724impl Clone for TraitObjectPtr {
725 /// v2-raw refcount bump via `v2_retain` on the pointed-to
726 /// HeapHeader. The clone owns its own share, retired at its own
727 /// `Drop`.
728 #[inline]
729 fn clone(&self) -> Self {
730 if !self.0.is_null() {
731 // SAFETY: per the construction-side contract, `self.0` points
732 // to a live `TraitObjectStorage` allocated via `_new` (or a
733 // legacy Arc-allocated one whose embedded HeapHeader is
734 // unused but still bumpable safely — atomic increment is
735 // sound on any aligned u32 within the legitimate allocation).
736 unsafe { crate::v2::refcount::v2_retain(&(*self.0).header) };
737 }
738 Self(self.0)
739 }
740}
741
742impl Drop for TraitObjectPtr {
743 /// Retire the owned share via `TraitObjectStorage::release_elem`
744 /// (HeapElement trait — calls `v2_release` and, on refcount=0,
745 /// runs `_drop` to dealloc the allocation + retire inner shares).
746 /// No-op on null wrappers.
747 #[inline]
748 fn drop(&mut self) {
749 if !self.0.is_null() {
750 use crate::v2::heap_element::HeapElement;
751 // SAFETY: per the construction-side contract this carrier owns
752 // one share on the HeapHeader-at-offset-0 refcount.
753 unsafe { TraitObjectStorage::release_elem(self.0) };
754 }
755 }
756}
757
758impl TraitObjectPtr {
759 /// Construct from a raw pointer obtained via `TraitObjectStorage::_new`.
760 /// The caller transfers one strong-count share to the wrapper.
761 #[inline]
762 pub fn new(ptr: *const TraitObjectStorage) -> Self {
763 Self(ptr)
764 }
765
766 /// Recover the underlying raw pointer. Does NOT bump refcount;
767 /// the returned pointer is borrowed for the wrapper's lifetime.
768 #[inline]
769 pub fn as_ptr(&self) -> *const TraitObjectStorage {
770 self.0
771 }
772
773 /// Whether the pointer is null.
774 #[inline]
775 pub fn is_null(&self) -> bool {
776 self.0.is_null()
777 }
778
779 /// Consume the wrapper without running Drop, returning the raw
780 /// pointer. The caller takes over the one refcount share. Mirror
781 /// of `Arc::into_raw`.
782 #[inline]
783 pub fn into_raw(self) -> *const TraitObjectStorage {
784 let ptr = self.0;
785 std::mem::forget(self);
786 ptr
787 }
788}
789
790// Same Deref-as-shortcut rationale as TypedObjectPtr — the wrapper owns
791// the share so the pointed-to storage is live for the wrapper's lifetime.
792impl std::ops::Deref for TraitObjectPtr {
793 type Target = TraitObjectStorage;
794 #[inline]
795 fn deref(&self) -> &TraitObjectStorage {
796 debug_assert!(
797 !self.0.is_null(),
798 "TraitObjectPtr::deref on null pointer (default-constructed wrapper \
799 must not be dereferenced)"
800 );
801 unsafe { &*self.0 }
802 }
803}
804
805// ── HashMap storage (Stage C P1(b), 2026-05-07) ─────────────────────────────
806//
807// Wave 2 Round 3b C2-joint ckpt-1 (2026-05-14): `HashMapValueBuf` deleted;
808// `HashMapData` replaced with `HashMapData<V>` generic per audit §C.4
809// option (a.2) — values buffer is `*mut TypedArray<V>` (per-V monomorphized
810// at compile time via the `HashMapValueElem` trait). `HashMapKindedRef`
811// enum bundles per-V `Arc<HashMapData<V>>` variants as the HeapValue-arm
812// carrier (ckpt-2 flips `HeapValue::HashMap(Arc<HashMapData>)` to
813// `HeapValue::HashMap(HashMapKindedRef)`). See ADR-006 §2.7.24 Q25.B
814// SUPERSEDED + `docs/cluster-audits/bulldozer-wave-1-inventory.md` §C.
815
816/// Per-V dispatcher trait for `HashMapData<V>::Drop` — releases the value
817/// buffer (`*mut TypedArray<V>`) at refcount-0 of the enclosing
818/// `Arc<HashMapData<V>>`.
819///
820/// Authority: ADR-006 §2.7.24 Q25.B SUPERSEDED + `bulldozer-wave-1-inventory.md`
821/// §C.4 option (a.2). The trait dispatches per-V release at compile time via
822/// the Rust type system — no runtime `NativeKind` probe, no `is_heap()` probe,
823/// no Bool-default fallback. Mirror of `v2::heap_element::HeapElement` shape,
824/// but operates on the OUTER `TypedArray<V>` allocation rather than on
825/// individual heap-element pointers.
826///
827/// Impls partition V into:
828///
829/// - POD scalar Vs (`i64`, `f64`, `u8` for Bool): `TypedArray::<V>::drop_array`
830/// frees the data buffer + the struct; no per-element work.
831/// - HeapHeader-equipped raw pointers (`*const StringObj`, `*const DecimalObj`):
832/// `TypedArray::<*const T>::drop_array_heap` walks the data buffer and
833/// calls `T::release_elem` per element, then frees the struct. Requires
834/// `T: v2::heap_element::HeapElement`.
835/// - `#[repr(transparent)]` newtype-as-element shapes (`TypedObjectPtr`,
836/// `TraitObjectPtr`): manual walk that `ptr::read`s each element to invoke
837/// its `Drop` (which retires the v2-raw refcount share via `release_elem`
838/// on the inner `*const TypedObjectStorage` / `*const TraitObjectStorage`).
839///
840/// Char (`char` codepoint) is reachable per §C.5 dead-but-derived disposition
841/// — included as a POD-scalar V.
842///
843/// # Safety
844///
845/// Implementors must guarantee:
846/// 1. `release_typed_array(ptr)` is sound when `ptr` points to a live
847/// `TypedArray<Self>` allocation produced by `TypedArray::<Self>::new` /
848/// `with_capacity` / `from_slice`.
849/// 2. After this call, `ptr` is invalid; the data buffer + struct are freed.
850/// 3. Per-element ownership semantics match the storage contract — POD
851/// elements need no per-element release; HeapHeader-equipped elements
852/// have their shares retired before the data buffer is freed.
853pub unsafe trait HashMapValueElem {
854 /// Release a `*mut TypedArray<Self>` allocation: retire per-element
855 /// shares (where applicable) + free the data buffer + free the struct.
856 ///
857 /// # Safety
858 /// `ptr` must point to a valid, live `TypedArray<Self>` allocated by
859 /// the v2-raw `TypedArray::<Self>` allocator (`new`, `with_capacity`,
860 /// `from_slice`). After this call returns, `ptr` is invalid.
861 unsafe fn release_typed_array(ptr: *mut crate::v2::typed_array::TypedArray<Self>)
862 where
863 Self: Sized;
864
865 /// Clone a single element with proper refcount-share semantics.
866 ///
867 /// - POD scalar Vs (`i64`/`f64`/`u8`/`char`): byte copy — no refcount work.
868 /// - HeapHeader-equipped raw pointers (`*const StringObj`/`*const DecimalObj`):
869 /// pointer copy + `v2_retain` on the pointed-to HeapHeader.
870 /// - `#[repr(transparent)]` ptr-newtypes (`TypedObjectPtr`/`TraitObjectPtr`):
871 /// delegate to the wrapper's `Clone` impl (which does v2_retain).
872 ///
873 /// Wave 2 Round 3b C2-joint ckpt-3 (2026-05-14): added to support the
874 /// per-V mutation API (insert / merge / get_share) on `HashMapData<V>`.
875 /// Per ADR-006 §2.7.24 Q25.B SUPERSEDED + audit §C.4.
876 ///
877 /// # Safety
878 /// `elem` must reference a live element of a `TypedArray<Self>` (or a
879 /// freshly-allocated element owned by the caller). The implementor
880 /// must produce a new owned share — for HeapElement / ptr-newtype V
881 /// this bumps the refcount on the pointed-to allocation; for POD V
882 /// it is a trivial copy.
883 unsafe fn share_clone(elem: &Self) -> Self
884 where
885 Self: Sized;
886
887 /// Release a single owned value (one refcount share). For POD V it is
888 /// a no-op (byte copy falls out of scope). For HeapElement V the
889 /// share is retired via `release_elem` on the pointer. For Ptr-newtype
890 /// V the wrapper's Drop runs automatically when the value drops.
891 ///
892 /// Wave 2 Round 3b C2-joint ckpt-3 (2026-05-14): added for the per-V
893 /// mutation API's overwrite path (insert when key already present —
894 /// the old value's share must be retired before the slot is overwritten).
895 ///
896 /// # Safety
897 /// `value` must be a valid owned V — for HeapElement / ptr-newtype V
898 /// types the caller transfers one refcount share to this method, which
899 /// retires it.
900 unsafe fn release_owned(value: Self)
901 where
902 Self: Sized;
903}
904
905// ── POD scalar V impls (i64 / f64 / u8 / char) ─────────────────────────────
906
907unsafe impl HashMapValueElem for i64 {
908 #[inline]
909 unsafe fn release_typed_array(ptr: *mut crate::v2::typed_array::TypedArray<Self>) {
910 // SAFETY: caller-bound contract; `i64` is Copy/POD — no per-element
911 // shares to retire.
912 unsafe { crate::v2::typed_array::TypedArray::<i64>::drop_array(ptr) }
913 }
914 #[inline]
915 unsafe fn share_clone(elem: &Self) -> Self {
916 *elem
917 }
918 #[inline]
919 unsafe fn release_owned(_value: Self) {
920 // POD: byte copy falls out of scope; no-op.
921 }
922}
923
924unsafe impl HashMapValueElem for f64 {
925 #[inline]
926 unsafe fn release_typed_array(ptr: *mut crate::v2::typed_array::TypedArray<Self>) {
927 unsafe { crate::v2::typed_array::TypedArray::<f64>::drop_array(ptr) }
928 }
929 #[inline]
930 unsafe fn share_clone(elem: &Self) -> Self {
931 *elem
932 }
933 #[inline]
934 unsafe fn release_owned(_value: Self) {}
935}
936
937unsafe impl HashMapValueElem for u8 {
938 /// Used as the `Bool` V (one byte per element).
939 #[inline]
940 unsafe fn release_typed_array(ptr: *mut crate::v2::typed_array::TypedArray<Self>) {
941 unsafe { crate::v2::typed_array::TypedArray::<u8>::drop_array(ptr) }
942 }
943 #[inline]
944 unsafe fn share_clone(elem: &Self) -> Self {
945 *elem
946 }
947 #[inline]
948 unsafe fn release_owned(_value: Self) {}
949}
950
951unsafe impl HashMapValueElem for char {
952 /// Char codepoint (4 bytes / element). Dead-but-derived per §C.5;
953 /// included for forward-cleanliness with the `HeapValue::Char`
954 /// xml/json marshal path.
955 #[inline]
956 unsafe fn release_typed_array(ptr: *mut crate::v2::typed_array::TypedArray<Self>) {
957 unsafe { crate::v2::typed_array::TypedArray::<char>::drop_array(ptr) }
958 }
959 #[inline]
960 unsafe fn share_clone(elem: &Self) -> Self {
961 *elem
962 }
963 #[inline]
964 unsafe fn release_owned(_value: Self) {}
965}
966
967// ── HeapHeader-equipped raw-pointer V impls (*const StringObj / *const DecimalObj) ──
968
969unsafe impl HashMapValueElem for *const crate::v2::string_obj::StringObj {
970 #[inline]
971 unsafe fn release_typed_array(ptr: *mut crate::v2::typed_array::TypedArray<Self>) {
972 // SAFETY: `StringObj: HeapElement`; `drop_array_heap` walks elements,
973 // calls `StringObj::release_elem` per `*const StringObj`, then frees
974 // the data buffer + struct.
975 unsafe {
976 crate::v2::typed_array::TypedArray::<*const crate::v2::string_obj::StringObj>::drop_array_heap(ptr)
977 }
978 }
979 #[inline]
980 unsafe fn share_clone(elem: &Self) -> Self {
981 // SAFETY: per the construction-side contract on
982 // `HashMapData<*const StringObj>` element buffer, *elem points at a
983 // live StringObj with HeapHeader at offset 0; v2_retain bumps the
984 // refcount via atomic increment.
985 if !elem.is_null() {
986 unsafe { crate::v2::refcount::v2_retain(&(**elem).header) };
987 }
988 *elem
989 }
990 #[inline]
991 unsafe fn release_owned(value: Self) {
992 // SAFETY: caller transfers one share on a live StringObj; route
993 // through HeapElement::release_elem to atomic-decrement + dealloc
994 // on refcount=0.
995 if !value.is_null() {
996 unsafe {
997 use crate::v2::heap_element::HeapElement;
998 crate::v2::string_obj::StringObj::release_elem(value);
999 }
1000 }
1001 }
1002}
1003
1004unsafe impl HashMapValueElem for *const crate::v2::decimal_obj::DecimalObj {
1005 #[inline]
1006 unsafe fn release_typed_array(ptr: *mut crate::v2::typed_array::TypedArray<Self>) {
1007 unsafe {
1008 crate::v2::typed_array::TypedArray::<*const crate::v2::decimal_obj::DecimalObj>::drop_array_heap(ptr)
1009 }
1010 }
1011 #[inline]
1012 unsafe fn share_clone(elem: &Self) -> Self {
1013 // SAFETY: see *const StringObj impl above. DecimalObj has
1014 // HeapHeader at offset 0 (HeapElement contract).
1015 if !elem.is_null() {
1016 unsafe { crate::v2::refcount::v2_retain(&(**elem).header) };
1017 }
1018 *elem
1019 }
1020 #[inline]
1021 unsafe fn release_owned(value: Self) {
1022 if !value.is_null() {
1023 unsafe {
1024 use crate::v2::heap_element::HeapElement;
1025 crate::v2::decimal_obj::DecimalObj::release_elem(value);
1026 }
1027 }
1028 }
1029}
1030
1031// ── Ptr-newtype V impls (TypedObjectPtr / TraitObjectPtr) ───────────────────
1032
1033unsafe impl HashMapValueElem for TypedObjectPtr {
1034 /// `TypedObjectPtr` is `#[repr(transparent)]` over `*const TypedObjectStorage`
1035 /// but has a manual `Drop` impl (calls `release_elem`). Walk the buffer
1036 /// via `ptr::read` to invoke each element's Drop (which retires the v2-raw
1037 /// HeapHeader share), then free the data allocation + struct.
1038 #[inline]
1039 unsafe fn release_typed_array(ptr: *mut crate::v2::typed_array::TypedArray<Self>) {
1040 unsafe {
1041 let arr = &*ptr;
1042 if arr.cap > 0 && !arr.data.is_null() {
1043 // Walk: read each element; the read transfers ownership to a
1044 // local `TypedObjectPtr`, which drops at scope-end via its
1045 // manual `Drop` impl (calls `release_elem` on the inner
1046 // `*const TypedObjectStorage`).
1047 for i in 0..arr.len {
1048 let _elem: TypedObjectPtr = std::ptr::read(arr.data.add(i as usize));
1049 }
1050 let data_layout =
1051 std::alloc::Layout::array::<TypedObjectPtr>(arr.cap as usize)
1052 .expect("invalid array layout");
1053 std::alloc::dealloc(arr.data as *mut u8, data_layout);
1054 }
1055 let layout = std::alloc::Layout::new::<crate::v2::typed_array::TypedArray<Self>>();
1056 std::alloc::dealloc(ptr as *mut u8, layout);
1057 }
1058 }
1059 #[inline]
1060 unsafe fn share_clone(elem: &Self) -> Self {
1061 // Delegate to the wrapper's Clone impl (which bumps the v2_retain
1062 // refcount on the inner *const TypedObjectStorage's HeapHeader).
1063 elem.clone()
1064 }
1065 #[inline]
1066 unsafe fn release_owned(_value: Self) {
1067 // TypedObjectPtr has a manual Drop impl that calls release_elem;
1068 // letting `_value` go out of scope runs Drop. No explicit work.
1069 }
1070}
1071
1072unsafe impl HashMapValueElem for TraitObjectPtr {
1073 /// Mirror of the `TypedObjectPtr` impl above; per-element Drop runs
1074 /// `release_elem` on `*const TraitObjectStorage`.
1075 #[inline]
1076 unsafe fn release_typed_array(ptr: *mut crate::v2::typed_array::TypedArray<Self>) {
1077 unsafe {
1078 let arr = &*ptr;
1079 if arr.cap > 0 && !arr.data.is_null() {
1080 for i in 0..arr.len {
1081 let _elem: TraitObjectPtr = std::ptr::read(arr.data.add(i as usize));
1082 }
1083 let data_layout =
1084 std::alloc::Layout::array::<TraitObjectPtr>(arr.cap as usize)
1085 .expect("invalid array layout");
1086 std::alloc::dealloc(arr.data as *mut u8, data_layout);
1087 }
1088 let layout = std::alloc::Layout::new::<crate::v2::typed_array::TypedArray<Self>>();
1089 std::alloc::dealloc(ptr as *mut u8, layout);
1090 }
1091 }
1092 #[inline]
1093 unsafe fn share_clone(elem: &Self) -> Self {
1094 // Delegate to TraitObjectPtr's Clone impl (v2_retain on inner
1095 // *const TraitObjectStorage's HeapHeader).
1096 elem.clone()
1097 }
1098 #[inline]
1099 unsafe fn release_owned(_value: Self) {
1100 // TraitObjectPtr's Drop impl runs at scope-end.
1101 }
1102}
1103
1104// ── Recursive HashMap-value V impl (HashMapKindedRef) ───────────────────────
1105//
1106// Wave N hashmap-value-v-arm follow-up (cluster-2 closure-wave-C,
1107// 2026-05-16). Per ADR-006 §2.7.24 Q25.B SUPERSEDED canonical pattern
1108// (HashMapKindedRef carrier + per-V monomorphization at the method tier)
1109// extended to a recursive HashMap-value V arm. The values buffer is a
1110// `*mut TypedArray<HashMapKindedRef>` — each element is the per-V
1111// kinded-ref payload (auto-derived Drop chains through the inner Arc).
1112//
1113// HashMapKindedRef is non-Copy and carries a Drop (auto-derived: each
1114// variant holds Arc<HashMapData<V>> whose Drop retires one strong-count
1115// share). The shape matches TypedObjectPtr / TraitObjectPtr (manual Drop
1116// + manual Clone): walk-with-ptr::read on release; delegate to the
1117// wrapper's Clone on share; let scope-end run Drop on release_owned.
1118
1119unsafe impl HashMapValueElem for HashMapKindedRef {
1120 /// `HashMapKindedRef` is non-Copy with an auto-derived Drop (each
1121 /// variant holds `Arc<HashMapData<V>>` whose Drop retires one
1122 /// strong-count share). Walk the buffer via `ptr::read` to invoke
1123 /// each element's Drop, then free the data allocation + struct.
1124 /// Mirror of the `TypedObjectPtr` / `TraitObjectPtr` impl shape.
1125 #[inline]
1126 unsafe fn release_typed_array(ptr: *mut crate::v2::typed_array::TypedArray<Self>) {
1127 unsafe {
1128 let arr = &*ptr;
1129 if arr.cap > 0 && !arr.data.is_null() {
1130 // Walk: read each element; the read transfers ownership
1131 // to a local `HashMapKindedRef`, which drops at scope-end
1132 // via its auto-derived Drop (chains through Arc::drop on
1133 // the inner `Arc<HashMapData<V_inner>>`).
1134 for i in 0..arr.len {
1135 let _elem: HashMapKindedRef =
1136 std::ptr::read(arr.data.add(i as usize));
1137 }
1138 let data_layout =
1139 std::alloc::Layout::array::<HashMapKindedRef>(arr.cap as usize)
1140 .expect("invalid array layout");
1141 std::alloc::dealloc(arr.data as *mut u8, data_layout);
1142 }
1143 let layout = std::alloc::Layout::new::<crate::v2::typed_array::TypedArray<Self>>();
1144 std::alloc::dealloc(ptr as *mut u8, layout);
1145 }
1146 }
1147 #[inline]
1148 unsafe fn share_clone(elem: &Self) -> Self {
1149 // Delegate to HashMapKindedRef's manual Clone impl (per-variant
1150 // Arc::clone on the inner `Arc<HashMapData<V_inner>>` — single
1151 // refcount bump per the §C.3 audit ground-truth).
1152 elem.clone()
1153 }
1154 #[inline]
1155 unsafe fn release_owned(_value: Self) {
1156 // HashMapKindedRef's auto-derived Drop runs at scope-end (per
1157 // variant: Arc::drop on the inner `Arc<HashMapData<V_inner>>`).
1158 }
1159}
1160
1161/// HashMap storage — keys buffer (string-typed v2-raw `*mut TypedArray<*const StringObj>`)
1162/// + per-V monomorphized values buffer (`*mut TypedArray<V>`) + eager
1163/// bucket-index for O(1) lookup.
1164///
1165/// **Wave 2 Round 3b C2-joint ckpt-1 (2026-05-14):** the parametric
1166/// `HashMapValueBuf` enum has been REPLACED with a generic type parameter `V`
1167/// constrained by `HashMapValueElem`. Per audit §C.4 option (a.2):
1168/// per-V monomorphization at compile time via the `HashMapKindedRef` carrier
1169/// (defined below). No runtime kind discriminator field on this struct (the
1170/// variant tag lives on `HashMapKindedRef` at the carrier layer).
1171///
1172/// The values buffer is a raw `*mut TypedArray<V>` — the v2-raw heap shape
1173/// (HeapHeader-at-offset-0 produced via `TypedArray::<V>::new`). Drop runs
1174/// `V::release_typed_array(self.values)` via the `HashMapValueElem` trait —
1175/// per-V monomorphized at compile time. The keys buffer is a `*mut
1176/// TypedArray<*const StringObj>` — v2-raw shape using the `HeapElement`
1177/// dispatch on `StringObj`.
1178///
1179/// Per-V monomorphizations supported at landing (mirror of §A migration):
1180/// `i64`, `f64`, `u8` (Bool), `*const StringObj`, `*const DecimalObj`,
1181/// `TypedObjectPtr`, `TraitObjectPtr`. DateTime / Timespan / Duration /
1182/// Instant / Char are dead per §C.5 (Char retained as POD-scalar arm only
1183/// for the dead-but-derived defensive path; no live root producer).
1184///
1185/// **Eager bucket-only at first landing** (preserved from Stage C): `index`
1186/// is built at construction and maintained incrementally on insert / remove.
1187/// The `shape_id` hidden-class fast-path that the pre-bulldozer architecture
1188/// used for ≤64-string-keyed-maps remains deferred to a separate
1189/// optimization workstream.
1190///
1191/// **Forbidden under Q25.B SUPERSEDED:**
1192/// - `Arc<TypedBuffer<V>>` field shape (the value-buffer carrier is
1193/// `*mut TypedArray<V>` per audit §C.4).
1194/// - HashMap-wide runtime kind discriminator on this struct (per-V
1195/// monomorphization at compile time via the carrier; no inline tag
1196/// byte on `HashMapData<V>` itself).
1197/// - Re-introducing `HashMapValueBuf` arms under any rename.
1198#[derive(Debug)]
1199pub struct HashMapData<V: HashMapValueElem> {
1200 /// Insertion-ordered keys — v2-raw `*mut TypedArray<*const StringObj>`.
1201 /// Owned by this struct (one strong-count share on the keys array's
1202 /// HeapHeader at offset 0). Drop calls `<*const StringObj as
1203 /// HashMapValueElem>::release_typed_array` to retire the share.
1204 pub keys: *mut crate::v2::typed_array::TypedArray<*const crate::v2::string_obj::StringObj>,
1205 /// Insertion-ordered values — v2-raw `*mut TypedArray<V>`. Owned by
1206 /// this struct. Drop calls `V::release_typed_array(self.values)`.
1207 pub values: *mut crate::v2::typed_array::TypedArray<V>,
1208 /// Eager bucket-index: hash → list of indices into `keys` / `values`
1209 /// arrays. Enables O(1) lookup at the user-facing `map.get(key)` path.
1210 /// Hash is computed via FNV-1a over the key string bytes.
1211 pub index: std::collections::HashMap<u64, Vec<u32>>,
1212}
1213
1214// SAFETY: `*mut TypedArray<T>` is `!Send + !Sync` by default. `HashMapData<V>`
1215// is safe to share across threads because:
1216// (1) `TypedArray<T>` is HeapHeader-equipped with atomic refcount ops
1217// (`v2_retain` / `v2_release` in `v2/refcount.rs`).
1218// (2) Element-level Send/Sync is preserved by the `HashMapValueElem` impls
1219// (StringObj / DecimalObj / TypedObjectPtr / TraitObjectPtr all carry
1220// manual `unsafe impl Send + Sync`; scalar V types are auto-Send/Sync).
1221// (3) `HashMapData<V>` is treated as immutable at the marshal boundary;
1222// mutation goes through `Arc::make_mut` on the consumer side (ckpt-3
1223// territory).
1224unsafe impl<V: HashMapValueElem> Send for HashMapData<V> {}
1225unsafe impl<V: HashMapValueElem> Sync for HashMapData<V> {}
1226
1227impl<V: HashMapValueElem> HashMapData<V> {
1228 /// Build an empty HashMapData with no entries.
1229 ///
1230 /// Allocates two v2-raw `TypedArray` storages (keys + values) at
1231 /// capacity 0. The struct owns one strong-count share on each.
1232 pub fn new() -> Self {
1233 Self {
1234 // `*const StringObj` is `Copy` (raw pointer), so the Copy-bounded
1235 // `TypedArray::<*const StringObj>::new` works here.
1236 keys: crate::v2::typed_array::TypedArray::<
1237 *const crate::v2::string_obj::StringObj,
1238 >::new(),
1239 // V may be non-Copy (e.g. `TypedObjectPtr` has manual `Drop`),
1240 // so use the non-Copy `TypedArray::<V>::new_generic` path
1241 // (allocation only; no element-level reads/writes).
1242 values: crate::v2::typed_array::TypedArray::<V>::new_generic(),
1243 index: std::collections::HashMap::new(),
1244 }
1245 }
1246
1247 /// Build from parallel buffers — caller transfers one strong-count share
1248 /// on each `TypedArray` to this struct. Computes the bucket index eagerly
1249 /// from the keys buffer.
1250 ///
1251 /// # Safety
1252 /// `keys` must point to a live `TypedArray<*const StringObj>` and
1253 /// `values` to a live `TypedArray<V>`, both with at least one
1254 /// strong-count share owned by the caller (transferred to this struct).
1255 /// `keys.len` must equal `values.len`.
1256 pub unsafe fn from_pairs(
1257 keys: *mut crate::v2::typed_array::TypedArray<*const crate::v2::string_obj::StringObj>,
1258 values: *mut crate::v2::typed_array::TypedArray<V>,
1259 ) -> Self {
1260 // SAFETY: caller-bound contract per docstring.
1261 let (n_keys, n_values) = unsafe {
1262 (
1263 // keys is `TypedArray<*const StringObj>` — Copy-bounded `len`.
1264 crate::v2::typed_array::TypedArray::len(keys),
1265 // values is `TypedArray<V>` where V may be non-Copy — use the
1266 // non-Copy `len_generic`.
1267 crate::v2::typed_array::TypedArray::len_generic(values),
1268 )
1269 };
1270 assert_eq!(
1271 n_keys, n_values,
1272 "HashMapData::from_pairs: keys/values length mismatch \
1273 (keys.len={}, values.len={})",
1274 n_keys, n_values,
1275 );
1276
1277 // Build the bucket index from the keys buffer. Walks `*const StringObj`
1278 // pointers without taking ownership; each `StringObj::as_str` is a
1279 // borrow that's valid while the keys buffer is alive.
1280 let mut index: std::collections::HashMap<u64, Vec<u32>> =
1281 std::collections::HashMap::new();
1282 // SAFETY: keys is live + len is the element count.
1283 let keys_slice: &[*const crate::v2::string_obj::StringObj] = unsafe {
1284 crate::v2::typed_array::TypedArray::as_slice(keys)
1285 };
1286 for (i, &key_ptr) in keys_slice.iter().enumerate() {
1287 // SAFETY: keys-buffer elements are live `*const StringObj` per the
1288 // construction-side contract (caller owned one share each; that
1289 // share moved into the buffer at `TypedArray::push`).
1290 let key_bytes = unsafe {
1291 let len = (*key_ptr).len as usize;
1292 if len == 0 {
1293 &[][..]
1294 } else {
1295 std::slice::from_raw_parts((*key_ptr).data, len)
1296 }
1297 };
1298 index
1299 .entry(fnv1a_hash(key_bytes))
1300 .or_default()
1301 .push(i as u32);
1302 }
1303
1304 Self {
1305 keys,
1306 values,
1307 index,
1308 }
1309 }
1310
1311 /// Number of entries.
1312 #[inline]
1313 pub fn len(&self) -> usize {
1314 // SAFETY: `self.keys` is live for the lifetime of `&self`.
1315 unsafe { crate::v2::typed_array::TypedArray::len(self.keys) as usize }
1316 }
1317
1318 /// Whether the map is empty.
1319 #[inline]
1320 pub fn is_empty(&self) -> bool {
1321 self.len() == 0
1322 }
1323
1324 /// Read the value at index `i`. Returns a copy of the `V` element
1325 /// (POD scalars copy bytes; `*const StringObj` / `*const DecimalObj` /
1326 /// `TypedObjectPtr` / `TraitObjectPtr` copy the pointer bits — caller
1327 /// must NOT treat the returned value as carrying refcount ownership
1328 /// unless the caller explicitly bumps the v2-raw refcount).
1329 ///
1330 /// For owned-share semantics use `value_at_owned` (ckpt-2 / ckpt-3
1331 /// territory; not in scope for ckpt-1 foundation).
1332 ///
1333 /// # Safety
1334 /// `i` must be less than `self.len()`.
1335 #[inline]
1336 pub unsafe fn value_at_raw(&self, i: usize) -> V
1337 where
1338 V: Copy,
1339 {
1340 // SAFETY: caller-bound bounds contract + values is live.
1341 unsafe {
1342 crate::v2::typed_array::TypedArray::get_unchecked(self.values, i as u32)
1343 }
1344 }
1345
1346 /// Look up a value by string key. Returns `Some(i)` (the index into the
1347 /// values buffer) if the key is present, else `None`. The returned
1348 /// index can be used with `value_at_raw` / `Arc::make_mut`-based
1349 /// mutation paths (ckpt-3 territory).
1350 ///
1351 /// O(1) via the bucket index plus a short bucket scan for collision
1352 /// disambiguation.
1353 pub fn get_index(&self, key: &str) -> Option<usize> {
1354 let hash = fnv1a_hash(key.as_bytes());
1355 let bucket = self.index.get(&hash)?;
1356 // SAFETY: keys is live; len is the bucket-recorded element count.
1357 let keys_slice = unsafe {
1358 crate::v2::typed_array::TypedArray::as_slice(self.keys)
1359 };
1360 for &idx in bucket {
1361 let i = idx as usize;
1362 // SAFETY: key-buffer elements are live `*const StringObj`.
1363 let stored = unsafe { keys_slice[i] };
1364 let stored_str = unsafe { crate::v2::string_obj::StringObj::as_str(stored) };
1365 if stored_str == key {
1366 return Some(i);
1367 }
1368 }
1369 None
1370 }
1371
1372 /// Whether the map contains the given key.
1373 #[inline]
1374 pub fn contains_key(&self, key: &str) -> bool {
1375 self.get_index(key).is_some()
1376 }
1377
1378 // ── Mutation API (Wave 2 Round 3b C2-joint ckpt-3, 2026-05-14) ────────
1379 //
1380 // Per-V mutation surface mirroring `HashSetData::insert/remove` shape
1381 // (line 1514+ above) but with parallel values-buffer maintenance.
1382 // ADR-006 §2.7.24 Q25.B SUPERSEDED + audit §C.4 option (a.2).
1383 //
1384 // Uses raw `ptr::write` / `ptr::read` against the `*mut TypedArray<V>`
1385 // values buffer (bypassing TypedArray::<T:Copy>::push/pop since
1386 // `TypedObjectPtr` / `TraitObjectPtr` are non-Copy). `HashMapValueElem
1387 // ::share_clone` handles per-V refcount-aware copying when callers need
1388 // to clone elements (e.g. `merge`).
1389 //
1390 // Caller-managed ownership: `insert`/`insert_share` take a `V` by value,
1391 // transferring one share. `remove` returns the `V` by value, transferring
1392 // the share to the caller. `merge` uses `share_clone` to bump shares on
1393 // the source's elements before inserting them locally.
1394
1395 /// Insert a key/value pair, transferring one share on `value` to the
1396 /// map. If the key was already present, the old value's share is
1397 /// retired (via `V::release_typed_array`-style single-element drop)
1398 /// and the slot is overwritten with `value`. Returns `true` on
1399 /// new-key insert, `false` on overwrite.
1400 ///
1401 /// The `key` is allocated as a new `StringObj` (one fresh
1402 /// `v2_retain`=1 share owned by the map).
1403 ///
1404 /// # Safety
1405 /// `value` must be a valid owned V — for HeapElement / ptr-newtype V
1406 /// types, the caller must transfer ownership of one refcount share
1407 /// to this method. POD V types (`i64`/`f64`/`u8`/`char`) trivially
1408 /// own themselves.
1409 pub unsafe fn insert(&mut self, key: &str, value: V) -> bool {
1410 let hash = fnv1a_hash(key.as_bytes());
1411 // Check for existing key — overwrite path.
1412 if let Some(bucket) = self.index.get(&hash) {
1413 for &idx in bucket {
1414 let i = idx as usize;
1415 // SAFETY: keys live + index points into keys range.
1416 let stored_ptr = unsafe {
1417 crate::v2::typed_array::TypedArray::get_unchecked(self.keys, idx)
1418 };
1419 let stored_str = unsafe { crate::v2::string_obj::StringObj::as_str(stored_ptr) };
1420 if stored_str == key {
1421 // Overwrite: read+drop the old value, write the new.
1422 unsafe {
1423 let data_ptr = (*self.values).data.add(i);
1424 let old_value: V = std::ptr::read(data_ptr);
1425 // Drop the old value's share by walking through a
1426 // single-element TypedArray. Simpler: rely on V's
1427 // Drop impl (or for HeapElement V types, manual
1428 // release_elem). For uniformity, route through the
1429 // share_clone-aware path: since old_value is owned,
1430 // letting it go out of scope at end of this block
1431 // invokes its Drop impl (TypedObjectPtr/TraitObjectPtr
1432 // Drop calls release_elem; *const StringObj has no
1433 // Drop — we must manually release).
1434 Self::drop_owned_value(old_value);
1435 std::ptr::write(data_ptr, value);
1436 }
1437 return false;
1438 }
1439 }
1440 }
1441 // New-key insert path. Allocate new StringObj for the key.
1442 let key_obj: *const crate::v2::string_obj::StringObj =
1443 crate::v2::string_obj::StringObj::new(key);
1444 let new_idx_u32 = unsafe { crate::v2::typed_array::TypedArray::len(self.keys) };
1445 // Push key into the StringObj keys buffer (Copy-bounded *const T).
1446 unsafe { crate::v2::typed_array::TypedArray::push(self.keys, key_obj) };
1447 // Push value into the values buffer via raw write (V may be non-Copy).
1448 unsafe { Self::values_push(self.values, value) };
1449 // Update bucket index.
1450 self.index.entry(hash).or_default().push(new_idx_u32);
1451 true
1452 }
1453
1454 /// Remove the entry under `key`. Returns the removed value (transferring
1455 /// one share to the caller) if present, else `None`.
1456 ///
1457 /// The bucket index is updated to reflect the buffer's post-removal
1458 /// indices: every entry after the removed slot shifts down by one
1459 /// position (mirror of `HashSetData::remove`).
1460 pub unsafe fn remove(&mut self, key: &str) -> Option<V> {
1461 let hash = fnv1a_hash(key.as_bytes());
1462 let removed_idx: usize = {
1463 let bucket = self.index.get(&hash)?;
1464 let mut found: Option<usize> = None;
1465 for (bucket_pos, &idx) in bucket.iter().enumerate() {
1466 // SAFETY: keys live.
1467 let stored_ptr = unsafe {
1468 crate::v2::typed_array::TypedArray::get_unchecked(self.keys, idx)
1469 };
1470 let stored_str = unsafe { crate::v2::string_obj::StringObj::as_str(stored_ptr) };
1471 if stored_str == key {
1472 found = Some(bucket_pos);
1473 break;
1474 }
1475 }
1476 let bucket_pos = found?;
1477 let bucket = self.index.get_mut(&hash).expect("bucket present");
1478 let removed_idx = bucket.swap_remove(bucket_pos) as usize;
1479 if bucket.is_empty() {
1480 self.index.remove(&hash);
1481 }
1482 removed_idx
1483 };
1484 // Read the value out (transferring share to caller).
1485 let removed_value: V = unsafe {
1486 let data_ptr = (*self.values).data.add(removed_idx);
1487 std::ptr::read(data_ptr)
1488 };
1489 // Read the key pointer out + release its share.
1490 let removed_key: *const crate::v2::string_obj::StringObj = unsafe {
1491 crate::v2::typed_array::TypedArray::get_unchecked(self.keys, removed_idx as u32)
1492 };
1493 unsafe {
1494 use crate::v2::heap_element::HeapElement;
1495 crate::v2::string_obj::StringObj::release_elem(removed_key);
1496 }
1497 // Shift remaining elements down by one (compact the buffers).
1498 let n_keys = unsafe { crate::v2::typed_array::TypedArray::len(self.keys) } as usize;
1499 unsafe {
1500 let keys_data = (*self.keys).data;
1501 let values_data = (*self.values).data;
1502 for j in removed_idx..n_keys - 1 {
1503 std::ptr::write(keys_data.add(j), std::ptr::read(keys_data.add(j + 1)));
1504 std::ptr::write(values_data.add(j), std::ptr::read(values_data.add(j + 1)));
1505 }
1506 (*self.keys).len -= 1;
1507 (*self.values).len -= 1;
1508 }
1509 // Renumber the bucket index entries pointing past the removed slot.
1510 for bucket in self.index.values_mut() {
1511 for slot in bucket.iter_mut() {
1512 if (*slot as usize) > removed_idx {
1513 *slot -= 1;
1514 }
1515 }
1516 }
1517 Some(removed_value)
1518 }
1519
1520 /// Look up a value by key. Returns a *share-cloned* copy of the stored
1521 /// value (the caller takes one fresh share — for POD V trivial copy;
1522 /// for HeapElement / ptr-newtype V the v2_retain happens via
1523 /// `HashMapValueElem::share_clone`).
1524 ///
1525 /// Returns `None` if the key is absent.
1526 pub fn get_share(&self, key: &str) -> Option<V> {
1527 let i = self.get_index(key)?;
1528 // SAFETY: i < len(values).
1529 let elem_ref: &V = unsafe { &*(*self.values).data.add(i) };
1530 Some(unsafe { V::share_clone(elem_ref) })
1531 }
1532
1533 /// Merge `other`'s entries into `self`, last-write-wins on key
1534 /// collision. Each value from `other` is share-cloned before being
1535 /// inserted (so `other`'s shares are preserved).
1536 pub unsafe fn merge(&mut self, other: &Self) {
1537 let n = other.len();
1538 for i in 0..n {
1539 // SAFETY: i < other.len().
1540 let key_ptr = unsafe {
1541 crate::v2::typed_array::TypedArray::get_unchecked(other.keys, i as u32)
1542 };
1543 let key_str = unsafe { crate::v2::string_obj::StringObj::as_str(key_ptr) };
1544 let value_ref: &V = unsafe { &*(*other.values).data.add(i) };
1545 let cloned_value = unsafe { V::share_clone(value_ref) };
1546 unsafe { self.insert(key_str, cloned_value) };
1547 }
1548 }
1549
1550 /// Push a single value onto the values buffer, growing the data
1551 /// allocation if needed. Bypasses `TypedArray::<T: Copy>::push` so
1552 /// non-Copy V types (TypedObjectPtr/TraitObjectPtr) work too.
1553 ///
1554 /// # Safety
1555 /// `values` must point to a live `TypedArray<V>`; `value` must be a
1556 /// valid owned V (caller transfers one share).
1557 unsafe fn values_push(values: *mut crate::v2::typed_array::TypedArray<V>, value: V) {
1558 use std::alloc::{alloc, realloc, Layout};
1559 unsafe {
1560 let arr = &mut *values;
1561 if arr.len == arr.cap {
1562 // Grow (doubling, min 4).
1563 let new_cap = if arr.cap == 0 { 4u32 } else { arr.cap.checked_mul(2).expect("capacity overflow") };
1564 let new_layout = Layout::array::<V>(new_cap as usize).expect("invalid array layout");
1565 let new_data = if arr.cap == 0 || arr.data.is_null() {
1566 alloc(new_layout) as *mut V
1567 } else {
1568 let old_layout = Layout::array::<V>(arr.cap as usize).expect("invalid array layout");
1569 realloc(arr.data as *mut u8, old_layout, new_layout.size()) as *mut V
1570 };
1571 assert!(!new_data.is_null(), "reallocation failed for HashMapData<V> values");
1572 arr.data = new_data;
1573 arr.cap = new_cap;
1574 }
1575 std::ptr::write(arr.data.add(arr.len as usize), value);
1576 arr.len += 1;
1577 }
1578 }
1579
1580 /// Drop an owned value, retiring its refcount share if it owns one.
1581 /// Per-V dispatch via `HashMapValueElem::release_owned`. For POD V
1582 /// (i64/f64/u8/char) this is a no-op; for HeapElement V the share is
1583 /// retired via `release_elem`; for Ptr-newtype V the wrapper's Drop
1584 /// impl runs at scope-end.
1585 ///
1586 /// # Safety
1587 /// `value` must be a valid owned V — callers transfer one share to
1588 /// this method; the method retires it.
1589 unsafe fn drop_owned_value(value: V) {
1590 unsafe { V::release_owned(value) }
1591 }
1592}
1593
1594impl<V: HashMapValueElem> Default for HashMapData<V> {
1595 fn default() -> Self {
1596 Self::new()
1597 }
1598}
1599
1600impl<V: HashMapValueElem> Drop for HashMapData<V> {
1601 /// Retire the per-buffer strong-count shares: keys (via
1602 /// `<*const StringObj as HashMapValueElem>::release_typed_array`) +
1603 /// values (via `V::release_typed_array`). Per-V monomorphized at compile
1604 /// time — no runtime kind probe.
1605 fn drop(&mut self) {
1606 if !self.keys.is_null() {
1607 // SAFETY: `self.keys` was allocated via the v2-raw
1608 // `TypedArray::<*const StringObj>` allocator and owns one
1609 // strong-count share. After this call `self.keys` is invalid.
1610 unsafe {
1611 <*const crate::v2::string_obj::StringObj as HashMapValueElem>::release_typed_array(
1612 self.keys,
1613 )
1614 }
1615 }
1616 if !self.values.is_null() {
1617 // SAFETY: `self.values` was allocated via the v2-raw
1618 // `TypedArray::<V>` allocator and owns one strong-count share.
1619 // Per-V dispatcher via `HashMapValueElem`.
1620 unsafe { V::release_typed_array(self.values) }
1621 }
1622 }
1623}
1624
1625/// Clone-on-write impl for `HashMapData<V>` (Wave 2 Round 3b C2-joint
1626/// ckpt-3, 2026-05-14). Allocates fresh keys + values buffers and
1627/// share-clones each element per the per-V `HashMapValueElem::share_clone`
1628/// dispatcher (and `v2_retain` on each key via the *const StringObj impl).
1629/// The fresh `HashMapData<V>` owns one refcount share on each per-element
1630/// allocation; the source's shares are untouched.
1631///
1632/// This impl is required for `Arc::make_mut(&mut Arc<HashMapData<V>>)` to
1633/// work at the consumer side (clone-on-write at the dispatch shell). Per
1634/// ADR-006 §2.7.24 Q25.B SUPERSEDED + audit §C.4 option (a.2).
1635impl<V: HashMapValueElem> Clone for HashMapData<V> {
1636 fn clone(&self) -> Self {
1637 let n = self.len();
1638 // Allocate fresh keys buffer with capacity n (Copy-bounded
1639 // `with_capacity` since *const StringObj is Copy).
1640 let new_keys = crate::v2::typed_array::TypedArray::<
1641 *const crate::v2::string_obj::StringObj,
1642 >::with_capacity(n as u32);
1643 // Allocate fresh values buffer with capacity n. Use the non-Copy
1644 // generic variant so V can be either Copy (POD/raw pointers) or
1645 // non-Copy (TypedObjectPtr / TraitObjectPtr).
1646 let new_values = crate::v2::typed_array::TypedArray::<V>::with_capacity_generic(n as u32);
1647 // Walk source elements; share_clone keys + values into the new buffers.
1648 unsafe {
1649 for i in 0..n {
1650 let key_ptr = crate::v2::typed_array::TypedArray::get_unchecked(
1651 self.keys, i as u32,
1652 );
1653 // Share-clone the key (v2_retain on the *const StringObj).
1654 let cloned_key = <*const crate::v2::string_obj::StringObj
1655 as HashMapValueElem>::share_clone(&key_ptr);
1656 std::ptr::write((*new_keys).data.add(i), cloned_key);
1657 let value_ref: &V = &*(*self.values).data.add(i);
1658 let cloned_value = V::share_clone(value_ref);
1659 std::ptr::write((*new_values).data.add(i), cloned_value);
1660 }
1661 (*new_keys).len = n as u32;
1662 (*new_values).len = n as u32;
1663 }
1664 Self {
1665 keys: new_keys,
1666 values: new_values,
1667 index: self.index.clone(),
1668 }
1669 }
1670}
1671
1672/// HashMapKindedRef — kinded carrier for `Arc<HashMapData<V>>` per audit
1673/// §C.4 option (a.2). Bundles per-V monomorphized payload types as enum
1674/// variants; the variant tag IS the `NativeKind` discriminator at the
1675/// carrier layer.
1676///
1677/// Used as the `HeapValue::HashMap` arm payload (ckpt-2 flips the variant
1678/// signature). Stays within shape-value / shape-runtime / shape-vm internal
1679/// Rust boundaries per ADR-006 §2.7.5 Cross-crate ABI policy — does NOT
1680/// leak into the extension contract raw-bits ABI at `module_exports.rs:21`.
1681///
1682/// **Manual Drop + Clone discipline** mirroring `TypedObjectPtr`: the
1683/// auto-derived `Drop` / `Clone` on the enclosing `HeapValue` enum chains
1684/// through `HashMapKindedRef`'s manual impls, which dispatch to per-variant
1685/// `Arc::drop` / `Arc::clone` on the typed inner `Arc<HashMapData<V>>`.
1686///
1687/// Per-V variants supported at landing (mirror of §C.4 audit shape;
1688/// post-D4 TypedObjectPtr canonical pattern):
1689///
1690/// - `I64` — `Arc<HashMapData<i64>>`
1691/// - `F64` — `Arc<HashMapData<f64>>`
1692/// - `Bool` — `Arc<HashMapData<u8>>`
1693/// - `Char` — `Arc<HashMapData<char>>` (dead-but-derived per §C.5)
1694/// - `String` — `Arc<HashMapData<*const StringObj>>`
1695/// - `Decimal` — `Arc<HashMapData<*const DecimalObj>>`
1696/// - `TypedObject` — `Arc<HashMapData<TypedObjectPtr>>`
1697/// - `TraitObject` — `Arc<HashMapData<TraitObjectPtr>>`
1698///
1699/// **Forbidden** (per CLAUDE.md broader-family regex + Q25.B SUPERSEDED
1700/// post-supersession #1):
1701///
1702/// - "HashMapKindedRef shim" / "HashMapKindedRef bridge" / "kinded-ref helper"
1703/// framing — refused on sight; the Ref-suffix is canonical per ADR-006
1704/// §2.7.6 / Q8 carrier-API-bound naming. Mirror of `KindedSlot::from_X`
1705/// constructor-shape; not a shim.
1706/// - Re-introducing `HashMapValueBuf` arms inside or alongside this enum
1707/// ("Q25.B-inside-enum carriers retained" / "documented intentional
1708/// duality"). The Wave 2 cadence shift authorization stands — per-V
1709/// monomorphization at the method tier with HashMapKindedRef carrier is
1710/// the deletion target, NOT a preserved-alongside alternative.
1711/// - HashMap-wide runtime kind discriminator on `HashMapData<V>` itself
1712/// (per audit §C.4 rationale: per-V monomorphization at compile time via
1713/// this carrier API; NO inline tag byte on `HashMapData<V>`).
1714#[derive(Debug)]
1715pub enum HashMapKindedRef {
1716 /// `Arc<HashMapData<i64>>` — V = i64 (POD scalar).
1717 I64(Arc<HashMapData<i64>>),
1718 /// `Arc<HashMapData<f64>>` — V = f64 (POD scalar).
1719 F64(Arc<HashMapData<f64>>),
1720 /// `Arc<HashMapData<u8>>` — V = u8 (Bool; one byte per element).
1721 Bool(Arc<HashMapData<u8>>),
1722 /// `Arc<HashMapData<char>>` — V = char (codepoint; dead-but-derived per §C.5).
1723 Char(Arc<HashMapData<char>>),
1724 /// `Arc<HashMapData<*const StringObj>>` — V = `*const StringObj`
1725 /// (HeapElement-equipped raw pointer).
1726 String(Arc<HashMapData<*const crate::v2::string_obj::StringObj>>),
1727 /// `Arc<HashMapData<*const DecimalObj>>` — V = `*const DecimalObj`
1728 /// (HeapElement-equipped raw pointer).
1729 Decimal(Arc<HashMapData<*const crate::v2::decimal_obj::DecimalObj>>),
1730 /// `Arc<HashMapData<TypedObjectPtr>>` — V = `TypedObjectPtr`
1731 /// (#[repr(transparent)] newtype over `*const TypedObjectStorage`,
1732 /// per ADR-006 §2.3 amendment D4 ckpt-final-prime² canonical pattern).
1733 TypedObject(Arc<HashMapData<TypedObjectPtr>>),
1734 /// `Arc<HashMapData<TraitObjectPtr>>` — V = `TraitObjectPtr`
1735 /// (#[repr(transparent)] newtype over `*const TraitObjectStorage`).
1736 TraitObject(Arc<HashMapData<TraitObjectPtr>>),
1737 /// `Arc<HashMapData<HashMapKindedRef>>` — V = `HashMapKindedRef` itself
1738 /// (recursive carrier). The inner HashMaps' values buffer is a flat
1739 /// array of `HashMapKindedRef` payloads (per-V kinded refs, each
1740 /// holding its own `Arc<HashMapData<V_inner>>`). Used by
1741 /// `HashMap.groupBy` to produce `HashMap<string, HashMap>` outputs.
1742 ///
1743 /// Wave N hashmap-value-v-arm follow-up (cluster-2 closure-wave-C,
1744 /// 2026-05-16). Per ADR-006 §2.7.24 Q25.B SUPERSEDED canonical
1745 /// pattern (HashMapKindedRef carrier + per-V monomorphization at
1746 /// the method tier) extended naturally to a recursive HashMap-value
1747 /// V arm via the existing `HashMapValueElem` trait dispatch shape.
1748 HashMap(Arc<HashMapData<HashMapKindedRef>>),
1749}
1750
1751impl Clone for HashMapKindedRef {
1752 /// Per-variant `Arc::clone` — single refcount bump on the inner
1753 /// `Arc<HashMapData<V>>`. No structural copy.
1754 fn clone(&self) -> Self {
1755 match self {
1756 HashMapKindedRef::I64(arc) => HashMapKindedRef::I64(Arc::clone(arc)),
1757 HashMapKindedRef::F64(arc) => HashMapKindedRef::F64(Arc::clone(arc)),
1758 HashMapKindedRef::Bool(arc) => HashMapKindedRef::Bool(Arc::clone(arc)),
1759 HashMapKindedRef::Char(arc) => HashMapKindedRef::Char(Arc::clone(arc)),
1760 HashMapKindedRef::String(arc) => HashMapKindedRef::String(Arc::clone(arc)),
1761 HashMapKindedRef::Decimal(arc) => HashMapKindedRef::Decimal(Arc::clone(arc)),
1762 HashMapKindedRef::TypedObject(arc) => HashMapKindedRef::TypedObject(Arc::clone(arc)),
1763 HashMapKindedRef::TraitObject(arc) => HashMapKindedRef::TraitObject(Arc::clone(arc)),
1764 HashMapKindedRef::HashMap(arc) => HashMapKindedRef::HashMap(Arc::clone(arc)),
1765 }
1766 }
1767}
1768
1769// Drop is auto-derived: each variant holds `Arc<HashMapData<V>>` whose Drop
1770// retires one strong-count share; on refcount-0 the inner `HashMapData<V>::Drop`
1771// runs and retires keys + values buffer shares via the `HashMapValueElem`
1772// dispatch. No manual `impl Drop` needed.
1773
1774impl HashMapKindedRef {
1775 /// The per-V `NativeKind` discriminator for the values buffer of this
1776 /// HashMap. Used at carrier boundaries (e.g. `HashMap.values()`
1777 /// projection to `TypedArrayData::<V>` arm + the parallel-kind stack
1778 /// track at §2.7.7 / Q9 stack reads of HashMap-iter yields) to feed
1779 /// the per-V Arc into the matching `KindedSlot::from_*` constructor.
1780 ///
1781 /// Per ADR-006 §2.7.6 / Q8 carrier-API-bound rule: one accessor per
1782 /// `NativeKind` heap variant — no per-V escape-hatch accessor (e.g.
1783 /// `as_string_arc()` returning `Arc<HashMapData<*const StringObj>>`)
1784 /// at this layer; consumers destructure the enum to recover the
1785 /// typed inner Arc.
1786 ///
1787 /// **Per-V NativeKind mapping** (Wave 2 Round 3b C2-joint ckpt-2
1788 /// 2026-05-14):
1789 ///
1790 /// - `I64` → `NativeKind::Int64`
1791 /// - `F64` → `NativeKind::Float64`
1792 /// - `Bool` → `NativeKind::Bool`
1793 /// - `Char` → `NativeKind::Char` (dead-but-derived per §C.5)
1794 /// - `String` → `NativeKind::Ptr(HeapKind::String)`
1795 /// - `Decimal` → `NativeKind::Ptr(HeapKind::Decimal)`
1796 /// - `TypedObject` → `NativeKind::Ptr(HeapKind::TypedObject)`
1797 /// - `TraitObject` → `NativeKind::Ptr(HeapKind::TraitObject)`
1798 /// - `HashMap` → `NativeKind::Ptr(HeapKind::HashMap)` (recursive carrier;
1799 /// Wave N hashmap-value-v-arm follow-up 2026-05-16)
1800 ///
1801 /// **StringV2 / DecimalV2 gate-flip dependency note:** at ckpt-2
1802 /// landing time (2026-05-14), the v2-raw `StringV2` / `DecimalV2`
1803 /// `NativeKind` variants were proposed in Round 3a' but the
1804 /// gate-flip from `NativeKind::Ptr(HeapKind::String)` →
1805 /// `NativeKind::StringV2` (et al.) had not propagated across all
1806 /// carrier APIs. This accessor maps `String` and `Decimal` arms to
1807 /// the heap-pointer variant per the post-3a-flip baseline; if a
1808 /// future gate-flip moves the canonical surface to StringV2/DecimalV2,
1809 /// this mapping is updated lockstep at the same wave (ckpt-3 or
1810 /// follow-up).
1811 #[inline]
1812 pub fn values_kind(&self) -> crate::NativeKind {
1813 use crate::NativeKind;
1814 match self {
1815 HashMapKindedRef::I64(_) => NativeKind::Int64,
1816 HashMapKindedRef::F64(_) => NativeKind::Float64,
1817 HashMapKindedRef::Bool(_) => NativeKind::Bool,
1818 HashMapKindedRef::Char(_) => NativeKind::Char,
1819 HashMapKindedRef::String(_) => NativeKind::Ptr(HeapKind::String),
1820 HashMapKindedRef::Decimal(_) => NativeKind::Ptr(HeapKind::Decimal),
1821 HashMapKindedRef::TypedObject(_) => NativeKind::Ptr(HeapKind::TypedObject),
1822 HashMapKindedRef::TraitObject(_) => NativeKind::Ptr(HeapKind::TraitObject),
1823 HashMapKindedRef::HashMap(_) => NativeKind::Ptr(HeapKind::HashMap),
1824 }
1825 }
1826
1827 /// Number of entries in the HashMap. Dispatches per-V to the inner
1828 /// `HashMapData<V>::len()` (same impl for every V — the keys buffer
1829 /// length, which equals the values buffer length per the from_pairs
1830 /// invariant).
1831 #[inline]
1832 pub fn len(&self) -> usize {
1833 match self {
1834 HashMapKindedRef::I64(arc) => arc.len(),
1835 HashMapKindedRef::F64(arc) => arc.len(),
1836 HashMapKindedRef::Bool(arc) => arc.len(),
1837 HashMapKindedRef::Char(arc) => arc.len(),
1838 HashMapKindedRef::String(arc) => arc.len(),
1839 HashMapKindedRef::Decimal(arc) => arc.len(),
1840 HashMapKindedRef::TypedObject(arc) => arc.len(),
1841 HashMapKindedRef::TraitObject(arc) => arc.len(),
1842 HashMapKindedRef::HashMap(arc) => arc.len(),
1843 }
1844 }
1845
1846 /// Whether the map is empty (zero entries). Dispatches per-V via `len()`.
1847 #[inline]
1848 pub fn is_empty(&self) -> bool {
1849 self.len() == 0
1850 }
1851
1852 /// Whether the map contains the given key. Dispatches per-V via the
1853 /// inner `HashMapData<V>::contains_key` (same impl for every V — keys
1854 /// are stringly-typed, so the lookup is V-agnostic).
1855 #[inline]
1856 pub fn contains_key(&self, key: &str) -> bool {
1857 match self {
1858 HashMapKindedRef::I64(arc) => arc.contains_key(key),
1859 HashMapKindedRef::F64(arc) => arc.contains_key(key),
1860 HashMapKindedRef::Bool(arc) => arc.contains_key(key),
1861 HashMapKindedRef::Char(arc) => arc.contains_key(key),
1862 HashMapKindedRef::String(arc) => arc.contains_key(key),
1863 HashMapKindedRef::Decimal(arc) => arc.contains_key(key),
1864 HashMapKindedRef::TypedObject(arc) => arc.contains_key(key),
1865 HashMapKindedRef::TraitObject(arc) => arc.contains_key(key),
1866 HashMapKindedRef::HashMap(arc) => arc.contains_key(key),
1867 }
1868 }
1869
1870 /// The `HeapKind` discriminator for `KindedSlot::from_hashmap` slot
1871 /// stamping (§2.7.6 / Q8 / Q9 parallel-kind track). Always
1872 /// `HeapKind::HashMap` regardless of the inner V — the V-discriminator
1873 /// is encoded in the `HashMapKindedRef` variant tag, not in the
1874 /// `HeapKind` ordinal (HashMap stays at ordinal 17).
1875 #[inline]
1876 pub const fn heap_kind(&self) -> HeapKind {
1877 HeapKind::HashMap
1878 }
1879}
1880
1881/// Per-V `{key: value, …}` formatter for `HashMapKindedRef`. Walks the
1882/// keys buffer + per-V values buffer; renders keys as quoted strings
1883/// and each value via the matching primitive `Display` (i64/f64/u8 as
1884/// "true"/"false"/char). For HeapElement / Ptr-newtype V we route
1885/// through the inner pointer's `Display` shape.
1886///
1887/// Wave 2 Round 3b C2-joint ckpt-3 (2026-05-14). ADR-006 §2.7.24 Q25.B
1888/// SUPERSEDED + audit §C.4.
1889fn hashmap_kref_display(
1890 kref: &HashMapKindedRef,
1891 f: &mut std::fmt::Formatter<'_>,
1892) -> std::fmt::Result {
1893 use std::fmt::Write as _;
1894 write!(f, "{{")?;
1895
1896 /// Read all keys as `&str` from the v2-raw `*mut TypedArray<*const StringObj>` buffer.
1897 ///
1898 /// # Safety
1899 /// `keys` must point to a live `TypedArray<*const StringObj>` whose
1900 /// elements are live StringObjs (the HashMapData<V> contract).
1901 unsafe fn read_keys<'a>(
1902 keys: *const crate::v2::typed_array::TypedArray<*const crate::v2::string_obj::StringObj>,
1903 ) -> Vec<&'a str> {
1904 unsafe {
1905 let n = crate::v2::typed_array::TypedArray::len(keys) as usize;
1906 let mut out = Vec::with_capacity(n);
1907 for i in 0..n {
1908 let ptr = crate::v2::typed_array::TypedArray::get_unchecked(keys, i as u32);
1909 out.push(crate::v2::string_obj::StringObj::as_str(ptr));
1910 }
1911 out
1912 }
1913 }
1914
1915 fn emit_key(f: &mut std::fmt::Formatter<'_>, i: usize, key: &str) -> std::fmt::Result {
1916 if i > 0 {
1917 f.write_str(", ")?;
1918 }
1919 write!(f, "\"{}\": ", key)
1920 }
1921
1922 match kref {
1923 HashMapKindedRef::I64(arc) => {
1924 let keys = unsafe { read_keys(arc.keys) };
1925 for (i, k) in keys.iter().enumerate() {
1926 emit_key(f, i, k)?;
1927 let v = unsafe { *(*arc.values).data.add(i) };
1928 write!(f, "{}", v)?;
1929 }
1930 }
1931 HashMapKindedRef::F64(arc) => {
1932 let keys = unsafe { read_keys(arc.keys) };
1933 for (i, k) in keys.iter().enumerate() {
1934 emit_key(f, i, k)?;
1935 let v = unsafe { *(*arc.values).data.add(i) };
1936 write!(f, "{}", v)?;
1937 }
1938 }
1939 HashMapKindedRef::Bool(arc) => {
1940 let keys = unsafe { read_keys(arc.keys) };
1941 for (i, k) in keys.iter().enumerate() {
1942 emit_key(f, i, k)?;
1943 let v: u8 = unsafe { *(*arc.values).data.add(i) };
1944 write!(f, "{}", v != 0)?;
1945 }
1946 }
1947 HashMapKindedRef::Char(arc) => {
1948 let keys = unsafe { read_keys(arc.keys) };
1949 for (i, k) in keys.iter().enumerate() {
1950 emit_key(f, i, k)?;
1951 let v: char = unsafe { *(*arc.values).data.add(i) };
1952 write!(f, "'{}'", v)?;
1953 }
1954 }
1955 HashMapKindedRef::String(arc) => {
1956 let keys = unsafe { read_keys(arc.keys) };
1957 for (i, k) in keys.iter().enumerate() {
1958 emit_key(f, i, k)?;
1959 let v_ptr: *const crate::v2::string_obj::StringObj =
1960 unsafe { *(*arc.values).data.add(i) };
1961 let s = unsafe { crate::v2::string_obj::StringObj::as_str(v_ptr) };
1962 write!(f, "\"{}\"", s)?;
1963 }
1964 }
1965 HashMapKindedRef::Decimal(arc) => {
1966 let keys = unsafe { read_keys(arc.keys) };
1967 for (i, k) in keys.iter().enumerate() {
1968 emit_key(f, i, k)?;
1969 let v_ptr: *const crate::v2::decimal_obj::DecimalObj =
1970 unsafe { *(*arc.values).data.add(i) };
1971 // DecimalObj::as_decimal returns the Decimal value via the
1972 // v2-raw payload (mirrors StringObj::as_str shape).
1973 let d = unsafe { (*v_ptr).value };
1974 let mut tmp = String::new();
1975 let _ = write!(tmp, "{}", d);
1976 f.write_str(&tmp)?;
1977 }
1978 }
1979 HashMapKindedRef::TypedObject(arc) => {
1980 let keys = unsafe { read_keys(arc.keys) };
1981 for (i, k) in keys.iter().enumerate() {
1982 emit_key(f, i, k)?;
1983 // Render as opaque tag — full recursive rendering lives at
1984 // printing.rs::format_typed_object (depth-budgeted).
1985 let v_ref: &TypedObjectPtr = unsafe { &*(*arc.values).data.add(i) };
1986 write!(f, "<typed_object:{:p}>", v_ref.as_ptr())?;
1987 }
1988 }
1989 HashMapKindedRef::TraitObject(arc) => {
1990 let keys = unsafe { read_keys(arc.keys) };
1991 for (i, k) in keys.iter().enumerate() {
1992 emit_key(f, i, k)?;
1993 let v_ref: &TraitObjectPtr = unsafe { &*(*arc.values).data.add(i) };
1994 write!(f, "<trait_object:{:p}>", v_ref.as_ptr())?;
1995 }
1996 }
1997 HashMapKindedRef::HashMap(arc) => {
1998 // Recursive carrier: each value is itself a HashMapKindedRef.
1999 // Recurse via this same display formatter (Wave N
2000 // hashmap-value-v-arm follow-up, cluster-2 closure-wave-C,
2001 // 2026-05-16).
2002 let keys = unsafe { read_keys(arc.keys) };
2003 for (i, k) in keys.iter().enumerate() {
2004 emit_key(f, i, k)?;
2005 let inner_ref: &HashMapKindedRef = unsafe { &*(*arc.values).data.add(i) };
2006 hashmap_kref_display(inner_ref, f)?;
2007 }
2008 }
2009 }
2010 write!(f, "}}")
2011}
2012
2013// ── Legacy HashMapValueBuf + non-generic HashMapData REMOVED (Wave 2 Round 3b
2014// C2-joint ckpt-1, 2026-05-14) ──────────────────────────────────────────
2015//
2016// The pre-Q25.B-SUPERSEDED `HashMapValueBuf` enum + non-generic `HashMapData`
2017// struct/impl have been removed. The replacement is `HashMapData<V>` +
2018// `HashMapKindedRef` + `HashMapValueElem` trait (above). Consumer sites at
2019// `HeapValue::HashMap` variant payload + 51 `Arc<HashMapData>` usages cascade
2020// in ckpt-2 (variant signature) + ckpt-3 (hashmap_methods.rs / printing.rs /
2021// xml.rs / json.rs / array_transform.rs / vm_impl/builtins.rs /
2022// trait_object_ops.rs) + ckpt-final (JIT FFI).
2023
2024// ── HashSet storage (Wave 13 W13-hashset-rebuild, 2026-05-10) ───────────────
2025
2026/// HashSet storage — one keyspace, no values. Mirror of `HashMapData`
2027/// with the values buffer dropped.
2028///
2029/// ADR-006 §2.7.15 / Q16 amendment (mirror of §2.7.9 FilterExpr / §2.7.13
2030/// Reference precedent for the cardinality-amendment shape, but Set is
2031/// a HashMap *sibling* — full `HeapValue::HashSet` arm rather than
2032/// pure-discriminator). Reuses the Stage C P1(b) Phase 2d Array shape
2033/// (`TypedBuffer<Arc<String>>`) for the keys buffer verbatim. Insertion
2034/// order is the canonical storage; the `index` is a sidecar acceleration
2035/// structure for O(1) `set.has(key)`.
2036///
2037/// **String-only keyspace at landing** (per the W9-set-methods owner
2038/// audit's Path A scope and the §2.7.15 Q16 ruling). Heterogeneous-
2039/// element keysets (int-keyed, TypedObject-keyed) are explicitly
2040/// out-of-scope; the Path B (`TypedSet<T>` per element kind) rebuild
2041/// is a future Phase-2c amendment with measurement.
2042#[derive(Debug)]
2043pub struct HashSetData {
2044 /// Insertion-ordered keys (string-typed buffer).
2045 ///
2046 /// Storage shape: `Arc<Vec<Arc<String>>>` post-V3-S5 ckpt-5-prime²a
2047 /// (Migration shape (a) per supervisor 2026-05-15 ratification —
2048 /// `TypedBuffer<T>` wrapper layer retired wholesale at ckpt-4;
2049 /// `Arc<Vec<T>>` is the smallest delta preserving `Arc::make_mut`
2050 /// clone-on-write semantics).
2051 pub keys: Arc<Vec<Arc<String>>>,
2052 /// Eager bucket-index: hash → list of indices into `keys` array.
2053 /// Enables O(1) lookup at `set.has(key)`. Hash is FNV-1a over the
2054 /// key string bytes — same as `HashMapData::index`.
2055 pub index: std::collections::HashMap<u64, Vec<u32>>,
2056}
2057
2058impl HashSetData {
2059 /// Build an empty HashSetData with no entries.
2060 pub fn new() -> Self {
2061 Self {
2062 keys: Arc::new(Vec::new()),
2063 index: std::collections::HashMap::new(),
2064 }
2065 }
2066
2067 /// Build from a `Vec<Arc<String>>` of keys, computing the bucket
2068 /// index eagerly. Duplicate keys in the input are collapsed
2069 /// (insertion-order preserved, first occurrence wins).
2070 pub fn from_keys(keys: Vec<Arc<String>>) -> Self {
2071 let mut out = Self::new();
2072 for k in keys {
2073 out.insert(k);
2074 }
2075 out
2076 }
2077
2078 /// Number of entries.
2079 #[inline]
2080 pub fn len(&self) -> usize {
2081 self.keys.len()
2082 }
2083
2084 /// Whether the set is empty.
2085 #[inline]
2086 pub fn is_empty(&self) -> bool {
2087 self.keys.is_empty()
2088 }
2089
2090 /// Whether the set contains the given key. O(1) via the bucket
2091 /// index plus a short bucket scan for collision disambiguation.
2092 pub fn contains(&self, key: &str) -> bool {
2093 let hash = fnv1a_hash(key.as_bytes());
2094 let Some(bucket) = self.index.get(&hash) else {
2095 return false;
2096 };
2097 for &idx in bucket {
2098 let i = idx as usize;
2099 if self.keys[i].as_str() == key {
2100 return true;
2101 }
2102 }
2103 false
2104 }
2105
2106 // ── Mutation API (Wave 13 W13-hashset-rebuild, 2026-05-10) ──────────────
2107 //
2108 // Mirror of HashMapData's W13-hashmap-mutation API with the values
2109 // buffer dropped. `Arc::make_mut` clone-on-write over the inner
2110 // `Arc<TypedBuffer<Arc<String>>>` keys plus parallel bucket-index
2111 // maintenance — same shape, one less buffer to mutate.
2112
2113 /// Insert a key. Returns `true` if the key was newly added,
2114 /// `false` if it was already present (no-op in the latter case).
2115 pub fn insert(&mut self, key: Arc<String>) -> bool {
2116 let hash = fnv1a_hash(key.as_bytes());
2117 if let Some(bucket) = self.index.get(&hash) {
2118 for &idx in bucket {
2119 let i = idx as usize;
2120 if self.keys[i].as_str() == key.as_str() {
2121 return false;
2122 }
2123 }
2124 }
2125 let new_idx = self.keys.len();
2126 Arc::make_mut(&mut self.keys).push(key);
2127 self.index.entry(hash).or_default().push(new_idx as u32);
2128 true
2129 }
2130
2131 /// Remove the entry under `key`. Returns `true` if the key was
2132 /// present (and removed), `false` if no entry existed. The bucket
2133 /// index is updated to reflect the buffer's post-removal indices:
2134 /// every entry after the removed slot shifts down by one position
2135 /// (mirror of `HashMapData::remove`).
2136 pub fn remove(&mut self, key: &str) -> bool {
2137 let hash = fnv1a_hash(key.as_bytes());
2138 let removed_idx: usize = {
2139 let Some(bucket) = self.index.get(&hash) else {
2140 return false;
2141 };
2142 let mut found: Option<usize> = None;
2143 for (bucket_pos, &idx) in bucket.iter().enumerate() {
2144 if self.keys[idx as usize].as_str() == key {
2145 found = Some(bucket_pos);
2146 break;
2147 }
2148 }
2149 let bucket_pos = match found {
2150 Some(p) => p,
2151 None => return false,
2152 };
2153 let bucket = self.index.get_mut(&hash).expect("bucket present");
2154 let removed_idx = bucket.swap_remove(bucket_pos) as usize;
2155 if bucket.is_empty() {
2156 self.index.remove(&hash);
2157 }
2158 removed_idx
2159 };
2160 Arc::make_mut(&mut self.keys).remove(removed_idx);
2161 for bucket in self.index.values_mut() {
2162 for slot in bucket.iter_mut() {
2163 if (*slot as usize) > removed_idx {
2164 *slot -= 1;
2165 }
2166 }
2167 }
2168 true
2169 }
2170}
2171
2172impl Default for HashSetData {
2173 fn default() -> Self {
2174 Self::new()
2175 }
2176}
2177
2178impl Clone for HashSetData {
2179 fn clone(&self) -> Self {
2180 Self {
2181 keys: Arc::clone(&self.keys),
2182 index: self.index.clone(),
2183 }
2184 }
2185}
2186
2187// ── Result / Option storage (ADR-006 §2.7.17 / Q18, W14-variant-codegen) ────
2188//
2189// Wave 14 W14-variant-codegen amendment: Result<T,E> and Option<T> are
2190// represented as kinded carriers `Arc<ResultData>` / `Arc<OptionData>`
2191// holding (a) a `is_ok` / `is_some` discriminator boolean and (b) a single
2192// payload `KindedSlot` carrying one strong-count share for the inner
2193// value. Mirrors the §2.7.16 IteratorState typed-Arc shape and the §2.5
2194// AnyError schema-keyed kind discipline (per-slot kind threaded
2195// alongside slot bits, drop dispatched on the kind label). Slot bits at
2196// the §2.7.7 stack tier are `Arc::into_raw(Arc<ResultData>)` /
2197// `Arc::into_raw(Arc<OptionData>)` directly with kind labels
2198// `NativeKind::Ptr(HeapKind::Result)` / `NativeKind::Ptr(HeapKind::Option)`.
2199//
2200// The payload `KindedSlot` lives inside the typed-Arc so the value's
2201// strong-count share is owned by the wrapper for the wrapper's lifetime;
2202// `KindedSlot::Drop` retires the inner share when the wrapper Drop runs
2203// (Arc refcount reaches zero). On clone, `KindedSlot::Clone` bumps the
2204// inner share. Same recursion-through-Arc discipline as
2205// `IteratorTransform::Map(Arc<HeapValue>)` per §2.7.16.
2206
2207/// Result<T, E> carrier. `is_ok` discriminates Ok vs Err; `payload` carries
2208/// the inner value (`T` for Ok, `E` for Err). Both arms share the same
2209/// payload slot — the variant tag is the discriminator, not the slot's
2210/// physical layout.
2211#[derive(Debug)]
2212pub struct ResultData {
2213 pub is_ok: bool,
2214 pub payload: crate::kinded_slot::KindedSlot,
2215}
2216
2217impl ResultData {
2218 /// Construct an Ok-tagged result.
2219 #[inline]
2220 pub fn ok(payload: crate::kinded_slot::KindedSlot) -> Self {
2221 Self { is_ok: true, payload }
2222 }
2223
2224 /// Construct an Err-tagged result.
2225 #[inline]
2226 pub fn err(payload: crate::kinded_slot::KindedSlot) -> Self {
2227 Self { is_ok: false, payload }
2228 }
2229}
2230
2231impl Clone for ResultData {
2232 /// Per-field clone — `KindedSlot::Clone` bumps the payload's
2233 /// strong-count share.
2234 fn clone(&self) -> Self {
2235 Self {
2236 is_ok: self.is_ok,
2237 payload: self.payload.clone(),
2238 }
2239 }
2240}
2241
2242/// Option<T> carrier. `is_some` discriminates Some vs None; `payload`
2243/// carries the inner value for Some. For None the payload is a
2244/// `KindedSlot::none()` placeholder (Bool-kind, zero bits) so
2245/// `KindedSlot::Drop` is a no-op.
2246#[derive(Debug)]
2247pub struct OptionData {
2248 pub is_some: bool,
2249 pub payload: crate::kinded_slot::KindedSlot,
2250}
2251
2252impl OptionData {
2253 /// Construct a Some-tagged option.
2254 #[inline]
2255 pub fn some(payload: crate::kinded_slot::KindedSlot) -> Self {
2256 Self { is_some: true, payload }
2257 }
2258
2259 /// Construct a None-tagged option (payload is a no-op KindedSlot).
2260 #[inline]
2261 pub fn none() -> Self {
2262 Self {
2263 is_some: false,
2264 payload: crate::kinded_slot::KindedSlot::none(),
2265 }
2266 }
2267}
2268
2269impl Clone for OptionData {
2270 /// Per-field clone — `KindedSlot::Clone` bumps the payload's
2271 /// strong-count share. For None the payload is a zero-bits Bool
2272 /// slot; clone is a no-op refcount-wise.
2273 fn clone(&self) -> Self {
2274 Self {
2275 is_some: self.is_some,
2276 payload: self.payload.clone(),
2277 }
2278 }
2279}
2280
2281/// FNV-1a hash for byte slices. Matches the `v2/typed_map.rs` hash
2282/// function so that key-hash semantics are consistent across the
2283/// HashMap-marshal layer and any future cross-cluster perf path.
2284#[inline]
2285fn fnv1a_hash(bytes: &[u8]) -> u64 {
2286 let mut h: u64 = 0xcbf29ce484222325;
2287 for &b in bytes {
2288 h ^= b as u64;
2289 h = h.wrapping_mul(0x100000001b3);
2290 }
2291 h
2292}
2293
2294// ── Deque storage (W15-deque, ADR-006 §2.7.19 / Q20, 2026-05-10) ───────────
2295
2296/// Double-ended queue storage. Heterogeneous element kinds are stored as
2297/// `Arc<HeapValue>` payloads (mirror of `HashMapData::values` per ADR-005
2298/// §1 single-discriminator) — the deque is element-kind-agnostic at
2299/// landing, in line with the W13-hashmap precedent.
2300///
2301/// ADR-006 §2.7.19 / Q20 amendment (Wave 15 W15-deque, 2026-05-10).
2302/// Mirror of the §2.7.15 HashSet shape (full `HeapValue::Deque` arm,
2303/// NOT pure-discriminator like FilterExpr / SharedCell): receivers
2304/// flow through `slot.as_heap_value()` for receiver classification at
2305/// method dispatch (`d.pushBack(...)` / `d.popFront()` / `d.size()`).
2306///
2307/// **Heterogeneous-element keyspace at landing.** Element kinds that
2308/// can be heap-wrapped (string / int via `BigInt(Arc<i64>)` / typed
2309/// arrays / typed objects / hashmaps / etc.) are accepted by the
2310/// mutation API; bare `Float64` / `Bool` results are rejected (no
2311/// matching `HeapValue::*` arm exists post-§2.3). Same coverage shape
2312/// as `HashMapData::values` storage (`hashmap_methods.rs::
2313/// result_slot_to_heap_value_arc`).
2314///
2315/// Per the W15-deque audit: `VecDeque<Arc<HeapValue>>` chosen over the
2316/// alternative `Vec<u64>` + parallel `Vec<NativeKind>` (per §2.7.7
2317/// stack ABI) — Deque is heterogeneous-element, not scalar-only, so
2318/// the parallel-kind track shape would force every push site to
2319/// carry both bits and kind through the deque API. The
2320/// `Arc<HeapValue>` shape collapses both into a single payload at the
2321/// element tier and matches the Stage C P1(b) HashMap precedent.
2322#[derive(Debug)]
2323pub struct DequeData {
2324 /// Insertion-ordered double-ended queue of heap-allocated element
2325 /// payloads. Element kinds are recovered via the canonical ADR-005
2326 /// §1 single-discriminator `HeapValue` match at the read site.
2327 pub items: std::collections::VecDeque<Arc<HeapValue>>,
2328}
2329
2330impl DequeData {
2331 /// Build an empty DequeData with no elements.
2332 pub fn new() -> Self {
2333 Self {
2334 items: std::collections::VecDeque::new(),
2335 }
2336 }
2337
2338 /// Build from a `Vec<Arc<HeapValue>>`. Insertion order is the
2339 /// front-to-back walk order.
2340 pub fn from_items(items: Vec<Arc<HeapValue>>) -> Self {
2341 Self {
2342 items: std::collections::VecDeque::from(items),
2343 }
2344 }
2345
2346 /// Number of elements.
2347 #[inline]
2348 pub fn len(&self) -> usize {
2349 self.items.len()
2350 }
2351
2352 /// Whether the deque is empty.
2353 #[inline]
2354 pub fn is_empty(&self) -> bool {
2355 self.items.is_empty()
2356 }
2357
2358 /// Borrow the front element without removing it. `None` when empty.
2359 pub fn peek_front(&self) -> Option<&Arc<HeapValue>> {
2360 self.items.front()
2361 }
2362
2363 /// Borrow the back element without removing it. `None` when empty.
2364 pub fn peek_back(&self) -> Option<&Arc<HeapValue>> {
2365 self.items.back()
2366 }
2367
2368 /// Borrow the element at `index` (front-counted). `None` when out
2369 /// of bounds.
2370 pub fn get(&self, index: usize) -> Option<&Arc<HeapValue>> {
2371 self.items.get(index)
2372 }
2373
2374 // ── Mutation API (W15-deque, 2026-05-10) ────────────────────────────────
2375 //
2376 // Mirror of HashMapData / HashSetData clone-on-write shape — callers
2377 // wrap mutation in `Arc::make_mut(&mut arc).push_back(...)` so the
2378 // shared-receiver semantics are preserved per ADR-006 §2.7.4.
2379
2380 /// Push an element onto the back of the deque.
2381 pub fn push_back(&mut self, value: Arc<HeapValue>) {
2382 self.items.push_back(value);
2383 }
2384
2385 /// Push an element onto the front of the deque.
2386 pub fn push_front(&mut self, value: Arc<HeapValue>) {
2387 self.items.push_front(value);
2388 }
2389
2390 /// Remove and return the back element. `None` when empty.
2391 pub fn pop_back(&mut self) -> Option<Arc<HeapValue>> {
2392 self.items.pop_back()
2393 }
2394
2395 /// Remove and return the front element. `None` when empty.
2396 pub fn pop_front(&mut self) -> Option<Arc<HeapValue>> {
2397 self.items.pop_front()
2398 }
2399}
2400
2401impl Default for DequeData {
2402 fn default() -> Self {
2403 Self::new()
2404 }
2405}
2406
2407// ── Channel storage (Wave 15 W15-channel, ADR-006 §2.7.20 / Q21,
2408// 2026-05-10) ──────────────────────────────────────────────────────────────
2409
2410/// MPSC-style synchronous channel storage.
2411///
2412/// ADR-006 §2.7.20 / Q21 amendment (Wave 15 W15-channel-rebuild,
2413/// 2026-05-10). Channel is a concurrency primitive; unlike the
2414/// HashMap/HashSet siblings (insertion-ordered immutable-on-clone
2415/// keys-buffer with `Arc::make_mut` clone-on-write), Channel needs
2416/// **interior mutability** so that two `Arc<ChannelData>` shares of
2417/// the same channel observe each other's `send` / `recv` mutations
2418/// (the producer and consumer endpoints share the same buffer). The
2419/// inner state therefore lives behind a `Mutex<ChannelInner>`; the
2420/// outer `Arc` is purely a refcount carrier.
2421///
2422/// **Sync same-thread path only at landing.** Cross-task / cross-
2423/// thread blocking `recv()` (the canonical async-channel use case)
2424/// requires integration with the §2.7.4 task-scheduler boundary
2425/// (`shape-vm/src/executor/task_scheduler.rs`), which is itself a
2426/// phase-2c surface; per the W15 playbook the async paths SURFACE
2427/// cleanly. The sync path (same-thread `send` then `recv`) lands
2428/// here end-to-end.
2429///
2430/// **Element typing.** The buffer stores `KindedSlot` payloads
2431/// directly so heterogeneous-element queues are first-class (a
2432/// channel can carry ints, strings, or typed objects without a
2433/// per-element-kind specialisation). Each slot owns one strong-
2434/// count share for heap-bearing kinds; the `KindedSlot::Drop`
2435/// dispatch retires shares cleanly when the channel itself drops.
2436/// This is the same shape `concurrency_methods.rs` (Mutex/Atomic/
2437/// Lazy) will use when those primitives rebuild — Channel is the
2438/// first concurrency primitive to land kinded.
2439///
2440/// **Closed flag.** `closed: bool` records whether the producer
2441/// side has signalled end-of-stream. After `close()` further
2442/// `send()` calls return a closed-channel error; `recv()` continues
2443/// to drain queued elements and only errors once the queue is
2444/// empty (canonical drain-on-close semantics).
2445#[derive(Debug)]
2446pub struct ChannelData {
2447 inner: std::sync::Mutex<ChannelInner>,
2448}
2449
2450/// Inner mutable state of a `ChannelData`. Held under `Mutex` so
2451/// concurrent `Arc<ChannelData>` shares observe each other's
2452/// mutations.
2453#[derive(Debug)]
2454struct ChannelInner {
2455 /// FIFO queue of pending kinded elements.
2456 queue: std::collections::VecDeque<crate::kinded_slot::KindedSlot>,
2457 /// Producer-side end-of-stream signal. Once set, further
2458 /// `send()` calls return a closed-channel error and `recv()`
2459 /// drains remaining elements before erroring.
2460 closed: bool,
2461}
2462
2463impl ChannelData {
2464 /// Build an empty open channel.
2465 pub fn new() -> Self {
2466 Self {
2467 inner: std::sync::Mutex::new(ChannelInner {
2468 queue: std::collections::VecDeque::new(),
2469 closed: false,
2470 }),
2471 }
2472 }
2473
2474 /// Number of pending elements. Useful for diagnostics; not part
2475 /// of the user-facing method surface.
2476 pub fn len(&self) -> usize {
2477 self.inner.lock().expect("channel mutex poisoned").queue.len()
2478 }
2479
2480 /// Whether the queue currently holds zero pending elements.
2481 pub fn is_empty(&self) -> bool {
2482 self.inner
2483 .lock()
2484 .expect("channel mutex poisoned")
2485 .queue
2486 .is_empty()
2487 }
2488
2489 /// Whether `close()` has been called.
2490 pub fn is_closed(&self) -> bool {
2491 self.inner.lock().expect("channel mutex poisoned").closed
2492 }
2493
2494 /// Append `slot` to the queue.
2495 ///
2496 /// Returns `Ok(())` on success, `Err(())` if the channel is
2497 /// already closed (callers surface this as a runtime error
2498 /// from the `send` method body).
2499 pub fn send(&self, slot: crate::kinded_slot::KindedSlot) -> Result<(), ()> {
2500 let mut inner = self.inner.lock().expect("channel mutex poisoned");
2501 if inner.closed {
2502 // Drop the slot — its share retires through KindedSlot::Drop.
2503 drop(slot);
2504 return Err(());
2505 }
2506 inner.queue.push_back(slot);
2507 Ok(())
2508 }
2509
2510 /// Pop the front element non-blocking.
2511 ///
2512 /// Returns `Some(slot)` if an element was available, `None`
2513 /// otherwise. Per ADR §2.7.20 the same-thread sync path is the
2514 /// supported surface; blocking `recv()` (await-style) requires
2515 /// the §2.7.4 task-scheduler boundary and is SURFACE'd at the
2516 /// method body.
2517 pub fn try_recv(&self) -> Option<crate::kinded_slot::KindedSlot> {
2518 self.inner
2519 .lock()
2520 .expect("channel mutex poisoned")
2521 .queue
2522 .pop_front()
2523 }
2524
2525 /// Mark the channel closed. Idempotent — calling close on an
2526 /// already-closed channel is a no-op.
2527 pub fn close(&self) {
2528 self.inner.lock().expect("channel mutex poisoned").closed = true;
2529 }
2530}
2531
2532impl Default for ChannelData {
2533 fn default() -> Self {
2534 Self::new()
2535 }
2536}
2537
2538// ── Mutex / Atomic / Lazy storage (Wave 17 W17-concurrency,
2539// ADR-006 §2.7.25, 2026-05-11) ──────────────────────────────────────────
2540//
2541// W17-concurrency rebuild: Mutex, Atomic, and Lazy are the three
2542// concurrency primitives left SURFACE'd by the strict-typing Phase-2
2543// bulldozer (the `HeapValue::Concurrency(ConcurrencyData::*)` enum form
2544// was deleted alongside `ValueWord`; see the deletion-fate comment in
2545// `executor/objects/concurrency_methods.rs:1-22`). Each lands as its own
2546// typed-Arc HeapValue arm per ADR-006 §2.3 / §2.7.25, mirror of the
2547// §2.7.20 Channel rebuild structure:
2548//
2549// - `Mutex<T>` carries a single `KindedSlot` payload protected by a
2550// `Mutex<MutexInner>`. Like `ChannelData`, two `Arc<MutexData>` shares
2551// observe each other's mutations — the canonical "shared cell with
2552// exclusion" shape. `lock()` is a no-op marker at landing (single-
2553// threaded VM); the contract is that the inner value is mutated under
2554// exclusion. `try_lock()` mirrors `lock()`. `set(value)` swaps the
2555// inner payload (KindedSlot Drop retires the prior share).
2556// - `Atomic<i64>` carries a `std::sync::atomic::AtomicI64` for the
2557// atomic operations (`load`, `store`, `fetch_add`, `fetch_sub`,
2558// `compare_exchange`). i64-only at landing per the playbook's
2559// "i64-priority" / "string-only" precedents (W15-priority-queue,
2560// W13-hashset). A typed-payload `Atomic<T>` is a future amendment
2561// with measurement.
2562// - `Lazy<T>` carries a `Mutex<LazyInner>` wrapping `(initializer:
2563// Option<KindedSlot>, value: Option<KindedSlot>)`. `get()` returns
2564// the cached value or runs the initializer closure (closure-call
2565// path unlocked by W17-make-closure, merged at `aa47364`).
2566// `is_initialized()` returns whether the value has been computed.
2567//
2568// Forbidden shapes refused (per CLAUDE.md "Renames to refuse on sight"
2569// + playbook §3 W17-concurrency forbidden list):
2570//
2571// - Generic "concurrency primitive" wrapper (`ConcurrencyData` enum
2572// shape from the deleted form). Each primitive is its own typed-Arc
2573// HeapValue arm.
2574// - Inline-scalar Mutex/Atomic carriers (these are always heap — the
2575// semantic identity is "this is a shared cell with mutation", which
2576// has no inline-scalar reduction).
2577// - Re-using `HeapKind::SharedCell` for Mutex (different semantics —
2578// `SharedCell` is binding-storage interior-mutability for `var`
2579// binding-form values, while `MutexData` is a runtime synchronization
2580// primitive user code asks for explicitly).
2581
2582/// `Mutex<T>` storage — a single typed payload protected by a Rust
2583/// `Mutex` so concurrent `Arc<MutexData>` shares observe each other's
2584/// mutations (the canonical "shared cell with exclusion" shape, mirror
2585/// of `ChannelData`'s `Mutex<ChannelInner>` interior-mutability shape).
2586///
2587/// At landing the VM is single-threaded so `lock()` / `try_lock()` are
2588/// no-op markers — the contract they preserve is "the inner value is
2589/// mutated under exclusion" (the same contract user code reasons
2590/// about). When the VM grows real concurrency, the same `Mutex` here
2591/// will serialize concurrent `lock()` calls without API churn.
2592///
2593/// The inner `Option<KindedSlot>` carries one strong-count share for
2594/// the wrapped value when present; `take()` / `replace()` discipline
2595/// preserves the share-discipline across `set(...)` (the old slot
2596/// drops, the new slot is owned by the cell).
2597#[derive(Debug)]
2598pub struct MutexData {
2599 inner: std::sync::Mutex<MutexInner>,
2600}
2601
2602#[derive(Debug)]
2603struct MutexInner {
2604 /// Wrapped value. `None` only transiently between `take` and replace
2605 /// during `set(...)` — never observable externally.
2606 value: Option<crate::kinded_slot::KindedSlot>,
2607}
2608
2609impl MutexData {
2610 /// Build a `MutexData` wrapping `value`.
2611 pub fn new(value: crate::kinded_slot::KindedSlot) -> Self {
2612 Self {
2613 inner: std::sync::Mutex::new(MutexInner { value: Some(value) }),
2614 }
2615 }
2616
2617 /// `lock()` — at landing a no-op marker (single-threaded VM). When
2618 /// the runtime grows real concurrency, this is the acquire point
2619 /// for the inner `std::sync::Mutex`.
2620 pub fn lock(&self) {
2621 let _g = self.inner.lock().expect("mutex poisoned");
2622 }
2623
2624 /// `try_lock()` — at landing always returns true (single-threaded
2625 /// VM; there's no contention to fail). Mirror of `lock()`.
2626 pub fn try_lock(&self) -> bool {
2627 self.inner.try_lock().is_ok()
2628 }
2629
2630 /// Read the current value (clone of the inner `KindedSlot`).
2631 /// `KindedSlot::Clone` bumps the inner share so the returned slot
2632 /// is independently owned.
2633 pub fn get(&self) -> crate::kinded_slot::KindedSlot {
2634 let inner = self.inner.lock().expect("mutex poisoned");
2635 inner
2636 .value
2637 .as_ref()
2638 .expect("mutex value present")
2639 .clone()
2640 }
2641
2642 /// Replace the wrapped value. The prior slot drops here
2643 /// (`KindedSlot::Drop` retires its inner share); the new slot is
2644 /// owned by the cell.
2645 pub fn set(&self, new_value: crate::kinded_slot::KindedSlot) {
2646 let mut inner = self.inner.lock().expect("mutex poisoned");
2647 inner.value = Some(new_value);
2648 }
2649}
2650
2651/// `Atomic<i64>` storage — wraps a `std::sync::atomic::AtomicI64` for
2652/// the atomic operations exposed by the `Atomic.load` / `store` /
2653/// `fetch_add` / `fetch_sub` / `compare_exchange` method surface.
2654///
2655/// **i64-only at landing** per the playbook's typed-payload deferral
2656/// precedent (W15-priority-queue i64-priority-only). A typed-payload
2657/// `Atomic<T>` is a future Phase-2c amendment with measurement.
2658///
2659/// Memory ordering is `SeqCst` (sequential consistency) throughout —
2660/// the simplest semantically-correct ordering. Relaxed-ordering
2661/// optimizations are a measured follow-up.
2662#[derive(Debug)]
2663pub struct AtomicData {
2664 value: std::sync::atomic::AtomicI64,
2665}
2666
2667impl AtomicData {
2668 /// Build an `AtomicData` with initial value `init`.
2669 pub fn new(init: i64) -> Self {
2670 Self {
2671 value: std::sync::atomic::AtomicI64::new(init),
2672 }
2673 }
2674
2675 /// Atomic load (SeqCst).
2676 pub fn load(&self) -> i64 {
2677 self.value.load(std::sync::atomic::Ordering::SeqCst)
2678 }
2679
2680 /// Atomic store (SeqCst).
2681 pub fn store(&self, v: i64) {
2682 self.value.store(v, std::sync::atomic::Ordering::SeqCst)
2683 }
2684
2685 /// Atomic fetch-add (SeqCst). Returns the prior value.
2686 pub fn fetch_add(&self, delta: i64) -> i64 {
2687 self.value
2688 .fetch_add(delta, std::sync::atomic::Ordering::SeqCst)
2689 }
2690
2691 /// Atomic fetch-sub (SeqCst). Returns the prior value.
2692 pub fn fetch_sub(&self, delta: i64) -> i64 {
2693 self.value
2694 .fetch_sub(delta, std::sync::atomic::Ordering::SeqCst)
2695 }
2696
2697 /// Atomic compare-exchange (SeqCst). Returns the prior value
2698 /// regardless of success — callers infer success by comparing to
2699 /// `expected`.
2700 pub fn compare_exchange(&self, expected: i64, new_v: i64) -> i64 {
2701 match self.value.compare_exchange(
2702 expected,
2703 new_v,
2704 std::sync::atomic::Ordering::SeqCst,
2705 std::sync::atomic::Ordering::SeqCst,
2706 ) {
2707 Ok(prev) => prev,
2708 Err(prev) => prev,
2709 }
2710 }
2711}
2712
2713/// `Lazy<T>` storage — wraps an initializer closure (`KindedSlot` of
2714/// kind `Ptr(HeapKind::Closure)`) and a cached value slot. `get()`
2715/// runs the initializer the first time and caches the result;
2716/// subsequent calls return the cached value.
2717///
2718/// Closure-call dispatch (`vm.call_value_immediate_nb`) is unlocked by
2719/// the W17-make-closure partial-gate (merged at `aa47364`); see
2720/// `executor/objects/concurrency_methods.rs::v2_lazy_get` for the
2721/// closure-call body.
2722///
2723/// **`Mutex<LazyInner>` for interior mutability**: like `ChannelData`,
2724/// two `Arc<LazyData>` shares observe each other's initialization
2725/// state. The `OnceCell`-style "init only happens once" guarantee is
2726/// preserved by the inner Mutex serializing concurrent `get()` calls
2727/// when the runtime grows real concurrency. At landing (single-
2728/// threaded VM) the mutex is uncontended.
2729#[derive(Debug)]
2730pub struct LazyData {
2731 inner: std::sync::Mutex<LazyInner>,
2732}
2733
2734#[derive(Debug)]
2735struct LazyInner {
2736 /// Initializer closure (`KindedSlot` of kind `Ptr(HeapKind::Closure)`).
2737 /// `None` after first successful `get()` — the closure is dropped
2738 /// once its result is cached.
2739 initializer: Option<crate::kinded_slot::KindedSlot>,
2740 /// Cached value. `None` before first `get()`, `Some` after.
2741 value: Option<crate::kinded_slot::KindedSlot>,
2742}
2743
2744impl LazyData {
2745 /// Build a `LazyData` wrapping `initializer` (expected to be a
2746 /// closure `KindedSlot`).
2747 pub fn new(initializer: crate::kinded_slot::KindedSlot) -> Self {
2748 Self {
2749 inner: std::sync::Mutex::new(LazyInner {
2750 initializer: Some(initializer),
2751 value: None,
2752 }),
2753 }
2754 }
2755
2756 /// Whether `get()` has been called and the value cached.
2757 pub fn is_initialized(&self) -> bool {
2758 self.inner
2759 .lock()
2760 .expect("lazy mutex poisoned")
2761 .value
2762 .is_some()
2763 }
2764
2765 /// Read the cached value if present, else `None`. The closure-call
2766 /// path (running the initializer) lives in the handler tier — this
2767 /// method is the storage-tier cache lookup. Returns a clone of the
2768 /// cached slot (one strong-count share bumped).
2769 pub fn cached(&self) -> Option<crate::kinded_slot::KindedSlot> {
2770 self.inner
2771 .lock()
2772 .expect("lazy mutex poisoned")
2773 .value
2774 .as_ref()
2775 .cloned()
2776 }
2777
2778 /// Take the initializer closure (for the handler tier to invoke
2779 /// via `vm.call_value_immediate_nb`). Returns `None` if the value
2780 /// is already cached (caller should use `cached()` instead).
2781 pub fn take_initializer(&self) -> Option<crate::kinded_slot::KindedSlot> {
2782 let mut inner = self.inner.lock().expect("lazy mutex poisoned");
2783 if inner.value.is_some() {
2784 return None;
2785 }
2786 inner.initializer.take()
2787 }
2788
2789 /// Cache the result of running the initializer. The initializer
2790 /// slot has already been dropped (via `take_initializer`); this
2791 /// installs the result. If a value was concurrently cached
2792 /// (impossible at single-threaded landing, but defensive for
2793 /// future concurrency), the new value drops cleanly via
2794 /// `KindedSlot::Drop`.
2795 pub fn store_result(&self, value: crate::kinded_slot::KindedSlot) {
2796 let mut inner = self.inner.lock().expect("lazy mutex poisoned");
2797 // The take_initializer caller is the only path that should
2798 // reach store_result, so value is None here at the
2799 // single-threaded landing.
2800 inner.value = Some(value);
2801 }
2802}
2803
2804// ── TraitObject storage (W17-trait-object-storage, ADR-006 §2.7.24 Q25.C,
2805// 2026-05-11) ──────────────────────────────────────────────────────────────
2806
2807/// `dyn Trait` storage — the typed-Arc replacement for the
2808/// bulldozer-deleted `HeapValue::TraitObject { value: Box<u64>,
2809/// vtable: Arc<VTable> }`. Pairs the boxed data half (always a
2810/// `TypedObject` per §Q25.C.4 universal-dyn ruling — scalars/strings
2811/// that implement traits are boxed into `TypedObject` first; the
2812/// auto-boxing rule lifts Rust's object-safety restrictions at the
2813/// cost of one heap indirection per `dyn` coerce) with the vtable
2814/// half (shared `Arc<VTable>` so per-impl vtables are constructed
2815/// once and IC-cached per §Q25.C.6).
2816///
2817/// **Forbidden alternative.** `Box<u64>` data half is explicitly
2818/// refused (ADR-006 §Q25.E #3 — kind-blind raw-bits storage, same
2819/// defection-attractor as the deleted ValueWord). The data half is
2820/// kinded by being a typed object with a schema — `Arc<TypedObjectStorage>`
2821/// recovers the per-field kind table via the schema_id.
2822///
2823/// **Identity contract.** `Arc::ptr_eq` on the vtable Arc is the
2824/// canonical equality for the §Q25.C.2 `Self`-arg runtime check;
2825/// `vtable.concrete_type_id` is the IC-stabilization key per §Q25.C.6.
2826///
2827/// Mirror of the §2.7.20 / §2.7.25 typed-Arc shape — refcount
2828/// discipline goes through the kind label (`HeapKind::TraitObject = 29`)
2829/// in `clone_with_kind` / `drop_with_kind`, NOT through `HeapValue`.
2830/// Method-receiver classification flows through `slot.as_heap_value()`
2831/// → `HeapValue::TraitObject(arc)` per ADR-005 §1 single-discriminator;
2832/// the `op_dyn_method_call` opcode handler (compiler-emission tier)
2833/// uses the recovered `Arc<TraitObjectStorage>` to look up the method
2834/// in `vtable.methods` and dispatch the appropriate `VTableEntry`.
2835///
2836/// **Wave 2 Agent E (2026-05-14): HeapHeader-equipped shape change.**
2837/// Per audit §4.3 Obstacle O-3.a resolution + ADR-006 §Q25.C.5 amendment,
2838/// the struct now carries a `HeapHeader` at offset 0 (`#[repr(C)]`) so
2839/// v2-raw raw-pointer allocations (`_new` / `_drop` + `impl HeapElement`)
2840/// can dispatch refcount on the header via `v2_retain` / `v2_release`.
2841/// Existing `Arc<TraitObjectStorage>` construction sites continue to work
2842/// unchanged — `Arc::new(TraitObjectStorage::new(...))` produces a Rust
2843/// `Arc`-wrapped instance whose embedded header sits at refcount=1 unused;
2844/// the dispatch arms continue to use `Arc::increment_strong_count` /
2845/// `Arc::decrement_strong_count` on those bits. The new `_new`-allocated
2846/// raw-pointer bits use the header's refcount via the `HeapElement` trait.
2847///
2848/// The inner `value: Arc<TypedObjectStorage>` field remains Arc-typed in
2849/// E's scope per Wave 1 §E.6 dispatch contract (the audit's E-a path
2850/// recommends both inner pointers become raw, but D2 owns the inner
2851/// `*mut TypedObjectStorage` flip in lockstep with TypedObjectStorage's
2852/// own Arc-path retirement; E's struct shape change exposes the
2853/// HeapHeader at offset 0 + manual lifecycle so subsequent rounds can
2854/// flip the inner field without re-shaping the outer carrier). The
2855/// `vtable: Arc<VTable>` field stays Arc-typed indefinitely under E's
2856/// scope — VTable lifecycle is decoupled from this migration (audit
2857/// §E.3 recommended a separate VTable HeapHeader migration if/when
2858/// IC devirtualization measurement justifies it).
2859#[repr(C)]
2860#[derive(Debug)]
2861pub struct TraitObjectStorage {
2862 /// v2-raw HeapHeader at offset 0 (8 bytes). Refcount/kind/flags.
2863 /// Initialized to `HeapHeader::new(HEAP_KIND_V2_TRAIT_OBJECT)` by
2864 /// `_new`; for `Arc`-wrapped instances allocated via
2865 /// `TraitObjectStorage::new` the header sits at refcount=1 unused
2866 /// (the enclosing `Arc` owns the lifecycle). See struct docstring.
2867 pub header: crate::v2::heap_header::HeapHeader,
2868
2869 /// The data half of the fat pointer — owned, heap-allocated as a
2870 /// `TypedObject`. Always present (never null); universal-dyn
2871 /// per-method auto-boxing makes the boxed value a real TypedObject
2872 /// even for scalar concrete types (per §Q25.C.1).
2873 ///
2874 /// **Wave 2 Round 4 D4 ckpt-3 (2026-05-14): inner-field shift from
2875 /// `Arc<TypedObjectStorage>` to `*const TypedObjectStorage`** per E
2876 /// (Round 2) close note + D3 R3a finding D3-1 — the 5th production-
2877 /// site class (audit-side parallel to D2's HashMapValueBuf cascade).
2878 /// The raw pointer was produced by `TypedObjectStorage::_new` (refcount
2879 /// initialized to 1 on the HeapHeader at offset 0). The carrier owns
2880 /// one strong-count share, retired at `_drop` / auto-derived `Drop`
2881 /// via `TypedObjectStorage::release_elem(ptr)` (NOT Rust `Arc::drop`).
2882 pub value: *const TypedObjectStorage,
2883
2884 /// The vtable half of the fat pointer. Shared via `Arc` across
2885 /// all `TraitObjectStorage` instances built from the same
2886 /// `(impl Trait for Type)` pair — vtable construction happens
2887 /// once per impl, the resulting `Arc<VTable>` is cached and
2888 /// cloned into each boxing site. IC stabilizes on
2889 /// `Arc::as_ptr(&vtable)` per §Q25.C.6.
2890 pub vtable: Arc<crate::value::VTable>,
2891}
2892
2893impl TraitObjectStorage {
2894 /// Build a `TraitObjectStorage` from its two halves. The caller
2895 /// owns one strong-count share on the v2-raw value pointer's
2896 /// HeapHeader-at-offset-0 refcount AND one strong-count share on
2897 /// the vtable Arc; the resulting struct owns both shares.
2898 ///
2899 /// **Wave 2 Round 4 D4 ckpt-3 (2026-05-14): `value` param signature
2900 /// shifted from `Arc<TypedObjectStorage>` to `*const TypedObjectStorage`**
2901 /// per E (Round 2) close note + D3 R3a finding D3-1 — caller produces
2902 /// the raw ptr via `TypedObjectStorage::_new` (refcount=1) or by
2903 /// `v2_retain`-bumping an existing live ptr. The carrier retires
2904 /// that share at `_drop` / auto-derived `Drop` via
2905 /// `TypedObjectStorage::release_elem(value)`.
2906 ///
2907 /// The embedded HeapHeader is initialized to refcount=1 with kind
2908 /// `HEAP_KIND_V2_TRAIT_OBJECT`. For `Arc<TraitObjectStorage>`
2909 /// instances the header sits unused (the enclosing `Arc` owns the
2910 /// lifecycle); the v2-raw `_new` path is the production carrier
2911 /// for the on-header refcount lifecycle.
2912 #[inline]
2913 pub fn new(value: *const TypedObjectStorage, vtable: Arc<crate::value::VTable>) -> Self {
2914 Self {
2915 header: crate::v2::heap_header::HeapHeader::new(
2916 crate::v2::heap_header::HEAP_KIND_V2_TRAIT_OBJECT,
2917 ),
2918 value,
2919 vtable,
2920 }
2921 }
2922
2923 /// Wave 2 Agent E (2026-05-14): v2-raw raw-pointer allocator.
2924 ///
2925 /// Allocates a new `TraitObjectStorage` on the heap and returns a raw
2926 /// pointer with refcount initialized to 1. Mirrors the `TypedObjectStorage::_new`
2927 /// precedent at `heap_value.rs` (D1, 2026-05-14) — `#[repr(C)]` struct
2928 /// with `HeapHeader` at offset 0; refcount discipline goes through
2929 /// `v2_retain` / `v2_release` via the `HeapElement` trait.
2930 ///
2931 /// Construction-side contract: the caller transfers ownership of one
2932 /// strong-count share on `value: Arc<TypedObjectStorage>` and on
2933 /// `vtable: Arc<VTable>` to the storage; the storage retires those
2934 /// shares at `_drop` (via the in-place `drop_in_place` on the field
2935 /// payloads). The inner Arcs follow normal Rust `Arc` discipline —
2936 /// only the outer struct's lifecycle is HeapHeader-managed.
2937 ///
2938 /// Callers (Wave 2 Round 2): replace the legacy pattern
2939 /// ```ignore
2940 /// let arc = Arc::new(TraitObjectStorage::new(value, vtable));
2941 /// let slot = ValueSlot::from_trait_object(arc);
2942 /// ```
2943 /// with the v2-raw pattern
2944 /// ```ignore
2945 /// let ptr = TraitObjectStorage::_new(value, vtable);
2946 /// let slot = ValueSlot::from_trait_object_raw(ptr);
2947 /// ```
2948 pub fn _new(
2949 value: *const TypedObjectStorage,
2950 vtable: Arc<crate::value::VTable>,
2951 ) -> *mut Self {
2952 let layout = std::alloc::Layout::new::<Self>();
2953 let ptr = unsafe { std::alloc::alloc(layout) as *mut Self };
2954 assert!(!ptr.is_null(), "allocation failed for TraitObjectStorage");
2955 unsafe {
2956 // SAFETY: `ptr` points to fresh, uninitialized memory of size
2957 // `Layout::new::<Self>()`. We write every field via `ptr::write`
2958 // to avoid running drop on uninitialized bytes (the existing
2959 // memory contains garbage, never a valid prior `Self`).
2960 std::ptr::write(
2961 &mut (*ptr).header,
2962 crate::v2::heap_header::HeapHeader::new(
2963 crate::v2::heap_header::HEAP_KIND_V2_TRAIT_OBJECT,
2964 ),
2965 );
2966 std::ptr::write(&mut (*ptr).value, value);
2967 std::ptr::write(&mut (*ptr).vtable, vtable);
2968 }
2969 ptr
2970 }
2971
2972 /// Wave 2 Agent E (2026-05-14): v2-raw raw-pointer deallocator.
2973 ///
2974 /// Runs `drop_in_place` on the inner `Arc<TypedObjectStorage>` value
2975 /// field and `Arc<VTable>` vtable field (retires one strong-count
2976 /// share on each via standard Rust `Arc::drop`), then deallocates
2977 /// the struct's heap memory via `Layout::new::<Self>()`.
2978 ///
2979 /// Mirrors the `TypedObjectStorage::_drop` precedent.
2980 ///
2981 /// # Safety
2982 /// `ptr` must point to a live `TraitObjectStorage` allocated via
2983 /// `Self::_new` with no remaining references. Must not be called
2984 /// more than once on the same pointer; must not be called on
2985 /// `Arc<TraitObjectStorage>`-allocated instances (those run
2986 /// through Rust's `Arc` drop machinery + the auto-derived shape).
2987 pub unsafe fn _drop(ptr: *mut Self) {
2988 unsafe {
2989 // Wave 2 Round 4 D4 ckpt-3 (2026-05-14): `value: *const
2990 // TypedObjectStorage` (v2-raw shape) is released via
2991 // `TypedObjectStorage::release_elem` (HeapElement trait —
2992 // calls `v2_release` on the inner HeapHeader; on refcount=0
2993 // the inner `TypedObjectStorage::_drop` runs the per-field
2994 // heap-mask walk + deallocates). The `vtable: Arc<VTable>`
2995 // field is retired via `drop_in_place` (standard Arc::drop).
2996 // The `header` field is POD — no Drop work owed.
2997 use crate::v2::heap_element::HeapElement;
2998 let inner_ptr = (*ptr).value;
2999 if !inner_ptr.is_null() {
3000 TypedObjectStorage::release_elem(inner_ptr);
3001 }
3002 std::ptr::drop_in_place(&mut (*ptr).vtable);
3003 // Deallocate the struct's heap memory.
3004 let layout = std::alloc::Layout::new::<Self>();
3005 std::alloc::dealloc(ptr as *mut u8, layout);
3006 }
3007 }
3008
3009 /// Convenience: look up a method by name in the vtable. Returns
3010 /// `None` for an unknown method (the dispatch tier surfaces this
3011 /// as a runtime error — under universal-dyn there is no compile-
3012 /// time `ETO-002` for "method not in trait" since the trait's
3013 /// declared method set is the surface checked at compile time;
3014 /// runtime lookup failures indicate a vtable-construction bug).
3015 #[inline]
3016 pub fn method(&self, name: &str) -> Option<&crate::value::VTableEntry> {
3017 self.vtable.methods.get(name)
3018 }
3019
3020 /// Identity check for the §Q25.C.2 `Self`-arg runtime contract.
3021 /// `Arc::ptr_eq` on vtable Arcs is the tightest comparison; both
3022 /// `TraitObjectStorage` instances must share the same vtable
3023 /// allocation (which happens when both came from the same
3024 /// `(impl Trait for Type)` pair).
3025 #[inline]
3026 pub fn vtable_eq(&self, other: &Self) -> bool {
3027 Arc::ptr_eq(&self.vtable, &other.vtable)
3028 }
3029}
3030
3031// Wave 2 Round 4 D4 ckpt-3 (2026-05-14): manual Send + Sync impls. The raw
3032// pointer `value: *const TypedObjectStorage` makes the struct !Send/!Sync
3033// by default; the safety argument mirrors `Arc<T>`'s — the pointer
3034// targets a heap allocation whose lifecycle is managed by the HeapHeader
3035// refcount (v2_retain/v2_release atomics in `v2/refcount.rs`), and
3036// TypedObjectStorage itself is Send + Sync (its fields are Box<[ValueSlot]>
3037// + Arc<[NativeKind]>, all of which are Send + Sync). Multi-thread
3038// observers share the inner via the same refcount-bumped raw pointer that
3039// the v2-raw carrier ABI uses across thread boundaries (KindedSlot Send +
3040// Sync; `Arc<TraitObjectStorage>` Send + Sync requires this).
3041unsafe impl Send for TraitObjectStorage {}
3042unsafe impl Sync for TraitObjectStorage {}
3043
3044impl Clone for TraitObjectStorage {
3045 /// Per-field clone — the inner value ptr's HeapHeader-at-offset-0
3046 /// refcount is bumped via `v2_retain`; the vtable Arc bumps its
3047 /// strong count by one. Cloning a `TraitObjectStorage` produces a
3048 /// fat-pointer carrier that observes the same underlying TypedObject
3049 /// and dispatches against the same VTable. The cloned struct's
3050 /// `header` is a fresh HeapHeader at refcount=1 (matches `Self::new`'s
3051 /// contract — the embedded header is unused on `Arc<TraitObjectStorage>`
3052 /// instances; it carries lifecycle only for `_new`-allocated raw-
3053 /// pointer instances).
3054 ///
3055 /// **Wave 2 Round 4 D4 ckpt-3 (2026-05-14): inner-value retain shifted
3056 /// from `Arc::clone(&self.value)` to `v2_retain(&(*self.value).header)`**
3057 /// per the `value: *const TypedObjectStorage` inner-field shift.
3058 fn clone(&self) -> Self {
3059 // SAFETY: `self.value` is a `*const TypedObjectStorage` allocated
3060 // via `TypedObjectStorage::_new` (refcount initialized to 1 on
3061 // the HeapHeader at offset 0). The `Clone` impl bumps that
3062 // refcount via `v2_retain` so the cloned struct owns its own
3063 // share, retired at its `_drop` / auto-derived `Drop` via
3064 // `TypedObjectStorage::release_elem(value)`.
3065 if !self.value.is_null() {
3066 unsafe { crate::v2::refcount::v2_retain(&(*self.value).header); }
3067 }
3068 Self {
3069 header: crate::v2::heap_header::HeapHeader::new(
3070 crate::v2::heap_header::HEAP_KIND_V2_TRAIT_OBJECT,
3071 ),
3072 value: self.value,
3073 vtable: Arc::clone(&self.vtable),
3074 }
3075 }
3076}
3077
3078// Wave 2 Agent E (2026-05-14): v2-raw HeapElement impl per ADR-006
3079// §Q25.C.5 amendment + audit §4.3 Obstacle O-3.a resolution. Constrains
3080// `TraitObjectStorage` to the HeapHeader-at-offset-0 v2-raw element-carrier
3081// contract so future call sites can store raw `*const TraitObjectStorage`
3082// bits and dispatch retain/release via the trait.
3083//
3084// The trait dispatches refcount through the on-header refcount via
3085// `v2_release` — distinct from the legacy `Arc<TraitObjectStorage>` path
3086// which dispatches via Rust `Arc::decrement_strong_count`. Per the struct
3087// docstring, both carrier shapes coexist at the struct level during the
3088// Wave 2 dispatch transition; the slot ABI discriminates them by
3089// allocation provenance (call sites that use `_new` and
3090// `from_trait_object_raw` follow the raw-pointer lifecycle; existing
3091// `Arc::new` + `from_trait_object` callers retain Arc-style lifecycle).
3092unsafe impl crate::v2::heap_element::HeapElement for TraitObjectStorage {
3093 unsafe fn release_elem(ptr: *const Self) {
3094 if unsafe { crate::v2::refcount::v2_release(&(*ptr).header) } {
3095 unsafe { Self::_drop(ptr as *mut Self) };
3096 }
3097 }
3098}
3099
3100// ── PriorityQueue storage (Wave 15 W15-priority-queue, 2026-05-10) ──────────
3101
3102/// PriorityQueue storage — i64-priority min-heap.
3103///
3104/// ADR-006 §2.7.18 / Q19 amendment (mirror of §2.7.15 HashSet precedent
3105/// for the cardinality-amendment shape). Storage is a binary min-heap
3106/// laid out in a single `Vec<i64>` over an `Arc<TypedBuffer<i64>>`
3107/// so the buffer-Arc pattern matches the rest of the typed-Arc heap
3108/// family (clone-on-write via `Arc::make_mut`, single atomic refcount
3109/// at slot drop).
3110///
3111/// **i64-priority-only at landing** (per the Wave 15 audit and the
3112/// §2.7.18 Q19 ruling). Heterogeneous-payload priority queues
3113/// (TypedObject-payload, payload-with-comparator-closure) are
3114/// explicitly out-of-scope; the playbook called out the i64-priority-
3115/// only design as the simpler valid path, and the smoke target
3116/// (`pq.push(3); pq.push(1); pq.push(2); pq.pop() == 1`) is exercised
3117/// end-to-end on this shape. A typed-payload rebuild (`PriorityQueue
3118/// <T, K>` with key-extractor and arbitrary `T` payloads) is a future
3119/// Phase-2c amendment with measurement.
3120///
3121/// The heap invariant is "min-heap": the minimum priority sits at
3122/// index 0 (`peek()` / `pop()` return it). Standard
3123/// sift-up-on-push / sift-down-on-pop maintenance, `O(log n)` per
3124/// push/pop.
3125#[derive(Debug)]
3126pub struct PriorityQueueData {
3127 /// Heap-ordered i64 priorities. Index 0 is the current min.
3128 /// Backed by an `Arc<Vec<i64>>` so a HeapValue clone is a single
3129 /// atomic refcount bump and `Arc::make_mut` is the canonical
3130 /// clone-on-write entry per the W13-hashmap-mutation precedent.
3131 ///
3132 /// Storage shape: `Arc<Vec<i64>>` post-V3-S5 ckpt-5-prime²a
3133 /// (Migration shape (a) per supervisor 2026-05-15 ratification —
3134 /// `TypedBuffer<T>` wrapper layer retired wholesale at ckpt-4;
3135 /// `Arc<Vec<T>>` is the smallest delta preserving `Arc::make_mut`
3136 /// clone-on-write semantics).
3137 pub heap: Arc<Vec<i64>>,
3138}
3139
3140impl PriorityQueueData {
3141 /// Build an empty PriorityQueueData with no entries.
3142 pub fn new() -> Self {
3143 Self {
3144 heap: Arc::new(Vec::new()),
3145 }
3146 }
3147
3148 /// Number of entries.
3149 #[inline]
3150 pub fn len(&self) -> usize {
3151 self.heap.len()
3152 }
3153
3154 /// Whether the queue is empty.
3155 #[inline]
3156 pub fn is_empty(&self) -> bool {
3157 self.heap.is_empty()
3158 }
3159
3160 /// Peek at the minimum (root) without removing it. Returns `None`
3161 /// for an empty queue.
3162 pub fn peek(&self) -> Option<i64> {
3163 self.heap.first().copied()
3164 }
3165
3166 /// Push a value, restoring the min-heap invariant via sift-up.
3167 /// Mirror of W13-hashmap-mutation `insert`: `Arc::make_mut`
3168 /// clone-on-write over the inner `Arc<Vec<i64>>`.
3169 pub fn push(&mut self, value: i64) {
3170 let buf = Arc::make_mut(&mut self.heap);
3171 buf.push(value);
3172 let last = buf.len() - 1;
3173 sift_up(buf, last);
3174 }
3175
3176 /// Pop the minimum value, restoring the min-heap invariant via
3177 /// sift-down. Returns `None` for an empty queue. Mirror of
3178 /// W13-hashmap-mutation `remove`: `Arc::make_mut` clone-on-write.
3179 pub fn pop(&mut self) -> Option<i64> {
3180 let buf = Arc::make_mut(&mut self.heap);
3181 if buf.is_empty() {
3182 return None;
3183 }
3184 let last = buf.len() - 1;
3185 buf.swap(0, last);
3186 let min = buf.pop();
3187 if !buf.is_empty() {
3188 sift_down(buf, 0);
3189 }
3190 min
3191 }
3192
3193 /// Return the heap contents as a flat `Vec<i64>` in heap-array
3194 /// order (NOT sorted). Used for the `toArray` method's `Vec<int>`
3195 /// projection; for the sorted form see `to_sorted_vec`.
3196 pub fn to_vec(&self) -> Vec<i64> {
3197 (*self.heap).clone()
3198 }
3199
3200 /// Return the heap contents as a sorted `Vec<i64>` (ascending —
3201 /// pop-order). Used for the `toSortedArray` method.
3202 pub fn to_sorted_vec(&self) -> Vec<i64> {
3203 let mut v: Vec<i64> = (*self.heap).clone();
3204 v.sort_unstable();
3205 v
3206 }
3207}
3208
3209impl Default for PriorityQueueData {
3210 fn default() -> Self {
3211 Self::new()
3212 }
3213}
3214
3215impl Clone for DequeData {
3216 fn clone(&self) -> Self {
3217 // Per-element `Arc<HeapValue>` clone bumps each element's strong-
3218 // count share; the resulting `VecDeque` is structurally
3219 // independent of the source. Mirror of `HashSetData::clone`'s
3220 // `Arc::clone(&keys)` shape but without the buffer-Arc indirection
3221 // (the per-element `Arc<HeapValue>` already provides the share).
3222 Self {
3223 items: self.items.iter().map(Arc::clone).collect(),
3224 }
3225 }
3226}
3227
3228impl Clone for PriorityQueueData {
3229 fn clone(&self) -> Self {
3230 Self {
3231 heap: Arc::clone(&self.heap),
3232 }
3233 }
3234}
3235
3236/// Sift up: restore the min-heap invariant after a push at index `i`
3237/// by walking parent links upward, swapping while the child is less
3238/// than its parent.
3239#[inline]
3240fn sift_up(data: &mut [i64], mut i: usize) {
3241 while i > 0 {
3242 let parent = (i - 1) / 2;
3243 if data[i] < data[parent] {
3244 data.swap(i, parent);
3245 i = parent;
3246 } else {
3247 break;
3248 }
3249 }
3250}
3251
3252/// Sift down: restore the min-heap invariant after a pop-replacement
3253/// at index `i` by walking down the smaller child link, swapping
3254/// while a child is less than the current node.
3255#[inline]
3256fn sift_down(data: &mut [i64], mut i: usize) {
3257 let n = data.len();
3258 loop {
3259 let left = 2 * i + 1;
3260 let right = 2 * i + 2;
3261 let mut smallest = i;
3262 if left < n && data[left] < data[smallest] {
3263 smallest = left;
3264 }
3265 if right < n && data[right] < data[smallest] {
3266 smallest = right;
3267 }
3268 if smallest == i {
3269 break;
3270 }
3271 data.swap(i, smallest);
3272 i = smallest;
3273 }
3274}
3275
3276// ── Range storage (W15-range, ADR-006 §2.7.23 / Q24, 2026-05-10) ────────────
3277
3278/// Range value carrier — an inclusive-or-exclusive integer interval with
3279/// step. Built by `MakeRange` from the surface syntax `start..end` (exclusive)
3280/// and `start..=end` (inclusive); produced as a typed `Arc<RangeData>` slot
3281/// labeled `NativeKind::Ptr(HeapKind::Range)`.
3282///
3283/// **Distinct from `IteratorState`.** Range is a value with identity
3284/// (`r.start`, `r.end`, `r.contains(x)`, `print(r)` -> `0..10`) — an
3285/// `IteratorState` is a stateful pipeline with a cursor. The `.iter()`
3286/// receiver method on Range converts a `RangeData` into a fresh
3287/// `IteratorState` with `IteratorSource::Range { start, end_exclusive,
3288/// step }`, where `end_exclusive` is `end + step` for inclusive ranges
3289/// (so `0..=10` step 1 produces values 0..11). `IteratorSource::Range`
3290/// already models the post-conversion shape (W13-iterator-state, ADR-006
3291/// §2.7.16); `RangeData` is the pre-iter receiver value.
3292///
3293/// **Bounds storage.** Today only `i64` integer ranges are representable.
3294/// The `Option<i64>` shape used by the deleted pre-bulldozer Range payload
3295/// (open ranges `..end`, `start..`, `..`) is deliberately NOT modeled here:
3296/// the surface syntax for open ranges still compiles via `op_make_range`
3297/// pushing a `PushNull` for the missing side, but the SURFACE handler
3298/// rejects them per the playbook's surface-and-stop discipline (open
3299/// ranges need an iterator-tier semantic for `for i in 0..` infinite
3300/// loops which is its own ADR follow-up). `step` is always positive —
3301/// matching the pre-strict-typing `0..n` Rust-shape semantics.
3302#[derive(Debug, Clone)]
3303pub struct RangeData {
3304 /// Inclusive lower bound.
3305 pub start: i64,
3306 /// Upper bound. When `inclusive == true`, the value `end` itself is
3307 /// reachable; when `inclusive == false`, `end` is exclusive (the
3308 /// surface-syntax `start..end` shape).
3309 pub end: i64,
3310 /// Per-iteration increment. Always positive; defaults to 1 from the
3311 /// `MakeRange` opcode (the surface syntax has no step suffix today).
3312 pub step: i64,
3313 /// Whether the upper bound is reachable (`start..=end` shape).
3314 pub inclusive: bool,
3315}
3316
3317impl RangeData {
3318 /// Construct a fresh range with the given bounds and step.
3319 #[inline]
3320 pub fn new(start: i64, end: i64, step: i64, inclusive: bool) -> Self {
3321 Self {
3322 start,
3323 end,
3324 step,
3325 inclusive,
3326 }
3327 }
3328
3329 /// Construct an exclusive range `start..end` with step 1 (matching
3330 /// the surface-syntax `0..n` shape).
3331 #[inline]
3332 pub fn exclusive(start: i64, end: i64) -> Self {
3333 Self::new(start, end, 1, false)
3334 }
3335
3336 /// Construct an inclusive range `start..=end` with step 1.
3337 #[inline]
3338 pub fn inclusive(start: i64, end: i64) -> Self {
3339 Self::new(start, end, 1, true)
3340 }
3341
3342 /// Effective exclusive end — `end + step` for inclusive ranges,
3343 /// `end` for exclusive ranges. Matches the upper bound used by
3344 /// `IteratorSource::Range`'s `end` field (which is exclusive by
3345 /// W13-iterator-state's contract).
3346 #[inline]
3347 pub fn end_exclusive(&self) -> i64 {
3348 if self.inclusive {
3349 self.end.saturating_add(self.step)
3350 } else {
3351 self.end
3352 }
3353 }
3354
3355 /// Element count. Mirrors `IteratorSource::Range::len` so a range
3356 /// and its post-`.iter()` IteratorState report the same count. For
3357 /// non-positive step or an empty interval, returns 0.
3358 #[inline]
3359 pub fn len(&self) -> usize {
3360 let end = self.end_exclusive();
3361 if self.step <= 0 || end <= self.start {
3362 return 0;
3363 }
3364 let span = (end - self.start) as u64;
3365 let step = self.step as u64;
3366 ((span + step - 1) / step) as usize
3367 }
3368
3369 /// Whether the range yields zero elements.
3370 #[inline]
3371 pub fn is_empty(&self) -> bool {
3372 self.len() == 0
3373 }
3374
3375 /// Whether `value` falls within the range. The check is bound-aware
3376 /// (inclusive vs exclusive end) but does NOT enforce step alignment
3377 /// — `(0..10).contains(5)` is true regardless of step. This matches
3378 /// the pre-bulldozer surface-syntax shape: `range.contains` is a
3379 /// bound test, not a "would .iter() yield this exact value" probe.
3380 #[inline]
3381 pub fn contains(&self, value: i64) -> bool {
3382 if value < self.start {
3383 return false;
3384 }
3385 if self.inclusive {
3386 value <= self.end
3387 } else {
3388 value < self.end
3389 }
3390 }
3391
3392 /// Materialize the range into a `Vec<i64>` of every yielded value
3393 /// (mirror of `.iter().collect()` for the pre-bulldozer
3394 /// `range.toArray()` method shape). Empty range -> empty vec.
3395 pub fn to_vec_i64(&self) -> Vec<i64> {
3396 let n = self.len();
3397 let mut out = Vec::with_capacity(n);
3398 let mut v = self.start;
3399 for _ in 0..n {
3400 out.push(v);
3401 v += self.step;
3402 }
3403 out
3404 }
3405}
3406
3407// ── TaskGroup storage (ADR-006 §2.3) ────────────────────────────────────────
3408
3409/// Task-group payload. Extracted from the inline
3410/// `HeapValue::TaskGroup { kind, task_ids }` struct variant per ADR-006 §2.3
3411/// so `HeapValue::TaskGroup` becomes a single-tuple `Arc<T>` payload like
3412/// every other ADR-006 §2.3 heap arm.
3413///
3414/// The struct preserves the `kind` discriminant and `task_ids` list verbatim
3415/// — clone semantics live on the enclosing `Arc<TaskGroupData>` (one atomic
3416/// refcount bump). Phase 1.B migrates the cascade pattern-match sites
3417/// (`shape-vm::executor::async_ops`, `shape-jit::ffi::async_ops`,
3418/// `shape-runtime::wire_conversion`, ...) from struct-variant destructuring
3419/// to `task_group.kind` / `task_group.task_ids` field reads.
3420#[derive(Debug, Clone)]
3421pub struct TaskGroupData {
3422 pub kind: u8,
3423 pub task_ids: Vec<u64>,
3424}
3425
3426// ── TypedObject storage (ADR-006 §2.3 / §2.5) ───────────────────────────────
3427
3428/// Schema-keyed object storage. Extracted from the inline
3429/// `HeapValue::TypedObject { schema_id, slots, heap_mask }` struct
3430/// variant per ADR-006 §2.3, so that:
3431///
3432/// 1. `HeapValue::TypedObject` becomes `HeapValue::TypedObject(Arc<TypedObjectStorage>)`
3433/// — a typed `Arc<T>` payload like every other ADR-006 §2.3 heap arm.
3434/// 2. The `Drop` impl (Step 5) lives on `TypedObjectStorage` and dispatches
3435/// per-field on `NativeKind` from the embedded `field_kinds: Arc<[NativeKind]>`
3436/// table — no schema-registry probe and no cross-crate function-pointer
3437/// hook at drop time.
3438///
3439/// Field invariants (ADR-006 §2.3):
3440///
3441/// - `schema_id` is the registry key for the TypeSchema. Kept for wire /
3442/// snapshot round-trip and downstream schema-aware code (printing,
3443/// marshal); not consulted at drop time.
3444/// - `slots` is a per-field 8-byte storage array. Field at index `i`
3445/// stores its bits per the schema's `FieldType` for that field.
3446/// - `heap_mask` has bit `i` set iff slot `i` holds a heap pointer
3447/// (`Arc<T>` raw pointer per ADR-006 §2.4). Bits beyond `slots.len()`
3448/// must be zero.
3449/// - `field_kinds` is an `Arc<[NativeKind]>` of length `slots.len()`,
3450/// one entry per field, carrying the proven `NativeKind` for that
3451/// field's slot bits. The Arc payload is **shared per-schema**: the
3452/// construction path (in `shape-runtime`) maps `schema_id ⇒ Arc<[NativeKind]>`
3453/// once per schema (one HashMap probe at the first construction; cached
3454/// for subsequent constructions) and clones the cached Arc into each
3455/// instance — so 1M Customer-objects of the same shape share one
3456/// `[NativeKind]` allocation. Drop is then constant-time per slot
3457/// without any cross-crate registry call.
3458///
3459/// Why the `Arc<[NativeKind]>` (Option B' per supervisor ruling, ADR-006 §17):
3460///
3461/// - **Option B** (per-instance `Box<[NativeKind]>`) was rejected: it
3462/// duplicates the same NativeKind sequence across every instance of a
3463/// schema (1M × 8 fields = 16MB cumulative duplication).
3464/// - **Option C** (function-pointer hook in shape-value installed by
3465/// shape-runtime) was rejected: it adds a cross-crate runtime hook for
3466/// metadata that's already known at construction time.
3467/// - **Option B'** (this one) does the lookup once at construction (where
3468/// the schema is in scope) and shares the result via Arc — 8-byte
3469/// pointer per instance, single payload allocation per schema, no
3470/// probe at drop. Per Q8 spirit: the schema lookup happens, but it's
3471/// profile-driven *preempted* to construction time and cached.
3472///
3473/// `TypedObjectStorage` is `pub` with `pub` fields so the existing
3474/// destructuring call sites can migrate by reading
3475/// `storage.schema_id` / `storage.slots` / `storage.heap_mask`. The
3476/// struct is intentionally not `Clone` — clone semantics belong to the
3477/// enclosing `Arc<TypedObjectStorage>` (one atomic refcount bump).
3478///
3479/// **Wave 2 Agent D1 (2026-05-14): HeapHeader-equipped shape change.**
3480/// Per audit §4.3 Obstacle O-3.a resolution + ADR-006 §2.3 amendment, the
3481/// struct now carries a `HeapHeader` at offset 0 (`#[repr(C)]`) so v2-raw
3482/// raw-pointer allocations (`_new` / `_drop` + `impl HeapElement`) can
3483/// dispatch refcount on the header via `v2_retain` / `v2_release`. Existing
3484/// `Arc<TypedObjectStorage>` construction sites continue to work
3485/// unchanged — `Arc::new(TypedObjectStorage::new(...))` produces a Rust
3486/// `Arc`-wrapped instance whose embedded header sits at refcount=1 unused;
3487/// the dispatch arms continue to use `Arc::increment_strong_count` /
3488/// `Arc::decrement_strong_count` on those bits. The new `_new`-allocated
3489/// raw-pointer bits use the header's refcount via the `HeapElement` trait.
3490/// Agent D2 (Wave 2 Round 2) migrates the 18 production construction sites
3491/// to the raw-pointer carrier; Agent E (Wave 2 Round 2) consumes the same
3492/// shape change for `TraitObjectStorage`. Until that migration completes,
3493/// both carriers coexist at the struct level; the slot-ABI discriminator
3494/// (`NativeKind::Ptr(HeapKind::TypedObject)`) is unchanged.
3495#[repr(C)]
3496#[derive(Debug)]
3497pub struct TypedObjectStorage {
3498 /// v2-raw HeapHeader at offset 0 (8 bytes). Refcount/kind/flags.
3499 /// Initialized to `HeapHeader::new(HEAP_KIND_V2_TYPED_OBJECT)` by
3500 /// `_new`; for `Arc`-wrapped instances allocated via
3501 /// `TypedObjectStorage::new` the header sits at refcount=1 unused
3502 /// (the enclosing `Arc` owns the lifecycle). See struct docstring.
3503 pub header: crate::v2::heap_header::HeapHeader,
3504 /// Registry key for the TypeSchema describing each slot's `FieldType`.
3505 pub schema_id: u64,
3506 /// Per-field 8-byte storage. Length matches the schema's field count.
3507 pub slots: Box<[crate::slot::ValueSlot]>,
3508 /// Bit `i` set ⇔ slot `i` holds a heap pointer that participates in
3509 /// Arc refcount discipline. Bits beyond `slots.len()` must be zero.
3510 pub heap_mask: u64,
3511 /// Per-field `NativeKind` table — same length as `slots`. **Shared
3512 /// per-schema** via `Arc`: every instance of the same schema clones
3513 /// the same payload (one atomic refcount bump per construction).
3514 /// Consulted by `Drop` to dispatch per-slot `Arc::decrement_strong_count`
3515 /// without any schema-registry probe.
3516 pub field_kinds: std::sync::Arc<[crate::native_kind::NativeKind]>,
3517}
3518
3519impl TypedObjectStorage {
3520 /// Construct a new `TypedObjectStorage`.
3521 ///
3522 /// Construction-side contract (callers in `shape-runtime`):
3523 ///
3524 /// 1. `slots.len() == field_kinds.len()` — one kind per slot.
3525 /// 2. For each bit `i` set in `heap_mask`, `field_kinds[i]` must be
3526 /// a heap-pointer kind (`NativeKind::String` or
3527 /// `NativeKind::Ptr(_)`) and the slot's `u64` must be the raw
3528 /// pointer of an `Arc::into_raw::<T>` for the matching `T`. Drop
3529 /// relies on this for soundness.
3530 /// 3. `field_kinds` should be the per-schema cached `Arc<[NativeKind]>`
3531 /// (callers maintain a `schema_id ⇒ Arc<[NativeKind]>` cache to
3532 /// avoid per-instance allocation).
3533 ///
3534 /// Returns the storage by value; the canonical wrap is
3535 /// `Arc::new(TypedObjectStorage::new(...))` immediately followed by
3536 /// `HeapValue::TypedObject(arc)` or `ValueSlot::from_typed_object(arc)`.
3537 #[inline]
3538 pub fn new(
3539 schema_id: u64,
3540 slots: Box<[crate::slot::ValueSlot]>,
3541 heap_mask: u64,
3542 field_kinds: std::sync::Arc<[crate::native_kind::NativeKind]>,
3543 ) -> Self {
3544 debug_assert_eq!(
3545 slots.len(),
3546 field_kinds.len(),
3547 "TypedObjectStorage::new: slots/field_kinds length mismatch \
3548 (slots={}, field_kinds={}) — every slot must have a proven NativeKind",
3549 slots.len(),
3550 field_kinds.len(),
3551 );
3552 Self {
3553 header: crate::v2::heap_header::HeapHeader::new(
3554 crate::v2::heap_header::HEAP_KIND_V2_TYPED_OBJECT,
3555 ),
3556 schema_id,
3557 slots,
3558 heap_mask,
3559 field_kinds,
3560 }
3561 }
3562
3563 /// Wave 2 Agent D1 (2026-05-14): v2-raw raw-pointer allocator.
3564 ///
3565 /// Allocates a new `TypedObjectStorage` on the heap and returns a raw
3566 /// pointer with refcount initialized to 1. Mirrors the `DecimalObj::new`
3567 /// / `StringObj::new` precedents at `crates/shape-value/src/v2/` —
3568 /// `#[repr(C)]` struct with `HeapHeader` at offset 0; refcount discipline
3569 /// goes through `v2_retain` / `v2_release` via the `HeapElement` trait.
3570 ///
3571 /// Construction-side contract: same as `new()` — `slots.len() ==
3572 /// field_kinds.len()`; heap-mask bits correspond to heap-kinded slots
3573 /// whose bits are `Arc::into_raw::<T>` for the matching `T`. The raw-
3574 /// pointer carrier owns one strong-count share for every heap-kinded
3575 /// slot it carries, retired by `_drop` at refcount=0.
3576 ///
3577 /// Callers (Wave 2 Agent D2, Round 2 cascade): construct via
3578 /// `TypedObjectStorage::_new(...)` and store the pointer in
3579 /// `ValueSlot::from_typed_object_raw(ptr)`. Drop runs at refcount=0
3580 /// via the `HeapElement::release_elem` trait method, NOT via Rust
3581 /// `Arc::drop` (the `Arc<TypedObjectStorage>` path is the legacy
3582 /// transitional carrier; both coexist at the struct level per the
3583 /// struct docstring).
3584 pub fn _new(
3585 schema_id: u64,
3586 slots: Box<[crate::slot::ValueSlot]>,
3587 heap_mask: u64,
3588 field_kinds: std::sync::Arc<[crate::native_kind::NativeKind]>,
3589 ) -> *mut Self {
3590 debug_assert_eq!(
3591 slots.len(),
3592 field_kinds.len(),
3593 "TypedObjectStorage::_new: slots/field_kinds length mismatch \
3594 (slots={}, field_kinds={}) — every slot must have a proven NativeKind",
3595 slots.len(),
3596 field_kinds.len(),
3597 );
3598 let layout = std::alloc::Layout::new::<Self>();
3599 let ptr = unsafe { std::alloc::alloc(layout) as *mut Self };
3600 assert!(!ptr.is_null(), "allocation failed for TypedObjectStorage");
3601 unsafe {
3602 // SAFETY: `ptr` points to fresh, uninitialized memory of size
3603 // `Layout::new::<Self>()`. We write every field via `ptr::write`
3604 // to avoid running drop on uninitialized bytes (the existing
3605 // memory contains garbage, never a valid prior `Self`).
3606 std::ptr::write(
3607 &mut (*ptr).header,
3608 crate::v2::heap_header::HeapHeader::new(
3609 crate::v2::heap_header::HEAP_KIND_V2_TYPED_OBJECT,
3610 ),
3611 );
3612 std::ptr::write(&mut (*ptr).schema_id, schema_id);
3613 std::ptr::write(&mut (*ptr).slots, slots);
3614 std::ptr::write(&mut (*ptr).heap_mask, heap_mask);
3615 std::ptr::write(&mut (*ptr).field_kinds, field_kinds);
3616 }
3617 ptr
3618 }
3619
3620 /// Wave 2 Agent D1 (2026-05-14): v2-raw raw-pointer deallocator.
3621 ///
3622 /// Runs the per-field heap-mask walk (releasing one strong-count share
3623 /// per heap-kinded slot via `Arc::decrement_strong_count`) and then
3624 /// deallocates the struct's heap memory via `Layout::new::<Self>()`.
3625 /// The field walk delegates to `drop_fields` so the same logic powers
3626 /// both the legacy `impl Drop for TypedObjectStorage` path (used by
3627 /// `Arc<TypedObjectStorage>` instances) and this raw-pointer path.
3628 ///
3629 /// Mirrors the `DecimalObj::drop` / `StringObj::drop` precedents.
3630 ///
3631 /// # Safety
3632 /// `ptr` must point to a live `TypedObjectStorage` allocated via
3633 /// `Self::_new` with no remaining references. Must not be called more
3634 /// than once on the same pointer; must not be called on
3635 /// `Arc<TypedObjectStorage>`-allocated instances (those run through
3636 /// Rust's `Arc` drop machinery + `impl Drop for TypedObjectStorage`).
3637 pub unsafe fn _drop(ptr: *mut Self) {
3638 unsafe {
3639 // Run the per-field heap-mask walk, retiring one Arc share per
3640 // heap-kinded slot. Same logic that `impl Drop` runs for the
3641 // legacy `Arc<TypedObjectStorage>` path.
3642 (*ptr).drop_fields();
3643 // Drop the in-place `Box<[ValueSlot]>` and `Arc<[NativeKind]>`
3644 // payloads so their allocations are freed. The `header`,
3645 // `schema_id`, and `heap_mask` fields are POD (`Copy` or
3646 // primitive) — no Drop work owed.
3647 std::ptr::drop_in_place(&mut (*ptr).slots);
3648 std::ptr::drop_in_place(&mut (*ptr).field_kinds);
3649 // Deallocate the struct's heap memory.
3650 let layout = std::alloc::Layout::new::<Self>();
3651 std::alloc::dealloc(ptr as *mut u8, layout);
3652 }
3653 }
3654
3655 /// Wave 2 Agent D1 (2026-05-14): shared per-field heap-mask walk.
3656 ///
3657 /// Walks `heap_mask`, dispatches per-slot on `field_kinds[i]`, and
3658 /// retires one strong-count share per heap-kinded slot via
3659 /// `Arc::decrement_strong_count::<T>` for the matching `T`. Same
3660 /// dispatch as `impl Drop for TypedObjectStorage` (and same as the
3661 /// 4-table-lockstep arms in `kinded_slot.rs::drop` /
3662 /// `vm_impl/stack.rs::drop_with_kind` / `closure_layout.rs::
3663 /// SharedCell::drop`). Called by both `impl Drop` (legacy
3664 /// `Arc<TypedObjectStorage>` path) and `_drop` (raw-pointer path).
3665 ///
3666 /// # Safety
3667 /// Caller must guarantee `self` is in a live state (slots /
3668 /// field_kinds / heap_mask all valid per the `new` / `_new`
3669 /// construction-side contract). Must run at most once per instance.
3670 unsafe fn drop_fields(&mut self) {
3671 use crate::heap_value::HeapKind;
3672 use crate::native_kind::NativeKind;
3673
3674 // Defensive: if construction left a length mismatch (debug_assert
3675 // catches it earlier), drop only the prefix where both bookkeeping
3676 // structures agree. Better a leak than UB.
3677 let n = self.slots.len().min(self.field_kinds.len());
3678 for i in 0..n {
3679 // heap_mask is u64; bits beyond 63 cannot be addressed today.
3680 if i >= 64 {
3681 break;
3682 }
3683 if (self.heap_mask >> i) & 1 == 0 {
3684 continue;
3685 }
3686 let bits = self.slots[i].raw();
3687 if bits == 0 {
3688 continue;
3689 }
3690 // SAFETY (each arm): the construction-side contract guarantees
3691 // that for every set heap_mask bit, the slot's bits are the
3692 // result of `Arc::into_raw::<T>` where `T` matches `field_kinds[i]`.
3693 // We reclaim exactly one strong-count share per slot via
3694 // `Arc::decrement_strong_count::<T>` and then never look at the
3695 // bits again.
3696 unsafe {
3697 match self.field_kinds[i] {
3698 NativeKind::String => {
3699 std::sync::Arc::decrement_strong_count(bits as *const String);
3700 }
3701 // Wave 2 Agent B (ADR-006 §2.7.5 amendment, 2026-05-14):
3702 // A TypedObject field of kind `NativeKind::StringV2` /
3703 // `NativeKind::DecimalV2` holds slot bits = `ptr as u64`
3704 // where `ptr: *const StringObj` / `*const DecimalObj`
3705 // — v2-raw carrier shape per the §H.4 H-c decision.
3706 // Refcount discipline goes through `release_elem`
3707 // (HeapElement trait — calls `v2_release` against the
3708 // HeapHeader at offset 0; on refcount=0 the carrier-side
3709 // `drop` deallocates the repr(C) 24-byte struct). NOT
3710 // `Arc::decrement_strong_count` — these are manually-
3711 // allocated carriers, not `Arc<T>` allocations.
3712 NativeKind::StringV2 => {
3713 use crate::v2::heap_element::HeapElement;
3714 crate::v2::string_obj::StringObj::release_elem(
3715 bits as *const crate::v2::string_obj::StringObj,
3716 );
3717 }
3718 NativeKind::DecimalV2 => {
3719 use crate::v2::heap_element::HeapElement;
3720 crate::v2::decimal_obj::DecimalObj::release_elem(
3721 bits as *const crate::v2::decimal_obj::DecimalObj,
3722 );
3723 }
3724 NativeKind::Ptr(hk) => match hk {
3725 HeapKind::String => {
3726 std::sync::Arc::decrement_strong_count(bits as *const String);
3727 }
3728 // r5c-2-β-δ-(α) (2026-05-20): `HeapKind::TypedArray`
3729 // dispatch arm RE-INSTATED. The V3-S5 ckpt-5-prime
3730 // retirement claimed "no live slot bits carry this
3731 // kind", but a struct with an `Array<T>` field is
3732 // exactly such a site — `field_tag_to_native_kind`
3733 // maps `FIELD_TAG_ARRAY` → `Ptr(HeapKind::TypedArray)`,
3734 // so the field slot stores a v2-raw `*mut TypedArray<T>`
3735 // carrier (HeapHeader at offset 0, refcount). When the
3736 // enclosing `TypedObjectStorage` is freed this walk
3737 // must release the field's share. `release_v2_typed_array`
3738 // retires one refcount share and, on the last share,
3739 // frees the array via the stamped-element-type
3740 // `drop_array` / `drop_array_heap`. Mirror of the
3741 // `TypedObject` field arm below (4-table lockstep,
3742 // ADR-006 §2.3 / §2.7.7).
3743 HeapKind::TypedArray => {
3744 crate::v2::typed_array::release_v2_typed_array(bits as *mut u8);
3745 }
3746 // Wave 2 Agent D4 ckpt-2 (ADR-006 §2.3 / §2.7.5
3747 // amendment, 2026-05-14): a `TypedObject` field of
3748 // kind `NativeKind::Ptr(HeapKind::TypedObject)`
3749 // holds slot bits = `ptr as u64` where
3750 // `ptr: *const TypedObjectStorage` (v2-raw carrier
3751 // per Agent D1's `_new` /
3752 // `impl HeapElement for TypedObjectStorage`).
3753 // Refcount discipline goes through `release_elem`
3754 // (HeapElement trait — calls `v2_release` against
3755 // the HeapHeader at offset 0; on refcount=0 the
3756 // carrier-side `_drop` runs the per-field
3757 // heap-mask walk and deallocates the `repr(C)`
3758 // struct). Mirror of the §2.7.5 StringV2 /
3759 // DecimalV2 release arms above (Agent B precedent).
3760 HeapKind::TypedObject => {
3761 use crate::v2::heap_element::HeapElement;
3762 TypedObjectStorage::release_elem(
3763 bits as *const TypedObjectStorage,
3764 );
3765 }
3766 HeapKind::HashMap => {
3767 // Wave 2 Round 3b C2-joint ckpt-2 (2026-05-14):
3768 // bits are `Arc::into_raw(Arc<HashMapKindedRef>)`
3769 // per ADR-006 §2.7.24 Q25.B SUPERSEDED carrier
3770 // shape. Release dispatches outer Arc decrement;
3771 // enum Drop chains to per-V `Arc<HashMapData<V>>`
3772 // release.
3773 std::sync::Arc::decrement_strong_count(
3774 bits as *const HashMapKindedRef,
3775 );
3776 }
3777 HeapKind::HashSet => {
3778 std::sync::Arc::decrement_strong_count(bits as *const HashSetData);
3779 }
3780 HeapKind::Deque => {
3781 std::sync::Arc::decrement_strong_count(bits as *const DequeData);
3782 }
3783 HeapKind::Channel => {
3784 std::sync::Arc::decrement_strong_count(bits as *const ChannelData);
3785 }
3786 HeapKind::Mutex => {
3787 std::sync::Arc::decrement_strong_count(bits as *const MutexData);
3788 }
3789 HeapKind::Atomic => {
3790 std::sync::Arc::decrement_strong_count(bits as *const AtomicData);
3791 }
3792 HeapKind::Lazy => {
3793 std::sync::Arc::decrement_strong_count(bits as *const LazyData);
3794 }
3795 // Wave 2 Agent D4 ckpt-2 (ADR-006 §2.7.24 /
3796 // Q25.C.5 + E close 2026-05-14): TraitObject
3797 // release via `HeapElement::release_elem` +
3798 // carrier-side `_drop` (per Agent E's
3799 // `impl HeapElement for TraitObjectStorage`).
3800 // Mirror of the TypedObject arm above.
3801 HeapKind::TraitObject => {
3802 use crate::v2::heap_element::HeapElement;
3803 TraitObjectStorage::release_elem(
3804 bits as *const TraitObjectStorage,
3805 );
3806 }
3807 HeapKind::Decimal => {
3808 std::sync::Arc::decrement_strong_count(
3809 bits as *const rust_decimal::Decimal,
3810 );
3811 }
3812 HeapKind::BigInt => {
3813 std::sync::Arc::decrement_strong_count(bits as *const i64);
3814 }
3815 HeapKind::DataTable => {
3816 std::sync::Arc::decrement_strong_count(
3817 bits as *const crate::datatable::DataTable,
3818 );
3819 }
3820 HeapKind::IoHandle => {
3821 std::sync::Arc::decrement_strong_count(bits as *const IoHandleData);
3822 }
3823 HeapKind::NativeView => {
3824 std::sync::Arc::decrement_strong_count(
3825 bits as *const NativeViewData,
3826 );
3827 }
3828 HeapKind::Content => {
3829 std::sync::Arc::decrement_strong_count(
3830 bits as *const crate::content::ContentNode,
3831 );
3832 }
3833 HeapKind::Instant => {
3834 std::sync::Arc::decrement_strong_count(
3835 bits as *const std::time::Instant,
3836 );
3837 }
3838 HeapKind::Temporal => {
3839 std::sync::Arc::decrement_strong_count(bits as *const TemporalData);
3840 }
3841 HeapKind::TableView => {
3842 std::sync::Arc::decrement_strong_count(bits as *const TableViewData);
3843 }
3844 HeapKind::TaskGroup => {
3845 std::sync::Arc::decrement_strong_count(bits as *const TaskGroupData);
3846 }
3847 HeapKind::FilterExpr => {
3848 std::sync::Arc::decrement_strong_count(
3849 bits as *const crate::value::FilterNode,
3850 );
3851 }
3852 HeapKind::Reference => {
3853 std::sync::Arc::decrement_strong_count(
3854 bits as *const crate::reference::RefTarget,
3855 );
3856 }
3857 HeapKind::Iterator => {
3858 std::sync::Arc::decrement_strong_count(
3859 bits as *const crate::iterator_state::IteratorState,
3860 );
3861 }
3862 HeapKind::PriorityQueue => {
3863 std::sync::Arc::decrement_strong_count(
3864 bits as *const PriorityQueueData,
3865 );
3866 }
3867 HeapKind::Range => {
3868 std::sync::Arc::decrement_strong_count(bits as *const RangeData);
3869 }
3870 HeapKind::Result => {
3871 std::sync::Arc::decrement_strong_count(bits as *const ResultData);
3872 }
3873 HeapKind::Option => {
3874 std::sync::Arc::decrement_strong_count(bits as *const OptionData);
3875 }
3876 HeapKind::Closure => {
3877 std::sync::Arc::decrement_strong_count(bits as *const HeapValue);
3878 }
3879 HeapKind::Future => {
3880 // No-op: future-id inline scalar.
3881 }
3882 HeapKind::ModuleFn => {
3883 // No-op: module-fn-id inline scalar.
3884 }
3885 HeapKind::Matrix => {
3886 std::sync::Arc::decrement_strong_count(bits as *const MatrixData);
3887 }
3888 HeapKind::MatrixSlice => {
3889 std::sync::Arc::decrement_strong_count(
3890 bits as *const MatrixSliceData,
3891 );
3892 }
3893 HeapKind::SharedCell => {
3894 std::sync::Arc::decrement_strong_count(
3895 bits as *const crate::v2::closure_layout::SharedCell,
3896 );
3897 }
3898 HeapKind::Char => {
3899 debug_assert!(
3900 false,
3901 "TypedObjectStorage::drop_fields: heap_mask bit {} set with \
3902 inline-scalar kind Char (schema_id={}); \
3903 construction-side soundness violation",
3904 i, self.schema_id
3905 );
3906 }
3907 HeapKind::NativeScalar => {
3908 debug_assert!(
3909 false,
3910 "TypedObjectStorage::drop_fields: NativeScalar kinded carrier \
3911 pending phase-2c kinded redesign (ADR-006 §2.7.4); \
3912 schema_id={}, bit {}",
3913 self.schema_id, i
3914 );
3915 }
3916 },
3917 other => {
3918 debug_assert!(
3919 false,
3920 "TypedObjectStorage::drop_fields: heap_mask bit {} set with \
3921 non-heap NativeKind {:?} (schema_id={}); \
3922 construction-side soundness violation",
3923 i, other, self.schema_id
3924 );
3925 }
3926 }
3927 }
3928 }
3929 }
3930
3931 /// In-place write of slot `idx` through a shared `&TypedObjectStorage`
3932 /// (i.e. through an `Arc<TypedObjectStorage>` with refcount > 1).
3933 /// Returns the prior `(bits, kind)` so the caller can run
3934 /// `drop_with_kind` on the released share. The caller transfers
3935 /// ownership of `new_bits` (one strong-count share for heap kinds) to
3936 /// the slot.
3937 ///
3938 /// This is the Q14 / ADR-006 §2.7.13 in-place write path for
3939 /// `RefTarget::TypedField` projection writes — the receiver `Arc`
3940 /// is shared between the ref carrier and the originating binding,
3941 /// so `Arc::get_mut` / `Arc::make_mut` cannot apply (refcount > 1
3942 /// by construction, and `TypedObjectStorage` is intentionally not
3943 /// `Clone` per the §2.5 documentation). The `Box<[ValueSlot]>`
3944 /// inside the storage is logically owned; the single-word `u64`
3945 /// inside each `ValueSlot` is written atomically (single-word
3946 /// aligned store on every supported architecture).
3947 ///
3948 /// # Safety
3949 ///
3950 /// Callers must guarantee:
3951 ///
3952 /// 1. **Single-threaded write**: the VM is single-threaded, and the
3953 /// refs that drive this path are constrained by the §3.1
3954 /// ref-escape analysis to stay within their originating task
3955 /// (refs cannot cross task boundaries — error B0014
3956 /// `NonSendableAcrossTaskBoundary`). No other thread may hold an
3957 /// `&Arc<TypedObjectStorage>` to the same storage at the same
3958 /// time the write executes.
3959 /// 2. **No aliased `&mut ValueSlot`**: callers must NOT mint a
3960 /// `&mut ValueSlot` to slot `idx` from any path while this write
3961 /// is in flight. The Q14 dispatch in `op_deref_store` /
3962 /// `op_set_index_ref` is the only caller, and it operates on
3963 /// `&TypedObjectStorage` exclusively.
3964 /// 3. **Kind invariance**: `new_kind` must equal
3965 /// `self.field_kinds[idx]`. The Q14 RefTarget carries the
3966 /// projected slot's kind at construction (`MakeFieldRef` sources
3967 /// it from `field_type_tag`); the post-proof `§2.7.5.1` contract
3968 /// forbids mid-life kind changes for typed fields. The caller
3969 /// debug_asserts this before calling.
3970 /// 4. **`heap_mask` bit consistency**: for heap-kinded slots
3971 /// (NativeKind::String or Ptr(HeapKind::_)), the corresponding
3972 /// `heap_mask` bit must already be set per the `TypedObjectStorage::new`
3973 /// construction-side contract, AND the prior bits must be a
3974 /// valid `Arc::into_raw::<T>` for the slot's kind. The returned
3975 /// `prior_bits` is exactly that share; the caller releases it via
3976 /// `drop_with_kind` after running the post-write barrier.
3977 ///
3978 /// Q14 / ADR-006 §2.7.13. Mirror of the `clone_with_kind` /
3979 /// `drop_with_kind` symmetry used by `RefTarget::Local` and
3980 /// `RefTarget::ModuleBinding` writes (`stack_write_kinded` and
3981 /// `module_binding_write_kinded` already encapsulate this pattern
3982 /// for non-projected places; this is the projected-place mirror).
3983 #[inline]
3984 pub unsafe fn write_slot_in_place(
3985 &self,
3986 idx: usize,
3987 new_bits: u64,
3988 ) -> u64 {
3989 debug_assert!(
3990 idx < self.slots.len(),
3991 "TypedObjectStorage::write_slot_in_place: idx {} out of bounds (slots.len = {})",
3992 idx,
3993 self.slots.len(),
3994 );
3995 // SAFETY: see method contract. Single-threaded VM; refs cannot
3996 // escape across task boundaries; no aliased `&mut ValueSlot`
3997 // outstanding by construction; `Box<[ValueSlot]>` is `Sized`-laid-
3998 // out and the slot's `u64` is naturally aligned. We cast through
3999 // `&[ValueSlot]` -> `*const ValueSlot` -> `*mut ValueSlot` to
4000 // perform the single-word write. The slot's `field_kinds[idx]`
4001 // is the kind invariant; the caller already debug_asserted kind
4002 // equality, so the slot's heap-mask bit (if set) still applies
4003 // to the new bits.
4004 let slot_ptr = self.slots.as_ptr().add(idx) as *mut crate::slot::ValueSlot;
4005 let prior = (*slot_ptr).raw();
4006 *slot_ptr = crate::slot::ValueSlot::from_raw(new_bits);
4007 prior
4008 }
4009}
4010
4011impl Drop for TypedObjectStorage {
4012 /// ADR-006 §2.5 + Wave 2 Agent D1 (2026-05-14): delegates to the shared
4013 /// `drop_fields` helper that walks `heap_mask` and dispatches per-slot
4014 /// on `field_kinds[i]`. The same helper powers `_drop` (raw-pointer
4015 /// path) so both the legacy `Arc<TypedObjectStorage>` lifecycle and
4016 /// the v2-raw raw-pointer lifecycle retire heap-slot Arc shares with
4017 /// identical semantics.
4018 ///
4019 /// Soundness contract (must hold by construction; see
4020 /// `TypedObjectStorage::new` / `_new`):
4021 ///
4022 /// - For every `i` where `heap_mask >> i & 1 == 1`, the slot's `u64`
4023 /// bits are the result of `Arc::into_raw::<T>` where `T` matches
4024 /// `field_kinds[i]` (per the per-HeapKind table in `drop_fields`).
4025 /// - `NativeKind::Ptr(HeapKind::{Future, ModuleFn, Char, NativeScalar})`
4026 /// are inline-scalar payloads (no `Arc<T>`); a heap_mask bit set
4027 /// with one of those kinds is a soundness violation surfaced by
4028 /// debug_assert in `drop_fields`.
4029 fn drop(&mut self) {
4030 // SAFETY: `drop_fields` walks the heap_mask + field_kinds arrays
4031 // and retires Arc shares per the construction-side contract. Runs
4032 // exactly once per instance (Rust's Drop machinery enforces this
4033 // for `Arc<TypedObjectStorage>` instances; raw-pointer instances
4034 // route through `_drop` which calls `drop_fields` directly and
4035 // never reaches here).
4036 unsafe { self.drop_fields(); }
4037 }
4038}
4039
4040// Wave 2 Agent D1 (2026-05-14): v2-raw HeapElement impl per ADR-006 §2.3
4041// amendment + audit §4.3 Obstacle O-3.a resolution. Constrains
4042// `TypedObjectStorage` to the HeapHeader-at-offset-0 v2-raw element-carrier
4043// contract so future call sites can store raw `*const TypedObjectStorage`
4044// bits in `TypedArray<*const TypedObjectStorage>` (audit §2.2 / §3.3 / S3
4045// territory) and dispatch per-element retain/release via the trait.
4046//
4047// The trait dispatches refcount through the on-header refcount via
4048// `v2_release` — distinct from the legacy `Arc<TypedObjectStorage>` path
4049// which dispatches via Rust `Arc::decrement_strong_count`. Per the struct
4050// docstring, both carrier shapes coexist at the struct level during the
4051// Wave 2 dispatch transition; the slot ABI discriminates them by
4052// allocation provenance (Agent D2's call sites use `_new` and the raw-
4053// pointer slot constructor; existing call sites use `Arc::new` and the
4054// legacy slot constructor).
4055unsafe impl crate::v2::heap_element::HeapElement for TypedObjectStorage {
4056 unsafe fn release_elem(ptr: *const Self) {
4057 if unsafe { crate::v2::refcount::v2_release(&(*ptr).header) } {
4058 unsafe { Self::_drop(ptr as *mut Self) };
4059 }
4060 }
4061}
4062
4063// ── TypedArray buckets (DELETED — V3-S5 ckpt-1, 2026-05-15) ──────────────────
4064//
4065// The `TypedArrayData` enum + impl blocks + `Display for TypedArrayData` +
4066// `typed_array_structural_eq` were DELETED here per ADR-006 §2.7.24 Q25.A
4067// SUPERSEDED + W12-typed-array-data-deletion-audit §3.5. The 22-variant
4068// enum migrates to v2-raw `TypedArray<T>` flat-struct per-T monomorphization.
4069// Consumer cascade lands across ckpt-2 (array_transform/aggregation/sets),
4070// ckpt-3 (array_operations/concat/object_creation), ckpt-4 (TypedBuffer +
4071// HeapValue::TypedArray arm + HeapKind::TypedArray ordinal), ckpt-5 (wire/
4072// json/marshal + 4-table lockstep delete), ckpt-6 (JIT FFI). The
4073// `Arc<TypedArrayData>` payload at heap_variants.rs:476 stays until ckpt-4.
4074//
4075// Refusal #1 binding: do not resurrect TypedArrayData under any rename
4076// (e.g. TypedArrayKind, TypedArrayCarrier, TypedBuffer<T> wrapper enum).
4077
4078// ── Temporal data ───────────────────────────────────────────────────────────
4079
4080/// Temporal data — consolidates Time, Duration, TimeSpan, Timeframe,
4081/// TimeReference, DateTimeExpr, and DataDateTimeRef.
4082#[derive(Debug, Clone)]
4083pub enum TemporalData {
4084 DateTime(chrono::DateTime<chrono::FixedOffset>),
4085 Duration(shape_ast::ast::Duration),
4086 TimeSpan(chrono::Duration),
4087 Timeframe(shape_ast::data::Timeframe),
4088 TimeReference(Box<shape_ast::ast::TimeReference>),
4089 DateTimeExpr(Box<shape_ast::ast::DateTimeExpr>),
4090 DataDateTimeRef(Box<shape_ast::ast::DataDateTimeRef>),
4091}
4092
4093impl TemporalData {
4094 #[inline]
4095 pub fn type_name(&self) -> &'static str {
4096 match self {
4097 TemporalData::DateTime(_) => "time",
4098 TemporalData::Duration(_) => "duration",
4099 TemporalData::TimeSpan(_) => "timespan",
4100 TemporalData::Timeframe(_) => "timeframe",
4101 TemporalData::TimeReference(_) => "time_reference",
4102 TemporalData::DateTimeExpr(_) => "datetime_expr",
4103 TemporalData::DataDateTimeRef(_) => "data_datetime_ref",
4104 }
4105 }
4106
4107 #[inline]
4108 pub fn is_truthy(&self) -> bool {
4109 true
4110 }
4111}
4112
4113impl fmt::Display for TemporalData {
4114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4115 match self {
4116 TemporalData::DateTime(t) => write!(f, "{}", t),
4117 TemporalData::Duration(d) => write!(f, "{:?}", d),
4118 TemporalData::TimeSpan(ts) => write!(f, "{}", ts),
4119 TemporalData::Timeframe(tf) => write!(f, "{:?}", tf),
4120 TemporalData::TimeReference(_) => write!(f, "<time_ref>"),
4121 TemporalData::DateTimeExpr(_) => write!(f, "<datetime_expr>"),
4122 TemporalData::DataDateTimeRef(_) => write!(f, "<data_datetime_ref>"),
4123 }
4124 }
4125}
4126
4127// ── Table view data ─────────────────────────────────────────────────────────
4128
4129/// Table view data — consolidates TypedTable, RowView, ColumnRef, and IndexedTable.
4130#[derive(Debug, Clone)]
4131pub enum TableViewData {
4132 TypedTable {
4133 schema_id: u64,
4134 table: Arc<crate::datatable::DataTable>,
4135 },
4136 RowView {
4137 schema_id: u64,
4138 table: Arc<crate::datatable::DataTable>,
4139 row_idx: usize,
4140 },
4141 ColumnRef {
4142 schema_id: u64,
4143 table: Arc<crate::datatable::DataTable>,
4144 col_id: u32,
4145 },
4146 IndexedTable {
4147 schema_id: u64,
4148 table: Arc<crate::datatable::DataTable>,
4149 index_col: u32,
4150 },
4151}
4152
4153impl TableViewData {
4154 #[inline]
4155 pub fn type_name(&self) -> &'static str {
4156 match self {
4157 TableViewData::TypedTable { .. } => "typed_table",
4158 TableViewData::RowView { .. } => "row",
4159 TableViewData::ColumnRef { .. } => "column",
4160 TableViewData::IndexedTable { .. } => "indexed_table",
4161 }
4162 }
4163
4164 #[inline]
4165 pub fn is_truthy(&self) -> bool {
4166 match self {
4167 TableViewData::TypedTable { table, .. } => table.row_count() > 0,
4168 TableViewData::RowView { .. } => true,
4169 TableViewData::ColumnRef { .. } => true,
4170 TableViewData::IndexedTable { table, .. } => table.row_count() > 0,
4171 }
4172 }
4173}
4174
4175impl fmt::Display for TableViewData {
4176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4177 match self {
4178 TableViewData::TypedTable { table, .. } => write!(
4179 f,
4180 "<typed_table:{}x{}>",
4181 table.row_count(),
4182 table.column_count()
4183 ),
4184 TableViewData::RowView { row_idx, .. } => write!(f, "<row:{}>", row_idx),
4185 TableViewData::ColumnRef { col_id, .. } => write!(f, "<column:{}>", col_id),
4186 TableViewData::IndexedTable { table, .. } => write!(
4187 f,
4188 "<indexed_table:{}x{}>",
4189 table.row_count(),
4190 table.column_count()
4191 ),
4192 }
4193 }
4194}
4195
4196// ── Generate HeapValue, HeapKind, kind(), is_truthy(), type_name() ──────────
4197//
4198// All generated from the single source of truth in define_heap_types!().
4199crate::define_heap_types!();
4200
4201// ── Manual Clone for HeapValue ──────────────────────────────────────────────
4202//
4203// ADR-006 §2.3 + Step 6: every heap-resident variant carries `Arc<T>` so its
4204// clone is one atomic refcount bump — no allocation, no payload copy. Inline
4205// scalars (`Future`, `Char`, `NativeScalar`) clone by `Copy`. `ClosureRaw`
4206// delegates to `OwnedClosureBlock::clone`, which already does a single
4207// `retain_typed_closure` refcount bump on the v2 closure block plus an Arc
4208// bump on the layout pointer.
4209//
4210// This impl is purely mechanical Arc::clone delegation — there is no
4211// `vw_clone` / `vw_drop` bookkeeping (the strict-typed bulldozer deleted
4212// every `ValueWord`-bearing variant).
4213impl Clone for HeapValue {
4214 fn clone(&self) -> Self {
4215 match self {
4216 // ADR-006 §2.3: Arc bump only — no allocation, no payload copy.
4217 HeapValue::String(v) => HeapValue::String(Arc::clone(v)),
4218 HeapValue::Decimal(v) => HeapValue::Decimal(Arc::clone(v)),
4219 HeapValue::BigInt(v) => HeapValue::BigInt(Arc::clone(v)),
4220 HeapValue::Future(v) => HeapValue::Future(*v),
4221 HeapValue::Char(v) => HeapValue::Char(*v),
4222 HeapValue::DataTable(v) => HeapValue::DataTable(Arc::clone(v)),
4223 HeapValue::Content(v) => HeapValue::Content(Arc::clone(v)),
4224 HeapValue::Instant(v) => HeapValue::Instant(Arc::clone(v)),
4225 HeapValue::IoHandle(v) => HeapValue::IoHandle(Arc::clone(v)),
4226 HeapValue::NativeScalar(v) => HeapValue::NativeScalar(*v),
4227 HeapValue::NativeView(v) => HeapValue::NativeView(Arc::clone(v)),
4228 // Wave 2 Round 4 D4 ckpt-final-prime² (2026-05-14): TypedObjectPtr's
4229 // Clone impl bumps the v2-raw HeapHeader-at-offset-0 refcount via
4230 // `v2_retain`, mirroring the typed-Arc clone shape of every other
4231 // heap arm.
4232 HeapValue::TypedObject(s) => HeapValue::TypedObject(s.clone()),
4233 // OwnedClosureBlock::clone is one refcount bump on the typed
4234 // closure block + one Arc bump on the shared layout.
4235 HeapValue::ClosureRaw(v) => HeapValue::ClosureRaw(v.clone()),
4236 HeapValue::TaskGroup(v) => HeapValue::TaskGroup(Arc::clone(v)),
4237 // V3-S5 ckpt-4 (2026-05-15): `HeapValue::TypedArray(v) =>
4238 // HeapValue::TypedArray(Arc::clone(v))` clone arm DELETED in
4239 // lockstep with the variant + the `TypedArrayData` enum
4240 // (ckpt-1) + `TypedBuffer<T>` / `AlignedTypedBuffer` wrapper
4241 // layer (ckpt-4). W12 audit §3.5/§B + ADR-006 §2.7.24 Q25.A
4242 // SUPERSEDED. Refusal #1 binding.
4243 HeapValue::Temporal(v) => HeapValue::Temporal(Arc::clone(v)),
4244 HeapValue::TableView(v) => HeapValue::TableView(Arc::clone(v)),
4245 // Wave 2 Round 3b C2-joint ckpt-2 (2026-05-14): payload is now
4246 // `HashMapKindedRef` (not `Arc<HashMapData>`); the enum's manual
4247 // Clone impl dispatches per-variant `Arc::clone` on the inner
4248 // `Arc<HashMapData<V>>` — preserving structural sharing.
4249 HeapValue::HashMap(v) => HeapValue::HashMap(v.clone()),
4250 // Wave 13 W13-hashset-rebuild (ADR-006 §2.7.15 / Q16,
4251 // 2026-05-10): mirror of HashMap — single strong-count bump
4252 // on the shared `Arc<HashSetData>`, no payload copy.
4253 HeapValue::HashSet(v) => HeapValue::HashSet(Arc::clone(v)),
4254 // Wave 15 W15-deque (ADR-006 §2.7.19 / Q20, 2026-05-10):
4255 // mirror of HashSet — single strong-count bump on the
4256 // shared `Arc<DequeData>`, no payload copy. Per-element
4257 // `Arc<HeapValue>` shares stay shared with the source.
4258 HeapValue::Deque(v) => HeapValue::Deque(Arc::clone(v)),
4259 // Wave-γ G-heap-filter-expr (ADR-006 §2.3 / Q8 amendment):
4260 // FilterExpr Arcs share the typed-Arc clone shape — single
4261 // strong-count bump, no payload copy.
4262 HeapValue::FilterExpr(v) => HeapValue::FilterExpr(Arc::clone(v)),
4263 // Wave 8 W8-T26 (ADR-006 §2.7.13 / Q14, 2026-05-10):
4264 // Reference Arcs share the typed-Arc clone shape — single
4265 // strong-count bump on the shared `Arc<RefTarget>`, no
4266 // payload copy.
4267 HeapValue::Reference(v) => HeapValue::Reference(Arc::clone(v)),
4268 // W13-iterator-state (ADR-006 §2.7.16 / Q17, 2026-05-10):
4269 // Iterator Arcs share the typed-Arc clone shape — single
4270 // strong-count bump on the shared `Arc<IteratorState>`. The
4271 // inner state is `Clone`-by-derive (typed-Arc payloads in
4272 // every field), but the outer `Arc` bump is the canonical
4273 // shared-receiver path.
4274 HeapValue::Iterator(v) => HeapValue::Iterator(Arc::clone(v)),
4275 // Wave 15 W15-channel-rebuild (ADR-006 §2.7.20 / Q21,
4276 // 2026-05-10): Channel Arcs share the typed-Arc clone shape
4277 // — single strong-count bump on the shared
4278 // `Arc<ChannelData>`. The inner `ChannelData` carries a
4279 // `Mutex<ChannelInner>` so two `Arc<ChannelData>` shares
4280 // observe each other's mutations; cloning the outer Arc
4281 // hands out a fresh endpoint of the same channel.
4282 HeapValue::Channel(v) => HeapValue::Channel(Arc::clone(v)),
4283 // Wave 15 W15-priority-queue (ADR-006 §2.7.18 / Q19,
4284 // 2026-05-10): mirror of HashSet — single strong-count bump
4285 // on the shared `Arc<PriorityQueueData>`, no payload copy.
4286 HeapValue::PriorityQueue(v) => HeapValue::PriorityQueue(Arc::clone(v)),
4287 // W15-range (ADR-006 §2.7.23 / Q24, 2026-05-10): Range Arcs
4288 // share the typed-Arc clone shape — single strong-count bump
4289 // on the shared `Arc<RangeData>`, no payload copy. RangeData
4290 // is small ({i64, i64, i64, bool}) so copies would be cheap,
4291 // but the shared-Arc shape matches the dispatch pattern of
4292 // every other heap arm.
4293 HeapValue::Range(v) => HeapValue::Range(Arc::clone(v)),
4294 // Wave 14 W14-variant-codegen (ADR-006 §2.7.17 / Q18,
4295 // 2026-05-10): Result/Option Arcs share the typed-Arc
4296 // clone shape — single strong-count bump on the shared
4297 // `Arc<ResultData>` / `Arc<OptionData>`. The inner
4298 // `KindedSlot` payload's share is preserved by the
4299 // shared Arc; ResultData/OptionData Clone (defined in
4300 // this file) does an inner KindedSlot Clone if the Arc
4301 // is unwrapped via Arc::make_mut.
4302 HeapValue::Result(v) => HeapValue::Result(Arc::clone(v)),
4303 HeapValue::Option(v) => HeapValue::Option(Arc::clone(v)),
4304 // W17-trait-object-storage (ADR-006 §2.7.24 / Q25.C,
4305 // 2026-05-11): TraitObject Arcs share the typed-Arc clone
4306 // shape — single strong-count bump on the shared
4307 // `Arc<TraitObjectStorage>`. Inner Arcs (`value: Arc<TypedObjectStorage>`
4308 // + `vtable: Arc<VTable>`) stay shared with the source;
4309 // `Arc::ptr_eq` on the vtable preserves the §Q25.C.2
4310 // `Self`-arg runtime identity contract across the clone.
4311 // Wave 2 Round 4 D4 ckpt-final-prime² (2026-05-14): TraitObjectPtr's
4312 // Clone impl bumps the v2-raw HeapHeader-at-offset-0 refcount via
4313 // `v2_retain`. Inner `value: *const TypedObjectStorage` and
4314 // `vtable: Arc<VTable>` shares are bumped through the Clone impl
4315 // of TraitObjectStorage itself (called by `_drop` / the per-field
4316 // discipline at refcount=0 release).
4317 HeapValue::TraitObject(v) => HeapValue::TraitObject(v.clone()),
4318 // W17-concurrency (ADR-006 §2.7.25, 2026-05-11): Mutex /
4319 // Atomic / Lazy Arcs share the typed-Arc clone shape —
4320 // single strong-count bump on the shared inner Arc, no
4321 // payload copy. Cloning a Mutex/Lazy yields a fresh
4322 // "endpoint" share of the same protected cell; Atomic
4323 // shares observe each other's load/store/fetch
4324 // operations. Same shape as Channel.
4325 HeapValue::Mutex(v) => HeapValue::Mutex(Arc::clone(v)),
4326 HeapValue::Atomic(v) => HeapValue::Atomic(Arc::clone(v)),
4327 HeapValue::Lazy(v) => HeapValue::Lazy(Arc::clone(v)),
4328 // W17-comptime-vm-dispatch (ADR-006 §2.7.26, 2026-05-12):
4329 // ModuleFn is an inline-scalar payload (no Arc).
4330 HeapValue::ModuleFn(v) => HeapValue::ModuleFn(*v),
4331 // ADR-006 §2.7.22 amendment (Round 18 S3, 2026-05-13): Matrix
4332 // and MatrixSlice arms share the typed-Arc clone shape — single
4333 // strong-count bump on the shared `Arc<MatrixData>` /
4334 // `Arc<MatrixSliceData>`. MatrixSlice's inner `parent: Arc<MatrixData>`
4335 // share stays shared with the source (cloning the slice does not
4336 // copy the parent matrix data). The HeapValue arm exists for the
4337 // ADR-005 §1 / ADR-006 §2.3 `HeapKind`↔`HeapValue` symmetry but
4338 // calling `slot.as_heap_value()` on Matrix/MatrixSlice-labeled
4339 // slot bits is unsound — slot bits are
4340 // `Arc::into_raw(Arc<MatrixData>)` / `Arc::into_raw(Arc<MatrixSliceData>)`,
4341 // NOT `Box<HeapValue>`. Pure-discriminator dispatch shape, mirror
4342 // of §2.7.9 FilterExpr / §2.7.13 Reference.
4343 HeapValue::Matrix(v) => HeapValue::Matrix(Arc::clone(v)),
4344 HeapValue::MatrixSlice(v) => HeapValue::MatrixSlice(Arc::clone(v)),
4345 }
4346 }
4347}
4348
4349// ── Shared comparison helpers ────────────────────────────────────────────────
4350
4351/// Cross-type numeric equality: BigInt vs Decimal.
4352#[inline]
4353fn bigint_decimal_eq(a: &i64, b: &rust_decimal::Decimal) -> bool {
4354 rust_decimal::Decimal::from(*a) == *b
4355}
4356
4357/// Cross-type numeric equality: NativeScalar vs BigInt.
4358#[inline]
4359fn native_scalar_bigint_eq(a: &NativeScalar, b: &i64) -> bool {
4360 a.as_i64().is_some_and(|v| v == *b)
4361}
4362
4363/// Cross-type numeric equality: NativeScalar vs Decimal.
4364#[inline]
4365fn native_scalar_decimal_eq(a: &NativeScalar, b: &rust_decimal::Decimal) -> bool {
4366 match a {
4367 NativeScalar::F32(v) => {
4368 rust_decimal::Decimal::from_f64_retain(*v as f64).is_some_and(|v| v == *b)
4369 }
4370 _ => a
4371 .as_i128()
4372 .map(|n| rust_decimal::Decimal::from_i128_with_scale(n, 0))
4373 .is_some_and(|to_dec| to_dec == *b),
4374 }
4375}
4376
4377/// Cross-type typed array equality: IntArray vs FloatArray (element-wise i64-as-f64).
4378///
4379/// NOTE (V3-S5 ckpt-5-prime²a, 2026-05-15): currently unreachable post-ckpt-4
4380/// HeapValue::TypedArray outer-arm deletion (no callers). Signature migrated
4381/// from `&TypedBuffer<i64>` / `&AlignedTypedBuffer` to `&[i64]` / `&[f64]` per
4382/// Migration shape (a). Retained for the eventual v2-raw `*mut TypedArray<T>`
4383/// per-T monomorphic rebuild (cluster-2 v2-raw-heap-audit territory).
4384#[inline]
4385fn int_float_array_eq(
4386 ints: &[i64],
4387 floats: &[f64],
4388) -> bool {
4389 ints.len() == floats.len()
4390 && ints
4391 .iter()
4392 .zip(floats.iter())
4393 .all(|(x, y)| (*x as f64) == *y)
4394}
4395
4396/// Matrix structural equality (row/col dimensions + element-wise).
4397#[inline]
4398fn matrix_eq(a: &MatrixData, b: &MatrixData) -> bool {
4399 a.rows == b.rows
4400 && a.cols == b.cols
4401 && a.data.len() == b.data.len()
4402 && a.data.iter().zip(b.data.iter()).all(|(x, y)| x == y)
4403}
4404
4405/// NativeView identity comparison.
4406#[inline]
4407fn native_view_eq(a: &NativeViewData, b: &NativeViewData) -> bool {
4408 a.ptr == b.ptr && a.mutable == b.mutable && a.layout.name == b.layout.name
4409}
4410
4411// `typed_array_structural_eq` DELETED — V3-S5 ckpt-1, 2026-05-15.
4412// Per-arm TypedArrayData structural equality dispatch is retired with the
4413// enum. Consumer sites at structural_eq + equals (HeapValue::TypedArray
4414// arm match) cascade-break here and surface for ckpt-4 (HeapValue::TypedArray
4415// arm rebuild atop v2-raw `*mut TypedArray<T>` per-T monomorphic dispatch).
4416
4417// ── Display ─────────────────────────────────────────────────────────────────
4418
4419impl fmt::Display for HeapValue {
4420 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
4421 match self {
4422 HeapValue::Char(c) => write!(f, "{}", c),
4423 HeapValue::String(s) => write!(f, "{}", s),
4424 HeapValue::TypedObject(_) => write!(f, "{{...}}"),
4425 HeapValue::ClosureRaw(owned) => {
4426 // SAFETY: OwnedClosureBlock's invariant guarantees the
4427 // pointer is live for the duration of `&self`.
4428 let fid = unsafe {
4429 crate::v2::closure_raw::typed_closure_function_id(owned.as_ptr())
4430 };
4431 write!(f, "<closure:{}>", fid)
4432 }
4433 HeapValue::Decimal(d) => write!(f, "{}", d),
4434 HeapValue::BigInt(i) => write!(f, "{}", i),
4435 HeapValue::DataTable(dt) => {
4436 write!(f, "<datatable:{}x{}>", dt.row_count(), dt.column_count())
4437 }
4438 HeapValue::TableView(tv) => write!(f, "{}", tv),
4439 HeapValue::Content(node) => write!(f, "{}", node),
4440 HeapValue::Instant(t) => write!(f, "<instant:{:?}>", t.elapsed()),
4441 HeapValue::IoHandle(data) => {
4442 let status = if data.is_open() { "open" } else { "closed" };
4443 write!(f, "<io_handle:{}:{}>", data.path, status)
4444 }
4445 HeapValue::Future(id) => write!(f, "<future:{}>", id),
4446 HeapValue::TaskGroup(tg) => {
4447 write!(f, "<task_group:{}>", tg.task_ids.len())
4448 }
4449 HeapValue::Temporal(td) => write!(f, "{}", td),
4450 HeapValue::NativeScalar(v) => write!(f, "{v}"),
4451 HeapValue::NativeView(v) => write!(
4452 f,
4453 "<{}:{}@0x{:x}>",
4454 if v.mutable { "cmut" } else { "cview" },
4455 v.layout.name,
4456 v.ptr
4457 ),
4458 // V3-S5 ckpt-4 (2026-05-15): `HeapValue::TypedArray(ta) =>
4459 // write!(f, "{}", ta)` Display arm DELETED in lockstep with the
4460 // variant + the `TypedArrayData` Display impl (ckpt-1). W12
4461 // audit §3.5/§B + ADR-006 §2.7.24 Q25.A SUPERSEDED.
4462 HeapValue::HashMap(kref) => {
4463 // Wave 2 Round 3b C2-joint ckpt-3 (2026-05-14): full per-V
4464 // entry dump. Walks `*mut TypedArray<*const StringObj>`
4465 // keys + per-V `*mut TypedArray<V>` values; formats as
4466 // `{"k1": v1, "k2": v2}` using each V's natural Display.
4467 // For TypedObject / TraitObject the inner value renders
4468 // as a summary tag — full recursive rendering lives at
4469 // printing.rs (which has the depth-budgeted recursive
4470 // Display via format_heap_value). ADR-006 §2.7.24 Q25.B
4471 // SUPERSEDED + audit §C.4.
4472 hashmap_kref_display(kref, f)
4473 }
4474 // Wave 13 W13-hashset-rebuild (ADR-006 §2.7.15 / Q16,
4475 // 2026-05-10): one-keyspace mirror of HashMap's Display
4476 // shape — `{"a", "b", ...}` braces with comma-separated
4477 // quoted strings, no values.
4478 HeapValue::HashSet(d) => {
4479 write!(f, "{{")?;
4480 for (i, k) in d.keys.iter().enumerate() {
4481 if i > 0 {
4482 write!(f, ", ")?;
4483 }
4484 write!(f, "\"{}\"", k)?;
4485 }
4486 write!(f, "}}")
4487 }
4488 // Wave 15 W15-deque (ADR-006 §2.7.19 / Q20, 2026-05-10):
4489 // render front-to-back as `Deque[elem1, elem2, ...]` —
4490 // dispatch each element through the canonical ADR-005 §1
4491 // single-discriminator `HeapValue` Display.
4492 HeapValue::Deque(d) => {
4493 write!(f, "Deque[")?;
4494 for (i, v) in d.items.iter().enumerate() {
4495 if i > 0 {
4496 write!(f, ", ")?;
4497 }
4498 write!(f, "{}", v)?;
4499 }
4500 write!(f, "]")
4501 }
4502 // Wave-γ G-heap-filter-expr (ADR-006 §2.3 amendment): no
4503 // user-facing FilterExpr literal exists; render as an opaque
4504 // tag for diagnostics. Construction-side bug if a FilterExpr
4505 // ever escapes into a user-visible Display path.
4506 HeapValue::FilterExpr(_) => write!(f, "<filter_expr>"),
4507 // Wave 8 W8-T26 (ADR-006 §2.7.13 / Q14, 2026-05-10): no
4508 // user-facing reference literal exists; render as an opaque
4509 // tag for diagnostics. References are within-program data
4510 // and don't cross any user-visible Display surface.
4511 HeapValue::Reference(_) => write!(f, "<ref>"),
4512 // W13-iterator-state (ADR-006 §2.7.16 / Q17, 2026-05-10):
4513 // iterator pipelines have no user-facing literal — render
4514 // as an opaque tag. Terminal operations (collect / forEach
4515 // / reduce / etc.) materialise the elements; an iterator
4516 // reaching the Display surface is "still lazy" by
4517 // construction.
4518 HeapValue::Iterator(_) => write!(f, "<iterator>"),
4519 // Wave 15 W15-channel-rebuild (ADR-006 §2.7.20 / Q21,
4520 // 2026-05-10): channels are concurrency primitives with no
4521 // user-facing literal; render as an opaque tag annotated
4522 // with current queue length and closed flag for
4523 // diagnostics.
4524 HeapValue::Channel(c) => {
4525 let len = c.len();
4526 let state = if c.is_closed() { "closed" } else { "open" };
4527 write!(f, "<channel:{}:{}>", state, len)
4528 }
4529 // Wave 15 W15-priority-queue (ADR-006 §2.7.18 / Q19,
4530 // 2026-05-10): one-keyspace mirror of HashSet's Display
4531 // shape — bracketed comma-separated values in heap-array
4532 // order. NOTE: heap-array order is not sort-order; for
4533 // sorted output the user must call `pq.toSortedArray()`.
4534 HeapValue::PriorityQueue(d) => {
4535 write!(f, "PriorityQueue[")?;
4536 for (i, v) in d.heap.iter().enumerate() {
4537 if i > 0 {
4538 write!(f, ", ")?;
4539 }
4540 write!(f, "{}", v)?;
4541 }
4542 write!(f, "]")
4543 }
4544 // W15-range (ADR-006 §2.7.23 / Q24, 2026-05-10): user-visible
4545 // literal form — `start..end` for exclusive, `start..=end`
4546 // for inclusive. Step is not part of the surface syntax (no
4547 // explicit step suffix) so it's not rendered. This matches
4548 // the pre-strict-typing print format and the `0..10` /
4549 // `0..=10` source syntax round-trip.
4550 HeapValue::Range(r) => {
4551 if r.inclusive {
4552 write!(f, "{}..={}", r.start, r.end)
4553 } else {
4554 write!(f, "{}..{}", r.start, r.end)
4555 }
4556 }
4557 // Wave 14 W14-variant-codegen (ADR-006 §2.7.17 / Q18,
4558 // 2026-05-10): render as `Ok(<inner>)` / `Err(<inner>)` /
4559 // `Some(<inner>)` / `None`. The inner Display goes
4560 // through the runtime kinded value-formatter at the
4561 // VM-tier `printing.rs` site (the heap_value `Display`
4562 // impl is a fallback for diagnostic prints; rich
4563 // formatting goes through the executor's
4564 // `format_kinded`). Renders inner as `<…>` opaque tag
4565 // here — the kinded formatter handles full pretty-print.
4566 HeapValue::Result(r) => {
4567 if r.is_ok {
4568 write!(f, "Ok(<...>)")
4569 } else {
4570 write!(f, "Err(<...>)")
4571 }
4572 }
4573 HeapValue::Option(o) => {
4574 if o.is_some {
4575 write!(f, "Some(<...>)")
4576 } else {
4577 write!(f, "None")
4578 }
4579 }
4580 // W17-trait-object-storage (ADR-006 §2.7.24 / Q25.C,
4581 // 2026-05-11): `dyn Trait` carriers render as an opaque
4582 // tag annotated with the first trait name (multi-trait
4583 // inheritance shows just the first) and the inner schema
4584 // id of the boxed TypedObject. Pretty-printing via the
4585 // boxed receiver's user-defined `Display`-equivalent is
4586 // a compiler-emission tier concern (call the trait's
4587 // `.format(self)` method through the vtable); the
4588 // storage-tier formatter is diagnostic-only.
4589 HeapValue::TraitObject(t) => {
4590 let trait_name = t
4591 .vtable
4592 .trait_names
4593 .first()
4594 .map(|s| s.as_str())
4595 .unwrap_or("?");
4596 // Wave 2 Round 4 D4 ckpt-3 (2026-05-14): `t.value` is
4597 // `*const TypedObjectStorage` (v2-raw shape) — field
4598 // access goes through an unsafe deref.
4599 // SAFETY: `t.value` is non-null per universal-dyn
4600 // construction; the `&HeapValue::TraitObject(t)` borrow
4601 // holds the carrier live for this scope.
4602 let inner_schema_id = unsafe { (*t.value).schema_id };
4603 write!(
4604 f,
4605 "<dyn {} #{}>",
4606 trait_name, inner_schema_id
4607 )
4608 }
4609 // W17-concurrency (ADR-006 §2.7.25, 2026-05-11): concurrency
4610 // primitives have no user-facing literal — render as opaque
4611 // tags annotated with diagnostic state. Mirror of Channel's
4612 // `<channel:state:len>` shape.
4613 HeapValue::Mutex(_) => write!(f, "<mutex>"),
4614 HeapValue::Atomic(a) => write!(f, "<atomic:{}>", a.load()),
4615 HeapValue::Lazy(l) => {
4616 if l.is_initialized() {
4617 write!(f, "<lazy:initialized>")
4618 } else {
4619 write!(f, "<lazy:pending>")
4620 }
4621 }
4622 // W17-comptime-vm-dispatch (ADR-006 §2.7.26, 2026-05-12):
4623 // ModuleFn references render as `<module_fn:id>`.
4624 HeapValue::ModuleFn(id) => write!(f, "<module_fn:{}>", id),
4625 // ADR-006 §2.7.22 amendment (Round 18 S3, 2026-05-13):
4626 // Matrix renders as `<Mat<number>:rows x cols>`, mirroring the
4627 // pre-amendment `TypedArrayData::Matrix` Display shape.
4628 // MatrixSlice renders as a flat `Vec<number>[...]` over the
4629 // projection's element slice, mirroring the pre-amendment
4630 // `TypedArrayData::FloatSlice` Display shape. These Display
4631 // surfaces are diagnostic fallbacks; pretty-printing of
4632 // Matrix/MatrixSlice values goes through `printing.rs` at
4633 // the VM tier.
4634 HeapValue::Matrix(m) => {
4635 write!(f, "<Mat<number>:{}x{}>", m.rows, m.cols)
4636 }
4637 HeapValue::MatrixSlice(s) => {
4638 let slice = s.as_slice();
4639 write!(f, "Vec<number>[")?;
4640 for (i, v) in slice.iter().enumerate() {
4641 if i > 0 {
4642 write!(f, ", ")?;
4643 }
4644 if *v == v.trunc() && v.abs() < 1e15 {
4645 write!(f, "{}", *v as i64)?;
4646 } else {
4647 write!(f, "{}", v)?;
4648 }
4649 }
4650 write!(f, "]")
4651 }
4652 }
4653 }
4654}
4655
4656// ── Hand-written methods (complex per-variant logic) ────────────────────────
4657
4658impl HeapValue {
4659 /// Obtain a [`crate::vm_closure_handle::VmClosureHandle`] over this
4660 /// heap value, if it is a `HeapValue::ClosureRaw`.
4661 ///
4662 /// Closure spec §14.2: the handle is the stable read API for
4663 /// closure state. Returns `None` for non-closure heap values.
4664 #[inline]
4665 pub fn as_closure_handle(&self) -> Option<crate::vm_closure_handle::VmClosureHandle<'_>> {
4666 match self {
4667 HeapValue::ClosureRaw(owned) => {
4668 // SAFETY: `OwnedClosureBlock::from_raw` upholds that
4669 // `as_header_ptr()` points to a live `TypedClosureHeader`
4670 // whose layout matches `owned.layout()`; both remain valid
4671 // for the duration of the `&self` borrow.
4672 let handle = unsafe {
4673 crate::vm_closure_handle::VmClosureHandle::raw(
4674 owned.as_header_ptr(),
4675 owned.layout().as_ref(),
4676 )
4677 };
4678 Some(handle)
4679 }
4680 _ => None,
4681 }
4682 }
4683
4684 /// Structural equality comparison for HeapValue.
4685 ///
4686 /// ADR-006 §2.3: `TypedArray` and `Temporal` payloads are now
4687 /// `Arc<TypedArrayData>` / `Arc<TemporalData>`; the per-arm dispatch
4688 /// dereferences the Arc once at the outer match and forwards into the
4689 /// inner enum via `typed_array_structural_eq` / direct `match`.
4690 pub fn structural_eq(&self, other: &HeapValue) -> bool {
4691 match (self, other) {
4692 (HeapValue::Char(a), HeapValue::Char(b)) => a == b,
4693 (HeapValue::String(a), HeapValue::String(b)) => a == b,
4694 // Cross-type: Char from string indexing vs String literal
4695 (HeapValue::Char(c), HeapValue::String(s))
4696 | (HeapValue::String(s), HeapValue::Char(c)) => {
4697 let mut buf = [0u8; 4];
4698 let cs = c.encode_utf8(&mut buf);
4699 cs == s.as_str()
4700 }
4701 (HeapValue::Decimal(a), HeapValue::Decimal(b)) => a == b,
4702 (HeapValue::BigInt(a), HeapValue::BigInt(b)) => a == b,
4703 (HeapValue::NativeScalar(a), HeapValue::NativeScalar(b)) => a == b,
4704 (HeapValue::NativeView(a), HeapValue::NativeView(b)) => native_view_eq(a, b),
4705 (HeapValue::Future(a), HeapValue::Future(b)) => a == b,
4706 (HeapValue::Temporal(a), HeapValue::Temporal(b)) => match (a.as_ref(), b.as_ref()) {
4707 (TemporalData::DateTime(x), TemporalData::DateTime(y)) => x == y,
4708 _ => false,
4709 },
4710 (HeapValue::Content(a), HeapValue::Content(b)) => a == b,
4711 (HeapValue::Instant(a), HeapValue::Instant(b)) => a == b,
4712 (HeapValue::IoHandle(a), HeapValue::IoHandle(b)) => {
4713 std::sync::Arc::ptr_eq(&a.resource, &b.resource)
4714 }
4715 // V3-S5 ckpt-4 (2026-05-15): `(HeapValue::TypedArray(...), ...)`
4716 // structural-eq arm DELETED. The `HeapValue::TypedArray` outer
4717 // arm was retired in lockstep with this ckpt-4 +
4718 // `typed_array_structural_eq` was deleted at V3-S5 ckpt-1
4719 // (heap_value.rs wholesale deletion per W12 audit §3.5 + ADR-006
4720 // §2.7.24 Q25.A SUPERSEDED). Per-arm TypedArrayData structural
4721 // equality dispatch retires with the enum; no replacement (the
4722 // v2-raw `TypedArray<T>` flat struct compares element-wise via
4723 // its own `==` impl, not through `HeapValue::structural_eq`).
4724 // Refusal #1 binding.
4725 // ADR-006 §2.7.22 amendment (Round 18 S3, 2026-05-13): Matrix
4726 // equality is structural (rows + cols + element-wise compare);
4727 // MatrixSlice equality is element-wise over the projection slice
4728 // (parent identity is NOT required — two slices with identical
4729 // elements compare equal even when projecting from different
4730 // parents). Mirror of the pre-amendment TypedArrayData::Matrix /
4731 // FloatSlice equality semantics.
4732 (HeapValue::Matrix(a), HeapValue::Matrix(b)) => matrix_eq(a, b),
4733 (HeapValue::MatrixSlice(a), HeapValue::MatrixSlice(b)) => {
4734 a.as_slice() == b.as_slice()
4735 }
4736 _ => false,
4737 }
4738 }
4739
4740 /// Check equality between two heap values.
4741 #[inline]
4742 pub fn equals(&self, other: &HeapValue) -> bool {
4743 match (self, other) {
4744 (HeapValue::Char(a), HeapValue::Char(b)) => a == b,
4745 (HeapValue::String(a), HeapValue::String(b)) => a == b,
4746 // Cross-type: Char from string indexing vs String literal
4747 (HeapValue::Char(c), HeapValue::String(s))
4748 | (HeapValue::String(s), HeapValue::Char(c)) => {
4749 let mut buf = [0u8; 4];
4750 let cs = c.encode_utf8(&mut buf);
4751 cs == s.as_str()
4752 }
4753 (HeapValue::TypedObject(a), HeapValue::TypedObject(b)) => {
4754 // Wave 2 Round 4 D4 ckpt-final-prime² (2026-05-14): payloads are
4755 // `TypedObjectPtr` (raw `*const TypedObjectStorage`); pointer-
4756 // equality is the fast path for shared storage.
4757 if a.as_ptr() == b.as_ptr() {
4758 return true;
4759 }
4760 if a.schema_id != b.schema_id
4761 || a.slots.len() != b.slots.len()
4762 || a.heap_mask != b.heap_mask
4763 {
4764 return false;
4765 }
4766 for i in 0..a.slots.len() {
4767 // Both heap-mask and primitive-mask: compare raw bits
4768 // for primitives. For heap slots, raw-bit equality is
4769 // also conservatively correct since `ValueSlot` heap
4770 // payloads are typed pointers — pointer-equality
4771 // implies value-equality for shared Arc'd payloads.
4772 if a.slots[i].raw() != b.slots[i].raw() {
4773 return false;
4774 }
4775 }
4776 true
4777 }
4778 // Track A.5: the canonical closure variant compares by function id.
4779 (HeapValue::ClosureRaw(a), HeapValue::ClosureRaw(b)) => {
4780 // SAFETY: both blocks are live per OwnedClosureBlock invariant.
4781 let fa = unsafe { crate::v2::closure_raw::typed_closure_function_id(a.as_ptr()) };
4782 let fb = unsafe { crate::v2::closure_raw::typed_closure_function_id(b.as_ptr()) };
4783 fa == fb
4784 }
4785 (HeapValue::Decimal(a), HeapValue::Decimal(b)) => a == b,
4786 (HeapValue::BigInt(a), HeapValue::BigInt(b)) => a == b,
4787 (HeapValue::BigInt(a), HeapValue::Decimal(b)) => bigint_decimal_eq(a.as_ref(), b.as_ref()),
4788 (HeapValue::Decimal(a), HeapValue::BigInt(b)) => bigint_decimal_eq(b.as_ref(), a.as_ref()),
4789 (HeapValue::DataTable(a), HeapValue::DataTable(b)) => Arc::ptr_eq(a, b),
4790 (HeapValue::TableView(a), HeapValue::TableView(b)) => match (a.as_ref(), b.as_ref()) {
4791 (
4792 TableViewData::TypedTable { schema_id: s1, table: t1 },
4793 TableViewData::TypedTable { schema_id: s2, table: t2 },
4794 ) => s1 == s2 && Arc::ptr_eq(t1, t2),
4795 (
4796 TableViewData::RowView { schema_id: s1, row_idx: r1, table: t1 },
4797 TableViewData::RowView { schema_id: s2, row_idx: r2, table: t2 },
4798 ) => s1 == s2 && r1 == r2 && Arc::ptr_eq(t1, t2),
4799 (
4800 TableViewData::ColumnRef { schema_id: s1, col_id: c1, table: t1 },
4801 TableViewData::ColumnRef { schema_id: s2, col_id: c2, table: t2 },
4802 ) => s1 == s2 && c1 == c2 && Arc::ptr_eq(t1, t2),
4803 (
4804 TableViewData::IndexedTable { schema_id: s1, index_col: c1, table: t1 },
4805 TableViewData::IndexedTable { schema_id: s2, index_col: c2, table: t2 },
4806 ) => s1 == s2 && c1 == c2 && Arc::ptr_eq(t1, t2),
4807 _ => false,
4808 },
4809 (HeapValue::Content(a), HeapValue::Content(b)) => a == b,
4810 (HeapValue::Instant(a), HeapValue::Instant(b)) => a == b,
4811 (HeapValue::IoHandle(a), HeapValue::IoHandle(b)) => {
4812 Arc::ptr_eq(&a.resource, &b.resource)
4813 }
4814 (HeapValue::Future(a), HeapValue::Future(b)) => a == b,
4815 (HeapValue::Temporal(a), HeapValue::Temporal(b)) => match (a.as_ref(), b.as_ref()) {
4816 (TemporalData::DateTime(x), TemporalData::DateTime(y)) => x == y,
4817 (TemporalData::Duration(x), TemporalData::Duration(y)) => x == y,
4818 (TemporalData::TimeSpan(x), TemporalData::TimeSpan(y)) => x == y,
4819 (TemporalData::Timeframe(x), TemporalData::Timeframe(y)) => x == y,
4820 _ => false,
4821 },
4822 (HeapValue::NativeScalar(a), HeapValue::NativeScalar(b)) => a == b,
4823 (HeapValue::NativeView(a), HeapValue::NativeView(b)) => native_view_eq(a, b),
4824 // V3-S5 ckpt-4 (2026-05-15): `(HeapValue::TypedArray(...), ...)`
4825 // equals arm DELETED in lockstep with the structural_eq arm
4826 // above + the outer `HeapValue::TypedArray` variant +
4827 // `typed_array_structural_eq` (ckpt-1). W12 audit §3.5 +
4828 // ADR-006 §2.7.24 Q25.A SUPERSEDED. Refusal #1 binding.
4829 // ADR-006 §2.7.22 amendment (Round 18 S3, 2026-05-13): Matrix
4830 // and MatrixSlice equality match the structural_eq shape above.
4831 (HeapValue::Matrix(a), HeapValue::Matrix(b)) => matrix_eq(a, b),
4832 (HeapValue::MatrixSlice(a), HeapValue::MatrixSlice(b)) => {
4833 a.as_slice() == b.as_slice()
4834 }
4835 // Cross-type numeric
4836 (HeapValue::NativeScalar(a), HeapValue::BigInt(b)) => native_scalar_bigint_eq(a, b.as_ref()),
4837 (HeapValue::BigInt(a), HeapValue::NativeScalar(b)) => native_scalar_bigint_eq(b, a.as_ref()),
4838 (HeapValue::NativeScalar(a), HeapValue::Decimal(b)) => {
4839 native_scalar_decimal_eq(a, b.as_ref())
4840 }
4841 (HeapValue::Decimal(a), HeapValue::NativeScalar(b)) => {
4842 native_scalar_decimal_eq(b, a.as_ref())
4843 }
4844 _ => false,
4845 }
4846 }
4847}
4848
4849#[cfg(test)]
4850mod closure_variant_regression {
4851 //! N2 — pin Track A.5's deletion of the legacy `HeapValue::Closure`
4852 //! variant. After the Phase 2b HeapKind trim, the `Closure` ordinal
4853 //! is no longer pre-bulldozer-stable (it moved from 3 to 2 along
4854 //! with the rest of the trim), but the discriminator must still map
4855 //! to the `ClosureRaw` pipeline.
4856 use super::*;
4857
4858 #[test]
4859 fn heap_kind_closure_routes_to_closure_raw() {
4860 // The Closure HeapKind discriminator is what HeapValue::ClosureRaw
4861 // returns from `kind()`; verify the routing is intact.
4862 // (The numeric ordinal is structural — see heap_variants.rs — and
4863 // not load-bearing for any external consumer per the Phase 2b
4864 // audit.)
4865 let _ = HeapKind::Closure;
4866 }
4867}
4868
4869#[cfg(test)]
4870mod typed_object_storage_drop {
4871 //! ADR-006 §2.5 / Step 5: pin the Drop impl's behaviour on
4872 //! `TypedObjectStorage`. The contract tested:
4873 //!
4874 //! 1. Heap-mask bits cause `Arc::decrement_strong_count::<T>` for the
4875 //! matching `field_kinds[i]` payload type.
4876 //! 2. Non-heap slots (heap_mask bit clear) are no-ops — even with
4877 //! non-zero raw bits (those bits are scalar field contents, not
4878 //! typed pointers).
4879 //! 3. `field_kinds` itself is shared via Arc — multiple instances of
4880 //! the same schema share one `[NativeKind]` allocation.
4881 use super::*;
4882 use crate::native_kind::NativeKind;
4883 use crate::slot::ValueSlot;
4884 use std::sync::Arc;
4885
4886 #[test]
4887 fn drop_decrements_arc_string_for_heap_string_slot() {
4888 let s: Arc<String> = Arc::new("phase-1a".to_string());
4889 // Hold a second strong ref so the test can observe the count drop.
4890 let witness = Arc::clone(&s);
4891 assert_eq!(Arc::strong_count(&witness), 2);
4892
4893 let slot = ValueSlot::from_string_arc(s);
4894 let kinds: Arc<[NativeKind]> = Arc::from(vec![NativeKind::String]);
4895 let storage = TypedObjectStorage::new(
4896 42,
4897 vec![slot].into_boxed_slice(),
4898 0b1, // bit 0 set
4899 kinds,
4900 );
4901
4902 // Construction stored the Arc raw pointer; nothing dropped yet.
4903 assert_eq!(Arc::strong_count(&witness), 2);
4904
4905 drop(storage);
4906
4907 // Drop walked heap_mask, dispatched on NativeKind::String, and
4908 // released the slot's strong count via Arc::decrement_strong_count.
4909 assert_eq!(Arc::strong_count(&witness), 1);
4910 }
4911
4912 #[test]
4913 fn drop_is_noop_for_non_heap_slot_with_non_zero_bits() {
4914 // Non-heap slot — heap_mask bit clear. Raw bits are an i64 value;
4915 // Drop must not interpret them as a pointer.
4916 let slot = ValueSlot::from_int(0x1234_5678);
4917 let kinds: Arc<[NativeKind]> = Arc::from(vec![NativeKind::Int64]);
4918 let storage = TypedObjectStorage::new(
4919 7,
4920 vec![slot].into_boxed_slice(),
4921 0, // no heap bits
4922 kinds,
4923 );
4924 // Just dropping the storage must not crash / dereference the bits.
4925 drop(storage);
4926 }
4927
4928 #[test]
4929 fn drop_skips_zero_pointer_slots() {
4930 // Heap-mask bit set but the slot was zeroed (e.g. moved-out) —
4931 // Drop must not call Arc::decrement_strong_count on null.
4932 let slot = ValueSlot::from_raw(0);
4933 let kinds: Arc<[NativeKind]> = Arc::from(vec![NativeKind::String]);
4934 let storage = TypedObjectStorage::new(
4935 9,
4936 vec![slot].into_boxed_slice(),
4937 0b1,
4938 kinds,
4939 );
4940 drop(storage);
4941 }
4942
4943 #[test]
4944 fn field_kinds_arc_is_shared_across_instances() {
4945 // Option B' invariant: two instances of the same schema clone the
4946 // same Arc<[NativeKind]> (one payload allocation per schema).
4947 let kinds: Arc<[NativeKind]> = Arc::from(vec![NativeKind::Int64, NativeKind::Bool]);
4948 let kinds_count_before = Arc::strong_count(&kinds);
4949
4950 let storage_a = TypedObjectStorage::new(
4951 1,
4952 vec![ValueSlot::from_int(0), ValueSlot::from_bool(true)].into_boxed_slice(),
4953 0,
4954 Arc::clone(&kinds),
4955 );
4956 let storage_b = TypedObjectStorage::new(
4957 1,
4958 vec![ValueSlot::from_int(1), ValueSlot::from_bool(false)].into_boxed_slice(),
4959 0,
4960 Arc::clone(&kinds),
4961 );
4962
4963 // Two instances + the test's own handle = +2 over the baseline.
4964 assert_eq!(Arc::strong_count(&kinds), kinds_count_before + 2);
4965
4966 // Both instances point at the same payload (no per-instance copy).
4967 assert!(Arc::ptr_eq(&storage_a.field_kinds, &storage_b.field_kinds));
4968 assert!(Arc::ptr_eq(&storage_a.field_kinds, &kinds));
4969
4970 drop(storage_a);
4971 drop(storage_b);
4972 // Each Drop released its share; only the test's handle remains.
4973 assert_eq!(Arc::strong_count(&kinds), kinds_count_before);
4974 }
4975
4976 #[test]
4977 fn drop_handles_mixed_heap_and_scalar_fields() {
4978 // Realistic shape: int + string + bool. Only the string slot
4979 // participates in refcount; the int/bool slots are scalar bits.
4980 let s: Arc<String> = Arc::new("mixed".to_string());
4981 let witness = Arc::clone(&s);
4982 assert_eq!(Arc::strong_count(&witness), 2);
4983
4984 let slots = vec![
4985 ValueSlot::from_int(99),
4986 ValueSlot::from_string_arc(s),
4987 ValueSlot::from_bool(true),
4988 ]
4989 .into_boxed_slice();
4990 let kinds: Arc<[NativeKind]> = Arc::from(vec![
4991 NativeKind::Int64,
4992 NativeKind::String,
4993 NativeKind::Bool,
4994 ]);
4995 let storage = TypedObjectStorage::new(
4996 13,
4997 slots,
4998 0b010, // only bit 1 (the string) is heap
4999 kinds,
5000 );
5001
5002 drop(storage);
5003 assert_eq!(Arc::strong_count(&witness), 1);
5004 }
5005
5006 #[test]
5007 fn drop_decrements_arc_typed_object_for_heap_pointer_slot() {
5008 // Nested TypedObject: outer storage holds a v2-raw
5009 // `*const TypedObjectStorage` in slot 0 via
5010 // `NativeKind::Ptr(HeapKind::TypedObject)`.
5011 //
5012 // W5 v0.3 fix (2026-05-17): migrated the inner construction to
5013 // the v2-raw `_new` allocator per
5014 // `executor/objects/property_access.rs::length_typed_object_empty`
5015 // rationale. The previous shape stored `ValueSlot::from_typed_object(
5016 // Arc<TypedObjectStorage>)` bits in a slot whose field_kinds entry
5017 // was `Ptr(HeapKind::TypedObject)`. The outer storage's
5018 // `drop_fields` dispatch on that entry calls
5019 // `TypedObjectStorage::release_elem` → `v2_release` → `_drop` →
5020 // `std::alloc::dealloc(ptr, Layout::new::<TypedObjectStorage>)`
5021 // on the Arc-allocated inner. That's a wrong-allocator-pair
5022 // free (Arc layout has `ArcInner` header before `T`) → SIGABRT.
5023 //
5024 // The witness probe is rewritten to the v2-raw equivalent: peek
5025 // the header refcount via raw pointer borrow.
5026 use std::sync::atomic::Ordering;
5027
5028 let inner_kinds: Arc<[NativeKind]> = Arc::from(vec![NativeKind::Int64]);
5029 // SAFETY: `_new` returns a refcount=1 raw pointer; the outer
5030 // storage takes that share, and `drop(outer)` runs drop_fields
5031 // which releases it via release_elem → _drop.
5032 unsafe {
5033 let inner_ptr = TypedObjectStorage::_new(
5034 100,
5035 vec![ValueSlot::from_int(7)].into_boxed_slice(),
5036 0,
5037 inner_kinds,
5038 );
5039 // Bump the inner refcount once so we have a witness share
5040 // that observes the outer's release. After outer drop the
5041 // refcount should be back to 1 (our witness only).
5042 crate::v2::refcount::v2_retain(&(*inner_ptr).header);
5043 assert_eq!((*inner_ptr).header.refcount.load(Ordering::SeqCst), 2);
5044
5045 let outer_kinds: Arc<[NativeKind]> =
5046 Arc::from(vec![NativeKind::Ptr(HeapKind::TypedObject)]);
5047 let outer = TypedObjectStorage::new(
5048 101,
5049 vec![ValueSlot::from_typed_object_raw(inner_ptr)].into_boxed_slice(),
5050 0b1,
5051 outer_kinds,
5052 );
5053
5054 drop(outer);
5055 assert_eq!((*inner_ptr).header.refcount.load(Ordering::SeqCst), 1);
5056
5057 // Witness cleanup: retire the share we minted above via
5058 // `release_elem` (refcount=1→0 → `_drop` runs internally) so
5059 // the inner allocation is freed and Miri reports no leak.
5060 use crate::v2::heap_element::HeapElement;
5061 TypedObjectStorage::release_elem(inner_ptr);
5062 }
5063 }
5064
5065 // ── Wave 2 Agent D1 v2-raw HeapHeader-equipped shape change tests ──────────
5066
5067 #[test]
5068 fn header_initializes_with_typed_object_kind_and_refcount_one() {
5069 // Wave 2 Agent D1: confirm `TypedObjectStorage::new` initializes the
5070 // HeapHeader at offset 0 with HEAP_KIND_V2_TYPED_OBJECT and refcount=1.
5071 // Used by Arc<TypedObjectStorage>-path callers (the legacy path); the
5072 // header's refcount sits unused for the Arc lifetime.
5073 let kinds: Arc<[NativeKind]> = Arc::from(vec![NativeKind::Int64]);
5074 let storage = TypedObjectStorage::new(
5075 1,
5076 vec![ValueSlot::from_int(0)].into_boxed_slice(),
5077 0,
5078 kinds,
5079 );
5080 assert_eq!(
5081 storage.header.kind(),
5082 crate::v2::heap_header::HEAP_KIND_V2_TYPED_OBJECT,
5083 );
5084 assert_eq!(storage.header.get_refcount(), 1);
5085 }
5086
5087 #[test]
5088 fn v2_raw_new_and_drop_round_trip() {
5089 // Wave 2 Agent D1: confirm the v2-raw allocator + deallocator pair
5090 // (`_new` + `_drop`) round-trips a simple scalar-only TypedObjectStorage
5091 // without leaking. Mirror of DecimalObj::test_drop_does_not_leak —
5092 // Miri / valgrind validate no leak.
5093 let kinds: Arc<[NativeKind]> = Arc::from(vec![NativeKind::Int64]);
5094 unsafe {
5095 let ptr = TypedObjectStorage::_new(
5096 7,
5097 vec![ValueSlot::from_int(42)].into_boxed_slice(),
5098 0,
5099 kinds,
5100 );
5101 assert!(!ptr.is_null());
5102 assert_eq!(
5103 (*ptr).header.kind(),
5104 crate::v2::heap_header::HEAP_KIND_V2_TYPED_OBJECT,
5105 );
5106 assert_eq!((*ptr).header.get_refcount(), 1);
5107 assert_eq!((*ptr).schema_id, 7);
5108 assert_eq!((&(*ptr).slots).len(), 1);
5109 assert_eq!((&(*ptr).slots)[0].as_i64(), 42);
5110 TypedObjectStorage::_drop(ptr);
5111 // ptr is dangling; cannot dereference further.
5112 }
5113 }
5114
5115 #[test]
5116 fn v2_raw_new_releases_string_share_at_drop() {
5117 // Wave 2 Agent D1: confirm `_drop` runs the heap-mask field walk
5118 // (mirror of `impl Drop`'s legacy behaviour) and retires one Arc
5119 // strong-count share per heap-kinded slot.
5120 let s: Arc<String> = Arc::new("v2-raw-test".to_string());
5121 let witness = Arc::clone(&s);
5122 assert_eq!(Arc::strong_count(&witness), 2);
5123
5124 let kinds: Arc<[NativeKind]> = Arc::from(vec![NativeKind::String]);
5125 unsafe {
5126 let ptr = TypedObjectStorage::_new(
5127 17,
5128 vec![ValueSlot::from_string_arc(s)].into_boxed_slice(),
5129 0b1,
5130 kinds,
5131 );
5132 assert_eq!(Arc::strong_count(&witness), 2);
5133 TypedObjectStorage::_drop(ptr);
5134 }
5135 assert_eq!(Arc::strong_count(&witness), 1);
5136 }
5137
5138 #[test]
5139 fn heap_element_release_elem_deallocates_at_refcount_zero() {
5140 // Wave 2 Agent D1: confirm `HeapElement::release_elem` decrements
5141 // via `v2_release` and deallocates at refcount=0. Mirror of
5142 // DecimalObj::test_heap_element_release_elem_to_zero.
5143 use crate::v2::heap_element::HeapElement;
5144 let kinds: Arc<[NativeKind]> = Arc::from(vec![NativeKind::Int64]);
5145 unsafe {
5146 let ptr = TypedObjectStorage::_new(
5147 3,
5148 vec![ValueSlot::from_int(0)].into_boxed_slice(),
5149 0,
5150 kinds,
5151 );
5152 // refcount=1; release_elem deallocates.
5153 TypedObjectStorage::release_elem(ptr);
5154 // ptr is dangling; valgrind / Miri confirms no leak.
5155 }
5156 }
5157
5158 #[test]
5159 fn heap_element_release_elem_preserves_held_share() {
5160 // Wave 2 Agent D1: confirm `release_elem` decrements but does NOT
5161 // deallocate when refcount > 1. Mirror of
5162 // DecimalObj::test_heap_element_release_elem_held_share.
5163 use crate::v2::heap_element::HeapElement;
5164 use crate::v2::refcount::{v2_get_refcount, v2_retain};
5165 let kinds: Arc<[NativeKind]> = Arc::from(vec![NativeKind::Int64]);
5166 unsafe {
5167 let ptr = TypedObjectStorage::_new(
5168 5,
5169 vec![ValueSlot::from_int(7)].into_boxed_slice(),
5170 0,
5171 kinds,
5172 );
5173 let header = &(*ptr).header as *const crate::v2::heap_header::HeapHeader;
5174
5175 v2_retain(header); // refcount = 2
5176 TypedObjectStorage::release_elem(ptr); // refcount = 1 (does not deallocate)
5177 assert_eq!(v2_get_refcount(header), 1);
5178
5179 // Clean up the held share.
5180 TypedObjectStorage::_drop(ptr);
5181 }
5182 }
5183
5184 #[test]
5185 fn header_field_is_at_offset_zero() {
5186 // Wave 2 Agent D1: confirm the #[repr(C)] field-order invariant —
5187 // the `header: HeapHeader` field sits at offset 0 of
5188 // TypedObjectStorage. This is the precondition for the
5189 // HeapElement::release_elem body's `v2_release(&(*ptr).header)` call
5190 // to read the refcount at the v2-raw canonical offset (offset 0
5191 // mirrors StringObj / DecimalObj precedents).
5192 let kinds: Arc<[NativeKind]> = Arc::from(vec![NativeKind::Int64]);
5193 let storage = TypedObjectStorage::new(
5194 1,
5195 vec![ValueSlot::from_int(0)].into_boxed_slice(),
5196 0,
5197 kinds,
5198 );
5199 let base = &storage as *const _ as usize;
5200 let header_offset = &storage.header as *const _ as usize - base;
5201 assert_eq!(header_offset, 0, "header must be at offset 0 (#[repr(C)] contract)");
5202 }
5203}
5204
5205// Wave 2 Round 3b C2-joint ckpt-3 (2026-05-14): rewritten against the
5206// per-V `HashMapData<V>` mutation API (insert / remove / get_share /
5207// merge). The pre-Q25.B-SUPERSEDED non-generic `HashMapData::insert(k,
5208// Arc<HeapValue>) / remove(k) -> bool / get(k) -> Option<Arc<HeapValue>>`
5209// shape is gone; tests below exercise the per-V semantics on the most
5210// common production-V cases:
5211//
5212// - `V = i64` (Q25.B I64 arm): POD/Copy V — pin len/contains/insert/remove
5213// semantics without refcount-share complications.
5214// - `V = *const StringObj` (Q25.B String arm): HeapElement V — pin the
5215// v2_retain / release_elem refcount-share threading via the
5216// `HashMapValueElem::share_clone` + `release_owned` dispatch.
5217//
5218// The previously-introduced undeclared feature gate (Round 3b ckpt-2)
5219// guarding this test module has been REMOVED per ckpt-3 dispatch
5220// Group F (mandatory non-negotiable): the gate was masquerading as a
5221// feature flag while being functionally `#[cfg(false)]` (no Cargo.toml
5222// declaration), matching CLAUDE.md Forbidden Rationalizations.
5223// ADR-006 §2.7.24 Q25.B SUPERSEDED + audit §C.4 option (a.2).
5224#[cfg(test)]
5225mod hashmap_mutation {
5226 //! Wave 2 Round 3b C2-joint ckpt-3 (2026-05-14): pin the
5227 //! `insert` / `remove` / `get_share` / `merge` API contracts on the
5228 //! post-Q25.B-SUPERSEDED `HashMapData<V>`. Storage-layer counterpart
5229 //! of `v2_set` / `v2_delete` / `v2_get` / `v2_merge` in
5230 //! `shape-vm/executor/objects/hashmap_methods.rs`.
5231 use super::*;
5232 use crate::v2::refcount::v2_get_refcount;
5233 use crate::v2::string_obj::StringObj;
5234 use std::sync::Arc;
5235
5236 // ── V = i64 (POD/Copy) ──────────────────────────────────────────────
5237
5238 #[test]
5239 fn i64_insert_appends_new_entry_and_grows_index() {
5240 let mut m: HashMapData<i64> = HashMapData::new();
5241 unsafe {
5242 assert!(m.insert("a", 1));
5243 assert!(m.insert("b", 2));
5244 }
5245 assert_eq!(m.len(), 2);
5246 // Bucket index has registrations for both keys' hashes.
5247 let h_a = fnv1a_hash(b"a");
5248 let h_b = fnv1a_hash(b"b");
5249 assert!(m.index.get(&h_a).is_some());
5250 assert!(m.index.get(&h_b).is_some());
5251 }
5252
5253 #[test]
5254 fn i64_insert_overwrites_existing_value_and_keeps_len() {
5255 let mut m: HashMapData<i64> = HashMapData::new();
5256 unsafe {
5257 assert!(m.insert("a", 1));
5258 // Overwrite returns false (existing key).
5259 assert!(!m.insert("a", 99));
5260 }
5261 assert_eq!(m.len(), 1);
5262 // get_share returns a fresh Copy of the i64 value.
5263 assert_eq!(m.get_share("a"), Some(99));
5264 }
5265
5266 #[test]
5267 fn i64_remove_present_key_returns_value_and_compacts() {
5268 let mut m: HashMapData<i64> = HashMapData::new();
5269 unsafe {
5270 m.insert("a", 1);
5271 m.insert("b", 2);
5272 assert_eq!(m.remove("a"), Some(1));
5273 }
5274 assert_eq!(m.len(), 1);
5275 assert!(m.get_share("a").is_none());
5276 // "b" should still be reachable — bucket index was renumbered.
5277 assert_eq!(m.get_share("b"), Some(2));
5278 }
5279
5280 #[test]
5281 fn i64_remove_missing_key_returns_none_and_is_noop() {
5282 let mut m: HashMapData<i64> = HashMapData::new();
5283 unsafe {
5284 m.insert("a", 1);
5285 assert_eq!(m.remove("nope"), None);
5286 }
5287 assert_eq!(m.len(), 1);
5288 assert_eq!(m.get_share("a"), Some(1));
5289 }
5290
5291 #[test]
5292 fn i64_merge_copies_other_entries_with_last_write_wins() {
5293 let mut a: HashMapData<i64> = HashMapData::new();
5294 let mut b: HashMapData<i64> = HashMapData::new();
5295 unsafe {
5296 a.insert("x", 1);
5297 a.insert("shared", 10);
5298 b.insert("y", 2);
5299 b.insert("shared", 99);
5300 a.merge(&b);
5301 }
5302 assert_eq!(a.len(), 3);
5303 assert_eq!(a.get_share("x"), Some(1));
5304 assert_eq!(a.get_share("y"), Some(2));
5305 // shared overwritten by b's value
5306 assert_eq!(a.get_share("shared"), Some(99));
5307 }
5308
5309 #[test]
5310 fn i64_smoke_set_set_delete_size() {
5311 // Storage-layer counterpart of W13-hashmap-mutation smoke:
5312 // let m = HashMap(); m.set("a", 1); m.set("b", 2); m.delete("a");
5313 // m.size() == 1
5314 let mut m: HashMapData<i64> = HashMapData::new();
5315 unsafe {
5316 m.insert("a", 1);
5317 m.insert("b", 2);
5318 assert_eq!(m.remove("a"), Some(1));
5319 }
5320 assert_eq!(m.len(), 1);
5321 assert!(m.get_share("a").is_none());
5322 assert_eq!(m.get_share("b"), Some(2));
5323 }
5324
5325 #[test]
5326 fn i64_arc_make_mut_clone_on_write_does_not_disturb_shared_handle() {
5327 // The shape-vm-side handlers Arc::make_mut the receiver share —
5328 // this exercises the per-V Clone impl which allocates fresh
5329 // keys + values buffers and share-clones each element.
5330 let mut owned: Arc<HashMapData<i64>> = Arc::new(HashMapData::new());
5331 unsafe { Arc::make_mut(&mut owned).insert("a", 1) };
5332 // Snapshot share — second observer.
5333 let snapshot = Arc::clone(&owned);
5334 // Mutate via the local share — should clone-on-write.
5335 unsafe { Arc::make_mut(&mut owned).insert("b", 2) };
5336 assert_eq!(owned.len(), 2);
5337 // Snapshot is undisturbed.
5338 assert_eq!(snapshot.len(), 1);
5339 assert_eq!(snapshot.get_share("a"), Some(1));
5340 assert!(snapshot.get_share("b").is_none());
5341 }
5342
5343 // ── V = *const StringObj (HeapElement) ──────────────────────────────
5344
5345 fn s_obj(s: &str) -> *const StringObj {
5346 StringObj::new(s) as *const StringObj
5347 }
5348
5349 #[test]
5350 fn string_insert_v2_retain_share_threading() {
5351 // Pin: insert transfers one share to the map; the original share
5352 // we keep here is the "witness" that survives.
5353 let mut m: HashMapData<*const StringObj> = HashMapData::new();
5354 let v1 = s_obj("hello");
5355 // Bump witness share so we can observe the map's share separately
5356 // — refcount=2 after retain.
5357 unsafe { crate::v2::refcount::v2_retain(&(*v1).header) };
5358 assert_eq!(unsafe { v2_get_refcount(&(*v1).header) }, 2);
5359 // Insert (transfers one share to the map).
5360 unsafe { m.insert("k", v1) };
5361 // Refcount: witness share + map share = 2 (unchanged because
5362 // we transferred 1 to the map, leaving 1 with the witness).
5363 assert_eq!(unsafe { v2_get_refcount(&(*v1).header) }, 2);
5364 // Map drops at end — retires its share. Manually drop to observe.
5365 drop(m);
5366 assert_eq!(unsafe { v2_get_refcount(&(*v1).header) }, 1);
5367 // Release the witness share.
5368 unsafe {
5369 use crate::v2::heap_element::HeapElement;
5370 StringObj::release_elem(v1);
5371 }
5372 }
5373
5374 #[test]
5375 fn string_insert_overwrite_retires_old_share() {
5376 // Pin: insert with existing key retires the old value's share via
5377 // V::release_owned.
5378 let mut m: HashMapData<*const StringObj> = HashMapData::new();
5379 let old = s_obj("old");
5380 let witness = old;
5381 unsafe { crate::v2::refcount::v2_retain(&(*old).header) }; // witness = 2
5382 unsafe { m.insert("k", old) };
5383 assert_eq!(unsafe { v2_get_refcount(&(*witness).header) }, 2);
5384 // Overwrite with new value; the old share inside the map is retired.
5385 let new_val = s_obj("new");
5386 unsafe { m.insert("k", new_val) };
5387 // Map no longer holds the original value's share — witness alone.
5388 assert_eq!(unsafe { v2_get_refcount(&(*witness).header) }, 1);
5389 // Release witness.
5390 unsafe {
5391 use crate::v2::heap_element::HeapElement;
5392 StringObj::release_elem(witness);
5393 }
5394 // m drops new_val + key allocations naturally at scope end.
5395 }
5396
5397 #[test]
5398 fn string_remove_transfers_share_to_caller() {
5399 // Pin: remove returns the value, transferring its share to the caller.
5400 let mut m: HashMapData<*const StringObj> = HashMapData::new();
5401 let v = s_obj("val");
5402 unsafe { crate::v2::refcount::v2_retain(&(*v).header) }; // witness shares = 2
5403 unsafe { m.insert("k", v) };
5404 assert_eq!(unsafe { v2_get_refcount(&(*v).header) }, 2);
5405 let removed = unsafe { m.remove("k") };
5406 assert!(removed.is_some());
5407 let removed_ptr = removed.unwrap();
5408 assert_eq!(removed_ptr, v);
5409 // Refcount unchanged: map released its share + remove transferred
5410 // a share to the caller (this fn) = net 0 change.
5411 assert_eq!(unsafe { v2_get_refcount(&(*v).header) }, 2);
5412 // Release the witness + the removed share.
5413 unsafe {
5414 use crate::v2::heap_element::HeapElement;
5415 StringObj::release_elem(removed_ptr);
5416 StringObj::release_elem(v);
5417 }
5418 }
5419
5420 #[test]
5421 fn string_get_share_bumps_refcount_for_caller() {
5422 // Pin: get_share returns a fresh refcount-share copy, leaving
5423 // the map's share intact.
5424 let mut m: HashMapData<*const StringObj> = HashMapData::new();
5425 let v = s_obj("val");
5426 unsafe { m.insert("k", v) }; // map owns the only share now.
5427 assert_eq!(unsafe { v2_get_refcount(&(*v).header) }, 1);
5428 let got = m.get_share("k").expect("present");
5429 assert_eq!(got, v);
5430 // Refcount bumped — map (1) + caller's share (1) = 2.
5431 assert_eq!(unsafe { v2_get_refcount(&(*v).header) }, 2);
5432 // Release caller's share.
5433 unsafe {
5434 use crate::v2::heap_element::HeapElement;
5435 StringObj::release_elem(got);
5436 }
5437 assert_eq!(unsafe { v2_get_refcount(&(*v).header) }, 1);
5438 // Map drops at scope end, retiring last share.
5439 }
5440
5441 #[test]
5442 fn string_merge_share_clones_each_other_entry() {
5443 // Pin: merge bumps refcount on each value cloned from other.
5444 let mut a: HashMapData<*const StringObj> = HashMapData::new();
5445 let mut b: HashMapData<*const StringObj> = HashMapData::new();
5446 let v_x = s_obj("x_val");
5447 let v_y = s_obj("y_val");
5448 unsafe {
5449 a.insert("x", v_x);
5450 b.insert("y", v_y);
5451 assert_eq!(v2_get_refcount(&(*v_x).header), 1);
5452 assert_eq!(v2_get_refcount(&(*v_y).header), 1);
5453 a.merge(&b);
5454 }
5455 assert_eq!(a.len(), 2);
5456 // After merge: y is share-cloned into a — refcount = 2 (a + b).
5457 assert_eq!(unsafe { v2_get_refcount(&(*v_y).header) }, 2);
5458 // x is unchanged (only in a).
5459 assert_eq!(unsafe { v2_get_refcount(&(*v_x).header) }, 1);
5460 }
5461
5462 // ── V = HashMapKindedRef (recursive carrier) ──────────────────────────
5463 //
5464 // Wave N hashmap-value-v-arm follow-up (cluster-2 closure-wave-C,
5465 // 2026-05-16). Pin the per-V `insert` / `len` / `get_share` API on
5466 // `HashMapData<HashMapKindedRef>`. Storage-layer counterpart of
5467 // `v2_group_by` in `shape-vm/executor/objects/hashmap_methods.rs`.
5468
5469 #[test]
5470 fn hashmap_value_v_insert_appends_and_grows_index() {
5471 let mut outer: HashMapData<HashMapKindedRef> = HashMapData::new();
5472 // Two inner buckets — one for "small", one for "large".
5473 let mut inner_small: HashMapData<i64> = HashMapData::new();
5474 let mut inner_large: HashMapData<i64> = HashMapData::new();
5475 unsafe {
5476 inner_small.insert("a", 1);
5477 inner_small.insert("b", 2);
5478 inner_large.insert("c", 100);
5479 outer.insert(
5480 "small",
5481 HashMapKindedRef::I64(Arc::new(inner_small)),
5482 );
5483 outer.insert(
5484 "large",
5485 HashMapKindedRef::I64(Arc::new(inner_large)),
5486 );
5487 }
5488 assert_eq!(outer.len(), 2);
5489 // Bucket index has registrations for both group keys.
5490 let h_small = fnv1a_hash(b"small");
5491 let h_large = fnv1a_hash(b"large");
5492 assert!(outer.index.get(&h_small).is_some());
5493 assert!(outer.index.get(&h_large).is_some());
5494 // get_share returns a fresh per-variant Arc::clone-bumped copy.
5495 let small_ref = outer.get_share("small").expect("small bucket present");
5496 match small_ref {
5497 HashMapKindedRef::I64(arc) => assert_eq!(arc.len(), 2),
5498 other => panic!("unexpected variant {:?}", other.values_kind()),
5499 }
5500 }
5501
5502 #[test]
5503 fn hashmap_value_v_remove_returns_inner_and_compacts() {
5504 let mut outer: HashMapData<HashMapKindedRef> = HashMapData::new();
5505 let mut inner_a: HashMapData<i64> = HashMapData::new();
5506 let mut inner_b: HashMapData<i64> = HashMapData::new();
5507 unsafe {
5508 inner_a.insert("x", 1);
5509 inner_b.insert("y", 2);
5510 outer.insert("group-a", HashMapKindedRef::I64(Arc::new(inner_a)));
5511 outer.insert("group-b", HashMapKindedRef::I64(Arc::new(inner_b)));
5512 let removed = outer.remove("group-a");
5513 assert!(removed.is_some());
5514 // Removed bucket has the expected inner shape.
5515 match removed.unwrap() {
5516 HashMapKindedRef::I64(arc) => {
5517 assert_eq!(arc.len(), 1);
5518 assert_eq!(arc.get_share("x"), Some(1));
5519 }
5520 other => panic!("unexpected variant {:?}", other.values_kind()),
5521 }
5522 }
5523 assert_eq!(outer.len(), 1);
5524 // "group-b" reachable post-renumber.
5525 let b_ref = outer.get_share("group-b").expect("group-b present");
5526 match b_ref {
5527 HashMapKindedRef::I64(arc) => assert_eq!(arc.get_share("y"), Some(2)),
5528 other => panic!("unexpected variant {:?}", other.values_kind()),
5529 }
5530 }
5531
5532 #[test]
5533 fn hashmap_value_v_clone_share_clones_inner_arcs() {
5534 // HashMapData<V>::Clone walks elements via share_clone — for
5535 // V = HashMapKindedRef this calls HashMapKindedRef::clone which
5536 // is per-variant Arc::clone on the inner Arc<HashMapData<V_inner>>.
5537 // The clone yields fresh buffer allocations holding bumped Arcs.
5538 let mut outer: HashMapData<HashMapKindedRef> = HashMapData::new();
5539 let inner: Arc<HashMapData<i64>> = {
5540 let mut d: HashMapData<i64> = HashMapData::new();
5541 unsafe { d.insert("k", 42) };
5542 Arc::new(d)
5543 };
5544 // Refcount before insert: 1 (only `inner` owns).
5545 assert_eq!(Arc::strong_count(&inner), 1);
5546 unsafe { outer.insert("g", HashMapKindedRef::I64(Arc::clone(&inner))) };
5547 // Refcount after insert: 2 (inner + outer's buffer share).
5548 assert_eq!(Arc::strong_count(&inner), 2);
5549 // Clone outer — share_clone bumps the inner Arc one more time.
5550 let _outer_clone = outer.clone();
5551 assert_eq!(Arc::strong_count(&inner), 3);
5552 // Drop clone; refcount drops back to 2.
5553 drop(_outer_clone);
5554 assert_eq!(Arc::strong_count(&inner), 2);
5555 }
5556
5557 #[test]
5558 fn hashmap_value_v_drop_releases_inner_arcs() {
5559 // HashMapData<HashMapKindedRef>::Drop calls
5560 // <HashMapKindedRef as HashMapValueElem>::release_typed_array,
5561 // which walks the buffer with ptr::read and lets each element
5562 // drop (auto-derived → Arc::drop on inner Arc<HashMapData<V_inner>>).
5563 let inner: Arc<HashMapData<i64>> = {
5564 let mut d: HashMapData<i64> = HashMapData::new();
5565 unsafe { d.insert("k", 7) };
5566 Arc::new(d)
5567 };
5568 assert_eq!(Arc::strong_count(&inner), 1);
5569 {
5570 let mut outer: HashMapData<HashMapKindedRef> = HashMapData::new();
5571 unsafe {
5572 outer.insert("g", HashMapKindedRef::I64(Arc::clone(&inner)));
5573 }
5574 assert_eq!(Arc::strong_count(&inner), 2);
5575 // outer drops at scope-end; the inner Arc share retires.
5576 }
5577 assert_eq!(Arc::strong_count(&inner), 1);
5578 }
5579}
5580
5581#[cfg(test)]
5582mod hashset_mutation {
5583 //! W13-hashset-rebuild (ADR-006 §2.7.15 / Q16, 2026-05-10): pin the
5584 //! `insert` / `remove` / `contains` API contracts on `HashSetData`.
5585 //! Mirror of `hashmap_mutation` with the values column dropped.
5586 use super::*;
5587 use std::sync::Arc;
5588 fn k(s: &str) -> Arc<String> {
5589 Arc::new(s.to_string())
5590 }
5591
5592 #[test]
5593 fn empty_set_has_zero_len_and_is_empty() {
5594 let s = HashSetData::new();
5595 assert_eq!(s.len(), 0);
5596 assert!(s.is_empty());
5597 assert!(!s.contains("a"));
5598 }
5599
5600 #[test]
5601 fn insert_returns_true_for_new_key_false_for_duplicate() {
5602 let mut s = HashSetData::new();
5603 assert!(s.insert(k("a")));
5604 assert!(s.insert(k("b")));
5605 assert_eq!(s.len(), 2);
5606 // Duplicate insert is a no-op.
5607 assert!(!s.insert(k("a")));
5608 assert_eq!(s.len(), 2);
5609 }
5610
5611 #[test]
5612 fn contains_finds_inserted_keys() {
5613 let mut s = HashSetData::new();
5614 s.insert(k("a"));
5615 s.insert(k("b"));
5616 assert!(s.contains("a"));
5617 assert!(s.contains("b"));
5618 assert!(!s.contains("c"));
5619 }
5620
5621 #[test]
5622 fn remove_returns_true_for_present_false_for_missing() {
5623 let mut s = HashSetData::new();
5624 s.insert(k("a"));
5625 s.insert(k("b"));
5626 assert!(s.remove("a"));
5627 assert!(!s.contains("a"));
5628 assert!(s.contains("b"));
5629 assert_eq!(s.len(), 1);
5630 // Missing-key remove is a no-op.
5631 assert!(!s.remove("c"));
5632 assert_eq!(s.len(), 1);
5633 }
5634
5635 #[test]
5636 fn from_keys_collapses_duplicates_first_wins() {
5637 let s = HashSetData::from_keys(vec![k("a"), k("b"), k("a"), k("c")]);
5638 assert_eq!(s.len(), 3);
5639 assert_eq!(s.keys[0].as_str(), "a");
5640 assert_eq!(s.keys[1].as_str(), "b");
5641 assert_eq!(s.keys[2].as_str(), "c");
5642 }
5643
5644 #[test]
5645 fn smoke_target_add_two_then_size() {
5646 // Storage-layer counterpart of the W13-hashset-rebuild smoke
5647 // target: `let s = Set(); s.add("a"); s.add("b"); print(s.size())`
5648 // outputs 2.
5649 let mut s = HashSetData::new();
5650 s.insert(k("a"));
5651 s.insert(k("b"));
5652 assert_eq!(s.len(), 2);
5653 }
5654
5655 #[test]
5656 fn arc_make_mut_clone_on_write_preserves_other_share() {
5657 // Pin the §2.7.4 / playbook clone-on-write invariant: when
5658 // `Arc<HashSetData>` has multiple shares, `Arc::make_mut`
5659 // clones the inner `HashSetData` so the other share stays
5660 // immutable. Mirror of `hashmap_mutation`'s clone-on-write
5661 // test.
5662 let mut a = Arc::new(HashSetData::new());
5663 Arc::make_mut(&mut a).insert(k("a"));
5664 let snapshot = Arc::clone(&a);
5665 // After the snapshot, mutating `a` clones the inner data.
5666 Arc::make_mut(&mut a).insert(k("b"));
5667 assert_eq!(a.len(), 2);
5668 // Snapshot retains the pre-mutation length.
5669 assert_eq!(snapshot.len(), 1);
5670 assert!(snapshot.contains("a"));
5671 assert!(!snapshot.contains("b"));
5672 }
5673}
5674
5675#[cfg(test)]
5676mod deque_mutation {
5677 //! W15-deque (ADR-006 §2.7.19 / Q20, 2026-05-10): pin the
5678 //! `push_front` / `push_back` / `pop_front` / `pop_back` API
5679 //! contracts on `DequeData`. Mirror of `hashset_mutation` with
5680 //! the bucket-index dropped (Deque is order-preserving with no
5681 //! deduplication, so no parallel hash structure is needed).
5682 use super::*;
5683 use std::sync::Arc;
5684
5685 fn s(text: &str) -> Arc<HeapValue> {
5686 Arc::new(HeapValue::String(Arc::new(text.to_string())))
5687 }
5688
5689 fn i(n: i64) -> Arc<HeapValue> {
5690 Arc::new(HeapValue::BigInt(Arc::new(n)))
5691 }
5692
5693 #[test]
5694 fn empty_deque_has_zero_len_and_is_empty() {
5695 let d = DequeData::new();
5696 assert_eq!(d.len(), 0);
5697 assert!(d.is_empty());
5698 assert!(d.peek_front().is_none());
5699 assert!(d.peek_back().is_none());
5700 assert!(d.get(0).is_none());
5701 }
5702
5703 #[test]
5704 fn push_back_and_pop_front_preserve_fifo_order() {
5705 // FIFO: push 1,2,3 to back, pop from front yields 1,2,3.
5706 let mut d = DequeData::new();
5707 d.push_back(i(1));
5708 d.push_back(i(2));
5709 d.push_back(i(3));
5710 assert_eq!(d.len(), 3);
5711 let p1 = d.pop_front().expect("front");
5712 assert!(matches!(p1.as_ref(), HeapValue::BigInt(b) if **b == 1));
5713 let p2 = d.pop_front().expect("front");
5714 assert!(matches!(p2.as_ref(), HeapValue::BigInt(b) if **b == 2));
5715 let p3 = d.pop_front().expect("front");
5716 assert!(matches!(p3.as_ref(), HeapValue::BigInt(b) if **b == 3));
5717 assert!(d.pop_front().is_none());
5718 }
5719
5720 #[test]
5721 fn push_front_and_pop_back_preserve_reverse_order() {
5722 // Reverse: push 1,2,3 to front, pop from back yields 1,2,3.
5723 let mut d = DequeData::new();
5724 d.push_front(i(1));
5725 d.push_front(i(2));
5726 d.push_front(i(3));
5727 // Now layout is [3, 2, 1] front-to-back.
5728 let p1 = d.pop_back().expect("back");
5729 assert!(matches!(p1.as_ref(), HeapValue::BigInt(b) if **b == 1));
5730 let p2 = d.pop_back().expect("back");
5731 assert!(matches!(p2.as_ref(), HeapValue::BigInt(b) if **b == 2));
5732 let p3 = d.pop_back().expect("back");
5733 assert!(matches!(p3.as_ref(), HeapValue::BigInt(b) if **b == 3));
5734 }
5735
5736 #[test]
5737 fn smoke_target_push_back_push_front_pop_back() {
5738 // Storage-layer counterpart of the W15-deque smoke target:
5739 // `let d = Deque(); d.push_back(1); d.push_front(0); d.pop_back()`
5740 // returns `1`. After the two pushes the layout is [0, 1]
5741 // front-to-back; pop_back yields 1.
5742 let mut d = DequeData::new();
5743 d.push_back(i(1));
5744 d.push_front(i(0));
5745 assert_eq!(d.len(), 2);
5746 let popped = d.pop_back().expect("back");
5747 assert!(matches!(popped.as_ref(), HeapValue::BigInt(b) if **b == 1));
5748 // Front element retained.
5749 assert_eq!(d.len(), 1);
5750 let front = d.peek_front().expect("front").clone();
5751 assert!(matches!(front.as_ref(), HeapValue::BigInt(b) if **b == 0));
5752 }
5753
5754 #[test]
5755 fn peek_front_back_and_get_borrow_without_removing() {
5756 let mut d = DequeData::new();
5757 d.push_back(s("a"));
5758 d.push_back(s("b"));
5759 d.push_back(s("c"));
5760 assert_eq!(d.len(), 3);
5761 let front = d.peek_front().expect("front");
5762 assert!(matches!(front.as_ref(), HeapValue::String(t) if t.as_str() == "a"));
5763 let back = d.peek_back().expect("back");
5764 assert!(matches!(back.as_ref(), HeapValue::String(t) if t.as_str() == "c"));
5765 let mid = d.get(1).expect("idx 1");
5766 assert!(matches!(mid.as_ref(), HeapValue::String(t) if t.as_str() == "b"));
5767 // Length unchanged after read-only borrows.
5768 assert_eq!(d.len(), 3);
5769 }
5770
5771 #[test]
5772 fn from_items_preserves_insertion_order() {
5773 let d = DequeData::from_items(vec![s("a"), s("b"), s("c")]);
5774 assert_eq!(d.len(), 3);
5775 assert!(matches!(d.get(0).unwrap().as_ref(), HeapValue::String(t) if t.as_str() == "a"));
5776 assert!(matches!(d.get(1).unwrap().as_ref(), HeapValue::String(t) if t.as_str() == "b"));
5777 assert!(matches!(d.get(2).unwrap().as_ref(), HeapValue::String(t) if t.as_str() == "c"));
5778 }
5779
5780 #[test]
5781 fn arc_make_mut_clone_on_write_preserves_other_share() {
5782 // Pin the §2.7.4 / playbook clone-on-write invariant: when
5783 // `Arc<DequeData>` has multiple shares, `Arc::make_mut` clones
5784 // the inner `DequeData` so the other share stays immutable.
5785 // Mirror of `hashset_mutation`'s clone-on-write test.
5786 let mut a = Arc::new(DequeData::new());
5787 Arc::make_mut(&mut a).push_back(i(1));
5788 let snapshot = Arc::clone(&a);
5789 // After the snapshot, mutating `a` clones the inner data.
5790 Arc::make_mut(&mut a).push_back(i(2));
5791 assert_eq!(a.len(), 2);
5792 // Snapshot retains the pre-mutation length.
5793 assert_eq!(snapshot.len(), 1);
5794 }
5795}
5796
5797mod priority_queue_mutation {
5798 //! W15-priority-queue (ADR-006 §2.7.18 / Q19, 2026-05-10): pin the
5799 //! `push` / `pop` / `peek` / heap-invariant API contracts on
5800 //! `PriorityQueueData`. Mirror of `hashset_mutation` for the
5801 //! cardinality-amendment shape, with i64-priority-only payload
5802 //! semantics per the §2.7.18 ruling.
5803 use super::*;
5804 use std::sync::Arc;
5805
5806 #[test]
5807 fn empty_pq_has_zero_len_and_is_empty() {
5808 let pq = PriorityQueueData::new();
5809 assert_eq!(pq.len(), 0);
5810 assert!(pq.is_empty());
5811 assert_eq!(pq.peek(), None);
5812 }
5813
5814 #[test]
5815 fn push_increases_len_and_pop_returns_min() {
5816 // Storage-layer counterpart of the W15-priority-queue smoke
5817 // target: `pq.push(3); pq.push(1); pq.push(2); pq.pop() == 1`.
5818 let mut pq = PriorityQueueData::new();
5819 pq.push(3);
5820 pq.push(1);
5821 pq.push(2);
5822 assert_eq!(pq.len(), 3);
5823 assert_eq!(pq.peek(), Some(1));
5824 assert_eq!(pq.pop(), Some(1));
5825 assert_eq!(pq.len(), 2);
5826 }
5827
5828 #[test]
5829 fn pop_returns_none_on_empty() {
5830 let mut pq = PriorityQueueData::new();
5831 assert_eq!(pq.pop(), None);
5832 }
5833
5834 #[test]
5835 fn pop_yields_ascending_order() {
5836 // Pin the min-heap invariant: repeated `pop()` yields keys
5837 // in ascending order regardless of insertion order.
5838 let mut pq = PriorityQueueData::new();
5839 for v in [5, 3, 7, 1, 9, 4, 2, 8, 6] {
5840 pq.push(v);
5841 }
5842 let mut out = Vec::new();
5843 while let Some(v) = pq.pop() {
5844 out.push(v);
5845 }
5846 assert_eq!(out, vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);
5847 }
5848
5849 #[test]
5850 fn to_sorted_vec_returns_ascending_without_consuming() {
5851 let mut pq = PriorityQueueData::new();
5852 for v in [3, 1, 4, 1, 5, 9, 2, 6] {
5853 pq.push(v);
5854 }
5855 let sorted = pq.to_sorted_vec();
5856 assert_eq!(sorted, vec![1, 1, 2, 3, 4, 5, 6, 9]);
5857 // Original PQ is undisturbed.
5858 assert_eq!(pq.len(), 8);
5859 }
5860
5861}
5862
5863mod channel_storage {
5864 //! W15-channel-rebuild (ADR-006 §2.7.20 / Q21, 2026-05-10): pin the
5865 //! `send` / `try_recv` / `close` / `is_closed` / `len` / `is_empty`
5866 //! API contracts on `ChannelData`. Sync same-thread path only —
5867 //! cross-task blocking `recv()` is the §2.7.4 task-scheduler
5868 //! boundary tracked separately.
5869 use super::*;
5870 use crate::kinded_slot::KindedSlot;
5871 use std::sync::Arc;
5872
5873 #[test]
5874 fn empty_channel_has_zero_len_and_is_empty_open() {
5875 let c = ChannelData::new();
5876 assert_eq!(c.len(), 0);
5877 assert!(c.is_empty());
5878 assert!(!c.is_closed());
5879 assert!(c.try_recv().is_none());
5880 }
5881
5882 #[test]
5883 fn send_then_try_recv_round_trips_int() {
5884 // Storage-layer counterpart of the W15-channel-rebuild smoke
5885 // target: `let c = Channel(); c.send(1); c.recv()` returns 1.
5886 let c = ChannelData::new();
5887 c.send(KindedSlot::from_int(1)).expect("send on open channel");
5888 let got = c.try_recv().expect("queued element");
5889 assert_eq!(got.as_i64(), Some(1));
5890 assert!(c.is_empty());
5891 }
5892
5893 #[test]
5894 fn fifo_send_recv_order() {
5895 // Producer pushes 1, 2, 3; consumer drains in the same order.
5896 let c = ChannelData::new();
5897 c.send(KindedSlot::from_int(1)).unwrap();
5898 c.send(KindedSlot::from_int(2)).unwrap();
5899 c.send(KindedSlot::from_int(3)).unwrap();
5900 assert_eq!(c.len(), 3);
5901 assert_eq!(c.try_recv().unwrap().as_i64(), Some(1));
5902 assert_eq!(c.try_recv().unwrap().as_i64(), Some(2));
5903 assert_eq!(c.try_recv().unwrap().as_i64(), Some(3));
5904 assert!(c.try_recv().is_none());
5905 }
5906
5907 #[test]
5908 fn close_blocks_further_sends_but_drains_queued() {
5909 // After close, send returns Err but queued elements still
5910 // recv cleanly (canonical drain-on-close semantics).
5911 let c = ChannelData::new();
5912 c.send(KindedSlot::from_int(7)).unwrap();
5913 c.close();
5914 assert!(c.is_closed());
5915 // Further send is rejected; the rejected slot is dropped
5916 // (refcount discipline preserved through KindedSlot::Drop).
5917 assert!(c.send(KindedSlot::from_int(8)).is_err());
5918 // Queued element still drains.
5919 assert_eq!(c.try_recv().unwrap().as_i64(), Some(7));
5920 assert!(c.try_recv().is_none());
5921 }
5922
5923 #[test]
5924 fn shared_arc_send_recv_observes_other_share() {
5925 // Two `Arc<ChannelData>` shares of the same channel observe
5926 // each other's mutations — the producer/consumer-endpoints
5927 // shape. Distinct from HashSet/HashMap (which are Arc::make_mut
5928 // clone-on-write) — Channel uses interior mutability via
5929 // Mutex.
5930 let producer = Arc::new(ChannelData::new());
5931 let consumer = Arc::clone(&producer);
5932 producer.send(KindedSlot::from_int(42)).unwrap();
5933 assert_eq!(consumer.len(), 1);
5934 let got = consumer.try_recv().unwrap();
5935 assert_eq!(got.as_i64(), Some(42));
5936 // After consumer drained, producer-side observes empty.
5937 assert!(producer.is_empty());
5938 }
5939
5940 #[test]
5941 fn dropping_channel_with_heap_payloads_retires_shares() {
5942 // Refcount discipline: KindedSlot payloads queued in the
5943 // channel own one strong-count share; dropping the channel
5944 // (last `Arc<ChannelData>` share retired) must drop each
5945 // queued slot and retire its inner share. Any Arc-leak would
5946 // surface as a non-zero strong-count after the channel
5947 // dropped.
5948 let s = Arc::new("payload".to_string());
5949 let weak = Arc::downgrade(&s);
5950 let c = ChannelData::new();
5951 c.send(KindedSlot::from_string_arc(s)).unwrap();
5952 // The queued slot owns the only strong share.
5953 assert_eq!(weak.strong_count(), 1);
5954 drop(c);
5955 assert_eq!(
5956 weak.strong_count(),
5957 0,
5958 "dropped Channel must retire queued KindedSlot shares"
5959 );
5960 }
5961
5962 #[test]
5963 fn closed_send_drops_rejected_payload_share() {
5964 // After close, a rejected send must NOT leak the payload
5965 // share — KindedSlot::Drop runs on the rejected slot.
5966 let c = ChannelData::new();
5967 c.close();
5968 let s = Arc::new("rejected".to_string());
5969 let weak = Arc::downgrade(&s);
5970 let slot = KindedSlot::from_string_arc(s);
5971 assert_eq!(weak.strong_count(), 1);
5972 // The rejected send consumes the slot and drops it internally.
5973 assert!(c.send(slot).is_err());
5974 assert_eq!(
5975 weak.strong_count(),
5976 0,
5977 "rejected-send slot must drop, not leak"
5978 );
5979 }
5980}
5981
5982// ── W14-variant-codegen unit tests (ADR-006 §2.7.17 / Q18) ──────────────────
5983
5984#[cfg(test)]
5985mod result_option_storage {
5986 //! W14-variant-codegen (ADR-006 §2.7.17 / Q18, 2026-05-10): pin the
5987 //! `ResultData::ok` / `err` and `OptionData::some` / `none` API
5988 //! contracts.
5989 use super::*;
5990 use crate::kinded_slot::KindedSlot;
5991 use std::sync::Arc;
5992
5993 #[test]
5994 fn ok_carrier_is_ok_true() {
5995 let payload = KindedSlot::from_int(42);
5996 let r = ResultData::ok(payload);
5997 assert!(r.is_ok);
5998 // Payload kind preserved.
5999 assert_eq!(r.payload.as_i64(), Some(42));
6000 }
6001
6002 #[test]
6003 fn err_carrier_is_ok_false() {
6004 let payload = KindedSlot::from_string_arc(Arc::new("oops".to_string()));
6005 let r = ResultData::err(payload);
6006 assert!(!r.is_ok);
6007 // Payload string preserved.
6008 assert_eq!(r.payload.as_str(), Some("oops"));
6009 }
6010
6011 #[test]
6012 fn result_clone_bumps_payload_share() {
6013 // The storage carrier's Clone path goes through KindedSlot's
6014 // explicit Clone impl, which dispatches retain on the payload's
6015 // kind. Verify that cloning the wrapper preserves the payload's
6016 // Arc identity (the inner Arc<String> share is bumped, not
6017 // duplicated).
6018 let payload_arc = Arc::new("hello".to_string());
6019 let payload = KindedSlot::from_string_arc(Arc::clone(&payload_arc));
6020 let r1 = ResultData::ok(payload);
6021 let r2 = r1.clone();
6022 // Pointer equality on the inner String: both wrappers
6023 // reference the same Arc<String> (the kinded clone bumped
6024 // the share, did not deep-copy the string body).
6025 assert_eq!(r1.payload.as_str(), Some("hello"));
6026 assert_eq!(r2.payload.as_str(), Some("hello"));
6027 // Original Arc retains an extra share from each KindedSlot
6028 // (1 own + 2 wrappers = 3 strong refs; the local `payload_arc`
6029 // is the third).
6030 assert!(Arc::strong_count(&payload_arc) >= 2);
6031 }
6032
6033 #[test]
6034 fn some_carrier_is_some_true() {
6035 let payload = KindedSlot::from_bool(true);
6036 let o = OptionData::some(payload);
6037 assert!(o.is_some);
6038 assert_eq!(o.payload.as_bool(), Some(true));
6039 }
6040
6041 #[test]
6042 fn none_carrier_is_some_false() {
6043 let o = OptionData::none();
6044 assert!(!o.is_some);
6045 // None payload is a placeholder Bool-kind zero-bits slot —
6046 // KindedSlot::Drop is a no-op on it (verified by the slot
6047 // raw bits and kind).
6048 assert_eq!(o.payload.slot().raw(), 0);
6049 }
6050
6051 #[test]
6052 fn smoke_target_ok_int_then_unwrap() {
6053 // Storage-layer counterpart of the W14-variant-codegen smoke
6054 // target: `let r = Ok(42); if r.is_ok() { print(r.unwrap_ok()) }`
6055 // outputs 42. The is_ok / unwrap_ok pair surfaces at the
6056 // storage tier as `r.is_ok` + `r.payload.as_i64()`.
6057 let r = ResultData::ok(KindedSlot::from_int(42));
6058 assert!(r.is_ok);
6059 assert_eq!(r.payload.as_i64(), Some(42));
6060 }
6061
6062 #[test]
6063 fn arc_wrap_typed_pointer_round_trip() {
6064 // Pin the typed-Arc raw-pointer dispatch contract: wrap an
6065 // Arc<ResultData> via Arc::into_raw, recover via
6066 // Arc::from_raw, verify pointer identity. This is the slot-
6067 // bits transit path that the §2.7.17 dispatch tables retire
6068 // in `clone_with_kind` / `drop_with_kind`.
6069 let arc = Arc::new(ResultData::ok(KindedSlot::from_int(7)));
6070 let bits = Arc::into_raw(arc) as u64;
6071 // Recover and verify is_ok.
6072 let arc2: Arc<ResultData> =
6073 unsafe { Arc::from_raw(bits as *const ResultData) };
6074 assert!(arc2.is_ok);
6075 assert_eq!(arc2.payload.as_i64(), Some(7));
6076 drop(arc2);
6077 }
6078}
6079
6080#[cfg(test)]
6081mod concurrency_storage {
6082 //! W17-concurrency (ADR-006 §2.7.25, 2026-05-11): pin the `lock`
6083 //! / `try_lock` / `set` / `get` API contracts on `MutexData`, the
6084 //! `load` / `store` / `fetch_add` / `fetch_sub` /
6085 //! `compare_exchange` contracts on `AtomicData`, and the
6086 //! `is_initialized` / `cached` / `take_initializer` /
6087 //! `store_result` contracts on `LazyData`. Storage-tier only —
6088 //! closure-call integration for `Lazy.get` lives at the handler
6089 //! tier (`executor/objects/concurrency_methods.rs`).
6090 use super::*;
6091 use crate::kinded_slot::KindedSlot;
6092 use std::sync::Arc;
6093
6094 // ── MutexData ──────────────────────────────────────────────────
6095
6096 #[test]
6097 fn mutex_new_holds_initial_value() {
6098 let m = MutexData::new(KindedSlot::from_int(42));
6099 assert_eq!(m.get().as_i64(), Some(42));
6100 }
6101
6102 #[test]
6103 fn mutex_lock_is_noop_at_landing() {
6104 let m = MutexData::new(KindedSlot::from_int(0));
6105 m.lock();
6106 // lock returns; observable state unchanged.
6107 assert_eq!(m.get().as_i64(), Some(0));
6108 }
6109
6110 #[test]
6111 fn mutex_try_lock_returns_true_uncontended() {
6112 let m = MutexData::new(KindedSlot::from_int(0));
6113 assert!(m.try_lock());
6114 }
6115
6116 #[test]
6117 fn mutex_set_replaces_value_and_drops_prior() {
6118 // Storage-layer counterpart of the smoke target's
6119 // `m.set(5); print(m.value)`.
6120 let m = MutexData::new(KindedSlot::from_int(0));
6121 m.set(KindedSlot::from_int(5));
6122 assert_eq!(m.get().as_i64(), Some(5));
6123 }
6124
6125 #[test]
6126 fn mutex_set_with_heap_payload_retires_shares() {
6127 // The prior slot drops cleanly when `set` replaces it; no
6128 // Arc-leak. A heap-bearing payload's strong-count returns to
6129 // zero after `set` and `drop(mutex)`.
6130 let s = Arc::new("initial".to_string());
6131 let weak = Arc::downgrade(&s);
6132 let m = MutexData::new(KindedSlot::from_string_arc(s));
6133 assert_eq!(weak.strong_count(), 1);
6134 m.set(KindedSlot::from_int(7));
6135 assert_eq!(
6136 weak.strong_count(),
6137 0,
6138 "Mutex.set must drop prior heap payload share"
6139 );
6140 drop(m);
6141 }
6142
6143 #[test]
6144 fn mutex_shared_arc_observes_set_mutations() {
6145 // Two `Arc<MutexData>` shares of the same mutex observe each
6146 // other's mutations — the producer/consumer-endpoints shape
6147 // (mirror of Channel).
6148 let m1 = Arc::new(MutexData::new(KindedSlot::from_int(0)));
6149 let m2 = Arc::clone(&m1);
6150 m1.set(KindedSlot::from_int(99));
6151 assert_eq!(m2.get().as_i64(), Some(99));
6152 }
6153
6154 // ── AtomicData ─────────────────────────────────────────────────
6155
6156 #[test]
6157 fn atomic_new_holds_initial_value() {
6158 let a = AtomicData::new(7);
6159 assert_eq!(a.load(), 7);
6160 }
6161
6162 #[test]
6163 fn atomic_store_replaces_value() {
6164 let a = AtomicData::new(0);
6165 a.store(42);
6166 assert_eq!(a.load(), 42);
6167 }
6168
6169 #[test]
6170 fn atomic_fetch_add_returns_prior_and_increments() {
6171 // Smoke-target storage layer: a starts at 0, fetch_add(1)
6172 // returns 0 (prior), load() returns 1.
6173 let a = AtomicData::new(0);
6174 let prior = a.fetch_add(1);
6175 assert_eq!(prior, 0);
6176 assert_eq!(a.load(), 1);
6177 }
6178
6179 #[test]
6180 fn atomic_fetch_sub_returns_prior_and_decrements() {
6181 let a = AtomicData::new(10);
6182 let prior = a.fetch_sub(3);
6183 assert_eq!(prior, 10);
6184 assert_eq!(a.load(), 7);
6185 }
6186
6187 #[test]
6188 fn atomic_compare_exchange_swaps_on_match() {
6189 let a = AtomicData::new(5);
6190 let prior = a.compare_exchange(5, 99);
6191 assert_eq!(prior, 5);
6192 assert_eq!(a.load(), 99);
6193 }
6194
6195 #[test]
6196 fn atomic_compare_exchange_keeps_on_mismatch() {
6197 let a = AtomicData::new(5);
6198 let prior = a.compare_exchange(7, 99);
6199 assert_eq!(prior, 5);
6200 assert_eq!(a.load(), 5);
6201 }
6202
6203 #[test]
6204 fn atomic_shared_arc_observes_other_share() {
6205 let a1 = Arc::new(AtomicData::new(0));
6206 let a2 = Arc::clone(&a1);
6207 a1.store(42);
6208 assert_eq!(a2.load(), 42);
6209 a2.fetch_add(8);
6210 assert_eq!(a1.load(), 50);
6211 }
6212
6213 // ── LazyData ───────────────────────────────────────────────────
6214
6215 #[test]
6216 fn lazy_new_is_not_initialized() {
6217 let dummy_closure = KindedSlot::from_int(0);
6218 // Note: at the storage tier we don't actually call the
6219 // closure — that lives at the handler tier. The closure
6220 // payload here is just any `KindedSlot`; `is_initialized`
6221 // looks at the cached value, not the initializer.
6222 let l = LazyData::new(dummy_closure);
6223 assert!(!l.is_initialized());
6224 }
6225
6226 #[test]
6227 fn lazy_take_initializer_then_store_result_marks_initialized() {
6228 // Simulates the handler-tier `lazy.get()` flow: take the
6229 // initializer, "run it" (the test substitutes a result), then
6230 // cache the result. After store_result, is_initialized=true
6231 // and cached() returns the stored value.
6232 let l = LazyData::new(KindedSlot::from_int(0));
6233 let init = l
6234 .take_initializer()
6235 .expect("initializer present before first get");
6236 assert!(!l.is_initialized());
6237 // "Run the initializer" — at storage-tier test we just drop
6238 // the initializer slot and synthesize a result.
6239 drop(init);
6240 l.store_result(KindedSlot::from_int(42));
6241 assert!(l.is_initialized());
6242 let got = l.cached().expect("cached after store_result");
6243 assert_eq!(got.as_i64(), Some(42));
6244 }
6245
6246 #[test]
6247 fn lazy_take_initializer_returns_none_after_caching() {
6248 // After cache is populated, `take_initializer` returns None
6249 // — the handler tier's get() uses this to detect "already
6250 // initialized, use cached() instead".
6251 let l = LazyData::new(KindedSlot::from_int(0));
6252 let _init = l.take_initializer().unwrap();
6253 l.store_result(KindedSlot::from_int(7));
6254 assert!(l.take_initializer().is_none());
6255 }
6256
6257 #[test]
6258 fn lazy_cached_returns_none_before_init() {
6259 let l = LazyData::new(KindedSlot::from_int(0));
6260 assert!(l.cached().is_none());
6261 }
6262
6263 #[test]
6264 fn lazy_dropping_lazy_with_heap_payload_retires_shares() {
6265 // Refcount discipline: the cached `KindedSlot` owns one
6266 // strong-count share; dropping the LazyData retires it.
6267 let s = Arc::new("cached_value".to_string());
6268 let weak = Arc::downgrade(&s);
6269 let l = LazyData::new(KindedSlot::from_int(0));
6270 l.store_result(KindedSlot::from_string_arc(s));
6271 assert_eq!(weak.strong_count(), 1);
6272 drop(l);
6273 assert_eq!(
6274 weak.strong_count(),
6275 0,
6276 "Dropped LazyData must retire cached KindedSlot's share"
6277 );
6278 }
6279}
6280
6281// Wave 2 Round 4 D4 ckpt-3 (2026-05-14): the `trait_object_storage` test mod
6282// was authored against `TraitObjectStorage.value: Arc<TypedObjectStorage>`
6283// + `make_object()` returning `Arc<TypedObjectStorage>`. Post inner-field
6284// shift to `*const TypedObjectStorage`, every test that does `Arc::clone(&obj)`
6285// or `Arc::downgrade(&obj)` no longer compiles, and every test that observes
6286// the inner refcount via the weak count needs to migrate to HeapHeader
6287// `get_refcount()` inspection. Ckpt-final adapts the tests in lockstep with
6288// the full HeapValue::TypedObject variant signature flip; intermediate
6289// close gate (broken cargo check OK per
6290// `docs/cluster-audits/bulldozer-multi-session-chain-pattern.md` §Discipline
6291// relaxed) preserves the test mod source verbatim under a never-match cfg
6292// so the ckpt-final adapter has the original assertions as the migration
6293// target.
6294#[cfg(any())]
6295mod trait_object_storage {
6296 //! W17-trait-object-storage (ADR-006 §2.7.24 / Q25.C, 2026-05-11):
6297 //! pin the `TraitObjectStorage` API + refcount-discipline contracts.
6298 //! Storage-tier only — `OpCode::BoxTraitObject` /
6299 //! `OpCode::DynMethodCall` emission and end-to-end dyn-coerce smoke
6300 //! live in W17-trait-object-emission (round 2 of Wave 2.6).
6301 //!
6302 //! Coverage:
6303 //! - Construction (`TraitObjectStorage::new`)
6304 //! - vtable / value field access
6305 //! - `method()` lookup
6306 //! - `vtable_eq()` identity contract per §Q25.C.2
6307 //! - Clone bumps both inner Arc strong counts
6308 //! - Drop retires both inner Arc strong counts
6309 //! - `KindedSlot::from_trait_object` retain-on-clone parity
6310 //! - `KindedSlot::from_trait_object` drop-decrement parity
6311 //! - `Arc<TraitObjectStorage>` clone roundtrip (via clone_with_kind
6312 //! contract through the kind label, not through HeapValue)
6313 //! - End-to-end retain/drop balance over multiple clones
6314 use super::*;
6315 use crate::kinded_slot::KindedSlot;
6316 use crate::native_kind::NativeKind;
6317 use crate::value::{VTable, VTableEntry};
6318 use std::collections::HashMap;
6319 use std::sync::Arc;
6320
6321 /// Build a minimal `TypedObjectStorage` for tests — single i64 field,
6322 /// no heap-typed slots. Mirror of the shape used by concurrency
6323 /// tests' `KindedSlot::from_int` payloads.
6324 ///
6325 /// **Wave 2 Round 4 D4 ckpt-3 (2026-05-14): returns `*mut
6326 /// TypedObjectStorage` (v2-raw `_new` shape)** per the
6327 /// `TraitObjectStorage.value: *const TypedObjectStorage` inner-field
6328 /// shift. Tests that previously observed inner refcount via
6329 /// `Arc::downgrade(&obj)` are surfaced as broken pending test-side
6330 /// migration to HeapHeader refcount inspection
6331 /// (`unsafe { (*ptr).header.get_refcount() }`).
6332 fn make_object(value: i64) -> *mut TypedObjectStorage {
6333 let mut slots: Vec<crate::slot::ValueSlot> = Vec::with_capacity(1);
6334 slots.push(crate::slot::ValueSlot::from_int(value));
6335 let field_kinds: Arc<[NativeKind]> = Arc::from(vec![NativeKind::Int64]);
6336 TypedObjectStorage::_new(
6337 42, // schema_id — arbitrary
6338 slots.into_boxed_slice(),
6339 0, // heap_mask: no heap slots
6340 field_kinds,
6341 )
6342 }
6343
6344 /// Build a minimal `VTable` for tests — one `Direct` method entry.
6345 fn make_vtable(trait_name: &str, concrete_type_id: u32, method: &str) -> Arc<VTable> {
6346 let mut methods: HashMap<String, VTableEntry> = HashMap::new();
6347 methods.insert(
6348 method.to_string(),
6349 VTableEntry::Direct { function_id: 7 },
6350 );
6351 Arc::new(VTable {
6352 trait_names: vec![trait_name.to_string()],
6353 concrete_type_id,
6354 methods,
6355 })
6356 }
6357
6358 #[test]
6359 fn new_holds_value_and_vtable_arcs() {
6360 let obj = make_object(1);
6361 let vt = make_vtable("Animal", 100, "name");
6362 let storage = TraitObjectStorage::new(Arc::clone(&obj), Arc::clone(&vt));
6363 // Both halves remain accessible; vtable's concrete_type_id
6364 // matches what the impl declared.
6365 assert_eq!(storage.value.schema_id, 42);
6366 assert_eq!(storage.vtable.concrete_type_id, 100);
6367 assert_eq!(storage.vtable.trait_names[0], "Animal");
6368 }
6369
6370 #[test]
6371 fn method_lookup_returns_entry_for_known_method() {
6372 let obj = make_object(1);
6373 let vt = make_vtable("Animal", 100, "name");
6374 let storage = TraitObjectStorage::new(obj, vt);
6375 let entry = storage
6376 .method("name")
6377 .expect("known method present in vtable");
6378 match entry {
6379 VTableEntry::Direct { function_id } => assert_eq!(*function_id, 7),
6380 _ => panic!("expected Direct entry"),
6381 }
6382 }
6383
6384 #[test]
6385 fn method_lookup_returns_none_for_unknown_method() {
6386 let obj = make_object(1);
6387 let vt = make_vtable("Animal", 100, "name");
6388 let storage = TraitObjectStorage::new(obj, vt);
6389 assert!(storage.method("speak").is_none());
6390 }
6391
6392 #[test]
6393 fn vtable_eq_identifies_same_vtable_share() {
6394 // Per §Q25.C.2, vtable-identity check uses `Arc::ptr_eq`. Two
6395 // carriers built from the same vtable Arc compare equal.
6396 let obj1 = make_object(1);
6397 let obj2 = make_object(2);
6398 let vt = make_vtable("Animal", 100, "name");
6399 let s1 = TraitObjectStorage::new(obj1, Arc::clone(&vt));
6400 let s2 = TraitObjectStorage::new(obj2, Arc::clone(&vt));
6401 assert!(s1.vtable_eq(&s2));
6402 }
6403
6404 #[test]
6405 fn vtable_eq_rejects_distinct_vtables() {
6406 // Two carriers built from distinct vtables — even if the
6407 // trait name matches — fail the identity check.
6408 let obj1 = make_object(1);
6409 let obj2 = make_object(2);
6410 let vt1 = make_vtable("Animal", 100, "name");
6411 let vt2 = make_vtable("Animal", 100, "name");
6412 let s1 = TraitObjectStorage::new(obj1, vt1);
6413 let s2 = TraitObjectStorage::new(obj2, vt2);
6414 assert!(!s1.vtable_eq(&s2));
6415 }
6416
6417 #[test]
6418 fn clone_bumps_both_inner_arcs() {
6419 // TraitObjectStorage::clone is a pair of Arc bumps. Verify
6420 // each inner Arc's strong-count increases by one.
6421 let obj = make_object(1);
6422 let vt = make_vtable("Animal", 100, "name");
6423 let obj_weak = Arc::downgrade(&obj);
6424 let vt_weak = Arc::downgrade(&vt);
6425 let storage = TraitObjectStorage::new(Arc::clone(&obj), Arc::clone(&vt));
6426 // Now: external obj/vt + storage's clones = 2 strong each.
6427 // Drop the externals so we can observe the storage's owned shares.
6428 drop(obj);
6429 drop(vt);
6430 assert_eq!(obj_weak.strong_count(), 1);
6431 assert_eq!(vt_weak.strong_count(), 1);
6432
6433 // Clone storage — both inner Arcs should bump.
6434 let storage2 = storage.clone();
6435 assert_eq!(obj_weak.strong_count(), 2);
6436 assert_eq!(vt_weak.strong_count(), 2);
6437
6438 drop(storage);
6439 assert_eq!(obj_weak.strong_count(), 1);
6440 assert_eq!(vt_weak.strong_count(), 1);
6441
6442 drop(storage2);
6443 assert_eq!(obj_weak.strong_count(), 0);
6444 assert_eq!(vt_weak.strong_count(), 0);
6445 }
6446
6447 #[test]
6448 fn kinded_slot_from_trait_object_drop_decrement() {
6449 // The §2.7.7 retain-on-read protocol via `KindedSlot::Drop`
6450 // must retire one `Arc<TraitObjectStorage>` share when the
6451 // slot drops. Verify the strong-count returns to zero after
6452 // both the original Arc and the KindedSlot drop.
6453 let obj = make_object(99);
6454 let vt = make_vtable("Animal", 100, "name");
6455 let storage = Arc::new(TraitObjectStorage::new(obj, vt));
6456 let weak = Arc::downgrade(&storage);
6457 let slot = KindedSlot::from_trait_object(Arc::clone(&storage));
6458 // External: 1 share. Slot: 1 share. Total: 2.
6459 assert_eq!(weak.strong_count(), 2);
6460
6461 // Drop the external Arc — slot still holds one share.
6462 drop(storage);
6463 assert_eq!(weak.strong_count(), 1);
6464
6465 // Drop the slot — retires the last share.
6466 drop(slot);
6467 assert_eq!(weak.strong_count(), 0);
6468 }
6469
6470 #[test]
6471 fn kinded_slot_clone_bumps_share() {
6472 // KindedSlot::Clone retains via clone_with_kind — verify the
6473 // share count tracks correctly across clones.
6474 let obj = make_object(1);
6475 let vt = make_vtable("Animal", 100, "name");
6476 let storage = Arc::new(TraitObjectStorage::new(obj, vt));
6477 let weak = Arc::downgrade(&storage);
6478 let slot1 = KindedSlot::from_trait_object(Arc::clone(&storage));
6479 let slot2 = slot1.clone();
6480 let slot3 = slot2.clone();
6481 // External + 3 slots = 4 shares.
6482 assert_eq!(weak.strong_count(), 4);
6483
6484 drop(storage);
6485 drop(slot1);
6486 drop(slot2);
6487 // 1 slot remaining.
6488 assert_eq!(weak.strong_count(), 1);
6489 drop(slot3);
6490 assert_eq!(weak.strong_count(), 0);
6491 }
6492
6493 #[test]
6494 fn kinded_slot_kind_label_is_ptr_trait_object() {
6495 // The kind label must be `NativeKind::Ptr(HeapKind::TraitObject)`
6496 // for the §2.7.7 dispatch tables (clone_with_kind /
6497 // drop_with_kind) to find the correct arm. This pins the
6498 // construction-side contract.
6499 let obj = make_object(1);
6500 let vt = make_vtable("Animal", 100, "name");
6501 let storage = Arc::new(TraitObjectStorage::new(obj, vt));
6502 let slot = KindedSlot::from_trait_object(storage);
6503 assert_eq!(slot.kind(), NativeKind::Ptr(HeapKind::TraitObject));
6504 }
6505
6506 #[test]
6507 fn slot_bits_recover_to_typed_arc_via_canonical_pattern() {
6508 // The canonical recovery pattern (per `3ac2f11` precedent):
6509 // bits = slot.raw(), Arc::from_raw, clone, into_raw. Verify
6510 // that round-tripping the raw bits through Arc::from_raw
6511 // recovers an Arc with the expected vtable identity.
6512 let obj = make_object(7);
6513 let vt = make_vtable("Animal", 100, "name");
6514 let storage = Arc::new(TraitObjectStorage::new(obj, vt));
6515 let original_vt_ptr = Arc::as_ptr(&storage.vtable);
6516 let slot = KindedSlot::from_trait_object(Arc::clone(&storage));
6517 let bits = slot.slot().raw();
6518
6519 // SAFETY: bits came from KindedSlot::from_trait_object which
6520 // stores `Arc::into_raw(Arc<TraitObjectStorage>)`. The slot
6521 // owns the share; we leak the recovered Arc back to keep the
6522 // slot's share intact for normal drop discipline.
6523 let recovered: Arc<TraitObjectStorage> =
6524 unsafe { Arc::from_raw(bits as *const TraitObjectStorage) };
6525 let cloned = Arc::clone(&recovered);
6526 let _ = Arc::into_raw(recovered); // restore slot's share
6527
6528 // Recovered Arc points to the same storage — same vtable Arc.
6529 assert!(Arc::ptr_eq(&cloned.vtable, &storage.vtable));
6530 // Pointer-equality on the inner vtable's raw pointer matches.
6531 assert_eq!(Arc::as_ptr(&cloned.vtable), original_vt_ptr);
6532
6533 drop(cloned);
6534 drop(slot);
6535 drop(storage);
6536 }
6537
6538 #[test]
6539 fn dropping_trait_object_with_typed_object_payload_retires_payload_share() {
6540 // Refcount discipline at the carrier level: drop the
6541 // TraitObjectStorage Arc to zero strong count, and verify
6542 // the inner TypedObject Arc's strong count returns to zero
6543 // too. This is the end-to-end retain/drop balance for the
6544 // fat-pointer carrier.
6545 let obj = make_object(99);
6546 let vt = make_vtable("Animal", 100, "name");
6547 let obj_weak = Arc::downgrade(&obj);
6548 let vt_weak = Arc::downgrade(&vt);
6549 let storage = Arc::new(TraitObjectStorage::new(obj, vt));
6550 // After move: storage holds the only shares of obj + vt.
6551 assert_eq!(obj_weak.strong_count(), 1);
6552 assert_eq!(vt_weak.strong_count(), 1);
6553
6554 drop(storage);
6555 assert_eq!(obj_weak.strong_count(), 0, "TypedObject share must retire");
6556 assert_eq!(vt_weak.strong_count(), 0, "VTable share must retire");
6557 }
6558
6559 // ── Wave 2 Agent E (2026-05-14): v2-raw HeapHeader migration tests ──────
6560
6561 #[test]
6562 fn new_initializes_heap_header() {
6563 // Wave 2 Agent E: confirm `TraitObjectStorage::new` initializes the
6564 // HeapHeader at offset 0 with HEAP_KIND_V2_TRAIT_OBJECT and
6565 // refcount=1. The header sits unused for `Arc<TraitObjectStorage>`
6566 // instances (Arc owns the lifecycle).
6567 let obj = make_object(1);
6568 let vt = make_vtable("Animal", 100, "name");
6569 let storage = TraitObjectStorage::new(obj, vt);
6570 assert_eq!(
6571 storage.header.kind(),
6572 crate::v2::heap_header::HEAP_KIND_V2_TRAIT_OBJECT,
6573 );
6574 assert_eq!(storage.header.get_refcount(), 1);
6575 }
6576
6577 #[test]
6578 fn v2_raw_new_drop_round_trip_balances_inner_arcs() {
6579 // Wave 2 Agent E: verify the v2-raw lifecycle (`_new` + `_drop`)
6580 // round-trips cleanly without leaking the inner Arc shares.
6581 // Mirror of D1's `TypedObjectStorage` round-trip test (commit
6582 // 0e4510d4).
6583 let obj = make_object(7);
6584 let vt = make_vtable("Animal", 100, "name");
6585 let obj_weak = Arc::downgrade(&obj);
6586 let vt_weak = Arc::downgrade(&vt);
6587 unsafe {
6588 let ptr = TraitObjectStorage::_new(obj, vt);
6589 assert!(!ptr.is_null());
6590 // Header refcount=1 + inner Arcs hold one share each.
6591 assert_eq!((*ptr).header.get_refcount(), 1);
6592 assert_eq!(obj_weak.strong_count(), 1);
6593 assert_eq!(vt_weak.strong_count(), 1);
6594 assert_eq!(
6595 (*ptr).header.kind(),
6596 crate::v2::heap_header::HEAP_KIND_V2_TRAIT_OBJECT,
6597 );
6598 // `_drop` retires the inner shares + deallocates.
6599 TraitObjectStorage::_drop(ptr);
6600 }
6601 assert_eq!(
6602 obj_weak.strong_count(),
6603 0,
6604 "TypedObject share must retire on _drop",
6605 );
6606 assert_eq!(
6607 vt_weak.strong_count(),
6608 0,
6609 "VTable share must retire on _drop",
6610 );
6611 }
6612
6613 #[test]
6614 fn heap_element_release_elem_to_zero_drops_payload() {
6615 // Wave 2 Agent E: verify the HeapElement trait dispatch
6616 // (`release_elem`) deallocates the carrier at refcount=0 and
6617 // retires the inner Arc shares. Mirror of D1's
6618 // `TypedObjectStorage::test_heap_element_release_elem_to_zero`.
6619 use crate::v2::heap_element::HeapElement;
6620 let obj = make_object(13);
6621 let vt = make_vtable("Animal", 100, "name");
6622 let obj_weak = Arc::downgrade(&obj);
6623 let vt_weak = Arc::downgrade(&vt);
6624 unsafe {
6625 let ptr = TraitObjectStorage::_new(obj, vt);
6626 assert_eq!((*ptr).header.get_refcount(), 1);
6627 // `release_elem` decrements to 0 and runs `_drop`.
6628 TraitObjectStorage::release_elem(ptr);
6629 }
6630 assert_eq!(
6631 obj_weak.strong_count(),
6632 0,
6633 "TypedObject share must retire after release_elem to zero",
6634 );
6635 assert_eq!(
6636 vt_weak.strong_count(),
6637 0,
6638 "VTable share must retire after release_elem to zero",
6639 );
6640 }
6641
6642 #[test]
6643 fn heap_element_release_elem_held_share_preserves_payload() {
6644 // Wave 2 Agent E: verify `release_elem` is a no-op at refcount > 1
6645 // (it decrements but does not deallocate). The inner Arc shares
6646 // remain held until the final `_drop`. Mirror of D1's
6647 // `TypedObjectStorage::test_heap_element_release_elem_held_share`.
6648 use crate::v2::heap_element::HeapElement;
6649 let obj = make_object(17);
6650 let vt = make_vtable("Animal", 100, "name");
6651 let obj_weak = Arc::downgrade(&obj);
6652 let vt_weak = Arc::downgrade(&vt);
6653 unsafe {
6654 let ptr = TraitObjectStorage::_new(obj, vt);
6655 // Bump refcount to 2 (simulate a second slot holding this
6656 // carrier).
6657 (*ptr).header.retain();
6658 assert_eq!((*ptr).header.get_refcount(), 2);
6659 // First release_elem: refcount=2 → 1, no dealloc.
6660 TraitObjectStorage::release_elem(ptr);
6661 assert_eq!((*ptr).header.get_refcount(), 1);
6662 // Inner shares still held.
6663 assert_eq!(obj_weak.strong_count(), 1);
6664 assert_eq!(vt_weak.strong_count(), 1);
6665 // Final _drop retires.
6666 TraitObjectStorage::_drop(ptr);
6667 }
6668 assert_eq!(obj_weak.strong_count(), 0);
6669 assert_eq!(vt_weak.strong_count(), 0);
6670 }
6671
6672 #[test]
6673 fn kinded_slot_from_trait_object_raw_constructor_kind_and_bits() {
6674 // Wave 2 Agent E: `KindedSlot::from_trait_object_raw` stores a
6675 // raw `*const TraitObjectStorage` directly (NOT `Arc::into_raw`)
6676 // with kind `NativeKind::Ptr(HeapKind::TraitObject)`. Mirror of
6677 // D1's `from_typed_object_raw_constructor_kind_and_bits` test.
6678 let obj = make_object(2);
6679 let vt = make_vtable("Animal", 100, "name");
6680 unsafe {
6681 let ptr = TraitObjectStorage::_new(obj, vt);
6682 let slot = KindedSlot::from_trait_object_raw(ptr);
6683 assert_eq!(slot.kind(), NativeKind::Ptr(HeapKind::TraitObject));
6684 // Slot bits are the raw pointer (NOT Arc::into_raw).
6685 assert_eq!(slot.slot().raw(), ptr as u64);
6686 // Forget the slot — Wave 2 transitional Arc-style dispatch
6687 // arms would call `Arc::decrement_strong_count` on raw
6688 // pointer bits (heap corruption) — see the D1 follow-up
6689 // lockstep requirement. Deallocate manually instead.
6690 std::mem::forget(slot);
6691 TraitObjectStorage::_drop(ptr);
6692 }
6693 }
6694
6695 #[test]
6696 fn v2_raw_carrier_size_matches_expected_layout() {
6697 // Wave 2 Agent E: pin the v2-raw struct layout — HeapHeader (8)
6698 // + Arc<TypedObjectStorage> (8) + Arc<VTable> (8) = 24 bytes
6699 // (matching the audit §E.3 24-byte size contract for the E-a
6700 // path). The inner Arcs stay 8-byte each in E's Round 2 scope;
6701 // D2's lockstep flip migrates the inner pointers but the
6702 // outer size contract is set here.
6703 assert_eq!(
6704 std::mem::size_of::<TraitObjectStorage>(),
6705 24,
6706 "TraitObjectStorage v2-raw layout must be 24 bytes \
6707 (HeapHeader 8 + Arc<TypedObjectStorage> 8 + Arc<VTable> 8)",
6708 );
6709 }
6710}