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}