From 748eea00c2012efff19fc5c8cb0a9f2b26775f54 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sat, 26 Sep 2026 10:21:23 -0700 Subject: [PATCH 1/2] test: run the exception-handling spec tests wasm-testsuite ships the exceptions proposal's tag, throw, throw_ref and try_table scripts under proposals/exceptions, and no test target ran them. All 105 assertions pass. --- crates/tinywasm/Cargo.toml | 4 ++++ crates/tinywasm/tests/generated/wasm-exceptions.csv | 1 + crates/tinywasm/tests/test-wasm-exceptions.rs | 11 +++++++++++ 3 files changed, 16 insertions(+) create mode 100644 crates/tinywasm/tests/generated/wasm-exceptions.csv create mode 100644 crates/tinywasm/tests/test-wasm-exceptions.rs diff --git a/crates/tinywasm/Cargo.toml b/crates/tinywasm/Cargo.toml index 7788770f..20972ce6 100644 --- a/crates/tinywasm/Cargo.toml +++ b/crates/tinywasm/Cargo.toml @@ -56,6 +56,10 @@ name = "test-wasm-custom-page-sizes" harness = false name = "test-wasm-custom" +[[test]] +harness = false +name = "test-wasm-exceptions" + [[test]] harness = false name = "test-wasm-gc" diff --git a/crates/tinywasm/tests/generated/wasm-exceptions.csv b/crates/tinywasm/tests/generated/wasm-exceptions.csv new file mode 100644 index 00000000..29e27e8e --- /dev/null +++ b/crates/tinywasm/tests/generated/wasm-exceptions.csv @@ -0,0 +1 @@ +0.11.0,105,0,[{"name":"tag.wast","passed":10,"failed":0},{"name":"throw.wast","passed":13,"failed":0},{"name":"throw_ref.wast","passed":15,"failed":0},{"name":"try_table.wast","passed":67,"failed":0}] diff --git a/crates/tinywasm/tests/test-wasm-exceptions.rs b/crates/tinywasm/tests/test-wasm-exceptions.rs new file mode 100644 index 00000000..483dc330 --- /dev/null +++ b/crates/tinywasm/tests/test-wasm-exceptions.rs @@ -0,0 +1,11 @@ +use tinywasm_cli::testsuite::TestSuite; +use wasm_testsuite::data::{Proposal, proposal}; + +fn main() -> Result<(), Box> { + TestSuite::set_log_level(log::LevelFilter::Off); + let mut test_suite = TestSuite::new(); + + test_suite.run_files(proposal(&Proposal::ExceptionHandling))?; + test_suite.save_csv("./tests/generated/wasm-exceptions.csv", env!("CARGO_PKG_VERSION"))?; + test_suite.report_status() +} From 6d8a7f778331a16c7dec3b17feca8a5fde900bb9 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sat, 26 Sep 2026 10:21:23 -0700 Subject: [PATCH 2/2] test: add exception unwinding cases Exnref versions of shapes that exposed unwinding bugs in another interpreter's exception handling: typed catch selection, mixed-width payloads with values left under the handler block, try_table results, handler scope once an outer catch takes the exception (also in a loop), payloads crossing call_indirect and return_call frames with a throw_ref in between, and branches out of try_table inside loops. --- .../wasm-custom/exception-unwinding.wast | 153 ++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 crates/tinywasm/tests/wasm-custom/exception-unwinding.wast diff --git a/crates/tinywasm/tests/wasm-custom/exception-unwinding.wast b/crates/tinywasm/tests/wasm-custom/exception-unwinding.wast new file mode 100644 index 00000000..a94af546 --- /dev/null +++ b/crates/tinywasm/tests/wasm-custom/exception-unwinding.wast @@ -0,0 +1,153 @@ +;; Several typed catch clauses: only the one matching the tag runs. +(module + (tag $a) (tag $b) (tag $c) + (func (export "typed-catch-picks-middle") (result i32) + (block $ha + (block $hb + (block $hc + (try_table (catch $a $ha) (catch $b $hb) (catch $c $hc) + (throw $b)) + (unreachable)) + (return (i32.const 3))) + (return (i32.const 2))) + (i32.const 1))) +(assert_return (invoke "typed-catch-picks-middle") (i32.const 2)) + +;; Mixed-width payloads, with values left on the stack below the handler block +;; and above the throw; throw_ref re-raises a caught payload after its try_table. +(module + (tag $mixed (param i32 i64 f64 v128)) + (tag $pair (param i32 i64)) + (func (export "mixed-payload") (result i32 i64 v128 i32 i64 f64 v128) + (i32.const 100) (i64.const 200) (v128.const i32x4 1 2 3 4) ;; under the handler block + (block $h (result i32 i64 f64 v128) + (try_table (catch $mixed $h) + (i32.const -1) (i64.const -1) (v128.const i32x4 -1 -1 -1 -1) ;; above it + (throw $mixed (i32.const 7) (i64.const 0x1_0000_0008) + (f64.const -10.25) (v128.const i64x2 5 6))) + (unreachable))) + (func (export "throw_ref-keeps-payload") (result i32 i64) + (local $x exnref) + (block $outer (result i32 i64) + (try_table (catch $pair $outer) + (block $h (result i32 i64 exnref) + (try_table (catch_ref $pair $h) + (throw $pair (i32.const 88) (i64.const -88))) + (unreachable)) + (local.set $x) (drop) (drop) ;; consume the handler's copy + (throw_ref (local.get $x))) ;; re-raise outside the try_table + (unreachable)))) +(assert_return (invoke "mixed-payload") + (i32.const 100) (i64.const 200) (v128.const i32x4 1 2 3 4) + (i32.const 7) (i64.const 0x1_0000_0008) (f64.const -10.25) (v128.const i64x2 5 6)) +(assert_return (invoke "throw_ref-keeps-payload") (i32.const 88) (i64.const -88)) + +;; A try_table with results: the normal exit, and a throw that ends the body. +(module + (tag $e (param i32)) + (tag $void) + (func (export "result-typed") (param i32) (result i32 i32) + (i32.const 5) + (block $h (result i32) + (try_table (result i32) (catch $e $h) + (i32.const 1000) + (br_if 0 (i32.const 7) (i32.eqz (local.get 0))) ;; normal exit, junk under + (throw $e (i32.const 99))))) ;; body ends in throw + (func (export "result-typed-catch_all") (result i32) + (block $h + (try_table (result i32) (catch_all $h) (throw $void)) + (return)) + (i32.const 33))) +(assert_return (invoke "result-typed" (i32.const 0)) (i32.const 5) (i32.const 7)) +(assert_return (invoke "result-typed" (i32.const 1)) (i32.const 5) (i32.const 99)) +(assert_return (invoke "result-typed-catch_all") (i32.const 33)) + +;; An exception leaving a nested try_table: once the outer handler catches it, +;; the inner handler is out of scope. Repeated in a loop. +(module + (tag $e1) (tag $e2) + (func (export "inner-handler-out-of-scope") (result i32) + (block $outer + (try_table (catch $e1 $outer) + (block $inner + (try_table (catch $e2 $inner) (throw $e1)) + (unreachable)) + (return (i32.const 99)))) + (throw $e2)) ;; only the inner try_table catches $e2 + (func (export "throw-to-outer-in-loop") (result i32) + (local $n i32) + (loop $again + (block $outer + (try_table (catch $e1 $outer) + (block $inner + (try_table (catch $e2 $inner) (throw $e1)) + (unreachable)) + (return (i32.const -1)))) + (br_if $again (i32.lt_u (local.tee $n (i32.add (local.get $n) (i32.const 1))) + (i32.const 100000)))) + (local.get $n))) +(assert_exception (invoke "inner-handler-out-of-scope")) +(assert_return (invoke "throw-to-outer-in-loop") (i32.const 100000)) + +;; A payload crossing frames through call_indirect and return_call, with a middle +;; frame that catches it and re-raises it with throw_ref. +(module + (tag $e (param i32 i64)) + (type $v (func)) + (table funcref (elem $thrower)) + (func $thrower (throw $e (i32.const 42) (i64.const -42))) + (func $via-indirect (call_indirect (type $v) (i32.const 0))) + (func $via-tail (return_call $via-indirect)) + (func $rethrower + (block $h (result i32 i64 exnref) + (try_table (catch_ref $e $h) (call $via-tail)) + (return)) + (throw_ref)) + (func (export "cross-frame-payload") (result i32 i32 i64) + (i32.const 7) + (block $h (result i32 i64) + (try_table (result i32 i64) (catch $e $h) + (i32.const 1) (i64.const 2) + (call $rethrower))))) ;; call is the body's last instruction +(assert_return (invoke "cross-frame-payload") (i32.const 7) (i32.const 42) (i64.const -42)) + +;; Branches out of a try_table inside loops, the handler being inactive after a +;; br, and a catch whose label is a loop. +(module + (tag $e) + (tag $next (param i32)) + (func (export "br-to-loop-from-try_table") (result i32) + (local $i i32) + (block $exit + (loop $body + (try_table (catch $e $exit) + (br_if $exit (i32.ge_u (local.tee $i (i32.add (local.get $i) (i32.const 1))) + (i32.const 100000))) + (br $body)))) + (local.get $i)) + (func (export "br-out-in-enclosing-loop") (result i32) + (local $i i32) + (loop $again + (block $out + (try_table (catch_all $out) (br $out))) + (br_if $again (i32.lt_u (local.tee $i (i32.add (local.get $i) (i32.const 1))) + (i32.const 100000)))) + (local.get $i)) + (func (export "handler-inactive-after-br") + (block $h + (block $out + (try_table (catch $e $h) (br $out))) + (throw $e))) + (func (export "catch-to-loop-label") (result i32) + (local $n i32) + (i32.const 0) + (loop $l (param i32) (result i32) + (local.set $n) + (try_table (result i32) (catch $next $l) + (if (i32.lt_u (local.get $n) (i32.const 1000)) + (then (throw $next (i32.add (local.get $n) (i32.const 1))))) + (local.get $n))))) +(assert_return (invoke "br-to-loop-from-try_table") (i32.const 100000)) +(assert_return (invoke "br-out-in-enclosing-loop") (i32.const 100000)) +(assert_exception (invoke "handler-inactive-after-br")) +(assert_return (invoke "catch-to-loop-label") (i32.const 1000))