Skip to main content

submilli_engine/runtime/
json.rs

1//! Host side of the `JSON.parse` compiler intrinsic.
2//!
3//! Exposes a single host fn under `submilli:json.parse` that
4//! `serde_json`-parses the source string and returns the normal
5//! `(ref null $Object)` representation used by `unknown`.
6
7use crate::runtime::host::{abi_arg, abi_result};
8use wasmtime::{
9    ArrayRef, ArrayRefPre, ArrayType, AsContextMut, Caller, FuncType, HeapType, Linker, RefType,
10    Rooted, StructRef, StructRefPre, Val, ValType,
11};
12
13mod format;
14
15use crate::runtime::StoreData;
16use crate::runtime::fuel;
17use crate::runtime::host::{
18    host_array_vtable, host_boxed_boolean_vtable, host_boxed_number_vtable, host_object_vtable,
19    host_string_vtable, read_string_arg, register_host_fn, register_host_fn_async,
20};
21use crate::runtime::intrinsic_types::intrinsic_types;
22pub(crate) use crate::runtime::json_text::JsonValue;
23use crate::runtime::prelude::vtable::serialization::Output;
24use crate::{PackageDeclaration, Param, Span, Type, ValueKind, ValueSymbol};
25
26pub const JSON_MODULE_NAME: &str = "submilli:json";
27
28pub(super) fn install_json_module(
29    linker: &mut Linker<StoreData>,
30    string_type: &ArrayType,
31) -> wasmtime::Result<()> {
32    let engine = linker.engine().clone();
33    let raw_string_param = ValType::Ref(RefType::new(false, HeapType::from(string_type.clone())));
34
35    let object_type = crate::runtime::intrinsic_types::build_intrinsic_types(&engine)?.object;
36    let unknown_result = ValType::Ref(RefType::new(true, HeapType::from(object_type)));
37
38    let ty = FuncType::new(
39        &engine,
40        [raw_string_param.clone()],
41        [unknown_result.clone()],
42    );
43    register_host_fn(
44        linker,
45        JSON_MODULE_NAME,
46        crate::mangle::host(JSON_MODULE_NAME, "parse"),
47        ty,
48        /* deterministic = */ true,
49        move |caller, params, results| -> wasmtime::Result<()> {
50            let raw = match *abi_arg(params, 0)? {
51                Val::AnyRef(Some(raw)) => raw.unwrap_array(&mut *caller)?,
52                _ => {
53                    return Err(super::host::fatal_host_error(
54                        "json.parse: invalid string payload",
55                    ));
56                }
57            };
58            let text = super::host::read_code_units(&mut *caller, raw, "json.parse")?;
59            fuel::charge(&mut *caller, fuel::PARSE, text.len() as u64)?;
60            // Invalid JSON raises a catchable SyntaxError carrying the
61            // position/expectation detail, rather than trapping uncatchably.
62            // Returning an `Err` is enough: the `register_host_fn` wrapper turns
63            // it into a catchable exception (via `throw_error`).
64            let value = super::json_text::parse(&text)
65                .map_err(|e| json_parse_error(&format!("JSON.parse: {}", e.message()), &e))?;
66            let allocator = JsonUnknownAllocator::new(&mut *caller)?;
67            *abi_result(results, 0)? = allocator.allocate(&mut *caller, &value)?;
68            Ok(())
69        },
70    )?;
71
72    // Bare strings use the same UTF-16 escaper as structural values so lone
73    // surrogates survive both direct stringify and the main result encoder.
74    let escape_ty = FuncType::new(
75        &engine,
76        [raw_string_param.clone()],
77        [raw_string_param.clone()],
78    );
79    register_host_fn(
80        linker,
81        JSON_MODULE_NAME,
82        crate::mangle::host(JSON_MODULE_NAME, "stringify"),
83        escape_ty,
84        /* deterministic = */ true,
85        |caller, params, results| -> wasmtime::Result<()> {
86            let raw = match *abi_arg(params, 0)? {
87                Val::AnyRef(Some(raw)) => raw.unwrap_array(&mut *caller)?,
88                _ => {
89                    return Err(super::host::fatal_host_error(
90                        "JSON.stringify: invalid string payload",
91                    ));
92                }
93            };
94            let len = raw.len(&mut *caller)?;
95            let _input =
96                super::limits::HostBytes::new(&caller.data().tenant_limits, u64::from(len) * 2)?;
97            let units = super::host::read_code_units(&mut *caller, raw, "JSON.stringify")?;
98            let output = crate::runtime::prelude::vtable::serialization::quoted(caller, &units)?;
99            let raw = super::host::write_code_units(&mut *caller, output.units())?;
100            *abi_result(results, 0)? = Val::AnyRef(Some(raw.to_anyref()));
101            Ok(())
102        },
103    )?;
104
105    let pretty_number_ty = FuncType::new(
106        &engine,
107        [raw_string_param.clone(), ValType::F64],
108        [raw_string_param.clone()],
109    );
110    register_host_fn(
111        linker,
112        JSON_MODULE_NAME,
113        crate::mangle::host(JSON_MODULE_NAME, "stringifyPrettyNumber"),
114        pretty_number_ty,
115        /* deterministic = */ true,
116        |caller, params, results| -> wasmtime::Result<()> {
117            let spaces = match *abi_arg(params, 1)? {
118                Val::F64(bits) => stringify_space_from_number(f64::from_bits(bits)),
119                ref other => {
120                    return Err(super::host::fatal_host_error(format!(
121                        "json.stringifyPrettyNumber expects f64, got {other:?}"
122                    )));
123                }
124            };
125            let raw = format::pretty(
126                caller,
127                abi_arg(params, 0)?,
128                spaces
129                    .as_bytes()
130                    .iter()
131                    .map(|byte| u16::from(*byte))
132                    .collect::<Vec<_>>()
133                    .as_slice(),
134            )?;
135            *abi_result(results, 0)? = Val::AnyRef(Some(raw.to_anyref()));
136            Ok(())
137        },
138    )?;
139
140    let pretty_string_ty = FuncType::new(
141        &engine,
142        [raw_string_param.clone(), raw_string_param.clone()],
143        [raw_string_param.clone()],
144    );
145    register_host_fn(
146        linker,
147        JSON_MODULE_NAME,
148        crate::mangle::host(JSON_MODULE_NAME, "stringifyPrettyString"),
149        pretty_string_ty,
150        /* deterministic = */ true,
151        |caller, params, results| -> wasmtime::Result<()> {
152            let raw_indent = match *abi_arg(params, 1)? {
153                Val::AnyRef(Some(raw)) => raw.unwrap_array(&mut *caller)?,
154                _ => {
155                    return Err(super::host::fatal_host_error(
156                        "JSON.stringify: invalid indent payload",
157                    ));
158                }
159            };
160            let len = raw_indent.len(&mut *caller)?.min(10);
161            let indent =
162                super::host::read_code_units_range(&mut *caller, raw_indent, 0, len as usize)?;
163            let raw = format::pretty(caller, abi_arg(params, 0)?, &indent)?;
164            *abi_result(results, 0)? = Val::AnyRef(Some(raw.to_anyref()));
165            Ok(())
166        },
167    )?;
168
169    let object_ref = ValType::Ref(RefType::new(false, HeapType::Struct));
170    let stringify_typed_object_ty = FuncType::new(
171        &engine,
172        [raw_string_param.clone(), ValType::I32, object_ref],
173        [raw_string_param.clone()],
174    );
175    register_host_fn_async(
176        linker,
177        JSON_MODULE_NAME,
178        crate::mangle::host(JSON_MODULE_NAME, "stringifyTypedObject"),
179        stringify_typed_object_ty,
180        /* deterministic = */ true,
181        |mut caller, params, results| {
182            Box::pin(async move {
183                let package =
184                    read_string_arg(caller, abi_arg(params, 0)?, "json.stringifyTypedObject")?;
185                let type_id = crate::TypeInfoId(
186                    (*abi_arg(params, 1)?)
187                        .i32()
188                        .ok_or_else(|| wasmtime::Error::msg("json.stringifyTypedObject type id"))?
189                        as u32,
190                );
191                let value = match abi_arg(params, 2)? {
192                    Val::AnyRef(Some(any)) => any.unwrap_struct(&mut caller)?,
193                    Val::AnyRef(None) => {
194                        return Err(wasmtime::Error::msg(
195                            "json.stringifyTypedObject value is null",
196                        ));
197                    }
198                    other => {
199                        return Err(wasmtime::Error::msg(format!(
200                            "json.stringifyTypedObject expects object, got {other:?}"
201                        )));
202                    }
203                };
204                let intr = intrinsic_types(&mut *caller)?;
205                let mut remaining = crate::runtime::MAX_STRUCTURAL_WALK_NODES;
206                // The typed walk first; a dynamic object or any value that
207                // doesn't fit its TypeInfo falls back to the vtable walk.
208                let typed = if contains_dynamic_object(
209                    caller,
210                    abi_arg(params, 2)?,
211                    &intr,
212                    0,
213                    &mut remaining,
214                )? {
215                    None
216                } else {
217                    stringify_typed_object(caller, &package, type_id, value)?
218                };
219                let json = match typed {
220                    Some(json) => json,
221                    None => stringify_dynamic_object(caller, abi_arg(params, 2)?).await?,
222                };
223                let raw = super::host::write_code_units(&mut *caller, json.units())?;
224                *abi_result(results, 0)? = Val::AnyRef(Some(raw.to_anyref()));
225                Ok(())
226            })
227        },
228    )?;
229
230    Ok(())
231}
232
233/// Serializes through each value's own vtable rather than a static TypeInfo.
234async fn stringify_dynamic_object(
235    caller: &mut Caller<'_, StoreData>,
236    value: &Val,
237) -> wasmtime::Result<Output> {
238    let intr = intrinsic_types(&mut *caller)?;
239    let serialized = crate::runtime::prelude::vtable::object_to_json(
240        caller,
241        value,
242        &intr.raw_string,
243        &intr.string,
244    )
245    .await?;
246    let string = boxed_struct(caller, serialized, "JSON serializer result")?;
247    let raw = match string.field(&mut *caller, 1)? {
248        Val::AnyRef(Some(raw)) => raw.unwrap_array(&mut *caller)?,
249        _ => {
250            return Err(super::host::fatal_host_error(
251                "JSON serializer returned invalid string payload",
252            ));
253        }
254    };
255    let len = raw.len(&mut *caller)?;
256    let _input = super::limits::HostBytes::new(&caller.data().tenant_limits, u64::from(len) * 2)?;
257    let units = crate::runtime::prelude::vtable::read_string_units(
258        caller,
259        &serialized,
260        "JSON.stringify dynamic object",
261    )?;
262    let mut output = Output::new(caller);
263    output.append(caller, &units)?;
264    Ok(output)
265}
266
267/// TypeInfo describes a static view. A spread can retain fields and values
268/// outside that view, including inside a statically shaped parent or array.
269fn contains_dynamic_object(
270    caller: &mut Caller<'_, StoreData>,
271    value: &Val,
272    intr: &crate::runtime::intrinsic_types::IntrinsicTypes,
273    depth: u32,
274    remaining: &mut u32,
275) -> wasmtime::Result<bool> {
276    charge_json_visit(caller, remaining)?;
277    // Let the existing bounded vtable walker report cycles or excessive depth.
278    if depth >= crate::runtime::MAX_VTABLE_WALK_DEPTH {
279        return Ok(true);
280    }
281    let Val::AnyRef(Some(value)) = value else {
282        return Ok(false);
283    };
284    let Some(object) = value.as_struct(&mut *caller)? else {
285        return Ok(false);
286    };
287    let values = if object.matches_ty(&*caller, &intr.object_shape)? {
288        let names = match object.field(&mut *caller, 1)? {
289            Val::AnyRef(Some(names)) => names.unwrap_array(&mut *caller)?,
290            _ => {
291                return Err(wasmtime::Error::msg(
292                    "JSON.stringify: invalid field-name array",
293                ));
294            }
295        };
296        let name_count = crate::runtime::prelude::object::field_count(
297            caller,
298            &Val::AnyRef(Some(object.to_anyref())),
299        )?;
300        fuel::charge(&mut *caller, fuel::ELEM, u64::from(name_count))?;
301        for index in 0..name_count {
302            let name = names.get(&mut *caller, index)?;
303            if crate::runtime::prelude::object::field_was_inserted(caller, &name)? {
304                return Ok(true);
305            }
306        }
307        let actual = object.field(&mut *caller, 0)?;
308        let dynamic = host_object_vtable(caller)?;
309        if let (Val::AnyRef(Some(actual)), Val::AnyRef(Some(dynamic))) = (actual, dynamic)
310            && Rooted::ref_eq(&*caller, &actual, &dynamic)?
311        {
312            return Ok(true);
313        }
314        object.field(&mut *caller, 2)?
315    } else if object.matches_ty(&*caller, &intr.array)? {
316        object.field(&mut *caller, 1)?
317    } else {
318        return Ok(false);
319    };
320    let Val::AnyRef(Some(values)) = values else {
321        return Ok(false);
322    };
323    let values = values.unwrap_array(&mut *caller)?;
324    let len = if object.matches_ty(&*caller, &intr.array)? {
325        super::array_storage::ArrayStorage::from_struct(caller, object)?.len
326    } else {
327        values.len(&mut *caller)?
328    };
329    for index in 0..len {
330        let value = values.get(&mut *caller, index)?;
331        if contains_dynamic_object(caller, &value, intr, depth + 1, remaining)? {
332            return Ok(true);
333        }
334    }
335    Ok(false)
336}
337
338/// Preflight reads values before dispatching any serialization hooks, so its
339/// visits need their own fuel and node budget.
340fn charge_json_visit(
341    caller: &mut Caller<'_, StoreData>,
342    remaining: &mut u32,
343) -> wasmtime::Result<()> {
344    *remaining = remaining.checked_sub(1).ok_or_else(|| {
345        super::host::range_error(format!(
346            "JSON.stringify exceeds {} structural visits; serialize a smaller value or reduce shared nesting",
347            crate::runtime::MAX_STRUCTURAL_WALK_NODES,
348        ))
349    })?;
350    fuel::charge(caller, fuel::ELEM, 1)
351}
352
353/// Whether a value fit the static view its TypeInfo describes. A value can be
354/// wider than that view: the shape of `{ v: null }` types its field `null`, yet
355/// a `{ v: number | null }` binding holding it may later store a number there.
356/// A mismatch abandons the typed walk for the dynamic serializer.
357#[derive(Clone, Copy, PartialEq, Eq)]
358enum Fit {
359    Matched,
360    Mismatch,
361}
362
363/// The typed serialization of `value`, or `None` when some value it reaches
364/// doesn't fit its TypeInfo.
365fn stringify_typed_object(
366    caller: &mut Caller<'_, StoreData>,
367    package: &str,
368    type_id: crate::TypeInfoId,
369    value: Rooted<StructRef>,
370) -> wasmtime::Result<Option<Output>> {
371    let (kind, _metadata) = read_type_kind(caller, package, type_id)?;
372    let crate::TypeInfoKind::Object { fields } = kind else {
373        return Err(super::host::fatal_host_error(
374            "JSON.stringifyTypedObject type is not an object",
375        ));
376    };
377    let mut remaining = crate::runtime::MAX_STRUCTURAL_WALK_NODES;
378    charge_json_visit(caller, &mut remaining)?;
379    let mut output = Output::new(caller);
380    let fit = stringify_typed_object_value(
381        caller,
382        package,
383        fields,
384        value,
385        &mut remaining,
386        0,
387        &mut output,
388    )?;
389    Ok((fit == Fit::Matched).then_some(output))
390}
391
392fn stringify_typed_object_value(
393    caller: &mut Caller<'_, StoreData>,
394    package: &str,
395    fields: Vec<crate::FieldInfo>,
396    value: Rooted<StructRef>,
397    remaining: &mut u32,
398    depth: u32,
399    output: &mut Output,
400) -> wasmtime::Result<Fit> {
401    let receiver = Val::AnyRef(Some(value.to_anyref()));
402    let intr = intrinsic_types(&mut *caller)?;
403    // The top-level object skips `value_fits_kind`, so check its shape here.
404    if !value.matches_ty(&*caller, &intr.object_shape)? {
405        return Ok(Fit::Mismatch);
406    }
407    output.append(caller, &[123])?;
408    let mut written = 0usize;
409    let mut listed = 0usize;
410    // TypeInfo object fields inherit the compiler's BTreeMap name order.
411    for field in fields {
412        let Some(raw) = crate::runtime::prelude::collection::object_field_present(
413            caller,
414            &receiver,
415            &field.name,
416        )?
417        else {
418            if field.optional {
419                continue;
420            }
421            return Ok(Fit::Mismatch);
422        };
423        listed = listed.saturating_add(1);
424        // JSON omits an undefined property; it still counts as listed.
425        if super::prelude::undefined::is_undefined(caller, &raw)? {
426            continue;
427        }
428        if written != 0 {
429            output.append(caller, &[44])?;
430        }
431        written = written.saturating_add(1);
432        append_quoted_text(caller, output, &field.name)?;
433        output.append(caller, &[58])?;
434        if matches!(raw, Val::AnyRef(None)) {
435            output.append(caller, &[110, 117, 108, 108])?;
436            continue;
437        }
438        let fit = stringify_type_info_val(
439            caller,
440            package,
441            field.type_id,
442            raw,
443            remaining,
444            depth + 1,
445            output,
446        )?;
447        if fit == Fit::Mismatch {
448            return Ok(Fit::Mismatch);
449        }
450    }
451    // An object stored through a narrower field type can carry fields this
452    // TypeInfo doesn't list; fall back so they are still serialized.
453    if crate::runtime::prelude::object::data_field_count(caller, &receiver)? > listed {
454        return Ok(Fit::Mismatch);
455    }
456    output.append(caller, &[125])?;
457    Ok(Fit::Matched)
458}
459
460fn stringify_type_info_val(
461    caller: &mut Caller<'_, StoreData>,
462    package: &str,
463    type_id: crate::TypeInfoId,
464    val: Val,
465    remaining: &mut u32,
466    depth: u32,
467    output: &mut Output,
468) -> wasmtime::Result<Fit> {
469    if depth >= crate::runtime::MAX_VTABLE_WALK_DEPTH {
470        return Err(super::host::range_error(
471            "JSON.stringify type traversal exceeds 128 levels; serialize a less deeply nested value",
472        ));
473    }
474    charge_json_visit(caller, remaining)?;
475    // Object properties are filtered by their owner. Array elements containing
476    // undefined have JSON's null representation, independently of static type.
477    if super::prelude::undefined::is_undefined(caller, &val)? {
478        output.append(caller, &[110, 117, 108, 108])?;
479        return Ok(Fit::Matched);
480    }
481    let (kind, _metadata) = read_type_kind(caller, package, type_id)?;
482    if !value_fits_kind(caller, &kind, &val)? {
483        return Ok(Fit::Mismatch);
484    }
485    match kind {
486        crate::TypeInfoKind::Null | crate::TypeInfoKind::Undefined => {
487            output.append(caller, &[110, 117, 108, 108])?;
488            Ok(Fit::Matched)
489        }
490        crate::TypeInfoKind::Boolean | crate::TypeInfoKind::BooleanLiteral(_) => {
491            let value = boxed_bool(caller, val)?;
492            output.append(
493                caller,
494                if value {
495                    &[116, 114, 117, 101]
496                } else {
497                    &[102, 97, 108, 115, 101]
498                },
499            )?;
500            Ok(Fit::Matched)
501        }
502        crate::TypeInfoKind::Number | crate::TypeInfoKind::NumberLiteral(_) => {
503            let n = boxed_number(caller, val)?;
504            if n.is_finite() {
505                append_text(caller, output, &json_number(n)?.to_string())?;
506            } else {
507                output.append(caller, &[110, 117, 108, 108])?;
508            }
509            Ok(Fit::Matched)
510        }
511        crate::TypeInfoKind::String | crate::TypeInfoKind::StringLiteral(_) => {
512            let string = boxed_struct(caller, val, "string")?;
513            let raw = match string.field(&mut *caller, 1)? {
514                Val::AnyRef(Some(raw)) => raw.unwrap_array(&mut *caller)?,
515                _ => {
516                    return Err(super::host::fatal_host_error(
517                        "JSON.stringify: malformed boxed string",
518                    ));
519                }
520            };
521            let count = raw.len(&mut *caller)?;
522            let _input =
523                super::limits::HostBytes::new(&caller.data().tenant_limits, u64::from(count) * 2)?;
524            let units = super::host::read_code_units(&mut *caller, raw, "JSON.stringify string")?;
525            output.append_escaped(caller, &units)?;
526            Ok(Fit::Matched)
527        }
528        crate::TypeInfoKind::Array { element } => {
529            stringify_array(caller, package, element, val, remaining, depth, output)
530        }
531        crate::TypeInfoKind::Tuple { elements, optional } => stringify_tuple(
532            caller,
533            package,
534            TupleSchema {
535                elements: &elements,
536                optional,
537            },
538            val,
539            remaining,
540            depth,
541            output,
542        ),
543        crate::TypeInfoKind::Object { fields } => {
544            let object = boxed_struct(caller, val, "object")?;
545            stringify_typed_object_value(caller, package, fields, object, remaining, depth, output)
546        }
547        crate::TypeInfoKind::Union { members }
548            if members.iter().any(|id| {
549                matches!(
550                    caller
551                        .data()
552                        .type_info
553                        .get(package)
554                        .and_then(|table| table.get(*id))
555                        .map(|info| &info.kind),
556                    Some(crate::TypeInfoKind::Null | crate::TypeInfoKind::Undefined)
557                )
558            }) =>
559        {
560            if matches!(val, Val::AnyRef(None)) {
561                output.append(caller, &[110, 117, 108, 108])?;
562                return Ok(Fit::Matched);
563            }
564            let non_null = members
565                .iter()
566                .find(|id| {
567                    !matches!(
568                        caller
569                            .data()
570                            .type_info
571                            .get(package)
572                            .and_then(|table| table.get(**id))
573                            .map(|info| &info.kind),
574                        Some(crate::TypeInfoKind::Null | crate::TypeInfoKind::Undefined)
575                    )
576                })
577                .ok_or_else(|| wasmtime::Error::msg("JSON.stringify: empty nullable union"))?;
578            stringify_type_info_val(
579                caller,
580                package,
581                *non_null,
582                val,
583                remaining,
584                depth + 1,
585                output,
586            )
587        }
588        other => Err(wasmtime::Error::msg(format!(
589            "JSON.stringify: host TypeInfo stringify for `{other:?}` is not implemented yet"
590        ))),
591    }
592}
593
594/// Whether `val` has the runtime representation `kind` describes. A nullable
595/// union defers to its member.
596fn value_fits_kind(
597    caller: &mut Caller<'_, StoreData>,
598    kind: &crate::TypeInfoKind,
599    val: &Val,
600) -> wasmtime::Result<bool> {
601    let intr = intrinsic_types(&mut *caller)?;
602    let expected = match kind {
603        crate::TypeInfoKind::Null => return Ok(matches!(val, Val::AnyRef(None))),
604        crate::TypeInfoKind::Undefined => {
605            return super::prelude::undefined::is_undefined(caller, val);
606        }
607        crate::TypeInfoKind::Boolean | crate::TypeInfoKind::BooleanLiteral(_) => {
608            &intr.boxed_boolean
609        }
610        crate::TypeInfoKind::Number | crate::TypeInfoKind::NumberLiteral(_) => &intr.boxed_number,
611        crate::TypeInfoKind::String | crate::TypeInfoKind::StringLiteral(_) => &intr.string,
612        crate::TypeInfoKind::Array { .. } | crate::TypeInfoKind::Tuple { .. } => &intr.array,
613        crate::TypeInfoKind::Object { .. } => &intr.object_shape,
614        _ => return Ok(true),
615    };
616    is_struct_of(caller, val, expected)
617}
618
619fn is_struct_of(
620    caller: &mut Caller<'_, StoreData>,
621    val: &Val,
622    ty: &wasmtime::StructType,
623) -> wasmtime::Result<bool> {
624    let Val::AnyRef(Some(any)) = val else {
625        return Ok(false);
626    };
627    match any.as_struct(&mut *caller)? {
628        Some(object) => object.matches_ty(&*caller, ty),
629        None => Ok(false),
630    }
631}
632
633fn stringify_array(
634    caller: &mut Caller<'_, StoreData>,
635    package: &str,
636    element: crate::TypeInfoId,
637    val: Val,
638    remaining: &mut u32,
639    depth: u32,
640    output: &mut Output,
641) -> wasmtime::Result<Fit> {
642    let storage = super::array_storage::ArrayStorage::read(caller, &val)?;
643    let raw = storage.backing;
644    let len = storage.len;
645    output.append(caller, &[91])?;
646    for index in 0..len {
647        let elem = raw.get(&mut *caller, index)?;
648        if index != 0 {
649            output.append(caller, &[44])?;
650        }
651        let fit =
652            stringify_type_info_val(caller, package, element, elem, remaining, depth + 1, output)?;
653        if fit == Fit::Mismatch {
654            return Ok(Fit::Mismatch);
655        }
656    }
657    output.append(caller, &[93])?;
658    Ok(Fit::Matched)
659}
660
661struct TupleSchema<'a> {
662    elements: &'a [crate::TypeInfoId],
663    optional: usize,
664}
665
666fn stringify_tuple(
667    caller: &mut Caller<'_, StoreData>,
668    package: &str,
669    schema: TupleSchema<'_>,
670    val: Val,
671    remaining: &mut u32,
672    depth: u32,
673    output: &mut Output,
674) -> wasmtime::Result<Fit> {
675    let TupleSchema { elements, optional } = schema;
676    let storage = super::array_storage::ArrayStorage::read(caller, &val)?;
677    let raw = storage.backing;
678    let len = storage.len;
679    let minimum = elements.len().checked_sub(optional).ok_or_else(|| {
680        super::host::fatal_host_error("JSON.stringify: invalid tuple optional count")
681    })?;
682    if !(minimum..=elements.len()).contains(&(len as usize)) {
683        return Ok(Fit::Mismatch);
684    }
685    output.append(caller, &[91])?;
686    for index in 0..len {
687        let elem = raw.get(&mut *caller, index)?;
688        let element = elements.get(index as usize).copied().ok_or_else(|| {
689            super::host::fatal_host_error("JSON.stringify: tuple element metadata missing")
690        })?;
691        if index != 0 {
692            output.append(caller, &[44])?;
693        }
694        let fit =
695            stringify_type_info_val(caller, package, element, elem, remaining, depth + 1, output)?;
696        if fit == Fit::Mismatch {
697            return Ok(Fit::Mismatch);
698        }
699    }
700    output.append(caller, &[93])?;
701    Ok(Fit::Matched)
702}
703
704/// Copy only the schema data used by this walk, admitting native storage before
705/// allocation. Literal values are irrelevant to serialization of their payload.
706fn read_type_kind(
707    caller: &mut Caller<'_, StoreData>,
708    package: &str,
709    type_id: crate::TypeInfoId,
710) -> wasmtime::Result<(crate::TypeInfoKind, super::limits::HostBytes)> {
711    let info = caller
712        .data()
713        .type_info
714        .get(package)
715        .and_then(|table| table.get(type_id))
716        .ok_or_else(|| {
717            super::host::fatal_host_error(format!(
718                "JSON.stringify: missing TypeInfo for {package}#{}",
719                type_id.as_u32()
720            ))
721        })?;
722    let entries = match &info.kind {
723        crate::TypeInfoKind::Object { fields } => fields.len(),
724        crate::TypeInfoKind::Tuple { elements, .. } => elements.len(),
725        crate::TypeInfoKind::Union { members } => members.len(),
726        _ => 0,
727    };
728    fuel::charge(&mut *caller, fuel::ELEM, (entries as u64).saturating_mul(2))?;
729    let info = caller
730        .data()
731        .type_info
732        .get(package)
733        .and_then(|table| table.get(type_id))
734        .ok_or_else(|| super::host::fatal_host_error("JSON schema disappeared"))?;
735    let (_, bytes) = match &info.kind {
736        crate::TypeInfoKind::Object { fields } => {
737            let bytes = fields
738                .iter()
739                .try_fold(0u64, |total, field| {
740                    total
741                        .checked_add(std::mem::size_of::<crate::FieldInfo>() as u64)
742                        .and_then(|n| n.checked_add(field.name.len() as u64))
743                })
744                .ok_or_else(|| super::host::fatal_host_error("JSON schema size overflow"))?;
745            (fields.len(), bytes)
746        }
747        crate::TypeInfoKind::Tuple { elements, .. } => (
748            elements.len(),
749            (elements.len() as u64) * std::mem::size_of::<crate::TypeInfoId>() as u64,
750        ),
751        crate::TypeInfoKind::Union { members } => (
752            members.len(),
753            (members.len() as u64) * std::mem::size_of::<crate::TypeInfoId>() as u64,
754        ),
755        _ => (0, 0),
756    };
757    let metadata = super::limits::HostBytes::new(&caller.data().tenant_limits, bytes)?;
758    // The sizing pass includes each record and its name, so deriving the
759    // copied name bytes avoids another schema walk.
760    let name_bytes = if matches!(&info.kind, crate::TypeInfoKind::Object { .. }) {
761        let records = (entries as u64)
762            .checked_mul(std::mem::size_of::<crate::FieldInfo>() as u64)
763            .ok_or_else(|| super::host::fatal_host_error("JSON schema record size overflow"))?;
764        bytes
765            .checked_sub(records)
766            .ok_or_else(|| super::host::fatal_host_error("JSON schema name size is invalid"))?
767    } else {
768        0
769    };
770    fuel::charge(&mut *caller, fuel::COPY, name_bytes)?;
771    let info = caller
772        .data()
773        .type_info
774        .get(package)
775        .and_then(|table| table.get(type_id))
776        .ok_or_else(|| super::host::fatal_host_error("JSON schema disappeared"))?;
777    let kind = match &info.kind {
778        crate::TypeInfoKind::Null => crate::TypeInfoKind::Null,
779        crate::TypeInfoKind::Undefined => crate::TypeInfoKind::Undefined,
780        crate::TypeInfoKind::Boolean | crate::TypeInfoKind::BooleanLiteral(_) => {
781            crate::TypeInfoKind::Boolean
782        }
783        crate::TypeInfoKind::Number | crate::TypeInfoKind::NumberLiteral(_) => {
784            crate::TypeInfoKind::Number
785        }
786        crate::TypeInfoKind::String | crate::TypeInfoKind::StringLiteral(_) => {
787            crate::TypeInfoKind::String
788        }
789        crate::TypeInfoKind::Array { element } => crate::TypeInfoKind::Array { element: *element },
790        crate::TypeInfoKind::Object { fields } => {
791            let mut copied = Vec::new();
792            copied
793                .try_reserve_exact(fields.len())
794                .map_err(super::host::fatal_host_error)?;
795            for field in fields {
796                let mut name = String::new();
797                name.try_reserve_exact(field.name.len())
798                    .map_err(super::host::fatal_host_error)?;
799                name.push_str(&field.name);
800                copied.push(crate::FieldInfo {
801                    name,
802                    type_id: field.type_id,
803                    optional: field.optional,
804                });
805            }
806            crate::TypeInfoKind::Object { fields: copied }
807        }
808        crate::TypeInfoKind::Tuple { elements, optional } => crate::TypeInfoKind::Tuple {
809            elements: copy_type_ids(elements)?,
810            optional: *optional,
811        },
812        crate::TypeInfoKind::Union { members } => crate::TypeInfoKind::Union {
813            members: copy_type_ids(members)?,
814        },
815        other => {
816            return Err(wasmtime::Error::msg(format!(
817                "JSON.stringify: host TypeInfo stringify for `{other:?}` is not implemented yet"
818            )));
819        }
820    };
821    Ok((kind, metadata))
822}
823
824fn copy_type_ids(ids: &[crate::TypeInfoId]) -> wasmtime::Result<Vec<crate::TypeInfoId>> {
825    let mut copied = Vec::new();
826    copied
827        .try_reserve_exact(ids.len())
828        .map_err(super::host::fatal_host_error)?;
829    copied.extend_from_slice(ids);
830    Ok(copied)
831}
832
833fn append_text(
834    caller: &mut Caller<'_, StoreData>,
835    output: &mut Output,
836    text: &str,
837) -> wasmtime::Result<()> {
838    let _input =
839        super::limits::HostBytes::new(&caller.data().tenant_limits, (text.len() as u64) * 2)?;
840    let units = text_units(caller, text)?;
841    output.append(caller, &units)
842}
843
844fn append_quoted_text(
845    caller: &mut Caller<'_, StoreData>,
846    output: &mut Output,
847    text: &str,
848) -> wasmtime::Result<()> {
849    let _input =
850        super::limits::HostBytes::new(&caller.data().tenant_limits, (text.len() as u64) * 2)?;
851    let units = text_units(caller, text)?;
852    output.append_escaped(caller, &units)
853}
854
855fn text_units(caller: &mut Caller<'_, StoreData>, text: &str) -> wasmtime::Result<Vec<u16>> {
856    fuel::charge(&mut *caller, fuel::SCAN, text.len() as u64)?;
857    let mut units = Vec::new();
858    units
859        .try_reserve_exact(text.len())
860        .map_err(super::host::fatal_host_error)?;
861    units.extend(text.encode_utf16());
862    Ok(units)
863}
864
865fn json_number(n: f64) -> wasmtime::Result<serde_json::Number> {
866    if !n.is_finite() {
867        return Err(wasmtime::Error::msg("JSON.stringify: non-finite number"));
868    }
869    if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 {
870        return Ok(serde_json::Number::from(n as i64));
871    }
872    serde_json::Number::from_f64(n)
873        .ok_or_else(|| wasmtime::Error::msg("JSON.stringify: non-finite number"))
874}
875
876/// Parse `text` as JSON and materialize it as an `unknown`-shaped guest value,
877/// the same construction `JSON.parse` performs.
878///
879/// Shared so a host package that hands back model-authored JSON — `llm.call<T>`,
880/// whose typed lowering then verifies the result structurally — builds the same
881/// object representation a program would get from `JSON.parse`, rather than a
882/// second, subtly different one. `context` prefixes the error so a malformed
883/// response is attributed to the call that produced it, not to `JSON.parse`.
884pub(crate) fn parse_json_as_unknown(
885    caller: &mut wasmtime::Caller<'_, StoreData>,
886    text: &str,
887    context: &str,
888) -> wasmtime::Result<Val> {
889    fuel::charge(&mut *caller, fuel::PARSE, text.len() as u64)?;
890    let units: Vec<u16> = text.encode_utf16().collect();
891    let value = super::json_text::parse(&units)
892        .map_err(|e| json_parse_error(&format!("{context}: {}", e.message()), &e))?;
893    let allocator = JsonUnknownAllocator::new(&mut *caller)?;
894    allocator.allocate(&mut *caller, &value)
895}
896
897/// The language error for a refused document: a number literal past the `f64`
898/// range is a `RangeError`, anything else a `SyntaxError`.
899fn json_parse_error(message: &str, error: &super::json_text::JsonError) -> wasmtime::Error {
900    match error {
901        super::json_text::JsonError::Range(_) => crate::runtime::host::range_error(message),
902        super::json_text::JsonError::Syntax(_) => crate::runtime::host::syntax_error(message),
903    }
904}
905
906pub(crate) fn boxed_struct(
907    ctx: &mut impl AsContextMut<Data = StoreData>,
908    val: Val,
909    name: &str,
910) -> wasmtime::Result<Rooted<StructRef>> {
911    match val {
912        Val::AnyRef(Some(any)) => any.unwrap_struct(&mut *ctx),
913        Val::AnyRef(None) => Err(wasmtime::Error::msg(format!(
914            "JSON.stringify: {name} is null"
915        ))),
916        other => Err(wasmtime::Error::msg(format!(
917            "JSON.stringify: expected boxed {name}, got {other:?}"
918        ))),
919    }
920}
921
922pub(crate) fn boxed_bool(
923    ctx: &mut impl AsContextMut<Data = StoreData>,
924    val: Val,
925) -> wasmtime::Result<bool> {
926    let s = boxed_struct(ctx, val, "boolean")?;
927    Ok(s.field(&mut *ctx, 1)?
928        .i32()
929        .ok_or_else(|| crate::runtime::host::invariant_trap("JSON ABI: expected boolean field"))?
930        != 0)
931}
932
933pub(crate) fn boxed_number(
934    ctx: &mut impl AsContextMut<Data = StoreData>,
935    val: Val,
936) -> wasmtime::Result<f64> {
937    let s = boxed_struct(ctx, val, "number")?;
938    let bits = s
939        .field(&mut *ctx, 1)?
940        .f64()
941        .ok_or_else(|| wasmtime::Error::msg("JSON.stringify: boxed number field"))?;
942    Ok(bits)
943}
944
945pub(crate) fn boxed_string(
946    ctx: &mut impl AsContextMut<Data = StoreData>,
947    val: Val,
948) -> wasmtime::Result<String> {
949    let s = boxed_struct(ctx, val, "string")?;
950    let raw = s.field(&mut *ctx, 1)?;
951    let arr = match raw {
952        Val::AnyRef(Some(any)) => any.unwrap_array(&mut *ctx)?,
953        other => {
954            return Err(wasmtime::Error::msg(format!(
955                "JSON.stringify: boxed string raw field got {other:?}"
956            )));
957        }
958    };
959    crate::runtime::read_submilli_string(&mut *ctx, arr)
960}
961
962fn stringify_space_from_number(space: f64) -> String {
963    if space.is_nan() || space <= 0.0 {
964        return String::new();
965    }
966    " ".repeat((space.floor() as usize).min(10))
967}
968
969pub(crate) struct JsonUnknownAllocator {
970    string_pre: StructRefPre,
971    boxed_number_pre: StructRefPre,
972    boxed_boolean_pre: StructRefPre,
973    array_pre: StructRefPre,
974    raw_array_pre: ArrayRefPre,
975    object_shape_pre: StructRefPre,
976    field_names_pre: ArrayRefPre,
977    object_fields_pre: ArrayRefPre,
978    string_vtable: Val,
979    boxed_number_vtable: Val,
980    boxed_boolean_vtable: Val,
981    array_vtable: Val,
982    object_vtable: Val,
983}
984
985impl JsonUnknownAllocator {
986    pub(crate) fn new(ctx: &mut impl AsContextMut<Data = StoreData>) -> wasmtime::Result<Self> {
987        let (
988            string_type,
989            boxed_number_type,
990            boxed_boolean_type,
991            array_type,
992            raw_array_type,
993            object_shape_type,
994            field_names_type,
995            object_fields_type,
996        ) = {
997            let abi =
998                ctx.as_context().data().host_abi.as_ref().ok_or_else(|| {
999                    wasmtime::Error::msg("host_abi unset (prelude not instantiated)")
1000                })?;
1001            (
1002                abi.string_type.clone(),
1003                abi.boxed_number_type.clone(),
1004                abi.boxed_boolean_type.clone(),
1005                abi.array_type.clone(),
1006                abi.raw_array_type.clone(),
1007                abi.object_shape_type.clone(),
1008                abi.field_names_type.clone(),
1009                abi.object_fields_type.clone(),
1010            )
1011        };
1012        let string_vtable = host_string_vtable(ctx)?;
1013        let boxed_number_vtable = host_boxed_number_vtable(ctx)?;
1014        let boxed_boolean_vtable = host_boxed_boolean_vtable(ctx)?;
1015        let array_vtable = host_array_vtable(ctx)?;
1016        let object_vtable = host_object_vtable(ctx)?;
1017
1018        Ok(Self {
1019            string_pre: StructRefPre::new(&mut *ctx, string_type),
1020            boxed_number_pre: StructRefPre::new(&mut *ctx, boxed_number_type),
1021            boxed_boolean_pre: StructRefPre::new(&mut *ctx, boxed_boolean_type),
1022            array_pre: StructRefPre::new(&mut *ctx, array_type),
1023            raw_array_pre: ArrayRefPre::new(&mut *ctx, raw_array_type),
1024            object_shape_pre: StructRefPre::new(&mut *ctx, object_shape_type),
1025            field_names_pre: ArrayRefPre::new(&mut *ctx, field_names_type),
1026            object_fields_pre: ArrayRefPre::new(&mut *ctx, object_fields_type),
1027            string_vtable,
1028            boxed_number_vtable,
1029            boxed_boolean_vtable,
1030            array_vtable,
1031            object_vtable,
1032        })
1033    }
1034
1035    pub(crate) fn allocate(
1036        &self,
1037        ctx: &mut impl AsContextMut<Data = StoreData>,
1038        value: &JsonValue,
1039    ) -> wasmtime::Result<Val> {
1040        // One GC value per node; strings and arrays charge their own copies.
1041        fuel::charge(&mut *ctx, fuel::ELEM, 1)?;
1042        match value {
1043            JsonValue::Null => Ok(Val::AnyRef(None)),
1044            JsonValue::Bool(b) => {
1045                let object = StructRef::new(
1046                    &mut *ctx,
1047                    &self.boxed_boolean_pre,
1048                    &[self.boxed_boolean_vtable, Val::I32(i32::from(*b))],
1049                )?;
1050                Ok(Val::AnyRef(Some(object.to_anyref())))
1051            }
1052            JsonValue::Number(f) => {
1053                let object = StructRef::new(
1054                    &mut *ctx,
1055                    &self.boxed_number_pre,
1056                    &[self.boxed_number_vtable, Val::F64(f.to_bits())],
1057                )?;
1058                Ok(Val::AnyRef(Some(object.to_anyref())))
1059            }
1060            JsonValue::String(units) => {
1061                let raw = super::host::write_code_units(&mut *ctx, units)?;
1062                let object = StructRef::new(
1063                    &mut *ctx,
1064                    &self.string_pre,
1065                    &[
1066                        self.string_vtable,
1067                        Val::AnyRef(Some(raw.to_anyref())),
1068                        Val::I64(0),
1069                    ],
1070                )?;
1071                Ok(Val::AnyRef(Some(object.to_anyref())))
1072            }
1073            JsonValue::Array(items) => {
1074                let mut elements = Vec::new();
1075                elements
1076                    .try_reserve_exact(items.len())
1077                    .map_err(crate::runtime::host::fatal_host_error)?;
1078                for item in items {
1079                    elements.push(self.allocate(&mut *ctx, item)?);
1080                }
1081                let raw = ArrayRef::new_fixed(&mut *ctx, &self.raw_array_pre, &elements)?;
1082                let object = StructRef::new(
1083                    &mut *ctx,
1084                    &self.array_pre,
1085                    &[
1086                        self.array_vtable,
1087                        Val::AnyRef(Some(raw.to_anyref())),
1088                        Val::I32(super::array_storage::checked_length(elements.len())? as i32),
1089                    ],
1090                )?;
1091                Ok(Val::AnyRef(Some(object.to_anyref())))
1092            }
1093            JsonValue::Object(map) => {
1094                let mut names = Vec::new();
1095                let mut values = Vec::new();
1096                names
1097                    .try_reserve_exact(map.len())
1098                    .map_err(crate::runtime::host::fatal_host_error)?;
1099                values
1100                    .try_reserve_exact(map.len())
1101                    .map_err(crate::runtime::host::fatal_host_error)?;
1102                for (name, item) in map {
1103                    let raw_name = super::host::write_code_units(&mut *ctx, &name.0)?;
1104                    let name_object = StructRef::new(
1105                        &mut *ctx,
1106                        &self.string_pre,
1107                        &[
1108                            self.string_vtable,
1109                            Val::AnyRef(Some(raw_name.to_anyref())),
1110                            Val::I64(0),
1111                        ],
1112                    )?;
1113                    names.push(Val::AnyRef(Some(name_object.to_anyref())));
1114                    values.push(self.allocate(&mut *ctx, item)?);
1115                }
1116                let field_names = ArrayRef::new_fixed(&mut *ctx, &self.field_names_pre, &names)?;
1117                let object_fields =
1118                    ArrayRef::new_fixed(&mut *ctx, &self.object_fields_pre, &values)?;
1119                let object = StructRef::new(
1120                    &mut *ctx,
1121                    &self.object_shape_pre,
1122                    &[
1123                        self.object_vtable,
1124                        Val::AnyRef(Some(field_names.to_anyref())),
1125                        Val::AnyRef(Some(object_fields.to_anyref())),
1126                        Val::AnyRef(None),
1127                    ],
1128                )?;
1129                Ok(Val::AnyRef(Some(object.to_anyref())))
1130            }
1131        }
1132    }
1133}
1134
1135pub(super) fn json_module_definitions() -> PackageDeclaration {
1136    let mut defs = PackageDeclaration::with_package(JSON_MODULE_NAME);
1137    insert_json_host_fn(&mut defs, "parse", vec![Param::new("text", Type::String)]);
1138    insert_json_host_fn(
1139        &mut defs,
1140        "stringify",
1141        vec![Param::new("value", Type::String)],
1142    );
1143    insert_json_host_fn(
1144        &mut defs,
1145        "stringifyPrettyNumber",
1146        vec![
1147            Param::new("json", Type::String),
1148            Param::new("space", Type::Number),
1149        ],
1150    );
1151    insert_json_host_fn(
1152        &mut defs,
1153        "stringifyPrettyString",
1154        vec![
1155            Param::new("json", Type::String),
1156            Param::new("space", Type::String),
1157        ],
1158    );
1159    insert_json_host_fn(
1160        &mut defs,
1161        "stringifyTypedObject",
1162        vec![
1163            Param::new("package", Type::String),
1164            Param::new("typeId", Type::Number),
1165            Param::new("value", Type::Unknown),
1166        ],
1167    );
1168    defs
1169}
1170
1171fn insert_json_host_fn(defs: &mut PackageDeclaration, name: &str, params: Vec<Param>) {
1172    defs.values.insert(
1173        name.to_string(),
1174        ValueSymbol {
1175            name: name.to_string(),
1176            mangled_name: crate::mangle::host(JSON_MODULE_NAME, name),
1177            declaration_span: Span::at(crate::FileId::JSON),
1178            kind: ValueKind::Function {
1179                generics: Vec::new(),
1180                params,
1181                ret: Type::Error,
1182                type_predicate: None,
1183                doc: None,
1184            },
1185        },
1186    );
1187}