-
Notifications
You must be signed in to change notification settings - Fork 397
Fix #5064: Add js.async { ... js.await(p) ... }
, and support JSPI on WebAssembly.
#5130
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
Conversation
39ba6a1
to
e587e37
Compare
e587e37
to
5b0aec8
Compare
There was a problem hiding this 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)
linker/jvm/src/main/scala/org/scalajs/linker/backend/closure/ClosureAstTransformer.scala
Outdated
Show resolved
Hide resolved
linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Printers.scala
Outdated
Show resolved
Hide resolved
linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala
Outdated
Show resolved
Hide resolved
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. |
5b0aec8
to
a803984
Compare
The only things it changes are:
Which is nicer in theory, though not any less code in practice. But ... So meh. Overall I think it's a bit simpler as is.
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 |
There was a problem hiding this 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
import
s 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
linker/shared/src/main/scala/org/scalajs/linker/backend/javascript/Trees.scala
Show resolved
Hide resolved
linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala
Show resolved
Hide resolved
linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/LoaderContent.scala
Show resolved
Hide resolved
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`).
a803984
to
d400f9d
Compare
There was a problem hiding this 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.
There was a problem hiding this 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.
2cf8722
to
2ff3bc6
Compare
Yes, I agree. Squashed + added "JavaScript function invocation". |
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.
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.
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.
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.
We introduce a new pair of primitive methods,
js.async
andjs.await
. They correspond to JavaScriptasync
functions andawait
expressions.js.await(p)
awaits aPromise
, but it must be directly scoped within ajs.async { ... }
block.At the IR level,
js.await(p)
directly translates to a dedicated IR nodeJSAwait(arg)
.js.async
blocks don't have a direct representation. Instead, the IR modelsasync function
s andasync =>
functions, asClosure
s with an additionnalasync
flag. This corresponds to the JavaScript model forasync/await
.A
js.async { body }
block therefore corresponds to an immediately-appliedasync Closure
, which in JavaScript would be written as(async () => body)()
.On the JavaScript platform, async
Closure
s andJSAwait
are directly compiled as their JavaScript equivalent.On WebAssembly, we leverage the JavaScript Promise Integration feature (JSPI). We turn async
Closure
s intoWebAssembly.promising
functions.js.await(p)
is compiled as a call to a uniquejsAwait
helper, which is declared as an "identity"new WebAssembly.Suspending((x) => x)
. The static scoping rule ofjs.await
guarantees that the suspending call is always performed in a valid context (one that will not throw aWebAssembly.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 correspondingWebAssembly.Suspending
calls. The former is introduced by ourjs.async
blocks, and the latter by calls tojs.await
.Normally,
js.await
must be directly enclosed within ajs.async
block. This ensures that it can be compiled to a JavaScriptasync
function andawait
expression.We introduce a compiler option to allow "orphan awaits". This way, we can decouple thejs.await
calls from theirjs.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 theirjs.async
blocks. The generated IR will then only link when targeting WebAssembly. Technically,js.await
requires an implicitjs.AwaitPermit
. The default one only allowsjs.await
directly insidejs.async
, and we can import a stronger one that allows orphan awaits.There must still be a
js.async
block dynamically enclosing anyjs.await
call (on the call stack), without intervening JS frame. If that dynamic property is not satisfied, aWebAssembly.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.