Skip to main content

rpc/natives/
catch_each.rs

1//! `(catch-each items var body)` host native — Tier 2's iteration-
2//! bounded error recovery (ADR-0025).
3//!
4//! Compiler-side lowering: the body is wrapped as a single-arg lambda
5//! and lifted to a real wasm fn via Tier 1.5's closure machinery. The
6//! call site extracts (funcref, env_anyref) from the `$closure_<sig>`
7//! struct and pushes them alongside the items pair-list, then emits
8//! `call $__nomi_catch_each`. The host walks the items chain in Rust:
9//! per element it invokes the funcref via `Func::call_async` with
10//! `(env, item)` args, recovers `wasmtime::Error`, classifies it, and
11//! collects the per-call outcome as a heterogeneous result cell.
12//!
13//! Each result cell is a list head: `(ok value)` for normal returns
14//! and `(err code msg)` for script-raised / engine-classified errors.
15//! Both ride `$pair` chains whose cars are `anyref`, so the resulting
16//! list is `pair<anyref>`; consumers reach into a cell with the
17//! standard `CAR` / `CADR` / `CADDR` accessors.
18//!
19//! Engine-managed traps are *not* catchable: when
20//! `classify_runtime_error` returns `OutOfFuel` or `EpochInterrupt`, the
21//! host fn re-throws the original error so it bubbles past `catch-each`
22//! to the outer caller. This rule is the load-bearing reason the fuel /
23//! epoch budgets continue to bound a hostile script — without it, a
24//! script could wrap an infinite loop in `catch-each` and keep running
25//! past the deadline.
26
27use scripting::checked_ref::CheckedAnyRef;
28use scripting::runtime::{
29    EngineError, alloc_pair_chain, alloc_string_ref, classify_runtime_error, err_code_and_message,
30};
31use wasmtime::{AnyRef, AsContextMut, Caller, Func, Linker, Rooted, StructRef, Val};
32
33use crate::session::SessionData;
34
35pub fn register(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
36    linker.func_wrap_async(
37        "nomi",
38        "__nomi_catch_each",
39        |mut caller: Caller<'_, SessionData>,
40         (cb, env, items): (
41            Option<Func>,
42            Option<Rooted<AnyRef>>,
43            Option<Rooted<StructRef>>,
44        )|
45         -> Box<
46            dyn std::future::Future<Output = wasmtime::Result<Option<Rooted<StructRef>>>> + Send,
47        > {
48            Box::new(async move {
49                let cb =
50                    cb.ok_or_else(|| wasmtime::Error::msg("catch-each: closure funcref is null"))?;
51                let item_anyrefs = collect_items(&mut caller, items)?;
52                let mut result_cells: Vec<Rooted<AnyRef>> = Vec::with_capacity(item_anyrefs.len());
53                for item in item_anyrefs {
54                    let cell = invoke_one(&mut caller, &cb, env, item).await?;
55                    result_cells.push(cell);
56                }
57                alloc_pair_chain(&mut caller, result_cells).await
58            })
59        },
60    )?;
61    Ok(())
62}
63
64/// Walks the `$pair` chain into a `Vec<Rooted<AnyRef>>` of cars without
65/// re-entering the guest. Each cell's car is the original item; the cdr
66/// chains through nullable `$pair` structs (field index 1) until null.
67fn collect_items(
68    caller: &mut Caller<'_, SessionData>,
69    head: Option<Rooted<StructRef>>,
70) -> wasmtime::Result<Vec<Rooted<AnyRef>>> {
71    let mut out: Vec<Rooted<AnyRef>> = Vec::new();
72    let mut cursor = head;
73    while let Some(node) = cursor {
74        let car_val = node.field(caller.as_context_mut(), 0)?;
75        let car = match car_val {
76            Val::AnyRef(Some(any)) => any,
77            Val::AnyRef(None) => {
78                return Err(wasmtime::Error::msg(
79                    "catch-each: items list contains a null car",
80                ));
81            }
82            _ => {
83                return Err(wasmtime::Error::msg(
84                    "catch-each: items list car field is not anyref",
85                ));
86            }
87        };
88        out.push(car);
89        let cdr_val = node.field(caller.as_context_mut(), 1)?;
90        cursor = match cdr_val {
91            Val::AnyRef(Some(any)) => Some(any.checked_struct(caller.as_context_mut())?),
92            _ => None,
93        };
94    }
95    Ok(out)
96}
97
98/// Invokes `cb(env, item)`. On `Ok`, returns a `(ok value)` cell.
99/// On `Err`, classifies the trap: `OutOfFuel` / `EpochInterrupt`
100/// re-throw to the outer caller (engine deadlines aren't catchable);
101/// every other classification produces an `(err code msg)` cell.
102async fn invoke_one(
103    caller: &mut Caller<'_, SessionData>,
104    cb: &Func,
105    env: Option<Rooted<AnyRef>>,
106    item: Rooted<AnyRef>,
107) -> wasmtime::Result<Rooted<AnyRef>> {
108    let mut results = [Val::AnyRef(None)];
109    let outcome = cb
110        .call_async(
111            caller.as_context_mut(),
112            &[Val::AnyRef(env), Val::AnyRef(Some(item))],
113            &mut results,
114        )
115        .await;
116    match outcome {
117        Ok(()) => {
118            let value_any = match results[0] {
119                Val::AnyRef(any) => any,
120                _ => {
121                    return Err(wasmtime::Error::msg(
122                        "catch-each: closure returned non-anyref Val variant",
123                    ));
124                }
125            };
126            build_ok_cell(caller, value_any).await
127        }
128        Err(err) => match classify_runtime_error(&err) {
129            EngineError::OutOfFuel | EngineError::EpochInterrupt => Err(err),
130            classified => build_err_cell(caller, &classified).await,
131        },
132    }
133}
134
135/// Builds the list `(ok value)` and returns its head as `anyref`. A
136/// `None` body return becomes `(ok)` — a single-element list — so the
137/// script can match on `(car cell) == 'ok` with `(cadr cell)` either
138/// holding the value or being absent.
139async fn build_ok_cell(
140    caller: &mut Caller<'_, SessionData>,
141    value: Option<Rooted<AnyRef>>,
142) -> wasmtime::Result<Rooted<AnyRef>> {
143    let tag = string_anyref(caller, b"ok")?;
144    let mut elems = vec![tag];
145    if let Some(v) = value {
146        elems.push(v);
147    }
148    pair_chain_to_anyref(caller, elems).await
149}
150
151/// Builds the list `(err code msg)` and returns its head as `anyref`.
152async fn build_err_cell(
153    caller: &mut Caller<'_, SessionData>,
154    classified: &EngineError,
155) -> wasmtime::Result<Rooted<AnyRef>> {
156    let tag = string_anyref(caller, b"err")?;
157    let (code, message) = err_code_and_message(classified);
158    let code_any = string_anyref(caller, code.as_bytes())?;
159    let message_any = string_anyref(caller, message.as_bytes())?;
160    pair_chain_to_anyref(caller, vec![tag, code_any, message_any]).await
161}
162
163async fn pair_chain_to_anyref(
164    caller: &mut Caller<'_, SessionData>,
165    elems: Vec<Rooted<AnyRef>>,
166) -> wasmtime::Result<Rooted<AnyRef>> {
167    let head = alloc_pair_chain(caller, elems)
168        .await?
169        .ok_or_else(|| wasmtime::Error::msg("catch-each: built an empty result cell"))?;
170    Ok(head.to_anyref())
171}
172
173fn string_anyref(
174    caller: &mut Caller<'_, SessionData>,
175    bytes: &[u8],
176) -> wasmtime::Result<Rooted<AnyRef>> {
177    Ok(alloc_string_ref(caller, bytes)?.to_anyref())
178}