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

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

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

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