Skip to main content

value_lang/
value.rs

1//! The NaN-boxed [`Value`] and its [`Unpacked`] view.
2//!
3//! This module holds the whole runtime representation. [`Value`] is the compact,
4//! eight-byte, `Copy` handle a bytecode interpreter passes around by value;
5//! [`Unpacked`] is the tagged-union view you match on when you need to branch on
6//! the kind. The two are duals: `Value::from(unpacked)` and `value.unpack()` round
7//! trip losslessly.
8
9use core::fmt;
10
11use intern_lang::Symbol;
12
13/// A runtime value, packed into a single 64-bit word by NaN-boxing.
14///
15/// A dynamic interpreter spends most of its time moving values between the stack,
16/// locals, and the operand of an instruction. Representing each one as a Rust
17/// `enum` costs sixteen bytes (a tag word plus the widest payload) and a branch on
18/// every copy. NaN-boxing folds the kind *and* the payload into the bit pattern of
19/// one `f64`-sized word, so a `Value` is `Copy`, eight bytes wide, and needs no
20/// discriminant alongside it.
21///
22/// The trick is that IEEE-754 leaves a large block of bit patterns unused: every
23/// quiet-NaN encoding names the same abstract "not a number". A `Value` stores a
24/// real [`f64`] as itself, and hides every other kind — [`nil`](Value::nil),
25/// booleans, 32-bit integers, and interned [`Symbol`]s — inside quiet-NaN payloads
26/// that no genuine float ever produces. Reading a value back is a mask and a
27/// compare; see [`unpack`](Value::unpack).
28///
29/// The encoding is entirely safe: it is built from [`f64::to_bits`] and
30/// [`f64::from_bits`] and integer arithmetic, with no pointers and no `unsafe`.
31/// Because it never boxes a pointer, it carries no heap data — strings and other
32/// interned identities travel as [`Symbol`] handles, resolved elsewhere against the
33/// interner that issued them.
34///
35/// # Equality
36///
37/// Equality follows IEEE-754 for floats and identity for everything else: two
38/// [`Float`](Unpacked::Float) values compare with `f64` semantics, so `NaN != NaN`
39/// and `0.0 == -0.0`; all other kinds (including a `Float` against a non-`Float`)
40/// compare by bit pattern. Because `NaN != NaN`, `Value` deliberately does **not**
41/// implement [`Eq`] or [`Hash`]; if you need a hashable key, branch on
42/// [`unpack`](Value::unpack) and hash the parts, or use [`bits`](Value::bits) when
43/// you have separately ensured no float is `NaN`.
44///
45/// # Examples
46///
47/// ```
48/// use value_lang::{Unpacked, Value};
49///
50/// let answer = Value::int(42);
51/// let ratio = Value::float(0.375);
52///
53/// assert!(answer.is_int());
54/// assert_eq!(answer.as_int(), Some(42));
55/// assert_eq!(ratio.as_float(), Some(0.375));
56///
57/// // Branch on the kind with `unpack`.
58/// match answer.unpack() {
59///     Unpacked::Int(n) => assert_eq!(n, 42),
60///     _ => unreachable!(),
61/// }
62///
63/// // A `Value` is eight bytes and `Copy`.
64/// assert_eq!(core::mem::size_of::<Value>(), 8);
65/// ```
66#[derive(Clone, Copy)]
67pub struct Value(u64);
68
69// --- NaN-box layout -------------------------------------------------------
70//
71// A 64-bit IEEE-754 double is `sign(1) | exponent(11) | mantissa(52)`. A value is
72// "boxed" (not a real float) when the exponent is all ones and the top two mantissa
73// bits are set — the `QNAN` pattern below. No finite double or infinity matches it,
74// and every genuine NaN is folded onto `CANON_NAN` on the way in, so the boxed space
75// is ours alone.
76//
77// Within a boxed word: bits 62..=50 are the fixed `QNAN` header, a 3-bit tag lives at
78// bits 34..=32, and a 32-bit payload occupies the low word. Booleans and nil need no
79// payload; `Int` stores an `i32` bit-for-bit; `Sym` stores a `Symbol`'s `NonZeroU32`.
80
81/// Quiet-NaN header: exponent all ones plus the two top mantissa bits.
82const QNAN: u64 = 0x7ffc_0000_0000_0000;
83
84/// Canonical float `NaN`. Every incoming `NaN` is folded onto this pattern, which
85/// sits *outside* the [`QNAN`] boxed space so it reads back as a float.
86const CANON_NAN: u64 = 0x7ff8_0000_0000_0000;
87
88/// Bit offset of the 3-bit kind tag.
89const TAG_SHIFT: u32 = 32;
90/// Mask selecting the tag once shifted down.
91const TAG_MASK: u64 = 0x7;
92
93const TAG_NIL: u64 = 1;
94const TAG_FALSE: u64 = 2;
95const TAG_TRUE: u64 = 3;
96const TAG_INT: u64 = 4;
97const TAG_SYM: u64 = 5;
98
99/// Builds the header for a boxed word carrying `tag`.
100const fn boxed(tag: u64) -> u64 {
101    QNAN | (tag << TAG_SHIFT)
102}
103
104const NIL_BITS: u64 = boxed(TAG_NIL);
105const FALSE_BITS: u64 = boxed(TAG_FALSE);
106const TRUE_BITS: u64 = boxed(TAG_TRUE);
107
108impl Value {
109    /// The unit value, `nil` — the absence of any other value.
110    ///
111    /// This is what an interpreter yields for an expression with no result, an
112    /// uninitialised local, or a missing map entry. It is also [`Value::default`].
113    ///
114    /// # Examples
115    ///
116    /// ```
117    /// use value_lang::Value;
118    ///
119    /// let v = Value::nil();
120    /// assert!(v.is_nil());
121    /// assert_eq!(v, Value::default());
122    /// ```
123    #[inline]
124    #[must_use]
125    pub const fn nil() -> Self {
126        Self(NIL_BITS)
127    }
128
129    /// A boolean value.
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// use value_lang::Value;
135    ///
136    /// assert_eq!(Value::bool(true).as_bool(), Some(true));
137    /// assert_eq!(Value::bool(false).as_bool(), Some(false));
138    /// ```
139    #[inline]
140    #[must_use]
141    pub const fn bool(b: bool) -> Self {
142        Self(if b { TRUE_BITS } else { FALSE_BITS })
143    }
144
145    /// A 32-bit signed integer.
146    ///
147    /// Integers are stored as an `i32` because that is what fits losslessly beside
148    /// the tag in a NaN-box payload; use [`float`](Value::float) when you need the
149    /// full magnitude and precision range of a double.
150    ///
151    /// # Examples
152    ///
153    /// ```
154    /// use value_lang::Value;
155    ///
156    /// assert_eq!(Value::int(-7).as_int(), Some(-7));
157    /// assert_eq!(Value::int(i32::MAX).as_int(), Some(i32::MAX));
158    /// ```
159    #[inline]
160    #[must_use]
161    pub const fn int(n: i32) -> Self {
162        // `n as u32` reinterprets the two's-complement bit pattern; `as_int`
163        // reverses it. The high 32 bits stay zero, so only the tag names the kind.
164        Self(boxed(TAG_INT) | (n as u32 as u64))
165    }
166
167    /// A floating-point value.
168    ///
169    /// Any finite double, both infinities, and `NaN` are accepted. Every `NaN` is
170    /// stored as one canonical bit pattern, so a round trip through `Value`
171    /// normalises `NaN` payloads (the value is still `NaN`, and still compares
172    /// unequal to itself).
173    ///
174    /// # Examples
175    ///
176    /// ```
177    /// use value_lang::Value;
178    ///
179    /// assert_eq!(Value::float(2.5).as_float(), Some(2.5));
180    /// assert_eq!(Value::float(f64::INFINITY).as_float(), Some(f64::INFINITY));
181    /// assert!(Value::float(f64::NAN).as_float().unwrap().is_nan());
182    /// ```
183    #[inline]
184    #[must_use]
185    pub fn float(f: f64) -> Self {
186        // Fold every NaN onto one pattern that lies outside the boxed space, so no
187        // computed NaN can ever be mistaken for a boxed kind.
188        if f.is_nan() {
189            Self(CANON_NAN)
190        } else {
191            Self(f.to_bits())
192        }
193    }
194
195    /// An interned [`Symbol`] — a compact handle for a string or identifier.
196    ///
197    /// The symbol's 32-bit id is packed directly into the value. It is only
198    /// meaningful with the interner that issued it; `Value` stores the handle, not
199    /// the bytes.
200    ///
201    /// # Examples
202    ///
203    /// ```
204    /// use intern_lang::Interner;
205    /// use value_lang::Value;
206    ///
207    /// let mut interner = Interner::new();
208    /// let name = interner.intern("total");
209    ///
210    /// let v = Value::sym(name);
211    /// assert_eq!(v.as_sym(), Some(name));
212    /// assert_eq!(interner.resolve(v.as_sym().unwrap()), Some("total"));
213    /// ```
214    #[inline]
215    #[must_use]
216    pub fn sym(s: Symbol) -> Self {
217        Self(boxed(TAG_SYM) | (s.as_u32() as u64))
218    }
219
220    /// Returns `true` when this value is [`nil`](Value::nil).
221    #[inline]
222    #[must_use]
223    pub fn is_nil(self) -> bool {
224        self.0 == NIL_BITS
225    }
226
227    /// Returns `true` when this value is a boolean.
228    #[inline]
229    #[must_use]
230    pub fn is_bool(self) -> bool {
231        self.0 == TRUE_BITS || self.0 == FALSE_BITS
232    }
233
234    /// Returns `true` when this value is a 32-bit integer.
235    #[inline]
236    #[must_use]
237    pub fn is_int(self) -> bool {
238        self.is_boxed() && self.tag() == TAG_INT
239    }
240
241    /// Returns `true` when this value is a float.
242    ///
243    /// Every value that is not a boxed kind is a float, including the infinities
244    /// and `NaN`.
245    #[inline]
246    #[must_use]
247    pub fn is_float(self) -> bool {
248        !self.is_boxed()
249    }
250
251    /// Returns `true` when this value is an interned [`Symbol`].
252    #[inline]
253    #[must_use]
254    pub fn is_sym(self) -> bool {
255        self.is_boxed() && self.tag() == TAG_SYM
256    }
257
258    /// Returns the boolean, or `None` if this value is not a boolean.
259    #[inline]
260    #[must_use]
261    pub fn as_bool(self) -> Option<bool> {
262        match self.0 {
263            TRUE_BITS => Some(true),
264            FALSE_BITS => Some(false),
265            _ => None,
266        }
267    }
268
269    /// Returns the integer, or `None` if this value is not an integer.
270    #[inline]
271    #[must_use]
272    pub fn as_int(self) -> Option<i32> {
273        if self.is_int() {
274            Some(self.0 as u32 as i32)
275        } else {
276            None
277        }
278    }
279
280    /// Returns the float, or `None` if this value is not a float.
281    ///
282    /// This does **not** convert an [`int`](Value::int) to a float; it returns
283    /// `None` for every non-float kind. Convert explicitly if you want coercion.
284    #[inline]
285    #[must_use]
286    pub fn as_float(self) -> Option<f64> {
287        if self.is_float() {
288            Some(f64::from_bits(self.0))
289        } else {
290            None
291        }
292    }
293
294    /// Returns the interned [`Symbol`], or `None` if this value is not a symbol.
295    #[inline]
296    #[must_use]
297    pub fn as_sym(self) -> Option<Symbol> {
298        if self.is_sym() {
299            Symbol::from_u32(self.0 as u32)
300        } else {
301            None
302        }
303    }
304
305    /// Returns the raw 64-bit NaN-box encoding.
306    ///
307    /// This is the exact bit pattern the value occupies. It is stable for a given
308    /// value and useful for building a custom hash or a compact serialization, but
309    /// note that two `NaN` floats share one canonical pattern and `0.0`/`-0.0` do
310    /// not — so raw bits are identity, not numeric equality.
311    #[inline]
312    #[must_use]
313    pub const fn bits(self) -> u64 {
314        self.0
315    }
316
317    /// Expands this value into its [`Unpacked`] tagged-union form for matching.
318    ///
319    /// This is the reverse of [`Value::from`]`(unpacked)`. Use it when you need to
320    /// branch on every kind at once rather than test one kind with an `as_*`
321    /// accessor.
322    ///
323    /// # Examples
324    ///
325    /// ```
326    /// use value_lang::{Unpacked, Value};
327    ///
328    /// fn describe(v: Value) -> &'static str {
329    ///     match v.unpack() {
330    ///         Unpacked::Nil => "nil",
331    ///         Unpacked::Bool(_) => "bool",
332    ///         Unpacked::Int(_) => "int",
333    ///         Unpacked::Float(_) => "float",
334    ///         Unpacked::Sym(_) => "sym",
335    ///     }
336    /// }
337    ///
338    /// assert_eq!(describe(Value::int(1)), "int");
339    /// assert_eq!(describe(Value::nil()), "nil");
340    /// ```
341    #[inline]
342    #[must_use]
343    pub fn unpack(self) -> Unpacked {
344        if self.is_float() {
345            return Unpacked::Float(f64::from_bits(self.0));
346        }
347        // Boxed: decode against the fixed patterns and the tag. The chain is total
348        // because construction only ever produces these five tags.
349        if self.0 == NIL_BITS {
350            Unpacked::Nil
351        } else if self.0 == FALSE_BITS {
352            Unpacked::Bool(false)
353        } else if self.0 == TRUE_BITS {
354            Unpacked::Bool(true)
355        } else if self.tag() == TAG_INT {
356            Unpacked::Int(self.0 as u32 as i32)
357        } else {
358            // The only remaining tag is `Sym`. `from_u32` cannot fail here — a
359            // symbol id is `NonZeroU32` — but if a hand-forged bit pattern ever
360            // carried a zero payload we degrade to `nil` rather than panic.
361            match Symbol::from_u32(self.0 as u32) {
362                Some(s) => Unpacked::Sym(s),
363                None => Unpacked::Nil,
364            }
365        }
366    }
367
368    /// Whether the word encodes a boxed (non-float) kind.
369    #[inline]
370    const fn is_boxed(self) -> bool {
371        (self.0 & QNAN) == QNAN
372    }
373
374    /// The 3-bit kind tag. Only meaningful when [`is_boxed`](Value::is_boxed).
375    #[inline]
376    const fn tag(self) -> u64 {
377        (self.0 >> TAG_SHIFT) & TAG_MASK
378    }
379}
380
381impl Default for Value {
382    /// The default value is [`nil`](Value::nil).
383    #[inline]
384    fn default() -> Self {
385        Self::nil()
386    }
387}
388
389impl PartialEq for Value {
390    /// See the [type-level note on equality](Value#equality): floats compare with
391    /// `f64` semantics, everything else by bit pattern.
392    #[inline]
393    fn eq(&self, other: &Self) -> bool {
394        if self.is_float() && other.is_float() {
395            f64::from_bits(self.0) == f64::from_bits(other.0)
396        } else {
397            self.0 == other.0
398        }
399    }
400}
401
402impl fmt::Debug for Value {
403    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
404        f.debug_tuple("Value").field(&self.unpack()).finish()
405    }
406}
407
408impl From<bool> for Value {
409    #[inline]
410    fn from(b: bool) -> Self {
411        Self::bool(b)
412    }
413}
414
415impl From<i32> for Value {
416    #[inline]
417    fn from(n: i32) -> Self {
418        Self::int(n)
419    }
420}
421
422impl From<f64> for Value {
423    #[inline]
424    fn from(f: f64) -> Self {
425        Self::float(f)
426    }
427}
428
429impl From<Symbol> for Value {
430    #[inline]
431    fn from(s: Symbol) -> Self {
432        Self::sym(s)
433    }
434}
435
436impl From<Unpacked> for Value {
437    #[inline]
438    fn from(u: Unpacked) -> Self {
439        match u {
440            Unpacked::Nil => Self::nil(),
441            Unpacked::Bool(b) => Self::bool(b),
442            Unpacked::Int(n) => Self::int(n),
443            Unpacked::Float(f) => Self::float(f),
444            Unpacked::Sym(s) => Self::sym(s),
445        }
446    }
447}
448
449/// The tagged-union view of a [`Value`], for exhaustive matching.
450///
451/// A [`Value`] hides its kind inside a bit pattern; `Unpacked` names it. Obtain one
452/// with [`Value::unpack`], and convert back with [`Value::from`]. This is the type
453/// to `match` on when an interpreter dispatches on the kind of an operand.
454///
455/// # Examples
456///
457/// ```
458/// use value_lang::{Unpacked, Value};
459///
460/// let v = Value::float(1.5);
461/// assert_eq!(v.unpack(), Unpacked::Float(1.5));
462/// assert_eq!(Value::from(Unpacked::Int(3)), Value::int(3));
463/// ```
464#[derive(Clone, Copy, Debug, PartialEq)]
465pub enum Unpacked {
466    /// The unit value. See [`Value::nil`].
467    Nil,
468    /// A boolean. See [`Value::bool`].
469    Bool(bool),
470    /// A 32-bit signed integer. See [`Value::int`].
471    Int(i32),
472    /// A double-precision float. See [`Value::float`].
473    Float(f64),
474    /// An interned symbol handle. See [`Value::sym`].
475    Sym(Symbol),
476}
477
478#[cfg(test)]
479mod tests {
480    #![allow(clippy::unwrap_used)]
481
482    use super::*;
483
484    #[test]
485    fn test_value_size_is_one_word() {
486        assert_eq!(core::mem::size_of::<Value>(), 8);
487        assert_eq!(core::mem::align_of::<Value>(), 8);
488    }
489
490    #[test]
491    fn test_nil_roundtrips_and_is_default() {
492        let v = Value::nil();
493        assert!(v.is_nil());
494        assert_eq!(v.unpack(), Unpacked::Nil);
495        assert_eq!(v, Value::default());
496        assert!(!v.is_bool());
497        assert!(!v.is_int());
498        assert!(!v.is_float());
499        assert!(!v.is_sym());
500    }
501
502    #[test]
503    fn test_bool_roundtrips_both_values() {
504        for b in [true, false] {
505            let v = Value::bool(b);
506            assert!(v.is_bool());
507            assert_eq!(v.as_bool(), Some(b));
508            assert_eq!(v.unpack(), Unpacked::Bool(b));
509        }
510        assert_ne!(Value::bool(true), Value::bool(false));
511    }
512
513    #[test]
514    fn test_int_roundtrips_including_extremes() {
515        for n in [0, 1, -1, i32::MIN, i32::MAX, 123_456, -987_654] {
516            let v = Value::int(n);
517            assert!(v.is_int());
518            assert_eq!(v.as_int(), Some(n));
519            assert_eq!(v.unpack(), Unpacked::Int(n));
520        }
521    }
522
523    #[test]
524    fn test_float_roundtrips_including_infinities() {
525        for f in [
526            0.0,
527            -0.0,
528            1.5,
529            -2.5,
530            f64::MIN,
531            f64::MAX,
532            f64::INFINITY,
533            f64::NEG_INFINITY,
534        ] {
535            let v = Value::float(f);
536            assert!(v.is_float());
537            assert_eq!(v.as_float(), Some(f));
538        }
539    }
540
541    #[test]
542    fn test_float_nan_is_canonical_and_unequal_to_itself() {
543        let v = Value::float(f64::NAN);
544        assert!(v.is_float());
545        assert!(v.as_float().unwrap().is_nan());
546        // IEEE-754: NaN never equals NaN, even bit-identical.
547        assert_ne!(v, v);
548        // A NaN with a noisy payload folds onto the same canonical pattern.
549        let noisy = Value::float(f64::from_bits(0x7ff8_0000_dead_beef));
550        assert_eq!(v.bits(), noisy.bits());
551    }
552
553    #[test]
554    fn test_float_signed_zero_compares_equal() {
555        assert_eq!(Value::float(0.0), Value::float(-0.0));
556        // ...but keeps distinct bits.
557        assert_ne!(Value::float(0.0).bits(), Value::float(-0.0).bits());
558    }
559
560    #[test]
561    fn test_sym_roundtrips() {
562        let s = Symbol::from_u32(7).unwrap();
563        let v = Value::sym(s);
564        assert!(v.is_sym());
565        assert_eq!(v.as_sym(), Some(s));
566        assert_eq!(v.unpack(), Unpacked::Sym(s));
567    }
568
569    #[test]
570    fn test_wrong_accessor_returns_none() {
571        let v = Value::int(1);
572        assert_eq!(v.as_bool(), None);
573        assert_eq!(v.as_float(), None);
574        assert_eq!(v.as_sym(), None);
575        assert_eq!(Value::float(1.0).as_int(), None);
576    }
577
578    #[test]
579    fn test_distinct_kinds_never_compare_equal() {
580        // A float 1.0 and an int 1 are different values.
581        assert_ne!(Value::int(1), Value::float(1.0));
582        assert_ne!(Value::nil(), Value::bool(false));
583        assert_ne!(Value::int(0), Value::nil());
584    }
585
586    #[test]
587    fn test_from_impls_match_constructors() {
588        assert_eq!(Value::from(true), Value::bool(true));
589        assert_eq!(Value::from(9_i32), Value::int(9));
590        assert_eq!(Value::from(1.25_f64), Value::float(1.25));
591        assert_eq!(Value::from(Unpacked::Nil), Value::nil());
592    }
593
594    #[test]
595    fn test_unpack_from_roundtrips() {
596        let s = Symbol::from_u32(3).unwrap();
597        for v in [
598            Value::nil(),
599            Value::bool(true),
600            Value::bool(false),
601            Value::int(-42),
602            Value::float(12.5),
603            Value::sym(s),
604        ] {
605            assert_eq!(Value::from(v.unpack()), v);
606        }
607    }
608
609    #[test]
610    fn test_debug_names_the_kind() {
611        extern crate alloc;
612        use alloc::format;
613        assert_eq!(format!("{:?}", Value::int(5)), "Value(Int(5))");
614        assert_eq!(format!("{:?}", Value::nil()), "Value(Nil)");
615    }
616}