diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /js/src/jit-test/tests/wasm/exceptions | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
25 files changed, 4692 insertions, 0 deletions
diff --git a/js/src/jit-test/tests/wasm/exceptions/bug-1744663-extended.js b/js/src/jit-test/tests/wasm/exceptions/bug-1744663-extended.js new file mode 100644 index 0000000000..6efd8f0bb1 --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/bug-1744663-extended.js @@ -0,0 +1,224 @@ +load(libdir + "eqArrayHelper.js"); + +// These tests ensure that the gc related errors in bug 1744663 are resolved. + +{ + let catchlessTry = ` + (try + (do (call $gc) + (throw $exn)))`; + let rethrow0 = ` + (try + (do (call $gc) + (throw $exn)) + (catch $exn (rethrow 0)))`; + let rethrow1 = ` + (try + (do (throw $exn)) + (catch_all + (try + (do (throw $exn)) + (catch $exn (rethrow 1)))))`; + let delegate0 = ` + (try + (do (call $gc) + (throw $exn)) + (delegate 0))`; + let delegate1 = ` + (block + (try + (do (call $gc) + (throw $exn)) + (delegate 1)))`; + let delegate0InCatch = ` + (try + (do (throw $exn)) + (catch_all + (try + (do (call $gc) + (throw $exn)) + (delegate 0))))`; + let delegate1InCatch = ` + (try + (do (throw $exn)) + (catch_all + (try + (do (call $gc) + (throw $exn)) + (delegate 1))))`; + + let rethrowingBodies = [catchlessTry, rethrow0, delegate0, delegate1, + rethrow1, delegate0InCatch, delegate1InCatch]; + + function rethrowingIndirectly(rethrowingBody) { + let exports = wasmEvalText( + `(module + (tag $exn (export "exn")) + (import "js" "gc" (func $gc)) + (func $throwExn (export "throwExn") ${rethrowingBody}))`, + {js: {gc: () => { gc(); }}} + ).exports; + + let mod = + `(module + (type $exnType (func)) + (type $indirectFunctype (func)) + (import "m" "exn" (tag $exn (type $exnType))) + (import "m" "throwExn" (func $throwExn (type $indirectFunctype))) + (table funcref (elem $throwExn)) + (func (export "testFunc") (result i32) + (try (result i32) + (do (call_indirect (type $indirectFunctype) (i32.const 0)) + (i32.const 0)) + (catch $exn (i32.const 1)))))`; + + let testFunction = wasmEvalText(mod, { m : exports}).exports.testFunc; + assertEq(testFunction(), 1); + }; + + for (let rethrowingBody of rethrowingBodies) { + //console.log("Calling rethrowingIndirectly with rethrowingBody = " + rethrowingBody); + rethrowingIndirectly(rethrowingBody); + } +} + +// The full test case that caused the original failure. + +{ + gczeal(2,1); // Collect after every allocation. + + let v128Type = " i32"; + let wrongV128 = "(i32.const 0)"; + let correctV128 = "(i32.const 1)"; + let checkV128Value = ""; + + if (wasmSimdEnabled()) { + v128Type = " v128"; + wrongV128 = "(v128.const i32x4 11 22 33 44)"; + correctV128 = "(v128.const i32x4 55 66 77 88)"; + checkV128Value = `;; Check the V128 value + (v128.const i32x4 55 66 77 88) + (i32x4.eq) + (i32x4.all_true)`; + } + + let exports = wasmEvalText( + `(module + (type $exnType (func (param i32 i64 f32 f64 externref ${v128Type}))) + (type $indirectFunctype (func (param i32 i64 f32 f64 externref ${v128Type}) + (result i32 i64 f32 f64 externref ${v128Type}))) + (tag $exn (export "exn") (type $exnType)) + (tag $emptyExn (export "emptyExn")) + (func $throwExn (export "throwExn") (param i32 i64 f32 f64 externref ${v128Type}) + (result i32 i64 f32 f64 externref ${v128Type}) + (local $ifPredicate i32) + (local.get 0) ;; i32 + (local.get 1) ;; i64 + (local.get 2) ;; f32 + (local.get 3) ;; f64 + (local.get 4) ;; ref + (local.get 5) ;; v128 or i32 + (try (param i32 i64 f32 f64 externref ${v128Type}) + (do + (if (param i32 i64 f32 f64 externref ${v128Type}) + (local.get $ifPredicate) + (then (throw $exn)) + (else (throw $exn)))) + (catch $exn + (try (param i32 i64 f32 f64 externref ${v128Type}) + (do (throw $exn)) + (catch_all (rethrow 1)))) + (catch_all)) + unreachable) + (func $throwEmptyExn (export "throwEmptyExn") + (param i32 i64 f32 f64 externref ${v128Type}) + (result i32 i64 f32 f64 externref ${v128Type}) + (throw $emptyExn) + unreachable) + (func $returnArgs (export "returnArgs") + (param i32 i64 f32 f64 externref ${v128Type}) + (result i32 i64 f32 f64 externref ${v128Type}) + (local.get 0) ;; i32 + (local.get 1) ;; i64 + (local.get 2) ;; f32 + (local.get 3) ;; f64 + (local.get 4) ;; ref + (local.get 5)) + (table (export "tab") funcref (elem $throwExn ;; 0 + $throwEmptyExn ;; 1 + $returnArgs)) ;; 2 + )`).exports; + + var mod = + `(module + (type $exnType (func (param i32 i64 f32 f64 externref ${v128Type}))) + (type $indirectFunctype (func (param i32 i64 f32 f64 externref ${v128Type}) + (result i32 i64 f32 f64 externref ${v128Type}))) + (import "m" "exn" (tag $exn (type $exnType))) + (import "m" "emptyExn" (tag $emptyExn)) + (import "m" "throwExn" (func $throwExn (type $indirectFunctype))) + (import "m" "throwEmptyExn" + (func $throwEmptyExn (type $indirectFunctype))) + (import "m" "returnArgs" + (func $returnArgs (type $indirectFunctype))) + (import "m" "tab" (table 3 funcref)) + (func (export "testFunc") (param $correctRef externref) + (param $wrongRef externref) + ;; The last i32 result is the v128 check. + (result i32 i64 f32 f64 externref i32) + (local $ifPredicate i32) + (try (result i32 i64 f32 f64 externref i32) + (do + ;; Wrong values + (i32.const 5) + (i64.const 6) + (f32.const 0.1) + (f64.const 0.6437) + (local.get $wrongRef) + ${wrongV128} + ;; throwEmptyExn + (call_indirect (type $indirectFunctype) (i32.const 1)) + drop ;; Drop the last v128 value. + (i32.const 0)) + (catch_all + (try (result i32 i64 f32 f64 externref ${v128Type}) + (do ;; Values to throw. + (i32.const 2) + (i64.const 3) + (f32.const 4) + (f64.const 13.37) + (local.get $correctRef) + ${correctV128} + (call_indirect (type $indirectFunctype) (i32.const 2)) ;; returnArgs + (call_indirect (type $indirectFunctype) (i32.const 0)) ;; throwExn + drop drop ;; Drop v128 and externref to do trivial and irrelevant ops. + (f64.const 5) + (f64.add) + (local.get $wrongRef) + ${wrongV128} + ;; throwEmptyExn + (call_indirect (type $indirectFunctype) (i32.const 1)) + unreachable) + (catch $emptyExn + ;; Wrong values + (i32.const 5) + (i64.const 6) + (f32.const 0.1) + (f64.const 0.6437) + (local.get $wrongRef) + ${wrongV128}) + (catch $exn) + (catch_all + ;; Wrong values + (i32.const 5) + (i64.const 6) + (f32.const 0.1) + (f64.const 0.6437) + (local.get $wrongRef) + ${wrongV128})) + ${checkV128Value}))))`; + + let testAllValtypes = wasmEvalText(mod, { m : exports}).exports.testFunc; + assertEqArray(testAllValtypes("foo", "bar"), + [2, 3n, 4, 13.37, "foo", 1]); +} diff --git a/js/src/jit-test/tests/wasm/exceptions/bug-1744663.js b/js/src/jit-test/tests/wasm/exceptions/bug-1744663.js new file mode 100644 index 0000000000..a63dc24e19 --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/bug-1744663.js @@ -0,0 +1,63 @@ +gczeal(2,1); + +function failureCallingTestFunction() { + let exports = wasmEvalText( + `(module + (tag $exn (export "exn")) + (func $throwExn (export "throwExn") + ;; Note that this does not fail if this function body is a plain (throw $exn). + (try + (do (throw $exn)))))` + ).exports; + + let mod = + `(module + (type $exnType (func)) + (type $indirectFunctype (func)) + (import "m" "exn" (tag $exn (type $exnType))) + (import "m" "throwExn" (func $throwExn (type $indirectFunctype))) + (table funcref (elem $throwExn)) + (func (export "testFunc") (result i32) + (try + (do (call_indirect (type $indirectFunctype) (i32.const 0))) + (catch_all)) + (i32.const 1)))`; + + let testFunction = wasmEvalText(mod, { m : exports}).exports.testFunc; + testFunction(); +}; + +function failureRethrow1() { + let exports = wasmEvalText( + `(module + (tag $exn (export "exn")) + (func $throwExn (export "throwExn") + (try + (do (throw $exn)) + (catch_all + (try + (do (throw $exn)) + (catch_all (rethrow 1)))))))` + ).exports; + + let mod = + `(module + (type $exnType (func)) + (type $indirectFunctype (func)) + (import "m" "exn" (tag $exn (type $exnType))) + (import "m" "throwExn" (func $throwExn (type $indirectFunctype))) + (table funcref (elem $throwExn)) + (func (export "testFunc") (result i32) + (try + (do (call_indirect (type $indirectFunctype) (i32.const 0))) + (catch_all)) + (i32.const 1)))`; + + let testFunction = wasmEvalText(mod, { m : exports}).exports.testFunc; + testFunction(); +}; + +console.log("Calling failureCallingTestFunction."); +failureCallingTestFunction(); +console.log("Calling failureRethrow1."); +failureRethrow1(); diff --git a/js/src/jit-test/tests/wasm/exceptions/bug-1747562.js b/js/src/jit-test/tests/wasm/exceptions/bug-1747562.js new file mode 100644 index 0000000000..e54b860664 --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/bug-1747562.js @@ -0,0 +1,35 @@ +let bytes = wasmTextToBinary(`(module + (type (;0;) (func)) + (func (;0;) (type 0) + block ;; label = @1 + try ;; label = @2 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + call 0 + delegate 0 + end) + (export "" (func 0)))`); +let module = new WebAssembly.Module(bytes); diff --git a/js/src/jit-test/tests/wasm/exceptions/bug-1747704.js b/js/src/jit-test/tests/wasm/exceptions/bug-1747704.js new file mode 100644 index 0000000000..f9f4292961 --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/bug-1747704.js @@ -0,0 +1,14 @@ +let {shouldTrap} = wasmEvalText(`(module + (func + try + i32.const 0 + table.get 0 + ref.is_null + drop + catch_all + end + ) + (table 0 funcref) + (export "shouldTrap" (func 0)) +)`).exports; +assertErrorMessage(shouldTrap, WebAssembly.RuntimeError, /table index/); diff --git a/js/src/jit-test/tests/wasm/exceptions/bug-1751699.js b/js/src/jit-test/tests/wasm/exceptions/bug-1751699.js new file mode 100644 index 0000000000..38268e7905 --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/bug-1751699.js @@ -0,0 +1,8 @@ +// |jit-test| skip-if: !('oomTest' in this) + +oomTest(() => { + wasmEvalText(` + (import "" "" (func $d)) + (func try call $d end) + `); +}); diff --git a/js/src/jit-test/tests/wasm/exceptions/bug-1767446.js b/js/src/jit-test/tests/wasm/exceptions/bug-1767446.js new file mode 100644 index 0000000000..5cf38a757c --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/bug-1767446.js @@ -0,0 +1,26 @@ +// Dead code elimination can remove wasm calls that may leave behind dangling +// try notes. +wasmEvalText(`(module + (type (;0;) (func (result i32 i32 i32))) + (func $main (type 0) (result i32 i32 i32) + try (result i32 i32 i32) ;; label = @1 + call $main + call $main + i32.const 541 + i32.const 0 + br_if 0 (;@1;) + br_if 0 (;@1;) + br_if 0 (;@1;) + br_if 0 (;@1;) + call $main + call $main + br_if 0 (;@1;) + br_if 0 (;@1;) + call $main + call $main + call $main + unreachable + end + unreachable + ) +)`); diff --git a/js/src/jit-test/tests/wasm/exceptions/bug-1788213.js b/js/src/jit-test/tests/wasm/exceptions/bug-1788213.js new file mode 100644 index 0000000000..4d0b4abcdc --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/bug-1788213.js @@ -0,0 +1,5 @@ +// |jit-test| skip-if: !('oomTest' in this) + +oomTest(() => { + wasmEvalText(`(import "" "" (tag $undef)) (func throw 0) (func (try (do)))`); +}); diff --git a/js/src/jit-test/tests/wasm/exceptions/bug-1791361.js b/js/src/jit-test/tests/wasm/exceptions/bug-1791361.js new file mode 100644 index 0000000000..9c4432de91 --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/bug-1791361.js @@ -0,0 +1,12 @@ +// |jit-test| skip-if: !('oomTest' in this) + +oomTest(() => { + wasmEvalText(` + (tag $d) + (func $anotherLocalFuncThrowsExn) + (func throw $d) + (func (try (do + call $anotherLocalFuncThrowsExn + ))) + `); +}); diff --git a/js/src/jit-test/tests/wasm/exceptions/bug-1797685.js b/js/src/jit-test/tests/wasm/exceptions/bug-1797685.js new file mode 100644 index 0000000000..d2f02a8fd6 --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/bug-1797685.js @@ -0,0 +1,8 @@ +assertErrorMessage(() => wasmEvalText(`(module + (func) + (func + try + call 0 + delegate 4294967295 + ) +)`), WebAssembly.CompileError, /delegate/); diff --git a/js/src/jit-test/tests/wasm/exceptions/caching.js b/js/src/jit-test/tests/wasm/exceptions/caching.js new file mode 100644 index 0000000000..5427de5102 --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/caching.js @@ -0,0 +1,28 @@ +// |jit-test| skip-if: !wasmCachingEnabled() + +load(libdir + "wasm-caching.js"); + +// Test that the tag section is cached correctly +testCached(`(module + (tag $t (export "t")) + (func (export "r") + throw $t + ) +)`, {}, i => { + assertErrorMessage(() => i.exports.r(), WebAssembly.Exception, /.*/); +}); + +// Test that try notes are cached correctly +testCached(`(module + (tag $t) + (func (export "r") (result i32) + try (result i32) + throw $t + i32.const 0 + catch $t + i32.const 1 + end + ) +)`, {}, i => { + assertEq(i.exports.r(), 1, "caught"); +}); diff --git a/js/src/jit-test/tests/wasm/exceptions/calls.js b/js/src/jit-test/tests/wasm/exceptions/calls.js new file mode 100644 index 0000000000..f12e5d1dbe --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/calls.js @@ -0,0 +1,407 @@ +// ----------------------------------------------------------------------------- +// This file contains tests checking Wasm functions with throwing functions and +// try-catch code involving more complex control flow, testing that multiple +// values returned from calls in try code are not affected by multiple branching +// towards the landing pad, as well as making sure exceptions carrying multiple +// values of any Wasm numtype transport the exception values correctly across +// calls. +// +// There are tests for local direct calls, for imported direct calls, for +// indirect calls in a local table with local functions, for indirect calls in a +// local table of imported functions, and for indirect calls in an imported +// table of imported functions. +// +// - TODO: Add reftype values, when support for reftypes in exceptions is +// implemented. +// ----------------------------------------------------------------------------- + +load(libdir + "eqArrayHelper.js"); + +// All individual tests take a string 'localThrow' as argument, and will be run +// with each element of the result of the following function. + +function generateLocalThrows(types, baseThrow) { + // Arguments: + // - 'types': A string of space separated Wasm types. + // - 'baseThrow': A string with a Wasm instruction sequence of Wasm functype + // `[${types}]-> [t*], which takes `types` arguments and ends + // up throwing the tag '$exn'. + // Result: + // - A JS array of strings, each representing a Wasm instruction sequence + // which is like `baseThrow', i.e., a Wasm instruction sequence of Wasm + // functype `[${types}]-> [t*], which takes `types` arguments and ends up + // throwing the tag '$exn'. The result does not include 'baseThrow'. + // + // All strings in Wasm text format. + + // Basic throws; + let catchlessTryThrow = + `(try (param ${types}) + (do ${baseThrow}))`; + + let catchAndThrow = + `(try (param ${types}) + (do ${baseThrow}) + (catch $exn + ${baseThrow}) + (catch_all))`; + + let blockThrow = + `(block (param ${types}) + ${baseThrow})`; + + // This Wasm code requires that the function it appears in has an i32 local + // with name "$ifPredicate". + let conditionalThrow = + `(if (param ${types}) + (local.get $ifPredicate) + (then ${baseThrow}) + (else ${baseThrow}))`; + + // Including try-delegate. + let baseDelegate = + `(try (param ${types}) + (do ${baseThrow}) + (delegate 0))`; + + // Delegate just outside the block. + let nestedDelegate1InBlock = + `(block $label1 (param ${types}) + (try (param ${types}) + (do ${baseThrow}) + (delegate $label1)))`; + + let basicThrows = [catchlessTryThrow, blockThrow, conditionalThrow, + baseDelegate, nestedDelegate1InBlock]; + + // Secondary throws (will end up inside a catch block). + + let baseRethrow = + `(rethrow 0)`; + + let nestedRethrow = + `(try (param ${types}) + (do + ${baseThrow}) + (catch $exn + (rethrow 1)) + (catch_all + (rethrow 0)))`; + + let catchAllRethrowOriginal = + `(try (param ${types}) + (do + ${baseThrow}) + (catch_all + (rethrow 1)))`; + + let secondaryThrows = + [].concat(basicThrows, + [baseRethrow, nestedRethrow, catchAllRethrowOriginal]); + + // Nestings. + + function basicNesting (basicThrow, secondaryThrow) { + return `(try (param ${types}) + (do ${basicThrow}) + (catch $exn + ${secondaryThrow}) + (catch_all))`; + }; + + let result = []; + + for (let basicThrow of basicThrows) { + result.push(basicThrow); + for (let secondaryThrow of secondaryThrows) { + result.push(basicNesting(basicThrow, secondaryThrow)); + } + } + + return result; +}; + +{ + // Some variables to be used in all tests. + let typesJS = ["i32", "i64", "f32", "f64", "externref"]; + let types = typesJS.join(" "); + + // The following depend on whether simd is enabled or not. We write it like + // this so we can run this test also when SIMD is not enabled. + let exntype = ""; + let wrongV128 = ""; + let throwV128 = ""; + let checkV128Value = ""; + + if (wasmSimdEnabled()) { + exntype = types + " v128"; + wrongV128 = `(v128.const i32x4 11 22 33 44)`; + throwV128 = `(v128.const i32x4 55 66 77 88)`; + checkV128Value = `;; Check the V128 value + ${throwV128} + (i32x4.eq) + (i32x4.all_true)`; + } else { + exntype = types + " i32"; + wrongV128 = "(i32.const 0)"; + throwV128 = "(i32.const 1)"; + checkV128Value = ""; + } + + let exnTypeDef = `(type $exnType (func (param ${exntype})))`; + + let throwValues = + `;; Values to throw. + (i32.const 2) + (i64.const 3) + (f32.const 4) + (f64.const 13.37) + (local.get $correctRef) + ${throwV128}`; + + // The last 1 is the result of the test that the v128 value is correct, done + // in wasm code (if simd is enabled). + let correctResultsJS = [2, 3n, 4, 13.37, "foo", 1]; + + let wrongValues = + `;; Wrong values + (i32.const 5) + (i64.const 6) + (f32.const 0.1) + (f64.const 0.6437) + (local.get $wrongRef) + ${wrongV128}`; + + // The individual tests. ----------------------------------------------------- + + function testDirectCallsThrowing(localThrow) { + // Test direct function calls throwing any numeric value. + + let throwifTypeInline = + // The result of the "throwif" function will be used as an argument the + // second time "throwif" is called. + `(param $ifPredicate i32) (param $correctRef externref) (result i32)`; + + let moduleHeaderThrowif = + `(module + ${exnTypeDef} + (tag $exn (export "exn") (type $exnType)) + (func $throwif (export "throwif") ${throwifTypeInline} + (if + (local.get $ifPredicate) + (then + ${throwValues} + ${localThrow})) + (i32.const 1))`; + + let testModuleRest = + `(tag $notThrownExn) + (func $doNothing) + (func (export "testFunc") (param $correctRef externref) + (param $wrongRef externref) + (result ${types} i32) + (local $ifPredicate i32) + (local.get $ifPredicate) + (try (param i32) (result ${exntype}) + (do + (local.get $wrongRef) + (call $throwif) ;; Returns 1. + (call $doNothing) ;; Does nothing. + (local.get $correctRef) + (call $throwif) ;; Throws $exn. + (drop) + ${wrongValues} ;; Won't reach this point. + ${localThrow} + unreachable) + (catch $notThrownExn + ${wrongValues}) + (catch $exn)) + ${checkV128Value}))`; + + function testDirectLocalCallsThrowing() { + let mod = moduleHeaderThrowif + testModuleRest; + // console.log("DIRECT LOCAL MOD = " + mod); // Uncomment for debugging. + + assertEqArray(wasmEvalText(mod).exports.testFunc("foo", "bar"), + correctResultsJS); + }; + + function testDirectImportedCallsThrowing() { + let exports = wasmEvalText(moduleHeaderThrowif + `)`).exports; + // Uncomment for debugging. + //console.log("DIRECT EXPORTS = " + moduleHeaderThrowif + ")"); + + let mod = + `(module + ${exnTypeDef} + (import "m" "exn" (tag $exn (type $exnType))) + (import "m" "throwif" (func $throwif ${throwifTypeInline}))` + + testModuleRest; + // console.log("DIRECT IMPORT MOD = " + mod); // Uncomment for debugging. + + assertEqArray( + wasmEvalText(mod, { m : exports}).exports.testFunc("foo", "bar"), + correctResultsJS); + }; + + testDirectLocalCallsThrowing(); + testDirectImportedCallsThrowing(); + }; + + function testIndirectCallsThrowing(localThrow) { + // Test indirect calls throwing exceptions. + + let indirectFunctypeInline = `(param ${exntype}) + (result ${exntype})`; + let getIndirectArgs = `(local.get 0) ;; i32 + (local.get 1) ;; i64 + (local.get 2) ;; f32 + (local.get 3) ;; f64 + (local.get 4) ;; ref + ;; v128 + (local.get 5)`; + + let testFunctypeInline = `(param $correctRef externref) + (param $wrongRef externref) + ;; The last i32 result is the v128 check. + (result ${types} i32)`; + + let moduleHeader = + `(module + ${exnTypeDef} + (type $indirectFunctype (func ${indirectFunctypeInline})) + (tag $exn (export "exn") (type $exnType)) + (tag $emptyExn (export "emptyExn")) + (func $throwExn (export "throwExn") ${indirectFunctypeInline} + (local $ifPredicate i32) + ${getIndirectArgs} + ${localThrow} + unreachable) + (func $throwEmptyExn (export "throwEmptyExn") + ${indirectFunctypeInline} + (throw $emptyExn) + unreachable) + (func $returnArgs (export "returnArgs") ${indirectFunctypeInline} + ${getIndirectArgs}) + (table (export "tab") funcref (elem $throwExn ;; 0 + $throwEmptyExn ;; 1 + $returnArgs)) ;; 2 + `; + + // The main test function happens to have the same Wasm functype as the + // indirect calls. + let testFuncHeader = `(func (export "testFunc") ${testFunctypeInline} + (local $ifPredicate i32) + `; + + // To test indirect calls to a local table of local functions + function moduleIndirectLocalLocal(functionBody) { + return moduleHeader + testFuncHeader + functionBody + `))`; + }; + + let exports = wasmEvalText(moduleHeader + ")").exports; + // Uncomment for debugging. + //console.log("INDIRECT EXPORTS = " + moduleHeader + ")"); + + let moduleHeaderImporting = + `(module + ${exnTypeDef} + (type $indirectFunctype (func ${indirectFunctypeInline})) + (import "m" "exn" (tag $exn (type $exnType))) + (import "m" "emptyExn" (tag $emptyExn)) + (import "m" "throwExn" (func $throwExn (type $indirectFunctype))) + (import "m" "throwEmptyExn" + (func $throwEmptyExn (type $indirectFunctype))) + (import "m" "returnArgs" + (func $returnArgs (type $indirectFunctype)))`; + + // To test indirect calls to a local table of imported functions. + function moduleIndirectLocalImport(functionBody) { + return moduleHeaderImporting + + `(table funcref (elem $throwExn $throwEmptyExn $returnArgs))` + + testFuncHeader + functionBody + `))`; + }; + + // To test indirect calls to an imported table of imported functions. + function moduleIndirectImportImport(functionBody) { + return moduleHeaderImporting + + `(import "m" "tab" (table 3 funcref))` + + testFuncHeader + functionBody + `))`; + }; + + function getModuleTextsForFunctionBody(functionBody) { + return [moduleIndirectLocalLocal(functionBody), + moduleIndirectLocalImport(functionBody), + moduleIndirectImportImport(functionBody)]; + }; + + // The function bodies for the tests. + + // Three indirect calls, the middle of which throws and will be caught. The + // results of the first and second indirect calls are used by the next + // indirect call. This should be called from try code, to check that the + // pad-branches don't interfere with the results of each call. + let indirectThrow = `${throwValues} + (call_indirect (type $indirectFunctype) (i32.const 2)) ;; returnArgs + (call_indirect (type $indirectFunctype) (i32.const 0)) ;; throwExn + drop drop ;; Drop v128 and externref to do trivial and irrelevant ops. + (f64.const 5) + (f64.add) + (local.get $wrongRef) + ${wrongV128} + ;; throwEmptyExn + (call_indirect (type $indirectFunctype) (i32.const 1)) + unreachable`; + + // Simple try indirect throw and catch. + let simpleTryIndirect = `(try (result ${exntype}) + (do ${indirectThrow}) + (catch $emptyExn + ${wrongValues}) + (catch $exn) + (catch_all + ${wrongValues})) + ${checkV128Value}`; + + // Indirect throw/catch_all, with a simple try indirect throw nested in the + // catch_all. + let nestedTryIndirect = + `(try (result ${types} i32) + (do + ${wrongValues} + ;; throwEmptyExn + (call_indirect (type $indirectFunctype) (i32.const 1)) + drop ;; Drop the last v128 value. + (i32.const 0)) + (catch_all + ${simpleTryIndirect}))`; + + let functionBodies = [simpleTryIndirect, nestedTryIndirect]; + + // Test throwing from indirect calls. + for (let functionBody of functionBodies) { + console.log("functionBody = : " + functionBody); // Uncomment for debugging. + + for (let mod of getModuleTextsForFunctionBody(functionBody)) { + //console.log("mod = : " + mod); // Uncomment for debugging. + + let testFunction = wasmEvalText(mod, { m : exports}).exports.testFunc; + assertEqArray(testFunction("foo", "bar"), + correctResultsJS); + } + } + }; + + // Run all tests. ------------------------------------------------------------ + + let localThrows = + ["(throw $exn)"].concat(generateLocalThrows(exntype, "(throw $exn)")); + + for (let localThrow of localThrows) { + // Uncomment for debugging. + //console.log("Testing with localThrow = " + localThrow); + + testDirectCallsThrowing(localThrow); + testIndirectCallsThrowing(localThrow); + } +} diff --git a/js/src/jit-test/tests/wasm/exceptions/directives.txt b/js/src/jit-test/tests/wasm/exceptions/directives.txt new file mode 100644 index 0000000000..b8ee0d8d7c --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/directives.txt @@ -0,0 +1 @@ +|jit-test| --wasm-exceptions; test-also=--wasm-compiler=optimizing; test-also=--wasm-compiler=baseline; test-also=--wasm-test-serialization; test-also=--test-wasm-await-tier2; include:wasm.js; skip-if: !wasmExceptionsEnabled() diff --git a/js/src/jit-test/tests/wasm/exceptions/events.js b/js/src/jit-test/tests/wasm/exceptions/events.js new file mode 100644 index 0000000000..00bb244546 --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/events.js @@ -0,0 +1,160 @@ +// Tests for tag section support + +load(libdir + "wasm-binary.js"); + +function wasmEval(code, imports) { + new WebAssembly.Instance(new WebAssembly.Module(code), imports).exports; +} + +function wasmError(code, errorType, regexp) { + assertErrorMessage(() => wasmEval(code, {}), errorType, regexp); +} + +const emptyType = { args: [], ret: VoidCode }; +const badExnType = { args: [], ret: I32Code }; + +wasmEvalText(` + (module + (type (func (param i32))) + (tag $exn (type 0))) +`); + +wasmError( + moduleWithSections([ + sigSection([emptyType]), + memorySection(0), + { name: tagId, body: [] }, + ]), + WebAssembly.CompileError, + /expected number of tags/ +); + +wasmError( + moduleWithSections([ + sigSection([emptyType]), + memorySection(0), + { name: tagId, body: [1, 1] }, + ]), + WebAssembly.CompileError, + /illegal tag kind/ +); + +wasmError( + moduleWithSections([ + sigSection([emptyType]), + memorySection(0), + { name: tagId, body: [1, 0] }, + ]), + WebAssembly.CompileError, + /expected function index in tag/ +); + +wasmEval( + moduleWithSections([ + sigSection([emptyType]), + memorySection(0), + tagSection([{ type: 0 }]), + ]) +); + +wasmError( + moduleWithSections([ + sigSection([badExnType]), + memorySection(0), + tagSection([{ type: 0 }]), + ]), + WebAssembly.CompileError, + /tag function types must not return anything/ +); + +wasmError( + moduleWithSections([ + sigSection([emptyType]), + memorySection(0), + tagSection([{ type: 1 }]), + ]), + WebAssembly.CompileError, + /function type index in tag out of bounds/ +); + +wasmError( + moduleWithSections([ + sigSection([emptyType]), + tagSection([{ type: 0 }]), + memorySection(0), + ]), + WebAssembly.CompileError, + /expected custom section/ +); + +(() => { + const body = [1]; + body.push(...string("mod")); + body.push(...string("exn")); + body.push(...varU32(TagCode)); + + wasmError( + moduleWithSections([ + sigSection([emptyType]), + { name: importId, body: body }, + ]), + WebAssembly.CompileError, + /expected tag kind/ + ); + + body.push(...varU32(0)); + wasmError( + moduleWithSections([ + sigSection([emptyType]), + { name: importId, body: body }, + ]), + WebAssembly.CompileError, + /expected function index in tag/ + ); + + body.push(...varU32(1)); + wasmError( + moduleWithSections([ + sigSection([emptyType]), + { name: importId, body: body }, + ]), + WebAssembly.CompileError, + /function type index in tag out of bounds/ + ); +})(); + +wasmEval( + moduleWithSections([ + sigSection([emptyType]), + memorySection(0), + tagSection([{ type: 0 }]), + exportSection([{ tagIndex: 0, name: "exn" }]), + ]) +); + +wasmError( + moduleWithSections([ + sigSection([emptyType]), + memorySection(0), + tagSection([{ type: 0 }]), + exportSection([{ tagIndex: 1, name: "exn" }]), + ]), + WebAssembly.CompileError, + /exported tag index out of bounds/ +); + +(() => { + const body = [1]; + body.push(...string("exn")); + body.push(...varU32(TagCode)); + wasmError( + moduleWithSections([ + sigSection([emptyType]), + memorySection(0), + tagSection([{ type: 0 }]), + { name: exportId, body: body }, + ]), + WebAssembly.CompileError, + /expected tag index/ + ); +})(); diff --git a/js/src/jit-test/tests/wasm/exceptions/example.js b/js/src/jit-test/tests/wasm/exceptions/example.js new file mode 100644 index 0000000000..f1c9f144b9 --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/example.js @@ -0,0 +1,28 @@ +// A diagram depicting the control flow graph of the function g below can be +// found in "js/src/wasm/WasmIonCompile.cpp". If you make any changes to this +// test file be sure to adjust the SMDOC documentation there as well. + +let g = wasmEvalText( + `(module + (tag $exn (param f64)) + (func $f) + (func (export "g") (param $arg i32) (result f64) + (local.get $arg) + (try (param i32) (result f64) + (do + (if (result f64) + (then + (f64.const 3)) + (else + (throw $exn (f64.const 6)))) + (call $f) + (f64.sub (f64.const 2))) ;; If $arg is 0 we end here, subtracting 3. + ;; If $arg is not 0 then the else-block throws $exn, caught below. + (catch $exn + (f64.add (f64.const 4))) ;; Adds 4 to the value in the $exn (6). + (catch_all ;; This shouldn't occur. + (f64.const 5)))))` +).exports.g; + +assertEq(g(0), 10); +assertEq(g(1), 1); diff --git a/js/src/jit-test/tests/wasm/exceptions/import-export.js b/js/src/jit-test/tests/wasm/exceptions/import-export.js new file mode 100644 index 0000000000..fe7646678f --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/import-export.js @@ -0,0 +1,100 @@ +// Tests for Wasm exception import and export. + +function testImports() { + var mod = ` + (module + (type (func (param i32 i32))) + (import "m" "exn" (tag (type 0)))) + `; + + assertErrorMessage( + () => wasmEvalText(mod, { m: { exn: "not a tag" } }), + WebAssembly.LinkError, + /import object field 'exn' is not a Tag/ + ); +} + +function testExports() { + var exports1 = wasmEvalText(` + (module (type (func)) (tag (export "exn") (type 0))) + `).exports; + + assertEq(typeof exports1.exn, "object"); + assertEq(exports1.exn instanceof WebAssembly.Tag, true); + + var exports2 = wasmEvalText(` + (module + (type (func (param i32 i32))) + (tag (export "exn") (type 0))) + `).exports; + + assertEq(typeof exports2.exn, "object"); + assertEq(exports2.exn instanceof WebAssembly.Tag, true); +} + +function testImportExport() { + var exports = wasmEvalText(` + (module + (type (func (param i32))) + (tag (export "exn") (type 0))) + `).exports; + + wasmEvalText( + ` + (module + (type (func (param i32))) + (import "m" "exn" (tag (type 0)))) + `, + { m: exports } + ); + + assertErrorMessage( + () => { + wasmEvalText( + ` + (module + (type (func (param))) + (import "m" "exn" (tag (type 0)))) + `, + { m: exports } + ); + }, + WebAssembly.LinkError, + /imported tag 'm.exn' signature mismatch/ + ); +} + +// Test imports/exports descriptions. +function testDescriptions() { + const imports = WebAssembly.Module.imports( + new WebAssembly.Module( + wasmTextToBinary(` + (module $m + (type (func)) + (import "m" "e" (tag (type 0)))) + `) + ) + ); + + const exports = WebAssembly.Module.exports( + new WebAssembly.Module( + wasmTextToBinary(` + (module + (type (func)) + (tag (export "e") (type 0))) + `) + ) + ); + + assertEq(imports[0].module, "m"); + assertEq(imports[0].name, "e"); + assertEq(imports[0].kind, "tag"); + + assertEq(exports[0].name, "e"); + assertEq(exports[0].kind, "tag"); +} + +testImports(); +testExports(); +testImportExport(); +testDescriptions(); diff --git a/js/src/jit-test/tests/wasm/exceptions/instructions.js b/js/src/jit-test/tests/wasm/exceptions/instructions.js new file mode 100644 index 0000000000..a184b2e99e --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/instructions.js @@ -0,0 +1,1528 @@ +// Tests for Wasm exception proposal instructions. + +// Test try blocks with no handlers. +assertEq( + wasmEvalText( + `(module + (func (export "f") (result i32) + try (result i32) (i32.const 0) end))` + ).exports.f(), + 0 +); + +assertEq( + wasmEvalText( + `(module + (func (export "f") (result i32) + try (result i32) (i32.const 0) (br 0) (i32.const 1) end))` + ).exports.f(), + 0 +); + +assertEq( + wasmEvalText( + `(module + (type (func)) + (tag $exn (type 0)) + (func (export "f") (result i32) + try (result i32) + try (result i32) + (throw $exn) + (i32.const 1) + end + drop + (i32.const 2) + catch $exn + (i32.const 0) + end))` + ).exports.f(), + 0 +); + +assertEq( + wasmEvalText( + `(module + (type (func)) + (tag $exn (type 0)) + (func (export "f") (result i32) + try (result i32) + try (result i32) + try + try + (throw $exn) + end + end + (i32.const 1) + end + drop + (i32.const 2) + catch $exn + (i32.const 0) + end))` + ).exports.f(), + 0 +); + +assertEq( + wasmEvalText( + `(module + (type (func)) + (tag $exn (type 0)) + (func (export "f") (result i32) + try (result i32) + try (result i32) + try + try + (throw $exn) + end + catch_all + rethrow 0 + end + (i32.const 1) + end + drop + (i32.const 2) + catch $exn + (i32.const 0) + end))` + ).exports.f(), + 0 +); + +// Test trivial try-catch with empty bodies. +assertEq( + wasmEvalText( + `(module + (type (func)) + (tag $exn (type 0)) + (func (export "f") (result i32) + try nop catch $exn end + (i32.const 0)))` + ).exports.f(), + 0 +); + +assertEq( + wasmEvalText( + `(module + (func (export "f") (result i32) + try nop catch_all end + (i32.const 0)))` + ).exports.f(), + 0 +); + +// Test try block with no throws +assertEq( + wasmEvalText( + `(module + (type (func)) + (tag $exn (type 0)) + (func (export "f") (result i32) + try (result i32) + (i32.const 0) + catch $exn + (i32.const 1) + end))` + ).exports.f(), + 0 +); + +// Ensure catch block is really not run when no throw occurs. +assertEq( + wasmEvalText( + `(module + (type (func)) + (tag $exn (type 0)) + (func (export "f") (result i32) (local i32) + try + (local.set 0 (i32.const 42)) + catch $exn + (local.set 0 (i32.const 99)) + end + (local.get 0)))` + ).exports.f(), + 42 +); + +// Simple uses of throw. +assertEq( + wasmEvalText( + `(module + (type (func (param i32))) + (tag $exn (type 0)) + (func (export "f") (result i32) + try (result i32) + (i32.const 42) + (throw $exn) + catch $exn + drop + (i32.const 1) + end))` + ).exports.f(), + 1 +); + +assertEq( + wasmEvalText( + `(module + (type (func (param i32))) + (tag $exn (type 0)) + (func $foo (param i32) (result i32) + (local.get 0) (throw $exn)) + (func (export "f") (result i32) + try (result i32) + (i32.const 42) + (call $foo) + catch $exn + drop + (i32.const 1) + end))` + ).exports.f(), + 1 +); + +// Simple uses of throw of some Wasm vectortype values (Simd128). +if (wasmSimdEnabled()) { + assertEq( + wasmEvalText( + `(module + (type (func (param v128 v128 v128 v128))) + (tag $exn (type 0)) + (func (export "f") (result i32) + try (result i32) + (v128.const i32x4 42 41 40 39) + (v128.const f32x4 4.2 4.1 0.40 3.9) + (v128.const i64x2 42 41) + (v128.const f64x2 4.2 4.1) + (throw $exn) + catch $exn + drop drop drop + (i64x2.all_true) + end))` + ).exports.f(), + 1 + ); + + assertEq( + wasmEvalText( + `(module + (type (func (param v128 v128 v128 v128))) + (tag $exn (type 0)) + (func $foo (param v128 v128 v128 v128) (result i32) + (throw $exn (local.get 0) + (local.get 1) + (local.get 2) + (local.get 3))) + (func (export "f") (result i32) + try (result i32) + (v128.const i32x4 42 41 40 39) + (v128.const f32x4 4.2 4.1 0.40 3.9) + (v128.const i64x2 42 41) + (v128.const f64x2 4.2 4.1) + (call $foo) + catch $exn + drop drop drop + (i64x2.all_true) + end))` + ).exports.f(), + 1 + ); + + { + let imports = + wasmEvalText( + `(module + (tag $exn (export "exn") (param v128)) + (func (export "throws") (param v128) (result v128) + (throw $exn (local.get 0)) + (v128.const i32x4 9 10 11 12)))`).exports; + + let mod = + `(module + (import "m" "exn" (tag $exn (param v128))) + (import "m" "throws" (func $throws (param v128) (result v128))) + (func (export "f") (result i32) (local v128) + (v128.const i32x4 1 2 3 4) + (local.tee 0) + (try (param v128) (result v128) + (do (call $throws)) + (catch $exn) + (catch_all (v128.const i32x4 5 6 7 8))) + (local.get 0) + (i32x4.eq) + (i32x4.all_true)))`; + + assertEq(wasmEvalText(mod, { m : imports }).exports.f(), 1); + } +} + +// Further nested call frames should be ok. +assertEq( + wasmEvalText( + `(module + (type (func (param i32))) + (tag $exn (type 0)) + (func $foo (param i32) (result i32) + (local.get 0) (call $bar)) + (func $bar (param i32) (result i32) + (local.get 0) (call $quux)) + (func $quux (param i32) (result i32) + (local.get 0) (throw $exn)) + (func (export "f") (result i32) + try (result i32) + (i32.const 42) + (call $foo) + catch $exn + drop + (i32.const 1) + end))` + ).exports.f(), + 1 +); + +// Basic throwing from loop. + +assertEq( + wasmEvalText( + `(module + (tag $exn) + ;; For the purpose of this test, the params below should be increasing. + (func (export "f") (param $depth_to_throw_exn i32) + (param $maximum_loop_iterations i32) + (result i32) + (local $loop_counter i32) + ;; The loop is counting down. + (local.get $maximum_loop_iterations) + (local.set $loop_counter) + (block $catch + (loop $loop + (if (i32.eqz (local.get $loop_counter)) + (then + (return (i32.const 440))) + (else + (try + (do + (if (i32.eq (local.get $depth_to_throw_exn) + (local.get $loop_counter)) + (then + (throw $exn)) + (else + (local.set $loop_counter + (i32.sub (local.get $loop_counter) + (i32.const 1)))))) + (catch $exn (br $catch)) + (catch_all)))) + (br $loop)) + (return (i32.const 10001))) + (i32.const 10000)))` + ).exports.f(2, 4), + 10000 +); + +// Ensure conditional throw works. +let conditional = wasmEvalText( + `(module + (type (func (param))) + (tag $exn (type 0)) + (func (export "f") (param i32) (result i32) + try (result i32) + (local.get 0) + if (result i32) + (throw $exn) + else + (i32.const 42) + end + catch $exn + (i32.const 99) + end))` +).exports.f; + +assertEq(conditional(0), 42); +assertEq(conditional(1), 99); + +// Ensure multiple & nested try-catch blocks work. +assertEq( + wasmEvalText( + `(module + (type (func (param))) + (tag $exn (type 0)) + (func $foo (throw $exn)) + (func (export "f") (result i32) (local i32) + try + nop + catch $exn + (local.set 0 (i32.const 99)) + end + try + (call $foo) + catch $exn + (local.set 0 (i32.const 42)) + end + (local.get 0)))` + ).exports.f(), + 42 +); + +assertEq( + wasmEvalText( + `(module + (type (func (param))) + (tag $exn (type 0)) + (func (export "f") (result i32) (local i32) + try + try + try + (throw $exn) + catch $exn + (local.set 0 (i32.const 42)) + end + catch $exn + (local.set 0 (i32.const 97)) + end + catch $exn + (local.set 0 (i32.const 98)) + end + (local.get 0)))` + ).exports.f(), + 42 +); + +assertEq( + wasmEvalText( + `(module + (tag $exn) + (func (export "f") (result i32) + (try + (do throw $exn) + (catch $exn + (try + (do (throw $exn)) + (catch $exn)))) + (i32.const 27)))` + ).exports.f(), + 27 +); + +{ + let nested_throw_in_block_and_in_catch = + wasmEvalText( + `(module + (tag $exn) + (func $throw + (throw $exn)) + (func (export "f") (param $arg i32) (result i32) + (block (result i32) + (try (result i32) + (do + (call $throw) + (unreachable)) + (catch $exn + (if (result i32) + (local.get $arg) + (then + (try (result i32) + (do + (call $throw) + (unreachable)) + (catch $exn + (i32.const 27)))) + (else + (i32.const 11))))))))` + ).exports.f; + + assertEq(nested_throw_in_block_and_in_catch(1), 27); + assertEq(nested_throw_in_block_and_in_catch(0), 11); +} + +assertEq( + wasmEvalText( + `(module + (tag $thrownExn) + (tag $notThrownExn) + (func (export "f") (result i32) + (try (result i32) + (do + (try (result i32) + (do (throw $thrownExn)) + (catch $notThrownExn + (i32.const 19)) + (catch $thrownExn + (i32.const 20)) + (catch_all + (i32.const 21)))))))` + ).exports.f(), + 20 +); + +// Test that uncaught exceptions get propagated. +assertEq( + wasmEvalText( + `(module + (tag $thrownExn) + (tag $notThrownExn) + (func (export "f") (result i32) (local i32) + (try + (do + (try + (do + (try + (do (throw $thrownExn)) + (catch $notThrownExn + (local.set 0 + (i32.or (local.get 0) + (i32.const 1)))))) + (catch $notThrownExn + (local.set 0 + (i32.or (local.get 0) + (i32.const 2)))))) + (catch $thrownExn + (local.set 0 + (i32.or (local.get 0) + (i32.const 4))))) + (local.get 0)))` + ).exports.f(), + 4 +); + +// Test tag dispatch for catches. +assertEq( + wasmEvalText( + `(module + (type (func (param))) + (tag $exn1 (type 0)) + (tag $exn2 (type 0)) + (tag $exn3 (type 0)) + (func (export "f") (result i32) + try (result i32) + throw $exn1 + catch $exn1 + i32.const 1 + catch $exn2 + i32.const 2 + catch $exn3 + i32.const 3 + end))` + ).exports.f(), + 1 +); + +assertEq( + wasmEvalText( + `(module + (type (func (param))) + (tag $exn1 (type 0)) + (tag $exn2 (type 0)) + (tag $exn3 (type 0)) + (func (export "f") (result i32) + try (result i32) + throw $exn2 + catch $exn1 + i32.const 1 + catch $exn2 + i32.const 2 + catch $exn3 + i32.const 3 + end))` + ).exports.f(), + 2 +); + +assertEq( + wasmEvalText( + `(module + (type (func (param))) + (tag $exn1 (type 0)) + (tag $exn2 (type 0)) + (tag $exn3 (type 0)) + (func (export "f") (result i32) + try (result i32) + throw $exn3 + catch $exn1 + i32.const 1 + catch $exn2 + i32.const 2 + catch $exn3 + i32.const 3 + end))` + ).exports.f(), + 3 +); + +assertEq( + wasmEvalText( + `(module + (type (func (param))) + (tag $exn1 (type 0)) + (tag $exn2 (type 0)) + (tag $exn3 (type 0)) + (tag $exn4 (type 0)) + (func (export "f") (result i32) + try (result i32) + try (result i32) + throw $exn4 + catch $exn1 + i32.const 1 + catch $exn2 + i32.const 2 + catch $exn3 + i32.const 3 + end + catch $exn4 + i32.const 4 + end))` + ).exports.f(), + 4 +); + +// Test usage of br before a throw. +assertEq( + wasmEvalText( + `(module + (type (func (param i32))) + (tag $exn (type 0)) + (func (export "f") (result i32) + try $l (result i32) + (i32.const 2) + (br $l) + (throw $exn) + catch $exn + drop + (i32.const 1) + end))` + ).exports.f(), + 2 +); + +assertEq( + wasmEvalText( + `(module + (type (func (param))) + (tag $exn (type 0)) + (func (export "f") (result i32) + try $l (result i32) + (throw $exn) + catch $exn + (i32.const 2) + (br $l) + rethrow 0 + end))` + ).exports.f(), + 2 +); + +assertEq( + wasmEvalText( + `(module + (type (func (param))) + (tag $exn (type 0)) + (func (export "f") (result i32) + try $l (result i32) + (throw $exn) + catch_all + (i32.const 2) + (br $l) + rethrow 0 + end))` + ).exports.f(), + 2 +); + +// Test br branching out of a catch block. +assertEq( + wasmEvalText( + `(module + (type (func (param i32))) + (tag $exn (type 0)) + (func (export "f") (result i32) + block $l (result i32) + block (result i32) + try (result i32) + (i32.const 42) + (throw $exn) + catch $exn + br $l + (i32.const 99) + end + end + end))` + ).exports.f(), + 42 +); + +// Test dead catch block. +assertEq( + wasmEvalText( + `(module + (type (func)) + (tag $exn (type 0)) + (func (export "f") (result i32) + i32.const 0 + return + try nop catch $exn end))` + ).exports.f(), + 0 +); + +assertEq( + wasmEvalText( + `(module + (func (export "f") (result i32) + i32.const 0 + return + try nop catch_all end))` + ).exports.f(), + 0 +); + +// Test catch with exception values pushed to stack. +assertEq( + wasmEvalText( + `(module + (type (func (param i32))) + (type (func (param i32))) + (type (func (param i64))) + (tag $exn (type 0)) + (tag $foo (type 1)) + (tag $bar (type 2)) + (func (export "f") (result i32) + try $l (result i32) + (i32.const 42) + (throw $exn) + catch $exn + catch_all + (i32.const 99) + end))` + ).exports.f(), + 42 +); + +// Throw an exception carrying more than one value. +assertEq( + wasmEvalText( + `(module + (type (func (param i32 i64 f32 f64))) + (tag $exn (type 0)) + (func (export "f") (result i32) + try $l (result i32 i64 f32 f64) + (i32.const 42) + (i64.const 84) + (f32.const 42.2) + (f64.const 84.4) + (throw $exn) + catch $exn + catch_all + (i32.const 99) + (i64.const 999) + (f32.const 99.9) + (f64.const 999.9) + end + drop drop drop))` + ).exports.f(), + 42 +); + +// This should also work inside nested frames. +assertEq( + wasmEvalText( + `(module + (type (func (param i32 i64 f32 f64))) + (tag $exn (type 0)) + (func $foo (param i32 i64 f32 f64) (result i32 i64 f32 f64) + (local.get 0) + (local.get 1) + (local.get 2) + (local.get 3) + (throw $exn)) + (func (export "f") (result i32) + try $l (result i32 i64 f32 f64) + (i32.const 42) + (i64.const 84) + (f32.const 42.2) + (f64.const 84.4) + (call $foo) + catch $exn + catch_all + (i32.const 99) + (i64.const 999) + (f32.const 99.9) + (f64.const 999.9) + end + drop drop drop))` + ).exports.f(), + 42 +); + +// Multiple tagged catch in succession. +assertEq( + wasmEvalText( + `(module + (type (func (param i32))) + (tag $exn1 (type 0)) + (tag $exn2 (type 0)) + (func (export "f") (result i32) + try (result i32) + (i32.const 42) + (throw $exn2) + catch $exn1 + catch $exn2 + catch_all + (i32.const 99) + end))` + ).exports.f(), + 42 +); + +assertEq( + wasmEvalText( + `(module + (tag $exn0) + (tag $exn1) + (tag $exn2) + (tag $exn3) + (tag $exn4) + (tag $exn5) + (func (export "f") (result i32) + (try (result i32) + (do (throw $exn4)) + (catch $exn5 (i32.const 5)) + (catch $exn2 (i32.const 2)) + (catch $exn4 (i32.const 4)) ;; Caught here. + (catch $exn4 (i32.const 44)))))` + ).exports.f(), + 4 +); + +// Try catch with block parameters. +assertEq( + wasmEvalText( + `(module + (type (func)) + (tag $exn (type 0)) + (func (export "f") (result i32) + (i32.const 42) + try (param i32) (result i32) + nop + catch $exn + (i32.const 99) + end))` + ).exports.f(), + 42 +); + +assertEq( + wasmEvalText( + `(module + (type (func (param i32))) + (tag $exn (type 0)) + (func (export "f") (result i32) + (i32.const 42) + try $l (param i32) (result i32) + (throw $exn) + catch $exn + catch_all + (i32.const 99) + end))` + ).exports.f(), + 42 +); + +// Test the catch_all case. +assertEq( + wasmEvalText( + `(module + (type (func (param i32))) + (tag $exn1 (type 0)) + (tag $exn2 (type 0)) + (func (export "f") (result i32) + try $l (result i32) + (i32.const 42) + (throw $exn2) + catch $exn1 + catch_all + (i32.const 99) + end))` + ).exports.f(), + 99 +); + +assertEq( + wasmEvalText( + `(module + (tag $exn (param i32)) + (func (export "f") (result i32) + try (result i32) + try (result i32) + (i32.const 42) + (throw $exn) + catch_all + (i32.const 99) + end + catch $exn + end))` + ).exports.f(), + 99 +); + +// Test foreign exception catch. +assertEq( + wasmEvalText( + `(module + (type (func)) + (import "m" "foreign" (func $foreign)) + (tag $exn (type 0)) + (func (export "f") (result i32) (local i32) + try $l + (call $foreign) + catch $exn + catch_all + (local.set 0 (i32.const 42)) + end + (local.get 0)))`, + { + m: { + foreign() { + throw 5; + }, + }, + } + ).exports.f(), + 42 +); + +// Exception handlers should not catch traps. +assertErrorMessage( + () => + wasmEvalText( + `(module + (type (func)) + (tag $exn (type 0)) + (func (export "f") (result i32) (local i32) + try $l + unreachable + catch $exn + (local.set 0 (i32.const 98)) + catch_all + (local.set 0 (i32.const 99)) + end + (local.get 0)))` + ).exports.f(), + WebAssembly.RuntimeError, + "unreachable executed" +); + +// Ensure that a RuntimeError created by the user is not filtered out +// as a trap emitted by the runtime (i.e., the filtering predicate is not +// observable from JS). +assertEq( + wasmEvalText( + `(module + (import "m" "foreign" (func $foreign)) + (func (export "f") (result i32) + try (result i32) + (call $foreign) + (i32.const 99) + catch_all + (i32.const 42) + end))`, + { + m: { + foreign() { + throw new WebAssembly.RuntimeError(); + }, + }, + } + ).exports.f(), + 42 +); + +// Test uncatchable JS exceptions (OOM & stack overflow). +{ + let f = wasmEvalText( + `(module + (import "m" "foreign" (func $foreign)) + (func (export "f") (result) + try + (call $foreign) + catch_all + end))`, + { + m: { + foreign() { + throwOutOfMemory(); + }, + }, + } + ).exports.f; + + var thrownVal; + try { + f(); + } catch (exn) { + thrownVal = exn; + } + + assertEq(thrownVal, "out of memory"); +} + +assertErrorMessage( + () => + wasmEvalText( + `(module + (import "m" "foreign" (func $foreign)) + (func (export "f") + try + (call $foreign) + catch_all + end))`, + { + m: { + foreign: function foreign() { + foreign(); + }, + }, + } + ).exports.f(), + Error, + "too much recursion" +); + +// Test all implemented instructions in a single module. +{ + let divFunctypeInline = + `(param $numerator i32) (param $denominator i32) (result i32)`; + + let safediv = wasmEvalText( + `(module + (tag $divexn (param i32 i32)) + (tag $notThrownExn (param i32)) + (func $throwingdiv ${divFunctypeInline} + (local.get $numerator) + (local.get $denominator) + (if (param i32 i32) (result i32) + (i32.eqz (local.get $denominator)) + (then + (try (param i32 i32) + (do (throw $divexn)) + (delegate 0)) + (i32.const 9)) + (else + i32.div_u))) + (func $safediv (export "safediv") ${divFunctypeInline} + (local.get $numerator) + (local.get $denominator) + (try (param i32 i32) (result i32) + (do + (call $throwingdiv)) + (catch $notThrownExn) + (catch $divexn + i32.add) + (catch_all + (i32.const 44)))))` + ).exports.safediv; + + assertEq(safediv(6, 3), 2); + assertEq(safediv(6, 0), 6); +} + +// Test simple rethrow. +assertEq( + wasmEvalText( + `(module + (type (func)) + (tag $exn (type 0)) + (func (export "f") (result i32) + try (result i32) + try + throw $exn + catch $exn + rethrow 0 + end + i32.const 1 + catch $exn + i32.const 27 + end))` + ).exports.f(), + 27 +); + +assertEq( + wasmEvalText( + `(module + (type (func)) + (tag $exn (type 0)) + (func (export "f") (result i32) + try (result i32) + try + throw $exn + catch_all + rethrow 0 + end + i32.const 1 + catch $exn + i32.const 27 + end))` + ).exports.f(), + 27 +); + +// Test rethrows in nested blocks. +assertEq( + wasmEvalText( + `(module + (type (func)) + (tag $exn (type 0)) + (func (export "f") (result i32) + try (result i32) + try + throw $exn + catch $exn + block + rethrow 1 + end + end + i32.const 1 + catch $exn + i32.const 27 + end))` + ).exports.f(), + 27 +); + +assertEq( + wasmEvalText( + `(module + (type (func)) + (tag $exn (type 0)) + (func (export "f") (result i32) + try (result i32) + try + throw $exn + catch_all + block + rethrow 1 + end + end + i32.const 1 + catch $exn + i32.const 27 + end))` + ).exports.f(), + 27 +); + +assertEq( + wasmEvalText( + `(module + (type (func)) + (tag $exn1 (type 0)) + (tag $exn2 (type 0)) + (func (export "f") (result i32) + try (result i32) + try + throw $exn1 + catch $exn1 + try + throw $exn2 + catch $exn2 + rethrow 1 + end + end + i32.const 0 + catch $exn1 + i32.const 1 + catch $exn2 + i32.const 2 + end))` + ).exports.f(), + 1 +); + +assertEq( + wasmEvalText( + `(module + (type (func)) + (tag $exn1 (type 0)) + (tag $exn2 (type 0)) + (func (export "f") (result i32) + try (result i32) + try + throw $exn1 + catch $exn1 + try + throw $exn2 + catch_all + rethrow 1 + end + end + i32.const 0 + catch $exn1 + i32.const 1 + catch $exn2 + i32.const 2 + end))` + ).exports.f(), + 1 +); + +// Test that rethrow makes the rest of the block dead code. +assertEq( + wasmEvalText( + `(module + (tag (param i32)) + (func (export "f") (result i32) + (try (result i32) + (do (i32.const 1)) + (catch 0 + (rethrow 0) + (i32.const 2)))))` + ).exports.f(), + 1 +); + +assertEq( + wasmEvalText( + `(module + (tag (param i32)) + (func (export "f") (result i32) + (try (result i32) + (do (try + (do (i32.const 13) + (throw 0)) + (catch 0 + (rethrow 0))) + (unreachable)) + (catch 0))))` + ).exports.f(), + 13 +); + +assertEq( + wasmEvalText( + `(module + (tag) + (func (export "f") (result i32) + (try (result i32) + (do + (try + (do (throw 0)) + (catch 0 + (i32.const 4) + (rethrow 0))) + (unreachable)) + (catch 0 + (i32.const 13)))))` + ).exports.f(), + 13 +); + +assertEq( + wasmEvalText( + `(module + (tag (param i32)) + (func (export "f") (result i32) + (try (result i32) + (do (try + (do (i32.const 13) + (throw 0)) + (catch 0 + (i32.const 111) + (rethrow 0))) + (i32.const 222)) + (catch 0))))` + ).exports.f(), + 13 +); + +// Test try-delegate blocks. + +// Dead delegate to caller +assertEq( + wasmEvalText( + `(module + (tag $exn (param)) + (func (export "f") (result i32) + i32.const 1 + br 0 + try + throw $exn + delegate 0))` + ).exports.f(), + 1 +); + +// Nested try-delegate. +assertEq( + wasmEvalText( + `(module + (tag $exn (param)) + (func (export "f") (result i32) + try (result i32) + try + try + throw $exn + delegate 0 + end + i32.const 0 + catch $exn + i32.const 1 + end))` + ).exports.f(), + 1 +); + +// Non-throwing and breaking try-delegate. +assertEq( + wasmEvalText( + `(module + (tag $exn (param)) + (func (export "f") (result i32) + try (result i32) + i32.const 1 + br 0 + delegate 0))` + ).exports.f(), + 1 +); + +assertEq( + wasmEvalText( + `(module + (tag $exn (param)) + (func (export "f") (result i32) + try (result i32) + i32.const 1 + return + delegate 0))` + ).exports.f(), + 1 + ); + +// More nested try-delegate. +assertEq( + wasmEvalText( + `(module + (type (func (param i32))) + (tag $exn (type 0)) + (func (export "f") (result i32) + try (result i32) + try + i32.const 42 + throw $exn + delegate 0 + i32.const 0 + catch $exn + i32.const 1 + i32.add + end))` + ).exports.f(), + 43 +); + +assertEq( + wasmEvalText( + `(module + (type (func (param i32))) + (tag $exn (type 0)) + (func (export "f") (result i32) + try (result i32) + try (result i32) + try + i32.const 42 + throw $exn + delegate 1 + i32.const 0 + catch $exn + i32.const 1 + i32.add + end + catch $exn + i32.const 2 + i32.add + end))` + ).exports.f(), + 44 +); + +assertEq( + wasmEvalText( + `(module + (type (func (param i32))) + (tag $exn (type 0)) + (func (export "f") (result i32) + try (result i32) + try (result i32) + try (result i32) + try + i32.const 42 + throw $exn + delegate 1 + i32.const 0 + catch $exn + i32.const 1 + i32.add + end + delegate 0 + catch $exn + i32.const 2 + i32.add + end))` + ).exports.f(), + 44 +); + +assertEq( + wasmEvalText( + `(module + (tag $exn (param)) + (func $g (param i32) (result i32) (i32.const 42)) + (func (export "f") (result i32) + try (result i32) + try $t + block (result i32) + (i32.const 4) + (call $g) + try + throw $exn + delegate $t + end + drop + end + i32.const 0 + catch_all + i32.const 1 + end))` + ).exports.f(), + 1 +); + +// Test delegation to function body and blocks. + +// Non-throwing. +assertEq( + wasmEvalText( + `(module + (tag $exn (param)) + (func (export "f") (result i32) + try (result i32) + i32.const 1 + delegate 0))` + ).exports.f(), + 1 +); + +// Block target. +assertEq( + wasmEvalText( + `(module + (tag $exn (param i32)) + (func (export "f") (result i32) + try (result i32) + block + try + i32.const 1 + throw $exn + delegate 0 + end + i32.const 0 + catch $exn + end))` + ).exports.f(), + 1 +); + +// Catch target. +assertEq( + wasmEvalText( + `(module + (tag $exn (param)) + (func (export "f") (result i32) + try (result i32) + try + throw $exn + catch $exn + try + throw $exn + delegate 0 + end + i32.const 0 + catch_all + i32.const 1 + end))` + ).exports.f(), + 1 +); + +// Target function body. +assertEq( + wasmEvalText( + `(module + (type (func (param i32))) + (tag $exn (type 0)) + (func (export "f") (result i32) + try (result i32) + call $g + catch $exn + end) + (func $g (result i32) + try (result i32) + try + i32.const 42 + throw $exn + delegate 1 + i32.const 0 + catch $exn + i32.const 1 + i32.add + end))` + ).exports.f(), + 42 +); + +// Try-delegate from inside a loop. +assertEq( + wasmEvalText( + `(module + (tag $exn) + ;; For the purpose of this test, the params below should be increasing. + (func (export "f") (param $depth_to_throw_exn i32) + (param $maximum_loop_iterations i32) + (result i32) + (local $loop_countdown i32) + ;; Counts how many times the loop was started. + (local $loop_verifier i32) + ;; The loop is counting down. + (local.get $maximum_loop_iterations) + (local.set $loop_countdown) + (try $catch_exn (result i32) + (do + (try + (do + (loop $loop + ;; Counts how many times the loop was started. + (local.set $loop_verifier + (i32.add (i32.const 1) + (local.get $loop_verifier))) + (if (i32.eqz (local.get $loop_countdown)) + (then (return (i32.const 440))) + (else + (try $rethrow_label + (do + (if (i32.eq (local.get $depth_to_throw_exn) + (local.get $loop_countdown)) + (then (throw $exn)) + (else + (local.set $loop_countdown + (i32.sub (local.get $loop_countdown) + (i32.const 1)))))) + (catch $exn (try + (do (rethrow $rethrow_label)) + (delegate $catch_exn)))))) + (br $loop))) + (catch_all unreachable)) + (i32.const 2000)) + (catch_all (i32.const 10000))) + (i32.add (local.get $loop_verifier))))` + ).exports.f(3, 5), + 10003 +); diff --git a/js/src/jit-test/tests/wasm/exceptions/ion-loop-phi.js b/js/src/jit-test/tests/wasm/exceptions/ion-loop-phi.js new file mode 100644 index 0000000000..bdf49ca97b --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/ion-loop-phi.js @@ -0,0 +1,59 @@ +// Finishing a loop will try to minimize phi nodes. We must properly replace +// phi nodes that escape a loop via catch block control flow patches. +wasmEvalText(`(module + (func) + (func (local i32) + try + loop + call 0 + i32.const 0 + br_if 0 + end + catch_all + end + ) +)`); + +// Same as above, but ensure that we check every enclosing try block for +// control flow patches, as delegate can tunnel outwards. +wasmEvalText(`(module + (func) + (func (local i32) + try + try + loop + call 0 + i32.const 0 + br_if 0 + end + delegate 0 + catch_all + end + ) +)`); + +// Ensure that we check the body block as delegate can target that. +wasmEvalText(`(module + (func) + (func (local i32) + loop + try + (; catch patch to try block is added ;) + call 0 + (; br_if ensures we will need a backedge ;) + i32.const 0 + br_if 1 + (; catch patches are relocated to body ;) + delegate 1 + (; finishing loop backedge must fixup patches stored in body ;) + end + + (; add another catch patch to body so that the landing pad will be a + join point between two edges, forcing a use of the dangling phi, + hitting the debug assertion + ;) + try + call 0 + delegate 0 + ) +)`); diff --git a/js/src/jit-test/tests/wasm/exceptions/js-api.js b/js/src/jit-test/tests/wasm/exceptions/js-api.js new file mode 100644 index 0000000000..4c2f018808 --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/js-api.js @@ -0,0 +1,490 @@ +// Tests for wasm exception proposal JS API features. + +load(libdir + "eqArrayHelper.js"); + +// WebAssembly.Tag tests. +assertErrorMessage( + () => WebAssembly.Tag(), + TypeError, + /calling a builtin Tag constructor without new is forbidden/ +); + +assertErrorMessage( + () => new WebAssembly.Tag(), + TypeError, + /At least 1 argument required/ +); + +assertErrorMessage( + () => new WebAssembly.Tag(3), + TypeError, + /first argument must be a tag descriptor/ +); + +assertErrorMessage( + () => new WebAssembly.Tag({ parameters: ["foobar"] }), + TypeError, + /bad value type/ +); + +new WebAssembly.Tag({ parameters: [] }); +new WebAssembly.Tag({ parameters: ["i32"] }); +new WebAssembly.Tag({ parameters: ["i32", "externref"] }); + +wasmEvalText(`(module (import "m" "e" (tag)))`, { + m: { e: new WebAssembly.Tag({ parameters: [] }) }, +}); + +wasmEvalText(`(module (import "m" "e" (tag (param i32))))`, { + m: { e: new WebAssembly.Tag({ parameters: ["i32"] }) }, +}); + +wasmEvalText(`(module (import "m" "e" (tag (param i32 i64))))`, { + m: { e: new WebAssembly.Tag({ parameters: ["i32", "i64"] }) }, +}); + +assertErrorMessage( + () => + wasmEvalText(`(module (import "m" "e" (tag (param i32))))`, { + m: { e: new WebAssembly.Tag({ parameters: [] }) }, + }), + WebAssembly.LinkError, + /imported tag 'm.e' signature mismatch/ +); + +assertErrorMessage( + () => + wasmEvalText(`(module (import "m" "e" (tag (param))))`, { + m: { e: new WebAssembly.Tag({ parameters: ["i32"] }) }, + }), + WebAssembly.LinkError, + /imported tag 'm.e' signature mismatch/ +); + +// Test WebAssembly.Tag methods. +// TODO: add runtime detection for js-types +// { +// let params = [ +// [], +// ["i32"], +// ["i32", "i64"], +// ["f32", "externref"], +// ["i32", "i64", "f32", "f64"], +// ]; + +// for (const arg of params) { +// const tag = new WebAssembly.Tag({ parameters: arg }); +// assertEqArray(tag.type().parameters, arg); +// } +// } + +// WebAssembly.Exception tests. +assertErrorMessage( + () => WebAssembly.Exception(), + TypeError, + /calling a builtin Exception constructor without new is forbidden/ +); + +assertErrorMessage( + () => new WebAssembly.Exception(), + TypeError, + /At least 2 arguments required/ +); + +assertErrorMessage( + () => new WebAssembly.Exception(3, []), + TypeError, + /first argument must be a WebAssembly.Tag/ +); + +const { tag1, tag2, tag3, tag4, tag5, tag6, tag7, tag8, tag9 } = wasmEvalText( + `(module + (tag (export "tag1") (param)) + (tag (export "tag2") (param i32)) + (tag (export "tag3") (param i32 f32)) + (tag (export "tag4") (param i32 externref i32)) + (tag (export "tag5") (param i32 externref i32 externref)) + (tag (export "tag6") (param funcref)) + (tag (export "tag7") (param i64)) + (tag (export "tag8") (param i32 f64)) + (tag (export "tag9") (param externref funcref)))` +).exports; + +new WebAssembly.Exception(tag1, []); +new WebAssembly.Exception(tag2, [3]); +new WebAssembly.Exception(tag3, [3, 5.5]); +new WebAssembly.Exception(tag4, [3, "foo", 4]); +new WebAssembly.Exception(tag5, [3, "foo", 4, "bar"]); + +assertErrorMessage( + () => new WebAssembly.Exception(tag2, []), + TypeError, + /expected 1 values but got 0/ +); + +assertErrorMessage( + () => new WebAssembly.Exception(tag2, [3n]), + TypeError, + /can't convert BigInt to number/ +); + +assertErrorMessage( + () => new WebAssembly.Exception(tag6, [undefined]), + TypeError, + /can only pass WebAssembly exported functions to funcref/ +); + +assertErrorMessage( + () => new WebAssembly.Exception(tag7, [undefined]), + TypeError, + /can't convert undefined to BigInt/ +); + +assertErrorMessage( + () => new WebAssembly.Exception(tag7, {}), + TypeError, + /\({}\) is not iterable/ +); + +assertErrorMessage( + () => new WebAssembly.Exception(tag7, 1), + TypeError, + /second argument must be an object/ +); + +// Test Exception methods. +{ + const exn1 = new WebAssembly.Exception(tag1, []); + assertEq(exn1.is(tag1), true); + assertEq(exn1.is(tag2), false); + assertErrorMessage( + () => exn1.is(), + TypeError, + /At least 1 argument required/ + ); + assertErrorMessage( + () => exn1.is(5), + TypeError, + /first argument must be a WebAssembly.Tag/ + ); + + const exn2 = new WebAssembly.Exception(tag2, [3]); + assertEq(exn2.getArg(tag2, 0), 3); + + assertEq(new WebAssembly.Exception(tag2, [undefined]).getArg(tag2, 0), 0); + + const exn4 = new WebAssembly.Exception(tag4, [3, "foo", 4]); + assertEq(exn4.getArg(tag4, 0), 3); + assertEq(exn4.getArg(tag4, 1), "foo"); + assertEq(exn4.getArg(tag4, 2), 4); + + const exn5 = new WebAssembly.Exception(tag5, [3, "foo", 4, "bar"]); + assertEq(exn5.getArg(tag5, 3), "bar"); + + const { funcref } = wasmEvalText( + `(module (func (export "funcref")))` + ).exports; + const exn9 = new WebAssembly.Exception(tag9, ["foo", funcref]); + assertEq(exn9.getArg(tag9, 0), "foo"); + assertEq(exn9.getArg(tag9, 1), funcref); + + assertErrorMessage( + () => exn2.getArg(), + TypeError, + /At least 2 arguments required/ + ); + assertErrorMessage( + () => exn2.getArg(5, 0), + TypeError, + /first argument must be a WebAssembly.Tag/ + ); + assertErrorMessage( + () => exn2.getArg(tag2, "foo"), + TypeError, + /bad Exception getArg index/ + ); + assertErrorMessage( + () => exn2.getArg(tag2, 10), + RangeError, + /bad Exception getArg index/ + ); +} + +// Test throwing a JS constructed exception to Wasm. +assertEq( + wasmEvalText( + `(module + (import "m" "exn" (tag $exn (param i32))) + (import "m" "f" (func $f)) + (func (export "f") (result i32) + try (result i32) + call $f + (i32.const 0) + catch $exn + end))`, + { + m: { + exn: tag2, + f: () => { + throw new WebAssembly.Exception(tag2, [42]); + }, + }, + } + ).exports.f(), + 42 +); + +assertEqArray( + wasmEvalText( + `(module + (import "m" "exn" (tag $exn (param i32 f32))) + (import "m" "f" (func $f)) + (func (export "f") (result i32 f32) + try (result i32 f32) + call $f + (i32.const 0) + (f32.const 0) + catch $exn + end))`, + { + m: { + exn: tag3, + f: () => { + throw new WebAssembly.Exception(tag3, [42, 5.5]); + }, + }, + } + ).exports.f(), + [42, 5.5] +); + +assertEqArray( + wasmEvalText( + `(module + (import "m" "exn" (tag $exn (param i32 f64))) + (import "m" "f" (func $f)) + (func (export "f") (result i32 f64) + try (result i32 f64) + call $f + (i32.const 0) + (f64.const 0) + catch $exn + end))`, + { + m: { + exn: tag8, + f: () => { + throw new WebAssembly.Exception(tag8, [9999, 9999]); + }, + }, + } + ).exports.f(), + [9999, 9999] +); + +assertEqArray( + wasmEvalText( + `(module + (import "m" "exn" (tag $exn (param i32 externref i32))) + (import "m" "f" (func $f)) + (func (export "f") (result i32 externref i32) + try (result i32 externref i32) + call $f + (i32.const 0) + (ref.null extern) + (i32.const 0) + catch $exn + end))`, + { + m: { + exn: tag4, + f: () => { + throw new WebAssembly.Exception(tag4, [42, "foo", 42]); + }, + }, + } + ).exports.f(), + [42, "foo", 42] +); + +assertEqArray( + wasmEvalText( + `(module + (import "m" "exn" (tag $exn (param i32 externref i32 externref))) + (import "m" "f" (func $f)) + (func (export "f") (result i32 externref i32 externref) + try (result i32 externref i32 externref) + call $f + (i32.const 0) + (ref.null extern) + (i32.const 0) + (ref.null extern) + catch $exn + end))`, + { + m: { + exn: tag5, + f: () => { + throw new WebAssembly.Exception(tag5, [42, "foo", 42, "bar"]); + }, + }, + } + ).exports.f(), + [42, "foo", 42, "bar"] +); + +{ + const { funcref } = wasmEvalText( + `(module (func (export "funcref")))` + ).exports; + assertEqArray( + wasmEvalText( + `(module + (import "m" "exn" (tag $exn (param externref funcref))) + (import "m" "f" (func $f)) + (func (export "f") (result externref funcref) + try (result externref funcref) + call $f + (ref.null extern) + (ref.null func) + catch $exn + end))`, + { + m: { + exn: tag9, + f: () => { + throw new WebAssembly.Exception(tag9, ["foo", funcref]); + }, + }, + } + ).exports.f(), + ["foo", funcref] + ); +} + +assertEq( + wasmEvalText( + `(module + (import "m" "exn" (tag $exn)) + (import "m" "f" (func $f)) + (func (export "f") (result i32) + try (result i32) + call $f + (i32.const 0) + catch $exn + (i32.const 0) + catch_all + (i32.const 1) + end))`, + { + m: { + exn: tag1, + f: () => { + throw new WebAssembly.Exception(tag2, [42]); + }, + }, + } + ).exports.f(), + 1 +); + +{ + const exn = new WebAssembly.Tag({ parameters: ["i32"] }); + assertEq( + wasmEvalText( + `(module + (import "m" "exn" (tag $exn (param i32))) + (import "m" "f" (func $f)) + (func (export "f") (result i32) + try (result i32) + call $f + (i32.const 0) + catch $exn + end))`, + { + m: { + exn, + f: () => { + throw new WebAssembly.Exception(exn, [42]); + }, + }, + } + ).exports.f(), + 42 + ); +} + +{ + const exn1 = new WebAssembly.Tag({ parameters: ["i32"] }); + const exn2 = new WebAssembly.Tag({ parameters: ["i32"] }); + assertEq( + wasmEvalText( + `(module + (import "m" "exn" (tag $exn (param i32))) + (import "m" "f" (func $f)) + (func (export "f") (result i32) + try (result i32) + call $f + (i32.const 0) + catch $exn + catch_all + (i32.const 1) + end))`, + { + m: { + exn: exn1, + f: () => { + throw new WebAssembly.Exception(exn2, [42]); + }, + }, + } + ).exports.f(), + 1 + ); +} + +// Test `getArg` on a Wasm-thrown exception. +assertEq( + (() => { + try { + wasmEvalText( + `(module + (import "m" "exn" (tag $exn (param i32 f64))) + (func (export "f") + (i32.const 9999) + (f64.const 9999) + throw $exn))`, + { m: { exn: tag8 } } + ).exports.f(); + } catch (exn) { + return exn.getArg(tag8, 1); + } + })(), + 9999 +); + +assertEqArray( + (() => { + try { + wasmEvalText( + `(module + (import "m" "exn" (tag $exn (param i32 externref i32 externref))) + (func (export "f") (param externref externref) + (i32.const 1) + (local.get 0) + (i32.const 2) + (local.get 1) + throw $exn))`, + { m: { exn: tag5 } } + ).exports.f("foo", "bar"); + } catch (exn) { + return [ + exn.getArg(tag5, 0), + exn.getArg(tag5, 1), + exn.getArg(tag5, 2), + exn.getArg(tag5, 3), + ]; + } + })(), + [1, "foo", 2, "bar"] +); diff --git a/js/src/jit-test/tests/wasm/exceptions/memory.js b/js/src/jit-test/tests/wasm/exceptions/memory.js new file mode 100644 index 0000000000..fef249ddf5 --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/memory.js @@ -0,0 +1,78 @@ +// Ensure memory operations work after a throw coming from a function call. +// These are also testing that the InstanceReg/HeapReg are set correctly after +// catching an exception. There are some variations to the kind of calls here; +// we test for direct/indirect calls of local/imported functions, and the +// indirect calls may come from a local table or an imported table. + +// Throw from a direct/indirect call of a local/imported function. +{ + let exports = wasmEvalText( + `(module $m + (memory $mem (data "bar")) + (tag $exn (export "exn")) + (tag $dummyExn (export "dummyExn")) + (func $throwsExn (export "throwsExn") + (throw $exn)) + (func $anotherThrowsExn + (throw $exn)) + (func $throwsDummyExn (export "throwsDummyExn") + (throw $dummyExn)) + (table (export "tab") funcref (elem $anotherThrowsExn $throwsDummyExn)))` + ).exports; + + function testMemoryAfterCall(callInstruction) { + assertEq( + wasmEvalText( + `(module + (import "m" "exn" (tag $exn)) + (tag $localExn (param i32)) + (type $t (func)) + (import "m" "tab" (table $importTable 2 funcref)) + (import "m" "throwsExn" (func $importFuncThrowsExn)) + (memory $mem (data "foo")) + (func $localFuncThrowsExn + (throw $exn)) + (table $localTable funcref + (elem $localFuncThrowsExn $importFuncThrowsExn)) + (func $anotherLocalFuncThrowsExn + (throw $exn)) + (func $throwsLocalExn + (throw $localExn + (i32.const 9))) + (func (export "testFunc") (result i32) + (try (result i32) + (do + ${callInstruction} + ;; All the rest should not be reachable. + (call $anotherLocalFuncThrowsExn) + (throw $exn) + (call $throwsLocalExn) + unreachable) + (catch $localExn) + (catch $exn + (i32.load8_u + (i32.const 0))) + (catch $exn + ;; This should be ignored. + unreachable))))`, + { m: exports } + ).exports.testFunc(), + 'foo'.charCodeAt(0) + ); + }; + + // Run test for various calls. + let callInstructions = + ["(call $anotherLocalFuncThrowsExn)", + "(call $importFuncThrowsExn)", + // Calls $localFuncThrowsExn. + "(call_indirect $localTable (type $t) (i32.const 0))", + // Calls $importFuncThrowsExn. + "(call_indirect $localTable (type $t) (i32.const 1))", + // Calls non exported function of the exports module $anotherThrowsExn. + "(call_indirect $importTable (type $t) (i32.const 0))"]; + + for (let callInstruction of callInstructions) { + testMemoryAfterCall(callInstruction); + } +} diff --git a/js/src/jit-test/tests/wasm/exceptions/prototypes.js b/js/src/jit-test/tests/wasm/exceptions/prototypes.js new file mode 100644 index 0000000000..757ed2ddba --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/prototypes.js @@ -0,0 +1,10 @@ +class _Tag extends WebAssembly.Tag {} +class _Exception extends WebAssembly.Exception {} + +let tag = new _Tag({parameters: []}); +assertEq(tag instanceof _Tag, true); +assertEq(tag instanceof WebAssembly.Tag, true); + +let exception = new _Exception(tag, []); +assertEq(exception instanceof _Exception, true); +assertEq(exception instanceof WebAssembly.Exception, true); diff --git a/js/src/jit-test/tests/wasm/exceptions/reftypes.js b/js/src/jit-test/tests/wasm/exceptions/reftypes.js new file mode 100644 index 0000000000..efd577dec8 --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/reftypes.js @@ -0,0 +1,141 @@ +// Exception tests that use reference types. + +load(libdir + "eqArrayHelper.js"); + +assertEq( + wasmEvalText( + `(module + (tag $exn (param externref)) + (func (export "f") (result externref) + try (result externref) + ref.null extern + throw $exn + catch $exn + end))` + ).exports.f(), + null +); + +{ + let f = wasmEvalText( + `(module + (tag $exn (param funcref)) + (func $f (export "f") (result funcref) + try (result funcref) + ref.func $f + throw $exn + catch $exn + end))` + ).exports.f; + assertEq(f(), f); +} + +{ + let f = wasmEvalText( + `(module + (tag $exn (param externref)) + (func (export "f") (param externref) (result externref) + try (result externref) + local.get 0 + throw $exn + catch $exn + end))` + ).exports.f; + + for (v of WasmExternrefValues) { + assertEq(f(v), v); + } +} + +assertEqArray( + wasmEvalText( + `(module + (tag $exn (param externref externref)) + (func (export "f") (param externref externref) + (result externref externref) + try (result externref externref) + local.get 0 + local.get 1 + throw $exn + catch $exn + end))` + ).exports.f("foo", "bar"), + ["foo", "bar"] +); + +assertEqArray( + wasmEvalText( + `(module + (tag $exn (param i32 i32 externref f32)) + (func (export "f") (param externref) + (result i32 i32 externref f32) + try (result i32 i32 externref f32) + i32.const 0 + i32.const 1 + local.get 0 + f32.const 2.0 + throw $exn + catch $exn + end))` + ).exports.f("foo"), + [0, 1, "foo", 2.0] +); + +assertEqArray( + wasmEvalText( + `(module + (tag $exn (param i32 i32 externref f32 externref f64)) + (func (export "f") (param externref externref) + (result i32 i32 externref f32 externref f64) + try (result i32 i32 externref f32 externref f64) + i32.const 0 + i32.const 1 + local.get 0 + f32.const 2.0 + local.get 1 + f64.const 3.0 + throw $exn + catch $exn + end))` + ).exports.f("foo", "bar"), + [0, 1, "foo", 2.0, "bar", 3.0] +); + +// Test to ensure that reference typed values are tracked correctly by +// the GC within exception objects. +{ + gczeal(2, 1); // Collect on every allocation. + + var thrower; + let exports = wasmEvalText( + `(module + (tag $exn (param externref)) + (import "m" "f" (func $f (result externref))) + (func (export "thrower") (param externref) + (local.get 0) + (throw $exn)) + (func (export "catcher") (result externref) + try (result externref) + (call $f) + catch $exn + end))`, + { + m: { + f: () => { + // The purpose of this intermediate try-catch in JS is to force + // some allocation to occur after Wasm's `throw`, triggering GC, and + // then rethrow back to Wasm to observe any errors. + try { + thrower("foo"); + } catch (e) { + let v = { x: 5 }; + throw e; + } + }, + }, + } + ).exports; + + thrower = exports.thrower; + assertEq(exports.catcher(), "foo"); +} diff --git a/js/src/jit-test/tests/wasm/exceptions/side-effects-in-try.js b/js/src/jit-test/tests/wasm/exceptions/side-effects-in-try.js new file mode 100644 index 0000000000..d5d5567fca --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/side-effects-in-try.js @@ -0,0 +1,234 @@ +// ----------------------------------------------------------------------------- +// The tests in this file assert that any side effects that happened in try code +// before an exception was thrown, will be known to the landing pad. It checks +// local throws and throws from direct calls (of local and imported functions). +// Side effects checked are changes to locals, and to globals. +// ----------------------------------------------------------------------------- + +load(libdir + "eqArrayHelper.js"); + + +function testSideEffectsOnLocals() { + // Locals set before and after throwing instructions, either locally, or from + // a direct wasm function call, for locals of all Wasm numtype and a local of + // reftype (externref), and the thrown exception carrying a value from each + // Wasm numtype and one of Wasm vectype (Simd128). Testing to see if the state + // of the locals at the moment $exn is thrown, is known to the landing pad + // when $exn is caught. + + let localThrow = "(throw $exn)"; + + // The following is taken from calls.js + // Some variables to be used in all tests. + let typesJS = ["i32", "i64", "f32", "f64"]; + let types = typesJS.join(" "); + let exnTypeDef = `(type $exnType (func (param ${types})))`; + let correctLocalValues = + `;; Correct local values + (i32.const 2) + (i64.const 3) + (f32.const 4) + (f64.const 13.37)`; + let correctLocalValuesJS = [2, 3n, 4, 13.37]; + + let wrongValues = + `;; Wrong values. + (i32.const 5) + (i64.const 6) + (f32.const 0.1) + (f64.const 0.6437)`; + let wrongValuesJS = [5, 6n, 0.1, 0.6437]; + + // These variables are specific to the tests in this file. + let throwValues = + `;; Values to throw and catch. + (i32.const 7) + (i64.const 8) + (f32.const 9) + (f64.const 27.11)`; + let thrownValuesJS = [7, 8n, 9, 27.11]; + + let correctResultsJS = function(externref) { + return [].concat(thrownValuesJS, + correctLocalValuesJS, + [externref, 1]); + } + + // Testing also locals of Wasm vectype. + // The following depend on whether simd is enabled or not. We write it like + // this so we can run this test also when SIMD is not enabled. + let wrongV128 = ""; + let correctV128 = ""; + let checkV128Value = ""; + + if (wasmSimdEnabled()) { + wrongV128 = `(v128.const i32x4 11 22 33 44)`; + correctV128 = `(v128.const i32x4 55 66 77 88)`; + checkV128Value = + ` ${correctV128} + (i32x4.eq) + (i32x4.all_true)`; + v128Type = " v128"; + } else { + wrongV128 = "(i32.const 0)"; + correctV128 = "(i32.const 1)"; + v128Type = " i32"; + } + + let localTypes = types + " externref"; + let resultTypes = types + " " + localTypes; + + // The last i32 in the results is the v128 check. + let testFuncTypeInline = + `(param $argCorrectRef externref) + (param $argWrongRef externref) + (result ${resultTypes} i32) + (local $localI32 i32) + (local $localI64 i64) + (local $localF32 f32) + (local $localF64 f64) + (local $localExternref externref) + (local $localV128 ${v128Type})`; + + let localsSet = + `;; Set locals. + (local.set $localV128) + (local.set $localExternref) + (local.set $localF64) + (local.set $localF32) + (local.set $localI64) + (local.set $localI32)`; + let localsGet = + `;; Get locals. + (local.get $localI32) + (local.get $localI64) + (local.get $localF32) + (local.get $localF64) + (local.get $localExternref) + (local.get $localV128)`; + + // The test module parts. ---------------------------------------------------- + + let importsModule = + `(module + (type $exnType (func (param ${types}))) + (tag $exn (export "exn") (type $exnType)) + (func (export "throwif") (param $ifPredicate i32) + (if (local.get $ifPredicate) + (then + ${throwValues} + ${localThrow}))))`; + + let moduleHeader = ` + (module + ${exnTypeDef} + (import "m" "exn" (tag $exn (type $exnType))) + (tag $emptyExn) + (import "m" "throwif" (func $throwif (param $ifPredicate i32))) + (func $wontThrow + (throw $emptyExn)) + (func $localCallThrow + ${throwValues} + ${localThrow}) + (func (export "testFunc") ${testFuncTypeInline} + (try (result ${resultTypes} ${v128Type}) + (do + ;; Locals not set. + (i32.const 0) ;; Predicate for $throwif. + (call $throwif) ;; So this doesn't throw. + ;; Set correct locals before throw to be caught. + ${correctLocalValues} + (local.get $argCorrectRef) + ${correctV128} + ${localsSet} + ;; Next up should be $exn being thrown locally or via a call.`; + + let moduleRest = ` ;; The above throw to $exn should be caught here --------. + ;; Set wrong locals after throw to be caught. ;; | + ${wrongValues} ;; | + (local.get $argWrongRef) ;; The wrong externref param. ;; | + ${wrongV128} ;; | + ${localsSet} ;; | + (call $wontThrow) ;; | + ${wrongValues} ;; | + ${localsGet});; End of try code. ;; | + (catch $emptyExn ;; | + ${wrongValues} ;; | + ${localsGet}) ;; | + (catch $exn ;; <---------------------------------------------------' + ${localsGet}) + (catch_all + ${wrongValues} + ${localsGet})) + ;; Check if the local has the correct v128 value. + ${checkV128Value}))`; + + let localThrowValues = ` + ${throwValues} + (throw $exn)`; + let directLocalCall = ` + (call $localCallThrow)`; + let directImportCall = ` + (i32.const 1) + (call $throwif)`; + + // Run test for side effects on locals before throwing an exception locally, + // or from a direct call. + + let callInstructions = [localThrowValues, directLocalCall, directImportCall]; + + for (let callThrow of callInstructions) { + console.log("callThrow = " + callThrow); // Uncomment for debugging. + moduleText = moduleHeader + callThrow + moduleRest; + console.log("moduleText = " + moduleText); // Uncomment for debugging. + assertEqArray( + wasmEvalText(moduleText, + { m : wasmEvalText(importsModule).exports } + ).exports.testFunc("foo", "wrongFoo"), + correctResultsJS("foo")); + } +} + +// Setting globals in try code, and testing to see if the changes are known to +// the landing pad. +function testGlobals() { + let test = function (type, initialValue, resultValue, wrongValue, coercion) { + let exports = wasmEvalText( + `(module + (tag (export "exn")) + (func (export "throws") + (throw 0)))` + ).exports; + + assertEq( + wasmEvalText( + `(module + (import "m" "exn" (tag $exn)) + (tag $notThrownExn) + (import "m" "throws" (func $throws)) + (global (mut ${type}) (${type}.const ${initialValue})) + (func (export "testFunc") (result ${type}) + (try (result ${type}) + (do + (global.set 0 (${type}.const ${resultValue})) + (call $throws) + (global.set 0 (${type}.const ${wrongValue})) + (global.get 0)) + (catch $notThrownExn + (${type}.const ${wrongValue})) + (catch $exn + (global.get 0)))))`, + { m: exports } + ).exports.testFunc(), coercion(resultValue)); + }; + + test("i32", 2, 7, 27, x => x); + test("i64", 2n, 7n, 27n, x => x); + test("f32", 0.3, 0.1, 0.6, Math.fround); + test("f64", 13.37, 0.6437244242412325666666, 4, x => x); +}; + +// Run all tests. + +testSideEffectsOnLocals(); +testGlobals(); diff --git a/js/src/jit-test/tests/wasm/exceptions/stack.js b/js/src/jit-test/tests/wasm/exceptions/stack.js new file mode 100644 index 0000000000..337ecd54b8 --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/stack.js @@ -0,0 +1,28 @@ +let tag = new WebAssembly.Tag({parameters: []}); + +function construct(options) { + return new WebAssembly.Exception(tag, [], options); +} +function noStack(options) { + assertEq(construct(options).stack, undefined, 'no stack'); +} +function hasStack(options) { + assertEq(typeof construct(options).stack === 'string', true, 'has stack'); +} + +// Test valid option constructors +noStack(undefined); +noStack(null); +noStack({}); +noStack({traceStack: false}); +noStack({traceStack: 0}); +hasStack({traceStack: true}); +hasStack({traceStack: 1}); + +// Test invalid option constructors +assertErrorMessage(() => construct('not an object'), TypeError, /cannot be converted/); + +// Test that 'stack' is read-only +let exception = construct({traceStack: true}); +exception.stack = 0; +assertEq(typeof exception.stack === 'string', true, 'is read-only'); diff --git a/js/src/jit-test/tests/wasm/exceptions/throw-to-js.js b/js/src/jit-test/tests/wasm/exceptions/throw-to-js.js new file mode 100644 index 0000000000..6ddc389cc1 --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/throw-to-js.js @@ -0,0 +1,343 @@ +// Tests for throwing exceptions to JS from Wasm. + +function assertWasmThrowsExn(thunk) { + let thrown = false; + + try { + thunk(); + } catch (exn) { + thrown = true; + assertEq(exn instanceof WebAssembly.Exception, true); + } + + assertEq(thrown, true, "missing exception"); +} + +// Test that handler-less trys don't catch anything. +assertWasmThrowsExn(() => + wasmEvalText( + `(module + (type (func (param))) + (tag $exn (type 0)) + (func (export "f") + try (throw $exn) end))` + ).exports.f() +); + +assertWasmThrowsExn(() => + wasmEvalText( + `(module + (type (func (param))) + (tag $exn (type 0)) + (func $g (throw $exn)) + (func (export "f") + try (call $g) end) +)` + ).exports.f() +); + +assertWasmThrowsExn(() => + wasmEvalText( + `(module + (type (func (param))) + (tag $exn (type 0)) + (func (export "f") + try try (throw $exn) end end))` + ).exports.f() +); + +assertWasmThrowsExn(() => + wasmEvalText( + `(module + (tag $exn (param)) + (func (export "f") + try + try + throw $exn + delegate 0 + end))` + ).exports.f() +); + +assertWasmThrowsExn(() => + wasmEvalText( + `(module + (tag $exn (param)) + (func (export "f") + try + try + throw $exn + delegate 1 + end))` + ).exports.f() +); + +assertWasmThrowsExn(() => + wasmEvalText( + `(module + (tag $exn (param)) + (func (export "f") + block + try + throw $exn + delegate 0 + end))` + ).exports.f() +); + +assertWasmThrowsExn(() => + wasmEvalText( + `(module + (tag $exn (param)) + (func (export "f") + loop + try + throw $exn + delegate 0 + end))` + ).exports.f() +); + +assertWasmThrowsExn(() => + wasmEvalText( + `(module + (tag $exn (param)) + (func (export "f") + (i32.const 1) + if + try + throw $exn + delegate 0 + end))` + ).exports.f() +); + +// Test throwing simple empty exceptions to JS. +assertWasmThrowsExn(() => + wasmEvalText( + `(module + (type (func (param))) + (tag $exn (type 0)) + (func (export "f") + (throw $exn)))` + ).exports.f() +); + +// Test that wasm preserves the values of non-object exceptions that pass +// through it back to JS. +assertThrowsValue( + () => + wasmEvalText( + `(module + (tag $exn) + (import "m" "import" (func $import)) + (func (export "f") + try + (call $import) + catch $exn + ;; this block shouldn't be reached + end))`, + { + m: { + import: () => { + throw 42; + }, + }, + } + ).exports.f(), + 42 +); + +// Like previous test, but using a rethrow instruction instead. +assertThrowsValue( + () => + wasmEvalText( + `(module + (import "m" "import" (func $import)) + (func (export "f") + try + (call $import) + catch_all + (rethrow 0) + end))`, + { + m: { + import: () => { + throw 42; + }, + }, + } + ).exports.f(), + 42 +); + +// Test for throwing to JS and then back to Wasm. +{ + var wasmThrower; + let exports = wasmEvalText( + `(module + (type (func (param i32))) + (tag $exn (type 0)) + (import "m" "import" (func $import (result i32))) + (func (export "thrower") + (i32.const 42) + (throw $exn)) + (func (export "catcher") (result i32) + try (result i32) + (call $import) + catch $exn + end))`, + { + m: { + import: () => { + return wasmThrower(); + }, + }, + } + ).exports; + + wasmThrower = exports.thrower; + assertEq(exports.catcher(), 42); +} + +// Tests for checking the tags of exceptions. +{ + let exports = wasmEvalText( + `(module + (type (func (param i32))) + (tag $exn (export "exn") (type 0)) + (func (export "thrower") + (i32.const 42) + (throw $exn)))` + ).exports; + + let imports = { + store: { + throws: () => { + return exports.thrower(); + }, + exn: exports.exn, + }, + }; + + // This passes the exception tag check and the exception is caught. + assertEq( + wasmEvalText( + `(module + (type (func (param i32))) + (import "store" "throws" (func $thrower (result i32))) + (import "store" "exn" (tag $exn (type 0))) + (func (export "catches") (result i32) + try (result i32) + (call $thrower) + catch $exn + (i32.const 15) + (i32.sub) + end))`, + imports + ).exports.catches(), + 27 + ); + + // This fails the exception tag check, despite the local exception having + // a matching signature. + assertWasmThrowsExn(() => + wasmEvalText( + `(module + (type (func (param i32))) + (import "store" "throws" (func $thrower (result i32))) + (tag $exn (type 0)) + (func (export "catchesFail") (result i32) + try (result i32) + (call $thrower) + catch $exn ;; This should not recognise $exn, thus not unpack 42. + end))`, + imports + ).exports.catchesFail() + ); +} + +// Test that JS finally block executes after a Wasm throw. +assertEq( + (() => { + try { + wasmEvalText( + `(module + (type (func (param))) + (tag $exn (type 0)) + (func (export "f") + (throw $exn)))` + ).exports.f(); + } finally { + return true; + } + return false; + })(), + true +); + +// Test that a wasm trap that passes through JS cannot be caught in Wasm. +{ + let throwTrap = wasmEvalText(`(module (func (export "f") unreachable))`) + .exports.f; + let catcher = wasmEvalText( + `(module + (type (func)) + (tag $exn (type 0)) + (import "m" "f" (func $foreign (param) (result))) + (func (export "f") + try + call $foreign + catch $exn + catch_all + end))`, + { + m: { + // JS frame that goes between the two wasm frames and just rethrows. + f: () => { + try { + throwTrap(); + } catch (e) { + throw e; + } + }, + }, + } + ).exports.f; + + assertErrorMessage( + () => catcher(), + WebAssembly.RuntimeError, + "unreachable executed" + ); +} + +// Test delegate throwing out of function. +assertWasmThrowsExn(() => + wasmEvalText( + `(module + (tag $exn (param)) + (func (export "f") (result i32) + try (result i32) + throw $exn + delegate 0))` + ).exports.f() +); + +assertWasmThrowsExn(() => + wasmEvalText( + `(module + (tag $exn (param)) + (func (export "f") (result i32) + try (result i32) + i32.const 0 + if + i32.const 1 + return + else + throw $exn + end + i32.const 0 + delegate 0))` + ).exports.f() +); diff --git a/js/src/jit-test/tests/wasm/exceptions/validation.js b/js/src/jit-test/tests/wasm/exceptions/validation.js new file mode 100644 index 0000000000..98a37042f1 --- /dev/null +++ b/js/src/jit-test/tests/wasm/exceptions/validation.js @@ -0,0 +1,662 @@ + +// Test wasm type validation for exception handling instructions. + +load(libdir + "wasm-binary.js"); + +function wasmValid(mod) { + assertEq(WebAssembly.validate(mod), true); +} + +function wasmInvalid(mod, pattern) { + assertEq(WebAssembly.validate(mod), false); + assertErrorMessage( + () => new WebAssembly.Module(mod), + WebAssembly.CompileError, + pattern + ); +} + +const emptyType = { args: [], ret: VoidCode }; +const i32Type = { args: [I32Code], ret: VoidCode }; +const toi32Type = { args: [], ret: I32Code }; +const i32Toi32Type = { args: [I32Code], ret: I32Code }; +const i32Toi64Type = { args: [I32Code], ret: I64Code }; +const i32i32Toi32Type = { args: [I32Code, I32Code], ret: I32Code }; + +function testValidateDecode() { + // Try blocks must have a block type code. + wasmInvalid( + moduleWithSections([ + sigSection([emptyType]), + declSection([0]), + tagSection([{ type: 0 }]), + bodySection([ + funcBody({ + locals: [], + body: [ + TryCode, + // Missing type code. + I32ConstCode, + 0x01, + CatchCode, + 0x00, + EndCode, + DropCode, + ReturnCode, + ], + }), + ]), + ]), + /bad type/ + ); + + // Catch must have a tag index. + wasmInvalid( + moduleWithSections([ + sigSection([emptyType]), + declSection([0]), + tagSection([{ type: 0 }]), + bodySection([ + funcBody( + { + locals: [], + body: [ + TryCode, + I32Code, + I32ConstCode, + 0x01, + CatchCode, + // Index missing. + ], + }, + (withEndCode = false) + ), + ]), + ]), + /expected tag index/ + ); + + // Rethrow must have a depth argument. + wasmInvalid( + moduleWithSections([ + sigSection([emptyType]), + declSection([0]), + tagSection([{ type: 0 }]), + bodySection([ + funcBody( + { + locals: [], + body: [ + RethrowCode, + // Index missing. + ], + }, + (withEndCode = false) + ), + ]), + ]), + /unable to read rethrow depth/ + ); + + // Delegate must have a depth argument. + wasmInvalid( + moduleWithSections([ + sigSection([emptyType]), + declSection([0]), + tagSection([{ type: 0 }]), + bodySection([ + funcBody( + { + locals: [], + body: [ + TryCode, + I32Code, + I32ConstCode, + 0x01, + DelegateCode, + // Index missing. + ], + }, + (withEndCode = false) + ), + ]), + ]), + /unable to read delegate depth/ + ); +} + +function testValidateThrow() { + valid = `(module + (type (func (param i32))) + (func $exn-zero + i32.const 0 + throw $exn1) + (tag $exn1 (type 0)))`; + + validSimd = `(module + (tag $exn (param v128)) + (func (export "f") (param v128) (result v128) + (try (result v128) + (do (v128.const f64x2 1 2) + (throw $exn)) + (catch $exn))))`; + + invalid0 = `(module + (type (func (param i32))) + (func $exn-zero + throw $exn1) + (tag $exn1 (type 0)))`; + error0 = /popping value from empty stack/; + + invalid1 = `(module + (type (func (param i32))) + (func $exn-zero + i64.const 0 + throw $exn1) + (tag $exn1 (type 0)))`; + error1 = /expression has type i64 but expected i32/; + + invalid2 = `(module + (type (func (param i32))) + (func $exn-zero + i32.const 0 + throw 1) + (tag $exn1 (type 0)))`; + error2 = /tag index out of range/; + + wasmValidateText(valid); + if (wasmSimdEnabled()) { + wasmValidateText(validSimd); + } + wasmFailValidateText(invalid0, error0); + wasmFailValidateText(invalid1, error1); + wasmFailValidateText(invalid2, error2); +} + +function testValidateTryCatch() { + function mod_with(fbody) { + return moduleWithSections([ + sigSection([emptyType, i32Type, i32i32Toi32Type]), + declSection([0]), + tagSection([{ type: 0 }, { type: 1 }]), + bodySection([ + funcBody({ + locals: [], + body: fbody, + }), + ]), + ]); + } + + const body1 = [ + // try (result i32) + TryCode, + I32Code, + // (i32.const 1) + I32ConstCode, + varU32(1), + // catch 1 + CatchCode, + varU32(1), + ]; + + const valid1 = mod_with(body1.concat([EndCode, DropCode, ReturnCode])); + const invalid1 = mod_with( + body1.concat([I32ConstCode, varU32(2), EndCode, DropCode, ReturnCode]) + ); + + const valid2 = mod_with([ + // (i32.const 0) (i32.const 0) + I32ConstCode, + varU32(0), + I32ConstCode, + varU32(0), + // try (param i32 i32) (result i32) drop drop (i32.const 1) + TryCode, + varS32(2), + DropCode, + DropCode, + I32ConstCode, + varU32(1), + // catch 0 (i32.const 2) end drop return + CatchCode, + varU32(0), + I32ConstCode, + varU32(2), + EndCode, + DropCode, + ReturnCode, + ]); + + wasmValid(valid1); + wasmInvalid(invalid1, /unused values not explicitly dropped/); + wasmValid(valid2); + + // Test handler-less try blocks. + wasmValidateText( + `(module (func try end))` + ); + + wasmValidateText( + `(module (func (result i32) try (result i32) (i32.const 1) end))` + ); + + wasmValidateText( + `(module + (func (result i32) + try (result i32) (i32.const 1) (br 0) end))` + ); + + wasmFailValidateText( + `(module + (func try (result i32) end))`, + /popping value from empty stack/ + ); +} + +function testValidateCatch() { + wasmInvalid( + moduleWithSections([ + sigSection([emptyType]), + declSection([0]), + bodySection([ + funcBody({ + locals: [], + body: [TryCode, VoidCode, CatchCode, varU32(0), EndCode], + }), + ]), + ]), + /tag index out of range/ + ); +} + +function testValidateCatchAll() { + wasmValidateText( + `(module + (tag $exn) + (func try catch $exn catch_all end))` + ); + + wasmValidateText( + `(module + (func (result i32) + try (result i32) + (i32.const 0) + catch_all + (i32.const 1) + end))` + ); + + wasmFailValidateText( + `(module + (tag $exn) + (func try catch_all catch 0 end))`, + /catch cannot follow a catch_all/ + ); + + wasmFailValidateText( + `(module + (tag $exn) + (func try (result i32) (i32.const 1) catch_all end drop))`, + /popping value from empty stack/ + ); + + wasmFailValidateText( + `(module + (tag $exn (param i32)) + (func try catch $exn drop catch_all drop end))`, + /popping value from empty stack/ + ); + + // We can't distinguish `else` and `catch_all` in error messages since they + // share the binary opcode. + wasmFailValidateText( + `(module + (tag $exn) + (func try catch_all catch_all end))`, + /catch_all can only be used within a try/ + ); + + wasmFailValidateText( + `(module + (tag $exn) + (func catch_all))`, + /catch_all can only be used within a try/ + ); +} + +function testValidateExnPayload() { + valid0 = moduleWithSections([ + sigSection([i32Type, i32Toi32Type]), + declSection([1]), + // (tag $exn (param i32)) + tagSection([{ type: 0 }]), + bodySection([ + // (func (param i32) (result i32) ... + funcBody({ + locals: [], + body: [ + // try (result i32) (local.get 0) (throw $exn) (i32.const 1) + TryCode, + I32Code, + LocalGetCode, + varU32(0), + ThrowCode, + varU32(0), + I32ConstCode, + varU32(1), + // catch $exn (i32.const 1) (i32.add) end + CatchCode, + varU32(0), + I32ConstCode, + varU32(1), + I32AddCode, + EndCode, + ], + }), + ]), + ]); + + // This is to ensure the following sentence from the spec overview holds: + // > "the operand stack is popped back to the size the operand stack had + // > when the try block was entered" + valid1 = moduleWithSections([ + sigSection([i32Type, toi32Type]), + declSection([1]), + // (tag $exn (param i32)) + tagSection([{ type: 0 }]), + bodySection([ + // (func (result i32) ... + funcBody({ + locals: [], + body: [ + // try (result i32) (i32.const 0) (i32.const 1) (throw $exn) drop + TryCode, + I32Code, + I32ConstCode, + varU32(0), + I32ConstCode, + varU32(1), + ThrowCode, + varU32(0), + DropCode, + // catch $exn drop (i32.const 2) end + CatchCode, + varU32(0), + DropCode, + I32ConstCode, + varU32(2), + EndCode, + ], + }), + ]), + ]); + + invalid0 = moduleWithSections([ + sigSection([i32Type, i32Toi64Type]), + declSection([1]), + // (tag $exn (param i32)) + tagSection([{ type: 0 }]), + bodySection([ + // (func (param i32) (result i64) ... + funcBody({ + locals: [], + body: [ + // try (result i64) (local.get 0) (throw $exn) (i64.const 0) + TryCode, + I64Code, + LocalGetCode, + varU32(0), + ThrowCode, + varU32(0), + I64ConstCode, + varU32(0), + // catch $exn end + CatchCode, + varU32(0), + EndCode, + ], + }), + ]), + ]); + + invalid1 = moduleWithSections([ + // (type (func)) + sigSection([emptyType]), + declSection([0]), + // (tag $exn (type 0)) + tagSection([{ type: 0 }]), + bodySection([ + // (func ... + funcBody({ + locals: [], + body: [ + // try catch 1 end + TryCode, + VoidCode, + CatchCode, + varU32(1), + EndCode, + ], + }), + ]), + ]); + + wasmValid(valid0); + wasmValid(valid1); + wasmInvalid(invalid0, /has type i32 but expected i64/); + wasmInvalid(invalid1, /tag index out of range/); +} + +function testValidateRethrow() { + wasmValidateText( + `(module + (tag $exn (param)) + (func + try + nop + catch $exn + rethrow 0 + end))` + ); + + wasmValidateText( + `(module + (tag $exn (param)) + (func + try + nop + catch_all + rethrow 0 + end))` + ); + + wasmValidateText( + `(module + (func (result i32) + try (result i32) + (i32.const 1) + catch_all + rethrow 0 + end))` + ); + + wasmValidateText( + `(module + (tag $exn (param)) + (func + try + nop + catch $exn + block + try + catch $exn + rethrow 0 + end + end + end))` + ); + + wasmValidateText( + `(module + (tag $exn (param)) + (func + try + nop + catch $exn + block + try + catch $exn + rethrow 2 + end + end + end))` + ); + + wasmFailValidateText( + `(module + (tag $exn (param)) + (func + try + nop + catch $exn + block + try + catch $exn + rethrow 1 + end + end + end))`, + /rethrow target was not a catch block/ + ); + + wasmFailValidateText( + `(module (func rethrow 0))`, + /rethrow target was not a catch block/ + ); + + wasmFailValidateText( + `(module (func try rethrow 0 end))`, + /rethrow target was not a catch block/ + ); + + wasmFailValidateText( + `(module (func try rethrow 0 catch_all end))`, + /rethrow target was not a catch block/ + ); + + wasmFailValidateText( + `(module + (tag $exn (param)) + (func + try + nop + catch $exn + block + try + catch $exn + rethrow 4 + end + end + end))`, + /rethrow depth exceeds current nesting level/ + ); +} + +function testValidateDelegate() { + wasmValidateText( + `(module + (tag $exn (param)) + (func + try + try + throw $exn + delegate 0 + catch $exn + end))` + ); + + wasmValidateText( + `(module + (tag $exn (param)) + (func + try + try + throw $exn + delegate 1 + catch $exn + end))` + ); + + wasmValidateText( + `(module + (tag $exn (param)) + (func + block + try + throw $exn + delegate 0 + end))` + ); + + wasmValidateText( + `(module + (tag $exn (param)) + (func + try + catch $exn + try + throw $exn + delegate 0 + end))` + ); + + wasmFailValidateText( + `(module + (tag $exn (param)) + (func (result i32) + try + throw $exn + delegate 0 + (i64.const 0) + end))`, + /type mismatch: expression has type i64 but expected i32/ + ); + + wasmFailValidateText( + `(module + (tag $exn (param)) + (func + try (result i32) + (i64.const 0) + delegate 0))`, + /type mismatch: expression has type i64 but expected i32/ + ); + + wasmFailValidateText( + `(module + (tag $exn (param)) + (func + try + try + throw $exn + delegate 2 + catch $exn + end))`, + /delegate depth exceeds current nesting level/ + ); + + wasmFailValidateText( + `(module (func delegate 0))`, + /delegate can only be used within a try/ + ); +} + +testValidateDecode(); +testValidateThrow(); +testValidateTryCatch(); +testValidateCatch(); +testValidateCatchAll(); +testValidateExnPayload(); +testValidateRethrow(); +testValidateDelegate(); |