Skip to main content

shape_runtime/
snapshot.rs

1//! Snapshotting and resumability support
2//!
3//! Provides binary, diff-friendly snapshots via a content-addressed store.
4
5use std::collections::{HashMap, HashSet};
6use std::fs;
7use std::io::{Read, Write};
8use std::path::PathBuf;
9use anyhow::{Context, Result};
10use serde::de::DeserializeOwned;
11use serde::{Deserialize, Serialize};
12use shape_value::{EnumPayload, EnumValue, PrintResult, PrintSpan, Upvalue, ValueWord};
13
14use crate::event_queue::WaitCondition;
15use crate::hashing::{HashDigest, hash_bytes};
16use shape_ast::ast::{DataDateTimeRef, DateTimeExpr, EnumDef, TimeReference, TypeAnnotation};
17use shape_ast::data::Timeframe;
18
19use crate::data::DataFrame;
20use shape_value::datatable::DataTable;
21
22/// Schema version for the snapshot binary format.
23///
24/// This version is embedded in every [`ExecutionSnapshot`] via the `version`
25/// field. Readers should check this value to determine whether they can
26/// decode a snapshot or need migration logic.
27///
28/// Version history:
29/// - v5 (current): ValueWord-native serialization — `nanboxed_to_serializable`
30///   and `serializable_to_nanboxed` operate on ValueWord directly without
31///   intermediate ValueWord conversion. Format is wire-compatible with v4
32///   (same `SerializableVMValue` enum), so v4 snapshots deserialize
33///   correctly without migration.
34pub const SNAPSHOT_VERSION: u32 = 5;
35
36pub(crate) const DEFAULT_CHUNK_LEN: usize = 4096;
37pub(crate) const BYTE_CHUNK_LEN: usize = 256 * 1024;
38
39/// Content-addressed snapshot store
40#[derive(Clone)]
41pub struct SnapshotStore {
42    root: PathBuf,
43}
44
45impl SnapshotStore {
46    pub fn new(root: impl Into<PathBuf>) -> Result<Self> {
47        let root = root.into();
48        fs::create_dir_all(root.join("blobs"))
49            .with_context(|| format!("failed to create snapshot blob dir at {}", root.display()))?;
50        fs::create_dir_all(root.join("snapshots"))
51            .with_context(|| format!("failed to create snapshot dir at {}", root.display()))?;
52        Ok(Self { root })
53    }
54
55    fn blob_path(&self, hash: &HashDigest) -> PathBuf {
56        self.root
57            .join("blobs")
58            .join(format!("{}.bin.zst", hash.hex()))
59    }
60
61    fn snapshot_path(&self, hash: &HashDigest) -> PathBuf {
62        self.root
63            .join("snapshots")
64            .join(format!("{}.bin.zst", hash.hex()))
65    }
66
67    pub fn put_blob(&self, data: &[u8]) -> Result<HashDigest> {
68        let hash = hash_bytes(data);
69        let path = self.blob_path(&hash);
70        if path.exists() {
71            return Ok(hash);
72        }
73        let compressed = zstd::stream::encode_all(data, 0)?;
74        let mut file = fs::File::create(&path)?;
75        file.write_all(&compressed)?;
76        Ok(hash)
77    }
78
79    pub fn get_blob(&self, hash: &HashDigest) -> Result<Vec<u8>> {
80        let path = self.blob_path(hash);
81        let mut file = fs::File::open(&path)
82            .with_context(|| format!("snapshot blob not found: {}", path.display()))?;
83        let mut buf = Vec::new();
84        file.read_to_end(&mut buf)?;
85        let decompressed = zstd::stream::decode_all(&buf[..])?;
86        Ok(decompressed)
87    }
88
89    pub fn put_struct<T: Serialize>(&self, value: &T) -> Result<HashDigest> {
90        let bytes = bincode::serialize(value)?;
91        self.put_blob(&bytes)
92    }
93
94    pub fn get_struct<T: for<'de> Deserialize<'de>>(&self, hash: &HashDigest) -> Result<T> {
95        let bytes = self.get_blob(hash)?;
96        Ok(bincode::deserialize(&bytes)?)
97    }
98
99    pub fn put_snapshot(&self, snapshot: &ExecutionSnapshot) -> Result<HashDigest> {
100        let bytes = bincode::serialize(snapshot)?;
101        let hash = hash_bytes(&bytes);
102        let path = self.snapshot_path(&hash);
103        if !path.exists() {
104            let compressed = zstd::stream::encode_all(&bytes[..], 0)?;
105            let mut file = fs::File::create(&path)?;
106            file.write_all(&compressed)?;
107        }
108        Ok(hash)
109    }
110
111    pub fn get_snapshot(&self, hash: &HashDigest) -> Result<ExecutionSnapshot> {
112        let path = self.snapshot_path(hash);
113        let mut file = fs::File::open(&path)
114            .with_context(|| format!("snapshot not found: {}", path.display()))?;
115        let mut buf = Vec::new();
116        file.read_to_end(&mut buf)?;
117        let decompressed = zstd::stream::decode_all(&buf[..])?;
118        Ok(bincode::deserialize(&decompressed)?)
119    }
120
121    /// List all snapshots in the store, returning (hash, snapshot) pairs.
122    ///
123    /// **Note:** This method eagerly loads and deserializes every snapshot in the
124    /// store directory into memory. For stores with many snapshots this may
125    /// become a bottleneck. A future improvement could return a lazy iterator
126    /// that streams snapshot metadata (hash + `created_at_ms`) without
127    /// deserializing full payloads until requested — e.g. via a
128    /// `SnapshotEntry { hash, created_at_ms }` header read, deferring full
129    /// `ExecutionSnapshot` deserialization to an explicit `.load()` call.
130    pub fn list_snapshots(&self) -> Result<Vec<(HashDigest, ExecutionSnapshot)>> {
131        let snapshots_dir = self.root.join("snapshots");
132        if !snapshots_dir.exists() {
133            return Ok(Vec::new());
134        }
135        let mut results = Vec::new();
136        for entry in fs::read_dir(&snapshots_dir)? {
137            let entry = entry?;
138            let path = entry.path();
139            if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
140                // Files are named "<hex>.bin.zst"
141                if let Some(hex) = name.strip_suffix(".bin.zst") {
142                    let hash = HashDigest::from_hex(hex);
143                    match self.get_snapshot(&hash) {
144                        Ok(snap) => results.push((hash, snap)),
145                        Err(_) => continue, // skip corrupt entries
146                    }
147                }
148            }
149        }
150        // Sort by creation time, newest first
151        results.sort_by(|a, b| b.1.created_at_ms.cmp(&a.1.created_at_ms));
152        Ok(results)
153    }
154
155    /// Delete a snapshot file by hash.
156    pub fn delete_snapshot(&self, hash: &HashDigest) -> Result<()> {
157        let path = self.snapshot_path(hash);
158        fs::remove_file(&path)
159            .with_context(|| format!("failed to delete snapshot: {}", path.display()))?;
160        Ok(())
161    }
162}
163
164/// A serializable snapshot of a Shape program's execution state.
165///
166/// The `version` field records which [`SNAPSHOT_VERSION`] was used to
167/// produce this snapshot. Readers must check this value before
168/// deserializing the referenced sub-snapshots (semantic, context, VM)
169/// to ensure binary compatibility or apply migration logic.
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct ExecutionSnapshot {
172    /// Schema version — should equal [`SNAPSHOT_VERSION`] at write time.
173    /// Used by readers to detect format changes and apply migrations.
174    pub version: u32,
175    pub created_at_ms: i64,
176    pub semantic_hash: HashDigest,
177    pub context_hash: HashDigest,
178    pub vm_hash: Option<HashDigest>,
179    pub bytecode_hash: Option<HashDigest>,
180    /// Path of the script that was executing when the snapshot was taken
181    #[serde(default)]
182    pub script_path: Option<String>,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct SemanticSnapshot {
187    pub exported_symbols: HashSet<String>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct ContextSnapshot {
192    pub data_load_mode: crate::context::DataLoadMode,
193    pub data_cache: Option<DataCacheSnapshot>,
194    pub current_id: Option<String>,
195    pub current_row_index: usize,
196    pub variable_scopes: Vec<HashMap<String, VariableSnapshot>>,
197    pub reference_datetime: Option<chrono::DateTime<chrono::Utc>>,
198    pub current_timeframe: Option<Timeframe>,
199    pub base_timeframe: Option<Timeframe>,
200    pub date_range: Option<(chrono::DateTime<chrono::Utc>, chrono::DateTime<chrono::Utc>)>,
201    pub range_start: usize,
202    pub range_end: usize,
203    pub range_active: bool,
204    pub type_alias_registry: HashMap<String, TypeAliasRuntimeEntrySnapshot>,
205    pub enum_registry: HashMap<String, EnumDef>,
206    #[serde(default)]
207    pub struct_type_registry: HashMap<String, shape_ast::ast::StructTypeDef>,
208    pub suspension_state: Option<SuspensionStateSnapshot>,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct VariableSnapshot {
213    pub value: SerializableVMValue,
214    pub kind: shape_ast::ast::VarKind,
215    pub is_initialized: bool,
216    pub is_function_scoped: bool,
217    pub format_hint: Option<String>,
218    pub format_overrides: Option<HashMap<String, SerializableVMValue>>,
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
222pub struct TypeAliasRuntimeEntrySnapshot {
223    pub base_type: String,
224    pub overrides: Option<HashMap<String, SerializableVMValue>>,
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct SuspensionStateSnapshot {
229    pub waiting_for: WaitCondition,
230    pub resume_pc: usize,
231    pub saved_locals: Vec<SerializableVMValue>,
232    pub saved_stack: Vec<SerializableVMValue>,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct VmSnapshot {
237    pub ip: usize,
238    pub stack: Vec<SerializableVMValue>,
239    pub locals: Vec<SerializableVMValue>,
240    pub module_bindings: Vec<SerializableVMValue>,
241    pub call_stack: Vec<SerializableCallFrame>,
242    pub loop_stack: Vec<SerializableLoopContext>,
243    pub timeframe_stack: Vec<Option<Timeframe>>,
244    pub exception_handlers: Vec<SerializableExceptionHandler>,
245    /// Content hash of the function blob that the top-level IP belongs to.
246    /// Used for relocating the IP after recompilation.
247    #[serde(default)]
248    pub ip_blob_hash: Option<[u8; 32]>,
249    /// Instruction offset within the function blob for the top-level IP.
250    /// Computed as `ip - function_entry_point` when saving; reconstructed
251    /// to absolute IP on restore. Only meaningful when `ip_blob_hash` is `Some`.
252    #[serde(default)]
253    pub ip_local_offset: Option<usize>,
254    /// Function ID that the top-level IP belongs to.
255    /// Used as a fallback when `ip_blob_hash` is not available.
256    #[serde(default)]
257    pub ip_function_id: Option<u16>,
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct SerializableCallFrame {
262    pub return_ip: usize,
263    pub locals_base: usize,
264    pub locals_count: usize,
265    pub function_id: Option<u16>,
266    pub upvalues: Option<Vec<SerializableVMValue>>,
267    /// Content hash of the function blob (for content-addressed state capture).
268    /// When present, `local_ip` stores the instruction offset relative to the
269    /// function's entry point rather than an absolute IP.
270    #[serde(default)]
271    pub blob_hash: Option<[u8; 32]>,
272    /// Instruction offset within the function blob.
273    /// Computed as `ip - function_entry_point` when saving; reconstructed to
274    /// absolute IP on restore. Only meaningful when `blob_hash` is `Some`.
275    #[serde(default)]
276    pub local_ip: Option<usize>,
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize)]
280pub struct SerializableLoopContext {
281    pub start: usize,
282    pub end: usize,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize)]
286pub struct SerializableExceptionHandler {
287    pub catch_ip: usize,
288    pub stack_size: usize,
289    pub call_depth: usize,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
293pub enum SerializableVMValue {
294    Int(i64),
295    Number(f64),
296    Decimal(rust_decimal::Decimal),
297    String(String),
298    Bool(bool),
299    None,
300    Some(Box<SerializableVMValue>),
301    Unit,
302    Timeframe(Timeframe),
303    Duration(shape_ast::ast::Duration),
304    Time(chrono::DateTime<chrono::FixedOffset>),
305    TimeSpan(i64), // millis
306    TimeReference(TimeReference),
307    DateTimeExpr(DateTimeExpr),
308    DataDateTimeRef(DataDateTimeRef),
309    Array(Vec<SerializableVMValue>),
310    Function(u16),
311    TypeAnnotation(TypeAnnotation),
312    TypeAnnotatedValue {
313        type_name: String,
314        value: Box<SerializableVMValue>,
315    },
316    Enum(EnumValueSnapshot),
317    Closure {
318        function_id: u16,
319        upvalues: Vec<SerializableVMValue>,
320    },
321    ModuleFunction(String),
322    TypedObject {
323        schema_id: u64,
324        /// Serialized slots: each slot is 8 bytes (raw bits for simple, serialized heap values for heap slots)
325        slot_data: Vec<SerializableVMValue>,
326        heap_mask: u64,
327    },
328    Range {
329        start: Option<Box<SerializableVMValue>>,
330        end: Option<Box<SerializableVMValue>>,
331        inclusive: bool,
332    },
333    Ok(Box<SerializableVMValue>),
334    Err(Box<SerializableVMValue>),
335    PrintResult(PrintableSnapshot),
336    SimulationCall {
337        name: String,
338        params: HashMap<String, SerializableVMValue>,
339    },
340    FunctionRef {
341        name: String,
342        closure: Option<Box<SerializableVMValue>>,
343    },
344    DataReference {
345        datetime: chrono::DateTime<chrono::FixedOffset>,
346        id: String,
347        timeframe: Timeframe,
348    },
349    Future(u64),
350    DataTable(BlobRef),
351    TypedTable {
352        schema_id: u64,
353        table: BlobRef,
354    },
355    RowView {
356        schema_id: u64,
357        table: BlobRef,
358        row_idx: usize,
359    },
360    ColumnRef {
361        schema_id: u64,
362        table: BlobRef,
363        col_id: u32,
364    },
365    IndexedTable {
366        schema_id: u64,
367        table: BlobRef,
368        index_col: u32,
369    },
370    /// Binary-serialized typed array (raw bytes via BlobRef).
371    TypedArray {
372        element_kind: TypedArrayElementKind,
373        blob: BlobRef,
374        len: usize,
375    },
376    /// Binary-serialized matrix (raw f64 bytes, row-major).
377    Matrix {
378        blob: BlobRef,
379        rows: u32,
380        cols: u32,
381    },
382    /// Dedicated HashMap variant preserving type identity.
383    HashMap {
384        keys: Vec<SerializableVMValue>,
385        values: Vec<SerializableVMValue>,
386    },
387    /// Placeholder for sidecar-split large blobs (Phase 3B).
388    /// Metadata fields preserve TypedArray len and Matrix rows/cols
389    /// so reassembly can reconstruct the exact original variant.
390    SidecarRef {
391        sidecar_id: u32,
392        blob_kind: BlobKind,
393        original_hash: HashDigest,
394        /// For TypedArray: element count. For Matrix: row count. Otherwise 0.
395        meta_a: u32,
396        /// For Matrix: column count. Otherwise 0.
397        meta_b: u32,
398    },
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct EnumValueSnapshot {
403    pub enum_name: String,
404    pub variant: String,
405    pub payload: EnumPayloadSnapshot,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
409pub enum EnumPayloadSnapshot {
410    Unit,
411    Tuple(Vec<SerializableVMValue>),
412    Struct(Vec<(String, SerializableVMValue)>),
413}
414
415#[derive(Debug, Clone, Serialize, Deserialize)]
416pub struct PrintableSnapshot {
417    pub rendered: String,
418    pub spans: Vec<PrintSpanSnapshot>,
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize)]
422pub enum PrintSpanSnapshot {
423    Literal {
424        text: String,
425        start: usize,
426        end: usize,
427        span_id: String,
428    },
429    Value {
430        text: String,
431        start: usize,
432        end: usize,
433        span_id: String,
434        variable_name: Option<String>,
435        raw_value: Box<SerializableVMValue>,
436        type_name: String,
437        current_format: String,
438        format_params: HashMap<String, SerializableVMValue>,
439    },
440}
441
442#[derive(Debug, Clone, Serialize, Deserialize)]
443pub struct BlobRef {
444    pub hash: HashDigest,
445    pub kind: BlobKind,
446}
447
448/// Element type for typed array binary serialization.
449#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
450pub enum TypedArrayElementKind {
451    I8,
452    I16,
453    I32,
454    I64,
455    U8,
456    U16,
457    U32,
458    U64,
459    F32,
460    F64,
461    Bool,
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize)]
465pub enum BlobKind {
466    DataTable,
467    /// Raw typed array bytes (element type encoded separately).
468    TypedArray(TypedArrayElementKind),
469    /// Raw f64 bytes in row-major order.
470    Matrix,
471}
472
473#[derive(Debug, Clone, Serialize, Deserialize)]
474pub struct ChunkedBlob {
475    pub chunk_hashes: Vec<HashDigest>,
476    pub total_len: usize,
477    pub chunk_len: usize,
478}
479
480#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct SerializableDataTable {
482    pub ipc_chunks: ChunkedBlob,
483    pub type_name: Option<String>,
484    pub schema_id: Option<u32>,
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize)]
488pub struct SerializableDataFrame {
489    pub id: String,
490    pub timeframe: Timeframe,
491    pub timestamps: ChunkedBlob,
492    pub columns: Vec<SerializableDataFrameColumn>,
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize)]
496pub struct SerializableDataFrameColumn {
497    pub name: String,
498    pub values: ChunkedBlob,
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize)]
502pub struct CacheKeySnapshot {
503    pub id: String,
504    pub timeframe: Timeframe,
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize)]
508pub struct CachedDataSnapshot {
509    pub key: CacheKeySnapshot,
510    pub historical: SerializableDataFrame,
511    pub current_index: usize,
512}
513
514#[derive(Debug, Clone, Serialize, Deserialize)]
515pub struct LiveBufferSnapshot {
516    pub key: CacheKeySnapshot,
517    pub rows: ChunkedBlob,
518}
519
520#[derive(Debug, Clone, Serialize, Deserialize)]
521pub struct DataCacheSnapshot {
522    pub historical: Vec<CachedDataSnapshot>,
523    pub live_buffer: Vec<LiveBufferSnapshot>,
524}
525
526pub(crate) fn store_chunked_vec<T: Serialize>(
527    values: &[T],
528    chunk_len: usize,
529    store: &SnapshotStore,
530) -> Result<ChunkedBlob> {
531    let chunk_len = chunk_len.max(1);
532    if values.is_empty() {
533        return Ok(ChunkedBlob {
534            chunk_hashes: Vec::new(),
535            total_len: 0,
536            chunk_len,
537        });
538    }
539    let mut hashes = Vec::new();
540    for chunk in values.chunks(chunk_len) {
541        let bytes = bincode::serialize(chunk)?;
542        let hash = store.put_blob(&bytes)?;
543        hashes.push(hash);
544    }
545    Ok(ChunkedBlob {
546        chunk_hashes: hashes,
547        total_len: values.len(),
548        chunk_len,
549    })
550}
551
552pub(crate) fn load_chunked_vec<T: DeserializeOwned>(
553    chunked: &ChunkedBlob,
554    store: &SnapshotStore,
555) -> Result<Vec<T>> {
556    if chunked.total_len == 0 {
557        return Ok(Vec::new());
558    }
559    let mut out = Vec::with_capacity(chunked.total_len);
560    for hash in &chunked.chunk_hashes {
561        let bytes = store.get_blob(hash)?;
562        let chunk: Vec<T> = bincode::deserialize(&bytes)?;
563        out.extend(chunk);
564    }
565    out.truncate(chunked.total_len);
566    Ok(out)
567}
568
569/// Store raw bytes in content-addressed chunks (256 KB each).
570pub fn store_chunked_bytes(data: &[u8], store: &SnapshotStore) -> Result<ChunkedBlob> {
571    if data.is_empty() {
572        return Ok(ChunkedBlob {
573            chunk_hashes: Vec::new(),
574            total_len: 0,
575            chunk_len: BYTE_CHUNK_LEN,
576        });
577    }
578    let mut hashes = Vec::new();
579    for chunk in data.chunks(BYTE_CHUNK_LEN) {
580        let hash = store.put_blob(chunk)?;
581        hashes.push(hash);
582    }
583    Ok(ChunkedBlob {
584        chunk_hashes: hashes,
585        total_len: data.len(),
586        chunk_len: BYTE_CHUNK_LEN,
587    })
588}
589
590/// Load raw bytes from content-addressed chunks.
591pub fn load_chunked_bytes(chunked: &ChunkedBlob, store: &SnapshotStore) -> Result<Vec<u8>> {
592    if chunked.total_len == 0 {
593        return Ok(Vec::new());
594    }
595    let mut out = Vec::with_capacity(chunked.total_len);
596    for hash in &chunked.chunk_hashes {
597        let bytes = store.get_blob(hash)?;
598        out.extend_from_slice(&bytes);
599    }
600    out.truncate(chunked.total_len);
601    Ok(out)
602}
603
604/// Reinterpret a byte slice as a slice of `T` (must be properly aligned and sized).
605///
606/// # Safety
607/// The byte slice must have a length that is a multiple of `size_of::<T>()`.
608fn bytes_as_slice<T: Copy>(bytes: &[u8]) -> &[T] {
609    let elem_size = std::mem::size_of::<T>();
610    assert!(
611        bytes.len() % elem_size == 0,
612        "byte slice length {} not a multiple of element size {}",
613        bytes.len(),
614        elem_size
615    );
616    let len = bytes.len() / elem_size;
617    unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const T, len) }
618}
619
620/// Reinterpret a slice of `T` as raw bytes.
621fn slice_as_bytes<T>(data: &[T]) -> &[u8] {
622    let byte_len = data.len() * std::mem::size_of::<T>();
623    unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, byte_len) }
624}
625
626// ===================== Conversion helpers =====================
627
628/// Serialize a ValueWord value to SerializableVMValue without materializing ValueWord.
629///
630/// For inline types (f64, i48, bool, None, Unit, Function), this avoids any heap
631/// allocation by reading directly from the ValueWord tag/payload. For heap types,
632/// it uses `as_heap_ref()` to inspect the HeapValue directly.
633pub fn nanboxed_to_serializable(
634    nb: &ValueWord,
635    store: &SnapshotStore,
636) -> Result<SerializableVMValue> {
637    use shape_value::value_word::NanTag;
638
639    match nb.tag() {
640        NanTag::F64 => Ok(SerializableVMValue::Number(nb.as_f64().unwrap())),
641        NanTag::I48 => Ok(SerializableVMValue::Int(nb.as_i64().unwrap())),
642        NanTag::Bool => Ok(SerializableVMValue::Bool(nb.as_bool().unwrap())),
643        NanTag::None => Ok(SerializableVMValue::None),
644        NanTag::Unit => Ok(SerializableVMValue::Unit),
645        NanTag::Function => Ok(SerializableVMValue::Function(nb.as_function().unwrap())),
646        NanTag::ModuleFunction => Ok(SerializableVMValue::ModuleFunction(format!(
647            "native#{}",
648            nb.as_module_function().unwrap()
649        ))),
650        NanTag::Heap => {
651            let hv = nb.as_heap_ref().unwrap();
652            heap_value_to_serializable(hv, store)
653        }
654        NanTag::Ref => Ok(SerializableVMValue::None), // References should not appear in snapshots
655    }
656}
657
658/// Serialize a HeapValue to SerializableVMValue.
659fn heap_value_to_serializable(
660    hv: &shape_value::heap_value::HeapValue,
661    store: &SnapshotStore,
662) -> Result<SerializableVMValue> {
663    use shape_value::heap_value::HeapValue;
664
665    Ok(match hv {
666        HeapValue::String(s) => SerializableVMValue::String((**s).clone()),
667        HeapValue::Decimal(d) => SerializableVMValue::Decimal(*d),
668        HeapValue::BigInt(i) => SerializableVMValue::Int(*i),
669        HeapValue::Array(arr) => {
670            let mut out = Vec::with_capacity(arr.len());
671            for v in arr.iter() {
672                out.push(nanboxed_to_serializable(v, store)?);
673            }
674            SerializableVMValue::Array(out)
675        }
676        HeapValue::Closure {
677            function_id,
678            upvalues,
679        } => {
680            let mut ups = Vec::new();
681            for up in upvalues.iter() {
682                let nb = up.get();
683                ups.push(nanboxed_to_serializable(&nb, store)?);
684            }
685            SerializableVMValue::Closure {
686                function_id: *function_id,
687                upvalues: ups,
688            }
689        }
690        HeapValue::TypedObject {
691            schema_id,
692            slots,
693            heap_mask,
694        } => {
695            let mut slot_data = Vec::with_capacity(slots.len());
696            for i in 0..slots.len() {
697                if *heap_mask & (1u64 << i) != 0 {
698                    let hv_inner = slots[i].as_heap_value();
699                    slot_data.push(heap_value_to_serializable(hv_inner, store)?);
700                } else {
701                    // Non-heap slot: raw bits are inline ValueWord representation.
702                    // Reconstruct the ValueWord and serialize it properly.
703                    let nb = unsafe { ValueWord::clone_from_bits(slots[i].raw()) };
704                    slot_data.push(nanboxed_to_serializable(&nb, store)?);
705                }
706            }
707            SerializableVMValue::TypedObject {
708                schema_id: *schema_id,
709                slot_data,
710                heap_mask: *heap_mask,
711            }
712        }
713        HeapValue::HostClosure(_)
714        | HeapValue::ExprProxy(_)
715        | HeapValue::FilterExpr(_)
716        | HeapValue::TaskGroup { .. }
717        | HeapValue::TraitObject { .. }
718        | HeapValue::ProjectedRef(_)
719        | HeapValue::NativeView(_) => {
720            return Err(anyhow::anyhow!(
721                "Cannot snapshot transient value: {}",
722                hv.type_name()
723            ));
724        }
725        HeapValue::Enum(ev) => SerializableVMValue::Enum(enum_to_snapshot(ev, store)?),
726        HeapValue::Some(inner) => {
727            SerializableVMValue::Some(Box::new(nanboxed_to_serializable(inner, store)?))
728        }
729        HeapValue::Ok(inner) => {
730            SerializableVMValue::Ok(Box::new(nanboxed_to_serializable(inner, store)?))
731        }
732        HeapValue::Err(inner) => {
733            SerializableVMValue::Err(Box::new(nanboxed_to_serializable(inner, store)?))
734        }
735        HeapValue::Range {
736            start,
737            end,
738            inclusive,
739        } => SerializableVMValue::Range {
740            start: match start {
741                Some(v) => Some(Box::new(nanboxed_to_serializable(v, store)?)),
742                None => None,
743            },
744            end: match end {
745                Some(v) => Some(Box::new(nanboxed_to_serializable(v, store)?)),
746                None => None,
747            },
748            inclusive: *inclusive,
749        },
750        HeapValue::Timeframe(tf) => SerializableVMValue::Timeframe(*tf),
751        HeapValue::Duration(d) => SerializableVMValue::Duration(d.clone()),
752        HeapValue::Time(t) => SerializableVMValue::Time(*t),
753        HeapValue::TimeSpan(span) => SerializableVMValue::TimeSpan(span.num_milliseconds()),
754        HeapValue::TimeReference(tr) => SerializableVMValue::TimeReference((**tr).clone()),
755        HeapValue::DateTimeExpr(expr) => SerializableVMValue::DateTimeExpr((**expr).clone()),
756        HeapValue::DataDateTimeRef(dref) => SerializableVMValue::DataDateTimeRef((**dref).clone()),
757        HeapValue::TypeAnnotation(ta) => SerializableVMValue::TypeAnnotation((**ta).clone()),
758        HeapValue::TypeAnnotatedValue { type_name, value } => {
759            SerializableVMValue::TypeAnnotatedValue {
760                type_name: type_name.clone(),
761                value: Box::new(nanboxed_to_serializable(value, store)?),
762            }
763        }
764        HeapValue::PrintResult(pr) => {
765            SerializableVMValue::PrintResult(print_result_to_snapshot(pr, store)?)
766        }
767        HeapValue::SimulationCall(data) => {
768            let mut out = HashMap::new();
769            for (k, v) in data.params.iter() {
770                // SimulationCallData.params stores ValueWord (structural boundary in shape-value)
771                out.insert(k.clone(), nanboxed_to_serializable(&v.clone(), store)?);
772            }
773            SerializableVMValue::SimulationCall {
774                name: data.name.clone(),
775                params: out,
776            }
777        }
778        HeapValue::FunctionRef { name, closure } => SerializableVMValue::FunctionRef {
779            name: name.clone(),
780            closure: match closure {
781                Some(c) => Some(Box::new(nanboxed_to_serializable(c, store)?)),
782                None => None,
783            },
784        },
785        HeapValue::DataReference(data) => SerializableVMValue::DataReference {
786            datetime: data.datetime,
787            id: data.id.clone(),
788            timeframe: data.timeframe,
789        },
790        HeapValue::Future(id) => SerializableVMValue::Future(*id),
791        HeapValue::DataTable(dt) => {
792            let ser = serialize_datatable(dt, store)?;
793            let hash = store.put_struct(&ser)?;
794            SerializableVMValue::DataTable(BlobRef {
795                hash,
796                kind: BlobKind::DataTable,
797            })
798        }
799        HeapValue::TypedTable { schema_id, table } => {
800            let ser = serialize_datatable(table, store)?;
801            let hash = store.put_struct(&ser)?;
802            SerializableVMValue::TypedTable {
803                schema_id: *schema_id,
804                table: BlobRef {
805                    hash,
806                    kind: BlobKind::DataTable,
807                },
808            }
809        }
810        HeapValue::RowView {
811            schema_id,
812            table,
813            row_idx,
814        } => {
815            let ser = serialize_datatable(table, store)?;
816            let hash = store.put_struct(&ser)?;
817            SerializableVMValue::RowView {
818                schema_id: *schema_id,
819                table: BlobRef {
820                    hash,
821                    kind: BlobKind::DataTable,
822                },
823                row_idx: *row_idx,
824            }
825        }
826        HeapValue::ColumnRef {
827            schema_id,
828            table,
829            col_id,
830        } => {
831            let ser = serialize_datatable(table, store)?;
832            let hash = store.put_struct(&ser)?;
833            SerializableVMValue::ColumnRef {
834                schema_id: *schema_id,
835                table: BlobRef {
836                    hash,
837                    kind: BlobKind::DataTable,
838                },
839                col_id: *col_id,
840            }
841        }
842        HeapValue::IndexedTable {
843            schema_id,
844            table,
845            index_col,
846        } => {
847            let ser = serialize_datatable(table, store)?;
848            let hash = store.put_struct(&ser)?;
849            SerializableVMValue::IndexedTable {
850                schema_id: *schema_id,
851                table: BlobRef {
852                    hash,
853                    kind: BlobKind::DataTable,
854                },
855                index_col: *index_col,
856            }
857        }
858        HeapValue::NativeScalar(v) => {
859            if let Some(i) = v.as_i64() {
860                SerializableVMValue::Int(i)
861            } else {
862                SerializableVMValue::Number(v.as_f64())
863            }
864        }
865        HeapValue::HashMap(d) => {
866            let mut out_keys = Vec::with_capacity(d.keys.len());
867            let mut out_values = Vec::with_capacity(d.values.len());
868            for k in d.keys.iter() {
869                out_keys.push(nanboxed_to_serializable(k, store)?);
870            }
871            for v in d.values.iter() {
872                out_values.push(nanboxed_to_serializable(v, store)?);
873            }
874            SerializableVMValue::HashMap {
875                keys: out_keys,
876                values: out_values,
877            }
878        }
879        HeapValue::Set(d) => {
880            let mut out = Vec::with_capacity(d.items.len());
881            for item in d.items.iter() {
882                out.push(nanboxed_to_serializable(item, store)?);
883            }
884            SerializableVMValue::Array(out)
885        }
886        HeapValue::Deque(d) => {
887            let mut out = Vec::with_capacity(d.items.len());
888            for item in d.items.iter() {
889                out.push(nanboxed_to_serializable(item, store)?);
890            }
891            SerializableVMValue::Array(out)
892        }
893        HeapValue::PriorityQueue(d) => {
894            let mut out = Vec::with_capacity(d.items.len());
895            for item in d.items.iter() {
896                out.push(nanboxed_to_serializable(item, store)?);
897            }
898            SerializableVMValue::Array(out)
899        }
900        HeapValue::Content(node) => SerializableVMValue::String(format!("{}", node)),
901        HeapValue::Instant(t) => {
902            SerializableVMValue::String(format!("<instant:{:?}>", t.elapsed()))
903        }
904        HeapValue::IoHandle(data) => {
905            let status = if data.is_open() { "open" } else { "closed" };
906            SerializableVMValue::String(format!("<io_handle:{}:{}>", data.path, status))
907        }
908        HeapValue::SharedCell(arc) => nanboxed_to_serializable(&arc.read().unwrap(), store)?,
909        HeapValue::IntArray(a) => {
910            let blob = store_chunked_bytes(slice_as_bytes(a.as_slice()), store)?;
911            let hash = store.put_struct(&blob)?;
912            SerializableVMValue::TypedArray {
913                element_kind: TypedArrayElementKind::I64,
914                blob: BlobRef {
915                    hash,
916                    kind: BlobKind::TypedArray(TypedArrayElementKind::I64),
917                },
918                len: a.len(),
919            }
920        }
921        HeapValue::FloatArray(a) => {
922            let blob = store_chunked_bytes(slice_as_bytes(a.as_slice()), store)?;
923            let hash = store.put_struct(&blob)?;
924            SerializableVMValue::TypedArray {
925                element_kind: TypedArrayElementKind::F64,
926                blob: BlobRef {
927                    hash,
928                    kind: BlobKind::TypedArray(TypedArrayElementKind::F64),
929                },
930                len: a.len(),
931            }
932        }
933        HeapValue::BoolArray(a) => {
934            let blob = store_chunked_bytes(slice_as_bytes(a.as_slice()), store)?;
935            let hash = store.put_struct(&blob)?;
936            SerializableVMValue::TypedArray {
937                element_kind: TypedArrayElementKind::Bool,
938                blob: BlobRef {
939                    hash,
940                    kind: BlobKind::TypedArray(TypedArrayElementKind::Bool),
941                },
942                len: a.len(),
943            }
944        }
945        HeapValue::I8Array(a) => {
946            let blob = store_chunked_bytes(slice_as_bytes(a.as_slice()), store)?;
947            let hash = store.put_struct(&blob)?;
948            SerializableVMValue::TypedArray {
949                element_kind: TypedArrayElementKind::I8,
950                blob: BlobRef {
951                    hash,
952                    kind: BlobKind::TypedArray(TypedArrayElementKind::I8),
953                },
954                len: a.len(),
955            }
956        }
957        HeapValue::I16Array(a) => {
958            let blob = store_chunked_bytes(slice_as_bytes(a.as_slice()), store)?;
959            let hash = store.put_struct(&blob)?;
960            SerializableVMValue::TypedArray {
961                element_kind: TypedArrayElementKind::I16,
962                blob: BlobRef {
963                    hash,
964                    kind: BlobKind::TypedArray(TypedArrayElementKind::I16),
965                },
966                len: a.len(),
967            }
968        }
969        HeapValue::I32Array(a) => {
970            let blob = store_chunked_bytes(slice_as_bytes(a.as_slice()), store)?;
971            let hash = store.put_struct(&blob)?;
972            SerializableVMValue::TypedArray {
973                element_kind: TypedArrayElementKind::I32,
974                blob: BlobRef {
975                    hash,
976                    kind: BlobKind::TypedArray(TypedArrayElementKind::I32),
977                },
978                len: a.len(),
979            }
980        }
981        HeapValue::U8Array(a) => {
982            let blob = store_chunked_bytes(slice_as_bytes(a.as_slice()), store)?;
983            let hash = store.put_struct(&blob)?;
984            SerializableVMValue::TypedArray {
985                element_kind: TypedArrayElementKind::U8,
986                blob: BlobRef {
987                    hash,
988                    kind: BlobKind::TypedArray(TypedArrayElementKind::U8),
989                },
990                len: a.len(),
991            }
992        }
993        HeapValue::U16Array(a) => {
994            let blob = store_chunked_bytes(slice_as_bytes(a.as_slice()), store)?;
995            let hash = store.put_struct(&blob)?;
996            SerializableVMValue::TypedArray {
997                element_kind: TypedArrayElementKind::U16,
998                blob: BlobRef {
999                    hash,
1000                    kind: BlobKind::TypedArray(TypedArrayElementKind::U16),
1001                },
1002                len: a.len(),
1003            }
1004        }
1005        HeapValue::U32Array(a) => {
1006            let blob = store_chunked_bytes(slice_as_bytes(a.as_slice()), store)?;
1007            let hash = store.put_struct(&blob)?;
1008            SerializableVMValue::TypedArray {
1009                element_kind: TypedArrayElementKind::U32,
1010                blob: BlobRef {
1011                    hash,
1012                    kind: BlobKind::TypedArray(TypedArrayElementKind::U32),
1013                },
1014                len: a.len(),
1015            }
1016        }
1017        HeapValue::U64Array(a) => {
1018            let blob = store_chunked_bytes(slice_as_bytes(a.as_slice()), store)?;
1019            let hash = store.put_struct(&blob)?;
1020            SerializableVMValue::TypedArray {
1021                element_kind: TypedArrayElementKind::U64,
1022                blob: BlobRef {
1023                    hash,
1024                    kind: BlobKind::TypedArray(TypedArrayElementKind::U64),
1025                },
1026                len: a.len(),
1027            }
1028        }
1029        HeapValue::F32Array(a) => {
1030            let blob = store_chunked_bytes(slice_as_bytes(a.as_slice()), store)?;
1031            let hash = store.put_struct(&blob)?;
1032            SerializableVMValue::TypedArray {
1033                element_kind: TypedArrayElementKind::F32,
1034                blob: BlobRef {
1035                    hash,
1036                    kind: BlobKind::TypedArray(TypedArrayElementKind::F32),
1037                },
1038                len: a.len(),
1039            }
1040        }
1041        HeapValue::Matrix(m) => {
1042            let raw_bytes = slice_as_bytes(m.data.as_slice());
1043            let blob = store_chunked_bytes(raw_bytes, store)?;
1044            let hash = store.put_struct(&blob)?;
1045            SerializableVMValue::Matrix {
1046                blob: BlobRef {
1047                    hash,
1048                    kind: BlobKind::Matrix,
1049                },
1050                rows: m.rows,
1051                cols: m.cols,
1052            }
1053        }
1054        HeapValue::FloatArraySlice {
1055            parent,
1056            offset,
1057            len,
1058        } => {
1059            // Materialize the slice to an owned float array for serialization
1060            let start = *offset as usize;
1061            let end = start + *len as usize;
1062            let owned: Vec<f64> = parent.data[start..end].to_vec();
1063            let blob = store_chunked_bytes(slice_as_bytes(&owned), store)?;
1064            let hash = store.put_struct(&blob)?;
1065            SerializableVMValue::TypedArray {
1066                element_kind: TypedArrayElementKind::F64,
1067                blob: BlobRef {
1068                    hash,
1069                    kind: BlobKind::TypedArray(TypedArrayElementKind::F64),
1070                },
1071                len: *len as usize,
1072            }
1073        }
1074        HeapValue::Char(c) => SerializableVMValue::String(c.to_string()),
1075        HeapValue::Iterator(_)
1076        | HeapValue::Generator(_)
1077        | HeapValue::Mutex(_)
1078        | HeapValue::Atomic(_)
1079        | HeapValue::Lazy(_)
1080        | HeapValue::Channel(_) => {
1081            return Err(anyhow::anyhow!(
1082                "Cannot snapshot transient value: {}",
1083                hv.type_name()
1084            ));
1085        }
1086    })
1087}
1088
1089/// Deserialize a SerializableVMValue directly to ValueWord, avoiding ValueWord intermediate.
1090///
1091/// For inline types (Int, Number, Bool, None, Unit, Function), this constructs the ValueWord
1092/// directly using inline constructors. For heap types, it uses typed ValueWord constructors
1093/// (from_string, from_array, from_decimal, etc.) to skip any intermediate conversion.
1094pub fn serializable_to_nanboxed(
1095    value: &SerializableVMValue,
1096    store: &SnapshotStore,
1097) -> Result<ValueWord> {
1098    Ok(match value {
1099        SerializableVMValue::Int(i) => ValueWord::from_i64(*i),
1100        SerializableVMValue::Number(n) => ValueWord::from_f64(*n),
1101        SerializableVMValue::Decimal(d) => ValueWord::from_decimal(*d),
1102        SerializableVMValue::String(s) => ValueWord::from_string(std::sync::Arc::new(s.clone())),
1103        SerializableVMValue::Bool(b) => ValueWord::from_bool(*b),
1104        SerializableVMValue::None => ValueWord::none(),
1105        SerializableVMValue::Unit => ValueWord::unit(),
1106        SerializableVMValue::Function(f) => ValueWord::from_function(*f),
1107        SerializableVMValue::ModuleFunction(_name) => ValueWord::from_module_function(0),
1108        SerializableVMValue::Some(v) => ValueWord::from_some(serializable_to_nanboxed(v, store)?),
1109        SerializableVMValue::Ok(v) => ValueWord::from_ok(serializable_to_nanboxed(v, store)?),
1110        SerializableVMValue::Err(v) => ValueWord::from_err(serializable_to_nanboxed(v, store)?),
1111        SerializableVMValue::Timeframe(tf) => ValueWord::from_timeframe(*tf),
1112        SerializableVMValue::Duration(d) => ValueWord::from_duration(d.clone()),
1113        SerializableVMValue::Time(t) => ValueWord::from_time(*t),
1114        SerializableVMValue::TimeSpan(ms) => {
1115            ValueWord::from_timespan(chrono::Duration::milliseconds(*ms))
1116        }
1117        SerializableVMValue::TimeReference(tr) => ValueWord::from_time_reference(tr.clone()),
1118        SerializableVMValue::DateTimeExpr(expr) => ValueWord::from_datetime_expr(expr.clone()),
1119        SerializableVMValue::DataDateTimeRef(dref) => {
1120            ValueWord::from_data_datetime_ref(dref.clone())
1121        }
1122        SerializableVMValue::Array(arr) => {
1123            let mut out = Vec::with_capacity(arr.len());
1124            for v in arr.iter() {
1125                out.push(serializable_to_nanboxed(v, store)?);
1126            }
1127            ValueWord::from_array(std::sync::Arc::new(out))
1128        }
1129        SerializableVMValue::TypeAnnotation(ta) => ValueWord::from_type_annotation(ta.clone()),
1130        SerializableVMValue::TypeAnnotatedValue { type_name, value } => {
1131            ValueWord::from_type_annotated_value(
1132                type_name.clone(),
1133                serializable_to_nanboxed(value, store)?,
1134            )
1135        }
1136        SerializableVMValue::Range {
1137            start,
1138            end,
1139            inclusive,
1140        } => ValueWord::from_range(
1141            match start {
1142                Some(v) => Some(serializable_to_nanboxed(v, store)?),
1143                None => None,
1144            },
1145            match end {
1146                Some(v) => Some(serializable_to_nanboxed(v, store)?),
1147                None => None,
1148            },
1149            *inclusive,
1150        ),
1151        SerializableVMValue::Future(id) => ValueWord::from_future(*id),
1152        SerializableVMValue::FunctionRef { name, closure } => ValueWord::from_function_ref(
1153            name.clone(),
1154            match closure {
1155                Some(c) => Some(serializable_to_nanboxed(c, store)?),
1156                None => None,
1157            },
1158        ),
1159        SerializableVMValue::DataReference {
1160            datetime,
1161            id,
1162            timeframe,
1163        } => ValueWord::from_data_reference(*datetime, id.clone(), *timeframe),
1164        SerializableVMValue::DataTable(blob) => {
1165            let ser: SerializableDataTable = store.get_struct(&blob.hash)?;
1166            ValueWord::from_datatable(std::sync::Arc::new(deserialize_datatable(ser, store)?))
1167        }
1168        SerializableVMValue::TypedTable { schema_id, table } => {
1169            let ser: SerializableDataTable = store.get_struct(&table.hash)?;
1170            ValueWord::from_typed_table(
1171                *schema_id,
1172                std::sync::Arc::new(deserialize_datatable(ser, store)?),
1173            )
1174        }
1175        SerializableVMValue::RowView {
1176            schema_id,
1177            table,
1178            row_idx,
1179        } => {
1180            let ser: SerializableDataTable = store.get_struct(&table.hash)?;
1181            ValueWord::from_row_view(
1182                *schema_id,
1183                std::sync::Arc::new(deserialize_datatable(ser, store)?),
1184                *row_idx,
1185            )
1186        }
1187        SerializableVMValue::ColumnRef {
1188            schema_id,
1189            table,
1190            col_id,
1191        } => {
1192            let ser: SerializableDataTable = store.get_struct(&table.hash)?;
1193            ValueWord::from_column_ref(
1194                *schema_id,
1195                std::sync::Arc::new(deserialize_datatable(ser, store)?),
1196                *col_id,
1197            )
1198        }
1199        SerializableVMValue::IndexedTable {
1200            schema_id,
1201            table,
1202            index_col,
1203        } => {
1204            let ser: SerializableDataTable = store.get_struct(&table.hash)?;
1205            ValueWord::from_indexed_table(
1206                *schema_id,
1207                std::sync::Arc::new(deserialize_datatable(ser, store)?),
1208                *index_col,
1209            )
1210        }
1211        SerializableVMValue::TypedArray {
1212            element_kind,
1213            blob,
1214            len,
1215        } => {
1216            let chunked: ChunkedBlob = store.get_struct(&blob.hash)?;
1217            let raw = load_chunked_bytes(&chunked, store)?;
1218            match element_kind {
1219                TypedArrayElementKind::I64 => {
1220                    let data: Vec<i64> = bytes_as_slice::<i64>(&raw)[..*len].to_vec();
1221                    ValueWord::from_int_array(std::sync::Arc::new(
1222                        shape_value::TypedBuffer::from_vec(data),
1223                    ))
1224                }
1225                TypedArrayElementKind::F64 => {
1226                    let data: Vec<f64> = bytes_as_slice::<f64>(&raw)[..*len].to_vec();
1227                    let aligned = shape_value::AlignedVec::from_vec(data);
1228                    ValueWord::from_float_array(std::sync::Arc::new(
1229                        shape_value::AlignedTypedBuffer::from_aligned(aligned),
1230                    ))
1231                }
1232                TypedArrayElementKind::Bool => {
1233                    let data: Vec<u8> = raw[..*len].to_vec();
1234                    ValueWord::from_bool_array(std::sync::Arc::new(
1235                        shape_value::TypedBuffer::from_vec(data),
1236                    ))
1237                }
1238                TypedArrayElementKind::I8 => {
1239                    let data: Vec<i8> = bytes_as_slice::<i8>(&raw)[..*len].to_vec();
1240                    ValueWord::from_i8_array(std::sync::Arc::new(
1241                        shape_value::TypedBuffer::from_vec(data),
1242                    ))
1243                }
1244                TypedArrayElementKind::I16 => {
1245                    let data: Vec<i16> = bytes_as_slice::<i16>(&raw)[..*len].to_vec();
1246                    ValueWord::from_i16_array(std::sync::Arc::new(
1247                        shape_value::TypedBuffer::from_vec(data),
1248                    ))
1249                }
1250                TypedArrayElementKind::I32 => {
1251                    let data: Vec<i32> = bytes_as_slice::<i32>(&raw)[..*len].to_vec();
1252                    ValueWord::from_i32_array(std::sync::Arc::new(
1253                        shape_value::TypedBuffer::from_vec(data),
1254                    ))
1255                }
1256                TypedArrayElementKind::U8 => {
1257                    let data: Vec<u8> = raw[..*len].to_vec();
1258                    ValueWord::from_u8_array(std::sync::Arc::new(
1259                        shape_value::TypedBuffer::from_vec(data),
1260                    ))
1261                }
1262                TypedArrayElementKind::U16 => {
1263                    let data: Vec<u16> = bytes_as_slice::<u16>(&raw)[..*len].to_vec();
1264                    ValueWord::from_u16_array(std::sync::Arc::new(
1265                        shape_value::TypedBuffer::from_vec(data),
1266                    ))
1267                }
1268                TypedArrayElementKind::U32 => {
1269                    let data: Vec<u32> = bytes_as_slice::<u32>(&raw)[..*len].to_vec();
1270                    ValueWord::from_u32_array(std::sync::Arc::new(
1271                        shape_value::TypedBuffer::from_vec(data),
1272                    ))
1273                }
1274                TypedArrayElementKind::U64 => {
1275                    let data: Vec<u64> = bytes_as_slice::<u64>(&raw)[..*len].to_vec();
1276                    ValueWord::from_u64_array(std::sync::Arc::new(
1277                        shape_value::TypedBuffer::from_vec(data),
1278                    ))
1279                }
1280                TypedArrayElementKind::F32 => {
1281                    let data: Vec<f32> = bytes_as_slice::<f32>(&raw)[..*len].to_vec();
1282                    ValueWord::from_f32_array(std::sync::Arc::new(
1283                        shape_value::TypedBuffer::from_vec(data),
1284                    ))
1285                }
1286            }
1287        }
1288        SerializableVMValue::Matrix { blob, rows, cols } => {
1289            let chunked: ChunkedBlob = store.get_struct(&blob.hash)?;
1290            let raw = load_chunked_bytes(&chunked, store)?;
1291            let data: Vec<f64> = bytes_as_slice::<f64>(&raw).to_vec();
1292            let aligned = shape_value::AlignedVec::from_vec(data);
1293            let matrix = shape_value::heap_value::MatrixData::from_flat(aligned, *rows, *cols);
1294            ValueWord::from_matrix(std::sync::Arc::new(matrix))
1295        }
1296        SerializableVMValue::HashMap { keys, values } => {
1297            let mut k_out = Vec::with_capacity(keys.len());
1298            for k in keys.iter() {
1299                k_out.push(serializable_to_nanboxed(k, store)?);
1300            }
1301            let mut v_out = Vec::with_capacity(values.len());
1302            for v in values.iter() {
1303                v_out.push(serializable_to_nanboxed(v, store)?);
1304            }
1305            ValueWord::from_hashmap_pairs(k_out, v_out)
1306        }
1307        SerializableVMValue::SidecarRef { .. } => {
1308            return Err(anyhow::anyhow!(
1309                "SidecarRef must be reassembled before deserialization"
1310            ));
1311        }
1312        SerializableVMValue::Enum(ev) => {
1313            // EnumPayload stores ValueWord internally (structural boundary in shape-value)
1314            let enum_val = snapshot_to_enum(ev, store)?;
1315            enum_val
1316        }
1317        SerializableVMValue::Closure {
1318            function_id,
1319            upvalues,
1320        } => {
1321            let mut ups = Vec::new();
1322            for v in upvalues.iter() {
1323                ups.push(Upvalue::new(serializable_to_nanboxed(v, store)?));
1324            }
1325            ValueWord::from_heap_value(shape_value::heap_value::HeapValue::Closure {
1326                function_id: *function_id,
1327                upvalues: ups,
1328            })
1329        }
1330        SerializableVMValue::TypedObject {
1331            schema_id,
1332            slot_data,
1333            heap_mask,
1334        } => {
1335            let mut slots = Vec::with_capacity(slot_data.len());
1336            let mut new_heap_mask: u64 = 0;
1337            for (i, sv) in slot_data.iter().enumerate() {
1338                if *heap_mask & (1u64 << i) != 0 {
1339                    // Heap slot: deserialize to ValueWord, then convert to slot.
1340                    // Backward compat: old snapshots may have inline types (Number,
1341                    // Bool, None, Unit, Function) marked as heap. nb_to_slot handles
1342                    // this by storing them as inline ValueSlots with is_heap=false.
1343                    let nb = serializable_to_nanboxed(sv, store)?;
1344                    let (slot, is_heap) = crate::type_schema::nb_to_slot(&nb);
1345                    slots.push(slot);
1346                    if is_heap {
1347                        new_heap_mask |= 1u64 << i;
1348                    }
1349                } else {
1350                    // Simple slot: extract f64 raw bits
1351                    let n = match sv {
1352                        SerializableVMValue::Number(n) => *n,
1353                        _ => 0.0,
1354                    };
1355                    slots.push(shape_value::ValueSlot::from_number(n));
1356                }
1357            }
1358            ValueWord::from_heap_value(shape_value::heap_value::HeapValue::TypedObject {
1359                schema_id: *schema_id,
1360                slots: slots.into_boxed_slice(),
1361                heap_mask: new_heap_mask,
1362            })
1363        }
1364        SerializableVMValue::PrintResult(pr) => {
1365            // PrintResult contains ValueWord internally (structural boundary in shape-value)
1366            let print_result = snapshot_to_print_result(pr, store)?;
1367            ValueWord::from_print_result(print_result)
1368        }
1369        SerializableVMValue::SimulationCall { name, params } => {
1370            // SimulationCallData stores ValueWord (structural boundary in shape-value)
1371            let mut out = HashMap::new();
1372            for (k, v) in params.iter() {
1373                out.insert(k.clone(), serializable_to_nanboxed(v, store)?.clone());
1374            }
1375            ValueWord::from_simulation_call(name.clone(), out)
1376        }
1377    })
1378}
1379
1380fn enum_to_snapshot(value: &EnumValue, store: &SnapshotStore) -> Result<EnumValueSnapshot> {
1381    Ok(EnumValueSnapshot {
1382        enum_name: value.enum_name.clone(),
1383        variant: value.variant.clone(),
1384        payload: match &value.payload {
1385            EnumPayload::Unit => EnumPayloadSnapshot::Unit,
1386            EnumPayload::Tuple(values) => {
1387                let mut out = Vec::new();
1388                for v in values.iter() {
1389                    out.push(nanboxed_to_serializable(v, store)?);
1390                }
1391                EnumPayloadSnapshot::Tuple(out)
1392            }
1393            EnumPayload::Struct(map) => {
1394                let mut out = Vec::new();
1395                for (k, v) in map.iter() {
1396                    out.push((k.clone(), nanboxed_to_serializable(v, store)?));
1397                }
1398                EnumPayloadSnapshot::Struct(out)
1399            }
1400        },
1401    })
1402}
1403
1404fn snapshot_to_enum(snapshot: &EnumValueSnapshot, store: &SnapshotStore) -> Result<ValueWord> {
1405    Ok(ValueWord::from_enum(EnumValue {
1406        enum_name: snapshot.enum_name.clone(),
1407        variant: snapshot.variant.clone(),
1408        payload: match &snapshot.payload {
1409            EnumPayloadSnapshot::Unit => EnumPayload::Unit,
1410            EnumPayloadSnapshot::Tuple(values) => {
1411                let mut out = Vec::new();
1412                for v in values.iter() {
1413                    out.push(serializable_to_nanboxed(v, store)?);
1414                }
1415                EnumPayload::Tuple(out)
1416            }
1417            EnumPayloadSnapshot::Struct(map) => {
1418                let mut out = HashMap::new();
1419                for (k, v) in map.iter() {
1420                    out.insert(k.clone(), serializable_to_nanboxed(v, store)?);
1421                }
1422                EnumPayload::Struct(out)
1423            }
1424        },
1425    }))
1426}
1427
1428fn print_result_to_snapshot(
1429    result: &PrintResult,
1430    store: &SnapshotStore,
1431) -> Result<PrintableSnapshot> {
1432    let mut spans = Vec::new();
1433    for span in result.spans.iter() {
1434        match span {
1435            PrintSpan::Literal {
1436                text,
1437                start,
1438                end,
1439                span_id,
1440            } => spans.push(PrintSpanSnapshot::Literal {
1441                text: text.clone(),
1442                start: *start,
1443                end: *end,
1444                span_id: span_id.clone(),
1445            }),
1446            PrintSpan::Value {
1447                text,
1448                start,
1449                end,
1450                span_id,
1451                variable_name,
1452                raw_value,
1453                type_name,
1454                current_format,
1455                format_params,
1456            } => {
1457                let mut params = HashMap::new();
1458                for (k, v) in format_params.iter() {
1459                    params.insert(k.clone(), nanboxed_to_serializable(&v.clone(), store)?);
1460                }
1461                spans.push(PrintSpanSnapshot::Value {
1462                    text: text.clone(),
1463                    start: *start,
1464                    end: *end,
1465                    span_id: span_id.clone(),
1466                    variable_name: variable_name.clone(),
1467                    raw_value: Box::new(nanboxed_to_serializable(raw_value.as_ref(), store)?),
1468                    type_name: type_name.clone(),
1469                    current_format: current_format.clone(),
1470                    format_params: params,
1471                });
1472            }
1473        }
1474    }
1475    Ok(PrintableSnapshot {
1476        rendered: result.rendered.clone(),
1477        spans,
1478    })
1479}
1480
1481fn snapshot_to_print_result(
1482    snapshot: &PrintableSnapshot,
1483    store: &SnapshotStore,
1484) -> Result<PrintResult> {
1485    let mut spans = Vec::new();
1486    for span in snapshot.spans.iter() {
1487        match span {
1488            PrintSpanSnapshot::Literal {
1489                text,
1490                start,
1491                end,
1492                span_id,
1493            } => {
1494                spans.push(PrintSpan::Literal {
1495                    text: text.clone(),
1496                    start: *start,
1497                    end: *end,
1498                    span_id: span_id.clone(),
1499                });
1500            }
1501            PrintSpanSnapshot::Value {
1502                text,
1503                start,
1504                end,
1505                span_id,
1506                variable_name,
1507                raw_value,
1508                type_name,
1509                current_format,
1510                format_params,
1511            } => {
1512                let mut params = HashMap::new();
1513                for (k, v) in format_params.iter() {
1514                    params.insert(k.clone(), serializable_to_nanboxed(v, store)?.clone());
1515                }
1516                spans.push(PrintSpan::Value {
1517                    text: text.clone(),
1518                    start: *start,
1519                    end: *end,
1520                    span_id: span_id.clone(),
1521                    variable_name: variable_name.clone(),
1522                    raw_value: Box::new(serializable_to_nanboxed(raw_value, store)?.clone()),
1523                    type_name: type_name.clone(),
1524                    current_format: current_format.clone(),
1525                    format_params: params,
1526                });
1527            }
1528        }
1529    }
1530    Ok(PrintResult {
1531        rendered: snapshot.rendered.clone(),
1532        spans,
1533    })
1534}
1535
1536pub(crate) fn serialize_dataframe(
1537    df: &DataFrame,
1538    store: &SnapshotStore,
1539) -> Result<SerializableDataFrame> {
1540    let mut columns: Vec<_> = df.columns.iter().collect();
1541    columns.sort_by(|a, b| a.0.cmp(b.0));
1542    let mut serialized_cols = Vec::with_capacity(columns.len());
1543    for (name, values) in columns.into_iter() {
1544        let blob = store_chunked_vec(values, DEFAULT_CHUNK_LEN, store)?;
1545        serialized_cols.push(SerializableDataFrameColumn {
1546            name: name.clone(),
1547            values: blob,
1548        });
1549    }
1550    Ok(SerializableDataFrame {
1551        id: df.id.clone(),
1552        timeframe: df.timeframe,
1553        timestamps: store_chunked_vec(&df.timestamps, DEFAULT_CHUNK_LEN, store)?,
1554        columns: serialized_cols,
1555    })
1556}
1557
1558pub(crate) fn deserialize_dataframe(
1559    serialized: SerializableDataFrame,
1560    store: &SnapshotStore,
1561) -> Result<DataFrame> {
1562    let timestamps: Vec<i64> = load_chunked_vec(&serialized.timestamps, store)?;
1563    let mut columns = HashMap::new();
1564    for col in serialized.columns.into_iter() {
1565        let values: Vec<f64> = load_chunked_vec(&col.values, store)?;
1566        columns.insert(col.name, values);
1567    }
1568    Ok(DataFrame {
1569        id: serialized.id,
1570        timeframe: serialized.timeframe,
1571        timestamps,
1572        columns,
1573    })
1574}
1575
1576fn serialize_datatable(dt: &DataTable, store: &SnapshotStore) -> Result<SerializableDataTable> {
1577    let mut buf = Vec::new();
1578    let schema = dt.inner().schema();
1579    let mut writer = arrow_ipc::writer::FileWriter::try_new(&mut buf, schema.as_ref())?;
1580    writer.write(dt.inner())?;
1581    writer.finish()?;
1582    let ipc_chunks = store_chunked_vec(&buf, BYTE_CHUNK_LEN, store)?;
1583    Ok(SerializableDataTable {
1584        ipc_chunks,
1585        type_name: dt.type_name().map(|s| s.to_string()),
1586        schema_id: dt.schema_id(),
1587    })
1588}
1589
1590fn deserialize_datatable(
1591    serialized: SerializableDataTable,
1592    store: &SnapshotStore,
1593) -> Result<DataTable> {
1594    let bytes = load_chunked_vec(&serialized.ipc_chunks, store)?;
1595    let cursor = std::io::Cursor::new(bytes);
1596    let mut reader = arrow_ipc::reader::FileReader::try_new(cursor, None)?;
1597    let batch = reader
1598        .next()
1599        .transpose()?
1600        .context("no RecordBatch in DataTable snapshot")?;
1601    let mut dt = DataTable::new(batch);
1602    if let Some(name) = serialized.type_name {
1603        dt = DataTable::with_type_name(dt.into_inner(), name);
1604    }
1605    if let Some(schema_id) = serialized.schema_id {
1606        dt = dt.with_schema_id(schema_id);
1607    }
1608    Ok(dt)
1609}
1610
1611#[cfg(test)]
1612mod tests {
1613    use super::*;
1614    use arrow_array::{Float64Array, Int64Array, RecordBatch};
1615    use arrow_schema::{DataType, Field, Schema};
1616    use std::sync::Arc;
1617
1618    /// Helper: build a 2-column Arrow DataTable (id: i64, value: f64).
1619    fn make_test_table(ids: &[i64], values: &[f64]) -> DataTable {
1620        let schema = Arc::new(Schema::new(vec![
1621            Field::new("id", DataType::Int64, false),
1622            Field::new("value", DataType::Float64, false),
1623        ]));
1624        let batch = RecordBatch::try_new(
1625            schema,
1626            vec![
1627                Arc::new(Int64Array::from(ids.to_vec())),
1628                Arc::new(Float64Array::from(values.to_vec())),
1629            ],
1630        )
1631        .unwrap();
1632        DataTable::new(batch)
1633    }
1634
1635    #[test]
1636    fn test_datatable_snapshot_roundtrip_preserves_original_data() {
1637        let dir = tempfile::tempdir().unwrap();
1638        let store_path = dir.path().join("store");
1639
1640        let original_dt = make_test_table(&[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], &[150.0; 10]);
1641        assert_eq!(original_dt.row_count(), 10);
1642        let original_nb = ValueWord::from_datatable(Arc::new(original_dt));
1643
1644        // Serialize to snapshot store
1645        let store = SnapshotStore::new(&store_path).unwrap();
1646        let serialized = nanboxed_to_serializable(&original_nb, &store).unwrap();
1647
1648        // Restore from snapshot — must have ORIGINAL data
1649        let restored_nb = serializable_to_nanboxed(&serialized, &store).unwrap();
1650        let restored = restored_nb.clone();
1651        let dt = restored
1652            .as_datatable()
1653            .expect("Expected DataTable after restore");
1654        assert_eq!(
1655            dt.row_count(),
1656            10,
1657            "restored table should have original 10 rows"
1658        );
1659    }
1660
1661    #[test]
1662    fn test_indexed_table_snapshot_roundtrip() {
1663        let dir = tempfile::tempdir().unwrap();
1664        let store = SnapshotStore::new(dir.path().join("store")).unwrap();
1665
1666        let dt = make_test_table(&[1, 2, 3], &[10.0, 20.0, 30.0]);
1667        let original_nb = ValueWord::from_indexed_table(1, Arc::new(dt), 0);
1668
1669        let serialized = nanboxed_to_serializable(&original_nb, &store).unwrap();
1670
1671        // Verify the serialized form is IndexedTable, NOT ColumnRef
1672        match &serialized {
1673            SerializableVMValue::IndexedTable {
1674                schema_id,
1675                index_col,
1676                ..
1677            } => {
1678                assert_eq!(schema_id, &1);
1679                assert_eq!(index_col, &0);
1680            }
1681            other => panic!(
1682                "Expected SerializableVMValue::IndexedTable, got {:?}",
1683                std::mem::discriminant(other)
1684            ),
1685        }
1686
1687        let restored = serializable_to_nanboxed(&serialized, &store)
1688            .unwrap()
1689            .clone();
1690        let (schema_id, table, index_col) = restored
1691            .as_indexed_table()
1692            .expect("Expected ValueWord::IndexedTable");
1693        assert_eq!(schema_id, 1);
1694        assert_eq!(index_col, 0);
1695        assert_eq!(table.row_count(), 3);
1696    }
1697
1698    #[test]
1699    fn test_typed_table_snapshot_roundtrip() {
1700        let dir = tempfile::tempdir().unwrap();
1701        let store = SnapshotStore::new(dir.path().join("store")).unwrap();
1702
1703        let dt = make_test_table(&[10, 20], &[1.5, 2.5]);
1704        let original_nb = ValueWord::from_typed_table(42, Arc::new(dt));
1705
1706        let serialized = nanboxed_to_serializable(&original_nb, &store).unwrap();
1707        let restored = serializable_to_nanboxed(&serialized, &store)
1708            .unwrap()
1709            .clone();
1710
1711        let (schema_id, table) = restored
1712            .as_typed_table()
1713            .expect("Expected ValueWord::TypedTable");
1714        assert_eq!(schema_id, 42);
1715        assert_eq!(table.row_count(), 2);
1716        let vals = table.get_f64_column("value").unwrap();
1717        assert!((vals.value(0) - 1.5).abs() < f64::EPSILON);
1718        assert!((vals.value(1) - 2.5).abs() < f64::EPSILON);
1719    }
1720
1721    // ---- Phase 3A: Collection serialization tests ----
1722
1723    #[test]
1724    fn test_float_array_typed_roundtrip() {
1725        let dir = tempfile::tempdir().unwrap();
1726        let store = SnapshotStore::new(dir.path().join("store")).unwrap();
1727
1728        let data: Vec<f64> = (0..1000).map(|i| i as f64 * 1.5).collect();
1729        let aligned = shape_value::AlignedVec::from_vec(data.clone());
1730        let buf = shape_value::AlignedTypedBuffer::from_aligned(aligned);
1731        let nb = ValueWord::from_float_array(Arc::new(buf));
1732
1733        let serialized = nanboxed_to_serializable(&nb, &store).unwrap();
1734        match &serialized {
1735            SerializableVMValue::TypedArray {
1736                element_kind, len, ..
1737            } => {
1738                assert_eq!(*element_kind, TypedArrayElementKind::F64);
1739                assert_eq!(*len, 1000);
1740            }
1741            other => panic!(
1742                "Expected TypedArray, got {:?}",
1743                std::mem::discriminant(other)
1744            ),
1745        }
1746
1747        let restored = serializable_to_nanboxed(&serialized, &store).unwrap();
1748        let hv = restored.as_heap_ref().unwrap();
1749        match hv {
1750            shape_value::heap_value::HeapValue::FloatArray(a) => {
1751                assert_eq!(a.len(), 1000);
1752                for i in 0..1000 {
1753                    assert!((a.as_slice()[i] - data[i]).abs() < f64::EPSILON);
1754                }
1755            }
1756            _ => panic!("Expected FloatArray after restore"),
1757        }
1758    }
1759
1760    #[test]
1761    fn test_int_array_typed_roundtrip() {
1762        let dir = tempfile::tempdir().unwrap();
1763        let store = SnapshotStore::new(dir.path().join("store")).unwrap();
1764
1765        let data: Vec<i64> = (0..500).map(|i| i * 3 - 100).collect();
1766        let buf = shape_value::TypedBuffer::from_vec(data.clone());
1767        let nb = ValueWord::from_int_array(Arc::new(buf));
1768
1769        let serialized = nanboxed_to_serializable(&nb, &store).unwrap();
1770        match &serialized {
1771            SerializableVMValue::TypedArray {
1772                element_kind, len, ..
1773            } => {
1774                assert_eq!(*element_kind, TypedArrayElementKind::I64);
1775                assert_eq!(*len, 500);
1776            }
1777            other => panic!(
1778                "Expected TypedArray, got {:?}",
1779                std::mem::discriminant(other)
1780            ),
1781        }
1782
1783        let restored = serializable_to_nanboxed(&serialized, &store).unwrap();
1784        let hv = restored.as_heap_ref().unwrap();
1785        match hv {
1786            shape_value::heap_value::HeapValue::IntArray(a) => {
1787                assert_eq!(a.len(), 500);
1788                for i in 0..500 {
1789                    assert_eq!(a.as_slice()[i], data[i]);
1790                }
1791            }
1792            _ => panic!("Expected IntArray after restore"),
1793        }
1794    }
1795
1796    #[test]
1797    fn test_matrix_roundtrip() {
1798        let dir = tempfile::tempdir().unwrap();
1799        let store = SnapshotStore::new(dir.path().join("store")).unwrap();
1800
1801        let data: Vec<f64> = (0..12).map(|i| i as f64).collect();
1802        let aligned = shape_value::AlignedVec::from_vec(data.clone());
1803        let matrix = shape_value::heap_value::MatrixData::from_flat(aligned, 3, 4);
1804        let nb = ValueWord::from_matrix(std::sync::Arc::new(matrix));
1805
1806        let serialized = nanboxed_to_serializable(&nb, &store).unwrap();
1807        match &serialized {
1808            SerializableVMValue::Matrix { rows, cols, .. } => {
1809                assert_eq!(*rows, 3);
1810                assert_eq!(*cols, 4);
1811            }
1812            other => panic!("Expected Matrix, got {:?}", std::mem::discriminant(other)),
1813        }
1814
1815        let restored = serializable_to_nanboxed(&serialized, &store).unwrap();
1816        let hv = restored.as_heap_ref().unwrap();
1817        match hv {
1818            shape_value::heap_value::HeapValue::Matrix(m) => {
1819                assert_eq!(m.rows, 3);
1820                assert_eq!(m.cols, 4);
1821                for i in 0..12 {
1822                    assert!((m.data.as_slice()[i] - data[i]).abs() < f64::EPSILON);
1823                }
1824            }
1825            _ => panic!("Expected Matrix after restore"),
1826        }
1827    }
1828
1829    #[test]
1830    fn test_hashmap_typed_roundtrip() {
1831        let dir = tempfile::tempdir().unwrap();
1832        let store = SnapshotStore::new(dir.path().join("store")).unwrap();
1833
1834        let keys = vec![
1835            ValueWord::from_string(Arc::new("a".to_string())),
1836            ValueWord::from_string(Arc::new("b".to_string())),
1837        ];
1838        let values = vec![ValueWord::from_i64(1), ValueWord::from_i64(2)];
1839        let nb = ValueWord::from_hashmap_pairs(keys, values);
1840
1841        let serialized = nanboxed_to_serializable(&nb, &store).unwrap();
1842        match &serialized {
1843            SerializableVMValue::HashMap { keys, values } => {
1844                assert_eq!(keys.len(), 2);
1845                assert_eq!(values.len(), 2);
1846            }
1847            other => panic!("Expected HashMap, got {:?}", std::mem::discriminant(other)),
1848        }
1849
1850        let restored = serializable_to_nanboxed(&serialized, &store).unwrap();
1851        let hv = restored.as_heap_ref().unwrap();
1852        match hv {
1853            shape_value::heap_value::HeapValue::HashMap(d) => {
1854                assert_eq!(d.keys.len(), 2);
1855                assert_eq!(d.values.len(), 2);
1856                // Verify index works (key lookup)
1857                let key_a = ValueWord::from_string(Arc::new("a".to_string()));
1858                let idx = d.find_key(&key_a);
1859                assert!(idx.is_some(), "should find key 'a' in rebuilt index");
1860            }
1861            _ => panic!("Expected HashMap after restore"),
1862        }
1863    }
1864
1865    #[test]
1866    fn test_bool_array_typed_roundtrip() {
1867        let dir = tempfile::tempdir().unwrap();
1868        let store = SnapshotStore::new(dir.path().join("store")).unwrap();
1869
1870        let data: Vec<u8> = vec![1, 0, 1, 1, 0];
1871        let buf = shape_value::TypedBuffer::from_vec(data.clone());
1872        let nb = ValueWord::from_bool_array(Arc::new(buf));
1873
1874        let serialized = nanboxed_to_serializable(&nb, &store).unwrap();
1875        match &serialized {
1876            SerializableVMValue::TypedArray {
1877                element_kind, len, ..
1878            } => {
1879                assert_eq!(*element_kind, TypedArrayElementKind::Bool);
1880                assert_eq!(*len, 5);
1881            }
1882            other => panic!(
1883                "Expected TypedArray, got {:?}",
1884                std::mem::discriminant(other)
1885            ),
1886        }
1887
1888        let restored = serializable_to_nanboxed(&serialized, &store).unwrap();
1889        let hv = restored.as_heap_ref().unwrap();
1890        match hv {
1891            shape_value::heap_value::HeapValue::BoolArray(a) => {
1892                assert_eq!(a.len(), 5);
1893                assert_eq!(a.as_slice(), &data);
1894            }
1895            _ => panic!("Expected BoolArray after restore"),
1896        }
1897    }
1898
1899    /// Comprehensive bincode round-trip test for ALL snapshot component types,
1900    /// exercising every type in the deserialization chain including Decimal.
1901    #[test]
1902    fn test_all_snapshot_components_bincode_roundtrip() {
1903        use rust_decimal::Decimal;
1904        use shape_ast::ast::VarKind;
1905        use shape_ast::data::Timeframe;
1906
1907        // 1. SerializableVMValue with Decimal
1908        let decimal_val = SerializableVMValue::Decimal(Decimal::new(31415, 4)); // 3.1415
1909        let bytes = bincode::serialize(&decimal_val).expect("serialize Decimal ValueWord");
1910        let decoded: SerializableVMValue =
1911            bincode::deserialize(&bytes).expect("deserialize Decimal ValueWord");
1912        match decoded {
1913            SerializableVMValue::Decimal(d) => assert_eq!(d, Decimal::new(31415, 4)),
1914            _ => panic!("wrong variant"),
1915        }
1916
1917        // 2. VmSnapshot with Decimal in stack/locals/module_bindings
1918        let vm_snap = VmSnapshot {
1919            ip: 42,
1920            stack: vec![
1921                SerializableVMValue::Int(1),
1922                SerializableVMValue::Decimal(Decimal::new(99999, 2)),
1923                SerializableVMValue::String("hello".into()),
1924            ],
1925            locals: vec![SerializableVMValue::Decimal(Decimal::new(0, 0))],
1926            module_bindings: vec![SerializableVMValue::Number(3.14)],
1927            call_stack: vec![],
1928            loop_stack: vec![],
1929            timeframe_stack: vec![],
1930            exception_handlers: vec![],
1931            ip_blob_hash: None,
1932            ip_local_offset: None,
1933            ip_function_id: None,
1934        };
1935        let bytes = bincode::serialize(&vm_snap).expect("serialize VmSnapshot");
1936        let decoded: VmSnapshot = bincode::deserialize(&bytes).expect("deserialize VmSnapshot");
1937        assert_eq!(decoded.ip, 42);
1938        assert_eq!(decoded.stack.len(), 3);
1939
1940        // 3. ContextSnapshot with Decimal in variable scopes
1941        let ctx_snap = ContextSnapshot {
1942            data_load_mode: crate::context::DataLoadMode::Async,
1943            data_cache: None,
1944            current_id: Some("test".into()),
1945            current_row_index: 0,
1946            variable_scopes: vec![{
1947                let mut scope = HashMap::new();
1948                scope.insert(
1949                    "price".into(),
1950                    VariableSnapshot {
1951                        value: SerializableVMValue::Decimal(Decimal::new(15099, 2)),
1952                        kind: VarKind::Let,
1953                        is_initialized: true,
1954                        is_function_scoped: false,
1955                        format_hint: None,
1956                        format_overrides: None,
1957                    },
1958                );
1959                scope
1960            }],
1961            reference_datetime: None,
1962            current_timeframe: Some(Timeframe::m1()),
1963            base_timeframe: None,
1964            date_range: None,
1965            range_start: 0,
1966            range_end: 0,
1967            range_active: false,
1968            type_alias_registry: HashMap::new(),
1969            enum_registry: HashMap::new(),
1970            struct_type_registry: HashMap::new(),
1971            suspension_state: None,
1972        };
1973        let bytes = bincode::serialize(&ctx_snap).expect("serialize ContextSnapshot");
1974        let decoded: ContextSnapshot =
1975            bincode::deserialize(&bytes).expect("deserialize ContextSnapshot");
1976        assert_eq!(decoded.variable_scopes.len(), 1);
1977
1978        // 4. SemanticSnapshot
1979        let sem_snap = SemanticSnapshot {
1980            exported_symbols: HashSet::new(),
1981        };
1982        let bytes = bincode::serialize(&sem_snap).expect("serialize SemanticSnapshot");
1983        let _decoded: SemanticSnapshot =
1984            bincode::deserialize(&bytes).expect("deserialize SemanticSnapshot");
1985
1986        // 5. ExecutionSnapshot (top-level)
1987        let exec_snap = ExecutionSnapshot {
1988            version: SNAPSHOT_VERSION,
1989            created_at_ms: 1234567890,
1990            semantic_hash: HashDigest::from_hex("abc123"),
1991            context_hash: HashDigest::from_hex("def456"),
1992            vm_hash: Some(HashDigest::from_hex("789aaa")),
1993            bytecode_hash: Some(HashDigest::from_hex("bbb000")),
1994            script_path: Some("/tmp/test.shape".into()),
1995        };
1996        let bytes = bincode::serialize(&exec_snap).expect("serialize ExecutionSnapshot");
1997        let decoded: ExecutionSnapshot =
1998            bincode::deserialize(&bytes).expect("deserialize ExecutionSnapshot");
1999        assert_eq!(decoded.version, SNAPSHOT_VERSION);
2000    }
2001}