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}