shape_vm/executor/printing.rs
1//! VM-native value formatting (ADR-006 §2.7.4 — output adapter).
2//!
3//! Formats runtime values held in a [`KindedSlot`] for `print()` /
4//! `format()` / REPL display. The pre-bulldozer implementation keyed off
5//! the deleted `ValueWord` carrier and `tag_bits::*` decode helpers; per
6//! ADR-006 §2.7.4 the formatter moves to a kinded carrier — `NativeKind`
7//! drives inline-scalar dispatch, and heap arms are read via
8//! `slot.as_heap_value()` + `HeapValue` match (Q8 ruling, preserves
9//! ADR-005 §1 single-discriminator).
10//!
11//! [`PrintResult`] and [`PrintSpan`] live in `shape_runtime::print_result`
12//! per §2.7.4; consumers of this formatter pair its output with those
13//! span-metadata carriers when feeding the output adapter.
14//!
15//! # Phase-1b-vm migration scope (E-printing close)
16//!
17//! Wave 6.5 / Wave-α `E-printing` ports the formatter SHAPE off the
18//! deleted `ValueWord` API: the public surface now takes `&KindedSlot`,
19//! dispatches on `NativeKind` for the inline-scalar arms (Int*, UInt*,
20//! IntSize, UIntSize, Bool, Float64), and reads heap-bearing kinds via
21//! the typed `Arc<T>` payload reconstruction shared with
22//! `KindedSlot::Drop` / `clone_with_kind`. Heap arms whose payload
23//! formatting depends on Phase-2c surfaces (TypedObject schema lookup,
24//! Content rendering, Temporal/DateTime formatting, TableView, Iterator
25//! / Generator state) surface as `todo!("phase-2c — see ADR-006
26//! §2.7.4")` placeholders rather than papering over with ValueWord-shape
27//! recovery, per the playbook's surface-and-stop discipline.
28
29use shape_runtime::type_schema::{EnumVariantKind, TypeSchema, TypeSchemaRegistry};
30use shape_value::heap_value::{
31 HeapKind, HeapValue, TypedObjectStorage,
32};
33use shape_value::{KindedSlot, NativeKind, ValueSlot};
34use std::sync::Arc;
35
36// Re-export the runtime-tier `PrintResult`/`PrintSpan` carriers for
37// formatter consumers — keeps the post-§2.7.4 import path coherent for
38// callers that still reach into `shape_vm::executor::printing` for the
39// output-adapter types.
40pub use shape_runtime::print_result::{PrintResult, PrintSpan};
41
42/// Formatter for runtime values represented as [`KindedSlot`].
43///
44/// Uses [`TypeSchemaRegistry`] to format `TypedObject` payloads with
45/// their schema-declared field names. Optionally accepts a reference
46/// resolver (Phase-2c) that dereferences ref-kind slots to their target
47/// values; absent the resolver, refs print as `<ref>`.
48///
49/// ADR-005 §1 single-discriminator preserved: heap arms are read via
50/// the slot's typed pointer + `HeapValue` match (Q8 ruling). No
51/// per-heap-variant accessors on `KindedSlot`; no parallel sum types.
52pub struct ValueFormatter<'a> {
53 /// Type schema registry for `TypedObject` field resolution.
54 schema_registry: &'a TypeSchemaRegistry,
55 /// Optional reference-resolver hook. Phase-2c wire-up: when present,
56 /// `Ref`-kind slots are dereferenced and the target formatted in
57 /// their place; when absent, refs print as `<ref>`.
58 deref_fn: Option<&'a dyn Fn(&KindedSlot) -> Option<KindedSlot>>,
59}
60
61impl<'a> ValueFormatter<'a> {
62 /// Create a formatter without a reference resolver — `Ref`-kind
63 /// slots will print as `<ref>`.
64 pub fn new(schema_registry: &'a TypeSchemaRegistry) -> Self {
65 Self {
66 schema_registry,
67 deref_fn: None,
68 }
69 }
70
71 /// Create a formatter with a reference resolver. The resolver is
72 /// invoked when a ref-kind slot is encountered; if it returns
73 /// `Some(target)` the target is formatted in place, otherwise the
74 /// ref prints as `<ref>`.
75 pub fn with_deref(
76 schema_registry: &'a TypeSchemaRegistry,
77 deref_fn: &'a dyn Fn(&KindedSlot) -> Option<KindedSlot>,
78 ) -> Self {
79 Self {
80 schema_registry,
81 deref_fn: Some(deref_fn),
82 }
83 }
84
85 /// Primary entry point: format a runtime value to a string.
86 ///
87 /// At the top level, raw strings render unquoted (so `print("hi")`
88 /// prints `hi`, not `"hi"`). Inside containers (TypedObject fields,
89 /// HashMap values, heap-array elements) strings are quoted via
90 /// [`Self::format_kinded_nested`].
91 pub fn format_kinded(&self, slot: &KindedSlot) -> String {
92 self.format_kinded_inner(slot, 0, false)
93 }
94
95 /// Format a runtime value as it appears nested inside another
96 /// container — quotes string values to disambiguate `{name: "alice"}`
97 /// vs `{name: alice}`.
98 pub fn format_kinded_nested(&self, slot: &KindedSlot) -> String {
99 self.format_kinded_inner(slot, 0, true)
100 }
101
102 /// Recursive helper with depth tracking. Caps recursion at depth 50
103 /// to bound output for cyclic / deeply nested values.
104 ///
105 /// `quote_strings` controls whether `String`-kind slots render as
106 /// `"hello"` (true, nested) or `hello` (false, top-level).
107 fn format_kinded_inner(&self, slot: &KindedSlot, depth: usize, quote_strings: bool) -> String {
108 if depth > 50 {
109 return "[max depth reached]".to_string();
110 }
111
112 let bits = slot.slot.raw();
113 match slot.kind {
114 // ── Inline scalars ──────────────────────────────────────────
115 // R5b-2-bool-null-sentinel-cluster (ADR-006 §2.7 +
116 // §2.7.7/Q9, 2026-05-19): canonical absence-of-value
117 // discriminator — prints as `null` (mirror of the
118 // pre-disposition `(0, NativeKind::Bool)` sentinel's
119 // intended display surface).
120 NativeKind::Null => "null".to_string(),
121 NativeKind::Bool => slot.slot.as_bool().to_string(),
122 NativeKind::Int8
123 | NativeKind::NullableInt8
124 | NativeKind::Int16
125 | NativeKind::NullableInt16
126 | NativeKind::Int32
127 | NativeKind::NullableInt32
128 | NativeKind::Int64
129 | NativeKind::NullableInt64
130 | NativeKind::IntSize
131 | NativeKind::NullableIntSize => slot.slot.as_i64().to_string(),
132 // r5c-2-β-CKPT-C u64-carrier-disambiguation (2026-05-20): a
133 // `UInt64` slot is now unambiguously a genuine scalar `u64` —
134 // the v2-typed-array pointer carrier moved to
135 // `NativeKind::Ptr(HeapKind::TypedArray)` (formatted in
136 // `format_heap_kind`'s `HeapKind::TypedArray` arm). The pre-fix
137 // `as_v2_typed_array(bits, UInt64)` probe dereferenced an
138 // arbitrary scalar `u64` value (e.g. `u64::MAX`) as a
139 // `*const HeapHeader` → SIGSEGV on `print(x)`. Render directly.
140 NativeKind::UInt64 | NativeKind::NullableUInt64 => {
141 slot.slot.as_u64().to_string()
142 }
143 NativeKind::UInt8
144 | NativeKind::NullableUInt8
145 | NativeKind::UInt16
146 | NativeKind::NullableUInt16
147 | NativeKind::UInt32
148 | NativeKind::NullableUInt32
149 | NativeKind::UIntSize
150 | NativeKind::NullableUIntSize => slot.slot.as_u64().to_string(),
151 NativeKind::Float64 | NativeKind::NullableFloat64 => {
152 format_number(slot.slot.as_f64())
153 }
154 // Round 19 S1.5 W12-nativekind-scalar-additions (2026-05-14):
155 // ADR-006 §2.7.5 amendment adds F32 + Char as 4-byte scalar
156 // variants. F32 prints via `format_number(f64::from(f32))`
157 // (lossless widening, same numeric formatting as F64); Char
158 // prints its codepoint as a single character (mirror of the
159 // pre-amendment `HeapKind::Char` printing arm).
160 NativeKind::Float32 => format_number(f64::from(f32::from_bits(bits as u32))),
161 NativeKind::Char => match char::from_u32(bits as u32) {
162 Some(c) if quote_strings => format!("'{}'", c),
163 Some(c) => c.to_string(),
164 None => format!("<invalid-char:0x{:x}>", bits),
165 },
166 // ── String (top-level NativeKind::String) ───────────────────
167 NativeKind::String => {
168 if bits == 0 {
169 return "None".to_string();
170 }
171 // SAFETY: per the construction-side contract on every
172 // `KindedSlot::from_string_arc`-shaped producer, `String`
173 // kind means the slot stores `Arc::into_raw::<String>`
174 // bits and the slot owns one strong-count share. Read
175 // the inner `&str` for the lifetime of `&self`.
176 let s: &String = unsafe { &*(bits as *const String) };
177 if quote_strings {
178 format!("\"{}\"", s)
179 } else {
180 s.clone()
181 }
182 }
183 // ── Wave 2 Agent B v2-raw carriers ───────────────────────────
184 // W12-StringV2-DecimalV2-NativeKind-additions (2026-05-14): the
185 // v2-raw `*const StringObj` / `*const DecimalObj` carriers print
186 // with the same surface as their Arc-wrapped siblings. The
187 // carrier shape (HeapHeader-equipped 24-byte `repr(C)` struct
188 // per `v2/string_obj.rs` / `v2/decimal_obj.rs`) is invisible to
189 // the print output — only the inner payload (UTF-8 bytes /
190 // Decimal value) is rendered.
191 NativeKind::StringV2 => {
192 if bits == 0 {
193 return "None".to_string();
194 }
195 // SAFETY: per the §2.7.5 amendment construction contract,
196 // kind=StringV2 means bits = `ptr as u64` pointing to a live
197 // `StringObj` with bumped refcount.
198 let ptr = bits as *const shape_value::v2::string_obj::StringObj;
199 let s: &str = unsafe { shape_value::v2::string_obj::StringObj::as_str(ptr) };
200 if quote_strings {
201 format!("\"{}\"", s)
202 } else {
203 s.to_string()
204 }
205 }
206 NativeKind::DecimalV2 => {
207 if bits == 0 {
208 return "None".to_string();
209 }
210 // SAFETY: per the §2.7.5 amendment construction contract,
211 // kind=DecimalV2 means bits = `ptr as u64` pointing to a live
212 // `DecimalObj` with bumped refcount.
213 let ptr = bits as *const shape_value::v2::decimal_obj::DecimalObj;
214 let value = unsafe { shape_value::v2::decimal_obj::DecimalObj::value(ptr) };
215 value.to_string()
216 }
217 // ── Heap-pointer kinds: dispatch via HeapKind ───────────────
218 NativeKind::Ptr(hk) => self.format_heap_kind(bits, hk, depth, quote_strings),
219 }
220 }
221
222 /// Heap-arm formatter: dispatches on `HeapKind` directly to read the
223 /// matching typed `Arc<T>` payload (Q8 — no per-heap-variant accessor
224 /// on the carrier; the kind discriminant is local).
225 ///
226 /// `quote_strings` propagates from the entry point — when this heap
227 /// arm is itself a child of a TypedObject/HashMap/array and the parent
228 /// asked for nested formatting, the leaf string render quotes.
229 fn format_heap_kind(
230 &self,
231 bits: u64,
232 hk: HeapKind,
233 depth: usize,
234 quote_strings: bool,
235 ) -> String {
236 if bits == 0 {
237 return "None".to_string();
238 }
239 match hk {
240 HeapKind::String => {
241 // SAFETY: typed-Arc payload per §2.4.
242 let s: &String = unsafe { &*(bits as *const String) };
243 if quote_strings {
244 format!("\"{}\"", s)
245 } else {
246 s.clone()
247 }
248 }
249 HeapKind::Decimal => {
250 let d: &rust_decimal::Decimal =
251 unsafe { &*(bits as *const rust_decimal::Decimal) };
252 format!("{}D", d)
253 }
254 HeapKind::BigInt => {
255 let i: &i64 = unsafe { &*(bits as *const i64) };
256 i.to_string()
257 }
258 HeapKind::Char => {
259 // `Char`-kind stores codepoint bits inline (no Arc<T>).
260 match char::from_u32(bits as u32) {
261 Some(c) => c.to_string(),
262 None => "[Invalid char]".to_string(),
263 }
264 }
265 HeapKind::TypedArray => {
266 // r5c-2-β-CKPT-C u64-carrier-disambiguation (2026-05-20):
267 // `Ptr(HeapKind::TypedArray)` is the canonical carrier for
268 // every v2-raw `*mut TypedArray<T>` pointer (the
269 // `NewTypedArray*` direct carrier + the refcounted
270 // struct-field / closure-capture carrier — both hold the
271 // identical pointer shape). Detect the array via the
272 // on-header kind + `_pad` element-type byte and render via
273 // the per-element walker. The legacy `Arc<TypedArrayData>`
274 // boxed carrier this ordinal once labelled is deleted; the
275 // pointee is unambiguously a v2-raw `TypedArray<T>`.
276 let _ = depth;
277 match crate::executor::v2_handlers::v2_array_detect::as_v2_typed_array(
278 bits,
279 NativeKind::Ptr(HeapKind::TypedArray),
280 ) {
281 Some(view) => self.format_v2_typed_array(&view),
282 // Kind says TypedArray but the bits failed v2 detection
283 // (missing `HEAP_KIND_V2_TYPED_ARRAY` header). Render an
284 // opaque tag rather than dereferencing further.
285 None => "[TypedArray:?]".to_string(),
286 }
287 }
288 HeapKind::TypedObject => {
289 // ADR-006 §2.7.4 / §2.7.6 / Q8 — walk the per-field slots,
290 // dispatch each on `field_kinds[i]` (the per-schema
291 // `Arc<[NativeKind]>` co-located with the storage), and
292 // resolve field names via the schema registry. SAFETY:
293 // construction-side contract on `KindedSlot::from_typed_object`
294 // — `TypedObject`-kind bits are `Arc::into_raw(Arc<TypedObjectStorage>)`.
295 let storage: &TypedObjectStorage =
296 unsafe { &*(bits as *const TypedObjectStorage) };
297 self.format_typed_object(storage, depth)
298 }
299 HeapKind::HashMap => {
300 // Wave 2 Round 3b C2-joint ckpt-2 (2026-05-14): bits are
301 // `Arc::into_raw(Arc<HashMapKindedRef>)` per ADR-006
302 // §2.7.24 Q25.B SUPERSEDED. Cast through the kinded ref
303 // wrapper; per-V Display dispatch lives at
304 // `format_hashmap(kref, depth)`.
305 let kref: &shape_value::heap_value::HashMapKindedRef =
306 unsafe { &*(bits as *const shape_value::heap_value::HashMapKindedRef) };
307 self.format_hashmap(kref, depth)
308 }
309 HeapKind::HashSet => {
310 // Wave 13 W13-hashset-rebuild (ADR-006 §2.7.15 / Q16,
311 // 2026-05-10): HashSetData stores a single
312 // `Vec<Arc<String>>` keys buffer (mirror of HashMapData
313 // with the values buffer dropped). Render as
314 // `{"a", "b", ...}`. SAFETY: construction-side contract
315 // on `KindedSlot::from_hashset`.
316 let _ = depth;
317 let set: &shape_value::heap_value::HashSetData =
318 unsafe { &*(bits as *const shape_value::heap_value::HashSetData) };
319 self.format_hashset(set)
320 }
321 HeapKind::DataTable => {
322 let dt: &shape_value::DataTable =
323 unsafe { &*(bits as *const shape_value::DataTable) };
324 format!("{}", dt)
325 }
326 HeapKind::Content => {
327 // W18.2 (R8 — output-adapter integration): route the
328 // Content payload through the kind-threaded
329 // `slot_extract_content` host-boundary helper and render
330 // via the adapter selected for `print()`. The audit
331 // dispatch pins TERMINAL as the default for `print()` —
332 // the per-adapter capabilities table at
333 // `content_dispatch::capabilities_for_adapter` keys off
334 // the same adapter constants (TERMINAL / HTML /
335 // MARKDOWN / JSON / PLAIN) and the surviving
336 // 6-renderer infrastructure projects the node per its
337 // own `ContentRenderer::render` impl. Pre-rebuild this
338 // arm called `ContentNode::Display` directly, which
339 // emitted plain text with no ANSI styling; the
340 // TerminalRenderer now resolves the per-span style
341 // (bold / italic / fg / bg) into escape codes that the
342 // `print()`-stdout sink renders directly. The
343 // `quote_strings` flag is unused for Content nodes —
344 // they're a self-contained renderable tree, not a leaf
345 // string that needs quoting in a parent container.
346 let _ = quote_strings;
347 let node: &shape_value::content::ContentNode =
348 unsafe { &*(bits as *const shape_value::content::ContentNode) };
349 let adapter = shape_runtime::content_dispatch::adapters::TERMINAL;
350 let _capabilities =
351 shape_runtime::content_dispatch::capabilities_for_adapter(adapter);
352 // The capabilities are passed implicitly via the
353 // renderer constructor — `TerminalRenderer::new()`
354 // builds a `RenderContext::terminal()` with ANSI on by
355 // default. The `_capabilities` lookup is kept as the
356 // documented dispatch surface per the audit
357 // (`capabilities_for_adapter` is the public selector);
358 // hooking it into the renderer constructors landed via
359 // the per-renderer `new`/`Default` shape — additional
360 // renderer constructors that take an explicit
361 // `RendererCapabilities` would replace the implicit
362 // construction here.
363 use shape_runtime::content_renderer::ContentRenderer;
364 let renderer = shape_runtime::renderers::terminal::TerminalRenderer::new();
365 renderer.render(node)
366 }
367 HeapKind::Instant => {
368 let t: &std::time::Instant =
369 unsafe { &*(bits as *const std::time::Instant) };
370 format!("<instant:{:?}>", t.elapsed())
371 }
372 HeapKind::IoHandle => {
373 let data: &shape_value::heap_value::IoHandleData =
374 unsafe { &*(bits as *const shape_value::heap_value::IoHandleData) };
375 let status = if data.is_open() { "open" } else { "closed" };
376 format!("<io_handle:{}:{}>", data.path, status)
377 }
378 HeapKind::NativeView => {
379 let v: &shape_value::heap_value::NativeViewData =
380 unsafe { &*(bits as *const shape_value::heap_value::NativeViewData) };
381 format!(
382 "<{}:{}@0x{:x}>",
383 if v.mutable { "cmut" } else { "cview" },
384 v.layout.name,
385 v.ptr
386 )
387 }
388 HeapKind::Temporal => {
389 // C1-temporal-lowering (Phase 2d Wave 2): Temporal carrier
390 // dispatch per ADR-006 §2.7.4. Slot bits are
391 // `Arc::into_raw::<TemporalData>` (set by
392 // `compiler/expressions/temporal.rs::compile_expr_duration`
393 // + `op_push_const`'s Duration arm in `stack_ops/mod.rs`
394 // and by the `TIMESPAN_METHODS` / `DATETIME_METHODS`
395 // PHF result construction in
396 // `objects/datetime_methods.rs::temporal_result`).
397 // `TemporalData`'s own `Display` impl already handles
398 // every arm (DateTime / Duration / TimeSpan / Timeframe /
399 // TimeReference / DateTimeExpr / DataDateTimeRef); we
400 // dispatch through it preserving ADR-005 §1's
401 // single-discriminator discipline (no per-arm peek at
402 // the `TemporalData` payload here).
403 let td: &shape_value::heap_value::TemporalData =
404 unsafe { &*(bits as *const shape_value::heap_value::TemporalData) };
405 format!("{}", td)
406 }
407 HeapKind::TableView => {
408 let tv: &shape_value::heap_value::TableViewData =
409 unsafe { &*(bits as *const shape_value::heap_value::TableViewData) };
410 format!("{}", tv)
411 }
412 HeapKind::TaskGroup => {
413 let tg: &shape_value::heap_value::TaskGroupData =
414 unsafe { &*(bits as *const shape_value::heap_value::TaskGroupData) };
415 let kind_str = match tg.kind {
416 0 => "All",
417 1 => "Race",
418 2 => "Any",
419 3 => "Settle",
420 _ => "Unknown",
421 };
422 format!("[TaskGroup:{}({})]", kind_str, tg.task_ids.len())
423 }
424 HeapKind::Closure => {
425 // ClosureRaw payload uses `OwnedClosureBlock` rather than
426 // `Arc<HeapValue>`; the formatter walked the legacy
427 // `HeapValue::ClosureRaw` arm via `as_closure_handle()`.
428 // The kinded read needs the §2.7.8 cell-storage rebuild
429 // (`B7-closure-cells`) before the closure's `function_id`
430 // can be reached without going through ValueWord.
431 todo!(
432 "phase-2c — see ADR-006 §2.7.4 / §2.7.8: closure \
433 formatting needs kinded ClosureRaw read (§2.7.8 / Q10 \
434 B7-closure-cells extension)"
435 );
436 }
437 HeapKind::Future => {
438 // Future-id is an inline scalar payload on its `HeapKind`;
439 // a `KindedSlot` flagged `Ptr(HeapKind::Future)` carries
440 // the future id directly in `bits`.
441 format!("[Future:{}]", bits)
442 }
443 HeapKind::NativeScalar => {
444 // NativeScalar is `Copy`/inline (≤ 16 bytes); the kinded
445 // surface for the `repr(C)` packed payload lands with
446 // Wave 5c native-interop body migration.
447 let _ = bits;
448 todo!(
449 "phase-2c — see ADR-006 §2.7.4: NativeScalar formatting \
450 needs the kinded native-interop carrier (Wave 5c \
451 dispatch_native_interop_builtin)"
452 );
453 }
454 HeapKind::FilterExpr => {
455 // Wave-γ G-heap-filter-expr (ADR-006 §2.3 / §2.7.6 / Q8
456 // amendment): FilterExpr trees are a transient query-DSL
457 // value; they don't have a user-facing print form. Render
458 // as an opaque tag for diagnostics.
459 let _ = bits;
460 "<filter_expr>".to_string()
461 }
462 HeapKind::Reference => {
463 // ADR-006 §2.7.13 / Q14 (Wave 8 W8-T26, 2026-05-10):
464 // Reference values are within-program data emitted by the
465 // `MakeRef` family and consumed locally by `DerefLoad` /
466 // `DerefStore` / `SetIndexRef`. They don't have a
467 // user-facing print form; render as an opaque tag.
468 let _ = bits;
469 "<ref>".to_string()
470 }
471 HeapKind::SharedCell => {
472 // Wave 8 W8-T25 (ADR-006 §2.7.12 / Q13 amendment,
473 // 2026-05-10): `SharedCell` cell-pointer slots are an
474 // interior-only cell-pointer shape; user-facing prints
475 // go through `op_load_shared_local` /
476 // `op_load_shared_capture` which strip the SharedCell
477 // outer label and dispatch on the cell's interior kind.
478 // Reaching this arm with a SharedCell-labeled slot at
479 // a print surface is a kind-source bug. Render as an
480 // opaque tag for diagnostics.
481 let _ = bits;
482 "<shared_cell>".to_string()
483 }
484 HeapKind::Iterator => {
485 // W13-iterator-state (ADR-006 §2.7.16 / Q17,
486 // 2026-05-10): iterator pipelines have no user-facing
487 // print form — terminals materialise their elements;
488 // an Iterator slot reaching the Display surface is
489 // "still lazy" by construction. Render as an opaque
490 // tag.
491 let _ = bits;
492 "<iterator>".to_string()
493 }
494 HeapKind::Deque => {
495 // Wave 15 W15-deque (ADR-006 §2.7.19 / Q20,
496 // 2026-05-10): DequeData stores a single
497 // `VecDeque<Arc<HeapValue>>` items buffer. Render as
498 // `Deque[elem1, elem2, ...]` front-to-back. SAFETY:
499 // construction-side contract on `KindedSlot::from_deque`
500 // — slot bits are `Arc::into_raw(Arc<DequeData>)`.
501 let deque: &shape_value::heap_value::DequeData =
502 unsafe { &*(bits as *const shape_value::heap_value::DequeData) };
503 self.format_deque(deque, depth)
504 }
505 HeapKind::Channel => {
506 // Wave 15 W15-channel-rebuild (ADR-006 §2.7.20 / Q21,
507 // 2026-05-10): channels are concurrency primitives
508 // with no user-facing literal; render as an opaque
509 // tag annotated with current queue length and closed
510 // flag for diagnostics. SAFETY: construction-side
511 // contract on `KindedSlot::from_channel` —
512 // `Channel`-kind bits are
513 // `Arc::into_raw(Arc<ChannelData>)`.
514 let ch: &shape_value::heap_value::ChannelData =
515 unsafe { &*(bits as *const shape_value::heap_value::ChannelData) };
516 let state = if ch.is_closed() { "closed" } else { "open" };
517 format!("<channel:{}:{}>", state, ch.len())
518 }
519 HeapKind::PriorityQueue => {
520 // Wave 15 W15-priority-queue (ADR-006 §2.7.18 / Q19,
521 // 2026-05-10): PriorityQueueData stores i64
522 // priorities heap-ordered in `Arc<TypedBuffer<i64>>`
523 // (mirror of HashSetData with the keys buffer carrying
524 // i64 instead of `Arc<String>`). Render as
525 // `PriorityQueue[1, 3, 2, ...]` in heap-array order
526 // (NOT sorted; for sorted output the user must call
527 // `pq.toSortedArray()`). SAFETY: construction-side
528 // contract on `KindedSlot::from_priority_queue`.
529 let pq: &shape_value::heap_value::PriorityQueueData =
530 unsafe { &*(bits as *const shape_value::heap_value::PriorityQueueData) };
531 self.format_priority_queue(pq)
532 }
533 HeapKind::Range => {
534 // W15-range (ADR-006 §2.7.23 / Q24, 2026-05-10):
535 // user-visible literal form `start..end` (exclusive)
536 // or `start..=end` (inclusive). Matches the surface
537 // syntax round-trip and the `HeapValue::Range` Display
538 // impl in `heap_value.rs`. SAFETY: construction-side
539 // contract on `KindedSlot::from_range` — Range-kind
540 // bits are `Arc::into_raw(Arc<RangeData>)`.
541 let r: &shape_value::heap_value::RangeData =
542 unsafe { &*(bits as *const shape_value::heap_value::RangeData) };
543 if r.inclusive {
544 format!("{}..={}", r.start, r.end)
545 } else {
546 format!("{}..{}", r.start, r.end)
547 }
548 }
549 HeapKind::Result => {
550 // Wave 14 W14-variant-codegen (ADR-006 §2.7.17 / Q18,
551 // 2026-05-10): render as `Ok(<inner>)` /
552 // `Err(<inner>)`. The inner-value formatter recurses
553 // through the kinded value-formatter on the payload's
554 // `KindedSlot`. SAFETY: construction-side contract on
555 // `KindedSlot::from_result` — Result-kind bits are
556 // `Arc::into_raw(Arc<ResultData>)`.
557 let r: &shape_value::heap_value::ResultData =
558 unsafe { &*(bits as *const shape_value::heap_value::ResultData) };
559 let inner = self.format_kinded_inner(&r.payload, depth + 1, true);
560 if r.is_ok {
561 format!("Ok({})", inner)
562 } else {
563 format!("Err({})", inner)
564 }
565 }
566 HeapKind::Option => {
567 // Wave 14 W14-variant-codegen (ADR-006 §2.7.17 / Q18,
568 // 2026-05-10): render as `Some(<inner>)` / `None`.
569 // SAFETY: construction-side contract on
570 // `KindedSlot::from_option`.
571 let o: &shape_value::heap_value::OptionData =
572 unsafe { &*(bits as *const shape_value::heap_value::OptionData) };
573 if o.is_some {
574 let inner = self.format_kinded_inner(&o.payload, depth + 1, true);
575 format!("Some({})", inner)
576 } else {
577 "None".to_string()
578 }
579 }
580 // W17-concurrency (ADR-006 §2.7.25, 2026-05-11):
581 // concurrency primitives have no user-facing literal —
582 // render as opaque tags annotated with diagnostic state.
583 // Mirror of Channel's `<channel:state:len>` shape.
584 // SAFETY: construction-side contract on
585 // `KindedSlot::from_mutex / from_atomic / from_lazy` —
586 // bits are `Arc::into_raw(Arc<MutexData / AtomicData /
587 // LazyData>)`.
588 HeapKind::Mutex => {
589 let _ = bits;
590 "<mutex>".to_string()
591 }
592 HeapKind::Atomic => {
593 let a: &shape_value::heap_value::AtomicData =
594 unsafe { &*(bits as *const shape_value::heap_value::AtomicData) };
595 format!("<atomic:{}>", a.load())
596 }
597 HeapKind::Lazy => {
598 let l: &shape_value::heap_value::LazyData =
599 unsafe { &*(bits as *const shape_value::heap_value::LazyData) };
600 if l.is_initialized() {
601 "<lazy:initialized>".to_string()
602 } else {
603 "<lazy:pending>".to_string()
604 }
605 }
606 // W17-trait-object-storage (ADR-006 §2.7.24 / Q25.C,
607 // 2026-05-11): a `dyn Trait` carrier renders as
608 // `<dyn TraitName #schema>` for diagnostics. Pretty-print
609 // via the boxed receiver's user-defined `Display`-style
610 // method is the compiler-emission tier's concern (call
611 // the trait's display method through the vtable); the
612 // storage-tier formatter is diagnostic-only. SAFETY:
613 // construction-side contract on
614 // `KindedSlot::from_trait_object` — TraitObject-kind
615 // bits are `Arc::into_raw(Arc<TraitObjectStorage>)`.
616 HeapKind::TraitObject => {
617 let t: &shape_value::heap_value::TraitObjectStorage = unsafe {
618 &*(bits as *const shape_value::heap_value::TraitObjectStorage)
619 };
620 let trait_name = t
621 .vtable
622 .trait_names
623 .first()
624 .map(|s| s.as_str())
625 .unwrap_or("?");
626 // Wave 2 Round 4 D4 ckpt-3 (2026-05-14): t.value is now
627 // `*const TypedObjectStorage` (raw); deref to read
628 // schema_id. SAFETY: t holds one v2-raw refcount share so
629 // t.value points to a live storage.
630 let schema_id = unsafe { (*t.value).schema_id };
631 format!("<dyn {} #{}>", trait_name, schema_id)
632 }
633 // W17-comptime-vm-dispatch (ADR-006 §2.7.26, 2026-05-12):
634 // ModuleFn references render as `<module_fn:id>`. Same
635 // inline-scalar pattern as Future — bits are the
636 // module_fn_id directly, no heap dispatch.
637 HeapKind::ModuleFn => {
638 format!("<module_fn:{}>", bits)
639 }
640 // ADR-006 §2.7.22 amendment (Round 18 S3, 2026-05-13):
641 // Matrix renders as `<Mat<number>:rows x cols>`; MatrixSlice
642 // renders as a flat `Vec<number>[...]` over the projection
643 // slice — preserves the pre-amendment user-facing print
644 // shape. SAFETY: construction-side contract on
645 // `KindedSlot::from_matrix` / `from_matrix_slice` — bits are
646 // `Arc::into_raw(Arc<MatrixData>) as u64` /
647 // `Arc::into_raw(Arc<MatrixSliceData>) as u64`.
648 HeapKind::Matrix => {
649 let m: &shape_value::heap_value::MatrixData =
650 unsafe { &*(bits as *const shape_value::heap_value::MatrixData) };
651 format!("<Mat<number>:{}x{}>", m.rows, m.cols)
652 }
653 HeapKind::MatrixSlice => {
654 let s: &shape_value::heap_value::MatrixSliceData =
655 unsafe { &*(bits as *const shape_value::heap_value::MatrixSliceData) };
656 let slice = s.as_slice();
657 let elems: Vec<String> =
658 slice.iter().map(|v| format_array_float(*v)).collect();
659 format!("[{}]", elems.join(", "))
660 }
661 }
662 }
663
664 /// Format a v2 typed array (raw `*mut TypedArray<T>` pointer) as
665 /// `[1, 2, 3]`. Element type comes from the heap-header `_pad` byte
666 /// stamped at allocation time; element bits / kind come from the
667 /// canonical kinded read helper
668 /// (`v2_array_detect::read_element`).
669 fn format_v2_typed_array(
670 &self,
671 view: &crate::executor::v2_handlers::v2_array_detect::V2TypedArrayView,
672 ) -> String {
673 use crate::executor::v2_handlers::v2_array_detect::read_element;
674 let mut out = String::with_capacity(2 + view.len as usize * 4);
675 out.push('[');
676 for i in 0..view.len {
677 if i > 0 {
678 out.push_str(", ");
679 }
680 // Per-element rendering — the element kind is one of
681 // Float64 / Int64 / Int32 / Bool per the v2 typed-array
682 // contract. Format each through the canonical scalar arms.
683 if let Some((bits, kind)) = read_element(view, i) {
684 let elem_slot =
685 KindedSlot::new(ValueSlot::from_raw(bits), kind);
686 out.push_str(&self.format_kinded_inner(&elem_slot, 0, true));
687 std::mem::forget(elem_slot);
688 } else {
689 out.push_str("?");
690 }
691 }
692 out.push(']');
693 out
694 }
695
696 // V3-S5 ckpt-5 (2026-05-15): `format_typed_array` DELETED. The
697 // function dispatched on `TypedArrayData::*` variants (deleted at
698 // ckpt-1) per W12-typed-array-data-deletion audit §3.5 + §3.6. The
699 // two callers (HeapKind::TypedArray arm in `format_kinded_inner` +
700 // HeapValue::TypedArray arm in `format_heap_value`) are both updated
701 // to a structured placeholder. Rebuild lands at ckpt-6 STRICT close
702 // per the per-T v2-raw `TypedArray<T>` direct-access target.
703
704 /// Apply a reference-resolver if configured, formatting the
705 /// dereferenced target. Returns `<ref>` when no resolver is wired up.
706 ///
707 /// Reserved for the Phase-2c ref-kind landing — until refs gain
708 /// their own NativeKind variant (or the kinded-ref ABI lands), this
709 /// helper is unused by the dispatch path above and stays here so the
710 /// resolver hook on `with_deref` keeps a coherent signature.
711 #[allow(dead_code)]
712 fn format_ref(&self, slot: &KindedSlot, depth: usize) -> String {
713 if let Some(deref) = &self.deref_fn {
714 if let Some(resolved) = deref(slot) {
715 return self.format_kinded_inner(&resolved, depth + 1, true);
716 }
717 }
718 "<ref>".to_string()
719 }
720
721 // ──────────────────────────────────────────────────────────────────────
722 // TypedObject + HashMap helpers (ADR-006 §2.7.4 / §2.7.6 / Q8)
723 // ──────────────────────────────────────────────────────────────────────
724
725 /// Format a `TypedObjectStorage` as `{field1: val1, field2: val2}`.
726 ///
727 /// Field names come from the schema registry when the storage's
728 /// `schema_id` resolves; otherwise positional `_0`, `_1` placeholders
729 /// are used so the formatter degrades gracefully when the registry is
730 /// not populated for a runtime-built object (e.g. anonymous record
731 /// literals before schema registration).
732 ///
733 /// Each slot is reified as a `KindedSlot { slot, kind: field_kinds[i] }`
734 /// and recursed through `format_kinded_inner` with `quote_strings = true`
735 /// so nested string fields render `"…"`. Slots are *borrowed*: we do
736 /// NOT clone the slot bits or transfer ownership; the parent
737 /// `TypedObjectStorage` keeps holding all heap shares for the
738 /// lifetime of `&self`.
739 fn format_typed_object(&self, storage: &TypedObjectStorage, depth: usize) -> String {
740 let schema = self.schema_registry.get_by_id(storage.schema_id as u32);
741 // W18.0 (User 2026-05-23 Item 1): enum-typed TypedObjects render
742 // as `Variant(payload)` / `Variant { field: v }` / `Variant`
743 // rather than the synthetic `{__variant: N, __payload_0: ...}`
744 // shape. Fallback to the generic record walk only when the
745 // schema lookup genuinely fails (rare — runtime-built objects
746 // without registered schema).
747 if let Some(s) = schema {
748 if s.is_enum() {
749 return self.format_enum_typed_object(s, storage, depth);
750 }
751 }
752 let n = storage.slots.len().min(storage.field_kinds.len());
753 let mut out = String::with_capacity(2 + n * 8);
754 out.push('{');
755 for i in 0..n {
756 if i > 0 {
757 out.push_str(", ");
758 }
759 // Field name: prefer the schema-resolved name; fall back to
760 // a positional placeholder so the formatter still produces
761 // human-readable output for schema-less objects.
762 let name: &str = schema
763 .and_then(|s| s.fields.get(i).map(|f| f.name.as_str()))
764 .unwrap_or("_");
765 if name == "_" {
766 out.push_str(&format!("_{}", i));
767 } else {
768 out.push_str(name);
769 }
770 out.push_str(": ");
771 // Reify the slot as a borrowed `KindedSlot` for the recursive
772 // formatter call. This carrier never owns a strong-count share
773 // — it is dropped via `mem::forget` at the end of the loop
774 // iteration so the parent storage retains every payload.
775 let slot = ValueSlot::from_raw(storage.slots[i].raw());
776 let kinded = KindedSlot::new(slot, storage.field_kinds[i]);
777 let rendered = self.format_kinded_inner(&kinded, depth + 1, true);
778 out.push_str(&rendered);
779 std::mem::forget(kinded);
780 }
781 out.push('}');
782 out
783 }
784
785 /// Format an enum-typed `TypedObjectStorage` as `Variant(payload)` /
786 /// `Variant { field: v }` / `Variant` per W18.0 (User 2026-05-23
787 /// Item 1).
788 ///
789 /// Reads slot 0 (`__variant` discriminator, I64) to recover the
790 /// variant ID, then dispatches on the variant's [`EnumVariantKind`]
791 /// to choose the render shape:
792 ///
793 /// - `Unit` → `Red`
794 /// - `Tuple` → `Blue(42)` / `Pair(1, 2)`
795 /// - `Struct` → `Point { x: 1, y: 2 }` (field names from
796 /// `EnumVariantKind::Struct(names)`)
797 ///
798 /// Falls through to the generic `{__variant: N, __payload_0: V}`
799 /// shape ONLY if the variant ID fails to resolve (e.g. enum_info
800 /// missing or discriminator out of range — should be rare since
801 /// compiler emits matched discriminators per audit §2.D).
802 fn format_enum_typed_object(
803 &self,
804 schema: &TypeSchema,
805 storage: &TypedObjectStorage,
806 depth: usize,
807 ) -> String {
808 let n = storage.slots.len().min(storage.field_kinds.len());
809 // Slot 0 is `__variant` (I64) per `new_enum`'s layout.
810 if n == 0 {
811 return schema.name.clone();
812 }
813 let variant_id = storage.slots[0].raw() as i64;
814 let info = schema
815 .get_enum_info()
816 .and_then(|ei| ei.variant_by_id(variant_id as u16));
817 let Some(info) = info else {
818 // Schema missing enum_info OR variant_id out of range.
819 // Fall back to the generic record walk so the formatter
820 // degrades visibly without lying about variant names.
821 return self.format_typed_object_generic(schema, storage, depth);
822 };
823
824 // Render the i-th payload slot through the canonical kinded
825 // formatter (same borrow pattern as `format_typed_object`).
826 let render_payload = |i: usize, out: &mut String| {
827 let slot_idx = i + 1; // skip __variant
828 if slot_idx >= n {
829 out.push_str("?");
830 return;
831 }
832 let slot = ValueSlot::from_raw(storage.slots[slot_idx].raw());
833 let kinded = KindedSlot::new(slot, storage.field_kinds[slot_idx]);
834 let rendered = self.format_kinded_inner(&kinded, depth + 1, true);
835 out.push_str(&rendered);
836 std::mem::forget(kinded);
837 };
838
839 match &info.kind {
840 EnumVariantKind::Unit => info.name.clone(),
841 EnumVariantKind::Tuple => {
842 let count = info.payload_fields as usize;
843 let mut out = String::with_capacity(info.name.len() + 2 + count * 4);
844 out.push_str(&info.name);
845 out.push('(');
846 for i in 0..count {
847 if i > 0 {
848 out.push_str(", ");
849 }
850 render_payload(i, &mut out);
851 }
852 out.push(')');
853 out
854 }
855 EnumVariantKind::Struct(field_names) => {
856 let count = field_names.len();
857 let mut out = String::with_capacity(info.name.len() + 4 + count * 8);
858 out.push_str(&info.name);
859 if count == 0 {
860 // Defensive: a zero-field struct variant renders
861 // identically to a unit variant.
862 return out;
863 }
864 out.push_str(" { ");
865 for (i, fname) in field_names.iter().enumerate() {
866 if i > 0 {
867 out.push_str(", ");
868 }
869 out.push_str(fname);
870 out.push_str(": ");
871 render_payload(i, &mut out);
872 }
873 out.push_str(" }");
874 out
875 }
876 }
877 }
878
879 /// Generic `{field: value, ...}` walk extracted from
880 /// `format_typed_object` so the enum-fallback path can reuse it
881 /// without re-running the enum check.
882 fn format_typed_object_generic(
883 &self,
884 schema: &TypeSchema,
885 storage: &TypedObjectStorage,
886 depth: usize,
887 ) -> String {
888 let n = storage.slots.len().min(storage.field_kinds.len());
889 let mut out = String::with_capacity(2 + n * 8);
890 out.push('{');
891 for i in 0..n {
892 if i > 0 {
893 out.push_str(", ");
894 }
895 let name: &str = schema
896 .fields
897 .get(i)
898 .map(|f| f.name.as_str())
899 .unwrap_or("_");
900 if name == "_" {
901 out.push_str(&format!("_{}", i));
902 } else {
903 out.push_str(name);
904 }
905 out.push_str(": ");
906 let slot = ValueSlot::from_raw(storage.slots[i].raw());
907 let kinded = KindedSlot::new(slot, storage.field_kinds[i]);
908 let rendered = self.format_kinded_inner(&kinded, depth + 1, true);
909 out.push_str(&rendered);
910 std::mem::forget(kinded);
911 }
912 out.push('}');
913 out
914 }
915
916 /// Format a `PriorityQueueData` as `PriorityQueue[v1, v2, ...]` in
917 /// heap-array order. Wave 15 W15-priority-queue (ADR-006 §2.7.18 /
918 /// Q19) — i64-priority min-heap render shape (mirror of HashSet's
919 /// render shape with the values column carrying i64 instead of
920 /// quoted strings).
921 fn format_priority_queue(
922 &self,
923 pq: &shape_value::heap_value::PriorityQueueData,
924 ) -> String {
925 let n = pq.heap.len();
926 let mut out = String::with_capacity(16 + n * 4);
927 out.push_str("PriorityQueue[");
928 for (i, v) in pq.heap.iter().enumerate() {
929 if i > 0 {
930 out.push_str(", ");
931 }
932 out.push_str(&format!("{}", v));
933 }
934 out.push(']');
935 out
936 }
937
938 /// Format a `HashSetData` as `{"a", "b", ...}`. Wave 13
939 /// W13-hashset-rebuild (ADR-006 §2.7.15) — one-keyspace mirror of
940 /// HashMap's render shape with the values column dropped.
941 fn format_hashset(&self, set: &shape_value::heap_value::HashSetData) -> String {
942 let n = set.keys.len();
943 let mut out = String::with_capacity(2 + n * 6);
944 out.push('{');
945 for (i, k) in set.keys.iter().enumerate() {
946 if i > 0 {
947 out.push_str(", ");
948 }
949 out.push_str(&format!("\"{}\"", k));
950 }
951 out.push('}');
952 out
953 }
954
955 /// Format a `DequeData` as `Deque[elem1, elem2, ...]` front-to-back.
956 /// Wave 15 W15-deque (ADR-006 §2.7.19) — heterogeneous-element mirror
957 /// of HashSet's render shape, dispatching per element through the
958 /// canonical ADR-005 §1 single-discriminator `HeapValue` Display.
959 fn format_deque(&self, deque: &shape_value::heap_value::DequeData, depth: usize) -> String {
960 let n = deque.items.len();
961 let mut out = String::with_capacity(8 + n * 4);
962 out.push_str("Deque[");
963 for (i, v) in deque.items.iter().enumerate() {
964 if i > 0 {
965 out.push_str(", ");
966 }
967 out.push_str(&self.format_heap_value(v, depth + 1));
968 }
969 out.push(']');
970 out
971 }
972
973 /// Format a `HashMapKindedRef` as `{"key1": val1, "key2": val2}`.
974 ///
975 /// **Wave 2 Round 3b C2-joint ckpt-3 (2026-05-14):** full per-V
976 /// keys/values walk. The keys buffer is `*mut TypedArray<*const StringObj>`;
977 /// the values buffer is per-V (`*mut TypedArray<V>`). Each entry's
978 /// value is rendered via the per-V Display shape (matching the
979 /// HeapValue::HashMap Display impl at `heap_value.rs:hashmap_kref_display`).
980 /// For TypedObject / TraitObject value variants we recurse through the
981 /// canonical `format_typed_object` / opaque tag path. ADR-006 §2.7.24
982 /// Q25.B SUPERSEDED + audit §C.4.
983 fn format_hashmap(
984 &self,
985 map: &shape_value::heap_value::HashMapKindedRef,
986 depth: usize,
987 ) -> String {
988 use shape_value::heap_value::HashMapKindedRef;
989 let mut out = String::with_capacity(2 + map.len() * 8);
990 out.push('{');
991
992 // Walk keys buffer; per-V dispatch the value rendering.
993 unsafe {
994 // The keys buffer + values buffer come from each variant's inner Arc.
995 // SAFETY: HashMapData<V>'s contract — keys is a live
996 // *mut TypedArray<*const StringObj>; *(arc.values) is a live
997 // TypedArray<V>.
998 let render_key = |out: &mut String, i: usize, k: &str| {
999 if i > 0 {
1000 out.push_str(", ");
1001 }
1002 out.push('"');
1003 out.push_str(k);
1004 out.push_str("\": ");
1005 };
1006
1007 // Read all keys generically.
1008 let read_keys = |keys_ptr: *const shape_value::v2::typed_array::TypedArray<
1009 *const shape_value::v2::string_obj::StringObj,
1010 >|
1011 -> Vec<&'static str> {
1012 let n = shape_value::v2::typed_array::TypedArray::len(keys_ptr) as usize;
1013 let mut ks = Vec::with_capacity(n);
1014 for i in 0..n {
1015 let ptr =
1016 shape_value::v2::typed_array::TypedArray::get_unchecked(keys_ptr, i as u32);
1017 ks.push(shape_value::v2::string_obj::StringObj::as_str(ptr));
1018 }
1019 ks
1020 };
1021
1022 match map {
1023 HashMapKindedRef::I64(arc) => {
1024 let keys = read_keys(arc.keys);
1025 for (i, k) in keys.iter().enumerate() {
1026 render_key(&mut out, i, k);
1027 let v = *(*arc.values).data.add(i);
1028 out.push_str(&v.to_string());
1029 }
1030 }
1031 HashMapKindedRef::F64(arc) => {
1032 let keys = read_keys(arc.keys);
1033 for (i, k) in keys.iter().enumerate() {
1034 render_key(&mut out, i, k);
1035 let v: f64 = *(*arc.values).data.add(i);
1036 out.push_str(&v.to_string());
1037 }
1038 }
1039 HashMapKindedRef::Bool(arc) => {
1040 let keys = read_keys(arc.keys);
1041 for (i, k) in keys.iter().enumerate() {
1042 render_key(&mut out, i, k);
1043 let v: u8 = *(*arc.values).data.add(i);
1044 out.push_str(if v != 0 { "true" } else { "false" });
1045 }
1046 }
1047 HashMapKindedRef::Char(arc) => {
1048 let keys = read_keys(arc.keys);
1049 for (i, k) in keys.iter().enumerate() {
1050 render_key(&mut out, i, k);
1051 let v: char = *(*arc.values).data.add(i);
1052 out.push('\'');
1053 out.push(v);
1054 out.push('\'');
1055 }
1056 }
1057 HashMapKindedRef::String(arc) => {
1058 let keys = read_keys(arc.keys);
1059 for (i, k) in keys.iter().enumerate() {
1060 render_key(&mut out, i, k);
1061 let v_ptr: *const shape_value::v2::string_obj::StringObj =
1062 *(*arc.values).data.add(i);
1063 let s = shape_value::v2::string_obj::StringObj::as_str(v_ptr);
1064 out.push('"');
1065 out.push_str(s);
1066 out.push('"');
1067 }
1068 }
1069 HashMapKindedRef::Decimal(arc) => {
1070 let keys = read_keys(arc.keys);
1071 for (i, k) in keys.iter().enumerate() {
1072 render_key(&mut out, i, k);
1073 let v_ptr: *const shape_value::v2::decimal_obj::DecimalObj =
1074 *(*arc.values).data.add(i);
1075 let d = (*v_ptr).value;
1076 out.push_str(&format!("{}D", d));
1077 }
1078 }
1079 HashMapKindedRef::TypedObject(arc) => {
1080 let keys = read_keys(arc.keys);
1081 for (i, k) in keys.iter().enumerate() {
1082 render_key(&mut out, i, k);
1083 let v_ref: &shape_value::heap_value::TypedObjectPtr =
1084 &*(*arc.values).data.add(i);
1085 if v_ref.is_null() {
1086 out.push_str("null");
1087 } else {
1088 let storage = &**v_ref;
1089 out.push_str(&self.format_typed_object(storage, depth + 1));
1090 }
1091 }
1092 }
1093 HashMapKindedRef::TraitObject(arc) => {
1094 let keys = read_keys(arc.keys);
1095 for (i, k) in keys.iter().enumerate() {
1096 render_key(&mut out, i, k);
1097 let v_ref: &shape_value::heap_value::TraitObjectPtr =
1098 &*(*arc.values).data.add(i);
1099 out.push_str(&format!("<trait_object:{:p}>", v_ref.as_ptr()));
1100 }
1101 }
1102 HashMapKindedRef::HashMap(arc) => {
1103 // Recursive carrier (Wave N hashmap-value-v-arm
1104 // follow-up, cluster-2 closure-wave-C, 2026-05-16).
1105 // Each inner element is itself a HashMapKindedRef;
1106 // recurse through format_hashmap.
1107 let keys = read_keys(arc.keys);
1108 for (i, k) in keys.iter().enumerate() {
1109 render_key(&mut out, i, k);
1110 let inner_ref: &shape_value::heap_value::HashMapKindedRef =
1111 &*(*arc.values).data.add(i);
1112 out.push_str(&self.format_hashmap(inner_ref, depth + 1));
1113 }
1114 }
1115 }
1116 }
1117 out.push('}');
1118 out
1119 }
1120
1121 /// Format a `HeapValue` reference (the value side of `HashMapData`'s
1122 /// `TypedBuffer<Arc<HeapValue>>` and the heterogeneous element arm of
1123 /// `the-deleted-heterogeneous-element-carrier`). Dispatches via the ADR-005 §1
1124 /// single-discriminator `HeapValue` match.
1125 fn format_heap_value(&self, hv: &HeapValue, depth: usize) -> String {
1126 if depth > 50 {
1127 return "[max depth reached]".to_string();
1128 }
1129 match hv {
1130 HeapValue::String(s) => format!("\"{}\"", s),
1131 HeapValue::Decimal(d) => format!("{}D", d),
1132 HeapValue::BigInt(b) => b.as_ref().to_string(),
1133 HeapValue::Char(c) => format!("'{}'", c),
1134 HeapValue::Future(id) => format!("[Future:{}]", id),
1135 // V3-S5 ckpt-5: HeapValue::TypedArray outer arm DELETED at
1136 // ckpt-4 in lockstep with `TypedArrayData` enum + `TypedBuffer<T>`
1137 // wrapper layer per W12 audit §3.6. The arm is gone from the
1138 // exhaustive `match hv` (match remains exhaustive on remaining
1139 // HeapValue variants).
1140 // HeapValue::TypedArray(arr) => self.format_typed_array(arr.as_ref(), depth),
1141 // Wave 2 Round 4 D4 ckpt-final-prime² (2026-05-14): TypedObjectPtr
1142 // derefs to &TypedObjectStorage; use `&**o` to bridge through the
1143 // outer `&` and the wrapper's Deref impl.
1144 HeapValue::TypedObject(o) => self.format_typed_object(&**o, depth),
1145 // Wave 2 Round 3b C2-joint ckpt-2 (2026-05-14): payload flipped
1146 // to `HashMapKindedRef`; pass the borrowed kinded ref directly.
1147 HeapValue::HashMap(m) => self.format_hashmap(m, depth),
1148 HeapValue::HashSet(s) => self.format_hashset(s.as_ref()),
1149 HeapValue::Deque(d) => self.format_deque(d.as_ref(), depth),
1150 HeapValue::DataTable(t) => format!("{}", t),
1151 HeapValue::Content(n) => {
1152 // W18.2 (R8 — output-adapter integration): mirror the
1153 // `format_heap_kind`'s `HeapKind::Content` arm. A
1154 // Content node reached via a HashMap-value /
1155 // heterogeneous-element walk renders through the
1156 // TerminalRenderer per the TERMINAL-as-default
1157 // print() dispatch.
1158 use shape_runtime::content_renderer::ContentRenderer;
1159 let renderer =
1160 shape_runtime::renderers::terminal::TerminalRenderer::new();
1161 renderer.render(n)
1162 }
1163 HeapValue::Instant(t) => format!("<instant:{:?}>", t.elapsed()),
1164 HeapValue::IoHandle(h) => {
1165 let status = if h.is_open() { "open" } else { "closed" };
1166 format!("<io_handle:{}:{}>", h.path, status)
1167 }
1168 HeapValue::NativeView(v) => format!(
1169 "<{}:{}@0x{:x}>",
1170 if v.mutable { "cmut" } else { "cview" },
1171 v.layout.name,
1172 v.ptr
1173 ),
1174 HeapValue::TableView(tv) => format!("{}", tv),
1175 HeapValue::TaskGroup(tg) => {
1176 let kind_str = match tg.kind {
1177 0 => "All",
1178 1 => "Race",
1179 2 => "Any",
1180 3 => "Settle",
1181 _ => "Unknown",
1182 };
1183 format!("[TaskGroup:{}({})]", kind_str, tg.task_ids.len())
1184 }
1185 HeapValue::NativeScalar(_) => "<native_scalar>".to_string(),
1186 HeapValue::Temporal(_) => "<temporal>".to_string(),
1187 HeapValue::ClosureRaw(_) => "<closure>".to_string(),
1188 HeapValue::FilterExpr(_) => "<filter_expr>".to_string(),
1189 HeapValue::Reference(_) => "<ref>".to_string(),
1190 HeapValue::Iterator(_) => "<iterator>".to_string(),
1191 HeapValue::Channel(c) => {
1192 let state = if c.is_closed() { "closed" } else { "open" };
1193 format!("<channel:{}:{}>", state, c.len())
1194 }
1195 HeapValue::PriorityQueue(p) => self.format_priority_queue(p.as_ref()),
1196 // W15-range (ADR-006 §2.7.23 / Q24, 2026-05-10): user-visible
1197 // literal form `start..end` / `start..=end` matching the
1198 // surface syntax round-trip.
1199 HeapValue::Range(r) => {
1200 if r.inclusive {
1201 format!("{}..={}", r.start, r.end)
1202 } else {
1203 format!("{}..{}", r.start, r.end)
1204 }
1205 }
1206 // Wave 14 W14-variant-codegen (ADR-006 §2.7.17 / Q18,
1207 // 2026-05-10): Result/Option carriers — render as
1208 // Ok/Err/Some/None tags. Inner is opaque at this fallback
1209 // path (the kinded formatter at the format_heap_kind site
1210 // handles full pretty-print).
1211 HeapValue::Result(r) => {
1212 if r.is_ok {
1213 "Ok(<...>)".to_string()
1214 } else {
1215 "Err(<...>)".to_string()
1216 }
1217 }
1218 HeapValue::Option(o) => {
1219 if o.is_some {
1220 "Some(<...>)".to_string()
1221 } else {
1222 "None".to_string()
1223 }
1224 }
1225 // W17-concurrency (ADR-006 §2.7.25, 2026-05-11):
1226 // concurrency-primitive carriers — render as opaque tags.
1227 HeapValue::Mutex(_) => "<mutex>".to_string(),
1228 HeapValue::Atomic(a) => format!("<atomic:{}>", a.load()),
1229 HeapValue::Lazy(l) => {
1230 if l.is_initialized() {
1231 "<lazy:initialized>".to_string()
1232 } else {
1233 "<lazy:pending>".to_string()
1234 }
1235 }
1236 // W17-trait-object-storage (ADR-006 §2.7.24 / Q25.C,
1237 // 2026-05-11): `dyn Trait` carrier — render as
1238 // `<dyn TraitName #schema>` for diagnostics. Pretty-print
1239 // via the boxed receiver's user-defined `Display`-style
1240 // method is the compiler-emission tier's concern.
1241 HeapValue::TraitObject(t) => {
1242 let trait_name = t
1243 .vtable
1244 .trait_names
1245 .first()
1246 .map(|s| s.as_str())
1247 .unwrap_or("?");
1248 // Wave 2 Round 4 D4 ckpt-3 (2026-05-14): t.value is now
1249 // `*const TypedObjectStorage` (raw); deref to read
1250 // schema_id. SAFETY: t holds one v2-raw refcount share so
1251 // t.value points to a live storage.
1252 let schema_id = unsafe { (*t.value).schema_id };
1253 format!("<dyn {} #{}>", trait_name, schema_id)
1254 }
1255 // W17-comptime-vm-dispatch (ADR-006 §2.7.26, 2026-05-12).
1256 HeapValue::ModuleFn(id) => format!("<module_fn:{}>", id),
1257 // ADR-006 §2.7.22 amendment (Round 18 S3, 2026-05-13):
1258 // Matrix renders as `<Mat<number>:rows x cols>`. MatrixSlice
1259 // renders as a flat `[v1, v2, ...]` over the projection slice
1260 // — preserves the pre-amendment FloatSlice Display shape.
1261 HeapValue::Matrix(m) => format!("<Mat<number>:{}x{}>", m.rows, m.cols),
1262 HeapValue::MatrixSlice(s) => {
1263 let slice = s.as_slice();
1264 let elems: Vec<String> =
1265 slice.iter().map(|v| format_array_float(*v)).collect();
1266 format!("[{}]", elems.join(", "))
1267 }
1268 }
1269 }
1270}
1271
1272/// Format a number, removing unnecessary decimal places.
1273fn format_number(n: f64) -> String {
1274 if n.is_nan() {
1275 "NaN".to_string()
1276 } else if n.is_infinite() {
1277 if n.is_sign_positive() {
1278 "Infinity".to_string()
1279 } else {
1280 "-Infinity".to_string()
1281 }
1282 } else if n.fract() == 0.0 && n.abs() < 1e15 {
1283 // Integer-like floats: always show .0 to distinguish from int.
1284 format!("{}.0", n as i64)
1285 } else {
1286 n.to_string()
1287 }
1288}
1289
1290/// Format a single float for inclusion in a typed-array element list.
1291/// Mirrors `format_number` but returns the integer-shape (`{n}.0`) for
1292/// whole-number floats that fit comfortably in `i64`.
1293fn format_array_float(v: f64) -> String {
1294 if v == v.trunc() && v.abs() < 1e15 {
1295 format!("{}.0", v as i64)
1296 } else {
1297 format!("{}", v)
1298 }
1299}
1300
1301#[cfg(test)]
1302mod tests {
1303 use super::*;
1304 use std::sync::Arc;
1305
1306 fn create_test_registry() -> TypeSchemaRegistry {
1307 TypeSchemaRegistry::new()
1308 }
1309
1310 #[test]
1311 fn test_format_inline_scalars() {
1312 let reg = create_test_registry();
1313 let formatter = ValueFormatter::new(®);
1314
1315 assert_eq!(
1316 formatter.format_kinded(&KindedSlot::from_int(42)),
1317 "42"
1318 );
1319 assert_eq!(
1320 formatter.format_kinded(&KindedSlot::from_int(-100)),
1321 "-100"
1322 );
1323 assert_eq!(
1324 formatter.format_kinded(&KindedSlot::from_bool(true)),
1325 "true"
1326 );
1327 assert_eq!(
1328 formatter.format_kinded(&KindedSlot::from_bool(false)),
1329 "false"
1330 );
1331 assert_eq!(
1332 formatter.format_kinded(&KindedSlot::from_number(3.14)),
1333 "3.14"
1334 );
1335 }
1336
1337 #[test]
1338 fn test_format_integer_like_float_shows_decimal_point() {
1339 let reg = create_test_registry();
1340 let formatter = ValueFormatter::new(®);
1341 assert_eq!(
1342 formatter.format_kinded(&KindedSlot::from_number(1.0)),
1343 "1.0"
1344 );
1345 assert_eq!(
1346 formatter.format_kinded(&KindedSlot::from_number(-5.0)),
1347 "-5.0"
1348 );
1349 assert_eq!(
1350 formatter.format_kinded(&KindedSlot::from_number(100.0)),
1351 "100.0"
1352 );
1353 }
1354
1355 #[test]
1356 fn test_format_special_floats() {
1357 assert_eq!(format_number(f64::NAN), "NaN");
1358 assert_eq!(format_number(f64::INFINITY), "Infinity");
1359 assert_eq!(format_number(f64::NEG_INFINITY), "-Infinity");
1360 }
1361
1362 #[test]
1363 fn test_format_string() {
1364 let reg = create_test_registry();
1365 let formatter = ValueFormatter::new(®);
1366
1367 let s = KindedSlot::from_string_arc(Arc::new("hello".to_string()));
1368 assert_eq!(formatter.format_kinded(&s), "hello");
1369 }
1370
1371 #[test]
1372 fn test_format_decimal() {
1373 let reg = create_test_registry();
1374 let formatter = ValueFormatter::new(®);
1375
1376 let d = KindedSlot::from_decimal(Arc::new(rust_decimal::Decimal::from(42)));
1377 assert_eq!(formatter.format_kinded(&d), "42D");
1378
1379 let d2 = KindedSlot::from_decimal(Arc::new(rust_decimal::Decimal::new(314, 2)));
1380 assert_eq!(formatter.format_kinded(&d2), "3.14D");
1381 }
1382
1383 #[test]
1384 fn test_format_bigint() {
1385 let reg = create_test_registry();
1386 let formatter = ValueFormatter::new(®);
1387
1388 let b = KindedSlot::from_bigint(Arc::new(123_i64));
1389 assert_eq!(formatter.format_kinded(&b), "123");
1390 }
1391
1392 #[test]
1393 fn test_format_char() {
1394 let reg = create_test_registry();
1395 let formatter = ValueFormatter::new(®);
1396
1397 let c = KindedSlot::from_char('A');
1398 assert_eq!(formatter.format_kinded(&c), "A");
1399
1400 let c2 = KindedSlot::from_char('λ');
1401 assert_eq!(formatter.format_kinded(&c2), "λ");
1402 }
1403
1404 /// r5c-2-β-CKPT-C u64-carrier-disambiguation regression guard.
1405 ///
1406 /// A `NativeKind::UInt64` slot is a genuine scalar `u64` — the
1407 /// formatter must render it as the unsigned integer value and must NOT
1408 /// dereference the bits as a `*const HeapHeader` (the pre-fix
1409 /// `as_v2_typed_array(bits, UInt64)` probe SIGSEGV'd on
1410 /// `let x: u64 = 18446744073709551615; print(x)`). Covers small,
1411 /// mid-range and `u64::MAX` values — `u64::MAX` is the load-bearing
1412 /// case: as a pointer it is non-canonical and would fault on deref;
1413 /// as a signed integer it would render `-1`.
1414 #[test]
1415 fn test_format_u64_scalar_renders_unsigned_no_deref() {
1416 let reg = create_test_registry();
1417 let formatter = ValueFormatter::new(®);
1418
1419 for &v in &[0u64, 42u64, 1000u64, u64::MAX, u64::MAX - 1] {
1420 let slot = KindedSlot::new(
1421 ValueSlot::from_raw(v),
1422 NativeKind::UInt64,
1423 );
1424 assert_eq!(formatter.format_kinded(&slot), v.to_string());
1425 std::mem::forget(slot);
1426 }
1427 }
1428
1429 /// A v2 typed array now flows through the kinded API under the
1430 /// `NativeKind::Ptr(HeapKind::TypedArray)` carrier kind (r5c-2-β-CKPT-C
1431 /// u64-carrier-disambiguation). The formatter's `HeapKind::TypedArray`
1432 /// arm detects the v2-raw `*mut TypedArray<T>` via the on-header kind +
1433 /// element-type byte and renders the elements.
1434 #[test]
1435 fn test_format_typed_array_via_ptr_carrier() {
1436 use shape_value::v2::typed_array::{TypedArray, ELEM_TYPE_I64};
1437 use crate::executor::v2_handlers::v2_array_detect::stamp_elem_type;
1438
1439 let reg = create_test_registry();
1440 let formatter = ValueFormatter::new(®);
1441
1442 let arr = TypedArray::<i64>::from_slice(&[7, 8, 9]);
1443 unsafe { stamp_elem_type(arr as *mut u8, ELEM_TYPE_I64) };
1444 let slot = KindedSlot::new(
1445 ValueSlot::from_raw(arr as usize as u64),
1446 NativeKind::Ptr(HeapKind::TypedArray),
1447 );
1448 assert_eq!(formatter.format_kinded(&slot), "[7, 8, 9]");
1449 std::mem::forget(slot);
1450 unsafe { TypedArray::<i64>::drop_array(arr) };
1451 }
1452
1453 /// W18.2 (R8 — output-adapter integration): a Content-kind slot
1454 /// renders through the TerminalRenderer per the TERMINAL-as-default
1455 /// adapter selection for `print()`. Pre-W18.2 the arm called
1456 /// `ContentNode::Display` directly, which emits plain text with no
1457 /// ANSI styling; the rebuilt arm projects through the surviving
1458 /// 6-renderer infrastructure so styled spans surface escape codes
1459 /// at the print() sink.
1460 ///
1461 /// W18.3 retired c-string syntax entirely (supervisor D2 2026-05-24);
1462 /// rich content is now produced via the builder-pattern `Content.*`
1463 /// namespace constructors. This test constructs the `ContentNode`
1464 /// directly to exercise the renderer surface.
1465 #[test]
1466 fn test_format_content_via_terminal_renderer() {
1467 use shape_value::content::{Color, ContentNode, NamedColor};
1468
1469 let reg = create_test_registry();
1470 let formatter = ValueFormatter::new(®);
1471
1472 // Plain text Content node — TerminalRenderer leaves un-styled
1473 // spans alone, so the surface should contain the literal text.
1474 let node = ContentNode::plain("hello");
1475 let arc = std::sync::Arc::new(node);
1476 let bits = std::sync::Arc::into_raw(arc) as u64;
1477 let slot = KindedSlot::new(
1478 ValueSlot::from_raw(bits),
1479 NativeKind::Ptr(HeapKind::Content),
1480 );
1481 let out = formatter.format_kinded(&slot);
1482 assert!(
1483 out.contains("hello"),
1484 "TerminalRenderer should surface plain Content text; got {:?}",
1485 out
1486 );
1487 std::mem::forget(slot);
1488 unsafe {
1489 let _ = std::sync::Arc::from_raw(
1490 bits as *const shape_value::content::ContentNode,
1491 );
1492 }
1493
1494 // Styled Content node — TerminalRenderer should emit ANSI
1495 // escape sequences (`\x1b[...]`) for the bold-red span. This is
1496 // the load-bearing W18.2 evidence: pre-rebuild the arm called
1497 // `ContentNode::Display` which produces NO `\x1b[...]` bytes.
1498 let styled = ContentNode::plain("hi")
1499 .with_bold()
1500 .with_fg(Color::Named(NamedColor::Red));
1501 let arc2 = std::sync::Arc::new(styled);
1502 let bits2 = std::sync::Arc::into_raw(arc2) as u64;
1503 let slot2 = KindedSlot::new(
1504 ValueSlot::from_raw(bits2),
1505 NativeKind::Ptr(HeapKind::Content),
1506 );
1507 let styled_out = formatter.format_kinded(&slot2);
1508 assert!(
1509 styled_out.contains("\x1b["),
1510 "TerminalRenderer should emit ANSI escape codes for styled \
1511 Content nodes; got {:?}",
1512 styled_out
1513 );
1514 assert!(styled_out.contains("hi"));
1515 std::mem::forget(slot2);
1516 unsafe {
1517 let _ = std::sync::Arc::from_raw(
1518 bits2 as *const shape_value::content::ContentNode,
1519 );
1520 }
1521 }
1522}