diff --git a/README.md b/README.md index cbc17b462f..837cde52f4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,18 @@ -[![CI for specs](https://github.com/WebAssembly/spec/actions/workflows/ci-spec.yml/badge.svg)](https://github.com/WebAssembly/spec/actions/workflows/ci-spec.yml) -[![CI for interpreter & tests](https://github.com/WebAssembly/spec/actions/workflows/ci-interpreter.yml/badge.svg)](https://github.com/WebAssembly/spec/actions/workflows/ci-interpreter.yml) +[![Build Status](https://travis-ci.org/WebAssembly/js-promise-integration.svg?branch=main)](https://travis-ci.org/WebAssembly/js-promise-integration) + +# JavaScript-Promise Integration Proposal for WebAssembly + +This repository is a clone of [github.com/WebAssembly/spec/](https://github.com/WebAssembly/spec/). +It is meant for discussion, prototype specification and implementation of a proposal to +add support for different patterns of non-local control flow to WebAssembly. + +* See the [overview](proposals/js-promise-integration/Overview.md) for a summary of the proposal. + +* See the [modified spec](https://webassembly.github.io/js-promise-integration/) for details. + +* See the [Design Portfolio](https://docs.google.com/document/d/16Us-pyte2-9DECJDfGm5tnUpfngJJOc8jbj54HMqE9Y/edit#heading=h.n1atlriavj6v) for detailed design from a V8 perspective. + +Original `README` from upstream repository follows… # spec diff --git a/document/core/index.rst b/document/core/index.rst index 0179df7be6..9a3c5fab52 100644 --- a/document/core/index.rst +++ b/document/core/index.rst @@ -3,7 +3,7 @@ WebAssembly Specification .. only:: html - | Release |release| + | Release |release| + js-promise-integration (Draft, |today|) | Editor: Andreas Rossberg diff --git a/document/core/util/macros.def b/document/core/util/macros.def index 82d731fbf7..8df1dee88b 100644 --- a/document/core/util/macros.def +++ b/document/core/util/macros.def @@ -3,13 +3,13 @@ .. External Standards .. ------------------ -.. |WasmDraft| replace:: |pagelink| -.. _WasmDraft: |pagelink| +.. |WasmDraft| replace:: https://webassembly.github.io/js-promise-integration/core/ +.. _WasmDraft: https://webassembly.github.io/js-promise-integration/core/ -.. |WasmIssues| replace:: |issuelink| -.. _WasmIssues: |issuelink| +.. |WasmIssues| replace:: https://github.com/webassembly/js-promise-integration/issues/ +.. _WasmIssues: https://github.com/webassembly/js-promise-integration/issues/ -.. |IEEE754| replace:: IEEE 754 +.. |IEEE754| replace:: IEEE 754-2019 .. _IEEE754: https://ieeexplore.ieee.org/document/8766229 .. |Unicode| replace:: Unicode diff --git a/document/js-api/index.bs b/document/js-api/index.bs index 88cace6a47..be01eca447 100644 --- a/document/js-api/index.bs +++ b/document/js-api/index.bs @@ -36,6 +36,15 @@ urlPrefix: https://tc39.github.io/ecma262/; spec: ECMASCRIPT text: NativeError; url: sec-nativeerror-constructors text: TypeError; url: sec-native-error-types-used-in-this-standard-typeerror text: RangeError; url: sec-native-error-types-used-in-this-standard-rangeerror + type: dfn; + text: AbstractClosure; url: sec-abstract-closure + text: CreateBuiltinFunction; url: sec-createbuiltinfunction + text: PromiseCapabilityRecord; url: sec-promisecapability-records + text: EvaluateCall; url: sec-evaluatecall + text: ExecutionContext; url: sec-execution-contexts + text: IsPromise; url: sec-ispromise + text: PerformPromiseThen; url: sec-performpromisethen + text: Execution Context Stack; url: execution-context-stack type: dfn url: sec-returnifabrupt-shorthands text: ! @@ -101,6 +110,8 @@ urlPrefix: https://webassembly.github.io/spec/core/; spec: WebAssembly; type: df text: func_alloc; url: appendix/embedding.html#embed-func-alloc text: func_type; url: appendix/embedding.html#embed-func-type text: func_invoke; url: appendix/embedding.html#embed-func-invoke + text: evaluation_suspend; url: appendix/embedding.html#embed-evaluation-suspend + text: evaluation_resume; url: appendix/embedding.html#embed-evaluation-resume text: table_alloc; url: appendix/embedding.html#embed-table-alloc text: table_type; url: appendix/embedding.html#embed-table-type text: table_read; url: appendix/embedding.html#embed-table-read @@ -295,6 +306,13 @@ Each [=agent=] is associated with the following [=ordered map=]s: * The Host value cache, mapping [=host address=]es to values. +

Execution Context Status Map

+ +Note: The Execution Context Status Map is used to enforce certain correspondences between JavaScript execution and WebAssembly execution; particularly in relation to the JavaScript Promise Integration API. + +Each [=agent=] is associated with the following [=ordered map=]: + * The Execution Context Status map is used to map [=execution context|execution contexts=] to [=stack status=] values. +

The WebAssembly Namespace

@@ -385,13 +403,18 @@ A {{Module}} object represents a single WebAssembly module. Each {{Module}} obje
         1. Let |o| be [=?=] [$Get$](|importObject|, |moduleName|).
         1. If [=Type=](|o|) is not Object, throw a {{TypeError}} exception.
         1. Let |v| be [=?=] [$Get$](|o|, |componentName|).
-        1. If |externtype| is of the form [=external-type/func=] |functype|,
-            1. If [$IsCallable$](|v|) is false, throw a {{LinkError}} exception.
-            1. If |v| has a \[[FunctionAddress]] internal slot, and therefore is an [=Exported Function=],
-                1. Let |funcaddr| be the value of |v|'s \[[FunctionAddress]] internal slot.
-            1. Otherwise,
-                1. [=Create a host function=] from |v| and |functype|, and let |funcaddr| be the result.
-                1. Let |index| be the number of external functions in |imports|. This value |index| is known as the index of the host function |funcaddr|.
+        1. If |externtype| is of the form [=func=] |functype|,
+            1. If [$IsCallable$](|v|) is true,
+                1. If |v| has a \[[FunctionAddress]] internal slot, and therefore is an [=Exported Function=],
+                    1. Let |funcaddr| be the value of |v|'s \[[FunctionAddress]] internal slot.
+                1. Otherwise,
+                    1. [=Create a host function=] from |v| and |functype|, and let |funcaddr| be the result.
+            1. Otherwise, if |v| has a \[[WrappedFunction]] internal slot,
+                    1. Let |func| be the value of |v|'s \[[WrappedFunction]] internal slot.
+                    1. Assert: [$IsCallable$](|func|) is true.
+                    1. [=Create a suspending function|create a suspending function=] from |func| and |functype|, and let |funcaddr| be the result.
+            1. Otherwise, throw a {{LinkError}} exception.
+            1. Let |index| be the number of external functions in |imports|. This value |index| is known as the index of the host function |funcaddr|.
             1. Let |externfunc| be the [=external value=] [=external value|func=] |funcaddr|.
             1. [=list/Append=] |externfunc| to |imports|.
         1. If |externtype| is of the form [=external-type/global=] mut |valtype|,
@@ -1112,7 +1135,7 @@ This slot holds a [=function address=] relative to the [=surrounding agent=]'s [
     1. Let |map| be the [=surrounding agent=]'s associated [=Exported Function cache=].
     1. If |map|[|funcaddr|] [=map/exists=],
         1. Return |map|[|funcaddr|].
-    1. Let |steps| be "[=call an Exported Function|call the Exported Function=] |funcaddr| with arguments."
+    1. Let |steps| be "[=call an Exported Function|call the Exported Function=] |funcaddr| with arguments.".
     1. Let |realm| be the [=current Realm=].
     1. Let |store| be the [=surrounding agent=]'s [=associated store=].
     1. Let |functype| be [=func_type=](|store|, |funcaddr|).
@@ -1126,13 +1149,9 @@ This slot holds a [=function address=] relative to the [=surrounding agent=]'s [
 
 
 
- To call an Exported Function with [=function address=] |funcaddr| and a [=list=] of JavaScript arguments |argValues|, perform the following steps: - - 1. Let |store| be the [=surrounding agent=]'s [=associated store=]. - 1. Let |functype| be [=func_type=](|store|, |funcaddr|). + To coerce JavaScript arguments from a |functype| and a [=list=] of JavaScript arguments |argValues|, perform the following steps 1. Let [|parameters|] → [|results|] be |functype|. 1. If |parameters| or |results| contain [=v128=] or [=exnref=], throw a {{TypeError}}. - Note: the above error is thrown each time the \[[Call]] method is invoked. 1. Let |args| be « ». 1. Let |i| be 0. @@ -1141,10 +1160,18 @@ This slot holds a [=function address=] relative to the [=surrounding agent=]'s [ 1. Otherwise, let |arg| be undefined. 1. [=list/Append=] [=ToWebAssemblyValue=](|arg|, |t|) to |args|. 1. Set |i| to |i| + 1. + 1. return |args|. +
+ +
+ To call an Exported Function with [=function address=] |funcaddr| and a [=list=] of JavaScript arguments |argValues|, perform the following steps: + 1. Let |store| be the [=surrounding agent=]'s [=associated store=]. + 1. Let |functype| be [=func_type=](|store|, |funcaddr|). + 1. Let |args| be the result of [=coerce JavaScript arguments|coercing arguments=] (|functype|,|argValues|). 1. Let (|store|, |ret|) be the result of [=func_invoke=](|store|, |funcaddr|, |args|). 1. Set the [=surrounding agent=]'s [=associated store=] to |store|. - 1. If |ret| is [=error=], throw an exception. This exception should be a WebAssembly {{RuntimeError}} exception, unless otherwise indicated by the WebAssembly error mapping. - 1. If |ret| is [=THROW=] [=ref.exn=] |exnaddr|, then + 1. If |ret| is [=error=], throw an exception. This exception must be a WebAssembly {{RuntimeError}} exception, unless otherwise indicated by the WebAssembly error mapping. + 1. If |ret| is [=THROW=] [=ref.exn=] |exnaddr|, then: 1. Let |tagaddr| be [=exn_tag=](|store|, |exnaddr|). 1. Let |payload| be [=exn_read=](|store|, |exnaddr|). 1. Let |jsTagAddr| be the result of [=get the JavaScript exception tag |getting the JavaScript exception tag=]. @@ -1168,14 +1195,16 @@ Note: [=call an Exported Function|Calling an Exported Function=] executes in the Note: Exported Functions do not have a \[[Construct]] method and thus it is not possible to call one with the `new` operator.
- To run a host function from the JavaScript object |func|, type |functype|, and [=list=] of [=WebAssembly values=] |arguments|, perform the following steps: - - 1. Let [|parameters|] → [|results|] be |functype|. - 1. If |parameters| or |results| contain [=v128=] or [=exnref=], throw a {{TypeError}}. + To coerce WebAssembly arguments from a [=list=] of |parameterTypes| and a [=list=] of JavaScript arguments |arguments|, perform the following steps + 1. If |parameterTypes| contain [=v128=], throw a {{TypeError}}. 1. Let |jsArguments| be « ». 1. [=list/iterate|For each=] |arg| of |arguments|, 1. [=list/Append=] [=!=] [=ToJSValue=](|arg|) to |jsArguments|. - 1. Let |ret| be [=?=] [$Call$](|func|, undefined, |jsArguments|). + 1. Return |jsArguments|. +
+ +
+ To coerce a JavaScript return from a JavaScript |ret| and a list of |results| types, perform the following steps: 1. Let |resultsSize| be |results|'s [=list/size=]. 1. If |resultsSize| is 0, return « ». 1. Otherwise, if |resultsSize| is 1, return « [=?=] [=ToWebAssemblyValue=](|ret|, |results|[0]) ». @@ -1190,6 +1219,18 @@ Note: Exported Functions do not have a \[[Construct]] method and thus it is not 1. Return |wasmValues|.
+
+ To coerce a JavaScript exception from a JavaScript exception |v|, perform the following steps: + 1. If |v| [=implements=] {{Exception}}, + 1. Let |type| be |v|.\[[Type]]. + 1. Let |payload| be |v|.\[[Payload]]. + 1. Otherwise, + 1. Let |type| be the [=JavaScript exception tag=]. + 1. Let |payload| be « ». + 1. Let |opaqueData| be [=ToWebAssemblyValue=](|v|, [=externref=]). + 1. Return the triple |type|, |payload| and |opaqueData|. +
+
To create a host function from the JavaScript object |func| and type |functype|, perform the following steps: @@ -1200,27 +1241,34 @@ Note: Exported Functions do not have a \[[Construct]] method and thus it is not 1. Let |relevant settings| be |realm|'s [=realm/settings object=]. 1. [=Prepare to run script=] with |relevant settings|. 1. [=Prepare to run a callback=] with |stored settings|. - 1. Let |result| be the result of [=run a host function|running a host function=] from |func|, |functype|, and |arguments|. + 1. Let [|parameters|] → [|resultTypes|] be |functype|. + 1. Let |jsArguments| be the result of [=coerce WebAssembly arguments=](|parameters|,|arguments|). + 1. Let |result| be the result of Completion([$Call$](|func|, undefined, |jsArguments|)). 1. [=Clean up after running a callback=] with |stored settings|. 1. [=Clean up after running script=] with |relevant settings|. 1. Assert: |result|.\[[Type]] is throw or normal. 1. Let |store| be the [=surrounding agent=]'s [=associated store=]. + 1. If |result|.\[[Type]] is normal, then: + 1. Return the result of performing [=coerce a JavaScript return=] on |resultTypes| and |ret|. 1. If |result|.\[[Type]] is throw, then: - 1. Let |v| be |result|.\[[Value]]. - 1. If |v| [=implements=] {{Exception}}, - 1. Let |address| be |v|.\[[Address]]. - 1. Otherwise, - 1. Let |type| be the result of [=get the JavaScript exception tag |getting the JavaScript exception tag=]. - 1. Let |payload| be [=!=] [=ToWebAssemblyValue=](|v|, [=externref=]). - 1. Let (|store|, |address|) be [=exn_alloc=](|store|, |type|, « |payload| »). - 1. Set the [=surrounding agent=]'s [=associated store=] to |store|. - 1. Execute the WebAssembly instructions ([=ref.exn=] |address|) ([=throw_ref=]). - 1. Otherwise, return |result|.\[[Value]]. + 1. Perform [=throw a JavaScript exception|throw the JavaScript exception=] on |result|.\[[Value]]. 1. Let (|store|, |funcaddr|) be [=func_alloc=](|store|, |functype|, |hostfunc|). 1. Set the [=surrounding agent=]'s [=associated store=] to |store|. 1. Return |funcaddr|.
+
+ To throw a JavaScript exception from the JavaScript object |v|, perform the following steps: + 1. If |v| [=implements=] {{Exception}}, + 1. Let |address| be |v|.\[[Address]]. + 1. Otherwise, + 1. Let |type| be the result of [=get the JavaScript exception tag |getting the JavaScript exception tag=]. + 1. Let |payload| be [=!=] [=ToWebAssemblyValue=](|v|, [=externref=]). + 1. Let (|store|, |address|) be [=exn_alloc=](|store|, |type|, « |payload| »). + 1. Set the [=surrounding agent=]'s [=associated store=] to |store|. + 1. Execute the WebAssembly instructions ([=ref.exn=] |address|) ([=throw_ref=]). +
+
The algorithm ToJSValue(|w|) coerces a [=WebAssembly value=] to a JavaScript value by performing the following steps: @@ -1250,7 +1298,6 @@ The algorithm ToJSValue(|w|) coerces a [=WebAssembly value=] to a Jav 1. If |w| is of the form [=ref.host=] |hostaddr|, return the result of [=retrieving a host value=] from |hostaddr|. 1. If |w| is of the form [=ref.extern=] |ref|, return [=ToJSValue=](|ref|). - Note: Number values which are equal to NaN may have various observable NaN payloads; see [$NumericToRawBytes$] for details.
@@ -1283,7 +1330,7 @@ The algorithm ToWebAssemblyValue(|v|, |type|) coerces a JavaScript va 1. Let |n| be an implementation-defined integer such that [=canon=]32 ≤ |n| < 2[=signif=](32). 1. Let |f32| be [=nan=](n). 1. Otherwise, - 1. Let |f32| be |number| rounded to the nearest representable value using IEEE 754-2008 round to nearest, ties to even mode. [[IEEE-754]] + \1. 1. Return [=f32.const=] |f32|. 1. If |type| is [=f64=], 1. Let |number| be [=?=] [$ToNumber$](|v|). @@ -1329,6 +1376,137 @@ The algorithm ToWebAssemblyValue(|v|, |type|) coerces a JavaScript va
+

JavaScript Promise Integration

+ +Note: The JavaScript Promise Integration API (JSPI) allows WebAssembly functions to suspend and resume their execution -- based on the behavior of JavaScript functions that return Promise objects. + +A {{Suspending}} marker object represents a JavaScript function whose calls via WebAssembly imports should be *suspended* when they return a Promise object. +Each {{Suspending}} marker object has a \[[WrappedFunction]] internal slot which holds a JavaScript function. + +In addition, the {{promising}} function takes as argument a WebAssembly function and returns a JavaScript function that returns a Promise that is resolved when the WebAssembly function completes. + +Each [=agent=] maintains a [=Execution Context Status map=], mapping from [=execution context|execution contexts=] to a status symbol. The purpose of this map is to ensure that applications do not try to suspend JavaScript frames and also to ensure that calls to imports marked with a {{Suspending}} marker object are properly balanced by corresponding uses of {{WebAssembly.promising}}. + +If present, a status can be one of two stack status values: + + * active. This signals that the associated execution context is actively executing and has the potential to be [=paused=]. + * paused. This signals that the associated execution is not currently involved in computation and has been paused. + +If an execution context is not present in the status mapping, then it may not be [=Pause|paused=] or [=Enter|reentered=]. + +When a new agent is created, its status mapping is set to the empty map. + +
+[Exposed=*]
+partial namespace WebAssembly {
+    Function promising(Function wasmFunc);
+};
+
+[LegacyNamespace=WebAssembly, Exposed=*]
+interface Suspending {
+    constructor(Function jsFun);
+};
+
+ +
+ The promising(|wasmFunc|) function, when invoked, performs the following steps: + 1. If [$IsCallable$](|wasmFunc|) is false, throw a {{TypeError}}. + 1. If |wasmFunc| does not have a \[[FunctionAddress]] internal slot, throw a {{TypeError}}. + 1. Let |builder| be a new [=AbstractClosure=] with no parameters that captures |wasmFunc| and performs [=run a Promising function=] when called. + 1. Returns the result of [=CreateBuiltinFunction=](|builder|,1,"", « »). +
+ +
+ The algorithm to run a Promising function from the JavaScript object |wasmFunc| and a [=list=] of [=WebAssembly values=] |arguments| consists of the following steps: + 1. Let |promise| be a new [=PromiseCapabilityRecord=]. + 1. Let |funcaddr| be the value of |wasmFunc|'s \[[FunctionAddress]] internal slot. + 1. Let |runner| be a new [=AbstractClosure=] with no parameters that captures |promise|, |funcaddr|, and |arguments| and performs the following steps when called: + 1. Perform [=evaluate a Promising function=](|promise|,|funcaddr|,|arguments|). + 1. Perform [=?=][$AsyncFunctionStart$](|promise|,|runner|). + 1. Return |promise|. +
+ +
+ The algorithm to evaluate a Promising function(|promise|, |funcaddr|, |arguments|) consists of the following steps: + 1. Let |store| be the [=surrounding agent=]'s [=associated store=]. + 1. Let |functype| be [=func_type=](|store|, |funcaddr|). + 1. Let |args| be the result of [=coerce JavaScript arguments|coercing arguments=] (|functype|,|arguments|). + 1. Let |map| be the [=surrounding agent=]'s associated [=Execution Context Status map=]. + 1. Let |ec| be the currently executing [=execution context=], i.e., the [=execution context=] that is at the top of the [=surrounding agent=]'s current [=execution context stack=]. + 1. Assert: |map| does not contain any entry for |ec|. + 1. Add an entry mapping |ec| to [=active=] in |map|. + 1. Let (|store|, |result|) be the result of [=func_invoke=](|store|, |funcaddr|, |args|). + 1. Assert: If control reaches here, we have done waiting for suspended imports. + 1. If the entry for |ec| in |map| is not [=active=] then throw a WebAssembly {{SuspendError}} exception. Otherwise, remove the entry for |ec| from [=map=]. + 1. Set the [=surrounding agent=]'s [=associated store=] to |store|. + 1. If |result| is [=error=], throw a WebAssembly {{RuntimeError}} exception, unless otherwise indicated by the WebAssembly error mapping. + 1. Otherwise, if |result| is of the form [=throw=] exnaddr, + 1. [=Reject=] |promise| with |result|. + 1. Otherwise, + 1. Assert: |result| is a [=list=] of WebAssembly values. + 1. Let |outArity| be the [=list/size=] of |result|. + 1. If |outArity| is 0, return undefined. + 1. Otherwise, if |outArity| is 1, let |jsReturnValue| be [=ToJSValue=](|result|[0]). + 1. Otherwise, + 1. Let |values| be « ». + 1. [=list/iterate|For each=] |r| of |result|, + 1. [=list/Append=] [=ToJSValue=](|r|) to |values|. + 1. Let |jsReturnValue| be [$CreateArrayFromList$](|values|). + 1. [=Resolve=] |promise| with |jsReturnValue|. + 1. Return UNUSED. +
+ +
+ The Suspending(|jsFun|) constructor, when invoked, performs the following steps: + 1. If [$IsCallable$](|jsFun|) is false, throw a {{TypeError}}. + 1. Let |suspendingProto| be \[[%WebAssembly.Suspending.prototype%]]. + 1. Let |susp| be the result of [$OrdinaryObjectCreate$](|suspendingProto|,«\[[WrappedFunction]]»). + 1. Set |susp|.\[[WrappedFunction]] to |jsFun|. + 1. Return |susp|. +
+ +
+To create a suspending function from a JavaScript function |func|, with type |functype| perform the following steps: + + 1. Assert: [$IsCallable$](|func|). + 1. Let |stored settings| be the incumbent settings object. + 1. Let |hostfunc| be a [=host function=] which performs the following steps when called with arguments |arguments|: + 1. Let |realm| be |func|'s [=associated Realm=]. + 1. Let |relevant settings| be |realm|'s [=realm/settings object=]. + 1. Let |async_context| be the [=surrounding agent=]'s [=running execution context=]. + 1. Let |map| be the [=surrounding agent=]'s associated [=Execution Context Status map=]. + 1. If the entry for |async_context| in |map| is not [=active=], then: + 1. Perform [=throw a JavaScript exception=] with a {{SuspendError}} exception. + 1. [=Prepare to run script=] with |relevant settings|. + 1. [=Prepare to run a callback=] with |stored settings|. + 1. Let [|parameters|] → [|resultTypes|] be |functype|. + 1. Let |jsArguments| be the result of [=coerce WebAssembly arguments=](|parameters|,|arguments|). + 1. Let |ret| be [$Completion$]([$Call$](|func|, undefined, |jsArguments|)). + 1. [=Clean up after running a callback=] with |stored settings|. + 1. [=Clean up after running script=] with |relevant settings|. + 1. Assert: |ret|.\[[Type]] is throw or normal. + 1. If |ret|.\[[Type]] is throw, then: + 1. Let |type|, |payload| and |opaqueData| be the result of [=coerce a JavaScript exception|coercing the JavaScript exception=] |ret|.\[[Value]]. + 1. [=WebAssembly/Throw=] with |type|, |payload| and |opaqueData|. + 1. Otherwise, if [=list/size=] of |ret| is 1 and [$IsPromise$](|ret|.\[[Value]][0]): + 1. Let |promise| be |ret|.\[[Value]][0]. + 1. Set the entry for |async_context| in |map| to [=paused=]. + 1. Let |awaitResult| be the result of performing [$Completion$]([$Await$](|promise|)). + 1. Note: We only invoke [$Await$] if the call to |func| has returned a Promise object. + 1. Note: This will suspend both this algorithm, and the WebAssembly function being invoked by the [=evaluate a Promising function=] algorithm. On return, |ret| will be either a normal completion or a throw completion. + 1. If the entry for |async_context| in |map| is not [=paused=] then: + 1. Perform [=throw a JavaScript exception=] with a {{SuspendError}}. + 1. Otherwise, set the entry to [=active=]. + 1. If |awaitResult|.\[[Type]] is throw, then: + 1. Let |type|, |payload| and |opaqueData| be the result of [=coerce a JavaScript exception|coercing the JavaScript exception=] |ret|.\[[Value]]. + 1. [=WebAssembly/Throw=] with |type|, |payload| and |opaqueData|. + 1. Otherwise, return the result of performing [=coerce a JavaScript return=] on |resultTypes| and |awaitResult|. + 1. Otherwise, return the result of performing [=coerce a JavaScript return=] on |resultTypes| and |ret|. + 1. Let |store| be the [=surrounding agent=]'s [=associated store=]. + 1. Let (|store|, |funcaddr|) be [=func_alloc=](|store|, |functype|, |hostfunc|). + 1. Set the [=surrounding agent=]'s [=associated store=] to |store|. + 1. Return |funcaddr|. +

Tags

@@ -1561,7 +1739,7 @@ constructor steps are: 1. If |resultType| is [=v128=] or [=exnref=], 1. Throw a {{TypeError}}. 1. [=list/Append=] [=?=] [=ToWebAssemblyValue=](|value|, |resultType|) to |wasmPayload|. -1. Let (|store|, |exceptionAddr|) be [=exn_alloc=](|store|, |exceptionTag|.\[[Address]], |wasmPayload|) +1. Let (|store|, |exceptionAddr|) be [=exn_alloc=](|store|, |exceptionTag|.\[[Address]], |wasmPayload|). 1. Set the [=surrounding agent=]'s [=associated store=] to |store|. 1. [=initialize an Exception object|Initialize=] **this** from |exceptionAddr|. 1. If |options|["traceStack"] is true, @@ -1617,7 +1795,7 @@ first use and cached. It always has the [=tag type=] « [=externref=] » → « To get the JavaScript exception tag, perform the following steps: 1. If the [=surrounding agent=]'s associated [=JavaScript exception tag=] has been initialized, - 1. return the [=surrounding agent=]'s associated [=JavaScript exception tag=] + 1. return the [=surrounding agent=]'s associated [=JavaScript exception tag=]. 1. Let |store| be the [=surrounding agent=]'s [=associated store=]. 1. Let (|store|, |tagAddress|) be [=tag_alloc=](|store|, « [=externref=] » → « »). 1. Set the current agent's [=associated store=] to |store|. @@ -1629,19 +1807,19 @@ To get the JavaScript exception tag, perform the following steps:

Error Objects

-WebAssembly defines the following Error classes: CompileError, LinkError, and RuntimeError. +WebAssembly defines the following Error classes: CompileError, LinkError, RuntimeError, and SuspendError.
When the [=namespace object=] for the {{WebAssembly}} namespace is [=create a namespace object|created=], the following steps must be run: 1. Let |namespaceObject| be the [=namespace object=]. -1. [=list/iterate|For each=] |error| of « "CompileError", "LinkError", "RuntimeError" », +1. [=list/iterate|For each=] |error| of « "CompileError", "LinkError", "RuntimeError", "SuspendError" », 1. Let |constructor| be a new object, implementing the [=NativeError Object Structure=], with NativeError set to |error|. 1. [=!=] [$DefineMethodProperty$](|namespaceObject|, |error|, |constructor|, false).
-Note: This defines {{CompileError}}, {{LinkError}}, and {{RuntimeError}} classes on the {{WebAssembly}} namespace, which are produced by the APIs defined in this specification. +Note: This defines {{CompileError}}, {{LinkError}}, {{RuntimeError}}, and {{SuspendError}} classes on the {{WebAssembly}} namespace, which are produced by the APIs defined in this specification. They expose the same interface as native JavaScript errors like {{TypeError}} and {{RangeError}}. Note: It is not currently possible to define this behavior using Web IDL. diff --git a/proposals/js-promise-integration/ImplementationGuide.md b/proposals/js-promise-integration/ImplementationGuide.md new file mode 100644 index 0000000000..693a691862 --- /dev/null +++ b/proposals/js-promise-integration/ImplementationGuide.md @@ -0,0 +1,181 @@ +# JSPI Implementation Notes + +## Introduction + +This note explores some of the techniques used in implementing JSPI in a WebAssembly engine and some of the issues that may be encountered. + +Implementing JSPI has some aspects which are quite specific to that API (in particular how Promise objects are handled) and also has many aspects that apply to stack switching and coroutining in general. This is particularly the case for some of the lower-level aspects of implementation. + +We will be addressing both low-level and high-level aspects of JSPI implementation. + +## Running a promising function + +Invoking a `promising` function has a number of distinct phases: + + 1. A new `Promise` object is created. This will be the value that is ultimately returned by the promising function. + 1. The wrapped WebAssembly function is called; with arguments coerced from JavaScript into WebAssembly values as dictated by the type of the WebAssembly function. + 1. The result of the call to the wrapped WebAssembly function is used to _fulfill_ the `Promise`: if the WebAssembly function returns normally, then its return value is coerced back into JavaScript and used to resolve the `Promise` object. If the WebAssembly returns with an exception, or traps, then the exception is used to reject the `Promise`. + 1. The `Promise` is returned. + +Crucially, the middle steps of the above algorithm are performed on a separate stack -- see [below](#stacks). This allows us to suspend the WebAssembly call but continue the application as a whole. + +### Suspending when `Promise`d something + +When a suspending import is called from WebAssembly a special wrapper is interposed between the WebAssembly call and the JavaScript function being called. This wrapper coerces arguments from WebAssembly into JavaScript and interprets the result of the JavaScript call: + + 1. If the JavaScript function returns a value that is not a `Promise` then that value is coerced back into WebAssembly -- as dictated by the type of the WebAssembly import -- and the wrapper returns the result. + 1. If the JavaScript function returns an exception, then the wrapper propagates the exception and rethrows it to the WebAssembly call. + 1. If the JavaScript function returns a `Promise` object then the wrapper will suspend the computation -- up to the innermost `promising` call -- capture the suspended computation in a continuation object and attach callbacks to that `Promise`: + + 1. For a successful resolution to the `Promise` the callback enters the captured continuation with the value returned in the `Promise` -- appropriately coerced into WebAssembly. + 1. For a rejected `Promise` the callback enters the captured continuation by throwing it an exception -- again as given to the callback function. + + After attaching the callbacks, the wrapper causes a return from the promising function's inner body. Depending on whether this was the first time the computation suspended or a subsequent time, this results in the top-level call to the promising function to return (with its `Promise`) or execution will continue within the micro-task scheduler (where no return result is expected). + +#### Formalizing with shift/reset + +The well known shift/reset framework allows us to write the above algorithm in a pseudo-code fashion: + +```[pseudo] +promising(F) => (A)=>{ + P = new Promise(); + reset( ()=>try + P.resolve(F(A)) + catch e => + P.reject(e)); + return P +} + +suspending(I) => (X)=> case I(X) in { + P where isAPromise(A) => shift k in { + P.then((u)=>k(u),(e)=>k throw e) + } + R default => R + } +``` + +Of course, the above pseudo code is intended for illustrative purposes only: it is not possible to express the semantics of promising functions in either JavaScript or WebAssembly at this time. + +Some observations: + +* The body of the `reset` expression is a zero-argument lambda. This reflects our assertion above that stack switching events are always associated with function calls. +* The variables `P` and `A` are free in the `reset` lambda -- they bind the created `Promise` object and the arguments to the call. +* The body of the `reset` lambda handles both successful returns from WebAssembly and exceptional returns. Note that we are not explicitly accounting for WebAssembly traps: they are modeled as exceptions. +* The `suspending` function performs a case analysis on the result of calling the actual imported function -- `I`. If the result looks like a `Promise` then we use `shift` to capture the computation -- up to the innermost `reset` -- and also use the returned continuation in the callback functions attached to the `Promise`. +* The form '`k throw e`' is intended to signify invoking the continuation with the exception `e` -- which is given as a parameter to the `reject` callback function. +* Traditionally, the `shift` operator refers to the innermost occurrence of a `reset` operator in order to delimit the captured continuation. In addition, exceptions are often also modeled in terms of `shift/reset`. However, the combination of the two constructs (promising/suspending and try/catch) is not well formed in this reading of `shift/reset`: the try/catch structure interferes with the promising/suspending structure. It is possible to extend the `shift/reset` formalism to correctly account for this but we choose not to here. +* The use of the `shift/reset` framework here is for illustrative purposes and is not intended to endorse that framework. + +## Stacks + +The fundamental strategy for implementing JSPI can be summarized using the phrase "run the promising function on a separate stack" - where _promising function_ refers to the result of using the `promising` API call to convert an exported WebAssembly function into one that binds its result into a `Promise` object. + +When the time comes to suspend the computation -- because a call to an import resulted in a `Promise` rather than a regular value -- it becomes straightforward to stash the separate stack into a data structure and continue with the original call. Recall that when a `promising` function returns, it returns a `Promise` object. The returned `Promise` object will be fulfilled (resolved or rejected) when the underlying WebAssembly function completes. + +If an import resulted in a suspension, the associated `Promise` object will have a _continuation_ attached to it -- via the `then`/`reject` callbacks. This continuation has the stashed stack as a captured variable. When the `Promise` object is fulfilled -- i.e., when the suspending import completes -- the stashed stack can be re-entered and the suspended computation resumed. + +This strategy allows us to rapidly _suspend_ the computation when we need to and to equally rapidly _resume_ when the computation can continue. In fact the operations of suspending and resuming are fundamentally constant time operations involving some 20 to 50 machine instructions. + +### Stack Switching + +The theoretical minimum number of machine instructions to switch between stacks is two: one instruction to save the current SP register in a known location and another to load the SP register. However, in practice, the number of executed instructions is significantly higher for a variety of reasons. + +All stack switching events are also associated with a function call. When a computation suspends, it does so (in the case of JSPI) because a function call resulted in a `Promise` being returned. When a function resumes -- due to its associated promise being fulfilled -- it does so in the context of a callback being called by the Browser's micro task queue runner. + +In addition, most JSPI-related stack switches also involve some form of _type coercion_ -- between JavaScript values and WebAssembly values. We typically combine this coercion with migrating values between computations. For example, when calling a `promising` function, the coercion of values from JavaScript to WebAssembly is arranged so that coerced values are directly spilled to the new stack -- rather than being first of all coerced and then copied. + +Apart from propagating values to a resumed coroutine, there are other factors that must be addressed: checking for stack overflow, managing the allocation of stacks, and responding to stack overflow by allowing stacks to grow in size. + +#### Checking for stack overflow + +Both JavaScript and WebAssembly call for a check for stack overflow on entry to functions. In addition, many engines require an _interrupt check_ that will cause teh engine to stop if an external interrupt is signaled. + +Typically, a stack overflow check involves comparing the value of the SP register with some limit. If we switch between stacks then we must also update the limit pointers -- since each stack will have its own limit. Since the limit pointers are not normally held in registers this increases the theoretical minimum number of instructions for a stack switch from two to four (the new stack limit must be reset -- which takes a minimum of two instructions on Arm).[notonc^] + +In V8, the stack overflow and interrupt checks are combined by using a special sentinel value for the stack limit when an interrupt is requested. In particular, a V8 computation is interrupted by setting the stack limit to a special sentinel value -- one that is guaranteed to cause the stack overflow check to fail. Then, if the comparison fails, we must also check that it was a _real_ stack overflow, or if it was an interrupt. + +[notonc^]: Note that stack overflow checks are _not_ performed when executing C code. This includes Web API calls made from JavaScript and WebAssembly programs; and it also includes garbage collection triggered by allocation failures. + +#### Taking a lock + +To avoid a potential race condition between requesting an interrupt and switching between stacks we use an exclusion zone to protect changing the stack limit pointers. + +Mutexes and other forms of memory lock have direct and indirect costs associated with them. Depending on the actual architecture of the platform, the memory synchronization across multiple cores implied by a lock can cause significant performance issues for stack switching. However, combining the checks does allow the infrequent case (interrupt) to be merged in with the frequent case (stack overflow). + +### Stack Allocation Strategies + +Using JSPI to realize responsive applications is fundamentally less resource intensive than using worker threads. This enables application patterns that are not feasible when using workers. For example, the simultaneous downloading of thousands of images -- using suspended coroutines to represent the processing tasks for each image. + +Recall that our strategy for JSPI involves allocating a stack for each call to a `promising` function. This raises the question of managing the stack memories when there may be a large number of suspendable computations. + +The stack memory must be separately allocated from the garbage collected heap because GC can (and does) relocate objects during garbage collection. Since a stack typically has location sensitive pointers embedded in it (for example inter-frame pointers) stack memory cannot easily be moved. + +A related question is how large to make the stack memories; especially given the constraints alluded to above. A primary difficulty in deciding the size of a stack memory is that we cannot know ahead of time how much stack space a given promising function needs. Using a large fixed size (say 1MB) is not practical on most devices; and in any case is very wasteful in memory. Using a small fixed size risks unnecessary stack overflow in promising (sic) functions. + +One solution takes the form of _growable stacks_: when a stack is first allocated for a promising function only a small stack memory is used -- for example: 32K bytes. This is likely to be sufficient for many, if not most, coroutines. However, if a given coroutine overflows this allocation then we _grow_ its stack. + +#### Growing by switching + +For the same reasons that we do not allocate stack memory in the garbage collected heap we also do not simply grow a stack by reallocating it. Instead, when a stack overflow is detected on a function call, we allocate a new stack and switch to it for the function call. This is the so-called _segmented stack_ approach to implementing growable stacks. + +We use a heuristic strategy to determine the size of the newly allocated stack segment. In particular, a primary concern is to avoid a performance cliff problem when segmenting stacks. This can arise when the function that causes the stack overflow/split to occur has many calls to functions itself. + +For example, if a function calls another function in the body of a loop and each such call causes the stack overflow/split to occur then that function's performance could suffer significantly. + +We resolve this with a combination of exponential backoff in the allocation size and local caching of stack segments. The former reduces the probability of subsequent overflow/splits occurring and the latter reduces the cost of the overflow itself. + +#### Caching stack memories + +Related to the issue of stack overflow for promising functions is the one of frequent but shallow calls to promising functions. Particularly since we don't use the GC allocated heap for stack memories, allocating and freeing stack memories can also be resource intensive. + +One straightforward approach to this is also to cache stack memories. A stack memory becomes available for reuse when the promising function finally returns. Instead of freeing the stack memory (or waiting for GC to trigger the release of the memory) we can aoivd some of the costs of allocation and freeing by maintaining a cache of stack memories. + +For applications that make frequent uses of JSPI promising but are not otherwise reentrant the effect will be that a cache of a single stack memory will likely be sufficient. We have observed significant performance benefits of such stack caching. + +## Integrating with embedder hosts + +One of the most immediate challenges with our strategy of using different stacks is that most embedder environments are not adapted to working with multiple stacks. This is particularly true for languages like C/C++. + +As a result, there are a number of design problems that need to be resolved when implementing JSPI: + +* How to actually have multiple stacks, and to switch between them. +* How to account for utilities and libraries that depend on being able to 'walk the stack'. +* How to safely invoke embedder code when the actual stack size may be much smaller than originally anticipated. + +### Control Flow Integrity + +Control Flow Integrity (CFI) refers to security techniques that are used to prevent so-called ‘return oriented programming’. Typically requiring some hardware support – to prevent bad actors from circumventing the effort – CFI aims to prevent the use of functions’ entry points and exit points except as intended by the developer. + +There are two common approaches used for CFI: signing function return addresses and maintaining a shadow stack in a private memory. In addition, so-called label target instructions are also used: to prevent jumping to an instruction address (or even within instructions) that was not anticipated. + +Clearly, stack switching of any form has the intended effect of not always returning as expected from a function call. This can be a particular problem with hardware maintained shadow stacks. When we switch stacks we must also inform the operating system and hardware that we have done so. + +On Intel hardware, creating a new shadow stack is a privileged operation: requiring a system call. However, switching between shadow stacks is not. The WebAssembly compiler, however, must emit the required instructions to switch the shadow stacks when switching between WebAssembly stacks – unfortunately further increasing the cost of a stack switch. + +Other techniques used for CFI protection are less onerous: return address signing[^signing] is not affected by a stack switch and jumps generated by the WebAssembly compiler can be associated with label target instructions. + +[^signing]: Return address signing is achieved by entangling the actual return address with the value of the SP register at the time of the call. This relies on the fact that most modern hardware implementations are fully fully 64 bit: physical addresses are limited to 48 bits. The remaining 16 bits in a 64 bit word can be used to encrypt the SP register with the return address. + +### Integrating with JavaScript + +One of the more tempting design choices revolves around whether or not to switch _from_ a secondary stack to the original main stack when calling a normal (i.e., not wrapped) import call to JavaScript. + +Switching back when calling JavaScript means that applications that do not use JSPI are not adversely affected by the JSPI implementation. It also simplifies the handling of so-called embedder code which is typically written in languages that do not honor stack limits. + +### Executing host code + +As noted above, most programming languages are not designed to be executed in a coroutining context. In addition, there are many algorithms that rely on being able to ‘walk the stack’. + +For example, many profilers operate by interrupting the targeted application on a periodic basis, inspecting the execution stack to see what functions are being called, and recording the result. Such profilers have to be rewritten or extended if they are to take into account the fact that an application may actually be using multiple stacks. + +Since a WebAssembly engine is designed to be embedded, executing on multiple stacks may violate assumptions that the embedder makes when invoking WebAssembly code. + +This issue surfaces not only for explicit imports to host APIs but also for the many cases where the WebAssembly engine must invoke embedder function to support the semantics of WebAssembly itself. For example, when allocating a `struct`, there are two paths that the engine may take: one where the memory is known to be available and one which may result in the garbage collector being invoked. By switching to the central stack for such slow paths minimises the risk of running hist code while hopefully preserving the performance benefits of not switching. + +## Expected Performance + +The fundamental performance promise of JSPI is that, once the cost of allocation of a stack is accounted for, the cost of switching – whether it is for suspending and resuming a WebAssembly code or whether it is to execute JavaScript and/or host code – is constant. + +The actual cost of a stack switch is of the order of five to 10 C function calls. Some of that may be avoidable in the future, but there are good reasons to believe that the minimum cost is 3 function calls. + +Note that this is significantly better than the expected costs in alternative strategies – such as stack copying or CPS transform. In those cases, stack switching is no longer constant but dependent on the depth of the computation being switched. \ No newline at end of file diff --git a/proposals/js-promise-integration/Overview.md b/proposals/js-promise-integration/Overview.md new file mode 100644 index 0000000000..f4033613e5 --- /dev/null +++ b/proposals/js-promise-integration/Overview.md @@ -0,0 +1,235 @@ +# JavaScript-Promise Integration Proposal + +## Summary + +The JavaScript Promise Integration (JSPI) API is an API that bridges the gap between synchronous WebAssembly applications and asynchronous Web APIs. It does so by mapping synchronous calls issued by the WebAssembly application into asynchronous Web API calls, suspending the application and resuming it when the asynchronous I/O operation is completed. Crucially, we are able to achieve this with very few changes to the WebAssembly application itself. + +This proposal makes no changes to the JavaScript language nor to the WebAssembly language. There are no new WebAssembly instructions or types specified. Semantically, all of the changes outlined are at the boundary between WebAssembly and JavaScript. + +## Motivation + +Many modern APIs on the Web are *asynchronous* in nature -- mediated by `Promise`s. Asynchronous APIs operate by splitting the offered functionality into two separate parts: the initiation of the operation and its resolution; with the latter coming some time after the first. Most importantly, the application continues execution after kicking off the operation; and is then notified when the operation completes. + +For example, the `fetch` API allows Web applications to access the contents associated with a URL; however, the `fetch` function does not directly return the results of the fetch; instead it returns a `Promise`. The connection between the fetch response and the original request is reestablished by attaching a *callback* to that `Promise`. The callback function can inspect the response, collect the data (if it is there of course) and re-enter the Web application. + +On the other hand, many applications that are compiled into WebAssembly originate from languages such as C/C++ which do not have mature coroutining features and where the APIs used are typically *synchronous* in nature. In the example of calling `fetch`, a legacy application would typically *block* until the result of the `fetch` is available. Web applications are strongly discouraged from blocking in this way; because blocking the main thread carries the risk of degrading the user experience. As a result there can be a significant mismatch between the application and web APIs. + +This proposal allows a WebAssembly application to interact with JavaScript APIs and functions that are oriented around `Promise`s. Furthermore, it allows the WebAssembly application to invoke so-called `Promise`-bearing imports and access the value of the `Promise`, without having to explicitly manage the asynchronous callbacks normally associated with `Promise`s. + +## Core concepts + +There are two elements in the JSPI API: the `WebAssembly.Suspending` constructor and the `WebAssembly.promising` function. + +The `WebAssembly.Suspending` object is used to mark imports to a WebAssembly module such that, when called, the WebAssembly code will suspend until the `Promise` returned by the import is resolved.[^nosuspend] + +[^nosuspend]:If the imported function did *not* return a `Promise`, then the results will be passed directly to the WebAssembly caller -- i.e., there will be no suspension in this case. + +When, at some point later, the `Promise` is resolved, and the WebAssembly module is *resumed* -- by the browser's event queue task runner -- then the value of the resolved `Promise` becomes the value of the WebAssembly call to the import. + +Again, if the `Promise` is rejected, then instead of resuming the WebAssembly module with the value, an exception will be propagated into the suspended computation. + +As a result, a WebAssembly module can import a `Promise` returning JavaScript function so that the WebAssembly code can treat a call to it as a *synchronous* call. + +The `WebAssembly.promising` function is used to wrap an exported WebAssembly function into one that returns a `Promise` -- where the returned value from the exported function becomes the basis of resolving the `Promise`.[^wrapping] + +[^wrapping]: The English language can be somewhat ambiguous when it comes to concepts such as marking and wrapping. In order to avoid such ambiguities, we use the term *marked function* to denote the result of applying `WebAssembly.promising` or `WebAssembly.Suspending` to a function. And we will use the term *wrapped function* to denote the argument function that is passed into those API calls -- i.e., the function that will be invoked as a result of invoking the marked function. + +The `promising` function and the `Suspending` object form a pair; when a WebAssembly computation is suspended due to a call to a `Suspending` import, it is the call to the `promising` export that is continued -- in the first instance. I.e., a call to a `promising` export finishes when the first call to a `Suspending` import results in the WebAssembly code being suspended. The value returned by the `promising` export is also a `Promise`; that will be resolved only when the wrapped export finally returns (or throws an exception). + +>Of course, in general, a particular call to a marked export may require multiple calls to `Suspending` imports, with multiple suspensions. However, other than the first one, all subsequent suspensions are visible only to the browser event queue task runner: the host application only sees the `Promise` initially created and is reactivated only when the wrapped export finally returns (or throws). + +Since they form a pair, it is not expected for an unmatched module to be meaningful: if a marked import suspends but the corresponding export (whose execution led to the call to the suspending import) is not marked then the engine is expected to *trap*. If an export function is marked, but its execution never results in a call to a marked import, then the marked function returns a fully resolved `Promise`. + +### Restriction + +Only WebAssembly computations may be suspended using JSPI; this is enforced by requiring that only WebAssembly frames are active between the call to a `promising` function and any call to a `Suspending` wrapped import. + +## Examples + +Considering the expected applications of this API, we can consider two simple scenarios: that of a so-called *legacy C* application -- which is written in the style of a non-interactive application using synchronous APIs for reading and writing files -- and the *responsive C* application; where the application was typically written using an eventloop internal architecture but still uses synchronous APIs for I/O. + +### Supporting Access to Asynchronous Functions + +Our first example seems quite trivial, with the WebAssembly module: + +```wasm +(module + (import "js" "init_state" (func $init_state (result f64))) + (import "js" "compute_delta" (func $compute_delta (result f64))) + (global $state f64) + (func $init (global.set $state (call $init_state))) + (start $init) + (func $get_state (export "get_state") (result f64) (global.get $state)) + (func $update_state (export "update_state") (result f64) + (local $delta f64) + (local.set $delta (call $compute_delta)) + (global.set (f64.add (global.get $state) (local.get $delta) )) + (global.get $state) + ) +) +``` + +In this example, we have a WebAssembly module that is a very simple state machine—driven from JavaScript. Whenever the JavaScript client code wishes to update the state, it invokes the exported `update_state` function. In turn, the WebAssembly `update_state` function calls an import, `compute_delta`, to compute a delta to add to the state. + +On the JavaScript side, though, the function we want to use for computing the delta turns out to need to be run asynchronously; that is, it returns a `Promise` of a `Number` rather than a `Number` itself. In addition, we want to implement the `compute_delta` import by using JavaScript `fetch` to get the delta from the url `www.example.com/data.txt`. + +This expectation is reified in the JavaScript code for `compute_delta`: + +```js +var compute_delta = () => + fetch('https://example.com/data.txt') + .then(res => res.text()) + .then(txt => parseFloat(txt)); +``` + +In order to prepare our code for asynchrony, we wrap the `compute_delta` function using the `WebAssembly.Suspending` constructor. The complete import object looks like: + +```js +var init_state = () => 2.71; +var importObj = {js: { + init_state: init_state, + compute_delta: new WebAssembly.Suspending(compute_delta)}}; +``` + +In addition to preparing the import, we must also handle the export side. The process of wrapping exports is a little different to wrapping imports; in part because we prepare imports before instantiating modules and we wrap exports afterwards: + +```js +var sampleModule = new WebAssembly.Instance(demoBuffer,importObj); +var promise_update = WebAssembly.promising(sampleModule.exports.update_state) +``` + +At runtime, a call to the JavaScript function `promise_update` will get a `Promise`. As part of resolving that `Promise`, the WebAssembly exported `$update_state` function is called, which results in a call to the `$compute_delta` import. That, in turn, uses `fetch` to access a remote file, and parse the result in order to give the actual floating point value back to the WebAssembly module. Since we use a `WebAssembly.Suspending` wrapped function to implement `$compute_delta`, the import call will be suspended. + +When the `fetch` completes, the result is parsed -- which will also cause a suspension since getting the text from a `Response` also results in a `Promise`. This too will cause the application to be suspended; but when that finally is resumed the text is parsed and the result returned as a float to `$compute_delta`. + +After updating the internal state, the original export `$update_state` returns, which causes the `Promise` originally returned by `promise_update` to be resolved. At that point, anyone awaiting that `Promise` will be given the value returned by `$update_state`. + +### Supporting Responsive Applications with Reentrancy + +A responsive application is able to respond to new requests even while suspended for existing ones. Note that we are not concerned with *multi-threaded* applications (which can also be responsive): only one computation is expected to be active at any one time and all others would be *suspended*. Typically, such responsive applications are already crafted using an eventloop style architecture; even if they still use synchronous APIs. + +In fact, our example above is already technically re-entrant! We can call `promise_update` even before other calls to `promise_update` have returned. However, JSPI does not guarantee that the updates are completed in any particular order: it is up to the application developer to ensure that this is safe. In our specific case, it does not matter because all the fetch calls result in the same floating point number being accumulated to the `$state` global variable. + +Not all applications can equally tolerate being reentrant in this way. Certainly, languages in the C family do not make this straightforward. In fact, an application would typically have to have been engineered appropriately, by, for example, ensuring that each call to a suspending import does not interfere with globally shared state. + +However, desktop applications, written for operating systems such as Mac OS and Windows, are often already structured in terms of an event loop that monitors input events and schedules UI effects. Such an application can often make good use of JSPI: perhaps by removing the application's event loop and replacing it with the browser's event loop. + +## Specification + +### WebIDL Interface + +```idl +[Exposed=*] +partial namespace WebAssembly { + Function promising(Function wasmFun); +}; + +[LegacyNamespace=WebAssembly, Exposed=*] +interface Suspending { + constructor(Function jsFun); +}; +``` + +The `Suspending` object's role is primarily to annotate a function in a way that enables the `WebAssembly.instantiate` function to implement the import in a special way. Note that `WebAssembly.Suspending` has no externally visible attributes other than those inherited from `Object`. However, it does have an internal slot -- `[[wrappedFunction]]` -- which is referenced in the specifics of the algorithms below. + +### Exporting Promises + +The `WebAssembly.promising` function takes a WebAssembly function, as exported by a WebAssembly instance, and returns a JavaScript function that returns a `Promise`. The returned `Promise` will be resolved by the result of invoking the exported WebAssembly function. + +#### `WebAssembly.promising`(*`wasmFun`*) + +The `WebAssembly.promising` function takes a WebAssembly function -- i.e., not a JavaScript function -- and returns a JavaScript function that evaluates *`wasmFun`* and returns a `Promise` with the result: + +0. Let `wasmFunc` be the exported WebAssembly function that is passed to the `WebAssembly.promising` function, +1. if *`wasmFun`* does not contain a *`[[FunctionAddress]]`* hidden slot throw a *`TypeError`* exception. +2. create a function that will, when called with arguments `args`: + 1. Let `promise` be a new `Promise` constructed as though by the `Promise`(`fn`) constructor, where `fn` is a function of two arguments `accept` and `reject` that: + 1. lets `context` be a new [execution context](https://tc39.es/ecma262/#sec-execution-contexts), and sets the *running execution context* to `context` (pushing the existing *running execution context* onto the *execution context stack*). + 2. lets `result` be the result of calling `wasmFunc(args)` (or any trap or thrown exception) + 3. releases the execution `context`, which includes releasing any execution resources associated with the context. + 4. If `result` is not an exception or a trap, calls the `accept` function argument with the appropriate value. + 5. If `result` is an exception, or if it is a trap, calls the `reject` function with the raised exception. + 2. Returns `promise` to `caller` +3. Return created function as value of `WebAssembly.promising` + +Note that, if the function `wasmFunc` suspends (by invoking a `Promise` returning import), then the `promise` will be returned to the `caller` before `wasmFunc` returns. When `wasmFunc` completes eventually, then `promise` will be resolved -- and one of `accept` or `reject` will be invoked by the browser's microtask runner. + +### Suspendable functions + +We use the `Suspendable` constructor to signal to WebAssembly that a given import should cause a suspension of WebAssembly execution. + +#### `new WebAssembly.Suspending(`*`jsFun`*`)` + +The `WebAssembly.Suspending` constructor takes a JavaScript `Function` as an argument which is embedded in the `Suspending` object as the value of the `[[wrappedFunction]]` hidden slot. + +>Note that *`jsFun`* is expected to be a *JavaScript function*. This allows us to ignore certain so-called corner cases in the usage of JSPI: in particular there is no special handling of WebAssembly functions passed to `WebAssembly.Suspending`. + +1. If IsCallable(*jsFun*) is `false`, throw a `TypeError` exception. +1. Let *suspendingProto* be `WebAssembly.Suspendable.%prototype%` +1. Let *susp* be the result of `OrdinaryObjectCreate`(*`suspendingProto`*) +1. Assign the `[[wrappedFunction]]` internal slot of *`susp`* to *`jsFun`* +1. Return *susp* + +The most direct way that external functions can be accessed from a WebAssembly module is via imports.[^WebAssembly.Function] Therefore, we modify the *read-the-imports* algorithm to account for imports annotated as `Suspendable`. + +[^WebAssembly.Function]: The other way is by using a `WebAssembly.Function` constructor, as proposed in the [js-types proposal](https://github.com/WebAssembly/js-types). However, the semantics of `WebAssembly.Function` also revolve around modules and importing. + +We replace the section dealing with functions: + +3.4. If *externtype* is of the form `func` *functype* ... + +with: + +3.4. If *externtype* is of the form `func` *functype* + + 1. If `$IsCallable$`(*`v`*) is `true` + 1. If *`v`* has a *`[[FunctionAddress]]`* internal slot, and therefore is an *Exported Function*, + * Let *`funcaddr`* be the value of *`v`*'s *`[[FunctionAddress]]`* internal slot. + 1. Otherwise, + * *Create a host function* from *`v`* and *`functype`*, and let *`funcaddr`* be the result. + 1. If *`v`* has a *`[[wrappedFunction]]`* internal slot: + 1. Let *`func`* be *`v`*'s *`[[wrappedFunction]]`* slot. + 1. Assert `$IsCallable$`(*`func`*). + 1. Create a *suspending function* from *`func`* and *`functype`*, and let *`funcaddr`* be the result. + 1. If `$IsCallable$`(*`v`*) is `false` and *`v`* does not have a *`[[wrappedFunction]]`* internal slot then throw a `LinkError` exception. + 1. Let `*index*` be the number of external functions in *`imports`*. This value `*index*` is known as the *index of the host function* *`funcaddr`*. + 1. Let *`externfunc`* be the external value *`funcaddr`* + 1. Append *`externfunc`* to *`imports`*. + +The *suspending function* is a function whose behavior is determined as follows: + +1. Let `context` refer to the execution context that is current at the time of a call to the *suspending function*. Let `wfunc` be the wrapped function that was used when creating the *suspending function*. +1. Traps if `context`'s state is not **Active**[`caller`] for some `caller` +1. Let `result` be the result of calling `wfunc(args)` (or any trap or thrown exception) where `args` are the additional arguments passed to the call when the imported function was called from the WebAssembly module. +1. If `$IsPromise(result)`: + 1. Lets `frames` be the stack frames since `caller` + 1. Traps if there are any frames of non-WebAssembly functions in `frames` + 1. Changes `context`'s state to **Suspended** + 1. Returns the result of `result.then(onFulfilled, onRejected)` with functions `onFulfilled` and `onRejected` that do the following: + 1. Asserts that `context`'s state is **Suspended** (should be guaranteed) + 2. Changes `context`'s state to **Active**[`caller'`], where `caller'` is the caller of `onFulfilled`/`onRejected` + 3. * In the case of `onFulfilled`, converts the given value to `externref` and returns that to `frames` + * In the case of `onRejected`, throws the given value up to `frames` as an exception according to the JS API of the [Exception Handling](https://github.com/WebAssembly/exception-handling/) proposal. +1. Otherwise, returns the result to the caller: + * If `result` is a normal value, return as value of *suspending function* + * If `result` is an exception, throw exception. + +## Frequently Asked Questions + +1. **Why do we prevent JavaScript programs from using this API?** + + JavaScript already has a way of managing computations that can suspend. This is semantically connected to JavaScript `Promise` objects and the `async` function syntax. However, a more important reason is that we do not wish to inadvertently introduce features that can affect the behavior of existing JavaScript programs. + +1. **Why does `WebAssembly.promising` return a JavaScript `Function`**? + + This allows us to be more precise in a few special cases. In particular, if two WebAssembly modules (Modules A & B) are *chained together* (e.g., the import of module A is provided by the export of module B) then we need precision about the interaction with JSPI: + 1. If the link is composed of a `Suspending`/`promising` pair (i.e., the module A import is wrapped with `new WebAssembly.Suspending` and the module B export is wrapped with `WebAssembly.promising`) then, if module B suspends (via one of its imports), then module A will also suspend. However, there will also be an additional `Promise`: created from the wrapped export from module B. + 1. If the link is direct -- without a `Suspending`/`promising` pair -- then the connect is transparent from the perspective of JSPI; provided that there are no JavaScript function calls in the link. + +1. **What are the changes to the API?** + + The primary change to the JSPI API are the removal of the `Suspender` object and the detachment from the [*Type Reflection Proposal*](https://github.com/WebAssembly/js-types). The former change means that: + + 1. Suspending imports are always assumed to be of JavaScript functions (even if an import is actually bound to a WebAssembly function), + 1. when a wrapped import returns a `Promise`, the computation between the innermost wrapped `promising` export and the import is suspended. Previously, this was identified by the explicit `Suspender` object; and + 1. the JSPI API no longer uses a modification of the `WebAssembly.Function` constructor to convey intentions. As a result, it is no longer necessary for JavaScript code that is responsible for managing the instantiation of a WebAssembly module to be aware of the types of WebAssembly functions in imports and exports. diff --git a/test/js-api/js-promise-integration/js-promise-integration.any.js b/test/js-api/js-promise-integration/js-promise-integration.any.js new file mode 100644 index 0000000000..d473971138 --- /dev/null +++ b/test/js-api/js-promise-integration/js-promise-integration.any.js @@ -0,0 +1,486 @@ +// META: global=jsshell +// META: script=/wasm/jsapi/wasm-module-builder.js + +// Test for invalid wrappers +test(() => { + assert_throws(TypeError, () => WebAssembly.promising({}), + /Argument 0 must be a function/); + assert_throws(TypeError, () => WebAssembly.promising(() => {}), + /Argument 0 must be a WebAssembly exported function/); + assert_throws(TypeError, () => WebAssembly.Suspending(() => {}), + /WebAssembly.Suspending must be invoked with 'new'/); + assert_throws(TypeError, () => new WebAssembly.Suspending({}), + /Argument 0 must be a function/); + function asmModule() { + "use asm"; + function x(v) { + v = v | 0; + } + return x; + } + assert_throws(TypeError, () => WebAssembly.promising(asmModule()), + /Argument 0 must be a WebAssembly exported function/); +}); + +test(() => { + let builder = new WasmModuleBuilder(); + builder.addGlobal(kWasmI32, true).exportAs('g'); + builder.addFunction("test", kSig_i_v) + .addBody([ + kExprI32Const, 42, + kExprGlobalSet, 0, + kExprI32Const, 0]).exportFunc(); + let instance = builder.instantiate(); + let wrapper = WebAssembly.promising(instance.exports.test); + wrapper(); + assert_equals(42, instance.exports.g.value); +}); + +promise_test(async () => { + let builder = new WasmModuleBuilder(); + import_index = builder.addImport('m', 'import', kSig_i_i); + builder.addFunction("test", kSig_i_i) + .addBody([ + kExprLocalGet, 0, + kExprCallFunction, import_index, // suspend + ]).exportFunc(); + let js_import = WebAssembly.Suspending(() => Promise.resolve(42)); + let instance = builder.instantiate({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(await export_promise, 42); +}, "Suspend once"); + +promise_test(async () => { + let builder = new WasmModuleBuilder(); + builder.addGlobal(kWasmI32, true).exportAs('g'); + import_index = builder.addImport('m', 'import', kSig_i_i); + // void test() { + // for (i = 0; i < 5; ++i) { + // g = g + await import(); + // } + // } + builder.addFunction("test", kSig_v_i) + .addLocals({ i32_count: 1}) + .addBody([ + kExprI32Const, 5, + kExprLocalSet, 1, + kExprLoop, kWasmVoid, + kExprLocalGet, 0, + kExprCallFunction, import_index, // suspend + kExprGlobalGet, 0, + kExprI32Add, + kExprGlobalSet, 0, + kExprLocalGet, 1, + kExprI32Const, 1, + kExprI32Sub, + kExprLocalTee, 1, + kExprBrIf, 0, + kExprEnd, + ]).exportFunc(); + let i = 0; + function js_import() { + return Promise.resolve(++i); + }; + let wasm_js_import = WebAssembly.Suspending(js_import); + let instance = builder.instantiate({m: {import: wasm_js_import}}); + let wrapped_export = WebAssembly.promising(instance.exports.test); + let export_promise = wrapped_export(); + assert_equals(instance.exports.g.value, 0); + assert_true(export_promise instanceof Promise); + await export_promise; + assert_equals(instance.exports.g.value, 15); +}, "Suspend/resume in a loop"); + +promise_test(async ()=>{ + let builder = new WasmModuleBuilder(); + import_index = builder.addImport('m', 'import', kSig_i_v); + builder.addFunction("test", kSig_i_v) + .addBody([ + kExprCallFunction, import_index, // suspend + ]).exportFunc(); + let js_import = new WebAssembly.Suspending(() => Promise.resolve(42)); + let instance = builder.instantiate({m: {import: js_import}}); + let wrapped_export = WebAssembly.promising(instance.exports.test); + assert_equals(await wrapped_export(), 42); + + // Also try with a JS function with a mismatching arity. + js_import = new WebAssembly.Suspending((unused) => Promise.resolve(42)); + instance = builder.instantiate({m: {import: js_import}}); + wrapped_export = WebAssembly.promising(instance.exports.test); + assert_equals(await wrapped_export(), 42); + + // Also try with a proxy. + js_import = new WebAssembly.Suspending(new Proxy(() => Promise.resolve(42), {})); + instance = builder.instantiate({m: {import: js_import}}); + wrapped_export = WebAssembly.promising(instance.exports.test); + assert_equals(await wrapped_export(), 42); +}); + +function recordAbeforeB(){ + let AbeforeB = []; + let setA = ()=>{ + AbeforeB.push("A") + } + let setB = ()=>{ + AbeforeB.push("B") + } + let isAbeforeB = ()=> + AbeforeB[0]=="A" && AbeforeB[1]=="B"; + + let showAbeforeB = ()=>{ + console.log(AbeforeB) + } + return {setA : setA, setB : setB, isAbeforeB :isAbeforeB,showAbeforeB:showAbeforeB} +} + +promise_test(async () => { + let builder = new WasmModuleBuilder(); + let AbeforeB = recordAbeforeB(); + import42_index = builder.addImport('m', 'import42', kSig_i_i); + importSetA_index = builder.addImport('m', 'setA', kSig_v_v); + builder.addFunction("test", kSig_i_i) + .addBody([ + kExprLocalGet, 0, + kExprCallFunction, import42_index, // suspend? + kExprCallFunction, importSetA_index + ]).exportFunc(); + let import42 = WebAssembly.Suspending(()=>Promise.resolve(42)); + let instance = builder.instantiate({m: {import42: import42, + setA:AbeforeB.setA}}); + + let wrapped_export = WebAssembly.promising(instance.exports.test); + +// AbeforeB.showAbeforeB(); + let exported_promise = wrapped_export(); +// AbeforeB.showAbeforeB(); + + AbeforeB.setB(); + + assert_equals(await exported_promise, 42); +// AbeforeB.showAbeforeB(); + + assert_false(AbeforeB.isAbeforeB()); +}, "Make sure we actually suspend"); + +promise_test(async () => { + let builder = new WasmModuleBuilder(); + let AbeforeB = recordAbeforeB(); + import42_index = builder.addImport('m', 'import42', kSig_i_i); + importSetA_index = builder.addImport('m', 'setA', kSig_v_v); + builder.addFunction("test", kSig_i_i) + .addBody([ + kExprLocalGet, 0, + kExprCallFunction, import42_index, // suspend? + kExprCallFunction, importSetA_index + ]).exportFunc(); + let import42 = WebAssembly.Suspending(()=>42); + let instance = builder.instantiate({m: {import42: import42, + setA:AbeforeB.setA}}); + + let wrapped_export = WebAssembly.promising(instance.exports.test); + + let exported_promise = wrapped_export(); + AbeforeB.setB(); + + assert_equals(await exported_promise, 42); + // AbeforeB.showAbeforeB(); + + assert_true(AbeforeB.isAbeforeB()); +}, "Do not suspend if the import's return value is not a Promise"); + +test(t => { + console.log("Throw after the first suspension"); + let tag = new WebAssembly.Tag({parameters: []}); + let builder = new WasmModuleBuilder(); + import_index = builder.addImport('m', 'import', kSig_i_i); + tag_index = builder.addImportedTag('m', 'tag', kSig_v_v); + builder.addFunction("test", kSig_i_i) + .addBody([ + kExprLocalGet, 0, + kExprCallFunction, import_index, + kExprThrow, tag_index + ]).exportFunc(); + function js_import() { + return Promise.resolve(); + }; + let wasm_js_import = WebAssembly.Suspending(js_import); + + let instance = builder.instantiate({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 WebAssembly.Exception(tag, []), export_promise); +}, "Throw after the first suspension"); + +promise_test(async (t) => { + console.log("Rejecting promise"); + let tag = new WebAssembly.Tag({parameters: ['i32']}); + let builder = new WasmModuleBuilder(); + import_index = builder.addImport('m', 'import', kSig_i_i); + tag_index = builder.addImportedTag('m', 'tag', kSig_v_i); + builder.addFunction("test", kSig_i_i) + .addBody([ + kExprTry, kWasmI32, + kExprLocalGet, 0, + kExprCallFunction, import_index, + kExprCatch, tag_index, + kExprEnd + ]).exportFunc(); + function js_import() { + return Promise.reject(new WebAssembly.Exception(tag, [42])); + }; + let wasm_js_import = WebAssembly.Suspending(js_import); + + let instance = builder.instantiate({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); + assert_equals(await export_promise, 42); +}, "Rejecting promise"); + +async function TestNestedSuspenders(suspend) { + console.log("nested suspending "+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 JS function returns a regular value and + // no computation is suspended. + let builder = new WasmModuleBuilder(); + inner_index = builder.addImport('m', 'inner', kSig_i_i); + outer_index = builder.addImport('m', 'outer', kSig_i_i); + builder.addFunction("outer", kSig_i_i) + .addBody([ + kExprLocalGet, 0, + kExprCallFunction, outer_index + ]).exportFunc(); + builder.addFunction("inner", kSig_i_i) + .addBody([ + kExprLocalGet, 0, + kExprCallFunction, inner_index + ]).exportFunc(); + + let inner = WebAssembly.Suspending(() => suspend ? Promise.resolve(42) : 43); + + let export_inner; + let outer = WebAssembly.Suspending(() => export_inner()); + + let instance = builder.instantiate({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); + if(suspend) + assert_equals(await result, 42); + else + assert_equals(await result, 43); +} + +promise_test(async () => { + TestNestedSuspenders(true); +}, "Test nested suspenders with suspension"); + +promise_test(async () => { + TestNestedSuspenders(false); +}, "Test nested suspenders with no suspension"); + +test(() => { + console.log("Call import with an invalid suspender"); + let builder = new WasmModuleBuilder(); + let import_index = builder.addImport('m', 'import', kSig_i_i); + builder.addFunction("test", kSig_i_i) + .addBody([ + kExprLocalGet, 0, + kExprCallFunction, import_index, // suspend + ]).exportFunc(); + builder.addFunction("return_suspender", kSig_i_i) + .addBody([ + kExprLocalGet, 0 + ]).exportFunc(); + let js_import = WebAssembly.Suspending(() => Promise.resolve(42)); + let instance = builder.instantiate({m: {import: js_import}}); + let suspender = WebAssembly.promising(instance.exports.return_suspender)(); + for (s of [suspender, null, undefined, {}]) { + assert_throws(WebAssembly.SuspendError, () => instance.exports.test(s)); + } +}, "Call import with an invalid suspender"); + +// Throw an exception before suspending. The export wrapper should return a +// promise rejected with the exception. +promise_test(async (t) => { + let tag = new WebAssembly.Tag({parameters: []}); + let builder = new WasmModuleBuilder(); + tag_index = builder.addImportedTag('m', 'tag', kSig_v_v); + builder.addFunction("test", kSig_i_v) + .addBody([ + kExprThrow, tag_index + ]).exportFunc(); + + let instance = builder.instantiate({m: {tag: tag}}); + let wrapped_export = WebAssembly.promising(instance.exports.test); + let export_promise = wrapped_export(); + + promise_rejects(t, new WebAssembly.Exception(tag, []), export_promise); +}); + +// Throw an exception after the first resume event, which propagates to the +// promise wrapper. +promise_test(async (t) => { + let tag = new WebAssembly.Tag({parameters: []}); + let builder = new WasmModuleBuilder(); + import_index = builder.addImport('m', 'import', kSig_i_v); + tag_index = builder.addImportedTag('m', 'tag', kSig_v_v); + builder.addFunction("test", kSig_i_v) + .addBody([ + kExprCallFunction, import_index, + kExprThrow, tag_index + ]).exportFunc(); + function js_import() { + return Promise.resolve(42); + }; + let wasm_js_import = new WebAssembly.Suspending(js_import); + + let instance = builder.instantiate({m: {import: wasm_js_import, tag: tag}}); + let wrapped_export = WebAssembly.promising(instance.exports.test); + let export_promise = wrapped_export(); + + promise_rejects(t, new WebAssembly.Exception(tag, []), export_promise); +}); + +promise_test(async () => { + let tag = new WebAssembly.Tag({parameters: ['i32']}); + let builder = new WasmModuleBuilder(); + import_index = builder.addImport('m', 'import', kSig_i_v); + tag_index = builder.addImportedTag('m', 'tag', kSig_v_i); + builder.addFunction("test", kSig_i_v) + .addBody([ + kExprTry, kWasmI32, + kExprCallFunction, import_index, + kExprCatch, tag_index, + kExprEnd, + ]).exportFunc(); + function js_import() { + return Promise.reject(new WebAssembly.Exception(tag, [42])); + }; + let wasm_js_import = new WebAssembly.Suspending(js_import); + + let instance = builder.instantiate({m: {import: wasm_js_import, tag: tag}}); + let wrapped_export = WebAssembly.promising(instance.exports.test); + assert_equals(await wrapped_export(), 42); +}); + +test(() => { + console.log("no return allowed"); + // Check that a promising function with no return is allowed. + let builder = new WasmModuleBuilder(); + builder.addFunction("export", kSig_v_v).addBody([]).exportFunc(); + let instance = builder.instantiate(); + let export_wrapper = WebAssembly.promising(instance.exports.export); + assert_true(export_wrapper instanceof Function); +}); + +promise_test(async (t) => { + let builder = new WasmModuleBuilder(); + builder.addFunction("test", kSig_i_v) + .addBody([ + kExprCallFunction, 0 + ]).exportFunc(); + let instance = builder.instantiate(); + let wrapper = WebAssembly.promising(instance.exports.test); + + promise_rejects(t, new Error(), wrapper(), /Maximum call stack size exceeded/); +}); + +promise_test(async (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 builder = new WasmModuleBuilder(); + let import1_index = builder.addImport("m", "import1", kSig_i_v); + let import2_index = builder.addImport("m", "import2", kSig_i_v); + builder.addFunction("export1", kSig_i_v) + .addBody([ + // export1 -> import1 (unwrapped) + kExprCallFunction, import1_index, + ]).exportFunc(); + builder.addFunction("export2", kSig_i_v) + .addBody([ + // export2 -> import2 (suspending) + kExprCallFunction, import2_index, + ]).exportFunc(); + let instance; + function import1() { + // import1 -> export2 (unwrapped) + instance.exports.export2(); + } + function import2() { + return Promise.resolve(0); + } + import2 = new WebAssembly.Suspending(import2); + instance = builder.instantiate( + {'m': + {'import1': import1, + 'import2': import2 + }}); + // export1 (promising) + let wrapper = WebAssembly.promising(instance.exports.export1); + promise_rejects(t, new WebAssembly.SuspendError(), wrapper()); +}); + +promise_test(async () => { + let builder1 = new WasmModuleBuilder(); + import_index = builder1.addImport('m', 'import', kSig_i_v); + builder1.addFunction("f", kSig_i_v) + .addBody([ + kExprCallFunction, import_index, // suspend + kExprI32Const, 1, + kExprI32Add, + ]).exportFunc(); + let js_import = new WebAssembly.Suspending(() => Promise.resolve(1)); + let instance1 = builder1.instantiate({m: {import: js_import}}); + let builder2 = new WasmModuleBuilder(); + import_index = builder2.addImport('m', 'import', kSig_i_v); + builder2.addFunction("main", kSig_i_v) + .addBody([ + kExprCallFunction, import_index, + kExprI32Const, 1, + kExprI32Add, + ]).exportFunc(); + let instance2 = builder2.instantiate({m: {import: instance1.exports.f}}); + let wrapped_export = WebAssembly.promising(instance2.exports.main); + assert_equals(await wrapped_export(), 3); +}); + +test(() => { + let builder = new WasmModuleBuilder(); + let js_tag = builder.addImportedTag("", "tag", kSig_v_r); + try_sig_index = builder.addType(kSig_i_v); + + let promise42 = new WebAssembly.Suspending(() => Promise.resolve(42)); + let kPromise42Ref = builder.addImport("", "promise42", kSig_i_v); + + builder.addFunction("test", kSig_i_v) + .addBody([ + kExprTry, try_sig_index, + kExprCallFunction, kPromise42Ref, + kExprReturn, // If there was no trap or exception, return + kExprCatch, js_tag, + kExprI32Const, 43, + kExprReturn, + kExprEnd, + ]) + .exportFunc(); + + let instance = builder.instantiate({"": { + promise42: promise42, + tag: WebAssembly.JSTag, + }}); + + assert_equals(43, instance.exports.test()); +},"catch the bad suspension"); diff --git a/test/js-api/wasm-module-builder.js b/test/js-api/wasm-module-builder.js index 04f19b2785..1ffb00cbac 100644 --- a/test/js-api/wasm-module-builder.js +++ b/test/js-api/wasm-module-builder.js @@ -966,7 +966,7 @@ class WasmModuleBuilder { } let type_index = (typeof type) == "number" ? type : this.addType(type); this.imports.push({module: module, name: name, kind: kExternalFunction, - type: type_index}); + type_index: type_index}); return this.num_imported_funcs++; }