-
Notifications
You must be signed in to change notification settings - Fork 395
Wasm: Language feature for the JavaScript Promise Integration proposal (JSPI) #5064
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
I have given this some more thought. I don't think we need
def suspendable[A](operation: => A): js.Promise[A] =
throw new Error("stub") That would define a Wasm This is essentially what I implemented in the hack in my personal project. From an API point of view it's nicer because it does not force the developer to export anything. If we do want to export the result, that is a one-liner of the form: @JSExportTopLevel("myPromisingFunc")
def myPromisingFunc(x: Int): js.Promise[String] =
suspendable { myComputation(x) } So overall, from a language API perspective, this whole feature would boil down to two primitive functions: At the IR level,
Any of the above solutions makes |
Interestingly, #5082 uses JSPI to implement |
Just to check my understanding of JSPI:
(from #5079 (comment)) Not being forced to have a microtask tick is essentially the superpower we are granted here, correct? (or does the import interface spec force the tick?). On the surface, it also looks like we can suspend in any function (not just ones as marked async), but IIUC, if we want to have a static guarantee that we do not trap (or otherwise fail) on suspend, we essentially need to introduce a similar "flag" on functions. (arguably, not being strictly forced to have that flag and use other means to not trap is also a superpower). |
No, that's at best something that could be more efficient. Anyway, even JSPI's
That is the superpower. Not so much that we don't need to flag functions, but that we can suspend an arbitrary number of stack frames! The functions in the middle of the stack (between the entry through This mechanism solves the "what color is your function?" problem. Every function can act blue or red, depending on who calls them. That's the semantic superpower. In addition, it does that efficiently (through suspension instead of CPS-transforming every function).
If we want that guarantee, then we're back to coloring functions, indeed. For the traps, I managed to convince the proposal champions to |
It’s great to see that #5082 only provides the
I’m curious about how "letting " let js.await be used anywhere instead of being restricted inside a js.async block" looks like. AFAIK, |
No, I don't think we should attempt to check further at the Scala.js level. With the changes in WebAssembly/js-promise-integration#55 the As I mentioned, capture checking would provide a way to build safer abstractions on top of those building blocks, but that's beyond our scope. |
I'm somewhat with @tanishiking on this one. Or at least, I feel we should:
|
It doesn't seem bad at all to me to have a predictable exception. We have plenty of APIs, both Scala.js-specific and not, that work that way. Examples include division by zero, string concatenation of a The problem with restricting this is that currently known solutions either
Finding a solution that hits the sweet spot in the middle is precisely what capture checking set out to achieve, but that is a multi-year research project. |
I think this is the key insight here. So I agree, we must find a way to expose this at least as a low-level API (and if we actually get exceptions, that's indeed more than enough IMO). I wonder if we should offer a "coloring" based sugar at the language level for users that just want async (IIUC #5082 essentially does that in its current state). |
With the JavaScript Promise Integration (JSPI), there can be as many frames as we want between a `WebAssembly.promising` function and the corresponding `WebAssembly.Suspending` calls. The former is introduced by our `js.async` blocks, and the latter by calls to `js.await`. Normally, `js.await` must be directly enclosed within a `js.async` block. This ensures that it can be compiled to a JavaScript `async` function and `await` expression. We introduce a compiler option to allow "orphan awaits". This way, we can decouple the `js.await` calls from their `js.async` blocks. The generated IR will then only link when targeting WebAssembly. There must still be a `js.async` block *dynamically* enclosing any `js.await` call (on the call stack), without intervening JS frame. If that dynamic property is not satisfied, a `WebAssembly.SuspendError` gets thrown. This last point is not yet implemented in Node.js at the time of this commit; instead such a situation traps. The corresponding test is therefore currently ignored.
With the JavaScript Promise Integration (JSPI), there can be as many frames as we want between a `WebAssembly.promising` function and the corresponding `WebAssembly.Suspending` calls. The former is introduced by our `js.async` blocks, and the latter by calls to `js.await`. Normally, `js.await` must be directly enclosed within a `js.async` block. This ensures that it can be compiled to a JavaScript `async` function and `await` expression. We introduce a compiler option to allow "orphan awaits". This way, we can decouple the `js.await` calls from their `js.async` blocks. The generated IR will then only link when targeting WebAssembly. There must still be a `js.async` block *dynamically* enclosing any `js.await` call (on the call stack), without intervening JS frame. If that dynamic property is not satisfied, a `WebAssembly.SuspendError` gets thrown. This last point is not yet implemented in Node.js at the time of this commit; instead such a situation traps. The corresponding test is therefore currently ignored.
With the JavaScript Promise Integration (JSPI), there can be as many frames as we want between a `WebAssembly.promising` function and the corresponding `WebAssembly.Suspending` calls. The former is introduced by our `js.async` bloc 8000 ks, and the latter by calls to `js.await`. Normally, `js.await` must be directly enclosed within a `js.async` block. This ensures that it can be compiled to a JavaScript `async` function and `await` expression. We introduce a compiler option to allow "orphan awaits". This way, we can decouple the `js.await` calls from their `js.async` blocks. The generated IR will then only link when targeting WebAssembly. There must still be a `js.async` block *dynamically* enclosing any `js.await` call (on the call stack), without intervening JS frame. If that dynamic property is not satisfied, a `WebAssembly.SuspendError` gets thrown. This last point is not yet implemented in Node.js at the time of this commit; instead such a situation traps. The corresponding test is therefore currently ignored.
There is vote scheduled for April 8 to move JSPI to Phase 4. At that point it will be basically stable. |
With the JavaScript Promise Integration (JSPI), there can be as many frames as we want between a `WebAssembly.promising` function and the corresponding `WebAssembly.Suspending` calls. The former is introduced by our `js.async` blocks, and the latter by calls to `js.await`. Normally, `js.await` must be directly enclosed within a `js.async` block. This ensures that it can be compiled to a JavaScript `async` function and `await` expression. We introduce a compiler option to allow "orphan awaits". This way, we can decouple the `js.await` calls from their `js.async` blocks. The generated IR will then only link when targeting WebAssembly. There must still be a `js.async` block *dynamically* enclosing any `js.await` call (on the call stack), without intervening JS frame. If that dynamic property is not satisfied, a `WebAssembly.SuspendError` gets thrown. This last point is not yet implemented in Node.js at the time of this commit; instead such a situation traps. The corresponding test is therefore currently ignored.
With the JavaScript Promise Integration (JSPI), there can be as many frames as we want between a `WebAssembly.promising` function and the corresponding `WebAssembly.Suspending` calls. The former is introduced by our `js.async` blocks, and the latter by calls to `js.await`. Normally, `js.await` must be directly enclosed within a `js.async` block. This ensures that it can be compiled to a JavaScript `async` function and `await` expression. We introduce a compiler option to allow "orphan awaits". This way, we can decouple the `js.await` calls from their `js.async` blocks. The generated IR will then only link when targeting WebAssembly. There must still be a `js.async` block *dynamically* enclosing any `js.await` call (on the call stack), without intervening JS frame. If that dynamic property is not satisfied, a `WebAssembly.SuspendError` gets thrown. This last point is not yet implemented in Node.js at the time of this commit; instead such a situation traps. The corresponding test is therefore currently ignored.
Fix #5064: Add `js.async { ... js.await(p) ... }`, and support JSPI on WebAssembly.
Uh oh!
There was an error while loading. Please reload this page.
Motivation
The JavaScript Promise Integration proposal (JSPI) for WebAssembly adds a superpower to WebAssembly, compared to JavaScript. It allows Wasm code to perform synchronous calls to
Promise
-returning JS functions, and suspend when thePromise
is not resolved yet. This is a power basically equivalent to Virtual Threads (aka green threads, coroutines, etc.), something that is not possible to implement with JavaScript alone. Basically a non-blockingawait(promise)
function.I successfully leveraged JSPI with Scala.js-on-Wasm in a hobby project to dramatically simplify the internal API :
sjrd/funlabyrinthe-scala@18bc1f2
However, to achieve it, I had to textually post-process the
main.js
file generated by Scala.js, hacking and slashing its abstractions to build my own. I used my internal knowledge of how the linker generates code to do this. This is not something we're supposed to do, obviously, and it will likely break when I next uprade Scala.js.It would be nice to have dedicated language support to leverage JSPI from Scala.js code.
Proposal
There are two sides to JSPI : importing a
WebAssembly.Suspending
function, and wrapping an export into aWebAssembly.promising
call.Promising Exports
We need a way to express that an exported
def
must be wrapped in aWebAssembly.promising
call. Note that the Wasm functionf
itself must be given topromising(f)
. Currently Scala.js wraps every exported function in a JavaScript-definedfunction
to protect its abstractions. This is why we cannot currently callpromising
in user-space.I propose a distinct
@JSExportPromising
annotation, to be used like@JSExportTopLevel
ondef
s:As seen from JavaScript, it would return a
js.Promise[String]
, following the API transformation applied byWebAssembly.promising
.Suspending Imports
On the other side, we need a way to express that we import a JS function as wrapped into a
new WebAssembly.Suspending(f)
constructor. Here as well, the resultingSuspending
instance must be directlyimport
ed into Wasm. Since Scala.js generates custom arrow functions for all imports to protect its abstractions, once again this cannot be achieved in user-space.I see two possibilities here.
The first one is simpler from the Scala.js language specification point of view. The second one is closer to the JSPI API design.
First option: offer a single primitive
await
:This can be implemented as a unique JS helper of the form
When calling any JS function that returns a
js.Promise
in its API, we can follow up with a call to this primitive to exploit JSPI and give us the result.Second option: offer a generic
@JSImportSuspending
annotation for@js.native def
s:This is closer to the spirit of the JSPI API design, but it means we also need an
@JSGlobalSuspending
, a variant with aglobalFallback
, etc.Implementation concerns
On the call path between a promising export and a suspending import call, there cannot be any JavaScript frame. This cannot reasonably be checked statically nor dynamically. However, it will be checked by the engine at run-time, which will
trap
if the condition is not upheld. There is currently no way to test ahead of time, and since atrap
is not catchable from Wasm, we cannot recover from that. That is quite annoying, since otherwise infastLinkJS
our linker guarantees that the code it generates never traps. For example, even a testing framework would have no way of catching this condition; it will crash instead.Design concerns
This feature is unique to the combination of JavaScript and Wasm. JS alone cannot do it, nor can Wasm alone. Therefore, this feature would be unique to Scala.js-on-Wasm! Given the current experimental status of our Wasm backend, it's not something we can really do today. However, it gives such a new superpower that I believe we should already start considering how we could offer that superpower to our users.
The text was updated successfully, but these errors were encountered: