Skip to main content

candela_vm/
data.rs

1// This file is derived from keel (https://github.com/horacehoff/keel),
2// Copyright 2026 Horace Hoff, licensed under the Apache License, Version 2.0.
3// It has been modified by the candela authors. See the NOTICE file.
4use crate::gc::GcState;
5use crate::gc::alloc_string;
6use crate::rt::EnumType;
7use crate::rt::Struct;
8use crate::vm::{MapPool, ObjectPool, RegisterFile, StringPool, char_byte_offset, count_chars};
9use smol_strc::SmolStr;
10use smol_strc::ToSmolStr;
11use std::hash::Hash;
12use std::hash::Hasher;
13use std::hint::cold_path;
14use std::hint::unreachable_unchecked;
15
16const NAN_BASE: u64 =
17    0b1111_1111_1111_1000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000;
18const PAYLOAD_MASK: u64 = 0b1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111_1111;
19const NAN_BOOL: u64 = NAN_BASE | (1 << 48);
20const NAN_STRING_SMALL: u64 = NAN_BASE | (2 << 48);
21const NAN_STRING_LARGE: u64 = NAN_BASE | (3 << 48);
22const NAN_ARRAY: u64 = NAN_BASE | (4 << 48);
23const NAN_NULL: u64 = NAN_BASE | (5 << 48);
24const NAN_INT: u64 = NAN_BASE | (6 << 48);
25const NAN_STRUCT: u64 = NAN_BASE | (7 << 48);
26const NAN_MAP: u64 = NAN_BASE | (7 << 48) | (1 << 47);
27/// Enum values claim the free all-zero type field on the quiet-NaN base
28/// (`NAN_ENUM == NAN_BASE`). The 48-bit payload packs the enum type id
29/// (bits 32-47) and an object-pool index (low 32), exactly like the struct
30/// encoding.
31///
32/// Hardware produces a quiet NaN with the sign bit set for an invalid operation
33/// such as `sqrt` of a negative number, and its bit pattern is exactly
34/// `NAN_BASE`: an enum of type id 0 at pool index 0. [`Data::float`] therefore
35/// rewrites any negative quiet NaN to [`CANONICAL_NAN`], which keeps every
36/// float outside the tag space.
37const NAN_ENUM: u64 = NAN_BASE;
38/// The positive quiet NaN every NaN float is stored as. Its sign bit is clear,
39/// so it reads back as a float rather than as an enum. NaN carries no value a
40/// candela program can inspect, so collapsing sign and payload changes nothing
41/// observable.
42const CANONICAL_NAN: u64 =
43    0b0111_1111_1111_1000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000_0000;
44/// The top bit of each of the six bytes an inlined string packs. A string
45/// with none of them set holds nothing but single-byte characters.
46const SMALL_STR_HIGH_BITS: u64 = 0x0000_8080_8080_8080;
47/// The bit that tells a function value from the list it is built as. A
48/// function value is an array: its slots are where the body starts and the
49/// cells it captured, and the object pool holds it and the collector walks it
50/// like any other. The bit is free in an array's payload, since a pool index
51/// spans the low 32, so carrying it costs the value nothing and costs a
52/// program that never makes one nothing at all.
53const NAN_FN_MARK: u64 = 1 << 47;
54/// How a function value reads wherever one becomes text. A function carries no
55/// signature at run time, so every function reads alike.
56pub const FUNCTION_TEXT: &str = "<fn>";
57pub const NULL: Data = Data::tagged(NAN_NULL);
58pub const FALSE: Data = Data::tagged(NAN_BOOL);
59pub const TRUE: Data = Data::tagged(NAN_BOOL | 1);
60
61/// A NaN-boxed word, plus the word an `int` needs.
62/// ### Layout:
63/// - first 13 bits are NaN
64/// - remaining 51 bits: 3 bits for type (4 for the MAP type) & 48 bits of actual payload (47 for the MAP type)
65///
66/// ### Data type:
67/// - BOOL = 001
68/// - STRING_SMALL = 010
69/// - STRING_LARGE = 011
70/// - ARRAY = 100
71/// - NULL = 101
72/// - INT = 110
73/// - STRUCT = 111
74/// - MAP = 1111
75///
76/// ### Functions
77/// A function value is an ARRAY with the top payload bit set, which is what
78/// tells it from the list it is built as.
79///
80/// ### Strings
81/// Strings are inlined if they're 6 bytes long or less.
82/// If they're longer, they're stored in `string_pool`.
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
84#[repr(C)]
85pub struct Data {
86    /// The tag and, for every type but `int`, the payload. It comes first so
87    /// an inlined string still reads out of the low bytes of the value.
88    boxed: u64,
89    /// An `int`, and zero for every other type. An `int` spans all 64 bits, so
90    /// the tagged word has no room left to hold one.
91    int: i64,
92}
93
94#[derive(Default, Clone, Copy)]
95pub struct DataHash(u64);
96
97impl Hasher for DataHash {
98    fn finish(&self) -> u64 {
99        self.0
100    }
101    fn write(&mut self, _bytes: &[u8]) {
102        unsafe { unreachable_unchecked() }
103    }
104    fn write_u64(&mut self, i: u64) {
105        self.0 = i;
106    }
107}
108
109/// The hash a pooled string is filed under as a map key: 64-bit FNV-1a over
110/// its bytes. A map key keeps it in the value, and a `.cdlb` records the key
111/// with it, so it has to come out the same on every machine and in every
112/// build.
113#[must_use]
114pub fn text_hash(text: &str) -> u64 {
115    text.bytes().fold(0xcbf2_9ce4_8422_2325, |hash, byte| {
116        (hash ^ u64::from(byte)).wrapping_mul(0x0000_0100_0000_01b3)
117    })
118}
119
120impl Hash for Data {
121    /// Both words in one, so an `int` hashes by its value and every other type
122    /// hashes by its box exactly as it did when the box was the whole value,
123    /// except a pooled string stored as a map key, which hashes by its text
124    /// (see [`Data::as_map_key`]). `DataHash` keeps only the last word written,
125    /// so writing them apart would collapse every non-`int` onto the same
126    /// hash.
127    #[inline(always)]
128    fn hash<H: Hasher>(&self, state: &mut H) {
129        state.write_u64(self.boxed ^ (self.int as u64));
130    }
131}
132
133pub trait PoolString {
134    fn pool_as_str(&self) -> &str;
135    fn move_to_slot(self, slot: &mut String);
136    fn push_to_pool(self, str_pool: &mut StringPool);
137    fn str_len(&self) -> usize;
138}
139
140impl PoolString for String {
141    #[inline(always)]
142    fn pool_as_str(&self) -> &str {
143        self.as_str()
144    }
145    #[inline(always)]
146    fn move_to_slot(self, slot: &mut String) {
147        *slot = self;
148    }
149    #[inline(always)]
150    fn push_to_pool(self, pool: &mut StringPool) {
151        pool.push(self);
152    }
153    #[inline(always)]
154    fn str_len(&self) -> usize {
155        self.len()
156    }
157}
158impl PoolString for &str {
159    #[inline(always)]
160    fn pool_as_str(&self) -> &str {
161        self
162    }
163    #[inline(always)]
164    fn move_to_slot(self, slot: &mut String) {
165        self.clone_into(slot);
166    }
167    #[inline(always)]
168    fn push_to_pool(self, string_pool: &mut StringPool) {
169        string_pool.push(self.to_owned());
170    }
171    #[inline(always)]
172    fn str_len(&self) -> usize {
173        self.len()
174    }
175}
176
177impl Data {
178    /// A value whose whole content is its NaN box. Every type but `int` is
179    /// built through here, which is what keeps the second word zero for them
180    /// and so keeps equality and hashing reading one word.
181    #[inline(always)]
182    const fn tagged(boxed: u64) -> Self {
183        Self { boxed, int: 0 }
184    }
185    /// The pair of words a `.cdlb` records a value as. The artifact keeps the
186    /// box and the integer rather than a type tag, so a value comes back
187    /// without being inspected.
188    #[must_use]
189    #[inline(always)]
190    pub const fn into_words(self) -> (u64, i64) {
191        (self.boxed, self.int)
192    }
193    /// Reads the value at `src` as two separate words.
194    ///
195    /// Most instructions write a result a word at a time: the tag and the
196    /// integer are stored apart. A copy that then read the value back as one
197    /// 16-byte load would have to wait for both stores to reach the cache,
198    /// because the processor forwards a pending store only to a load that
199    /// fits inside it. Reading the words apart lets each one forward. The
200    /// reads are volatile so the compiler cannot fuse them back into one.
201    ///
202    /// # Safety
203    ///
204    /// `src` must point to a live value.
205    #[inline(always)]
206    pub unsafe fn load(src: *const Self) -> Self {
207        let words = src.cast::<u64>();
208        unsafe {
209            Self {
210                boxed: words.read_volatile(),
211                int: words.add(1).read_volatile() as i64,
212            }
213        }
214    }
215    /// Whether this is `false`. A bool keeps its whole value in the box, so
216    /// the box alone answers, and the integer word is never read.
217    #[inline(always)]
218    pub const fn is_false(self) -> bool {
219        self.boxed == NAN_BOOL
220    }
221    /// Whether this is `true`, read from the box alone.
222    #[inline(always)]
223    pub const fn is_true(self) -> bool {
224        self.boxed == NAN_BOOL | 1
225    }
226    /// Rebuilds a value from the words [`Data::into_words`] handed out.
227    #[must_use]
228    #[inline(always)]
229    pub const fn from_words(boxed: u64, int: i64) -> Self {
230        Self { boxed, int }
231    }
232    #[inline(always)]
233    pub const fn tag(self) -> u64 {
234        self.boxed & !0xFFFF_FFFF // also includes type_id
235    }
236    #[inline(always)]
237    pub const fn is_null(self) -> bool {
238        self.boxed == NAN_NULL
239    }
240    #[inline(always)]
241    pub const fn bool(b: bool) -> Self {
242        Self::tagged(NAN_BOOL | b as u64)
243    }
244    #[inline(always)]
245    pub fn as_bool(self) -> bool {
246        debug_assert!(self.is_bool());
247        (self.boxed & 1) != 0
248    }
249    #[inline(always)]
250    pub const fn is_bool(self) -> bool {
251        (self.boxed & !PAYLOAD_MASK) == NAN_BOOL
252    }
253    #[inline(always)]
254    pub const fn float(n: f64) -> Self {
255        let bits = n.to_bits();
256        // Every float except a negative quiet NaN leaves at least one bit of
257        // NAN_BASE clear and so cannot be mistaken for a tagged value. A
258        // negative quiet NaN is stored as the positive one instead.
259        if (bits & NAN_BASE) == NAN_BASE {
260            Self::tagged(CANONICAL_NAN)
261        } else {
262            Self::tagged(bits)
263        }
264    }
265    #[inline(always)]
266    pub const fn as_float(self) -> f64 {
267        debug_assert!(self.is_float());
268        f64::from_bits(self.boxed)
269    }
270    #[inline(always)]
271    pub const fn is_float(self) -> bool {
272        (self.boxed & NAN_BASE) != NAN_BASE
273    }
274    #[inline(always)]
275    /// Convert the given integer to a tagged integer.
276    /// The box carries the tag and the second word carries all 64 bits.
277    pub const fn int(n: i64) -> Self {
278        Self {
279            boxed: NAN_INT,
280            int: n,
281        }
282    }
283    #[inline(always)]
284    pub const fn as_int(self) -> i64 {
285        debug_assert!(self.is_int());
286        self.int
287    }
288    #[inline(always)]
289    pub const fn is_int(self) -> bool {
290        (self.boxed & !PAYLOAD_MASK) == NAN_INT
291    }
292    #[inline(always)]
293    pub const fn array(id: u32) -> Self {
294        Self::tagged(NAN_ARRAY | id as u64)
295    }
296    #[inline(always)]
297    pub const fn as_array(self) -> usize {
298        // Enum values share the object pool with arrays/structs and are compared
299        // through this accessor in `obj_eq`.
300        debug_assert!(self.is_array() || self.is_struct() || self.is_enum());
301        (self.boxed & 0xFFFF_FFFF) as usize
302    }
303    #[inline(always)]
304    pub const fn is_array(self) -> bool {
305        (self.boxed & !PAYLOAD_MASK) == NAN_ARRAY
306    }
307    /// A function value over the pool entry `id`: the array a call reads the
308    /// body location and the captured cells out of, marked as the function it
309    /// is.
310    #[inline(always)]
311    pub const fn function(id: u32) -> Self {
312        Self::tagged(NAN_ARRAY | NAN_FN_MARK | id as u64)
313    }
314    /// Whether this value is a function. A function value answers
315    /// [`Self::is_array`] too, since everything that holds, walks and frees an
316    /// array holds, walks and frees this one.
317    #[inline(always)]
318    pub const fn is_function(self) -> bool {
319        (self.boxed & !PAYLOAD_MASK) == NAN_ARRAY && (self.boxed & NAN_FN_MARK) != 0
320    }
321    /// This will create a new inlined string.
322    /// In debug it'll panic (just in case).
323    /// The caller guarantees that `s` is never longer than 6 bytes.
324    #[inline(always)]
325    pub fn small_str(s: &str) -> Self {
326        debug_assert!(s.len() <= 6);
327        let mut payload: u64 = 0;
328        // Packs 6 bytes into the payload, filling up the 48 payload bits
329        for (i, byte) in s.as_bytes().iter().enumerate() {
330            payload |= (*byte as u64) << (i * 8);
331        }
332        Self::tagged(NAN_STRING_SMALL | (payload & PAYLOAD_MASK))
333    }
334    #[inline(always)]
335    pub const fn large_str_id(id: u64) -> Self {
336        Self::tagged(NAN_STRING_LARGE | id)
337    }
338    /// This value as a map stores it for a key. A string in the pool takes a
339    /// hash of its text into its second word, which only an `int` otherwise
340    /// uses, arranged so the value hashes as the text does rather than as the
341    /// slot: two strings with the same text in different slots then land in
342    /// the same bucket, where the map compares them by text. See
343    /// [`crate::vm::KeyByText`].
344    #[inline(always)]
345    #[must_use]
346    pub fn as_map_key(self, string_pool: &StringPool) -> Self {
347        if self.is_large_str() {
348            Self {
349                boxed: self.boxed,
350                int: (text_hash(self.as_str(string_pool)) ^ self.boxed) as i64,
351            }
352        } else {
353            self
354        }
355    }
356    /// Same as str(), except this never runs the GC because this function is called by the compiler
357    #[inline(always)]
358    pub fn p_str(s: &str, string_pool: &mut StringPool) -> Self {
359        Self::interned(s, string_pool).unwrap_or_else(|| {
360            let string_pool_id = string_pool.len() as u64;
361            string_pool.push(s.to_owned());
362            Self::tagged(NAN_STRING_LARGE | string_pool_id)
363        })
364    }
365    /// `s` as a value without allocating: inline if it is six bytes or
366    /// shorter, else the pool slot already holding it, if one does.
367    pub fn interned(s: &str, string_pool: &mut StringPool) -> Option<Self> {
368        if s.len() <= 6 {
369            return Some(Self::small_str(s));
370        }
371        let id = string_pool.iter().position(|existing| existing == s)?;
372        // The program may hold nothing that reaches this slot, so a cycle in
373        // progress counts it as reached from here on.
374        string_pool.shade(id);
375        Some(Self::tagged(NAN_STRING_LARGE | id as u64))
376    }
377    /// Allocates a string, storing it directly inside the value if it is six
378    /// bytes or shorter and in the string pool otherwise. A pooled string can
379    /// start a collection, so every value the program still holds has to be
380    /// reachable from `registers` or `recursion_stack`.
381    #[inline(always)]
382    pub fn string<S: PoolString>(
383        s: S,
384        array_pool: &mut ObjectPool,
385        map_pool: &mut MapPool,
386        string_pool: &mut StringPool,
387        registers: &RegisterFile,
388        recursion_stack: &RegisterFile,
389        gc: &mut GcState,
390    ) -> Self {
391        if s.str_len() <= 6 {
392            Self::small_str(s.pool_as_str())
393        } else {
394            let id = alloc_string(
395                s,
396                array_pool,
397                map_pool,
398                string_pool,
399                registers,
400                recursion_stack,
401                gc,
402            );
403            Self::tagged(NAN_STRING_LARGE | id)
404        }
405    }
406    #[inline(always)]
407    pub fn as_str(&self, string_pool: &StringPool) -> &str {
408        debug_assert!(self.is_string());
409        if (self.boxed & !PAYLOAD_MASK) == NAN_STRING_SMALL {
410            let len = self.small_str_len();
411            let ptr = self as *const Self as *const u8;
412            unsafe {
413                let slice = std::slice::from_raw_parts(ptr, len);
414                std::str::from_utf8_unchecked(slice)
415            }
416        } else {
417            let payload = (self.boxed & PAYLOAD_MASK) as usize;
418            unsafe { &*(string_pool[payload].as_str() as *const str) }
419        }
420    }
421    /// True when every character of this string takes a single byte. A
422    /// character position on such a string is a byte position, which is the
423    /// route indexing and slicing take when they can.
424    #[inline(always)]
425    pub fn str_is_ascii(&self, string_pool: &StringPool) -> bool {
426        if self.is_large_str() {
427            string_pool.is_ascii(self.get_str_pool_id())
428        } else {
429            self.boxed & SMALL_STR_HIGH_BITS == 0
430        }
431    }
432    /// How many characters this string has. Positions on a string count
433    /// characters, so this is what `len` answers and what an index or a slice
434    /// bound is checked against.
435    #[inline(always)]
436    pub fn str_char_len(&self, string_pool: &StringPool) -> usize {
437        if self.is_large_str() {
438            string_pool.char_len(self.get_str_pool_id())
439        } else if self.boxed & SMALL_STR_HIGH_BITS == 0 {
440            self.small_str_len()
441        } else {
442            cold_path();
443            count_chars(self.as_str(string_pool))
444        }
445    }
446    /// The byte offset character `char_idx` starts at. The string has at least
447    /// `char_idx` characters; at exactly that many the answer is its byte
448    /// length, which is what a slice bound on the end of the string asks for.
449    #[inline(always)]
450    pub fn str_byte_offset(&self, char_idx: usize, string_pool: &StringPool) -> usize {
451        if self.str_is_ascii(string_pool) {
452            char_idx
453        } else if self.is_large_str() {
454            string_pool.byte_offset(self.get_str_pool_id(), char_idx)
455        } else {
456            char_byte_offset(self.as_str(string_pool), char_idx)
457        }
458    }
459    /// How many bytes an inlined string takes.
460    #[inline(always)]
461    const fn small_str_len(&self) -> usize {
462        let payload = self.boxed & PAYLOAD_MASK;
463        ((64 - payload.leading_zeros()) as usize + 7) >> 3
464    }
465    #[inline(always)]
466    pub const fn is_string(self) -> bool {
467        // this works because NAN_TAG_STRING_LARGE == NAN_TAG_STRING_SMALL + (1 << 48)
468        (self.boxed & !PAYLOAD_MASK).wrapping_sub(NAN_STRING_SMALL) <= const { 1u64 << 48 }
469    }
470    /// Increments the integer stored in this Data in-place. Wraps.
471    #[inline(always)]
472    pub const fn inc_int(&mut self) {
473        debug_assert!(self.is_int());
474        self.int = self.int.wrapping_add(1);
475    }
476    /// Decrements the integer stored in this Data in-place. Wraps.
477    #[inline(always)]
478    pub const fn dec_int(&mut self) {
479        debug_assert!(self.is_int());
480        self.int = self.int.wrapping_sub(1);
481    }
482    /// Writes src + 1 into self. Wraps.
483    #[inline(always)]
484    pub const fn inc_into(&mut self, src: Self) {
485        debug_assert!(src.is_int());
486        self.boxed = NAN_INT;
487        self.int = src.int.wrapping_add(1);
488    }
489    /// Writes src - 1 into self. Wraps.
490    #[inline(always)]
491    pub const fn dec_into(&mut self, src: Self) {
492        debug_assert!(src.is_int());
493        self.boxed = NAN_INT;
494        self.int = src.int.wrapping_sub(1);
495    }
496    #[inline(always)]
497    pub const fn is_large_str(self) -> bool {
498        (self.boxed & !PAYLOAD_MASK) == NAN_STRING_LARGE
499    }
500    #[inline(always)]
501    pub const fn get_str_pool_id(self) -> usize {
502        debug_assert!(self.is_large_str());
503        (self.boxed & PAYLOAD_MASK) as usize
504    }
505    #[inline(always)]
506    pub const fn struct_instance(type_id: u16, id: u32) -> Self {
507        Self::tagged(NAN_STRUCT | ((type_id as u64) << 32) | id as u64)
508    }
509    #[inline(always)]
510    pub const fn as_struct(self) -> usize {
511        // Enum values reuse the struct field-access instructions
512        // (`GetFieldStruct`/`SetFieldStruct`), which extract the low-32 object
513        // index regardless of the box tag, so an enum value is accepted here.
514        debug_assert!(self.is_struct() || self.is_enum());
515        (self.boxed & 0xFFFF_FFFF) as usize
516    }
517    #[inline(always)]
518    pub const fn enum_instance(type_id: u16, id: u32) -> Self {
519        Self::tagged(NAN_ENUM | ((type_id as u64) << 32) | id as u64)
520    }
521    #[inline(always)]
522    pub const fn as_enum(self) -> usize {
523        debug_assert!(self.is_enum());
524        (self.boxed & 0xFFFF_FFFF) as usize
525    }
526    #[inline(always)]
527    pub const fn enum_type_id(self) -> u16 {
528        debug_assert!(self.is_enum());
529        ((self.boxed >> 32) & 0xFFFF) as u16
530    }
531    #[inline(always)]
532    pub const fn is_enum(self) -> bool {
533        (self.boxed & !PAYLOAD_MASK) == NAN_ENUM
534    }
535    #[inline(always)]
536    pub const fn struct_type_id(self) -> u16 {
537        ((self.boxed >> 32) & 0xFFFF) as u16
538    }
539    #[inline(always)]
540    pub const fn is_struct(self) -> bool {
541        (self.boxed & !PAYLOAD_MASK) == NAN_STRUCT && (self.boxed & (1 << 47)) == 0
542    }
543    #[inline(always)]
544    pub const fn map(id: u32) -> Self {
545        Self::tagged(NAN_MAP | id as u64)
546    }
547    #[inline(always)]
548    pub const fn as_map(self) -> usize {
549        debug_assert!(self.is_map());
550        (self.boxed & 0xFFFF_FFFF) as usize
551    }
552    #[inline(always)]
553    pub const fn is_map(self) -> bool {
554        (self.boxed & !PAYLOAD_MASK) == NAN_STRUCT && (self.boxed & (1 << 47)) != 0
555    }
556    /// A short runtime type name, used in downcast error messages. Reads only
557    /// the box tag, so it needs no pools.
558    #[must_use]
559    pub const fn type_name(self) -> &'static str {
560        if self.is_int() {
561            "int"
562        } else if self.is_float() {
563            "float"
564        } else if self.is_bool() {
565            "bool"
566        } else if self.is_null() {
567            "null"
568        } else if self.is_string() {
569            "string"
570        } else if self.is_array() {
571            "list"
572        } else if self.is_map() {
573            "map"
574        } else if self.is_struct() {
575            "struct"
576        } else if self.is_enum() {
577            "enum"
578        } else {
579            "value"
580        }
581    }
582
583    #[allow(clippy::too_many_arguments)]
584    pub fn format(
585        self,
586        obj_pool: &ObjectPool,
587        string_pool: &StringPool,
588        map_pool: &MapPool,
589        structs: &[Struct],
590        enums: &[EnumType],
591        show_str: bool,
592    ) -> SmolStr {
593        if self.is_float() {
594            format_float(self.as_float())
595        } else if self.is_int() {
596            self.as_int().to_smolstr()
597        } else if self.is_bool() {
598            self.as_bool().to_smolstr()
599        } else if self.is_string() {
600            if show_str {
601                self.as_str(string_pool).to_smolstr()
602            } else {
603                format_args!("\"{}\"", self.as_str(string_pool)).to_smolstr()
604            }
605        } else if self.is_function() {
606            SmolStr::new_static(FUNCTION_TEXT)
607        } else if self.is_array() {
608            format_args!(
609                "[{}]",
610                obj_pool[self.as_array()]
611                    .iter()
612                    .map(|x| x.format(obj_pool, string_pool, map_pool, structs, enums, false))
613                    .collect::<Vec<SmolStr>>()
614                    .join(",")
615            )
616            .to_smolstr()
617        } else if self.is_null() {
618            SmolStr::new_static("null")
619        } else if self.is_struct() {
620            let declared = structs.get(self.struct_type_id() as usize);
621            let s_name = declared.map_or("struct", |s| s.name.as_str());
622            format_args!(
623                "{} {{{}}}",
624                s_name,
625                obj_pool[self.as_struct()]
626                    .iter()
627                    .enumerate()
628                    .map(|(idx, x)| {
629                        let value =
630                            x.format(obj_pool, string_pool, map_pool, structs, enums, false);
631                        // A slot the declaration does not name belongs to a
632                        // pool entry read as a struct it is not, and has no
633                        // name to give. The value it holds still says what is
634                        // there.
635                        match declared.and_then(|s| s.fields.get(idx)) {
636                            Some((field, ..)) => format_args!("{field}:{value}").to_smolstr(),
637                            None => value,
638                        }
639                    })
640                    .collect::<Vec<SmolStr>>()
641                    .join(",")
642            )
643            .to_smolstr()
644        } else if self.is_enum() {
645            let entry = &obj_pool[self.as_enum()];
646            // The variant tag is the entry's first word. Look the variant up
647            // instead of indexing on trust: a word that is no tag, which is
648            // what an entry a register only aliases can hold, would otherwise
649            // index the variant table out of range. An entry carrying no
650            // readable tag has no variant name to print.
651            let variant = enums
652                .get(self.enum_type_id() as usize)
653                .filter(|_| entry.first().is_some_and(|word| word.is_int()))
654                .and_then(|e| e.variants.get(entry[0].as_int() as usize));
655            let Some(variant) = variant else {
656                return SmolStr::new_static("enum");
657            };
658            if entry.len() <= 1 {
659                variant.name.clone()
660            } else {
661                format_args!(
662                    "{}({})",
663                    variant.name,
664                    entry[1..]
665                        .iter()
666                        .map(|x| x.format(obj_pool, string_pool, map_pool, structs, enums, false))
667                        .collect::<Vec<SmolStr>>()
668                        .join(",")
669                )
670                .to_smolstr()
671            }
672        } else if self.is_map() {
673            let m = &map_pool[self.as_map()];
674            format_args!(
675                "{{{}}}",
676                m.iter()
677                    .map(|(key, val)| {
678                        format_args!(
679                            "{}:{}",
680                            key.format(obj_pool, string_pool, map_pool, structs, enums, false),
681                            val.format(obj_pool, string_pool, map_pool, structs, enums, false),
682                        )
683                        .to_smolstr()
684                    })
685                    .collect::<Vec<SmolStr>>()
686                    .join(",")
687            )
688            .to_smolstr()
689        } else {
690            unsafe { unreachable_unchecked() }
691        }
692    }
693}
694
695/// How candela writes a float: with a decimal point or an exponent, so a whole
696/// float reads as a float rather than as an int. An infinity and a
697/// not-a-number have no such form and print as themselves.
698///
699/// Every float goes through here on its way to text: `string`, `print`, and a
700/// float inside a printed array, map, struct or enum.
701#[must_use]
702pub fn format_float(value: f64) -> SmolStr {
703    SmolStr::from(zmij::Buffer::new().format(value))
704}
705
706impl From<f64> for Data {
707    #[inline(always)]
708    fn from(value: f64) -> Self {
709        Self::float(value)
710    }
711}
712impl From<Data> for f64 {
713    #[inline(always)]
714    fn from(value: Data) -> Self {
715        value.as_float()
716    }
717}
718
719impl From<i64> for Data {
720    #[inline(always)]
721    fn from(value: i64) -> Self {
722        Self::int(value)
723    }
724}
725impl From<Data> for i64 {
726    #[inline(always)]
727    fn from(value: Data) -> Self {
728        value.as_int()
729    }
730}
731
732impl From<bool> for Data {
733    #[inline(always)]
734    fn from(value: bool) -> Self {
735        Self::tagged(NAN_BOOL | (value as u64))
736    }
737}
738impl From<Data> for bool {
739    #[inline(always)]
740    fn from(value: Data) -> Self {
741        value.as_bool()
742    }
743}
744
745#[cfg(test)]
746mod format_tests {
747    use super::Data;
748    use crate::rt::{DataType, EnumType, EnumVariant, Span, Struct};
749    use crate::vm::{MapPool, ObjectPool, Pool, StringPool};
750    use smol_strc::SmolStr;
751
752    fn one_struct() -> Vec<Struct> {
753        vec![Struct {
754            name: SmolStr::new_static("P"),
755            fields: Box::new([
756                (
757                    SmolStr::new_static("x"),
758                    DataType::Int,
759                    Span { start: 0, end: 0 },
760                ),
761                (
762                    SmolStr::new_static("y"),
763                    DataType::Int,
764                    Span { start: 0, end: 0 },
765                ),
766            ]),
767            id: 0,
768            name_span: Span { start: 0, end: 0 },
769        }]
770    }
771
772    /// A struct names its fields wherever it becomes text, nested in a list as
773    /// much as on its own, so `string` and a printed collection read the way
774    /// `print` does.
775    #[test]
776    fn a_struct_formats_with_its_field_names() {
777        let structs = one_struct();
778        let obj_pool: ObjectPool = Pool(vec![
779            vec![Data::int(1), Data::int(2)],
780            vec![Data::struct_instance(0, 0)],
781        ]);
782        let rendered = |value: Data| {
783            value.format(
784                &obj_pool,
785                &StringPool::default(),
786                &Pool(Vec::new()),
787                &structs,
788                &[],
789                true,
790            )
791        };
792        assert_eq!(rendered(Data::struct_instance(0, 0)), "P {x:1,y:2}");
793        assert_eq!(rendered(Data::array(1)), "[P {x:1,y:2}]");
794    }
795
796    /// A pool entry read as a struct the table does not declare has no names to
797    /// print, and still says what it holds instead of indexing the field list
798    /// with a slot that is not there.
799    #[test]
800    fn a_struct_with_no_declaration_formats_as_its_slots() {
801        let obj_pool: ObjectPool = Pool(vec![vec![Data::int(1), Data::int(2)]]);
802        let rendered = Data::struct_instance(0, 0).format(
803            &obj_pool,
804            &StringPool::default(),
805            &Pool(Vec::new()),
806            &[],
807            &[],
808            true,
809        );
810        assert_eq!(rendered, "struct {1,2}");
811    }
812
813    /// A function value reads alike wherever it becomes text, on its own and
814    /// inside a list, since a function carries no signature to tell one from
815    /// another at run time.
816    #[test]
817    fn a_function_value_formats_as_the_function_marker() {
818        let obj_pool: ObjectPool = Pool(vec![
819            vec![Data::int(12)],
820            vec![Data::function(0)],
821            vec![Data::int(12)],
822        ]);
823        let rendered = |value: Data| {
824            value.format(
825                &obj_pool,
826                &StringPool::default(),
827                &Pool(Vec::new()),
828                &[],
829                &[],
830                true,
831            )
832        };
833        assert_eq!(rendered(Data::function(0)), "<fn>");
834        assert_eq!(rendered(Data::array(1)), "[<fn>]");
835        // The list the value is built as reads as a list, mark or no mark.
836        assert_eq!(rendered(Data::array(2)), "[12]");
837    }
838
839    /// Everything that holds, walks and frees an array does the same for a
840    /// function value: the mark rides in a bit the pool index leaves free.
841    #[test]
842    fn a_function_value_is_an_array_underneath() {
843        let value = Data::function(7);
844        assert!(value.is_array());
845        assert!(value.is_function());
846        assert_eq!(value.as_array(), 7);
847        assert!(!Data::array(7).is_function());
848    }
849
850    fn one_enum() -> Vec<EnumType> {
851        vec![EnumType {
852            name: SmolStr::new_static("Value"),
853            variants: Box::new([EnumVariant {
854                name: SmolStr::new_static("Int"),
855                payload: Box::new([DataType::Int]),
856                name_span: Span { start: 0, end: 0 },
857            }]),
858            id: 0,
859            name_span: Span { start: 0, end: 0 },
860        }]
861    }
862
863    /// An enum value nested in an array formats through the same path a
864    /// top-level one does, tag and payload included.
865    #[test]
866    fn an_enum_inside_an_array_formats_as_its_variant() {
867        let enums = one_enum();
868        let obj_pool: ObjectPool = Pool(vec![
869            vec![Data::int(0), Data::int(3)],
870            vec![Data::enum_instance(0, 0)],
871        ]);
872        let rendered = Data::array(1).format(
873            &obj_pool,
874            &StringPool::default(),
875            &Pool(Vec::new()),
876            &[],
877            &enums,
878            true,
879        );
880        assert_eq!(rendered, "[Int(3)]");
881    }
882
883    /// A pool entry whose first word is no variant tag has no variant to name,
884    /// and says so rather than indexing the variant table with whatever the
885    /// word happened to be.
886    #[test]
887    fn an_entry_with_no_readable_tag_formats_as_the_bare_type() {
888        let enums = one_enum();
889        let string_pool = StringPool::default();
890        let map_pool: MapPool = Pool(Vec::new());
891        for entry in [
892            // An array of enum values, read as if it were an enum value.
893            vec![Data::enum_instance(0, 0)],
894            // A tag past the last variant.
895            vec![Data::int(42)],
896            // A negative tag.
897            vec![Data::int(-1)],
898            // No tag at all.
899            Vec::new(),
900        ] {
901            let obj_pool: ObjectPool = Pool(vec![entry]);
902            let rendered = Data::enum_instance(0, 0).format(
903                &obj_pool,
904                &string_pool,
905                &map_pool,
906                &[],
907                &enums,
908                true,
909            );
910            assert_eq!(rendered, "enum");
911        }
912    }
913}
914
915#[cfg(test)]
916mod float_boxing_tests {
917    use super::Data;
918
919    /// Hardware answers an invalid operation with a quiet NaN whose sign bit is
920    /// set, and that bit pattern is the enum tag. Boxing has to keep it a float,
921    /// or reading it back walks the enum table with a type id of 0 and a pool
922    /// index of 0.
923    #[test]
924    fn negative_quiet_nan_stays_a_float() {
925        let hardware_nan = f64::from_bits(0xFFF8_0000_0000_0000);
926        assert!(hardware_nan.is_nan());
927
928        let boxed = Data::float(hardware_nan);
929        assert!(boxed.is_float());
930        assert!(!boxed.is_enum());
931        assert!(!boxed.is_struct());
932        assert!(!boxed.is_map());
933        assert!(boxed.as_float().is_nan());
934    }
935
936    #[test]
937    fn every_nan_boxes_to_one_pattern() {
938        let signed = Data::float(f64::from_bits(0xFFF8_0000_0000_0000));
939        let unsigned = Data::float(f64::NAN);
940        let with_payload = Data::float(f64::from_bits(0xFFF8_0000_DEAD_BEEF));
941        assert_eq!(signed, unsigned);
942        assert_eq!(signed, with_payload);
943    }
944
945    #[test]
946    fn infinities_and_ordinary_floats_round_trip() {
947        for value in [
948            0.0,
949            -0.0,
950            1.5,
951            -1.5,
952            f64::MIN,
953            f64::MAX,
954            f64::INFINITY,
955            f64::NEG_INFINITY,
956        ] {
957            let boxed = Data::float(value);
958            assert!(boxed.is_float(), "{value} must box as a float");
959            assert_eq!(boxed.as_float().to_bits(), value.to_bits());
960        }
961    }
962
963    /// The tagged types have to stay distinguishable from every float, which is
964    /// what makes the NaN check above necessary in the first place.
965    #[test]
966    fn tagged_values_are_not_floats() {
967        assert!(!Data::int(7).is_float());
968        assert!(!Data::bool(true).is_float());
969        assert!(!Data::array(0).is_float());
970        assert!(!Data::map(0).is_float());
971        assert!(!Data::enum_instance(0, 0).is_float());
972        assert!(!super::NULL.is_float());
973    }
974}