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