Skip to main content

shape_runtime/
json_value.rs

1//! Typed sum-type for parsed-data trees.
2//!
3//! Replaces the `ValueWord`-tree return that pre-bulldozer parsers
4//! (`json` / `yaml` / `toml` / `msgpack` / `xml`) used. The strict-typed
5//! answer is a single concrete enum with the union of variants needed
6//! across all five formats; consumers pattern-match exhaustively.
7//!
8//! Insertion order of `Object` fields is preserved by storing key-value
9//! pairs in a `Vec` rather than a `HashMap`. This matches the on-the-wire
10//! ordering of JSON / TOML / YAML / MsgPack and lets round-trip
11//! serialization stay byte-identical.
12//!
13//! See `docs/defections.md` (2026-05-06 — typed JsonValue) for the
14//! rationale, and (2026-05-07 — N7 unified workstream — ε disposition)
15//! for the universal-intermediate role.
16//!
17//! ADR-005: `JsonValue` is the parser-intermediate / wire-form translation
18//! layer, NOT a runtime storage type for user objects. Runtime objects live
19//! in `HeapValue::TypedObject` with a flat schema-driven slot array. The
20//! typed-parse path (`__parse_typed`) projects `JsonValue` to `TypedObject`
21//! before reaching user code; only the untyped `json.parse` path surfaces
22//! `JsonValue` to user code (as the `Json` enum in
23//! `stdlib-src/core/json_value.shape`). See
24//! `docs/adr/005-typed-slot-construction.md`.
25
26use shape_value::heap_value::HeapValue;
27
28#[derive(Debug, Clone, PartialEq)]
29pub enum JsonValue {
30    Null,
31    Bool(bool),
32    Int(i64),
33    Number(f64),
34    String(String),
35    Bytes(Vec<u8>),
36    Array(Vec<JsonValue>),
37    Object(Vec<(String, JsonValue)>),
38}
39
40impl JsonValue {
41    /// Return the type-name of this value as a static string. Useful for
42    /// error messages without allocating.
43    pub fn type_name(&self) -> &'static str {
44        match self {
45            JsonValue::Null => "null",
46            JsonValue::Bool(_) => "bool",
47            JsonValue::Int(_) => "int",
48            JsonValue::Number(_) => "number",
49            JsonValue::String(_) => "string",
50            JsonValue::Bytes(_) => "bytes",
51            JsonValue::Array(_) => "array",
52            JsonValue::Object(_) => "object",
53        }
54    }
55}
56
57/// Walk a `HeapValue` tree and produce a `JsonValue`.
58///
59/// Universal intermediate per the N7 ε disposition (`docs/defections.md`,
60/// 2026-05-07). Format-specific encoders take `&JsonValue` (NOT
61/// `&HeapValue`) and produce per-format bytes/string. Mirrors json.rs's
62/// parse-side `serde_json_to_json_value` (`stdlib/json.rs:172-196`) in
63/// reverse.
64///
65/// Recursion lives at the JsonValue layer (Array/Object children); the
66/// `ConcreteReturn` leaf-only invariant is preserved.
67///
68/// # Variant classification (REFINEMENT-1A + REFINEMENT-1B-ITEM-A)
69///
70/// **Mechanical-yes (5)**: String, BigInt, Char, TypedArray, HashMap
71/// + TypedObject schema-aware (1) — produce a JsonValue directly or
72/// recurse.
73///
74/// **Categorically-non-data Reject (5)**: Future, IoHandle, NativeView,
75/// ClosureRaw, TaskGroup — `Err("cannot serialize: <variant>")`
76/// permanently. These hold runtime resources; no serialization policy
77/// can convert them to wire format.
78///
79/// **Architectural-choice deferred (7)**: Decimal, DataTable, Content,
80/// Temporal, TableView, Instant, NativeScalar — first-landing
81/// `Err(<policy not yet decided>)`. Each represents a user-visible
82/// behavioral commitment requiring explicit decision per consumer
83/// demand.
84///
85/// V3-S5 ckpt-5-prime (2026-05-15): the **TypedArrayData inner-dispatch**
86/// description below previously named the 13-arm `typed_array_to_json_value`
87/// helper. That helper + the `HeapValue::TypedArray(ta)` outer arm here are
88/// RETIRED in lockstep with the deleted `HeapValue::TypedArray` variant
89/// (ckpt-4) + deleted `TypedArrayData` inner enum (ckpt-1). The v2-raw
90/// `*mut TypedArray<T>` JSON-serialisation path lands at the ckpt-5-prime²
91/// + ckpt-6 producer/consumer storage-shape migration (per W12 audit §3.6
92/// — no `*mut TypedArray<T>` value ever reaches `heap_to_json_value`
93/// post-V3-S5 ckpt-5: the JSON projection happens at the marshal layer
94/// before the value becomes a `HeapValue`). Refusal #1 binding.
95pub fn heap_to_json_value(hv: &HeapValue) -> Result<JsonValue, String> {
96    match hv {
97        // Mechanical-yes top-level (4 after V3-S5 ckpt-5-prime TypedArray retirement)
98        HeapValue::String(s) => Ok(JsonValue::String((**s).clone())),
99        HeapValue::BigInt(n) => Ok(JsonValue::Int(**n)),
100        HeapValue::Char(c) => Ok(JsonValue::String(c.to_string())),
101        HeapValue::HashMap(kref) => {
102            // Wave 2 Round 3b C2-joint ckpt-4 (2026-05-14): per-V walk
103            // reading keys (`*mut TypedArray<*const StringObj>` → `&str`)
104            // and values (`*mut TypedArray<V>` → `JsonValue` per V).
105            // ADR-006 §2.7.24 Q25.B SUPERSEDED + audit §C.4.
106            use shape_value::heap_value::HashMapKindedRef;
107            let n = kref.len();
108            let mut out: Vec<(String, JsonValue)> = Vec::with_capacity(n);
109            // Read keys helper: walk `*mut TypedArray<*const StringObj>` for any V.
110            let keys_ptr = match kref {
111                HashMapKindedRef::I64(arc) => arc.keys,
112                HashMapKindedRef::F64(arc) => arc.keys,
113                HashMapKindedRef::Bool(arc) => arc.keys,
114                HashMapKindedRef::Char(arc) => arc.keys,
115                HashMapKindedRef::String(arc) => arc.keys,
116                HashMapKindedRef::Decimal(arc) => arc.keys,
117                HashMapKindedRef::TypedObject(arc) => arc.keys,
118                HashMapKindedRef::TraitObject(arc) => arc.keys,
119                HashMapKindedRef::HashMap(arc) => arc.keys,
120            };
121            for i in 0..n {
122                let key: String = unsafe {
123                    let ptr = shape_value::v2::typed_array::TypedArray::get_unchecked(
124                        keys_ptr, i as u32,
125                    );
126                    shape_value::v2::string_obj::StringObj::as_str(ptr).to_owned()
127                };
128                let value: JsonValue = match kref {
129                    HashMapKindedRef::I64(arc) => {
130                        let v: i64 = unsafe { *(*arc.values).data.add(i) };
131                        JsonValue::Int(v)
132                    }
133                    HashMapKindedRef::F64(arc) => {
134                        let v: f64 = unsafe { *(*arc.values).data.add(i) };
135                        JsonValue::Number(v)
136                    }
137                    HashMapKindedRef::Bool(arc) => {
138                        let v: u8 = unsafe { *(*arc.values).data.add(i) };
139                        JsonValue::Bool(v != 0)
140                    }
141                    HashMapKindedRef::Char(arc) => {
142                        let v: char = unsafe { *(*arc.values).data.add(i) };
143                        JsonValue::String(v.to_string())
144                    }
145                    HashMapKindedRef::String(arc) => {
146                        let ptr: *const shape_value::v2::string_obj::StringObj =
147                            unsafe { *(*arc.values).data.add(i) };
148                        JsonValue::String(unsafe {
149                            shape_value::v2::string_obj::StringObj::as_str(ptr).to_owned()
150                        })
151                    }
152                    HashMapKindedRef::Decimal(_) => {
153                        return Err("HeapValue::HashMap<string, decimal> → JsonValue: \
154                            decimal serialization policy not yet decided (precision \
155                            preservation vs lossy f64 cast). Surface-and-stop per \
156                            playbook §6."
157                            .to_string());
158                    }
159                    HashMapKindedRef::TypedObject(_) => {
160                        return Err("HeapValue::HashMap<string, TypedObject> → JsonValue: \
161                            nested TypedObject serialization requires the schema \
162                            walker which is its own cluster. Surface-and-stop."
163                            .to_string());
164                    }
165                    HashMapKindedRef::TraitObject(_) => {
166                        return Err("HeapValue::HashMap<string, TraitObject> → JsonValue: \
167                            no canonical JSON shape for TraitObject. Surface-and-stop."
168                            .to_string());
169                    }
170                    HashMapKindedRef::HashMap(arc) => {
171                        // Recursive carrier (Wave N hashmap-value-v-arm
172                        // follow-up, cluster-2 closure-wave-C, 2026-05-16).
173                        // Read the inner HashMapKindedRef, wrap as a fresh
174                        // HeapValue::HashMap, recurse. The recursive call
175                        // takes ownership semantics by reference; we
176                        // share-clone the inner Arc so the recursive
177                        // call doesn't accidentally drop our share.
178                        let inner_ref: &HashMapKindedRef =
179                            unsafe { &*(*arc.values).data.add(i) };
180                        let inner_hv = HeapValue::HashMap(inner_ref.clone());
181                        heap_to_json_value(&inner_hv)?
182                    }
183                };
184                out.push((key, value));
185            }
186            Ok(JsonValue::Object(out))
187        }
188
189        // Wave 13 W13-hashset-rebuild (ADR-006 §2.7.15 / Q16,
190        // 2026-05-10): Set serializes as a JSON array of strings (the
191        // §2.7.15 amendment's documented wire shape — string-only
192        // keyspace at landing). One mechanical-yes mapping; no
193        // architectural-choice deferral.
194        HeapValue::HashSet(d) => Ok(JsonValue::Array(
195            d.keys
196                .iter()
197                .map(|k| JsonValue::String((**k).clone()))
198                .collect(),
199        )),
200
201        // Wave 15 W15-deque (ADR-006 §2.7.19 / Q20, 2026-05-10):
202        // Deque serializes as a JSON array of front-to-back elements.
203        // Each element dispatches through the canonical ADR-005 §1
204        // single-discriminator `HeapValue` recursion. Same mechanical-
205        // yes mapping shape as HashSet (string-array specialisation
206        // generalised to heterogeneous-element).
207        HeapValue::Deque(d) => {
208            let mut elems: Vec<JsonValue> = Vec::with_capacity(d.items.len());
209            for v in d.items.iter() {
210                elems.push(heap_to_json_value(v)?);
211            }
212            Ok(JsonValue::Array(elems))
213        }
214
215        // TypedObject schema-aware (1)
216        HeapValue::TypedObject(storage) => typed_object_to_json_value(
217            storage.schema_id,
218            &storage.slots,
219            storage.heap_mask,
220        ),
221
222        // Categorically-non-data Reject (5)
223        HeapValue::Future(_) => Err("cannot serialize: Future".into()),
224        HeapValue::IoHandle(_) => Err("cannot serialize: IoHandle".into()),
225        HeapValue::NativeView(_) => Err("cannot serialize: NativeView (C view)".into()),
226        HeapValue::ClosureRaw(_) => Err("cannot serialize: closure".into()),
227        HeapValue::TaskGroup(_) => Err("cannot serialize: TaskGroup".into()),
228
229        // Architectural-choice deferred (7) — first-landing Err per supervisor
230        // PB 1/4 + REFINEMENT-1A. Each policy = separate sub-decision when first
231        // consumer needs it.
232        HeapValue::Decimal(_) => {
233            Err("Decimal serialization policy not yet decided (N7 architectural-choice deferral)".into())
234        }
235        HeapValue::DataTable(_) => Err(
236            "DataTable serialization policy not yet decided (N7 architectural-choice deferral)"
237                .into(),
238        ),
239        HeapValue::Content(_) => {
240            Err("Content serialization policy not yet decided (N7 architectural-choice deferral)".into())
241        }
242        HeapValue::Temporal(_) => {
243            Err("Temporal serialization policy not yet decided (N7 architectural-choice deferral)".into())
244        }
245        HeapValue::TableView(_) => {
246            Err("TableView serialization policy not yet decided (N7 architectural-choice deferral)".into())
247        }
248        HeapValue::Instant(_) => Err(
249            "Instant serialization policy not yet decided (N7 architectural-choice deferral; Instant is monotonic, not absolute — ISO-8601 inapplicable without epoch convention)"
250                .into(),
251        ),
252        HeapValue::NativeScalar(_) => Err(
253            "NativeScalar serialization policy not yet decided (N7 architectural-choice deferral; Ptr inner kind is hostile to JSON)"
254                .into(),
255        ),
256        // Wave-γ G-heap-filter-expr (ADR-006 §2.3 / Q8 amendment): a
257        // FilterExpr tree is a transient query-DSL value; it has no JSON
258        // representation. Reject in the same shape as the other non-data
259        // variants.
260        HeapValue::FilterExpr(_) => Err("cannot serialize: FilterExpr".into()),
261        // ADR-006 §2.7.13 / Q14 (Wave 8 W8-T26, 2026-05-10): Reference
262        // values are within-program data and never cross the JSON
263        // serialization boundary. Reject in the same shape as
264        // FilterExpr.
265        HeapValue::Reference(_) => Err("cannot serialize: Reference".into()),
266        // W13-iterator-state (ADR-006 §2.7.16 / Q17, 2026-05-10):
267        // Iterator pipelines are lazy within-program values and never
268        // cross the JSON serialization boundary. Reject in the same
269        // shape as FilterExpr / Reference (callers materialise via
270        // collect / forEach / etc. before serialisation).
271        HeapValue::Iterator(_) => Err("cannot serialize: Iterator".into()),
272        // Wave 15 W15-channel-rebuild (ADR-006 §2.7.20 / Q21,
273        // 2026-05-10): channels are concurrency primitives with
274        // interior `Mutex<ChannelInner>` state; the queue contents
275        // are runtime-mutable and don't have a stable serialized
276        // form. Reject in the same shape as FilterExpr / Iterator.
277        HeapValue::Channel(_) => Err("cannot serialize: Channel".into()),
278
279        // Wave 15 W15-priority-queue (ADR-006 §2.7.18 / Q19,
280        // 2026-05-10): PriorityQueue serialises as a JSON array of
281        // i64 priorities in heap-array order (the §2.7.18 amendment's
282        // documented wire shape — i64-priority-only at landing). The
283        // sorted shape is exposed only via `pq.toSortedArray()`; raw
284        // serialisation preserves heap order to match Display.
285        HeapValue::PriorityQueue(d) => Ok(JsonValue::Array(
286            d.heap
287                .iter()
288                .map(|v| JsonValue::Int(*v))
289                .collect(),
290        )),
291
292        // W15-range (ADR-006 §2.7.23 / Q24, 2026-05-10): Range
293        // serializes as a JSON array of materialised i64 values —
294        // mirror of HashSet's "array of strings" serialization shape
295        // (one mechanical-yes mapping; no architectural-choice
296        // deferral). Empty ranges produce an empty array. Step is
297        // baked into the materialisation, not exposed as a separate
298        // field.
299        HeapValue::Range(r) => Ok(JsonValue::Array(
300            r.to_vec_i64()
301                .into_iter()
302                .map(JsonValue::Int)
303                .collect(),
304        )),
305        // Wave 14 W14-variant-codegen (ADR-006 §2.7.17 / Q18, 2026-05-10):
306        // Result/Option carriers are within-program control-flow values;
307        // serialisation policy is deferred to the AnyError marshal /
308        // unwrapped-inner-value path. Reject in the same shape as
309        // Iterator until the policy is decided.
310        HeapValue::Result(_) => Err("cannot serialize: Result".into()),
311        HeapValue::Option(_) => Err("cannot serialize: Option".into()),
312        // W17-concurrency (ADR-006 §2.7.25, 2026-05-11): concurrency
313        // primitives carry runtime-mutable interior state (Mutex inner
314        // value, atomic counter, lazy initializer) and don't have a
315        // stable serialized form. Reject in the same shape as
316        // Channel / Iterator.
317        HeapValue::Mutex(_) => Err("cannot serialize: Mutex".into()),
318        HeapValue::Atomic(_) => Err("cannot serialize: Atomic".into()),
319        HeapValue::Lazy(_) => Err("cannot serialize: Lazy".into()),
320        // W17-trait-object-storage (ADR-006 §2.7.24 / Q25.C, 2026-05-11):
321        // a `dyn Trait` carrier has no stable JSON form — the boxed
322        // value's schema is dynamic, and serializing through the
323        // vtable would require a `to_json()` trait method that
324        // doesn't exist at the language level. Reject in the same
325        // shape as the concurrency primitives. The compiler-emission
326        // tier may later add a `Serializable` trait whose impls
327        // self-serialize through the vtable — that's a follow-up.
328        HeapValue::TraitObject(_) => Err("cannot serialize: TraitObject".into()),
329        // W17-comptime-vm-dispatch (ADR-006 §2.7.26, 2026-05-12):
330        // ModuleFn references are VM-internal callable handles with
331        // no stable serialised form — they index `module_fn_table`
332        // which is rebuilt per-VM-instance, not part of the
333        // serialisable program state.
334        HeapValue::ModuleFn(_) => Err("cannot serialize: ModuleFn".into()),
335        // ADR-006 §2.7.22 amendment (Round 18 S3, 2026-05-13): Matrix /
336        // MatrixSlice JSON serialization-policy is N7-architectural-choice
337        // deferred (mirror of the pre-amendment
338        // `TypedArrayData::Matrix` / `FloatSlice` rejection at this layer;
339        // 2D-layout encoding is undecided — nested array-of-arrays vs
340        // flat row-major vs `{rows, cols, data}` forms have different
341        // round-trip properties). MatrixSlice inherits the same deferral.
342        HeapValue::Matrix(_) => Err(
343            "Matrix serialization policy not yet decided (N7 architectural-choice deferral; multiple natural encodings: nested array-of-arrays vs flat row-major vs {rows, cols, data})"
344                .into(),
345        ),
346        HeapValue::MatrixSlice(_) => Err(
347            "MatrixSlice serialization policy not yet decided (N7 architectural-choice deferral; structurally inherits Matrix's encoding question)"
348                .into(),
349        ),
350    }
351}
352
353// V3-S5 ckpt-5-prime (2026-05-15): `typed_array_to_json_value` helper RETIRED
354// per W12 audit §3.6. The helper pattern-matched on the deleted `TypedArrayData`
355// enum (retired at ckpt-1) and was called by the deleted `HeapValue::TypedArray`
356// outer arm (retired at ckpt-4) above. The 13 mechanical-yes inner-arm
357// dispatches (I8/I16/I32/I64/U8/U16/U32/U64/F32/F64/Bool/String + later
358// Decimal/BigInt/Char/TypedObject from W17-typed-carrier-bundle-A) lose their
359// landing point with the carrier enum gone. The v2-raw `*mut TypedArray<T>`
360// JSON-serialisation path lands at the ckpt-5-prime² + ckpt-6 producer/
361// consumer storage-shape migration (per-element-type marshal-layer projection
362// before the value becomes a `HeapValue`). Refusal #1 binding: do not
363// reintroduce under any rename/shim/bridge.
364
365/// Walk a `HeapValue::TypedObject` and produce `JsonValue::Object`.
366///
367/// Schema lookup via `lookup_schema_by_id_public`; per-FieldDef
368/// `field_type` dispatch using `wire_name()` for JSON field name.
369/// Heap-typed fields are read via `slot.as_heap_value()` and recursed
370/// into `heap_to_json_value`; inline-typed fields are read via
371/// `slot.as_i64()` / `as_f64()` / `as_bool()` per the FieldType arm.
372///
373/// Mirrors json.rs's parse-side `build_typed_object_from_json` in
374/// reverse direction.
375fn typed_object_to_json_value(
376    schema_id: u64,
377    slots: &[shape_value::ValueSlot],
378    heap_mask: u64,
379) -> Result<JsonValue, String> {
380    use crate::type_schema::{lookup_schema_by_id_public, FieldType};
381
382    let schema = lookup_schema_by_id_public(schema_id as u32).ok_or_else(|| {
383        format!(
384            "heap_to_json_value: unknown TypedObject schema id {}",
385            schema_id
386        )
387    })?;
388
389    let mut pairs: Vec<(String, JsonValue)> = Vec::with_capacity(schema.fields.len());
390    for field in &schema.fields {
391        let idx = field.index as usize;
392        if idx >= slots.len() {
393            return Err(format!(
394                "heap_to_json_value: TypedObject field '{}' index {} out of bounds (slots.len()={})",
395                field.name,
396                idx,
397                slots.len()
398            ));
399        }
400        let slot = &slots[idx];
401        let is_heap = (heap_mask & (1u64 << field.index)) != 0;
402        let child = match (&field.field_type, is_heap) {
403            (FieldType::I64, false)
404            | (FieldType::I8, false)
405            | (FieldType::U8, false)
406            | (FieldType::I16, false)
407            | (FieldType::U16, false)
408            | (FieldType::I32, false)
409            | (FieldType::U32, false)
410            | (FieldType::U64, false) => JsonValue::Int(slot.as_i64()),
411            (FieldType::F64, false) => JsonValue::Number(slot.as_f64()),
412            (FieldType::Bool, false) => JsonValue::Bool(slot.as_bool()),
413            (FieldType::Timestamp, false) => {
414                // Timestamp is i64 ms-since-epoch — distinct from Instant
415                // (which is monotonic). Same architectural-choice as Temporal/
416                // Instant (user-visible behavioral commitment); first-landing
417                // Err per N7 deferral.
418                return Err(format!(
419                    "Timestamp serialization policy not yet decided (N7 architectural-choice deferral; field '{}')",
420                    field.name
421                ));
422            }
423            (FieldType::Decimal, _) => {
424                return Err(format!(
425                    "Decimal serialization policy not yet decided (N7 architectural-choice deferral; field '{}')",
426                    field.name
427                ));
428            }
429            (_, true) => heap_to_json_value(slot.as_heap_value())?,
430            // Inline scalar types where storage doesn't match field_type
431            // (Array/Object/Any when not heap-tagged; impossible if heap_mask
432            // is correct).
433            (other, false) => {
434                return Err(format!(
435                    "heap_to_json_value: TypedObject field '{}' has field_type {} but heap_mask bit clear (corrupt mask?)",
436                    field.name, other
437                ));
438            }
439        };
440        pairs.push((field.wire_name().to_string(), child));
441    }
442    Ok(JsonValue::Object(pairs))
443}
444
445/// Convert a `JsonValue` into a `serde_json::Value`.
446///
447/// Inverse of `serde_json_to_json_value` (`stdlib/json.rs:172-196`).
448/// Used by N7 consumers that produce JSON strings: `json.stringify`
449/// (C7), `http.post_json` (C8), `http.put_json` (C9). Pair with
450/// `heap_to_json_value` to round-trip a `HeapValue` tree to a JSON
451/// string via `serde_json::to_string(&v)?` / `to_string_pretty(&v)?`.
452///
453/// `JsonValue::Bytes` maps to `serde_json::Value::Array` of `u8`-as-
454/// `Number` per JSON's no-byte-array convention. `JsonValue::Bytes` is
455/// not currently produced by `heap_to_json_value` (the C2 walker has
456/// no path that emits Bytes); included here for completeness +
457/// bidirectional symmetry with future 3.C msgpack-binary parse paths
458/// per supervisor PB 3/4.
459pub fn json_value_to_serde_json(jv: &JsonValue) -> serde_json::Value {
460    match jv {
461        JsonValue::Null => serde_json::Value::Null,
462        JsonValue::Bool(b) => serde_json::Value::Bool(*b),
463        JsonValue::Int(i) => serde_json::Value::Number((*i).into()),
464        JsonValue::Number(f) => serde_json::Number::from_f64(*f)
465            .map(serde_json::Value::Number)
466            .unwrap_or(serde_json::Value::Null),
467        JsonValue::String(s) => serde_json::Value::String(s.clone()),
468        JsonValue::Bytes(bytes) => serde_json::Value::Array(
469            bytes
470                .iter()
471                .map(|&b| serde_json::Value::Number(b.into()))
472                .collect(),
473        ),
474        JsonValue::Array(arr) => {
475            serde_json::Value::Array(arr.iter().map(json_value_to_serde_json).collect())
476        }
477        JsonValue::Object(pairs) => {
478            let mut map = serde_json::Map::with_capacity(pairs.len());
479            for (k, v) in pairs.iter() {
480                map.insert(k.clone(), json_value_to_serde_json(v));
481            }
482            serde_json::Value::Object(map)
483        }
484    }
485}
486
487/// Convert a `JsonValue` into a `serde_yaml::Value`.
488///
489/// Used by N7 consumer C10 (`yaml.stringify`). Pair with
490/// `heap_to_json_value` to round-trip a `HeapValue` tree to a YAML
491/// string via `serde_yaml::to_string(&v)?`.
492///
493/// Lossy mapping shape parallels parse-side yaml.rs precedent
494/// (yaml.rs:75-78 unwraps `serde_yaml::Value::Tagged`); on the encode
495/// side, we never produce Tagged, so no lossy path. `JsonValue::Bytes`
496/// maps to `Value::Sequence` of `u8` numbers (YAML has no native byte
497/// type); reserved for future msgpack-binary roundtrip via 3.C.
498pub fn json_value_to_serde_yaml(jv: &JsonValue) -> serde_yaml::Value {
499    match jv {
500        JsonValue::Null => serde_yaml::Value::Null,
501        JsonValue::Bool(b) => serde_yaml::Value::Bool(*b),
502        JsonValue::Int(i) => serde_yaml::Value::Number((*i).into()),
503        JsonValue::Number(f) => serde_yaml::Value::Number((*f).into()),
504        JsonValue::String(s) => serde_yaml::Value::String(s.clone()),
505        JsonValue::Bytes(bytes) => serde_yaml::Value::Sequence(
506            bytes
507                .iter()
508                .map(|&b| serde_yaml::Value::Number((b as u64).into()))
509                .collect(),
510        ),
511        JsonValue::Array(arr) => {
512            serde_yaml::Value::Sequence(arr.iter().map(json_value_to_serde_yaml).collect())
513        }
514        JsonValue::Object(pairs) => {
515            let mut map = serde_yaml::Mapping::with_capacity(pairs.len());
516            for (k, v) in pairs.iter() {
517                map.insert(
518                    serde_yaml::Value::String(k.clone()),
519                    json_value_to_serde_yaml(v),
520                );
521            }
522            serde_yaml::Value::Mapping(map)
523        }
524    }
525}
526
527/// Convert a `JsonValue` into a `toml::Value`.
528///
529/// Used by N7 consumer C11 (`toml.stringify`). Pair with
530/// `heap_to_json_value` to round-trip a `HeapValue` tree to a TOML
531/// string via `toml::to_string(&v)?`. **Replaces** the legacy
532/// `nanboxed_to_toml_value` walker (`stdlib/toml_module.rs:67-107`)
533/// entirely; that walker used deleted ValueWord accessors and is
534/// removed by C11.
535///
536/// **TOML constraint**: TOML has no native null. `JsonValue::Null` maps
537/// to `toml::Value::String("null")` — the same lossy sentinel used by
538/// the legacy `nanboxed_to_toml_value` walker (`toml_module.rs:68-70`),
539/// preserved here for round-trip behavior continuity. Reconsidering
540/// this sentinel is a future architectural-choice sub-decision (the
541/// alternative — refusing serialization with Err — would be a behavioral
542/// regression vs the legacy walker; held as future N7 sub-disposition).
543///
544/// **TOML constraint**: TOML's top-level must be a Table. This helper
545/// returns a `toml::Value` of any shape; the consumer (`toml.stringify`
546/// body in C11) is responsible for verifying root-level Table when
547/// passing to `toml::to_string`. Surfacing root-level non-Table as Err
548/// is C11's responsibility, not this helper's.
549///
550/// `JsonValue::Bytes` maps to `Array` of `u8`-as-Integer (TOML has no
551/// native byte type); reserved for future msgpack-binary roundtrip via
552/// 3.C.
553pub fn json_value_to_toml_value(jv: &JsonValue) -> toml::Value {
554    match jv {
555        JsonValue::Null => toml::Value::String("null".to_string()),
556        JsonValue::Bool(b) => toml::Value::Boolean(*b),
557        JsonValue::Int(i) => toml::Value::Integer(*i),
558        JsonValue::Number(f) => toml::Value::Float(*f),
559        JsonValue::String(s) => toml::Value::String(s.clone()),
560        JsonValue::Bytes(bytes) => toml::Value::Array(
561            bytes
562                .iter()
563                .map(|&b| toml::Value::Integer(b as i64))
564                .collect(),
565        ),
566        JsonValue::Array(arr) => {
567            toml::Value::Array(arr.iter().map(json_value_to_toml_value).collect())
568        }
569        JsonValue::Object(pairs) => {
570            let mut map = toml::map::Map::new();
571            for (k, v) in pairs.iter() {
572                map.insert(k.clone(), json_value_to_toml_value(v));
573            }
574            toml::Value::Table(map)
575        }
576    }
577}
578
579/// Encode a `JsonValue` to MessagePack bytes.
580///
581/// Used by N7 consumers C12 (`msgpack.encode`) and C13
582/// (`msgpack.encode_bytes`). Pair with `heap_to_json_value` to
583/// round-trip a `HeapValue` tree to MessagePack-encoded bytes.
584///
585/// **Routing**: this helper internally converts the `JsonValue` to a
586/// `serde_json::Value` via `json_value_to_serde_json` (C3) and then
587/// calls `rmp_serde::to_vec` on the result. The external surface is a
588/// single named `&JsonValue → Result<Vec<u8>, String>` contract;
589/// consumers do NOT see the internal serde_json::Value intermediate.
590///
591/// **Why this shape (Option C per team-lead authorization)**: the
592/// `rmpv::Value` library is NOT in workspace deps, only `rmp-serde` and
593/// `rmp` are. The legacy msgpack path
594/// (`stdlib/msgpack_module.rs:104-107` pre-bulldozer) routed
595/// `value.to_json_value()` (deleted) through
596/// `rmp_serde::to_vec(&json_value)` — the routing-through-serde_json
597/// pattern is precedent. Option C preserves this structural pattern
598/// while exposing a single named JsonValue→bytes helper, decoupling
599/// consumer-body from internal routing (forbidden state: "consumer-
600/// body couples with internal routing" is unrepresentable; future
601/// rmpv-adoption for performance won't change this helper's external
602/// contract).
603///
604/// **Naming correction**: the original REFINEMENT-1A scope brief
605/// paraphrased C6 as `json_value_to_rmpv_value`. Team-lead self-flagged
606/// this as paraphrase error: supervisor PB 1/4 said "C3-C6 per-format
607/// encoders (json/yaml/toml/msgpack)" with implicit naming, NOT a
608/// literal `rmpv` requirement. The signature here matches the actual
609/// supervisor framing; rmpv is not used.
610pub fn json_value_to_msgpack_bytes(jv: &JsonValue) -> Result<Vec<u8>, String> {
611    let serde_json_v = json_value_to_serde_json(jv);
612    rmp_serde::to_vec(&serde_json_v).map_err(|e| format!("msgpack encode failed: {}", e))
613}