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