1
//! Tier 2 architectural spike: confirm wasmtime supports re-entrant
2
//! funcref calls from a host fn with per-call `Err` recovery, and that
3
//! engine-managed traps (OutOfFuel) propagate without being catchable.
4
//!
5
//! This test underwrites the `__nomi_catch_each` host native design in
6
//! the master plan: walking items in Rust, calling a script-supplied
7
//! closure per item, recovering script-raised errors per call.
8
//!
9
//! What's verified:
10
//! - A host fn can receive a `Func` via wasm and call it back via
11
//!   `Func::call`, with multiple invocations per host-fn call.
12
//! - When the called fn returns `Err`, control returns cleanly to the
13
//!   host caller; it can keep iterating and call the same fn again.
14
//! - `OutOfFuel` traps cannot be silently swallowed: once the fuel
15
//!   budget is exhausted inside the called fn, the trap propagates
16
//!   through the host fn and reaches the outer caller.
17

            
18
use scripting::runtime::{EngineError, EngineOpts, build_engine, classify_runtime_error};
19
use wasmtime::{Caller, Func, Linker, Module, Store, Val, ValType};
20

            
21
/// Smallest wasm module exercising the re-entrancy pattern. The
22
/// exported `walk` fn calls into the host's `host_walk`, passing along
23
/// a callback funcref + arg-count. `host_walk` calls the callback that
24
/// many times with arg = 0..count and packs the per-call outcomes into
25
/// a single i32: low byte = ok-count, second byte = err-count.
26
const SPIKE_WAT: &str = r#"
27
(module
28
  (import "spike" "host_walk"
29
    (func $host_walk (param funcref) (param i32) (result i32)))
30

            
31
  (func (export "walk") (param $cb funcref) (param $n i32) (result i32)
32
    local.get $cb
33
    local.get $n
34
    call $host_walk)
35
)
36
"#;
37

            
38
#[test]
39
1
fn host_can_re_enter_funcref_and_recover_errors() {
40
1
    let engine = build_engine(EngineOpts::baseline()).expect("build engine");
41
1
    let module = Module::new(&engine, SPIKE_WAT).expect("compile spike module");
42

            
43
1
    let mut store: Store<()> = Store::new(&engine, ());
44
1
    store.set_epoch_deadline(1_000_000);
45
1
    let mut linker: Linker<()> = Linker::new(&engine);
46

            
47
1
    linker
48
1
        .func_wrap(
49
1
            "spike",
50
1
            "host_walk",
51
1
            move |mut caller: Caller<'_, ()>, cb: Option<Func>, n: i32| -> wasmtime::Result<i32> {
52
1
                let cb = cb.ok_or_else(|| wasmtime::Error::msg("null callback funcref"))?;
53
1
                let mut ok_count: i32 = 0;
54
1
                let mut err_count: i32 = 0;
55
5
                for i in 0..n {
56
5
                    let mut results = [Val::I32(0)];
57
5
                    match cb.call(&mut caller, &[Val::I32(i)], &mut results) {
58
4
                        Ok(()) => ok_count += 1,
59
1
                        Err(e) => {
60
1
                            let classified = classify_runtime_error(&e);
61
1
                            if matches!(
62
1
                                classified,
63
                                EngineError::OutOfFuel | EngineError::EpochInterrupt
64
                            ) {
65
                                return Err(e);
66
1
                            }
67
1
                            err_count += 1;
68
                        }
69
                    }
70
                }
71
1
                Ok((err_count << 8) | ok_count)
72
1
            },
73
        )
74
1
        .expect("register host_walk");
75

            
76
1
    let instance = linker
77
1
        .instantiate(&mut store, &module)
78
1
        .expect("instantiate");
79

            
80
    // Add a callback that fails on i == 2 — simulates `(error 'oops)`.
81
1
    let callback = Func::new(
82
1
        &mut store,
83
1
        wasmtime::FuncType::new(
84
1
            &engine,
85
1
            [ValType::I32].iter().cloned(),
86
1
            [ValType::I32].iter().cloned(),
87
        ),
88
5
        |_caller, args, results| {
89
5
            let i = args[0].i32().expect("i32 arg");
90
5
            if i == 2 {
91
1
                return Err(wasmtime::Error::msg("__nomi_raise:oops:i was two"));
92
4
            }
93
4
            results[0] = Val::I32(i * 10);
94
4
            Ok(())
95
5
        },
96
    );
97

            
98
1
    let walk = instance.get_func(&mut store, "walk").expect("walk export");
99
1
    let mut results = [Val::I32(0)];
100
1
    walk.call(
101
1
        &mut store,
102
1
        &[Val::FuncRef(Some(callback)), Val::I32(5)],
103
1
        &mut results,
104
    )
105
1
    .expect("walk call");
106
1
    let packed = results[0].i32().expect("i32 result");
107
1
    let ok = packed & 0xff;
108
1
    let err = (packed >> 8) & 0xff;
109
1
    assert_eq!(ok, 4, "expected 4 ok calls, got {ok}");
110
1
    assert_eq!(err, 1, "expected 1 err call, got {err}");
111
1
}
112

            
113
#[test]
114
1
fn out_of_fuel_during_callback_propagates_through_host_walk() {
115
1
    let engine = build_engine(EngineOpts::baseline().with_fuel()).expect("build engine");
116
1
    let module = Module::new(&engine, SPIKE_WAT).expect("compile spike module");
117

            
118
1
    let mut store: Store<()> = Store::new(&engine, ());
119
1
    store.set_epoch_deadline(1_000_000);
120
1
    store.set_fuel(20).expect("set fuel");
121

            
122
1
    let mut linker: Linker<()> = Linker::new(&engine);
123
1
    linker
124
1
        .func_wrap(
125
1
            "spike",
126
1
            "host_walk",
127
1
            move |mut caller: Caller<'_, ()>, cb: Option<Func>, n: i32| -> wasmtime::Result<i32> {
128
1
                let cb = cb.ok_or_else(|| wasmtime::Error::msg("null callback funcref"))?;
129
1
                let mut ok_count: i32 = 0;
130
1
                for i in 0..n {
131
1
                    let mut results = [Val::I32(0)];
132
1
                    match cb.call(&mut caller, &[Val::I32(i)], &mut results) {
133
                        Ok(()) => ok_count += 1,
134
1
                        Err(e) => {
135
1
                            let classified = classify_runtime_error(&e);
136
                            if matches!(
137
1
                                classified,
138
                                EngineError::OutOfFuel | EngineError::EpochInterrupt
139
                            ) {
140
1
                                return Err(e);
141
                            }
142
                        }
143
                    }
144
                }
145
                Ok(ok_count)
146
1
            },
147
        )
148
1
        .expect("register host_walk");
149

            
150
1
    let instance = linker
151
1
        .instantiate(&mut store, &module)
152
1
        .expect("instantiate");
153

            
154
    // Callback contains a busy loop that drains fuel; OutOfFuel must
155
    // bubble through host_walk to the outer call site.
156
1
    let busy_wat = r#"
157
1
        (module
158
1
          (func (export "busy") (param i32) (result i32)
159
1
            (local $i i32)
160
1
            i32.const 0
161
1
            local.set $i
162
1
            (loop
163
1
              local.get $i
164
1
              i32.const 1
165
1
              i32.add
166
1
              local.set $i
167
1
              local.get $i
168
1
              i32.const 1000000
169
1
              i32.lt_s
170
1
              br_if 0)
171
1
            local.get $i))
172
1
    "#;
173
1
    let busy_module = Module::new(&engine, busy_wat).expect("compile busy module");
174
1
    let busy_instance = Linker::<()>::new(&engine)
175
1
        .instantiate(&mut store, &busy_module)
176
1
        .expect("instantiate busy");
177
1
    let callback = busy_instance
178
1
        .get_func(&mut store, "busy")
179
1
        .expect("busy export");
180

            
181
1
    let walk = instance.get_func(&mut store, "walk").expect("walk export");
182
1
    let mut results = [Val::I32(0)];
183
1
    let outcome = walk.call(
184
1
        &mut store,
185
1
        &[Val::FuncRef(Some(callback)), Val::I32(10)],
186
1
        &mut results,
187
    );
188
1
    let err = outcome.expect_err("walk must fail with OutOfFuel");
189
1
    assert!(
190
1
        matches!(classify_runtime_error(&err), EngineError::OutOfFuel),
191
        "expected OutOfFuel, got {:?}",
192
        classify_runtime_error(&err)
193
    );
194
1
}