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