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        // Round 19 S1.5 W12-nativekind-scalar-additions (2026-05-14):
77        // ADR-006 §2.7.5 amendment adds F32 + Char as 4-byte scalar
78        // variants. Wire projection: F32 widens to `WireValue::Number`
79        // (`f64::from(f32)` is lossless); Char projects to a single-
80        // codepoint string (mirror of the `HeapValue::Char` arm below)
81        // because `WireValue` has no dedicated Char variant.
82        NativeKind::Float32 => WireValue::Number(f64::from(f32::from_bits(bits as u32))),
83        NativeKind::Char => match char::from_u32(bits as u32) {
84            Some(c) => WireValue::String(c.to_string()),
85            None => WireValue::Null,
86        },
87        NativeKind::String => {
88            // bits is an Arc<String> raw pointer
89            let ptr = bits as *const String;
90            // SAFETY: kind contract pins this slot to an Arc<String> raw ptr.
91            let s = unsafe { &*ptr };
92            WireValue::String(s.clone())
93        }
94        // Wave 2 Agent B W12-StringV2-DecimalV2-NativeKind-additions
95        // (ADR-006 §2.7.5 amendment, 2026-05-14): the v2-raw `*const StringObj`
96        // carrier projects to the same `WireValue::String` wire shape as
97        // `NativeKind::String` (Arc-wrapped sibling), via the carrier's
98        // `as_str` accessor reading the UTF-8 payload at offset 8 (data ptr)
99        // / 16 (len) of the `repr(C)` struct. The slot bits are NOT an
100        // `Arc<T>` pointer — `StringObj` is a manually-allocated `repr(C)`
101        // 24-byte carrier per `v2/string_obj.rs`.
102        NativeKind::StringV2 => {
103            if bits == 0 {
104                return WireValue::Null;
105            }
106            // SAFETY: per the §2.7.5 amendment construction contract,
107            // kind=StringV2 means bits = `ptr as u64` pointing to a live
108            // `StringObj` with bumped refcount — the slot owns one
109            // v2-retain share for the duration of this call.
110            let ptr = bits as *const shape_value::v2::string_obj::StringObj;
111            let s: &str = unsafe { shape_value::v2::string_obj::StringObj::as_str(ptr) };
112            WireValue::String(s.to_string())
113        }
114        // Wave 2 Agent B: the v2-raw `*const DecimalObj` carrier projects
115        // to `WireValue::Number` (the same wire shape as
116        // `HeapValue::Decimal` per `heap_value_to_wire` below) via the
117        // carrier's `value` accessor reading the inline `rust_decimal::Decimal`
118        // at offset 8 of the `repr(C)` struct.
119        NativeKind::DecimalV2 => {
120            if bits == 0 {
121                return WireValue::Null;
122            }
123            // SAFETY: per the §2.7.5 amendment construction contract,
124            // kind=DecimalV2 means bits = `ptr as u64` pointing to a live
125            // `DecimalObj` with bumped refcount.
126            let ptr = bits as *const shape_value::v2::decimal_obj::DecimalObj;
127            let value = unsafe { shape_value::v2::decimal_obj::DecimalObj::value(ptr) };
128            WireValue::Number(value.to_string().parse().unwrap_or(0.0))
129        }
130        NativeKind::Ptr(hk) => heap_to_wire(bits, hk, ctx),
131    }
132}
133
134/// Project an `Arc<HeapValue>` raw pointer slot to `WireValue`,
135/// dispatching on the pre-known `HeapKind` rather than probing the
136/// heap object's self-reported `kind()`.
137fn heap_to_wire(bits: u64, hk: HeapKind, ctx: &Context) -> WireValue {
138    if bits == 0 {
139        return WireValue::Null;
140    }
141    let ptr = bits as *const HeapValue;
142    // SAFETY: NativeKind::Ptr(hk) contract — bits is a valid Arc<HeapValue> ptr.
143    let hv = unsafe { &*ptr };
144    debug_assert_eq!(
145        hv.kind(),
146        hk,
147        "slot kind {:?} does not match HeapValue::{:?}",
148        hk,
149        hv.kind()
150    );
151    heap_value_to_wire(hv, ctx)
152}
153
154/// Project a `&HeapValue` to `WireValue` by dispatching on its
155/// surviving variants. Reused by the snapshot path (Phase 2b
156/// snapshot.rs commit) which has the same heap projection needs.
157pub fn heap_value_to_wire(hv: &HeapValue, ctx: &Context) -> WireValue {
158    match hv {
159        HeapValue::String(s) => WireValue::String((**s).clone()),
160        HeapValue::Decimal(d) => WireValue::Number(d.to_string().parse().unwrap_or(0.0)),
161        HeapValue::BigInt(i) => WireValue::Integer(**i),
162        HeapValue::Char(c) => WireValue::String(c.to_string()),
163        HeapValue::Future(id) => WireValue::String(format!("<future:{}>", id)),
164        HeapValue::DataTable(dt) => datatable_to_wire(dt.as_ref()),
165        HeapValue::Content(_node) => {
166            // Phase 1.B: the JSON-renderer integration for Content trees
167            // is the deferred Phase 2c content-marshalling rebuild — see
168            // ADR-006 §2.7.4. Until then, surface a placeholder
169            // WireValue rather than emit a partial / wrong-shape
170            // serialization.
171            WireValue::String("<content:phase-2c-rebuild>".to_string())
172        }
173        HeapValue::Instant(t) => WireValue::String(format!("{:?}", **t)),
174        HeapValue::IoHandle(_h) => {
175            // Phase 1.B: IoHandleData no longer exposes a stable `id()`
176            // accessor; the handle's identity is structural (the inner
177            // OS resource) rather than a numeric tag. Phase 2c surfaces
178            // a kind-threaded handle-printer.
179            WireValue::String("<io_handle>".to_string())
180        }
181        HeapValue::NativeScalar(v) => match v {
182            shape_value::heap_value::NativeScalar::I8(n) => WireValue::I8(*n),
183            shape_value::heap_value::NativeScalar::U8(n) => WireValue::U8(*n),
184            shape_value::heap_value::NativeScalar::I16(n) => WireValue::I16(*n),
185            shape_value::heap_value::NativeScalar::U16(n) => WireValue::U16(*n),
186            shape_value::heap_value::NativeScalar::I32(n) => WireValue::I32(*n),
187            shape_value::heap_value::NativeScalar::I64(n) => WireValue::I64(*n),
188            shape_value::heap_value::NativeScalar::U32(n) => WireValue::U32(*n),
189            shape_value::heap_value::NativeScalar::U64(n) => WireValue::U64(*n),
190            shape_value::heap_value::NativeScalar::Isize(n) => WireValue::Isize(*n as i64),
191            shape_value::heap_value::NativeScalar::Usize(n) => WireValue::Usize(*n as u64),
192            shape_value::heap_value::NativeScalar::Ptr(n) => WireValue::Ptr(*n as u64),
193            shape_value::heap_value::NativeScalar::F32(n) => WireValue::F32(*n),
194        },
195        HeapValue::NativeView(v) => WireValue::Object(
196            [
197                (
198                    "__type".to_string(),
199                    WireValue::String(if v.mutable { "cmut" } else { "cview" }.to_string()),
200                ),
201                (
202                    "layout".to_string(),
203                    WireValue::String(v.layout.name.clone()),
204                ),
205                (
206                    "ptr".to_string(),
207                    WireValue::String(format!("0x{:x}", v.ptr)),
208                ),
209            ]
210            .into_iter()
211            .collect(),
212        ),
213        HeapValue::TypedObject(storage) => {
214            // ADR-005 §Forbidden / Q10 forward pointer: wire serialization
215            // must NOT re-introduce Box<HeapValue> slot wrapping. The
216            // schema-driven kind threading below is ADR-005-aligned (typed
217            // slot bits + schema; no intermediate HeapValue materialization
218            // on deserialization).
219            let schema_id = storage.schema_id;
220            let slots = &storage.slots;
221            let schema = ctx
222                .type_schema_registry()
223                .get_by_id(schema_id as u32)
224                .cloned()
225                .or_else(|| crate::type_schema::lookup_schema_by_id_public(schema_id as u32));
226            if let Some(schema) = schema {
227                let mut map = BTreeMap::new();
228                for field_def in &schema.fields {
229                    let idx = field_def.index as usize;
230                    if idx >= slots.len() {
231                        continue;
232                    }
233                    let Some(field_kind) = schema.field_kind(idx) else {
234                        continue;
235                    };
236                    let field_bits = slots[idx].raw();
237                    let field_wire = slot_to_wire(field_bits, field_kind, ctx);
238                    map.insert(field_def.name.clone(), field_wire);
239                }
240                WireValue::Object(map)
241            } else {
242                WireValue::String(format!("<typed_object:schema#{}>", schema_id))
243            }
244        }
245        HeapValue::ClosureRaw(_handle) => {
246            // Phase 1.B: OwnedClosureBlock no longer exposes a public
247            // `function_id()` accessor on the runtime side (the typed-
248            // closure slot ABI carries the function-id via the
249            // `TypedClosureHeader` itself). Phase 2c lands a
250            // schema-aware closure printer.
251            WireValue::String("<closure>".to_string())
252        }
253        HeapValue::TaskGroup(_data) => {
254            WireValue::String("<task_group>".to_string())
255        }
256        // V3-S5 ckpt-5-prime (2026-05-15): `HeapValue::TypedArray(arc)` arm
257        // RETIRED in lockstep with the deleted `HeapValue::TypedArray` variant
258        // (ckpt-4) + deleted `TypedArrayData` inner enum (ckpt-1). Wire
259        // serialisation of v2-raw `*mut TypedArray<T>` pointers lands at the
260        // ckpt-5-prime² + ckpt-6 producer/consumer storage-shape migration
261        // (per-element-type marshal-layer projection before the value becomes
262        // a `HeapValue`). The `typed_array_to_wire` helper below is RETIRED
263        // in the same lockstep. Refusal #1 binding.
264        HeapValue::Temporal(td) => temporal_to_wire(&**td),
265        HeapValue::TableView(tv) => match &**tv {
266            shape_value::heap_value::TableViewData::TypedTable { table, schema_id } => {
267                datatable_to_wire_with_schema(table.as_ref(), Some(*schema_id as u32))
268            }
269            shape_value::heap_value::TableViewData::IndexedTable { table, .. } => {
270                datatable_to_wire(table.as_ref())
271            }
272            shape_value::heap_value::TableViewData::RowView { .. }
273            | shape_value::heap_value::TableViewData::ColumnRef { .. } => {
274                WireValue::String("<table_view:phase-2c>".to_string())
275            }
276        },
277        HeapValue::HashMap(_) => {
278            // Phase 1.B (ADR-006 §2.7.4): kind-threaded HashMap-to-wire
279            // serialization is the deferred Phase 2c marshal rebuild.
280            WireValue::String("<hashmap:phase-2c>".to_string())
281        }
282        // Wave 13 W13-hashset-rebuild (ADR-006 §2.7.15 / Q16,
283        // 2026-05-10): Set wire serialization follows the same
284        // phase-2c deferral shape as HashMap; surface as an opaque
285        // tag until the marshal rebuild lands.
286        HeapValue::HashSet(_) => WireValue::String("<hashset:phase-2c>".to_string()),
287        // Wave 15 W15-deque (ADR-006 §2.7.19 / Q20, 2026-05-10):
288        // Deque wire serialization follows the same phase-2c deferral
289        // shape as HashMap / HashSet — opaque tag until the marshal
290        // rebuild lands.
291        HeapValue::Deque(_) => WireValue::String("<deque:phase-2c>".to_string()),
292        // Wave-γ G-heap-filter-expr (ADR-006 §2.3 / Q8 amendment):
293        // FilterExpr trees are transient query-DSL values; they don't
294        // cross the wire boundary today. Surface as an opaque tag.
295        HeapValue::FilterExpr(_) => WireValue::String("<filter_expr>".to_string()),
296        // ADR-006 §2.7.13 / Q14 (Wave 8 W8-T26, 2026-05-10): Reference
297        // values are within-program data and never cross the wire
298        // boundary. Surface as an opaque tag, same as FilterExpr.
299        HeapValue::Reference(_) => WireValue::String("<ref>".to_string()),
300        // W13-iterator-state (ADR-006 §2.7.16 / Q17, 2026-05-10):
301        // Iterator pipelines are lazy within-program values and never
302        // cross the wire boundary (callers materialise via collect /
303        // forEach / etc. before serialisation). Surface as an opaque
304        // tag, same as FilterExpr / Reference.
305        HeapValue::Iterator(_) => WireValue::String("<iterator>".to_string()),
306        // Wave 15 W15-channel-rebuild (ADR-006 §2.7.20 / Q21, 2026-05-10):
307        // channels are concurrency primitives with interior
308        // `Mutex<ChannelInner>` state; no wire serialization at landing —
309        // same phase-2c deferral shape as HashMap / HashSet. Surface as
310        // an opaque tag for diagnostics.
311        HeapValue::Channel(_) => WireValue::String("<channel:phase-2c>".to_string()),
312        // Wave 15 W15-priority-queue (ADR-006 §2.7.18 / Q19,
313        // 2026-05-10): PriorityQueue wire serialisation projects to a
314        // `WireValue::Array` of i64 priorities in heap-array order
315        // (mirror of the JSON shape — i64-priority-only at landing).
316        HeapValue::PriorityQueue(d) => WireValue::Array(
317            d.heap
318                .iter()
319                .map(|v| WireValue::Integer(*v))
320                .collect(),
321        ),
322        // W15-range (ADR-006 §2.7.23 / Q24, 2026-05-10): Range
323        // serializes as a JSON-ish `{"start", "end", "step",
324        // "inclusive"}` payload via the `as_array_for_wire` shape
325        // (range bounds + step are tiny scalars; lossless round-trip).
326        // Wire serialization here just stamps the literal-form string
327        // — full structured wire is the deferred Phase 2c marshal
328        // rebuild same as HashMap / HashSet (which surface as opaque
329        // tags above). Matches the playbook's "wire/JSON conversion
330        // arms (rejection or proper)" guidance.
331        HeapValue::Range(r) => {
332            let s = if r.inclusive {
333                format!("{}..={}", r.start, r.end)
334            } else {
335                format!("{}..{}", r.start, r.end)
336            };
337            WireValue::String(s)
338        }
339        // Wave 14 W14-variant-codegen (ADR-006 §2.7.17 / Q18, 2026-05-10):
340        // Result/Option carriers are within-program control-flow values;
341        // wire serialisation goes through the AnyError schema for thrown
342        // errors and the unwrapped inner value for `Ok(_)` / `Some(_)`.
343        // Until those marshal paths land, surface as an opaque tag —
344        // same Phase-2c deferral shape as HashMap / HashSet / Iterator.
345        HeapValue::Result(_) => WireValue::String("<result:phase-2c>".to_string()),
346        HeapValue::Option(_) => WireValue::String("<option:phase-2c>".to_string()),
347        // W17-concurrency (ADR-006 §2.7.25, 2026-05-11): concurrency
348        // primitives are runtime-tier handles with no wire shape.
349        // Surface as opaque tags — same Phase-2c deferral shape as
350        // Channel / HashMap / HashSet.
351        HeapValue::Mutex(_) => WireValue::String("<mutex:phase-2c>".to_string()),
352        HeapValue::Atomic(_) => WireValue::String("<atomic:phase-2c>".to_string()),
353        HeapValue::Lazy(_) => WireValue::String("<lazy:phase-2c>".to_string()),
354        // W17-trait-object-storage (ADR-006 §2.7.24 / Q25.C, 2026-05-11):
355        // `dyn Trait` carriers have no wire shape — same Phase-2c
356        // deferral as concurrency primitives. A future `Serializable`
357        // trait could route through the vtable, but that's emission-tier
358        // work outside this sub-cluster.
359        HeapValue::TraitObject(_) => WireValue::String("<trait_object:phase-2c>".to_string()),
360        // W17-comptime-vm-dispatch (ADR-006 §2.7.26, 2026-05-12):
361        // ModuleFn references are VM-internal callable handles
362        // — same opaque-tag shape as the concurrency primitives.
363        HeapValue::ModuleFn(id) => WireValue::String(format!("<module_fn:{}>", id)),
364        // ADR-006 §2.7.22 amendment (Round 18 S3, 2026-05-13): Matrix /
365        // MatrixSlice wire serialisation inherits the N7-architectural-
366        // choice deferral from the pre-amendment
367        // `TypedArrayData::Matrix` / `FloatSlice` shape (the 2D-layout
368        // encoding policy is undecided). Surface as opaque tags —
369        // same Phase-2c deferral pattern as the concurrency primitives.
370        HeapValue::Matrix(m) => {
371            WireValue::String(format!("<matrix:{}x{}:phase-2c>", m.rows, m.cols))
372        }
373        HeapValue::MatrixSlice(s) => {
374            WireValue::String(format!("<matrix_slice:{}:phase-2c>", s.len))
375        }
376    }
377}
378
379// V3-S5 ckpt-5-prime (2026-05-15): `typed_array_to_wire` helper RETIRED per W12
380// audit §3.6 + handover §0 wholesale-deletion cascade. The helper
381// pattern-matched on the deleted `TypedArrayData` enum (retired at ckpt-1) and
382// was called by the deleted `HeapValue::TypedArray` outer arm (retired at
383// ckpt-4) above. The v2-raw `*mut TypedArray<T>` wire-serialisation path lands
384// at the ckpt-5-prime² + ckpt-6 producer/consumer storage-shape migration
385// (per-element-type marshal-layer projection before the value becomes a
386// `HeapValue`). Refusal #1 binding.
387
388fn temporal_to_wire(td: &shape_value::heap_value::TemporalData) -> WireValue {
389    use shape_value::heap_value::TemporalData;
390    match td {
391        TemporalData::DateTime(dt) => WireValue::Timestamp(dt.timestamp_millis()),
392        TemporalData::TimeSpan(d) => WireValue::Duration {
393            value: d.num_milliseconds() as f64,
394            unit: WireDurationUnit::Milliseconds,
395        },
396        TemporalData::Duration(d) => WireValue::Duration {
397            value: d.value,
398            unit: WireDurationUnit::Milliseconds,
399        },
400        TemporalData::Timeframe(_)
401        | TemporalData::TimeReference(_)
402        | TemporalData::DateTimeExpr(_)
403        | TemporalData::DataDateTimeRef(_) => WireValue::String(format!("<{}>", td.type_name())),
404    }
405}
406
407/// Project a `WireValue` to typed slot bits, given the kind the caller
408/// wants. Returns [`MarshalError::KindMismatch`] when wire shape doesn't
409/// match the expected kind.
410///
411/// For heap kinds, this allocates a new `Arc<HeapValue>` and returns
412/// the raw pointer as bits — caller takes ownership of the heap
413/// reference (one strong count).
414pub fn wire_to_slot(wire: &WireValue, expected_kind: NativeKind) -> Result<u64, MarshalError> {
415    match (wire, expected_kind) {
416        (WireValue::Number(n), NativeKind::Float64) => Ok(f64::to_bits(*n)),
417        (WireValue::Integer(i), NativeKind::Int64) => Ok(*i as u64),
418        (WireValue::Bool(b), NativeKind::Bool) => Ok(*b as u64),
419        (WireValue::Null, NativeKind::NullableFloat64) => Ok(f64::to_bits(f64::NAN)),
420        (WireValue::String(s), NativeKind::String) => {
421            let arc = Arc::new(s.clone());
422            Ok(Arc::into_raw(arc) as u64)
423        }
424        (WireValue::I8(n), NativeKind::Int8) => Ok((*n as i64) as u64),
425        (WireValue::I16(n), NativeKind::Int16) => Ok((*n as i64) as u64),
426        (WireValue::I32(n), NativeKind::Int32) => Ok((*n as i64) as u64),
427        (WireValue::U8(n), NativeKind::UInt8) => Ok(*n as u64),
428        (WireValue::U16(n), NativeKind::UInt16) => Ok(*n as u64),
429        (WireValue::U32(n), NativeKind::UInt32) => Ok(*n as u64),
430        (WireValue::U64(n), NativeKind::UInt64) => Ok(*n),
431        // Heap kinds are constructed by allocating Arc<HeapValue> with the
432        // matching variant. Each surviving HeapKind variant is handled here
433        // as stdlib mass migration (Phase 2c) and the snapshot replay path
434        // discover concrete consumers.
435        (WireValue::String(s), NativeKind::Ptr(HeapKind::String)) => {
436            let arc = Arc::new(HeapValue::String(Arc::new(s.clone())));
437            Ok(Arc::into_raw(arc) as u64)
438        }
439        (WireValue::Table(table), NativeKind::Ptr(HeapKind::DataTable)) => {
440            let dt = datatable_from_ipc_bytes(&table.ipc_bytes, None, None)
441                .map_err(MarshalError::Body)?;
442            let arc = Arc::new(HeapValue::DataTable(Arc::new(dt)));
443            Ok(Arc::into_raw(arc) as u64)
444        }
445        // Calling site passed a wire/kind pair we don't currently handle.
446        // The strict-typed answer is to extend this match, not fall back —
447        // each new case represents a concrete stdlib/wire shape, and
448        // pattern-match exhaustiveness is the discipline.
449        _ => Err(MarshalError::Body(format!(
450            "wire_to_slot: no projection for wire variant into kind {:?}",
451            expected_kind
452        ))),
453    }
454}
455
456/// Wrap a typed slot in a `ValueEnvelope` with optional metadata.
457///
458/// `type_name` is the user-facing Shape type name (e.g. `"int"`,
459/// `"DataTable"`, `"MyType"`). The envelope's `type_info` is populated
460/// from the type registry when available.
461pub fn slot_to_envelope(
462    bits: u64,
463    kind: NativeKind,
464    type_name: &str,
465    ctx: &Context,
466) -> ValueEnvelope {
467    let value = slot_to_wire(bits, kind, ctx);
468    let _ = type_name;
469    let _ = ctx;
470    // Phase 1.B (ADR-006 §2.7.4): the type-info / type-registry lookup
471    // path that resolved a `TypeRegistry` from `TypeRegistry::default()`
472    // is gone; the rebuilt path queries `TypeRegistry::for_number` /
473    // primitives + the runtime's per-schema cache. Until the kind-
474    // threaded envelope lookup lands in Phase 2c, fall back to the
475    // wire-side inference helper.
476    ValueEnvelope::from_value(value)
477}
478
479/// If the slot carries a renderable Content shape (Content node, DataTable,
480/// or TableView), return `(content_json, content_html, content_terminal)`.
481/// Otherwise all three are `None`.
482pub fn slot_extract_content(
483    bits: u64,
484    kind: NativeKind,
485) -> (Option<serde_json::Value>, Option<String>, Option<String>) {
486    let NativeKind::Ptr(hk) = kind else {
487        return (None, None, None);
488    };
489    if bits == 0 {
490        return (None, None, None);
491    }
492    let hv = unsafe { &*(bits as *const HeapValue) };
493    let node = match (hk, hv) {
494        (HeapKind::Content, HeapValue::Content(node)) => Some((**node).clone()),
495        (HeapKind::DataTable, HeapValue::DataTable(dt)) => Some(
496            crate::content_dispatch::datatable_to_content_node(dt.as_ref(), None),
497        ),
498        (HeapKind::TableView, HeapValue::TableView(arc)) => match &**arc {
499            shape_value::heap_value::TableViewData::TypedTable { table, .. }
500            | shape_value::heap_value::TableViewData::IndexedTable { table, .. } => Some(
501                crate::content_dispatch::datatable_to_content_node(table.as_ref(), None),
502            ),
503            // RowView / ColumnRef are deferred Phase 2c content
504            // adapters — no current renderer.
505            _ => None,
506        },
507        _ => None,
508    };
509    let Some(_node) = node else {
510        return (None, None, None);
511    };
512
513    // Phase 1.B (ADR-006 §2.7.4): the JSON / HTML / terminal renderer
514    // adapters for `ContentNode` are part of the deferred Phase 2c
515    // content-marshal rebuild. Until then, return `None` for all three
516    // payloads rather than emit a partial / wrong-shape rendering.
517    (None, None, None)
518}
519
520// ───────────────────────── DataTable ↔ wire/IPC ─────────────────────────
521//
522// Typed-handle conversions. These don't go through `(bits, kind)` —
523// the caller passes a `&DataTable` directly, which is the typed-Rust
524// equivalent of NativeKind::Ptr(HeapKind::DataTable). The marshal layer
525// uses these internally when projecting a DataTable slot.
526
527pub fn datatable_to_wire(dt: &DataTable) -> WireValue {
528    datatable_to_wire_with_schema(dt, dt.schema_id())
529}
530
531fn datatable_to_wire_with_schema(dt: &DataTable, schema_id: Option<u32>) -> WireValue {
532    match datatable_to_ipc_bytes(dt) {
533        Ok(ipc_bytes) => WireValue::Table(WireTable {
534            ipc_bytes,
535            type_name: None,
536            schema_id,
537            row_count: dt.row_count(),
538            column_count: dt.column_count(),
539        }),
540        Err(e) => WireValue::String(format!("<datatable_serialize_error: {}>", e)),
541    }
542}
543
544pub fn datatable_to_ipc_bytes(dt: &DataTable) -> std::result::Result<Vec<u8>, String> {
545    // The DataTable now wraps a `RecordBatch` directly (`inner()`); the
546    // pre-bulldozer `to_arrow_batch` accessor is gone since the wrapper
547    // is the batch.
548    let arrow_batch = dt.inner();
549    let schema = arrow_batch.schema();
550    let mut buf = Vec::new();
551    {
552        let mut writer = FileWriter::try_new(&mut buf, &schema)
553            .map_err(|e| format!("Arrow IPC writer init failed: {}", e))?;
554        writer
555            .write(arrow_batch)
556            .map_err(|e| format!("Arrow IPC write failed: {}", e))?;
557        writer
558            .finish()
559            .map_err(|e| format!("Arrow IPC finish failed: {}", e))?;
560    }
561    Ok(buf)
562}
563
564pub fn datatable_from_ipc_bytes(
565    bytes: &[u8],
566    column_overrides: Option<&[shape_value::datatable::ColumnPtrs]>,
567    schema_id_override: Option<u32>,
568) -> std::result::Result<DataTable, String> {
569    let cursor = std::io::Cursor::new(bytes);
570    let reader = FileReader::try_new(cursor, None)
571        .map_err(|e| format!("Arrow IPC reader init failed: {}", e))?;
572    let mut batches = Vec::new();
573    for batch in reader {
574        batches.push(batch.map_err(|e| format!("Arrow IPC batch read failed: {}", e))?);
575    }
576    if batches.is_empty() {
577        return Err(
578            "datatable_from_ipc_bytes: empty IPC stream — no Arrow RecordBatch to wrap".to_string(),
579        );
580    }
581    // The first batch is the canonical wrapper; concatenation is a
582    // Phase 2c rebuild item alongside the broader DataTable IPC layer.
583    let first = batches.into_iter().next().unwrap();
584    let _ = column_overrides;
585    let dt = DataTable::new(first);
586    let dt = if let Some(sid) = schema_id_override {
587        dt.with_schema_id(sid)
588    } else {
589        dt
590    };
591    Ok(dt)
592}