Skip to main content

subetha_cxc/
shared_nan_value.rs

1//! `SharedNaNValue` - 64-bit NaN-boxed heterogeneous value cell.
2//!
3//! Packs `f64 | i32 | u32 | bool | nil | OffsetPtr<T>` into a single
4//! `u64`, distinguishing types via the IEEE 754 NaN bit patterns the
5//! FPU never produces during normal computation.
6//!
7//! # Encoding
8//!
9//! ```text
10//!   bit 63        bit 51    bits 50-48    bits 47-0
11//!   [sign=1][exp=0x7FF][qNaN=1][ tag(3) ][   payload (48)   ]
12//! ```
13//!
14//! The boxed prefix is `0xFFF8_0000_0000_0000` (sign=1 + all-ones
15//! exponent + qNaN bit). Real float NaNs from computation usually
16//! have sign=0, so we don't collide with them. To be safe, every
17//! `from_f64(NaN)` canonicalises the bit pattern to
18//! `0x7FF8_0000_0000_0000` (positive canonical qNaN) so the stored
19//! bits never look boxed when they aren't.
20//!
21//! # Type tags (3 bits, 8 slots; 6 used, 2 reserved)
22//!
23//! | tag | meaning   | payload encoding              |
24//! |-----|-----------|-------------------------------|
25//! | 0   | nil       | payload bits ignored (all 0)  |
26//! | 1   | i32       | low 32 bits                   |
27//! | 2   | u32       | low 32 bits                   |
28//! | 3   | bool      | low 1 bit                     |
29//! | 4   | OffsetPtr | low 32 bits = index           |
30//! | 5   | reserved  | for TaggedOffsetPtr           |
31//! | 6   | reserved  |                               |
32//! | 7   | reserved  |                               |
33//!
34//! # Cross-process angle
35//!
36//! When tag = 4 (OffsetPtr), the payload is a 32-bit INDEX, not a
37//! virtual address. Same `u64` bit pattern resolves to the same
38//! pointer in every process that maps the underlying SharedRegion.
39//! That's what makes this primitive cross-process safe where V8 /
40//! SpiderMonkey NaN boxing is single-process only.
41//!
42//! # Use cases
43//!
44//! - Cross-process scripting / dynamic-language interpreters.
45//! - Heterogeneous config maps:
46//!   `SharedHashMap<K, SharedNaNValue>` where V can be int/float/
47//!   bool/ptr without per-variant storage.
48//! - Tagged-union slots in shared state.
49//! - Weakly-typed message payloads in event streams.
50
51use crate::shared_region::OffsetPtr;
52
53/// Mask covering the boxed-marker prefix (top 13 bits).
54pub const BOXED_MASK: u64 = 0xFFF8_0000_0000_0000;
55/// The exact bit pattern that marks a boxed value.
56pub const BOXED_PREFIX: u64 = 0xFFF8_0000_0000_0000;
57/// Bit position of the type tag.
58pub const TAG_SHIFT: u64 = 48;
59/// Mask for the 3-bit tag once shifted into low bits.
60pub const TAG_MASK: u64 = 0x7;
61/// Mask for the 48-bit payload.
62pub const PAYLOAD_MASK: u64 = 0x0000_FFFF_FFFF_FFFF;
63
64/// Canonical positive qNaN. Any NaN input to `from_f64` is rewritten
65/// to this so we never accidentally write a bit pattern that looks
66/// boxed.
67pub const CANONICAL_QNAN: u64 = 0x7FF8_0000_0000_0000;
68
69// Tag constants.
70pub const TAG_NIL: u64 = 0;
71pub const TAG_I32: u64 = 1;
72pub const TAG_U32: u64 = 2;
73pub const TAG_BOOL: u64 = 3;
74pub const TAG_OFFSET_PTR: u64 = 4;
75pub const TAG_TAGGED_OFFSET_PTR: u64 = 5;  // reserved for TaggedOffsetPtr
76
77/// Discriminator for a SharedNaNValue's payload.
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
79pub enum NaNValueType {
80    F64,
81    Nil,
82    I32,
83    U32,
84    Bool,
85    OffsetPtr,
86    Reserved(u64),
87}
88
89/// 64-bit NaN-boxed value. Stores one of: f64, i32, u32, bool, nil,
90/// or `OffsetPtr<T>`. Discriminated via the IEEE 754 NaN bit pattern.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
92#[repr(C)]
93pub struct SharedNaNValue {
94    raw: u64,
95}
96
97#[inline]
98const fn pack(tag: u64, payload: u64) -> u64 {
99    BOXED_PREFIX | (tag << TAG_SHIFT) | (payload & PAYLOAD_MASK)
100}
101
102impl SharedNaNValue {
103    /// The nil value.
104    pub const NIL: Self = Self { raw: pack(TAG_NIL, 0) };
105
106    // ----- constructors -----
107
108    /// Wrap an `f64`. NaN inputs are canonicalised to the positive
109    /// canonical qNaN so the resulting bits never look boxed.
110    pub fn from_f64(v: f64) -> Self {
111        if v.is_nan() {
112            // Force any NaN to a known-not-boxed pattern.
113            Self { raw: CANONICAL_QNAN }
114        } else {
115            Self { raw: v.to_bits() }
116        }
117    }
118
119    pub const fn from_i32(v: i32) -> Self {
120        // Cast to u64 with sign extension limited to low 32 bits;
121        // mask to fit in 32 bits.
122        Self { raw: pack(TAG_I32, (v as u32) as u64) }
123    }
124
125    pub const fn from_u32(v: u32) -> Self {
126        Self { raw: pack(TAG_U32, v as u64) }
127    }
128
129    pub const fn from_bool(v: bool) -> Self {
130        Self { raw: pack(TAG_BOOL, v as u64) }
131    }
132
133    /// Wrap an OffsetPtr by storing its 32-bit index. The T parameter
134    /// is type-erased; callers reconstruct it on extraction.
135    pub fn from_offset_ptr<T>(p: OffsetPtr<T>) -> Self {
136        Self { raw: pack(TAG_OFFSET_PTR, p.index as u64) }
137    }
138
139    /// Construct from raw bits. Useful for serialisation /
140    /// cross-process passing.
141    #[inline]
142    pub const fn from_raw(raw: u64) -> Self { Self { raw } }
143
144    /// Get the raw u64 representation.
145    #[inline]
146    pub const fn raw(self) -> u64 { self.raw }
147
148    // ----- type queries -----
149
150    #[inline]
151    fn is_boxed(self) -> bool {
152        (self.raw & BOXED_MASK) == BOXED_PREFIX
153    }
154
155    #[inline]
156    fn tag(self) -> u64 {
157        (self.raw >> TAG_SHIFT) & TAG_MASK
158    }
159
160    pub fn type_tag(self) -> NaNValueType {
161        if !self.is_boxed() { return NaNValueType::F64; }
162        match self.tag() {
163            TAG_NIL => NaNValueType::Nil,
164            TAG_I32 => NaNValueType::I32,
165            TAG_U32 => NaNValueType::U32,
166            TAG_BOOL => NaNValueType::Bool,
167            TAG_OFFSET_PTR => NaNValueType::OffsetPtr,
168            other => NaNValueType::Reserved(other),
169        }
170    }
171
172    pub fn is_f64(self) -> bool { !self.is_boxed() }
173    pub fn is_nil(self) -> bool { self.is_boxed() && self.tag() == TAG_NIL }
174    pub fn is_i32(self) -> bool { self.is_boxed() && self.tag() == TAG_I32 }
175    pub fn is_u32(self) -> bool { self.is_boxed() && self.tag() == TAG_U32 }
176    pub fn is_bool(self) -> bool { self.is_boxed() && self.tag() == TAG_BOOL }
177    pub fn is_offset_ptr(self) -> bool {
178        self.is_boxed() && self.tag() == TAG_OFFSET_PTR
179    }
180
181    // ----- extractors -----
182
183    pub fn as_f64(self) -> Option<f64> {
184        if self.is_f64() { Some(f64::from_bits(self.raw)) } else { None }
185    }
186
187    pub fn as_i32(self) -> Option<i32> {
188        if self.is_i32() {
189            Some((self.raw & 0xFFFF_FFFF) as u32 as i32)
190        } else { None }
191    }
192
193    pub fn as_u32(self) -> Option<u32> {
194        if self.is_u32() {
195            Some((self.raw & 0xFFFF_FFFF) as u32)
196        } else { None }
197    }
198
199    pub fn as_bool(self) -> Option<bool> {
200        if self.is_bool() { Some((self.raw & 1) != 0) } else { None }
201    }
202
203    pub fn as_offset_ptr<T>(self) -> Option<OffsetPtr<T>> {
204        if self.is_offset_ptr() {
205            Some(OffsetPtr::new((self.raw & 0xFFFF_FFFF) as u32))
206        } else { None }
207    }
208}
209
210impl Default for SharedNaNValue {
211    fn default() -> Self { Self::NIL }
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217    use crate::shared_region::OffsetPtr;
218
219    #[test]
220    fn nil_round_trip() {
221        let v = SharedNaNValue::NIL;
222        assert!(v.is_nil());
223        assert_eq!(v.type_tag(), NaNValueType::Nil);
224        assert_eq!(SharedNaNValue::default(), SharedNaNValue::NIL);
225    }
226
227    #[test]
228    fn i32_round_trip_positive_and_negative() {
229        for v in [0i32, 1, -1, 42, -42, i32::MAX, i32::MIN] {
230            let n = SharedNaNValue::from_i32(v);
231            assert!(n.is_i32(), "{v} should be i32");
232            assert_eq!(n.as_i32(), Some(v));
233            assert_eq!(n.type_tag(), NaNValueType::I32);
234        }
235    }
236
237    #[test]
238    fn u32_round_trip() {
239        for v in [0u32, 1, 42, u32::MAX, u32::MAX / 2] {
240            let n = SharedNaNValue::from_u32(v);
241            assert!(n.is_u32());
242            assert_eq!(n.as_u32(), Some(v));
243            assert_eq!(n.type_tag(), NaNValueType::U32);
244        }
245    }
246
247    #[test]
248    fn bool_round_trip() {
249        let t = SharedNaNValue::from_bool(true);
250        let f = SharedNaNValue::from_bool(false);
251        assert!(t.is_bool() && f.is_bool());
252        assert_eq!(t.as_bool(), Some(true));
253        assert_eq!(f.as_bool(), Some(false));
254    }
255
256    #[test]
257    fn f64_round_trip_normal_values() {
258        for v in [0.0f64, 1.0, -1.0, std::f64::consts::PI, 1e100, -1e-100,
259                  f64::INFINITY, f64::NEG_INFINITY] {
260            let n = SharedNaNValue::from_f64(v);
261            assert!(n.is_f64(), "{v} should be f64");
262            assert_eq!(n.as_f64(), Some(v));
263            assert_eq!(n.type_tag(), NaNValueType::F64);
264        }
265    }
266
267    #[test]
268    fn f64_nan_canonicalised() {
269        // A specific NaN input gets canonicalised; we lose the
270        // specific bit pattern but the result is still .is_nan().
271        let n = SharedNaNValue::from_f64(f64::NAN);
272        assert!(n.is_f64());
273        let extracted = n.as_f64().unwrap();
274        assert!(extracted.is_nan());
275        assert_eq!(n.raw(), CANONICAL_QNAN);
276    }
277
278    #[test]
279    fn f64_with_sign_1_nan_doesnt_collide_with_boxed() {
280        // Construct a "boxed-looking" NaN by hand. from_f64 should
281        // detect it as NaN and canonicalise.
282        let evil = f64::from_bits(0xFFF8_FFFF_FFFF_FFFF);
283        assert!(evil.is_nan());
284        let n = SharedNaNValue::from_f64(evil);
285        assert!(n.is_f64());
286        // After canonicalisation it's the positive canonical qNaN.
287        assert_eq!(n.raw(), CANONICAL_QNAN);
288    }
289
290    #[test]
291    fn offset_ptr_round_trip() {
292        let p: OffsetPtr<u64> = OffsetPtr::new(42);
293        let n = SharedNaNValue::from_offset_ptr(p);
294        assert!(n.is_offset_ptr());
295        let p2: OffsetPtr<u64> = n.as_offset_ptr().unwrap();
296        assert_eq!(p, p2);
297        assert_eq!(p2.index, 42);
298    }
299
300    #[test]
301    fn offset_ptr_phantom_type_erased_then_reconstructed() {
302        // T is erased in the boxed value; caller reconstructs with
303        // any T at extraction time.
304        let p: OffsetPtr<u64> = OffsetPtr::new(0xABCD);
305        let n = SharedNaNValue::from_offset_ptr(p);
306        // Extract as a different T.
307        #[derive(Clone, Copy, Debug, PartialEq)]
308        struct Foo { x: u32 }
309        let p2: OffsetPtr<Foo> = n.as_offset_ptr().unwrap();
310        assert_eq!(p2.index, 0xABCD);
311    }
312
313    #[test]
314    fn raw_round_trip_preserves_value() {
315        let n = SharedNaNValue::from_i32(-7);
316        let raw = n.raw();
317        let restored = SharedNaNValue::from_raw(raw);
318        assert_eq!(n, restored);
319        assert_eq!(restored.as_i32(), Some(-7));
320    }
321
322    #[test]
323    fn type_queries_are_mutually_exclusive() {
324        let i = SharedNaNValue::from_i32(42);
325        assert!(i.is_i32());
326        assert!(!i.is_u32());
327        assert!(!i.is_bool());
328        assert!(!i.is_f64());
329        assert!(!i.is_nil());
330        assert!(!i.is_offset_ptr());
331    }
332
333    #[test]
334    fn wrong_type_extractor_returns_none() {
335        let i = SharedNaNValue::from_i32(42);
336        assert_eq!(i.as_f64(), None);
337        assert_eq!(i.as_u32(), None);
338        assert_eq!(i.as_bool(), None);
339        assert_eq!(i.as_offset_ptr::<u64>(), None);
340    }
341
342    #[test]
343    fn equality_and_hash() {
344        use std::collections::HashSet;
345        let a = SharedNaNValue::from_i32(42);
346        let b = SharedNaNValue::from_i32(42);
347        let c = SharedNaNValue::from_i32(43);
348        assert_eq!(a, b);
349        assert_ne!(a, c);
350        let mut s = HashSet::new();
351        s.insert(a);
352        assert!(s.contains(&b));
353        assert!(!s.contains(&c));
354    }
355
356    #[test]
357    fn cross_process_via_shared_hash_map() {
358        // Demonstrate: store heterogeneous V in SharedHashMap<K, NaNValue>.
359        use crate::SharedHashMap;
360        let mut p = std::env::temp_dir();
361        p.push(format!("subetha-nan-shm-{}.bin", std::process::id()));
362        let m: SharedHashMap<u32, SharedNaNValue>
363            = SharedHashMap::create(&p, 16).unwrap();
364        m.insert(0, SharedNaNValue::from_i32(42)).unwrap();
365        m.insert(1, SharedNaNValue::from_f64(2.5)).unwrap();
366        m.insert(2, SharedNaNValue::from_bool(true)).unwrap();
367        m.insert(3, SharedNaNValue::NIL).unwrap();
368        m.insert(4, SharedNaNValue::from_offset_ptr::<u64>(OffsetPtr::new(99))).unwrap();
369        assert_eq!(m.get(&0).unwrap().as_i32(), Some(42));
370        assert_eq!(m.get(&1).unwrap().as_f64(), Some(2.5));
371        assert_eq!(m.get(&2).unwrap().as_bool(), Some(true));
372        assert!(m.get(&3).unwrap().is_nil());
373        let ptr: OffsetPtr<u64> = m.get(&4).unwrap().as_offset_ptr().unwrap();
374        assert_eq!(ptr.index, 99);
375        std::fs::remove_file(&p).ok();
376    }
377
378    #[test]
379    fn size_is_8_bytes() {
380        assert_eq!(std::mem::size_of::<SharedNaNValue>(), 8);
381    }
382
383    #[test]
384    fn reserved_tags_decode_as_reserved() {
385        let raw = pack(6, 0);  // tag 6 is reserved
386        let n = SharedNaNValue::from_raw(raw);
387        assert_eq!(n.type_tag(), NaNValueType::Reserved(6));
388    }
389}