From 0c132a1c16de44be55edebe1c5a357b2d88181c0 Mon Sep 17 00:00:00 2001 From: Ross Tate Date: Mon, 23 Aug 2021 11:00:54 -0400 Subject: [PATCH 01/24] Initialize js-promise-integration repo --- README.md | 15 +- document/core/index.rst | 2 +- document/core/util/macros.def | 10 +- proposals/js-promise-integration/Overview.md | 163 +++++++++++++++++++ 4 files changed, 182 insertions(+), 8 deletions(-) create mode 100644 proposals/js-promise-integration/Overview.md diff --git a/README.md b/README.md index cbc17b462f..e126c19513 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,16 @@ -[![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. + +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 f2c6961250..0de3f09da0 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/proposals/js-promise-integration/Overview.md b/proposals/js-promise-integration/Overview.md new file mode 100644 index 0000000000..48884b8bed --- /dev/null +++ b/proposals/js-promise-integration/Overview.md @@ -0,0 +1,163 @@ +# JavaScript-Promise Integration Proposal + +## Summary + +The purpose of this proposal is to provide relatively efficient and relatively ergonimic interop between JavaScript promises and WebAssembly but working under the constraint that the only changes are to the JS API and not to core wasm. +The expectation is that the [Stack-Switching proposal](https://github.com/WebAssembly/stack-switching) will eventually extend core WebAssembly with the functionality to implement the operations we provide in this proposal directly within WebAssembly, along with many other valuable stack-switching operations, but that this particular use case for stack switching had sufficient urgency to merit a faster path via just the JS API. +For more information, please refer to the notes and slides for the [June 28, 2021 Stack Subgroup Meeting](https://github.com/WebAssembly/meetings/blob/main/stack/2021/sg-6-28.md), which details the usage scenarios and factors we took into consideration and summarizes the rationale for how we arrived at the following design. + +Following feedback that the Stacks Subgroup had received from TC39, this proposal allows *only* WebAssembly stacks to be suspended—it makes no changes to the JavaScript language and, in particular, does not indirectly enable support for detached `asycn`/`await` in JavaScript. + +This proposal depends (loosely) on the [js-types](https://github.com/WebAssembly/js-types/) proposal, which introduces `WebAssembly.Function` as a subclass of `Function`. + +## Interface + +The proposal is to add the following interface, constructor, and methods to the JS API, with further details on their semantics below. + +``` +interface Suspender { + constructor(); + Function suspendOnReturnedPromise(Function func); // import wrapper + // overloaded: WebAssembly.Function suspendOnReturnedPromise(WebAssembly.Function func); + WebAssembly.Function returnPromiseOnSuspend(WebAssembly.Function func); // export wrapper +} +``` + +## Example + +The following is an example of how we expect one to use this API. +In our usage scenarios, we found it useful to consider WebAssembly modules to conceputally have "synchronous" and "asynchronous" imports and exports. +The current JS API supports only "synchronous" imports and exports. +The methods of the Suspender interface are used to wrap relevant imports and exports in order to make "asynchronous", with the Suspender object itself explicitly connecting these imports and exports together to facilitate both implementation and composability. + +WebAssembly (`demo.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) + (global.set (f64.add (global.get $state) (call $compute_delta))) + (global.get $state) + ) +) +``` + +Text (`data.txt`): +``` +19827.987 +``` + +JavaScript: +``` +var suspender = new Suspender(); +var init_state = () => 2.71; +var compute_delta = () => fetch('data.txt').then(res => res.text()).then(txt => parseFloat(txt)); +var importObj = {js: { + init_state: init_state, + compute_delta: suspender.suspendOnReturnedPromise(compute_delta) +}}; + +fetch('demo.wasm').then(response => + response.arrayBuffer() +).then(buffer => + WebAssembly.instantiate(buffer, importObj) +).then(({module, instance}) => { + var get_state = instance.exports.get_state; + var update_state = suspender.returnPromiseOnSuspend(instance.exports.update_state); + ... +}); +``` + +In this example, we have a WebAssembly module that is a very simplistic state machine—every time you update the state, it simply calls an import 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. + +We can bridge this synchrony gap by using the new JS API. +In the example, an import of the WebAssembly module is wrapped using `suspender.suspendOnReturnedPromise`, and an export is wrapped using `suspender.returnPromiseOnSuspend`, both using the same `suspender`. +That `suspender` connects to the two together. +It makes it so that, if ever the (unwrapped) import returns a Promise, the (wrapped) export returns a Promise, with all the computation in between being "suspended" until the import's Promise resolves. +The wrapping of the export is essentially adding an `async` marker, and the wrapping of the import is essentially adding an `await` marker, but unlike JavaScript we do not have to explicitly thread `async`/`await` all the way through all the intermediate WebAssembly functions! + +Meanwhile, the call made to the `init_state` during initialization necessarily returns without suspending, and calls to the export `get_state` also always returns without suspending, so the proposal still supports the existing "synchronous" imports and exports the WebAssembly ecosystem uses today. +Of course, there are many details being skimmed over, such as the fact that if a synchronous export calls an asynchronous import then the program will trap if the import tries to suspend. +The following provides a more detailed specification as well as some implementation strategy. + +## Specification + +A `Suspender` is in one of the following states: +* **Inactive** - not being used at the moment +* **Active**[`caller`] - control is inside the `Suspender`, with `caller` being the function that called into the `Suspender` and is expecting an `externref` to be returned +* **Suspended** - currently waiting for some promise to resolve + +The method `suspender.returnPromiseOnSuspend(func)` asserts that `func` is a `WebAssembly.Function` with a function type of the form `[ti*] -> [to]` and then returns a `WebAssembly.Function` with function type `[ti*] -> [externref]` that does the following when called with arguments `args`: +1. Traps if `suspender`'s state is not **Inactive** +2. Changes `suspender`'s state to **Active**[`caller`] (where `caller` is the current caller) +3. Lets `result` be the result of calling `func(args)` (or any trap or thrown exception) +4. Asserts that `suspender`'s state is **Active**[`caller'`] for some `caller'` (should be guaranteed, though the caller might have changed) +5. Changes `suspender`'s state to **Inactive** +6. Returns (or rethrows) `result` to `caller'` + +The method `suspender.suspendOnReturnedPromise(func)` +* if `func` is a `WebAssembly.Function`, then asserts that its function type is of the form `[t*] -> [externref]` and returns a `WebAssembly.Function` with function type `[t*] -> [externref]`; +* otherwise, asserts that `func` is a `Function` and returns a `Function`. + +In either case, the function returned by `suspender.suspendOnReturnedPromise(func)` does the following when called with arguments `args`: +1. Lets `result` be the result of calling `func(args)` (or any trap or thrown exception) +2. If `result` is not a returned Promise, then returns (or rethrows) `result` +3. Traps if `suspender`'s state is not **Active**[`caller`] for some `caller` +4. Lets `frames` be the stack frames since `caller` +5. Traps if there are any frames of non-suspendable functions in `frames` +6. Changes `suspender`'s state to **Suspended** +7. Returns the result of `result.then(onFulfilled, onRejected)` with functions `onFulfilled` and `onRejected` that do the following: + 1. Asserts that `suspender`'s state is **Suspended** (should be guaranteed) + 2. Changes `suspender`'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 + +A function is suspendable if it was +* defined by a WebAssembly module, +* returned by `suspendOnReturnedPromise`, +* returned by `returnPromiseOnSuspend`, +* or generated by [creating a host function](https://webassembly.github.io/spec/js-api/index.html#create-a-host-function) for a suspendable function + +Importantly, functions written in JavaScript are *not* suspendable, conforming to feedback from members of [TC39](https://tc39.es/), and host functions (except for the few listed above) are *not* suspendable, conforming to feedback from engine maintainers. + +## Implementation + +The following is an implementation strategy for this proposal. +It assumes engine support for stack-switching, which of course is where the main implementation challenges lie. + +There are two kinds of stacks: a host (and JavaScript) stack, and a WebAssembly stack. Every WebAssembly stack has a suspender field called `suspender`. Every thread has a host stack. + +Every `Suspender` has two stack-reference fields: one called `caller` and one called `suspended`. +* In the **Inactive** state, both fields are null. +* In the **Active** state, the `caller` field references the (suspended) stack of the caller, and the `suspended` field is null +* In the **Suspended** state, the `suspended` field references the (suspended) WebAssembly stack currently associated with the suspender, and the `caller` field is null. + +`suspender.returnPromiseOnSuspend(func)(args)` is implemented by +1. Checking that `suspender.caller` and `suspended.suspended` are null (trapping otherwise) +2. Letting `stack` be a newly allocated WebAssembly stack associated with `suspender` +3. Switching to `stack` and storing the former stack in `suspender.caller` +4. Letting `result` be the result of `func(args)` (or any trap or thrown exception) +5. Switching to `suspender.caller` and setting it to null +6. Freeing `stack` +7. Returning (or rethrowing) `result` + +`suspender.suspendOnReturnedPromise(func)(args)` is implemented by +1. Calling `func(args)`, catching any trap or thrown exception +2. If `result` is not a returned Promise, returning (or rethrowing) `result` +3. Checking that `suspender.caller` is not null (trapping otherwise) +4. Let `stack` be the current stack +5. While `stack` is not a WebAssembly stack associated with `suspender`: + * Checking that `stack` is a WebAssembly stack (trapping otherwise) + * Updating `stack` to be `stack.suspender.caller` +6. Switching to `suspender.caller`, setting it to null, and storing the former stack in `suspender.suspended` +7. Returning the result of `result.then(onFulfilled, onRejected)` with functions `onFulfilled` and `onRejected` that are implemented by + 1. Switching to `suspender.suspended`, setting it to null, and storing the former stack in `suspender.caller` + 2. * In the case of `onFulfilled`, converting the given value to `externref` and returning it + * In the case of `onRejected`, rethrowing the given value + +The implementation of the function generated by [creating a host function](https://webassembly.github.io/spec/js-api/index.html#create-a-host-function) for a suspendable function is changed to first switch to the host stack of the current thread (if not already on it) and to lastly switch back to the former stack. From 5e252b09892269edbbc21e8b2ea8cf5d8a64718b Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Fri, 14 Jan 2022 10:36:32 -0800 Subject: [PATCH 02/24] Update README.md Added link to V8 design portfolio for stack switching --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index e126c19513..837cde52f4 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,8 @@ add support for different patterns of non-local control flow to WebAssembly. * 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 From 7506990193ea0a714b589663ca2a805fd4dff1c2 Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Tue, 13 Aug 2024 15:18:42 -0700 Subject: [PATCH 03/24] Pull interesting files from old branch --- proposals/js-promise-integration/Overview.md | 342 +++++++++++------- .../js-promise-integration.any.js | 285 +++++++++++++++ 2 files changed, 492 insertions(+), 135 deletions(-) create mode 100644 test/js-api/js-promise-integration/js-promise-integration.any.js diff --git a/proposals/js-promise-integration/Overview.md b/proposals/js-promise-integration/Overview.md index 48884b8bed..f4033613e5 100644 --- a/proposals/js-promise-integration/Overview.md +++ b/proposals/js-promise-integration/Overview.md @@ -2,162 +2,234 @@ ## Summary -The purpose of this proposal is to provide relatively efficient and relatively ergonimic interop between JavaScript promises and WebAssembly but working under the constraint that the only changes are to the JS API and not to core wasm. -The expectation is that the [Stack-Switching proposal](https://github.com/WebAssembly/stack-switching) will eventually extend core WebAssembly with the functionality to implement the operations we provide in this proposal directly within WebAssembly, along with many other valuable stack-switching operations, but that this particular use case for stack switching had sufficient urgency to merit a faster path via just the JS API. -For more information, please refer to the notes and slides for the [June 28, 2021 Stack Subgroup Meeting](https://github.com/WebAssembly/meetings/blob/main/stack/2021/sg-6-28.md), which details the usage scenarios and factors we took into consideration and summarizes the rationale for how we arrived at the following design. +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. -Following feedback that the Stacks Subgroup had received from TC39, this proposal allows *only* WebAssembly stacks to be suspended—it makes no changes to the JavaScript language and, in particular, does not indirectly enable support for detached `asycn`/`await` in JavaScript. +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. -This proposal depends (loosely) on the [js-types](https://github.com/WebAssembly/js-types/) proposal, which introduces `WebAssembly.Function` as a subclass of `Function`. +## Motivation -## Interface +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. -The proposal is to add the following interface, constructor, and methods to the JS API, with further details on their semantics below. +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. -``` -interface Suspender { - constructor(); - Function suspendOnReturnedPromise(Function func); // import wrapper - // overloaded: WebAssembly.Function suspendOnReturnedPromise(WebAssembly.Function func); - WebAssembly.Function returnPromiseOnSuspend(WebAssembly.Function func); // export wrapper -} -``` +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. -## Example +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. -The following is an example of how we expect one to use this API. -In our usage scenarios, we found it useful to consider WebAssembly modules to conceputally have "synchronous" and "asynchronous" imports and exports. -The current JS API supports only "synchronous" imports and exports. -The methods of the Suspender interface are used to wrap relevant imports and exports in order to make "asynchronous", with the Suspender object itself explicitly connecting these imports and exports together to facilitate both implementation and composability. +## Core concepts -WebAssembly (`demo.wasm`): -``` +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) - (global.set (f64.add (global.get $state) (call $compute_delta))) - (global.get $state) - ) + (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) + ) ) ``` -Text (`data.txt`): -``` -19827.987 -``` +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. -JavaScript: +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)); ``` -var suspender = new Suspender(); + +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 compute_delta = () => fetch('data.txt').then(res => res.text()).then(txt => parseFloat(txt)); var importObj = {js: { init_state: init_state, - compute_delta: suspender.suspendOnReturnedPromise(compute_delta) -}}; - -fetch('demo.wasm').then(response => - response.arrayBuffer() -).then(buffer => - WebAssembly.instantiate(buffer, importObj) -).then(({module, instance}) => { - var get_state = instance.exports.get_state; - var update_state = suspender.returnPromiseOnSuspend(instance.exports.update_state); - ... -}); + compute_delta: new WebAssembly.Suspending(compute_delta)}}; ``` -In this example, we have a WebAssembly module that is a very simplistic state machine—every time you update the state, it simply calls an import 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 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. -We can bridge this synchrony gap by using the new JS API. -In the example, an import of the WebAssembly module is wrapped using `suspender.suspendOnReturnedPromise`, and an export is wrapped using `suspender.returnPromiseOnSuspend`, both using the same `suspender`. -That `suspender` connects to the two together. -It makes it so that, if ever the (unwrapped) import returns a Promise, the (wrapped) export returns a Promise, with all the computation in between being "suspended" until the import's Promise resolves. -The wrapping of the export is essentially adding an `async` marker, and the wrapping of the import is essentially adding an `await` marker, but unlike JavaScript we do not have to explicitly thread `async`/`await` all the way through all the intermediate WebAssembly functions! +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. -Meanwhile, the call made to the `init_state` during initialization necessarily returns without suspending, and calls to the export `get_state` also always returns without suspending, so the proposal still supports the existing "synchronous" imports and exports the WebAssembly ecosystem uses today. -Of course, there are many details being skimmed over, such as the fact that if a synchronous export calls an asynchronous import then the program will trap if the import tries to suspend. -The following provides a more detailed specification as well as some implementation strategy. +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 -A `Suspender` is in one of the following states: -* **Inactive** - not being used at the moment -* **Active**[`caller`] - control is inside the `Suspender`, with `caller` being the function that called into the `Suspender` and is expecting an `externref` to be returned -* **Suspended** - currently waiting for some promise to resolve - -The method `suspender.returnPromiseOnSuspend(func)` asserts that `func` is a `WebAssembly.Function` with a function type of the form `[ti*] -> [to]` and then returns a `WebAssembly.Function` with function type `[ti*] -> [externref]` that does the following when called with arguments `args`: -1. Traps if `suspender`'s state is not **Inactive** -2. Changes `suspender`'s state to **Active**[`caller`] (where `caller` is the current caller) -3. Lets `result` be the result of calling `func(args)` (or any trap or thrown exception) -4. Asserts that `suspender`'s state is **Active**[`caller'`] for some `caller'` (should be guaranteed, though the caller might have changed) -5. Changes `suspender`'s state to **Inactive** -6. Returns (or rethrows) `result` to `caller'` - -The method `suspender.suspendOnReturnedPromise(func)` -* if `func` is a `WebAssembly.Function`, then asserts that its function type is of the form `[t*] -> [externref]` and returns a `WebAssembly.Function` with function type `[t*] -> [externref]`; -* otherwise, asserts that `func` is a `Function` and returns a `Function`. - -In either case, the function returned by `suspender.suspendOnReturnedPromise(func)` does the following when called with arguments `args`: -1. Lets `result` be the result of calling `func(args)` (or any trap or thrown exception) -2. If `result` is not a returned Promise, then returns (or rethrows) `result` -3. Traps if `suspender`'s state is not **Active**[`caller`] for some `caller` -4. Lets `frames` be the stack frames since `caller` -5. Traps if there are any frames of non-suspendable functions in `frames` -6. Changes `suspender`'s state to **Suspended** -7. Returns the result of `result.then(onFulfilled, onRejected)` with functions `onFulfilled` and `onRejected` that do the following: - 1. Asserts that `suspender`'s state is **Suspended** (should be guaranteed) - 2. Changes `suspender`'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 - -A function is suspendable if it was -* defined by a WebAssembly module, -* returned by `suspendOnReturnedPromise`, -* returned by `returnPromiseOnSuspend`, -* or generated by [creating a host function](https://webassembly.github.io/spec/js-api/index.html#create-a-host-function) for a suspendable function - -Importantly, functions written in JavaScript are *not* suspendable, conforming to feedback from members of [TC39](https://tc39.es/), and host functions (except for the few listed above) are *not* suspendable, conforming to feedback from engine maintainers. - -## Implementation - -The following is an implementation strategy for this proposal. -It assumes engine support for stack-switching, which of course is where the main implementation challenges lie. - -There are two kinds of stacks: a host (and JavaScript) stack, and a WebAssembly stack. Every WebAssembly stack has a suspender field called `suspender`. Every thread has a host stack. - -Every `Suspender` has two stack-reference fields: one called `caller` and one called `suspended`. -* In the **Inactive** state, both fields are null. -* In the **Active** state, the `caller` field references the (suspended) stack of the caller, and the `suspended` field is null -* In the **Suspended** state, the `suspended` field references the (suspended) WebAssembly stack currently associated with the suspender, and the `caller` field is null. - -`suspender.returnPromiseOnSuspend(func)(args)` is implemented by -1. Checking that `suspender.caller` and `suspended.suspended` are null (trapping otherwise) -2. Letting `stack` be a newly allocated WebAssembly stack associated with `suspender` -3. Switching to `stack` and storing the former stack in `suspender.caller` -4. Letting `result` be the result of `func(args)` (or any trap or thrown exception) -5. Switching to `suspender.caller` and setting it to null -6. Freeing `stack` -7. Returning (or rethrowing) `result` - -`suspender.suspendOnReturnedPromise(func)(args)` is implemented by -1. Calling `func(args)`, catching any trap or thrown exception -2. If `result` is not a returned Promise, returning (or rethrowing) `result` -3. Checking that `suspender.caller` is not null (trapping otherwise) -4. Let `stack` be the current stack -5. While `stack` is not a WebAssembly stack associated with `suspender`: - * Checking that `stack` is a WebAssembly stack (trapping otherwise) - * Updating `stack` to be `stack.suspender.caller` -6. Switching to `suspender.caller`, setting it to null, and storing the former stack in `suspender.suspended` -7. Returning the result of `result.then(onFulfilled, onRejected)` with functions `onFulfilled` and `onRejected` that are implemented by - 1. Switching to `suspender.suspended`, setting it to null, and storing the former stack in `suspender.caller` - 2. * In the case of `onFulfilled`, converting the given value to `externref` and returning it - * In the case of `onRejected`, rethrowing the given value - -The implementation of the function generated by [creating a host function](https://webassembly.github.io/spec/js-api/index.html#create-a-host-function) for a suspendable function is changed to first switch to the host stack of the current thread (if not already on it) and to lastly switch back to the former stack. +### 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..f14073a73a --- /dev/null +++ b/test/js-api/js-promise-integration/js-promise-integration.any.js @@ -0,0 +1,285 @@ +// META: global=jsshell +// META: script=/wasm/jsapi/wasm-module-builder.js + +function ToPromising(wasm_export) { + let sig = WebAssembly.Function.type(wasm_export); + 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(() => { + let builder = new WasmModuleBuilder(); + let sig_i_ri = makeSig([kWasmAnyRef, kWasmI32], [kWasmI32]); + let sig_v_ri = makeSig([kWasmAnyRef, kWasmI32], []); + builder.addImport('m', 'import', sig_v_ri); + builder.addFunction("export", sig_i_ri) + .addBody([kExprLocalGet, 1]).exportFunc(); + builder.addFunction("void_export", kSig_v_r).addBody([]).exportFunc(); + function js_import(i) {} + + // Wrap the import, instantiate the module, and wrap the export. + let import_wrapper = new WebAssembly.Function( + {parameters: ['externref', 'i32'], results: []}, + js_import, + {suspending: 'first'}); + let instance = builder.instantiate({'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(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 = WebAssembly.Function.type(export_wrapper); + assert_array_equals(['i32'], export_sig.parameters); + assert_array_equals(['externref'], export_sig.results); + + let import_sig = WebAssembly.Function.type(import_wrapper); + 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 = WebAssembly.Function.type(void_export_wrapper); + 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 builder = new WasmModuleBuilder(); + import_index = builder.addImport('m', 'import', kSig_i_r); + builder.addFunction("test", kSig_i_r) + .addBody([ + kExprLocalGet, 0, + kExprCallFunction, import_index, // suspend + ]).exportFunc(); + let js_import = new WebAssembly.Function( + {parameters: ['externref'], results: ['i32']}, + () => Promise.resolve(42), + {suspending: 'first'}); + let instance = builder.instantiate({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 builder = new WasmModuleBuilder(); + builder.addGlobal(kWasmI32, true).exportAs('g'); + import_index = builder.addImport('m', 'import', kSig_i_r); + // void test() { + // for (i = 0; i < 5; ++i) { + // g = g + await import(); + // } + // } + builder.addFunction("test", kSig_v_r) + .addLocals({ i32_count: 1}) + .addBody([ + kExprI32Const, 5, + kExprLocalSet, 1, + kExprLoop, kWasmStmt, + 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 = new WebAssembly.Function( + {parameters: ['externref'], results: ['i32']}, + js_import, + {suspending: 'first'}); + let instance = builder.instantiate({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"); + +test(() => { + let builder = new WasmModuleBuilder(); + import_index = builder.addImport('m', 'import', kSig_i_r); + builder.addFunction("test", kSig_i_r) + .addBody([ + kExprLocalGet, 0, + kExprCallFunction, import_index, // suspend + ]).exportFunc(); + function js_import() { + return 42 + }; + let wasm_js_import = new WebAssembly.Function( + {parameters: ['externref'], results: ['i32']}, + js_import, + {suspending: 'first'}); + let instance = builder.instantiate({m: {import: wasm_js_import}}); + let wrapped_export = ToPromising(instance.exports.test); + assert_equals(42, wrapped_export()); +}, "Do not suspend if the import's return value is not a Promise"); + +test(t => { + let tag = new WebAssembly.Tag({parameters: []}); + let builder = new WasmModuleBuilder(); + import_index = builder.addImport('m', 'import', kSig_i_r); + tag_index = builder.addImportedException('m', 'tag', kSig_v_v); + builder.addFunction("test", kSig_i_r) + .addBody([ + kExprLocalGet, 0, + kExprCallFunction, import_index, + kExprThrow, tag_index + ]).exportFunc(); + function js_import() { + return Promise.resolve(); + }; + let wasm_js_import = new WebAssembly.Function( + {parameters: ['externref'], results: ['i32']}, + js_import, + {suspending: 'first'}); + + let instance = builder.instantiate({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 WebAssembly.Exception(tag, []), export_promise); +}, "Throw after the first suspension"); + +promise_test(async () => { + let tag = new WebAssembly.Tag({parameters: ['i32']}); + let builder = new WasmModuleBuilder(); + import_index = builder.addImport('m', 'import', kSig_i_r); + tag_index = builder.addImportedException('m', 'tag', kSig_v_i); + builder.addFunction("test", kSig_i_r) + .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 = new WebAssembly.Function( + {parameters: ['externref'], results: ['i32']}, + js_import, + {suspending: 'first'}); + + let instance = builder.instantiate({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); + assert_equals(42, await 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 JS function returns a regular value and + // no computation is suspended. + let builder = new WasmModuleBuilder(); + inner_index = builder.addImport('m', 'inner', kSig_i_r); + outer_index = builder.addImport('m', 'outer', kSig_i_r); + builder.addFunction("outer", kSig_i_r) + .addBody([ + kExprLocalGet, 0, + kExprCallFunction, outer_index + ]).exportFunc(); + builder.addFunction("inner", kSig_i_r) + .addBody([ + kExprLocalGet, 0, + kExprCallFunction, inner_index + ]).exportFunc(); + + 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']}, + () => export_inner(), + {suspending: 'first'}); + + let instance = builder.instantiate({m: {inner, outer}}); + export_inner = ToPromising(instance.exports.inner); + let export_outer = ToPromising(instance.exports.outer); + let result = export_outer(); + if (suspend) { + assert_true(result instanceof Promise); + assert_equals(42, await result); + } else { + assert_equals(43, result); + } +} + +test(() => { + TestNestedSuspenders(true); +}, "Test nested suspenders with suspension"); + +test(() => { + TestNestedSuspenders(false); +}, "Test nested suspenders with no suspension"); + +test(() => { + let builder = new WasmModuleBuilder(); + let import_index = builder.addImport('m', 'import', kSig_i_r); + builder.addFunction("test", kSig_i_r) + .addBody([ + kExprLocalGet, 0, + kExprCallFunction, import_index, // suspend + ]).exportFunc(); + builder.addFunction("return_suspender", kSig_r_r) + .addBody([ + kExprLocalGet, 0 + ]).exportFunc(); + let js_import = new WebAssembly.Function( + {parameters: ['externref'], results: ['i32']}, + () => Promise.resolve(42), + {suspending: 'first'}); + let instance = builder.instantiate({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"); From 38553768bb7d9d637ad3c382932feaf5c23731f9 Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Thu, 19 Sep 2024 11:22:16 -0700 Subject: [PATCH 04/24] Updated test suite for jspi to reflect new API --- .../js-promise-integration.any.js | 472 ++++++++++++------ 1 file changed, 328 insertions(+), 144 deletions(-) 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 index f14073a73a..e931e8c4e0 100644 --- a/test/js-api/js-promise-integration/js-promise-integration.any.js +++ b/test/js-api/js-promise-integration/js-promise-integration.any.js @@ -1,108 +1,80 @@ // META: global=jsshell // META: script=/wasm/jsapi/wasm-module-builder.js -function ToPromising(wasm_export) { - let sig = WebAssembly.Function.type(wasm_export); - 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'}); +function Promising(wasm_export) { + return WebAssembly.promising(wasm_export); +} + +function Suspending(jsFun){ + return new WebAssembly.Suspending(jsFun); } +// 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(); - let sig_i_ri = makeSig([kWasmAnyRef, kWasmI32], [kWasmI32]); - let sig_v_ri = makeSig([kWasmAnyRef, kWasmI32], []); - builder.addImport('m', 'import', sig_v_ri); - builder.addFunction("export", sig_i_ri) - .addBody([kExprLocalGet, 1]).exportFunc(); - builder.addFunction("void_export", kSig_v_r).addBody([]).exportFunc(); - function js_import(i) {} - - // Wrap the import, instantiate the module, and wrap the export. - let import_wrapper = new WebAssembly.Function( - {parameters: ['externref', 'i32'], results: []}, - js_import, - {suspending: 'first'}); - let instance = builder.instantiate({'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(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 = WebAssembly.Function.type(export_wrapper); - assert_array_equals(['i32'], export_sig.parameters); - assert_array_equals(['externref'], export_sig.results); - - let import_sig = WebAssembly.Function.type(import_wrapper); - 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 = WebAssembly.Function.type(void_export_wrapper); - assert_array_equals([], void_export_sig.parameters); - assert_array_equals(['externref'], void_export_sig.results); -}, "Test import and export type checking"); + builder.addGlobal(kWasmI32, true, false).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(); + assertEquals(42, instance.exports.g.value); +}); promise_test(async () => { let builder = new WasmModuleBuilder(); - import_index = builder.addImport('m', 'import', kSig_i_r); - builder.addFunction("test", kSig_i_r) + 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 = new WebAssembly.Function( - {parameters: ['externref'], results: ['i32']}, - () => Promise.resolve(42), - {suspending: 'first'}); + let js_import = Suspending(() => Promise.resolve(42)); let instance = builder.instantiate({m: {import: js_import}}); - let wrapped_export = ToPromising(instance.exports.test); + let wrapped_export = Promising(instance.exports.test); let export_promise = wrapped_export(); assert_true(export_promise instanceof Promise); - assert_equals(42, await export_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_r); + 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_r) + builder.addFunction("test", kSig_v_i) .addLocals({ i32_count: 1}) .addBody([ kExprI32Const, 5, kExprLocalSet, 1, - kExprLoop, kWasmStmt, + kExprLoop, kWasmVoid, kExprLocalGet, 0, kExprCallFunction, import_index, // suspend kExprGlobalGet, 0, @@ -119,45 +91,120 @@ promise_test(async () => { function js_import() { return Promise.resolve(++i); }; - let wasm_js_import = new WebAssembly.Function( - {parameters: ['externref'], results: ['i32']}, - js_import, - {suspending: 'first'}); + let wasm_js_import = Suspending(js_import); let instance = builder.instantiate({m: {import: wasm_js_import}}); - let wrapped_export = ToPromising(instance.exports.test); + let wrapped_export = Promising(instance.exports.test); let export_promise = wrapped_export(); - assert_equals(0, instance.exports.g.value); + assert_equals(instance.exports.g.value, 0); assert_true(export_promise instanceof Promise); await export_promise; - assert_equals(15, instance.exports.g.value); + assert_equals(instance.exports.g.value, 15); }, "Suspend/resume in a loop"); -test(() => { +promise_test(async ()=>{ let builder = new WasmModuleBuilder(); - import_index = builder.addImport('m', 'import', kSig_i_r); - builder.addFunction("test", kSig_i_r) + 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, import_index, // suspend + kExprCallFunction, import42_index, // suspend? + kExprCallFunction, importSetA_index ]).exportFunc(); - function js_import() { - return 42 - }; - let wasm_js_import = new WebAssembly.Function( - {parameters: ['externref'], results: ['i32']}, - js_import, - {suspending: 'first'}); - let instance = builder.instantiate({m: {import: wasm_js_import}}); - let wrapped_export = ToPromising(instance.exports.test); - assert_equals(42, wrapped_export()); + let import42 = Suspending(()=>Promise.resolve(42)); + let instance = builder.instantiate({m: {import42: import42, + setA:AbeforeB.setA}}); + + let wrapped_export = Promising(instance.exports.test); + +// AbeforeB.showAbeforeB(); + 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 = Suspending(()=>42); + let instance = builder.instantiate({m: {import42: import42, + setA:AbeforeB.setA}}); + + let wrapped_export = Promising(instance.exports.test); + + 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_r); - tag_index = builder.addImportedException('m', 'tag', kSig_v_v); - builder.addFunction("test", kSig_i_r) + 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, @@ -166,47 +213,43 @@ test(t => { function js_import() { return Promise.resolve(); }; - let wasm_js_import = new WebAssembly.Function( - {parameters: ['externref'], results: ['i32']}, - js_import, - {suspending: 'first'}); + let wasm_js_import = Suspending(js_import); let instance = builder.instantiate({m: {import: wasm_js_import, tag: tag}}); - let wrapped_export = ToPromising(instance.exports.test); + let wrapped_export = 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 () => { +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_r); - tag_index = builder.addImportedException('m', 'tag', kSig_v_i); - builder.addFunction("test", kSig_i_r) + 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, + kExprEnd ]).exportFunc(); function js_import() { return Promise.reject(new WebAssembly.Exception(tag, [42])); }; - let wasm_js_import = new WebAssembly.Function( - {parameters: ['externref'], results: ['i32']}, - js_import, - {suspending: 'first'}); + let wasm_js_import = Suspending(js_import); let instance = builder.instantiate({m: {import: wasm_js_import, tag: tag}}); - let wrapped_export = ToPromising(instance.exports.test); + let wrapped_export = Promising(instance.exports.test); let export_promise = wrapped_export(); assert_true(export_promise instanceof Promise); - assert_equals(42, await export_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 @@ -217,69 +260,210 @@ async function TestNestedSuspenders(suspend) { // 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_r); - outer_index = builder.addImport('m', 'outer', kSig_i_r); - builder.addFunction("outer", kSig_i_r) + 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_r) + builder.addFunction("inner", kSig_i_i) .addBody([ kExprLocalGet, 0, kExprCallFunction, inner_index ]).exportFunc(); - let inner = new WebAssembly.Function( - {parameters: ['externref'], results: ['i32']}, - () => suspend ? Promise.resolve(42) : 43, - {suspending: 'first'}); + let inner = Suspending(() => suspend ? Promise.resolve(42) : 43); let export_inner; - let outer = new WebAssembly.Function( - {parameters: ['externref'], results: ['i32']}, - () => export_inner(), - {suspending: 'first'}); + let outer = Suspending(() => export_inner()); let instance = builder.instantiate({m: {inner, outer}}); - export_inner = ToPromising(instance.exports.inner); - let export_outer = ToPromising(instance.exports.outer); + export_inner = Promising(instance.exports.inner); + let export_outer = Promising(instance.exports.outer); let result = export_outer(); - if (suspend) { - assert_true(result instanceof Promise); - assert_equals(42, await result); - } else { - assert_equals(43, result); - } + assert_true(result instanceof Promise); + if(suspend) + assert_equals(await result, 42); + else + assert_equals(await result, 43); } -test(() => { +promise_test(async () => { TestNestedSuspenders(true); }, "Test nested suspenders with suspension"); -test(() => { +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_r); - builder.addFunction("test", kSig_i_r) + 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_r_r) + builder.addFunction("return_suspender", kSig_i_i) .addBody([ kExprLocalGet, 0 ]).exportFunc(); - let js_import = new WebAssembly.Function( - {parameters: ['externref'], results: ['i32']}, - () => Promise.resolve(42), - {suspending: 'first'}); + let js_import = Suspending(() => Promise.resolve(42)); let instance = builder.instantiate({m: {import: js_import}}); - let suspender = ToPromising(instance.exports.return_suspender)(); + let suspender = Promising(instance.exports.return_suspender)(); for (s of [suspender, null, undefined, {}]) { assert_throws(WebAssembly.RuntimeError, () => 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); + let export_sig = export_wrapper.type(); + assert_array_equals(export_sig.parameters, []); + assert_array_equals(export_sig.results, ['externref']); +}); + +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, RangeError, 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, WebAssembly.RuntimeError, wrapper(), + /trying to suspend JS frames/); +}); + +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); +}); From 928c4d7e3cfafc7f3c1aeb718b4d24d4daea8c04 Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Thu, 19 Sep 2024 14:27:38 -0700 Subject: [PATCH 05/24] A new document outlining some of the implementation issues in JSPI --- .../ImplementationGuide.md | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 proposals/js-promise-integration/ImplementationGuide.md 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 From e422a12698508be728a421042a5f2cbd141c2b95 Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Thu, 19 Sep 2024 16:07:47 -0700 Subject: [PATCH 06/24] Update to reflect upstream changes to index.bs --- document/js-api/index.bs | 225 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 210 insertions(+), 15 deletions(-) diff --git a/document/js-api/index.bs b/document/js-api/index.bs index 88cace6a47..39b506caaa 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 @@ -385,13 +396,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]] insternal 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|, @@ -408,6 +424,10 @@ A {{Module}} object represents a single WebAssembly module. Each {{Module}} obje 1. Let |store| be the [=surrounding agent=]'s [=associated store=]. 1. Let (|store|, |globaladdr|) be [=global_alloc=](|store|, [=const=] |valtype|, |value|). 1. Set the [=surrounding agent=]'s [=associated store=] to |store|. + 1. Otherwise, if |v| [=implements=] {{Global}}, + 1. Let |globaladdr| be |v|.\[[Global]]. + 1. Otherwise, + 1. Throw a {{LinkError}} exception. 1. Let |externglobal| be [=external value|global=] |globaladdr|. 1. [=list/Append=] |externglobal| to |imports|. 1. If |externtype| is of the form [=external-type/mem=] memtype, @@ -1126,12 +1146,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}}. + 1. If |parameters| or |results| contain [=v128=], throw a {{TypeError}}. Note: the above error is thrown each time the \[[Call]] method is invoked. 1. Let |args| be « ». @@ -1141,6 +1158,14 @@ 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. @@ -1169,13 +1194,23 @@ Note: Exported Functions do not have a \[[Construct]] method and thus it is not
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|] → [|resultTypes|] be |functype|. + 1. Let |jsArguments| be the result of [=coerce WebAssembly arguments=](|parameters|,|arguments|) + 1. Let |ret| be [=?=] [$Call$](|func|, undefined, |jsArguments|). + 1. Return the result of performing [=coerce a JavaScript return=] on |resultTypes| and |ret|. +
- 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 +1225,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: @@ -1329,6 +1376,154 @@ The algorithm ToWebAssemblyValue(|v|, |type|) coerces a JavaScript va
+

JavaScript Promise Integration

+ +A {{Suspending}} object represents a JavaScript function whose calls via WebAssembly imports should be *suspended* when they return a Promise object. +Each {{Suspending}} 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. + +
+[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 parameters that captures |wasmFunc| that, when invoked, performs [=run a Promising function=]. + 1. Returns [=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=] of no arguments that captures |promise|, |funcaddr|, |arguments| that, when invoked, performs 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 (|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. Set the [=surrounding agent=]'s [=associated store=] to |store|. + 1. If |result| is [=error=], throw an exception. This exception should be a WebAssembly {{RuntimeError}} exception, unless otherwise indicated by the WebAssembly error mapping. + 1. Otherwise, if |result| is of the form [=throw=] exnaddr, + 1. Perform [=EvaluateCall=] (|promise|.\[[Reject]],|result|.,false). + 1. Return UNDETERMINED + 1. Otherwise, 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. Perform [=EvaluateCall=] (|promise|.\[[Resolve]],|jsReturnValue|,false) + 1. Return UNDETERMINED + 1. Let |con| be [=CreateBuiltinFunction=](|runner|,0,"",<<>>) + 1. Perform [$Call$](|con|, undefined, <<>>) + 1. Returns |promise| +
+ +Note: The extra |$Call$| in the above algorithm ensures that the creation of the Promise is separated from the fullfilling of that Promise. In effect, this allows suspension of the fullfillment to occur whilst allowing the creation of the Promise itself to continue. + +
+ 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|) + 1. Assign the \[[wrappedFunction]] internal slot of |susp| 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. [=Prepare to run script=] with |relevant settings|. + 1. [=Prepare to run a callback=] with |stored settings|. + 1. Let [|parameters|] → [|results|] be |functype|. + 1. Let |jsArguments| be the result of [=coerce WebAssembly arguments=](|parameters|,|arguments|) + 1. Let |ret| be [=?=] [$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. Perform the [=Pause=] procedure, returning a new continuation |k|. + 1. Let |resolved| be an [=AbstractClosure=] with parameters (|v|) that captures |functype|, |k| and performs the following steps when called: + 1. Let |resultsSize| be |results|'s [=list/size=]. + 1. If |resultsSize| is not 1, throw a {{TypeError}}. + 1. Let |resultType| be [|results|].[0]. + 1. Let |returnValue| be [=ToWebAssemblyValue=](|v|, |resultType|). + 1. Perform the [=Enter=] procedure, passing it the captured continuation |k| the coerced |returnValue| and |async_context|. + 1. Return undefined. + 1. Let |onResolved| be [=CreateBuiltinFunction=](|resolved|,1,"",[]) + 1. Let |rejected| be an [=AbstractClosure=] with parameters (|e|) that captures |async_context| and performs the following steps when called: + 1. Perform the [=Reject=] procedure, passing it the captured continuation |k| the exception value |e| and |async_context|. + 1. Return undefined. + 1. Let |onRejected| be [=CreateBuiltinFunction=](|rejected|,1,"",[]) + 1. Perform [$PerformPromiseThen$](|promise|, |onResolved|, |onRejected|). + 1. Return undefined + 1. Return the result of performing [=coerce a JavaScript return=] on |results| 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|. +
+ +
+An [=execution context=] can be marked Paused to signal that the computation associated with the [=execution context=] has been paused. +
+ +
+The Pause abstract operation takes a currently executing algorithm and pauses it. The most important parameter |ec| of this operation is an +[=Execution Context=] which must be the most recent entry in the [=Execution Context Stack=] and which defines which operation is being paused. +The result of pausing an operation is a continuation |k| and a newly surfaced [=Execution Context=] -- which must be the [=Execution Context=] immediately +beneath the one on the [=Execution Context Stack=] that is [=Paused|paused=]. + + 1. If |ec| is not the most recent entry on the [=Execution Context Stack=] then trap. + 1. Let |k| be a continuation that represents the remainder of the computation associated with |ec|. + 1. Remove |ec| from the [=Execution Context Stack=] and mark it as being [=Paused|paused=]. + 1. Assert that the [=Execution Context Stack=] is not empty. + 1. Return |k| +
+ +
+The Enter abstract operation takes a continuation object |k|, a value object |v| and an [=execution context=] |ec|, and resumes the operations defined by |k|. + 1. If |ec| is not marked as [=Paused|paused=] then trap. + 1. Mark |ec| as not [=Paused|paused=]. + 1. Push |ec| on to the [=Execution Context Stack=], making |ec| the current execution context. + 1. Resume the operations defined by |k|, passing |v| as the value of the last instruction being performed by |k| + 1. Return undefined +
+ +
+The Reject abstract operation takes a continuation object |k|, an exception object |e| and an [=execution context=] |ec|, and resumes the operations defined by |k| by throwing an exception to the [=Paused|paused=] computation. + 1. If |ec| is not marked as [=Paused|paused=] then trap. + 1. Mark |ec| as not [=Paused|paused=]. + 1. Push |ec| on to the [=Execution Context Stack=], making |ec| the current execution context. + 1. Let |type|, |payload| and |opaqueData| be the result of performing [=coerce a JavaScript exception=} on |e| + 1. Resume the operations defined by |k|, performing [=WebAssembly/Throw=] with |type|, |payload| and |opaqueData|. +
+

Tags

From 8586e74b27e3a7ac506ba4774597fd26bd2b489d Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Tue, 24 Sep 2024 16:08:17 -0700 Subject: [PATCH 07/24] Clarified how execution contexts get their status. Also tightened up language obout requiring traps when promise/suspending not correctly lined up. --- document/js-api/index.bs | 66 +++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 24 deletions(-) diff --git a/document/js-api/index.bs b/document/js-api/index.bs index 39b506caaa..b757f752d7 100644 --- a/document/js-api/index.bs +++ b/document/js-api/index.bs @@ -306,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

@@ -1378,11 +1385,22 @@ 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}} object represents a JavaScript function whose calls via WebAssembly imports should be *suspended* when they return a Promise object. Each {{Suspending}} 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. 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 {
@@ -1411,13 +1429,17 @@ interface Suspending {
         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 trap. Otherwise, remove the entry for |ec| from [=map=].
         1. Set the [=surrounding agent=]'s [=associated store=] to |store|.
         1. If |result| is [=error=], throw an exception. This exception should be a WebAssembly {{RuntimeError}} exception, unless otherwise indicated by the WebAssembly error mapping.
         1. Otherwise, if |result| is of the form [=throw=] exnaddr,
             1. Perform [=EvaluateCall=] (|promise|.\[[Reject]],|result|.,false).
-            1. Return UNDETERMINED
         1. Otherwise, assert |result| is a [=list=] of WebAssembly values
             1. Let |outArity| be the [=list/size=] of |result|.
             1. If |outArity| is 0, return undefined.
@@ -1428,7 +1450,6 @@ interface Suspending {
                1. [=list/Append=] [=ToJSValue=](|r|) to |values|.
                1. let |jsReturnValue| be [$CreateArrayFromList$](|values|).
             1. Perform [=EvaluateCall=] (|promise|.\[[Resolve]],|jsReturnValue|,false)
-            1. Return UNDETERMINED
     1. Let |con| be [=CreateBuiltinFunction=](|runner|,0,"",<<>>)
     1. Perform [$Call$](|con|, undefined, <<>>)
     1. Returns |promise|
@@ -1467,21 +1488,18 @@ To create a suspending function from a JavaScript function |func|, wi
             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. Perform the [=Pause=] procedure, returning a new continuation |k|.
-            1. Let |resolved| be an [=AbstractClosure=] with parameters (|v|) that captures |functype|, |k| and performs the following steps when called:
+            1. Perform the [=Pause=] procedure on |async_context|, returning a new continuation |k|.
+            1. Let |resolved| be an [=AbstractClosure=] with parameters (|v|) that captures |functype|, |async_context| and |k| and performs the following steps when called:
                 1. Let |resultsSize| be |results|'s [=list/size=].
                 1. If |resultsSize| is not 1, throw a {{TypeError}}.
                 1. Let |resultType| be [|results|].[0].
                 1. Let |returnValue| be [=ToWebAssemblyValue=](|v|, |resultType|).
                 1. Perform the [=Enter=] procedure, passing it the captured continuation |k| the coerced |returnValue| and |async_context|.
-                1. Return undefined.
             1. Let |onResolved| be [=CreateBuiltinFunction=](|resolved|,1,"",[])
             1. Let |rejected| be an [=AbstractClosure=] with parameters (|e|) that captures |async_context| and performs the following steps when called:
                 1. Perform the [=Reject=] procedure, passing it the captured continuation |k| the exception value |e| and |async_context|.
-                1. Return undefined.
             1. Let |onRejected| be [=CreateBuiltinFunction=](|rejected|,1,"",[])
             1. Perform [$PerformPromiseThen$](|promise|, |onResolved|, |onRejected|). 
-            1. Return undefined
         1. Return the result of performing [=coerce a JavaScript return=] on |results| and |ret|.
     1. Let |store| be the [=surrounding agent=]'s [=associated store=].
     1. Let (|store|, |funcaddr|) be [=func_alloc=](|store|, |functype|, |hostfunc|).
@@ -1489,38 +1507,38 @@ To create a suspending function from a JavaScript function |func|, wi
     1. Return |funcaddr|.
 
-
-An [=execution context=] can be marked Paused to signal that the computation associated with the [=execution context=] has been paused. -
-
-The Pause abstract operation takes a currently executing algorithm and pauses it. The most important parameter |ec| of this operation is an -[=Execution Context=] which must be the most recent entry in the [=Execution Context Stack=] and which defines which operation is being paused. -The result of pausing an operation is a continuation |k| and a newly surfaced [=Execution Context=] -- which must be the [=Execution Context=] immediately -beneath the one on the [=Execution Context Stack=] that is [=Paused|paused=]. +The Pause abstract operation takes a currently executing algorithm and pauses it. The most important parameter |ec| of this operation is an [=Execution Context=] which must be the most recent entry in the [=Execution Context Stack=] and which defines which operation is being paused. + +The result of pausing an operation is a continuation |k| and a newly surfaced [=Execution Context=] -- which must be the [=Execution Context=] immediately beneath the one on the [=Execution Context Stack=] that is [=paused=]. 1. If |ec| is not the most recent entry on the [=Execution Context Stack=] then trap. + 1. Let |map| be the [=surrounding agent=]'s associated [=Execution Context Status map=]. + 1. If the entry for |ec| in |map| is not [=active=] then trap. 1. Let |k| be a continuation that represents the remainder of the computation associated with |ec|. - 1. Remove |ec| from the [=Execution Context Stack=] and mark it as being [=Paused|paused=]. + 1. Remove |ec| from the [=Execution Context Stack=]. + 1. Set the entry for |ec| in |map| to [=paused=]. 1. Assert that the [=Execution Context Stack=] is not empty. 1. Return |k|
The Enter abstract operation takes a continuation object |k|, a value object |v| and an [=execution context=] |ec|, and resumes the operations defined by |k|. - 1. If |ec| is not marked as [=Paused|paused=] then trap. - 1. Mark |ec| as not [=Paused|paused=]. - 1. Push |ec| on to the [=Execution Context Stack=], making |ec| the current execution context. + 1. Let |map| be the [=surrounding agent=]'s associated [=Execution Context Status map=]. + 1. If the entry for |ec| in |map| is not [=paused=] then trap. + 1. Set the entry for |ec| in |map| to [=active=]. + 1. Push |ec| on to the [=surrounding agent=]'s [=Execution Context Stack=], making |ec| the current execution context. 1. Resume the operations defined by |k|, passing |v| as the value of the last instruction being performed by |k| 1. Return undefined
-The Reject abstract operation takes a continuation object |k|, an exception object |e| and an [=execution context=] |ec|, and resumes the operations defined by |k| by throwing an exception to the [=Paused|paused=] computation. - 1. If |ec| is not marked as [=Paused|paused=] then trap. - 1. Mark |ec| as not [=Paused|paused=]. - 1. Push |ec| on to the [=Execution Context Stack=], making |ec| the current execution context. - 1. Let |type|, |payload| and |opaqueData| be the result of performing [=coerce a JavaScript exception=} on |e| +The Reject abstract operation takes a continuation object |k|, an exception object |e| and an [=execution context=] |ec|, and resumes the operations defined by |k| by throwing an exception to the [=paused=] computation. + 1. Let |map| be the [=surrounding agent=]'s associated [=Execution Context Status map=]. + 1. If the entry for |ec| in |map| is not [=paused=] then trap. + 1. Set the entry for |ec| in |map| to [=active=]. + 1. Push |ec| on to the [=surrounding agent=]'s [=Execution Context Stack=], making |ec| the current execution context. + 1. Let |type|, |payload| and |opaqueData| be the result of performing [=coerce a JavaScript exception=] on |e| 1. Resume the operations defined by |k|, performing [=WebAssembly/Throw=] with |type|, |payload| and |opaqueData|.
From 8de2a62e5be17c223b990ce285ffcfe608298d9f Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Wed, 25 Sep 2024 15:57:58 -0700 Subject: [PATCH 08/24] Apply suggestions from code review Some of the changes that follow from reviewer's remarks. Co-authored-by: Shu-yu Guo --- document/js-api/index.bs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/document/js-api/index.bs b/document/js-api/index.bs index b757f752d7..c96c194ae7 100644 --- a/document/js-api/index.bs +++ b/document/js-api/index.bs @@ -409,9 +409,9 @@ A {{Module}} object represents a single WebAssembly module. Each {{Module}} obje 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]] insternal slot + 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. 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|. @@ -1165,7 +1165,7 @@ 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| + 1. return |args|.
@@ -1418,14 +1418,14 @@ interface Suspending { 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 parameters that captures |wasmFunc| that, when invoked, performs [=run a Promising function=]. - 1. Returns [=CreateBuiltinFunction=](|builder|,1,"",<<>>) + 1. Returns [=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=] of no arguments that captures |promise|, |funcaddr|, |arguments| that, when invoked, performs the following steps: + 1. Let |runner| be a new [=AbstractClosure=] with no arguments that captures |promise|, |funcaddr|, and |arguments| that performs the following steps when called: 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|) @@ -1462,7 +1462,7 @@ Note: The extra |$Call$| in the above algorithm ensures that the creation of the 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|) - 1. Assign the \[[wrappedFunction]] internal slot of |susp| to |jsFun| + 1. Assign the \[[WrappedFunction]] internal slot of |susp| to |jsFun|. 1. Return |susp|
@@ -1494,7 +1494,7 @@ To create a suspending function from a JavaScript function |func|, wi 1. If |resultsSize| is not 1, throw a {{TypeError}}. 1. Let |resultType| be [|results|].[0]. 1. Let |returnValue| be [=ToWebAssemblyValue=](|v|, |resultType|). - 1. Perform the [=Enter=] procedure, passing it the captured continuation |k| the coerced |returnValue| and |async_context|. + 1. Perform the [=Enter=] procedure, passing it the captured continuation |k|, the coerced |returnValue|, and |async_context|. 1. Let |onResolved| be [=CreateBuiltinFunction=](|resolved|,1,"",[]) 1. Let |rejected| be an [=AbstractClosure=] with parameters (|e|) that captures |async_context| and performs the following steps when called: 1. Perform the [=Reject=] procedure, passing it the captured continuation |k| the exception value |e| and |async_context|. From 48430f5265e80ffc794a37ddefb21c1d5cd5862d Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Tue, 1 Oct 2024 15:00:52 -0700 Subject: [PATCH 09/24] Slight refactor of the suspend operation Make it clear that a 'long jump' in the meta-interpreter is being affected. --- document/js-api/index.bs | 58 ++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/document/js-api/index.bs b/document/js-api/index.bs index c96c194ae7..198b8b09aa 100644 --- a/document/js-api/index.bs +++ b/document/js-api/index.bs @@ -1425,36 +1425,41 @@ interface Suspending { 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 arguments that captures |promise|, |funcaddr|, and |arguments| that performs the following steps when called: - 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 trap. Otherwise, remove the entry for |ec| from [=map=]. - 1. Set the [=surrounding agent=]'s [=associated store=] to |store|. - 1. If |result| is [=error=], throw an exception. This exception should be a WebAssembly {{RuntimeError}} exception, unless otherwise indicated by the WebAssembly error mapping. - 1. Otherwise, if |result| is of the form [=throw=] exnaddr, - 1. Perform [=EvaluateCall=] (|promise|.\[[Reject]],|result|.,false). - 1. Otherwise, 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. Perform [=EvaluateCall=] (|promise|.\[[Resolve]],|jsReturnValue|,false) + 1. Let |runner| be a new [=AbstractClosure=] with no arguments that captures |promise|, |funcaddr|, and |arguments| that performs [=evaluate a Promising function=](|promise|,|funcaddr|,|arguments|) 1. Let |con| be [=CreateBuiltinFunction=](|runner|,0,"",<<>>) 1. Perform [$Call$](|con|, undefined, <<>>) 1. Returns |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 trap. Otherwise, remove the entry for |ec| from [=map=]. + 1. Set the [=surrounding agent=]'s [=associated store=] to |store|. + 1. If |result| is [=error=], throw an exception. This exception should be a WebAssembly {{RuntimeError}} exception, unless otherwise indicated by the WebAssembly error mapping. + 1. Otherwise, if |result| is of the form [=throw=] exnaddr, + 1. Perform [=EvaluateCall=] (|promise|.\[[Reject]],|result|.,false). + 1. Otherwise, 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. Perform [=EvaluateCall=] (|promise|.\[[Resolve]],|jsReturnValue|,false) + 1. Return undefined. +
+ Note: The extra |$Call$| in the above algorithm ensures that the creation of the Promise is separated from the fullfilling of that Promise. In effect, this allows suspension of the fullfillment to occur whilst allowing the creation of the Promise itself to continue.
@@ -1499,7 +1504,8 @@ To create a suspending function from a JavaScript function |func|, wi 1. Let |rejected| be an [=AbstractClosure=] with parameters (|e|) that captures |async_context| and performs the following steps when called: 1. Perform the [=Reject=] procedure, passing it the captured continuation |k| the exception value |e| and |async_context|. 1. Let |onRejected| be [=CreateBuiltinFunction=](|rejected|,1,"",[]) - 1. Perform [$PerformPromiseThen$](|promise|, |onResolved|, |onRejected|). + 1. Perform [$PerformPromiseThen$](|promise|, |onResolved|, |onRejected|). + 1. Note: this terminates the innermost call to [=evaluate a Promising function=] algorithm. Control will return to the caller of that algorithm. 1. Return the result of performing [=coerce a JavaScript return=] on |results| and |ret|. 1. Let |store| be the [=surrounding agent=]'s [=associated store=]. 1. Let (|store|, |funcaddr|) be [=func_alloc=](|store|, |functype|, |hostfunc|). From ab4b5d3a1a1d7c86d1c5412e19246ce001e5c6d7 Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Thu, 3 Oct 2024 15:49:39 -0700 Subject: [PATCH 10/24] Revised, as suggested by reviewer. --- document/js-api/index.bs | 68 +++++++++------------------------------- 1 file changed, 15 insertions(+), 53 deletions(-) diff --git a/document/js-api/index.bs b/document/js-api/index.bs index 198b8b09aa..97b3f6ec70 100644 --- a/document/js-api/index.bs +++ b/document/js-api/index.bs @@ -1426,8 +1426,7 @@ interface Suspending { 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 arguments that captures |promise|, |funcaddr|, and |arguments| that performs [=evaluate a Promising function=](|promise|,|funcaddr|,|arguments|) - 1. Let |con| be [=CreateBuiltinFunction=](|runner|,0,"",<<>>) - 1. Perform [$Call$](|con|, undefined, <<>>) + 1. Perform [=?=][$AsyncFunctionStart$](|promise|,|runner|) 1. Returns |promise|
@@ -1457,7 +1456,7 @@ interface Suspending { 1. [=list/Append=] [=ToJSValue=](|r|) to |values|. 1. let |jsReturnValue| be [$CreateArrayFromList$](|values|). 1. Perform [=EvaluateCall=] (|promise|.\[[Resolve]],|jsReturnValue|,false) - 1. Return undefined. + 1. Return UNUSED. Note: The extra |$Call$| in the above algorithm ensures that the creation of the Promise is separated from the fullfilling of that Promise. In effect, this allows suspension of the fullfillment to occur whilst allowing the creation of the Promise itself to continue. @@ -1480,9 +1479,11 @@ To create a suspending function from a JavaScript function |func|, wi 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 trap. 1. [=Prepare to run script=] with |relevant settings|. 1. [=Prepare to run a callback=] with |stored settings|. - 1. Let [|parameters|] → [|results|] be |functype|. + 1. Let [|parameters|] → [|resultTypes|] be |functype|. 1. Let |jsArguments| be the result of [=coerce WebAssembly arguments=](|parameters|,|arguments|) 1. Let |ret| be [=?=] [$Call$](|func|, undefined, |jsArguments|). 1. [=Clean up after running a callback=] with |stored settings|. @@ -1493,62 +1494,23 @@ To create a suspending function from a JavaScript function |func|, wi 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. Perform the [=Pause=] procedure on |async_context|, returning a new continuation |k|. - 1. Let |resolved| be an [=AbstractClosure=] with parameters (|v|) that captures |functype|, |async_context| and |k| and performs the following steps when called: - 1. Let |resultsSize| be |results|'s [=list/size=]. - 1. If |resultsSize| is not 1, throw a {{TypeError}}. - 1. Let |resultType| be [|results|].[0]. - 1. Let |returnValue| be [=ToWebAssemblyValue=](|v|, |resultType|). - 1. Perform the [=Enter=] procedure, passing it the captured continuation |k|, the coerced |returnValue|, and |async_context|. - 1. Let |onResolved| be [=CreateBuiltinFunction=](|resolved|,1,"",[]) - 1. Let |rejected| be an [=AbstractClosure=] with parameters (|e|) that captures |async_context| and performs the following steps when called: - 1. Perform the [=Reject=] procedure, passing it the captured continuation |k| the exception value |e| and |async_context|. - 1. Let |onRejected| be [=CreateBuiltinFunction=](|rejected|,1,"",[]) - 1. Perform [$PerformPromiseThen$](|promise|, |onResolved|, |onRejected|). - 1. Note: this terminates the innermost call to [=evaluate a Promising function=] algorithm. Control will return to the caller of that algorithm. - 1. Return the result of performing [=coerce a JavaScript return=] on |results| and |ret|. + 1. Set the entry for |async_context| in |map| to [=paused=]. + 1. Let |awaitResult| be the result of performing [$Await$](|promise|) + 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 an exceptional completion. + 1. If the entry for |async_context| in |map| is not [=paused=] then trap, 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|. - - -
-The Pause abstract operation takes a currently executing algorithm and pauses it. The most important parameter |ec| of this operation is an [=Execution Context=] which must be the most recent entry in the [=Execution Context Stack=] and which defines which operation is being paused. -The result of pausing an operation is a continuation |k| and a newly surfaced [=Execution Context=] -- which must be the [=Execution Context=] immediately beneath the one on the [=Execution Context Stack=] that is [=paused=]. - - 1. If |ec| is not the most recent entry on the [=Execution Context Stack=] then trap. - 1. Let |map| be the [=surrounding agent=]'s associated [=Execution Context Status map=]. - 1. If the entry for |ec| in |map| is not [=active=] then trap. - 1. Let |k| be a continuation that represents the remainder of the computation associated with |ec|. - 1. Remove |ec| from the [=Execution Context Stack=]. - 1. Set the entry for |ec| in |map| to [=paused=]. - 1. Assert that the [=Execution Context Stack=] is not empty. - 1. Return |k| +Note that we only invoke [$Await$] on the result of calling the JavaScript function |func| if it has returned a Promise object.
-
-The Enter abstract operation takes a continuation object |k|, a value object |v| and an [=execution context=] |ec|, and resumes the operations defined by |k|. - 1. Let |map| be the [=surrounding agent=]'s associated [=Execution Context Status map=]. - 1. If the entry for |ec| in |map| is not [=paused=] then trap. - 1. Set the entry for |ec| in |map| to [=active=]. - 1. Push |ec| on to the [=surrounding agent=]'s [=Execution Context Stack=], making |ec| the current execution context. - 1. Resume the operations defined by |k|, passing |v| as the value of the last instruction being performed by |k| - 1. Return undefined -
- -
-The Reject abstract operation takes a continuation object |k|, an exception object |e| and an [=execution context=] |ec|, and resumes the operations defined by |k| by throwing an exception to the [=paused=] computation. - 1. Let |map| be the [=surrounding agent=]'s associated [=Execution Context Status map=]. - 1. If the entry for |ec| in |map| is not [=paused=] then trap. - 1. Set the entry for |ec| in |map| to [=active=]. - 1. Push |ec| on to the [=surrounding agent=]'s [=Execution Context Stack=], making |ec| the current execution context. - 1. Let |type|, |payload| and |opaqueData| be the result of performing [=coerce a JavaScript exception=] on |e| - 1. Resume the operations defined by |k|, performing [=WebAssembly/Throw=] with |type|, |payload| and |opaqueData|. -
- -

Tags

The tag_alloc(|store|, |parameters|) algorithm creates a new [=tag address=] for |parameters| in |store| and returns the updated store and the [=tag address=]. From 6604a37149be5dbc4233577b9e74086a2cfcc5b4 Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Thu, 3 Oct 2024 16:12:30 -0700 Subject: [PATCH 11/24] Fix some minor typos --- document/js-api/index.bs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/document/js-api/index.bs b/document/js-api/index.bs index 97b3f6ec70..9a20d051ad 100644 --- a/document/js-api/index.bs +++ b/document/js-api/index.bs @@ -1417,15 +1417,15 @@ interface Suspending { 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 parameters that captures |wasmFunc| that, when invoked, performs [=run a Promising function=]. - 1. Returns [=CreateBuiltinFunction=](|builder|,1,"", « ») . + 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 arguments that captures |promise|, |funcaddr|, and |arguments| that performs [=evaluate a Promising function=](|promise|,|funcaddr|,|arguments|) + 1. Let |runner| be a new [=AbstractClosure=] with no parameters that captures |promise|, |funcaddr|, and |arguments| and performs [=evaluate a Promising function=](|promise|,|funcaddr|,|arguments|) when called. 1. Perform [=?=][$AsyncFunctionStart$](|promise|,|runner|) 1. Returns |promise|
@@ -1440,7 +1440,7 @@ interface Suspending { 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. Assert: if control reaches here, we have done waiting for suspended imports. 1. If the entry for |ec| in |map| is not [=active=] then trap. Otherwise, remove the entry for |ec| from [=map=]. 1. Set the [=surrounding agent=]'s [=associated store=] to |store|. 1. If |result| is [=error=], throw an exception. This exception should be a WebAssembly {{RuntimeError}} exception, unless otherwise indicated by the WebAssembly error mapping. @@ -1464,10 +1464,10 @@ Note: The extra |$Call$| in the above algorithm ensures that the creation of the
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|) + 1. Let |suspendingProto| be \[[%WebAssembly.Suspending.prototype%]]. + 1. Let |susp| be the result of [$OrdinaryObjectCreate$](|suspendingProto|,«\[[WrappedFunction]]»). 1. Assign the \[[WrappedFunction]] internal slot of |susp| to |jsFun|. - 1. Return |susp| + 1. Return |susp|.
@@ -1484,7 +1484,7 @@ To create a suspending function from a JavaScript function |func|, wi 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 |jsArguments| be the result of [=coerce WebAssembly arguments=](|parameters|,|arguments|). 1. Let |ret| be [=?=] [$Call$](|func|, undefined, |jsArguments|). 1. [=Clean up after running a callback=] with |stored settings|. 1. [=Clean up after running script=] with |relevant settings|. @@ -1495,7 +1495,7 @@ To create a suspending function from a JavaScript function |func|, wi 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 [$Await$](|promise|) + 1. Let |awaitResult| be the result of performing [$Await$](|promise|). 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 an exceptional completion. 1. If the entry for |async_context| in |map| is not [=paused=] then trap, otherwise, set the entry to [=active=]. 1. If |awaitResult|.\[[Type]] is throw, then: From 25d04812e562235634d34654d6e52247fb6ed6a5 Mon Sep 17 00:00:00 2001 From: Yury Delendik Date: Mon, 28 Oct 2024 08:01:04 -0500 Subject: [PATCH 12/24] Fix JSPI tests --- .../js-promise-integration.any.js | 16 +++++++--------- test/js-api/wasm-module-builder.js | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) 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 index e931e8c4e0..1ee95aad3f 100644 --- a/test/js-api/js-promise-integration/js-promise-integration.any.js +++ b/test/js-api/js-promise-integration/js-promise-integration.any.js @@ -32,7 +32,7 @@ test(() => { test(() => { let builder = new WasmModuleBuilder(); - builder.addGlobal(kWasmI32, true, false).exportAs('g'); + builder.addGlobal(kWasmI32, true).exportAs('g'); builder.addFunction("test", kSig_i_v) .addBody([ kExprI32Const, 42, @@ -41,7 +41,7 @@ test(() => { let instance = builder.instantiate(); let wrapper = WebAssembly.promising(instance.exports.test); wrapper(); - assertEquals(42, instance.exports.g.value); + assert_equals(42, instance.exports.g.value); }); promise_test(async () => { @@ -161,7 +161,7 @@ promise_test(async () => { let wrapped_export = Promising(instance.exports.test); // AbeforeB.showAbeforeB(); - exported_promise = wrapped_export(); + let exported_promise = wrapped_export(); // AbeforeB.showAbeforeB(); AbeforeB.setB(); @@ -189,7 +189,7 @@ promise_test(async () => { let wrapped_export = Promising(instance.exports.test); - exported_promise = wrapped_export(); + let exported_promise = wrapped_export(); AbeforeB.setB(); assert_equals(await exported_promise, 42); @@ -389,9 +389,7 @@ test(() => { builder.addFunction("export", kSig_v_v).addBody([]).exportFunc(); let instance = builder.instantiate(); let export_wrapper = WebAssembly.promising(instance.exports.export); - let export_sig = export_wrapper.type(); - assert_array_equals(export_sig.parameters, []); - assert_array_equals(export_sig.results, ['externref']); + assert_true(export_wrapper instanceof Function); }); promise_test(async (t) => { @@ -403,7 +401,7 @@ promise_test(async (t) => { let instance = builder.instantiate(); let wrapper = WebAssembly.promising(instance.exports.test); - promise_rejects(t, RangeError, wrapper(),/Maximum call stack size exceeded/); + promise_rejects(t, new Error(), wrapper(), /Maximum call stack size exceeded/); }); promise_test(async (t) => { @@ -440,7 +438,7 @@ promise_test(async (t) => { }}); // export1 (promising) let wrapper = WebAssembly.promising(instance.exports.export1); - promise_rejects(t, WebAssembly.RuntimeError, wrapper(), + promise_rejects(t, new WebAssembly.RuntimeError(), wrapper(), /trying to suspend JS frames/); }); 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++; } From b95be4c6cadde96d0b3b625016c8e97dbc13497d Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Mon, 28 Oct 2024 16:20:08 -0700 Subject: [PATCH 13/24] Update index.bs Some minor tweaks in response to reviewer's remarks. --- document/js-api/index.bs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/document/js-api/index.bs b/document/js-api/index.bs index 9a20d051ad..736593fbed 100644 --- a/document/js-api/index.bs +++ b/document/js-api/index.bs @@ -808,7 +808,7 @@ Immediately after a WebAssembly [=memory.grow=] instruction executes, perform th
1. If the top of the stack is not [=i32.const=] (−1), 1. Let |frame| be the [=current frame=]. - 1. Assert: due to validation, |frame|.[=frame/module=].[=moduleinst/memaddrs=][0] exists. + 1. Assert: Due to validation, |frame|.[=frame/module=].[=moduleinst/memaddrs=][0] exists. 1. Let |memaddr| be the memory address |frame|.[=frame/module=].[=moduleinst/memaddrs=][0]. 1. [=Refresh the memory buffer=] of |memaddr|.
@@ -1440,13 +1440,15 @@ interface Suspending { 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. Assert: If control reaches here, we have done waiting for suspended imports. 1. If the entry for |ec| in |map| is not [=active=] then trap. Otherwise, remove the entry for |ec| from [=map=]. 1. Set the [=surrounding agent=]'s [=associated store=] to |store|. 1. If |result| is [=error=], throw an exception. This exception should be a WebAssembly {{RuntimeError}} exception, unless otherwise indicated by the WebAssembly error mapping. 1. Otherwise, if |result| is of the form [=throw=] exnaddr, - 1. Perform [=EvaluateCall=] (|promise|.\[[Reject]],|result|.,false). - 1. Otherwise, assert |result| is a [=list=] of WebAssembly values + 1. Perform [$Call$] (|promise|.\[[Reject]],|result|.,false). + 1. Note: The result of the above call is ignored. + 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]). @@ -1454,8 +1456,8 @@ interface Suspending { 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. Perform [=EvaluateCall=] (|promise|.\[[Resolve]],|jsReturnValue|,false) + 1. Let |jsReturnValue| be [$CreateArrayFromList$](|values|). + 1. Perform [$Call$] (|promise|.\[[Resolve]],|jsReturnValue|,false) 1. Return UNUSED.
@@ -1485,7 +1487,7 @@ To create a suspending function from a JavaScript function |func|, wi 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 [=?=] [$Call$](|func|, undefined, |jsArguments|). + 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. @@ -1495,8 +1497,9 @@ To create a suspending function from a JavaScript function |func|, wi 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 [$Await$](|promise|). - 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 an exceptional completion. + 1. Let |awaitResult| be the result of performing [$Completion$]([$Await$](|promise|)). + 1. Note: that 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 trap, 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]]. @@ -1507,8 +1510,6 @@ To create a suspending function from a JavaScript function |func|, wi 1. Let (|store|, |funcaddr|) be [=func_alloc=](|store|, |functype|, |hostfunc|). 1. Set the [=surrounding agent=]'s [=associated store=] to |store|. 1. Return |funcaddr|. - -Note that we only invoke [$Await$] on the result of calling the JavaScript function |func| if it has returned a Promise object.

Tags

From a94f54f095e81422203b425d02983ccd1d51358c Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Tue, 29 Oct 2024 15:16:30 -0700 Subject: [PATCH 14/24] Update document/js-api/index.bs Co-authored-by: Shu-yu Guo --- document/js-api/index.bs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/document/js-api/index.bs b/document/js-api/index.bs index 736593fbed..57612c43b4 100644 --- a/document/js-api/index.bs +++ b/document/js-api/index.bs @@ -1498,7 +1498,7 @@ To create a suspending function from a JavaScript function |func|, wi 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: that we only invoke [$Await$] if the call to |func| has returned a Promise object. + 1. Note: That 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 trap, otherwise, set the entry to [=active=]. 1. If |awaitResult|.\[[Type]] is throw, then: From 9a2299f5daae75ba783cea9cbd4401af76eb8ac7 Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Tue, 29 Oct 2024 15:16:38 -0700 Subject: [PATCH 15/24] Update document/js-api/index.bs Co-authored-by: Shu-yu Guo --- document/js-api/index.bs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/document/js-api/index.bs b/document/js-api/index.bs index 57612c43b4..8d4fcfc90a 100644 --- a/document/js-api/index.bs +++ b/document/js-api/index.bs @@ -1499,7 +1499,7 @@ To create a suspending function from a JavaScript function |func|, wi 1. Set the entry for |async_context| in |map| to [=paused=]. 1. Let |awaitResult| be the result of performing [$Completion$]([$Await$](|promise|)). 1. Note: That 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. 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 trap, 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]]. From 54e77cb3e9d1ea984c765291af38d6a712fdda6a Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Tue, 29 Oct 2024 15:16:57 -0700 Subject: [PATCH 16/24] Update document/js-api/index.bs Co-authored-by: Shu-yu Guo --- document/js-api/index.bs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/document/js-api/index.bs b/document/js-api/index.bs index 8d4fcfc90a..81de369bf8 100644 --- a/document/js-api/index.bs +++ b/document/js-api/index.bs @@ -404,7 +404,7 @@ A {{Module}} object represents a single WebAssembly module. Each {{Module}} obje 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 [=func=] |functype|, - 1. If [$IsCallable$](|v|) is true + 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, From 9aac0fc25e5303e375fdf6891debdebe15c0cd43 Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Wed, 30 Oct 2024 10:25:46 -0700 Subject: [PATCH 17/24] Update index.bs use 'marker' suffix to better distinguish Suspending objects from functions --- document/js-api/index.bs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/document/js-api/index.bs b/document/js-api/index.bs index 81de369bf8..a6eded5101 100644 --- a/document/js-api/index.bs +++ b/document/js-api/index.bs @@ -409,8 +409,8 @@ A {{Module}} object represents a single WebAssembly module. Each {{Module}} obje 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. 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. @@ -1387,8 +1387,8 @@ The algorithm ToWebAssemblyValue(|v|, |type|) coerces a JavaScript va 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}} object represents a JavaScript function whose calls via WebAssembly imports should be *suspended* when they return a Promise object. -Each {{Suspending}} object has a \[[wrappedFunction]] internal slot which holds a JavaScript function. +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. From d89f2f78dfb53b93903828e3812fd12529c4e6f6 Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Wed, 30 Oct 2024 16:17:32 -0700 Subject: [PATCH 18/24] Update index.bs Small changes, responding to remarks by reviewer. --- document/js-api/index.bs | 48 +++++++++++++++------------------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/document/js-api/index.bs b/document/js-api/index.bs index a6eded5101..42e6f138fe 100644 --- a/document/js-api/index.bs +++ b/document/js-api/index.bs @@ -409,7 +409,7 @@ A {{Module}} object represents a single WebAssembly module. Each {{Module}} obje 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. 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. @@ -431,10 +431,6 @@ A {{Module}} object represents a single WebAssembly module. Each {{Module}} obje 1. Let |store| be the [=surrounding agent=]'s [=associated store=]. 1. Let (|store|, |globaladdr|) be [=global_alloc=](|store|, [=const=] |valtype|, |value|). 1. Set the [=surrounding agent=]'s [=associated store=] to |store|. - 1. Otherwise, if |v| [=implements=] {{Global}}, - 1. Let |globaladdr| be |v|.\[[Global]]. - 1. Otherwise, - 1. Throw a {{LinkError}} exception. 1. Let |externglobal| be [=external value|global=] |globaladdr|. 1. [=list/Append=] |externglobal| to |imports|. 1. If |externtype| is of the form [=external-type/mem=] memtype, @@ -1155,8 +1151,7 @@ This slot holds a [=function address=] relative to the [=surrounding agent=]'s [
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=], throw a {{TypeError}}. - + 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. @@ -1172,7 +1167,7 @@ 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|). - 1. Let |args| be the result of [=coerce JavaScript arguments|coercing arguments=] (|functype|,|argValues|) + 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. @@ -1199,14 +1194,6 @@ 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|] → [|resultTypes|] be |functype|. - 1. Let |jsArguments| be the result of [=coerce WebAssembly arguments=](|parameters|,|arguments|) - 1. Let |ret| be [=?=] [$Call$](|func|, undefined, |jsArguments|). - 1. Return the result of performing [=coerce a JavaScript return=] on |resultTypes| and |ret|. -
-
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}}. @@ -1240,7 +1227,7 @@ Note: Exported Functions do not have a \[[Construct]] method and thus it is not 1. Otherwise, 1. Let |type| be the [=JavaScript exception tag=]. 1. Let |payload| be « ». - 1. Let |opaqueData| be [=ToWebAssemblyValue=](|v|, [=externref=]) + 1. Let |opaqueData| be [=ToWebAssemblyValue=](|v|, [=externref=]). 1. Return the triple |type|, |payload| and |opaqueData|.
@@ -1254,11 +1241,15 @@ 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}}, @@ -1269,7 +1260,6 @@ Note: Exported Functions do not have a \[[Construct]] method and thus it is not 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. Let (|store|, |funcaddr|) be [=func_alloc=](|store|, |functype|, |hostfunc|). 1. Set the [=surrounding agent=]'s [=associated store=] to |store|. 1. Return |funcaddr|. @@ -1425,9 +1415,10 @@ interface Suspending { 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 [=evaluate a Promising function=](|promise|,|funcaddr|,|arguments|) when called. - 1. Perform [=?=][$AsyncFunctionStart$](|promise|,|runner|) - 1. Returns |promise| + 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|.
@@ -1443,10 +1434,9 @@ interface Suspending { 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 trap. Otherwise, remove the entry for |ec| from [=map=]. 1. Set the [=surrounding agent=]'s [=associated store=] to |store|. - 1. If |result| is [=error=], throw an exception. This exception should be a WebAssembly {{RuntimeError}} exception, unless otherwise indicated by the WebAssembly error mapping. + 1. If |result| is [=error=], throw an exception. This exception must be a WebAssembly {{RuntimeError}} exception, unless otherwise indicated by the WebAssembly error mapping. 1. Otherwise, if |result| is of the form [=throw=] exnaddr, - 1. Perform [$Call$] (|promise|.\[[Reject]],|result|.,false). - 1. Note: The result of the above call is ignored. + 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|. @@ -1457,18 +1447,16 @@ interface Suspending { 1. [=list/iterate|For each=] |r| of |result|, 1. [=list/Append=] [=ToJSValue=](|r|) to |values|. 1. Let |jsReturnValue| be [$CreateArrayFromList$](|values|). - 1. Perform [$Call$] (|promise|.\[[Resolve]],|jsReturnValue|,false) + 1. [=Resolve=] |promise| with |jsReturnValue|. 1. Return UNUSED.
-Note: The extra |$Call$| in the above algorithm ensures that the creation of the Promise is separated from the fullfilling of that Promise. In effect, this allows suspension of the fullfillment to occur whilst allowing the creation of the Promise itself to continue. -
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. Assign the \[[WrappedFunction]] internal slot of |susp| to |jsFun|. + 1. Set |susp|.\[[WrappedFunction]] to |jsFun|. 1. Return |susp|.
@@ -1498,7 +1486,7 @@ To create a suspending function from a JavaScript function |func|, wi 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: That we only invoke [$Await$] if the call to |func| has returned a Promise object. + 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 trap, otherwise, set the entry to [=active=]. 1. If |awaitResult|.\[[Type]] is throw, then: From 9209ee793af20a4d14dd05adc2d3ad786daaf776 Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Thu, 31 Oct 2024 10:45:48 -0700 Subject: [PATCH 19/24] Update index.bs Small tweak: Add an explanation for the role of the map Big tweak: Arrange to throw a JS RuntimeError rather than trap when JSPI usage not properly balanced. --- document/js-api/index.bs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/document/js-api/index.bs b/document/js-api/index.bs index 42e6f138fe..235d9bda33 100644 --- a/document/js-api/index.bs +++ b/document/js-api/index.bs @@ -1382,7 +1382,9 @@ Each {{Suspending}} marker object has a \[[WrappedFunction]] internal slot which 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. If present, a status can be one of two stack status values: +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. @@ -1432,7 +1434,7 @@ interface Suspending { 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 trap. Otherwise, remove the entry for |ec| from [=map=]. + 1. If the entry for |ec| in |map| is not [=active=] then throw a WebAssembly {{RuntimeError}} 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 an exception. This exception must be a WebAssembly {{RuntimeError}} exception, unless otherwise indicated by the WebAssembly error mapping. 1. Otherwise, if |result| is of the form [=throw=] exnaddr, @@ -1470,7 +1472,9 @@ To create a suspending function from a JavaScript function |func|, wi 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 trap. + 1. If the entry for |async_context| in |map| is not [=active=], then: + 1. Let |type|, |payload| and |opaqueData| be the result of [=coerce a JavaScript exception|coercing the JavaScript exception=] {{RuntimeError}}. + 1. [=WebAssembly/Throw=] with |type|, |payload| and |opaqueData|. 1. [=Prepare to run script=] with |relevant settings|. 1. [=Prepare to run a callback=] with |stored settings|. 1. Let [|parameters|] → [|resultTypes|] be |functype|. @@ -1488,7 +1492,10 @@ To create a suspending function from a JavaScript function |func|, wi 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 trap, otherwise, set the entry to [=active=]. + 1. If the entry for |async_context| in |map| is not [=paused=] then: + 1. Let |type|, |payload| and |opaqueData| be the result of [=coerce a JavaScript exception|coercing the JavaScript exception=] {{RuntimeError}}. + 1. [=WebAssembly/Throw=] with |type|, |payload| and |opaqueData|. + 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|. From 0b8f55730dca7d6dfeabad12d4b6471887c2a552 Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Thu, 31 Oct 2024 11:41:43 -0700 Subject: [PATCH 20/24] Update index.bs Tidy up punctuation. --- document/js-api/index.bs | 43 +++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/document/js-api/index.bs b/document/js-api/index.bs index 235d9bda33..e62fd2643b 100644 --- a/document/js-api/index.bs +++ b/document/js-api/index.bs @@ -1135,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|). @@ -1171,7 +1171,7 @@ This slot holds a [=function address=] relative to the [=surrounding agent=]'s [ 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 [=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=]. @@ -1200,7 +1200,7 @@ Note: Exported Functions do not have a \[[Construct]] method and thus it is not 1. Let |jsArguments| be « ». 1. [=list/iterate|For each=] |arg| of |arguments|, 1. [=list/Append=] [=!=] [=ToJSValue=](|arg|) to |jsArguments|. - 1. Return |jsArguments| + 1. Return |jsArguments|.
@@ -1242,7 +1242,7 @@ Note: Exported Functions do not have a \[[Construct]] method and thus it is not 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 |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|. @@ -1251,20 +1251,24 @@ Note: Exported Functions do not have a \[[Construct]] method and thus it is not 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. Perform [=throw a JavaScript exception|throw the JavaScript exception=] |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: @@ -1294,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.
@@ -1327,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|). @@ -1427,7 +1430,7 @@ interface Suspending { 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 |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|. @@ -1439,8 +1442,8 @@ interface Suspending { 1. If |result| is [=error=], throw an exception. This exception must be 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. 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]). @@ -1738,7 +1741,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, @@ -1794,7 +1797,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|. From fe35784504d6a062ae48da4640090202a11b3706 Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Thu, 31 Oct 2024 13:27:49 -0700 Subject: [PATCH 21/24] Update index.bs Clean up throwing exceptions from JS (a little bit) --- document/js-api/index.bs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/document/js-api/index.bs b/document/js-api/index.bs index e62fd2643b..fb94460a0b 100644 --- a/document/js-api/index.bs +++ b/document/js-api/index.bs @@ -1251,7 +1251,7 @@ Note: Exported Functions do not have a \[[Construct]] method and thus it is not 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. Perform [=throw a JavaScript exception|throw the JavaScript exception=] |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|. @@ -1439,7 +1439,7 @@ interface Suspending { 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 {{RuntimeError}} 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 an exception. This exception must be a WebAssembly {{RuntimeError}} exception, unless otherwise indicated by the WebAssembly error mapping. + 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, @@ -1476,8 +1476,7 @@ To create a suspending function from a JavaScript function |func|, wi 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. Let |type|, |payload| and |opaqueData| be the result of [=coerce a JavaScript exception|coercing the JavaScript exception=] {{RuntimeError}}. - 1. [=WebAssembly/Throw=] with |type|, |payload| and |opaqueData|. + 1. Perform [=throw a JavaScript exception=] with a {{RuntimeError}} exception. 1. [=Prepare to run script=] with |relevant settings|. 1. [=Prepare to run a callback=] with |stored settings|. 1. Let [|parameters|] → [|resultTypes|] be |functype|. @@ -1496,8 +1495,7 @@ To create a suspending function from a JavaScript function |func|, wi 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. Let |type|, |payload| and |opaqueData| be the result of [=coerce a JavaScript exception|coercing the JavaScript exception=] {{RuntimeError}}. - 1. [=WebAssembly/Throw=] with |type|, |payload| and |opaqueData|. + 1. Perform [=throw a JavaScript exception=] with a {{RuntimeError}}. 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]]. From 3a2016e6037956cee4c715ea17a46367f5beecff Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Tue, 12 Nov 2024 14:59:18 -0800 Subject: [PATCH 22/24] Add SuspendError Use the new standard exception type SuspendError if a problem arises with trying to suspend. --- document/js-api/index.bs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/document/js-api/index.bs b/document/js-api/index.bs index fb94460a0b..21eb20a04a 100644 --- a/document/js-api/index.bs +++ b/document/js-api/index.bs @@ -1170,7 +1170,7 @@ This slot holds a [=function address=] relative to the [=surrounding agent=]'s [ 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 [=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|). @@ -1437,7 +1437,7 @@ interface Suspending { 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 {{RuntimeError}} exception. Otherwise, remove the entry for |ec| from [=map=]. + 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, @@ -1476,7 +1476,7 @@ To create a suspending function from a JavaScript function |func|, wi 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 {{RuntimeError}} exception. + 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|. @@ -1495,7 +1495,7 @@ To create a suspending function from a JavaScript function |func|, wi 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 {{RuntimeError}}. + 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]]. @@ -1807,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. From ce7705ffdd96b0de36805c004adfa7e3d3d290f5 Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Thu, 19 Dec 2024 14:37:29 -0800 Subject: [PATCH 23/24] Adjust jspi to use new exception type. --- .../js-promise-integration.any.js | 77 ++++++++++++------- 1 file changed, 48 insertions(+), 29 deletions(-) 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 index 1ee95aad3f..d473971138 100644 --- a/test/js-api/js-promise-integration/js-promise-integration.any.js +++ b/test/js-api/js-promise-integration/js-promise-integration.any.js @@ -1,14 +1,6 @@ // META: global=jsshell // META: script=/wasm/jsapi/wasm-module-builder.js -function Promising(wasm_export) { - return WebAssembly.promising(wasm_export); -} - -function Suspending(jsFun){ - return new WebAssembly.Suspending(jsFun); -} - // Test for invalid wrappers test(() => { assert_throws(TypeError, () => WebAssembly.promising({}), @@ -52,9 +44,9 @@ promise_test(async () => { kExprLocalGet, 0, kExprCallFunction, import_index, // suspend ]).exportFunc(); - let js_import = Suspending(() => Promise.resolve(42)); + let js_import = WebAssembly.Suspending(() => Promise.resolve(42)); let instance = builder.instantiate({m: {import: js_import}}); - let wrapped_export = Promising(instance.exports.test); + 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); @@ -91,9 +83,9 @@ promise_test(async () => { function js_import() { return Promise.resolve(++i); }; - let wasm_js_import = Suspending(js_import); + let wasm_js_import = WebAssembly.Suspending(js_import); let instance = builder.instantiate({m: {import: wasm_js_import}}); - let wrapped_export = Promising(instance.exports.test); + 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); @@ -154,11 +146,11 @@ promise_test(async () => { kExprCallFunction, import42_index, // suspend? kExprCallFunction, importSetA_index ]).exportFunc(); - let import42 = Suspending(()=>Promise.resolve(42)); + let import42 = WebAssembly.Suspending(()=>Promise.resolve(42)); let instance = builder.instantiate({m: {import42: import42, setA:AbeforeB.setA}}); - let wrapped_export = Promising(instance.exports.test); + let wrapped_export = WebAssembly.promising(instance.exports.test); // AbeforeB.showAbeforeB(); let exported_promise = wrapped_export(); @@ -183,11 +175,11 @@ promise_test(async () => { kExprCallFunction, import42_index, // suspend? kExprCallFunction, importSetA_index ]).exportFunc(); - let import42 = Suspending(()=>42); + let import42 = WebAssembly.Suspending(()=>42); let instance = builder.instantiate({m: {import42: import42, setA:AbeforeB.setA}}); - let wrapped_export = Promising(instance.exports.test); + let wrapped_export = WebAssembly.promising(instance.exports.test); let exported_promise = wrapped_export(); AbeforeB.setB(); @@ -213,10 +205,10 @@ test(t => { function js_import() { return Promise.resolve(); }; - let wasm_js_import = Suspending(js_import); + let wasm_js_import = WebAssembly.Suspending(js_import); let instance = builder.instantiate({m: {import: wasm_js_import, tag: tag}}); - let wrapped_export = Promising(instance.exports.test); + 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); @@ -239,10 +231,10 @@ promise_test(async (t) => { function js_import() { return Promise.reject(new WebAssembly.Exception(tag, [42])); }; - let wasm_js_import = Suspending(js_import); + let wasm_js_import = WebAssembly.Suspending(js_import); let instance = builder.instantiate({m: {import: wasm_js_import, tag: tag}}); - let wrapped_export = Promising(instance.exports.test); + 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); @@ -273,14 +265,14 @@ async function TestNestedSuspenders(suspend) { kExprCallFunction, inner_index ]).exportFunc(); - let inner = Suspending(() => suspend ? Promise.resolve(42) : 43); + let inner = WebAssembly.Suspending(() => suspend ? Promise.resolve(42) : 43); let export_inner; - let outer = Suspending(() => export_inner()); + let outer = WebAssembly.Suspending(() => export_inner()); let instance = builder.instantiate({m: {inner, outer}}); - export_inner = Promising(instance.exports.inner); - let export_outer = Promising(instance.exports.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) @@ -310,11 +302,11 @@ test(() => { .addBody([ kExprLocalGet, 0 ]).exportFunc(); - let js_import = Suspending(() => Promise.resolve(42)); + let js_import = WebAssembly.Suspending(() => Promise.resolve(42)); let instance = builder.instantiate({m: {import: js_import}}); - let suspender = Promising(instance.exports.return_suspender)(); + let suspender = WebAssembly.promising(instance.exports.return_suspender)(); for (s of [suspender, null, undefined, {}]) { - assert_throws(WebAssembly.RuntimeError, () => instance.exports.test(s)); + assert_throws(WebAssembly.SuspendError, () => instance.exports.test(s)); } }, "Call import with an invalid suspender"); @@ -438,8 +430,7 @@ promise_test(async (t) => { }}); // export1 (promising) let wrapper = WebAssembly.promising(instance.exports.export1); - promise_rejects(t, new WebAssembly.RuntimeError(), wrapper(), - /trying to suspend JS frames/); + promise_rejects(t, new WebAssembly.SuspendError(), wrapper()); }); promise_test(async () => { @@ -465,3 +456,31 @@ promise_test(async () => { 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"); From 7b776cf6da38b50b23928ab544de753f34916511 Mon Sep 17 00:00:00 2001 From: Francis McCabe Date: Tue, 23 Jun 2026 14:18:21 -0700 Subject: [PATCH 24/24] Apply suggestions from code review Co-authored-by: Francis McCabe --- document/js-api/index.bs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/document/js-api/index.bs b/document/js-api/index.bs index 21eb20a04a..be01eca447 100644 --- a/document/js-api/index.bs +++ b/document/js-api/index.bs @@ -804,7 +804,7 @@ Immediately after a WebAssembly [=memory.grow=] instruction executes, perform th
1. If the top of the stack is not [=i32.const=] (−1), 1. Let |frame| be the [=current frame=]. - 1. Assert: Due to validation, |frame|.[=frame/module=].[=moduleinst/memaddrs=][0] exists. + 1. Assert: due to validation, |frame|.[=frame/module=].[=moduleinst/memaddrs=][0] exists. 1. Let |memaddr| be the memory address |frame|.[=frame/module=].[=moduleinst/memaddrs=][0]. 1. [=Refresh the memory buffer=] of |memaddr|.