summaryrefslogtreecommitdiffstats
path: root/js/src/jit-test/tests/wasm/js-promise-integration
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:43:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-12 05:43:14 +0000
commit8dd16259287f58f9273002717ec4d27e97127719 (patch)
tree3863e62a53829a84037444beab3abd4ed9dfc7d0 /js/src/jit-test/tests/wasm/js-promise-integration
parentReleasing progress-linux version 126.0.1-1~progress7.99u1. (diff)
downloadfirefox-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')
-rw-r--r--js/src/jit-test/tests/wasm/js-promise-integration/basic.js147
-rw-r--r--js/src/jit-test/tests/wasm/js-promise-integration/basic2.js97
-rw-r--r--js/src/jit-test/tests/wasm/js-promise-integration/directives.txt1
-rw-r--r--js/src/jit-test/tests/wasm/js-promise-integration/gc-2.js69
-rw-r--r--js/src/jit-test/tests/wasm/js-promise-integration/gc.js67
-rw-r--r--js/src/jit-test/tests/wasm/js-promise-integration/js-promise-integration.new.js270
-rw-r--r--js/src/jit-test/tests/wasm/js-promise-integration/js-promise-integration.old.js406
-rw-r--r--js/src/jit-test/tests/wasm/js-promise-integration/multi.js36
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)
+ });
+});