Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 61946f8

Browse files
committed
Scala.js: Support js.async and js.await, including JSPI on Wasm.
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.
1 parent ce3fcb3 commit 61946f8

File tree

6 files changed

+121
-5
lines changed

6 files changed

+121
-5
lines changed

‎.github/workflows/ci.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,10 @@ jobs:
155155
156156
- name: Scala.js Test
157157
run: |
158-
./project/scripts/sbt ";sjsSandbox/run ;sjsSandbox/test ;sjsJUnitTests/test ;set sjsJUnitTests/scalaJSLinkerConfig ~= switchToESModules ;sjsJUnitTests/test ;sjsCompilerTests/test"
158+
./project/scripts/sbt ";sjsSandbox/run ;sjsSandbox/test ;sjsJUnitTests/test ;set sjsJUnitTests/scalaJSLinkerConfig ~= switchToESModules ;sjsJUnitTests/test"
159+
./project/scripts/sbt ";sjsJUnitTests/clean ;set sjsJUnitTests/scalaJSLinkerConfig ~= switchToLatestESVersion ;sjsJUnitTests/test"
160+
./project/scripts/sbt ";sjsJUnitTests/clean ;set sjsJUnitTests/scalaJSLinkerConfig ~= switchToLatestESVersion ;set Global/enableWebAssembly := true; sjsJUnitTests/test"
161+
./project/scripts/sbt ";sjsCompilerTests/test"
159162
160163
test_windows_fast:
161164
runs-on: [self-hosted, Windows]

‎compiler/src/dotty/tools/backend/sjs/JSCodeGen.scala

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class JSCodeGen()(using genCtx: Context) {
7777

7878
val currentClassSym = new ScopedVar[Symbol]
7979
private val delambdafyTargetDefDefs = new ScopedVar[MutableSymbolMap[DefDef]]
80+
private val methodsAllowingJSAwait = new ScopedVar[mutable.Set[Symbol]]
8081
private val currentMethodSym = new ScopedVar[Symbol]
8182
private val localNames = new ScopedVar[LocalNameGenerator]
8283
private val thisLocalVarName = new ScopedVar[Option[LocalName]]
@@ -91,6 +92,7 @@ class JSCodeGen()(using genCtx: Context) {
9192
withScopedVars(
9293
currentClassSym := null,
9394
delambdafyTargetDefDefs := null,
95+
methodsAllowingJSAwait := null,
9496
currentMethodSym := null,
9597
localNames := null,
9698
thisLocalVarName := null,
@@ -267,6 +269,7 @@ class JSCodeGen()(using genCtx: Context) {
267269
withScopedVars(
268270
currentClassSym := sym,
269271
delambdafyTargetDefDefs := MutableSymbolMap(),
272+
methodsAllowingJSAwait := mutable.Set.empty,
270273
) {
271274
val tree = if (sym.isJSType) {
272275
if (!sym.is(Trait) && sym.isNonNativeJSClass)
@@ -2424,6 +2427,7 @@ class JSCodeGen()(using genCtx: Context) {
24242427
withScopedVars(
24252428
currentClassSym := sym,
24262429
delambdafyTargetDefDefs := MutableSymbolMap(),
2430+
methodsAllowingJSAwait := mutable.Set.empty,
24272431
) {
24282432
genNonNativeJSClass(typeDef)
24292433
}
@@ -4073,6 +4077,54 @@ class JSCodeGen()(using genCtx: Context) {
40734077
// js.import.meta
40744078
js.JSImportMeta()
40754079

4080+
case JS_ASYNC =>
4081+
// js.async(arg)
4082+
assert(args.size == 1,
4083+
s"Expected exactly 1 argument for JS primitive $code but got " +
4084+
s"${args.size} at $pos")
4085+
4086+
def extractStatsAndClosure(arg: Tree): (List[Tree], Closure) = (arg: @unchecked) match
4087+
case arg: Closure =>
4088+
(Nil, arg)
4089+
case Block(outerStats, expr) =>
4090+
val (innerStats, closure) = extractStatsAndClosure(expr)
4091+
(outerStats ::: innerStats, closure)
4092+
4093+
val (stats, fun @ Closure(_, target, _)) = extractStatsAndClosure(args.head)
4094+
methodsAllowingJSAwait += target.symbol
4095+
val genStats = stats.map(genStat(_))
4096+
val asyncExpr = genClosure(fun) match {
4097+
case js.NewLambda(_, closure: js.Closure)
4098+
if closure.params.isEmpty && closure.resultType == jstpe.AnyType =>
4099+
val newFlags = closure.flags.withTyped(false).withAsync(true)
4100+
js.JSFunctionApply(closure.copy(flags = newFlags), Nil)
4101+
case other =>
4102+
throw FatalError(
4103+
s"Unexpected tree generated for the Function0 argument to js.async at ${tree.sourcePos}: $other")
4104+
}
4105+
js.Block(genStats, asyncExpr)
4106+
4107+
case JS_AWAIT =>
4108+
// js.await(arg)
4109+
val (arg, permitValue) = genArgs2
4110+
if (!methodsAllowingJSAwait.contains(currentMethodSym)) {
4111+
// This is an orphan await
4112+
if (!(args(1).tpe <:< jsdefn.WasmJSPI_allowOrphanJSAwaitModuleClassRef)) {
4113+
report.error(
4114+
"Illegal use of js.await().\n" +
4115+
"It can only be used inside a js.async {...} block, without any lambda,\n" +
4116+
"by-name argument or nested method in-between.\n" +
4117+
"If you compile for WebAssembly, you can allow arbitrary js.await()\n" +
4118+
"calls by adding the following import:\n" +
4119+
"import scala.scalajs.js.wasm.JSPI.allowOrphanJSAwait",
4120+
tree.sourcePos)
4121+
}
4122+
}
4123+
/* In theory we should evaluate `permit` after `arg` but before the `JSAwait`.
4124+
* It *should* always be side-effect-free, though, so we just discard it.
4125+
*/
4126+
js.JSAwait(arg)
4127+
40764128
case DYNAMIC_IMPORT =>
40774129
// runtime.dynamicImport
40784130
assert(args.size == 1,

‎compiler/src/dotty/tools/backend/sjs/JSDefinitions.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ final class JSDefinitions()(using Context) {
4242
def JSPackage_undefined(using Context) = JSPackage_undefinedR.symbol
4343
@threadUnsafe lazy val JSPackage_dynamicImportR = ScalaJSJSPackageClass.requiredMethodRef("dynamicImport")
4444
def JSPackage_dynamicImport(using Context) = JSPackage_dynamicImportR.symbol
45+
@threadUnsafe lazy val JSPackage_asyncR = ScalaJSJSPackageClass.requiredMethodRef("async")
46+
def JSPackage_async(using Context) = JSPackage_asyncR.symbol
47+
@threadUnsafe lazy val JSPackage_awaitR = ScalaJSJSPackageClass.requiredMethodRef("await")
48+
def JSPackage_await(using Context) = JSPackage_awaitR.symbol
4549

4650
@threadUnsafe lazy val JSNativeAnnotType: TypeRef = requiredClassRef("scala.scalajs.js.native")
4751
def JSNativeAnnot(using Context) = JSNativeAnnotType.symbol.asClass
@@ -210,6 +214,10 @@ final class JSDefinitions()(using Context) {
210214
@threadUnsafe lazy val Special_unwrapFromThrowableR = SpecialPackageClass.requiredMethodRef("unwrapFromThrowable")
211215
def Special_unwrapFromThrowable(using Context) = Special_unwrapFromThrowableR.symbol
212216

217+
@threadUnsafe lazy val WasmJSPIModuleRef = requiredModuleRef("scala.scalajs.js.wasm.JSPI")
218+
@threadUnsafe lazy val WasmJSPI_allowOrphanJSAwaitModuleRef = requiredModuleRef("scala.scalajs.js.wasm.JSPI.allowOrphanJSAwait")
219+
def WasmJSPI_allowOrphanJSAwaitModuleClassRef(using Context) = WasmJSPIModuleRef.select(WasmJSPI_allowOrphanJSAwaitModuleRef.symbol)
220+
213221
@threadUnsafe lazy val WrappedArrayType: TypeRef = requiredClassRef("scala.scalajs.js.WrappedArray")
214222
def WrappedArrayClass(using Context) = WrappedArrayType.symbol.asClass
215223

‎compiler/src/dotty/tools/backend/sjs/JSPrimitives.scala

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@ object JSPrimitives {
3030
inline val JS_IMPORT = JS_NEW_TARGET + 1 // js.import.apply(specifier)
3131
inline val JS_IMPORT_META = JS_IMPORT + 1 // js.import.meta
3232

33-
inline val CONSTRUCTOROF = JS_IMPORT_META + 1 // runtime.constructorOf(clazz)
33+
inline val JS_ASYNC = JS_IMPORT_META + 1 // js.async
34+
inline val JS_AWAIT = JS_ASYNC + 1 // js.await
35+
36+
inline val CONSTRUCTOROF = JS_AWAIT + 1 // runtime.constructorOf(clazz)
3437
inline val CREATE_INNER_JS_CLASS = CONSTRUCTOROF + 1 // runtime.createInnerJSClass
3538
inline val CREATE_LOCAL_JS_CLASS = CREATE_INNER_JS_CLASS + 1 // runtime.createLocalJSClass
3639
inline val WITH_CONTEXTUAL_JS_CLASS_VALUE = CREATE_LOCAL_JS_CLASS + 1 // runtime.withContextualJSClassValue
@@ -110,6 +113,8 @@ class JSPrimitives(ictx: Context) extends DottyPrimitives(ictx) {
110113

111114
addPrimitive(jsdefn.JSPackage_typeOf, TYPEOF)
112115
addPrimitive(jsdefn.JSPackage_native, JS_NATIVE)
116+
addPrimitive(jsdefn.JSPackage_async, JS_ASYNC)
117+
addPrimitive(jsdefn.JSPackage_await, JS_AWAIT)
113118

114119
addPrimitive(defn.BoxedUnit_UNIT, UNITVAL)
115120

‎project/Build.scala

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import com.typesafe.tools.mima.plugin.MimaPlugin.autoImport._
3333
import org.scalajs.sbtplugin.ScalaJSPlugin
3434
import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
3535

36+
import org.scalajs.linker.interface.ESVersion
37+
3638
import sbtbuildinfo.BuildInfoPlugin
3739
import sbtbuildinfo.BuildInfoPlugin.autoImport._
3840
import sbttastymima.TastyMiMaPlugin
@@ -2772,7 +2774,9 @@ object Build {
27722774
case FullOptStage => (Test / fullLinkJS / scalaJSLinkerConfig).value
27732775
}
27742776

2775-
if (linkerConfig.moduleKind != ModuleKind.NoModule && !linkerConfig.closureCompiler)
2777+
val isWebAssembly = linkerConfig.experimentalUseWebAssembly
2778+
2779+
if (linkerConfig.moduleKind != ModuleKind.NoModule && !linkerConfig.closureCompiler && !isWebAssembly)
27762780
Seq(baseDirectory.value / "test-require-multi-modules")
27772781
else
27782782
Nil
@@ -2800,6 +2804,8 @@ object Build {
28002804

28012805
val moduleKind = linkerConfig.moduleKind
28022806
val hasModules = moduleKind != ModuleKind.NoModule
2807+
val hasAsyncAwait = linkerConfig.esFeatures.esVersion >= ESVersion.ES2017
2808+
val isWebAssembly = linkerConfig.experimentalUseWebAssembly
28032809

28042810
def conditionally(cond: Boolean, subdir: String): Seq[File] =
28052811
if (!cond) Nil
@@ -2829,9 +2835,12 @@ object Build {
28292835

28302836
++ conditionally(!hasModules, "js/src/test/require-no-modules")
28312837
++ conditionally(hasModules, "js/src/test/require-modules")
2832-
++ conditionally(hasModules && !linkerConfig.closureCompiler, "js/src/test/require-multi-modules")
2838+
++ conditionally(hasModules && !linkerConfig.closureCompiler&&!isWebAssembly, "js/src/test/require-multi-modules")
28332839
++ conditionally(moduleKind == ModuleKind.ESModule, "js/src/test/require-dynamic-import")
28342840
++ conditionally(moduleKind == ModuleKind.ESModule, "js/src/test/require-esmodule")
2841+
2842+
++ conditionally(hasAsyncAwait, "js/src/test/require-async-await")
2843+
++ conditionally(hasAsyncAwait && isWebAssembly, "js/src/test/require-orphan-await")
28352844
)
28362845
},
28372846

‎project/DottyJSPlugin.scala

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,34 @@ import sbt.Keys.*
66
import org.scalajs.sbtplugin.ScalaJSPlugin
77
import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
88

9-
import org.scalajs.linker.interface.StandardConfig
9+
import org.scalajs.linker.interface.{ESVersion, StandardConfig}
10+
11+
import org.scalajs.jsenv.nodejs.NodeJSEnv
1012

1113
object DottyJSPlugin extends AutoPlugin {
1214

1315
object autoImport {
1416
val switchToESModules: StandardConfig => StandardConfig =
1517
config => config.withModuleKind(ModuleKind.ESModule)
18+
19+
val switchToLatestESVersion: StandardConfig => StandardConfig =
20+
config => config.withESFeatures(_.withESVersion(ESVersion.ES2021))
21+
22+
val enableWebAssembly: SettingKey[Boolean] =
23+
settingKey("enable all the configuration items required for WebAssembly")
1624
}
1725

26+
import autoImport._
27+
1828
val writePackageJSON = taskKey[Unit](
1929
"Write package.json to configure module type for Node.js")
2030

2131
override def requires: Plugins = ScalaJSPlugin
2232

33+
override def globalSettings: Seq[Setting[_]] = Def.settings(
34+
enableWebAssembly := false,
35+
)
36+
2337
override def projectSettings: Seq[Setting[_]] = Def.settings(
2438

2539
/* #11709 Remove the dependency on scala3-library that ScalaJSPlugin adds.
@@ -40,6 +54,31 @@ object DottyJSPlugin extends AutoPlugin {
4054
// Typecheck the Scala.js IR found on the classpath
4155
scalaJSLinkerConfig ~= (_.withCheckIR(true)),
4256

57+
// Maybe configure WebAssembly
58+
scalaJSLinkerConfig := {
59+
val prev = scalaJSLinkerConfig.value
60+
if (enableWebAssembly.value) {
61+
prev
62+
.withModuleKind(ModuleKind.ESModule)
63+
.withExperimentalUseWebAssembly(true)
64+
} else {
65+
prev
66+
}
67+
},
68+
jsEnv := {
69+
val baseConfig = NodeJSEnv.Config()
70+
val config = if (enableWebAssembly.value) {
71+
baseConfig.withArgs(List(
72+
"--experimental-wasm-exnref",
73+
"--experimental-wasm-imported-strings", // for JS string builtins
74+
"--experimental-wasm-jspi", // for JSPI, used by async/await
75+
))
76+
} else {
77+
baseConfig
78+
}
79+
new NodeJSEnv(config)
80+
},
81+
4382
Compile / jsEnvInput := (Compile / jsEnvInput).dependsOn(writePackageJSON).value,
4483
Test / jsEnvInput := (Test / jsEnvInput).dependsOn(writePackageJSON).value,
4584

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /