diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-12 05:43:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-06-12 05:43:14 +0000 |
commit | 8dd16259287f58f9273002717ec4d27e97127719 (patch) | |
tree | 3863e62a53829a84037444beab3abd4ed9dfc7d0 /js/src/jit-test/tests/wasm/js-promise-integration | |
parent | Releasing progress-linux version 126.0.1-1~progress7.99u1. (diff) | |
download | firefox-8dd16259287f58f9273002717ec4d27e97127719.tar.xz firefox-8dd16259287f58f9273002717ec4d27e97127719.zip |
Merging upstream version 127.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/src/jit-test/tests/wasm/js-promise-integration')
8 files changed, 1093 insertions, 0 deletions
diff --git a/js/src/jit-test/tests/wasm/js-promise-integration/basic.js b/js/src/jit-test/tests/wasm/js-promise-integration/basic.js new file mode 100644 index 0000000000..8603886b07 --- /dev/null +++ b/js/src/jit-test/tests/wasm/js-promise-integration/basic.js @@ -0,0 +1,147 @@ +// Example from the proposal. + +var compute_delta = async (i) => Promise.resolve(i / 100 || 1); + +var suspending_compute_delta = new WebAssembly.Function( + { parameters: ['externref', 'i32'], results: ['f64'] }, + compute_delta, + { suspending: "first" } +); + +var ins = wasmEvalText(`(module + (import "js" "init_state" (func $init_state (result f64))) + (import "js" "compute_delta" + (func $compute_delta_import (param externref) (param i32) (result f64))) + + (global $suspender (mut externref) (ref.null extern)) + (global $state (mut f64) (f64.const nan)) + (func $init (global.set $state (call $init_state))) + (start $init) + + (func $compute_delta (param i32) (result f64) + (local $suspender_copy externref) + (;return (f64.const 0.3);) + (;unreachable;) + (global.get $suspender) + (local.tee $suspender_copy) + (local.get 0) + (call $compute_delta_import) + (local.get $suspender_copy) + (global.set $suspender) + (return) + ) + (func $get_state (export "get_state") (result f64) (global.get $state)) + (func $update_state (param i32) (result f64) + (global.set $state (f64.add + (global.get $state) (call $compute_delta (local.get 0)))) + (global.get $state) + ) + + (func (export "update_state_export") + (param $suspender externref) (param i32) (result f64) + (local.get $suspender) + (global.set $suspender) + (local.get 1) + (call $update_state) + (return) + ) +)`, { + js: { + init_state() { return 0; }, + compute_delta: suspending_compute_delta, + }, +}); + +var update_state = new WebAssembly.Function( + { parameters: ['i32'], results: ['externref'] }, + ins.exports.update_state_export, + { promising: "first" } +); + +var res = update_state(4); +var tasks = res.then((r) => { + print(r); + assertEq(ins.exports.get_state(), .04); +}); + +assertEq(ins.exports.get_state(), 0); + +// Also test with exceptions/traps. + +async function test(c) { + var compute_delta = (i) => Promise.resolve(i/100 || 1); + if (c == 1) compute_delta = async (i) => {throw "ff"}; + if (c == 2) compute_delta = () => {throw "ff";} + + var suspending_compute_delta = new WebAssembly.Function( + {parameters:['externref', 'i32'], results:['f64']}, + compute_delta, + {suspending:"first"} + ); + + var ins = wasmEvalText(`(module + (import "js" "init_state" (func $init_state (result f64))) + (import "js" "compute_delta" + (func $compute_delta_import (param externref) (param i32) (result f64))) + + (global $suspender (mut externref) (ref.null extern)) + (global $state (mut f64) (f64.const nan)) + (func $init (global.set $state (call $init_state))) + (start $init) + + (func $compute_delta (param i32) (result f64) + (local $suspender_copy externref) + (;return (f64.const 0.3);) + ${c == 3 ? "(unreachable)" : ""} + (global.get $suspender) + (local.tee $suspender_copy) + (local.get 0) + (call $compute_delta_import) + ${c == 4 ? "(unreachable)" : ""} + (local.get $suspender_copy) + (global.set $suspender) + (return) + ) + (func $get_state (export "get_state") (result f64) (global.get $state)) + (func $update_state (param i32) (result f64) + (global.set $state (f64.add + (global.get $state) (call $compute_delta (local.get 0)))) + (global.get $state) + ) + + (func (export "update_state_export") + (param $suspender externref) (param i32) (result f64) + (local.get $suspender) + (global.set $suspender) + (local.get 1) + (call $update_state) + (return) + ) + )`, { + js: { + init_state() { return 0; }, + compute_delta: suspending_compute_delta, + }, + }); + + var update_state = new WebAssembly.Function( + {parameters:['i32'], results:['externref']}, + ins.exports.update_state_export, + {promising : "first"} + ); + + var res = update_state(4); + var p = res.then((r) => { + assertEq(c, 0); + assertEq(ins.exports.get_state(), .04); + }).catch(_ => { + assertEq(c > 0, true); + }); + + assertEq(ins.exports.get_state(), 0); + await p; +} + +for (let c = 0; c < 5; c++) { + tasks = tasks.then(() => test(c)); +} diff --git a/js/src/jit-test/tests/wasm/js-promise-integration/basic2.js b/js/src/jit-test/tests/wasm/js-promise-integration/basic2.js new file mode 100644 index 0000000000..d381951b2c --- /dev/null +++ b/js/src/jit-test/tests/wasm/js-promise-integration/basic2.js @@ -0,0 +1,97 @@ +// New API experiments. +// Example from the proposal. + +var compute_delta = async (i) => Promise.resolve(i / 100 || 1); + +var suspending_compute_delta = new WebAssembly.Suspending( + compute_delta +); +var ins = wasmEvalText(`(module + (import "js" "init_state" (func $init_state (result f64))) + (import "js" "compute_delta" + (func $compute_delta (param i32) (result f64))) + + (global $suspender (mut externref) (ref.null extern)) + (global $state (mut f64) (f64.const nan)) + (func $init (global.set $state (call $init_state))) + (start $init) + + (func $get_state (export "get_state") (result f64) (global.get $state)) + (func (export "update_state_export") (param i32) (result f64) + (global.set $state (f64.add + (global.get $state) (call $compute_delta (local.get 0)))) + (global.get $state) + ) +)`, { + js: { + init_state() { return 0; }, + compute_delta: suspending_compute_delta, + }, +}); + +var update_state = WebAssembly.promising( + ins.exports.update_state_export +); + +var res = update_state(4); +var tasks = res.then((r) => { + print(r); + assertEq(ins.exports.get_state(), .04); +}); + +assertEq(ins.exports.get_state(), 0); + +// Also test with exceptions/traps. + +async function test(c) { + var compute_delta = (i) => Promise.resolve(i/100 || 1); + if (c == 1) compute_delta = async (i) => {throw "ff"}; + if (c == 2) compute_delta = () => {throw "ff";} + + var suspending_compute_delta = new WebAssembly.Suspending( + compute_delta + ); + var ins = wasmEvalText(`(module + (import "js" "init_state" (func $init_state (result f64))) + (import "js" "compute_delta" + (func $compute_delta (param i32) (result f64))) + + (global $suspender (mut externref) (ref.null extern)) + (global $state (mut f64) (f64.const nan)) + (func $init (global.set $state (call $init_state))) + (start $init) + + (func $get_state (export "get_state") (result f64) (global.get $state)) + (func (export "update_state_export") (param i32) (result f64) + ${c == 3 ? "(unreachable)" : ""} + (global.set $state (f64.add + (global.get $state) (call $compute_delta (local.get 0)))) + ${c == 4 ? "(unreachable)" : ""} + (global.get $state) + ) + )`, { + js: { + init_state() { return 0; }, + compute_delta: suspending_compute_delta, + }, + }); + + var update_state = WebAssembly.promising( + ins.exports.update_state_export +); + + var res = update_state(4); + var p = res.then((r) => { + assertEq(c, 0); + assertEq(ins.exports.get_state(), .04); + }).catch(_ => { + assertEq(c > 0, true); + }); + + assertEq(ins.exports.get_state(), 0); + await p; +} + +for (let c = 0; c < 5; c++) { + tasks = tasks.then(() => test(c)); +} diff --git a/js/src/jit-test/tests/wasm/js-promise-integration/directives.txt b/js/src/jit-test/tests/wasm/js-promise-integration/directives.txt new file mode 100644 index 0000000000..b15152352a --- /dev/null +++ b/js/src/jit-test/tests/wasm/js-promise-integration/directives.txt @@ -0,0 +1 @@ +|jit-test| include:wasm.js; --setpref=wasm_js_promise_integration=true; skip-if: !wasmJSPromiseIntegrationEnabled() diff --git a/js/src/jit-test/tests/wasm/js-promise-integration/gc-2.js b/js/src/jit-test/tests/wasm/js-promise-integration/gc-2.js new file mode 100644 index 0000000000..d17539807e --- /dev/null +++ b/js/src/jit-test/tests/wasm/js-promise-integration/gc-2.js @@ -0,0 +1,69 @@ +// Example from the proposal. + +gczeal(2,5); + +var compute_delta = (i) => Promise.resolve(i / 100 || 1); + +var suspending_compute_delta = new WebAssembly.Function( + { parameters: ['externref', 'i32'], results: ['f64'] }, + compute_delta, + { suspending: "first" } +); + +var ins = wasmEvalText(`(module + (import "js" "init_state" (func $init_state (result f64))) + (import "js" "compute_delta" + (func $compute_delta_import (param externref) (param i32) (result f64))) + + (global $suspender (mut externref) (ref.null extern)) + (global $state (mut f64) (f64.const nan)) + (func $init (global.set $state (call $init_state))) + (start $init) + + (func $compute_delta (param i32) (result f64) + (local $suspender_copy externref) + (;return (f64.const 0.3);) + (;unreachable;) + (global.get $suspender) + (local.tee $suspender_copy) + (local.get 0) + (call $compute_delta_import) + (local.get $suspender_copy) + (global.set $suspender) + (return) + ) + (func $get_state (export "get_state") (result f64) (global.get $state)) + (func $update_state (param i32) (result f64) + (global.set $state (f64.add + (global.get $state) (call $compute_delta (local.get 0)))) + (global.get $state) + ) + + (func (export "update_state_export") + (param $suspender externref) (param i32) (result f64) + (local.get $suspender) + (global.set $suspender) + (local.get 1) + (call $update_state) + (return) + ) +)`, { + js: { + init_state() { return 0; }, + compute_delta: suspending_compute_delta, + }, +}); + +var update_state = new WebAssembly.Function( + { parameters: ['i32'], results: ['externref'] }, + ins.exports.update_state_export, + { promising: "first" } +); + +var res = update_state(4); +var tasks = res.then((r) => { + print(r); + assertEq(ins.exports.get_state(), .04); +}); + +assertEq(ins.exports.get_state(), 0); diff --git a/js/src/jit-test/tests/wasm/js-promise-integration/gc.js b/js/src/jit-test/tests/wasm/js-promise-integration/gc.js new file mode 100644 index 0000000000..c688b99cb5 --- /dev/null +++ b/js/src/jit-test/tests/wasm/js-promise-integration/gc.js @@ -0,0 +1,67 @@ +// Test if we can trace roots on the alternative stack. + +let i = 0; +function js_import() { + return Promise.resolve({i: ++i}); +}; +let wasm_js_import = new WebAssembly.Function( + {parameters: ['externref'], results: ['externref']}, + js_import, + {suspending: 'first'}); + +let wasm_gc_import = new WebAssembly.Function( + {parameters: ['externref'], results: []}, + async () => { gc(); }, + {suspending: 'first'}); + +var ins = wasmEvalText(`(module + (import "m" "import" + (func (param externref) (result externref))) + (import "m" "gc" (func (param externref))) + (import "m" "conv" + (func (param externref) (result i32))) + + (global (export "g") (mut i32) (i32.const 0)) + + (func (export "test") (param externref) + (local i32) + i32.const 5 + local.set 1 + loop + local.get 0 + call 0 + local.get 0 + call 1 + call 2 + global.get 0 + i32.add + global.set 0 + local.get 1 + i32.const 1 + i32.sub + local.tee 1 + br_if 0 + end + ) + +)`, { + m: { + import: wasm_js_import, + gc: wasm_gc_import, + conv: ({i}) => i, + }, +}); + + +let wrapped_export = new WebAssembly.Function( + {parameters:[], results:['externref']}, + ins.exports.test, + {promising : "first"} +); + +let export_promise = wrapped_export(); +assertEq(0, ins.exports.g.value); +assertEq(true, export_promise instanceof Promise); +export_promise.then(() => + assertEq(15, ins.exports.g.value) +); diff --git a/js/src/jit-test/tests/wasm/js-promise-integration/js-promise-integration.new.js b/js/src/jit-test/tests/wasm/js-promise-integration/js-promise-integration.new.js new file mode 100644 index 0000000000..d45d47b072 --- /dev/null +++ b/js/src/jit-test/tests/wasm/js-promise-integration/js-promise-integration.new.js @@ -0,0 +1,270 @@ +// New version of JS promise integration API +// Modified https://github.com/WebAssembly/js-promise-integration/tree/main/test/js-api/js-promise-integration + +var tests = Promise.resolve(); +function test(fn, n) { + + tests = tests.then(() => { + let t = {res: null}; + print("# " + n); + fn(t); + return t.res; + }); +} +function promise_test(fn, n) { + tests = tests.then(() => { + print("# " + n); + return fn(); + }); +} +function assert_true(f) { assertEq(f, true); } +function assert_equals(a, b) { assertEq(a, b); } +function assert_array_equals(a, b) { + assert_equals(a.length, a.length); + for (let i = 0; i < a.length; i++) { + assert_equals(a[i], b[i]); + } +} +function assert_throws(ex, fn) { + try { + fn(); assertEq(false, true); + } catch(e) { + assertEq(e instanceof ex, true); + } +} +function promise_rejects(t, obj, p) { + t.res = p.then(() => { + assertEq(true, false); + }, (e) => { + assertEq(e instanceof obj.constructor, true); + }); +} + +function ToPromising(wasm_export) { + let sig = wasm_export.type(); + assert_true(sig.parameters.length > 0); + assert_equals('externref', sig.parameters[0]); + let wrapper_sig = { + parameters: sig.parameters.slice(1), + results: ['externref'] + }; + return new WebAssembly.Function( + wrapper_sig, wasm_export, {promising: 'first'}); +} + +promise_test(async () => { + let js_import = new WebAssembly.Suspending( + () => Promise.resolve(42) + ); + let instance = wasmEvalText(`(module + (import "m" "import" (func $import (result i32))) + (func (export "test") (result i32) + call $import + ) + )`, {m: {import: js_import}}); + let wrapped_export = WebAssembly.promising(instance.exports.test); + let export_promise = wrapped_export(); + assert_true(export_promise instanceof Promise); + assert_equals(42, await export_promise); +}, "Suspend once"); + +promise_test(async () => { + let i = 0; + function js_import() { + return Promise.resolve(++i); + }; + let wasm_js_import = new WebAssembly.Suspending(js_import); + // void test() { + // for (i = 0; i < 5; ++i) { + // g = g + await import(); + // } + // } + let instance = wasmEvalText(`(module + (import "m" "import" (func $import (param externref) (result i32))) + (global (export "g") (mut i32) (i32.const 0)) + (func (export "test") (param externref) + (local i32) + i32.const 5 + local.set 1 + loop + local.get 0 + call $import + global.get 0 + i32.add + global.set 0 + local.get 1 + i32.const 1 + i32.sub + local.tee 1 + br_if 0 + end + ) + )`, {m: {import: wasm_js_import}}); + let wrapped_export = WebAssembly.promising(instance.exports.test); + let export_promise = wrapped_export(); + assert_equals(0, instance.exports.g.value); + assert_true(export_promise instanceof Promise); + await export_promise; + assert_equals(15, instance.exports.g.value); +}, "Suspend/resume in a loop"); + +promise_test(async () => { + function js_import() { + return 42 + }; + let wasm_js_import = new WebAssembly.Suspending(js_import); + let instance = wasmEvalText(`(module + (import "m" "import" (func $import (param externref) (result i32))) + (global (export "g") (mut i32) (i32.const 0)) + (func (export "test") (param externref) (result i32) + local.get 0 + call $import + global.set 0 + global.get 0 + ) + )`, {m: {import: wasm_js_import}}); + let wrapped_export = WebAssembly.promising(instance.exports.test); + await wrapped_export(); + assert_equals(42, instance.exports.g.value); +}, "Do not suspend if the import's return value is not a Promise"); + +test(t => { + let tag = new WebAssembly.Tag({parameters: []}); + function js_import() { + return Promise.resolve(); + }; + let wasm_js_import = new WebAssembly.Suspending(js_import); + function js_throw() { + throw new Error(); + } + + let instance = wasmEvalText(`(module + (import "m" "import" (func $import (param externref) (result i32))) + (import "m" "js_throw" (func $js_throw)) + (func (export "test") (param externref) (result i32) + local.get 0 + call $import + call $js_throw + ) + )`, {m: {import: wasm_js_import, js_throw}}); + let wrapped_export = WebAssembly.promising(instance.exports.test); + let export_promise = wrapped_export(); + assert_true(export_promise instanceof Promise); + promise_rejects(t, new Error(), export_promise); +}, "Throw after the first suspension"); + +// TODO: Use wasm exception handling to check that the exception can be caught in wasm. + +test(t => { + let tag = new WebAssembly.Tag({parameters: ['i32']}); + function js_import() { + return Promise.reject(new Error()); + }; + let wasm_js_import = new WebAssembly.Suspending(js_import); + + let instance = wasmEvalText(`(module + (import "m" "import" (func $import (param externref) (result i32))) + (func (export "test") (param externref) (result i32) + local.get 0 + call $import + ) + )`, {m: {import: wasm_js_import, tag: tag}}); + let wrapped_export = WebAssembly.promising(instance.exports.test); + let export_promise = wrapped_export(); + assert_true(export_promise instanceof Promise); + promise_rejects(t, new Error(), export_promise); +}, "Rejecting promise"); + +async function TestNestedSuspenders(suspend) { + // Nest two suspenders. The call chain looks like: + // outer (wasm) -> outer (js) -> inner (wasm) -> inner (js) + // If 'suspend' is true, the inner JS function returns a Promise, which + // suspends the inner wasm function, which returns a Promise, which suspends + // the outer wasm function, which returns a Promise. The inner Promise + // resolves first, which resumes the inner continuation. Then the outer + // promise resolves which resumes the outer continuation. + // If 'suspend' is false, the inner and outer JS functions return a regular + // value and no computation is suspended. + + let inner = new WebAssembly.Suspending( + () => suspend ? Promise.resolve(42) : 43, + ); + + let export_inner; + let outer = new WebAssembly.Suspending( + () => suspend ? export_inner() : 42, + ); + + let instance = wasmEvalText(`(module + (import "m" "inner" (func $inner (param externref) (result i32))) + (import "m" "outer" (func $outer (param externref) (result i32))) + (func (export "outer") (param externref) (result i32) + local.get 0 + call $outer + ) + (func (export "inner") (param externref) (result i32) + local.get 0 + call $inner + ) + )`, {m: {inner, outer}}); + export_inner = WebAssembly.promising(instance.exports.inner); + let export_outer = WebAssembly.promising(instance.exports.outer); + let result = export_outer(); + assert_true(result instanceof Promise); + assert_equals(42, await result); +} + +promise_test(async () => { + return TestNestedSuspenders(true); +}, "Test nested suspenders with suspension"); + +promise_test(async () => { + return TestNestedSuspenders(false); +}, "Test nested suspenders with no suspension"); + + +test(t => { + let instance = wasmEvalText(`(module + (func (export "test") (result i32) + call 0 + ) + )`); + let wrapper = WebAssembly.promising(instance.exports.test); + promise_rejects(t, new InternalError(), wrapper()); +}, "Stack overflow"); + +// TODO: Test suspension with funcref. + +test(t => { + // The call stack of this test looks like: + // export1 -> import1 -> export2 -> import2 + // Where export1 is "promising" and import2 is "suspending". Returning a + // promise from import2 should trap because of the JS import in the middle. + let instance; + function import1() { + // import1 -> export2 (unwrapped) + instance.exports.export2(); + } + function import2() { + return Promise.resolve(0); + } + import2 = new WebAssembly.Suspending(import2); + instance = wasmEvalText(`(module + (import "m" "import1" (func $import1 (result i32))) + (import "m" "import2" (func $import2 (result i32))) + (func (export "export1") (result i32) + ;; export1 -> import1 (unwrapped) + call $import1 + ) + (func (export "export2") (result i32) + ;; export2 -> import2 (suspending) + call $import2 + ) + )`, + {'m': {'import1': import1, 'import2': import2}}); + // export1 (promising) + let wrapper = WebAssembly.promising(instance.exports.export1); + promise_rejects(t, new WebAssembly.RuntimeError(), wrapper()); +}, "Test that trying to suspend JS frames traps"); + +tests.then(() => print('Done')); diff --git a/js/src/jit-test/tests/wasm/js-promise-integration/js-promise-integration.old.js b/js/src/jit-test/tests/wasm/js-promise-integration/js-promise-integration.old.js new file mode 100644 index 0000000000..cc743eeaaa --- /dev/null +++ b/js/src/jit-test/tests/wasm/js-promise-integration/js-promise-integration.old.js @@ -0,0 +1,406 @@ +// Old version of JS promise integration API +// Modified https://github.com/WebAssembly/js-promise-integration/tree/main/test/js-api/js-promise-integration + +var tests = Promise.resolve(); +function test(fn, n) { + + tests = tests.then(() => { + let t = {res: null}; + print("# " + n); + fn(t); + return t.res; + }); +} +function promise_test(fn, n) { + tests = tests.then(() => { + print("# " + n); + return fn(); + }); +} +function assert_true(f) { assertEq(f, true); } +function assert_equals(a, b) { assertEq(a, b); } +function assert_array_equals(a, b) { + assert_equals(a.length, a.length); + for (let i = 0; i < a.length; i++) { + assert_equals(a[i], b[i]); + } +} +function assert_throws(ex, fn) { + try { + fn(); assertEq(false, true); + } catch(e) { + assertEq(e instanceof ex, true); + } +} +function promise_rejects(t, obj, p) { + t.res = p.then(() => { + assertEq(true, false); + }, (e) => { + assertEq(e instanceof obj.constructor, true); + }); +} + +function ToPromising(wasm_export) { + let sig = wasm_export.type(); + assert_true(sig.parameters.length > 0); + assert_equals('externref', sig.parameters[0]); + let wrapper_sig = { + parameters: sig.parameters.slice(1), + results: ['externref'] + }; + return new WebAssembly.Function( + wrapper_sig, wasm_export, {promising: 'first'}); +} + +test(() => { + function js_import(i) {} + + let import_wrapper = new WebAssembly.Function( + {parameters: ['externref', 'i32'], results: []}, + js_import, + {suspending: 'first'}); + let instance = wasmEvalText(`(module + (import "m" "import" (func (param externref i32))) + (func (export "export") (param externref i32) (result i32) + local.get 1 + ) + (func (export "void_export") (param externref)) + )`, {'m': {'import': import_wrapper}}); + let export_wrapper = ToPromising(instance.exports.export); + + // Bad flag value. + assert_throws(TypeError, () => new WebAssembly.Function( + {parameters: ['externref', 'i32'], results: []}, + js_import, + {suspending: 'foo'})); + + assert_throws(TypeError, () => new WebAssembly.Function( + {parameters: ['i32'], results: ['externref']}, + instance.exports.export, + {promising: 'foo'})); + + // Signature mismatch. + assert_throws(Error /*TypeError*/, () => new WebAssembly.Function( + {parameters: ['externref'], results: []}, + new WebAssembly.Function( + {parameters: [], results: ['i32']}, js_import), + {suspending: 'first'})); + + assert_throws(TypeError, () => new WebAssembly.Function( + {parameters: ['externref', 'i32'], results: ['i32']}, + instance.exports.export, + {promising: 'first'})); + + // Check the wrapper signatures. + // let export_sig = export_wrapper.type(); + // assert_array_equals(['i32'], export_sig.parameters); + // assert_array_equals(['externref'], export_sig.results); + + let import_sig = import_wrapper.type(); + assert_array_equals(['externref', 'i32'], import_sig.parameters); + assert_array_equals([], import_sig.results); + // let void_export_wrapper = ToPromising(instance.exports.void_export); + // let void_export_sig = void_export_wrapper.type(); + // assert_array_equals([], void_export_sig.parameters); + // assert_array_equals(['externref'], void_export_sig.results); +}, "Test import and export type checking"); + +promise_test(async () => { + let js_import = new WebAssembly.Function( + {parameters: ['externref'], results: ['i32']}, + () => Promise.resolve(42), + {suspending: 'first'}); + let instance = wasmEvalText(`(module + (import "m" "import" (func $import (param externref) (result i32))) + (func (export "test") (param externref) (result i32) + local.get 0 + call $import + ) + )`, {m: {import: js_import}}); + let wrapped_export = ToPromising(instance.exports.test); + let export_promise = wrapped_export(); + assert_true(export_promise instanceof Promise); + assert_equals(42, await export_promise); +}, "Suspend once"); + +promise_test(async () => { + let i = 0; + function js_import() { + return Promise.resolve(++i); + }; + let wasm_js_import = new WebAssembly.Function( + {parameters: ['externref'], results: ['i32']}, + js_import, + {suspending: 'first'}); + // void test() { + // for (i = 0; i < 5; ++i) { + // g = g + await import(); + // } + // } + let instance = wasmEvalText(`(module + (import "m" "import" (func $import (param externref) (result i32))) + (global (export "g") (mut i32) (i32.const 0)) + (func (export "test") (param externref) + (local i32) + i32.const 5 + local.set 1 + loop + local.get 0 + call $import + global.get 0 + i32.add + global.set 0 + local.get 1 + i32.const 1 + i32.sub + local.tee 1 + br_if 0 + end + ) + )`, {m: {import: wasm_js_import}}); + let wrapped_export = ToPromising(instance.exports.test); + let export_promise = wrapped_export(); + assert_equals(0, instance.exports.g.value); + assert_true(export_promise instanceof Promise); + await export_promise; + assert_equals(15, instance.exports.g.value); +}, "Suspend/resume in a loop"); + +promise_test(async () => { + function js_import() { + return 42 + }; + let wasm_js_import = new WebAssembly.Function( + {parameters: ['externref'], results: ['i32']}, + js_import, + {suspending: 'first'}); + let instance = wasmEvalText(`(module + (import "m" "import" (func $import (param externref) (result i32))) + (global (export "g") (mut i32) (i32.const 0)) + (func (export "test") (param externref) (result i32) + local.get 0 + call $import + global.set 0 + global.get 0 + ) + )`, {m: {import: wasm_js_import}}); + let wrapped_export = ToPromising(instance.exports.test); + await wrapped_export(); + assert_equals(42, instance.exports.g.value); +}, "Do not suspend if the import's return value is not a Promise"); + +test(t => { + let tag = new WebAssembly.Tag({parameters: []}); + function js_import() { + return Promise.resolve(); + }; + let wasm_js_import = new WebAssembly.Function( + {parameters: ['externref'], results: ['i32']}, + js_import, + {suspending: 'first'}); + function js_throw() { + throw new Error(); + } + + let instance = wasmEvalText(`(module + (import "m" "import" (func $import (param externref) (result i32))) + (import "m" "js_throw" (func $js_throw)) + (func (export "test") (param externref) (result i32) + local.get 0 + call $import + call $js_throw + ) + )`, {m: {import: wasm_js_import, js_throw}}); + let wrapped_export = ToPromising(instance.exports.test); + let export_promise = wrapped_export(); + assert_true(export_promise instanceof Promise); + promise_rejects(t, new Error(), export_promise); +}, "Throw after the first suspension"); + +// TODO: Use wasm exception handling to check that the exception can be caught in wasm. + +test(t => { + let tag = new WebAssembly.Tag({parameters: ['i32']}); + function js_import() { + return Promise.reject(new Error()); + }; + let wasm_js_import = new WebAssembly.Function( + {parameters: ['externref'], results: ['i32']}, + js_import, + {suspending: 'first'}); + + let instance = wasmEvalText(`(module + (import "m" "import" (func $import (param externref) (result i32))) + (func (export "test") (param externref) (result i32) + local.get 0 + call $import + ) + )`, {m: {import: wasm_js_import, tag: tag}}); + let wrapped_export = ToPromising(instance.exports.test); + let export_promise = wrapped_export(); + assert_true(export_promise instanceof Promise); + promise_rejects(t, new Error(), export_promise); +}, "Rejecting promise"); + +async function TestNestedSuspenders(suspend) { + // Nest two suspenders. The call chain looks like: + // outer (wasm) -> outer (js) -> inner (wasm) -> inner (js) + // If 'suspend' is true, the inner JS function returns a Promise, which + // suspends the inner wasm function, which returns a Promise, which suspends + // the outer wasm function, which returns a Promise. The inner Promise + // resolves first, which resumes the inner continuation. Then the outer + // promise resolves which resumes the outer continuation. + // If 'suspend' is false, the inner and outer JS functions return a regular + // value and no computation is suspended. + + let inner = new WebAssembly.Function( + {parameters: ['externref'], results: ['i32']}, + () => suspend ? Promise.resolve(42) : 43, + {suspending: 'first'}); + + let export_inner; + let outer = new WebAssembly.Function( + {parameters: ['externref'], results: ['i32']}, + () => suspend ? export_inner() : 42, + {suspending: 'first'}); + + let instance = wasmEvalText(`(module + (import "m" "inner" (func $inner (param externref) (result i32))) + (import "m" "outer" (func $outer (param externref) (result i32))) + (func (export "outer") (param externref) (result i32) + local.get 0 + call $outer + ) + (func (export "inner") (param externref) (result i32) + local.get 0 + call $inner + ) + )`, {m: {inner, outer}}); + export_inner = ToPromising(instance.exports.inner); + let export_outer = ToPromising(instance.exports.outer); + let result = export_outer(); + assert_true(result instanceof Promise); + assert_equals(42, await result); +} + +promise_test(async () => { + return TestNestedSuspenders(true); +}, "Test nested suspenders with suspension"); + +promise_test(async () => { + return TestNestedSuspenders(false); +}, "Test nested suspenders with no suspension"); + +test(() => { + let js_import = new WebAssembly.Function( + {parameters: ['externref'], results: ['i32']}, + () => Promise.resolve(42), + {suspending: 'first'}); + let instance = wasmEvalText(`(module + (import "m" "import" (func $import (param externref) (result i32))) + (func (export "test") (param externref) (result i32) + local.get 0 + call $import + ) + (func (export "return_suspender") (param externref) (result externref) + local.get 0 + ) + )`, {m: {import: js_import}}); + let suspender = ToPromising(instance.exports.return_suspender)(); + for (s of [suspender, null, undefined, {}]) { + assert_throws(WebAssembly.RuntimeError, () => instance.exports.test(s)); + } +}, "Call import with an invalid suspender"); + +test(t => { + let instance = wasmEvalText(`(module + (func (export "test") (param externref) (result i32) + local.get 0 + call 0 + ) + )`); + let wrapper = ToPromising(instance.exports.test); + promise_rejects(t, new InternalError(), wrapper()); +}, "Stack overflow"); + +test (() => { + let js_import = new WebAssembly.Function( + {parameters: ['externref'], results: ['i32']}, + () => Promise.resolve(42), + {suspending: 'first'}); + let instance = wasmEvalText(`(module + (import "m" "import" (func $import (param externref) (result i32))) + (func (export "test") (param externref) (result i32) + local.get 0 + call $import + ) + (func (export "return_suspender") (param externref) (result externref) + local.get 0 + ) + )`, {m: {import: js_import}}); + let suspender = ToPromising(instance.exports.return_suspender)(); + for (s of [suspender, null, undefined, {}]) { + assert_throws(WebAssembly.RuntimeError, () => instance.exports.test(s)); + } +}, "Pass an invalid suspender"); + +// TODO: Test suspension with funcref. + +test(t => { + // The call stack of this test looks like: + // export1 -> import1 -> export2 -> import2 + // Where export1 is "promising" and import2 is "suspending". Returning a + // promise from import2 should trap because of the JS import in the middle. + let instance; + function import1() { + // import1 -> export2 (unwrapped) + instance.exports.export2(); + } + function import2() { + return Promise.resolve(0); + } + import2 = new WebAssembly.Function( + {parameters: ['externref'], results: ['i32']}, + import2, + {suspending: 'first'}); + instance = wasmEvalText(`(module + (import "m" "import1" (func $import1 (result i32))) + (import "m" "import2" (func $import2 (param externref) (result i32))) + (global (mut externref) (ref.null extern)) + (func (export "export1") (param externref) (result i32) + ;; export1 -> import1 (unwrapped) + local.get 0 + global.set 0 + call $import1 + ) + (func (export "export2") (result i32) + ;; export2 -> import2 (suspending) + global.get 0 + call $import2 + ) + )`, + {'m': {'import1': import1, 'import2': import2}}); + // export1 (promising) + let wrapper = new WebAssembly.Function( + {parameters: [], results: ['externref']}, + instance.exports.export1, + {promising: 'first'}); + promise_rejects(t, new WebAssembly.RuntimeError(), wrapper()); +}, "Test that trying to suspend JS frames traps"); + +"Invalid test. Skipping..." || test(() => { + let js_import = new WebAssembly.Function( + {parameters: ['externref'], results: ['i32']}, + () => 42, + {suspending: 'first'}); + let instance = wasmEvalText(`(module + (import "m" "import" (func $import (param externref) (result i32))) + (func (export "test") (param externref) (result i32) + local.get 0 + call $import + ) + )`, {m: {import: js_import}}); + assert_equals(42, instance.exports.test(null)); +}, "Pass an invalid suspender to the import and return a non-promise"); + +tests.then(() => print('Done')); diff --git a/js/src/jit-test/tests/wasm/js-promise-integration/multi.js b/js/src/jit-test/tests/wasm/js-promise-integration/multi.js new file mode 100644 index 0000000000..7713637642 --- /dev/null +++ b/js/src/jit-test/tests/wasm/js-promise-integration/multi.js @@ -0,0 +1,36 @@ +// Multiple promises at the same time. + +function js_import() { + return Promise.resolve(42); +} +var wasm_js_import = new WebAssembly.Function( + { parameters: ['externref'], results: ['i32'] }, + js_import, + { suspending: 'first' }); + +var ins = wasmEvalText(`(module + (import "m" "import" (func $f (param externref) (result i32))) + (func (export "test") (param externref) (result i32) + local.get 0 + call $f + ) +)`, {"m": {import: wasm_js_import}}); + +let wrapped_export = new WebAssembly.Function( + { + parameters: [], + results: ['externref'] + }, + ins.exports.test, { promising: 'first' }); + +Promise.resolve().then(() => { + wrapped_export().then(i => { + assertEq(42, i) + }); +}); + +Promise.resolve().then(() => { + wrapped_export().then(i => { + assertEq(42, i) + }); +}); |