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}