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::String(format!("{}", node)),
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/// If the ValueWord value is a Content node, render it as JSON, HTML, and terminal strings.
584///
585/// Returns `(content_json, content_html, content_terminal)` — all `None` if the value is not Content.
586pub fn nb_extract_content(
587    nb: &ValueWord,
588) -> (Option<serde_json::Value>, Option<String>, Option<String>) {
589    use crate::content_renderer::ContentRenderer;
590
591    // Check if value is already a Content node
592    let node: Option<shape_value::content::ContentNode> =
593        if let Some(HeapValue::Content(node)) = nb.as_heap_ref() {
594            Some(node.as_ref().clone())
595        } else if let Some(HeapValue::DataTable(dt)) = nb.as_heap_ref() {
596            // Auto-wrap DataTable as ContentNode::Table
597            Some(crate::content_dispatch::datatable_to_content_node(dt, None))
598        } else if let Some(HeapValue::TypedTable { table, .. }) = nb.as_heap_ref() {
599            Some(crate::content_dispatch::datatable_to_content_node(
600                table, None,
601            ))
602        } else {
603            None
604        };
605
606    if let Some(ref node) = node {
607        let json_renderer = crate::renderers::json::JsonRenderer;
608        let html_renderer = crate::renderers::html::HtmlRenderer::new();
609        let terminal_renderer = crate::renderers::terminal::TerminalRenderer::new();
610
611        let json_str = json_renderer.render(node);
612        let json_value = serde_json::from_str(&json_str).unwrap_or(serde_json::Value::Null);
613        let html_str = html_renderer.render(node);
614        let terminal_str = terminal_renderer.render(node);
615
616        (Some(json_value), Some(html_str), Some(terminal_str))
617    } else {
618        (None, None, None)
619    }
620}
621
622/// Convert a WireValue to a ValueWord value without ValueWord intermediate.
623///
624/// This is the ValueWord-native equivalent of `wire_to_value`. For simple types
625/// (null, bool, number, integer) it constructs ValueWord inline values directly.
626pub fn wire_to_nb(wire: &WireValue) -> ValueWord {
627    match wire {
628        WireValue::Null => ValueWord::none(),
629        WireValue::Bool(b) => ValueWord::from_bool(*b),
630        WireValue::Number(n) => ValueWord::from_f64(*n),
631        WireValue::Integer(i) => ValueWord::from_i64(*i),
632        WireValue::I8(n) => ValueWord::from_native_i8(*n),
633        WireValue::U8(n) => ValueWord::from_native_u8(*n),
634        WireValue::I16(n) => ValueWord::from_native_i16(*n),
635        WireValue::U16(n) => ValueWord::from_native_u16(*n),
636        WireValue::I32(n) => ValueWord::from_native_i32(*n),
637        WireValue::U32(n) => ValueWord::from_native_u32(*n),
638        WireValue::I64(n) => {
639            ValueWord::from_native_scalar(shape_value::heap_value::NativeScalar::I64(*n))
640        }
641        WireValue::U64(n) => ValueWord::from_native_u64(*n),
642        WireValue::Isize(n) => match isize::try_from(*n) {
643            Ok(v) => ValueWord::from_native_isize(v),
644            Err(_) => ValueWord::none(),
645        },
646        WireValue::Usize(n) => match usize::try_from(*n) {
647            Ok(v) => ValueWord::from_native_usize(v),
648            Err(_) => ValueWord::none(),
649        },
650        WireValue::Ptr(n) => match usize::try_from(*n) {
651            Ok(v) => ValueWord::from_native_scalar(shape_value::heap_value::NativeScalar::Ptr(v)),
652            Err(_) => ValueWord::none(),
653        },
654        WireValue::F32(n) => ValueWord::from_native_f32(*n),
655        WireValue::String(s) => ValueWord::from_string(Arc::new(s.clone())),
656
657        WireValue::Timestamp(ts) => match chrono::DateTime::from_timestamp_millis(*ts) {
658            Some(dt) => ValueWord::from_time_utc(dt),
659            None => ValueWord::none(),
660        },
661
662        WireValue::Duration { value, unit } => {
663            let (ast_value, ast_unit) = match unit {
664                WireDurationUnit::Nanoseconds => (
665                    *value / 1_000_000_000.0,
666                    shape_ast::ast::DurationUnit::Seconds,
667                ),
668                WireDurationUnit::Microseconds => {
669                    (*value / 1_000_000.0, shape_ast::ast::DurationUnit::Seconds)
670                }
671                WireDurationUnit::Milliseconds => {
672                    (*value / 1_000.0, shape_ast::ast::DurationUnit::Seconds)
673                }
674                WireDurationUnit::Seconds => (*value, shape_ast::ast::DurationUnit::Seconds),
675                WireDurationUnit::Minutes => (*value, shape_ast::ast::DurationUnit::Minutes),
676                WireDurationUnit::Hours => (*value, shape_ast::ast::DurationUnit::Hours),
677                WireDurationUnit::Days => (*value, shape_ast::ast::DurationUnit::Days),
678                WireDurationUnit::Weeks => (*value, shape_ast::ast::DurationUnit::Weeks),
679            };
680            ValueWord::from_duration(shape_ast::ast::Duration {
681                value: ast_value,
682                unit: ast_unit,
683            })
684        }
685
686        WireValue::Array(arr) => {
687            let elements: Vec<ValueWord> = arr.iter().map(wire_to_nb).collect();
688            ValueWord::from_array(Arc::new(elements))
689        }
690
691        WireValue::Object(obj) => {
692            // Check for enum encoding
693            let enum_name = obj.get("__enum").and_then(|v| match v {
694                WireValue::String(s) => Some(s.clone()),
695                _ => None,
696            });
697            let variant = obj.get("__variant").and_then(|v| match v {
698                WireValue::String(s) => Some(s.clone()),
699                _ => None,
700            });
701
702            if let (Some(enum_name), Some(variant)) = (enum_name, variant) {
703                let payload = match obj.get("__fields") {
704                    None => shape_value::EnumPayload::Unit,
705                    Some(WireValue::Array(values)) => {
706                        shape_value::EnumPayload::Tuple(values.iter().map(wire_to_nb).collect())
707                    }
708                    Some(WireValue::Object(fields)) => {
709                        let map: std::collections::HashMap<String, ValueWord> = fields
710                            .iter()
711                            .map(|(k, v)| (k.clone(), wire_to_nb(v)))
712                            .collect();
713                        shape_value::EnumPayload::Struct(map)
714                    }
715                    _ => shape_value::EnumPayload::Unit,
716                };
717                ValueWord::from_enum(shape_value::EnumValue {
718                    enum_name,
719                    variant,
720                    payload,
721                })
722            } else {
723                // Regular object -> TypedObject
724                let pairs: Vec<(String, ValueWord)> = obj
725                    .iter()
726                    .map(|(k, v)| (k.clone(), wire_to_nb(v)))
727                    .collect();
728                let pair_refs: Vec<(&str, ValueWord)> =
729                    pairs.iter().map(|(k, v)| (k.as_str(), v.clone())).collect();
730                crate::type_schema::typed_object_from_nb_pairs(&pair_refs)
731            }
732        }
733
734        WireValue::Table(table) => {
735            match datatable_from_ipc_bytes(
736                &table.ipc_bytes,
737                table.type_name.as_deref(),
738                table.schema_id,
739            ) {
740                Ok(dt) => ValueWord::from_datatable(Arc::new(dt)),
741                Err(_) => ValueWord::none(),
742            }
743        }
744
745        WireValue::Result { ok, value } => {
746            let inner = wire_to_nb(value);
747            if *ok {
748                ValueWord::from_ok(inner)
749            } else {
750                ValueWord::from_err(inner)
751            }
752        }
753
754        WireValue::Range {
755            start,
756            end,
757            inclusive,
758        } => ValueWord::from_range(
759            start.as_ref().map(|v| wire_to_nb(v)),
760            end.as_ref().map(|v| wire_to_nb(v)),
761            *inclusive,
762        ),
763
764        WireValue::FunctionRef { name } => ValueWord::from_function_ref(name.clone(), None),
765
766        WireValue::PrintResult(result) => {
767            // Convert back as rendered string (same as wire_to_value)
768            ValueWord::from_string(Arc::new(result.rendered.clone()))
769        }
770    }
771}
772
773/// Convert a ValueWord value to a ValueEnvelope with full metadata.
774pub fn nb_to_envelope(nb: &ValueWord, type_name: &str, ctx: &Context) -> ValueEnvelope {
775    let wire_value = nb_to_wire(nb, ctx);
776    let type_info = TypeInfo::primitive(type_name);
777    let registry = TypeRegistry::new("Default");
778    ValueEnvelope::new(wire_value, type_info, registry)
779}
780
781/// Extract type info and wire value from a ValueWord value.
782///
783/// ValueWord-native equivalent of [`extract_typed_value`].
784pub fn nb_extract_typed_value(nb: &ValueWord, ctx: &Context) -> (WireValue, Option<TypeInfo>) {
785    let wire_value = nb_to_wire(nb, ctx);
786    let type_name = nb.type_name();
787    let type_info = TypeInfo::primitive(type_name);
788    (wire_value, Some(type_info))
789}
790
791/// Convert a ValueWord value to ValueEnvelope with inferred type information.
792///
793/// ValueWord-native equivalent of [`typed_value_to_envelope`].
794pub fn nb_typed_value_to_envelope(nb: &ValueWord, ctx: &Context) -> ValueEnvelope {
795    let type_name = nb.type_name();
796    nb_to_envelope(nb, type_name, ctx)
797}
798
799/// Convert an EnumValue to WireValue (shared between ValueWord and ValueWord paths)
800fn enum_to_wire(enum_value: &shape_value::EnumValue, ctx: &Context) -> WireValue {
801    let mut obj = BTreeMap::new();
802    obj.insert(
803        "__enum".to_string(),
804        WireValue::String(enum_value.enum_name.clone()),
805    );
806    obj.insert(
807        "__variant".to_string(),
808        WireValue::String(enum_value.variant.clone()),
809    );
810    match &enum_value.payload {
811        shape_value::EnumPayload::Unit => {}
812        shape_value::EnumPayload::Tuple(values) => {
813            obj.insert(
814                "__fields".to_string(),
815                WireValue::Array(values.iter().map(|v| nb_to_wire(v, ctx)).collect()),
816            );
817        }
818        shape_value::EnumPayload::Struct(fields) => {
819            let field_map: BTreeMap<String, WireValue> = fields
820                .iter()
821                .map(|(k, v)| (k.clone(), nb_to_wire(v, ctx)))
822                .collect();
823            obj.insert("__fields".to_string(), WireValue::Object(field_map));
824        }
825    }
826    WireValue::Object(obj)
827}
828
829fn datatable_to_wire(dt: &DataTable) -> WireValue {
830    datatable_to_wire_with_schema(dt, dt.schema_id())
831}
832
833fn datatable_to_wire_with_schema(dt: &DataTable, schema_id: Option<u32>) -> WireValue {
834    match datatable_to_ipc_bytes(dt) {
835        Ok(ipc_bytes) => WireValue::Table(WireTable {
836            ipc_bytes,
837            type_name: dt.type_name().map(|s| s.to_string()),
838            schema_id,
839            row_count: dt.row_count(),
840            column_count: dt.column_count(),
841        }),
842        Err(_) => WireValue::String(format!("{}", dt)),
843    }
844}
845
846/// Serialize a [`DataTable`] to Arrow IPC bytes.
847pub fn datatable_to_ipc_bytes(dt: &DataTable) -> std::result::Result<Vec<u8>, String> {
848    let mut buf = Vec::new();
849    let schema = dt.inner().schema();
850    let mut writer = FileWriter::try_new(&mut buf, schema.as_ref())
851        .map_err(|e| format!("failed to create Arrow IPC writer: {e}"))?;
852    writer
853        .write(dt.inner())
854        .map_err(|e| format!("failed to write Arrow IPC batch: {e}"))?;
855    writer
856        .finish()
857        .map_err(|e| format!("failed to finalize Arrow IPC writer: {e}"))?;
858    Ok(buf)
859}
860
861/// Deserialize Arrow IPC bytes into a [`DataTable`].
862pub fn datatable_from_ipc_bytes(
863    ipc_bytes: &[u8],
864    type_name: Option<&str>,
865    schema_id: Option<u32>,
866) -> std::result::Result<DataTable, String> {
867    if ipc_bytes.is_empty() {
868        return Err("empty Arrow IPC payload".to_string());
869    }
870
871    let cursor = std::io::Cursor::new(ipc_bytes);
872    let mut reader = FileReader::try_new(cursor, None)
873        .map_err(|e| format!("failed to create Arrow IPC reader: {e}"))?;
874    let batch = reader
875        .next()
876        .transpose()
877        .map_err(|e| format!("failed reading Arrow IPC batch: {e}"))?
878        .ok_or_else(|| "Arrow IPC payload has no record batches".to_string())?;
879
880    let mut dt = DataTable::new(batch);
881    if let Some(name) = type_name {
882        dt = DataTable::with_type_name(dt.into_inner(), name.to_string());
883    }
884    if let Some(id) = schema_id {
885        dt = dt.with_schema_id(id);
886    }
887    Ok(dt)
888}
889
890/// Helper to infer full metadata including from registry
891/// Note: Meta definitions have been removed; formatting now uses Display trait.
892fn infer_metadata_with_ctx(
893    _value: &ValueWord,
894    type_name: &str,
895    _ctx: &Context,
896) -> (TypeInfo, TypeRegistry) {
897    let type_info = TypeInfo::primitive(type_name);
898    let registry = TypeRegistry::new("Default");
899    (type_info, registry)
900}
901
902#[cfg(test)]
903mod tests {
904    use super::*;
905    use crate::type_methods::TypeMethodRegistry;
906    use crate::type_schema::typed_object_to_hashmap_nb;
907    use shape_value::ValueSlot;
908    use shape_value::heap_value::HeapValue;
909    use std::sync::Arc;
910
911    fn get_dummy_context() -> Context {
912        Context::new_empty_with_registry(Arc::new(TypeMethodRegistry::new()))
913    }
914
915    #[test]
916    fn test_basic_value_conversion() {
917        let ctx = get_dummy_context();
918        // Number
919        let wire = value_to_wire(&ValueWord::from_f64(42.5), &ctx);
920        assert_eq!(wire, WireValue::Number(42.5));
921
922        // Whole number remains number
923        let wire = value_to_wire(&ValueWord::from_f64(42.0), &ctx);
924        assert_eq!(wire, WireValue::Number(42.0));
925
926        // String
927        let wire = value_to_wire(&ValueWord::from_string(Arc::new("hello".to_string())), &ctx);
928        assert_eq!(wire, WireValue::String("hello".to_string()));
929
930        // Bool
931        let wire = value_to_wire(&ValueWord::from_bool(true), &ctx);
932        assert_eq!(wire, WireValue::Bool(true));
933
934        // None
935        let wire = value_to_wire(&ValueWord::none(), &ctx);
936        assert_eq!(wire, WireValue::Null);
937    }
938
939    #[test]
940    fn test_array_conversion() {
941        let ctx = get_dummy_context();
942        let arr = ValueWord::from_array(Arc::new(vec![
943            ValueWord::from_f64(1.0),
944            ValueWord::from_f64(2.0),
945            ValueWord::from_f64(3.0),
946        ]));
947        let wire = value_to_wire(&arr, &ctx);
948
949        if let WireValue::Array(items) = wire {
950            assert_eq!(items.len(), 3);
951            assert_eq!(items[0], WireValue::Number(1.0));
952            assert_eq!(items[1], WireValue::Number(2.0));
953            assert_eq!(items[2], WireValue::Number(3.0));
954        } else {
955            panic!("Expected Array");
956        }
957    }
958
959    #[test]
960    fn test_object_conversion() {
961        let ctx = get_dummy_context();
962        let obj = crate::type_schema::typed_object_from_pairs(&[
963            ("x", ValueWord::from_f64(10.0)),
964            ("y", ValueWord::from_f64(20.0)),
965        ]);
966
967        let wire = value_to_wire(&obj, &ctx);
968
969        if let WireValue::Object(map) = wire {
970            assert_eq!(map.get("x"), Some(&WireValue::Number(10.0)));
971            assert_eq!(map.get("y"), Some(&WireValue::Number(20.0)));
972        } else {
973            panic!("Expected Object");
974        }
975    }
976
977    #[test]
978    fn test_builtin_typed_object_converts_to_wire_object() {
979        let ctx = get_dummy_context();
980        let any_error_schema_id = ctx
981            .type_schema_registry()
982            .get("__AnyError")
983            .expect("__AnyError schema should exist")
984            .id as u64;
985
986        let slots = vec![
987            ValueSlot::from_heap(HeapValue::String(Arc::new("AnyError".to_string()))),
988            ValueSlot::from_heap(HeapValue::String(Arc::new("boom".to_string()))),
989            ValueSlot::none(), // cause: None (inline)
990            ValueSlot::none(), // trace_info: None (inline)
991            ValueSlot::from_heap(HeapValue::String(Arc::new("boom".to_string()))),
992            ValueSlot::from_heap(HeapValue::String(Arc::new("E_BANG".to_string()))),
993        ];
994
995        let nb = ValueWord::from_heap_value(HeapValue::TypedObject {
996            schema_id: any_error_schema_id,
997            slots: slots.into_boxed_slice(),
998            heap_mask: 0b11_0011, // bits 0,1,4,5 = heap; bits 2,3 = inline none
999        });
1000
1001        let wire = nb_to_wire(&nb, &ctx);
1002        match wire {
1003            WireValue::Object(map) => {
1004                assert_eq!(
1005                    map.get("category"),
1006                    Some(&WireValue::String("AnyError".into()))
1007                );
1008                assert_eq!(map.get("message"), Some(&WireValue::String("boom".into())));
1009                assert_eq!(map.get("code"), Some(&WireValue::String("E_BANG".into())));
1010            }
1011            other => panic!("Expected WireValue::Object, got {:?}", other),
1012        }
1013    }
1014
1015    #[test]
1016    fn test_unknown_schema_anyerror_uses_builtin_fallback_decoder() {
1017        let ctx = get_dummy_context();
1018
1019        let slots = vec![
1020            ValueSlot::from_heap(HeapValue::String(Arc::new("AnyError".to_string()))),
1021            ValueSlot::from_heap(HeapValue::String(Arc::new("boom".to_string()))),
1022            ValueSlot::none(), // cause: None (inline)
1023            ValueSlot::none(), // trace_info: None (inline)
1024            ValueSlot::from_heap(HeapValue::String(Arc::new("boom".to_string()))),
1025            ValueSlot::from_heap(HeapValue::String(Arc::new("E_BANG".to_string()))),
1026        ];
1027
1028        let nb = ValueWord::from_heap_value(HeapValue::TypedObject {
1029            schema_id: 9_999_999,
1030            slots: slots.into_boxed_slice(),
1031            heap_mask: 0b11_0011, // bits 0,1,4,5 = heap; bits 2,3 = inline none
1032        });
1033
1034        let wire = nb_to_wire(&nb, &ctx);
1035        match wire {
1036            WireValue::Object(map) => {
1037                assert_eq!(
1038                    map.get("category"),
1039                    Some(&WireValue::String("AnyError".into()))
1040                );
1041                assert_eq!(map.get("message"), Some(&WireValue::String("boom".into())));
1042                assert_eq!(map.get("code"), Some(&WireValue::String("E_BANG".into())));
1043            }
1044            other => panic!("Expected WireValue::Object, got {:?}", other),
1045        }
1046    }
1047
1048    #[test]
1049    fn test_timestamp_conversion() {
1050        let ctx = get_dummy_context();
1051        use chrono::TimeZone;
1052        let dt = chrono::Utc
1053            .with_ymd_and_hms(2024, 1, 15, 10, 30, 0)
1054            .unwrap();
1055        let wire = value_to_wire(&ValueWord::from_time_utc(dt), &ctx);
1056
1057        if let WireValue::Timestamp(ts) = wire {
1058            assert_eq!(ts, 1705314600000);
1059        } else {
1060            panic!("Expected Timestamp");
1061        }
1062    }
1063
1064    #[test]
1065    fn test_result_conversion() {
1066        let ctx = get_dummy_context();
1067        let ok_val = ValueWord::from_ok(ValueWord::from_f64(42.0));
1068        let wire = value_to_wire(&ok_val, &ctx);
1069
1070        if let WireValue::Result { ok, value } = wire {
1071            assert!(ok);
1072            assert_eq!(*value, WireValue::Number(42.0));
1073        } else {
1074            panic!("Expected Result");
1075        }
1076    }
1077
1078    #[test]
1079    fn test_wire_to_nb_anyerror_trace_frame_key_order_is_stable() {
1080        use std::collections::BTreeMap;
1081
1082        let mut frame = BTreeMap::new();
1083        frame.insert(
1084            "function".to_string(),
1085            WireValue::String("duckdb.connect".to_string()),
1086        );
1087        frame.insert(
1088            "file".to_string(),
1089            WireValue::String("extension:duckdb".to_string()),
1090        );
1091        frame.insert("line".to_string(), WireValue::Null);
1092        frame.insert("ip".to_string(), WireValue::Null);
1093
1094        let nb = wire_to_nb(&WireValue::Object(frame));
1095        let decoded =
1096            typed_object_to_hashmap_nb(&nb).expect("Trace frame wire object should decode");
1097        assert_eq!(
1098            decoded.get("function").and_then(|v| v.as_str()),
1099            Some("duckdb.connect")
1100        );
1101        assert_eq!(
1102            decoded.get("file").and_then(|v| v.as_str()),
1103            Some("extension:duckdb")
1104        );
1105    }
1106
1107    #[test]
1108    fn test_any_error_result_roundtrip() {
1109        use crate::type_schema::typed_object_from_pairs;
1110        let ctx = get_dummy_context();
1111
1112        let empty_trace = typed_object_from_pairs(&[]);
1113        let cause = typed_object_from_pairs(&[
1114            (
1115                "category",
1116                ValueWord::from_string(Arc::new("AnyError".to_string())),
1117            ),
1118            (
1119                "payload",
1120                ValueWord::from_string(Arc::new("low level".to_string())),
1121            ),
1122            ("cause", ValueWord::none()),
1123            ("trace_info", empty_trace),
1124        ]);
1125
1126        let empty_trace2 = typed_object_from_pairs(&[]);
1127        let outer = typed_object_from_pairs(&[
1128            (
1129                "category",
1130                ValueWord::from_string(Arc::new("AnyError".to_string())),
1131            ),
1132            (
1133                "payload",
1134                ValueWord::from_string(Arc::new("high level".to_string())),
1135            ),
1136            ("cause", cause),
1137            ("trace_info", empty_trace2),
1138            (
1139                "code",
1140                ValueWord::from_string(Arc::new("OPTION_NONE".to_string())),
1141            ),
1142        ]);
1143
1144        let err = ValueWord::from_err(outer);
1145        let wire = value_to_wire(&err, &ctx);
1146
1147        let WireValue::Result { ok, value } = &wire else {
1148            panic!("Expected wire Result");
1149        };
1150        assert!(!ok);
1151        match value.as_ref() {
1152            WireValue::Object(map) => {
1153                assert_eq!(
1154                    map.get("category"),
1155                    Some(&WireValue::String("AnyError".to_string()))
1156                );
1157                assert_eq!(
1158                    map.get("code"),
1159                    Some(&WireValue::String("OPTION_NONE".to_string()))
1160                );
1161            }
1162            other => panic!("Expected AnyError object payload, got {:?}", other),
1163        }
1164
1165        let roundtrip = wire_to_nb(&wire);
1166        let hv = roundtrip.as_heap_ref().expect("Expected heap value");
1167        match hv {
1168            HeapValue::Err(inner) => {
1169                let inner_hv = inner.as_heap_ref().expect("Expected heap inner");
1170                assert!(
1171                    matches!(inner_hv, HeapValue::TypedObject { .. }),
1172                    "Expected TypedObject inside Err"
1173                );
1174            }
1175            other => panic!("Expected Err, got {:?}", other.kind()),
1176        }
1177    }
1178
1179    #[test]
1180    fn test_envelope_creation() {
1181        let ctx = get_dummy_context();
1182        let nb = ValueWord::from_f64(3.14);
1183        let envelope = nb_to_envelope(&nb, "number", &ctx);
1184
1185        match &envelope.value {
1186            WireValue::Number(n) => assert!((*n - 3.14).abs() < f64::EPSILON),
1187            other => panic!("Expected Number, got {:?}", other),
1188        }
1189    }
1190
1191    #[test]
1192    fn test_roundtrip_basic() {
1193        let ctx = get_dummy_context();
1194        let nb = ValueWord::from_string(Arc::new("test".to_string()));
1195        let wire = nb_to_wire(&nb, &ctx);
1196        let back = wire_to_nb(&wire);
1197
1198        if let Some(s) = back.as_str() {
1199            assert_eq!(s, "test");
1200        } else {
1201            panic!("Expected String");
1202        }
1203    }
1204
1205    // ===== ValueWord-native conversion tests =====
1206
1207    #[test]
1208    fn test_nb_to_wire_basic_types() {
1209        let ctx = get_dummy_context();
1210
1211        // f64 -> Number (fractional)
1212        let wire = nb_to_wire(&ValueWord::from_f64(42.5), &ctx);
1213        assert_eq!(wire, WireValue::Number(42.5));
1214
1215        // f64 whole -> Number
1216        let wire = nb_to_wire(&ValueWord::from_f64(42.0), &ctx);
1217        assert_eq!(wire, WireValue::Number(42.0));
1218
1219        // i48 -> Integer
1220        let wire = nb_to_wire(&ValueWord::from_i64(99), &ctx);
1221        assert_eq!(wire, WireValue::Integer(99));
1222
1223        // Negative i48 -> Integer
1224        let wire = nb_to_wire(&ValueWord::from_i64(-7), &ctx);
1225        assert_eq!(wire, WireValue::Integer(-7));
1226
1227        // Bool
1228        let wire = nb_to_wire(&ValueWord::from_bool(true), &ctx);
1229        assert_eq!(wire, WireValue::Bool(true));
1230
1231        let wire = nb_to_wire(&ValueWord::from_bool(false), &ctx);
1232        assert_eq!(wire, WireValue::Bool(false));
1233
1234        // None
1235        let wire = nb_to_wire(&ValueWord::none(), &ctx);
1236        assert_eq!(wire, WireValue::Null);
1237
1238        // Unit
1239        let wire = nb_to_wire(&ValueWord::unit(), &ctx);
1240        assert_eq!(wire, WireValue::Null);
1241    }
1242
1243    #[test]
1244    fn test_nb_to_wire_string() {
1245        let ctx = get_dummy_context();
1246        let nb = ValueWord::from_string(Arc::new("hello".to_string()));
1247        let wire = nb_to_wire(&nb, &ctx);
1248        assert_eq!(wire, WireValue::String("hello".to_string()));
1249    }
1250
1251    #[test]
1252    fn test_nb_to_wire_array() {
1253        let ctx = get_dummy_context();
1254        let nb = ValueWord::from_array(Arc::new(vec![
1255            ValueWord::from_f64(1.0),
1256            ValueWord::from_i64(2),
1257            ValueWord::from_bool(true),
1258        ]));
1259        let wire = nb_to_wire(&nb, &ctx);
1260
1261        if let WireValue::Array(items) = wire {
1262            assert_eq!(items.len(), 3);
1263            assert_eq!(items[0], WireValue::Number(1.0));
1264            assert_eq!(items[1], WireValue::Integer(2));
1265            assert_eq!(items[2], WireValue::Bool(true));
1266        } else {
1267            panic!("Expected Array");
1268        }
1269    }
1270
1271    #[test]
1272    fn test_nb_to_wire_result() {
1273        let ctx = get_dummy_context();
1274
1275        // Ok result
1276        let ok = ValueWord::from_ok(ValueWord::from_i64(42));
1277        let wire = nb_to_wire(&ok, &ctx);
1278        if let WireValue::Result { ok, value } = wire {
1279            assert!(ok);
1280            assert_eq!(*value, WireValue::Integer(42));
1281        } else {
1282            panic!("Expected Result");
1283        }
1284
1285        // Err result
1286        let err = ValueWord::from_err(ValueWord::from_string(Arc::new("oops".to_string())));
1287        let wire = nb_to_wire(&err, &ctx);
1288        if let WireValue::Result { ok, value } = wire {
1289            assert!(!ok);
1290            assert_eq!(*value, WireValue::String("oops".to_string()));
1291        } else {
1292            panic!("Expected Result");
1293        }
1294    }
1295
1296    #[test]
1297    fn test_nb_to_wire_some() {
1298        let ctx = get_dummy_context();
1299        let some = ValueWord::from_some(ValueWord::from_f64(3.14));
1300        let wire = nb_to_wire(&some, &ctx);
1301        // Some unwraps to inner value
1302        assert_eq!(wire, WireValue::Number(3.14));
1303    }
1304
1305    #[test]
1306    fn test_nb_to_wire_matches_vmvalue() {
1307        // Verify that nb_to_wire produces the same output as value_to_wire for basic types
1308        let ctx = get_dummy_context();
1309
1310        let test_values: Vec<ValueWord> = vec![
1311            ValueWord::from_f64(42.5),
1312            ValueWord::from_f64(42.0),
1313            ValueWord::from_i64(100),
1314            ValueWord::from_i64(-100),
1315            ValueWord::from_bool(true),
1316            ValueWord::from_bool(false),
1317            ValueWord::none(),
1318            ValueWord::unit(),
1319            ValueWord::from_string(Arc::new("test".to_string())),
1320            ValueWord::from_array(Arc::new(vec![
1321                ValueWord::from_f64(1.0),
1322                ValueWord::from_i64(2),
1323            ])),
1324        ];
1325
1326        for nb in &test_values {
1327            let vmv = nb.clone();
1328            let wire_from_vmv = value_to_wire(&vmv, &ctx);
1329            let wire_from_nb = nb_to_wire(nb, &ctx);
1330            assert_eq!(
1331                wire_from_vmv,
1332                wire_from_nb,
1333                "Mismatch for ValueWord type {:?}: ValueWord path = {:?}, ValueWord path = {:?}",
1334                nb.tag(),
1335                wire_from_vmv,
1336                wire_from_nb
1337            );
1338        }
1339    }
1340
1341    #[test]
1342    fn test_wire_to_nb_basic_types() {
1343        // Null -> None
1344        let nb = wire_to_nb(&WireValue::Null);
1345        assert!(nb.is_none());
1346
1347        // Bool
1348        let nb = wire_to_nb(&WireValue::Bool(true));
1349        assert_eq!(nb.as_bool(), Some(true));
1350
1351        // Number
1352        let nb = wire_to_nb(&WireValue::Number(3.14));
1353        assert_eq!(nb.as_f64(), Some(3.14));
1354
1355        // Integer
1356        let nb = wire_to_nb(&WireValue::Integer(42));
1357        assert_eq!(nb.as_i64(), Some(42));
1358
1359        // String
1360        let nb = wire_to_nb(&WireValue::String("hello".to_string()));
1361        assert_eq!(nb.as_str(), Some("hello"));
1362    }
1363
1364    #[test]
1365    fn test_wire_to_nb_array() {
1366        let wire = WireValue::Array(vec![
1367            WireValue::Integer(1),
1368            WireValue::Number(2.5),
1369            WireValue::Bool(false),
1370        ]);
1371        let nb = wire_to_nb(&wire);
1372        let arr = nb.as_any_array().expect("Expected array").to_generic();
1373        assert_eq!(arr.len(), 3);
1374        assert_eq!(arr[0].as_i64(), Some(1));
1375        assert_eq!(arr[1].as_f64(), Some(2.5));
1376        assert_eq!(arr[2].as_bool(), Some(false));
1377    }
1378
1379    #[test]
1380    fn test_wire_to_nb_result() {
1381        // Ok result
1382        let wire = WireValue::Result {
1383            ok: true,
1384            value: Box::new(WireValue::Integer(7)),
1385        };
1386        let nb = wire_to_nb(&wire);
1387        let inner = nb.as_ok_inner().expect("Expected Ok");
1388        assert_eq!(inner.as_i64(), Some(7));
1389
1390        // Err result
1391        let wire = WireValue::Result {
1392            ok: false,
1393            value: Box::new(WireValue::String("fail".to_string())),
1394        };
1395        let nb = wire_to_nb(&wire);
1396        let inner = nb.as_err_inner().expect("Expected Err");
1397        assert_eq!(inner.as_str(), Some("fail"));
1398    }
1399
1400    #[test]
1401    fn test_nb_roundtrip_basic() {
1402        let ctx = get_dummy_context();
1403
1404        // String roundtrip
1405        let original = ValueWord::from_string(Arc::new("roundtrip".to_string()));
1406        let wire = nb_to_wire(&original, &ctx);
1407        let back = wire_to_nb(&wire);
1408        assert_eq!(back.as_str(), Some("roundtrip"));
1409
1410        // Integer roundtrip
1411        let original = ValueWord::from_i64(12345);
1412        let wire = nb_to_wire(&original, &ctx);
1413        let back = wire_to_nb(&wire);
1414        assert_eq!(back.as_i64(), Some(12345));
1415
1416        // Float roundtrip
1417        let original = ValueWord::from_f64(2.718);
1418        let wire = nb_to_wire(&original, &ctx);
1419        let back = wire_to_nb(&wire);
1420        assert_eq!(back.as_f64(), Some(2.718));
1421
1422        // Bool roundtrip
1423        let original = ValueWord::from_bool(true);
1424        let wire = nb_to_wire(&original, &ctx);
1425        let back = wire_to_nb(&wire);
1426        assert_eq!(back.as_bool(), Some(true));
1427
1428        // None roundtrip
1429        let wire = nb_to_wire(&ValueWord::none(), &ctx);
1430        let back = wire_to_nb(&wire);
1431        assert!(back.is_none());
1432    }
1433
1434    #[test]
1435    fn test_nb_roundtrip_array() {
1436        let ctx = get_dummy_context();
1437        let original = ValueWord::from_array(Arc::new(vec![
1438            ValueWord::from_i64(10),
1439            ValueWord::from_f64(20.5),
1440            ValueWord::from_string(Arc::new("x".to_string())),
1441        ]));
1442        let wire = nb_to_wire(&original, &ctx);
1443        let back = wire_to_nb(&wire);
1444        let arr = back.as_any_array().expect("Expected array").to_generic();
1445        assert_eq!(arr.len(), 3);
1446        assert_eq!(arr[0].as_i64(), Some(10));
1447        assert_eq!(arr[1].as_f64(), Some(20.5));
1448        assert_eq!(arr[2].as_str(), Some("x"));
1449    }
1450
1451    #[test]
1452    fn test_nb_to_wire_decimal() {
1453        let ctx = get_dummy_context();
1454        let d = rust_decimal::Decimal::new(314, 2); // 3.14
1455        let nb = ValueWord::from_decimal(d);
1456        let wire = nb_to_wire(&nb, &ctx);
1457        assert_eq!(wire, WireValue::Number(3.14));
1458    }
1459
1460    #[test]
1461    fn test_nb_to_wire_typed_object() {
1462        let ctx = get_dummy_context();
1463        let obj = crate::type_schema::typed_object_from_pairs(&[
1464            ("a", ValueWord::from_f64(1.0)),
1465            ("b", ValueWord::from_string(Arc::new("two".to_string()))),
1466        ]);
1467        let nb = obj;
1468        let wire = nb_to_wire(&nb, &ctx);
1469
1470        if let WireValue::Object(map) = wire {
1471            assert_eq!(map.get("a"), Some(&WireValue::Number(1.0)));
1472            assert_eq!(map.get("b"), Some(&WireValue::String("two".to_string())));
1473        } else {
1474            panic!("Expected Object, got {:?}", wire);
1475        }
1476    }
1477
1478    #[test]
1479    fn test_nb_envelope_creation() {
1480        let ctx = get_dummy_context();
1481        let nb = ValueWord::from_f64(3.14);
1482        let envelope = nb_to_envelope(&nb, "Number", &ctx);
1483        assert_eq!(envelope.type_info.name, "Number");
1484    }
1485
1486    #[test]
1487    fn test_native_scalar_wire_roundtrip_preserves_width() {
1488        let ctx = get_dummy_context();
1489        let cases = vec![
1490            (
1491                ValueWord::from_native_i8(-8),
1492                WireValue::I8(-8),
1493                shape_value::heap_value::NativeScalar::I8(-8),
1494            ),
1495            (
1496                ValueWord::from_native_u8(255),
1497                WireValue::U8(255),
1498                shape_value::heap_value::NativeScalar::U8(255),
1499            ),
1500            (
1501                ValueWord::from_native_i16(-1024),
1502                WireValue::I16(-1024),
1503                shape_value::heap_value::NativeScalar::I16(-1024),
1504            ),
1505            (
1506                ValueWord::from_native_u16(65530),
1507                WireValue::U16(65530),
1508                shape_value::heap_value::NativeScalar::U16(65530),
1509            ),
1510            (
1511                ValueWord::from_native_i32(-123_456),
1512                WireValue::I32(-123_456),
1513                shape_value::heap_value::NativeScalar::I32(-123_456),
1514            ),
1515            (
1516                ValueWord::from_native_u32(4_000_000_000),
1517                WireValue::U32(4_000_000_000),
1518                shape_value::heap_value::NativeScalar::U32(4_000_000_000),
1519            ),
1520            (
1521                ValueWord::from_native_scalar(shape_value::heap_value::NativeScalar::I64(
1522                    -9_223_372_036_854_775_000,
1523                )),
1524                WireValue::I64(-9_223_372_036_854_775_000),
1525                shape_value::heap_value::NativeScalar::I64(-9_223_372_036_854_775_000),
1526            ),
1527            (
1528                ValueWord::from_native_u64(18_000_000_000),
1529                WireValue::U64(18_000_000_000),
1530                shape_value::heap_value::NativeScalar::U64(18_000_000_000),
1531            ),
1532            (
1533                ValueWord::from_native_isize(12345isize),
1534                WireValue::Isize(12345),
1535                shape_value::heap_value::NativeScalar::Isize(12345isize),
1536            ),
1537            (
1538                ValueWord::from_native_usize(54321usize),
1539                WireValue::Usize(54321),
1540                shape_value::heap_value::NativeScalar::Usize(54321usize),
1541            ),
1542            (
1543                ValueWord::from_native_ptr(0x1234usize),
1544                WireValue::Ptr(0x1234),
1545                shape_value::heap_value::NativeScalar::Ptr(0x1234usize),
1546            ),
1547            (
1548                ValueWord::from_native_f32(3.5f32),
1549                WireValue::F32(3.5f32),
1550                shape_value::heap_value::NativeScalar::F32(3.5f32),
1551            ),
1552        ];
1553
1554        for (nb, expected_wire, expected_scalar) in cases {
1555            let wire = nb_to_wire(&nb, &ctx);
1556            assert_eq!(wire, expected_wire);
1557
1558            let roundtrip = wire_to_nb(&wire);
1559            assert_eq!(roundtrip.as_native_scalar(), Some(expected_scalar));
1560        }
1561    }
1562}