Skip to main content

qala_compiler/
value.rs

1//! the runtime value representation, in two types.
2//!
3//! [`ConstValue`] is the constant-pool entry -- what [`crate::chunk::Chunk`]'s
4//! `constants` vector holds and what codegen builds when it folds a literal
5//! expression. it is a plain tagged enum with seven variants in v1: the five
6//! primitive Qala types ([`ConstValue::I64`], [`ConstValue::F64`],
7//! [`ConstValue::Bool`], [`ConstValue::Byte`], [`ConstValue::Str`]), a unit
8//! [`ConstValue::Void`], and [`ConstValue::Function`] -- a `u16` function
9//! index. heap objects (arrays, structs, enum-variant payloads) are NOT
10//! representable as a `ConstValue`; the VM builds those at runtime via the
11//! `MAKE_*` opcodes. constant folding in codegen only folds primitive-typed
12//! expressions (and string concatenations of two literal strings); a fold
13//! that would produce a heap object falls through to runtime construction.
14//!
15//! [`Value`] is the NaN-boxed 8-byte runtime value: every slot on the VM's
16//! value stack is a `Value`. an IEEE 754 `f64` is stored verbatim as its own
17//! bits; every non-float kind ([`bool`], [`byte`](Value::byte), `void`, a
18//! heap pointer) lives in the payload of a quiet NaN whose reserved high bits
19//! select the kind. the codec is total and safe -- `f64::to_bits` /
20//! `f64::from_bits` only, no `unsafe`, no `transmute`. there is no `i64`
21//! encoding here: an `i64` value is a heap object and a `Value` holding one
22//! is a pointer (the VM owns that heap; `value.rs` is only the bit codec).
23//!
24//! the [`Display`](std::fmt::Display) impl on `ConstValue` is byte-
25//! deterministic: the same value renders to the same bytes every run. the
26//! disassembler reads this directly and the playground's bytecode panel
27//! reads the disassembler's output; non-determinism here would manifest as
28//! visual flicker on every re-render, so the rule is locked in tests.
29//!
30//! no `serde` derives: this phase keeps both types in-process only. the WASM
31//! bridge phase may add a derived form (or a hand-rolled serde proxy) when
32//! disassembler output crosses the JS boundary; deferred to that phase.
33
34/// a value the constant pool can store.
35///
36/// derives `Debug, Clone, PartialEq`. NOT `Eq` -- [`ConstValue::F64`] holds
37/// `f64`, and `f64` is not `Eq` (`NaN != NaN`). this mirrors how the AST and
38/// `QalaError` skip `Eq` for the same reason.
39#[derive(Debug, Clone, PartialEq)]
40pub enum ConstValue {
41    /// a 64-bit signed integer, the result of any `i64`-typed expression.
42    I64(i64),
43    /// a 64-bit IEEE 754 float. follows IEEE 754 for non-finite results
44    /// (`inf`, `-inf`, `NaN`); the [`Display`](std::fmt::Display) impl
45    /// renders these explicitly so the output stays stable across Rust
46    /// toolchains.
47    F64(f64),
48    /// a boolean -- the result of any `bool`-typed comparison, logic, or
49    /// literal.
50    Bool(bool),
51    /// an 8-bit unsigned byte literal -- the result of a `b'X'` literal or a
52    /// byte-typed sub-expression. rendered in lowercase-hex form by
53    /// [`Display`](std::fmt::Display).
54    Byte(u8),
55    /// an owned string. the constant pool holds the source-literal form;
56    /// string-interpolation results live on the VM's heap and never reach
57    /// the constant pool.
58    Str(String),
59    /// the value-shaped result of a void-typed expression (a no-trailing-
60    /// value block, or the `()` of a void-returning function). renders as
61    /// `()`.
62    Void,
63    /// an index into the enclosing program's chunks. resolved by codegen
64    /// during call-site emission; the VM dispatches by index. width `u16`
65    /// matches every other indexed operand in the opcode set (so up to
66    /// 65536 functions per program).
67    Function(u16),
68}
69
70/// render a [`ConstValue`] byte-deterministically.
71///
72/// the disassembler and the playground's bytecode panel both read this
73/// output; identical input produces identical bytes on every machine and
74/// every Rust toolchain. non-finite floats (`NaN`, `inf`, `-inf`) are
75/// hand-spelled because `f64`'s `Display` of these values is platform-stable
76/// in current Rust but a deliberate spelling avoids future drift.
77///
78/// string values render with no escape processing: a `Str` containing a
79/// single quote will produce visually ambiguous output, but the rendering is
80/// still deterministic -- acceptable in v1, the constant pool is in-process
81/// only this phase.
82impl std::fmt::Display for ConstValue {
83    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84        match self {
85            ConstValue::I64(n) => write!(f, "{n}"),
86            ConstValue::F64(x) => {
87                if x.is_nan() {
88                    f.write_str("NaN")
89                } else if *x == f64::INFINITY {
90                    f.write_str("inf")
91                } else if *x == f64::NEG_INFINITY {
92                    f.write_str("-inf")
93                } else {
94                    write!(f, "{x}")
95                }
96            }
97            ConstValue::Bool(b) => f.write_str(if *b { "true" } else { "false" }),
98            ConstValue::Byte(b) => write!(f, "b'\\x{b:02x}'"),
99            ConstValue::Str(s) => write!(f, "'{s}'"),
100            ConstValue::Void => f.write_str("()"),
101            ConstValue::Function(id) => write!(f, "fn#{id}"),
102        }
103    }
104}
105
106// ---- NaN-boxed runtime value -----------------------------------------------
107
108/// the quiet-NaN base: all 11 exponent bits set, plus the top mantissa bit
109/// (bit 51, the quiet bit) set. the FPU emits exactly this canonical NaN for
110/// every NaN-producing operation, so a real computed NaN has these top bits
111/// and no more -- it never collides with a tagged value below.
112///
113/// `allow(dead_code)`: this is the documented anchor the four `TAG_*`
114/// prefixes are defined above; the codec never reads it directly (a real
115/// NaN takes the untagged `f64` path via [`Value::is_tagged`]), but the
116/// round-trip test asserts every tag prefix sits strictly above it. keeping
117/// the named constant makes the bit layout self-documenting.
118#[allow(dead_code)]
119const QNAN: u64 = 0x7FF8_0000_0000_0000;
120
121/// kind tag for a function value. the low data field carries the `u16`
122/// function id directly -- a function is NOT a heap object, it is a tagged
123/// scalar like a `bool` or a `byte`. this tag sits one step below `TAG_BOOL`
124/// and one step above `QNAN`, so it is still a quiet-NaN pattern the FPU
125/// never emits, and `is_tagged` covers it.
126const TAG_FN: u64 = 0x7FFB_0000_0000_0000;
127
128/// kind tag for a [`bool`] value. the low data field carries `0` or `1`.
129const TAG_BOOL: u64 = 0x7FFC_0000_0000_0000;
130
131/// kind tag for a `byte` value. the low data field carries the `u8`.
132const TAG_BYTE: u64 = 0x7FFD_0000_0000_0000;
133
134/// kind tag for the singleton `void` value. the data field is unused (0).
135const TAG_VOID: u64 = 0x7FFE_0000_0000_0000;
136
137/// kind tag for a heap pointer. the low 48-bit data field carries the heap
138/// slot index (a `u32` slot fits with room to spare).
139const TAG_PTR: u64 = 0x7FFF_0000_0000_0000;
140
141/// masks off the top 16 bits -- the kind-select field. a tagged value's top
142/// 16 bits land in `0x7FFB..=0x7FFF`; a real `f64` (including a computed NaN,
143/// whose top bits are `0x7FF8`) does not.
144const KIND_MASK: u64 = 0xFFFF_0000_0000_0000;
145
146/// masks off the low 48 bits -- the data field a tagged value carries.
147const DATA_MASK: u64 = 0x0000_FFFF_FFFF_FFFF;
148
149/// the NaN-boxed 8-byte runtime value.
150///
151/// `Value` is a `u64` newtype. a real IEEE 754 `f64` is stored verbatim via
152/// [`f64::to_bits`]; every non-float kind is a quiet NaN whose reserved top
153/// 16 bits select the kind and whose low 48 bits carry the data. derives
154/// `Clone, Copy, PartialEq` -- one machine word, cheaper to copy than to
155/// reference. NOT `Eq`: two `Value`s wrapping `f64::NAN` are not equal, the
156/// same IEEE 754 rule the `ConstValue` enum follows.
157///
158/// there is no `i64` kind. every `i64` value is a heap object and a `Value`
159/// holding one is a [`Value::pointer`]; `value.rs` carries no integer
160/// encoding. this keeps the codec a single clean match and every arithmetic
161/// opcode a single path (the uniform-heap-box decision -- see the module
162/// research notes).
163#[derive(Clone, Copy, PartialEq)]
164pub struct Value(u64);
165
166impl Value {
167    /// box an `f64`, storing its bits verbatim. a finite value, `inf`,
168    /// `-inf`, `-0.0`, and a genuine `NaN` all round-trip: [`Value::as_f64`]
169    /// returns `Some` for every one of them, because a computed NaN's tag
170    /// bits (`0x7FF8`) are not in the reserved tagged range.
171    pub fn from_f64(x: f64) -> Value {
172        Value(x.to_bits())
173    }
174
175    /// box a `bool`. the data field is `0` for `false`, `1` for `true`.
176    pub fn bool(b: bool) -> Value {
177        Value(TAG_BOOL | b as u64)
178    }
179
180    /// box a `byte`. the data field carries the `u8` value.
181    pub fn byte(b: u8) -> Value {
182        Value(TAG_BYTE | b as u64)
183    }
184
185    /// the singleton `void` value -- the runtime shape of a void-typed
186    /// expression's result. the data field is unused.
187    pub fn void() -> Value {
188        Value(TAG_VOID)
189    }
190
191    /// box a heap pointer. `slot` is the index of a [`crate::vm`] heap
192    /// object; the 48-bit data field holds a `u32` slot with room to spare.
193    pub fn pointer(slot: u32) -> Value {
194        Value(TAG_PTR | slot as u64)
195    }
196
197    /// box a function value. `id` is the function's index into
198    /// `Program.chunks` (a CALL operand). a function value is a tagged scalar,
199    /// NOT a heap object: the `u16` id rides directly in the data field. the
200    /// VM's CONST handler builds one of these for a `ConstValue::Function`,
201    /// and the higher-order stdlib functions (`map` / `filter` / `reduce`)
202    /// recover the id via [`Value::as_function`].
203    pub fn function(id: u16) -> Value {
204        Value(TAG_FN | id as u64)
205    }
206
207    /// the raw 64-bit pattern. exposed for the VM's `get_state` rendering and
208    /// for tests that need to inspect the encoding directly.
209    pub fn bits(self) -> u64 {
210        self.0
211    }
212
213    /// `true` when this value is a tagged box (a function, `bool`, `byte`,
214    /// `void`, or pointer), `false` when it is a real `f64`.
215    ///
216    /// the test masks the top 16 bits and range-checks `0x7FFB..=0x7FFF`. a
217    /// genuine computed `NaN` has top bits `0x7FF8` and is therefore NOT
218    /// tagged -- it correctly reads back as an `f64`. never use
219    /// [`f64::is_nan`] for this test: a real NaN and a boxed value share the
220    /// exponent and quiet-bit pattern, only the reserved tag bits separate
221    /// them.
222    pub fn is_tagged(self) -> bool {
223        let top = self.0 & KIND_MASK;
224        (TAG_FN..=TAG_PTR).contains(&top)
225    }
226
227    /// decode this value as an `f64`. returns `Some` for any value that is
228    /// not a tagged box -- a finite float, `inf`, `-inf`, `-0.0`, and a
229    /// genuine `NaN` all decode here. returns `None` for a `bool` / `byte` /
230    /// `void` / pointer.
231    pub fn as_f64(self) -> Option<f64> {
232        if self.is_tagged() {
233            None
234        } else {
235            Some(f64::from_bits(self.0))
236        }
237    }
238
239    /// decode this value as a `bool`. returns `None` when the value is not a
240    /// boxed `bool`.
241    pub fn as_bool(self) -> Option<bool> {
242        if self.0 & KIND_MASK == TAG_BOOL {
243            Some(self.0 & DATA_MASK != 0)
244        } else {
245            None
246        }
247    }
248
249    /// decode this value as a `byte`. returns `None` when the value is not a
250    /// boxed `byte`. the data field is masked to 8 bits so a stray high bit
251    /// cannot widen the result.
252    pub fn as_byte(self) -> Option<u8> {
253        if self.0 & KIND_MASK == TAG_BYTE {
254            Some((self.0 & 0xFF) as u8)
255        } else {
256            None
257        }
258    }
259
260    /// `true` when this value is the singleton `void`. returns a `bool`
261    /// rather than `Option<()>` -- `void` carries no data, so the only
262    /// question is whether the value is `void` at all.
263    pub fn as_void(self) -> bool {
264        self.0 == TAG_VOID
265    }
266
267    /// decode this value as a heap pointer slot index. returns `None` when
268    /// the value is not a boxed pointer. the data field is masked to 32 bits
269    /// so the result is exactly the `u32` slot [`Value::pointer`] stored.
270    pub fn as_pointer(self) -> Option<u32> {
271        if self.0 & KIND_MASK == TAG_PTR {
272            Some((self.0 & 0xFFFF_FFFF) as u32)
273        } else {
274            None
275        }
276    }
277
278    /// decode this value as a function id. returns `None` when the value is
279    /// not a boxed function. the data field is masked to 16 bits so the
280    /// result is exactly the `u16` id [`Value::function`] stored.
281    pub fn as_function(self) -> Option<u16> {
282        if self.0 & KIND_MASK == TAG_FN {
283            Some((self.0 & 0xFFFF) as u16)
284        } else {
285            None
286        }
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    /// every primitive case the Display impl must format exactly; the test
295    /// loop below asserts the locked spelling for each.
296    fn cases() -> Vec<(ConstValue, &'static str)> {
297        vec![
298            (ConstValue::I64(42), "42"),
299            (ConstValue::I64(0), "0"),
300            (ConstValue::I64(-1), "-1"),
301            (ConstValue::I64(i64::MIN), "-9223372036854775808"),
302            (ConstValue::I64(i64::MAX), "9223372036854775807"),
303            (ConstValue::Bool(true), "true"),
304            (ConstValue::Bool(false), "false"),
305            (ConstValue::Byte(0), "b'\\x00'"),
306            (ConstValue::Byte(0xff), "b'\\xff'"),
307            (ConstValue::Byte(0xa3), "b'\\xa3'"),
308            (ConstValue::Str("hello".to_string()), "'hello'"),
309            (ConstValue::Str(String::new()), "''"),
310            (ConstValue::Void, "()"),
311            (ConstValue::Function(0), "fn#0"),
312            (ConstValue::Function(65535), "fn#65535"),
313        ]
314    }
315
316    #[test]
317    fn display_i64_uses_rust_default_decimal_form() {
318        assert_eq!(ConstValue::I64(42).to_string(), "42");
319        // i64::MIN as a literal: the spelling must match the source-text form a
320        // negative literal would produce.
321        assert_eq!(
322            ConstValue::I64(i64::MIN).to_string(),
323            "-9223372036854775808"
324        );
325    }
326
327    #[test]
328    fn display_f64_round_trips_finite_values_byte_identically() {
329        // a finite value renders to a deterministic spelling; the test does
330        // not pin the exact byte form (Rust's Display is round-trippable but
331        // not a specific text) -- it pins that two calls produce the same
332        // bytes, which IS the contract the disassembler depends on.
333        let v = ConstValue::F64(3.5);
334        assert_eq!(v.to_string(), "3.5");
335        let v2 = ConstValue::F64(0.1 + 0.2);
336        assert_eq!(v2.to_string(), v2.to_string());
337    }
338
339    #[test]
340    fn display_f64_spells_non_finite_explicitly() {
341        assert_eq!(ConstValue::F64(f64::INFINITY).to_string(), "inf");
342        assert_eq!(ConstValue::F64(f64::NEG_INFINITY).to_string(), "-inf");
343        assert_eq!(ConstValue::F64(f64::NAN).to_string(), "NaN");
344    }
345
346    #[test]
347    fn display_bool_uses_lowercase_keywords() {
348        assert_eq!(ConstValue::Bool(true).to_string(), "true");
349        assert_eq!(ConstValue::Bool(false).to_string(), "false");
350    }
351
352    #[test]
353    fn display_byte_uses_lowercase_two_digit_hex() {
354        assert_eq!(ConstValue::Byte(0).to_string(), "b'\\x00'");
355        assert_eq!(ConstValue::Byte(0xff).to_string(), "b'\\xff'");
356        assert_eq!(ConstValue::Byte(0xa3).to_string(), "b'\\xa3'");
357    }
358
359    #[test]
360    fn display_str_wraps_inner_bytes_in_single_quotes_without_escaping() {
361        assert_eq!(ConstValue::Str("hello".to_string()).to_string(), "'hello'");
362        assert_eq!(ConstValue::Str(String::new()).to_string(), "''");
363    }
364
365    #[test]
366    fn display_void_renders_as_empty_tuple_form() {
367        assert_eq!(ConstValue::Void.to_string(), "()");
368    }
369
370    #[test]
371    fn display_function_uses_hash_sigil_and_decimal_id() {
372        assert_eq!(ConstValue::Function(0).to_string(), "fn#0");
373        assert_eq!(ConstValue::Function(65535).to_string(), "fn#65535");
374    }
375
376    #[test]
377    fn every_variant_renders_to_its_locked_spelling() {
378        for (value, expected) in cases() {
379            assert_eq!(value.to_string(), expected, "Display drift for {value:?}");
380        }
381    }
382
383    #[test]
384    fn display_is_deterministic_across_repeated_calls() {
385        // a determinism stress test: build a sequence covering every variant
386        // (including the three non-finite f64 forms), render it joined by
387        // commas twice, assert the two renderings are byte-identical. a
388        // hidden non-determinism (e.g. iterating a HashMap of byte values)
389        // would surface here.
390        let values: Vec<ConstValue> = vec![
391            ConstValue::I64(1),
392            ConstValue::F64(2.5),
393            ConstValue::F64(f64::INFINITY),
394            ConstValue::F64(f64::NEG_INFINITY),
395            ConstValue::F64(f64::NAN),
396            ConstValue::Bool(true),
397            ConstValue::Bool(false),
398            ConstValue::Byte(0x10),
399            ConstValue::Str("hi".to_string()),
400            ConstValue::Void,
401            ConstValue::Function(7),
402        ];
403        let first: String = values
404            .iter()
405            .map(|v| v.to_string())
406            .collect::<Vec<_>>()
407            .join(",");
408        let second: String = values
409            .iter()
410            .map(|v| v.to_string())
411            .collect::<Vec<_>>()
412            .join(",");
413        assert_eq!(first, second, "Display is non-deterministic");
414    }
415
416    #[test]
417    fn partial_eq_holds_for_finite_primitives_and_breaks_for_nan() {
418        // the typed_ast precedent: derive PartialEq, not Eq, because f64 is
419        // not Eq. the rule survives at PartialEq -- NaN != NaN.
420        assert_eq!(ConstValue::I64(1), ConstValue::I64(1));
421        assert_eq!(ConstValue::F64(0.0), ConstValue::F64(0.0));
422        assert_eq!(ConstValue::Bool(true), ConstValue::Bool(true));
423        assert_eq!(ConstValue::Byte(0xab), ConstValue::Byte(0xab));
424        assert_eq!(
425            ConstValue::Str("x".to_string()),
426            ConstValue::Str("x".to_string())
427        );
428        assert_eq!(ConstValue::Void, ConstValue::Void);
429        assert_eq!(ConstValue::Function(3), ConstValue::Function(3));
430        // IEEE 754: a NaN is not equal to itself.
431        assert_ne!(ConstValue::F64(f64::NAN), ConstValue::F64(f64::NAN));
432        // distinct variants never compare equal.
433        assert_ne!(ConstValue::I64(0), ConstValue::Bool(false));
434    }
435
436    // ---- NaN-boxed Value round-trip battery --------------------------------
437
438    #[test]
439    fn value_is_exactly_eight_bytes() {
440        // the headline NaN-boxing contract: every stack slot is one machine
441        // word. a regression here (e.g. a stray enum tag) is caught now.
442        assert_eq!(std::mem::size_of::<Value>(), 8);
443    }
444
445    #[test]
446    fn from_f64_round_trips_a_finite_value() {
447        let v = Value::from_f64(3.5);
448        assert!(!v.is_tagged(), "a finite f64 must not be tagged");
449        assert_eq!(v.as_f64(), Some(3.5));
450    }
451
452    #[test]
453    fn from_f64_round_trips_positive_and_negative_infinity() {
454        let pos = Value::from_f64(f64::INFINITY);
455        assert!(!pos.is_tagged());
456        assert_eq!(pos.as_f64(), Some(f64::INFINITY));
457
458        let neg = Value::from_f64(f64::NEG_INFINITY);
459        assert!(!neg.is_tagged());
460        assert_eq!(neg.as_f64(), Some(f64::NEG_INFINITY));
461    }
462
463    #[test]
464    fn from_f64_round_trips_a_genuine_nan_as_a_real_float() {
465        // the canonical-NaN-collision guard: a computed NaN must decode back
466        // as an f64, NOT as a tagged value. is_tagged uses the tag-mask range
467        // check, so f64::NAN's 0x7FF8 prefix falls through to the float path.
468        let v = Value::from_f64(f64::NAN);
469        assert!(!v.is_tagged(), "a real NaN must not read as tagged");
470        let decoded = v.as_f64().expect("a NaN must decode as an f64");
471        assert!(decoded.is_nan(), "the decoded value must still be a NaN");
472        // and it does not decode as any tagged kind.
473        assert_eq!(v.as_bool(), None);
474        assert_eq!(v.as_byte(), None);
475        assert!(!v.as_void());
476        assert_eq!(v.as_pointer(), None);
477        assert_eq!(v.as_function(), None);
478    }
479
480    #[test]
481    fn from_f64_preserves_the_sign_bit_of_negative_zero() {
482        // -0.0 and 0.0 are bit-distinct; the codec stores raw bits, so the
483        // sign survives and -0.0 stays an f64 (it is not tagged).
484        let v = Value::from_f64(-0.0);
485        assert!(!v.is_tagged());
486        let decoded = v.as_f64().expect("-0.0 decodes as an f64");
487        assert!(
488            decoded == 0.0 && decoded.is_sign_negative(),
489            "sign bit lost"
490        );
491    }
492
493    #[test]
494    fn bool_round_trips_true_and_false() {
495        let t = Value::bool(true);
496        assert!(t.is_tagged());
497        assert_eq!(t.as_bool(), Some(true));
498        // a bool is not any other kind.
499        assert_eq!(t.as_f64(), None);
500        assert_eq!(t.as_byte(), None);
501
502        let f = Value::bool(false);
503        assert!(f.is_tagged());
504        assert_eq!(f.as_bool(), Some(false));
505    }
506
507    #[test]
508    fn byte_round_trips_low_high_and_mid_values() {
509        for b in [0x00u8, 0xFF, 0xA3] {
510            let v = Value::byte(b);
511            assert!(v.is_tagged(), "a byte must be tagged");
512            assert_eq!(v.as_byte(), Some(b), "byte round-trip failed for {b:#x}");
513            // a byte is not a bool or a float.
514            assert_eq!(v.as_bool(), None);
515            assert_eq!(v.as_f64(), None);
516        }
517    }
518
519    #[test]
520    fn void_is_a_tagged_singleton() {
521        let v = Value::void();
522        assert!(v.is_tagged());
523        assert!(v.as_void(), "void must read as void");
524        // void carries no data and is no other kind.
525        assert_eq!(v.as_bool(), None);
526        assert_eq!(v.as_byte(), None);
527        assert_eq!(v.as_f64(), None);
528        assert_eq!(v.as_pointer(), None);
529        // two voids are equal -- the singleton property. asserted with `==`
530        // rather than assert_eq! because `Value` carries no `Debug` derive.
531        assert!(Value::void() == Value::void(), "void is not a singleton");
532    }
533
534    #[test]
535    fn pointer_round_trips_slot_zero_and_slot_u32_max() {
536        for slot in [0u32, 1, 0xABCD, u32::MAX] {
537            let v = Value::pointer(slot);
538            assert!(v.is_tagged(), "a pointer must be tagged");
539            assert_eq!(
540                v.as_pointer(),
541                Some(slot),
542                "pointer round-trip failed for slot {slot}"
543            );
544            // a pointer is not a primitive kind.
545            assert!(!v.as_void());
546            assert_eq!(v.as_bool(), None);
547        }
548    }
549
550    #[test]
551    fn is_tagged_is_false_for_every_float_and_true_for_every_box() {
552        // the discriminator the VM relies on. every float case -- including
553        // the non-finite ones -- must read as not-tagged; every boxed case
554        // must read as tagged.
555        let floats = [
556            Value::from_f64(0.0),
557            Value::from_f64(-0.0),
558            Value::from_f64(1.0),
559            Value::from_f64(-7.25),
560            Value::from_f64(f64::INFINITY),
561            Value::from_f64(f64::NEG_INFINITY),
562            Value::from_f64(f64::NAN),
563            Value::from_f64(f64::MIN),
564            Value::from_f64(f64::MAX),
565        ];
566        for f in floats {
567            assert!(!f.is_tagged(), "float {:#018x} read as tagged", f.bits());
568        }
569        let boxes = [
570            Value::function(0),
571            Value::function(u16::MAX),
572            Value::bool(true),
573            Value::bool(false),
574            Value::byte(0),
575            Value::byte(0xFF),
576            Value::void(),
577            Value::pointer(0),
578            Value::pointer(u32::MAX),
579        ];
580        for b in boxes {
581            assert!(b.is_tagged(), "box {:#018x} read as not-tagged", b.bits());
582        }
583    }
584
585    #[test]
586    fn the_five_tag_prefixes_are_distinct_and_none_equals_the_bare_qnan() {
587        // a computed NaN never decodes as a tagged value: the five reserved
588        // tag prefixes are pairwise distinct and all sit strictly above the
589        // canonical-NaN prefix QNAN (0x7FF8).
590        // the canonical NaN's top 16 bits are below the tagged range. this
591        // claim does not depend on the loop variable, so it is a single
592        // compile-time check outside the loop.
593        const _: () = assert!(
594            (QNAN & KIND_MASK) < TAG_FN,
595            "QNAN prefix is inside the tagged range"
596        );
597
598        let tags = [TAG_FN, TAG_BOOL, TAG_BYTE, TAG_VOID, TAG_PTR];
599        for (i, a) in tags.iter().enumerate() {
600            for b in &tags[i + 1..] {
601                assert_ne!(a, b, "two tag prefixes collide");
602            }
603            assert!(*a > QNAN, "tag prefix {a:#018x} not above QNAN");
604        }
605    }
606
607    #[test]
608    fn function_round_trips_low_high_and_mid_ids() {
609        // a function value is a tagged scalar carrying the u16 fn-id directly;
610        // no heap object. every id round-trips through function/as_function.
611        for id in [0u16, 1, 0x1234, u16::MAX] {
612            let v = Value::function(id);
613            assert!(v.is_tagged(), "a function must be tagged");
614            assert_eq!(
615                v.as_function(),
616                Some(id),
617                "function round-trip failed for id {id}"
618            );
619            // a function is no other kind.
620            assert_eq!(v.as_bool(), None);
621            assert_eq!(v.as_byte(), None);
622            assert_eq!(v.as_f64(), None);
623            assert_eq!(v.as_pointer(), None);
624            assert!(!v.as_void());
625        }
626    }
627
628    #[test]
629    fn a_function_is_not_a_pointer_and_a_pointer_is_not_a_function() {
630        // the two tags must not alias: a heap pointer at slot 5 and a function
631        // with id 5 are distinct values that decode to distinct kinds.
632        let ptr = Value::pointer(5);
633        let func = Value::function(5);
634        assert_ne!(ptr.bits(), func.bits(), "pointer and function tags alias");
635        assert_eq!(ptr.as_pointer(), Some(5));
636        assert_eq!(ptr.as_function(), None);
637        assert_eq!(func.as_function(), Some(5));
638        assert_eq!(func.as_pointer(), None);
639    }
640}