Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

sjrd
Copy link
Member

@sjrd sjrd commented Feb 10, 2025

We introduce 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 functions and async =>functions, as Closures 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)().


On the JavaScript platform, async Closures and JSAwait are directly compiled as their JavaScript equivalent.

On WebAssembly, we leverage the JavaScript Promise Integration feature (JSPI). We turn async Closures into WebAssembly.promising functions. js.await(p) is compiled as a call to a unique jsAwait helper, which is declared as an "identity" new WebAssembly.Suspending((x) => x). The static scoping rule of js.await guarantees that the suspending call is always performed in a valid context (one that will not throw a WebAssembly.SuspendError).


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 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.
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.

@sjrd sjrd force-pushed the async-await-and-wasm-jspi branch from 39ba6a1 to e587e37 Compare February 11, 2025 09:03
@sjrd sjrd force-pushed the async-await-and-wasm-jspi branch from e587e37 to 5b0aec8 Compare March 16, 2025 16:07
@sjrd sjrd marked this pull request as ready for review March 16, 2025 16:07
@sjrd sjrd requested a review from gzm0 March 16, 2025 16:07
Copy link
Contributor

@gzm0 gzm0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've done an initial pass, this looks really cool.

Overall design looks reasonable and simple enough.

Questions I have:

  • Have you considered flagging orphans statically in the compilation stage already? (No idea if that ends up better, just a thought I had, current proposal doesn't look too terrible either).
  • Are you confident we can eliminate top level async IIFEs in the emission stage (at a later point in time)? (IIUC, for both backbends they lead to unnecessary nesting ATM, the top level function could be async itself)

@gzm0
Copy link
Contributor

gzm0 commented Mar 17, 2025

Oh, sorry, my last comment wasn't clear: I have not looked at the code in detail yet, just the overall structure. I will follow up with a thorough review.

@sjrd sjrd force-pushed the async-await-and-wasm-jspi branch from 5b0aec8 to a803984 Compare March 18, 2025 12:57
@sjrd
Copy link
Member Author

sjrd commented Mar 18, 2025

Yield was indeed unrelated; removed.

Have you considered flagging orphans statically in the compilation stage already? (No idea if that ends up better, just a thought I had, current proposal doesn't look too terrible either).

The only things it changes are:

  • ClassDefChecker now checks that orphan awaits are actually declared as such.
  • Infos doesn't track whether it's in an async closure anymore; it knows from the JSAwait node.

Which is nicer in theory, though not any less code in practice. But ... JSAwait needs an additional boolean flag, which everyone else needs to pass around without actually using it. Even the two emitters don't care about it.

So meh. Overall I think it's a bit simpler as is.

Are you confident we can eliminate top level async IIFEs in the emission stage (at a later point in time)? (IIUC, for both backbends they lead to unnecessary nesting ATM, the top level function could be async itself)

To be extra sure, I made a PoC in sjrd/scala-js@async-await-and-wasm-jspi...sjrd:scala-js:opt-full-async-body . So it's definitely doable.

I'm not 100% sure that the PoC is correct at the moment. There might be a potential scope clash between the enclosing params' names and local identifiers declared inside the async closure. I think it can happen if for some reason one of the enclosing params is not used inside the closure. But that should be fixable I believe.

Copy link
Contributor

@gzm0 gzm0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! This is cool. A pity we cannot use it for the html test runner :-/

@@ -43,6 +43,9 @@ trait ScalaJSOptions {
* See the warning itself or #4129 for context.
*/
def warnGlobalExecutionContext: Boolean

/** Whether to tolerate orphan `js.await` calls for WebAssembly's JSPI. */
def allowOrphanJSAwait: Boolean
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered making this a language import? IIUC this is really a local property of the code. As in:
For any given .scala file, this is either required or unnecessary.

Admittedly, this is also the case for fixClassOf, but that is really a hack.

  • genStaticForwardersForNonTopLevelObjects
  • sourceURIMaps

Can be switched independently of the code.

  • warnGlobalExecutionContext is a workaround for not being able to disable warnings on earlier scala versions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we have the ability to make something like a language import in a compiler plugin.

imports get removed very early in the compiler pipeline. We still see them at the jsinterop phase, but not later. We would have to record during that phase which js.await is allowed to be orphan, in a way that we can check in the backend. It's possible with a custom scalajs.runtime method, but it's a significant investment for something like that.

Also analyses for unused imports will not understand our magic import, so they will systematically report that import as unused.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I see. That seems too much effort.

Have you considered two different await methods, only one of them allowed in orphan position? Maybe, with some smart import organization, we could essentially have something like a language import, just with import shadowing. The js prefix makes this a bit tricky :-/ (implementing it in the compiler would be trivial though).

An alternative would be to have an implicit param orphanMode to js.await that defaults to disallow via low-prio implicit. However, then users could technically pass that type/value around, which we do not want.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a commit (to be squashed if accepted) that uses the implicit trick to look like a language import. I'm not concerned about the fact that the value can be passed around. It allows global behavior, so moving passing it around is fine.

It would be a concern if we used the AwaitPermit to allow non orphan awaits in the first place, by giving having js.await synthesize an implicit permit given to its closure. But that would not be nearly good enough, since it could be captured and hence not reflect the directly enclosing requirement.

We introduce 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)()`.

---

On the JavaScript platform, async `Closure`s and `JSAwait` are
directly compiled as their JavaScript equivalent.

On WebAssembly, we leverage the JavaScript Promise Integration
feature (JSPI). We turn async `Closure`s into
WebAssembly.promising` functions. `js.await(p)` is compiled as
a call to a unique `jsAwait` helper, which is declared as
an "identity" `new WebAssembly.Suspending((x) => x)`. The static
scoping rule of `js.await` guarantees that the suspending call
is always performed in a valid context (one that will not throw
a `WebAssembly.SuspendError`).
@sjrd sjrd force-pushed the async-await-and-wasm-jspi branch from a803984 to d400f9d Compare March 24, 2025 10:42
@sjrd sjrd requested a review from gzm0 March 24, 2025 10:42
Copy link
Contributor

@gzm0 gzm0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still uneasy about the compiler flag. I've added some alternative suggestions.

It feels we're making it unergonomic for users to deal with code that needs orphans if we require a compiler flag for this.

@sjrd sjrd requested a review from gzm0 April 5, 2025 09:21
Copy link
Contributor

@gzm0 gzm0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO the proposed commit provides a better user interface. The changes to the library are bit more extensive, but IMO acceptable.

@sjrd if you agree with that sentiment, I feel we should go forward with the "language import" version.

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.
@sjrd sjrd force-pushed the async-await-and-wasm-jspi branch from 2cf8722 to 2ff3bc6 Compare April 6, 2025 09:13
@sjrd
Copy link
Member Author

sjrd commented Apr 6, 2025

Yes, I agree. Squashed + added "JavaScript function invocation".

@sjrd sjrd merged commit cc670fb into scala-js:main Apr 6, 2025
3 checks passed
@sjrd sjrd deleted the async-await-and-wasm-jspi branch April 6, 2025 12:52
sjrd added a commit to sjrd/dotty that referenced this pull request Aug 30, 2025
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.
sjrd added a commit to sjrd/dotty that referenced this pull request Sep 2, 2025
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.
sjrd added a commit to sjrd/dotty that referenced this pull request Sep 2, 2025
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.
sjrd added a commit to sjrd/dotty that referenced this pull request Sep 10, 2025
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.
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.

3 participants