Skip to main content

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.