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::runtime::{
28    EngineError, alloc_pair_chain, alloc_string_ref, classify_runtime_error, err_code_and_message,
29};
30use wasmtime::{AnyRef, AsContextMut, Caller, Func, Linker, Rooted, StructRef, Val};
31
32use crate::session::SessionData;
33
34pub fn register(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
35    linker.func_wrap_async(
36        "nomi",
37        "__nomi_catch_each",
38        |mut caller: Caller<'_, SessionData>,
39         (cb, env, items): (
40            Option<Func>,
41            Option<Rooted<AnyRef>>,
42            Option<Rooted<StructRef>>,
43        )|
44         -> Box<
45            dyn std::future::Future<Output = wasmtime::Result<Option<Rooted<StructRef>>>> + Send,
46        > {
47            Box::new(async move {
48                let cb =
49                    cb.ok_or_else(|| wasmtime::Error::msg("catch-each: closure funcref is null"))?;
50                let item_anyrefs = collect_items(&mut caller, items)?;
51                let mut result_cells: Vec<Rooted<AnyRef>> = Vec::with_capacity(item_anyrefs.len());
52                for item in item_anyrefs {
53                    let cell = invoke_one(&mut caller, &cb, env, item).await?;
54                    result_cells.push(cell);
55                }
56                alloc_pair_chain(&mut caller, result_cells).await
57            })
58        },
59    )?;
60    Ok(())
61}
62
63/// Walks the `$pair` chain into a `Vec<Rooted<AnyRef>>` of cars without
64/// re-entering the guest. Each cell's car is the original item; the cdr
65/// chains through nullable `$pair` structs (field index 1) until null.
66fn collect_items(
67    caller: &mut Caller<'_, SessionData>,
68    head: Option<Rooted<StructRef>>,
69) -> wasmtime::Result<Vec<Rooted<AnyRef>>> {
70    let mut out: Vec<Rooted<AnyRef>> = Vec::new();
71    let mut cursor = head;
72    while let Some(node) = cursor {
73        let car_val = node.field(caller.as_context_mut(), 0)?;
74        let car = match car_val {
75            Val::AnyRef(Some(any)) => any,
76            Val::AnyRef(None) => {
77                return Err(wasmtime::Error::msg(
78                    "catch-each: items list contains a null car",
79                ));
80            }
81            _ => {
82                return Err(wasmtime::Error::msg(
83                    "catch-each: items list car field is not anyref",
84                ));
85            }
86        };
87        out.push(car);
88        let cdr_val = node.field(caller.as_context_mut(), 1)?;
89        cursor = match cdr_val {
90            Val::AnyRef(Some(any)) => Some(any.unwrap_struct(caller.as_context_mut())?),
91            _ => None,
92        };
93    }
94    Ok(out)
95}
96
97/// Invokes `cb(env, item)`. On `Ok`, returns a `(ok value)` cell.
98/// On `Err`, classifies the trap: `OutOfFuel` / `EpochInterrupt`
99/// re-throw to the outer caller (engine deadlines aren't catchable);
100/// every other classification produces an `(err code msg)` cell.
101async fn invoke_one(
102    caller: &mut Caller<'_, SessionData>,
103    cb: &Func,
104    env: Option<Rooted<AnyRef>>,
105    item: Rooted<AnyRef>,
106) -> wasmtime::Result<Rooted<AnyRef>> {
107    let mut results = [Val::AnyRef(None)];
108    let outcome = cb
109        .call_async(
110            caller.as_context_mut(),
111            &[Val::AnyRef(env), Val::AnyRef(Some(item))],
112            &mut results,
113        )
114        .await;
115    match outcome {
116        Ok(()) => {
117            let value_any = match results[0] {
118                Val::AnyRef(any) => any,
119                _ => {
120                    return Err(wasmtime::Error::msg(
121                        "catch-each: closure returned non-anyref Val variant",
122                    ));
123                }
124            };
125            build_ok_cell(caller, value_any).await
126        }
127        Err(err) => match classify_runtime_error(&err) {
128            EngineError::OutOfFuel | EngineError::EpochInterrupt => Err(err),
129            classified => build_err_cell(caller, &classified).await,
130        },
131    }
132}
133
134/// Builds the list `(ok value)` and returns its head as `anyref`. A
135/// `None` body return becomes `(ok)` — a single-element list — so the
136/// script can match on `(car cell) == 'ok` with `(cadr cell)` either
137/// holding the value or being absent.
138async fn build_ok_cell(
139    caller: &mut Caller<'_, SessionData>,
140    value: Option<Rooted<AnyRef>>,
141) -> wasmtime::Result<Rooted<AnyRef>> {
142    let tag = string_anyref(caller, b"ok")?;
143    let mut elems = vec![tag];
144    if let Some(v) = value {
145        elems.push(v);
146    }
147    pair_chain_to_anyref(caller, elems).await
148}
149
150/// Builds the list `(err code msg)` and returns its head as `anyref`.
151async fn build_err_cell(
152    caller: &mut Caller<'_, SessionData>,
153    classified: &EngineError,
154) -> wasmtime::Result<Rooted<AnyRef>> {
155    let tag = string_anyref(caller, b"err")?;
156    let (code, message) = err_code_and_message(classified);
157    let code_any = string_anyref(caller, code.as_bytes())?;
158    let message_any = string_anyref(caller, message.as_bytes())?;
159    pair_chain_to_anyref(caller, vec![tag, code_any, message_any]).await
160}
161
162async fn pair_chain_to_anyref(
163    caller: &mut Caller<'_, SessionData>,
164    elems: Vec<Rooted<AnyRef>>,
165) -> wasmtime::Result<Rooted<AnyRef>> {
166    let head = alloc_pair_chain(caller, elems)
167        .await?
168        .ok_or_else(|| wasmtime::Error::msg("catch-each: built an empty result cell"))?;
169    Ok(head.to_anyref())
170}
171
172fn string_anyref(
173    caller: &mut Caller<'_, SessionData>,
174    bytes: &[u8],
175) -> wasmtime::Result<Rooted<AnyRef>> {
176    Ok(alloc_string_ref(caller, bytes)?.to_anyref())
177}