Skip to main content

shape_runtime/
wire_conversion.rs

1//! Conversion between runtime values and wire format.
2//!
3//! Phase 2b kind-threaded rewrite. Public functions take `(bits: u64,
4//! kind: NativeKind)` pairs threaded from the FunctionBlob's compile-
5//! time slot-kind metadata; internal dispatch is a `match kind { ... }`
6//! with no tag-bit probing. Heap slots use `NativeKind::Ptr(HeapKind)` —
7//! the kind tells the dispatcher which `HeapValue` arm decodes the
8//! bits without probing the heap object's self-reported discriminant
9//! in production (debug-only consistency check).
10//!
11//! See `docs/defections.md` 2026-05-06 (Phase 2b unified marshal +
12//! wire/snapshot kind threading) for the architectural rationale.
13//!
14//! ## API
15//!
16//! - [`slot_to_wire`] — project (bits, kind) into a `WireValue`.
17//! - [`wire_to_slot`] — project a `WireValue` into typed slot bits,
18//!   given the `expected_kind` the caller wants. Returns
19//!   `Result<u64, MarshalError>`.
20//! - [`slot_to_envelope`] — wrap a typed slot in a `ValueEnvelope` with
21//!   metadata.
22//! - [`slot_extract_content`] — extract Content node renderings from a
23//!   slot whose kind says it carries Content / DataTable / TableView.
24//! - [`datatable_to_wire`] / [`datatable_to_ipc_bytes`] /
25//!   [`datatable_from_ipc_bytes`] — typed `DataTable` ↔ wire/IPC.
26
27use crate::Context;
28use crate::marshal::MarshalError;
29use arrow_ipc::{reader::FileReader, writer::FileWriter};
30use shape_value::heap_value::HeapValue;
31use shape_value::{DataTable, HeapKind, NativeKind};
32use shape_wire::{
33    DurationUnit as WireDurationUnit, ValueEnvelope, WireTable, WireValue,
34};
35use std::collections::BTreeMap;
36use std::sync::Arc;
37
38/// Project a typed slot's `(bits, kind)` to a `WireValue`.
39///
40/// The `kind` fully determines the projection — no tag-bit probing.
41/// For `NativeKind::Ptr(hk)`, the function casts `bits` to
42/// `*const HeapValue`, debug-asserts the kind matches, and dispatches
43/// per `HeapValue` arm.
44pub fn slot_to_wire(bits: u64, kind: NativeKind, ctx: &Context) -> WireValue {
45    match kind {
46        NativeKind::Float64 => WireValue::Number(f64::from_bits(bits)),
47        NativeKind::NullableFloat64 => {
48            let v = f64::from_bits(bits);
49            if v.is_nan() {
50                WireValue::Null
51            } else {
52                WireValue::Number(v)
53            }
54        }
55        NativeKind::Int64 => WireValue::Integer(bits as i64),
56        NativeKind::NullableInt64 => WireValue::Integer(bits as i64),
57        NativeKind::Int8 => WireValue::I8(bits as i8),
58        NativeKind::Int16 => WireValue::I16(bits as i16),
59        NativeKind::Int32 => WireValue::I32(bits as i32),
60        NativeKind::UInt8 => WireValue::U8(bits as u8),
61        NativeKind::UInt16 => WireValue::U16(bits as u16),
62        NativeKind::UInt32 => WireValue::U32(bits as u32),
63        NativeKind::UInt64 => WireValue::U64(bits),
64        NativeKind::IntSize => WireValue::Isize(bits as i64),
65        NativeKind::UIntSize => WireValue::Usize(bits),
66        NativeKind::NullableInt8
67        | NativeKind::NullableInt16
68        | NativeKind::NullableInt32
69        | NativeKind::NullableUInt8
70        | NativeKind::NullableUInt16
71        | NativeKind::NullableUInt32
72        | NativeKind::NullableUInt64
73        | NativeKind::NullableIntSize
74        | NativeKind::NullableUIntSize => WireValue::Integer(bits as i64),
75        NativeKind::Bool => WireValue::Bool(bits != 0),
76        // R5b-2-bool-null-sentinel-cluster (ADR-006 §2.7 + §2.7.5 +
77        // §2.7.7/Q9, 2026-05-19): `NativeKind::Null` is the canonical
78        // absence-of-value discriminator. Pre-disposition `(0u64,
79        // NativeKind::Bool)` was the null sentinel which collided with
80        // legitimate `false` bool slots (both encoded as bits=0); the
81        // SURFACE-G6-NONE-OUTPUT-ADAPTER reproducer (`fn bar() ->
82        // Option<int> { None }; bar()` at top level) materialized
83        // `None` as `{"Bool": false}`. Post-disposition: kind IS the
84        // discriminator per §2.7.7/Q9 — `NativeKind::Null` slots
85        // project to `WireValue::Null` directly, restoring soundness.
86        NativeKind::Null => WireValue::Null,
87        // Round 19 S1.5 W12-nativekind-scalar-additions (2026-05-14):
88        // ADR-006 §2.7.5 amendment adds F32 + Char as 4-byte scalar
89        // variants. Wire projection: F32 widens to `WireValue::Number`
90        // (`f64::from(f32)` is lossless); Char projects to a single-
91        // codepoint string (mirror of the `HeapValue::Char` arm below)
92        // because `WireValue` has no dedicated Char variant.
93        NativeKind::Float32 => WireValue::Number(f64::from(f32::from_bits(bits as u32))),
94        NativeKind::Char => match char::from_u32(bits as u32) {
95            Some(c) => WireValue::String(c.to_string()),
96            None => WireValue::Null,
97        },
98        NativeKind::String => {
99            // bits is an Arc<String> raw pointer
100            let ptr = bits as *const String;
101            // SAFETY: kind contract pins this slot to an Arc<String> raw ptr.
102            let s = unsafe { &*ptr };
103            WireValue::String(s.clone())
104        }
105        // Wave 2 Agent B W12-StringV2-DecimalV2-NativeKind-additions
106        // (ADR-006 §2.7.5 amendment, 2026-05-14): the v2-raw `*const StringObj`
107        // carrier projects to the same `WireValue::String` wire shape as
108        // `NativeKind::String` (Arc-wrapped sibling), via the carrier's
109        // `as_str` accessor reading the UTF-8 payload at offset 8 (data ptr)
110        // / 16 (len) of the `repr(C)` struct. The slot bits are NOT an
111        // `Arc<T>` pointer — `StringObj` is a manually-allocated `repr(C)`
112        // 24-byte carrier per `v2/string_obj.rs`.
113        NativeKind::StringV2 => {
114            if bits == 0 {
115                return WireValue::Null;
116            }
117            // SAFETY: per the §2.7.5 amendment construction contract,
118            // kind=StringV2 means bits = `ptr as u64` pointing to a live
119            // `StringObj` with bumped refcount — the slot owns one
120            // v2-retain share for the duration of this call.
121            let ptr = bits as *const shape_value::v2::string_obj::StringObj;
122            let s: &str = unsafe { shape_value::v2::string_obj::StringObj::as_str(ptr) };
123            WireValue::String(s.to_string())
124        }
125        // Wave 2 Agent B: the v2-raw `*const DecimalObj` carrier projects
126        // to `WireValue::Number` (the same wire shape as
127        // `HeapValue::Decimal` per `heap_value_to_wire` below) via the
128        // carrier's `value` accessor reading the inline `rust_decimal::Decimal`
129        // at offset 8 of the `repr(C)` struct.
130        NativeKind::DecimalV2 => {
131            if bits == 0 {
132                return WireValue::Null;
133            }
134            // SAFETY: per the §2.7.5 amendment construction contract,
135            // kind=DecimalV2 means bits = `ptr as u64` pointing to a live
136            // `DecimalObj` with bumped refcount.
137            let ptr = bits as *const shape_value::v2::decimal_obj::DecimalObj;
138            let value = unsafe { shape_value::v2::decimal_obj::DecimalObj::value(ptr) };
139            WireValue::Number(value.to_string().parse().unwrap_or(0.0))
140        }
141        NativeKind::Ptr(hk) => heap_to_wire(bits, hk, ctx),
142    }
143}
144
145/// Project an `Arc<HeapValue>` raw pointer slot to `WireValue`,
146/// dispatching on the pre-known `HeapKind` rather than probing the
147/// heap object's self-reported `kind()`.
148fn heap_to_wire(bits: u64, hk: HeapKind, ctx: &Context) -> WireValue {
149    if bits == 0 {
150        return WireValue::Null;
151    }
152    // Defensive: `HeapKind::Char` is an inline-codepoint label, NOT an
153    // `Arc<HeapValue>` pointer — its `bits` are a raw UTF-32 codepoint.
154    // The canonical post-amendment carrier is the scalar `NativeKind::Char`
155    // (handled in `slot_to_wire`), but any producer that still stamps the
156    // pre-amendment `Ptr(HeapKind::Char)` label must not reach the
157    // `*const HeapValue` cast below — casting a codepoint (e.g. 0x63) to a
158    // HeapValue pointer and dereferencing it is a misaligned-pointer abort.
159    // This arm projects the codepoint directly, mirroring the
160    // `NativeKind::Char` arm and `HeapValue::Char` arm.
161    if hk == HeapKind::Char {
162        return match char::from_u32(bits as u32) {
163            Some(c) => WireValue::String(c.to_string()),
164            None => WireValue::Null,
165        };
166    }
167    // WS-3 F2b: `HeapKind::Result` / `HeapKind::Option` are typed-Arc
168    // dispatch labels — their bits are `Arc::into_raw(Arc<ResultData>)` /
169    // `Arc::into_raw(Arc<OptionData>)`, NOT an `Arc<HeapValue>`. Casting
170    // those bits to `*const HeapValue` (the path below) reads a
171    // `ResultData`/`OptionData` as a `HeapValue` enum — type confusion +
172    // UB. This crashed (SIGSEGV) whenever a `Result`/`Option` was the
173    // program's terminal value (e.g. `fn main() -> Result<int,string>`
174    // returning `Ok(0)` as the trailing expression). Project the typed
175    // payload directly, mirroring `printing.rs`'s `HeapKind::Result` /
176    // `HeapKind::Option` formatter arms.
177    if hk == HeapKind::Result {
178        // SAFETY: `KindedSlot::from_result` construction contract —
179        // Result-kind bits are `Arc::into_raw(Arc<ResultData>)`.
180        let r: &shape_value::heap_value::ResultData =
181            unsafe { &*(bits as *const shape_value::heap_value::ResultData) };
182        let inner = slot_to_wire(r.payload.raw(), r.payload.kind(), ctx);
183        return WireValue::Result {
184            ok: r.is_ok,
185            value: Box::new(inner),
186        };
187    }
188    if hk == HeapKind::Option {
189        // SAFETY: `KindedSlot::from_option` construction contract —
190        // Option-kind bits are `Arc::into_raw(Arc<OptionData>)`.
191        let o: &shape_value::heap_value::OptionData =
192            unsafe { &*(bits as *const shape_value::heap_value::OptionData) };
193        if o.is_some {
194            return slot_to_wire(o.payload.raw(), o.payload.kind(), ctx);
195        }
196        return WireValue::Null;
197    }
198    let ptr = bits as *const HeapValue;
199    // SAFETY: NativeKind::Ptr(hk) contract — bits is a valid Arc<HeapValue> ptr.
200    let hv = unsafe { &*ptr };
201    debug_assert_eq!(
202        hv.kind(),
203        hk,
204        "slot kind {:?} does not match HeapValue::{:?}",
205        hk,
206        hv.kind()
207    );
208    heap_value_to_wire(hv, ctx)
209}
210
211/// Project a `&HeapValue` to `WireValue` by dispatching on its
212/// surviving variants. Reused by the snapshot path (Phase 2b
213/// snapshot.rs commit) which has the same heap projection needs.
214pub fn heap_value_to_wire(hv: &HeapValue, ctx: &Context) -> WireValue {
215    match hv {
216        HeapValue::String(s) => WireValue::String((**s).clone()),
217        HeapValue::Decimal(d) => WireValue::Number(d.to_string().parse().unwrap_or(0.0)),
218        HeapValue::BigInt(i) => WireValue::Integer(**i),
219        HeapValue::Char(c) => WireValue::String(c.to_string()),
220        HeapValue::Future(id) => WireValue::String(format!("<future:{}>", id)),
221        HeapValue::DataTable(dt) => datatable_to_wire(dt.as_ref()),
222        HeapValue::Content(_node) => {
223            // Phase 1.B: the JSON-renderer integration for Content trees
224            // is the deferred Phase 2c content-marshalling rebuild — see
225            // ADR-006 §2.7.4. Until then, surface a placeholder
226            // WireValue rather than emit a partial / wrong-shape
227            // serialization.
228            WireValue::String("<content:phase-2c-rebuild>".to_string())
229        }
230        HeapValue::Instant(t) => WireValue::String(format!("{:?}", **t)),
231        HeapValue::IoHandle(_h) => {
232            // Phase 1.B: IoHandleData no longer exposes a stable `id()`
233            // accessor; the handle's identity is structural (the inner
234            // OS resource) rather than a numeric tag. Phase 2c surfaces
235            // a kind-threaded handle-printer.
236            WireValue::String("<io_handle>".to_string())
237        }
238        HeapValue::NativeScalar(v) => match v {
239            shape_value::heap_value::NativeScalar::I8(n) => WireValue::I8(*n),
240            shape_value::heap_value::NativeScalar::U8(n) => WireValue::U8(*n),
241            shape_value::heap_value::NativeScalar::I16(n) => WireValue::I16(*n),
242            shape_value::heap_value::NativeScalar::U16(n) => WireValue::U16(*n),
243            shape_value::heap_value::NativeScalar::I32(n) => WireValue::I32(*n),
244            shape_value::heap_value::NativeScalar::I64(n) => WireValue::I64(*n),
245            shape_value::heap_value::NativeScalar::U32(n) => WireValue::U32(*n),
246            shape_value::heap_value::NativeScalar::U64(n) => WireValue::U64(*n),
247            shape_value::heap_value::NativeScalar::Isize(n) => WireValue::Isize(*n as i64),
248            shape_value::heap_value::NativeScalar::Usize(n) => WireValue::Usize(*n as u64),
249            shape_value::heap_value::NativeScalar::Ptr(n) => WireValue::Ptr(*n as u64),
250            shape_value::heap_value::NativeScalar::F32(n) => WireValue::F32(*n),
251        },
252        HeapValue::NativeView(v) => WireValue::Object(
253            [
254                (
255                    "__type".to_string(),
256                    WireValue::String(if v.mutable { "cmut" } else { "cview" }.to_string()),
257                ),
258                (
259                    "layout".to_string(),
260                    WireValue::String(v.layout.name.clone()),
261                ),
262                (
263                    "ptr".to_string(),
264                    WireValue::String(format!("0x{:x}", v.ptr)),
265                ),
266            ]
267            .into_iter()
268            .collect(),
269        ),
270        HeapValue::TypedObject(storage) => {
271            // ADR-005 §Forbidden / Q10 forward pointer: wire serialization
272            // must NOT re-introduce Box<HeapValue> slot wrapping. The
273            // schema-driven kind threading below is ADR-005-aligned (typed
274            // slot bits + schema; no intermediate HeapValue materialization
275            // on deserialization).
276            let schema_id = storage.schema_id;
277            let slots = &storage.slots;
278            let schema = ctx
279                .type_schema_registry()
280                .get_by_id(schema_id as u32)
281                .cloned()
282                .or_else(|| crate::type_schema::lookup_schema_by_id_public(schema_id as u32));
283            if let Some(schema) = schema {
284                let mut map = BTreeMap::new();
285                for field_def in &schema.fields {
286                    let idx = field_def.index as usize;
287                    if idx >= slots.len() {
288                        continue;
289                    }
290                    let Some(field_kind) = schema.field_kind(idx) else {
291                        continue;
292                    };
293                    let field_bits = slots[idx].raw();
294                    let field_wire = slot_to_wire(field_bits, field_kind, ctx);
295                    map.insert(field_def.name.clone(), field_wire);
296                }
297                WireValue::Object(map)
298            } else {
299                WireValue::String(format!("<typed_object:schema#{}>", schema_id))
300            }
301        }
302        HeapValue::ClosureRaw(_handle) => {
303            // Phase 1.B: OwnedClosureBlock no longer exposes a public
304            // `function_id()` accessor on the runtime side (the typed-
305            // closure slot ABI carries the function-id via the
306            // `TypedClosureHeader` itself). Phase 2c lands a
307            // schema-aware closure printer.
308            WireValue::String("<closure>".to_string())
309        }
310        HeapValue::TaskGroup(_data) => {
311            WireValue::String("<task_group>".to_string())
312        }
313        // V3-S5 ckpt-5-prime (2026-05-15): `HeapValue::TypedArray(arc)` arm
314        // RETIRED in lockstep with the deleted `HeapValue::TypedArray` variant
315        // (ckpt-4) + deleted `TypedArrayData` inner enum (ckpt-1). Wire
316        // serialisation of v2-raw `*mut TypedArray<T>` pointers lands at the
317        // ckpt-5-prime² + ckpt-6 producer/consumer storage-shape migration
318        // (per-element-type marshal-layer projection before the value becomes
319        // a `HeapValue`). The `typed_array_to_wire` helper below is RETIRED
320        // in the same lockstep. Refusal #1 binding.
321        HeapValue::Temporal(td) => temporal_to_wire(&**td),
322        HeapValue::TableView(tv) => match &**tv {
323            shape_value::heap_value::TableViewData::TypedTable { table, schema_id } => {
324                datatable_to_wire_with_schema(table.as_ref(), Some(*schema_id as u32))
325            }
326            shape_value::heap_value::TableViewData::IndexedTable { table, .. } => {
327                datatable_to_wire(table.as_ref())
328            }
329            shape_value::heap_value::TableViewData::RowView { .. }
330            | shape_value::heap_value::TableViewData::ColumnRef { .. } => {
331                WireValue::String("<table_view:phase-2c>".to_string())
332            }
333        },
334        HeapValue::HashMap(_) => {
335            // Phase 1.B (ADR-006 §2.7.4): kind-threaded HashMap-to-wire
336            // serialization is the deferred Phase 2c marshal rebuild.
337            WireValue::String("<hashmap:phase-2c>".to_string())
338        }
339        // Wave 13 W13-hashset-rebuild (ADR-006 §2.7.15 / Q16,
340        // 2026-05-10): Set wire serialization follows the same
341        // phase-2c deferral shape as HashMap; surface as an opaque
342        // tag until the marshal rebuild lands.
343        HeapValue::HashSet(_) => WireValue::String("<hashset:phase-2c>".to_string()),
344        // Wave 15 W15-deque (ADR-006 §2.7.19 / Q20, 2026-05-10):
345        // Deque wire serialization follows the same phase-2c deferral
346        // shape as HashMap / HashSet — opaque tag until the marshal
347        // rebuild lands.
348        HeapValue::Deque(_) => WireValue::String("<deque:phase-2c>".to_string()),
349        // Wave-γ G-heap-filter-expr (ADR-006 §2.3 / Q8 amendment):
350        // FilterExpr trees are transient query-DSL values; they don't
351        // cross the wire boundary today. Surface as an opaque tag.
352        HeapValue::FilterExpr(_) => WireValue::String("<filter_expr>".to_string()),
353        // ADR-006 §2.7.13 / Q14 (Wave 8 W8-T26, 2026-05-10): Reference
354        // values are within-program data and never cross the wire
355        // boundary. Surface as an opaque tag, same as FilterExpr.
356        HeapValue::Reference(_) => WireValue::String("<ref>".to_string()),
357        // W13-iterator-state (ADR-006 §2.7.16 / Q17, 2026-05-10):
358        // Iterator pipelines are lazy within-program values and never
359        // cross the wire boundary (callers materialise via collect /
360        // forEach / etc. before serialisation). Surface as an opaque
361        // tag, same as FilterExpr / Reference.
362        HeapValue::Iterator(_) => WireValue::String("<iterator>".to_string()),
363        // Wave 15 W15-channel-rebuild (ADR-006 §2.7.20 / Q21, 2026-05-10):
364        // channels are concurrency primitives with interior
365        // `Mutex<ChannelInner>` state; no wire serialization at landing —
366        // same phase-2c deferral shape as HashMap / HashSet. Surface as
367        // an opaque tag for diagnostics.
368        HeapValue::Channel(_) => WireValue::String("<channel:phase-2c>".to_string()),
369        // Wave 15 W15-priority-queue (ADR-006 §2.7.18 / Q19,
370        // 2026-05-10): PriorityQueue wire serialisation projects to a
371        // `WireValue::Array` of i64 priorities in heap-array order
372        // (mirror of the JSON shape — i64-priority-only at landing).
373        HeapValue::PriorityQueue(d) => WireValue::Array(
374            d.heap
375                .iter()
376                .map(|v| WireValue::Integer(*v))
377                .collect(),
378        ),
379        // W15-range (ADR-006 §2.7.23 / Q24, 2026-05-10): Range
380        // serializes as a JSON-ish `{"start", "end", "step",
381        // "inclusive"}` payload via the `as_array_for_wire` shape
382        // (range bounds + step are tiny scalars; lossless round-trip).
383        // Wire serialization here just stamps the literal-form string
384        // — full structured wire is the deferred Phase 2c marshal
385        // rebuild same as HashMap / HashSet (which surface as opaque
386        // tags above). Matches the playbook's "wire/JSON conversion
387        // arms (rejection or proper)" guidance.
388        HeapValue::Range(r) => {
389            let s = if r.inclusive {
390                format!("{}..={}", r.start, r.end)
391            } else {
392                format!("{}..{}", r.start, r.end)
393            };
394            WireValue::String(s)
395        }
396        // Wave 14 W14-variant-codegen (ADR-006 §2.7.17 / Q18, 2026-05-10):
397        // Result/Option carriers are within-program control-flow values;
398        // wire serialisation goes through the AnyError schema for thrown
399        // errors and the unwrapped inner value for `Ok(_)` / `Some(_)`.
400        // Until those marshal paths land, surface as an opaque tag —
401        // same Phase-2c deferral shape as HashMap / HashSet / Iterator.
402        HeapValue::Result(_) => WireValue::String("<result:phase-2c>".to_string()),
403        HeapValue::Option(_) => WireValue::String("<option:phase-2c>".to_string()),
404        // W17-concurrency (ADR-006 §2.7.25, 2026-05-11): concurrency
405        // primitives are runtime-tier handles with no wire shape.
406        // Surface as opaque tags — same Phase-2c deferral shape as
407        // Channel / HashMap / HashSet.
408        HeapValue::Mutex(_) => WireValue::String("<mutex:phase-2c>".to_string()),
409        HeapValue::Atomic(_) => WireValue::String("<atomic:phase-2c>".to_string()),
410        HeapValue::Lazy(_) => WireValue::String("<lazy:phase-2c>".to_string()),
411        // W17-trait-object-storage (ADR-006 §2.7.24 / Q25.C, 2026-05-11):
412        // `dyn Trait` carriers have no wire shape — same Phase-2c
413        // deferral as concurrency primitives. A future `Serializable`
414        // trait could route through the vtable, but that's emission-tier
415        // work outside this sub-cluster.
416        HeapValue::TraitObject(_) => WireValue::String("<trait_object:phase-2c>".to_string()),
417        // W17-comptime-vm-dispatch (ADR-006 §2.7.26, 2026-05-12):
418        // ModuleFn references are VM-internal callable handles
419        // — same opaque-tag shape as the concurrency primitives.
420        HeapValue::ModuleFn(id) => WireValue::String(format!("<module_fn:{}>", id)),
421        // ADR-006 §2.7.22 amendment (Round 18 S3, 2026-05-13): Matrix /
422        // MatrixSlice wire serialisation inherits the N7-architectural-
423        // choice deferral from the pre-amendment
424        // `TypedArrayData::Matrix` / `FloatSlice` shape (the 2D-layout
425        // encoding policy is undecided). Surface as opaque tags —
426        // same Phase-2c deferral pattern as the concurrency primitives.
427        HeapValue::Matrix(m) => {
428            WireValue::String(format!("<matrix:{}x{}:phase-2c>", m.rows, m.cols))
429        }
430        HeapValue::MatrixSlice(s) => {
431            WireValue::String(format!("<matrix_slice:{}:phase-2c>", s.len))
432        }
433    }
434}
435
436// V3-S5 ckpt-5-prime (2026-05-15): `typed_array_to_wire` helper RETIRED per W12
437// audit §3.6 + handover §0 wholesale-deletion cascade. The helper
438// pattern-matched on the deleted `TypedArrayData` enum (retired at ckpt-1) and
439// was called by the deleted `HeapValue::TypedArray` outer arm (retired at
440// ckpt-4) above. The v2-raw `*mut TypedArray<T>` wire-serialisation path lands
441// at the ckpt-5-prime² + ckpt-6 producer/consumer storage-shape migration
442// (per-element-type marshal-layer projection before the value becomes a
443// `HeapValue`). Refusal #1 binding.
444
445fn temporal_to_wire(td: &shape_value::heap_value::TemporalData) -> WireValue {
446    use shape_value::heap_value::TemporalData;
447    match td {
448        TemporalData::DateTime(dt) => WireValue::Timestamp(dt.timestamp_millis()),
449        TemporalData::TimeSpan(d) => WireValue::Duration {
450            value: d.num_milliseconds() as f64,
451            unit: WireDurationUnit::Milliseconds,
452        },
453        TemporalData::Duration(d) => WireValue::Duration {
454            value: d.value,
455            unit: WireDurationUnit::Milliseconds,
456        },
457        TemporalData::Timeframe(_)
458        | TemporalData::TimeReference(_)
459        | TemporalData::DateTimeExpr(_)
460        | TemporalData::DataDateTimeRef(_) => WireValue::String(format!("<{}>", td.type_name())),
461    }
462}
463
464/// Project a `WireValue` to typed slot bits, given the kind the caller
465/// wants. Returns [`MarshalError::KindMismatch`] when wire shape doesn't
466/// match the expected kind.
467///
468/// For heap kinds, this allocates a new `Arc<HeapValue>` and returns
469/// the raw pointer as bits — caller takes ownership of the heap
470/// reference (one strong count).
471pub fn wire_to_slot(wire: &WireValue, expected_kind: NativeKind) -> Result<u64, MarshalError> {
472    match (wire, expected_kind) {
473        (WireValue::Number(n), NativeKind::Float64) => Ok(f64::to_bits(*n)),
474        (WireValue::Integer(i), NativeKind::Int64) => Ok(*i as u64),
475        (WireValue::Bool(b), NativeKind::Bool) => Ok(*b as u64),
476        (WireValue::Null, NativeKind::NullableFloat64) => Ok(f64::to_bits(f64::NAN)),
477        (WireValue::String(s), NativeKind::String) => {
478            let arc = Arc::new(s.clone());
479            Ok(Arc::into_raw(arc) as u64)
480        }
481        (WireValue::I8(n), NativeKind::Int8) => Ok((*n as i64) as u64),
482        (WireValue::I16(n), NativeKind::Int16) => Ok((*n as i64) as u64),
483        (WireValue::I32(n), NativeKind::Int32) => Ok((*n as i64) as u64),
484        (WireValue::U8(n), NativeKind::UInt8) => Ok(*n as u64),
485        (WireValue::U16(n), NativeKind::UInt16) => Ok(*n as u64),
486        (WireValue::U32(n), NativeKind::UInt32) => Ok(*n as u64),
487        (WireValue::U64(n), NativeKind::UInt64) => Ok(*n),
488        // Heap kinds are constructed by allocating Arc<HeapValue> with the
489        // matching variant. Each surviving HeapKind variant is handled here
490        // as stdlib mass migration (Phase 2c) and the snapshot replay path
491        // discover concrete consumers.
492        (WireValue::String(s), NativeKind::Ptr(HeapKind::String)) => {
493            let arc = Arc::new(HeapValue::String(Arc::new(s.clone())));
494            Ok(Arc::into_raw(arc) as u64)
495        }
496        (WireValue::Table(table), NativeKind::Ptr(HeapKind::DataTable)) => {
497            let dt = datatable_from_ipc_bytes(&table.ipc_bytes, None, None)
498                .map_err(MarshalError::Body)?;
499            let arc = Arc::new(HeapValue::DataTable(Arc::new(dt)));
500            Ok(Arc::into_raw(arc) as u64)
501        }
502        // Calling site passed a wire/kind pair we don't currently handle.
503        // The strict-typed answer is to extend this match, not fall back —
504        // each new case represents a concrete stdlib/wire shape, and
505        // pattern-match exhaustiveness is the discipline.
506        _ => Err(MarshalError::Body(format!(
507            "wire_to_slot: no projection for wire variant into kind {:?}",
508            expected_kind
509        ))),
510    }
511}
512
513/// Wrap a typed slot in a `ValueEnvelope` with optional metadata.
514///
515/// `type_name` is the user-facing Shape type name (e.g. `"int"`,
516/// `"DataTable"`, `"MyType"`). The envelope's `type_info` is populated
517/// from the type registry when available.
518pub fn slot_to_envelope(
519    bits: u64,
520    kind: NativeKind,
521    type_name: &str,
522    ctx: &Context,
523) -> ValueEnvelope {
524    let value = slot_to_wire(bits, kind, ctx);
525    let _ = type_name;
526    let _ = ctx;
527    // Phase 1.B (ADR-006 §2.7.4): the type-info / type-registry lookup
528    // path that resolved a `TypeRegistry` from `TypeRegistry::default()`
529    // is gone; the rebuilt path queries `TypeRegistry::for_number` /
530    // primitives + the runtime's per-schema cache. Until the kind-
531    // threaded envelope lookup lands in Phase 2c, fall back to the
532    // wire-side inference helper.
533    ValueEnvelope::from_value(value)
534}
535
536/// If the slot carries a renderable Content shape (Content node, DataTable,
537/// or TableView), return `(content_json, content_html, content_terminal)`.
538/// Otherwise all three are `None`.
539///
540/// W18.2 (R8 — output-adapter integration): the kind-threaded content
541/// dispatch is rebuilt on top of the surviving 6-renderer infrastructure
542/// (TERMINAL / HTML / MARKDOWN / JSON / PLAIN). Slot bits are dispatched
543/// per `HeapKind`, the underlying typed payload is materialised as a
544/// `ContentNode`, and each renderer projects the node into its own
545/// output shape:
546///
547/// - JSON via [`crate::renderers::json::JsonRenderer`] parsed into
548///   [`serde_json::Value`] (the renderer guarantees valid JSON per the
549///   `renderers::cross_renderer_tests::json_is_valid_json` regression).
550/// - HTML via [`crate::renderers::html::HtmlRenderer`].
551/// - Terminal via [`crate::renderers::terminal::TerminalRenderer`] (ANSI
552///   escapes; consumers route to PLAIN when the sink is non-TTY).
553///
554/// Pre-rebuild this function returned `(None, None, None)` for every
555/// Content-bearing slot per the Phase 1.B placeholder — that scar is
556/// resolved here.
557pub fn slot_extract_content(
558    bits: u64,
559    kind: NativeKind,
560) -> (Option<serde_json::Value>, Option<String>, Option<String>) {
561    let NativeKind::Ptr(hk) = kind else {
562        return (None, None, None);
563    };
564    if bits == 0 {
565        return (None, None, None);
566    }
567    // SAFETY: `Ptr(HeapKind::*)` contract — bits is a valid typed pointer
568    // for the labelled heap kind. The `(hk, hv)` cross-check below is
569    // belt-and-braces; an inconsistent label is a producer-side bug.
570    let node = match hk {
571        HeapKind::Content => {
572            let hv = unsafe { &*(bits as *const HeapValue) };
573            match hv {
574                HeapValue::Content(node) => Some((**node).clone()),
575                _ => None,
576            }
577        }
578        HeapKind::DataTable => {
579            let dt: &shape_value::DataTable =
580                unsafe { &*(bits as *const shape_value::DataTable) };
581            Some(crate::content_dispatch::datatable_to_content_node(dt, None))
582        }
583        HeapKind::TableView => {
584            let tv: &shape_value::heap_value::TableViewData =
585                unsafe { &*(bits as *const shape_value::heap_value::TableViewData) };
586            match tv {
587                shape_value::heap_value::TableViewData::TypedTable { table, .. }
588                | shape_value::heap_value::TableViewData::IndexedTable { table, .. } => Some(
589                    crate::content_dispatch::datatable_to_content_node(table.as_ref(), None),
590                ),
591                // RowView / ColumnRef are deferred Phase 2c content
592                // adapters — no current renderer.
593                _ => None,
594            }
595        }
596        _ => None,
597    };
598    let Some(node) = node else {
599        return (None, None, None);
600    };
601
602    // W18.2: route the materialised `ContentNode` through each of the
603    // three renderer adapters wired into the host boundary. The JSON
604    // renderer's output is guaranteed-valid JSON (see
605    // `renderers::cross_renderer_tests::json_is_valid_json`); the
606    // `serde_json::from_str` fallback to `None` is defensive only.
607    use crate::content_renderer::ContentRenderer;
608    let json_renderer = crate::renderers::json::JsonRenderer;
609    let html_renderer = crate::renderers::html::HtmlRenderer::new();
610    let terminal_renderer = crate::renderers::terminal::TerminalRenderer::new();
611    let content_json: Option<serde_json::Value> =
612        serde_json::from_str(&json_renderer.render(&node)).ok();
613    let content_html = Some(html_renderer.render(&node));
614    let content_terminal = Some(terminal_renderer.render(&node));
615    (content_json, content_html, content_terminal)
616}
617
618// ───────────────────────── DataTable ↔ wire/IPC ─────────────────────────
619//
620// Typed-handle conversions. These don't go through `(bits, kind)` —
621// the caller passes a `&DataTable` directly, which is the typed-Rust
622// equivalent of NativeKind::Ptr(HeapKind::DataTable). The marshal layer
623// uses these internally when projecting a DataTable slot.
624
625pub fn datatable_to_wire(dt: &DataTable) -> WireValue {
626    datatable_to_wire_with_schema(dt, dt.schema_id())
627}
628
629fn datatable_to_wire_with_schema(dt: &DataTable, schema_id: Option<u32>) -> WireValue {
630    match datatable_to_ipc_bytes(dt) {
631        Ok(ipc_bytes) => WireValue::Table(WireTable {
632            ipc_bytes,
633            type_name: None,
634            schema_id,
635            row_count: dt.row_count(),
636            column_count: dt.column_count(),
637        }),
638        Err(e) => WireValue::String(format!("<datatable_serialize_error: {}>", e)),
639    }
640}
641
642pub fn datatable_to_ipc_bytes(dt: &DataTable) -> std::result::Result<Vec<u8>, String> {
643    // The DataTable now wraps a `RecordBatch` directly (`inner()`); the
644    // pre-bulldozer `to_arrow_batch` accessor is gone since the wrapper
645    // is the batch.
646    let arrow_batch = dt.inner();
647    let schema = arrow_batch.schema();
648    let mut buf = Vec::new();
649    {
650        let mut writer = FileWriter::try_new(&mut buf, &schema)
651            .map_err(|e| format!("Arrow IPC writer init failed: {}", e))?;
652        writer
653            .write(arrow_batch)
654            .map_err(|e| format!("Arrow IPC write failed: {}", e))?;
655        writer
656            .finish()
657            .map_err(|e| format!("Arrow IPC finish failed: {}", e))?;
658    }
659    Ok(buf)
660}
661
662pub fn datatable_from_ipc_bytes(
663    bytes: &[u8],
664    column_overrides: Option<&[shape_value::datatable::ColumnPtrs]>,
665    schema_id_override: Option<u32>,
666) -> std::result::Result<DataTable, String> {
667    let cursor = std::io::Cursor::new(bytes);
668    let reader = FileReader::try_new(cursor, None)
669        .map_err(|e| format!("Arrow IPC reader init failed: {}", e))?;
670    let mut batches = Vec::new();
671    for batch in reader {
672        batches.push(batch.map_err(|e| format!("Arrow IPC batch read failed: {}", e))?);
673    }
674    if batches.is_empty() {
675        return Err(
676            "datatable_from_ipc_bytes: empty IPC stream — no Arrow RecordBatch to wrap".to_string(),
677        );
678    }
679    // The first batch is the canonical wrapper; concatenation is a
680    // Phase 2c rebuild item alongside the broader DataTable IPC layer.
681    let first = batches.into_iter().next().unwrap();
682    let _ = column_overrides;
683    let dt = DataTable::new(first);
684    let dt = if let Some(sid) = schema_id_override {
685        dt.with_schema_id(sid)
686    } else {
687        dt
688    };
689    Ok(dt)
690}
691
692#[cfg(test)]
693mod u64_wire_tests {
694    //! R5c-2-β-γ checkpoint (b) u64-carrier — wire/snapshot round-trip.
695    //!
696    //! A `NativeKind::UInt64` slot must round-trip through MessagePack
697    //! wire serialization with its full `0..2^64` range intact: a value
698    //! above `i64::MAX` must NOT be lossy-projected to a signed
699    //! `WireValue::Integer`. The carrier projects to `WireValue::U64`
700    //! (full-range), and `wire_to_slot` recovers the exact bits. The
701    //! snapshot path serializes the parallel `Vec<u64>` data + the slot's
702    //! `NativeKind` verbatim (per ADR-006 §2.7.7), so the same bit/kind
703    //! pair is preserved there by construction.
704
705    use super::{slot_to_wire, wire_to_slot};
706    use crate::context::ExecutionContext;
707    use shape_value::NativeKind;
708    use shape_wire::WireValue;
709
710    fn roundtrip(bits: u64) -> u64 {
711        let ctx = ExecutionContext::new_empty();
712        let wire = slot_to_wire(bits, NativeKind::UInt64, &ctx);
713        // Full-range u64 must project to the dedicated U64 wire variant,
714        // not a lossy signed Integer.
715        assert!(
716            matches!(wire, WireValue::U64(_)),
717            "u64 slot must project to WireValue::U64, got {:?}",
718            wire
719        );
720        wire_to_slot(&wire, NativeKind::UInt64).expect("u64 wire should decode")
721    }
722
723    #[test]
724    fn u64_max_round_trips() {
725        assert_eq!(roundtrip(u64::MAX), u64::MAX);
726    }
727
728    #[test]
729    fn u64_above_i64_max_round_trips_lossless() {
730        // i64::MAX + 1 — the first value a signed projection would corrupt.
731        let v = (i64::MAX as u64) + 1;
732        assert_eq!(roundtrip(v), v);
733    }
734
735    #[test]
736    fn u64_small_round_trips() {
737        assert_eq!(roundtrip(0), 0);
738        assert_eq!(roundtrip(42), 42);
739    }
740
741    #[test]
742    fn u64_wire_variant_preserves_full_range() {
743        // The wire value itself must carry the full unsigned magnitude.
744        let ctx = ExecutionContext::new_empty();
745        match slot_to_wire(u64::MAX, NativeKind::UInt64, &ctx) {
746            WireValue::U64(n) => assert_eq!(n, u64::MAX),
747            other => panic!("expected WireValue::U64, got {:?}", other),
748        }
749    }
750}
751
752#[cfg(test)]
753mod char_wire_tests {
754    //! β-fix CKPT-A char-carrier — `charAt` return-value wire projection.
755    //!
756    //! `op_string_char_at` (and its char-producing siblings) push a
757    //! scalar Unicode codepoint. Pre-fix the slot was stamped
758    //! `NativeKind::Ptr(HeapKind::Char)` — a scalar codepoint mislabeled
759    //! as an `Arc<HeapValue>` pointer. When that slot was the program
760    //! return value, `heap_to_wire` cast the codepoint bits (e.g. 0x63 =
761    //! 'c') to `*const HeapValue` and dereferenced it, triggering a
762    //! misaligned-pointer non-unwinding abort (SIGABRT).
763    //!
764    //! The producer-side fix stamps the canonical scalar
765    //! `NativeKind::Char` (ADR-006 §2.7.5). The defensive `heap_to_wire`
766    //! `HeapKind::Char` early-arm guarantees that even a mislabeled
767    //! `Ptr(HeapKind::Char)` slot projects the codepoint directly
768    //! instead of dereferencing it as a heap object.
769
770    use super::{heap_to_wire, slot_to_wire};
771    use crate::context::ExecutionContext;
772    use shape_value::{HeapKind, NativeKind};
773    use shape_wire::WireValue;
774
775    /// The canonical post-fix carrier: a scalar `NativeKind::Char` slot
776    /// (codepoint inline) projects to a single-codepoint string with no
777    /// pointer dereference.
778    #[test]
779    fn native_kind_char_slot_projects_to_string() {
780        let ctx = ExecutionContext::new_empty();
781        // 'c' = U+0063 — the `"abc".reverse().charAt(0)` reproducer result.
782        let wire = slot_to_wire('c' as u64, NativeKind::Char, &ctx);
783        assert_eq!(wire, WireValue::String("c".to_string()));
784    }
785
786    /// `charAt(0)` of `"abc"` returns 'a' — direct (non-reversed) path.
787    #[test]
788    fn native_kind_char_slot_first_codepoint() {
789        let ctx = ExecutionContext::new_empty();
790        let wire = slot_to_wire('a' as u64, NativeKind::Char, &ctx);
791        assert_eq!(wire, WireValue::String("a".to_string()));
792    }
793
794    /// Defensive: a `Ptr(HeapKind::Char)`-labeled slot (a mislabeled
795    /// scalar codepoint, e.g. emitted by any un-migrated producer) must
796    /// NOT be dereferenced as an `Arc<HeapValue>`. The `heap_to_wire`
797    /// early-arm projects the codepoint directly. Pre-fix this input
798    /// aborted the process with a misaligned-pointer panic.
799    #[test]
800    fn heap_kind_char_label_does_not_deref_codepoint() {
801        let ctx = ExecutionContext::new_empty();
802        // 0x63 ('c') is NOT 8-byte aligned and is not a valid HeapValue
803        // pointer — the pre-fix catch-all would have aborted here.
804        let wire = heap_to_wire('c' as u64, HeapKind::Char, &ctx);
805        assert_eq!(wire, WireValue::String("c".to_string()));
806    }
807
808    /// Defensive arm also covers a non-ASCII multi-byte codepoint.
809    #[test]
810    fn heap_kind_char_label_handles_unicode_codepoint() {
811        let ctx = ExecutionContext::new_empty();
812        let wire = heap_to_wire('λ' as u64, HeapKind::Char, &ctx);
813        assert_eq!(wire, WireValue::String("λ".to_string()));
814    }
815
816    /// A `Ptr(HeapKind::Char)` slot routed through the public
817    /// `slot_to_wire` entry point (the program-return-value path) also
818    /// projects safely — this is the exact path the SIGABRT reproducer
819    /// exercised.
820    #[test]
821    fn slot_to_wire_char_label_return_value_path_is_safe() {
822        let ctx = ExecutionContext::new_empty();
823        let wire = slot_to_wire(
824            'c' as u64,
825            NativeKind::Ptr(HeapKind::Char),
826            &ctx,
827        );
828        assert_eq!(wire, WireValue::String("c".to_string()));
829    }
830}
831
832#[cfg(test)]
833mod ws3_f2b_result_option_wire_tests {
834    //! WS-3 F2b — `Result` / `Option` program-return-value wire projection.
835    //!
836    //! `HeapKind::Result` / `HeapKind::Option` are typed-Arc dispatch
837    //! labels — their slot bits are `Arc::into_raw(Arc<ResultData>)` /
838    //! `Arc::into_raw(Arc<OptionData>)`, NOT an `Arc<HeapValue>`. Pre-fix,
839    //! `heap_to_wire`'s catch-all cast those bits to `*const HeapValue`
840    //! and dereferenced — reading a `ResultData`/`OptionData` as a
841    //! `HeapValue` enum (type confusion + UB). This crashed (SIGSEGV)
842    //! whenever a `Result`/`Option` was the program's terminal value
843    //! (e.g. `fn main() -> Result<int,string>` returning `Ok(0)`), which
844    //! made every `?`-using program crash once it compiled.
845    //!
846    //! The fix adds dedicated `HeapKind::Result` / `HeapKind::Option`
847    //! arms that read the typed payload directly.
848
849    use super::slot_to_wire;
850    use crate::context::ExecutionContext;
851    use shape_value::heap_value::{OptionData, ResultData};
852    use shape_value::kinded_slot::KindedSlot;
853    use shape_wire::WireValue;
854    use std::sync::Arc;
855
856    #[test]
857    fn ok_result_projects_to_wire_result_ok() {
858        let ctx = ExecutionContext::new_empty();
859        let payload = KindedSlot::from_int(42);
860        let slot = KindedSlot::from_result(Arc::new(ResultData::ok(payload)));
861        let wire = slot_to_wire(slot.raw(), slot.kind(), &ctx);
862        match wire {
863            WireValue::Result { ok, value } => {
864                assert!(ok, "Ok(42) must wire with ok=true");
865                assert_eq!(*value, WireValue::Integer(42));
866            }
867            other => panic!("expected WireValue::Result, got {:?}", other),
868        }
869    }
870
871    #[test]
872    fn err_result_projects_to_wire_result_err() {
873        let ctx = ExecutionContext::new_empty();
874        let payload = KindedSlot::from_int(7);
875        let slot = KindedSlot::from_result(Arc::new(ResultData::err(payload)));
876        let wire = slot_to_wire(slot.raw(), slot.kind(), &ctx);
877        match wire {
878            WireValue::Result { ok, value } => {
879                assert!(!ok, "Err(7) must wire with ok=false");
880                assert_eq!(*value, WireValue::Integer(7));
881            }
882            other => panic!("expected WireValue::Result, got {:?}", other),
883        }
884    }
885
886    #[test]
887    fn some_option_projects_to_inner_value() {
888        let ctx = ExecutionContext::new_empty();
889        let payload = KindedSlot::from_int(5);
890        let slot = KindedSlot::from_option(Arc::new(OptionData::some(payload)));
891        let wire = slot_to_wire(slot.raw(), slot.kind(), &ctx);
892        // Null-coding semantics: `Some(x) ≡ x`.
893        assert_eq!(wire, WireValue::Integer(5));
894    }
895
896    #[test]
897    fn none_option_projects_to_null() {
898        let ctx = ExecutionContext::new_empty();
899        let slot = KindedSlot::from_option(Arc::new(OptionData::none()));
900        let wire = slot_to_wire(slot.raw(), slot.kind(), &ctx);
901        assert_eq!(wire, WireValue::Null);
902    }
903}