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

            
27
use scripting::runtime::{
28
    EngineError, alloc_pair_chain, alloc_string_ref, classify_runtime_error, err_code_and_message,
29
};
30
use wasmtime::{AnyRef, AsContextMut, Caller, Func, Linker, Rooted, StructRef, Val};
31

            
32
use crate::session::SessionData;
33

            
34
6044
pub fn register(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
35
6044
    linker.func_wrap_async(
36
6044
        "nomi",
37
6044
        "__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
175
        > {
47
175
            Box::new(async move {
48
175
                let cb =
49
175
                    cb.ok_or_else(|| wasmtime::Error::msg("catch-each: closure funcref is null"))?;
50
175
                let item_anyrefs = collect_items(&mut caller, items)?;
51
175
                let mut result_cells: Vec<Rooted<AnyRef>> = Vec::with_capacity(item_anyrefs.len());
52
400
                for item in item_anyrefs {
53
400
                    let cell = invoke_one(&mut caller, &cb, env, item).await?;
54
375
                    result_cells.push(cell);
55
                }
56
150
                alloc_pair_chain(&mut caller, result_cells).await
57
175
            })
58
175
        },
59
    )?;
60
6044
    Ok(())
61
6044
}
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.
66
175
fn collect_items(
67
175
    caller: &mut Caller<'_, SessionData>,
68
175
    head: Option<Rooted<StructRef>>,
69
175
) -> wasmtime::Result<Vec<Rooted<AnyRef>>> {
70
175
    let mut out: Vec<Rooted<AnyRef>> = Vec::new();
71
175
    let mut cursor = head;
72
625
    while let Some(node) = cursor {
73
450
        let car_val = node.field(caller.as_context_mut(), 0)?;
74
450
        let car = match car_val {
75
450
            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
450
        out.push(car);
88
450
        let cdr_val = node.field(caller.as_context_mut(), 1)?;
89
450
        cursor = match cdr_val {
90
300
            Val::AnyRef(Some(any)) => Some(any.unwrap_struct(caller.as_context_mut())?),
91
150
            _ => None,
92
        };
93
    }
94
175
    Ok(out)
95
175
}
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.
101
400
async fn invoke_one(
102
400
    caller: &mut Caller<'_, SessionData>,
103
400
    cb: &Func,
104
400
    env: Option<Rooted<AnyRef>>,
105
400
    item: Rooted<AnyRef>,
106
400
) -> wasmtime::Result<Rooted<AnyRef>> {
107
400
    let mut results = [Val::AnyRef(None)];
108
400
    let outcome = cb
109
400
        .call_async(
110
400
            caller.as_context_mut(),
111
400
            &[Val::AnyRef(env), Val::AnyRef(Some(item))],
112
400
            &mut results,
113
400
        )
114
400
        .await;
115
400
    match outcome {
116
        Ok(()) => {
117
250
            let value_any = match results[0] {
118
250
                Val::AnyRef(any) => any,
119
                _ => {
120
                    return Err(wasmtime::Error::msg(
121
                        "catch-each: closure returned non-anyref Val variant",
122
                    ));
123
                }
124
            };
125
250
            build_ok_cell(caller, value_any).await
126
        }
127
150
        Err(err) => match classify_runtime_error(&err) {
128
25
            EngineError::OutOfFuel | EngineError::EpochInterrupt => Err(err),
129
125
            classified => build_err_cell(caller, &classified).await,
130
        },
131
    }
132
400
}
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.
138
250
async fn build_ok_cell(
139
250
    caller: &mut Caller<'_, SessionData>,
140
250
    value: Option<Rooted<AnyRef>>,
141
250
) -> wasmtime::Result<Rooted<AnyRef>> {
142
250
    let tag = string_anyref(caller, b"ok")?;
143
250
    let mut elems = vec![tag];
144
250
    if let Some(v) = value {
145
250
        elems.push(v);
146
250
    }
147
250
    pair_chain_to_anyref(caller, elems).await
148
250
}
149

            
150
/// Builds the list `(err code msg)` and returns its head as `anyref`.
151
125
async fn build_err_cell(
152
125
    caller: &mut Caller<'_, SessionData>,
153
125
    classified: &EngineError,
154
125
) -> wasmtime::Result<Rooted<AnyRef>> {
155
125
    let tag = string_anyref(caller, b"err")?;
156
125
    let (code, message) = err_code_and_message(classified);
157
125
    let code_any = string_anyref(caller, code.as_bytes())?;
158
125
    let message_any = string_anyref(caller, message.as_bytes())?;
159
125
    pair_chain_to_anyref(caller, vec![tag, code_any, message_any]).await
160
125
}
161

            
162
375
async fn pair_chain_to_anyref(
163
375
    caller: &mut Caller<'_, SessionData>,
164
375
    elems: Vec<Rooted<AnyRef>>,
165
375
) -> wasmtime::Result<Rooted<AnyRef>> {
166
375
    let head = alloc_pair_chain(caller, elems)
167
375
        .await?
168
375
        .ok_or_else(|| wasmtime::Error::msg("catch-each: built an empty result cell"))?;
169
375
    Ok(head.to_anyref())
170
375
}
171

            
172
625
fn string_anyref(
173
625
    caller: &mut Caller<'_, SessionData>,
174
625
    bytes: &[u8],
175
625
) -> wasmtime::Result<Rooted<AnyRef>> {
176
625
    Ok(alloc_string_ref(caller, bytes)?.to_anyref())
177
625
}