Lines
90.29 %
Functions
31.11 %
Branches
100 %
//! `(catch-each items var body)` host native — Tier 2's iteration-
//! bounded error recovery (ADR-0025).
//!
//! Compiler-side lowering: the body is wrapped as a single-arg lambda
//! and lifted to a real wasm fn via Tier 1.5's closure machinery. The
//! call site extracts (funcref, env_anyref) from the `$closure_<sig>`
//! struct and pushes them alongside the items pair-list, then emits
//! `call $__nomi_catch_each`. The host walks the items chain in Rust:
//! per element it invokes the funcref via `Func::call_async` with
//! `(env, item)` args, recovers `wasmtime::Error`, classifies it, and
//! collects the per-call outcome as a heterogeneous result cell.
//! Each result cell is a list head: `(ok value)` for normal returns
//! and `(err code msg)` for script-raised / engine-classified errors.
//! Both ride `$pair` chains whose cars are `anyref`, so the resulting
//! list is `pair<anyref>`; consumers reach into a cell with the
//! standard `CAR` / `CADR` / `CADDR` accessors.
//! Engine-managed traps are *not* catchable: when
//! `classify_runtime_error` returns `OutOfFuel` or `EpochInterrupt`, the
//! host fn re-throws the original error so it bubbles past `catch-each`
//! to the outer caller. This rule is the load-bearing reason the fuel /
//! epoch budgets continue to bound a hostile script — without it, a
//! script could wrap an infinite loop in `catch-each` and keep running
//! past the deadline.
use scripting::runtime::{
EngineError, alloc_pair_chain, alloc_string_ref, classify_runtime_error, err_code_and_message,
};
use wasmtime::{AnyRef, AsContextMut, Caller, Func, Linker, Rooted, StructRef, Val};
use crate::session::SessionData;
pub fn register(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
linker.func_wrap_async(
"nomi",
"__nomi_catch_each",
|mut caller: Caller<'_, SessionData>,
(cb, env, items): (
Option<Func>,
Option<Rooted<AnyRef>>,
Option<Rooted<StructRef>>,
)|
-> Box<
dyn std::future::Future<Output = wasmtime::Result<Option<Rooted<StructRef>>>> + Send,
> {
Box::new(async move {
let cb =
cb.ok_or_else(|| wasmtime::Error::msg("catch-each: closure funcref is null"))?;
let item_anyrefs = collect_items(&mut caller, items)?;
let mut result_cells: Vec<Rooted<AnyRef>> = Vec::with_capacity(item_anyrefs.len());
for item in item_anyrefs {
let cell = invoke_one(&mut caller, &cb, env, item).await?;
result_cells.push(cell);
}
alloc_pair_chain(&mut caller, result_cells).await
})
},
)?;
Ok(())
/// Walks the `$pair` chain into a `Vec<Rooted<AnyRef>>` of cars without
/// re-entering the guest. Each cell's car is the original item; the cdr
/// chains through nullable `$pair` structs (field index 1) until null.
fn collect_items(
caller: &mut Caller<'_, SessionData>,
head: Option<Rooted<StructRef>>,
) -> wasmtime::Result<Vec<Rooted<AnyRef>>> {
let mut out: Vec<Rooted<AnyRef>> = Vec::new();
let mut cursor = head;
while let Some(node) = cursor {
let car_val = node.field(caller.as_context_mut(), 0)?;
let car = match car_val {
Val::AnyRef(Some(any)) => any,
Val::AnyRef(None) => {
return Err(wasmtime::Error::msg(
"catch-each: items list contains a null car",
));
_ => {
"catch-each: items list car field is not anyref",
out.push(car);
let cdr_val = node.field(caller.as_context_mut(), 1)?;
cursor = match cdr_val {
Val::AnyRef(Some(any)) => Some(any.unwrap_struct(caller.as_context_mut())?),
_ => None,
Ok(out)
/// Invokes `cb(env, item)`. On `Ok`, returns a `(ok value)` cell.
/// On `Err`, classifies the trap: `OutOfFuel` / `EpochInterrupt`
/// re-throw to the outer caller (engine deadlines aren't catchable);
/// every other classification produces an `(err code msg)` cell.
async fn invoke_one(
cb: &Func,
env: Option<Rooted<AnyRef>>,
item: Rooted<AnyRef>,
) -> wasmtime::Result<Rooted<AnyRef>> {
let mut results = [Val::AnyRef(None)];
let outcome = cb
.call_async(
caller.as_context_mut(),
&[Val::AnyRef(env), Val::AnyRef(Some(item))],
&mut results,
)
.await;
match outcome {
Ok(()) => {
let value_any = match results[0] {
Val::AnyRef(any) => any,
"catch-each: closure returned non-anyref Val variant",
build_ok_cell(caller, value_any).await
Err(err) => match classify_runtime_error(&err) {
EngineError::OutOfFuel | EngineError::EpochInterrupt => Err(err),
classified => build_err_cell(caller, &classified).await,
/// Builds the list `(ok value)` and returns its head as `anyref`. A
/// `None` body return becomes `(ok)` — a single-element list — so the
/// script can match on `(car cell) == 'ok` with `(cadr cell)` either
/// holding the value or being absent.
async fn build_ok_cell(
value: Option<Rooted<AnyRef>>,
let tag = string_anyref(caller, b"ok")?;
let mut elems = vec![tag];
if let Some(v) = value {
elems.push(v);
pair_chain_to_anyref(caller, elems).await
/// Builds the list `(err code msg)` and returns its head as `anyref`.
async fn build_err_cell(
classified: &EngineError,
let tag = string_anyref(caller, b"err")?;
let (code, message) = err_code_and_message(classified);
let code_any = string_anyref(caller, code.as_bytes())?;
let message_any = string_anyref(caller, message.as_bytes())?;
pair_chain_to_anyref(caller, vec![tag, code_any, message_any]).await
async fn pair_chain_to_anyref(
elems: Vec<Rooted<AnyRef>>,
let head = alloc_pair_chain(caller, elems)
.await?
.ok_or_else(|| wasmtime::Error::msg("catch-each: built an empty result cell"))?;
Ok(head.to_anyref())
fn string_anyref(
bytes: &[u8],
Ok(alloc_string_ref(caller, bytes)?.to_anyref())