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}