Skip to main content

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(&reg);
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(&reg);
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(&reg);
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(&reg);
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(&reg);
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(&reg);
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(&reg);
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(&reg);
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(&reg);
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}