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}