Skip to main content

submilli_engine/runtime/
host.rs

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