Skip to main content

shape_vm/executor/
printing.rs

1//! VM-native value formatting
2//!
3//! This module provides native formatting for ValueWord values without converting to
4//! runtime values. It respects TypeSchema for TypedObjects with their field names.
5//!
6//! The primary entry point is `format_nb()` which formats ValueWord values directly
7//! using NanTag/HeapValue dispatch.
8
9use shape_runtime::type_schema::TypeSchemaRegistry;
10use shape_runtime::type_schema::field_types::FieldType;
11use shape_runtime::type_system::annotation_to_string;
12use shape_value::heap_value::HeapValue;
13use shape_value::{NanTag, ValueWord};
14
15/// Formatter for ValueWord values
16///
17/// Uses TypeSchemaRegistry to format TypedObjects with their field names.
18/// Optionally accepts a reference resolver to dereference `&ref` values
19/// (requires VM stack access, so only available during execution).
20pub struct ValueFormatter<'a> {
21    /// Type schema registry for TypedObject field resolution
22    schema_registry: &'a TypeSchemaRegistry,
23    /// Optional callback to resolve reference values to their targets.
24    /// When provided, refs are dereferenced and their underlying values printed.
25    /// When absent, refs display as `<ref>`.
26    deref_fn: Option<&'a dyn Fn(&ValueWord) -> Option<ValueWord>>,
27}
28
29/// Backward-compat alias used by test code.
30#[cfg(test)]
31pub type VMValueFormatter<'a> = ValueFormatter<'a>;
32
33impl<'a> ValueFormatter<'a> {
34    /// Create a new formatter
35    pub fn new(schema_registry: &'a TypeSchemaRegistry) -> Self {
36        Self {
37            schema_registry,
38            deref_fn: None,
39        }
40    }
41
42    /// Create a formatter with a reference resolver.
43    ///
44    /// The resolver is called when a `NanTag::Ref` or `HeapValue::ProjectedRef`
45    /// is encountered, allowing the formatter to print the underlying value
46    /// instead of `<ref>`.
47    pub fn with_deref(
48        schema_registry: &'a TypeSchemaRegistry,
49        deref_fn: &'a dyn Fn(&ValueWord) -> Option<ValueWord>,
50    ) -> Self {
51        Self {
52            schema_registry,
53            deref_fn: Some(deref_fn),
54        }
55    }
56
57    /// Format a ValueWord to string (test-only, delegates to ValueWord path)
58    #[cfg(test)]
59    pub fn format(&self, value: &ValueWord) -> String {
60        let nb = value.clone();
61        self.format_nb_with_depth(&nb, 0)
62    }
63
64    /// Format a ValueWord value to string — primary entry point.
65    ///
66    /// Uses NanTag/HeapValue dispatch for inline types (f64, i48, bool, None,
67    /// Unit) and heap types (String, Array, TypedObject, Decimal, etc.).
68    pub fn format_nb(&self, value: &ValueWord) -> String {
69        self.format_nb_with_depth(value, 0)
70    }
71
72    /// Format a ValueWord value with depth tracking.
73    fn format_nb_with_depth(&self, value: &ValueWord, depth: usize) -> String {
74        if depth > 50 {
75            return "[max depth reached]".to_string();
76        }
77
78        // Fast path: inline types (no heap access needed)
79        match value.tag() {
80            NanTag::F64 => {
81                if let Some(n) = value.as_f64() {
82                    return format_number(n);
83                }
84                // Shouldn't happen, but fallback
85                return "NaN".to_string();
86            }
87            NanTag::I48 => {
88                if let Some(i) = value.as_i64() {
89                    return i.to_string();
90                }
91                return "0".to_string();
92            }
93            NanTag::Bool => {
94                if let Some(b) = value.as_bool() {
95                    return b.to_string();
96                }
97                return "false".to_string();
98            }
99            NanTag::None => return "None".to_string(),
100            NanTag::Unit => return "()".to_string(),
101            NanTag::Function => {
102                if let Some(id) = value.as_function() {
103                    return format!("[Function:{}]", id);
104                }
105                return "[Function]".to_string();
106            }
107            NanTag::ModuleFunction => return "[ModuleFunction]".to_string(),
108            NanTag::Ref => {
109                if let Some(deref) = &self.deref_fn {
110                    if let Some(resolved) = deref(value) {
111                        return self.format_nb_with_depth(&resolved, depth + 1);
112                    }
113                }
114                return "<ref>".to_string();
115            }
116            NanTag::Heap => {}
117        }
118
119        // Heap path: dispatch on HeapValue variant
120        match value.as_heap_ref() {
121            Some(HeapValue::String(s)) => s.as_ref().clone(),
122            Some(HeapValue::Array(arr)) => self.format_nanboxed_array(arr.as_ref(), depth),
123            Some(HeapValue::ProjectedRef(_)) => {
124                if let Some(deref) = &self.deref_fn {
125                    if let Some(resolved) = deref(value) {
126                        return self.format_nb_with_depth(&resolved, depth + 1);
127                    }
128                }
129                "<ref>".to_string()
130            }
131            Some(HeapValue::TypedObject {
132                schema_id,
133                slots,
134                heap_mask,
135            }) => self.format_typed_object(*schema_id as u32, slots, *heap_mask, depth),
136            Some(HeapValue::Decimal(d)) => format!("{}D", d),
137            Some(HeapValue::BigInt(i)) => i.to_string(),
138            Some(HeapValue::Closure { function_id, .. }) => format!("[Closure:{}]", function_id),
139            Some(HeapValue::HostClosure(_)) => "<HostClosure>".to_string(),
140            Some(HeapValue::DataTable(dt)) => format!("{}", dt),
141            Some(HeapValue::TypedTable { table, .. }) => format!("{}", table),
142            Some(HeapValue::RowView { table, row_idx, .. }) => {
143                format!("[Row {} of {} rows]", row_idx, table.row_count())
144            }
145            Some(HeapValue::ColumnRef { table, col_id, .. }) => {
146                let col = table.inner().column(*col_id as usize);
147                let dtype = col.data_type();
148                let type_str = match dtype {
149                    arrow_schema::DataType::Float64 => "f64",
150                    arrow_schema::DataType::Int64 => "i64",
151                    arrow_schema::DataType::Boolean => "bool",
152                    arrow_schema::DataType::Utf8 | arrow_schema::DataType::LargeUtf8 => "string",
153                    _ => "unknown",
154                };
155                let name = table
156                    .column_names()
157                    .get(*col_id as usize)
158                    .cloned()
159                    .unwrap_or_else(|| format!("col_{}", col_id));
160                format!("Column<{}>({}, {} rows)", type_str, name, col.len())
161            }
162            Some(HeapValue::IndexedTable {
163                table, index_col, ..
164            }) => {
165                let col_name = table
166                    .column_names()
167                    .get(*index_col as usize)
168                    .cloned()
169                    .unwrap_or_else(|| format!("col_{}", index_col));
170                format!(
171                    "IndexedTable({} rows, index: {})",
172                    table.row_count(),
173                    col_name
174                )
175            }
176            Some(HeapValue::Range {
177                start,
178                end,
179                inclusive,
180            }) => {
181                let start_str = start
182                    .as_ref()
183                    .map(|s| self.format_nb_with_depth(s, depth + 1))
184                    .unwrap_or_default();
185                let end_str = end
186                    .as_ref()
187                    .map(|e| self.format_nb_with_depth(e, depth + 1))
188                    .unwrap_or_default();
189                let op = if *inclusive { "..=" } else { ".." };
190                format!("{}{}{}", start_str, op, end_str)
191            }
192            Some(HeapValue::Enum(e)) => {
193                use shape_value::enums::EnumPayload;
194                match &e.payload {
195                    EnumPayload::Unit => e.variant.clone(),
196                    EnumPayload::Tuple(values) => {
197                        let parts: Vec<String> = values
198                            .iter()
199                            .map(|v| self.format_nb_with_depth(v, depth + 1))
200                            .collect();
201                        format!("{}({})", e.variant, parts.join(", "))
202                    }
203                    EnumPayload::Struct(fields) => {
204                        let mut parts: Vec<String> = fields
205                            .iter()
206                            .map(|(k, v)| {
207                                format!("{}: {}", k, self.format_nb_with_depth(v, depth + 1))
208                            })
209                            .collect();
210                        parts.sort();
211                        format!("{} {{ {} }}", e.variant, parts.join(", "))
212                    }
213                }
214            }
215            Some(HeapValue::Some(inner)) => {
216                format!("Some({})", self.format_nb_with_depth(inner, depth + 1))
217            }
218            Some(HeapValue::Ok(inner)) => {
219                format!("Ok({})", self.format_nb_with_depth(inner, depth + 1))
220            }
221            Some(HeapValue::Err(inner)) => {
222                format!("Err({})", self.format_nb_with_depth(inner, depth + 1))
223            }
224            Some(HeapValue::Future(id)) => format!("[Future:{}]", id),
225            Some(HeapValue::TaskGroup { kind, task_ids }) => {
226                let kind_str = match kind {
227                    0 => "All",
228                    1 => "Race",
229                    2 => "Any",
230                    3 => "Settle",
231                    _ => "Unknown",
232                };
233                format!("[TaskGroup:{}({})]", kind_str, task_ids.len())
234            }
235            Some(HeapValue::TraitObject { value, .. }) => {
236                self.format_nb_with_depth(value, depth + 1)
237            }
238            Some(HeapValue::ExprProxy(col)) => format!("<ExprProxy:{}>", col),
239            Some(HeapValue::FilterExpr(node)) => format!("<FilterExpr:{:?}>", node),
240            Some(HeapValue::Time(t)) => t.to_rfc3339(),
241            Some(HeapValue::Duration(duration)) => format!("{}{:?}", duration.value, duration.unit),
242            Some(HeapValue::TimeSpan(ts)) => format!("{:?}", ts),
243            Some(HeapValue::Timeframe(tf)) => format!("{:?}", tf),
244            Some(HeapValue::TimeReference(value)) => format!("{:?}", value),
245            Some(HeapValue::DateTimeExpr(value)) => format!("{:?}", value),
246            Some(HeapValue::DataDateTimeRef(value)) => format!("{:?}", value),
247            Some(HeapValue::TypeAnnotation(value)) => annotation_to_string(value),
248            Some(HeapValue::TypeAnnotatedValue { value, .. }) => {
249                self.format_nb_with_depth(value, depth + 1)
250            }
251            Some(HeapValue::PrintResult(p)) => p.rendered.clone(),
252            Some(HeapValue::SimulationCall(_)) => "[SimulationCall]".to_string(),
253            Some(HeapValue::FunctionRef { .. }) => "[FunctionRef]".to_string(),
254            Some(HeapValue::DataReference(_)) => "[DataReference]".to_string(),
255            Some(HeapValue::NativeScalar(v)) => v.to_string(),
256            Some(HeapValue::NativeView(v)) => format!(
257                "<{}:{}@0x{:x}>",
258                if v.mutable { "cmut" } else { "cview" },
259                v.layout.name,
260                v.ptr
261            ),
262            Some(HeapValue::HashMap(d)) => {
263                let mut parts = Vec::new();
264                for (k, v) in d.keys.iter().zip(d.values.iter()) {
265                    parts.push(format!(
266                        "{}: {}",
267                        self.format_nb_with_depth(k, depth + 1),
268                        self.format_nb_with_depth(v, depth + 1)
269                    ));
270                }
271                format!("HashMap{{{}}}", parts.join(", "))
272            }
273            Some(HeapValue::Set(d)) => {
274                let parts: Vec<String> = d
275                    .items
276                    .iter()
277                    .map(|v| self.format_nb_with_depth(v, depth + 1))
278                    .collect();
279                format!("Set{{{}}}", parts.join(", "))
280            }
281            Some(HeapValue::Deque(d)) => {
282                let parts: Vec<String> = d
283                    .items
284                    .iter()
285                    .map(|v| self.format_nb_with_depth(v, depth + 1))
286                    .collect();
287                format!("Deque[{}]", parts.join(", "))
288            }
289            Some(HeapValue::PriorityQueue(d)) => {
290                let parts: Vec<String> = d
291                    .items
292                    .iter()
293                    .map(|v| self.format_nb_with_depth(v, depth + 1))
294                    .collect();
295                format!("PriorityQueue[{}]", parts.join(", "))
296            }
297            Some(HeapValue::Content(node)) => format!("{}", node),
298            Some(HeapValue::Instant(t)) => format!("<instant:{:?}>", t.elapsed()),
299            Some(HeapValue::IoHandle(data)) => {
300                let status = if data.is_open() { "open" } else { "closed" };
301                format!("<io_handle:{}:{}>", data.path, status)
302            }
303            Some(HeapValue::SharedCell(arc)) => {
304                self.format_nb_with_depth(&arc.read().unwrap(), depth)
305            }
306            Some(HeapValue::IntArray(a)) => {
307                let elems: Vec<String> = a.iter().map(|v| v.to_string()).collect();
308                format!("[{}]", elems.join(", "))
309            }
310            Some(HeapValue::FloatArray(a)) => {
311                let elems: Vec<String> = a
312                    .iter()
313                    .map(|v| {
314                        if *v == v.trunc() && v.abs() < 1e15 {
315                            format!("{}.0", *v as i64)
316                        } else {
317                            format!("{}", v)
318                        }
319                    })
320                    .collect();
321                format!("[{}]", elems.join(", "))
322            }
323            Some(HeapValue::FloatArraySlice { parent, offset, len }) => {
324                let off = *offset as usize;
325                let slice_len = *len as usize;
326                let data = &parent.data[off..off + slice_len];
327                let elems: Vec<String> = data
328                    .iter()
329                    .map(|v| {
330                        if *v == v.trunc() && v.abs() < 1e15 {
331                            format!("{}.0", *v as i64)
332                        } else {
333                            format!("{}", v)
334                        }
335                    })
336                    .collect();
337                format!("[{}]", elems.join(", "))
338            }
339            Some(HeapValue::BoolArray(a)) => {
340                let elems: Vec<String> = a
341                    .iter()
342                    .map(|v| if *v != 0 { "true" } else { "false" }.to_string())
343                    .collect();
344                format!("[{}]", elems.join(", "))
345            }
346            Some(HeapValue::I8Array(a)) => {
347                let elems: Vec<String> = a.data.iter().map(|v| v.to_string()).collect();
348                format!("[{}]", elems.join(", "))
349            }
350            Some(HeapValue::I16Array(a)) => {
351                let elems: Vec<String> = a.data.iter().map(|v| v.to_string()).collect();
352                format!("[{}]", elems.join(", "))
353            }
354            Some(HeapValue::I32Array(a)) => {
355                let elems: Vec<String> = a.data.iter().map(|v| v.to_string()).collect();
356                format!("[{}]", elems.join(", "))
357            }
358            Some(HeapValue::U8Array(a)) => {
359                let elems: Vec<String> = a.data.iter().map(|v| v.to_string()).collect();
360                format!("[{}]", elems.join(", "))
361            }
362            Some(HeapValue::U16Array(a)) => {
363                let elems: Vec<String> = a.data.iter().map(|v| v.to_string()).collect();
364                format!("[{}]", elems.join(", "))
365            }
366            Some(HeapValue::U32Array(a)) => {
367                let elems: Vec<String> = a.data.iter().map(|v| v.to_string()).collect();
368                format!("[{}]", elems.join(", "))
369            }
370            Some(HeapValue::U64Array(a)) => {
371                let elems: Vec<String> = a.data.iter().map(|v| v.to_string()).collect();
372                format!("[{}]", elems.join(", "))
373            }
374            Some(HeapValue::F32Array(a)) => {
375                let elems: Vec<String> = a
376                    .data
377                    .iter()
378                    .map(|v| {
379                        if *v == v.trunc() && v.abs() < 1e15 {
380                            format!("{}.0", *v as i64)
381                        } else {
382                            format!("{}", v)
383                        }
384                    })
385                    .collect();
386                format!("[{}]", elems.join(", "))
387            }
388            Some(HeapValue::Matrix(m)) => {
389                format!("<Mat<number>:{}x{}>", m.rows, m.cols)
390            }
391            Some(HeapValue::Iterator(it)) => {
392                format!("<iterator:pos={}>", it.position)
393            }
394            Some(HeapValue::Generator(g)) => {
395                format!("<generator:state={}>", g.state)
396            }
397            Some(HeapValue::Mutex(_)) => "<mutex>".to_string(),
398            Some(HeapValue::Atomic(a)) => {
399                format!(
400                    "<atomic:{}>",
401                    a.inner.load(std::sync::atomic::Ordering::Relaxed)
402                )
403            }
404            Some(HeapValue::Lazy(l)) => {
405                let initialized = l.value.lock().map(|g| g.is_some()).unwrap_or(false);
406                if initialized {
407                    "<lazy:initialized>".to_string()
408                } else {
409                    "<lazy:pending>".to_string()
410                }
411            }
412            Some(HeapValue::Channel(c)) => {
413                if c.is_sender() {
414                    "<channel:sender>".to_string()
415                } else {
416                    "<channel:receiver>".to_string()
417                }
418            }
419            Some(HeapValue::Char(c)) => c.to_string(),
420            None => format!("<unknown:{}>", value.type_name()),
421        }
422    }
423
424    /// Format an array of ValueWord values
425    fn format_nanboxed_array(&self, arr: &[ValueWord], depth: usize) -> String {
426        let elements: Vec<String> = arr
427            .iter()
428            .map(|nb| self.format_nb_with_depth(nb, depth + 1))
429            .collect();
430        format!("[{}]", elements.join(", "))
431    }
432
433    /// Format a TypedObject using its schema
434    fn format_typed_object(
435        &self,
436        schema_id: u32,
437        slots: &[shape_value::ValueSlot],
438        heap_mask: u64,
439        depth: usize,
440    ) -> String {
441        let schema_ref = self.schema_registry.get_by_id(schema_id);
442        let schema = if let Some(s) = schema_ref {
443            s
444        } else {
445            let nb = ValueWord::from_heap_value(HeapValue::TypedObject {
446                schema_id: schema_id as u64,
447                slots: slots.to_vec().into_boxed_slice(),
448                heap_mask,
449            });
450            if let Some(map) = shape_runtime::type_schema::typed_object_to_hashmap_nb(&nb) {
451                let mut fields: Vec<(String, String)> = map
452                    .into_iter()
453                    .map(|(k, v)| (k, self.format_nb_with_depth(&v, depth + 1)))
454                    .collect();
455                fields.sort_by(|a, b| a.0.cmp(&b.0));
456                return format!(
457                    "{{ {} }}",
458                    fields
459                        .into_iter()
460                        .map(|(k, v)| format!("{}: {}", k, v))
461                        .collect::<Vec<_>>()
462                        .join(", ")
463                );
464            }
465            return format!("[TypedObject:{}]", schema_id);
466        };
467
468        // Check if this is an enum type - format specially
469        if let Some(enum_info) = schema.get_enum_info() {
470            return self.format_enum(&schema.name, enum_info, slots, heap_mask, depth);
471        }
472
473        // Default format: { field1: val1, field2: val2, ... }
474        let mut fields = Vec::new();
475
476        for field in &schema.fields {
477            let field_index = field.index as usize;
478            if field_index < slots.len() {
479                let formatted = self.format_slot_value(
480                    slots,
481                    heap_mask,
482                    field_index,
483                    &field.field_type,
484                    depth + 1,
485                );
486                fields.push(format!("{}: {}", field.name, formatted));
487            }
488        }
489
490        if fields.is_empty() {
491            "{}".to_string()
492        } else {
493            format!("{{ {} }}", fields.join(", "))
494        }
495    }
496
497    /// Format an enum value using its variant info.
498    /// Shows only the variant name (not the full `Enum::Variant` path).
499    fn format_enum(
500        &self,
501        _enum_name: &str,
502        enum_info: &shape_runtime::type_schema::EnumInfo,
503        slots: &[shape_value::ValueSlot],
504        heap_mask: u64,
505        depth: usize,
506    ) -> String {
507        // Read variant ID from slot 0
508        if slots.is_empty() {
509            return "?".to_string();
510        }
511
512        let variant_id = slots[0].as_i64() as u16;
513
514        // Look up variant by ID
515        let variant = match enum_info.variant_by_id(variant_id) {
516            Some(v) => v,
517            None => return format!("?[{}]", variant_id),
518        };
519
520        // Unit variant (no payload)
521        if variant.payload_fields == 0 {
522            return variant.name.clone();
523        }
524
525        // Variant with payload - read payload values from slots 1+
526        let mut payload_values = Vec::new();
527        for i in 0..variant.payload_fields {
528            let slot_idx = 1 + i as usize;
529            if slot_idx < slots.len() {
530                payload_values.push(self.format_slot_value(
531                    slots,
532                    heap_mask,
533                    slot_idx,
534                    &FieldType::Any,
535                    depth + 1,
536                ));
537            }
538        }
539
540        if payload_values.is_empty() {
541            variant.name.clone()
542        } else if payload_values.len() == 1 {
543            // Single payload - use parentheses style
544            format!("{}({})", variant.name, payload_values[0])
545        } else {
546            // Multiple payloads - use tuple style with variant name
547            format!("{}({})", variant.name, payload_values.join(", "))
548        }
549    }
550
551    /// Format a TypedObject slot value directly from its ValueSlot.
552    /// Heap slots are converted via `as_heap_nb()` and formatted with `format_nb_with_depth`.
553    /// Non-heap slots are dispatched by field type to read the correct representation.
554    fn format_slot_value(
555        &self,
556        slots: &[shape_value::ValueSlot],
557        heap_mask: u64,
558        index: usize,
559        field_type: &FieldType,
560        depth: usize,
561    ) -> String {
562        if index >= slots.len() {
563            return "None".to_string();
564        }
565        let slot = &slots[index];
566        if heap_mask & (1u64 << index) != 0 {
567            // Heap slot: read as HeapValue, wrap in ValueWord, format directly
568            let nb = slot.as_heap_nb();
569            self.format_nb_with_depth(&nb, depth)
570        } else {
571            // Non-heap: dispatch on field type to read raw bits correctly
572            match field_type {
573                FieldType::I64 | FieldType::Timestamp => slot.as_i64().to_string(),
574                FieldType::Bool => slot.as_bool().to_string(),
575                FieldType::F64 | FieldType::Decimal => format_number(slot.as_f64()),
576                // Any other non-heap type: reconstruct via as_value_word to
577                // preserve inline NanTag variants for correct Display formatting
578                _ => {
579                    let vw = slot.as_value_word(false);
580                    self.format_nb_with_depth(&vw, depth)
581                }
582            }
583        }
584    }
585}
586
587/// Format a number, removing unnecessary decimal places
588fn format_number(n: f64) -> String {
589    if n.is_nan() {
590        "NaN".to_string()
591    } else if n.is_infinite() {
592        if n.is_sign_positive() {
593            "Infinity".to_string()
594        } else {
595            "-Infinity".to_string()
596        }
597    } else if n.fract() == 0.0 && n.abs() < 1e15 {
598        // Integer-like floats: always show .0 to distinguish from int
599        format!("{}.0", n as i64)
600    } else {
601        // Use default formatting
602        n.to_string()
603    }
604}
605
606#[cfg(test)]
607mod tests {
608    use super::*;
609    use std::sync::Arc;
610
611    fn create_test_registry() -> TypeSchemaRegistry {
612        TypeSchemaRegistry::new()
613    }
614
615    fn predeclared_object(fields: &[(&str, ValueWord)]) -> ValueWord {
616        let field_names: Vec<String> = fields.iter().map(|(name, _)| (*name).to_string()).collect();
617        let _ = shape_runtime::type_schema::register_predeclared_any_schema(&field_names);
618        shape_runtime::type_schema::typed_object_from_pairs(fields)
619    }
620
621    #[test]
622    fn test_format_primitives() {
623        let schema_reg = create_test_registry();
624        let formatter = VMValueFormatter::new(&schema_reg);
625
626        assert_eq!(formatter.format(&ValueWord::from_f64(42.0)), "42.0");
627        assert_eq!(formatter.format(&ValueWord::from_f64(3.14)), "3.14");
628        assert_eq!(
629            formatter.format(&ValueWord::from_string(Arc::new("hello".to_string()))),
630            "hello"
631        );
632        assert_eq!(formatter.format(&ValueWord::from_bool(true)), "true");
633        assert_eq!(formatter.format(&ValueWord::none()), "None");
634        assert_eq!(formatter.format(&ValueWord::unit()), "()");
635    }
636
637    #[test]
638    fn test_format_array() {
639        let schema_reg = create_test_registry();
640        let formatter = VMValueFormatter::new(&schema_reg);
641
642        let arr = ValueWord::from_array(Arc::new(vec![
643            ValueWord::from_f64(1.0),
644            ValueWord::from_f64(2.0),
645            ValueWord::from_f64(3.0),
646        ]));
647        assert_eq!(formatter.format(&arr), "[1.0, 2.0, 3.0]");
648    }
649
650    #[test]
651    fn test_format_object() {
652        let schema_reg = create_test_registry();
653        let formatter = VMValueFormatter::new(&schema_reg);
654
655        let value = predeclared_object(&[
656            ("x", ValueWord::from_f64(1.0)),
657            ("y", ValueWord::from_f64(2.0)),
658        ]);
659
660        let formatted = formatter.format(&value);
661        // TypedObject fields come from schema order
662        assert!(formatted.contains("x: 1.0"));
663        assert!(formatted.contains("y: 2.0"));
664    }
665
666    #[test]
667    fn test_format_number_integers() {
668        assert_eq!(format_number(42.0), "42.0");
669        assert_eq!(format_number(-100.0), "-100.0");
670        assert_eq!(format_number(0.0), "0.0");
671    }
672
673    #[test]
674    fn test_format_number_decimals() {
675        assert_eq!(format_number(3.14), "3.14");
676        assert_eq!(format_number(-2.5), "-2.5");
677    }
678
679    #[test]
680    fn test_format_number_special() {
681        assert_eq!(format_number(f64::NAN), "NaN");
682        assert_eq!(format_number(f64::INFINITY), "Infinity");
683        assert_eq!(format_number(f64::NEG_INFINITY), "-Infinity");
684    }
685
686    #[test]
687    fn test_format_decimal() {
688        let schema_reg = create_test_registry();
689        let formatter = VMValueFormatter::new(&schema_reg);
690
691        let d = ValueWord::from_decimal(rust_decimal::Decimal::from(42));
692        assert_eq!(formatter.format(&d), "42D");
693
694        let d2 = ValueWord::from_decimal(rust_decimal::Decimal::new(314, 2)); // 3.14 exactly
695        assert_eq!(formatter.format(&d2), "3.14D");
696    }
697
698    #[test]
699    fn test_format_int() {
700        let schema_reg = create_test_registry();
701        let formatter = VMValueFormatter::new(&schema_reg);
702
703        assert_eq!(formatter.format(&ValueWord::from_i64(42)), "42");
704        assert_eq!(formatter.format(&ValueWord::from_i64(-100)), "-100");
705        assert_eq!(formatter.format(&ValueWord::from_i64(0)), "0");
706    }
707
708    // ===== ValueWord format_nb tests =====
709
710    #[test]
711    fn test_format_nb_primitives() {
712        let schema_reg = create_test_registry();
713        let formatter = VMValueFormatter::new(&schema_reg);
714
715        assert_eq!(formatter.format_nb(&ValueWord::from_f64(42.0)), "42.0");
716        assert_eq!(formatter.format_nb(&ValueWord::from_f64(3.14)), "3.14");
717        assert_eq!(
718            formatter.format_nb(&ValueWord::from_string(Arc::new("hello".to_string()))),
719            "hello"
720        );
721        assert_eq!(formatter.format_nb(&ValueWord::from_bool(true)), "true");
722        assert_eq!(formatter.format_nb(&ValueWord::from_bool(false)), "false");
723        assert_eq!(formatter.format_nb(&ValueWord::none()), "None");
724        assert_eq!(formatter.format_nb(&ValueWord::unit()), "()");
725    }
726
727    #[test]
728    fn test_format_nb_integers() {
729        let schema_reg = create_test_registry();
730        let formatter = VMValueFormatter::new(&schema_reg);
731
732        assert_eq!(formatter.format_nb(&ValueWord::from_i64(42)), "42");
733        assert_eq!(formatter.format_nb(&ValueWord::from_i64(-100)), "-100");
734        assert_eq!(formatter.format_nb(&ValueWord::from_i64(0)), "0");
735    }
736
737    #[test]
738    fn test_format_nb_decimal() {
739        let schema_reg = create_test_registry();
740        let formatter = VMValueFormatter::new(&schema_reg);
741
742        assert_eq!(
743            formatter.format_nb(&ValueWord::from_decimal(rust_decimal::Decimal::from(42))),
744            "42D"
745        );
746        assert_eq!(
747            formatter.format_nb(&ValueWord::from_decimal(rust_decimal::Decimal::new(314, 2))),
748            "3.14D"
749        );
750    }
751
752    #[test]
753    fn test_format_nb_array() {
754        let schema_reg = create_test_registry();
755        let formatter = VMValueFormatter::new(&schema_reg);
756
757        let arr = ValueWord::from_array(Arc::new(vec![
758            ValueWord::from_f64(1.0),
759            ValueWord::from_f64(2.0),
760            ValueWord::from_f64(3.0),
761        ]));
762        assert_eq!(formatter.format_nb(&arr), "[1.0, 2.0, 3.0]");
763    }
764
765    #[test]
766    fn test_format_nb_mixed_array() {
767        let schema_reg = create_test_registry();
768        let formatter = VMValueFormatter::new(&schema_reg);
769
770        let arr = ValueWord::from_array(Arc::new(vec![
771            ValueWord::from_i64(1),
772            ValueWord::from_string(Arc::new("two".to_string())),
773            ValueWord::from_bool(true),
774        ]));
775        assert_eq!(formatter.format_nb(&arr), "[1, two, true]");
776    }
777
778    #[test]
779    fn test_format_nb_object() {
780        let schema_reg = create_test_registry();
781        let formatter = VMValueFormatter::new(&schema_reg);
782
783        let value = predeclared_object(&[
784            ("x", ValueWord::from_f64(1.0)),
785            ("y", ValueWord::from_f64(2.0)),
786        ]);
787        let nb = value;
788
789        let formatted = formatter.format_nb(&nb);
790        assert!(formatted.contains("x: 1.0"));
791        assert!(formatted.contains("y: 2.0"));
792    }
793
794    #[test]
795    fn test_format_nb_special_numbers() {
796        let schema_reg = create_test_registry();
797        let formatter = VMValueFormatter::new(&schema_reg);
798
799        assert_eq!(formatter.format_nb(&ValueWord::from_f64(f64::NAN)), "NaN");
800        assert_eq!(
801            formatter.format_nb(&ValueWord::from_f64(f64::INFINITY)),
802            "Infinity"
803        );
804        assert_eq!(
805            formatter.format_nb(&ValueWord::from_f64(f64::NEG_INFINITY)),
806            "-Infinity"
807        );
808    }
809
810    #[test]
811    fn test_format_nb_result_types() {
812        let schema_reg = create_test_registry();
813        let formatter = VMValueFormatter::new(&schema_reg);
814
815        assert_eq!(
816            formatter.format_nb(&ValueWord::from_ok(ValueWord::from_i64(42))),
817            "Ok(42)"
818        );
819        assert_eq!(
820            formatter.format_nb(&ValueWord::from_err(ValueWord::from_string(Arc::new(
821                "fail".to_string()
822            )))),
823            "Err(fail)"
824        );
825        assert_eq!(
826            formatter.format_nb(&ValueWord::from_some(ValueWord::from_f64(3.14))),
827            "Some(3.14)"
828        );
829    }
830
831    #[test]
832    fn test_format_nb_consistency_with_vmvalue() {
833        // Verify that format_nb produces the same output as format for common types
834        let schema_reg = create_test_registry();
835        let formatter = VMValueFormatter::new(&schema_reg);
836
837        let test_cases: Vec<ValueWord> = vec![
838            ValueWord::from_f64(42.0),
839            ValueWord::from_f64(3.14),
840            ValueWord::from_i64(99),
841            ValueWord::from_bool(true),
842            ValueWord::none(),
843            ValueWord::unit(),
844            ValueWord::from_string(Arc::new("test".to_string())),
845        ];
846
847        for val in &test_cases {
848            assert_eq!(
849                formatter.format(val),
850                formatter.format_nb(val),
851                "Mismatch for ValueWord: {:?}",
852                val
853            );
854        }
855    }
856
857    // ===== LOW-1: Float display always shows .0 =====
858
859    #[test]
860    fn test_float_display_shows_decimal_point() {
861        let schema_reg = create_test_registry();
862        let formatter = VMValueFormatter::new(&schema_reg);
863
864        // Integer-like floats must show .0
865        assert_eq!(formatter.format_nb(&ValueWord::from_f64(1.0)), "1.0");
866        assert_eq!(formatter.format_nb(&ValueWord::from_f64(0.0)), "0.0");
867        assert_eq!(formatter.format_nb(&ValueWord::from_f64(-5.0)), "-5.0");
868        assert_eq!(formatter.format_nb(&ValueWord::from_f64(100.0)), "100.0");
869
870        // Non-integer floats show normally
871        assert_eq!(formatter.format_nb(&ValueWord::from_f64(1.5)), "1.5");
872        assert_eq!(formatter.format_nb(&ValueWord::from_f64(0.1)), "0.1");
873
874        // Integers (i48) should NOT show .0
875        assert_eq!(formatter.format_nb(&ValueWord::from_i64(1)), "1");
876        assert_eq!(formatter.format_nb(&ValueWord::from_i64(0)), "0");
877        assert_eq!(formatter.format_nb(&ValueWord::from_i64(-5)), "-5");
878    }
879
880    // ===== LOW-5: Enum display shows variant name only =====
881
882    #[test]
883    fn test_enum_display_variant_only() {
884        let schema_reg = create_test_registry();
885        let formatter = VMValueFormatter::new(&schema_reg);
886
887        // Unit variant
888        let e = ValueWord::from_enum(shape_value::EnumValue {
889            enum_name: "Direction".to_string(),
890            variant: "North".to_string(),
891            payload: shape_value::enums::EnumPayload::Unit,
892        });
893        assert_eq!(formatter.format_nb(&e), "North");
894
895        // Tuple variant
896        let e = ValueWord::from_enum(shape_value::EnumValue {
897            enum_name: "Shape".to_string(),
898            variant: "Circle".to_string(),
899            payload: shape_value::enums::EnumPayload::Tuple(vec![ValueWord::from_f64(5.0)]),
900        });
901        assert_eq!(formatter.format_nb(&e), "Circle(5.0)");
902    }
903
904    // ===== LOW-9: References show <ref> (or dereferenced value via VM) =====
905
906    #[test]
907    fn test_ref_display_without_resolver() {
908        let schema_reg = create_test_registry();
909        let formatter = VMValueFormatter::new(&schema_reg);
910
911        // Without a resolver, inline stack refs show <ref>
912        let ref_val = ValueWord::from_ref(42);
913        let formatted = formatter.format_nb(&ref_val);
914        assert_eq!(formatted, "<ref>");
915    }
916
917    #[test]
918    fn test_ref_display_with_resolver() {
919        let schema_reg = create_test_registry();
920        // Resolver that returns a concrete value for any ref
921        let resolver = |_v: &ValueWord| -> Option<ValueWord> {
922            Some(ValueWord::from_i64(99))
923        };
924        let formatter = ValueFormatter::with_deref(&schema_reg, &resolver);
925
926        let ref_val = ValueWord::from_ref(42);
927        let formatted = formatter.format_nb(&ref_val);
928        assert_eq!(formatted, "99");
929    }
930}