8000 Fix #5064: Optionally allow orphan `js.await(p)` on WebAssembly. · sjrd/scala-js@e587e37 · GitHub
[go: up one dir, main page]

Skip to content

Commit e587e37

Browse files
committed
Fix scala-js#5064: 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. 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.
1 parent f15ddb6 commit e587e37

File tree

13 files changed

+204
-27
lines changed

13 files changed

+204
-27
lines changed

compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5339,11 +5339,13 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
53395339

53405340
case JS_AWAIT =>
53415341
// js.await(arg)
5342-
if (!methodsAllowingJSAwait.contains(currentMethodSym)) {
5342+
if (!scalaJSOpts.allowOrphanJSAwait && !methodsAllowingJSAwait.contains(currentMethodSym)) {
53435343
reporter.error(pos,
53445344
"Illegal use of js.await().\n" +
53455345
"It can only be used inside a js.async {...} block, without any lambda,\n" +
5346-
"by-name argument or nested method in-between.")
5346+
"by-name argument or nested method in-between.\n" +
5347+
"If you compile for WebAssembly, you can allow arbitrary js.await()\n" +
5348+
"calls with -P:scalajs:allowOrphanJSAwait.")
53475349
}
53485350
val arg = genArgs1
53495351
js.JSAwait(arg)

compiler/src/main/scala/org/scalajs/nscplugin/ScalaJSOptions.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ trait ScalaJSOptions {
4343
* See the warning itself or #4129 for context.
4444
*/
4545
def warnGlobalExecutionContext: Boolean
46+
47+
/** Whether to tolerate orphan `js.await` calls for WebAssembly's JSPI. */
48+
def allowOrphanJSAwait: Boolean
4649
}
4750

4851
object ScalaJSOptions {

compiler/src/main/scala/org/scalajs/nscplugin/ScalaJSPlugin.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ class ScalaJSPlugin(val global: Global) extends NscPlugin {
6363
relSourceMap.toList.map(URIMap(_, absSourceMap))
6464
}
6565
var warnGlobalExecutionContext: Boolean = true
66+
var allowOrphanJSAwait: Boolean = false
6667
var _sourceURIMaps: List[URIMap] = Nil
6768
var relSourceMap: Option[URI] = None
6869
var absSourceMap: Option[URI] = None
@@ -113,6 +114,8 @@ class ScalaJSPlugin(val global: Global) extends NscPlugin {
113114
genStaticForwardersForNonTopLevelObjects = true
114115
} else if (option == "nowarnGlobalExecutionContext") {
115116
warnGlobalExecutionContext = false
117+
} else if (option == "allowOrphanJSAwait") {
118+
allowOrphanJSAwait = true
116119
} else if (option.startsWith("mapSourceURI:")) {
117120
val uris = option.stripPrefix("mapSourceURI:").split("->")
118121

compiler/src/test/scala/org/scalajs/nscplugin/test/JSAsyncAwaitTest.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ class JSAsyncAwaitTest extends DirectTest with TestHelpers {
3535
|newSource1.scala:5: error: Illegal use of js.await().
3636
|It can only be used inside a js.async {...} block, without any lambda,
3737
|by-name argument or nested method in-between.
38+
|If you compile for WebAssembly, you can allow arbitrary js.await()
39+
|calls with -P:scalajs:allowOrphanJSAwait.
3840
| js.await(x)
3941
| ^
4042
"""
@@ -51,6 +53,8 @@ class JSAsyncAwaitTest extends DirectTest with TestHelpers {
5153
|newSource1.scala:5: error: Illegal use of js.await().
5254
|It can only be used inside a js.async {...} block, without any lambda,
5355
|by-name argument or nested method in-between.
56+
|If you compile for WebAssembly, you can allow arbitrary js.await()
57+
|calls with -P:scalajs:allowOrphanJSAwait.
5458
| val f: () => Int = () => js.await(x)
5559
| ^
5660
"""
@@ -67,6 +71,8 @@ class JSAsyncAwaitTest extends DirectTest with TestHelpers {
6771
|newSource1.scala:5: error: Illegal use of js.await().
6872
|It can only be used inside a js.async {...} block, without any lambda,
6973
|by-name argument or nested method in-between.
74+
|If you compile for WebAssembly, you can allow arbitrary js.await()
75+
|calls with -P:scalajs:allowOrphanJSAwait.
7076
| def f(): Int = js.await(x)
7177
| ^
7278
"""

ir/shared/src/main/scala/org/scalajs/ir/Trees.scala

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -219,12 +219,15 @@ object Trees {
219219

220220
/** `await arg`.
221221
*
222-
* This is directly equivalent to a JavaScript `await` expression. This node
223-
* is only valid within a [[Closure]] node with the `async` flag.
222+
* This is directly equivalent to a JavaScript `await` expression.
223+
*
224+
* If used directly within a [[Closure]] node with the `async` flag, this
225+
* node is always valid. However, when used anywhere else, it is an "orphan"
226+
* await. Orphan awaits only link when targeting WebAssembly.
224227
*
225228
* This is not a `UnaryOp` because of the above strict scoping rule. For
226-
* example, it is not safe to pull this node out of or into an intervening
227-
* closure, contrary to `UnaryOp`s.
229+
* example, unless it is orphan to begin with, it is not safe to pull this
230+
* node out of or into an intervening closure, contrary to `UnaryOp`s.
228231
*/
229232
sealed case class JSAwait(arg: Tree)(implicit val pos: Position) extends Tree {
230233
val tpe = AnyType

library/src/main/scala/scala/scalajs/js/package.scala

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,19 @@ package object js {
192192
* not be nested in any local method, class, by-name argument or closure.
193193
* The latter includes `for` comprehensions. They may appear within
194194
* conditional branches, `while` loops and `try/catch/finally` blocks.
195+
*
196+
* <h2>Orphan `await`s in WebAssembly</h2>
197+
*
198+
* When compiling for Scala.js-on-Wasm only, you can allow calls to
199+
* `js.await` anywhere, using the compiler option
200+
* `-P:scalajs:allowOrphanJSAwait`.
201+
*
202+
* Calls to orphan `js.await`s are validated at run-time. There must exist
203+
* a dynamically enclosing `js.async { ... }` block on the call stack.
204+
* Moreover, there cannot be any JavaScript frame (function invocation) in
205+
* the call stack between the `js.async { ... }` block and the call to
206+
* `js.await`. If those conditions are not met, a JavaScript exception of
207+
* type `WebAssembly.SuspendError` gets thrown.
195208
*/
196209
def async[A](body: => A): js.Promise[A] =
197210
throw new java.lang.Error("stub")

linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analysis.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,8 @@ object Analysis {
217217

218218
final case class AsyncWithoutES2017Support(from: From) extends Error
219219

220+
final case class OrphanAwaitWithoutWebAssembly(from: From) extends Error
221+
220222
final case class InvalidLinkTimeProperty(
221223
linkTimePropertyName: String,
222224
linkTimePropertyType: Type,
@@ -281,6 +283,8 @@ object Analysis {
281283
"Uses the ** operator with an ECMAScript version older than ES 2016"
282284
case AsyncWithoutES2017Support(_) =>
283285
"Uses an async block with an ECMAScript version older than ES 2017"
286+
case OrphanAwaitWithoutWebAssembly(_) =>
287+
"Uses an orphan await (outside of an async block) without targeting WebAssembly"
284288
case InvalidLinkTimeProperty(name, tpe, _) =>
285289
s"Uses invalid link-time property ${name} of type ${tpe}"
286290
}

linker/shared/src/main/scala/org/scalajs/linker/analyzer/Analyzer.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1527,6 +1527,11 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean,
15271527
_errors ::= AsyncWithoutES2017Support(from)
15281528
}
15291529

1530+
if ((globalFlags & ReachabilityInfo.FlagUsedOrphanAwait) != 0 &&
1531+
!config.coreSpec.targetIsWebAssembly) {
1532+
_errors ::= OrphanAwaitWithoutWebAssembly(from)
1533+
}
1534+
15301535
if ((globalFlags & ReachabilityInfo.FlagUsedClassSuperClass) != 0) {
15311536
_classSuperClassUsed.set(true)
15321537
}

linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,9 @@ object Infos {
115115
final val FlagAccessedImportMeta = 1 << 2
116116
final val FlagUsedExponentOperator = 1 << 3
117117
final val FlagUsedAsync = 1 << 4
118-
final val FlagUsedClassSuperClass = 1 << 5
119-
final val FlagNeedsDesugaring = 1 << 6
118+
final val FlagUsedOrphanAwait = 1 << 5
119+
final val FlagUsedClassSuperClass = 1 << 6
120+
final val FlagNeedsDesugaring = 1 << 7
120121
}
121122

122123
/** Things from a given class that are reached by one method. */
@@ -416,6 +417,9 @@ object Infos {
416417
def addUsedAsync(): this.type =
417418
setFlag(ReachabilityInfo.FlagUsedAsync)
418419

420+
def addUsedOrphanAwait(): this.type =
421+
setFlag(ReachabilityInfo.FlagUsedOrphanAwait)
422+
419423
def addUsedIntLongDivModByMaybeZero(): this.type =
420424
addInstantiatedClass(ArithmeticExceptionClass, StringArgConstructorName)
421425

@@ -592,6 +596,13 @@ object Infos {
592596
private final class GenInfoTraverser(version: Version) extends Traverser {
593597
private val builder = new ReachabilityInfoBuilder(version)
594598

599+
/** Whether we are currently in the body of an `async` closure.
600+
*
601+
* If we encounter a `JSAwait` node while this is `false`, it is an
602+
* orphan await.
603+
*/
604+
private var inAsync: Boolean = false
605+
595606
def generateMethodInfo(methodDef: MethodDef): MethodInfo = {
596607
val methodName = methodDef.methodName
597608
methodName.paramTypeRefs.foreach(builder.maybeAddReferencedClass)
@@ -672,6 +683,22 @@ object Infos {
672683
}
673684
traverse(rhs)
674685

686+
/* Closure may have to adjust the inAsync flag before and after
687+
* traversing its body.
688+
*/
689+
case Closure(flags, _, _, _, _, body, captureValues) =>
690+
if (flags.async)
691+
builder.addUsedAsync()
692+
693+
// No point in using a try..finally. Instances of this class are single-use.
694+
val savedInAsync = inAsync
695+
inAsync = flags.async
696+
traverse(body)
697+
inAsync = savedInAsync
698+
699+
// Capture values are in the enclosing scope; not the scope of the closure
700+
captureValues.foreach(traverse(_))
701+
675702
// In all other cases, we'll have to call super.traverse()
676703
case _ =>
677704
tree match {
@@ -789,9 +816,9 @@ object Infos {
789816
case JSBinaryOp(JSBinaryOp.**, _, _) =>
790817
builder.addUsedExponentOperator()
791818

792-
case Closure(flags, _, _, _, _, _, _) =>
793-
if (flags.async)
794-
builder.addUsedAsync()
819+
case JSAwait(_) =>
820+
if (!inAsync)
821+
builder.addUsedOrphanAwait()
795822

796823
case LoadJSConstructor(className) =>
797824
builder.addInstantiatedClass(className)

linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -789,8 +789,6 @@ private final class ClassDefChecker(classDef: ClassDef,
789789
checkTree(default, env)
790790

791791
case JSAwait(arg) =>
792-
if (!env.inAsync)
793-
reportError(i"Illegal `await` outside of `async` closure")
794792
checkTree(arg, env)
795793

796794
case Debugger() =>
@@ -1086,7 +1084,6 @@ private final class ClassDefChecker(classDef: ClassDef,
10861084
.fromParams(captureParams ++ params ++ restParam)
10871085
.withHasNewTarget(!flags.arrow)
10881086
.withMaybeThisType(!flags.arrow, AnyType)
1089-
.withInAsync(flags.async)
10901087
checkTree(body, bodyEnv)
10911088
}
10921089
}
@@ -1183,9 +1180,7 @@ object ClassDefChecker {
11831180
/** Return types by label. */
11841181
val returnLabels: Set[LabelName],
11851182
/** Whether usages of `this` are restricted in this scope. */
1186-
val isThisRestricted: Boolean,
1187-
/** Whether we are in an `async` closure, where `await` expressions are valid. */
1188-
val inAsync: Boolean
1183+
val isThisRestricted: Boolean
11891184
) {
11901185
import Env._
11911186

@@ -1208,17 +1203,13 @@ object ClassDefChecker {
12081203
def withIsThisRestricted(isThisRestricted: Boolean): Env =
12091204
copy(isThisRestricted = isThisRestricted)
12101205

1211-
def withInAsync(inAsync: Boolean): Env =
1212-
copy(inAsync = inAsync)
1213-
12141206
private def copy(
12151207
hasNewTarget: Boolean = hasNewTarget,
12161208
locals: Map[LocalName, LocalDef] = locals,
12171209
returnLabels: Set[LabelName] = returnLabels,
1218-
isThisRestricted: Boolean = isThisRestricted,
1219-
inAsync: Boolean = inAsync
1210+
isThisRestricted: Boolean = isThisRestricted
12201211
): Env = {
1221-
new Env(hasNewTarget, locals, returnLabels, isThisRestricted, inAsync)
1212+
new Env(hasNewTarget, locals, returnLabels, isThisRestricted)
12221213
}
12231214
}
12241215

@@ -1228,8 +1219,7 @@ object ClassDefChecker {
12281219
hasNewTarget = false,
12291220
locals = Map.empty,
12301221
returnLabels = Set.empty,
1231-
isThisRestricted = false,
1232-
inAsync = false
1222+
isThisRestricted = false
12331223
)
12341224
}
12351225

@@ -1242,8 +1232,7 @@ object ClassDefChecker {
12421232
hasNewTarget = false,
12431233
paramLocalDefs.toMap,
12441234
Set.empty,
1245-
isThisRestricted = false,
1246-
inAsync = false
1235+
isThisRestricted = false
12471236
)
12481237
}
12491238
}

linker/shared/src/test/scala/org/scalajs/linker/AnalyzerTest.scala

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -702,6 +702,25 @@ class AnalyzerTest {
702702
}
703703
}
704704

705+
@Test
706+
def orphanAwaitWithoutWebAssembly(): AsyncResult = await {
707+
val classDefs = Seq(
708+
mainTestClassDef {
709+
JSAwait(int(5))
710+
}
711+
)
712+
713+
val moduleInitializer = MainTestModuleInitializers
714+
715+
val analysis = computeAnalysis(classDefs,
716+
moduleInitializers = MainTestModuleInitializers,
717+
config = StandardConfig().withESFeatures(_.withESVersion(ESVersion.ES2017)))
718+
719+
assertContainsError("OrphanAwaitWithoutWebAssembly", analysis) {
720+
case OrphanAwaitWithoutWebAssembly(_) => true
721+
}
722+
}
723+
705724
@Test
706725
def importMetaWithoutESModule(): AsyncResult = await {
707726
val classDefs = Seq(

project/Build.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2259,6 +2259,8 @@ object Build {
22592259
esVersion >= ESVersion.ES2016) :::
22602260
includeIf(testDir / "require-async-await",
22612261
esVersion >= ESVersion.ES2017) :::
2262+
includeIf(testDir / "require-orphan-await",
2263+
esVersion >= ESVersion.ES2017 && isWebAssembly) :::
22622264
includeIf(testDir / "require-modules",
22632265
hasModules) :::
22642266
includeIf(testDir / "require-no-modules",
@@ -2286,6 +2288,20 @@ object Build {
22862288
Test / scalacOptions ++= scalaJSCompilerOption("genStaticForwardersForNonTopLevelObjects"),
22872289
Test / scalacOptions ++= scalaJSCompilerOption("nowarnGlobalExecutionContext"),
22882290

2291+
Test / scalacOptions ++= {
2292+
val linkerConfig = scalaJSStage.value match {
2293+
case FastOptStage => (scalaJSLinkerConfig in (Compile, fastLinkJS)).value
2294+
case FullOptStage => (scalaJSLinkerConfig in (Compile, fullLinkJS)).value
2295+
}
2296+
2297+
if (linkerConfig.experimentalUseWebAssembly &&
2298+
linkerConfig.esFeatures.esVersion >= ESVersion.ES2017) {
2299+
scalaJSCompilerOption("allowOrphanJSAwait")
2300+
} else {
2301+
Nil
2302+
}
2303+
},
2304+
22892305
scalaJSLinkerConfig ~= { _.withSemantics(TestSuiteLinkerOptions.semantics _) },
22902306
scalaJSModuleInitializers in Test ++= TestSuiteLinkerOptions.moduleInitializers,
22912307

0 commit comments

Comments
 (0)
0