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