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