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::checked_ref::CheckedAnyRef;
28
use scripting::runtime::{
29
    EngineError, alloc_pair_chain, alloc_string_ref, classify_runtime_error, err_code_and_message,
30
};
31
use wasmtime::{AnyRef, AsContextMut, Caller, Func, Linker, Rooted, StructRef, Val};
32

            
33
use crate::session::SessionData;
34

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

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

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

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