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 string without replacing lone surrogates at a UTF-8 boundary.
608pub(crate) fn read_string_arg_units(
609    caller: &mut Caller<'_, StoreData>,
610    val: &Val,
611    name: &str,
612) -> wasmtime::Result<Vec<u16>> {
613    let any = match val {
614        Val::AnyRef(Some(any)) => *any,
615        _ => return Err(type_error(format!("{name} expects a string"))),
616    };
617    let string = any.unwrap_struct(&mut *caller)?;
618    let raw = match string.field(&mut *caller, 1)? {
619        Val::AnyRef(Some(raw)) => raw.unwrap_array(&mut *caller)?,
620        _ => {
621            return Err(fatal_host_error(format!(
622                "{name}: malformed string payload"
623            )));
624        }
625    };
626    read_code_units(caller, raw, name)
627}
628
629/// Read a real `$Array<$string>` into `Vec<String>`. Null slots → empty string.
630pub fn read_string_array_arg(
631    caller: &mut Caller<'_, StoreData>,
632    val: &Val,
633    name: &str,
634) -> wasmtime::Result<Vec<String>> {
635    let any = match val {
636        Val::AnyRef(Some(any)) => *any,
637        Val::AnyRef(None) => return Err(type_error(format!("{name} arg is null"))),
638        other => {
639            return Err(type_error(format!(
640                "{name} expects a string[], got {other:?}"
641            )));
642        }
643    };
644    let object = any
645        .as_struct(&mut *caller)?
646        .ok_or_else(|| type_error(format!("{name}: expected $Array struct")))?;
647    let storage = super::array_storage::ArrayStorage::from_struct(caller, object)?;
648    let raw = storage.backing;
649    let len = storage.len;
650    let mut out = Vec::new();
651    out.try_reserve_exact(len as usize)
652        .map_err(fatal_host_error)?;
653    for i in 0..len {
654        match raw.get(&mut *caller, i)? {
655            Val::AnyRef(Some(elem)) => out.push(read_string_from_anyref(caller, elem, name)?),
656            Val::AnyRef(None) => out.push(String::new()),
657            other => {
658                return Err(type_error(format!(
659                    "{name}: slot {i} expects a $string, got {other:?}"
660                )));
661            }
662        }
663    }
664    Ok(out)
665}
666
667/// Encodes a Rust string as a Submilli packed-UTF-16 `(array (mut i16))` —
668/// the bare `$rawString` payload, without the `$string` object wrapper.
669pub fn write_submilli_string(
670    mut ctx: impl AsContextMut<Data = StoreData>,
671    s: &str,
672) -> wasmtime::Result<Rooted<ArrayRef>> {
673    let units = encode_utf16(&mut ctx, s)?;
674    write_code_units(ctx, &units)
675}
676
677/// UTF-8 to UTF-16, charged as a scan of the input.
678pub(crate) fn encode_utf16(
679    ctx: impl AsContextMut<Data = StoreData>,
680    s: &str,
681) -> wasmtime::Result<Vec<u16>> {
682    fuel::charge(ctx, fuel::SCAN, s.len() as u64)?;
683    // A UTF-8 byte count bounds its UTF-16 unit count. Reserve fallibly once
684    // before encoding, so extend cannot trigger infallible capacity growth.
685    let mut units = Vec::new();
686    units.try_reserve_exact(s.len()).map_err(fatal_host_error)?;
687    units.extend(s.encode_utf16());
688    Ok(units)
689}
690
691/// Builds a `$rawString` payload from UTF-16 code units in one pass. Every
692/// string result the host builds from units ends here, so this is where its
693/// copy is charged.
694pub(crate) fn write_code_units(
695    mut ctx: impl AsContextMut<Data = StoreData>,
696    units: &[u16],
697) -> wasmtime::Result<Rooted<ArrayRef>> {
698    fuel::charge(&mut ctx, fuel::COPY, units.len() as u64)?;
699    let array_ty = string_array_type(ctx.as_context().engine());
700    let pre = ArrayRefPre::new(&mut ctx, array_ty);
701    ArrayRef::new_from_i16_slice(&mut ctx, &pre, units)
702}
703
704/// A `$rawString` payload's UTF-16 code units, copied in one pass rather than
705/// one `get` per unit: string host functions call this on every receiver, so
706/// this is where the copy of every string argument is charged.
707///
708/// A payload that is not an `i16` array is a catchable error labelled `name`:
709/// a program can reach this with a non-string, through a `toJson` inserted
710/// into a `Record`.
711pub(crate) fn read_code_units(
712    mut ctx: impl AsContextMut<Data = StoreData>,
713    raw: Rooted<ArrayRef>,
714    name: &str,
715) -> wasmtime::Result<Vec<u16>> {
716    let len = raw.len(&mut ctx)?;
717    fuel::charge(&mut ctx, fuel::COPY, u64::from(len))?;
718    let len = usize::try_from(len).map_err(fatal_host_error)?;
719    let mut units = Vec::new();
720    units.try_reserve_exact(len).map_err(fatal_host_error)?;
721    units.resize(len, 0);
722    raw.copy_to_i16_slice(&mut ctx, &mut units)
723        .map_err(|error| wasmtime::Error::msg(format!("{name}: {error}")))?;
724    Ok(units)
725}
726
727/// `len` code units of a `$rawString` payload from `offset`, copied in one
728/// pass and charged for what is copied, so an accessor that needs a few units
729/// of a long string does not pay for all of it. Only typed `$string` receivers
730/// reach this, so a failure is the host's own mistake, not the program's.
731pub(crate) fn read_code_units_range(
732    mut ctx: impl AsContextMut<Data = StoreData>,
733    raw: Rooted<ArrayRef>,
734    offset: usize,
735    len: usize,
736) -> wasmtime::Result<Vec<u16>> {
737    fuel::charge(&mut ctx, fuel::COPY, len as u64)?;
738    let offset = u32::try_from(offset).map_err(fatal_host_error)?;
739    let mut units = Vec::new();
740    units.try_reserve_exact(len).map_err(fatal_host_error)?;
741    units.resize(len, 0);
742    raw.read_i16(&mut ctx, offset, &mut units)
743        .map_err(fatal_host_error)?;
744    Ok(units)
745}
746
747/// One code unit of a `$rawString` payload; see [`read_code_units_range`].
748pub(crate) fn read_code_unit(
749    mut ctx: impl AsContextMut<Data = StoreData>,
750    raw: Rooted<ArrayRef>,
751    index: usize,
752) -> wasmtime::Result<u16> {
753    let index = u32::try_from(index).map_err(fatal_host_error)?;
754    match raw.get(&mut ctx, index).map_err(fatal_host_error)? {
755        Val::I32(unit) => Ok(unit as u16),
756        other => Err(fatal_host_error(format!("code unit {index} is {other:?}"))),
757    }
758}
759
760/// Runtime handles host functions use to build *real* `$Object`-subtype structs
761/// (currently `$string`/`$Array`/`$Uint8Array`) on the host side instead of
762/// returning the raw payload array for a Wasm shim to re-wrap. The canonical
763/// `StructType`/`ArrayType` handles are declared directly via `RecGroupBuilder`
764/// (see [`build_intrinsic_types`]); the vtable value is read from the prelude
765/// instance per call.
766pub struct HostAbi {
767    pub(crate) member_functions: std::collections::BTreeMap<String, wasmtime::Func>,
768    pub(crate) string_type: StructType,
769    pub(crate) uint8_type: StructType,
770    pub(crate) array_type: StructType,
771    pub(crate) raw_array_type: ArrayType,
772    pub(crate) boxed_number_type: StructType,
773    pub(crate) boxed_boolean_type: StructType,
774    pub(crate) field_names_type: ArrayType,
775    pub(crate) object_shape_type: StructType,
776    pub(crate) object_fields_type: ArrayType,
777    // The collection backings. They are `$Object` subtypes sharing the object
778    // vtable, so the universal slots reach them but must not read their bucket
779    // arrays as an `$ObjectShape` payload — and that check runs once per node
780    // of the structural walk, so the types are recovered here rather than
781    // rebuilt per call.
782    pub(crate) map_backing_type: StructType,
783    pub(crate) set_backing_type: StructType,
784    // The host-owned vtable globals, kept as direct handles so host fns read
785    // them straight from the store instead of via the prelude's re-exports.
786    pub(crate) string_vtable: Global,
787    pub(crate) array_vtable: Global,
788    pub(crate) object_vtable: Global,
789    pub(crate) boxed_number_vtable: Global,
790    pub(crate) boxed_boolean_vtable: Global,
791    pub(crate) uint8_array_vtable: Global,
792    pub(crate) bigint_vtable: Global,
793    pub(crate) closure_vtable: Global,
794    pub(crate) regex_vtable: Global,
795    pub(crate) regex_match_box_vtable: Global,
796    /// Identity vtable for host-only backing structs (stdlib `URL`, `Response`,
797    /// fs `Stat`, …) — opaque `toString`/`toJson`, reference equality.
798    pub(crate) opaque_vtable: Global,
799    pub(crate) temporal: super::prelude::temporal::TemporalAbi,
800    // The host-owned `Map`/`Set` tombstone sentinel — a bare `$Object` marking a
801    // deleted probe slot. Compared by reference identity during probing.
802    pub(crate) map_tombstone: Global,
803    pub(crate) collection_null: Global,
804    pub(crate) error_type: StructType,
805    /// The one struct type shared by every built-in `Error` subclass —
806    /// identical shape, so they canonicalize together; the vtables carry the
807    /// identity.
808    pub(crate) error_subclass_type: StructType,
809    pub(crate) error_vtable: Global,
810    pub(crate) range_error_vtable: Global,
811    pub(crate) quota_exceeded_vtable: Global,
812    pub(crate) type_error_vtable: Global,
813    pub(crate) syntax_error_vtable: Global,
814    pub(crate) uri_error_vtable: Global,
815    pub(crate) reference_error_vtable: Global,
816    pub(crate) permission_denied_vtable: Global,
817    pub(crate) timeout_error_vtable: Global,
818    pub(crate) error_field_names: Global,
819    pub(crate) permission_denied_field_names: Global,
820    pub(crate) error_tag: wasmtime::Tag,
821}
822
823/// The store-bound handles the host Error constructors read per call.
824pub(crate) struct HostAbiHandles {
825    pub error_type: StructType,
826    pub error_subclass_type: StructType,
827    pub object_fields_type: ArrayType,
828    pub error_vtable: Global,
829    pub range_error_vtable: Global,
830    pub quota_exceeded_vtable: Global,
831    pub type_error_vtable: Global,
832    pub syntax_error_vtable: Global,
833    pub uri_error_vtable: Global,
834    pub reference_error_vtable: Global,
835    pub permission_denied_vtable: Global,
836    pub timeout_vtable: Global,
837    pub error_field_names: Global,
838    pub permission_denied_field_names: Global,
839}
840
841pub(crate) fn error_abi(caller: &Caller<'_, StoreData>) -> wasmtime::Result<HostAbiHandles> {
842    let abi = caller
843        .data()
844        .host_abi
845        .as_ref()
846        .ok_or_else(|| wasmtime::Error::msg("host_abi unset (prelude not instantiated)"))?;
847    Ok(HostAbiHandles {
848        error_type: abi.error_type.clone(),
849        error_subclass_type: abi.error_subclass_type.clone(),
850        object_fields_type: abi.object_fields_type.clone(),
851        error_vtable: abi.error_vtable,
852        range_error_vtable: abi.range_error_vtable,
853        quota_exceeded_vtable: abi.quota_exceeded_vtable,
854        type_error_vtable: abi.type_error_vtable,
855        syntax_error_vtable: abi.syntax_error_vtable,
856        uri_error_vtable: abi.uri_error_vtable,
857        reference_error_vtable: abi.reference_error_vtable,
858        permission_denied_vtable: abi.permission_denied_vtable,
859        timeout_vtable: abi.timeout_error_vtable,
860        error_field_names: abi.error_field_names,
861        permission_denied_field_names: abi.permission_denied_field_names,
862    })
863}
864
865/// Read a host-owned vtable global from the cached handle in [`HostAbi`]. Works
866/// from any store context (a `Caller` or a `StoreContextMut`).
867fn host_vtable(
868    ctx: &mut impl AsContextMut<Data = StoreData>,
869    select: impl FnOnce(&HostAbi) -> Global,
870) -> wasmtime::Result<Val> {
871    let global =
872        {
873            let abi =
874                ctx.as_context().data().host_abi.as_ref().ok_or_else(|| {
875                    wasmtime::Error::msg("host_abi unset (prelude not instantiated)")
876                })?;
877            select(abi)
878        };
879    Ok(global.get(&mut *ctx))
880}
881
882pub(crate) fn host_string_vtable(
883    ctx: &mut impl AsContextMut<Data = StoreData>,
884) -> wasmtime::Result<Val> {
885    host_vtable(ctx, |abi| abi.string_vtable)
886}
887
888pub(crate) fn host_array_vtable(
889    ctx: &mut impl AsContextMut<Data = StoreData>,
890) -> wasmtime::Result<Val> {
891    host_vtable(ctx, |abi| abi.array_vtable)
892}
893
894pub(crate) fn host_object_vtable(
895    ctx: &mut impl AsContextMut<Data = StoreData>,
896) -> wasmtime::Result<Val> {
897    host_vtable(ctx, |abi| abi.object_vtable)
898}
899
900pub(crate) fn host_boxed_number_vtable(
901    ctx: &mut impl AsContextMut<Data = StoreData>,
902) -> wasmtime::Result<Val> {
903    host_vtable(ctx, |abi| abi.boxed_number_vtable)
904}
905
906pub(crate) fn host_boxed_boolean_vtable(
907    ctx: &mut impl AsContextMut<Data = StoreData>,
908) -> wasmtime::Result<Val> {
909    host_vtable(ctx, |abi| abi.boxed_boolean_vtable)
910}
911
912pub(crate) fn host_uint8_array_vtable(
913    ctx: &mut impl AsContextMut<Data = StoreData>,
914) -> wasmtime::Result<Val> {
915    host_vtable(ctx, |abi| abi.uint8_array_vtable)
916}
917
918pub(crate) fn host_bigint_vtable(
919    ctx: &mut impl AsContextMut<Data = StoreData>,
920) -> wasmtime::Result<Val> {
921    host_vtable(ctx, |abi| abi.bigint_vtable)
922}
923
924pub(crate) fn host_closure_vtable(
925    ctx: &mut impl AsContextMut<Data = StoreData>,
926) -> wasmtime::Result<Val> {
927    host_vtable(ctx, |abi| abi.closure_vtable)
928}
929
930pub(crate) fn host_regex_vtable(
931    ctx: &mut impl AsContextMut<Data = StoreData>,
932) -> wasmtime::Result<Val> {
933    host_vtable(ctx, |abi| abi.regex_vtable)
934}
935
936pub(crate) fn host_regex_match_box_vtable(
937    ctx: &mut impl AsContextMut<Data = StoreData>,
938) -> wasmtime::Result<Val> {
939    host_vtable(ctx, |abi| abi.regex_match_box_vtable)
940}
941
942pub(crate) fn host_opaque_vtable(
943    ctx: &mut impl AsContextMut<Data = StoreData>,
944) -> wasmtime::Result<Val> {
945    host_vtable(ctx, |abi| abi.opaque_vtable)
946}
947
948/// Read the host-owned `Map`/`Set` tombstone sentinel from [`HostAbi`].
949pub(crate) fn host_map_tombstone(
950    ctx: &mut impl AsContextMut<Data = StoreData>,
951) -> wasmtime::Result<Val> {
952    host_vtable(ctx, |abi| abi.map_tombstone)
953}
954/// Private non-null representation of a null collection key.
955pub(crate) fn host_collection_null(
956    ctx: &mut impl AsContextMut<Data = StoreData>,
957) -> wasmtime::Result<Val> {
958    host_vtable(ctx, |abi| abi.collection_null)
959}
960
961/// Build a real `$string` (vtable + packed-UTF-16 payload). Requires
962/// `StoreData::host_abi`, set once the prelude instantiates.
963pub fn write_submilli_string_struct(
964    caller: &mut Caller<'_, StoreData>,
965    s: &str,
966) -> wasmtime::Result<Rooted<StructRef>> {
967    let units = encode_utf16(&mut *caller, s)?;
968    write_submilli_string_struct_units(caller, &units)
969}
970
971/// Build a real `$string` directly from UTF-16 code units — the
972/// surrogate-faithful path for callers (e.g. `String.fromCharCode`) whose
973/// output may contain lone surrogates that a Rust `String` can't carry.
974/// Requires `StoreData::host_abi`, set once the prelude instantiates.
975pub fn write_submilli_string_struct_units(
976    caller: &mut Caller<'_, StoreData>,
977    units: &[u16],
978) -> wasmtime::Result<Rooted<StructRef>> {
979    // `write_code_units` charges the copy.
980    let raw = write_code_units(&mut *caller, units)?;
981    let string_type = {
982        let abi = caller
983            .data()
984            .host_abi
985            .as_ref()
986            .ok_or_else(|| wasmtime::Error::msg("host_abi unset (prelude not instantiated)"))?;
987        abi.string_type.clone()
988    };
989    let vtable = host_string_vtable(caller)?;
990    let pre = StructRefPre::new(&mut *caller, string_type);
991    StructRef::new(
992        &mut *caller,
993        &pre,
994        &[vtable, Val::AnyRef(Some(raw.to_anyref())), Val::I64(0)],
995    )
996}
997
998/// Build a real `$Uint8Array` (vtable + packed-i8 payload). Mirror of
999/// [`write_submilli_string_struct`]. Requires `StoreData::host_abi`.
1000pub fn write_submilli_uint8array_struct(
1001    caller: &mut Caller<'_, StoreData>,
1002    bytes: &[u8],
1003) -> wasmtime::Result<Rooted<StructRef>> {
1004    let array_ty = uint8_array_array_type(caller.engine());
1005    let raw = write_uint8_array(&mut *caller, array_ty, bytes)?;
1006    let uint8_type = {
1007        let abi = caller
1008            .data()
1009            .host_abi
1010            .as_ref()
1011            .ok_or_else(|| wasmtime::Error::msg("host_abi unset (prelude not instantiated)"))?;
1012        abi.uint8_type.clone()
1013    };
1014    let vtable = host_uint8_array_vtable(caller)?;
1015    let pre = StructRefPre::new(&mut *caller, uint8_type);
1016    StructRef::new(
1017        &mut *caller,
1018        &pre,
1019        &[vtable, Val::AnyRef(Some(raw.to_anyref()))],
1020    )
1021}
1022
1023/// Unbox a `$boxed_number` value to its `f64` payload (field 1).
1024pub(crate) fn read_boxed_number(
1025    caller: &mut Caller<'_, StoreData>,
1026    val: &Val,
1027    context: &str,
1028) -> wasmtime::Result<f64> {
1029    let Val::AnyRef(Some(any)) = val else {
1030        return Err(type_error(format!("{context}: expected a boxed number")));
1031    };
1032    let st = any
1033        .as_struct(&mut *caller)?
1034        .ok_or_else(|| type_error(format!("{context}: expected a boxed number")))?;
1035    match st.field(&mut *caller, 1)? {
1036        Val::F64(bits) => Ok(f64::from_bits(bits)),
1037        other => Err(wasmtime::Error::msg(format!(
1038            "{context}: boxed-number payload is {other:?}"
1039        ))),
1040    }
1041}
1042
1043/// Box an `f64` into a `$boxed_number` — the erased-object form of a `number`
1044/// (e.g. a `number | null` field or result). Requires `StoreData::host_abi`.
1045pub(crate) fn write_boxed_number_struct(
1046    caller: &mut Caller<'_, StoreData>,
1047    n: f64,
1048) -> wasmtime::Result<Rooted<StructRef>> {
1049    let boxed_type = {
1050        let abi = caller
1051            .data()
1052            .host_abi
1053            .as_ref()
1054            .ok_or_else(|| wasmtime::Error::msg("host_abi unset (prelude not instantiated)"))?;
1055        abi.boxed_number_type.clone()
1056    };
1057    let vtable = host_boxed_number_vtable(caller)?;
1058    let pre = StructRefPre::new(&mut *caller, boxed_type);
1059    StructRef::new(&mut *caller, &pre, &[vtable, Val::F64(n.to_bits())])
1060}
1061
1062/// Build a real `$Array` (vtable + `$rawArray` backing) from already-built
1063/// element object refs (e.g. `$string`s for a `string[]`). Each element `Val`
1064/// must be a `(ref null $Object)` — a subtype ref or `Val::AnyRef(None)`.
1065pub fn write_submilli_array_struct(
1066    caller: &mut Caller<'_, StoreData>,
1067    elements: &[Val],
1068) -> wasmtime::Result<Rooted<StructRef>> {
1069    let len = super::array_storage::checked_length(elements.len())?;
1070    fuel::charge(&mut *caller, fuel::ELEM, u64::from(len))?;
1071    write_submilli_array_struct_precharged(caller, elements)
1072}
1073
1074/// The caller has admitted ELEM once for each element while producing it.
1075pub(crate) fn write_submilli_array_struct_precharged(
1076    caller: &mut Caller<'_, StoreData>,
1077    elements: &[Val],
1078) -> wasmtime::Result<Rooted<StructRef>> {
1079    let len = super::array_storage::checked_length(elements.len())?;
1080    let (array_type, raw_array_type) = {
1081        let abi = caller
1082            .data()
1083            .host_abi
1084            .as_ref()
1085            .ok_or_else(|| wasmtime::Error::msg("host_abi unset (prelude not instantiated)"))?;
1086        (abi.array_type.clone(), abi.raw_array_type.clone())
1087    };
1088    let raw_pre = ArrayRefPre::new(&mut *caller, raw_array_type);
1089    let raw = ArrayRef::new_fixed(&mut *caller, &raw_pre, elements)?;
1090    let vtable = host_array_vtable(caller)?;
1091    let pre = StructRefPre::new(&mut *caller, array_type);
1092    StructRef::new(
1093        &mut *caller,
1094        &pre,
1095        &[
1096            vtable,
1097            Val::AnyRef(Some(raw.to_anyref())),
1098            Val::I32(len as i32),
1099        ],
1100    )
1101}
1102
1103/// Marker for a host failure that should surface to the guest as the built-in
1104/// `RangeError` subclass rather than a base `Error`. Return
1105/// `Err(range_error(...))` from a host-fn body; the `register_host_fn` wrapper
1106/// downcasts for it when converting the `Err` into a guest throw. The message
1107/// is the guest-visible `e.message`.
1108#[derive(Debug)]
1109pub struct RangeError(pub String);
1110
1111impl std::fmt::Display for RangeError {
1112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1113        f.write_str(&self.0)
1114    }
1115}
1116
1117impl std::error::Error for RangeError {}
1118
1119/// A host failure that throws the built-in `RangeError` at the guest boundary.
1120pub fn range_error(message: impl Into<String>) -> wasmtime::Error {
1121    wasmtime::Error::new(RangeError(message.into()))
1122}
1123
1124/// Marker for a host failure that should surface to the guest as the built-in
1125/// `QuotaExceededError` subclass rather than a base `Error`. Return
1126/// `Err(quota_exceeded_error(...))` from a host-fn body; the `register_host_fn` wrapper
1127/// downcasts for it when converting the `Err` into a guest throw. The message
1128/// is the guest-visible `e.message`.
1129#[derive(Debug)]
1130pub struct QuotaExceededError(pub String);
1131
1132impl std::fmt::Display for QuotaExceededError {
1133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1134        f.write_str(&self.0)
1135    }
1136}
1137
1138impl std::error::Error for QuotaExceededError {}
1139
1140/// A host failure that throws the built-in `QuotaExceededError` at the guest boundary.
1141pub fn quota_exceeded_error(message: impl Into<String>) -> wasmtime::Error {
1142    wasmtime::Error::new(QuotaExceededError(message.into()))
1143}
1144
1145/// Marker for a host failure that should surface to the guest as the built-in
1146/// `TimeoutError` subclass: a wait outside the program ran out of time. Same
1147/// contract as [`QuotaExceededError`].
1148#[derive(Debug)]
1149pub struct TimeoutError(pub String);
1150
1151impl std::fmt::Display for TimeoutError {
1152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1153        f.write_str(&self.0)
1154    }
1155}
1156
1157impl std::error::Error for TimeoutError {}
1158
1159/// A host failure that throws the built-in `TimeoutError` at the guest boundary.
1160pub fn timeout_error(message: impl Into<String>) -> wasmtime::Error {
1161    wasmtime::Error::new(TimeoutError(message.into()))
1162}
1163
1164/// Marker for a host failure that should surface to the guest as the built-in
1165/// `SyntaxError` subclass — same contract as [`RangeError`].
1166#[derive(Debug)]
1167pub struct SyntaxError(pub String);
1168
1169impl std::fmt::Display for SyntaxError {
1170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1171        f.write_str(&self.0)
1172    }
1173}
1174
1175impl std::error::Error for SyntaxError {}
1176
1177/// A host failure that throws the built-in `SyntaxError` at the guest boundary.
1178pub fn syntax_error(message: impl Into<String>) -> wasmtime::Error {
1179    wasmtime::Error::new(SyntaxError(message.into()))
1180}
1181
1182/// Marker for a host failure that should surface to the guest as the built-in
1183/// `URIError` subclass — same contract as [`RangeError`].
1184#[derive(Debug)]
1185pub struct UriError(pub String);
1186
1187impl std::fmt::Display for UriError {
1188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1189        f.write_str(&self.0)
1190    }
1191}
1192
1193impl std::error::Error for UriError {}
1194
1195/// A host failure that throws the built-in `URIError` at the guest boundary.
1196pub fn uri_error(message: impl Into<String>) -> wasmtime::Error {
1197    wasmtime::Error::new(UriError(message.into()))
1198}
1199
1200/// Marker for a host failure that should surface to the guest as the built-in
1201/// `TypeError` subclass — same contract as [`RangeError`]. Used for
1202/// argument-boundary type mismatches (a null or wrong-typed value where the
1203/// ABI promised another type) and for spec-`TypeError` conditions (invalid
1204/// URL, fatal text decode, unsupported HTTP method).
1205#[derive(Debug)]
1206pub struct TypeError(pub String);
1207
1208impl std::fmt::Display for TypeError {
1209    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1210        f.write_str(&self.0)
1211    }
1212}
1213
1214impl std::error::Error for TypeError {}
1215
1216/// A host failure that throws the built-in `TypeError` at the guest boundary.
1217pub fn type_error(message: impl Into<String>) -> wasmtime::Error {
1218    wasmtime::Error::new(TypeError(message.into()))
1219}
1220
1221/// Marker for a capability denial that should surface to the guest as the
1222/// built-in `PermissionDeniedError` subclass, carrying the structured fields
1223/// policy-aware recovery code reads (`e.capability`, `e.caller`, `e.reason`).
1224/// Same contract as [`RangeError`].
1225#[derive(Debug)]
1226#[non_exhaustive]
1227pub struct PermissionDenied {
1228    pub caller: String,
1229    pub capability: String,
1230    pub reason: String,
1231    source: DenialSource,
1232}
1233
1234/// Which layer refused. Absent from the guest ABI: the guest sees
1235/// `caller`/`capability`/`reason` as before. It selects the closing paragraph of
1236/// the rendered message and, for a denial that escapes the program, the
1237/// `source` the embedder reports.
1238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1239pub enum DenialSource {
1240    /// The operator's configured policy said no.
1241    Policy,
1242    /// A runtime invariant refused ahead of the policy. No rule can grant it,
1243    /// so the message must not suggest asking for one.
1244    Invariant,
1245    /// The path is in a volume mounted read-only. Unlike a policy decision,
1246    /// writing somewhere else is a legitimate response.
1247    ReadOnly,
1248    /// The harness, or a person it asked, refused an application tool call.
1249    Harness,
1250}
1251
1252/// A denial the runtime threw that escaped the program, as the embedder sees it.
1253#[derive(Debug, Clone, PartialEq, Eq)]
1254pub struct Denial {
1255    pub caller: String,
1256    pub capability: String,
1257    pub source: DenialSource,
1258}
1259
1260impl DenialSource {
1261    pub fn as_str(self) -> &'static str {
1262        match self {
1263            Self::Policy => "policy",
1264            Self::Invariant => "invariant",
1265            Self::ReadOnly => "read_only",
1266            Self::Harness => "harness",
1267        }
1268    }
1269}
1270
1271/// The denials the runtime itself threw, keyed by the generation-stamped handle
1272/// of the thrown `$Error`. A program-constructed `PermissionDeniedError` is
1273/// never entered, so it can be told apart from a real denial when it escapes.
1274#[derive(Default)]
1275pub(crate) struct ThrownDenials {
1276    entries: std::collections::VecDeque<(Rooted<StructRef>, Denial)>,
1277}
1278
1279/// Bound on the table. Past it the oldest entries go, so at worst an old
1280/// caught-and-rethrown denial escapes as an ordinary runtime error.
1281const MAX_THROWN_DENIALS: usize = 64;
1282
1283impl ThrownDenials {
1284    /// Drops entries whose object was collected, then adds `denial`.
1285    fn record(&mut self, store: impl AsContext, handle: Rooted<StructRef>, denial: Denial) {
1286        self.entries.retain(|(entry, _)| entry.ty(&store).is_ok());
1287        if self.entries.len() >= MAX_THROWN_DENIALS {
1288            self.entries.pop_front();
1289        }
1290        self.entries.push_back((handle, denial));
1291    }
1292
1293    /// The denial whose thrown object is `thrown`. An entry matches only when
1294    /// its generation check still succeeds: a collected object's slot may now
1295    /// hold a program-built error with the same index.
1296    pub(crate) fn find(
1297        &mut self,
1298        store: impl AsContext,
1299        thrown: &Rooted<AnyRef>,
1300    ) -> Option<Denial> {
1301        self.entries.retain(|(entry, _)| entry.ty(&store).is_ok());
1302        self.entries
1303            .iter()
1304            .find(|(entry, _)| Rooted::ref_eq(&store, entry, thrown).unwrap_or(false))
1305            .map(|(_, denial)| denial.clone())
1306    }
1307}
1308
1309/// Runs `f` on the store's denial table with the store readable alongside it.
1310///
1311/// Both [`ThrownDenials::record`] and [`ThrownDenials::find`] check handles against the
1312/// store, which cannot be borrowed while the table inside its data is. The table is moved
1313/// out for the call and put back here, so no caller can lose it on an early return.
1314pub(crate) fn with_thrown_denials<T: AsContextMut<Data = StoreData>, R>(
1315    store: &mut T,
1316    f: impl FnOnce(&mut ThrownDenials, &T) -> R,
1317) -> R {
1318    let mut table = std::mem::take(&mut store.as_context_mut().data_mut().thrown_denials);
1319    let result = f(&mut table, store);
1320    store.as_context_mut().data_mut().thrown_denials = table;
1321    result
1322}
1323
1324impl PermissionDenied {
1325    /// The fields an embedder reports when this denial escapes the program.
1326    fn denial(&self) -> Denial {
1327        Denial {
1328            caller: self.caller.clone(),
1329            capability: self.capability.clone(),
1330            source: self.source,
1331        }
1332    }
1333
1334    /// Whether the operator's policy refused, as opposed to a runtime invariant
1335    /// refusing ahead of it. A caller that treats a denial as a filter rather
1336    /// than an error — `session.list` omitting keys the policy hides — must act
1337    /// only on the policy's answer; an invariant denial means the check itself
1338    /// could not be made, which no filter may swallow.
1339    pub fn is_policy(&self) -> bool {
1340        matches!(self.source, DenialSource::Policy)
1341    }
1342}
1343
1344impl std::fmt::Display for PermissionDenied {
1345    /// The denial message an LLM sees. The closing paragraph is load-bearing:
1346    /// observed models treat a denial as an obstacle and reroute through raw
1347    /// HTTP or another package, so the message itself must say the decision is
1348    /// final. The two variants differ in what "final" means — a policy decision
1349    /// is the operator's to change, an invariant is nobody's.
1350    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1351        let Self {
1352            caller,
1353            capability,
1354            reason,
1355            source,
1356        } = self;
1357        write!(
1358            f,
1359            "permission denied: caller={caller} capability={capability}: {reason}."
1360        )?;
1361        f.write_str(match source {
1362            DenialSource::Policy => {
1363                " This request is forbidden by the operator's policy — do not work \
1364                 around the denial (another package, raw HTTP, or other arguments to \
1365                 reach the same thing); report it and stop."
1366            }
1367            DenialSource::Invariant => {
1368                " Do not work around this denial: not through another package, not by \
1369                 asking a package to fetch the value and hand it back, not through raw \
1370                 HTTP. Report it and stop."
1371            }
1372            DenialSource::ReadOnly => {
1373                " This volume cannot be written from this blueprint; write under a \
1374                 writable path instead (fs.info() lists each mount and its access)."
1375            }
1376            DenialSource::Harness => {
1377                " The application refused this call — do not work around the refusal \
1378                 (another tool, raw HTTP, or other arguments to reach the same thing); \
1379                 report it and stop."
1380            }
1381        })
1382    }
1383}
1384
1385impl std::error::Error for PermissionDenied {}
1386
1387/// A policy denial, thrown at the guest boundary as the built-in
1388/// `PermissionDeniedError`.
1389pub fn permission_denied(
1390    caller: impl Into<String>,
1391    capability: impl Into<String>,
1392    reason: impl Into<String>,
1393) -> wasmtime::Error {
1394    denied(caller, capability, reason, DenialSource::Policy)
1395}
1396
1397/// A denial the policy never got to weigh in on, because the runtime refuses
1398/// this caller/capability pair outright.
1399pub fn permission_denied_invariant(
1400    caller: impl Into<String>,
1401    capability: impl Into<String>,
1402    reason: impl Into<String>,
1403) -> wasmtime::Error {
1404    denied(caller, capability, reason, DenialSource::Invariant)
1405}
1406
1407/// A write into a volume mounted read-only.
1408pub fn permission_denied_read_only(
1409    caller: impl Into<String>,
1410    capability: impl Into<String>,
1411    reason: impl Into<String>,
1412) -> wasmtime::Error {
1413    denied(caller, capability, reason, DenialSource::ReadOnly)
1414}
1415
1416/// An application tool call the harness, or a person it asked, refused.
1417pub fn permission_denied_by_harness(
1418    caller: impl Into<String>,
1419    capability: impl Into<String>,
1420    reason: impl Into<String>,
1421) -> wasmtime::Error {
1422    denied(caller, capability, reason, DenialSource::Harness)
1423}
1424
1425fn denied(
1426    caller: impl Into<String>,
1427    capability: impl Into<String>,
1428    reason: impl Into<String>,
1429    source: DenialSource,
1430) -> wasmtime::Error {
1431    wasmtime::Error::new(PermissionDenied {
1432        caller: caller.into(),
1433        capability: capability.into(),
1434        reason: reason.into(),
1435        source,
1436    })
1437}
1438
1439fn builtin_class_of(err: &wasmtime::Error) -> super::prelude::error::BuiltinErrorClass {
1440    if err.downcast_ref::<RangeError>().is_some() {
1441        super::prelude::error::BuiltinErrorClass::Range
1442    } else if err.downcast_ref::<QuotaExceededError>().is_some() {
1443        super::prelude::error::BuiltinErrorClass::QuotaExceeded
1444    } else if err.downcast_ref::<TimeoutError>().is_some() {
1445        super::prelude::error::BuiltinErrorClass::Timeout
1446    } else if err.downcast_ref::<TypeError>().is_some() {
1447        super::prelude::error::BuiltinErrorClass::Type
1448    } else if err.downcast_ref::<SyntaxError>().is_some() {
1449        super::prelude::error::BuiltinErrorClass::Syntax
1450    } else if err.downcast_ref::<UriError>().is_some() {
1451        super::prelude::error::BuiltinErrorClass::Uri
1452    } else if err.downcast_ref::<PermissionDenied>().is_some() {
1453        super::prelude::error::BuiltinErrorClass::PermissionDenied
1454    } else {
1455        super::prelude::error::BuiltinErrorClass::Error
1456    }
1457}
1458
1459/// The own-field texts for the guest error instance, in payload-slot order
1460/// (matching `BuiltinErrorClass::own_fields`). Empty for field-less classes.
1461fn own_field_texts(err: &wasmtime::Error) -> Vec<&str> {
1462    match err.downcast_ref::<PermissionDenied>() {
1463        Some(pd) => vec![&pd.caller, &pd.capability, &pd.reason],
1464        None => Vec::new(),
1465    }
1466}
1467
1468/// Raise a catchable Submilli `Error` from inside a host function.
1469///
1470/// Host-function failures normally surface as Wasm traps, which `try/catch`
1471/// cannot intercept. This allocates the `$Error` directly (same construction
1472/// as the host `Error` constructor), wraps it in an exception object for the
1473/// host-owned tag, and throws it via the store — the engine unwinds to the
1474/// nearest `try_table` in the calling Wasm frame. `register_host_fn` wraps
1475/// every host body so any returned `Err` routes through here automatically;
1476/// the caller must `return Err(..)` the result unchanged.
1477///
1478/// Always returns an `Err`-carrying value. Before the prelude instantiates
1479/// (`host_abi` unset) it falls back to a plain-message error — an uncatchable
1480/// trap with the right text, matching the pre-catchable behavior.
1481pub fn throw_error(caller: &mut Caller<'_, StoreData>, message: &str) -> wasmtime::Error {
1482    throw_error_as(
1483        caller,
1484        super::prelude::error::BuiltinErrorClass::Error,
1485        message,
1486        &[],
1487    )
1488}
1489
1490/// A broken host invariant or exhausted host allocation must terminate the run,
1491/// rather than becoming an exception that guest code can catch and ignore.
1492/// Reaching the memory cap terminates it too, under its own name: see
1493/// [`is_memory_exhausted`](super::limits::is_memory_exhausted).
1494#[derive(Debug)]
1495pub struct FatalHostError(String);
1496
1497impl std::fmt::Display for FatalHostError {
1498    fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1499        write!(out, "internal host error: {}", self.0)
1500    }
1501}
1502
1503impl std::error::Error for FatalHostError {}
1504
1505/// Check the storage shape before a callback can perform effects. Concrete GC
1506/// subtype validation remains the engine's responsibility; guest dynamic values
1507/// are validated by the operation that consumes them.
1508pub(crate) fn check_host_abi(
1509    ty: &FuncType,
1510    params: &[Val],
1511    results: &[Val],
1512) -> wasmtime::Result<()> {
1513    if params.len() != ty.params().len() || results.len() != ty.results().len() {
1514        return Err(invariant_trap("host ABI: incorrect buffer length"));
1515    }
1516    for (value, expected) in params.iter().zip(ty.params()) {
1517        let valid = match (value, expected) {
1518            (Val::I32(_), ValType::I32)
1519            | (Val::I64(_), ValType::I64)
1520            | (Val::F32(_), ValType::F32)
1521            | (Val::F64(_), ValType::F64)
1522            | (Val::V128(_), ValType::V128) => true,
1523            (value, ValType::Ref(reference)) => {
1524                let nullable = reference.is_nullable();
1525                match (value, reference.heap_type()) {
1526                    (
1527                        Val::FuncRef(value),
1528                        HeapType::Func | HeapType::NoFunc | HeapType::ConcreteFunc(_),
1529                    ) => nullable || value.is_some(),
1530                    (Val::ExternRef(value), HeapType::Extern | HeapType::NoExtern) => {
1531                        nullable || value.is_some()
1532                    }
1533                    (Val::ExnRef(value), HeapType::Exn | HeapType::NoExn) => {
1534                        nullable || value.is_some()
1535                    }
1536                    (
1537                        Val::AnyRef(value),
1538                        HeapType::Any
1539                        | HeapType::Eq
1540                        | HeapType::I31
1541                        | HeapType::Struct
1542                        | HeapType::ConcreteStruct(_)
1543                        | HeapType::Array
1544                        | HeapType::ConcreteArray(_)
1545                        | HeapType::None,
1546                    ) => nullable || value.is_some(),
1547                    _ => false,
1548                }
1549            }
1550            _ => false,
1551        };
1552        if !valid {
1553            return Err(invariant_trap(
1554                "host ABI: incorrect argument representation",
1555            ));
1556        }
1557    }
1558    Ok(())
1559}
1560
1561pub(crate) fn abi_arg(params: &[Val], index: usize) -> wasmtime::Result<&Val> {
1562    params
1563        .get(index)
1564        .ok_or_else(|| invariant_trap("host ABI: missing argument"))
1565}
1566
1567pub(crate) fn abi_result(results: &mut [Val], index: usize) -> wasmtime::Result<&mut Val> {
1568    results
1569        .get_mut(index)
1570        .ok_or_else(|| invariant_trap("host ABI: missing result slot"))
1571}
1572
1573/// An invalid host invariant terminates execution without raising a guest exception.
1574pub(crate) fn invariant_trap(message: &'static str) -> wasmtime::Error {
1575    wasmtime::Error::new(wasmtime::Trap::UnreachableCodeReached).context(message)
1576}
1577
1578pub fn fatal_host_error(message: impl std::fmt::Display) -> wasmtime::Error {
1579    wasmtime::Error::new(FatalHostError(message.to_string()))
1580}
1581
1582/// Convert a host body's `Err` into the guest throw of the matching built-in
1583/// class, carrying any structured own fields the marker holds.
1584///
1585/// [`register_host_fn`] applies this at its boundary; a body installed as a
1586/// bare `Func` (the universal vtable slots) has to call it itself, or its
1587/// failure reaches the program as an uncatchable host error.
1588pub(crate) fn throw_host_error(
1589    caller: &mut Caller<'_, StoreData>,
1590    err: wasmtime::Error,
1591) -> wasmtime::Error {
1592    if ends_the_run(&err) || err.is::<wasmtime::ThrownException>() {
1593        return err;
1594    }
1595    let class = builtin_class_of(&err);
1596    let own = own_field_texts(&err);
1597    let denial = err
1598        .downcast_ref::<PermissionDenied>()
1599        .map(PermissionDenied::denial);
1600    throw_error_with(caller, class, &err.to_string(), &own, denial)
1601}
1602
1603/// Whether `err` must reach the embedder as it is instead of becoming a guest
1604/// throw. An engine trap is one: a host body that re-enters guest code (an
1605/// array callback, a getter under `JSON.stringify`) returns the nested call's
1606/// trap, and a program that could catch it there would outlive its fuel,
1607/// deadline or stack limit.
1608pub(crate) fn ends_the_run(err: &wasmtime::Error) -> bool {
1609    err.is::<FatalHostError>()
1610        || err.is::<wasmtime::Trap>()
1611        || super::limits::is_memory_exhausted(err)
1612}
1613
1614fn throw_error_as(
1615    caller: &mut Caller<'_, StoreData>,
1616    class: super::prelude::error::BuiltinErrorClass,
1617    message: &str,
1618    own_fields: &[&str],
1619) -> wasmtime::Error {
1620    throw_error_with(caller, class, message, own_fields, None)
1621}
1622
1623/// [`throw_error_as`], also remembering `denial` against the thrown object.
1624fn throw_error_with(
1625    caller: &mut Caller<'_, StoreData>,
1626    class: super::prelude::error::BuiltinErrorClass,
1627    message: &str,
1628    own_fields: &[&str],
1629    denial: Option<Denial>,
1630) -> wasmtime::Error {
1631    match throw_error_inner(caller, class, message, own_fields, denial) {
1632        Ok(err) => err,
1633        // The error could not be built because the run is out of memory, which
1634        // ends the run as such rather than as a host failure.
1635        Err(cause) if super::limits::is_memory_exhausted(&cause) => cause,
1636        Err(cause) => fatal_host_error(format!("{message}; error construction failed: {cause:#}")),
1637    }
1638}
1639
1640fn throw_error_inner(
1641    caller: &mut Caller<'_, StoreData>,
1642    class: super::prelude::error::BuiltinErrorClass,
1643    message: &str,
1644    own_fields: &[&str],
1645    denial: Option<Denial>,
1646) -> wasmtime::Result<wasmtime::Error> {
1647    let tag = caller
1648        .data()
1649        .host_abi
1650        .as_ref()
1651        .ok_or_else(|| wasmtime::Error::msg("host_abi unset (prelude not instantiated)"))?
1652        .error_tag;
1653    let error_struct =
1654        super::prelude::error::construct_from_message(caller, class, message, own_fields)?;
1655    let error = Val::AnyRef(Some(error_struct.to_anyref()));
1656    let exn_ty = wasmtime::ExnType::from_tag_type(&tag.ty(&*caller))?;
1657    let pre = wasmtime::ExnRefPre::new(&mut *caller, exn_ty);
1658    let exn = wasmtime::ExnRef::new(&mut *caller, &pre, &tag, &[error])?;
1659    match caller.as_context_mut().throw::<()>(exn) {
1660        Err(thrown) => {
1661            if let Some(denial) = denial {
1662                with_thrown_denials(caller, |table, store| {
1663                    table.record(store, error_struct, denial);
1664                });
1665            }
1666            Ok(wasmtime::Error::new(thrown))
1667        }
1668        // `throw` always returns the pending-exception error.
1669        Ok(()) => Err(fatal_host_error(
1670            "engine did not return its pending-exception error",
1671        )),
1672    }
1673}
1674
1675pub(crate) fn string_array_type(engine: &Engine) -> ArrayType {
1676    ArrayType::new(engine, FieldType::new(Mutability::Var, StorageType::I16))
1677}
1678
1679/// Register a host fn under `mangled_name` — the linker field codegen imports
1680/// for it: `mangle::host(module, name)` for stdlib/host modules, or a prelude
1681/// method's dispatch key (e.g. `submilli:prelude#String#repeat`) so codegen's
1682/// method lookup resolves to it.
1683///
1684/// `_deterministic` is reserved for Phase-2 durable-log wrapping; unused today.
1685///
1686/// Ordinary host errors become catchable guest errors via [`throw_error`].
1687/// [`FatalHostError`], an engine trap and a reached memory cap bypass conversion
1688/// and terminate execution. A body that already raised a throw (its `Err` is a
1689/// `ThrownException`) is passed through untouched, so the pending exception
1690/// isn't clobbered. Bodies receive `&mut Caller` (not an owned `Caller`) so the
1691/// wrapper can still use the caller to raise the throw after the body returns.
1692///
1693/// The import is synchronous: the engine services it inline in its dispatch loop
1694/// rather than suspending the interpreter and resuming through the async driver.
1695/// A body that awaits, or that re-enters guest code, belongs on
1696/// [`register_host_fn_async`] instead.
1697pub fn register_host_fn(
1698    linker: &mut Linker<StoreData>,
1699    module: &str,
1700    mangled_name: crate::MangledName,
1701    ty: FuncType,
1702    _deterministic: bool,
1703    body: impl Fn(&mut Caller<'_, StoreData>, &[Val], &mut [Val]) -> wasmtime::Result<()>
1704    + Send
1705    + Sync
1706    + 'static,
1707) -> wasmtime::Result<()> {
1708    let call_fuel = call_fuel_of(module);
1709    let ends_its_calls = begins_calls(module);
1710    let abi = ty.clone();
1711    linker.func_new(
1712        module,
1713        mangled_name.as_str(),
1714        ty,
1715        move |mut hc, params, results| {
1716            check_host_abi(&abi, params, results)?;
1717            let marker = ends_its_calls.then(|| enter_host_call(&hc)).flatten();
1718            let outcome =
1719                charge_host_fuel(&mut hc, call_fuel).and_then(|()| body(&mut hc, params, results));
1720            if let Some(marker) = marker {
1721                exit_host_call(&hc, marker, outcome.is_ok());
1722            }
1723            match outcome {
1724                Ok(()) => Ok(()),
1725                // Already a thrown exception (pending on the store) — propagate as-is.
1726                Err(err) if err.is::<wasmtime::ThrownException>() => Err(err),
1727                Err(err) => Err(throw_host_error(&mut hc, err)),
1728            }
1729        },
1730    )?;
1731    Ok(())
1732}
1733
1734/// The modules whose host functions gate capabilities, so the recorder ends a call they
1735/// begin when they return. Other modules skip the recorder check entirely. A module that
1736/// gates must be listed: its calls would otherwise stay open until the run ends.
1737pub(crate) const CALL_MODULES: &[&str] = &[
1738    crate::stdlib::fs::MODULE_NAME,
1739    crate::stdlib::http::MODULE_NAME,
1740    crate::stdlib::llm::MODULE_NAME,
1741    crate::stdlib::embedding::MODULE_NAME,
1742    crate::stdlib::session::MODULE_NAME,
1743    crate::stdlib::secrets::MODULE_NAME,
1744    crate::stdlib::variables::MODULE_NAME,
1745    crate::stdlib::git::MODULE_NAME,
1746    crate::stdlib::code::MODULE_NAME,
1747    crate::stdlib::security::MODULE_NAME,
1748    crate::stdlib::agents::MODULE_NAME,
1749    crate::stdlib::skills::MODULE_NAME,
1750    super::mcp::MCP_MODULE_NAME,
1751    super::tools::TOOLS_MODULE_NAME,
1752];
1753
1754pub(crate) fn begins_calls(module: &str) -> bool {
1755    CALL_MODULES.contains(&module)
1756}
1757
1758fn enter_host_call(store: &impl wasmtime::AsContext<Data = StoreData>) -> Option<u64> {
1759    store
1760        .as_context()
1761        .data()
1762        .security_check
1763        .recorder()
1764        .map(super::decision::DecisionRecorder::enter_host_call)
1765}
1766
1767fn exit_host_call(store: &impl wasmtime::AsContext<Data = StoreData>, marker: u64, returned: bool) {
1768    if let Some(recorder) = store.as_context().data().security_check.recorder() {
1769        recorder.exit_host_call(marker, returned);
1770    }
1771}
1772
1773/// The flat fuel of one call into `module`. `submilli:test` is free: it only
1774/// exists under `submilli build test`, and a test's own labels and assertions
1775/// are not the program's work.
1776fn call_fuel_of(module: &str) -> u64 {
1777    if module == crate::stdlib::test::MODULE_NAME {
1778        0
1779    } else {
1780        super::fuel::CALL
1781    }
1782}
1783
1784/// Async sibling of [`register_host_fn`]: registers under `mangled_name`.
1785/// The body returns a boxed future; the same `Err` → [`throw_error`] mapping
1786/// applies once it resolves.
1787pub fn register_host_fn_async<F>(
1788    linker: &mut Linker<StoreData>,
1789    module: &str,
1790    mangled_name: crate::MangledName,
1791    ty: FuncType,
1792    _deterministic: bool,
1793    body: F,
1794) -> wasmtime::Result<()>
1795where
1796    F: for<'a> Fn(
1797            &'a mut Caller<'_, StoreData>,
1798            &'a [Val],
1799            &'a mut [Val],
1800        ) -> Pin<Box<dyn Future<Output = wasmtime::Result<()>> + Send + 'a>>
1801        + Send
1802        + Sync
1803        + 'static,
1804{
1805    // `Arc` so each invocation owns a cheap clone the returned future can hold —
1806    // a bare `&body` reference to the `Fn`'s captured state can't escape it.
1807    let body = std::sync::Arc::new(body);
1808    let call_fuel = call_fuel_of(module);
1809    let ends_its_calls = begins_calls(module);
1810    let abi = ty.clone();
1811    linker.func_new_async(
1812        module,
1813        mangled_name.as_str(),
1814        ty,
1815        move |mut hc, params, results| {
1816            let body = std::sync::Arc::clone(&body);
1817            let shape = check_host_abi(&abi, params, results);
1818            Box::new(async move {
1819                shape?;
1820                let marker = ends_its_calls.then(|| enter_host_call(&hc)).flatten();
1821                let outcome = match charge_host_fuel(&mut hc, call_fuel) {
1822                    Ok(()) => body(&mut hc, params, results).await,
1823                    Err(err) => Err(err),
1824                };
1825                if let Some(marker) = marker {
1826                    exit_host_call(&hc, marker, outcome.is_ok());
1827                }
1828                match outcome {
1829                    Ok(()) => Ok(()),
1830                    Err(err) if err.is::<wasmtime::ThrownException>() => Err(err),
1831                    Err(err) => Err(throw_host_error(&mut hc, err)),
1832                }
1833            })
1834        },
1835    )?;
1836    Ok(())
1837}
1838
1839/// PackageDeclaration for host fns the consumer imports directly. `console.log` and
1840/// `number.toString` are excluded — they're wrapped by prelude exports and must
1841/// not appear as free-function bindings in the user's scope.
1842pub fn host_package_declarations() -> Vec<PackageDeclaration> {
1843    vec![
1844        number_module_definitions(),
1845        super::prelude::temporal::shared::temporal_module_package_declaration(),
1846    ]
1847}
1848
1849/// Compiler-internal host fn definitions — wired into codegen but not visible in user scope.
1850pub fn internal_host_package_declarations() -> Vec<PackageDeclaration> {
1851    vec![
1852        super::json::json_module_definitions(),
1853        super::mcp::mcp_call_package_declaration(),
1854        super::tools::tools_call_package_declaration(),
1855    ]
1856}
1857
1858pub fn stdlib_package_declarations() -> Vec<PackageDeclaration> {
1859    crate::stdlib::stdlib_package_declarations()
1860}
1861
1862fn number_module_definitions() -> PackageDeclaration {
1863    let mut defs = PackageDeclaration::with_package(NUMBER_MODULE_NAME);
1864    let insert = |defs: &mut PackageDeclaration,
1865                  name: &str,
1866                  params: Vec<Param>,
1867                  ret: Type,
1868                  doc: Option<crate::DocComment>| {
1869        defs.values.insert(
1870            name.to_string(),
1871            ValueSymbol {
1872                name: name.to_string(),
1873                mangled_name: crate::mangle::host(NUMBER_MODULE_NAME, name),
1874                declaration_span: Span::at(crate::FileId::NUMBER),
1875                kind: ValueKind::Function {
1876                    generics: Vec::new(),
1877                    params,
1878                    ret,
1879                    type_predicate: None,
1880                    doc,
1881                },
1882            },
1883        );
1884    };
1885    insert(
1886        &mut defs,
1887        "toNumber",
1888        vec![Param::new("string", Type::String)],
1889        Type::Number,
1890        crate::doc(
1891            crate::FileId::NUMBER,
1892            "/**\n * Strict whole-string decimal parse.\n * Returns `NaN` when `string` is not a valid number.\n * @param string The text to parse.\n */",
1893        ),
1894    );
1895    insert(
1896        &mut defs,
1897        "parseInt",
1898        vec![Param::new("string", Type::String), parse_int_radix_param()],
1899        Type::Number,
1900        crate::doc(
1901            crate::FileId::NUMBER,
1902            "/**\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 */",
1903        ),
1904    );
1905    insert(
1906        &mut defs,
1907        "parseFloat",
1908        vec![Param::new("string", Type::String)],
1909        Type::Number,
1910        crate::doc(
1911            crate::FileId::NUMBER,
1912            "/**\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 */",
1913        ),
1914    );
1915    defs
1916}
1917
1918/// `parseInt`'s `radix`: omitted or `undefined` passes 0, which
1919/// `parse_int_with_radix` reads as "detect a `0x` prefix, else base 10", as in JS.
1920/// The host receives an `f64`, so the declared type stays `number`; a
1921/// defaulted parameter still accepts `undefined` at the call.
1922pub(crate) fn parse_int_radix_param() -> Param {
1923    Param::with_default("radix", Type::Number, crate::DefaultValue::Number(0.0))
1924}
1925
1926#[cfg(test)]
1927mod tests {
1928    use super::*;
1929
1930    #[test]
1931    fn host_abi_rejects_bad_shapes_without_numeric_defaults() {
1932        let engine = Engine::default();
1933        let ty = FuncType::new(&engine, [ValType::F64, ValType::I32], [ValType::I64]);
1934        for (params, results) in [
1935            (vec![], vec![Val::I64(0)]),
1936            (vec![Val::F64(0), Val::I32(0)], vec![]),
1937            (vec![Val::I32(0), Val::I32(0)], vec![Val::I64(0)]),
1938            (vec![Val::F64(0), Val::F64(0)], vec![Val::I64(0)]),
1939        ] {
1940            let error = check_host_abi(&ty, &params, &results).unwrap_err();
1941            assert!(error.is::<wasmtime::Trap>());
1942        }
1943        check_host_abi(
1944            &ty,
1945            &[Val::F64(f64::NAN.to_bits()), Val::I32(1)],
1946            &[Val::I64(0)],
1947        )
1948        .unwrap();
1949    }
1950
1951    fn denial(capability: &str) -> Denial {
1952        Denial {
1953            caller: "main".into(),
1954            capability: capability.into(),
1955            source: DenialSource::Policy,
1956        }
1957    }
1958
1959    /// A lookup matches only the very object the runtime threw. After the
1960    /// collector reclaims that object, a later allocation can take over its
1961    /// slot, and the stale entry must not vouch for it.
1962    #[test]
1963    fn a_collected_denial_never_matches_a_later_object() {
1964        let (engine, mut store, _) = async_store();
1965        let ty = StructType::new(
1966            &engine,
1967            [FieldType::new(Mutability::Const, StorageType::I8)],
1968        )
1969        .expect("struct type");
1970        let pre = StructRefPre::new(&mut store, ty);
1971        let mut denials = ThrownDenials::default();
1972
1973        let thrown = {
1974            let mut scope = wasmtime::RootScope::new(&mut store);
1975            let thrown = wasmtime::StructRef::new(&mut scope, &pre, &[Val::I32(1)]).expect("alloc");
1976            denials.record(&scope, thrown, denial("fs.read"));
1977            let found = denials.find(&scope, &thrown.to_anyref());
1978            assert_eq!(found.map(|d| d.capability), Some("fs.read".to_string()));
1979            thrown.to_anyref()
1980        };
1981        store.gc();
1982
1983        let mut scope = wasmtime::RootScope::new(&mut store);
1984        let forged = wasmtime::StructRef::new(&mut scope, &pre, &[Val::I32(2)]).expect("alloc");
1985        assert!(
1986            denials.find(&scope, &forged.to_anyref()).is_none(),
1987            "a program-built error is not a runtime denial"
1988        );
1989        assert!(
1990            denials.find(&scope, &thrown).is_none(),
1991            "the collected denial's entry is dropped"
1992        );
1993        assert!(denials.entries.is_empty());
1994    }
1995
1996    #[test]
1997    fn the_denial_table_drops_its_oldest_entries_past_the_cap() {
1998        let (engine, mut store, _) = async_store();
1999        let ty = StructType::new(
2000            &engine,
2001            [FieldType::new(Mutability::Const, StorageType::I8)],
2002        )
2003        .expect("struct type");
2004        let pre = StructRefPre::new(&mut store, ty);
2005        let mut denials = ThrownDenials::default();
2006        let mut scope = wasmtime::RootScope::new(&mut store);
2007        let mut thrown = Vec::new();
2008        for i in 0..=MAX_THROWN_DENIALS {
2009            let object =
2010                wasmtime::StructRef::new(&mut scope, &pre, &[Val::I32(i as i32)]).expect("alloc");
2011            denials.record(&scope, object, denial(&format!("cap.{i}")));
2012            thrown.push(object.to_anyref());
2013        }
2014        assert_eq!(denials.entries.len(), MAX_THROWN_DENIALS);
2015        assert!(denials.find(&scope, &thrown[0]).is_none(), "oldest dropped");
2016        let newest = denials.find(&scope, &thrown[MAX_THROWN_DENIALS]);
2017        assert_eq!(
2018            newest.map(|d| d.capability),
2019            Some(format!("cap.{MAX_THROWN_DENIALS}"))
2020        );
2021    }
2022
2023    #[tokio::test]
2024    async fn run_ending_errors_bypass_guest_catch_in_both_wrappers() {
2025        #[derive(Clone, Copy, PartialEq, Debug)]
2026        enum Failure {
2027            Ordinary,
2028            Fatal,
2029            MissingArgument,
2030            MissingResult,
2031            // What a body that re-entered guest code returns when that call trapped.
2032            NestedTrap(wasmtime::Trap),
2033        }
2034        let failures = [
2035            Failure::Ordinary,
2036            Failure::Fatal,
2037            Failure::MissingArgument,
2038            Failure::MissingResult,
2039            Failure::NestedTrap(wasmtime::Trap::StackOverflow),
2040            Failure::NestedTrap(wasmtime::Trap::OutOfFuel),
2041            Failure::NestedTrap(wasmtime::Trap::Interrupt),
2042            Failure::NestedTrap(wasmtime::Trap::UnreachableCodeReached),
2043            Failure::NestedTrap(wasmtime::Trap::CastFailure),
2044            Failure::NestedTrap(wasmtime::Trap::NullReference),
2045            Failure::NestedTrap(wasmtime::Trap::ArrayOutOfBounds),
2046        ];
2047        for asynchronous in [false, true] {
2048            for kind in failures {
2049                let (engine, mut store, mut linker) = async_store();
2050                crate::runtime::install_runtime_async(&mut linker, &mut store)
2051                    .await
2052                    .expect("runtime");
2053                let name = crate::mangle::host(TEST_MODULE, "failure");
2054                let failure = move || match kind {
2055                    Failure::Fatal => fatal_host_error("test operation: invalid state")
2056                        .context("outer host context"),
2057                    Failure::NestedTrap(trap) => {
2058                        wasmtime::Error::new(trap).context("outer host context")
2059                    }
2060                    Failure::MissingArgument => abi_arg(&[], usize::MAX).unwrap_err(),
2061                    Failure::MissingResult => abi_result(&mut [], usize::MAX).unwrap_err(),
2062                    Failure::Ordinary => wasmtime::Error::msg("ordinary operation failure"),
2063                };
2064                let ty = FuncType::new(&engine, [], []);
2065                if asynchronous {
2066                    register_host_fn_async(
2067                        &mut linker,
2068                        TEST_MODULE,
2069                        name.clone(),
2070                        ty,
2071                        true,
2072                        move |_, _, _| Box::pin(async move { Err(failure()) }),
2073                    )
2074                    .expect("async registration");
2075                } else {
2076                    register_host_fn(
2077                        &mut linker,
2078                        TEST_MODULE,
2079                        name.clone(),
2080                        ty,
2081                        true,
2082                        move |_, _, _| Err(failure()),
2083                    )
2084                    .expect("sync registration");
2085                }
2086                let source = format!(
2087                    r#"(module
2088                    (import "{TEST_MODULE}" "{name}" (func $failure))
2089                    (func (export "attempt") (result i32)
2090                        (block $caught
2091                            (try_table (catch_all $caught) (call $failure))
2092                            (return (i32.const 0)))
2093                        (i32.const 1))
2094                    (func (export "healthy") (result i32) (i32.const 42)))"#
2095                );
2096                let buffer = wast::parser::ParseBuffer::new(&source).expect("WAT tokens");
2097                let mut wat = wast::parser::parse::<wast::Wat>(&buffer).expect("WAT");
2098                let module =
2099                    wasmtime::Module::new(&engine, wat.encode().expect("encode")).expect("module");
2100                let instance = linker
2101                    .instantiate_async(&mut store, &module)
2102                    .await
2103                    .expect("instance");
2104                let attempt = instance
2105                    .get_typed_func::<(), i32>(&mut store, "attempt")
2106                    .expect("attempt");
2107                let result = attempt.call_async(&mut store, ()).await;
2108                match kind {
2109                    Failure::Fatal => {
2110                        let err =
2111                            result.expect_err("fatal failure cannot enter the guest catch handler");
2112                        assert!(err.is::<FatalHostError>(), "typed cause lost: {err:#}");
2113                        assert!(format!("{err:#}").contains("invalid state"));
2114                    }
2115                    Failure::MissingArgument | Failure::MissingResult => {
2116                        let err = result.expect_err("ABI invariants bypass guest catch");
2117                        assert_eq!(
2118                            err.downcast_ref::<wasmtime::Trap>(),
2119                            Some(&wasmtime::Trap::UnreachableCodeReached)
2120                        );
2121                    }
2122                    Failure::NestedTrap(trap) => {
2123                        let err = result.expect_err("a trap cannot enter the guest catch handler");
2124                        assert_eq!(
2125                            err.downcast_ref::<wasmtime::Trap>(),
2126                            Some(&trap),
2127                            "trap kind lost: {err:#}"
2128                        );
2129                    }
2130                    Failure::Ordinary => {
2131                        assert_eq!(result.expect("guest catches ordinary failure"), 1);
2132                    }
2133                }
2134                let healthy = instance
2135                    .get_typed_func::<(), i32>(&mut store, "healthy")
2136                    .expect("healthy");
2137                assert_eq!(
2138                    healthy
2139                        .call_async(&mut store, ())
2140                        .await
2141                        .expect("subsequent call"),
2142                    42
2143                );
2144            }
2145        }
2146    }
2147
2148    const TEST_MODULE: &str = "test:sync";
2149
2150    /// An async-enabled store plus an empty linker — no guest module is needed to
2151    /// observe which engine registration kind a wrapper produced.
2152    fn async_store() -> (Engine, Store<StoreData>, Linker<StoreData>) {
2153        let cfg = crate::runtime::RuntimeConfig::default();
2154        let engine = cfg.engine().expect("engine");
2155        let data = StoreData::with_vfs(crate::runtime::Vfs::tempdir().expect("tempdir"));
2156        let store = cfg.store_async(&engine, data).expect("store");
2157        let linker = Linker::<StoreData>::new(&engine);
2158        (engine, store, linker)
2159    }
2160
2161    fn resolve(
2162        linker: &Linker<StoreData>,
2163        store: &mut Store<StoreData>,
2164        name: &crate::MangledName,
2165    ) -> wasmtime::Func {
2166        match linker
2167            .get(&mut *store, TEST_MODULE, name.as_str())
2168            .expect("registered function resolves")
2169        {
2170            wasmtime::Extern::Func(func) => func,
2171            other => panic!("expected a func, got {other:?}"),
2172        }
2173    }
2174
2175    /// The acceptance gate: a `register_host_fn` body must be a *synchronous*
2176    /// engine import. An async import rejects `Func::call` outright ("cannot call
2177    /// an async host function synchronously"), so a passing call is the proof.
2178    #[test]
2179    fn sync_bodies_are_callable_synchronously_on_an_async_store() {
2180        let (engine, mut store, mut linker) = async_store();
2181        let name = crate::mangle::host(TEST_MODULE, "answer");
2182        register_host_fn(
2183            &mut linker,
2184            TEST_MODULE,
2185            name.clone(),
2186            FuncType::new(&engine, [], [ValType::I32]),
2187            true,
2188            |_caller, _params, results| {
2189                results[0] = Val::I32(42);
2190                Ok(())
2191            },
2192        )
2193        .expect("register");
2194
2195        let func = resolve(&linker, &mut store, &name);
2196        let mut results = [Val::I32(0)];
2197        func.call(&mut store, &[], &mut results)
2198            .expect("synchronous call reaches the body");
2199        assert_eq!(results[0].i32(), Some(42));
2200    }
2201
2202    /// An ordinary `Err` still routes through the wrapper's error mapping. Without
2203    /// an instantiated prelude `throw_error_as` falls back to a plain-message
2204    /// error, which is what makes the message observable here; guest-level
2205    /// `try/catch` of a sync host error is covered end-to-end by the
2206    /// `host_error_catchable` and `classes/range_error_host_throws` fixtures
2207    /// (`String.normalize`, `String.repeat`, BigInt divide).
2208    #[test]
2209    fn an_ordinary_error_keeps_its_message() {
2210        let (engine, mut store, mut linker) = async_store();
2211        let name = crate::mangle::host(TEST_MODULE, "boom");
2212        register_host_fn(
2213            &mut linker,
2214            TEST_MODULE,
2215            name.clone(),
2216            FuncType::new(&engine, [], []),
2217            true,
2218            |_caller, _params, _results| Err(wasmtime::Error::msg("boom")),
2219        )
2220        .expect("register");
2221
2222        let func = resolve(&linker, &mut store, &name);
2223        let err = func
2224            .call(&mut store, &[], &mut [])
2225            .expect_err("the body fails");
2226        assert!(format!("{err}").contains("boom"), "got: {err}");
2227    }
2228
2229    #[test]
2230    fn fatal_host_errors_preserve_their_marker() {
2231        let (engine, mut store, mut linker) = async_store();
2232        let name = crate::mangle::host(TEST_MODULE, "fatal");
2233        register_host_fn(
2234            &mut linker,
2235            TEST_MODULE,
2236            name.clone(),
2237            FuncType::new(&engine, [], []),
2238            true,
2239            |_, _, _| Err(super::fatal_host_error("broken dynamic object ABI")),
2240        )
2241        .expect("register");
2242        let func = resolve(&linker, &mut store, &name);
2243        let error = func
2244            .call(&mut store, &[], &mut [])
2245            .expect_err("fatal host error");
2246        assert!(
2247            error.is::<super::FatalHostError>(),
2248            "fatal errors must bypass guest conversion: {error}"
2249        );
2250    }
2251
2252    #[tokio::test]
2253    async fn async_fatal_host_errors_preserve_their_marker() {
2254        let (engine, mut store, mut linker) = async_store();
2255        let name = crate::mangle::host(TEST_MODULE, "fatal_async");
2256        super::register_host_fn_async(
2257            &mut linker,
2258            TEST_MODULE,
2259            name.clone(),
2260            FuncType::new(&engine, [], []),
2261            true,
2262            |_, _, _| Box::pin(async { Err(super::fatal_host_error("broken dynamic object ABI")) }),
2263        )
2264        .expect("register");
2265        let func = resolve(&linker, &mut store, &name);
2266        let error = func
2267            .call_async(&mut store, &[], &mut [])
2268            .await
2269            .expect_err("fatal host error");
2270        assert!(
2271            error.is::<super::FatalHostError>(),
2272            "fatal errors must bypass guest conversion: {error}"
2273        );
2274    }
2275
2276    /// A body that already threw hands back a `ThrownException`; converting it
2277    /// again would clobber the exception pending on the store.
2278    #[test]
2279    fn a_thrown_exception_passes_through_unconverted() {
2280        let (engine, mut store, mut linker) = async_store();
2281        let name = crate::mangle::host(TEST_MODULE, "rethrow");
2282        register_host_fn(
2283            &mut linker,
2284            TEST_MODULE,
2285            name.clone(),
2286            FuncType::new(&engine, [], []),
2287            true,
2288            |_caller, _params, _results| Err(wasmtime::Error::new(wasmtime::ThrownException)),
2289        )
2290        .expect("register");
2291
2292        let func = resolve(&linker, &mut store, &name);
2293        let err = func
2294            .call(&mut store, &[], &mut [])
2295            .expect_err("the body throws");
2296        assert!(
2297            err.is::<wasmtime::ThrownException>(),
2298            "the pending exception must survive; got: {err}"
2299        );
2300    }
2301}