use std::future::Future;
use std::pin::Pin;
use wasmtime::{
AnyRef, ArrayRef, ArrayRefPre, ArrayType, AsContext, AsContextMut, Caller, Engine, FieldType,
FuncType, Global, HeapType, Linker, Mutability, RefType, Rooted, StorageType, Store, StructRef,
StructRefPre, StructType, Val, ValType,
};
use crate::runtime::fuel::{self, charge_host_fuel};
use crate::runtime::intrinsic_types::{build_intrinsic_types, intrinsic_types};
pub(crate) use crate::runtime::intrinsic_types::{
intrinsic_array_type, intrinsic_bigint_type, intrinsic_string_type, intrinsic_uint8_array_type,
};
use crate::runtime::number::{parse_float_js, parse_int_with_radix, string_to_number_js};
use crate::runtime::{StoreData, read_submilli_string};
use crate::stdlib::Stdlib;
use crate::{PackageDeclaration, Param, Span, Type, ValueKind, ValueSymbol};
pub const NUMBER_MODULE_NAME: &str = "submilli:number";
pub const INTERNAL_MODULE_NAME: &str = "__submilli_internal";
pub async fn install_async(
linker: &mut Linker<StoreData>,
store: &mut Store<StoreData>,
) -> wasmtime::Result<()> {
install_async_for(linker, store, Stdlib::core()).await
}
pub async fn install_async_for(
linker: &mut Linker<StoreData>,
store: &mut Store<StoreData>,
stdlib: Stdlib,
) -> wasmtime::Result<()> {
install_host_functions_for(linker, stdlib)?;
install_store_bound(linker, store)
}
pub fn install_host_functions(linker: &mut Linker<StoreData>) -> wasmtime::Result<()> {
install_host_functions_for(linker, Stdlib::core())
}
pub fn install_host_functions_for(
linker: &mut Linker<StoreData>,
stdlib: Stdlib,
) -> wasmtime::Result<()> {
install_core_host_functions(linker)?;
stdlib.install_host_functions(linker)?;
super::mcp::install_mcp_async(linker)?;
Ok(())
}
pub fn install_store_bound(
linker: &mut Linker<StoreData>,
store: &mut Store<StoreData>,
) -> wasmtime::Result<()> {
install_prelude(linker, store)?;
crate::stdlib::install_store_bound(linker, store)
}
fn install_prelude(
linker: &mut Linker<StoreData>,
store: &mut Store<StoreData>,
) -> wasmtime::Result<()> {
let (vtables, error_host) = super::prelude::install_vtables(linker, store)?;
let intr = build_intrinsic_types(store.engine())?;
let map_tombstone =
super::prelude::map::build_tombstone(store, intr.object.clone(), vtables.object)?;
let collection_null =
super::prelude::map::build_tombstone(store, intr.object.clone(), vtables.object)?;
let temporal = super::prelude::temporal::install_abi(linker, store, &intr)?;
let error_subclass_type =
super::prelude::error::build_error_subclass_types(store.engine(), &intr)?.1;
let map_backing_type = super::prelude::map::map_backing_struct(store.engine(), &intr)?;
let set_backing_type = super::prelude::set::set_backing_struct(store.engine(), &intr)?;
let member_functions = super::prelude::member::functions(linker, store);
store.data_mut().host_abi = Some(HostAbi {
member_functions,
string_type: intr.string,
uint8_type: intr.uint8_array,
array_type: intr.array,
raw_array_type: intr.raw_array,
boxed_number_type: intr.boxed_number,
boxed_boolean_type: intr.boxed_boolean,
field_names_type: intr.field_names,
object_shape_type: intr.object_shape,
object_fields_type: intr.object_fields,
map_backing_type,
set_backing_type,
string_vtable: vtables.string,
array_vtable: vtables.array,
object_vtable: vtables.object,
boxed_number_vtable: vtables.boxed_number,
boxed_boolean_vtable: vtables.boxed_boolean,
uint8_array_vtable: vtables.uint8_array,
bigint_vtable: vtables.bigint,
closure_vtable: vtables.closure,
regex_vtable: vtables.regex,
regex_match_box_vtable: vtables.regex_match_box,
opaque_vtable: vtables.opaque,
temporal,
map_tombstone,
collection_null,
error_type: intr.error,
error_subclass_type,
error_vtable: error_host.vtable,
range_error_vtable: error_host.range_vtable,
quota_exceeded_vtable: error_host.quota_exceeded_vtable,
type_error_vtable: error_host.type_vtable,
syntax_error_vtable: error_host.syntax_vtable,
uri_error_vtable: error_host.uri_vtable,
reference_error_vtable: error_host.reference_vtable,
permission_denied_vtable: error_host.permission_denied_vtable,
error_field_names: error_host.field_names,
permission_denied_field_names: error_host.permission_denied_field_names,
error_tag: error_host.tag,
});
Ok(())
}
fn install_core_host_functions(linker: &mut Linker<StoreData>) -> wasmtime::Result<()> {
let engine = linker.engine().clone();
let string_type = string_array_type(&engine);
install_number_module(linker)?;
super::prelude::install(linker)?;
super::prelude::bigint::ops::install(linker, &string_type)?;
install_internal_module(linker, &string_type)?;
super::json::install_json_module(linker, &string_type)?;
Ok(())
}
fn install_internal_module(
linker: &mut Linker<StoreData>,
string_type: &ArrayType,
) -> wasmtime::Result<()> {
use base64::Engine as _;
use base64::alphabet;
use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig, NO_PAD, PAD};
let engine = linker.engine().clone();
let raw_uint8_array_type = uint8_array_array_type(&engine);
let raw_uint8_array_param = ValType::Ref(RefType::new(
false,
HeapType::from(raw_uint8_array_type.clone()),
));
let raw_uint8_array_result = raw_uint8_array_param.clone();
let raw_string_param = ValType::Ref(RefType::new(false, HeapType::from(string_type.clone())));
let raw_string_result = raw_string_param.clone();
let ty = FuncType::new(
&engine,
[raw_uint8_array_param.clone(), ValType::I32, ValType::I32],
[raw_string_result.clone()],
);
register_host_fn(
linker,
INTERNAL_MODULE_NAME,
crate::mangle::host(INTERNAL_MODULE_NAME, "uint8array_to_base64"),
ty,
true,
|caller, params, results| -> wasmtime::Result<()> {
let bytes =
read_uint8_array_arg(&mut *caller, abi_arg(params, 0)?, "uint8array_to_base64")?;
let alphabet_flag = (*abi_arg(params, 1)?)
.i32()
.ok_or_else(|| invariant_trap("host ABI: expected i32"))?;
let omit_padding = (*abi_arg(params, 2)?)
.i32()
.ok_or_else(|| invariant_trap("host ABI: expected i32"))?
!= 0;
let alphabet = if alphabet_flag == 1 {
&alphabet::URL_SAFE
} else {
&alphabet::STANDARD
};
let config: GeneralPurposeConfig = if omit_padding { NO_PAD } else { PAD };
let engine = GeneralPurpose::new(alphabet, config);
fuel::charge(&mut *caller, fuel::SCAN, bytes.len() as u64)?;
let encoded = engine.encode(&bytes);
let arr = write_submilli_string(&mut *caller, &encoded)?;
*abi_result(results, 0)? = Val::AnyRef(Some(arr.to_anyref()));
Ok(())
},
)?;
let ty = FuncType::new(
&engine,
[raw_string_param.clone(), ValType::I32],
[raw_uint8_array_result.clone()],
);
let raw_uint8_array_type_for_decode = raw_uint8_array_type.clone();
register_host_fn(
linker,
INTERNAL_MODULE_NAME,
crate::mangle::host(INTERNAL_MODULE_NAME, "uint8array_from_base64"),
ty,
true,
move |caller, params, results| -> wasmtime::Result<()> {
let s = read_string_arg(&mut *caller, abi_arg(params, 0)?, "uint8array_from_base64")?;
fuel::charge(&mut *caller, fuel::SCAN, s.len() as u64)?;
let alphabet_flag = (*abi_arg(params, 1)?)
.i32()
.ok_or_else(|| invariant_trap("host ABI: expected i32"))?;
let alphabet = if alphabet_flag == 1 {
&alphabet::URL_SAFE
} else {
&alphabet::STANDARD
};
let config = if s.as_bytes().last() == Some(&b'=') {
PAD
} else {
NO_PAD
};
let decoded = GeneralPurpose::new(alphabet, config).decode(s.as_bytes());
let bytes =
decoded.map_err(|e| wasmtime::Error::msg(format!("Uint8Array.fromBase64: {e}")))?;
let arr = write_uint8_array(
&mut *caller,
raw_uint8_array_type_for_decode.clone(),
&bytes,
)?;
*abi_result(results, 0)? = Val::AnyRef(Some(arr.to_anyref()));
Ok(())
},
)?;
let ty = FuncType::new(
&engine,
[raw_string_param.clone()],
[raw_uint8_array_result.clone()],
);
let raw_uint8_array_type_for_encode = raw_uint8_array_type.clone();
register_host_fn(
linker,
INTERNAL_MODULE_NAME,
crate::mangle::host(INTERNAL_MODULE_NAME, "textencoder_encode"),
ty,
true,
move |caller, params, results| -> wasmtime::Result<()> {
let s = read_string_arg(&mut *caller, abi_arg(params, 0)?, "textencoder_encode")?;
let bytes = s.into_bytes();
let arr = write_uint8_array(
&mut *caller,
raw_uint8_array_type_for_encode.clone(),
&bytes,
)?;
*abi_result(results, 0)? = Val::AnyRef(Some(arr.to_anyref()));
Ok(())
},
)?;
let ty = FuncType::new(
&engine,
[raw_uint8_array_param],
[raw_string_result.clone()],
);
register_host_fn(
linker,
INTERNAL_MODULE_NAME,
crate::mangle::host(INTERNAL_MODULE_NAME, "textdecoder_decode"),
ty,
true,
|caller, params, results| -> wasmtime::Result<()> {
let bytes =
read_uint8_array_arg(&mut *caller, abi_arg(params, 0)?, "textdecoder_decode")?;
let s = std::str::from_utf8(&bytes).map_err(|e| {
type_error(format!(
"TextDecoder.decode: invalid UTF-8 at byte {}: {e}",
e.valid_up_to(),
))
})?;
let arr = write_submilli_string(&mut *caller, s)?;
*abi_result(results, 0)? = Val::AnyRef(Some(arr.to_anyref()));
Ok(())
},
)?;
Ok(())
}
pub(crate) fn write_uint8_array(
mut ctx: impl AsContextMut<Data = StoreData>,
array_ty: ArrayType,
bytes: &[u8],
) -> wasmtime::Result<Rooted<ArrayRef>> {
fuel::charge(&mut ctx, fuel::COPY, bytes.len() as u64)?;
write_uint8_array_precharged(ctx, array_ty, bytes)
}
pub(crate) fn write_uint8_array_precharged(
mut ctx: impl AsContextMut<Data = StoreData>,
array_ty: ArrayType,
bytes: &[u8],
) -> wasmtime::Result<Rooted<ArrayRef>> {
let pre = ArrayRefPre::new(&mut ctx, array_ty);
ArrayRef::new_from_i8_slice(&mut ctx, &pre, bytes)
}
pub(crate) fn uint8_array_array_type(engine: &Engine) -> ArrayType {
ArrayType::new(engine, FieldType::new(Mutability::Var, StorageType::I8))
}
pub(crate) fn read_uint8_array_arg(
caller: &mut Caller<'_, StoreData>,
val: &Val,
name: &str,
) -> wasmtime::Result<Vec<u8>> {
let arr = uint8_array_backing(caller, val, name)?;
let len = usize::try_from(arr.len(&mut *caller)?).map_err(fatal_host_error)?;
read_uint8_array_range(caller, arr, 0, len, name)
}
pub(crate) fn read_uint8_array_range(
caller: &mut Caller<'_, StoreData>,
arr: Rooted<ArrayRef>,
offset: usize,
len: usize,
name: &str,
) -> wasmtime::Result<Vec<u8>> {
fuel::charge(&mut *caller, fuel::COPY, len as u64)?;
let offset = u32::try_from(offset).map_err(fatal_host_error)?;
let mut out = Vec::new();
out.try_reserve_exact(len).map_err(fatal_host_error)?;
out.resize(len, 0);
arr.read_i8(&mut *caller, offset, &mut out)
.map_err(|error| type_error(format!("{name}: {error}")))?;
Ok(out)
}
pub(crate) fn read_uint8(
caller: &mut Caller<'_, StoreData>,
arr: Rooted<ArrayRef>,
index: usize,
) -> wasmtime::Result<u8> {
let index = u32::try_from(index).map_err(fatal_host_error)?;
match arr.get(&mut *caller, index).map_err(fatal_host_error)? {
Val::I32(byte) => Ok(byte as u8),
other => Err(fatal_host_error(format!("byte {index} is {other:?}"))),
}
}
pub(crate) fn uint8_array_backing(
caller: &mut Caller<'_, StoreData>,
val: &Val,
name: &str,
) -> wasmtime::Result<Rooted<ArrayRef>> {
let any = match val {
Val::AnyRef(Some(any)) => *any,
Val::AnyRef(None) => {
return Err(type_error(format!("{name} arg is null")));
}
other => {
return Err(type_error(format!(
"{name} expects a Uint8Array, got {other:?}"
)));
}
};
let arr = if let Some(st) = any.as_struct(&mut *caller)? {
let uint8_ty = intrinsic_types(&mut *caller)?.uint8_array.clone();
if !st.matches_ty(&*caller, &uint8_ty)? {
return Err(type_error(format!("{name} expects a Uint8Array")));
}
match st.field(&mut *caller, 1)? {
Val::AnyRef(Some(inner)) => inner.unwrap_array(&mut *caller)?,
other => {
return Err(wasmtime::Error::msg(format!(
"{name}: malformed $Uint8Array payload {other:?}"
)));
}
}
} else {
any.unwrap_array(&mut *caller)?
};
Ok(arr)
}
fn install_number_module(linker: &mut Linker<StoreData>) -> wasmtime::Result<()> {
type FormatOp = fn(f64, f64) -> Result<String, String>;
let engine = linker.engine().clone();
let string_struct = intrinsic_string_type(&engine)?;
let string_struct_result =
ValType::Ref(RefType::new(false, HeapType::ConcreteStruct(string_struct)));
let string_struct_param = string_struct_result.clone();
let to_string_ty = FuncType::new(&engine, [ValType::F64], [string_struct_result.clone()]);
register_host_fn(
linker,
NUMBER_MODULE_NAME,
crate::mangle::host(NUMBER_MODULE_NAME, "toString"),
to_string_ty,
true,
|caller, params, results| -> wasmtime::Result<()> {
let n = match *abi_arg(params, 0)? {
Val::F64(bits) => f64::from_bits(bits),
ref other => {
return Err(type_error(format!(
"number.toString expects f64, got {other:?}"
)));
}
};
let s = format_number(n);
let st = write_submilli_string_struct(caller, &s)?;
*abi_result(results, 0)? = Val::AnyRef(Some(st.to_anyref()));
Ok(())
},
)?;
let to_number_ty = FuncType::new(&engine, [string_struct_param.clone()], [ValType::F64]);
register_host_fn(
linker,
NUMBER_MODULE_NAME,
crate::mangle::host(NUMBER_MODULE_NAME, "toNumber"),
to_number_ty,
true,
|caller, params, results| -> wasmtime::Result<()> {
let s = read_string_arg(&mut *caller, abi_arg(params, 0)?, "number.toNumber")?;
let value = string_to_number_js(&s);
*abi_result(results, 0)? = Val::F64(value.to_bits());
Ok(())
},
)?;
let parse_int_ty = FuncType::new(
&engine,
[string_struct_param.clone(), ValType::F64],
[ValType::F64],
);
register_host_fn(
linker,
NUMBER_MODULE_NAME,
crate::mangle::host(NUMBER_MODULE_NAME, "parseInt"),
parse_int_ty,
true,
|caller, params, results| -> wasmtime::Result<()> {
let s = read_string_arg(&mut *caller, abi_arg(params, 0)?, "number.parseInt")?;
let radix = match *abi_arg(params, 1)? {
Val::F64(bits) => f64::from_bits(bits),
ref other => {
return Err(type_error(format!(
"number.parseInt expects f64 radix, got {other:?}"
)));
}
};
let value = parse_int_with_radix(&s, radix);
*abi_result(results, 0)? = Val::F64(value.to_bits());
Ok(())
},
)?;
let parse_float_ty = FuncType::new(&engine, [string_struct_param], [ValType::F64]);
register_host_fn(
linker,
NUMBER_MODULE_NAME,
crate::mangle::host(NUMBER_MODULE_NAME, "parseFloat"),
parse_float_ty,
true,
|caller, params, results| -> wasmtime::Result<()> {
let s = read_string_arg(&mut *caller, abi_arg(params, 0)?, "number.parseFloat")?;
let value = parse_float_js(&s);
*abi_result(results, 0)? = Val::F64(value.to_bits());
Ok(())
},
)?;
let format_ty = FuncType::new(
&engine,
[ValType::F64, ValType::F64],
[string_struct_result],
);
for (name, op) in [
("toFixed", crate::runtime::number::to_fixed_js as FormatOp),
("toPrecision", crate::runtime::number::to_precision_js),
("toExponential", crate::runtime::number::to_exponential_js),
("toStringRadix", crate::runtime::number::to_string_radix_js),
] {
register_number_formatter(linker, name, format_ty.clone(), op)?;
}
Ok(())
}
fn register_number_formatter(
linker: &mut Linker<StoreData>,
name: &'static str,
ty: FuncType,
op: fn(f64, f64) -> Result<String, String>,
) -> wasmtime::Result<()> {
register_host_fn(
linker,
NUMBER_MODULE_NAME,
crate::mangle::host(NUMBER_MODULE_NAME, name),
ty,
true,
move |caller, params, results| -> wasmtime::Result<()> {
let (Val::F64(x_bits), Val::F64(arg_bits)) = (abi_arg(params, 0)?, abi_arg(params, 1)?)
else {
wasmtime::bail!("number.{name} expects (f64, f64)");
};
let formatted = op(f64::from_bits(*x_bits), f64::from_bits(*arg_bits))
.map_err(wasmtime::Error::msg)?;
let st = write_submilli_string_struct(caller, &formatted)?;
*abi_result(results, 0)? = Val::AnyRef(Some(st.to_anyref()));
Ok(())
},
)
}
fn format_number(n: f64) -> String {
if n.is_nan() {
"NaN".to_string()
} else if n.is_infinite() {
if n > 0.0 {
"Infinity".to_string()
} else {
"-Infinity".to_string()
}
} else if n == 0.0 {
"0".to_string()
} else {
n.to_string()
}
}
fn read_string_from_anyref(
caller: &mut Caller<'_, StoreData>,
any: Rooted<AnyRef>,
name: &str,
) -> wasmtime::Result<String> {
let arr = if let Some(st) = any.as_struct(&mut *caller)? {
match st.field(&mut *caller, 1)? {
Val::AnyRef(Some(inner)) => inner.unwrap_array(&mut *caller)?,
other => {
return Err(wasmtime::Error::msg(format!(
"{name}: malformed $string payload {other:?}"
)));
}
}
} else {
any.unwrap_array(&mut *caller)?
};
read_submilli_string(caller, arr)
}
pub fn read_string_arg(
caller: &mut Caller<'_, StoreData>,
val: &Val,
name: &str,
) -> wasmtime::Result<String> {
match val {
Val::AnyRef(Some(any)) => read_string_from_anyref(caller, *any, name),
Val::AnyRef(None) => Err(type_error(format!("{name} arg is null"))),
other => Err(type_error(format!(
"{name} expects a string, got {other:?}"
))),
}
}
pub fn read_string_array_arg(
caller: &mut Caller<'_, StoreData>,
val: &Val,
name: &str,
) -> wasmtime::Result<Vec<String>> {
let any = match val {
Val::AnyRef(Some(any)) => *any,
Val::AnyRef(None) => return Err(type_error(format!("{name} arg is null"))),
other => {
return Err(type_error(format!(
"{name} expects a string[], got {other:?}"
)));
}
};
let object = any
.as_struct(&mut *caller)?
.ok_or_else(|| type_error(format!("{name}: expected $Array struct")))?;
let storage = super::array_storage::ArrayStorage::from_struct(caller, object)?;
let raw = storage.backing;
let len = storage.len;
let mut out = Vec::new();
out.try_reserve_exact(len as usize)
.map_err(fatal_host_error)?;
for i in 0..len {
match raw.get(&mut *caller, i)? {
Val::AnyRef(Some(elem)) => out.push(read_string_from_anyref(caller, elem, name)?),
Val::AnyRef(None) => out.push(String::new()),
other => {
return Err(type_error(format!(
"{name}: slot {i} expects a $string, got {other:?}"
)));
}
}
}
Ok(out)
}
pub fn write_submilli_string(
mut ctx: impl AsContextMut<Data = StoreData>,
s: &str,
) -> wasmtime::Result<Rooted<ArrayRef>> {
let units = encode_utf16(&mut ctx, s)?;
write_code_units(ctx, &units)
}
pub(crate) fn encode_utf16(
ctx: impl AsContextMut<Data = StoreData>,
s: &str,
) -> wasmtime::Result<Vec<u16>> {
fuel::charge(ctx, fuel::SCAN, s.len() as u64)?;
Ok(s.encode_utf16().collect())
}
pub(crate) fn write_code_units(
mut ctx: impl AsContextMut<Data = StoreData>,
units: &[u16],
) -> wasmtime::Result<Rooted<ArrayRef>> {
fuel::charge(&mut ctx, fuel::COPY, units.len() as u64)?;
let array_ty = string_array_type(ctx.as_context().engine());
let pre = ArrayRefPre::new(&mut ctx, array_ty);
ArrayRef::new_from_i16_slice(&mut ctx, &pre, units)
}
pub(crate) fn read_code_units(
mut ctx: impl AsContextMut<Data = StoreData>,
raw: Rooted<ArrayRef>,
name: &str,
) -> wasmtime::Result<Vec<u16>> {
let len = raw.len(&mut ctx)?;
fuel::charge(&mut ctx, fuel::COPY, u64::from(len))?;
let len = usize::try_from(len).map_err(fatal_host_error)?;
let mut units = Vec::new();
units.try_reserve_exact(len).map_err(fatal_host_error)?;
units.resize(len, 0);
raw.copy_to_i16_slice(&mut ctx, &mut units)
.map_err(|error| wasmtime::Error::msg(format!("{name}: {error}")))?;
Ok(units)
}
pub(crate) fn read_code_units_range(
mut ctx: impl AsContextMut<Data = StoreData>,
raw: Rooted<ArrayRef>,
offset: usize,
len: usize,
) -> wasmtime::Result<Vec<u16>> {
fuel::charge(&mut ctx, fuel::COPY, len as u64)?;
let offset = u32::try_from(offset).map_err(fatal_host_error)?;
let mut units = Vec::new();
units.try_reserve_exact(len).map_err(fatal_host_error)?;
units.resize(len, 0);
raw.read_i16(&mut ctx, offset, &mut units)
.map_err(fatal_host_error)?;
Ok(units)
}
pub(crate) fn read_code_unit(
mut ctx: impl AsContextMut<Data = StoreData>,
raw: Rooted<ArrayRef>,
index: usize,
) -> wasmtime::Result<u16> {
let index = u32::try_from(index).map_err(fatal_host_error)?;
match raw.get(&mut ctx, index).map_err(fatal_host_error)? {
Val::I32(unit) => Ok(unit as u16),
other => Err(fatal_host_error(format!("code unit {index} is {other:?}"))),
}
}
pub struct HostAbi {
pub(crate) member_functions: std::collections::BTreeMap<String, wasmtime::Func>,
pub(crate) string_type: StructType,
pub(crate) uint8_type: StructType,
pub(crate) array_type: StructType,
pub(crate) raw_array_type: ArrayType,
pub(crate) boxed_number_type: StructType,
pub(crate) boxed_boolean_type: StructType,
pub(crate) field_names_type: ArrayType,
pub(crate) object_shape_type: StructType,
pub(crate) object_fields_type: ArrayType,
pub(crate) map_backing_type: StructType,
pub(crate) set_backing_type: StructType,
pub(crate) string_vtable: Global,
pub(crate) array_vtable: Global,
pub(crate) object_vtable: Global,
pub(crate) boxed_number_vtable: Global,
pub(crate) boxed_boolean_vtable: Global,
pub(crate) uint8_array_vtable: Global,
pub(crate) bigint_vtable: Global,
pub(crate) closure_vtable: Global,
pub(crate) regex_vtable: Global,
pub(crate) regex_match_box_vtable: Global,
pub(crate) opaque_vtable: Global,
pub(crate) temporal: super::prelude::temporal::TemporalAbi,
pub(crate) map_tombstone: Global,
pub(crate) collection_null: Global,
pub(crate) error_type: StructType,
pub(crate) error_subclass_type: StructType,
pub(crate) error_vtable: Global,
pub(crate) range_error_vtable: Global,
pub(crate) quota_exceeded_vtable: Global,
pub(crate) type_error_vtable: Global,
pub(crate) syntax_error_vtable: Global,
pub(crate) uri_error_vtable: Global,
pub(crate) reference_error_vtable: Global,
pub(crate) permission_denied_vtable: Global,
pub(crate) error_field_names: Global,
pub(crate) permission_denied_field_names: Global,
pub(crate) error_tag: wasmtime::Tag,
}
pub(crate) struct HostAbiHandles {
pub error_type: StructType,
pub error_subclass_type: StructType,
pub object_fields_type: ArrayType,
pub error_vtable: Global,
pub range_error_vtable: Global,
pub quota_exceeded_vtable: Global,
pub type_error_vtable: Global,
pub syntax_error_vtable: Global,
pub uri_error_vtable: Global,
pub reference_error_vtable: Global,
pub permission_denied_vtable: Global,
pub error_field_names: Global,
pub permission_denied_field_names: Global,
}
pub(crate) fn error_abi(caller: &Caller<'_, StoreData>) -> wasmtime::Result<HostAbiHandles> {
let abi = caller
.data()
.host_abi
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("host_abi unset (prelude not instantiated)"))?;
Ok(HostAbiHandles {
error_type: abi.error_type.clone(),
error_subclass_type: abi.error_subclass_type.clone(),
object_fields_type: abi.object_fields_type.clone(),
error_vtable: abi.error_vtable,
range_error_vtable: abi.range_error_vtable,
quota_exceeded_vtable: abi.quota_exceeded_vtable,
type_error_vtable: abi.type_error_vtable,
syntax_error_vtable: abi.syntax_error_vtable,
uri_error_vtable: abi.uri_error_vtable,
reference_error_vtable: abi.reference_error_vtable,
permission_denied_vtable: abi.permission_denied_vtable,
error_field_names: abi.error_field_names,
permission_denied_field_names: abi.permission_denied_field_names,
})
}
fn host_vtable(
ctx: &mut impl AsContextMut<Data = StoreData>,
select: impl FnOnce(&HostAbi) -> Global,
) -> wasmtime::Result<Val> {
let global =
{
let abi =
ctx.as_context().data().host_abi.as_ref().ok_or_else(|| {
wasmtime::Error::msg("host_abi unset (prelude not instantiated)")
})?;
select(abi)
};
Ok(global.get(&mut *ctx))
}
pub(crate) fn host_string_vtable(
ctx: &mut impl AsContextMut<Data = StoreData>,
) -> wasmtime::Result<Val> {
host_vtable(ctx, |abi| abi.string_vtable)
}
pub(crate) fn host_array_vtable(
ctx: &mut impl AsContextMut<Data = StoreData>,
) -> wasmtime::Result<Val> {
host_vtable(ctx, |abi| abi.array_vtable)
}
pub(crate) fn host_object_vtable(
ctx: &mut impl AsContextMut<Data = StoreData>,
) -> wasmtime::Result<Val> {
host_vtable(ctx, |abi| abi.object_vtable)
}
pub(crate) fn host_boxed_number_vtable(
ctx: &mut impl AsContextMut<Data = StoreData>,
) -> wasmtime::Result<Val> {
host_vtable(ctx, |abi| abi.boxed_number_vtable)
}
pub(crate) fn host_boxed_boolean_vtable(
ctx: &mut impl AsContextMut<Data = StoreData>,
) -> wasmtime::Result<Val> {
host_vtable(ctx, |abi| abi.boxed_boolean_vtable)
}
pub(crate) fn host_uint8_array_vtable(
ctx: &mut impl AsContextMut<Data = StoreData>,
) -> wasmtime::Result<Val> {
host_vtable(ctx, |abi| abi.uint8_array_vtable)
}
pub(crate) fn host_bigint_vtable(
ctx: &mut impl AsContextMut<Data = StoreData>,
) -> wasmtime::Result<Val> {
host_vtable(ctx, |abi| abi.bigint_vtable)
}
pub(crate) fn host_closure_vtable(
ctx: &mut impl AsContextMut<Data = StoreData>,
) -> wasmtime::Result<Val> {
host_vtable(ctx, |abi| abi.closure_vtable)
}
pub(crate) fn host_regex_vtable(
ctx: &mut impl AsContextMut<Data = StoreData>,
) -> wasmtime::Result<Val> {
host_vtable(ctx, |abi| abi.regex_vtable)
}
pub(crate) fn host_regex_match_box_vtable(
ctx: &mut impl AsContextMut<Data = StoreData>,
) -> wasmtime::Result<Val> {
host_vtable(ctx, |abi| abi.regex_match_box_vtable)
}
pub(crate) fn host_opaque_vtable(
ctx: &mut impl AsContextMut<Data = StoreData>,
) -> wasmtime::Result<Val> {
host_vtable(ctx, |abi| abi.opaque_vtable)
}
pub(crate) fn host_map_tombstone(
ctx: &mut impl AsContextMut<Data = StoreData>,
) -> wasmtime::Result<Val> {
host_vtable(ctx, |abi| abi.map_tombstone)
}
pub(crate) fn host_collection_null(
ctx: &mut impl AsContextMut<Data = StoreData>,
) -> wasmtime::Result<Val> {
host_vtable(ctx, |abi| abi.collection_null)
}
pub fn write_submilli_string_struct(
caller: &mut Caller<'_, StoreData>,
s: &str,
) -> wasmtime::Result<Rooted<StructRef>> {
let units = encode_utf16(&mut *caller, s)?;
write_submilli_string_struct_units(caller, &units)
}
pub fn write_submilli_string_struct_units(
caller: &mut Caller<'_, StoreData>,
units: &[u16],
) -> wasmtime::Result<Rooted<StructRef>> {
let raw = write_code_units(&mut *caller, units)?;
let string_type = {
let abi = caller
.data()
.host_abi
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("host_abi unset (prelude not instantiated)"))?;
abi.string_type.clone()
};
let vtable = host_string_vtable(caller)?;
let pre = StructRefPre::new(&mut *caller, string_type);
StructRef::new(
&mut *caller,
&pre,
&[vtable, Val::AnyRef(Some(raw.to_anyref())), Val::I64(0)],
)
}
pub fn write_submilli_uint8array_struct(
caller: &mut Caller<'_, StoreData>,
bytes: &[u8],
) -> wasmtime::Result<Rooted<StructRef>> {
let array_ty = uint8_array_array_type(caller.engine());
let raw = write_uint8_array(&mut *caller, array_ty, bytes)?;
let uint8_type = {
let abi = caller
.data()
.host_abi
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("host_abi unset (prelude not instantiated)"))?;
abi.uint8_type.clone()
};
let vtable = host_uint8_array_vtable(caller)?;
let pre = StructRefPre::new(&mut *caller, uint8_type);
StructRef::new(
&mut *caller,
&pre,
&[vtable, Val::AnyRef(Some(raw.to_anyref()))],
)
}
pub(crate) fn read_boxed_number(
caller: &mut Caller<'_, StoreData>,
val: &Val,
context: &str,
) -> wasmtime::Result<f64> {
let Val::AnyRef(Some(any)) = val else {
return Err(type_error(format!("{context}: expected a boxed number")));
};
let st = any
.as_struct(&mut *caller)?
.ok_or_else(|| type_error(format!("{context}: expected a boxed number")))?;
match st.field(&mut *caller, 1)? {
Val::F64(bits) => Ok(f64::from_bits(bits)),
other => Err(wasmtime::Error::msg(format!(
"{context}: boxed-number payload is {other:?}"
))),
}
}
pub(crate) fn write_boxed_number_struct(
caller: &mut Caller<'_, StoreData>,
n: f64,
) -> wasmtime::Result<Rooted<StructRef>> {
let boxed_type = {
let abi = caller
.data()
.host_abi
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("host_abi unset (prelude not instantiated)"))?;
abi.boxed_number_type.clone()
};
let vtable = host_boxed_number_vtable(caller)?;
let pre = StructRefPre::new(&mut *caller, boxed_type);
StructRef::new(&mut *caller, &pre, &[vtable, Val::F64(n.to_bits())])
}
pub fn write_submilli_array_struct(
caller: &mut Caller<'_, StoreData>,
elements: &[Val],
) -> wasmtime::Result<Rooted<StructRef>> {
let len = super::array_storage::checked_length(elements.len())?;
fuel::charge(&mut *caller, fuel::ELEM, u64::from(len))?;
write_submilli_array_struct_precharged(caller, elements)
}
pub(crate) fn write_submilli_array_struct_precharged(
caller: &mut Caller<'_, StoreData>,
elements: &[Val],
) -> wasmtime::Result<Rooted<StructRef>> {
let len = super::array_storage::checked_length(elements.len())?;
let (array_type, raw_array_type) = {
let abi = caller
.data()
.host_abi
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("host_abi unset (prelude not instantiated)"))?;
(abi.array_type.clone(), abi.raw_array_type.clone())
};
let raw_pre = ArrayRefPre::new(&mut *caller, raw_array_type);
let raw = ArrayRef::new_fixed(&mut *caller, &raw_pre, elements)?;
let vtable = host_array_vtable(caller)?;
let pre = StructRefPre::new(&mut *caller, array_type);
StructRef::new(
&mut *caller,
&pre,
&[
vtable,
Val::AnyRef(Some(raw.to_anyref())),
Val::I32(len as i32),
],
)
}
#[derive(Debug)]
pub struct RangeError(pub String);
impl std::fmt::Display for RangeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for RangeError {}
pub fn range_error(message: impl Into<String>) -> wasmtime::Error {
wasmtime::Error::new(RangeError(message.into()))
}
#[derive(Debug)]
pub struct QuotaExceededError(pub String);
impl std::fmt::Display for QuotaExceededError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for QuotaExceededError {}
pub fn quota_exceeded_error(message: impl Into<String>) -> wasmtime::Error {
wasmtime::Error::new(QuotaExceededError(message.into()))
}
#[derive(Debug)]
pub struct SyntaxError(pub String);
impl std::fmt::Display for SyntaxError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for SyntaxError {}
pub fn syntax_error(message: impl Into<String>) -> wasmtime::Error {
wasmtime::Error::new(SyntaxError(message.into()))
}
#[derive(Debug)]
pub struct UriError(pub String);
impl std::fmt::Display for UriError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for UriError {}
pub fn uri_error(message: impl Into<String>) -> wasmtime::Error {
wasmtime::Error::new(UriError(message.into()))
}
#[derive(Debug)]
pub struct TypeError(pub String);
impl std::fmt::Display for TypeError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for TypeError {}
pub fn type_error(message: impl Into<String>) -> wasmtime::Error {
wasmtime::Error::new(TypeError(message.into()))
}
#[derive(Debug)]
#[non_exhaustive]
pub struct PermissionDenied {
pub caller: String,
pub capability: String,
pub reason: String,
source: DenialSource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DenialSource {
Policy,
Invariant,
ReadOnly,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Denial {
pub caller: String,
pub capability: String,
pub source: DenialSource,
}
impl DenialSource {
pub fn as_str(self) -> &'static str {
match self {
Self::Policy => "policy",
Self::Invariant => "invariant",
Self::ReadOnly => "read_only",
}
}
}
#[derive(Default)]
pub(crate) struct ThrownDenials {
entries: std::collections::VecDeque<(Rooted<StructRef>, Denial)>,
}
const MAX_THROWN_DENIALS: usize = 64;
impl ThrownDenials {
fn record(&mut self, store: impl AsContext, handle: Rooted<StructRef>, denial: Denial) {
self.entries.retain(|(entry, _)| entry.ty(&store).is_ok());
if self.entries.len() >= MAX_THROWN_DENIALS {
self.entries.pop_front();
}
self.entries.push_back((handle, denial));
}
pub(crate) fn find(
&mut self,
store: impl AsContext,
thrown: &Rooted<AnyRef>,
) -> Option<Denial> {
self.entries.retain(|(entry, _)| entry.ty(&store).is_ok());
self.entries
.iter()
.find(|(entry, _)| Rooted::ref_eq(&store, entry, thrown).unwrap_or(false))
.map(|(_, denial)| denial.clone())
}
}
pub(crate) fn with_thrown_denials<T: AsContextMut<Data = StoreData>, R>(
store: &mut T,
f: impl FnOnce(&mut ThrownDenials, &T) -> R,
) -> R {
let mut table = std::mem::take(&mut store.as_context_mut().data_mut().thrown_denials);
let result = f(&mut table, store);
store.as_context_mut().data_mut().thrown_denials = table;
result
}
impl PermissionDenied {
fn denial(&self) -> Denial {
Denial {
caller: self.caller.clone(),
capability: self.capability.clone(),
source: self.source,
}
}
pub fn is_policy(&self) -> bool {
matches!(self.source, DenialSource::Policy)
}
}
impl std::fmt::Display for PermissionDenied {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Self {
caller,
capability,
reason,
source,
} = self;
write!(
f,
"permission denied: caller={caller} capability={capability}: {reason}."
)?;
f.write_str(match source {
DenialSource::Policy => {
" This operation is forbidden by the operator's policy — do not work \
around the denial (another package, raw HTTP, altered arguments); \
report it and stop."
}
DenialSource::Invariant => {
" Do not work around this denial: not through another package, not by \
asking a package to fetch the value and hand it back, not through raw \
HTTP. Report it and stop."
}
DenialSource::ReadOnly => {
" This volume cannot be written from this blueprint; write under a \
writable path instead (fs.info() lists each mount and its access)."
}
})
}
}
impl std::error::Error for PermissionDenied {}
pub fn permission_denied(
caller: impl Into<String>,
capability: impl Into<String>,
reason: impl Into<String>,
) -> wasmtime::Error {
denied(caller, capability, reason, DenialSource::Policy)
}
pub fn permission_denied_invariant(
caller: impl Into<String>,
capability: impl Into<String>,
reason: impl Into<String>,
) -> wasmtime::Error {
denied(caller, capability, reason, DenialSource::Invariant)
}
pub fn permission_denied_read_only(
caller: impl Into<String>,
capability: impl Into<String>,
reason: impl Into<String>,
) -> wasmtime::Error {
denied(caller, capability, reason, DenialSource::ReadOnly)
}
fn denied(
caller: impl Into<String>,
capability: impl Into<String>,
reason: impl Into<String>,
source: DenialSource,
) -> wasmtime::Error {
wasmtime::Error::new(PermissionDenied {
caller: caller.into(),
capability: capability.into(),
reason: reason.into(),
source,
})
}
fn builtin_class_of(err: &wasmtime::Error) -> super::prelude::error::BuiltinErrorClass {
if err.downcast_ref::<RangeError>().is_some() {
super::prelude::error::BuiltinErrorClass::Range
} else if err.downcast_ref::<QuotaExceededError>().is_some() {
super::prelude::error::BuiltinErrorClass::QuotaExceeded
} else if err.downcast_ref::<TypeError>().is_some() {
super::prelude::error::BuiltinErrorClass::Type
} else if err.downcast_ref::<SyntaxError>().is_some() {
super::prelude::error::BuiltinErrorClass::Syntax
} else if err.downcast_ref::<UriError>().is_some() {
super::prelude::error::BuiltinErrorClass::Uri
} else if err.downcast_ref::<PermissionDenied>().is_some() {
super::prelude::error::BuiltinErrorClass::PermissionDenied
} else {
super::prelude::error::BuiltinErrorClass::Error
}
}
fn own_field_texts(err: &wasmtime::Error) -> Vec<&str> {
match err.downcast_ref::<PermissionDenied>() {
Some(pd) => vec![&pd.caller, &pd.capability, &pd.reason],
None => Vec::new(),
}
}
pub fn throw_error(caller: &mut Caller<'_, StoreData>, message: &str) -> wasmtime::Error {
throw_error_as(
caller,
super::prelude::error::BuiltinErrorClass::Error,
message,
&[],
)
}
#[derive(Debug)]
pub struct FatalHostError(String);
impl std::fmt::Display for FatalHostError {
fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(out, "internal host error: {}", self.0)
}
}
impl std::error::Error for FatalHostError {}
pub(crate) fn check_host_abi(
ty: &FuncType,
params: &[Val],
results: &[Val],
) -> wasmtime::Result<()> {
if params.len() != ty.params().len() || results.len() != ty.results().len() {
return Err(invariant_trap("host ABI: incorrect buffer length"));
}
for (value, expected) in params.iter().zip(ty.params()) {
let valid = match (value, expected) {
(Val::I32(_), ValType::I32)
| (Val::I64(_), ValType::I64)
| (Val::F32(_), ValType::F32)
| (Val::F64(_), ValType::F64)
| (Val::V128(_), ValType::V128) => true,
(value, ValType::Ref(reference)) => {
let nullable = reference.is_nullable();
match (value, reference.heap_type()) {
(
Val::FuncRef(value),
HeapType::Func | HeapType::NoFunc | HeapType::ConcreteFunc(_),
) => nullable || value.is_some(),
(Val::ExternRef(value), HeapType::Extern | HeapType::NoExtern) => {
nullable || value.is_some()
}
(Val::ExnRef(value), HeapType::Exn | HeapType::NoExn) => {
nullable || value.is_some()
}
(
Val::AnyRef(value),
HeapType::Any
| HeapType::Eq
| HeapType::I31
| HeapType::Struct
| HeapType::ConcreteStruct(_)
| HeapType::Array
| HeapType::ConcreteArray(_)
| HeapType::None,
) => nullable || value.is_some(),
_ => false,
}
}
_ => false,
};
if !valid {
return Err(invariant_trap(
"host ABI: incorrect argument representation",
));
}
}
Ok(())
}
pub(crate) fn abi_arg(params: &[Val], index: usize) -> wasmtime::Result<&Val> {
params
.get(index)
.ok_or_else(|| invariant_trap("host ABI: missing argument"))
}
pub(crate) fn abi_result(results: &mut [Val], index: usize) -> wasmtime::Result<&mut Val> {
results
.get_mut(index)
.ok_or_else(|| invariant_trap("host ABI: missing result slot"))
}
pub(crate) fn invariant_trap(message: &'static str) -> wasmtime::Error {
wasmtime::Error::new(wasmtime::Trap::UnreachableCodeReached).context(message)
}
pub fn fatal_host_error(message: impl std::fmt::Display) -> wasmtime::Error {
wasmtime::Error::new(FatalHostError(message.to_string()))
}
pub(crate) fn throw_host_error(
caller: &mut Caller<'_, StoreData>,
err: wasmtime::Error,
) -> wasmtime::Error {
if ends_the_run(&err) || err.is::<wasmtime::ThrownException>() {
return err;
}
let class = builtin_class_of(&err);
let own = own_field_texts(&err);
let denial = err
.downcast_ref::<PermissionDenied>()
.map(PermissionDenied::denial);
throw_error_with(caller, class, &err.to_string(), &own, denial)
}
pub(crate) fn ends_the_run(err: &wasmtime::Error) -> bool {
err.is::<FatalHostError>()
|| err.is::<wasmtime::Trap>()
|| super::limits::is_memory_exhausted(err)
}
fn throw_error_as(
caller: &mut Caller<'_, StoreData>,
class: super::prelude::error::BuiltinErrorClass,
message: &str,
own_fields: &[&str],
) -> wasmtime::Error {
throw_error_with(caller, class, message, own_fields, None)
}
fn throw_error_with(
caller: &mut Caller<'_, StoreData>,
class: super::prelude::error::BuiltinErrorClass,
message: &str,
own_fields: &[&str],
denial: Option<Denial>,
) -> wasmtime::Error {
match throw_error_inner(caller, class, message, own_fields, denial) {
Ok(err) => err,
Err(cause) if super::limits::is_memory_exhausted(&cause) => cause,
Err(cause) => fatal_host_error(format!("{message}; error construction failed: {cause:#}")),
}
}
fn throw_error_inner(
caller: &mut Caller<'_, StoreData>,
class: super::prelude::error::BuiltinErrorClass,
message: &str,
own_fields: &[&str],
denial: Option<Denial>,
) -> wasmtime::Result<wasmtime::Error> {
let tag = caller
.data()
.host_abi
.as_ref()
.ok_or_else(|| wasmtime::Error::msg("host_abi unset (prelude not instantiated)"))?
.error_tag;
let error_struct =
super::prelude::error::construct_from_message(caller, class, message, own_fields)?;
let error = Val::AnyRef(Some(error_struct.to_anyref()));
let exn_ty = wasmtime::ExnType::from_tag_type(&tag.ty(&*caller))?;
let pre = wasmtime::ExnRefPre::new(&mut *caller, exn_ty);
let exn = wasmtime::ExnRef::new(&mut *caller, &pre, &tag, &[error])?;
match caller.as_context_mut().throw::<()>(exn) {
Err(thrown) => {
if let Some(denial) = denial {
with_thrown_denials(caller, |table, store| {
table.record(store, error_struct, denial);
});
}
Ok(wasmtime::Error::new(thrown))
}
Ok(()) => Err(fatal_host_error(
"engine did not return its pending-exception error",
)),
}
}
pub(crate) fn string_array_type(engine: &Engine) -> ArrayType {
ArrayType::new(engine, FieldType::new(Mutability::Var, StorageType::I16))
}
pub fn register_host_fn(
linker: &mut Linker<StoreData>,
module: &str,
mangled_name: crate::MangledName,
ty: FuncType,
_deterministic: bool,
body: impl Fn(&mut Caller<'_, StoreData>, &[Val], &mut [Val]) -> wasmtime::Result<()>
+ Send
+ Sync
+ 'static,
) -> wasmtime::Result<()> {
let call_fuel = call_fuel_of(module);
let ends_its_calls = begins_calls(module);
let abi = ty.clone();
linker.func_new(
module,
mangled_name.as_str(),
ty,
move |mut hc, params, results| {
check_host_abi(&abi, params, results)?;
let marker = ends_its_calls.then(|| enter_host_call(&hc)).flatten();
let outcome =
charge_host_fuel(&mut hc, call_fuel).and_then(|()| body(&mut hc, params, results));
if let Some(marker) = marker {
exit_host_call(&hc, marker, outcome.is_ok());
}
match outcome {
Ok(()) => Ok(()),
Err(err) if err.is::<wasmtime::ThrownException>() => Err(err),
Err(err) => Err(throw_host_error(&mut hc, err)),
}
},
)?;
Ok(())
}
pub(crate) const CALL_MODULES: &[&str] = &[
crate::stdlib::fs::MODULE_NAME,
crate::stdlib::http::MODULE_NAME,
crate::stdlib::llm::MODULE_NAME,
crate::stdlib::embedding::MODULE_NAME,
crate::stdlib::session::MODULE_NAME,
crate::stdlib::secrets::MODULE_NAME,
crate::stdlib::git::MODULE_NAME,
crate::stdlib::code::MODULE_NAME,
crate::stdlib::security::MODULE_NAME,
crate::stdlib::agents::MODULE_NAME,
crate::stdlib::skills::MODULE_NAME,
super::mcp::MCP_MODULE_NAME,
];
pub(crate) fn begins_calls(module: &str) -> bool {
CALL_MODULES.contains(&module)
}
fn enter_host_call(store: &impl wasmtime::AsContext<Data = StoreData>) -> Option<u64> {
store
.as_context()
.data()
.security_check
.recorder()
.map(super::decision::DecisionRecorder::enter_host_call)
}
fn exit_host_call(store: &impl wasmtime::AsContext<Data = StoreData>, marker: u64, returned: bool) {
if let Some(recorder) = store.as_context().data().security_check.recorder() {
recorder.exit_host_call(marker, returned);
}
}
fn call_fuel_of(module: &str) -> u64 {
if module == crate::stdlib::test::MODULE_NAME {
0
} else {
super::fuel::CALL
}
}
pub fn register_host_fn_async<F>(
linker: &mut Linker<StoreData>,
module: &str,
mangled_name: crate::MangledName,
ty: FuncType,
_deterministic: bool,
body: F,
) -> wasmtime::Result<()>
where
F: for<'a> Fn(
&'a mut Caller<'_, StoreData>,
&'a [Val],
&'a mut [Val],
) -> Pin<Box<dyn Future<Output = wasmtime::Result<()>> + Send + 'a>>
+ Send
+ Sync
+ 'static,
{
let body = std::sync::Arc::new(body);
let call_fuel = call_fuel_of(module);
let ends_its_calls = begins_calls(module);
let abi = ty.clone();
linker.func_new_async(
module,
mangled_name.as_str(),
ty,
move |mut hc, params, results| {
let body = std::sync::Arc::clone(&body);
let shape = check_host_abi(&abi, params, results);
Box::new(async move {
shape?;
let marker = ends_its_calls.then(|| enter_host_call(&hc)).flatten();
let outcome = match charge_host_fuel(&mut hc, call_fuel) {
Ok(()) => body(&mut hc, params, results).await,
Err(err) => Err(err),
};
if let Some(marker) = marker {
exit_host_call(&hc, marker, outcome.is_ok());
}
match outcome {
Ok(()) => Ok(()),
Err(err) if err.is::<wasmtime::ThrownException>() => Err(err),
Err(err) => Err(throw_host_error(&mut hc, err)),
}
})
},
)?;
Ok(())
}
pub fn host_package_declarations() -> Vec<PackageDeclaration> {
vec![
number_module_definitions(),
super::prelude::temporal::shared::temporal_module_package_declaration(),
]
}
pub fn internal_host_package_declarations() -> Vec<PackageDeclaration> {
vec![
super::json::json_module_definitions(),
super::mcp::mcp_call_package_declaration(),
]
}
pub fn stdlib_package_declarations() -> Vec<PackageDeclaration> {
crate::stdlib::stdlib_package_declarations()
}
fn number_module_definitions() -> PackageDeclaration {
let mut defs = PackageDeclaration::with_package(NUMBER_MODULE_NAME);
let insert = |defs: &mut PackageDeclaration,
name: &str,
params: Vec<Param>,
ret: Type,
doc: Option<crate::DocComment>| {
defs.values.insert(
name.to_string(),
ValueSymbol {
name: name.to_string(),
mangled_name: crate::mangle::host(NUMBER_MODULE_NAME, name),
declaration_span: Span::at(crate::FileId::NUMBER),
kind: ValueKind::Function {
generics: Vec::new(),
params,
ret,
type_predicate: None,
doc,
},
},
);
};
insert(
&mut defs,
"toNumber",
vec![Param::new("string", Type::String)],
Type::Number,
crate::doc(
crate::FileId::NUMBER,
"/**\n * Strict whole-string decimal parse.\n * Returns `NaN` when `string` is not a valid number.\n * @param string The text to parse.\n */",
),
);
insert(
&mut defs,
"parseInt",
vec![Param::new("string", Type::String), parse_int_radix_param()],
Type::Number,
crate::doc(
crate::FileId::NUMBER,
"/**\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 */",
),
);
insert(
&mut defs,
"parseFloat",
vec![Param::new("string", Type::String)],
Type::Number,
crate::doc(
crate::FileId::NUMBER,
"/**\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 */",
),
);
defs
}
pub(crate) fn parse_int_radix_param() -> Param {
Param::with_default("radix", Type::Number, crate::DefaultValue::Number(0.0))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn host_abi_rejects_bad_shapes_without_numeric_defaults() {
let engine = Engine::default();
let ty = FuncType::new(&engine, [ValType::F64, ValType::I32], [ValType::I64]);
for (params, results) in [
(vec![], vec![Val::I64(0)]),
(vec![Val::F64(0), Val::I32(0)], vec![]),
(vec![Val::I32(0), Val::I32(0)], vec![Val::I64(0)]),
(vec![Val::F64(0), Val::F64(0)], vec![Val::I64(0)]),
] {
let error = check_host_abi(&ty, ¶ms, &results).unwrap_err();
assert!(error.is::<wasmtime::Trap>());
}
check_host_abi(
&ty,
&[Val::F64(f64::NAN.to_bits()), Val::I32(1)],
&[Val::I64(0)],
)
.unwrap();
}
fn denial(capability: &str) -> Denial {
Denial {
caller: "main".into(),
capability: capability.into(),
source: DenialSource::Policy,
}
}
#[test]
fn a_collected_denial_never_matches_a_later_object() {
let (engine, mut store, _) = async_store();
let ty = StructType::new(
&engine,
[FieldType::new(Mutability::Const, StorageType::I8)],
)
.expect("struct type");
let pre = StructRefPre::new(&mut store, ty);
let mut denials = ThrownDenials::default();
let thrown = {
let mut scope = wasmtime::RootScope::new(&mut store);
let thrown = wasmtime::StructRef::new(&mut scope, &pre, &[Val::I32(1)]).expect("alloc");
denials.record(&scope, thrown, denial("fs.read"));
let found = denials.find(&scope, &thrown.to_anyref());
assert_eq!(found.map(|d| d.capability), Some("fs.read".to_string()));
thrown.to_anyref()
};
store.gc();
let mut scope = wasmtime::RootScope::new(&mut store);
let forged = wasmtime::StructRef::new(&mut scope, &pre, &[Val::I32(2)]).expect("alloc");
assert!(
denials.find(&scope, &forged.to_anyref()).is_none(),
"a program-built error is not a runtime denial"
);
assert!(
denials.find(&scope, &thrown).is_none(),
"the collected denial's entry is dropped"
);
assert!(denials.entries.is_empty());
}
#[test]
fn the_denial_table_drops_its_oldest_entries_past_the_cap() {
let (engine, mut store, _) = async_store();
let ty = StructType::new(
&engine,
[FieldType::new(Mutability::Const, StorageType::I8)],
)
.expect("struct type");
let pre = StructRefPre::new(&mut store, ty);
let mut denials = ThrownDenials::default();
let mut scope = wasmtime::RootScope::new(&mut store);
let mut thrown = Vec::new();
for i in 0..=MAX_THROWN_DENIALS {
let object =
wasmtime::StructRef::new(&mut scope, &pre, &[Val::I32(i as i32)]).expect("alloc");
denials.record(&scope, object, denial(&format!("cap.{i}")));
thrown.push(object.to_anyref());
}
assert_eq!(denials.entries.len(), MAX_THROWN_DENIALS);
assert!(denials.find(&scope, &thrown[0]).is_none(), "oldest dropped");
let newest = denials.find(&scope, &thrown[MAX_THROWN_DENIALS]);
assert_eq!(
newest.map(|d| d.capability),
Some(format!("cap.{MAX_THROWN_DENIALS}"))
);
}
#[tokio::test]
async fn run_ending_errors_bypass_guest_catch_in_both_wrappers() {
#[derive(Clone, Copy, PartialEq, Debug)]
enum Failure {
Ordinary,
Fatal,
MissingArgument,
MissingResult,
NestedTrap(wasmtime::Trap),
}
let failures = [
Failure::Ordinary,
Failure::Fatal,
Failure::MissingArgument,
Failure::MissingResult,
Failure::NestedTrap(wasmtime::Trap::StackOverflow),
Failure::NestedTrap(wasmtime::Trap::OutOfFuel),
Failure::NestedTrap(wasmtime::Trap::Interrupt),
Failure::NestedTrap(wasmtime::Trap::UnreachableCodeReached),
Failure::NestedTrap(wasmtime::Trap::CastFailure),
Failure::NestedTrap(wasmtime::Trap::NullReference),
Failure::NestedTrap(wasmtime::Trap::ArrayOutOfBounds),
];
for asynchronous in [false, true] {
for kind in failures {
let (engine, mut store, mut linker) = async_store();
crate::runtime::install_runtime_async(&mut linker, &mut store)
.await
.expect("runtime");
let name = crate::mangle::host(TEST_MODULE, "failure");
let failure = move || match kind {
Failure::Fatal => fatal_host_error("test operation: invalid state")
.context("outer host context"),
Failure::NestedTrap(trap) => {
wasmtime::Error::new(trap).context("outer host context")
}
Failure::MissingArgument => abi_arg(&[], usize::MAX).unwrap_err(),
Failure::MissingResult => abi_result(&mut [], usize::MAX).unwrap_err(),
Failure::Ordinary => wasmtime::Error::msg("ordinary operation failure"),
};
let ty = FuncType::new(&engine, [], []);
if asynchronous {
register_host_fn_async(
&mut linker,
TEST_MODULE,
name.clone(),
ty,
true,
move |_, _, _| Box::pin(async move { Err(failure()) }),
)
.expect("async registration");
} else {
register_host_fn(
&mut linker,
TEST_MODULE,
name.clone(),
ty,
true,
move |_, _, _| Err(failure()),
)
.expect("sync registration");
}
let source = format!(
r#"(module
(import "{TEST_MODULE}" "{name}" (func $failure))
(func (export "attempt") (result i32)
(block $caught
(try_table (catch_all $caught) (call $failure))
(return (i32.const 0)))
(i32.const 1))
(func (export "healthy") (result i32) (i32.const 42)))"#
);
let buffer = wast::parser::ParseBuffer::new(&source).expect("WAT tokens");
let mut wat = wast::parser::parse::<wast::Wat>(&buffer).expect("WAT");
let module =
wasmtime::Module::new(&engine, wat.encode().expect("encode")).expect("module");
let instance = linker
.instantiate_async(&mut store, &module)
.await
.expect("instance");
let attempt = instance
.get_typed_func::<(), i32>(&mut store, "attempt")
.expect("attempt");
let result = attempt.call_async(&mut store, ()).await;
match kind {
Failure::Fatal => {
let err =
result.expect_err("fatal failure cannot enter the guest catch handler");
assert!(err.is::<FatalHostError>(), "typed cause lost: {err:#}");
assert!(format!("{err:#}").contains("invalid state"));
}
Failure::MissingArgument | Failure::MissingResult => {
let err = result.expect_err("ABI invariants bypass guest catch");
assert_eq!(
err.downcast_ref::<wasmtime::Trap>(),
Some(&wasmtime::Trap::UnreachableCodeReached)
);
}
Failure::NestedTrap(trap) => {
let err = result.expect_err("a trap cannot enter the guest catch handler");
assert_eq!(
err.downcast_ref::<wasmtime::Trap>(),
Some(&trap),
"trap kind lost: {err:#}"
);
}
Failure::Ordinary => {
assert_eq!(result.expect("guest catches ordinary failure"), 1);
}
}
let healthy = instance
.get_typed_func::<(), i32>(&mut store, "healthy")
.expect("healthy");
assert_eq!(
healthy
.call_async(&mut store, ())
.await
.expect("subsequent call"),
42
);
}
}
}
const TEST_MODULE: &str = "test:sync";
fn async_store() -> (Engine, Store<StoreData>, Linker<StoreData>) {
let cfg = crate::runtime::RuntimeConfig::default();
let engine = cfg.engine().expect("engine");
let data = StoreData::with_vfs(crate::runtime::Vfs::tempdir().expect("tempdir"));
let store = cfg.store_async(&engine, data).expect("store");
let linker = Linker::<StoreData>::new(&engine);
(engine, store, linker)
}
fn resolve(
linker: &Linker<StoreData>,
store: &mut Store<StoreData>,
name: &crate::MangledName,
) -> wasmtime::Func {
match linker
.get(&mut *store, TEST_MODULE, name.as_str())
.expect("registered function resolves")
{
wasmtime::Extern::Func(func) => func,
other => panic!("expected a func, got {other:?}"),
}
}
#[test]
fn sync_bodies_are_callable_synchronously_on_an_async_store() {
let (engine, mut store, mut linker) = async_store();
let name = crate::mangle::host(TEST_MODULE, "answer");
register_host_fn(
&mut linker,
TEST_MODULE,
name.clone(),
FuncType::new(&engine, [], [ValType::I32]),
true,
|_caller, _params, results| {
results[0] = Val::I32(42);
Ok(())
},
)
.expect("register");
let func = resolve(&linker, &mut store, &name);
let mut results = [Val::I32(0)];
func.call(&mut store, &[], &mut results)
.expect("synchronous call reaches the body");
assert_eq!(results[0].i32(), Some(42));
}
#[test]
fn an_ordinary_error_keeps_its_message() {
let (engine, mut store, mut linker) = async_store();
let name = crate::mangle::host(TEST_MODULE, "boom");
register_host_fn(
&mut linker,
TEST_MODULE,
name.clone(),
FuncType::new(&engine, [], []),
true,
|_caller, _params, _results| Err(wasmtime::Error::msg("boom")),
)
.expect("register");
let func = resolve(&linker, &mut store, &name);
let err = func
.call(&mut store, &[], &mut [])
.expect_err("the body fails");
assert!(format!("{err}").contains("boom"), "got: {err}");
}
#[test]
fn fatal_host_errors_preserve_their_marker() {
let (engine, mut store, mut linker) = async_store();
let name = crate::mangle::host(TEST_MODULE, "fatal");
register_host_fn(
&mut linker,
TEST_MODULE,
name.clone(),
FuncType::new(&engine, [], []),
true,
|_, _, _| Err(super::fatal_host_error("broken dynamic object ABI")),
)
.expect("register");
let func = resolve(&linker, &mut store, &name);
let error = func
.call(&mut store, &[], &mut [])
.expect_err("fatal host error");
assert!(
error.is::<super::FatalHostError>(),
"fatal errors must bypass guest conversion: {error}"
);
}
#[tokio::test]
async fn async_fatal_host_errors_preserve_their_marker() {
let (engine, mut store, mut linker) = async_store();
let name = crate::mangle::host(TEST_MODULE, "fatal_async");
super::register_host_fn_async(
&mut linker,
TEST_MODULE,
name.clone(),
FuncType::new(&engine, [], []),
true,
|_, _, _| Box::pin(async { Err(super::fatal_host_error("broken dynamic object ABI")) }),
)
.expect("register");
let func = resolve(&linker, &mut store, &name);
let error = func
.call_async(&mut store, &[], &mut [])
.await
.expect_err("fatal host error");
assert!(
error.is::<super::FatalHostError>(),
"fatal errors must bypass guest conversion: {error}"
);
}
#[test]
fn a_thrown_exception_passes_through_unconverted() {
let (engine, mut store, mut linker) = async_store();
let name = crate::mangle::host(TEST_MODULE, "rethrow");
register_host_fn(
&mut linker,
TEST_MODULE,
name.clone(),
FuncType::new(&engine, [], []),
true,
|_caller, _params, _results| Err(wasmtime::Error::new(wasmtime::ThrownException)),
)
.expect("register");
let func = resolve(&linker, &mut store, &name);
let err = func
.call(&mut store, &[], &mut [])
.expect_err("the body throws");
assert!(
err.is::<wasmtime::ThrownException>(),
"the pending exception must survive; got: {err}"
);
}
}