Skip to main content

submilli_engine/runtime/
host.rs

1//! Host-function and prelude registration for the Submilli runtime.
2
3use std::future::Future;
4use std::pin::Pin;
5
6use wasmtime::{
7    AnyRef, ArrayRef, ArrayRefPre, ArrayType, AsContext, AsContextMut, Caller, Engine, FieldType,
8    FuncType, Global, HeapType, Linker, Mutability, RefType, Rooted, StorageType, Store, StructRef,
9    StructRefPre, StructType, Val, ValType,
10};
11
12use crate::runtime::fuel::{self, charge_host_fuel};
13use crate::runtime::intrinsic_types::{build_intrinsic_types, intrinsic_types};
14pub(crate) use crate::runtime::intrinsic_types::{
15    intrinsic_array_type, intrinsic_bigint_type, intrinsic_string_type, intrinsic_uint8_array_type,
16};
17use crate::runtime::number::{parse_float_js, parse_int_with_radix, string_to_number_js};
18use crate::runtime::{StoreData, read_submilli_string};
19use crate::stdlib::Stdlib;
20use crate::{PackageDeclaration, Param, Span, Type, ValueKind, ValueSymbol};
21
22pub const NUMBER_MODULE_NAME: &str = "submilli:number";
23
24/// Not user-importable; `__` prefix keeps it out of any stdlib namespace.
25pub const INTERNAL_MODULE_NAME: &str = "__submilli_internal";
26
27/// Install the full runtime: the store-less host fns (prelude + stdlib + MCP)
28/// followed by the store-bound prelude state. There are no runtime Wasm
29/// modules — every built-in surface is Rust host fns resolved straight from
30/// the linker.
31pub async fn install_async(
32    linker: &mut Linker<StoreData>,
33    store: &mut Store<StoreData>,
34) -> wasmtime::Result<()> {
35    install_async_for(linker, store, Stdlib::core()).await
36}
37
38/// [`install_async`] for an embedder that offers `stdlib`.
39pub async fn install_async_for(
40    linker: &mut Linker<StoreData>,
41    store: &mut Store<StoreData>,
42    stdlib: Stdlib,
43) -> wasmtime::Result<()> {
44    install_host_functions_for(linker, stdlib)?;
45    install_store_bound(linker, store)
46}
47
48pub fn install_host_functions(linker: &mut Linker<StoreData>) -> wasmtime::Result<()> {
49    install_host_functions_for(linker, Stdlib::core())
50}
51
52/// [`install_host_functions`] for an embedder that offers `stdlib`.
53pub fn install_host_functions_for(
54    linker: &mut Linker<StoreData>,
55    stdlib: Stdlib,
56) -> wasmtime::Result<()> {
57    install_core_host_functions(linker)?;
58    stdlib.install_host_functions(linker)?;
59    super::mcp::install_mcp_async(linker)?;
60    super::tools::install_tools_async(linker)?;
61    Ok(())
62}
63
64/// The store-bound half of [`install_async`], for embedders that keep a
65/// reusable base linker of host fns and only need the per-store state.
66pub fn install_store_bound(
67    linker: &mut Linker<StoreData>,
68    store: &mut Store<StoreData>,
69) -> wasmtime::Result<()> {
70    install_prelude(linker, store)?;
71    crate::stdlib::install_store_bound(linker, store)
72}
73
74/// Install the store-bound prelude state: the host-owned vtable globals, the
75/// error tag/vtable, the Temporal ABI, and the cached [`HostAbi`] handles.
76/// There is no prelude Wasm module — the prelude surface is Rust host fns.
77fn install_prelude(
78    linker: &mut Linker<StoreData>,
79    store: &mut Store<StoreData>,
80) -> wasmtime::Result<()> {
81    let (vtables, error_host) = super::prelude::install_vtables(linker, store)?;
82    let intr = build_intrinsic_types(store.engine())?;
83    let map_tombstone =
84        super::prelude::map::build_tombstone(store, intr.object.clone(), vtables.object)?;
85    let collection_null =
86        super::prelude::map::build_tombstone(store, intr.object.clone(), vtables.object)?;
87    let temporal = super::prelude::temporal::install_abi(linker, store, &intr)?;
88    let error_subclass_type =
89        super::prelude::error::build_error_subclass_types(store.engine(), &intr)?.1;
90    let map_backing_type = super::prelude::map::map_backing_struct(store.engine(), &intr)?;
91    let set_backing_type = super::prelude::set::set_backing_struct(store.engine(), &intr)?;
92    let member_functions = super::prelude::member::functions(linker, store);
93    store.data_mut().host_abi = Some(HostAbi {
94        member_functions,
95        string_type: intr.string,
96        uint8_type: intr.uint8_array,
97        array_type: intr.array,
98        raw_array_type: intr.raw_array,
99        boxed_number_type: intr.boxed_number,
100        boxed_boolean_type: intr.boxed_boolean,
101        field_names_type: intr.field_names,
102        object_shape_type: intr.object_shape,
103        object_fields_type: intr.object_fields,
104        map_backing_type,
105        set_backing_type,
106        string_vtable: vtables.string,
107        array_vtable: vtables.array,
108        object_vtable: vtables.object,
109        boxed_number_vtable: vtables.boxed_number,
110        boxed_boolean_vtable: vtables.boxed_boolean,
111        uint8_array_vtable: vtables.uint8_array,
112        bigint_vtable: vtables.bigint,
113        closure_vtable: vtables.closure,
114        regex_vtable: vtables.regex,
115        regex_match_box_vtable: vtables.regex_match_box,
116        opaque_vtable: vtables.opaque,
117        temporal,
118        map_tombstone,
119        collection_null,
120        error_type: intr.error,
121        error_subclass_type,
122        error_vtable: error_host.vtable,
123        range_error_vtable: error_host.range_vtable,
124        quota_exceeded_vtable: error_host.quota_exceeded_vtable,
125        type_error_vtable: error_host.type_vtable,
126        syntax_error_vtable: error_host.syntax_vtable,
127        uri_error_vtable: error_host.uri_vtable,
128        reference_error_vtable: error_host.reference_vtable,
129        permission_denied_vtable: error_host.permission_denied_vtable,
130        timeout_error_vtable: error_host.timeout_vtable,
131        error_field_names: error_host.field_names,
132        permission_denied_field_names: error_host.permission_denied_field_names,
133        error_tag: error_host.tag,
134    });
135    Ok(())
136}
137
138fn install_core_host_functions(linker: &mut Linker<StoreData>) -> wasmtime::Result<()> {
139    let engine = linker.engine().clone();
140
141    // Wasmtime requires exact type matching at instantiation: a host
142    // function imported as `(ref $string)` must register with that
143    // concrete element type, not the generic `(ref array)` that
144    // typed `Rooted<ArrayRef>` registration produces. Build the
145    // string array type by hand — same shape as the prelude's
146    // `$string`: `(array (mut i16))` packed UTF-16 — and use
147    // `Linker::func_new` (untyped) so the FuncType matches the
148    // consumer's expected import signature byte-for-byte.
149    let string_type = string_array_type(&engine);
150
151    install_number_module(linker)?;
152    super::prelude::install(linker)?;
153    super::prelude::bigint::ops::install(linker, &string_type)?;
154    install_internal_module(linker, &string_type)?;
155    super::json::install_json_module(linker, &string_type)?;
156    Ok(())
157}
158
159fn install_internal_module(
160    linker: &mut Linker<StoreData>,
161    string_type: &ArrayType,
162) -> wasmtime::Result<()> {
163    use base64::Engine as _;
164    use base64::alphabet;
165    use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig, NO_PAD, PAD};
166
167    let engine = linker.engine().clone();
168
169    let raw_uint8_array_type = uint8_array_array_type(&engine);
170    let raw_uint8_array_param = ValType::Ref(RefType::new(
171        false,
172        HeapType::from(raw_uint8_array_type.clone()),
173    ));
174    let raw_uint8_array_result = raw_uint8_array_param.clone();
175    let raw_string_param = ValType::Ref(RefType::new(false, HeapType::from(string_type.clone())));
176    let raw_string_result = raw_string_param.clone();
177
178    let ty = FuncType::new(
179        &engine,
180        [raw_uint8_array_param.clone(), ValType::I32, ValType::I32],
181        [raw_string_result.clone()],
182    );
183    register_host_fn(
184        linker,
185        INTERNAL_MODULE_NAME,
186        crate::mangle::host(INTERNAL_MODULE_NAME, "uint8array_to_base64"),
187        ty,
188        |caller, params, results| -> wasmtime::Result<()> {
189            let bytes =
190                read_uint8_array_arg(&mut *caller, abi_arg(params, 0)?, "uint8array_to_base64")?;
191            let alphabet_flag = (*abi_arg(params, 1)?)
192                .i32()
193                .ok_or_else(|| invariant_trap("host ABI: expected i32"))?;
194            let omit_padding = (*abi_arg(params, 2)?)
195                .i32()
196                .ok_or_else(|| invariant_trap("host ABI: expected i32"))?
197                != 0;
198            let alphabet = if alphabet_flag == 1 {
199                &alphabet::URL_SAFE
200            } else {
201                &alphabet::STANDARD
202            };
203            let config: GeneralPurposeConfig = if omit_padding { NO_PAD } else { PAD };
204            let engine = GeneralPurpose::new(alphabet, config);
205            fuel::charge(&mut *caller, fuel::SCAN, bytes.len() as u64)?;
206            let encoded = engine.encode(&bytes);
207            let arr = write_submilli_string(&mut *caller, &encoded)?;
208            *abi_result(results, 0)? = Val::AnyRef(Some(arr.to_anyref()));
209            Ok(())
210        },
211    )?;
212
213    let ty = FuncType::new(
214        &engine,
215        [raw_string_param.clone(), ValType::I32],
216        [raw_uint8_array_result.clone()],
217    );
218    let raw_uint8_array_type_for_decode = raw_uint8_array_type.clone();
219    register_host_fn(
220        linker,
221        INTERNAL_MODULE_NAME,
222        crate::mangle::host(INTERNAL_MODULE_NAME, "uint8array_from_base64"),
223        ty,
224        move |caller, params, results| -> wasmtime::Result<()> {
225            let s = read_string_arg(&mut *caller, abi_arg(params, 0)?, "uint8array_from_base64")?;
226            fuel::charge(&mut *caller, fuel::SCAN, s.len() as u64)?;
227            let alphabet_flag = (*abi_arg(params, 1)?)
228                .i32()
229                .ok_or_else(|| invariant_trap("host ABI: expected i32"))?;
230            let alphabet = if alphabet_flag == 1 {
231                &alphabet::URL_SAFE
232            } else {
233                &alphabet::STANDARD
234            };
235            // A terminal '=' selects canonical padding; otherwise require none.
236            let config = if s.as_bytes().last() == Some(&b'=') {
237                PAD
238            } else {
239                NO_PAD
240            };
241            let decoded = GeneralPurpose::new(alphabet, config).decode(s.as_bytes());
242            let bytes =
243                decoded.map_err(|e| wasmtime::Error::msg(format!("Uint8Array.fromBase64: {e}")))?;
244            let arr = write_uint8_array(
245                &mut *caller,
246                raw_uint8_array_type_for_decode.clone(),
247                &bytes,
248            )?;
249            *abi_result(results, 0)? = Val::AnyRef(Some(arr.to_anyref()));
250            Ok(())
251        },
252    )?;
253
254    let ty = FuncType::new(
255        &engine,
256        [raw_string_param.clone()],
257        [raw_uint8_array_result.clone()],
258    );
259    let raw_uint8_array_type_for_encode = raw_uint8_array_type.clone();
260    register_host_fn(
261        linker,
262        INTERNAL_MODULE_NAME,
263        crate::mangle::host(INTERNAL_MODULE_NAME, "textencoder_encode"),
264        ty,
265        move |caller, params, results| -> wasmtime::Result<()> {
266            let s = read_string_arg(&mut *caller, abi_arg(params, 0)?, "textencoder_encode")?;
267            let bytes = s.into_bytes();
268            let arr = write_uint8_array(
269                &mut *caller,
270                raw_uint8_array_type_for_encode.clone(),
271                &bytes,
272            )?;
273            *abi_result(results, 0)? = Val::AnyRef(Some(arr.to_anyref()));
274            Ok(())
275        },
276    )?;
277
278    let ty = FuncType::new(
279        &engine,
280        [raw_uint8_array_param],
281        [raw_string_result.clone()],
282    );
283    register_host_fn(
284        linker,
285        INTERNAL_MODULE_NAME,
286        crate::mangle::host(INTERNAL_MODULE_NAME, "textdecoder_decode"),
287        ty,
288        |caller, params, results| -> wasmtime::Result<()> {
289            let bytes =
290                read_uint8_array_arg(&mut *caller, abi_arg(params, 0)?, "textdecoder_decode")?;
291            let s = std::str::from_utf8(&bytes).map_err(|e| {
292                type_error(format!(
293                    "TextDecoder.decode: invalid UTF-8 at byte {}: {e}",
294                    e.valid_up_to(),
295                ))
296            })?;
297            let arr = write_submilli_string(&mut *caller, s)?;
298            *abi_result(results, 0)? = Val::AnyRef(Some(arr.to_anyref()));
299            Ok(())
300        },
301    )?;
302
303    Ok(())
304}
305
306/// Every byte payload the host builds ends here, so this is where its copy is
307/// charged.
308pub(crate) fn write_uint8_array(
309    mut ctx: impl AsContextMut<Data = StoreData>,
310    array_ty: ArrayType,
311    bytes: &[u8],
312) -> wasmtime::Result<Rooted<ArrayRef>> {
313    fuel::charge(&mut ctx, fuel::COPY, bytes.len() as u64)?;
314    write_uint8_array_precharged(ctx, array_ty, bytes)
315}
316
317/// [`write_uint8_array`] for a caller that has already charged `COPY(len)`.
318pub(crate) fn write_uint8_array_precharged(
319    mut ctx: impl AsContextMut<Data = StoreData>,
320    array_ty: ArrayType,
321    bytes: &[u8],
322) -> wasmtime::Result<Rooted<ArrayRef>> {
323    let pre = ArrayRefPre::new(&mut ctx, array_ty);
324    ArrayRef::new_from_i8_slice(&mut ctx, &pre, bytes)
325}
326
327pub(crate) fn uint8_array_array_type(engine: &Engine) -> ArrayType {
328    ArrayType::new(engine, FieldType::new(Mutability::Var, StorageType::I8))
329}
330
331pub(crate) fn read_uint8_array_arg(
332    caller: &mut Caller<'_, StoreData>,
333    val: &Val,
334    name: &str,
335) -> wasmtime::Result<Vec<u8>> {
336    let arr = uint8_array_backing(caller, val, name)?;
337    let len = usize::try_from(arr.len(&mut *caller)?).map_err(fatal_host_error)?;
338    read_uint8_array_range(caller, arr, 0, len, name)
339}
340
341/// `len` bytes of a `$Uint8Array` from `offset`, copied in one pass and
342/// charged for what is copied. A failed read is a catchable error labelled
343/// `name`: a raw-ABI package can pass a payload that is not an `i8` array.
344pub(crate) fn read_uint8_array_range(
345    caller: &mut Caller<'_, StoreData>,
346    arr: Rooted<ArrayRef>,
347    offset: usize,
348    len: usize,
349    name: &str,
350) -> wasmtime::Result<Vec<u8>> {
351    fuel::charge(&mut *caller, fuel::COPY, len as u64)?;
352    let offset = u32::try_from(offset).map_err(fatal_host_error)?;
353    let mut out = Vec::new();
354    out.try_reserve_exact(len).map_err(fatal_host_error)?;
355    out.resize(len, 0);
356    arr.read_i8(&mut *caller, offset, &mut out)
357        .map_err(|error| type_error(format!("{name}: {error}")))?;
358    Ok(out)
359}
360
361/// One byte of a `$Uint8Array`. The index must be in bounds.
362pub(crate) fn read_uint8(
363    caller: &mut Caller<'_, StoreData>,
364    arr: Rooted<ArrayRef>,
365    index: usize,
366) -> wasmtime::Result<u8> {
367    let index = u32::try_from(index).map_err(fatal_host_error)?;
368    match arr.get(&mut *caller, index).map_err(fatal_host_error)? {
369        Val::I32(byte) => Ok(byte as u8),
370        other => Err(fatal_host_error(format!("byte {index} is {other:?}"))),
371    }
372}
373
374/// The byte array behind a `$Uint8Array` argument, left where it is.
375pub(crate) fn uint8_array_backing(
376    caller: &mut Caller<'_, StoreData>,
377    val: &Val,
378    name: &str,
379) -> wasmtime::Result<Rooted<ArrayRef>> {
380    let any = match val {
381        Val::AnyRef(Some(any)) => *any,
382        Val::AnyRef(None) => {
383            return Err(type_error(format!("{name} arg is null")));
384        }
385        other => {
386            return Err(type_error(format!(
387                "{name} expects a Uint8Array, got {other:?}"
388            )));
389        }
390    };
391    // Accept either a real `$Uint8Array` struct (field-1 payload) or a bare
392    // `$rawUint8Array` array (a host package still on the raw ABI).
393    let arr = if let Some(st) = any.as_struct(&mut *caller)? {
394        // Only the guest `$Uint8Array` struct carries a payload in field 1.
395        // Any other struct (a package's backing struct with a hidden byte
396        // field, say) is refused rather than read as bytes.
397        let uint8_ty = intrinsic_types(&mut *caller)?.uint8_array.clone();
398        if !st.matches_ty(&*caller, &uint8_ty)? {
399            return Err(type_error(format!("{name} expects a Uint8Array")));
400        }
401        match st.field(&mut *caller, 1)? {
402            Val::AnyRef(Some(inner)) => inner.unwrap_array(&mut *caller)?,
403            other => {
404                return Err(wasmtime::Error::msg(format!(
405                    "{name}: malformed $Uint8Array payload {other:?}"
406                )));
407            }
408        }
409    } else {
410        any.unwrap_array(&mut *caller)?
411    };
412    Ok(arr)
413}
414
415fn install_number_module(linker: &mut Linker<StoreData>) -> wasmtime::Result<()> {
416    type FormatOp = fn(f64, f64) -> Result<String, String>;
417    let engine = linker.engine().clone();
418    let string_struct = intrinsic_string_type(&engine)?;
419    let string_struct_result =
420        ValType::Ref(RefType::new(false, HeapType::ConcreteStruct(string_struct)));
421    let string_struct_param = string_struct_result.clone();
422
423    let to_string_ty = FuncType::new(&engine, [ValType::F64], [string_struct_result.clone()]);
424    register_host_fn(
425        linker,
426        NUMBER_MODULE_NAME,
427        crate::mangle::host(NUMBER_MODULE_NAME, "toString"),
428        to_string_ty,
429        |caller, params, results| -> wasmtime::Result<()> {
430            let n = match *abi_arg(params, 0)? {
431                Val::F64(bits) => f64::from_bits(bits),
432                ref other => {
433                    return Err(type_error(format!(
434                        "number.toString expects f64, got {other:?}"
435                    )));
436                }
437            };
438            let s = format_number(n);
439            let st = write_submilli_string_struct(caller, &s)?;
440            *abi_result(results, 0)? = Val::AnyRef(Some(st.to_anyref()));
441            Ok(())
442        },
443    )?;
444
445    let to_number_ty = FuncType::new(&engine, [string_struct_param.clone()], [ValType::F64]);
446    register_host_fn(
447        linker,
448        NUMBER_MODULE_NAME,
449        crate::mangle::host(NUMBER_MODULE_NAME, "toNumber"),
450        to_number_ty,
451        |caller, params, results| -> wasmtime::Result<()> {
452            let s = read_string_arg(&mut *caller, abi_arg(params, 0)?, "number.toNumber")?;
453            let value = string_to_number_js(&s);
454            *abi_result(results, 0)? = Val::F64(value.to_bits());
455            Ok(())
456        },
457    )?;
458
459    let parse_int_ty = FuncType::new(
460        &engine,
461        [string_struct_param.clone(), ValType::F64],
462        [ValType::F64],
463    );
464    register_host_fn(
465        linker,
466        NUMBER_MODULE_NAME,
467        crate::mangle::host(NUMBER_MODULE_NAME, "parseInt"),
468        parse_int_ty,
469        |caller, params, results| -> wasmtime::Result<()> {
470            let s = read_string_arg(&mut *caller, abi_arg(params, 0)?, "number.parseInt")?;
471            let radix = match *abi_arg(params, 1)? {
472                Val::F64(bits) => f64::from_bits(bits),
473                ref other => {
474                    return Err(type_error(format!(
475                        "number.parseInt expects f64 radix, got {other:?}"
476                    )));
477                }
478            };
479            let value = parse_int_with_radix(&s, radix);
480            *abi_result(results, 0)? = Val::F64(value.to_bits());
481            Ok(())
482        },
483    )?;
484
485    let parse_float_ty = FuncType::new(&engine, [string_struct_param], [ValType::F64]);
486    register_host_fn(
487        linker,
488        NUMBER_MODULE_NAME,
489        crate::mangle::host(NUMBER_MODULE_NAME, "parseFloat"),
490        parse_float_ty,
491        |caller, params, results| -> wasmtime::Result<()> {
492            let s = read_string_arg(&mut *caller, abi_arg(params, 0)?, "number.parseFloat")?;
493            let value = parse_float_js(&s);
494            *abi_result(results, 0)? = Val::F64(value.to_bits());
495            Ok(())
496        },
497    )?;
498
499    // Formatting methods share a (value, digits-or-radix) -> string shape;
500    // a NaN second argument is the omitted-optional sentinel where one applies.
501    let format_ty = FuncType::new(
502        &engine,
503        [ValType::F64, ValType::F64],
504        [string_struct_result],
505    );
506    for (name, op) in [
507        ("toFixed", crate::runtime::number::to_fixed_js as FormatOp),
508        ("toPrecision", crate::runtime::number::to_precision_js),
509        ("toExponential", crate::runtime::number::to_exponential_js),
510        ("toStringRadix", crate::runtime::number::to_string_radix_js),
511    ] {
512        register_number_formatter(linker, name, format_ty.clone(), op)?;
513    }
514
515    Ok(())
516}
517
518fn register_number_formatter(
519    linker: &mut Linker<StoreData>,
520    name: &'static str,
521    ty: FuncType,
522    op: fn(f64, f64) -> Result<String, String>,
523) -> wasmtime::Result<()> {
524    register_host_fn(
525        linker,
526        NUMBER_MODULE_NAME,
527        crate::mangle::host(NUMBER_MODULE_NAME, name),
528        ty,
529        move |caller, params, results| -> wasmtime::Result<()> {
530            let (Val::F64(x_bits), Val::F64(arg_bits)) = (abi_arg(params, 0)?, abi_arg(params, 1)?)
531            else {
532                wasmtime::bail!("number.{name} expects (f64, f64)");
533            };
534            let formatted = op(f64::from_bits(*x_bits), f64::from_bits(*arg_bits))
535                .map_err(wasmtime::Error::msg)?;
536            let st = write_submilli_string_struct(caller, &formatted)?;
537            *abi_result(results, 0)? = Val::AnyRef(Some(st.to_anyref()));
538            Ok(())
539        },
540    )
541}
542
543/// ECMAScript ToString for f64 — spells NaN/Infinity/-Infinity (Rust's default prints "inf").
544fn format_number(n: f64) -> String {
545    if n.is_nan() {
546        "NaN".to_string()
547    } else if n.is_infinite() {
548        if n > 0.0 {
549            "Infinity".to_string()
550        } else {
551            "-Infinity".to_string()
552        }
553    } else if n == 0.0 {
554        // JS `Number#toString` normalizes -0 to "0"; Rust's f64 formatting
555        // keeps the sign. `n == 0.0` matches both +0 and -0.
556        "0".to_string()
557    } else {
558        n.to_string()
559    }
560}
561
562/// Decode a string from an `anyref` that is either a real `$string` struct
563/// (read its field-1 payload array) or a bare `$rawString` array (raw ABI).
564fn read_string_from_anyref(
565    caller: &mut Caller<'_, StoreData>,
566    any: Rooted<AnyRef>,
567    name: &str,
568) -> wasmtime::Result<String> {
569    let arr = if let Some(st) = any.as_struct(&mut *caller)? {
570        match st.field(&mut *caller, 1)? {
571            Val::AnyRef(Some(inner)) => inner.unwrap_array(&mut *caller)?,
572            other => {
573                return Err(wasmtime::Error::msg(format!(
574                    "{name}: malformed $string payload {other:?}"
575                )));
576            }
577        }
578    } else {
579        any.unwrap_array(&mut *caller)?
580    };
581    read_submilli_string(caller, arr)
582}
583
584pub fn read_string_arg(
585    caller: &mut Caller<'_, StoreData>,
586    val: &Val,
587    name: &str,
588) -> wasmtime::Result<String> {
589    match val {
590        Val::AnyRef(Some(any)) => read_string_from_anyref(caller, *any, name),
591        Val::AnyRef(None) => Err(type_error(format!("{name} arg is null"))),
592        other => Err(type_error(format!(
593            "{name} expects a string, got {other:?}"
594        ))),
595    }
596}
597
598/// Read a string without replacing lone surrogates at a UTF-8 boundary.
599pub(crate) fn read_string_arg_units(
600    caller: &mut Caller<'_, StoreData>,
601    val: &Val,
602    name: &str,
603) -> wasmtime::Result<Vec<u16>> {
604    let any = match val {
605        Val::AnyRef(Some(any)) => *any,
606        _ => return Err(type_error(format!("{name} expects a string"))),
607    };
608    let string = any.unwrap_struct(&mut *caller)?;
609    let raw = match string.field(&mut *caller, 1)? {
610        Val::AnyRef(Some(raw)) => raw.unwrap_array(&mut *caller)?,
611        _ => {
612            return Err(fatal_host_error(format!(
613                "{name}: malformed string payload"
614            )));
615        }
616    };
617    read_code_units(caller, raw, name)
618}
619
620/// Read a real `$Array<$string>` into `Vec<String>`. Null slots → empty string.
621pub fn read_string_array_arg(
622    caller: &mut Caller<'_, StoreData>,
623    val: &Val,
624    name: &str,
625) -> wasmtime::Result<Vec<String>> {
626    let any = match val {
627        Val::AnyRef(Some(any)) => *any,
628        Val::AnyRef(None) => return Err(type_error(format!("{name} arg is null"))),
629        other => {
630            return Err(type_error(format!(
631                "{name} expects a string[], got {other:?}"
632            )));
633        }
634    };
635    let object = any
636        .as_struct(&mut *caller)?
637        .ok_or_else(|| type_error(format!("{name}: expected $Array struct")))?;
638    let storage = super::array_storage::ArrayStorage::from_struct(caller, object)?;
639    let raw = storage.backing;
640    let len = storage.len;
641    let mut out = Vec::new();
642    out.try_reserve_exact(len as usize)
643        .map_err(fatal_host_error)?;
644    for i in 0..len {
645        match raw.get(&mut *caller, i)? {
646            Val::AnyRef(Some(elem)) => out.push(read_string_from_anyref(caller, elem, name)?),
647            Val::AnyRef(None) => out.push(String::new()),
648            other => {
649                return Err(type_error(format!(
650                    "{name}: slot {i} expects a $string, got {other:?}"
651                )));
652            }
653        }
654    }
655    Ok(out)
656}
657
658/// Encodes a Rust string as a Submilli packed-UTF-16 `(array (mut i16))` —
659/// the bare `$rawString` payload, without the `$string` object wrapper.
660pub fn write_submilli_string(
661    mut ctx: impl AsContextMut<Data = StoreData>,
662    s: &str,
663) -> wasmtime::Result<Rooted<ArrayRef>> {
664    let units = encode_utf16(&mut ctx, s)?;
665    write_code_units(ctx, &units)
666}
667
668/// UTF-8 to UTF-16, charged as a scan of the input.
669pub(crate) fn encode_utf16(
670    ctx: impl AsContextMut<Data = StoreData>,
671    s: &str,
672) -> wasmtime::Result<Vec<u16>> {
673    fuel::charge(ctx, fuel::SCAN, s.len() as u64)?;
674    // A UTF-8 byte count bounds its UTF-16 unit count. Reserve fallibly once
675    // before encoding, so extend cannot trigger infallible capacity growth.
676    let mut units = Vec::new();
677    units.try_reserve_exact(s.len()).map_err(fatal_host_error)?;
678    units.extend(s.encode_utf16());
679    Ok(units)
680}
681
682/// Builds a `$rawString` payload from UTF-16 code units in one pass. Every
683/// string result the host builds from units ends here, so this is where its
684/// copy is charged.
685pub(crate) fn write_code_units(
686    mut ctx: impl AsContextMut<Data = StoreData>,
687    units: &[u16],
688) -> wasmtime::Result<Rooted<ArrayRef>> {
689    fuel::charge(&mut ctx, fuel::COPY, units.len() as u64)?;
690    let array_ty = string_array_type(ctx.as_context().engine());
691    let pre = ArrayRefPre::new(&mut ctx, array_ty);
692    ArrayRef::new_from_i16_slice(&mut ctx, &pre, units)
693}
694
695/// A `$rawString` payload's UTF-16 code units, copied in one pass rather than
696/// one `get` per unit: string host functions call this on every receiver, so
697/// this is where the copy of every string argument is charged.
698///
699/// A payload that is not an `i16` array is a catchable error labelled `name`:
700/// a program can reach this with a non-string, through a `toJson` inserted
701/// into a `Record`.
702pub(crate) fn read_code_units(
703    mut ctx: impl AsContextMut<Data = StoreData>,
704    raw: Rooted<ArrayRef>,
705    name: &str,
706) -> wasmtime::Result<Vec<u16>> {
707    let len = raw.len(&mut ctx)?;
708    fuel::charge(&mut ctx, fuel::COPY, u64::from(len))?;
709    let len = usize::try_from(len).map_err(fatal_host_error)?;
710    let mut units = Vec::new();
711    units.try_reserve_exact(len).map_err(fatal_host_error)?;
712    units.resize(len, 0);
713    raw.copy_to_i16_slice(&mut ctx, &mut units)
714        .map_err(|error| wasmtime::Error::msg(format!("{name}: {error}")))?;
715    Ok(units)
716}
717
718/// `len` code units of a `$rawString` payload from `offset`, copied in one
719/// pass and charged for what is copied, so an accessor that needs a few units
720/// of a long string does not pay for all of it. Only typed `$string` receivers
721/// reach this, so a failure is the host's own mistake, not the program's.
722pub(crate) fn read_code_units_range(
723    mut ctx: impl AsContextMut<Data = StoreData>,
724    raw: Rooted<ArrayRef>,
725    offset: usize,
726    len: usize,
727) -> wasmtime::Result<Vec<u16>> {
728    fuel::charge(&mut ctx, fuel::COPY, len as u64)?;
729    let offset = u32::try_from(offset).map_err(fatal_host_error)?;
730    let mut units = Vec::new();
731    units.try_reserve_exact(len).map_err(fatal_host_error)?;
732    units.resize(len, 0);
733    raw.read_i16(&mut ctx, offset, &mut units)
734        .map_err(fatal_host_error)?;
735    Ok(units)
736}
737
738/// One code unit of a `$rawString` payload; see [`read_code_units_range`].
739pub(crate) fn read_code_unit(
740    mut ctx: impl AsContextMut<Data = StoreData>,
741    raw: Rooted<ArrayRef>,
742    index: usize,
743) -> wasmtime::Result<u16> {
744    let index = u32::try_from(index).map_err(fatal_host_error)?;
745    match raw.get(&mut ctx, index).map_err(fatal_host_error)? {
746        Val::I32(unit) => Ok(unit as u16),
747        other => Err(fatal_host_error(format!("code unit {index} is {other:?}"))),
748    }
749}
750
751/// Runtime handles host functions use to build *real* `$Object`-subtype structs
752/// (currently `$string`/`$Array`/`$Uint8Array`) on the host side instead of
753/// returning the raw payload array for a Wasm shim to re-wrap. The canonical
754/// `StructType`/`ArrayType` handles are declared directly via `RecGroupBuilder`
755/// (see [`build_intrinsic_types`]); the vtable value is read from the prelude
756/// instance per call.
757pub struct HostAbi {
758    pub(crate) member_functions: std::collections::BTreeMap<String, wasmtime::Func>,
759    pub(crate) string_type: StructType,
760    pub(crate) uint8_type: StructType,
761    pub(crate) array_type: StructType,
762    pub(crate) raw_array_type: ArrayType,
763    pub(crate) boxed_number_type: StructType,
764    pub(crate) boxed_boolean_type: StructType,
765    pub(crate) field_names_type: ArrayType,
766    pub(crate) object_shape_type: StructType,
767    pub(crate) object_fields_type: ArrayType,
768    // The collection backings. They are `$Object` subtypes sharing the object
769    // vtable, so the universal slots reach them but must not read their bucket
770    // arrays as an `$ObjectShape` payload — and that check runs once per node
771    // of the structural walk, so the types are recovered here rather than
772    // rebuilt per call.
773    pub(crate) map_backing_type: StructType,
774    pub(crate) set_backing_type: StructType,
775    // The host-owned vtable globals, kept as direct handles so host fns read
776    // them straight from the store instead of via the prelude's re-exports.
777    pub(crate) string_vtable: Global,
778    pub(crate) array_vtable: Global,
779    pub(crate) object_vtable: Global,
780    pub(crate) boxed_number_vtable: Global,
781    pub(crate) boxed_boolean_vtable: Global,
782    pub(crate) uint8_array_vtable: Global,
783    pub(crate) bigint_vtable: Global,
784    pub(crate) closure_vtable: Global,
785    pub(crate) regex_vtable: Global,
786    pub(crate) regex_match_box_vtable: Global,
787    /// Identity vtable for host-only backing structs (stdlib `URL`, `Response`,
788    /// fs `Stat`, …) — opaque `toString`/`toJson`, reference equality.
789    pub(crate) opaque_vtable: Global,
790    pub(crate) temporal: super::prelude::temporal::TemporalAbi,
791    // The host-owned `Map`/`Set` tombstone sentinel — a bare `$Object` marking a
792    // deleted probe slot. Compared by reference identity during probing.
793    pub(crate) map_tombstone: Global,
794    pub(crate) collection_null: Global,
795    pub(crate) error_type: StructType,
796    /// The one struct type shared by every built-in `Error` subclass —
797    /// identical shape, so they canonicalize together; the vtables carry the
798    /// identity.
799    pub(crate) error_subclass_type: StructType,
800    pub(crate) error_vtable: Global,
801    pub(crate) range_error_vtable: Global,
802    pub(crate) quota_exceeded_vtable: Global,
803    pub(crate) type_error_vtable: Global,
804    pub(crate) syntax_error_vtable: Global,
805    pub(crate) uri_error_vtable: Global,
806    pub(crate) reference_error_vtable: Global,
807    pub(crate) permission_denied_vtable: Global,
808    pub(crate) timeout_error_vtable: Global,
809    pub(crate) error_field_names: Global,
810    pub(crate) permission_denied_field_names: Global,
811    pub(crate) error_tag: wasmtime::Tag,
812}
813
814/// The store-bound handles the host Error constructors read per call.
815pub(crate) struct HostAbiHandles {
816    pub error_type: StructType,
817    pub error_subclass_type: StructType,
818    pub object_fields_type: ArrayType,
819    pub error_vtable: Global,
820    pub range_error_vtable: Global,
821    pub quota_exceeded_vtable: Global,
822    pub type_error_vtable: Global,
823    pub syntax_error_vtable: Global,
824    pub uri_error_vtable: Global,
825    pub reference_error_vtable: Global,
826    pub permission_denied_vtable: Global,
827    pub timeout_vtable: Global,
828    pub error_field_names: Global,
829    pub permission_denied_field_names: Global,
830}
831
832pub(crate) fn error_abi(caller: &Caller<'_, StoreData>) -> wasmtime::Result<HostAbiHandles> {
833    let abi = caller
834        .data()
835        .host_abi
836        .as_ref()
837        .ok_or_else(|| wasmtime::Error::msg("host_abi unset (prelude not instantiated)"))?;
838    Ok(HostAbiHandles {
839        error_type: abi.error_type.clone(),
840        error_subclass_type: abi.error_subclass_type.clone(),
841        object_fields_type: abi.object_fields_type.clone(),
842        error_vtable: abi.error_vtable,
843        range_error_vtable: abi.range_error_vtable,
844        quota_exceeded_vtable: abi.quota_exceeded_vtable,
845        type_error_vtable: abi.type_error_vtable,
846        syntax_error_vtable: abi.syntax_error_vtable,
847        uri_error_vtable: abi.uri_error_vtable,
848        reference_error_vtable: abi.reference_error_vtable,
849        permission_denied_vtable: abi.permission_denied_vtable,
850        timeout_vtable: abi.timeout_error_vtable,
851        error_field_names: abi.error_field_names,
852        permission_denied_field_names: abi.permission_denied_field_names,
853    })
854}
855
856/// Read a host-owned vtable global from the cached handle in [`HostAbi`]. Works
857/// from any store context (a `Caller` or a `StoreContextMut`).
858fn host_vtable(
859    ctx: &mut impl AsContextMut<Data = StoreData>,
860    select: impl FnOnce(&HostAbi) -> Global,
861) -> wasmtime::Result<Val> {
862    let global =
863        {
864            let abi =
865                ctx.as_context().data().host_abi.as_ref().ok_or_else(|| {
866                    wasmtime::Error::msg("host_abi unset (prelude not instantiated)")
867                })?;
868            select(abi)
869        };
870    Ok(global.get(&mut *ctx))
871}
872
873pub(crate) fn host_string_vtable(
874    ctx: &mut impl AsContextMut<Data = StoreData>,
875) -> wasmtime::Result<Val> {
876    host_vtable(ctx, |abi| abi.string_vtable)
877}
878
879pub(crate) fn host_array_vtable(
880    ctx: &mut impl AsContextMut<Data = StoreData>,
881) -> wasmtime::Result<Val> {
882    host_vtable(ctx, |abi| abi.array_vtable)
883}
884
885pub(crate) fn host_object_vtable(
886    ctx: &mut impl AsContextMut<Data = StoreData>,
887) -> wasmtime::Result<Val> {
888    host_vtable(ctx, |abi| abi.object_vtable)
889}
890
891pub(crate) fn host_boxed_number_vtable(
892    ctx: &mut impl AsContextMut<Data = StoreData>,
893) -> wasmtime::Result<Val> {
894    host_vtable(ctx, |abi| abi.boxed_number_vtable)
895}
896
897pub(crate) fn host_boxed_boolean_vtable(
898    ctx: &mut impl AsContextMut<Data = StoreData>,
899) -> wasmtime::Result<Val> {
900    host_vtable(ctx, |abi| abi.boxed_boolean_vtable)
901}
902
903pub(crate) fn host_uint8_array_vtable(
904    ctx: &mut impl AsContextMut<Data = StoreData>,
905) -> wasmtime::Result<Val> {
906    host_vtable(ctx, |abi| abi.uint8_array_vtable)
907}
908
909pub(crate) fn host_bigint_vtable(
910    ctx: &mut impl AsContextMut<Data = StoreData>,
911) -> wasmtime::Result<Val> {
912    host_vtable(ctx, |abi| abi.bigint_vtable)
913}
914
915pub(crate) fn host_closure_vtable(
916    ctx: &mut impl AsContextMut<Data = StoreData>,
917) -> wasmtime::Result<Val> {
918    host_vtable(ctx, |abi| abi.closure_vtable)
919}
920
921pub(crate) fn host_regex_vtable(
922    ctx: &mut impl AsContextMut<Data = StoreData>,
923) -> wasmtime::Result<Val> {
924    host_vtable(ctx, |abi| abi.regex_vtable)
925}
926
927pub(crate) fn host_regex_match_box_vtable(
928    ctx: &mut impl AsContextMut<Data = StoreData>,
929) -> wasmtime::Result<Val> {
930    host_vtable(ctx, |abi| abi.regex_match_box_vtable)
931}
932
933pub(crate) fn host_opaque_vtable(
934    ctx: &mut impl AsContextMut<Data = StoreData>,
935) -> wasmtime::Result<Val> {
936    host_vtable(ctx, |abi| abi.opaque_vtable)
937}
938
939/// Read the host-owned `Map`/`Set` tombstone sentinel from [`HostAbi`].
940pub(crate) fn host_map_tombstone(
941    ctx: &mut impl AsContextMut<Data = StoreData>,
942) -> wasmtime::Result<Val> {
943    host_vtable(ctx, |abi| abi.map_tombstone)
944}
945/// Private non-null representation of a null collection key.
946pub(crate) fn host_collection_null(
947    ctx: &mut impl AsContextMut<Data = StoreData>,
948) -> wasmtime::Result<Val> {
949    host_vtable(ctx, |abi| abi.collection_null)
950}
951
952/// Build a real `$string` (vtable + packed-UTF-16 payload). Requires
953/// `StoreData::host_abi`, set once the prelude instantiates.
954pub fn write_submilli_string_struct(
955    caller: &mut Caller<'_, StoreData>,
956    s: &str,
957) -> wasmtime::Result<Rooted<StructRef>> {
958    let units = encode_utf16(&mut *caller, s)?;
959    write_submilli_string_struct_units(caller, &units)
960}
961
962/// Build a real `$string` directly from UTF-16 code units — the
963/// surrogate-faithful path for callers (e.g. `String.fromCharCode`) whose
964/// output may contain lone surrogates that a Rust `String` can't carry.
965/// Requires `StoreData::host_abi`, set once the prelude instantiates.
966pub fn write_submilli_string_struct_units(
967    caller: &mut Caller<'_, StoreData>,
968    units: &[u16],
969) -> wasmtime::Result<Rooted<StructRef>> {
970    // `write_code_units` charges the copy.
971    let raw = write_code_units(&mut *caller, units)?;
972    let string_type = {
973        let abi = caller
974            .data()
975            .host_abi
976            .as_ref()
977            .ok_or_else(|| wasmtime::Error::msg("host_abi unset (prelude not instantiated)"))?;
978        abi.string_type.clone()
979    };
980    let vtable = host_string_vtable(caller)?;
981    let pre = StructRefPre::new(&mut *caller, string_type);
982    StructRef::new(
983        &mut *caller,
984        &pre,
985        &[vtable, Val::AnyRef(Some(raw.to_anyref())), Val::I64(0)],
986    )
987}
988
989/// Build a real `$Uint8Array` (vtable + packed-i8 payload). Mirror of
990/// [`write_submilli_string_struct`]. Requires `StoreData::host_abi`.
991pub fn write_submilli_uint8array_struct(
992    caller: &mut Caller<'_, StoreData>,
993    bytes: &[u8],
994) -> wasmtime::Result<Rooted<StructRef>> {
995    let array_ty = uint8_array_array_type(caller.engine());
996    let raw = write_uint8_array(&mut *caller, array_ty, bytes)?;
997    let uint8_type = {
998        let abi = caller
999            .data()
1000            .host_abi
1001            .as_ref()
1002            .ok_or_else(|| wasmtime::Error::msg("host_abi unset (prelude not instantiated)"))?;
1003        abi.uint8_type.clone()
1004    };
1005    let vtable = host_uint8_array_vtable(caller)?;
1006    let pre = StructRefPre::new(&mut *caller, uint8_type);
1007    StructRef::new(
1008        &mut *caller,
1009        &pre,
1010        &[vtable, Val::AnyRef(Some(raw.to_anyref()))],
1011    )
1012}
1013
1014/// Unbox a `$boxed_number` value to its `f64` payload (field 1).
1015pub(crate) fn read_boxed_number(
1016    caller: &mut Caller<'_, StoreData>,
1017    val: &Val,
1018    context: &str,
1019) -> wasmtime::Result<f64> {
1020    let Val::AnyRef(Some(any)) = val else {
1021        return Err(type_error(format!("{context}: expected a boxed number")));
1022    };
1023    let st = any
1024        .as_struct(&mut *caller)?
1025        .ok_or_else(|| type_error(format!("{context}: expected a boxed number")))?;
1026    match st.field(&mut *caller, 1)? {
1027        Val::F64(bits) => Ok(f64::from_bits(bits)),
1028        other => Err(wasmtime::Error::msg(format!(
1029            "{context}: boxed-number payload is {other:?}"
1030        ))),
1031    }
1032}
1033
1034/// Box an `f64` into a `$boxed_number` — the erased-object form of a `number`
1035/// (e.g. a `number | null` field or result). Requires `StoreData::host_abi`.
1036pub(crate) fn write_boxed_number_struct(
1037    caller: &mut Caller<'_, StoreData>,
1038    n: f64,
1039) -> wasmtime::Result<Rooted<StructRef>> {
1040    let boxed_type = {
1041        let abi = caller
1042            .data()
1043            .host_abi
1044            .as_ref()
1045            .ok_or_else(|| wasmtime::Error::msg("host_abi unset (prelude not instantiated)"))?;
1046        abi.boxed_number_type.clone()
1047    };
1048    let vtable = host_boxed_number_vtable(caller)?;
1049    let pre = StructRefPre::new(&mut *caller, boxed_type);
1050    StructRef::new(&mut *caller, &pre, &[vtable, Val::F64(n.to_bits())])
1051}
1052
1053/// Build a real `$Array` (vtable + `$rawArray` backing) from already-built
1054/// element object refs (e.g. `$string`s for a `string[]`). Each element `Val`
1055/// must be a `(ref null $Object)` — a subtype ref or `Val::AnyRef(None)`.
1056pub fn write_submilli_array_struct(
1057    caller: &mut Caller<'_, StoreData>,
1058    elements: &[Val],
1059) -> wasmtime::Result<Rooted<StructRef>> {
1060    let len = super::array_storage::checked_length(elements.len())?;
1061    fuel::charge(&mut *caller, fuel::ELEM, u64::from(len))?;
1062    write_submilli_array_struct_precharged(caller, elements)
1063}
1064
1065/// The caller has admitted ELEM once for each element while producing it.
1066pub(crate) fn write_submilli_array_struct_precharged(
1067    caller: &mut Caller<'_, StoreData>,
1068    elements: &[Val],
1069) -> wasmtime::Result<Rooted<StructRef>> {
1070    let len = super::array_storage::checked_length(elements.len())?;
1071    let (array_type, raw_array_type) = {
1072        let abi = caller
1073            .data()
1074            .host_abi
1075            .as_ref()
1076            .ok_or_else(|| wasmtime::Error::msg("host_abi unset (prelude not instantiated)"))?;
1077        (abi.array_type.clone(), abi.raw_array_type.clone())
1078    };
1079    let raw_pre = ArrayRefPre::new(&mut *caller, raw_array_type);
1080    let raw = ArrayRef::new_fixed(&mut *caller, &raw_pre, elements)?;
1081    let vtable = host_array_vtable(caller)?;
1082    let pre = StructRefPre::new(&mut *caller, array_type);
1083    StructRef::new(
1084        &mut *caller,
1085        &pre,
1086        &[
1087            vtable,
1088            Val::AnyRef(Some(raw.to_anyref())),
1089            Val::I32(len as i32),
1090        ],
1091    )
1092}
1093
1094/// Marker for a host failure that should surface to the guest as the built-in
1095/// `RangeError` subclass rather than a base `Error`. Return
1096/// `Err(range_error(...))` from a host-fn body; the `register_host_fn` wrapper
1097/// downcasts for it when converting the `Err` into a guest throw. The message
1098/// is the guest-visible `e.message`.
1099#[derive(Debug)]
1100pub struct RangeError(pub String);
1101
1102impl std::fmt::Display for RangeError {
1103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1104        f.write_str(&self.0)
1105    }
1106}
1107
1108impl std::error::Error for RangeError {}
1109
1110/// A host failure that throws the built-in `RangeError` at the guest boundary.
1111pub fn range_error(message: impl Into<String>) -> wasmtime::Error {
1112    wasmtime::Error::new(RangeError(message.into()))
1113}
1114
1115/// Marker for a host failure that should surface to the guest as the built-in
1116/// `QuotaExceededError` subclass rather than a base `Error`. Return
1117/// `Err(quota_exceeded_error(...))` from a host-fn body; the `register_host_fn` wrapper
1118/// downcasts for it when converting the `Err` into a guest throw. The message
1119/// is the guest-visible `e.message`.
1120#[derive(Debug)]
1121pub struct QuotaExceededError(pub String);
1122
1123impl std::fmt::Display for QuotaExceededError {
1124    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1125        f.write_str(&self.0)
1126    }
1127}
1128
1129impl std::error::Error for QuotaExceededError {}
1130
1131/// A host failure that throws the built-in `QuotaExceededError` at the guest boundary.
1132pub fn quota_exceeded_error(message: impl Into<String>) -> wasmtime::Error {
1133    wasmtime::Error::new(QuotaExceededError(message.into()))
1134}
1135
1136/// Marker for a host failure that should surface to the guest as the built-in
1137/// `TimeoutError` subclass: a wait outside the program ran out of time. Same
1138/// contract as [`QuotaExceededError`].
1139#[derive(Debug)]
1140pub struct TimeoutError(pub String);
1141
1142impl std::fmt::Display for TimeoutError {
1143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1144        f.write_str(&self.0)
1145    }
1146}
1147
1148impl std::error::Error for TimeoutError {}
1149
1150/// A host failure that throws the built-in `TimeoutError` at the guest boundary.
1151pub fn timeout_error(message: impl Into<String>) -> wasmtime::Error {
1152    wasmtime::Error::new(TimeoutError(message.into()))
1153}
1154
1155/// Marker for a host failure that should surface to the guest as the built-in
1156/// `SyntaxError` subclass — same contract as [`RangeError`].
1157#[derive(Debug)]
1158pub struct SyntaxError(pub String);
1159
1160impl std::fmt::Display for SyntaxError {
1161    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1162        f.write_str(&self.0)
1163    }
1164}
1165
1166impl std::error::Error for SyntaxError {}
1167
1168/// A host failure that throws the built-in `SyntaxError` at the guest boundary.
1169pub fn syntax_error(message: impl Into<String>) -> wasmtime::Error {
1170    wasmtime::Error::new(SyntaxError(message.into()))
1171}
1172
1173/// Marker for a host failure that should surface to the guest as the built-in
1174/// `URIError` subclass — same contract as [`RangeError`].
1175#[derive(Debug)]
1176pub struct UriError(pub String);
1177
1178impl std::fmt::Display for UriError {
1179    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1180        f.write_str(&self.0)
1181    }
1182}
1183
1184impl std::error::Error for UriError {}
1185
1186/// A host failure that throws the built-in `URIError` at the guest boundary.
1187pub fn uri_error(message: impl Into<String>) -> wasmtime::Error {
1188    wasmtime::Error::new(UriError(message.into()))
1189}
1190
1191/// Marker for a host failure that should surface to the guest as the built-in
1192/// `TypeError` subclass — same contract as [`RangeError`]. Used for
1193/// argument-boundary type mismatches (a null or wrong-typed value where the
1194/// ABI promised another type) and for spec-`TypeError` conditions (invalid
1195/// URL, fatal text decode, unsupported HTTP method).
1196#[derive(Debug)]
1197pub struct TypeError(pub String);
1198
1199impl std::fmt::Display for TypeError {
1200    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1201        f.write_str(&self.0)
1202    }
1203}
1204
1205impl std::error::Error for TypeError {}
1206
1207/// A host failure that throws the built-in `TypeError` at the guest boundary.
1208pub fn type_error(message: impl Into<String>) -> wasmtime::Error {
1209    wasmtime::Error::new(TypeError(message.into()))
1210}
1211
1212/// Marker for a capability denial that should surface to the guest as the
1213/// built-in `PermissionDeniedError` subclass, carrying the structured fields
1214/// policy-aware recovery code reads (`e.capability`, `e.caller`, `e.reason`).
1215/// Same contract as [`RangeError`].
1216#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1217#[non_exhaustive]
1218pub struct PermissionDenied {
1219    pub caller: String,
1220    pub capability: String,
1221    pub reason: String,
1222    source: DenialSource,
1223}
1224
1225/// Which layer refused. Absent from the guest ABI: the guest sees
1226/// `caller`/`capability`/`reason` as before. It selects the closing paragraph of
1227/// the rendered message and, for a denial that escapes the program, the
1228/// `source` the embedder reports.
1229#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
1230pub enum DenialSource {
1231    /// The operator's configured policy said no.
1232    Policy,
1233    /// A runtime invariant refused ahead of the policy. No rule can grant it,
1234    /// so the message must not suggest asking for one.
1235    Invariant,
1236    /// The path is in a volume mounted read-only. Unlike a policy decision,
1237    /// writing somewhere else is a legitimate response.
1238    ReadOnly,
1239    /// The harness, or a person it asked, refused an application tool call.
1240    Harness,
1241}
1242
1243/// A denial the runtime threw that escaped the program, as the embedder sees it.
1244#[derive(Debug, Clone, PartialEq, Eq)]
1245pub struct Denial {
1246    pub caller: String,
1247    pub capability: String,
1248    pub source: DenialSource,
1249}
1250
1251impl DenialSource {
1252    pub fn as_str(self) -> &'static str {
1253        match self {
1254            Self::Policy => "policy",
1255            Self::Invariant => "invariant",
1256            Self::ReadOnly => "read_only",
1257            Self::Harness => "harness",
1258        }
1259    }
1260}
1261
1262/// The denials the runtime itself threw, keyed by the generation-stamped handle
1263/// of the thrown `$Error`. A program-constructed `PermissionDeniedError` is
1264/// never entered, so it can be told apart from a real denial when it escapes.
1265#[derive(Default)]
1266pub(crate) struct ThrownDenials {
1267    entries: std::collections::VecDeque<(Rooted<StructRef>, Denial)>,
1268}
1269
1270/// Bound on the table. Past it the oldest entries go, so at worst an old
1271/// caught-and-rethrown denial escapes as an ordinary runtime error.
1272const MAX_THROWN_DENIALS: usize = 64;
1273
1274impl ThrownDenials {
1275    /// Drops entries whose object was collected, then adds `denial`.
1276    fn record(&mut self, store: impl AsContext, handle: Rooted<StructRef>, denial: Denial) {
1277        self.entries.retain(|(entry, _)| entry.ty(&store).is_ok());
1278        if self.entries.len() >= MAX_THROWN_DENIALS {
1279            self.entries.pop_front();
1280        }
1281        self.entries.push_back((handle, denial));
1282    }
1283
1284    /// The denial whose thrown object is `thrown`. An entry matches only when
1285    /// its generation check still succeeds: a collected object's slot may now
1286    /// hold a program-built error with the same index.
1287    pub(crate) fn find(
1288        &mut self,
1289        store: impl AsContext,
1290        thrown: &Rooted<AnyRef>,
1291    ) -> Option<Denial> {
1292        self.entries.retain(|(entry, _)| entry.ty(&store).is_ok());
1293        self.entries
1294            .iter()
1295            .find(|(entry, _)| Rooted::ref_eq(&store, entry, thrown).unwrap_or(false))
1296            .map(|(_, denial)| denial.clone())
1297    }
1298}
1299
1300/// Runs `f` on the store's denial table with the store readable alongside it.
1301///
1302/// Both [`ThrownDenials::record`] and [`ThrownDenials::find`] check handles against the
1303/// store, which cannot be borrowed while the table inside its data is. The table is moved
1304/// out for the call and put back here, so no caller can lose it on an early return.
1305pub(crate) fn with_thrown_denials<T: AsContextMut<Data = StoreData>, R>(
1306    store: &mut T,
1307    f: impl FnOnce(&mut ThrownDenials, &T) -> R,
1308) -> R {
1309    let mut table = std::mem::take(&mut store.as_context_mut().data_mut().thrown_denials);
1310    let result = f(&mut table, store);
1311    store.as_context_mut().data_mut().thrown_denials = table;
1312    result
1313}
1314
1315impl PermissionDenied {
1316    /// The fields an embedder reports when this denial escapes the program.
1317    fn denial(&self) -> Denial {
1318        Denial {
1319            caller: self.caller.clone(),
1320            capability: self.capability.clone(),
1321            source: self.source,
1322        }
1323    }
1324
1325    /// Whether the operator's policy refused, as opposed to a runtime invariant
1326    /// refusing ahead of it. A caller that treats a denial as a filter rather
1327    /// than an error — `session.list` omitting keys the policy hides — must act
1328    /// only on the policy's answer; an invariant denial means the check itself
1329    /// could not be made, which no filter may swallow.
1330    pub fn is_policy(&self) -> bool {
1331        matches!(self.source, DenialSource::Policy)
1332    }
1333}
1334
1335impl std::fmt::Display for PermissionDenied {
1336    /// The denial message an LLM sees. The closing paragraph is load-bearing:
1337    /// observed models treat a denial as an obstacle and reroute through raw
1338    /// HTTP or another package, so the message itself must say the decision is
1339    /// final. The two variants differ in what "final" means — a policy decision
1340    /// is the operator's to change, an invariant is nobody's.
1341    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1342        let Self {
1343            caller,
1344            capability,
1345            reason,
1346            source,
1347        } = self;
1348        write!(
1349            f,
1350            "permission denied: caller={caller} capability={capability}: {reason}."
1351        )?;
1352        f.write_str(match source {
1353            DenialSource::Policy => {
1354                " This request is forbidden by the operator's policy — do not work \
1355                 around the denial (another package, raw HTTP, or other arguments to \
1356                 reach the same thing); report it and stop."
1357            }
1358            DenialSource::Invariant => {
1359                " Do not work around this denial: not through another package, not by \
1360                 asking a package to fetch the value and hand it back, not through raw \
1361                 HTTP. Report it and stop."
1362            }
1363            DenialSource::ReadOnly => {
1364                " This volume cannot be written from this blueprint; write under a \
1365                 writable path instead (fs.info() lists each mount and its access)."
1366            }
1367            DenialSource::Harness => {
1368                " The application refused this call — do not work around the refusal \
1369                 (another tool, raw HTTP, or other arguments to reach the same thing); \
1370                 report it and stop."
1371            }
1372        })
1373    }
1374}
1375
1376impl std::error::Error for PermissionDenied {}
1377
1378/// A policy denial, thrown at the guest boundary as the built-in
1379/// `PermissionDeniedError`.
1380pub fn permission_denied(
1381    caller: impl Into<String>,
1382    capability: impl Into<String>,
1383    reason: impl Into<String>,
1384) -> wasmtime::Error {
1385    denied(caller, capability, reason, DenialSource::Policy)
1386}
1387
1388/// A denial the policy never got to weigh in on, because the runtime refuses
1389/// this caller/capability pair outright.
1390pub fn permission_denied_invariant(
1391    caller: impl Into<String>,
1392    capability: impl Into<String>,
1393    reason: impl Into<String>,
1394) -> wasmtime::Error {
1395    denied(caller, capability, reason, DenialSource::Invariant)
1396}
1397
1398/// A write into a volume mounted read-only.
1399pub fn permission_denied_read_only(
1400    caller: impl Into<String>,
1401    capability: impl Into<String>,
1402    reason: impl Into<String>,
1403) -> wasmtime::Error {
1404    denied(caller, capability, reason, DenialSource::ReadOnly)
1405}
1406
1407/// An application tool call the harness, or a person it asked, refused.
1408pub fn permission_denied_by_harness(
1409    caller: impl Into<String>,
1410    capability: impl Into<String>,
1411    reason: impl Into<String>,
1412) -> wasmtime::Error {
1413    denied(caller, capability, reason, DenialSource::Harness)
1414}
1415
1416fn denied(
1417    caller: impl Into<String>,
1418    capability: impl Into<String>,
1419    reason: impl Into<String>,
1420    source: DenialSource,
1421) -> wasmtime::Error {
1422    wasmtime::Error::new(PermissionDenied {
1423        caller: caller.into(),
1424        capability: capability.into(),
1425        reason: reason.into(),
1426        source,
1427    })
1428}
1429
1430fn builtin_class_of(err: &wasmtime::Error) -> super::prelude::error::BuiltinErrorClass {
1431    if err.downcast_ref::<RangeError>().is_some() {
1432        super::prelude::error::BuiltinErrorClass::Range
1433    } else if err.downcast_ref::<QuotaExceededError>().is_some() {
1434        super::prelude::error::BuiltinErrorClass::QuotaExceeded
1435    } else if err.downcast_ref::<TimeoutError>().is_some() {
1436        super::prelude::error::BuiltinErrorClass::Timeout
1437    } else if err.downcast_ref::<TypeError>().is_some() {
1438        super::prelude::error::BuiltinErrorClass::Type
1439    } else if err.downcast_ref::<SyntaxError>().is_some() {
1440        super::prelude::error::BuiltinErrorClass::Syntax
1441    } else if err.downcast_ref::<UriError>().is_some() {
1442        super::prelude::error::BuiltinErrorClass::Uri
1443    } else if err.downcast_ref::<PermissionDenied>().is_some() {
1444        super::prelude::error::BuiltinErrorClass::PermissionDenied
1445    } else {
1446        super::prelude::error::BuiltinErrorClass::Error
1447    }
1448}
1449
1450/// The own-field texts for the guest error instance, in payload-slot order
1451/// (matching `BuiltinErrorClass::own_fields`). Empty for field-less classes.
1452fn own_field_texts(err: &wasmtime::Error) -> Vec<&str> {
1453    match err.downcast_ref::<PermissionDenied>() {
1454        Some(pd) => vec![&pd.caller, &pd.capability, &pd.reason],
1455        None => Vec::new(),
1456    }
1457}
1458
1459/// Raise a catchable Submilli `Error` from inside a host function.
1460///
1461/// Host-function failures normally surface as Wasm traps, which `try/catch`
1462/// cannot intercept. This allocates the `$Error` directly (same construction
1463/// as the host `Error` constructor), wraps it in an exception object for the
1464/// host-owned tag, and throws it via the store — the engine unwinds to the
1465/// nearest `try_table` in the calling Wasm frame. `register_host_fn` wraps
1466/// every host body so any returned `Err` routes through here automatically;
1467/// the caller must `return Err(..)` the result unchanged.
1468///
1469/// Always returns an `Err`-carrying value. Before the prelude instantiates
1470/// (`host_abi` unset) it falls back to a plain-message error — an uncatchable
1471/// trap with the right text, matching the pre-catchable behavior.
1472pub fn throw_error(caller: &mut Caller<'_, StoreData>, message: &str) -> wasmtime::Error {
1473    throw_error_as(
1474        caller,
1475        super::prelude::error::BuiltinErrorClass::Error,
1476        message,
1477        &[],
1478    )
1479}
1480
1481/// A broken host invariant or exhausted host allocation must terminate the run,
1482/// rather than becoming an exception that guest code can catch and ignore.
1483/// Reaching the memory cap terminates it too, under its own name: see
1484/// [`is_memory_exhausted`](super::limits::is_memory_exhausted).
1485#[derive(Debug)]
1486pub struct FatalHostError(String);
1487
1488impl std::fmt::Display for FatalHostError {
1489    fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1490        write!(out, "internal host error: {}", self.0)
1491    }
1492}
1493
1494impl std::error::Error for FatalHostError {}
1495
1496/// Check the storage shape before a callback can perform effects. Concrete GC
1497/// subtype validation remains the engine's responsibility; guest dynamic values
1498/// are validated by the operation that consumes them.
1499pub(crate) fn check_host_abi(
1500    ty: &FuncType,
1501    params: &[Val],
1502    results: &[Val],
1503) -> wasmtime::Result<()> {
1504    if params.len() != ty.params().len() || results.len() != ty.results().len() {
1505        return Err(invariant_trap("host ABI: incorrect buffer length"));
1506    }
1507    for (value, expected) in params.iter().zip(ty.params()) {
1508        let valid = match (value, expected) {
1509            (Val::I32(_), ValType::I32)
1510            | (Val::I64(_), ValType::I64)
1511            | (Val::F32(_), ValType::F32)
1512            | (Val::F64(_), ValType::F64)
1513            | (Val::V128(_), ValType::V128) => true,
1514            (value, ValType::Ref(reference)) => {
1515                let nullable = reference.is_nullable();
1516                match (value, reference.heap_type()) {
1517                    (
1518                        Val::FuncRef(value),
1519                        HeapType::Func | HeapType::NoFunc | HeapType::ConcreteFunc(_),
1520                    ) => nullable || value.is_some(),
1521                    (Val::ExternRef(value), HeapType::Extern | HeapType::NoExtern) => {
1522                        nullable || value.is_some()
1523                    }
1524                    (Val::ExnRef(value), HeapType::Exn | HeapType::NoExn) => {
1525                        nullable || value.is_some()
1526                    }
1527                    (
1528                        Val::AnyRef(value),
1529                        HeapType::Any
1530                        | HeapType::Eq
1531                        | HeapType::I31
1532                        | HeapType::Struct
1533                        | HeapType::ConcreteStruct(_)
1534                        | HeapType::Array
1535                        | HeapType::ConcreteArray(_)
1536                        | HeapType::None,
1537                    ) => nullable || value.is_some(),
1538                    _ => false,
1539                }
1540            }
1541            _ => false,
1542        };
1543        if !valid {
1544            return Err(invariant_trap(
1545                "host ABI: incorrect argument representation",
1546            ));
1547        }
1548    }
1549    Ok(())
1550}
1551
1552pub(crate) fn abi_arg(params: &[Val], index: usize) -> wasmtime::Result<&Val> {
1553    params
1554        .get(index)
1555        .ok_or_else(|| invariant_trap("host ABI: missing argument"))
1556}
1557
1558pub(crate) fn abi_result(results: &mut [Val], index: usize) -> wasmtime::Result<&mut Val> {
1559    results
1560        .get_mut(index)
1561        .ok_or_else(|| invariant_trap("host ABI: missing result slot"))
1562}
1563
1564/// An invalid host invariant terminates execution without raising a guest exception.
1565pub(crate) fn invariant_trap(message: &'static str) -> wasmtime::Error {
1566    wasmtime::Error::new(wasmtime::Trap::UnreachableCodeReached).context(message)
1567}
1568
1569pub fn fatal_host_error(message: impl std::fmt::Display) -> wasmtime::Error {
1570    wasmtime::Error::new(FatalHostError(message.to_string()))
1571}
1572
1573/// Convert a host body's `Err` into the guest throw of the matching built-in
1574/// class, carrying any structured own fields the marker holds.
1575///
1576/// [`register_host_fn`] applies this at its boundary; a body installed as a
1577/// bare `Func` (the universal vtable slots) has to call it itself, or its
1578/// failure reaches the program as an uncatchable host error.
1579pub(crate) fn throw_host_error(
1580    caller: &mut Caller<'_, StoreData>,
1581    err: wasmtime::Error,
1582) -> wasmtime::Error {
1583    if ends_the_run(&err) || err.is::<wasmtime::ThrownException>() {
1584        return err;
1585    }
1586    let class = builtin_class_of(&err);
1587    let own = own_field_texts(&err);
1588    let denial = err
1589        .downcast_ref::<PermissionDenied>()
1590        .map(PermissionDenied::denial);
1591    throw_error_with(caller, class, &err.to_string(), &own, denial)
1592}
1593
1594/// Whether `err` must reach the embedder as it is instead of becoming a guest
1595/// throw. An engine trap is one: a host body that re-enters guest code (an
1596/// array callback, a getter under `JSON.stringify`) returns the nested call's
1597/// trap, and a program that could catch it there would outlive its fuel,
1598/// deadline or stack limit.
1599pub(crate) fn ends_the_run(err: &wasmtime::Error) -> bool {
1600    err.is::<FatalHostError>()
1601        || err.is::<wasmtime::Trap>()
1602        || super::limits::is_memory_exhausted(err)
1603}
1604
1605fn throw_error_as(
1606    caller: &mut Caller<'_, StoreData>,
1607    class: super::prelude::error::BuiltinErrorClass,
1608    message: &str,
1609    own_fields: &[&str],
1610) -> wasmtime::Error {
1611    throw_error_with(caller, class, message, own_fields, None)
1612}
1613
1614/// [`throw_error_as`], also remembering `denial` against the thrown object.
1615fn throw_error_with(
1616    caller: &mut Caller<'_, StoreData>,
1617    class: super::prelude::error::BuiltinErrorClass,
1618    message: &str,
1619    own_fields: &[&str],
1620    denial: Option<Denial>,
1621) -> wasmtime::Error {
1622    match throw_error_inner(caller, class, message, own_fields, denial) {
1623        Ok(err) => err,
1624        // Building a guest error can itself hit fuel or memory limits. Preserve
1625        // the run-ending cause instead of relabeling it as a host failure.
1626        Err(cause) if ends_the_run(&cause) => cause,
1627        Err(cause) => fatal_host_error(format!("{message}; error construction failed: {cause:#}")),
1628    }
1629}
1630
1631fn throw_error_inner(
1632    caller: &mut Caller<'_, StoreData>,
1633    class: super::prelude::error::BuiltinErrorClass,
1634    message: &str,
1635    own_fields: &[&str],
1636    denial: Option<Denial>,
1637) -> wasmtime::Result<wasmtime::Error> {
1638    let tag = caller
1639        .data()
1640        .host_abi
1641        .as_ref()
1642        .ok_or_else(|| wasmtime::Error::msg("host_abi unset (prelude not instantiated)"))?
1643        .error_tag;
1644    let error_struct =
1645        super::prelude::error::construct_from_message(caller, class, message, own_fields)?;
1646    let error = Val::AnyRef(Some(error_struct.to_anyref()));
1647    let exn_ty = wasmtime::ExnType::from_tag_type(&tag.ty(&*caller))?;
1648    let pre = wasmtime::ExnRefPre::new(&mut *caller, exn_ty);
1649    let exn = wasmtime::ExnRef::new(&mut *caller, &pre, &tag, &[error])?;
1650    match caller.as_context_mut().throw::<()>(exn) {
1651        Err(thrown) => {
1652            if let Some(denial) = denial {
1653                with_thrown_denials(caller, |table, store| {
1654                    table.record(store, error_struct, denial);
1655                });
1656            }
1657            Ok(wasmtime::Error::new(thrown))
1658        }
1659        // `throw` always returns the pending-exception error.
1660        Ok(()) => Err(fatal_host_error(
1661            "engine did not return its pending-exception error",
1662        )),
1663    }
1664}
1665
1666pub(crate) fn string_array_type(engine: &Engine) -> ArrayType {
1667    ArrayType::new(engine, FieldType::new(Mutability::Var, StorageType::I16))
1668}
1669
1670/// Register a host fn under `mangled_name` — the linker field codegen imports
1671/// for it: `mangle::host(module, name)` for stdlib/host modules, or a prelude
1672/// method's dispatch key (e.g. `submilli:prelude#String#repeat`) so codegen's
1673/// method lookup resolves to it.
1674///
1675/// Ordinary host errors become catchable guest errors via [`throw_error`].
1676/// [`FatalHostError`], an engine trap and a reached memory cap bypass conversion
1677/// and terminate execution. A body that already raised a throw (its `Err` is a
1678/// `ThrownException`) is passed through untouched, so the pending exception
1679/// isn't clobbered. Bodies receive `&mut Caller` (not an owned `Caller`) so the
1680/// wrapper can still use the caller to raise the throw after the body returns.
1681///
1682/// The import is synchronous: the engine services it inline in its dispatch loop
1683/// rather than suspending the interpreter and resuming through the async driver.
1684/// A body that awaits, or that re-enters guest code, belongs on
1685/// [`register_host_fn_async`] instead.
1686pub fn register_host_fn(
1687    linker: &mut Linker<StoreData>,
1688    module: &str,
1689    mangled_name: crate::MangledName,
1690    ty: FuncType,
1691    body: impl Fn(&mut Caller<'_, StoreData>, &[Val], &mut [Val]) -> wasmtime::Result<()>
1692    + Send
1693    + Sync
1694    + 'static,
1695) -> wasmtime::Result<()> {
1696    let call_fuel = call_fuel_of(module);
1697    let ends_its_calls = begins_calls(module);
1698    let abi = ty.clone();
1699    linker.func_new(
1700        module,
1701        mangled_name.as_str(),
1702        ty,
1703        move |mut hc, params, results| {
1704            check_host_abi(&abi, params, results)?;
1705            let marker = ends_its_calls.then(|| enter_host_call(&hc)).flatten();
1706            let outcome =
1707                charge_host_fuel(&mut hc, call_fuel).and_then(|()| body(&mut hc, params, results));
1708            if let Some(marker) = marker {
1709                exit_host_call(&hc, marker, outcome.is_ok());
1710            }
1711            match outcome {
1712                Ok(()) => Ok(()),
1713                // Already a thrown exception (pending on the store) — propagate as-is.
1714                Err(err) if err.is::<wasmtime::ThrownException>() => Err(err),
1715                Err(err) => Err(throw_host_error(&mut hc, err)),
1716            }
1717        },
1718    )?;
1719    Ok(())
1720}
1721
1722/// The modules whose host functions gate capabilities, so the recorder ends a call they
1723/// begin when they return. Other modules skip the recorder check entirely. A module that
1724/// gates must be listed: its calls would otherwise stay open until the run ends.
1725pub(crate) const CALL_MODULES: &[&str] = &[
1726    crate::stdlib::fs::MODULE_NAME,
1727    crate::stdlib::http::MODULE_NAME,
1728    crate::stdlib::llm::MODULE_NAME,
1729    crate::stdlib::embedding::MODULE_NAME,
1730    crate::stdlib::session::MODULE_NAME,
1731    crate::stdlib::secrets::MODULE_NAME,
1732    crate::stdlib::variables::MODULE_NAME,
1733    crate::stdlib::git::MODULE_NAME,
1734    crate::stdlib::code::MODULE_NAME,
1735    crate::stdlib::security::MODULE_NAME,
1736    crate::stdlib::agents::MODULE_NAME,
1737    crate::stdlib::skills::MODULE_NAME,
1738    super::mcp::MCP_MODULE_NAME,
1739    super::tools::TOOLS_MODULE_NAME,
1740];
1741
1742pub(crate) fn begins_calls(module: &str) -> bool {
1743    CALL_MODULES.contains(&module)
1744}
1745
1746fn enter_host_call(store: &impl wasmtime::AsContext<Data = StoreData>) -> Option<u64> {
1747    store
1748        .as_context()
1749        .data()
1750        .security_check
1751        .recorder()
1752        .map(super::decision::DecisionRecorder::enter_host_call)
1753}
1754
1755fn exit_host_call(store: &impl wasmtime::AsContext<Data = StoreData>, marker: u64, returned: bool) {
1756    if let Some(recorder) = store.as_context().data().security_check.recorder() {
1757        recorder.exit_host_call(marker, returned);
1758    }
1759}
1760
1761/// The flat fuel of one call into `module`. `submilli:test` is free: it only
1762/// exists under `submilli build test`, and a test's own labels and assertions
1763/// are not the program's work.
1764fn call_fuel_of(module: &str) -> u64 {
1765    if module == crate::stdlib::test::MODULE_NAME {
1766        0
1767    } else {
1768        super::fuel::CALL
1769    }
1770}
1771
1772/// Async sibling of [`register_host_fn`]: registers under `mangled_name`.
1773/// The body returns a boxed future; the same `Err` → [`throw_error`] mapping
1774/// applies once it resolves.
1775pub fn register_host_fn_async<F>(
1776    linker: &mut Linker<StoreData>,
1777    module: &str,
1778    mangled_name: crate::MangledName,
1779    ty: FuncType,
1780    body: F,
1781) -> wasmtime::Result<()>
1782where
1783    F: for<'a> Fn(
1784            &'a mut Caller<'_, StoreData>,
1785            &'a [Val],
1786            &'a mut [Val],
1787        ) -> Pin<Box<dyn Future<Output = wasmtime::Result<()>> + Send + 'a>>
1788        + Send
1789        + Sync
1790        + 'static,
1791{
1792    // `Arc` so each invocation owns a cheap clone the returned future can hold —
1793    // a bare `&body` reference to the `Fn`'s captured state can't escape it.
1794    let body = std::sync::Arc::new(body);
1795    let call_fuel = call_fuel_of(module);
1796    let ends_its_calls = begins_calls(module);
1797    let abi = ty.clone();
1798    linker.func_new_async(
1799        module,
1800        mangled_name.as_str(),
1801        ty,
1802        move |mut hc, params, results| {
1803            let body = std::sync::Arc::clone(&body);
1804            let shape = check_host_abi(&abi, params, results);
1805            Box::new(async move {
1806                shape?;
1807                let marker = ends_its_calls.then(|| enter_host_call(&hc)).flatten();
1808                let outcome = match charge_host_fuel(&mut hc, call_fuel) {
1809                    Ok(()) => body(&mut hc, params, results).await,
1810                    Err(err) => Err(err),
1811                };
1812                if let Some(marker) = marker {
1813                    exit_host_call(&hc, marker, outcome.is_ok());
1814                }
1815                match outcome {
1816                    Ok(()) => Ok(()),
1817                    Err(err) if err.is::<wasmtime::ThrownException>() => Err(err),
1818                    Err(err) => Err(throw_host_error(&mut hc, err)),
1819                }
1820            })
1821        },
1822    )?;
1823    Ok(())
1824}
1825
1826/// Register a host function that returns one Wasm value. The adapter owns the
1827/// ABI slot; the body performs argument conversion, native work, then conversion
1828/// of its result, in that order.
1829pub fn register_host_value_async<F>(
1830    linker: &mut Linker<StoreData>,
1831    module: &str,
1832    mangled_name: crate::MangledName,
1833    ty: FuncType,
1834    body: F,
1835) -> wasmtime::Result<()>
1836where
1837    F: for<'a> Fn(
1838            &'a mut Caller<'_, StoreData>,
1839            &'a [Val],
1840        ) -> Pin<Box<dyn Future<Output = wasmtime::Result<Val>> + Send + 'a>>
1841        + Send
1842        + Sync
1843        + 'static,
1844{
1845    if ty.results().len() != 1 {
1846        return Err(fatal_host_error(
1847            "value-returning host function requires one ABI result",
1848        ));
1849    }
1850    register_host_fn_async(
1851        linker,
1852        module,
1853        mangled_name,
1854        ty,
1855        move |caller, params, results| {
1856            let value = body(caller, params);
1857            Box::pin(async move {
1858                *abi_result(results, 0)? = value.await?;
1859                Ok(())
1860            })
1861        },
1862    )
1863}
1864
1865/// Value-returning counterpart for iterator callbacks reached through call_ref.
1866pub(crate) fn host_value_async<F>(
1867    store: impl AsContextMut<Data = StoreData>,
1868    ty: FuncType,
1869    body: F,
1870) -> wasmtime::Func
1871where
1872    F: for<'a> Fn(
1873            &'a mut Caller<'_, StoreData>,
1874            &'a [Val],
1875        ) -> Box<dyn Future<Output = wasmtime::Result<Val>> + Send + 'a>
1876        + Send
1877        + Sync
1878        + 'static,
1879{
1880    let body = std::sync::Arc::new(body);
1881    fuel::host_func_async(store, ty, move |mut caller, params, results| {
1882        let body = std::sync::Arc::clone(&body);
1883        Box::new(async move {
1884            let [result] = results else {
1885                return Err(fatal_host_error(
1886                    "value-returning callback requires one ABI result",
1887                ));
1888            };
1889            *result = Box::into_pin(body(&mut caller, params))
1890                .await
1891                .map_err(|error| throw_host_error(&mut caller, error))?;
1892            Ok(())
1893        })
1894    })
1895}
1896
1897/// PackageDeclaration for host fns the consumer imports directly. `console.log` and
1898/// `number.toString` are excluded — they're wrapped by prelude exports and must
1899/// not appear as free-function bindings in the user's scope.
1900pub fn host_package_declarations() -> Vec<PackageDeclaration> {
1901    vec![
1902        number_module_definitions(),
1903        super::prelude::temporal::shared::temporal_module_package_declaration(),
1904    ]
1905}
1906
1907/// Compiler-internal host fn definitions — wired into codegen but not visible in user scope.
1908pub fn internal_host_package_declarations() -> Vec<PackageDeclaration> {
1909    vec![
1910        super::json::json_module_definitions(),
1911        super::mcp::mcp_call_package_declaration(),
1912        super::tools::tools_call_package_declaration(),
1913    ]
1914}
1915
1916pub fn stdlib_package_declarations() -> Vec<PackageDeclaration> {
1917    crate::stdlib::stdlib_package_declarations()
1918}
1919
1920fn number_module_definitions() -> PackageDeclaration {
1921    let mut defs = PackageDeclaration::with_package(NUMBER_MODULE_NAME);
1922    let insert = |defs: &mut PackageDeclaration,
1923                  name: &str,
1924                  params: Vec<Param>,
1925                  ret: Type,
1926                  doc: Option<crate::DocComment>| {
1927        defs.values.insert(
1928            name.to_string(),
1929            ValueSymbol {
1930                name: name.to_string(),
1931                mangled_name: crate::mangle::host(NUMBER_MODULE_NAME, name),
1932                declaration_span: Span::at(crate::FileId::NUMBER),
1933                kind: ValueKind::Function {
1934                    generics: Vec::new(),
1935                    params,
1936                    ret,
1937                    type_predicate: None,
1938                    doc,
1939                },
1940            },
1941        );
1942    };
1943    insert(
1944        &mut defs,
1945        "toNumber",
1946        vec![Param::new("string", Type::String)],
1947        Type::Number,
1948        crate::doc(
1949            crate::FileId::NUMBER,
1950            "/**\n * Strict whole-string decimal parse.\n * Returns `NaN` when `string` is not a valid number.\n * @param string The text to parse.\n */",
1951        ),
1952    );
1953    insert(
1954        &mut defs,
1955        "parseInt",
1956        vec![Param::new("string", Type::String), parse_int_radix_param()],
1957        Type::Number,
1958        crate::doc(
1959            crate::FileId::NUMBER,
1960            "/**\n * Parses `string` as an integer in the given `radix`. Stops at the first non-digit character. Returns `NaN` if no digits are found.\n * @param string The text to parse.\n * @param radix The base (2-36). Omitted or `undefined`, a `0x` prefix reads as base 16 and anything else as base 10.\n */",
1961        ),
1962    );
1963    insert(
1964        &mut defs,
1965        "parseFloat",
1966        vec![Param::new("string", Type::String)],
1967        Type::Number,
1968        crate::doc(
1969            crate::FileId::NUMBER,
1970            "/**\n * Parses `string` as a floating-point number. Stops at the first non-numeric character. Returns `NaN` if no digits are found.\n * @param string The text to parse.\n */",
1971        ),
1972    );
1973    defs
1974}
1975
1976/// `parseInt`'s `radix`: omitted or `undefined` passes 0, which
1977/// `parse_int_with_radix` reads as "detect a `0x` prefix, else base 10", as in JS.
1978/// The host receives an `f64`, so the declared type stays `number`; a
1979/// defaulted parameter still accepts `undefined` at the call.
1980pub(crate) fn parse_int_radix_param() -> Param {
1981    Param::with_default("radix", Type::Number, crate::DefaultValue::Number(0.0))
1982}
1983
1984#[cfg(test)]
1985mod tests {
1986    use super::*;
1987
1988    #[test]
1989    fn host_abi_rejects_bad_shapes_without_numeric_defaults() {
1990        let engine = Engine::default();
1991        let ty = FuncType::new(&engine, [ValType::F64, ValType::I32], [ValType::I64]);
1992        for (params, results) in [
1993            (vec![], vec![Val::I64(0)]),
1994            (vec![Val::F64(0), Val::I32(0)], vec![]),
1995            (vec![Val::I32(0), Val::I32(0)], vec![Val::I64(0)]),
1996            (vec![Val::F64(0), Val::F64(0)], vec![Val::I64(0)]),
1997        ] {
1998            let error = check_host_abi(&ty, &params, &results).unwrap_err();
1999            assert!(error.is::<wasmtime::Trap>());
2000        }
2001        check_host_abi(
2002            &ty,
2003            &[Val::F64(f64::NAN.to_bits()), Val::I32(1)],
2004            &[Val::I64(0)],
2005        )
2006        .unwrap();
2007    }
2008
2009    fn denial(capability: &str) -> Denial {
2010        Denial {
2011            caller: "main".into(),
2012            capability: capability.into(),
2013            source: DenialSource::Policy,
2014        }
2015    }
2016
2017    /// A lookup matches only the very object the runtime threw. After the
2018    /// collector reclaims that object, a later allocation can take over its
2019    /// slot, and the stale entry must not vouch for it.
2020    #[test]
2021    fn a_collected_denial_never_matches_a_later_object() {
2022        let (engine, mut store, _) = async_store();
2023        let ty = StructType::new(
2024            &engine,
2025            [FieldType::new(Mutability::Const, StorageType::I8)],
2026        )
2027        .expect("struct type");
2028        let pre = StructRefPre::new(&mut store, ty);
2029        let mut denials = ThrownDenials::default();
2030
2031        let thrown = {
2032            let mut scope = wasmtime::RootScope::new(&mut store);
2033            let thrown = wasmtime::StructRef::new(&mut scope, &pre, &[Val::I32(1)]).expect("alloc");
2034            denials.record(&scope, thrown, denial("fs.read"));
2035            let found = denials.find(&scope, &thrown.to_anyref());
2036            assert_eq!(found.map(|d| d.capability), Some("fs.read".to_string()));
2037            thrown.to_anyref()
2038        };
2039        store.gc();
2040
2041        let mut scope = wasmtime::RootScope::new(&mut store);
2042        let forged = wasmtime::StructRef::new(&mut scope, &pre, &[Val::I32(2)]).expect("alloc");
2043        assert!(
2044            denials.find(&scope, &forged.to_anyref()).is_none(),
2045            "a program-built error is not a runtime denial"
2046        );
2047        assert!(
2048            denials.find(&scope, &thrown).is_none(),
2049            "the collected denial's entry is dropped"
2050        );
2051        assert!(denials.entries.is_empty());
2052    }
2053
2054    #[test]
2055    fn the_denial_table_drops_its_oldest_entries_past_the_cap() {
2056        let (engine, mut store, _) = async_store();
2057        let ty = StructType::new(
2058            &engine,
2059            [FieldType::new(Mutability::Const, StorageType::I8)],
2060        )
2061        .expect("struct type");
2062        let pre = StructRefPre::new(&mut store, ty);
2063        let mut denials = ThrownDenials::default();
2064        let mut scope = wasmtime::RootScope::new(&mut store);
2065        let mut thrown = Vec::new();
2066        for i in 0..=MAX_THROWN_DENIALS {
2067            let object =
2068                wasmtime::StructRef::new(&mut scope, &pre, &[Val::I32(i as i32)]).expect("alloc");
2069            denials.record(&scope, object, denial(&format!("cap.{i}")));
2070            thrown.push(object.to_anyref());
2071        }
2072        assert_eq!(denials.entries.len(), MAX_THROWN_DENIALS);
2073        assert!(denials.find(&scope, &thrown[0]).is_none(), "oldest dropped");
2074        let newest = denials.find(&scope, &thrown[MAX_THROWN_DENIALS]);
2075        assert_eq!(
2076            newest.map(|d| d.capability),
2077            Some(format!("cap.{MAX_THROWN_DENIALS}"))
2078        );
2079    }
2080
2081    #[tokio::test]
2082    async fn run_ending_errors_bypass_guest_catch_in_both_wrappers() {
2083        #[derive(Clone, Copy, PartialEq, Debug)]
2084        enum Failure {
2085            Ordinary,
2086            Fatal,
2087            MissingArgument,
2088            MissingResult,
2089            // What a body that re-entered guest code returns when that call trapped.
2090            NestedTrap(wasmtime::Trap),
2091        }
2092        let failures = [
2093            Failure::Ordinary,
2094            Failure::Fatal,
2095            Failure::MissingArgument,
2096            Failure::MissingResult,
2097            Failure::NestedTrap(wasmtime::Trap::StackOverflow),
2098            Failure::NestedTrap(wasmtime::Trap::OutOfFuel),
2099            Failure::NestedTrap(wasmtime::Trap::Interrupt),
2100            Failure::NestedTrap(wasmtime::Trap::UnreachableCodeReached),
2101            Failure::NestedTrap(wasmtime::Trap::CastFailure),
2102            Failure::NestedTrap(wasmtime::Trap::NullReference),
2103            Failure::NestedTrap(wasmtime::Trap::ArrayOutOfBounds),
2104        ];
2105        for asynchronous in [false, true] {
2106            for kind in failures {
2107                let (engine, mut store, mut linker) = async_store();
2108                crate::runtime::install_runtime_async(&mut linker, &mut store)
2109                    .await
2110                    .expect("runtime");
2111                let name = crate::mangle::host(TEST_MODULE, "failure");
2112                let failure = move || match kind {
2113                    Failure::Fatal => fatal_host_error("test operation: invalid state")
2114                        .context("outer host context"),
2115                    Failure::NestedTrap(trap) => {
2116                        wasmtime::Error::new(trap).context("outer host context")
2117                    }
2118                    Failure::MissingArgument => abi_arg(&[], usize::MAX).unwrap_err(),
2119                    Failure::MissingResult => abi_result(&mut [], usize::MAX).unwrap_err(),
2120                    Failure::Ordinary => wasmtime::Error::msg("ordinary operation failure"),
2121                };
2122                let ty = FuncType::new(&engine, [], []);
2123                if asynchronous {
2124                    register_host_fn_async(
2125                        &mut linker,
2126                        TEST_MODULE,
2127                        name.clone(),
2128                        ty,
2129                        move |_, _, _| Box::pin(async move { Err(failure()) }),
2130                    )
2131                    .expect("async registration");
2132                } else {
2133                    register_host_fn(
2134                        &mut linker,
2135                        TEST_MODULE,
2136                        name.clone(),
2137                        ty,
2138                        move |_, _, _| Err(failure()),
2139                    )
2140                    .expect("sync registration");
2141                }
2142                let source = format!(
2143                    r#"(module
2144                    (import "{TEST_MODULE}" "{name}" (func $failure))
2145                    (func (export "attempt") (result i32)
2146                        (block $caught
2147                            (try_table (catch_all $caught) (call $failure))
2148                            (return (i32.const 0)))
2149                        (i32.const 1))
2150                    (func (export "healthy") (result i32) (i32.const 42)))"#
2151                );
2152                let buffer = wast::parser::ParseBuffer::new(&source).expect("WAT tokens");
2153                let mut wat = wast::parser::parse::<wast::Wat>(&buffer).expect("WAT");
2154                let module =
2155                    wasmtime::Module::new(&engine, wat.encode().expect("encode")).expect("module");
2156                let instance = linker
2157                    .instantiate_async(&mut store, &module)
2158                    .await
2159                    .expect("instance");
2160                let attempt = instance
2161                    .get_typed_func::<(), i32>(&mut store, "attempt")
2162                    .expect("attempt");
2163                let result = attempt.call_async(&mut store, ()).await;
2164                match kind {
2165                    Failure::Fatal => {
2166                        let err =
2167                            result.expect_err("fatal failure cannot enter the guest catch handler");
2168                        assert!(err.is::<FatalHostError>(), "typed cause lost: {err:#}");
2169                        assert!(format!("{err:#}").contains("invalid state"));
2170                    }
2171                    Failure::MissingArgument | Failure::MissingResult => {
2172                        let err = result.expect_err("ABI invariants bypass guest catch");
2173                        assert_eq!(
2174                            err.downcast_ref::<wasmtime::Trap>(),
2175                            Some(&wasmtime::Trap::UnreachableCodeReached)
2176                        );
2177                    }
2178                    Failure::NestedTrap(trap) => {
2179                        let err = result.expect_err("a trap cannot enter the guest catch handler");
2180                        assert_eq!(
2181                            err.downcast_ref::<wasmtime::Trap>(),
2182                            Some(&trap),
2183                            "trap kind lost: {err:#}"
2184                        );
2185                    }
2186                    Failure::Ordinary => {
2187                        assert_eq!(result.expect("guest catches ordinary failure"), 1);
2188                    }
2189                }
2190                let healthy = instance
2191                    .get_typed_func::<(), i32>(&mut store, "healthy")
2192                    .expect("healthy");
2193                assert_eq!(
2194                    healthy
2195                        .call_async(&mut store, ())
2196                        .await
2197                        .expect("subsequent call"),
2198                    42
2199                );
2200            }
2201        }
2202    }
2203
2204    const TEST_MODULE: &str = "test:sync";
2205
2206    /// An async-enabled store plus an empty linker — no guest module is needed to
2207    /// observe which engine registration kind a wrapper produced.
2208    fn async_store() -> (Engine, Store<StoreData>, Linker<StoreData>) {
2209        let cfg = crate::runtime::RuntimeConfig::default();
2210        let engine = cfg.engine().expect("engine");
2211        let data = StoreData::with_vfs(
2212            "test-run".into(),
2213            crate::runtime::Vfs::tempdir().expect("tempdir"),
2214        );
2215        let store = cfg.store_async(&engine, data).expect("store");
2216        let linker = Linker::<StoreData>::new(&engine);
2217        (engine, store, linker)
2218    }
2219
2220    fn resolve(
2221        linker: &Linker<StoreData>,
2222        store: &mut Store<StoreData>,
2223        name: &crate::MangledName,
2224    ) -> wasmtime::Func {
2225        match linker
2226            .get(&mut *store, TEST_MODULE, name.as_str())
2227            .expect("registered function resolves")
2228        {
2229            wasmtime::Extern::Func(func) => func,
2230            other => panic!("expected a func, got {other:?}"),
2231        }
2232    }
2233
2234    /// The acceptance gate: a `register_host_fn` body must be a *synchronous*
2235    /// engine import. An async import rejects `Func::call` outright ("cannot call
2236    /// an async host function synchronously"), so a passing call is the proof.
2237    #[test]
2238    fn sync_bodies_are_callable_synchronously_on_an_async_store() {
2239        let (engine, mut store, mut linker) = async_store();
2240        let name = crate::mangle::host(TEST_MODULE, "answer");
2241        register_host_fn(
2242            &mut linker,
2243            TEST_MODULE,
2244            name.clone(),
2245            FuncType::new(&engine, [], [ValType::I32]),
2246            |_caller, _params, results| {
2247                results[0] = Val::I32(42);
2248                Ok(())
2249            },
2250        )
2251        .expect("register");
2252
2253        let func = resolve(&linker, &mut store, &name);
2254        let mut results = [Val::I32(0)];
2255        func.call(&mut store, &[], &mut results)
2256            .expect("synchronous call reaches the body");
2257        assert_eq!(results[0].i32(), Some(42));
2258    }
2259
2260    /// An ordinary `Err` still routes through the wrapper's error mapping. Without
2261    /// an instantiated prelude `throw_error_as` falls back to a plain-message
2262    /// error, which is what makes the message observable here; guest-level
2263    /// `try/catch` of a sync host error is covered end-to-end by the
2264    /// `host_error_catchable` and `classes/range_error_host_throws` fixtures
2265    /// (`String.normalize`, `String.repeat`, BigInt divide).
2266    #[test]
2267    fn an_ordinary_error_keeps_its_message() {
2268        let (engine, mut store, mut linker) = async_store();
2269        let name = crate::mangle::host(TEST_MODULE, "boom");
2270        register_host_fn(
2271            &mut linker,
2272            TEST_MODULE,
2273            name.clone(),
2274            FuncType::new(&engine, [], []),
2275            |_caller, _params, _results| Err(wasmtime::Error::msg("boom")),
2276        )
2277        .expect("register");
2278
2279        let func = resolve(&linker, &mut store, &name);
2280        let err = func
2281            .call(&mut store, &[], &mut [])
2282            .expect_err("the body fails");
2283        assert!(format!("{err}").contains("boom"), "got: {err}");
2284    }
2285
2286    #[test]
2287    fn fatal_host_errors_preserve_their_marker() {
2288        let (engine, mut store, mut linker) = async_store();
2289        let name = crate::mangle::host(TEST_MODULE, "fatal");
2290        register_host_fn(
2291            &mut linker,
2292            TEST_MODULE,
2293            name.clone(),
2294            FuncType::new(&engine, [], []),
2295            |_, _, _| Err(super::fatal_host_error("broken dynamic object ABI")),
2296        )
2297        .expect("register");
2298        let func = resolve(&linker, &mut store, &name);
2299        let error = func
2300            .call(&mut store, &[], &mut [])
2301            .expect_err("fatal host error");
2302        assert!(
2303            error.is::<super::FatalHostError>(),
2304            "fatal errors must bypass guest conversion: {error}"
2305        );
2306    }
2307
2308    #[tokio::test]
2309    async fn async_fatal_host_errors_preserve_their_marker() {
2310        let (engine, mut store, mut linker) = async_store();
2311        let name = crate::mangle::host(TEST_MODULE, "fatal_async");
2312        super::register_host_fn_async(
2313            &mut linker,
2314            TEST_MODULE,
2315            name.clone(),
2316            FuncType::new(&engine, [], []),
2317            |_, _, _| Box::pin(async { Err(super::fatal_host_error("broken dynamic object ABI")) }),
2318        )
2319        .expect("register");
2320        let func = resolve(&linker, &mut store, &name);
2321        let error = func
2322            .call_async(&mut store, &[], &mut [])
2323            .await
2324            .expect_err("fatal host error");
2325        assert!(
2326            error.is::<super::FatalHostError>(),
2327            "fatal errors must bypass guest conversion: {error}"
2328        );
2329    }
2330
2331    /// A body that already threw hands back a `ThrownException`; converting it
2332    /// again would clobber the exception pending on the store.
2333    #[test]
2334    fn a_thrown_exception_passes_through_unconverted() {
2335        let (engine, mut store, mut linker) = async_store();
2336        let name = crate::mangle::host(TEST_MODULE, "rethrow");
2337        register_host_fn(
2338            &mut linker,
2339            TEST_MODULE,
2340            name.clone(),
2341            FuncType::new(&engine, [], []),
2342            |_caller, _params, _results| Err(wasmtime::Error::new(wasmtime::ThrownException)),
2343        )
2344        .expect("register");
2345
2346        let func = resolve(&linker, &mut store, &name);
2347        let err = func
2348            .call(&mut store, &[], &mut [])
2349            .expect_err("the body throws");
2350        assert!(
2351            err.is::<wasmtime::ThrownException>(),
2352            "the pending exception must survive; got: {err}"
2353        );
2354    }
2355}