use wasmtime::*;
mod test_utils {
use std::any::*;
use std::panic::AssertUnwindSafe;
use wasmtime::{bail, *};
pub struct Runner {
pub engine: Engine,
pub store: Store<()>,
}
impl Runner {
pub fn new() -> Runner {
let mut config = Config::default();
config.wasm_function_references(true);
config.wasm_exceptions(true);
config.wasm_stack_switching(true);
config.wasm_gc(true);
let engine = Engine::new(&config).unwrap();
let store = Store::<()>::new(&engine, ());
Runner { engine, store }
}
pub fn run_test<Results: WasmResults>(
mut self,
wat: &str,
imports: &[Extern],
) -> Result<Results> {
let module = Module::new(&self.engine, wat)?;
let instance = Instance::new(&mut self.store, &module, imports)?;
let entry = instance.get_typed_func::<(), Results>(&mut self.store, "entry")?;
entry.call(&mut self.store, ())
}
pub fn run_test_expect_panic(
mut self,
wat: &str,
imports: &[Extern],
) -> Box<dyn Any + Send + 'static> {
let module = Module::new(&self.engine, wat).unwrap();
let instance = Instance::new(&mut self.store, &module, imports).unwrap();
let entry = instance.get_func(&mut self.store, "entry").unwrap();
std::panic::catch_unwind(AssertUnwindSafe(|| {
drop(entry.call(&mut self.store, &[], &mut []))
}))
.unwrap_err()
}
}
pub fn make_i32_inc_host_func(runner: &mut Runner) -> Func {
Func::new(
&mut runner.store,
FuncType::new(&runner.engine, vec![ValType::I32], vec![ValType::I32]),
|mut _caller, args: &[Val], results: &mut [Val]| {
let res = match args {
[Val::I32(i)] => i + 1,
_ => bail!("Error: Received illegal argument (should be single i32)"),
};
results[0] = Val::I32(res);
Ok(())
},
)
}
pub fn make_i32_inc_via_export_host_func(
runner: &mut Runner,
export_func: &'static str,
) -> Func {
Func::new(
&mut runner.store,
FuncType::new(&runner.engine, vec![ValType::I32], vec![ValType::I32]),
|mut caller, args: &[Val], results: &mut [Val]| {
let export = caller
.get_export(export_func)
.ok_or_else(|| wasmtime::format_err!("could not get export"))?;
let func = export
.into_func()
.ok_or_else(|| wasmtime::format_err!("export is not a Func"))?;
let func_typed = func.typed::<i32, i32>(caller.as_context())?;
let arg = args[0].unwrap_i32();
let res = func_typed.call(caller.as_context_mut(), arg + 1)?;
results[0] = Val::I32(res + 1);
Ok(())
},
)
}
pub fn make_panicking_host_func(store: &mut Store<()>, msg: &'static str) -> Func {
Func::wrap(store, move || -> () { std::panic::panic_any(msg) })
}
}
mod wasi {
use wasmtime::{Config, Engine, Linker, Module, Result, Store};
use wasmtime_wasi::WasiCtxBuilder;
use wasmtime_wasi::p1::{self, WasiP1Ctx};
fn run_wasi_test(wat: &'static str) -> Result<i32> {
let mut config = Config::new();
config
.wasm_exceptions(true)
.wasm_function_references(true)
.wasm_stack_switching(true);
let engine = Engine::new(&config)?;
let mut linker: Linker<WasiP1Ctx> = Linker::new(&engine);
p1::add_to_linker_sync(&mut linker, |t| t)?;
let wasi_ctx = WasiCtxBuilder::new().inherit_stdio().build_p1();
let mut store: Store<WasiP1Ctx> = Store::new(&engine, wasi_ctx);
let module = Module::new(&engine, wat)?;
let func = linker
.module(&mut store, "", &module)?
.get_default(&mut store, "")?
.typed::<(), i32>(&store)?;
func.call(&mut store, ())
}
async fn run_wasi_test_async(wat: &'static str) -> Result<i32> {
let mut config = Config::new();
config
.wasm_exceptions(true)
.wasm_function_references(true)
.wasm_stack_switching(true);
let engine = Engine::new(&config)?;
let mut linker: Linker<WasiP1Ctx> = Linker::new(&engine);
p1::add_to_linker_async(&mut linker, |t| t)?;
let wasi_ctx = WasiCtxBuilder::new().inherit_stdio().build_p1();
let mut store: Store<WasiP1Ctx> = Store::new(&engine, wasi_ctx);
let module = Module::new(&engine, wat)?;
let func = linker
.module_async(&mut store, "", &module)
.await?
.get_default(&mut store, "")?
.typed::<(), i32>(&store)?;
func.call_async(&mut store, ()).await
}
static WRITE_SOMETHING_WAT: &str = r#"
(module
(type $ft (func (result i32)))
(type $ct (cont $ft))
(import "wasi_snapshot_preview1" "fd_write"
(func $print (param $fd i32)
(param $iovec i32)
(param $len i32)
(param $written i32) (result i32)))
(memory 1)
(export "memory" (memory 0))
;; 9 is the offset to write to
(data (i32.const 9) "something\n")
(func $f (result i32)
(i32.const 0) ;; offset
(i32.const 9) ;; value start of the string
(i32.store)
(i32.const 4) ;; offset
(i32.const 10) ;; value, the length of the string
(i32.store offset=0 align=2) ;; size_buf_len
(i32.const 1) ;; 1 for stdout
(i32.const 0) ;; 0 as we stored the beginning of __wasi_ciovec_t
(i32.const 1) ;;
(i32.const 20) ;; nwritten
(call $print)
)
(elem declare func $f)
(func (export "_start") (result i32)
(ref.func $f)
(cont.new $ct)
(resume $ct)
)
)"#;
#[cfg_attr(any(asan, miri), ignore)]
#[test]
fn write_something_test() -> Result<()> {
assert_eq!(run_wasi_test(WRITE_SOMETHING_WAT)?, 0);
Ok(())
}
#[cfg_attr(any(asan, miri), ignore)]
#[tokio::test]
async fn write_something_test_async() -> Result<()> {
assert_eq!(run_wasi_test_async(WRITE_SOMETHING_WAT).await?, 0);
Ok(())
}
static SCHED_YIELD_WAT: &'static str = r#"
(module
(type $ft (func (result i32)))
(type $ct (cont $ft))
(import "wasi_snapshot_preview1" "sched_yield"
(func $sched_yield (result i32)))
(memory 1)
(export "memory" (memory 0))
(func $g (result i32)
(call $sched_yield))
(elem declare func $g)
(func (export "_start") (result i32)
(cont.new $ct (ref.func $g))
(resume $ct)
)
)"#;
#[cfg_attr(any(asan, miri), ignore)]
#[test]
fn sched_yield_test() -> Result<()> {
assert_eq!(run_wasi_test(SCHED_YIELD_WAT)?, 0);
Ok(())
}
#[cfg_attr(any(asan, miri), ignore)]
#[tokio::test]
async fn sched_yield_test_async() -> Result<()> {
assert_eq!(run_wasi_test_async(SCHED_YIELD_WAT).await?, 0);
Ok(())
}
}
#[cfg_attr(any(asan, miri), ignore)]
#[test]
fn inter_instance_suspend() -> Result<()> {
let mut config = Config::default();
config.wasm_function_references(true);
config.wasm_exceptions(true);
config.wasm_stack_switching(true);
let engine = Engine::new(&config)?;
let mut store = Store::<()>::new(&engine, ());
let wat_other = r#"
(module
(type $ft (func))
(type $ct (cont $ft))
(tag $tag)
(func $suspend (export "suspend")
(suspend $tag)
)
(func $resume (export "resume") (param $f (ref $ct))
(block $handler (result (ref $ct))
(resume $ct (on $tag $handler) (local.get $f))
(unreachable)
)
(drop)
)
)
"#;
let wat_main = r#"
(module
(type $ft (func))
(type $ct (cont $ft))
(import "other" "suspend" (func $suspend))
(import "other" "resume" (func $resume (param (ref $ct))))
(elem declare func $suspend)
(func $entry (export "entry")
(call $resume (cont.new $ct (ref.func $suspend)))
)
)
"#;
let module_other = Module::new(&engine, wat_other)?;
let other_inst1 = Instance::new(&mut store, &module_other, &[])?;
let other_inst2 = Instance::new(&mut store, &module_other, &[])?;
let suspend = other_inst1.get_func(&mut store, "suspend").unwrap();
let resume = other_inst2.get_func(&mut store, "resume").unwrap();
let module_main = Module::new(&engine, wat_main)?;
let main_instance = Instance::new(&mut store, &module_main, &[suspend.into(), resume.into()])?;
let entry_func = main_instance.get_func(&mut store, "entry").unwrap();
let result = entry_func.call(&mut store, &[], &mut []);
assert!(result.is_err());
Ok(())
}
mod host {
use super::test_utils::*;
use wasmtime::*;
const CONTINUATION_TRAP_MODULE: &str = r#"
(module
(import "host" "reenter" (func $host-reenter))
(import "host" "trap" (func $host-trap))
(type $ft (func))
(type $ct (cont $ft))
(global $stacks-left (mut i32) (i32.const 0))
(global $frames-left (mut i32) (i32.const 0))
(global $frames-per-stack (mut i32) (i32.const 0))
(global $trap-in-host (mut i32) (i32.const 0))
(global $g (export "g") (mut i32) (i32.const 0))
(func $increment
(global.set $g
(i32.add (global.get $g) (i32.const 1))))
(func $continue (export "continue")
(if (i32.gt_u (global.get $frames-left) (i32.const 0))
(then
(global.set $frames-left
(i32.sub (global.get $frames-left) (i32.const 1)))
(call $host-reenter))
(else
(if (i32.gt_u (global.get $stacks-left) (i32.const 0))
(then
;; Install a new child stack.
(global.set $stacks-left
(i32.sub (global.get $stacks-left) (i32.const 1)))
(global.set $frames-left (global.get $frames-per-stack))
(resume $ct (cont.new $ct (ref.func $continue))))
(else
;; Call either the host provided trap, or the native Wasm trapping instruction `unreachable`.
(if (global.get $trap-in-host)
(then (call $host-trap))
(else unreachable))))))
;; No frame, on any stack, may resume after the terminal trap.
(call $increment))
(func (export "run")
(param $stacks i32)
(param $frames-per-stack-arg i32)
(param $trap-in-host-arg i32)
(global.set $stacks-left (local.get $stacks))
(global.set $frames-left (local.get $frames-per-stack-arg))
(global.set $frames-per-stack (local.get $frames-per-stack-arg))
(global.set $trap-in-host (local.get $trap-in-host-arg))
(call $continue))
(elem declare func $continue)
)
"#;
fn increment_global<T>(caller: &mut Caller<'_, T>) -> Result<()> {
let g = caller.get_export("g").unwrap().into_global().unwrap();
let value = g.get(&mut *caller).unwrap_i32();
g.set(caller, Val::I32(value + 1))
}
fn assert_expected_trap(error: &Error, trap_in_host: bool, stacks: i32, frames_per_stack: i32) {
if trap_in_host {
assert!(
format!("{error:#}").contains("intentional host trap"),
"unexpected error for stacks={stacks}, frames_per_stack={frames_per_stack}: {error:#}"
);
} else {
assert_eq!(
error.downcast_ref::<Trap>(),
Some(&Trap::UnreachableCodeReached),
"unexpected error for stacks={stacks}, frames_per_stack={frames_per_stack}: {error:#}"
);
}
}
fn host_trap() -> Result<()> {
bail!("intentional host trap")
}
#[test]
#[cfg_attr(any(asan, miri), ignore)]
fn traps_cross_continuation_stacks_and_host_frames() -> Result<()> {
let mut config = Config::new();
config.wasm_stack_switching(true);
let engine = Engine::new(&config)?;
let module = Module::new(&engine, CONTINUATION_TRAP_MODULE)?;
for stacks in 0..=10 {
for frames_per_stack in 0..=10 {
for trap_in_host in [false, true] {
let mut store = Store::new(&engine, ());
let reenter =
Func::wrap(&mut store, |mut caller: Caller<'_, ()>| -> Result<()> {
let continue_ = caller
.get_export("continue")
.unwrap()
.into_func()
.unwrap()
.typed::<(), ()>(&caller)?;
continue_.call(&mut caller, ())?;
increment_global(&mut caller)
});
let host_trap = Func::wrap(&mut store, host_trap);
let instance =
Instance::new(&mut store, &module, &[reenter.into(), host_trap.into()])?;
let run = instance.get_typed_func::<(i32, i32, i32), ()>(&mut store, "run")?;
let error = run
.call(
&mut store,
(stacks, frames_per_stack, i32::from(trap_in_host)),
)
.unwrap_err();
assert_expected_trap(&error, trap_in_host, stacks, frames_per_stack);
let g = instance.get_global(&mut store, "g").unwrap();
assert_eq!(g.get(&mut store).unwrap_i32(), 0);
}
}
}
Ok(())
}
#[derive(Default)]
struct CatchState {
error: Option<Error>,
}
#[test]
#[cfg_attr(any(asan, miri), ignore)]
fn parent_frames_resume_after_host_catches_trap() -> Result<()> {
let mut config = Config::new();
config.wasm_stack_switching(true);
let engine = Engine::new(&config)?;
let module = Module::new(&engine, CONTINUATION_TRAP_MODULE)?;
for stacks in 0..=10 {
for frames_per_stack in 1..=10 {
for trap_in_host in [false, true] {
let mut store = Store::new(&engine, CatchState::default());
let reenter = Func::wrap(
&mut store,
|mut caller: Caller<'_, CatchState>| -> Result<()> {
let continue_ = caller
.get_export("continue")
.unwrap()
.into_func()
.unwrap()
.typed::<(), ()>(&caller)?;
if let Err(error) = continue_.call(&mut caller, ()) {
let previous = caller.data_mut().error.replace(error);
assert!(
previous.is_none(),
"more than one host frame caught a trap"
);
}
increment_global(&mut caller)
},
);
let host_trap = Func::wrap(&mut store, host_trap);
let instance =
Instance::new(&mut store, &module, &[reenter.into(), host_trap.into()])?;
let run = instance.get_typed_func::<(i32, i32, i32), ()>(&mut store, "run")?;
run.call(
&mut store,
(stacks, frames_per_stack, i32::from(trap_in_host)),
)?;
let error = store
.data()
.error
.as_ref()
.expect("a host frame should have caught the terminal trap");
assert_expected_trap(error, trap_in_host, stacks, frames_per_stack);
let expected = (stacks + 1) * (2 * frames_per_stack + 1) - 1;
let g = instance.get_global(&mut store, "g").unwrap();
assert_eq!(
g.get(&mut store).unwrap_i32(),
expected,
"wrong count for stacks={stacks}, frames_per_stack={frames_per_stack}"
);
}
}
}
Ok(())
}
#[cfg_attr(any(asan, miri), ignore)]
#[test]
fn call_host_from_continuation() -> Result<()> {
let wat = r#"
(module
(type $ft (func (result i32)))
(type $ct (cont $ft))
(import "" "" (func $host_func_a (param i32) (result i32)))
(func $a (export "a") (result i32)
(call $host_func_a (i32.const 122))
)
(func $entry (export "entry") (result i32)
(resume $ct (cont.new $ct (ref.func $a)))
)
)
"#;
let mut runner = Runner::new();
let host_func_a = make_i32_inc_host_func(&mut runner);
let result = runner.run_test::<i32>(wat, &[host_func_a.into()]).unwrap();
assert_eq!(result, 123);
Ok(())
}
#[cfg_attr(any(asan, miri), ignore)]
#[test]
fn re_enter_wasm_ok1() -> Result<()> {
let wat = r#"
(module
(type $ft (func (param i32) (result i32)))
(type $ct (cont $ft))
(import "" "" (func $host_func_a (param i32) (result i32)))
(func $a (export "a") (param $x i32) (result i32)
(call $host_func_a (local.get $x))
)
(func $b (export "b") (param $x i32) (result i32)
(resume $ct (local.get $x) (cont.new $ct (ref.func $c)))
)
(func $c (export "c") (param $x i32) (result i32)
(return (i32.add (local.get $x) (i32.const 1)))
)
(func $entry (export "entry") (result i32)
(call $a (i32.const 120))
)
)
"#;
let mut runner = Runner::new();
let host_func_a = make_i32_inc_via_export_host_func(&mut runner, "b");
let result = runner.run_test::<i32>(wat, &[host_func_a.into()]).unwrap();
assert_eq!(result, 123);
Ok(())
}
#[cfg_attr(any(asan, miri), ignore)]
#[test]
fn re_enter_wasm_ok2() -> Result<()> {
let wat = r#"
(module
(type $ft (func (param i32) (result i32)))
(type $ct (cont $ft))
(import "" "" (func $host_func_a (param i32) (result i32)))
(func $a (export "a") (param $x i32) (result i32)
;; Running continuation before calling into host is fine
(resume $ct (local.get $x) (cont.new $ct (ref.func $c)))
(drop)
(call $host_func_a (local.get $x))
)
(func $b (export "b") (param $x i32) (result i32)
(resume $ct (local.get $x) (cont.new $ct (ref.func $c)))
)
(func $c (export "c") (param $x i32) (result i32)
(return (i32.add (local.get $x) (i32.const 1)))
)
(func $entry (export "entry") (result i32)
(call $a (i32.const 120))
)
)
"#;
let mut runner = Runner::new();
let host_func_a = make_i32_inc_via_export_host_func(&mut runner, "b");
let result = runner.run_test::<i32>(wat, &[host_func_a.into()]).unwrap();
assert_eq!(result, 123);
Ok(())
}
#[cfg_attr(any(asan, miri), ignore)]
#[test]
fn re_enter_wasm_from_continuation() -> Result<()> {
let wat = r#"
(module
(type $ft (func (param i32) (result i32)))
(type $ct (cont $ft))
(import "" "" (func $host_func_a (param i32) (result i32)))
(func $a (export "a") (param $x i32) (result i32)
(call $host_func_a (local.get $x))
)
(func $b (export "b") (param $x i32) (result i32)
(return (i32.add (local.get $x) (i32.const 1)))
)
(func $entry (export "entry") (result i32)
(resume $ct (i32.const 120) (cont.new $ct (ref.func $a)))
)
)
"#;
let mut runner = Runner::new();
let host_func_a = make_i32_inc_via_export_host_func(&mut runner, "b");
let result = runner.run_test::<i32>(&wat, &[host_func_a.into()])?;
assert_eq!(result, 123);
Ok(())
}
#[cfg_attr(any(asan, miri), ignore)]
#[test]
fn call_host_from_continuation_nested_suspend_ok() -> Result<()> {
let wat = r#"
(module
(type $ft (func (param i32) (result i32)))
(type $ct (cont $ft))
(tag $t (result i32))
(import "" "" (func $host_func_a (param i32) (result i32)))
(func $a (export "a") (param $x i32) (result i32)
(call $host_func_a (local.get $x))
)
(func $b (export "b") (param $x i32) (result i32)
(block $h (result (ref $ct))
(resume $ct (on $t $h) (local.get $x) (cont.new $ct (ref.func $c)))
(unreachable)
)
(drop)
;; note that we do not run the continuation to completion
(i32.add (local.get $x) (i32.const 1))
)
(func $c (export "c") (param $x i32) (result i32)
(suspend $t)
)
(func $entry (export "entry") (result i32)
(resume $ct (i32.const 120) (cont.new $ct (ref.func $a)))
)
)
"#;
let mut runner = Runner::new();
let host_func_a = make_i32_inc_via_export_host_func(&mut runner, "b");
let result = runner.run_test::<i32>(&wat, &[host_func_a.into()])?;
assert_eq!(result, 123);
Ok(())
}
#[cfg_attr(any(asan, miri), ignore)]
#[test]
fn call_host_from_continuation_nested_suspend_unhandled() -> Result<()> {
let wat = r#"
(module
(type $ft (func (param i32) (result i32)))
(type $ct (cont $ft))
(tag $t (result i32))
(import "" "" (func $host_func_a (param i32) (result i32)))
(func $a (export "a") (param $x i32) (result i32)
(call $host_func_a (local.get $x))
)
(func $b (export "b") (param $x i32) (result i32)
(resume $ct (local.get $x) (cont.new $ct (ref.func $c)))
)
(func $c (export "c") (param $x i32) (result i32)
(suspend $t)
)
(func $entry (export "entry")
(block $h (result (ref $ct))
(return
(resume $ct
(on $t $h)
(i32.const 123)
(cont.new $ct (ref.func $a))))
)
(drop)
)
)
"#;
let mut runner = Runner::new();
let host_func_a = make_i32_inc_via_export_host_func(&mut runner, "b");
let error = runner
.run_test::<()>(&wat, &[host_func_a.into()])
.unwrap_err();
assert_eq!(
error.downcast_ref::<Trap>(),
Some(&Trap::UnhandledTag),
"unexpected error: {error:#}"
);
Ok(())
}
}
mod traps {
use super::test_utils::*;
use wasmtime::*;
fn run_test_expect_trap_backtrace(wat: &str, expected_trap: Trap, expected_backtrace: &[&str]) {
let runner = Runner::new();
let result = runner.run_test::<()>(wat, &[]);
let err = result.expect_err("Was expecting wasm execution to yield error");
assert!(err.root_cause().is::<Trap>());
assert_eq!(*err.downcast_ref::<Trap>().unwrap(), expected_trap);
let trace = err.downcast_ref::<WasmBacktrace>().unwrap();
let actual_func_name_it = trace
.frames()
.iter()
.map(|frame| {
frame
.func_name()
.expect("Expecting all functions in actual backtrace to have names")
})
.rev();
let expected_func_name_it = expected_backtrace.iter().copied();
assert!(actual_func_name_it.eq(expected_func_name_it));
}
#[cfg_attr(any(asan, miri), ignore)]
#[test]
fn trap_in_continuation_nested() -> Result<()> {
let wat = r#"
(module
(type $ft (func))
(type $ct (cont $ft))
(func $entry (export "entry")
(call $a)
)
(func $a (export "a")
(resume $ct (cont.new $ct (ref.func $b)))
)
(func $b (export "b")
(call $c)
)
(func $c (export "c")
(resume $ct (cont.new $ct (ref.func $d)))
)
(func $d (export "d")
(call $e)
)
(func $e (export "e")
(resume $ct (cont.new $ct (ref.func $f)))
)
(func $f (export "f")
(unreachable)
)
)
"#;
run_test_expect_trap_backtrace(
wat,
Trap::UnreachableCodeReached,
&["entry", "a", "b", "c", "d", "e", "f"],
);
Ok(())
}
#[cfg_attr(any(asan, miri), ignore)]
#[test]
fn trap_in_continuation_back_to_parent() -> Result<()> {
let wat = r#"
(module
(type $ft (func))
(type $ct (cont $ft))
(func $entry (export "entry")
(call $a)
)
(func $a (export "a")
(resume $ct (cont.new $ct (ref.func $b)))
)
(func $b (export "b")
(call $c)
)
(func $c (export "c")
(resume $ct (cont.new $ct (ref.func $d)))
(unreachable)
)
(func $d (export "d")
(call $e)
)
(func $e (export "e"))
)
"#;
run_test_expect_trap_backtrace(
wat,
Trap::UnreachableCodeReached,
&["entry", "a", "b", "c"],
);
Ok(())
}
#[cfg_attr(any(asan, miri), ignore)]
#[test]
fn trap_in_continuation_back_to_main() -> Result<()> {
let wat = r#"
(module
(type $ft (func))
(type $ct (cont $ft))
(func $entry (export "entry")
(call $a)
)
(func $a (export "a")
(resume $ct (cont.new $ct (ref.func $b)))
(unreachable)
)
(func $b (export "b")
(call $c)
)
(func $c (export "c")
(resume $ct (cont.new $ct (ref.func $d)))
)
(func $d (export "d")
(call $e)
)
(func $e (export "e"))
)
"#;
run_test_expect_trap_backtrace(wat, Trap::UnreachableCodeReached, &["entry", "a"]);
Ok(())
}
#[cfg_attr(any(asan, miri), ignore)]
#[test]
fn trap_in_continuation_suspend() -> Result<()> {
let wat = r#"
(module
(type $ft (func))
(type $ct (cont $ft))
(tag $t)
(func $entry (export "entry")
(call $a)
)
(func $a (export "a")
(resume $ct (cont.new $ct (ref.func $b)))
(unreachable)
)
(func $b (export "b")
(call $c)
)
(func $c (export "c")
(block $handler (result (ref $ct))
(resume $ct (on $t $handler) (cont.new $ct (ref.func $d)))
(return)
)
(unreachable)
)
(func $d (export "d")
(call $e)
)
(func $e (export "e")
(suspend $t)
)
)
"#;
run_test_expect_trap_backtrace(
wat,
Trap::UnreachableCodeReached,
&["entry", "a", "b", "c"],
);
Ok(())
}
#[cfg_attr(any(asan, miri), ignore)]
#[test]
fn trap_in_continuation_suspend_resume() -> Result<()> {
let wat = r#"
(module
(type $ft (func))
(type $ct (cont $ft))
(tag $t)
(func $entry (export "entry")
(call $a)
)
(func $a (export "a")
(resume $ct (cont.new $ct (ref.func $b)))
)
(func $b (export "b")
(resume $ct (call $c))
)
(func $c (export "c") (result (ref $ct))
(block $handler (result (ref $ct))
(resume $ct (on $t $handler) (cont.new $ct (ref.func $d)))
;; We never want to get here, but also don't want to use
;; (unreachable), which is the trap we deliberately use in
;; this test. Instead, we call a null function ref here,
;; which is guaranteed to trap.
(call_ref $ft (ref.null $ft))
(return (cont.new $ct (ref.func $d)))
)
;; implicitly returning the continuation here
)
(func $d (export "d")
(call $e)
(unreachable)
)
(func $e (export "e")
(suspend $t)
)
)
"#;
run_test_expect_trap_backtrace(
wat,
Trap::UnreachableCodeReached,
&["entry", "a", "b", "d"],
);
Ok(())
}
#[cfg_attr(any(asan, miri), ignore)]
#[test]
fn trap_in_continuation_forward() -> Result<()> {
let wat = r#"
(module
(type $ft (func))
(type $ct (cont $ft))
(tag $t)
(func $entry (export "entry")
(call $a)
)
(func $a (export "a")
(block $handler (result (ref $ct))
(resume $ct (on $t $handler) (cont.new $ct (ref.func $b)))
;; We don't actually want to get here
(return)
)
(unreachable)
)
(func $b (export "b")
(call $c)
)
(func $c (export "c")
(resume $ct (cont.new $ct (ref.func $d)))
)
(func $d (export "d")
(call $e)
)
(func $e (export "e")
(suspend $t)
)
)
"#;
run_test_expect_trap_backtrace(wat, Trap::UnreachableCodeReached, &["entry", "a"]);
Ok(())
}
#[cfg_attr(any(asan, miri), ignore)]
#[test]
fn trap_in_continuation_forward_resume() -> Result<()> {
let wat = r#"
(module
(type $ft (func))
(type $ct (cont $ft))
(tag $t)
(global $k (mut (ref null $ct)) (ref.null $ct))
(func $entry (export "entry")
(call $a)
)
(func $a (export "a")
(resume $ct (cont.new $ct (ref.func $b)))
)
(func $b (export "b")
(block $handler (result (ref $ct))
(resume $ct (on $t $handler) (cont.new $ct (ref.func $c)))
;; We don't actually want to get here
(return)
)
(global.set $k)
;; $f will resume $k
(resume $ct (cont.new $ct (ref.func $f)))
)
(func $c (export "c")
(resume $ct (cont.new $ct (ref.func $d)))
)
(func $d (export "d")
(call $e)
)
(func $e (export "e")
(suspend $t)
(unreachable)
)
(func $f (export "f")
(resume $ct (global.get $k))
)
)
"#;
run_test_expect_trap_backtrace(
wat,
Trap::UnreachableCodeReached,
&["entry", "a", "b", "f", "c", "d", "e"],
);
Ok(())
}
#[cfg_attr(any(asan, miri), ignore)]
#[test]
fn trap_switch_and_resume() -> Result<()> {
let wat = r#"
(module
(rec
(type $ft0 (func (param (ref null $ct0))))
(type $ct0 (cont $ft0)))
(type $ft1 (func))
(type $ct1 (cont $ft1))
(tag $t)
(func $a (type $ft1)
(cont.new $ct1 (ref.func $b))
(resume $ct1)
)
(elem declare func $a)
(func $b (type $ft1)
(cont.new $ct0 (ref.func $c))
(switch $ct0 $t)
;; we want a backtrace here
(unreachable)
)
(elem declare func $b)
(func $c (type $ft0)
(local.get 0)
(cont.new $ct0 (ref.func $d))
(resume $ct0)
)
(elem declare func $c)
(func $d (type $ft0)
(block $handler (result (ref $ct1))
(ref.null $ct0) ;; passed as payload
(local.get 0) ;; resumed
(resume $ct0 (on $t $handler))
(unreachable) ;; f1 will suspend after the switch
)
(resume $ct1)
)
(elem declare func $d)
(func $entry (export "entry")
(cont.new $ct1 (ref.func $a))
(resume $ct1 (on $t switch))
)
)
"#;
run_test_expect_trap_backtrace(
wat,
Trap::UnreachableCodeReached,
&["entry", "c", "d", "a", "b"],
);
Ok(())
}
#[cfg_attr(any(asan, miri), ignore)]
#[test]
fn panic_in_continuation() -> Result<()> {
let wat = r#"
(module
(type $ft (func))
(type $ct (cont $ft))
(import "" "" (func $f))
(func $entry (export "entry")
(call $a)
)
(func $a (export "a")
(resume $ct (cont.new $ct (ref.func $b)))
)
(func $b (export "b")
(call $c)
)
(func $c (export "c")
(resume $ct (cont.new $ct (ref.func $d)))
)
(func $d (export "d")
(call $e)
)
(func $e (export "e")
(call $f)
)
)
"#;
let mut runner = Runner::new();
let msg = "Host function f panics";
let f = make_panicking_host_func(&mut runner.store, msg);
let error = runner.run_test_expect_panic(wat, &[f.into()]);
assert_eq!(error.downcast_ref::<&'static str>(), Some(&msg));
Ok(())
}
#[cfg_attr(any(asan, miri), ignore)]
#[test]
fn stack_overflow_in_continuation() -> Result<()> {
let wat = r#"
(module
(type $ft (func (param i32)))
(type $ct (cont $ft))
(func $entry (export "entry")
(call $a)
)
(func $a (export "a")
;; We ask for a billion recursive calls
(i32.const 1_000_000_000)
(resume $ct (cont.new $ct (ref.func $overflow)))
)
(func $overflow (export "overflow") (param $i i32)
(block $continue
(local.get $i)
;; return if $i == 0
(br_if $continue)
(return)
)
(i32.sub (local.get $i) (i32.const 1))
(call $overflow)
)
)
"#;
let runner = Runner::new();
let error = runner
.run_test::<()>(wat, &[])
.expect_err("Expecting execution to yield error");
assert!(error.root_cause().is::<Trap>());
assert_eq!(*error.downcast_ref::<Trap>().unwrap(), Trap::StackOverflow);
Ok(())
}
}