Skip to main content

shape_value/
external_value.rs

1//! ExternalValue: a serde-serializable value for display, wire, and debug.
2//!
3//! ExternalValue is the canonical format for values that cross system boundaries:
4//! - Wire serialization (JSON, MessagePack, etc.)
5//! - Debugger display
6//! - REPL output
7//! - Remote protocol
8//!
9//! It contains NO function refs, closures, raw pointers, or VM internals.
10//! All variants are safe to serialize with serde.
11
12use crate::heap_value::HeapValue;
13use crate::tags;
14use crate::value_word::ValueWord;
15use std::collections::BTreeMap;
16use std::fmt;
17
18/// A serde-serializable value with no VM internals.
19///
20/// This is the "external" representation of a Shape value, suitable for
21/// display, wire serialization, and debugging. It contains no function
22/// references, closures, or raw pointers.
23#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
24#[serde(tag = "type", content = "value")]
25pub enum ExternalValue {
26    /// 64-bit float
27    Number(f64),
28    /// 64-bit signed integer
29    Int(i64),
30    /// Boolean
31    Bool(bool),
32    /// String
33    String(String),
34    /// None / null
35    None,
36    /// Unit (void)
37    Unit,
38    /// Homogeneous or heterogeneous array
39    Array(Vec<ExternalValue>),
40    /// Untyped object (field name -> value)
41    Object(BTreeMap<String, ExternalValue>),
42    /// Typed object with schema name
43    TypedObject {
44        name: String,
45        fields: BTreeMap<String, ExternalValue>,
46    },
47    /// Enum variant
48    Enum {
49        name: String,
50        variant: String,
51        data: Box<ExternalValue>,
52    },
53    /// Duration
54    Duration { secs: u64, nanos: u32 },
55    /// Timestamp (ISO 8601 string for portability)
56    Time(String),
57    /// Decimal (string representation for precision)
58    Decimal(String),
59    /// Error message
60    Error(String),
61    /// Result::Ok
62    Ok(Box<ExternalValue>),
63    /// Range
64    Range {
65        start: Option<Box<ExternalValue>>,
66        end: Option<Box<ExternalValue>>,
67        inclusive: bool,
68    },
69    /// DataTable summary (not full data — just metadata)
70    DataTable { rows: usize, columns: Vec<String> },
71    /// Opaque value that cannot be externalized (function, closure, etc.)
72    Opaque(String),
73}
74
75/// Trait for looking up schema metadata from a schema_id.
76///
77/// Implemented by `TypeSchemaRegistry` in shape-runtime.
78/// This abstraction lets shape-value convert TypedObjects to ExternalValue
79/// without depending on shape-runtime.
80pub trait SchemaLookup {
81    /// Get the type name for a schema_id, or None if unknown.
82    fn type_name(&self, schema_id: u64) -> Option<&str>;
83
84    /// Get ordered field names for a schema_id, or None if unknown.
85    fn field_names(&self, schema_id: u64) -> Option<Vec<&str>>;
86}
87
88/// A no-op schema lookup for contexts where schema info is unavailable.
89/// TypedObjects will be converted with placeholder names.
90pub struct NoSchemaLookup;
91
92impl SchemaLookup for NoSchemaLookup {
93    fn type_name(&self, _schema_id: u64) -> Option<&str> {
94        std::option::Option::None
95    }
96    fn field_names(&self, _schema_id: u64) -> Option<Vec<&str>> {
97        std::option::Option::None
98    }
99}
100
101/// Convert a ValueWord value to an ExternalValue.
102///
103/// This is the canonical way to externalize a VM value for display, wire, or debug.
104/// Schema lookup is needed to resolve TypedObject field names.
105pub fn nb_to_external(nb: &ValueWord, schemas: &dyn SchemaLookup) -> ExternalValue {
106    let bits = nb.raw_bits();
107
108    if !tags::is_tagged(bits) {
109        let f = f64::from_bits(bits);
110        return if f.is_nan() {
111            ExternalValue::Number(f64::NAN)
112        } else {
113            ExternalValue::Number(f)
114        };
115    }
116
117    match tags::get_tag(bits) {
118        tags::TAG_INT => ExternalValue::Int(tags::sign_extend_i48(tags::get_payload(bits))),
119        tags::TAG_BOOL => ExternalValue::Bool(tags::get_payload(bits) != 0),
120        tags::TAG_NONE => ExternalValue::None,
121        tags::TAG_UNIT => ExternalValue::Unit,
122        tags::TAG_FUNCTION => {
123            ExternalValue::Opaque(format!("<function:{}>", tags::get_payload(bits) as u16))
124        }
125        tags::TAG_MODULE_FN => {
126            ExternalValue::Opaque(format!("<module_fn:{}>", tags::get_payload(bits) as u32))
127        }
128        tags::TAG_REF => ExternalValue::Opaque("<ref>".to_string()),
129        tags::TAG_HEAP => {
130            if let Some(hv) = nb.as_heap_ref() {
131                heap_to_external(hv, schemas)
132            } else {
133                ExternalValue::Opaque("<invalid_heap>".to_string())
134            }
135        }
136        _ => ExternalValue::Opaque("<unknown_tag>".to_string()),
137    }
138}
139
140/// Convert a HeapValue to an ExternalValue.
141fn heap_to_external(hv: &HeapValue, schemas: &dyn SchemaLookup) -> ExternalValue {
142    match hv {
143        HeapValue::String(s) => ExternalValue::String((**s).clone()),
144        HeapValue::Array(arr) => {
145            let items: Vec<ExternalValue> =
146                arr.iter().map(|v| nb_to_external(v, schemas)).collect();
147            ExternalValue::Array(items)
148        }
149        HeapValue::TypedObject {
150            schema_id,
151            slots,
152            heap_mask,
153        } => {
154            let type_name = schemas
155                .type_name(*schema_id)
156                .unwrap_or("unknown")
157                .to_string();
158            let field_names_opt = schemas.field_names(*schema_id);
159
160            let mut fields = BTreeMap::new();
161            let names: Vec<String> = if let Some(names) = field_names_opt {
162                names.iter().map(|s| s.to_string()).collect()
163            } else {
164                (0..slots.len()).map(|i| format!("_{i}")).collect()
165            };
166
167            for (i, name) in names.into_iter().enumerate() {
168                if i >= slots.len() {
169                    break;
170                }
171                let is_heap = (heap_mask >> i) & 1 == 1;
172                let ev = if is_heap {
173                    // Heap slot: convert via HeapValue
174                    let nb_val = slots[i].as_heap_nb();
175                    nb_to_external(&nb_val, schemas)
176                } else {
177                    // Non-heap slot: raw bits, interpret as f64 (most common for non-heap fields)
178                    ExternalValue::Number(slots[i].as_f64())
179                };
180                fields.insert(name, ev);
181            }
182
183            ExternalValue::TypedObject {
184                name: type_name,
185                fields,
186            }
187        }
188        HeapValue::Closure { function_id, .. } => {
189            ExternalValue::Opaque(format!("<closure:{function_id}>"))
190        }
191        HeapValue::Decimal(d) => ExternalValue::Decimal(d.to_string()),
192        HeapValue::BigInt(i) => ExternalValue::Int(*i),
193        HeapValue::HostClosure(_) => ExternalValue::Opaque("<host_closure>".to_string()),
194
195        // DataTable family
196        HeapValue::DataTable(dt) => ExternalValue::DataTable {
197            rows: dt.row_count(),
198            columns: dt.column_names().iter().map(|s| s.to_string()).collect(),
199        },
200        HeapValue::TypedTable { table, .. } => ExternalValue::DataTable {
201            rows: table.row_count(),
202            columns: table.column_names().iter().map(|s| s.to_string()).collect(),
203        },
204        HeapValue::RowView { .. } => ExternalValue::Opaque("<row_view>".to_string()),
205        HeapValue::ColumnRef { .. } => ExternalValue::Opaque("<column_ref>".to_string()),
206        HeapValue::IndexedTable { table, .. } => ExternalValue::DataTable {
207            rows: table.row_count(),
208            columns: table.column_names().iter().map(|s| s.to_string()).collect(),
209        },
210
211        // Container types
212        HeapValue::Range {
213            start,
214            end,
215            inclusive,
216        } => ExternalValue::Range {
217            start: start.as_ref().map(|v| Box::new(nb_to_external(v, schemas))),
218            end: end.as_ref().map(|v| Box::new(nb_to_external(v, schemas))),
219            inclusive: *inclusive,
220        },
221        HeapValue::Enum(e) => ExternalValue::Enum {
222            name: e.enum_name.clone(),
223            variant: e.variant.clone(),
224            data: Box::new(match &e.payload {
225                crate::enums::EnumPayload::Unit => ExternalValue::None,
226                crate::enums::EnumPayload::Tuple(nbs) => {
227                    if nbs.len() == 1 {
228                        nb_to_external(&nbs[0], schemas)
229                    } else {
230                        ExternalValue::Array(
231                            nbs.iter().map(|v| nb_to_external(v, schemas)).collect(),
232                        )
233                    }
234                }
235                crate::enums::EnumPayload::Struct(fields) => {
236                    let mut map = BTreeMap::new();
237                    for (k, v) in fields {
238                        map.insert(k.clone(), nb_to_external(v, schemas));
239                    }
240                    ExternalValue::Object(map)
241                }
242            }),
243        },
244        HeapValue::Some(v) => nb_to_external(v, schemas),
245        HeapValue::Ok(v) => ExternalValue::Ok(Box::new(nb_to_external(v, schemas))),
246        HeapValue::Err(v) => ExternalValue::Error(format!("{:?}", nb_to_external(v, schemas))),
247
248        // Async
249        HeapValue::Future(id) => ExternalValue::Opaque(format!("<future:{id}>")),
250        HeapValue::TaskGroup { kind, task_ids } => {
251            ExternalValue::Opaque(format!("<task_group:kind={kind},tasks={}>", task_ids.len()))
252        }
253
254        // Trait dispatch
255        HeapValue::TraitObject { value, .. } => nb_to_external(value, schemas),
256
257        // SQL pushdown
258        HeapValue::ExprProxy(s) => ExternalValue::Opaque(format!("<expr_proxy:{s}>")),
259        HeapValue::FilterExpr(_) => ExternalValue::Opaque("<filter_expr>".to_string()),
260
261        // Time types
262        HeapValue::Time(t) => ExternalValue::Time(t.to_rfc3339()),
263        HeapValue::Duration(d) => {
264            let secs = d.value as u64;
265            ExternalValue::Duration { secs, nanos: 0 }
266        }
267        HeapValue::TimeSpan(ts) => ExternalValue::Duration {
268            secs: ts.num_seconds().unsigned_abs(),
269            nanos: (ts.subsec_nanos().unsigned_abs()),
270        },
271        HeapValue::Timeframe(tf) => ExternalValue::String(format!("{tf:?}")),
272
273        // AST types
274        HeapValue::TimeReference(_) => ExternalValue::Opaque("<time_reference>".to_string()),
275        HeapValue::DateTimeExpr(_) => ExternalValue::Opaque("<datetime_expr>".to_string()),
276        HeapValue::DataDateTimeRef(_) => ExternalValue::Opaque("<data_datetime_ref>".to_string()),
277        HeapValue::TypeAnnotation(_) => ExternalValue::Opaque("<type_annotation>".to_string()),
278        HeapValue::TypeAnnotatedValue { type_name, value } => {
279            let inner = nb_to_external(value, schemas);
280            ExternalValue::TypedObject {
281                name: type_name.clone(),
282                fields: BTreeMap::from([("value".to_string(), inner)]),
283            }
284        }
285        HeapValue::PrintResult(pr) => ExternalValue::String(pr.rendered.clone()),
286        HeapValue::SimulationCall(data) => ExternalValue::Opaque(format!(
287            "<simulation_call:{} params={}>",
288            data.name,
289            data.params.len()
290        )),
291        HeapValue::FunctionRef { name, .. } => {
292            ExternalValue::Opaque(format!("<function_ref:{name}>"))
293        }
294        HeapValue::DataReference(data) => {
295            let mut fields = BTreeMap::new();
296            fields.insert(
297                "datetime".to_string(),
298                ExternalValue::Time(data.datetime.to_rfc3339()),
299            );
300            fields.insert("id".to_string(), ExternalValue::String(data.id.clone()));
301            fields.insert(
302                "timeframe".to_string(),
303                ExternalValue::String(format!("{:?}", data.timeframe)),
304            );
305            ExternalValue::Object(fields)
306        }
307
308        HeapValue::Instant(t) => ExternalValue::Opaque(format!("<instant:{:?}>", t.elapsed())),
309
310        HeapValue::IoHandle(data) => {
311            let status = if data.is_open() { "open" } else { "closed" };
312            ExternalValue::Opaque(format!("<io_handle:{}:{}>", data.path, status))
313        }
314
315        HeapValue::NativeScalar(v) => {
316            if let Some(i) = v.as_i64() {
317                ExternalValue::Int(i)
318            } else {
319                ExternalValue::Number(v.as_f64())
320            }
321        }
322        HeapValue::NativeView(v) => ExternalValue::Opaque(format!(
323            "<{}:{}@0x{:x}>",
324            if v.mutable { "cmut" } else { "cview" },
325            v.layout.name,
326            v.ptr
327        )),
328        HeapValue::HashMap(d) => {
329            let mut fields = BTreeMap::new();
330            for (k, v) in d.keys.iter().zip(d.values.iter()) {
331                fields.insert(format!("{}", k), nb_to_external(v, schemas));
332            }
333            ExternalValue::Object(fields)
334        }
335        HeapValue::Set(d) => {
336            ExternalValue::Array(d.items.iter().map(|v| nb_to_external(v, schemas)).collect())
337        }
338        HeapValue::Deque(d) => {
339            ExternalValue::Array(d.items.iter().map(|v| nb_to_external(v, schemas)).collect())
340        }
341        HeapValue::PriorityQueue(d) => {
342            ExternalValue::Array(d.items.iter().map(|v| nb_to_external(v, schemas)).collect())
343        }
344        HeapValue::Content(node) => ExternalValue::String(format!("{}", node)),
345        HeapValue::SharedCell(arc) => nb_to_external(&arc.read().unwrap(), schemas),
346        HeapValue::IntArray(a) => {
347            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v)).collect())
348        }
349        HeapValue::FloatArray(a) => {
350            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Number(v)).collect())
351        }
352        HeapValue::BoolArray(a) => {
353            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Bool(v != 0)).collect())
354        }
355        HeapValue::I8Array(a) => {
356            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
357        }
358        HeapValue::I16Array(a) => {
359            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
360        }
361        HeapValue::I32Array(a) => {
362            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
363        }
364        HeapValue::U8Array(a) => {
365            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
366        }
367        HeapValue::U16Array(a) => {
368            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
369        }
370        HeapValue::U32Array(a) => {
371            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
372        }
373        HeapValue::U64Array(a) => {
374            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
375        }
376        HeapValue::F32Array(a) => {
377            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Number(v as f64)).collect())
378        }
379        HeapValue::Matrix(m) => {
380            ExternalValue::Opaque(format!("<Mat<number>:{}x{}>", m.rows, m.cols))
381        }
382        HeapValue::Iterator(_) => ExternalValue::Opaque("<iterator>".to_string()),
383        HeapValue::Generator(_) => ExternalValue::Opaque("<generator>".to_string()),
384        HeapValue::Mutex(_) => ExternalValue::Opaque("<mutex>".to_string()),
385        HeapValue::Atomic(a) => {
386            ExternalValue::Int(a.inner.load(std::sync::atomic::Ordering::Relaxed))
387        }
388        HeapValue::Channel(c) => {
389            if c.is_sender() {
390                ExternalValue::Opaque("<channel:sender>".to_string())
391            } else {
392                ExternalValue::Opaque("<channel:receiver>".to_string())
393            }
394        }
395        HeapValue::Lazy(l) => {
396            if let Ok(guard) = l.value.lock() {
397                if let Some(val) = guard.as_ref() {
398                    return nb_to_external(val, schemas);
399                }
400            }
401            ExternalValue::Opaque("<lazy:uninitialized>".to_string())
402        }
403    }
404}
405
406/// Convert an ExternalValue back to a ValueWord value.
407///
408/// This is used for deserializing wire values back into the VM.
409/// Note: Opaque values cannot be round-tripped.
410pub fn external_to_nb(ev: &ExternalValue, schemas: &dyn SchemaLookup) -> ValueWord {
411    let _ = schemas; // schemas needed for TypedObject reconstruction (future use)
412    match ev {
413        ExternalValue::Number(n) => ValueWord::from_f64(*n),
414        ExternalValue::Int(i) => ValueWord::from_i64(*i),
415        ExternalValue::Bool(b) => ValueWord::from_bool(*b),
416        ExternalValue::String(s) => ValueWord::from_string(std::sync::Arc::new(s.clone())),
417        ExternalValue::None => ValueWord::none(),
418        ExternalValue::Unit => ValueWord::unit(),
419        ExternalValue::Array(items) => {
420            let nbs: Vec<ValueWord> = items.iter().map(|v| external_to_nb(v, schemas)).collect();
421            ValueWord::from_array(std::sync::Arc::new(nbs))
422        }
423        ExternalValue::Decimal(s) => {
424            if let Ok(d) = s.parse::<rust_decimal::Decimal>() {
425                ValueWord::from_decimal(d)
426            } else {
427                ValueWord::from_string(std::sync::Arc::new(s.clone()))
428            }
429        }
430        ExternalValue::Ok(inner) => ValueWord::from_ok(external_to_nb(inner, schemas)),
431        ExternalValue::Error(msg) => {
432            ValueWord::from_err(ValueWord::from_string(std::sync::Arc::new(msg.clone())))
433        }
434        ExternalValue::Range {
435            start,
436            end,
437            inclusive,
438        } => ValueWord::from_range(
439            start.as_ref().map(|v| external_to_nb(v, schemas)),
440            end.as_ref().map(|v| external_to_nb(v, schemas)),
441            *inclusive,
442        ),
443        // Complex types that can't be fully round-tripped return None
444        ExternalValue::Object(_)
445        | ExternalValue::TypedObject { .. }
446        | ExternalValue::Enum { .. }
447        | ExternalValue::Duration { .. }
448        | ExternalValue::Time(_)
449        | ExternalValue::DataTable { .. }
450        | ExternalValue::Opaque(_) => ValueWord::none(),
451    }
452}
453
454impl fmt::Display for ExternalValue {
455    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
456        match self {
457            ExternalValue::Number(n) => {
458                if n.is_nan() {
459                    write!(f, "NaN")
460                } else if n.is_infinite() {
461                    if n.is_sign_positive() {
462                        write!(f, "Infinity")
463                    } else {
464                        write!(f, "-Infinity")
465                    }
466                } else if *n == (*n as i64) as f64 && n.is_finite() {
467                    // Display whole numbers without decimal point
468                    write!(f, "{}", *n as i64)
469                } else {
470                    write!(f, "{n}")
471                }
472            }
473            ExternalValue::Int(i) => write!(f, "{i}"),
474            ExternalValue::Bool(b) => write!(f, "{b}"),
475            ExternalValue::String(s) => write!(f, "{s}"),
476            ExternalValue::None => write!(f, "none"),
477            ExternalValue::Unit => write!(f, "()"),
478            ExternalValue::Array(items) => {
479                write!(f, "[")?;
480                for (i, item) in items.iter().enumerate() {
481                    if i > 0 {
482                        write!(f, ", ")?;
483                    }
484                    write!(f, "{item}")?;
485                }
486                write!(f, "]")
487            }
488            ExternalValue::Object(fields) => {
489                write!(f, "{{")?;
490                for (i, (k, v)) in fields.iter().enumerate() {
491                    if i > 0 {
492                        write!(f, ", ")?;
493                    }
494                    write!(f, "{k}: {v}")?;
495                }
496                write!(f, "}}")
497            }
498            ExternalValue::TypedObject { name, fields } => {
499                write!(f, "{name} {{")?;
500                for (i, (k, v)) in fields.iter().enumerate() {
501                    if i > 0 {
502                        write!(f, ", ")?;
503                    }
504                    write!(f, "{k}: {v}")?;
505                }
506                write!(f, "}}")
507            }
508            ExternalValue::Enum {
509                name,
510                variant,
511                data,
512            } => {
513                write!(f, "{name}::{variant}")?;
514                if **data != ExternalValue::None {
515                    write!(f, "({data})")?;
516                }
517                Ok(())
518            }
519            ExternalValue::Duration { secs, nanos } => {
520                if *nanos > 0 {
521                    write!(f, "{secs}.{:09}s", nanos)
522                } else {
523                    write!(f, "{secs}s")
524                }
525            }
526            ExternalValue::Time(iso) => write!(f, "{iso}"),
527            ExternalValue::Decimal(d) => write!(f, "{d}"),
528            ExternalValue::Error(msg) => write!(f, "Error({msg})"),
529            ExternalValue::Ok(inner) => write!(f, "Ok({inner})"),
530            ExternalValue::Range {
531                start,
532                end,
533                inclusive,
534            } => {
535                if let Some(s) = start {
536                    write!(f, "{s}")?;
537                }
538                if *inclusive {
539                    write!(f, "..=")?;
540                } else {
541                    write!(f, "..")?;
542                }
543                if let Some(e) = end {
544                    write!(f, "{e}")?;
545                }
546                Ok(())
547            }
548            ExternalValue::DataTable { rows, columns } => {
549                write!(f, "DataTable({rows} rows, {} cols)", columns.len())
550            }
551            ExternalValue::Opaque(desc) => write!(f, "{desc}"),
552        }
553    }
554}
555
556#[cfg(test)]
557mod tests {
558    use super::*;
559    use crate::value_word::ValueWord;
560
561    #[test]
562    fn test_number_roundtrip() {
563        let nb = ValueWord::from_f64(3.14);
564        let ev = nb_to_external(&nb, &NoSchemaLookup);
565        assert!(matches!(ev, ExternalValue::Number(n) if (n - 3.14).abs() < f64::EPSILON));
566        let back = external_to_nb(&ev, &NoSchemaLookup);
567        assert!((back.as_f64().unwrap() - 3.14).abs() < f64::EPSILON);
568    }
569
570    #[test]
571    fn test_int_roundtrip() {
572        let nb = ValueWord::from_i64(42);
573        let ev = nb_to_external(&nb, &NoSchemaLookup);
574        assert_eq!(ev, ExternalValue::Int(42));
575        let back = external_to_nb(&ev, &NoSchemaLookup);
576        assert_eq!(back.as_i64().unwrap(), 42);
577    }
578
579    #[test]
580    fn test_bool_roundtrip() {
581        let nb = ValueWord::from_bool(true);
582        let ev = nb_to_external(&nb, &NoSchemaLookup);
583        assert_eq!(ev, ExternalValue::Bool(true));
584    }
585
586    #[test]
587    fn test_string_roundtrip() {
588        let nb = ValueWord::from_string(std::sync::Arc::new("hello".to_string()));
589        let ev = nb_to_external(&nb, &NoSchemaLookup);
590        assert_eq!(ev, ExternalValue::String("hello".to_string()));
591        let back = external_to_nb(&ev, &NoSchemaLookup);
592        assert_eq!(back.as_str().unwrap(), "hello");
593    }
594
595    #[test]
596    fn test_none_and_unit() {
597        assert_eq!(
598            nb_to_external(&ValueWord::none(), &NoSchemaLookup),
599            ExternalValue::None
600        );
601        assert_eq!(
602            nb_to_external(&ValueWord::unit(), &NoSchemaLookup),
603            ExternalValue::Unit
604        );
605    }
606
607    #[test]
608    fn test_function_is_opaque() {
609        let nb = ValueWord::from_function(42);
610        let ev = nb_to_external(&nb, &NoSchemaLookup);
611        assert!(matches!(ev, ExternalValue::Opaque(_)));
612    }
613
614    #[test]
615    fn test_array() {
616        let arr = vec![ValueWord::from_i64(1), ValueWord::from_i64(2)];
617        let nb = ValueWord::from_array(std::sync::Arc::new(arr));
618        let ev = nb_to_external(&nb, &NoSchemaLookup);
619        assert_eq!(
620            ev,
621            ExternalValue::Array(vec![ExternalValue::Int(1), ExternalValue::Int(2)])
622        );
623    }
624
625    #[test]
626    fn test_display() {
627        assert_eq!(format!("{}", ExternalValue::Number(3.14)), "3.14");
628        assert_eq!(format!("{}", ExternalValue::Int(42)), "42");
629        assert_eq!(format!("{}", ExternalValue::Bool(true)), "true");
630        assert_eq!(format!("{}", ExternalValue::String("hi".into())), "hi");
631        assert_eq!(format!("{}", ExternalValue::None), "none");
632        assert_eq!(format!("{}", ExternalValue::Unit), "()");
633        assert_eq!(
634            format!(
635                "{}",
636                ExternalValue::Array(vec![ExternalValue::Int(1), ExternalValue::Int(2)])
637            ),
638            "[1, 2]"
639        );
640    }
641
642    #[test]
643    fn test_serde_json_roundtrip() {
644        let ev = ExternalValue::TypedObject {
645            name: "Candle".to_string(),
646            fields: BTreeMap::from([
647                ("open".to_string(), ExternalValue::Number(100.0)),
648                ("close".to_string(), ExternalValue::Number(105.5)),
649            ]),
650        };
651        let json = serde_json::to_string(&ev).unwrap();
652        let back: ExternalValue = serde_json::from_str(&json).unwrap();
653        assert_eq!(ev, back);
654    }
655}