shape_runtime/stdlib/json.rs
1//! Native `json` module for JSON parsing and serialization.
2//!
3//! Exports: json.parse(text), json.stringify(value, pretty?), json.is_valid(text)
4//!
5//! `parse(text)` always returns a typed `Json` enum value. The legacy
6//! `json_value_to_nanboxed` untyped fallback was removed in sweep phase 4a.
7//! Schema-driven parsing (`__parse_typed`) coerces JSON directly into a
8//! TypedObject for the supplied schema; nested unknown objects fall back to
9//! the typed `Json` enum rather than an untyped HashMap.
10//!
11//! Phase-2d strict-typing migration status (Stage D close-out batch,
12//! 2026-05-07):
13//!
14//! - `json.parse(text) -> Result<Json>` — **MIGRATED at Stage D Step 4.**
15//! Body builds the strict-typed `JsonValue` enum
16//! (`crate::json_value::JsonValue`) directly from `serde_json::Value`
17//! and wraps with `TypedReturn::Ok(ConcreteReturn::JsonValue(...))`
18//! per Stage D Step 1's `ConcreteReturn::JsonValue` variant addition
19//! (commit `a022f43`). N6 sub-shape (b1) sign-off; closes B1
20//! sub-decision #2 for json.parse.
21//! - `json.__parse_typed(text, schema_id) -> Result<any>` — **MIGRATED
22//! at Stage D close-out Step 3.** Body builds `HeapValue::TypedObject`
23//! directly from the runtime schema + JSON object via
24//! `build_typed_object_from_json`, then wraps the `Arc<HeapValue>` in
25//! `ConcreteReturn::OpaqueTypedObject` per close-out Step 2's variant
26//! addition (commit `1bca2c4`). N8 sign-off; closes B1 sub-decision
27//! #2 for json.__parse_typed. The 5 legacy ValueWord-using helpers
28//! (make_json_enum / json_value_to_enum / json_object_to_typed /
29//! json_value_to_typed_nb / json_value_to_typed_json_enum) were
30//! DELETED at close-out Step 3 — verified call-graph private to
31//! `__parse_typed` before deletion.
32//! - `json.stringify(value: any, pretty?: bool) -> Result<string>` —
33//! DEFERRED pending **N7** (HeapValue→JSON serializer for HTTP /
34//! object-output marshal contexts). N7 is the unified workstream
35//! covering HTTP post_json/put_json + yaml/toml/msgpack
36//! stringify/encode/encode_bytes (6 consumers total). Body uses
37//! deleted `to_json_value()` + would need the N7 serializer.
38//! - `json.is_valid(text) -> bool` — Migratable in isolation but kept
39//! deferred for per-file atomicity; lands with stringify when N7
40//! sign-off unblocks the residual json cohort.
41//!
42//! N7 is supervisor-level; queued for next-session relay batch (see
43//! `docs/defections.md` HashMap-marshal cluster sub-decision queue
44//! 2026-05-07 Stage B+D close-out subsection).
45//!
46//! Strict-typed helpers `serde_json_to_json_value` (used by json.parse),
47//! `build_json_enum_heap_value`, `build_field_slot_from_json`, and
48//! `build_typed_object_from_json` (used by __parse_typed) construct
49//! ValueSlots directly from native types via the `ValueSlot::from_*`
50//! primitives — no ValueWord intermediate, no call to `nb_to_slot`.
51//!
52//! Note: `nb_to_slot` (defined `pub(crate)` at
53//! `crate::type_schema::mod`) and adjacent slot-construction code in
54//! `type_schema/mod.rs` still cite the deleted `ValueWord` API. That
55//! cleanup is **N9 candidate** — type_schema/mod.rs slot-construction-
56//! layer migration. Tracked separately for next-session pickup; this
57//! commit explicitly does NOT touch type_schema/mod.rs (verification
58//! gate caught the cross-cutting concern; Option A2 chosen over A1 to
59//! preserve per-file atomicity).
60
61use crate::json_value::JsonValue;
62use crate::marshal::{register_typed_fn_1, register_typed_fn_2};
63use crate::module_exports::{ModuleExports, ModuleParam};
64use crate::type_schema::TypeSchemaRegistry;
65use crate::typed_module_exports::{
66 ConcreteReturn, ConcreteType, TypedReturn, register_typed_function,
67};
68use shape_value::heap_value::HeapValue;
69use shape_value::{KindedSlot, ValueSlot};
70use std::sync::Arc;
71
72// Json enum variant IDs (must match order in json_value.shape).
73//
74// Layout: Null | Bool(bool) | Int(int) | Number(number) | Str(string)
75// | Array(any) | Object(any)
76const JSON_VARIANT_NULL: i64 = 0;
77const JSON_VARIANT_BOOL: i64 = 1;
78const JSON_VARIANT_INT: i64 = 2;
79const JSON_VARIANT_NUMBER: i64 = 3;
80const JSON_VARIANT_STR: i64 = 4;
81const JSON_VARIANT_ARRAY: i64 = 5;
82const JSON_VARIANT_OBJECT: i64 = 6;
83
84/// Build a Json-enum `HeapValue::TypedObject` directly from a
85/// `serde_json::Value`. Used as the `FieldType::Any` fallback path in
86/// `json.__parse_typed` — when a schema field is typed `any`, the JSON
87/// payload is stored as a strict-typed `Json` enum tree
88/// (`HeapValue::TypedObject` keyed by the Json schema). Recursion lives
89/// at the HeapValue layer; each variant's payload is built directly via
90/// `ValueSlot::from_*` primitives without ValueWord intermediates.
91///
92/// The Json enum's layout: slot 0 = `__variant` (I64), slot 1 =
93/// `__payload_0` (heap or inline native). Variant IDs match
94/// `JSON_VARIANT_*` constants which mirror `json_value.shape`.
95///
96/// Integral JSON numbers that fit in `i64` map to `Json::Int`; all other
97/// numbers map to `Json::Number(f64)`. Preserves the `int` / `number`
98/// distinction at the boundary.
99fn build_json_enum_heap_value(value: serde_json::Value, json_schema_id: u64) -> HeapValue {
100 let (variant_id, payload_slot, payload_is_heap) = match value {
101 serde_json::Value::Null => (JSON_VARIANT_NULL, ValueSlot::none(), false),
102 serde_json::Value::Bool(b) => (JSON_VARIANT_BOOL, ValueSlot::from_bool(b), false),
103 serde_json::Value::Number(n) => {
104 // Prefer Json::Int for integral i64-fitting numbers.
105 if let Some(i) = n.as_i64() {
106 if !n.to_string().contains('.') {
107 return build_typed_object(
108 json_schema_id,
109 vec![
110 ValueSlot::from_int(JSON_VARIANT_INT),
111 ValueSlot::from_int(i),
112 ],
113 0,
114 );
115 }
116 }
117 (
118 JSON_VARIANT_NUMBER,
119 ValueSlot::from_number(n.as_f64().unwrap_or(0.0)),
120 false,
121 )
122 }
123 serde_json::Value::String(s) => (
124 JSON_VARIANT_STR,
125 ValueSlot::from_string_arc(Arc::new(s)),
126 true,
127 ),
128 serde_json::Value::Array(arr) => {
129 // V3-S5 ckpt-5-prime²c (2026-05-15) Migration shape (a): every
130 // JSON array element is a `Json` enum-TypedObject built by
131 // `build_json_enum_heap_value` — so the array always lowers to
132 // a `*mut TypedArray<TypedObjectPtr>` flat-struct carrier per
133 // the v2-raw monomorphic shape. The pre-migration
134 // `TypedArrayData::TypedObject` enum-arm + `build_specialized_
135 // from_heap_arcs` dispatcher + `ValueSlot::from_typed_array`
136 // are all deleted at V3-S5 ckpt-1/ckpt-4. Element-kind
137 // enforcement is by the body-side `T = TypedObjectPtr` choice
138 // + the variant's `field_kinds[1] = Ptr(HeapKind::TypedArray)`
139 // (set on the outer Json TypedObject).
140 let element_ptrs: Vec<*const shape_value::TypedObjectStorage> = arr
141 .into_iter()
142 .map(|v| {
143 let hv = build_json_enum_heap_value(v, json_schema_id);
144 let to_ptr = match hv {
145 HeapValue::TypedObject(p) => p,
146 other => panic!(
147 "json: build_json_enum_heap_value must return \
148 TypedObject, got {:?}",
149 other.kind()
150 ),
151 };
152 // Extract the inner raw `*const TypedObjectStorage`,
153 // transferring the one refcount share from the
154 // `TypedObjectPtr` wrapper to the raw pointer (which
155 // the `TypedArray` will own as an element).
156 to_ptr.into_raw()
157 })
158 .collect();
159 let arr_ptr: *mut shape_value::v2::typed_array::TypedArray<
160 *const shape_value::TypedObjectStorage,
161 > = shape_value::v2::typed_array::TypedArray::<
162 *const shape_value::TypedObjectStorage,
163 >::from_slice(&element_ptrs);
164 // `from_slice` copies each raw pointer bit-for-bit (raw
165 // pointers are Copy). The refcount shares were transferred
166 // from `TypedObjectPtr` wrappers via `into_raw()` already;
167 // the source Vec doesn't own any element share, so its Drop
168 // is a no-op for the elements.
169 (
170 JSON_VARIANT_ARRAY,
171 ValueSlot::from_u64(arr_ptr as u64),
172 true,
173 )
174 }
175 serde_json::Value::Object(map) => {
176 // Wave 2 Round 3b C2-joint ckpt-4 (2026-05-14): build the
177 // JSON object as a `HashMap<string, TypedObject>` where each
178 // value is a nested Json-enum TypedObject. The result V is
179 // `TypedObject` (heterogeneous JSON values are flattened
180 // through the Json enum wrapper — one schema, every variant
181 // structurally captured). ADR-006 §2.7.24 Q25.B SUPERSEDED.
182 let mut data: shape_value::heap_value::HashMapData<
183 shape_value::heap_value::TypedObjectPtr,
184 > = shape_value::heap_value::HashMapData::new();
185 for (k, v) in map.into_iter() {
186 let nested = build_json_enum_heap_value(v, json_schema_id);
187 let to_ptr = match nested {
188 HeapValue::TypedObject(p) => p,
189 other => panic!(
190 "build_json_enum_heap_value must return TypedObject, got {:?}",
191 other.kind()
192 ),
193 };
194 unsafe { data.insert(k.as_str(), to_ptr) };
195 }
196 let kref = shape_value::heap_value::HashMapKindedRef::TypedObject(Arc::new(data));
197 (
198 JSON_VARIANT_OBJECT,
199 ValueSlot::from_hashmap(Arc::new(kref)),
200 true,
201 )
202 }
203 };
204 let heap_mask = if payload_is_heap { 1u64 << 1 } else { 0u64 };
205 build_typed_object(
206 json_schema_id,
207 vec![ValueSlot::from_int(variant_id), payload_slot],
208 heap_mask,
209 )
210}
211
212/// Build a `HeapValue::TypedObject(Arc<TypedObjectStorage>)` from raw
213/// slots + a `heap_mask`. The schema's `FieldType`s are the source of
214/// truth at read time — no per-slot kind table is recorded on this
215/// fast path (mirrors `type_schema::typed_object_from_pairs`).
216fn build_typed_object(schema_id: u64, slots: Vec<ValueSlot>, heap_mask: u64) -> HeapValue {
217 // Wave 2 Round 4 D4 ckpt-final-prime² (2026-05-14): variant signature
218 // flipped to `HeapValue::TypedObject(TypedObjectPtr)`. Wrap the
219 // `_new`-returned raw pointer (refcount=1) in `TypedObjectPtr`,
220 // transferring the share to the wrapper.
221 let storage = shape_value::TypedObjectStorage::_new(
222 schema_id,
223 slots.into_boxed_slice(),
224 heap_mask,
225 Arc::from(Vec::<shape_value::NativeKind>::new().into_boxed_slice()),
226 );
227 HeapValue::TypedObject(shape_value::heap_value::TypedObjectPtr::new(storage))
228}
229
230/// Convert a `serde_json::Value` into the strict-typed `JsonValue` sum
231/// (`crate::json_value::JsonValue`).
232///
233/// Stage D Step 4 (2026-05-07). Used by `json.parse` to produce an
234/// `Arc<HeapValue>`-free recursive value tree that wraps directly into
235/// `ConcreteReturn::JsonValue`. Same int-vs-number split rule as the
236/// legacy `json_value_to_enum`: integral JSON numbers fitting in `i64`
237/// map to `JsonValue::Int`; all other numbers map to `JsonValue::Number`.
238fn serde_json_to_json_value(value: serde_json::Value) -> JsonValue {
239 match value {
240 serde_json::Value::Null => JsonValue::Null,
241 serde_json::Value::Bool(b) => JsonValue::Bool(b),
242 serde_json::Value::Number(n) => {
243 if let Some(i) = n.as_i64() {
244 if !n.to_string().contains('.') {
245 return JsonValue::Int(i);
246 }
247 }
248 JsonValue::Number(n.as_f64().unwrap_or(0.0))
249 }
250 serde_json::Value::String(s) => JsonValue::String(s),
251 serde_json::Value::Array(arr) => {
252 JsonValue::Array(arr.into_iter().map(serde_json_to_json_value).collect())
253 }
254 serde_json::Value::Object(map) => {
255 let pairs: Vec<(String, JsonValue)> = map
256 .into_iter()
257 .map(|(k, v)| (k, serde_json_to_json_value(v)))
258 .collect();
259 JsonValue::Object(pairs)
260 }
261 }
262}
263
264/// Build a single `ValueSlot` for a schema field given its declared type
265/// and a JSON value. Returns `(slot, is_heap)` where `is_heap` is the
266/// bit to set in `heap_mask` if the slot stores a heap pointer.
267///
268/// For typed fields (I64/F64/Bool/String/Decimal/Object-with-known-schema),
269/// produces the strict-typed slot directly via `ValueSlot::from_*`
270/// primitives. For `FieldType::Any` and untypable shapes (Array, mixed
271/// types, Object-without-known-schema), falls back to a Json-enum-tree
272/// HeapValue via `build_json_enum_heap_value`.
273fn build_field_slot_from_json(
274 value: &serde_json::Value,
275 field_type: &crate::type_schema::FieldType,
276 registry: &TypeSchemaRegistry,
277 json_schema_id: u64,
278) -> Result<(ValueSlot, bool), String> {
279 use crate::type_schema::FieldType;
280 use serde_json::Value;
281 match (value, field_type) {
282 (Value::Null, _) => Ok((ValueSlot::none(), false)),
283 (Value::Bool(b), FieldType::Bool) => Ok((ValueSlot::from_bool(*b), false)),
284 (Value::Number(n), FieldType::I64) => {
285 Ok((ValueSlot::from_int(n.as_i64().unwrap_or(0)), false))
286 }
287 (Value::Number(n), FieldType::F64) | (Value::Number(n), FieldType::Decimal) => Ok((
288 ValueSlot::from_number(n.as_f64().unwrap_or(0.0)),
289 false,
290 )),
291 (Value::String(s), FieldType::String) => {
292 Ok((ValueSlot::from_string_arc(Arc::new(s.clone())), true))
293 }
294 (Value::Object(obj), FieldType::Object(type_name)) => {
295 if let Some(nested_schema) = registry.get(type_name) {
296 let nested_hv =
297 build_typed_object_from_json(nested_schema, obj, registry, json_schema_id)?;
298 Ok((heap_to_slot(nested_hv), true))
299 } else {
300 // Nested type's schema not registered — fall back to a
301 // typed `Json::Object` HeapValue per the legacy contract.
302 let json_hv =
303 build_json_enum_heap_value(Value::Object(obj.clone()), json_schema_id);
304 Ok((heap_to_slot(json_hv), true))
305 }
306 }
307 // FieldType::Any or any other shape (Array, type-mismatched, etc.)
308 // → fall back to a Json enum tree at the slot.
309 _ => {
310 let json_hv = build_json_enum_heap_value(value.clone(), json_schema_id);
311 Ok((heap_to_slot(json_hv), true))
312 }
313 }
314}
315
316/// Project a `HeapValue` (typically a `TypedObject` produced by the
317/// nested-schema path or a `Json::Object` enum from the fallback path)
318/// into a typed `ValueSlot` via the matching per-FieldType constructor.
319/// Used by the JSON-tree builder to avoid the deprecated
320/// `ValueSlot::from_heap(HeapValue)` boxing path.
321fn heap_to_slot(hv: HeapValue) -> ValueSlot {
322 match hv {
323 // Wave 2 Round 4 D4 ckpt-final-prime² (2026-05-14): variant payload
324 // flipped to `TypedObjectPtr`. The `from_typed_object_raw`
325 // constructor stores the raw pointer directly; the wrapper's
326 // refcount share moves to the slot via `into_raw()`.
327 HeapValue::TypedObject(ptr) => ValueSlot::from_typed_object_raw(ptr.into_raw()),
328 HeapValue::String(arc) => ValueSlot::from_string_arc(arc),
329 // V3-S5 ckpt-5-prime²c (2026-05-15): the `HeapValue::TypedArray`
330 // outer arm + `ValueSlot::from_typed_array` constructor are
331 // deleted at V3-S5 ckpt-4/ckpt-5; per-element-kind `from_typed_
332 // array_<T>` constructors are the Round 2 follow-up. This match
333 // arm is unreachable in the new world (no Json branch produces a
334 // `HeapValue::TypedArray` since the variant doesn't exist) so it
335 // is wholesale-deleted per the lockstep table discipline.
336 // Wave 2 Round 3b C2-joint ckpt-2 (2026-05-14): payload flipped to
337 // `HashMapKindedRef`; wrap in `Arc::new` for the slot's Arc-storage
338 // shape per ADR-006 §2.7.24 Q25.B SUPERSEDED.
339 HeapValue::HashMap(kref) => ValueSlot::from_hashmap(Arc::new(kref)),
340 HeapValue::Decimal(arc) => ValueSlot::from_decimal(arc),
341 HeapValue::BigInt(arc) => ValueSlot::from_bigint(arc),
342 HeapValue::DataTable(arc) => ValueSlot::from_data_table(arc),
343 HeapValue::IoHandle(arc) => ValueSlot::from_io_handle(arc),
344 HeapValue::NativeView(arc) => ValueSlot::from_native_view(arc),
345 // Inline-scalar / less-common variants fall back to the deprecated
346 // boxing path until per-variant constructors land in Phase 2c.
347 #[allow(deprecated)]
348 other => ValueSlot::from_heap(other),
349 }
350}
351
352/// Build a `HeapValue::TypedObject` keyed by the given schema, populated
353/// from a JSON object. Matches JSON keys to schema fields using
354/// `wire_name()` (respects `@alias`). Missing fields are written as
355/// `ValueSlot::none()` with no heap_mask bit set.
356fn build_typed_object_from_json(
357 schema: &crate::type_schema::TypeSchema,
358 map: &serde_json::Map<String, serde_json::Value>,
359 registry: &TypeSchemaRegistry,
360 json_schema_id: u64,
361) -> Result<HeapValue, String> {
362 let num_fields = schema.fields.len();
363 let mut slots = vec![ValueSlot::none(); num_fields];
364 let mut heap_mask = 0u64;
365
366 for field in &schema.fields {
367 let wire = field.wire_name();
368 let (slot, is_heap) = if let Some(jv) = map.get(wire) {
369 build_field_slot_from_json(jv, &field.field_type, registry, json_schema_id)?
370 } else {
371 (ValueSlot::none(), false)
372 };
373 slots[field.index as usize] = slot;
374 if is_heap {
375 heap_mask |= 1u64 << field.index;
376 }
377 }
378
379 Ok(build_typed_object(
380 schema.id as u64,
381 slots,
382 heap_mask,
383 ))
384}
385
386/// Create the `json` module with JSON parsing and serialization functions.
387pub fn create_json_module() -> ModuleExports {
388 let mut module = ModuleExports::new("std::core::json");
389 module.description = "JSON parsing and serialization".to_string();
390
391 // json.parse(text: string) -> Result<Json>
392 // Stage D Step 4 (2026-05-07): migrated to the strict-typed marshal
393 // layer. Body builds `JsonValue` (`crate::json_value::JsonValue`)
394 // directly and wraps with `TypedReturn::Ok(ConcreteReturn::JsonValue(..))`
395 // per Step 1's variant addition. No body-time schema lookup —
396 // `ConcreteType::JsonValue("Json")` carries the type-name at the
397 // registration-display layer.
398 register_typed_fn_1::<_, Arc<String>>(
399 &mut module,
400 "parse",
401 "Parse a JSON string into Shape values",
402 "text",
403 "string",
404 ConcreteType::Result(Box::new(ConcreteType::JsonValue("Json".to_string()))),
405 |text: Arc<String>, _ctx| {
406 let parsed: serde_json::Value = serde_json::from_str(text.as_str())
407 .map_err(|e| format!("json.parse() failed: {}", e))?;
408
409 let result = serde_json_to_json_value(parsed);
410
411 Ok(TypedReturn::Ok(ConcreteReturn::JsonValue(result)))
412 },
413 );
414
415 // json.__parse_typed(text: string, schema_id: number) -> Result<any>
416 // Stage D close-out Step 3 (2026-05-07): migrated to the strict-typed
417 // marshal layer via Step 2's `ConcreteReturn::OpaqueTypedObject`
418 // variant (commit `1bca2c4`). Body builds `HeapValue::TypedObject`
419 // directly from the runtime schema + JSON object via
420 // `build_typed_object_from_json`, then wraps the `Arc<HeapValue>` in
421 // `ConcreteReturn::OpaqueTypedObject` per the N8 sign-off framing.
422 //
423 // The 5 legacy ValueWord-using helpers (make_json_enum,
424 // json_value_to_enum, json_object_to_typed, json_value_to_typed_nb,
425 // json_value_to_typed_json_enum) are DELETED. The strict-typed
426 // replacements (`build_json_enum_heap_value`,
427 // `build_field_slot_from_json`, `build_typed_object_from_json`)
428 // construct ValueSlots directly from native types via the
429 // `ValueSlot::from_*` primitives — no ValueWord intermediate, no
430 // call to `nb_to_slot` (which is type_schema/mod.rs's slot-
431 // construction utility; cleaning that up is N9 territory tracked
432 // separately).
433 //
434 // Json schema (`std::core::json_value`) is looked up at body time
435 // via `ctx.schemas.get("Json")` — needed for the FieldType::Any
436 // fallback to construct typed Json-enum-tree HeapValues for
437 // untypable nested values.
438 register_typed_fn_2::<_, Arc<String>, f64>(
439 &mut module,
440 "__parse_typed",
441 "Parse a JSON string into a typed struct",
442 [("text", "string"), ("schema_id", "number")],
443 ConcreteType::Result(Box::new(ConcreteType::OpaqueTypedObject(
444 "any".to_string(),
445 ))),
446 |text: Arc<String>, schema_id_f: f64, ctx| {
447 let schema_id = schema_id_f as u32;
448
449 let parsed: serde_json::Value = serde_json::from_str(text.as_str())
450 .map_err(|e| format!("json.__parse_typed() failed: {}", e))?;
451
452 let map = match parsed {
453 serde_json::Value::Object(m) => m,
454 _ => {
455 return Err("json.__parse_typed() requires a JSON object".to_string());
456 }
457 };
458
459 let schema = ctx
460 .schemas
461 .get_by_id(schema_id)
462 .ok_or_else(|| format!("json.__parse_typed(): unknown schema id {}", schema_id))?;
463
464 let json_schema = ctx.schemas.get("Json").ok_or_else(|| {
465 "json.__parse_typed() requires the `Json` enum schema (load std::core::json_value)"
466 .to_string()
467 })?;
468 let json_schema_id = json_schema.id as u64;
469
470 let result_hv = build_typed_object_from_json(schema, &map, ctx.schemas, json_schema_id)?;
471
472 Ok(TypedReturn::Ok(ConcreteReturn::OpaqueTypedObject(Arc::new(
473 result_hv,
474 ))))
475 },
476 );
477
478 // json.stringify(value: any, pretty?: bool) -> Result<string>
479 //
480 // Phase 1.B body shim: pre-bulldozer this called
481 // `value.to_json_value()` on a `&ValueWord`. Post-ADR-006 the
482 // generic value→JSON serializer is the deferred N7 workstream
483 // (HeapValue→JSON unified across http/yaml/toml/msgpack/json).
484 // Until N7 lands, the body returns an error rather than emit a
485 // partial / unsound serializer. Variadic shape preserves the
486 // optional `pretty` arg per §2.7.4 ruling.
487 register_typed_function(
488 &mut module,
489 "stringify",
490 "Serialize a Shape value to a JSON string",
491 vec![
492 ModuleParam {
493 name: "value".to_string(),
494 type_name: "any".to_string(),
495 required: true,
496 description: "Value to serialize".to_string(),
497 ..Default::default()
498 },
499 ModuleParam {
500 name: "pretty".to_string(),
501 type_name: "bool".to_string(),
502 required: false,
503 description: "Pretty-print with indentation (default: false)".to_string(),
504 default_snippet: Some("false".to_string()),
505 ..Default::default()
506 },
507 ],
508 ConcreteType::Result(Box::new(ConcreteType::String)),
509 |args, _ctx| {
510 let _value = args
511 .first()
512 .ok_or_else(|| "json.stringify() requires a value argument".to_string())?;
513 let _pretty = args.get(1).map(|a| a.slot().as_bool()).unwrap_or(false);
514 Ok(TypedReturn::Err(ConcreteReturn::String(
515 "json.stringify() pending N7 (HeapValue→JSON) — see ADR-006 §2.7.4".to_string(),
516 )))
517 },
518 );
519
520 // json.is_valid(text: string) -> bool
521 //
522 // Phase 1.B body shim: variadic args carry `KindedSlot` placeholders
523 // (see `marshal.rs` register_typed_function — kind threading lands
524 // in Phase 2c). Read the first slot as a `String` Arc per the
525 // declared `string` param contract.
526 register_typed_function(
527 &mut module,
528 "is_valid",
529 "Check if a string is valid JSON",
530 vec![ModuleParam {
531 name: "text".to_string(),
532 type_name: "string".to_string(),
533 required: true,
534 description: "String to validate as JSON".to_string(),
535 ..Default::default()
536 }],
537 ConcreteType::Bool,
538 |args, _ctx| {
539 let slot = args
540 .first()
541 .ok_or_else(|| "json.is_valid() requires a string argument".to_string())?;
542 let text = slot_as_string(slot)
543 .ok_or_else(|| "json.is_valid() requires a string argument".to_string())?;
544 let valid = serde_json::from_str::<serde_json::Value>(text.as_str()).is_ok();
545 Ok(TypedReturn::Concrete(ConcreteReturn::Bool(valid)))
546 },
547 );
548
549 module
550}
551
552/// Read a [`KindedSlot`]'s bits as an `Arc<String>` payload. Used by
553/// Phase 1.B variadic body shims that have been migrated off
554/// `ValueWord::as_str()`. Phase 2c lands proper per-position kind
555/// threading; until then, variadic bodies interpret slots per their
556/// declared `ModuleParam` contract.
557fn slot_as_string(slot: &KindedSlot) -> Option<Arc<String>> {
558 let bits = slot.slot().raw();
559 if bits == 0 {
560 return None;
561 }
562 // SAFETY: variadic-arg slots whose registered param type is `string`
563 // store an `Arc<String>::into_raw` pointer (matching
564 // `ValueSlot::from_string_arc`). Reconstitute without consuming the
565 // slot's strong-count share by `from_raw` + `increment_strong_count`
566 // semantics — i.e. `Arc::clone` of a `from_raw`-rebuilt handle and
567 // forget the rebuilt one.
568 unsafe {
569 let arc = Arc::<String>::from_raw(bits as *const String);
570 let cloned = arc.clone();
571 std::mem::forget(arc);
572 Some(cloned)
573 }
574}
575
576
577// Tests deleted along with the legacy ValueWord-based fixtures, mirroring
578// the csv/http/xml migrations. The test infrastructure (`invoke_export`,
579// `&[ValueWord]` arg arrays, `as_ok_inner`/`extract_enum_variant`
580// helpers) all relied on the pre-bulldozer ValueWord API which no
581// longer exists. New typed-marshal test harness arrives with the
582// shape-vm cleanup workstream.