Skip to main content

shape_runtime/
wire_conversion.rs

1//! Conversion between runtime values and wire format.
2//!
3//! This module provides conversions between the runtime's value types
4//! and the serializable `WireValue` type used for REPL communication
5//! and external tool integration.
6//!
7//! ## API (ValueWord-native)
8//!
9//! - [`nb_to_wire`] / [`wire_to_nb`] — convert directly without ValueWord intermediate
10//! - [`nb_to_envelope`] — wrap a ValueWord in a ValueEnvelope with metadata
11//! - [`nb_extract_typed_value`] / [`nb_typed_value_to_envelope`] — typed extraction
12
13use crate::Context;
14use arrow_ipc::{reader::FileReader, writer::FileWriter};
15use shape_value::heap_value::HeapValue;
16use shape_value::{DataTable, ValueSlot, ValueWord};
17use shape_wire::{
18    DurationUnit as WireDurationUnit, ValueEnvelope, WireTable, WireValue,
19    metadata::{TypeInfo, TypeRegistry},
20};
21use std::collections::BTreeMap;
22use std::sync::Arc;
23
24#[inline]
25fn slot_to_nb(
26    slots: &[ValueSlot],
27    heap_mask: u64,
28    idx: usize,
29    _field_type: &crate::type_schema::FieldType,
30) -> ValueWord {
31    if idx >= slots.len() {
32        return ValueWord::none();
33    }
34    if heap_mask & (1u64 << idx) != 0 {
35        return slots[idx].as_heap_nb();
36    }
37
38    // Non-heap slot: raw bits are a ValueWord representation (inline f64, i48,
39    // bool, none, unit, function, module_fn). Reconstruct from raw bits.
40    // Safety: bits were stored by nb_to_slot from a valid inline ValueWord.
41    unsafe { ValueWord::clone_from_bits(slots[idx].raw()) }
42}
43
44#[inline]
45fn slot_to_nb_raw(slots: &[ValueSlot], heap_mask: u64, idx: usize) -> ValueWord {
46    if idx >= slots.len() {
47        return ValueWord::none();
48    }
49    if heap_mask & (1u64 << idx) != 0 {
50        slots[idx].as_heap_nb()
51    } else {
52        // Non-heap slot: reconstruct ValueWord from raw bits.
53        // Safety: bits were stored from a valid inline ValueWord.
54        unsafe { ValueWord::clone_from_bits(slots[idx].raw()) }
55    }
56}
57
58fn nb_to_wire_with_builtin_fallback(nb: &ValueWord, ctx: &Context) -> WireValue {
59    if let Some(HeapValue::TypedObject {
60        slots, heap_mask, ..
61    }) = nb.as_heap_ref()
62        && let Some(value) = fallback_builtin_typedobject_to_wire(slots, *heap_mask, ctx)
63    {
64        return value;
65    }
66    nb_to_wire(nb, ctx)
67}
68
69fn fallback_builtin_typedobject_to_wire(
70    slots: &[ValueSlot],
71    heap_mask: u64,
72    ctx: &Context,
73) -> Option<WireValue> {
74    use crate::type_schema::builtin_schemas::*;
75
76    // AnyError: [category, payload, cause, trace_info, message, code]
77    if slots.len() >= 6 {
78        let category = slot_to_nb_raw(slots, heap_mask, ANYERROR_CATEGORY);
79        if category.as_str() == Some("AnyError") {
80            let mut obj = BTreeMap::new();
81            obj.insert(
82                "category".to_string(),
83                WireValue::String("AnyError".to_string()),
84            );
85            obj.insert(
86                "payload".to_string(),
87                nb_to_wire_with_builtin_fallback(
88                    &slot_to_nb_raw(slots, heap_mask, ANYERROR_PAYLOAD),
89                    ctx,
90                ),
91            );
92
93            let cause_nb = slot_to_nb_raw(slots, heap_mask, ANYERROR_CAUSE);
94            obj.insert(
95                "cause".to_string(),
96                if cause_nb.is_none() {
97                    WireValue::Null
98                } else {
99                    nb_to_wire_with_builtin_fallback(&cause_nb, ctx)
100                },
101            );
102            obj.insert(
103                "trace_info".to_string(),
104                nb_to_wire_with_builtin_fallback(
105                    &slot_to_nb_raw(slots, heap_mask, ANYERROR_TRACE_INFO),
106                    ctx,
107                ),
108            );
109            obj.insert(
110                "message".to_string(),
111                nb_to_wire_with_builtin_fallback(
112                    &slot_to_nb_raw(slots, heap_mask, ANYERROR_MESSAGE),
113                    ctx,
114                ),
115            );
116            obj.insert(
117                "code".to_string(),
118                nb_to_wire_with_builtin_fallback(
119                    &slot_to_nb_raw(slots, heap_mask, ANYERROR_CODE),
120                    ctx,
121                ),
122            );
123            return Some(WireValue::Object(obj));
124        }
125    }
126
127    // TraceInfo (full/single): [kind, frames|frame]
128    if slots.len() >= 2 {
129        let kind_nb = slot_to_nb_raw(slots, heap_mask, TRACEINFO_FULL_KIND);
130        if let Some(kind) = kind_nb.as_str()
131            && (kind == "full" || kind == "single")
132        {
133            let mut obj = BTreeMap::new();
134            obj.insert("kind".to_string(), WireValue::String(kind.to_string()));
135            if kind == "single" {
136                obj.insert(
137                    "frame".to_string(),
138                    nb_to_wire_with_builtin_fallback(
139                        &slot_to_nb_raw(slots, heap_mask, TRACEINFO_SINGLE_FRAME),
140                        ctx,
141                    ),
142                );
143            } else {
144                obj.insert(
145                    "frames".to_string(),
146                    nb_to_wire_with_builtin_fallback(
147                        &slot_to_nb_raw(slots, heap_mask, TRACEINFO_FULL_FRAMES),
148                        ctx,
149                    ),
150                );
151            }
152            return Some(WireValue::Object(obj));
153        }
154    }
155
156    // TraceFrame: [ip, line, file, function]
157    if slots.len() >= 4 {
158        let mut obj = BTreeMap::new();
159        obj.insert(
160            "ip".to_string(),
161            nb_to_wire_with_builtin_fallback(&slot_to_nb_raw(slots, heap_mask, TRACEFRAME_IP), ctx),
162        );
163        obj.insert(
164            "line".to_string(),
165            nb_to_wire_with_builtin_fallback(
166                &slot_to_nb_raw(slots, heap_mask, TRACEFRAME_LINE),
167                ctx,
168            ),
169        );
170        obj.insert(
171            "file".to_string(),
172            nb_to_wire_with_builtin_fallback(
173                &slot_to_nb_raw(slots, heap_mask, TRACEFRAME_FILE),
174                ctx,
175            ),
176        );
177        obj.insert(
178            "function".to_string(),
179            nb_to_wire_with_builtin_fallback(
180                &slot_to_nb_raw(slots, heap_mask, TRACEFRAME_FUNCTION),
181                ctx,
182            ),
183        );
184        return Some(WireValue::Object(obj));
185    }
186
187    None
188}
189
190/// Convert a ValueWord to WireValue (internal helper for structural ValueWord fields).
191///
192/// Used by `nb_heap_to_wire` for types that still embed ValueWord
193/// (SimulationCallData.params, PrintSpan.raw_value).
194fn value_to_wire(value: &ValueWord, ctx: &Context) -> WireValue {
195    nb_to_wire(&value.clone(), ctx)
196}
197
198/// Convert a ValueWord value to WireValue without materializing ValueWord.
199///
200/// This is the ValueWord-native equivalent of `value_to_wire`. For inline types
201/// (f64, i48, bool, None, Unit) it avoids heap allocation entirely. For heap types
202/// it dispatches on HeapValue directly.
203pub fn nb_to_wire(nb: &ValueWord, ctx: &Context) -> WireValue {
204    use shape_value::value_word::NanTag;
205
206    match nb.tag() {
207        NanTag::None | NanTag::Unit => WireValue::Null,
208
209        NanTag::Bool => WireValue::Bool(nb.as_bool().unwrap_or(false)),
210
211        NanTag::F64 => {
212            let n = nb.as_f64().unwrap_or(0.0);
213            WireValue::Number(n)
214        }
215
216        NanTag::I48 => WireValue::Integer(nb.as_i64().unwrap_or(0)),
217
218        NanTag::Function => {
219            let id = nb.as_function().unwrap_or(0);
220            WireValue::String(format!("<function#{}>", id))
221        }
222
223        NanTag::ModuleFunction => {
224            let idx = nb.as_module_function().unwrap_or(0);
225            WireValue::String(format!("<native:{}>", idx))
226        }
227
228        NanTag::Heap => nb_heap_to_wire(nb, ctx),
229
230        NanTag::Ref => WireValue::Null, // References should not appear in wire output
231    }
232}
233
234/// Convert a heap-tagged ValueWord to WireValue by dispatching on HeapValue.
235fn nb_heap_to_wire(nb: &ValueWord, ctx: &Context) -> WireValue {
236    let hv = match nb.as_heap_ref() {
237        Some(hv) => hv,
238        None => return WireValue::Null,
239    };
240    match hv {
241        HeapValue::String(s) => WireValue::String((**s).clone()),
242
243        HeapValue::Array(arr) => {
244            WireValue::Array(arr.iter().map(|elem| nb_to_wire(elem, ctx)).collect())
245        }
246
247        HeapValue::Decimal(d) => WireValue::Number(d.to_string().parse().unwrap_or(0.0)),
248
249        HeapValue::BigInt(i) => WireValue::Integer(*i),
250        HeapValue::ProjectedRef(_) => WireValue::Null,
251
252        HeapValue::Time(dt) => WireValue::Timestamp(dt.timestamp_millis()),
253
254        HeapValue::TimeSpan(duration) => {
255            let millis = duration.num_milliseconds();
256            WireValue::Duration {
257                value: millis as f64,
258                unit: WireDurationUnit::Milliseconds,
259            }
260        }
261
262        HeapValue::Duration(duration) => {
263            let value = duration.value;
264            let wire_unit = match duration.unit {
265                shape_ast::ast::DurationUnit::Seconds => WireDurationUnit::Seconds,
266                shape_ast::ast::DurationUnit::Minutes => WireDurationUnit::Minutes,
267                shape_ast::ast::DurationUnit::Hours => WireDurationUnit::Hours,
268                shape_ast::ast::DurationUnit::Days => WireDurationUnit::Days,
269                shape_ast::ast::DurationUnit::Weeks => WireDurationUnit::Weeks,
270                shape_ast::ast::DurationUnit::Months => {
271                    return WireValue::Duration {
272                        value: value * 30.0,
273                        unit: WireDurationUnit::Days,
274                    };
275                }
276                shape_ast::ast::DurationUnit::Years => {
277                    return WireValue::Duration {
278                        value: value * 365.0,
279                        unit: WireDurationUnit::Days,
280                    };
281                }
282                shape_ast::ast::DurationUnit::Samples => {
283                    return WireValue::Duration {
284                        value,
285                        unit: WireDurationUnit::Days,
286                    };
287                }
288            };
289            WireValue::Duration {
290                value,
291                unit: wire_unit,
292            }
293        }
294
295        HeapValue::Enum(enum_value) => enum_to_wire(enum_value, ctx),
296
297        HeapValue::Some(inner) => nb_to_wire(inner, ctx),
298
299        HeapValue::Ok(inner) => WireValue::Result {
300            ok: true,
301            value: Box::new(nb_to_wire(inner, ctx)),
302        },
303
304        HeapValue::Err(inner) => WireValue::Result {
305            ok: false,
306            value: Box::new(nb_to_wire(inner, ctx)),
307        },
308
309        HeapValue::Range {
310            start,
311            end,
312            inclusive,
313        } => WireValue::Range {
314            start: start.as_ref().map(|v| Box::new(nb_to_wire(v, ctx))),
315            end: end.as_ref().map(|v| Box::new(nb_to_wire(v, ctx))),
316            inclusive: *inclusive,
317        },
318
319        HeapValue::FunctionRef { name, .. } => WireValue::FunctionRef { name: name.clone() },
320
321        HeapValue::Closure { .. } => WireValue::FunctionRef {
322            name: "<closure>".to_string(),
323        },
324
325        HeapValue::Timeframe(tf) => WireValue::String(format!("{}", tf)),
326
327        HeapValue::DataReference(data) => {
328            let mut obj = BTreeMap::new();
329            obj.insert(
330                "datetime".to_string(),
331                WireValue::Timestamp(data.datetime.timestamp_millis()),
332            );
333            obj.insert("id".to_string(), WireValue::String(data.id.clone()));
334            obj.insert(
335                "timeframe".to_string(),
336                WireValue::String(format!("{}", data.timeframe)),
337            );
338            WireValue::Object(obj)
339        }
340
341        HeapValue::SimulationCall(data) => {
342            let mut obj = BTreeMap::new();
343            obj.insert(
344                "__type".to_string(),
345                WireValue::String("SimulationCall".to_string()),
346            );
347            obj.insert("name".to_string(), WireValue::String(data.name.clone()));
348            // SimulationCallData.params stores ValueWord (structural boundary in shape-value)
349            let params_wire: BTreeMap<String, WireValue> = data
350                .params
351                .iter()
352                .map(|(k, v)| (k.clone(), value_to_wire(v, ctx)))
353                .collect();
354            obj.insert("params".to_string(), WireValue::Object(params_wire));
355            WireValue::Object(obj)
356        }
357
358        HeapValue::PrintResult(result) => {
359            use shape_wire::print_result::{WirePrintResult, WirePrintSpan};
360
361            let wire_spans: Vec<WirePrintSpan> = result
362                .spans
363                .iter()
364                .map(|span| match span {
365                    shape_value::PrintSpan::Literal {
366                        text,
367                        start,
368                        end,
369                        span_id,
370                    } => WirePrintSpan::Literal {
371                        text: text.clone(),
372                        start: *start,
373                        end: *end,
374                        span_id: span_id.clone(),
375                    },
376                    shape_value::PrintSpan::Value {
377                        text,
378                        start,
379                        end,
380                        span_id,
381                        variable_name,
382                        raw_value,
383                        type_name,
384                        current_format,
385                        format_params,
386                    } => {
387                        // raw_value is Box<ValueWord> (structural boundary in shape-value)
388                        let raw_wire = value_to_wire(raw_value, ctx);
389                        let (type_info, type_registry) =
390                            infer_metadata_with_ctx(raw_value, type_name, ctx);
391
392                        let params_wire: std::collections::HashMap<String, WireValue> =
393                            format_params
394                                .iter()
395                                .map(|(k, v)| (k.clone(), value_to_wire(v, ctx)))
396                                .collect();
397
398                        WirePrintSpan::Value {
399                            text: text.clone(),
400                            start: *start,
401                            end: *end,
402                            span_id: span_id.clone(),
403                            variable_name: variable_name.clone(),
404                            raw_value: Box::new(raw_wire),
405                            type_info: Box::new(type_info),
406                            current_format: current_format.clone(),
407                            type_registry,
408                            format_params: params_wire,
409                        }
410                    }
411                })
412                .collect();
413
414            WireValue::PrintResult(WirePrintResult {
415                rendered: result.rendered.clone(),
416                spans: wire_spans,
417            })
418        }
419
420        HeapValue::TypedObject {
421            schema_id,
422            slots,
423            heap_mask,
424        } => {
425            let schema = ctx
426                .type_schema_registry()
427                .get_by_id(*schema_id as u32)
428                .cloned()
429                .or_else(|| crate::type_schema::lookup_schema_by_id_public(*schema_id as u32));
430
431            if let Some(schema) = schema {
432                let mut map = BTreeMap::new();
433                for field_def in &schema.fields {
434                    let idx = field_def.index as usize;
435                    if idx < slots.len() {
436                        let field_nb = slot_to_nb(slots, *heap_mask, idx, &field_def.field_type);
437                        map.insert(
438                            field_def.name.clone(),
439                            nb_to_wire_with_builtin_fallback(&field_nb, ctx),
440                        );
441                    }
442                }
443                WireValue::Object(map)
444            } else if let Some(fallback) =
445                fallback_builtin_typedobject_to_wire(slots, *heap_mask, ctx)
446            {
447                fallback
448            } else {
449                WireValue::String(format!("<typed_object:schema#{}>", schema_id))
450            }
451        }
452
453        HeapValue::TypeAnnotatedValue { value, .. } => nb_to_wire(value, ctx),
454
455        HeapValue::Future(id) => WireValue::String(format!("<future:{}>", id)),
456
457        HeapValue::DataTable(dt) => datatable_to_wire(dt.as_ref()),
458
459        HeapValue::TypedTable { table, schema_id } => {
460            datatable_to_wire_with_schema(table.as_ref(), Some(*schema_id as u32))
461        }
462
463        HeapValue::RowView { row_idx, .. } => WireValue::String(format!("<Row:{}>", row_idx)),
464        HeapValue::ColumnRef { col_id, .. } => WireValue::String(format!("<ColumnRef:{}>", col_id)),
465        HeapValue::IndexedTable { table, .. } => datatable_to_wire(table.as_ref()),
466        HeapValue::HostClosure(_) => WireValue::String("<HostClosure>".to_string()),
467        HeapValue::ExprProxy(col) => WireValue::String(format!("<ExprProxy:{}>", col)),
468        HeapValue::FilterExpr(_) => WireValue::String("<FilterExpr>".to_string()),
469        HeapValue::TaskGroup { .. } => WireValue::String("<TaskGroup>".to_string()),
470        HeapValue::TraitObject { value, .. } => nb_to_wire(value, ctx),
471
472        // Rare AST types — format as debug strings
473        HeapValue::TimeReference(tr) => WireValue::String(format!("{:?}", tr)),
474        HeapValue::DateTimeExpr(expr) => WireValue::String(format!("{:?}", expr)),
475        HeapValue::DataDateTimeRef(dref) => WireValue::String(format!("{:?}", dref)),
476        HeapValue::TypeAnnotation(ann) => WireValue::String(format!("{:?}", ann)),
477
478        HeapValue::NativeScalar(v) => match v {
479            shape_value::heap_value::NativeScalar::I8(n) => WireValue::I8(*n),
480            shape_value::heap_value::NativeScalar::U8(n) => WireValue::U8(*n),
481            shape_value::heap_value::NativeScalar::I16(n) => WireValue::I16(*n),
482            shape_value::heap_value::NativeScalar::U16(n) => WireValue::U16(*n),
483            shape_value::heap_value::NativeScalar::I32(n) => WireValue::I32(*n),
484            shape_value::heap_value::NativeScalar::I64(n) => WireValue::I64(*n),
485            shape_value::heap_value::NativeScalar::U32(n) => WireValue::U32(*n),
486            shape_value::heap_value::NativeScalar::U64(n) => WireValue::U64(*n),
487            shape_value::heap_value::NativeScalar::Isize(n) => WireValue::Isize(*n as i64),
488            shape_value::heap_value::NativeScalar::Usize(n) => WireValue::Usize(*n as u64),
489            shape_value::heap_value::NativeScalar::Ptr(n) => WireValue::Ptr(*n as u64),
490            shape_value::heap_value::NativeScalar::F32(n) => WireValue::F32(*n),
491        },
492        HeapValue::NativeView(v) => WireValue::Object(
493            [
494                (
495                    "__type".to_string(),
496                    WireValue::String(if v.mutable { "cmut" } else { "cview" }.to_string()),
497                ),
498                (
499                    "layout".to_string(),
500                    WireValue::String(v.layout.name.clone()),
501                ),
502                (
503                    "ptr".to_string(),
504                    WireValue::String(format!("0x{:x}", v.ptr)),
505                ),
506            ]
507            .into_iter()
508            .collect(),
509        ),
510        HeapValue::HashMap(d) => {
511            let pairs: Vec<(String, WireValue)> = d
512                .keys
513                .iter()
514                .zip(d.values.iter())
515                .map(|(k, v)| (format!("{}", k), nb_to_wire(v, ctx)))
516                .collect();
517            WireValue::Object(pairs.into_iter().collect())
518        }
519        HeapValue::Set(d) => WireValue::Array(d.items.iter().map(|v| nb_to_wire(v, ctx)).collect()),
520        HeapValue::Deque(d) => {
521            WireValue::Array(d.items.iter().map(|v| nb_to_wire(v, ctx)).collect())
522        }
523        HeapValue::PriorityQueue(d) => {
524            WireValue::Array(d.items.iter().map(|v| nb_to_wire(v, ctx)).collect())
525        }
526        HeapValue::Content(node) => WireValue::Content(node.as_ref().clone()),
527        HeapValue::Instant(t) => WireValue::String(format!("<instant:{:?}>", t.elapsed())),
528        HeapValue::IoHandle(data) => {
529            let status = if data.is_open() { "open" } else { "closed" };
530            WireValue::String(format!("<io_handle:{}:{}>", data.path, status))
531        }
532        HeapValue::SharedCell(arc) => nb_to_wire(&arc.read().unwrap(), ctx),
533        HeapValue::IntArray(a) => {
534            WireValue::Array(a.iter().map(|&v| WireValue::Integer(v)).collect())
535        }
536        HeapValue::FloatArray(a) => {
537            WireValue::Array(a.iter().map(|&v| WireValue::Number(v)).collect())
538        }
539        HeapValue::FloatArraySlice {
540            parent,
541            offset,
542            len,
543        } => {
544            let start = *offset as usize;
545            let end = start + *len as usize;
546            let slice = &parent.data[start..end];
547            WireValue::Array(slice.iter().map(|&v| WireValue::Number(v)).collect())
548        }
549        HeapValue::BoolArray(a) => {
550            WireValue::Array(a.iter().map(|&v| WireValue::Bool(v != 0)).collect())
551        }
552        HeapValue::I8Array(a) => {
553            WireValue::Array(a.iter().map(|&v| WireValue::Integer(v as i64)).collect())
554        }
555        HeapValue::I16Array(a) => {
556            WireValue::Array(a.iter().map(|&v| WireValue::Integer(v as i64)).collect())
557        }
558        HeapValue::I32Array(a) => {
559            WireValue::Array(a.iter().map(|&v| WireValue::Integer(v as i64)).collect())
560        }
561        HeapValue::U8Array(a) => {
562            WireValue::Array(a.iter().map(|&v| WireValue::Integer(v as i64)).collect())
563        }
564        HeapValue::U16Array(a) => {
565            WireValue::Array(a.iter().map(|&v| WireValue::Integer(v as i64)).collect())
566        }
567        HeapValue::U32Array(a) => {
568            WireValue::Array(a.iter().map(|&v| WireValue::Integer(v as i64)).collect())
569        }
570        HeapValue::U64Array(a) => {
571            WireValue::Array(a.iter().map(|&v| WireValue::Integer(v as i64)).collect())
572        }
573        HeapValue::F32Array(a) => {
574            WireValue::Array(a.iter().map(|&v| WireValue::Number(v as f64)).collect())
575        }
576        HeapValue::Matrix(m) => WireValue::String(format!("<Mat<number>:{}x{}>", m.rows, m.cols)),
577        HeapValue::Iterator(_) => WireValue::String("<iterator>".to_string()),
578        HeapValue::Generator(_) => WireValue::String("<generator>".to_string()),
579        HeapValue::Mutex(_) => WireValue::String("<mutex>".to_string()),
580        HeapValue::Atomic(a) => {
581            WireValue::Integer(a.inner.load(std::sync::atomic::Ordering::Relaxed))
582        }
583        HeapValue::Lazy(_) => WireValue::String("<lazy>".to_string()),
584        HeapValue::Channel(c) => {
585            if c.is_sender() {
586                WireValue::String("<channel:sender>".to_string())
587            } else {
588                WireValue::String("<channel:receiver>".to_string())
589            }
590        }
591        HeapValue::Char(c) => WireValue::String(c.to_string()),
592    }
593}
594
595/// Extracted content with multiple render targets.
596pub struct ExtractedContent {
597    /// The raw ContentNode (for re-rendering on other targets or wire serialization)
598    pub content_node: shape_value::content::ContentNode,
599    /// JSON renderer output
600    pub content_json: serde_json::Value,
601    /// HTML renderer output
602    pub content_html: String,
603    /// Terminal renderer output (ANSI)
604    pub content_terminal: String,
605}
606
607/// If the ValueWord value is a Content node, render it as JSON, HTML, and terminal strings.
608///
609/// Returns `(content_json, content_html, content_terminal)` — all `None` if the value is not Content.
610pub fn nb_extract_content(
611    nb: &ValueWord,
612) -> (Option<serde_json::Value>, Option<String>, Option<String>) {
613    let extracted = nb_extract_content_full(nb);
614    match extracted {
615        Some(e) => (
616            Some(e.content_json),
617            Some(e.content_html),
618            Some(e.content_terminal),
619        ),
620        None => (None, None, None),
621    }
622}
623
624/// Extract content with full detail including the raw ContentNode.
625pub fn nb_extract_content_full(nb: &ValueWord) -> Option<ExtractedContent> {
626    use crate::content_renderer::ContentRenderer;
627
628    // Check if value is already a Content node
629    let node: Option<shape_value::content::ContentNode> =
630        if let Some(HeapValue::Content(node)) = nb.as_heap_ref() {
631            Some(node.as_ref().clone())
632        } else if let Some(HeapValue::DataTable(dt)) = nb.as_heap_ref() {
633            // Auto-wrap DataTable as ContentNode::Table
634            Some(crate::content_dispatch::datatable_to_content_node(dt, None))
635        } else if let Some(HeapValue::TypedTable { table, .. }) = nb.as_heap_ref() {
636            Some(crate::content_dispatch::datatable_to_content_node(
637                table, None,
638            ))
639        } else {
640            None
641        };
642
643    let node = node?;
644
645    let json_renderer = crate::renderers::json::JsonRenderer;
646    let html_renderer = crate::renderers::html::HtmlRenderer::new();
647    let terminal_renderer = crate::renderers::terminal::TerminalRenderer::new();
648
649    let json_str = json_renderer.render(&node);
650    let content_json = serde_json::from_str(&json_str).unwrap_or(serde_json::Value::Null);
651    let content_html = html_renderer.render(&node);
652    let content_terminal = terminal_renderer.render(&node);
653
654    Some(ExtractedContent {
655        content_node: node,
656        content_json,
657        content_html,
658        content_terminal,
659    })
660}
661
662/// Convert a WireValue to a ValueWord value without ValueWord intermediate.
663///
664/// This is the ValueWord-native equivalent of `wire_to_value`. For simple types
665/// (null, bool, number, integer) it constructs ValueWord inline values directly.
666pub fn wire_to_nb(wire: &WireValue) -> ValueWord {
667    match wire {
668        WireValue::Null => ValueWord::none(),
669        WireValue::Bool(b) => ValueWord::from_bool(*b),
670        WireValue::Number(n) => ValueWord::from_f64(*n),
671        WireValue::Integer(i) => ValueWord::from_i64(*i),
672        WireValue::I8(n) => ValueWord::from_native_i8(*n),
673        WireValue::U8(n) => ValueWord::from_native_u8(*n),
674        WireValue::I16(n) => ValueWord::from_native_i16(*n),
675        WireValue::U16(n) => ValueWord::from_native_u16(*n),
676        WireValue::I32(n) => ValueWord::from_native_i32(*n),
677        WireValue::U32(n) => ValueWord::from_native_u32(*n),
678        WireValue::I64(n) => {
679            ValueWord::from_native_scalar(shape_value::heap_value::NativeScalar::I64(*n))
680        }
681        WireValue::U64(n) => ValueWord::from_native_u64(*n),
682        WireValue::Isize(n) => match isize::try_from(*n) {
683            Ok(v) => ValueWord::from_native_isize(v),
684            Err(_) => ValueWord::none(),
685        },
686        WireValue::Usize(n) => match usize::try_from(*n) {
687            Ok(v) => ValueWord::from_native_usize(v),
688            Err(_) => ValueWord::none(),
689        },
690        WireValue::Ptr(n) => match usize::try_from(*n) {
691            Ok(v) => ValueWord::from_native_scalar(shape_value::heap_value::NativeScalar::Ptr(v)),
692            Err(_) => ValueWord::none(),
693        },
694        WireValue::F32(n) => ValueWord::from_native_f32(*n),
695        WireValue::String(s) => ValueWord::from_string(Arc::new(s.clone())),
696
697        WireValue::Timestamp(ts) => match chrono::DateTime::from_timestamp_millis(*ts) {
698            Some(dt) => ValueWord::from_time_utc(dt),
699            None => ValueWord::none(),
700        },
701
702        WireValue::Duration { value, unit } => {
703            let (ast_value, ast_unit) = match unit {
704                WireDurationUnit::Nanoseconds => (
705                    *value / 1_000_000_000.0,
706                    shape_ast::ast::DurationUnit::Seconds,
707                ),
708                WireDurationUnit::Microseconds => {
709                    (*value / 1_000_000.0, shape_ast::ast::DurationUnit::Seconds)
710                }
711                WireDurationUnit::Milliseconds => {
712                    (*value / 1_000.0, shape_ast::ast::DurationUnit::Seconds)
713                }
714                WireDurationUnit::Seconds => (*value, shape_ast::ast::DurationUnit::Seconds),
715                WireDurationUnit::Minutes => (*value, shape_ast::ast::DurationUnit::Minutes),
716                WireDurationUnit::Hours => (*value, shape_ast::ast::DurationUnit::Hours),
717                WireDurationUnit::Days => (*value, shape_ast::ast::DurationUnit::Days),
718                WireDurationUnit::Weeks => (*value, shape_ast::ast::DurationUnit::Weeks),
719            };
720            ValueWord::from_duration(shape_ast::ast::Duration {
721                value: ast_value,
722                unit: ast_unit,
723            })
724        }
725
726        WireValue::Array(arr) => {
727            let elements: Vec<ValueWord> = arr.iter().map(wire_to_nb).collect();
728            ValueWord::from_array(Arc::new(elements))
729        }
730
731        WireValue::Object(obj) => {
732            // Check for enum encoding
733            let enum_name = obj.get("__enum").and_then(|v| match v {
734                WireValue::String(s) => Some(s.clone()),
735                _ => None,
736            });
737            let variant = obj.get("__variant").and_then(|v| match v {
738                WireValue::String(s) => Some(s.clone()),
739                _ => None,
740            });
741
742            if let (Some(enum_name), Some(variant)) = (enum_name, variant) {
743                let payload = match obj.get("__fields") {
744                    None => shape_value::EnumPayload::Unit,
745                    Some(WireValue::Array(values)) => {
746                        shape_value::EnumPayload::Tuple(values.iter().map(wire_to_nb).collect())
747                    }
748                    Some(WireValue::Object(fields)) => {
749                        let map: std::collections::HashMap<String, ValueWord> = fields
750                            .iter()
751                            .map(|(k, v)| (k.clone(), wire_to_nb(v)))
752                            .collect();
753                        shape_value::EnumPayload::Struct(map)
754                    }
755                    _ => shape_value::EnumPayload::Unit,
756                };
757                ValueWord::from_enum(shape_value::EnumValue {
758                    enum_name,
759                    variant,
760                    payload,
761                })
762            } else {
763                // Regular object -> TypedObject
764                let pairs: Vec<(String, ValueWord)> = obj
765                    .iter()
766                    .map(|(k, v)| (k.clone(), wire_to_nb(v)))
767                    .collect();
768                let pair_refs: Vec<(&str, ValueWord)> =
769                    pairs.iter().map(|(k, v)| (k.as_str(), v.clone())).collect();
770                crate::type_schema::typed_object_from_nb_pairs(&pair_refs)
771            }
772        }
773
774        WireValue::Table(table) => {
775            match datatable_from_ipc_bytes(
776                &table.ipc_bytes,
777                table.type_name.as_deref(),
778                table.schema_id,
779            ) {
780                Ok(dt) => ValueWord::from_datatable(Arc::new(dt)),
781                Err(_) => ValueWord::none(),
782            }
783        }
784
785        WireValue::Result { ok, value } => {
786            let inner = wire_to_nb(value);
787            if *ok {
788                ValueWord::from_ok(inner)
789            } else {
790                ValueWord::from_err(inner)
791            }
792        }
793
794        WireValue::Range {
795            start,
796            end,
797            inclusive,
798        } => ValueWord::from_range(
799            start.as_ref().map(|v| wire_to_nb(v)),
800            end.as_ref().map(|v| wire_to_nb(v)),
801            *inclusive,
802        ),
803
804        WireValue::FunctionRef { name } => ValueWord::from_function_ref(name.clone(), None),
805
806        WireValue::PrintResult(result) => {
807            // Convert back as rendered string (same as wire_to_value)
808            ValueWord::from_string(Arc::new(result.rendered.clone()))
809        }
810
811        WireValue::Content(node) => {
812            ValueWord::from_heap_value(HeapValue::Content(Box::new(node.clone())))
813        }
814    }
815}
816
817/// Convert a ValueWord value to a ValueEnvelope with full metadata.
818pub fn nb_to_envelope(nb: &ValueWord, type_name: &str, ctx: &Context) -> ValueEnvelope {
819    let wire_value = nb_to_wire(nb, ctx);
820    let type_info = TypeInfo::primitive(type_name);
821    let registry = TypeRegistry::new("Default");
822    ValueEnvelope::new(wire_value, type_info, registry)
823}
824
825/// Extract type info and wire value from a ValueWord value.
826///
827/// ValueWord-native equivalent of [`extract_typed_value`].
828pub fn nb_extract_typed_value(nb: &ValueWord, ctx: &Context) -> (WireValue, Option<TypeInfo>) {
829    let wire_value = nb_to_wire(nb, ctx);
830    let type_name = nb.type_name();
831    let type_info = TypeInfo::primitive(type_name);
832    (wire_value, Some(type_info))
833}
834
835/// Convert a ValueWord value to ValueEnvelope with inferred type information.
836///
837/// ValueWord-native equivalent of [`typed_value_to_envelope`].
838pub fn nb_typed_value_to_envelope(nb: &ValueWord, ctx: &Context) -> ValueEnvelope {
839    let type_name = nb.type_name();
840    nb_to_envelope(nb, type_name, ctx)
841}
842
843/// Convert an EnumValue to WireValue (shared between ValueWord and ValueWord paths)
844fn enum_to_wire(enum_value: &shape_value::EnumValue, ctx: &Context) -> WireValue {
845    let mut obj = BTreeMap::new();
846    obj.insert(
847        "__enum".to_string(),
848        WireValue::String(enum_value.enum_name.clone()),
849    );
850    obj.insert(
851        "__variant".to_string(),
852        WireValue::String(enum_value.variant.clone()),
853    );
854    match &enum_value.payload {
855        shape_value::EnumPayload::Unit => {}
856        shape_value::EnumPayload::Tuple(values) => {
857            obj.insert(
858                "__fields".to_string(),
859                WireValue::Array(values.iter().map(|v| nb_to_wire(v, ctx)).collect()),
860            );
861        }
862        shape_value::EnumPayload::Struct(fields) => {
863            let field_map: BTreeMap<String, WireValue> = fields
864                .iter()
865                .map(|(k, v)| (k.clone(), nb_to_wire(v, ctx)))
866                .collect();
867            obj.insert("__fields".to_string(), WireValue::Object(field_map));
868        }
869    }
870    WireValue::Object(obj)
871}
872
873fn datatable_to_wire(dt: &DataTable) -> WireValue {
874    datatable_to_wire_with_schema(dt, dt.schema_id())
875}
876
877fn datatable_to_wire_with_schema(dt: &DataTable, schema_id: Option<u32>) -> WireValue {
878    match datatable_to_ipc_bytes(dt) {
879        Ok(ipc_bytes) => WireValue::Table(WireTable {
880            ipc_bytes,
881            type_name: dt.type_name().map(|s| s.to_string()),
882            schema_id,
883            row_count: dt.row_count(),
884            column_count: dt.column_count(),
885        }),
886        Err(_) => WireValue::String(format!("{}", dt)),
887    }
888}
889
890/// Serialize a [`DataTable`] to Arrow IPC bytes.
891pub fn datatable_to_ipc_bytes(dt: &DataTable) -> std::result::Result<Vec<u8>, String> {
892    let mut buf = Vec::new();
893    let schema = dt.inner().schema();
894    let mut writer = FileWriter::try_new(&mut buf, schema.as_ref())
895        .map_err(|e| format!("failed to create Arrow IPC writer: {e}"))?;
896    writer
897        .write(dt.inner())
898        .map_err(|e| format!("failed to write Arrow IPC batch: {e}"))?;
899    writer
900        .finish()
901        .map_err(|e| format!("failed to finalize Arrow IPC writer: {e}"))?;
902    Ok(buf)
903}
904
905/// Deserialize Arrow IPC bytes into a [`DataTable`].
906pub fn datatable_from_ipc_bytes(
907    ipc_bytes: &[u8],
908    type_name: Option<&str>,
909    schema_id: Option<u32>,
910) -> std::result::Result<DataTable, String> {
911    if ipc_bytes.is_empty() {
912        return Err("empty Arrow IPC payload".to_string());
913    }
914
915    let cursor = std::io::Cursor::new(ipc_bytes);
916    let mut reader = FileReader::try_new(cursor, None)
917        .map_err(|e| format!("failed to create Arrow IPC reader: {e}"))?;
918    let batch = reader
919        .next()
920        .transpose()
921        .map_err(|e| format!("failed reading Arrow IPC batch: {e}"))?
922        .ok_or_else(|| "Arrow IPC payload has no record batches".to_string())?;
923
924    let mut dt = DataTable::new(batch);
925    if let Some(name) = type_name {
926        dt = DataTable::with_type_name(dt.into_inner(), name.to_string());
927    }
928    if let Some(id) = schema_id {
929        dt = dt.with_schema_id(id);
930    }
931    Ok(dt)
932}
933
934/// Helper to infer full metadata including from registry
935/// Note: Meta definitions have been removed; formatting now uses Display trait.
936fn infer_metadata_with_ctx(
937    _value: &ValueWord,
938    type_name: &str,
939    _ctx: &Context,
940) -> (TypeInfo, TypeRegistry) {
941    let type_info = TypeInfo::primitive(type_name);
942    let registry = TypeRegistry::new("Default");
943    (type_info, registry)
944}
945
946#[cfg(test)]
947mod tests {
948    use super::*;
949    use crate::type_methods::TypeMethodRegistry;
950    use crate::type_schema::typed_object_to_hashmap_nb;
951    use shape_value::ValueSlot;
952    use shape_value::heap_value::HeapValue;
953    use std::sync::Arc;
954
955    fn get_dummy_context() -> Context {
956        Context::new_empty_with_registry(Arc::new(TypeMethodRegistry::new()))
957    }
958
959    #[test]
960    fn test_basic_value_conversion() {
961        let ctx = get_dummy_context();
962        // Number
963        let wire = value_to_wire(&ValueWord::from_f64(42.5), &ctx);
964        assert_eq!(wire, WireValue::Number(42.5));
965
966        // Whole number remains number
967        let wire = value_to_wire(&ValueWord::from_f64(42.0), &ctx);
968        assert_eq!(wire, WireValue::Number(42.0));
969
970        // String
971        let wire = value_to_wire(&ValueWord::from_string(Arc::new("hello".to_string())), &ctx);
972        assert_eq!(wire, WireValue::String("hello".to_string()));
973
974        // Bool
975        let wire = value_to_wire(&ValueWord::from_bool(true), &ctx);
976        assert_eq!(wire, WireValue::Bool(true));
977
978        // None
979        let wire = value_to_wire(&ValueWord::none(), &ctx);
980        assert_eq!(wire, WireValue::Null);
981    }
982
983    #[test]
984    fn test_array_conversion() {
985        let ctx = get_dummy_context();
986        let arr = ValueWord::from_array(Arc::new(vec![
987            ValueWord::from_f64(1.0),
988            ValueWord::from_f64(2.0),
989            ValueWord::from_f64(3.0),
990        ]));
991        let wire = value_to_wire(&arr, &ctx);
992
993        if let WireValue::Array(items) = wire {
994            assert_eq!(items.len(), 3);
995            assert_eq!(items[0], WireValue::Number(1.0));
996            assert_eq!(items[1], WireValue::Number(2.0));
997            assert_eq!(items[2], WireValue::Number(3.0));
998        } else {
999            panic!("Expected Array");
1000        }
1001    }
1002
1003    #[test]
1004    fn test_object_conversion() {
1005        let ctx = get_dummy_context();
1006        let obj = crate::type_schema::typed_object_from_pairs(&[
1007            ("x", ValueWord::from_f64(10.0)),
1008            ("y", ValueWord::from_f64(20.0)),
1009        ]);
1010
1011        let wire = value_to_wire(&obj, &ctx);
1012
1013        if let WireValue::Object(map) = wire {
1014            assert_eq!(map.get("x"), Some(&WireValue::Number(10.0)));
1015            assert_eq!(map.get("y"), Some(&WireValue::Number(20.0)));
1016        } else {
1017            panic!("Expected Object");
1018        }
1019    }
1020
1021    #[test]
1022    fn test_builtin_typed_object_converts_to_wire_object() {
1023        let ctx = get_dummy_context();
1024        let any_error_schema_id = ctx
1025            .type_schema_registry()
1026            .get("__AnyError")
1027            .expect("__AnyError schema should exist")
1028            .id as u64;
1029
1030        let slots = vec![
1031            ValueSlot::from_heap(HeapValue::String(Arc::new("AnyError".to_string()))),
1032            ValueSlot::from_heap(HeapValue::String(Arc::new("boom".to_string()))),
1033            ValueSlot::none(), // cause: None (inline)
1034            ValueSlot::none(), // trace_info: None (inline)
1035            ValueSlot::from_heap(HeapValue::String(Arc::new("boom".to_string()))),
1036            ValueSlot::from_heap(HeapValue::String(Arc::new("E_BANG".to_string()))),
1037        ];
1038
1039        let nb = ValueWord::from_heap_value(HeapValue::TypedObject {
1040            schema_id: any_error_schema_id,
1041            slots: slots.into_boxed_slice(),
1042            heap_mask: 0b11_0011, // bits 0,1,4,5 = heap; bits 2,3 = inline none
1043        });
1044
1045        let wire = nb_to_wire(&nb, &ctx);
1046        match wire {
1047            WireValue::Object(map) => {
1048                assert_eq!(
1049                    map.get("category"),
1050                    Some(&WireValue::String("AnyError".into()))
1051                );
1052                assert_eq!(map.get("message"), Some(&WireValue::String("boom".into())));
1053                assert_eq!(map.get("code"), Some(&WireValue::String("E_BANG".into())));
1054            }
1055            other => panic!("Expected WireValue::Object, got {:?}", other),
1056        }
1057    }
1058
1059    #[test]
1060    fn test_unknown_schema_anyerror_uses_builtin_fallback_decoder() {
1061        let ctx = get_dummy_context();
1062
1063        let slots = vec![
1064            ValueSlot::from_heap(HeapValue::String(Arc::new("AnyError".to_string()))),
1065            ValueSlot::from_heap(HeapValue::String(Arc::new("boom".to_string()))),
1066            ValueSlot::none(), // cause: None (inline)
1067            ValueSlot::none(), // trace_info: None (inline)
1068            ValueSlot::from_heap(HeapValue::String(Arc::new("boom".to_string()))),
1069            ValueSlot::from_heap(HeapValue::String(Arc::new("E_BANG".to_string()))),
1070        ];
1071
1072        let nb = ValueWord::from_heap_value(HeapValue::TypedObject {
1073            schema_id: 9_999_999,
1074            slots: slots.into_boxed_slice(),
1075            heap_mask: 0b11_0011, // bits 0,1,4,5 = heap; bits 2,3 = inline none
1076        });
1077
1078        let wire = nb_to_wire(&nb, &ctx);
1079        match wire {
1080            WireValue::Object(map) => {
1081                assert_eq!(
1082                    map.get("category"),
1083                    Some(&WireValue::String("AnyError".into()))
1084                );
1085                assert_eq!(map.get("message"), Some(&WireValue::String("boom".into())));
1086                assert_eq!(map.get("code"), Some(&WireValue::String("E_BANG".into())));
1087            }
1088            other => panic!("Expected WireValue::Object, got {:?}", other),
1089        }
1090    }
1091
1092    #[test]
1093    fn test_timestamp_conversion() {
1094        let ctx = get_dummy_context();
1095        use chrono::TimeZone;
1096        let dt = chrono::Utc
1097            .with_ymd_and_hms(2024, 1, 15, 10, 30, 0)
1098            .unwrap();
1099        let wire = value_to_wire(&ValueWord::from_time_utc(dt), &ctx);
1100
1101        if let WireValue::Timestamp(ts) = wire {
1102            assert_eq!(ts, 1705314600000);
1103        } else {
1104            panic!("Expected Timestamp");
1105        }
1106    }
1107
1108    #[test]
1109    fn test_result_conversion() {
1110        let ctx = get_dummy_context();
1111        let ok_val = ValueWord::from_ok(ValueWord::from_f64(42.0));
1112        let wire = value_to_wire(&ok_val, &ctx);
1113
1114        if let WireValue::Result { ok, value } = wire {
1115            assert!(ok);
1116            assert_eq!(*value, WireValue::Number(42.0));
1117        } else {
1118            panic!("Expected Result");
1119        }
1120    }
1121
1122    #[test]
1123    fn test_wire_to_nb_anyerror_trace_frame_key_order_is_stable() {
1124        use std::collections::BTreeMap;
1125
1126        let mut frame = BTreeMap::new();
1127        frame.insert(
1128            "function".to_string(),
1129            WireValue::String("duckdb.connect".to_string()),
1130        );
1131        frame.insert(
1132            "file".to_string(),
1133            WireValue::String("extension:duckdb".to_string()),
1134        );
1135        frame.insert("line".to_string(), WireValue::Null);
1136        frame.insert("ip".to_string(), WireValue::Null);
1137
1138        let nb = wire_to_nb(&WireValue::Object(frame));
1139        let decoded =
1140            typed_object_to_hashmap_nb(&nb).expect("Trace frame wire object should decode");
1141        assert_eq!(
1142            decoded.get("function").and_then(|v| v.as_str()),
1143            Some("duckdb.connect")
1144        );
1145        assert_eq!(
1146            decoded.get("file").and_then(|v| v.as_str()),
1147            Some("extension:duckdb")
1148        );
1149    }
1150
1151    #[test]
1152    fn test_any_error_result_roundtrip() {
1153        use crate::type_schema::typed_object_from_pairs;
1154        let ctx = get_dummy_context();
1155
1156        let empty_trace = typed_object_from_pairs(&[]);
1157        let cause = typed_object_from_pairs(&[
1158            (
1159                "category",
1160                ValueWord::from_string(Arc::new("AnyError".to_string())),
1161            ),
1162            (
1163                "payload",
1164                ValueWord::from_string(Arc::new("low level".to_string())),
1165            ),
1166            ("cause", ValueWord::none()),
1167            ("trace_info", empty_trace),
1168        ]);
1169
1170        let empty_trace2 = typed_object_from_pairs(&[]);
1171        let outer = typed_object_from_pairs(&[
1172            (
1173                "category",
1174                ValueWord::from_string(Arc::new("AnyError".to_string())),
1175            ),
1176            (
1177                "payload",
1178                ValueWord::from_string(Arc::new("high level".to_string())),
1179            ),
1180            ("cause", cause),
1181            ("trace_info", empty_trace2),
1182            (
1183                "code",
1184                ValueWord::from_string(Arc::new("OPTION_NONE".to_string())),
1185            ),
1186        ]);
1187
1188        let err = ValueWord::from_err(outer);
1189        let wire = value_to_wire(&err, &ctx);
1190
1191        let WireValue::Result { ok, value } = &wire else {
1192            panic!("Expected wire Result");
1193        };
1194        assert!(!ok);
1195        match value.as_ref() {
1196            WireValue::Object(map) => {
1197                assert_eq!(
1198                    map.get("category"),
1199                    Some(&WireValue::String("AnyError".to_string()))
1200                );
1201                assert_eq!(
1202                    map.get("code"),
1203                    Some(&WireValue::String("OPTION_NONE".to_string()))
1204                );
1205            }
1206            other => panic!("Expected AnyError object payload, got {:?}", other),
1207        }
1208
1209        let roundtrip = wire_to_nb(&wire);
1210        let hv = roundtrip.as_heap_ref().expect("Expected heap value");
1211        match hv {
1212            HeapValue::Err(inner) => {
1213                let inner_hv = inner.as_heap_ref().expect("Expected heap inner");
1214                assert!(
1215                    matches!(inner_hv, HeapValue::TypedObject { .. }),
1216                    "Expected TypedObject inside Err"
1217                );
1218            }
1219            other => panic!("Expected Err, got {:?}", other.kind()),
1220        }
1221    }
1222
1223    #[test]
1224    fn test_envelope_creation() {
1225        let ctx = get_dummy_context();
1226        let nb = ValueWord::from_f64(3.14);
1227        let envelope = nb_to_envelope(&nb, "number", &ctx);
1228
1229        match &envelope.value {
1230            WireValue::Number(n) => assert!((*n - 3.14).abs() < f64::EPSILON),
1231            other => panic!("Expected Number, got {:?}", other),
1232        }
1233    }
1234
1235    #[test]
1236    fn test_roundtrip_basic() {
1237        let ctx = get_dummy_context();
1238        let nb = ValueWord::from_string(Arc::new("test".to_string()));
1239        let wire = nb_to_wire(&nb, &ctx);
1240        let back = wire_to_nb(&wire);
1241
1242        if let Some(s) = back.as_str() {
1243            assert_eq!(s, "test");
1244        } else {
1245            panic!("Expected String");
1246        }
1247    }
1248
1249    // ===== ValueWord-native conversion tests =====
1250
1251    #[test]
1252    fn test_nb_to_wire_basic_types() {
1253        let ctx = get_dummy_context();
1254
1255        // f64 -> Number (fractional)
1256        let wire = nb_to_wire(&ValueWord::from_f64(42.5), &ctx);
1257        assert_eq!(wire, WireValue::Number(42.5));
1258
1259        // f64 whole -> Number
1260        let wire = nb_to_wire(&ValueWord::from_f64(42.0), &ctx);
1261        assert_eq!(wire, WireValue::Number(42.0));
1262
1263        // i48 -> Integer
1264        let wire = nb_to_wire(&ValueWord::from_i64(99), &ctx);
1265        assert_eq!(wire, WireValue::Integer(99));
1266
1267        // Negative i48 -> Integer
1268        let wire = nb_to_wire(&ValueWord::from_i64(-7), &ctx);
1269        assert_eq!(wire, WireValue::Integer(-7));
1270
1271        // Bool
1272        let wire = nb_to_wire(&ValueWord::from_bool(true), &ctx);
1273        assert_eq!(wire, WireValue::Bool(true));
1274
1275        let wire = nb_to_wire(&ValueWord::from_bool(false), &ctx);
1276        assert_eq!(wire, WireValue::Bool(false));
1277
1278        // None
1279        let wire = nb_to_wire(&ValueWord::none(), &ctx);
1280        assert_eq!(wire, WireValue::Null);
1281
1282        // Unit
1283        let wire = nb_to_wire(&ValueWord::unit(), &ctx);
1284        assert_eq!(wire, WireValue::Null);
1285    }
1286
1287    #[test]
1288    fn test_nb_to_wire_string() {
1289        let ctx = get_dummy_context();
1290        let nb = ValueWord::from_string(Arc::new("hello".to_string()));
1291        let wire = nb_to_wire(&nb, &ctx);
1292        assert_eq!(wire, WireValue::String("hello".to_string()));
1293    }
1294
1295    #[test]
1296    fn test_nb_to_wire_array() {
1297        let ctx = get_dummy_context();
1298        let nb = ValueWord::from_array(Arc::new(vec![
1299            ValueWord::from_f64(1.0),
1300            ValueWord::from_i64(2),
1301            ValueWord::from_bool(true),
1302        ]));
1303        let wire = nb_to_wire(&nb, &ctx);
1304
1305        if let WireValue::Array(items) = wire {
1306            assert_eq!(items.len(), 3);
1307            assert_eq!(items[0], WireValue::Number(1.0));
1308            assert_eq!(items[1], WireValue::Integer(2));
1309            assert_eq!(items[2], WireValue::Bool(true));
1310        } else {
1311            panic!("Expected Array");
1312        }
1313    }
1314
1315    #[test]
1316    fn test_nb_to_wire_result() {
1317        let ctx = get_dummy_context();
1318
1319        // Ok result
1320        let ok = ValueWord::from_ok(ValueWord::from_i64(42));
1321        let wire = nb_to_wire(&ok, &ctx);
1322        if let WireValue::Result { ok, value } = wire {
1323            assert!(ok);
1324            assert_eq!(*value, WireValue::Integer(42));
1325        } else {
1326            panic!("Expected Result");
1327        }
1328
1329        // Err result
1330        let err = ValueWord::from_err(ValueWord::from_string(Arc::new("oops".to_string())));
1331        let wire = nb_to_wire(&err, &ctx);
1332        if let WireValue::Result { ok, value } = wire {
1333            assert!(!ok);
1334            assert_eq!(*value, WireValue::String("oops".to_string()));
1335        } else {
1336            panic!("Expected Result");
1337        }
1338    }
1339
1340    #[test]
1341    fn test_nb_to_wire_some() {
1342        let ctx = get_dummy_context();
1343        let some = ValueWord::from_some(ValueWord::from_f64(3.14));
1344        let wire = nb_to_wire(&some, &ctx);
1345        // Some unwraps to inner value
1346        assert_eq!(wire, WireValue::Number(3.14));
1347    }
1348
1349    #[test]
1350    fn test_nb_to_wire_matches_vmvalue() {
1351        // Verify that nb_to_wire produces the same output as value_to_wire for basic types
1352        let ctx = get_dummy_context();
1353
1354        let test_values: Vec<ValueWord> = vec![
1355            ValueWord::from_f64(42.5),
1356            ValueWord::from_f64(42.0),
1357            ValueWord::from_i64(100),
1358            ValueWord::from_i64(-100),
1359            ValueWord::from_bool(true),
1360            ValueWord::from_bool(false),
1361            ValueWord::none(),
1362            ValueWord::unit(),
1363            ValueWord::from_string(Arc::new("test".to_string())),
1364            ValueWord::from_array(Arc::new(vec![
1365                ValueWord::from_f64(1.0),
1366                ValueWord::from_i64(2),
1367            ])),
1368        ];
1369
1370        for nb in &test_values {
1371            let vmv = nb.clone();
1372            let wire_from_vmv = value_to_wire(&vmv, &ctx);
1373            let wire_from_nb = nb_to_wire(nb, &ctx);
1374            assert_eq!(
1375                wire_from_vmv,
1376                wire_from_nb,
1377                "Mismatch for ValueWord type {:?}: ValueWord path = {:?}, ValueWord path = {:?}",
1378                nb.tag(),
1379                wire_from_vmv,
1380                wire_from_nb
1381            );
1382        }
1383    }
1384
1385    #[test]
1386    fn test_wire_to_nb_basic_types() {
1387        // Null -> None
1388        let nb = wire_to_nb(&WireValue::Null);
1389        assert!(nb.is_none());
1390
1391        // Bool
1392        let nb = wire_to_nb(&WireValue::Bool(true));
1393        assert_eq!(nb.as_bool(), Some(true));
1394
1395        // Number
1396        let nb = wire_to_nb(&WireValue::Number(3.14));
1397        assert_eq!(nb.as_f64(), Some(3.14));
1398
1399        // Integer
1400        let nb = wire_to_nb(&WireValue::Integer(42));
1401        assert_eq!(nb.as_i64(), Some(42));
1402
1403        // String
1404        let nb = wire_to_nb(&WireValue::String("hello".to_string()));
1405        assert_eq!(nb.as_str(), Some("hello"));
1406    }
1407
1408    #[test]
1409    fn test_wire_to_nb_array() {
1410        let wire = WireValue::Array(vec![
1411            WireValue::Integer(1),
1412            WireValue::Number(2.5),
1413            WireValue::Bool(false),
1414        ]);
1415        let nb = wire_to_nb(&wire);
1416        let arr = nb.as_any_array().expect("Expected array").to_generic();
1417        assert_eq!(arr.len(), 3);
1418        assert_eq!(arr[0].as_i64(), Some(1));
1419        assert_eq!(arr[1].as_f64(), Some(2.5));
1420        assert_eq!(arr[2].as_bool(), Some(false));
1421    }
1422
1423    #[test]
1424    fn test_wire_to_nb_result() {
1425        // Ok result
1426        let wire = WireValue::Result {
1427            ok: true,
1428            value: Box::new(WireValue::Integer(7)),
1429        };
1430        let nb = wire_to_nb(&wire);
1431        let inner = nb.as_ok_inner().expect("Expected Ok");
1432        assert_eq!(inner.as_i64(), Some(7));
1433
1434        // Err result
1435        let wire = WireValue::Result {
1436            ok: false,
1437            value: Box::new(WireValue::String("fail".to_string())),
1438        };
1439        let nb = wire_to_nb(&wire);
1440        let inner = nb.as_err_inner().expect("Expected Err");
1441        assert_eq!(inner.as_str(), Some("fail"));
1442    }
1443
1444    #[test]
1445    fn test_nb_roundtrip_basic() {
1446        let ctx = get_dummy_context();
1447
1448        // String roundtrip
1449        let original = ValueWord::from_string(Arc::new("roundtrip".to_string()));
1450        let wire = nb_to_wire(&original, &ctx);
1451        let back = wire_to_nb(&wire);
1452        assert_eq!(back.as_str(), Some("roundtrip"));
1453
1454        // Integer roundtrip
1455        let original = ValueWord::from_i64(12345);
1456        let wire = nb_to_wire(&original, &ctx);
1457        let back = wire_to_nb(&wire);
1458        assert_eq!(back.as_i64(), Some(12345));
1459
1460        // Float roundtrip
1461        let original = ValueWord::from_f64(2.718);
1462        let wire = nb_to_wire(&original, &ctx);
1463        let back = wire_to_nb(&wire);
1464        assert_eq!(back.as_f64(), Some(2.718));
1465
1466        // Bool roundtrip
1467        let original = ValueWord::from_bool(true);
1468        let wire = nb_to_wire(&original, &ctx);
1469        let back = wire_to_nb(&wire);
1470        assert_eq!(back.as_bool(), Some(true));
1471
1472        // None roundtrip
1473        let wire = nb_to_wire(&ValueWord::none(), &ctx);
1474        let back = wire_to_nb(&wire);
1475        assert!(back.is_none());
1476    }
1477
1478    #[test]
1479    fn test_nb_roundtrip_array() {
1480        let ctx = get_dummy_context();
1481        let original = ValueWord::from_array(Arc::new(vec![
1482            ValueWord::from_i64(10),
1483            ValueWord::from_f64(20.5),
1484            ValueWord::from_string(Arc::new("x".to_string())),
1485        ]));
1486        let wire = nb_to_wire(&original, &ctx);
1487        let back = wire_to_nb(&wire);
1488        let arr = back.as_any_array().expect("Expected array").to_generic();
1489        assert_eq!(arr.len(), 3);
1490        assert_eq!(arr[0].as_i64(), Some(10));
1491        assert_eq!(arr[1].as_f64(), Some(20.5));
1492        assert_eq!(arr[2].as_str(), Some("x"));
1493    }
1494
1495    #[test]
1496    fn test_nb_to_wire_decimal() {
1497        let ctx = get_dummy_context();
1498        let d = rust_decimal::Decimal::new(314, 2); // 3.14
1499        let nb = ValueWord::from_decimal(d);
1500        let wire = nb_to_wire(&nb, &ctx);
1501        assert_eq!(wire, WireValue::Number(3.14));
1502    }
1503
1504    #[test]
1505    fn test_nb_to_wire_typed_object() {
1506        let ctx = get_dummy_context();
1507        let obj = crate::type_schema::typed_object_from_pairs(&[
1508            ("a", ValueWord::from_f64(1.0)),
1509            ("b", ValueWord::from_string(Arc::new("two".to_string()))),
1510        ]);
1511        let nb = obj;
1512        let wire = nb_to_wire(&nb, &ctx);
1513
1514        if let WireValue::Object(map) = wire {
1515            assert_eq!(map.get("a"), Some(&WireValue::Number(1.0)));
1516            assert_eq!(map.get("b"), Some(&WireValue::String("two".to_string())));
1517        } else {
1518            panic!("Expected Object, got {:?}", wire);
1519        }
1520    }
1521
1522    #[test]
1523    fn test_nb_envelope_creation() {
1524        let ctx = get_dummy_context();
1525        let nb = ValueWord::from_f64(3.14);
1526        let envelope = nb_to_envelope(&nb, "Number", &ctx);
1527        assert_eq!(envelope.type_info.name, "Number");
1528    }
1529
1530    #[test]
1531    fn test_native_scalar_wire_roundtrip_preserves_width() {
1532        let ctx = get_dummy_context();
1533        let cases = vec![
1534            (
1535                ValueWord::from_native_i8(-8),
1536                WireValue::I8(-8),
1537                shape_value::heap_value::NativeScalar::I8(-8),
1538            ),
1539            (
1540                ValueWord::from_native_u8(255),
1541                WireValue::U8(255),
1542                shape_value::heap_value::NativeScalar::U8(255),
1543            ),
1544            (
1545                ValueWord::from_native_i16(-1024),
1546                WireValue::I16(-1024),
1547                shape_value::heap_value::NativeScalar::I16(-1024),
1548            ),
1549            (
1550                ValueWord::from_native_u16(65530),
1551                WireValue::U16(65530),
1552                shape_value::heap_value::NativeScalar::U16(65530),
1553            ),
1554            (
1555                ValueWord::from_native_i32(-123_456),
1556                WireValue::I32(-123_456),
1557                shape_value::heap_value::NativeScalar::I32(-123_456),
1558            ),
1559            (
1560                ValueWord::from_native_u32(4_000_000_000),
1561                WireValue::U32(4_000_000_000),
1562                shape_value::heap_value::NativeScalar::U32(4_000_000_000),
1563            ),
1564            (
1565                ValueWord::from_native_scalar(shape_value::heap_value::NativeScalar::I64(
1566                    -9_223_372_036_854_775_000,
1567                )),
1568                WireValue::I64(-9_223_372_036_854_775_000),
1569                shape_value::heap_value::NativeScalar::I64(-9_223_372_036_854_775_000),
1570            ),
1571            (
1572                ValueWord::from_native_u64(18_000_000_000),
1573                WireValue::U64(18_000_000_000),
1574                shape_value::heap_value::NativeScalar::U64(18_000_000_000),
1575            ),
1576            (
1577                ValueWord::from_native_isize(12345isize),
1578                WireValue::Isize(12345),
1579                shape_value::heap_value::NativeScalar::Isize(12345isize),
1580            ),
1581            (
1582                ValueWord::from_native_usize(54321usize),
1583                WireValue::Usize(54321),
1584                shape_value::heap_value::NativeScalar::Usize(54321usize),
1585            ),
1586            (
1587                ValueWord::from_native_ptr(0x1234usize),
1588                WireValue::Ptr(0x1234),
1589                shape_value::heap_value::NativeScalar::Ptr(0x1234usize),
1590            ),
1591            (
1592                ValueWord::from_native_f32(3.5f32),
1593                WireValue::F32(3.5f32),
1594                shape_value::heap_value::NativeScalar::F32(3.5f32),
1595            ),
1596        ];
1597
1598        for (nb, expected_wire, expected_scalar) in cases {
1599            let wire = nb_to_wire(&nb, &ctx);
1600            assert_eq!(wire, expected_wire);
1601
1602            let roundtrip = wire_to_nb(&wire);
1603            assert_eq!(roundtrip.as_native_scalar(), Some(expected_scalar));
1604        }
1605    }
1606}