Skip to content

Conversation

sjrd
Copy link
Member

@sjrd sjrd commented Aug 30, 2025

This PR forwards two PRs from Scala.js for Scala 2: scala-js/scala-js#5081 and scala-js/scala-js#5130.

@sjrd sjrd force-pushed the sjs-async-await branch 2 times, most recently from 5174016 to 42404e1 Compare August 30, 2025 16:19
@sjrd sjrd changed the title WiP Scala.js async/await, including JSPI. Scala.js: Support js.async and js.await, including JSPI on Wasm. Aug 30, 2025
@sjrd
Copy link
Member Author

sjrd commented Aug 30, 2025

Looks like Node.js is too old. @hamzaremmal Could we upgrade to the latest Node.js (24.7.0) on the runners?

sjrd added 3 commits September 2, 2025 10:50
Previously, if an `@JSName` annotation had an argument that was
not a literal, but a reference to a constant expression (such as
a `final val`), it would not be constant-folded in the generated
Scala.js IR.

This produced worse code than necessary. For Wasm, it was
particularly bad, as the names must then be evaluated on the Wasm
side instead of being pushed to the custom JS helpers.
This is forward port of the Scala.js commit
scala-js/scala-js@0d16b42

The body of `Closure` nodes always has a simple shape that calls a
helper method. We previously generated that call in the body of the
`js.Closure`, and marked the target method `@inline` so that the
optimizer would always inline it.

Instead, we now directly "inline" it from the codegen, by
generating the `js.MethodDef` right inside the `js.Closure` scope.

As is, this does not change the generated code. However, it may
speed up (cold) linker runs, since it will have less work to do.
Notably, it performs two fewer knowledge queries to find and inline
the target method. It also reduces the total amount of methods to
manipulate in the incremental analysis.

More importantly, this will be necessary later if we want to add
support for `async/await` or `function*/yield`. Indeed, for those,
we will need `await`/`yield` expressions to be lexically scoped
in the body of their enclosing closure. That won't work if they are
in the body of a separate helper method.
This is forward port of the compiler changes in the two commits of
the Scala.js PR scala-js/scala-js#5130

---

We add support for a new pair of primitive methods, `js.async` and
`js.await`. They correspond to JavaScript `async` functions and
`await` expressions.

`js.await(p)` awaits a `Promise`, but it must be directly scoped
within a `js.async { ... }` block.

At the IR level, `js.await(p)` directly translates to a dedicated
IR node `JSAwait(arg)`. `js.async` blocks don't have a direct
representation. Instead, the IR models `async function`s and
`async =>`functions, as `Closure`s with an additionnal `async`
flag. This corresponds to the JavaScript model for `async/await`.

A `js.async { body }` block therefore corresponds to an
immediately-applied `async Closure`, which in JavaScript would be
written as `(async () => body)()`.

---

We then optionally allow orphan `js.await(p)` on WebAssembly.

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 sort of "language import" 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. Technically, `js.await` requires an
implicit `js.AwaitPermit`. The default one only allows `js.await`
directly inside `js.async`, and we can import a stronger one that
allows orphan awaits.

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.

---

Since these features fundamentally depend on a) the target ES
version and b) the WebAssembly backend, we add more configurations
of Scala.js to the Scala 3 CI.
@natsukagami natsukagami self-requested a review September 2, 2025 11:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant