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        HeapValue::ProjectedRef(..) => ExternalValue::Opaque("<ref>".to_string()),
211
212        // Container types
213        HeapValue::Range {
214            start,
215            end,
216            inclusive,
217        } => ExternalValue::Range {
218            start: start.as_ref().map(|v| Box::new(nb_to_external(v, schemas))),
219            end: end.as_ref().map(|v| Box::new(nb_to_external(v, schemas))),
220            inclusive: *inclusive,
221        },
222        HeapValue::Enum(e) => ExternalValue::Enum {
223            name: e.enum_name.clone(),
224            variant: e.variant.clone(),
225            data: Box::new(match &e.payload {
226                crate::enums::EnumPayload::Unit => ExternalValue::None,
227                crate::enums::EnumPayload::Tuple(nbs) => {
228                    if nbs.len() == 1 {
229                        nb_to_external(&nbs[0], schemas)
230                    } else {
231                        ExternalValue::Array(
232                            nbs.iter().map(|v| nb_to_external(v, schemas)).collect(),
233                        )
234                    }
235                }
236                crate::enums::EnumPayload::Struct(fields) => {
237                    let mut map = BTreeMap::new();
238                    for (k, v) in fields {
239                        map.insert(k.clone(), nb_to_external(v, schemas));
240                    }
241                    ExternalValue::Object(map)
242                }
243            }),
244        },
245        HeapValue::Some(v) => nb_to_external(v, schemas),
246        HeapValue::Ok(v) => ExternalValue::Ok(Box::new(nb_to_external(v, schemas))),
247        HeapValue::Err(v) => ExternalValue::Error(format!("{:?}", nb_to_external(v, schemas))),
248
249        // Async
250        HeapValue::Future(id) => ExternalValue::Opaque(format!("<future:{id}>")),
251        HeapValue::TaskGroup { kind, task_ids } => {
252            ExternalValue::Opaque(format!("<task_group:kind={kind},tasks={}>", task_ids.len()))
253        }
254
255        // Trait dispatch
256        HeapValue::TraitObject { value, .. } => nb_to_external(value, schemas),
257
258        // SQL pushdown
259        HeapValue::ExprProxy(s) => ExternalValue::Opaque(format!("<expr_proxy:{s}>")),
260        HeapValue::FilterExpr(_) => ExternalValue::Opaque("<filter_expr>".to_string()),
261
262        // Time types
263        HeapValue::Time(t) => ExternalValue::Time(t.to_rfc3339()),
264        HeapValue::Duration(d) => {
265            let secs = d.value as u64;
266            ExternalValue::Duration { secs, nanos: 0 }
267        }
268        HeapValue::TimeSpan(ts) => ExternalValue::Duration {
269            secs: ts.num_seconds().unsigned_abs(),
270            nanos: (ts.subsec_nanos().unsigned_abs()),
271        },
272        HeapValue::Timeframe(tf) => ExternalValue::String(format!("{tf:?}")),
273
274        // AST types
275        HeapValue::TimeReference(_) => ExternalValue::Opaque("<time_reference>".to_string()),
276        HeapValue::DateTimeExpr(_) => ExternalValue::Opaque("<datetime_expr>".to_string()),
277        HeapValue::DataDateTimeRef(_) => ExternalValue::Opaque("<data_datetime_ref>".to_string()),
278        HeapValue::TypeAnnotation(_) => ExternalValue::Opaque("<type_annotation>".to_string()),
279        HeapValue::TypeAnnotatedValue { type_name, value } => {
280            let inner = nb_to_external(value, schemas);
281            ExternalValue::TypedObject {
282                name: type_name.clone(),
283                fields: BTreeMap::from([("value".to_string(), inner)]),
284            }
285        }
286        HeapValue::PrintResult(pr) => ExternalValue::String(pr.rendered.clone()),
287        HeapValue::SimulationCall(data) => ExternalValue::Opaque(format!(
288            "<simulation_call:{} params={}>",
289            data.name,
290            data.params.len()
291        )),
292        HeapValue::FunctionRef { name, .. } => {
293            ExternalValue::Opaque(format!("<function_ref:{name}>"))
294        }
295        HeapValue::DataReference(data) => {
296            let mut fields = BTreeMap::new();
297            fields.insert(
298                "datetime".to_string(),
299                ExternalValue::Time(data.datetime.to_rfc3339()),
300            );
301            fields.insert("id".to_string(), ExternalValue::String(data.id.clone()));
302            fields.insert(
303                "timeframe".to_string(),
304                ExternalValue::String(format!("{:?}", data.timeframe)),
305            );
306            ExternalValue::Object(fields)
307        }
308
309        HeapValue::Instant(t) => ExternalValue::Opaque(format!("<instant:{:?}>", t.elapsed())),
310
311        HeapValue::IoHandle(data) => {
312            let status = if data.is_open() { "open" } else { "closed" };
313            ExternalValue::Opaque(format!("<io_handle:{}:{}>", data.path, status))
314        }
315
316        HeapValue::NativeScalar(v) => {
317            if let Some(i) = v.as_i64() {
318                ExternalValue::Int(i)
319            } else {
320                ExternalValue::Number(v.as_f64())
321            }
322        }
323        HeapValue::NativeView(v) => ExternalValue::Opaque(format!(
324            "<{}:{}@0x{:x}>",
325            if v.mutable { "cmut" } else { "cview" },
326            v.layout.name,
327            v.ptr
328        )),
329        HeapValue::HashMap(d) => {
330            let mut fields = BTreeMap::new();
331            for (k, v) in d.keys.iter().zip(d.values.iter()) {
332                fields.insert(format!("{}", k), nb_to_external(v, schemas));
333            }
334            ExternalValue::Object(fields)
335        }
336        HeapValue::Set(d) => {
337            ExternalValue::Array(d.items.iter().map(|v| nb_to_external(v, schemas)).collect())
338        }
339        HeapValue::Deque(d) => {
340            ExternalValue::Array(d.items.iter().map(|v| nb_to_external(v, schemas)).collect())
341        }
342        HeapValue::PriorityQueue(d) => {
343            ExternalValue::Array(d.items.iter().map(|v| nb_to_external(v, schemas)).collect())
344        }
345        HeapValue::Content(node) => ExternalValue::String(format!("{}", node)),
346        HeapValue::SharedCell(arc) => nb_to_external(&arc.read().unwrap(), schemas),
347        HeapValue::IntArray(a) => {
348            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v)).collect())
349        }
350        HeapValue::FloatArray(a) => {
351            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Number(v)).collect())
352        }
353        HeapValue::BoolArray(a) => {
354            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Bool(v != 0)).collect())
355        }
356        HeapValue::I8Array(a) => {
357            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
358        }
359        HeapValue::I16Array(a) => {
360            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
361        }
362        HeapValue::I32Array(a) => {
363            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
364        }
365        HeapValue::U8Array(a) => {
366            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
367        }
368        HeapValue::U16Array(a) => {
369            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
370        }
371        HeapValue::U32Array(a) => {
372            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
373        }
374        HeapValue::U64Array(a) => {
375            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Int(v as i64)).collect())
376        }
377        HeapValue::F32Array(a) => {
378            ExternalValue::Array(a.iter().map(|&v| ExternalValue::Number(v as f64)).collect())
379        }
380        HeapValue::Matrix(m) => {
381            ExternalValue::Opaque(format!("<Mat<number>:{}x{}>", m.rows, m.cols))
382        }
383        HeapValue::Iterator(_) => ExternalValue::Opaque("<iterator>".to_string()),
384        HeapValue::Generator(_) => ExternalValue::Opaque("<generator>".to_string()),
385        HeapValue::Mutex(_) => ExternalValue::Opaque("<mutex>".to_string()),
386        HeapValue::Atomic(a) => {
387            ExternalValue::Int(a.inner.load(std::sync::atomic::Ordering::Relaxed))
388        }
389        HeapValue::Channel(c) => {
390            if c.is_sender() {
391                ExternalValue::Opaque("<channel:sender>".to_string())
392            } else {
393                ExternalValue::Opaque("<channel:receiver>".to_string())
394            }
395        }
396        HeapValue::Lazy(l) => {
397            if let Ok(guard) = l.value.lock() {
398                if let Some(val) = guard.as_ref() {
399                    return nb_to_external(val, schemas);
400                }
401            }
402            ExternalValue::Opaque("<lazy:uninitialized>".to_string())
403        }
404        HeapValue::Char(c) => ExternalValue::String(c.to_string()),
405        HeapValue::FloatArraySlice {
406            parent,
407            offset,
408            len,
409        } => {
410            let slice = &parent.data[*offset as usize..(*offset + *len) as usize];
411            ExternalValue::Array(slice.iter().map(|&v| ExternalValue::Number(v)).collect())
412        }
413    }
414}
415
416/// Convert an ExternalValue back to a ValueWord value.
417///
418/// This is used for deserializing wire values back into the VM.
419/// Note: Opaque values cannot be round-tripped.
420pub fn external_to_nb(ev: &ExternalValue, schemas: &dyn SchemaLookup) -> ValueWord {
421    let _ = schemas; // schemas needed for TypedObject reconstruction (future use)
422    match ev {
423        ExternalValue::Number(n) => ValueWord::from_f64(*n),
424        ExternalValue::Int(i) => ValueWord::from_i64(*i),
425        ExternalValue::Bool(b) => ValueWord::from_bool(*b),
426        ExternalValue::String(s) => ValueWord::from_string(std::sync::Arc::new(s.clone())),
427        ExternalValue::None => ValueWord::none(),
428        ExternalValue::Unit => ValueWord::unit(),
429        ExternalValue::Array(items) => {
430            let nbs: Vec<ValueWord> = items.iter().map(|v| external_to_nb(v, schemas)).collect();
431            ValueWord::from_array(std::sync::Arc::new(nbs))
432        }
433        ExternalValue::Decimal(s) => {
434            if let Ok(d) = s.parse::<rust_decimal::Decimal>() {
435                ValueWord::from_decimal(d)
436            } else {
437                ValueWord::from_string(std::sync::Arc::new(s.clone()))
438            }
439        }
440        ExternalValue::Ok(inner) => ValueWord::from_ok(external_to_nb(inner, schemas)),
441        ExternalValue::Error(msg) => {
442            ValueWord::from_err(ValueWord::from_string(std::sync::Arc::new(msg.clone())))
443        }
444        ExternalValue::Range {
445            start,
446            end,
447            inclusive,
448        } => ValueWord::from_range(
449            start.as_ref().map(|v| external_to_nb(v, schemas)),
450            end.as_ref().map(|v| external_to_nb(v, schemas)),
451            *inclusive,
452        ),
453        // Complex types that can't be fully round-tripped return None
454        ExternalValue::Object(_)
455        | ExternalValue::TypedObject { .. }
456        | ExternalValue::Enum { .. }
457        | ExternalValue::Duration { .. }
458        | ExternalValue::Time(_)
459        | ExternalValue::DataTable { .. }
460        | ExternalValue::Opaque(_) => ValueWord::none(),
461    }
462}
463
464impl fmt::Display for ExternalValue {
465    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
466        match self {
467            ExternalValue::Number(n) => {
468                if n.is_nan() {
469                    write!(f, "NaN")
470                } else if n.is_infinite() {
471                    if n.is_sign_positive() {
472                        write!(f, "Infinity")
473                    } else {
474                        write!(f, "-Infinity")
475                    }
476                } else if *n == (*n as i64) as f64 && n.is_finite() {
477                    // Display whole numbers without decimal point
478                    write!(f, "{}", *n as i64)
479                } else {
480                    write!(f, "{n}")
481                }
482            }
483            ExternalValue::Int(i) => write!(f, "{i}"),
484            ExternalValue::Bool(b) => write!(f, "{b}"),
485            ExternalValue::String(s) => write!(f, "{s}"),
486            ExternalValue::None => write!(f, "none"),
487            ExternalValue::Unit => write!(f, "()"),
488            ExternalValue::Array(items) => {
489                write!(f, "[")?;
490                for (i, item) in items.iter().enumerate() {
491                    if i > 0 {
492                        write!(f, ", ")?;
493                    }
494                    write!(f, "{item}")?;
495                }
496                write!(f, "]")
497            }
498            ExternalValue::Object(fields) => {
499                write!(f, "{{")?;
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::TypedObject { name, fields } => {
509                write!(f, "{name} {{")?;
510                for (i, (k, v)) in fields.iter().enumerate() {
511                    if i > 0 {
512                        write!(f, ", ")?;
513                    }
514                    write!(f, "{k}: {v}")?;
515                }
516                write!(f, "}}")
517            }
518            ExternalValue::Enum {
519                name,
520                variant,
521                data,
522            } => {
523                write!(f, "{name}::{variant}")?;
524                if **data != ExternalValue::None {
525                    write!(f, "({data})")?;
526                }
527                Ok(())
528            }
529            ExternalValue::Duration { secs, nanos } => {
530                if *nanos > 0 {
531                    write!(f, "{secs}.{:09}s", nanos)
532                } else {
533                    write!(f, "{secs}s")
534                }
535            }
536            ExternalValue::Time(iso) => write!(f, "{iso}"),
537            ExternalValue::Decimal(d) => write!(f, "{d}"),
538            ExternalValue::Error(msg) => write!(f, "Error({msg})"),
539            ExternalValue::Ok(inner) => write!(f, "Ok({inner})"),
540            ExternalValue::Range {
541                start,
542                end,
543                inclusive,
544            } => {
545                if let Some(s) = start {
546                    write!(f, "{s}")?;
547                }
548                if *inclusive {
549                    write!(f, "..=")?;
550                } else {
551                    write!(f, "..")?;
552                }
553                if let Some(e) = end {
554                    write!(f, "{e}")?;
555                }
556                Ok(())
557            }
558            ExternalValue::DataTable { rows, columns } => {
559                write!(f, "DataTable({rows} rows, {} cols)", columns.len())
560            }
561            ExternalValue::Opaque(desc) => write!(f, "{desc}"),
562        }
563    }
564}
565
566#[cfg(test)]
567mod tests {
568    use super::*;
569    use crate::value_word::ValueWord;
570
571    #[test]
572    fn test_number_roundtrip() {
573        let nb = ValueWord::from_f64(3.14);
574        let ev = nb_to_external(&nb, &NoSchemaLookup);
575        assert!(matches!(ev, ExternalValue::Number(n) if (n - 3.14).abs() < f64::EPSILON));
576        let back = external_to_nb(&ev, &NoSchemaLookup);
577        assert!((back.as_f64().unwrap() - 3.14).abs() < f64::EPSILON);
578    }
579
580    #[test]
581    fn test_int_roundtrip() {
582        let nb = ValueWord::from_i64(42);
583        let ev = nb_to_external(&nb, &NoSchemaLookup);
584        assert_eq!(ev, ExternalValue::Int(42));
585        let back = external_to_nb(&ev, &NoSchemaLookup);
586        assert_eq!(back.as_i64().unwrap(), 42);
587    }
588
589    #[test]
590    fn test_bool_roundtrip() {
591        let nb = ValueWord::from_bool(true);
592        let ev = nb_to_external(&nb, &NoSchemaLookup);
593        assert_eq!(ev, ExternalValue::Bool(true));
594    }
595
596    #[test]
597    fn test_string_roundtrip() {
598        let nb = ValueWord::from_string(std::sync::Arc::new("hello".to_string()));
599        let ev = nb_to_external(&nb, &NoSchemaLookup);
600        assert_eq!(ev, ExternalValue::String("hello".to_string()));
601        let back = external_to_nb(&ev, &NoSchemaLookup);
602        assert_eq!(back.as_str().unwrap(), "hello");
603    }
604
605    #[test]
606    fn test_none_and_unit() {
607        assert_eq!(
608            nb_to_external(&ValueWord::none(), &NoSchemaLookup),
609            ExternalValue::None
610        );
611        assert_eq!(
612            nb_to_external(&ValueWord::unit(), &NoSchemaLookup),
613            ExternalValue::Unit
614        );
615    }
616
617    #[test]
618    fn test_function_is_opaque() {
619        let nb = ValueWord::from_function(42);
620        let ev = nb_to_external(&nb, &NoSchemaLookup);
621        assert!(matches!(ev, ExternalValue::Opaque(_)));
622    }
623
624    #[test]
625    fn test_array() {
626        let arr = vec![ValueWord::from_i64(1), ValueWord::from_i64(2)];
627        let nb = ValueWord::from_array(std::sync::Arc::new(arr));
628        let ev = nb_to_external(&nb, &NoSchemaLookup);
629        assert_eq!(
630            ev,
631            ExternalValue::Array(vec![ExternalValue::Int(1), ExternalValue::Int(2)])
632        );
633    }
634
635    #[test]
636    fn test_display() {
637        assert_eq!(format!("{}", ExternalValue::Number(3.14)), "3.14");
638        assert_eq!(format!("{}", ExternalValue::Int(42)), "42");
639        assert_eq!(format!("{}", ExternalValue::Bool(true)), "true");
640        assert_eq!(format!("{}", ExternalValue::String("hi".into())), "hi");
641        assert_eq!(format!("{}", ExternalValue::None), "none");
642        assert_eq!(format!("{}", ExternalValue::Unit), "()");
643        assert_eq!(
644            format!(
645                "{}",
646                ExternalValue::Array(vec![ExternalValue::Int(1), ExternalValue::Int(2)])
647            ),
648            "[1, 2]"
649        );
650    }
651
652    #[test]
653    fn test_serde_json_roundtrip() {
654        let ev = ExternalValue::TypedObject {
655            name: "Candle".to_string(),
656            fields: BTreeMap::from([
657                ("open".to_string(), ExternalValue::Number(100.0)),
658                ("close".to_string(), ExternalValue::Number(105.5)),
659            ]),
660        };
661        let json = serde_json::to_string(&ev).unwrap();
662        let back: ExternalValue = serde_json::from_str(&json).unwrap();
663        assert_eq!(ev, back);
664    }
665}