Skip to content

Commit d1165c2

Browse files
authored
test: exception-handling spec tests and unwinding cases (#71)
no test target ran the exceptions proposal's spec scripts: wasm-testsuite ships `tag.wast`, `throw.wast`, `throw_ref.wast` and `try_table.wast` only under `proposals/exceptions`. - Added a test-wasm-exceptions target that runs them, like test-wasm-gc does for GC, and puts results in tests/generated/wasm-exceptions.csv . All 105 expectations pass (w00t!). - tests/wasm-custom/exception-unwinding.wast adds 19 assertions for unwinding shapes the spec scripts only partly cover (maybe I should contribute those to upstream CTS at some point): - typed catch selection; - mixed-width payloads, with values left under the handler block; - `try_table` results on both exits; - the inner handler going out of scope once an outer catch takes the exception, repeated in a loop; - a payload crossing `call_indirect` and `return_call` frames, re-raised with `throw_ref` in between; - branches out of `try_table` inside loops, including a catch whose label is a loop. These shapes found bugs in other interpreter's exception handling (most of those fixes were already contributed, and they're in my forks' patches used by my WasmBench app). tinywasm passes all of them, so no production code changes in this PR, this is just to guard against future regressions against the spec and give a nice safety net for upcoming optimization work :)
1 parent a361d8f commit d1165c2

4 files changed

Lines changed: 169 additions & 0 deletions

File tree

‎crates/tinywasm/Cargo.toml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ name = "test-wasm-custom-page-sizes"
5656
harness = false
5757
name = "test-wasm-custom"
5858

59+
[[test]]
60+
harness = false
61+
name = "test-wasm-exceptions"
62+
5963
[[test]]
6064
harness = false
6165
name = "test-wasm-gc"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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}]
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use tinywasm_cli::testsuite::TestSuite;
2+
use wasm_testsuite::data::{Proposal, proposal};
3+
4+
fn main() -> Result<(), Box<dyn core::error::Error>> {
5+
TestSuite::set_log_level(log::LevelFilter::Off);
6+
let mut test_suite = TestSuite::new();
7+
8+
test_suite.run_files(proposal(&Proposal::ExceptionHandling))?;
9+
test_suite.save_csv("./tests/generated/wasm-exceptions.csv", env!("CARGO_PKG_VERSION"))?;
10+
test_suite.report_status()
11+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
;; Several typed catch clauses: only the one matching the tag runs.
2+
(module
3+
(tag $a) (tag $b) (tag $c)
4+
(func (export "typed-catch-picks-middle") (result i32)
5+
(block $ha
6+
(block $hb
7+
(block $hc
8+
(try_table (catch $a $ha) (catch $b $hb) (catch $c $hc)
9+
(throw $b))
10+
(unreachable))
11+
(return (i32.const 3)))
12+
(return (i32.const 2)))
13+
(i32.const 1)))
14+
(assert_return (invoke "typed-catch-picks-middle") (i32.const 2))
15+
16+
;; Mixed-width payloads, with values left on the stack below the handler block
17+
;; and above the throw; throw_ref re-raises a caught payload after its try_table.
18+
(module
19+
(tag $mixed (param i32 i64 f64 v128))
20+
(tag $pair (param i32 i64))
21+
(func (export "mixed-payload") (result i32 i64 v128 i32 i64 f64 v128)
22+
(i32.const 100) (i64.const 200) (v128.const i32x4 1 2 3 4) ;; under the handler block
23+
(block $h (result i32 i64 f64 v128)
24+
(try_table (catch $mixed $h)
25+
(i32.const -1) (i64.const -1) (v128.const i32x4 -1 -1 -1 -1) ;; above it
26+
(throw $mixed (i32.const 7) (i64.const 0x1_0000_0008)
27+
(f64.const -10.25) (v128.const i64x2 5 6)))
28+
(unreachable)))
29+
(func (export "throw_ref-keeps-payload") (result i32 i64)
30+
(local $x exnref)
31+
(block $outer (result i32 i64)
32+
(try_table (catch $pair $outer)
33+
(block $h (result i32 i64 exnref)
34+
(try_table (catch_ref $pair $h)
35+
(throw $pair (i32.const 88) (i64.const -88)))
36+
(unreachable))
37+
(local.set $x) (drop) (drop) ;; consume the handler's copy
38+
(throw_ref (local.get $x))) ;; re-raise outside the try_table
39+
(unreachable))))
40+
(assert_return (invoke "mixed-payload")
41+
(i32.const 100) (i64.const 200) (v128.const i32x4 1 2 3 4)
42+
(i32.const 7) (i64.const 0x1_0000_0008) (f64.const -10.25) (v128.const i64x2 5 6))
43+
(assert_return (invoke "throw_ref-keeps-payload") (i32.const 88) (i64.const -88))
44+
45+
;; A try_table with results: the normal exit, and a throw that ends the body.
46+
(module
47+
(tag $e (param i32))
48+
(tag $void)
49+
(func (export "result-typed") (param i32) (result i32 i32)
50+
(i32.const 5)
51+
(block $h (result i32)
52+
(try_table (result i32) (catch $e $h)
53+
(i32.const 1000)
54+
(br_if 0 (i32.const 7) (i32.eqz (local.get 0))) ;; normal exit, junk under
55+
(throw $e (i32.const 99))))) ;; body ends in throw
56+
(func (export "result-typed-catch_all") (result i32)
57+
(block $h
58+
(try_table (result i32) (catch_all $h) (throw $void))
59+
(return))
60+
(i32.const 33)))
61+
(assert_return (invoke "result-typed" (i32.const 0)) (i32.const 5) (i32.const 7))
62+
(assert_return (invoke "result-typed" (i32.const 1)) (i32.const 5) (i32.const 99))
63+
(assert_return (invoke "result-typed-catch_all") (i32.const 33))
64+
65+
;; An exception leaving a nested try_table: once the outer handler catches it,
66+
;; the inner handler is out of scope. Repeated in a loop.
67+
(module
68+
(tag $e1) (tag $e2)
69+
(func (export "inner-handler-out-of-scope") (result i32)
70+
(block $outer
71+
(try_table (catch $e1 $outer)
72+
(block $inner
73+
(try_table (catch $e2 $inner) (throw $e1))
74+
(unreachable))
75+
(return (i32.const 99))))
76+
(throw $e2)) ;; only the inner try_table catches $e2
77+
(func (export "throw-to-outer-in-loop") (result i32)
78+
(local $n i32)
79+
(loop $again
80+
(block $outer
81+
(try_table (catch $e1 $outer)
82+
(block $inner
83+
(try_table (catch $e2 $inner) (throw $e1))
84+
(unreachable))
85+
(return (i32.const -1))))
86+
(br_if $again (i32.lt_u (local.tee $n (i32.add (local.get $n) (i32.const 1)))
87+
(i32.const 100000))))
88+
(local.get $n)))
89+
(assert_exception (invoke "inner-handler-out-of-scope"))
90+
(assert_return (invoke "throw-to-outer-in-loop") (i32.const 100000))
91+
92+
;; A payload crossing frames through call_indirect and return_call, with a middle
93+
;; frame that catches it and re-raises it with throw_ref.
94+
(module
95+
(tag $e (param i32 i64))
96+
(type $v (func))
97+
(table funcref (elem $thrower))
98+
(func $thrower (throw $e (i32.const 42) (i64.const -42)))
99+
(func $via-indirect (call_indirect (type $v) (i32.const 0)))
100+
(func $via-tail (return_call $via-indirect))
101+
(func $rethrower
102+
(block $h (result i32 i64 exnref)
103+
(try_table (catch_ref $e $h) (call $via-tail))
104+
(return))
105+
(throw_ref))
106+
(func (export "cross-frame-payload") (result i32 i32 i64)
107+
(i32.const 7)
108+
(block $h (result i32 i64)
109+
(try_table (result i32 i64) (catch $e $h)
110+
(i32.const 1) (i64.const 2)
111+
(call $rethrower))))) ;; call is the body's last instruction
112+
(assert_return (invoke "cross-frame-payload") (i32.const 7) (i32.const 42) (i64.const -42))
113+
114+
;; Branches out of a try_table inside loops, the handler being inactive after a
115+
;; br, and a catch whose label is a loop.
116+
(module
117+
(tag $e)
118+
(tag $next (param i32))
119+
(func (export "br-to-loop-from-try_table") (result i32)
120+
(local $i i32)
121+
(block $exit
122+
(loop $body
123+
(try_table (catch $e $exit)
124+
(br_if $exit (i32.ge_u (local.tee $i (i32.add (local.get $i) (i32.const 1)))
125+
(i32.const 100000)))
126+
(br $body))))
127+
(local.get $i))
128+
(func (export "br-out-in-enclosing-loop") (result i32)
129+
(local $i i32)
130+
(loop $again
131+
(block $out
132+
(try_table (catch_all $out) (br $out)))
133+
(br_if $again (i32.lt_u (local.tee $i (i32.add (local.get $i) (i32.const 1)))
134+
(i32.const 100000))))
135+
(local.get $i))
136+
(func (export "handler-inactive-after-br")
137+
(block $h
138+
(block $out
139+
(try_table (catch $e $h) (br $out)))
140+
(throw $e)))
141+
(func (export "catch-to-loop-label") (result i32)
142+
(local $n i32)
143+
(i32.const 0)
144+
(loop $l (param i32) (result i32)
145+
(local.set $n)
146+
(try_table (result i32) (catch $next $l)
147+
(if (i32.lt_u (local.get $n) (i32.const 1000))
148+
(then (throw $next (i32.add (local.get $n) (i32.const 1)))))
149+
(local.get $n)))))
150+
(assert_return (invoke "br-to-loop-from-try_table") (i32.const 100000))
151+
(assert_return (invoke "br-out-in-enclosing-loop") (i32.const 100000))
152+
(assert_exception (invoke "handler-inactive-after-br"))
153+
(assert_return (invoke "catch-to-loop-label") (i32.const 1000))

0 commit comments

Comments
 (0)