Skip to main content

catallaxy_runtime/
value.rs

1//! NaN-boxed values.
2//!
3//! A [`Value`] is one 64-bit word. Doubles are stored as themselves; the negative quiet-NaN space
4//! with the top 16 bits in `0xFFF9..=0xFFFC` encodes everything else:
5//!
6//! | top 16 bits | payload |
7//! |-------------|---------|
8//! | `0xFFF9` | 48-bit heap pointer |
9//! | `0xFFFA` | 48-bit signed small integer |
10//! | `0xFFFB` | special: `nil` (0), `false` (2), `true` (3), and internal markers |
11//!
12//! Every NaN produced by arithmetic is canonicalized to `0x7FF8_0000_0000_0000`, so no float ever
13//! collides with a tag.
14
15use catallaxy_gc::Header;
16
17#[derive(Copy, Clone, PartialEq, Eq, Hash)]
18#[repr(transparent)]
19pub struct Value(pub u64);
20
21const TAG_SHIFT: u32 = 48;
22const TAG_PTR: u64 = 0xFFF9;
23const TAG_INT: u64 = 0xFFFA;
24const TAG_SPECIAL: u64 = 0xFFFB;
25const PAYLOAD_MASK: u64 = (1 << 48) - 1;
26const CANONICAL_NAN: u64 = 0x7FF8_0000_0000_0000;
27
28pub const SMALL_INT_MIN: i64 = -(1 << 47);
29pub const SMALL_INT_MAX: i64 = (1 << 47) - 1;
30
31impl Value {
32    pub const NIL: Value = Value(TAG_SPECIAL << TAG_SHIFT);
33    pub const FALSE: Value = Value((TAG_SPECIAL << TAG_SHIFT) | 2);
34    pub const TRUE: Value = Value((TAG_SPECIAL << TAG_SHIFT) | 3);
35    /// Marks an empty slot in internal tables and an uninitialized register. Never user-visible.
36    pub const EMPTY: Value = Value((TAG_SPECIAL << TAG_SHIFT) | 4);
37    /// A dict tombstone.
38    pub const TOMBSTONE: Value = Value((TAG_SPECIAL << TAG_SHIFT) | 5);
39
40    #[inline(always)]
41    pub fn from_f64(f: f64) -> Value {
42        if f.is_nan() {
43            Value(CANONICAL_NAN)
44        } else {
45            Value(f.to_bits())
46        }
47    }
48
49    #[inline(always)]
50    pub fn bool(b: bool) -> Value {
51        if b {
52            Value::TRUE
53        } else {
54            Value::FALSE
55        }
56    }
57
58    /// Encodes a small integer. The caller guarantees `i` fits in 48 bits.
59    #[inline(always)]
60    pub fn small_int(i: i64) -> Value {
61        debug_assert!(fits_small(i));
62        Value((TAG_INT << TAG_SHIFT) | (i as u64 & PAYLOAD_MASK))
63    }
64
65    #[inline(always)]
66    pub fn from_ptr(p: *const Header) -> Value {
67        debug_assert!((p as u64) & !PAYLOAD_MASK == 0);
68        Value((TAG_PTR << TAG_SHIFT) | p as u64)
69    }
70
71    #[inline(always)]
72    fn tag(self) -> u64 {
73        self.0 >> TAG_SHIFT
74    }
75
76    #[inline(always)]
77    pub fn is_float(self) -> bool {
78        self.tag() < TAG_PTR || self.tag() > 0xFFFC
79    }
80    #[inline(always)]
81    pub fn is_small_int(self) -> bool {
82        self.tag() == TAG_INT
83    }
84    #[inline(always)]
85    pub fn is_ptr(self) -> bool {
86        self.tag() == TAG_PTR
87    }
88    #[inline(always)]
89    pub fn is_nil(self) -> bool {
90        self.0 == Value::NIL.0
91    }
92    #[inline(always)]
93    pub fn is_bool(self) -> bool {
94        self.0 == Value::TRUE.0 || self.0 == Value::FALSE.0
95    }
96    #[inline(always)]
97    pub fn is_true(self) -> bool {
98        self.0 == Value::TRUE.0
99    }
100    #[inline(always)]
101    pub fn is_empty_marker(self) -> bool {
102        self.0 == Value::EMPTY.0
103    }
104
105    #[inline(always)]
106    pub fn as_f64(self) -> f64 {
107        f64::from_bits(self.0)
108    }
109    #[inline(always)]
110    pub fn as_small_int(self) -> i64 {
111        ((self.0 << 16) as i64) >> 16
112    }
113    #[inline(always)]
114    pub fn as_ptr(self) -> *mut Header {
115        (self.0 & PAYLOAD_MASK) as *mut Header
116    }
117    /// Both operands are small integers (one check for binary fast paths).
118    #[inline(always)]
119    pub fn both_small_ints(a: Value, b: Value) -> bool {
120        (a.tag() == TAG_INT) & (b.tag() == TAG_INT)
121    }
122
123    /// The heap header if this is a pointer.
124    #[inline(always)]
125    pub fn header(self) -> Option<&'static Header> {
126        if self.is_ptr() {
127            // SAFETY: pointer values always refer to live heap objects while reachable.
128            Some(unsafe { &*self.as_ptr() })
129        } else {
130            None
131        }
132    }
133}
134
135#[inline(always)]
136pub fn fits_small(i: i64) -> bool {
137    ((i << 16) >> 16) == i
138}
139
140impl std::fmt::Debug for Value {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        if self.is_float() {
143            write!(f, "{}f", self.as_f64())
144        } else if self.is_small_int() {
145            write!(f, "{}i", self.as_small_int())
146        } else if self.is_nil() {
147            write!(f, "nil")
148        } else if self.is_bool() {
149            write!(f, "{}", self.is_true())
150        } else if self.is_ptr() {
151            let cls = crate::class::class_of(*self);
152            write!(f, "<{} @{:p}>", cls.name, self.as_ptr())
153        } else {
154            write!(f, "<special {:x}>", self.0)
155        }
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn encodings_round_trip() {
165        for i in [0i64, 1, -1, SMALL_INT_MAX, SMALL_INT_MIN, 123456789] {
166            let v = Value::small_int(i);
167            assert!(v.is_small_int() && !v.is_float() && !v.is_ptr());
168            assert_eq!(v.as_small_int(), i);
169        }
170        for f in [0.0, -0.0, 1.5, f64::INFINITY, f64::NEG_INFINITY, f64::MIN_POSITIVE, -1e300] {
171            let v = Value::from_f64(f);
172            assert!(v.is_float());
173            assert_eq!(v.as_f64().to_bits(), f.to_bits());
174        }
175        let nan = Value::from_f64(f64::NAN);
176        assert!(nan.is_float() && nan.as_f64().is_nan());
177        let neg_nan = Value::from_f64(-f64::NAN);
178        assert!(neg_nan.is_float());
179        assert!(!Value::NIL.is_float() && Value::NIL.is_nil());
180        assert!(Value::TRUE.is_bool() && Value::TRUE.is_true());
181        assert!(!fits_small(SMALL_INT_MAX + 1));
182        assert!(!fits_small(SMALL_INT_MIN - 1));
183    }
184}