Skip to main content

sui_bytecode/
nanbox.rs

1//! NaN-boxed value representation for the VM stack.
2//!
3//! Packs all value types into exactly 8 bytes using the quiet NaN
4//! payload bits of IEEE 754 doubles. This eliminates heap allocation
5//! for scalars and makes the value stack cache-friendly.
6//!
7//! # Layout
8//!
9//! IEEE 754 double:
10//! ```text
11//! [sign:1] [exponent:11] [mantissa:52]
12//! ```
13//!
14//! A quiet NaN has exponent = all 1s and mantissa MSB = 1.
15//! We use the remaining bits for a type tag + payload:
16//!
17//! ```text
18//! Float:  any valid f64 that is not a signaling NaN with our tag pattern
19//! Tagged: 0x7FF8_xxxx_xxxx_xxxx  (quiet NaN space)
20//!   Tag bits [48..51] encode the type:
21//!     0x0 = Null
22//!     0x1 = Bool(false)
23//!     0x2 = Bool(true)
24//!     0x3 = Int (payload: i48 in bits [0..47])
25//!     0x4 = Pointer to heap object (payload: 48-bit pointer)
26//! ```
27//!
28//! # Performance Impact
29//!
30//! - Stack entries: 8 bytes instead of 40-80 bytes (enum VMValue)
31//! - No heap allocation for null, bool, int, float
32//! - Cache-friendly: entire stack fits in L1/L2 for typical expressions
33//! - Copy is a single 64-bit register move
34
35use std::collections::BTreeMap;
36use std::fmt;
37use std::rc::Rc;
38
39use crate::error::VMError;
40use crate::intern::Symbol;
41use crate::value::{HigherOrderBuiltin, ThunkState, VMBuiltin, VMClosure, VMThunk, VMValue};
42
43/// Quiet NaN base: exponent all 1s, mantissa MSB = 1.
44const QNAN: u64 = 0x7FF8_0000_0000_0000;
45/// Mask for the tag bits (bits 48-51, 4 bits = 16 possible tags).
46const TAG_SHIFT: u64 = 48;
47/// 48-bit payload mask.
48const PAYLOAD_MASK: u64 = 0x0000_FFFF_FFFF_FFFF;
49
50// Tag values (shifted into position).
51const TAG_NULL: u64 = QNAN | (0x0 << TAG_SHIFT);
52const TAG_FALSE: u64 = QNAN | (0x1 << TAG_SHIFT);
53const TAG_TRUE: u64 = QNAN | (0x2 << TAG_SHIFT);
54const TAG_INT: u64 = QNAN | (0x3 << TAG_SHIFT);
55const TAG_PTR: u64 = QNAN | (0x4 << TAG_SHIFT);
56
57/// Tag extraction mask: QNAN + tag bits.
58const TAG_MASK: u64 = QNAN | (0xF << TAG_SHIFT);
59
60/// A NaN-boxed value: 8 bytes encoding any VM value type.
61///
62/// Floats are stored as-is (their bit pattern). Non-float values
63/// are encoded in the NaN payload space.
64pub struct NanBox(u64);
65
66/// Heap-allocated object referenced by a `NanBox` pointer tag.
67///
68/// Contains the non-scalar VM value types: String, Path, List, Attrs,
69/// Closure, Builtin, and Thunk.
70///
71/// `BigInt` is a special case: it holds an `i64` value that doesn't
72/// fit in NanBox's 48-bit tagged payload.  CppNix uses `int64_t` for
73/// every Nix integer; without this heap fallback NanBox::int would
74/// silently demote out-of-range values to f64, losing precision and
75/// breaking arithmetic identity.
76pub enum HeapObject {
77    String(String),
78    Path(String),
79    List(Vec<NanBox>),
80    Attrs(BTreeMap<Symbol, NanBox>),
81    Closure(VMClosure),
82    Builtin(VMBuiltin),
83    Thunk(VMThunk),
84    HigherOrderBuiltin(HigherOrderBuiltin),
85    BigInt(i64),
86}
87
88impl NanBox {
89    // ── Constructors ──────────────────────────────────────────
90
91    /// Create a null value.
92    #[inline(always)]
93    #[must_use]
94    pub const fn null() -> Self {
95        Self(TAG_NULL)
96    }
97
98    /// Create a boolean value.
99    #[inline(always)]
100    #[must_use]
101    pub const fn bool(b: bool) -> Self {
102        if b {
103            Self(TAG_TRUE)
104        } else {
105            Self(TAG_FALSE)
106        }
107    }
108
109    /// Create an integer value preserving the full `i64` range.
110    ///
111    /// Values that fit in 48 bits (sign-extended) take the inline
112    /// fast path — one tagged NaN, no allocation.  Values outside
113    /// that range are boxed as [`HeapObject::BigInt`] so the full
114    /// `i64` round-trips without precision loss.
115    ///
116    /// This is the load-bearing contract for CppNix compatibility:
117    /// `i64::MAX - 1` must round-trip exactly, otherwise any flake
118    /// containing large integer literals (timestamps, byte counts,
119    /// builtins.bitAnd over wide masks) would diverge.
120    #[inline(always)]
121    #[must_use]
122    pub fn int(n: i64) -> Self {
123        let fits = (n << 16) >> 16 == n;
124        if fits {
125            let payload = (n as u64) & PAYLOAD_MASK;
126            Self(TAG_INT | payload)
127        } else {
128            Self::heap(HeapObject::BigInt(n))
129        }
130    }
131
132    /// Create a float value.
133    #[inline(always)]
134    #[must_use]
135    pub fn float(f: f64) -> Self {
136        Self(f.to_bits())
137    }
138
139    /// Create a pointer to a heap-allocated object.
140    #[must_use]
141    pub fn heap(obj: HeapObject) -> Self {
142        let boxed = Rc::new(obj);
143        let ptr = Rc::into_raw(boxed) as u64;
144        debug_assert!(
145            ptr & !PAYLOAD_MASK == 0,
146            "pointer exceeds 48 bits"
147        );
148        Self(TAG_PTR | (ptr & PAYLOAD_MASK))
149    }
150
151    /// Create a string value (heap-allocated).
152    #[must_use]
153    pub fn string(s: String) -> Self {
154        Self::heap(HeapObject::String(s))
155    }
156
157    /// Create a path value (heap-allocated).
158    #[must_use]
159    pub fn path(s: String) -> Self {
160        Self::heap(HeapObject::Path(s))
161    }
162
163    /// Create a list value (heap-allocated).
164    #[must_use]
165    pub fn list(items: Vec<NanBox>) -> Self {
166        Self::heap(HeapObject::List(items))
167    }
168
169    /// Create an attrs value (heap-allocated).
170    #[must_use]
171    pub fn attrs(map: BTreeMap<Symbol, NanBox>) -> Self {
172        Self::heap(HeapObject::Attrs(map))
173    }
174
175    /// Create a closure value (heap-allocated).
176    #[must_use]
177    pub fn closure(c: VMClosure) -> Self {
178        Self::heap(HeapObject::Closure(c))
179    }
180
181    /// Create a builtin function value (heap-allocated).
182    #[must_use]
183    pub fn builtin(b: VMBuiltin) -> Self {
184        Self::heap(HeapObject::Builtin(b))
185    }
186
187    /// Create a thunk value (heap-allocated).
188    #[must_use]
189    pub fn thunk(t: VMThunk) -> Self {
190        Self::heap(HeapObject::Thunk(t))
191    }
192
193    /// Create a higher-order builtin value (heap-allocated).
194    #[must_use]
195    pub fn higher_order_builtin(h: HigherOrderBuiltin) -> Self {
196        Self::heap(HeapObject::HigherOrderBuiltin(h))
197    }
198
199    // ── Type checks ───────────────────────────────────────────
200
201    /// Check if this value is a float (not a tagged NaN).
202    #[inline(always)]
203    #[must_use]
204    pub fn is_float(&self) -> bool {
205        // A value is a float if it's not in our tagged NaN space.
206        // Our tags all have the QNAN pattern. Regular floats don't
207        // (unless they happen to be NaN, which we treat as float).
208        (self.0 & TAG_MASK) != TAG_INT
209            && (self.0 & TAG_MASK) != TAG_NULL
210            && (self.0 & TAG_MASK) != TAG_FALSE
211            && (self.0 & TAG_MASK) != TAG_TRUE
212            && (self.0 & TAG_MASK) != TAG_PTR
213    }
214
215    #[inline(always)]
216    #[must_use]
217    pub fn is_null(&self) -> bool {
218        self.0 == TAG_NULL
219    }
220
221    #[inline(always)]
222    #[must_use]
223    pub fn is_bool(&self) -> bool {
224        self.0 == TAG_TRUE || self.0 == TAG_FALSE
225    }
226
227    #[inline(always)]
228    #[must_use]
229    pub fn is_int(&self) -> bool {
230        if (self.0 & TAG_MASK) == TAG_INT {
231            return true;
232        }
233        matches!(self.as_heap(), Some(HeapObject::BigInt(_)))
234    }
235
236    #[inline(always)]
237    #[must_use]
238    pub fn is_ptr(&self) -> bool {
239        (self.0 & TAG_MASK) == TAG_PTR
240    }
241
242    // ── Extractors ────────────────────────────────────────────
243
244    /// Extract a boolean. Returns `None` if not a bool.
245    #[inline(always)]
246    #[must_use]
247    pub fn as_bool(&self) -> Option<bool> {
248        if self.0 == TAG_TRUE {
249            Some(true)
250        } else if self.0 == TAG_FALSE {
251            Some(false)
252        } else {
253            None
254        }
255    }
256
257    /// Extract an integer. Returns `None` if not an int.
258    ///
259    /// Handles both the inline 48-bit fast path AND the heap-boxed
260    /// `BigInt` fallback so the full `i64` range round-trips.
261    #[inline(always)]
262    #[must_use]
263    pub fn as_int(&self) -> Option<i64> {
264        if (self.0 & TAG_MASK) == TAG_INT {
265            // Sign-extend from 48 bits.
266            let raw = (self.0 & PAYLOAD_MASK) as i64;
267            let extended = (raw << 16) >> 16;
268            return Some(extended);
269        }
270        if let Some(HeapObject::BigInt(n)) = self.as_heap() {
271            return Some(*n);
272        }
273        None
274    }
275
276    /// Extract a float. Returns `None` if this is a tagged value.
277    #[inline(always)]
278    #[must_use]
279    pub fn as_float(&self) -> Option<f64> {
280        if self.is_float() {
281            Some(f64::from_bits(self.0))
282        } else {
283            None
284        }
285    }
286
287    /// Extract the heap object. Returns `None` if not a pointer.
288    #[must_use]
289    pub fn as_heap(&self) -> Option<&HeapObject> {
290        if (self.0 & TAG_MASK) == TAG_PTR {
291            let ptr = (self.0 & PAYLOAD_MASK) as *const HeapObject;
292            // SAFETY: the pointer was created from Rc::into_raw and is valid
293            // as long as at least one NanBox referencing it exists.
294            Some(unsafe { &*ptr })
295        } else {
296            None
297        }
298    }
299
300    // ── VM-facing helpers ──────────────────────────────────────
301
302    /// Return the Nix type name for this value (mirrors `VMValue::type_name`).
303    #[must_use]
304    pub fn type_name(&self) -> &'static str {
305        if self.is_null() {
306            "null"
307        } else if self.is_bool() {
308            "bool"
309        } else if self.is_int() {
310            "int"
311        } else if self.is_float() {
312            "float"
313        } else if let Some(obj) = self.as_heap() {
314            match obj {
315                HeapObject::String(_) => "string",
316                HeapObject::Path(_) => "path",
317                HeapObject::List(_) => "list",
318                HeapObject::Attrs(_) => "set",
319                HeapObject::Closure(_) | HeapObject::Builtin(_) | HeapObject::HigherOrderBuiltin(_) => "lambda",
320                HeapObject::Thunk(_) => "thunk",
321                HeapObject::BigInt(_) => "int",
322            }
323        } else {
324            "unknown"
325        }
326    }
327
328    /// Check if this value is truthy (for conditionals).
329    /// Only booleans are valid; everything else is a type error.
330    pub fn is_truthy(&self) -> Result<bool, VMError> {
331        if self.0 == TAG_TRUE {
332            Ok(true)
333        } else if self.0 == TAG_FALSE {
334            Ok(false)
335        } else if self.is_null() {
336            Ok(false)
337        } else {
338            // Non-bool values — type error. Thunks should be forced
339            // before calling is_truthy.
340            Err(VMError::TypeError {
341                expected: "bool",
342                got: self.type_name(),
343                context: "condition".to_string(),
344            })
345        }
346    }
347
348    /// Check if this is a string.
349    #[inline(always)]
350    #[must_use]
351    pub fn is_string(&self) -> bool {
352        if let Some(HeapObject::String(_)) = self.as_heap() { true } else { false }
353    }
354
355    /// Check if this is a path.
356    #[inline(always)]
357    #[must_use]
358    pub fn is_path(&self) -> bool {
359        if let Some(HeapObject::Path(_)) = self.as_heap() { true } else { false }
360    }
361
362    /// Check if this is a list.
363    #[inline(always)]
364    #[must_use]
365    pub fn is_list(&self) -> bool {
366        if let Some(HeapObject::List(_)) = self.as_heap() { true } else { false }
367    }
368
369    /// Check if this is an attrset.
370    #[inline(always)]
371    #[must_use]
372    pub fn is_attrs(&self) -> bool {
373        if let Some(HeapObject::Attrs(_)) = self.as_heap() { true } else { false }
374    }
375
376    /// Check if this is a closure.
377    #[inline(always)]
378    #[must_use]
379    pub fn is_closure(&self) -> bool {
380        if let Some(HeapObject::Closure(_)) = self.as_heap() { true } else { false }
381    }
382
383    /// Check if this is a builtin.
384    #[inline(always)]
385    #[must_use]
386    pub fn is_builtin(&self) -> bool {
387        if let Some(HeapObject::Builtin(_)) = self.as_heap() { true } else { false }
388    }
389
390    /// Check if this is a thunk.
391    #[inline(always)]
392    #[must_use]
393    pub fn is_thunk(&self) -> bool {
394        if let Some(HeapObject::Thunk(_)) = self.as_heap() { true } else { false }
395    }
396
397    /// Check if this is a higher-order builtin.
398    #[inline(always)]
399    #[must_use]
400    pub fn is_higher_order_builtin(&self) -> bool {
401        matches!(self.as_heap(), Some(HeapObject::HigherOrderBuiltin(_)))
402    }
403
404    /// Extract a string reference. Returns `None` if not a string.
405    #[must_use]
406    pub fn as_string(&self) -> Option<&str> {
407        if let Some(HeapObject::String(s)) = self.as_heap() {
408            Some(s.as_str())
409        } else {
410            None
411        }
412    }
413
414    /// Extract a path reference. Returns `None` if not a path.
415    #[must_use]
416    pub fn as_path(&self) -> Option<&str> {
417        if let Some(HeapObject::Path(p)) = self.as_heap() {
418            Some(p.as_str())
419        } else {
420            None
421        }
422    }
423
424    /// Extract a list reference. Returns `None` if not a list.
425    #[must_use]
426    pub fn as_list(&self) -> Option<&[NanBox]> {
427        if let Some(HeapObject::List(items)) = self.as_heap() {
428            Some(items.as_slice())
429        } else {
430            None
431        }
432    }
433
434    /// Extract an attrs reference. Returns `None` if not an attrset.
435    #[must_use]
436    pub fn as_attrs(&self) -> Option<&BTreeMap<Symbol, NanBox>> {
437        if let Some(HeapObject::Attrs(map)) = self.as_heap() {
438            Some(map)
439        } else {
440            None
441        }
442    }
443
444    /// Extract a closure reference. Returns `None` if not a closure.
445    #[must_use]
446    pub fn as_closure(&self) -> Option<&VMClosure> {
447        if let Some(HeapObject::Closure(c)) = self.as_heap() {
448            Some(c)
449        } else {
450            None
451        }
452    }
453
454    /// Extract a builtin reference. Returns `None` if not a builtin.
455    #[must_use]
456    pub fn as_builtin(&self) -> Option<&VMBuiltin> {
457        if let Some(HeapObject::Builtin(b)) = self.as_heap() {
458            Some(b)
459        } else {
460            None
461        }
462    }
463
464    /// Extract a thunk reference. Returns `None` if not a thunk.
465    #[must_use]
466    pub fn as_thunk(&self) -> Option<&VMThunk> {
467        if let Some(HeapObject::Thunk(t)) = self.as_heap() {
468            Some(t)
469        } else {
470            None
471        }
472    }
473
474    /// Extract a higher-order builtin reference.
475    #[must_use]
476    pub fn as_higher_order_builtin(&self) -> Option<&HigherOrderBuiltin> {
477        if let Some(HeapObject::HigherOrderBuiltin(h)) = self.as_heap() {
478            Some(h)
479        } else {
480            None
481        }
482    }
483
484    // ── Conversion to/from VMValue ────────────────────────────
485
486    /// Convert a `VMValue` to a `NanBox`.
487    pub fn from_vmvalue(val: &VMValue) -> Self {
488        match val {
489            VMValue::Null => Self::null(),
490            VMValue::Bool(b) => Self::bool(*b),
491            VMValue::Int(n) => Self::int(*n),
492            VMValue::Float(f) => Self::float(*f),
493            VMValue::String(s) => Self::string(s.clone()),
494            VMValue::Path(p) => Self::path(p.clone()),
495            VMValue::List(items) => {
496                let boxed: Vec<NanBox> = items.iter().map(|v| Self::from_vmvalue(v)).collect();
497                Self::list(boxed)
498            }
499            VMValue::Attrs(attrs) => {
500                let boxed: BTreeMap<Symbol, NanBox> = attrs
501                    .iter()
502                    .map(|(k, v)| (*k, Self::from_vmvalue(v)))
503                    .collect();
504                Self::attrs(boxed)
505            }
506            VMValue::Closure(c) => Self::closure(c.clone()),
507            VMValue::Builtin(b) => Self::builtin(b.clone()),
508            VMValue::Thunk(t) => Self::thunk(t.clone()),
509            VMValue::HigherOrderBuiltin(h) => Self::higher_order_builtin(h.clone()),
510        }
511    }
512
513    /// Convert a `NanBox` back to a `VMValue`.
514    pub fn to_vmvalue(&self) -> VMValue {
515        if self.is_null() {
516            VMValue::Null
517        } else if let Some(b) = self.as_bool() {
518            VMValue::Bool(b)
519        } else if let Some(n) = self.as_int() {
520            VMValue::Int(n)
521        } else if let Some(f) = self.as_float() {
522            VMValue::Float(f)
523        } else if let Some(obj) = self.as_heap() {
524            match obj {
525                HeapObject::String(s) => VMValue::String(s.clone()),
526                HeapObject::Path(p) => VMValue::Path(p.clone()),
527                HeapObject::List(items) => {
528                    VMValue::List(items.iter().map(NanBox::to_vmvalue).collect())
529                }
530                HeapObject::Attrs(attrs) => {
531                    let map = attrs
532                        .iter()
533                        .map(|(k, v)| (*k, v.to_vmvalue()))
534                        .collect();
535                    VMValue::Attrs(map)
536                }
537                HeapObject::Closure(c) => VMValue::Closure(c.clone()),
538                HeapObject::Builtin(b) => VMValue::Builtin(b.clone()),
539                HeapObject::Thunk(t) => {
540                    // Unwrap Done thunks to avoid re-wrapping forced values.
541                    // This is critical: deep_force resolves thunks to NanBox
542                    // values, but the HeapObject::Thunk wrapper persists.
543                    // Without this unwrap, builtins see VMValue::Thunk instead
544                    // of the concrete value, causing type errors.
545                    let state = t.state.take();
546                    match state {
547                        Some(ThunkState::Done(boxed)) => {
548                            t.state.set(Some(ThunkState::Done(boxed.clone())));
549                            *boxed
550                        }
551                        other => {
552                            t.state.set(other);
553                            VMValue::Thunk(t.clone())
554                        }
555                    }
556                }
557                HeapObject::HigherOrderBuiltin(h) => VMValue::HigherOrderBuiltin(h.clone()),
558                HeapObject::BigInt(n) => VMValue::Int(*n),
559            }
560        } else {
561            // Should not happen.
562            VMValue::Null
563        }
564    }
565}
566
567impl Clone for HeapObject {
568    fn clone(&self) -> Self {
569        match self {
570            HeapObject::String(s) => HeapObject::String(s.clone()),
571            HeapObject::Path(p) => HeapObject::Path(p.clone()),
572            HeapObject::List(items) => HeapObject::List(items.clone()),
573            HeapObject::Attrs(attrs) => HeapObject::Attrs(attrs.clone()),
574            HeapObject::Closure(c) => HeapObject::Closure(c.clone()),
575            HeapObject::Builtin(b) => HeapObject::Builtin(b.clone()),
576            HeapObject::Thunk(t) => HeapObject::Thunk(t.clone()),
577            HeapObject::HigherOrderBuiltin(h) => HeapObject::HigherOrderBuiltin(h.clone()),
578            HeapObject::BigInt(n) => HeapObject::BigInt(*n),
579        }
580    }
581}
582
583impl PartialEq for NanBox {
584    fn eq(&self, other: &Self) -> bool {
585        // Fast path: same bits means same value (covers scalars and same heap ptrs).
586        if self.0 == other.0 {
587            return true;
588        }
589
590        // Float comparison (handle NaN != NaN).
591        if self.is_float() && other.is_float() {
592            return self.as_float() == other.as_float();
593        }
594
595        // Int/Float cross-type comparison (Nix coerces int to float).
596        if self.is_int() && other.is_float() {
597            if let (Some(i), Some(f)) = (self.as_int(), other.as_float()) {
598                return (i as f64) == f;
599            }
600        }
601        if self.is_float() && other.is_int() {
602            if let (Some(f), Some(i)) = (self.as_float(), other.as_int()) {
603                return f == (i as f64);
604            }
605        }
606
607        // Heap object deep comparison.
608        if self.is_ptr() && other.is_ptr() {
609            if let (Some(a), Some(b)) = (self.as_heap(), other.as_heap()) {
610                return heap_eq(a, b);
611            }
612        }
613
614        false
615    }
616}
617
618impl Eq for NanBox {}
619
620/// Deep equality comparison for heap objects.
621fn heap_eq(a: &HeapObject, b: &HeapObject) -> bool {
622    match (a, b) {
623        (HeapObject::String(a), HeapObject::String(b)) => a == b,
624        (HeapObject::Path(a), HeapObject::Path(b)) => a == b,
625        (HeapObject::List(a), HeapObject::List(b)) => a == b,
626        (HeapObject::Attrs(a), HeapObject::Attrs(b)) => a == b,
627        _ => false,
628    }
629}
630
631// Implement Drop for NanBox to properly handle Rc ref counting.
632impl Drop for NanBox {
633    fn drop(&mut self) {
634        if (self.0 & TAG_MASK) == TAG_PTR {
635            let ptr = (self.0 & PAYLOAD_MASK) as *const HeapObject;
636            // SAFETY: this pointer was created with Rc::into_raw.
637            // We reconstruct the Rc to decrement the reference count.
638            unsafe {
639                let _ = Rc::from_raw(ptr);
640            }
641        }
642    }
643}
644
645// Clone must increment the Rc.
646impl Clone for NanBox {
647    fn clone(&self) -> Self {
648        if (self.0 & TAG_MASK) == TAG_PTR {
649            let ptr = (self.0 & PAYLOAD_MASK) as *const HeapObject;
650            // SAFETY: reconstruct Rc, clone it (increment refcount), leak both.
651            unsafe {
652                let rc = Rc::from_raw(ptr);
653                let cloned = Rc::clone(&rc);
654                let _ = Rc::into_raw(rc); // don't drop the original
655                let new_ptr = Rc::into_raw(cloned);
656                Self(TAG_PTR | (new_ptr as u64 & PAYLOAD_MASK))
657            }
658        } else {
659            Self(self.0)
660        }
661    }
662}
663
664impl fmt::Debug for NanBox {
665    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
666        if self.is_null() {
667            write!(f, "NanBox(null)")
668        } else if let Some(b) = self.as_bool() {
669            write!(f, "NanBox({b})")
670        } else if let Some(n) = self.as_int() {
671            write!(f, "NanBox({n})")
672        } else if let Some(fl) = self.as_float() {
673            write!(f, "NanBox({fl})")
674        } else if let Some(obj) = self.as_heap() {
675            match obj {
676                HeapObject::String(s) => write!(f, "NanBox(\"{s}\")"),
677                HeapObject::Path(p) => write!(f, "NanBox(path:{p})"),
678                HeapObject::List(items) => write!(f, "NanBox(list[{}])", items.len()),
679                HeapObject::Attrs(map) => write!(f, "NanBox(attrs[{}])", map.len()),
680                HeapObject::Closure(c) => write!(f, "NanBox({c:?})"),
681                HeapObject::Builtin(b) => write!(f, "NanBox({b:?})"),
682                HeapObject::Thunk(_) => write!(f, "NanBox(<thunk>)"),
683                HeapObject::HigherOrderBuiltin(h) => write!(f, "NanBox({h:?})"),
684                HeapObject::BigInt(n) => write!(f, "NanBox(bigint:{n})"),
685            }
686        } else {
687            write!(f, "NanBox(0x{:016x})", self.0)
688        }
689    }
690}
691
692#[cfg(test)]
693mod tests {
694    use super::*;
695
696    #[test]
697    fn null_roundtrip() {
698        let v = NanBox::null();
699        assert!(v.is_null());
700        assert_eq!(v.to_vmvalue(), VMValue::Null);
701    }
702
703    #[test]
704    fn bool_roundtrip() {
705        let t = NanBox::bool(true);
706        let f = NanBox::bool(false);
707        assert_eq!(t.as_bool(), Some(true));
708        assert_eq!(f.as_bool(), Some(false));
709        assert_eq!(t.to_vmvalue(), VMValue::Bool(true));
710        assert_eq!(f.to_vmvalue(), VMValue::Bool(false));
711    }
712
713    #[test]
714    fn int_roundtrip() {
715        for n in [0i64, 1, -1, 42, -42, 1000000, -1000000, i32::MAX as i64, i32::MIN as i64] {
716            let v = NanBox::int(n);
717            assert!(v.is_int(), "should be int for {n}");
718            assert_eq!(v.as_int(), Some(n), "roundtrip failed for {n}");
719        }
720    }
721
722    #[test]
723    fn int_roundtrip_full_i64_range() {
724        // Boundary values across the inline ↔ heap split.
725        // 48-bit signed range is [-(2^47), 2^47 - 1] = [-140737488355328, 140737488355327].
726        let boundaries: &[i64] = &[
727            i64::MAX,
728            i64::MIN,
729            i64::MAX - 1,
730            i64::MIN + 1,
731            2_i64.pow(53),       // f64 mantissa boundary — would lose precision under old impl
732            -(2_i64.pow(53)),
733            2_i64.pow(47),       // just above inline range
734            -(2_i64.pow(47)) - 1,
735            2_i64.pow(47) - 1,   // inline boundary
736            -(2_i64.pow(47)),
737            9_223_372_036_854_775_806, // canonical operator-facing literal
738            -9_223_372_036_854_775_807,
739        ];
740        for &n in boundaries {
741            let v = NanBox::int(n);
742            assert!(v.is_int(), "is_int false for boundary {n}");
743            assert_eq!(v.as_int(), Some(n), "round-trip failed for boundary {n}");
744            assert_eq!(v.type_name(), "int", "type_name wrong for boundary {n}");
745            // VMValue round-trip preserves the value.
746            match v.to_vmvalue() {
747                VMValue::Int(m) => assert_eq!(m, n, "VMValue round-trip lost precision at {n}"),
748                other => panic!("VMValue round-trip produced {other:?} for {n}"),
749            }
750        }
751    }
752
753    #[test]
754    fn int_overflow_does_not_silently_demote_to_float() {
755        // The bug this guards: previously NanBox::int(9223372036854775806)
756        // produced an f64 (lossy), then `9223372036854775806 - 1` returned
757        // 9223372036854775808.0 — wrong by 1 AND wrong type.
758        let big = 9_223_372_036_854_775_806_i64;
759        let v = NanBox::int(big);
760        assert!(!v.is_float(), "BigInt must not be classified as float");
761        assert_eq!(v.as_int(), Some(big));
762        assert_eq!(v.as_float(), None, "BigInt must not pose as float");
763    }
764
765    #[test]
766    fn float_roundtrip() {
767        for f in [0.0f64, 1.0, -1.0, 3.14, f64::INFINITY, f64::NEG_INFINITY] {
768            let v = NanBox::float(f);
769            assert!(v.is_float(), "should be float for {f}");
770            assert_eq!(v.as_float(), Some(f), "roundtrip failed for {f}");
771        }
772    }
773
774    #[test]
775    fn string_roundtrip() {
776        let v = NanBox::string("hello".to_string());
777        assert!(v.is_ptr());
778        match v.to_vmvalue() {
779            VMValue::String(s) => assert_eq!(s, "hello"),
780            other => panic!("expected String, got {other:?}"),
781        }
782    }
783
784    #[test]
785    fn clone_heap_value() {
786        let v1 = NanBox::string("test".to_string());
787        let v2 = v1.clone();
788        match v2.to_vmvalue() {
789            VMValue::String(s) => assert_eq!(s, "test"),
790            other => panic!("expected String, got {other:?}"),
791        }
792        // Both should be valid after clone.
793        match v1.to_vmvalue() {
794            VMValue::String(s) => assert_eq!(s, "test"),
795            other => panic!("expected String, got {other:?}"),
796        }
797    }
798
799    #[test]
800    fn vmvalue_roundtrip_scalars() {
801        let cases = [
802            VMValue::Null,
803            VMValue::Bool(true),
804            VMValue::Bool(false),
805            VMValue::Int(42),
806            VMValue::Int(-1),
807            VMValue::Float(3.14),
808        ];
809        for val in &cases {
810            let boxed = NanBox::from_vmvalue(val);
811            let back = boxed.to_vmvalue();
812            assert_eq!(*val, back, "roundtrip failed for {val:?}");
813        }
814    }
815
816    #[test]
817    fn vmvalue_roundtrip_string() {
818        let val = VMValue::String("hello world".to_string());
819        let boxed = NanBox::from_vmvalue(&val);
820        let back = boxed.to_vmvalue();
821        assert_eq!(val, back);
822    }
823
824    #[test]
825    fn vmvalue_roundtrip_list() {
826        let val = VMValue::List(vec![VMValue::Int(1), VMValue::Int(2), VMValue::Int(3)]);
827        let boxed = NanBox::from_vmvalue(&val);
828        let back = boxed.to_vmvalue();
829        assert_eq!(val, back);
830    }
831
832    #[test]
833    fn vmvalue_roundtrip_builtin() {
834        use crate::value::VMBuiltin;
835        use std::rc::Rc;
836        let b = VMBuiltin {
837            name: "test",
838            func: Rc::new(|_| Ok(VMValue::Null)),
839            arity: 1,
840        };
841        let val = VMValue::Builtin(b);
842        let boxed = NanBox::from_vmvalue(&val);
843        assert!(boxed.is_builtin());
844        match boxed.to_vmvalue() {
845            VMValue::Builtin(b) => assert_eq!(b.name, "test"),
846            other => panic!("expected Builtin, got {other:?}"),
847        }
848    }
849
850    #[test]
851    fn vmvalue_roundtrip_thunk() {
852        use crate::chunk::Chunk;
853        use std::rc::Rc;
854        let thunk = VMThunk::new(Rc::new(Chunk::new()), Vec::new());
855        let val = VMValue::Thunk(thunk);
856        let boxed = NanBox::from_vmvalue(&val);
857        assert!(boxed.is_thunk());
858        match boxed.to_vmvalue() {
859            VMValue::Thunk(_) => {} // ok
860            other => panic!("expected Thunk, got {other:?}"),
861        }
862    }
863
864    #[test]
865    fn type_name_all_types() {
866        assert_eq!(NanBox::null().type_name(), "null");
867        assert_eq!(NanBox::bool(true).type_name(), "bool");
868        assert_eq!(NanBox::int(42).type_name(), "int");
869        assert_eq!(NanBox::float(3.14).type_name(), "float");
870        assert_eq!(NanBox::string("hi".to_string()).type_name(), "string");
871        assert_eq!(NanBox::path("/tmp".to_string()).type_name(), "path");
872        assert_eq!(NanBox::list(vec![]).type_name(), "list");
873        assert_eq!(NanBox::attrs(BTreeMap::new()).type_name(), "set");
874    }
875
876    #[test]
877    fn nanbox_equality() {
878        assert_eq!(NanBox::null(), NanBox::null());
879        assert_eq!(NanBox::bool(true), NanBox::bool(true));
880        assert_ne!(NanBox::bool(true), NanBox::bool(false));
881        assert_eq!(NanBox::int(42), NanBox::int(42));
882        assert_ne!(NanBox::int(1), NanBox::int(2));
883        assert_eq!(NanBox::float(3.14), NanBox::float(3.14));
884        assert_eq!(NanBox::string("a".to_string()), NanBox::string("a".to_string()));
885        assert_ne!(NanBox::string("a".to_string()), NanBox::string("b".to_string()));
886    }
887
888    #[test]
889    fn nanbox_int_float_coercion() {
890        assert_eq!(NanBox::int(1), NanBox::float(1.0));
891        assert_eq!(NanBox::float(1.0), NanBox::int(1));
892        assert_ne!(NanBox::int(1), NanBox::float(1.5));
893    }
894
895    #[test]
896    fn is_truthy_bool() {
897        assert!(NanBox::bool(true).is_truthy().unwrap());
898        assert!(!NanBox::bool(false).is_truthy().unwrap());
899    }
900
901    #[test]
902    fn is_truthy_non_bool_errors() {
903        // Integers are still type errors in conditions.
904        assert!(NanBox::int(1).is_truthy().is_err());
905        // Null is permissively treated as false (VM workaround).
906        assert_eq!(NanBox::null().is_truthy().unwrap(), false);
907    }
908
909    #[test]
910    fn size_is_8_bytes() {
911        assert_eq!(std::mem::size_of::<NanBox>(), 8);
912    }
913}