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

Skip to content

Commit a803984

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 4f6a714 commit a803984

File tree

13 files changed

+204
-27
lines changed
Filter options

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
@@ -5355,11 +5355,13 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
53555355

53565356
case JS_AWAIT =>
53575357
// js.await(arg)
5358-
if (!methodsAllowingJSAwait.contains(currentMethodSym)) {
5358+
if (!scalaJSOpts.allowOrphanJSAwait && !methodsAllowingJSAwait.contains(currentMethodSym)) {
53595359
reporter.error(pos,
53605360
"Illegal use of js.await().\n" +
53615361
"It can only be used inside a js.async {...} block, without any lambda,\n" +
5362-
"by-name argument or nested method in-between.")
5362+
"by-name argument or nested method in-between.\n" +
5363+
"If you compile for WebAssembly, you can allow arbitrary js.await()\n" +
5364+
"calls with -P:scalajs:allowOrphanJSAwait.")
53635365
}
53645366
val arg = genArgs1
53655367
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
@@ -220,12 +220,15 @@ object Trees {
220220

221221
/** `await arg`.
222222
*
223-
* This is directly equivalent to a JavaScript `await` expression. This node
224-
* is only valid within a [[Closure]] node with the `async` flag.
223+
* This is directly equivalent to a JavaScript `await` expression.
224+
*
225+
* If used directly within a [[Closure]] node with the `async` flag, this
226+
* node is always valid. However, when used anywhere else, it is an "orphan"
227+
* await. Orphan awaits only link when targeting WebAssembly.
225228
*
226229
* This is not a `UnaryOp` because of the above strict scoping rule. For
227-
* example, it is not safe to pull this node out of or into an intervening
228-
* closure, contrary to `UnaryOp`s.
230+
* example, unless it is orphan to begin with, it is not safe to pull this
231+
* node out of or into an intervening closure, contrary to `UnaryOp`s.
229232
*/
230233
sealed case class JSAwait(arg: Tree)(implicit val pos: Position) extends Tree {
231234
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
@@ -1526,6 +1526,11 @@ private class AnalyzerRun(config: CommonPhaseConfig, initial: Boolean,
15261526
_errors ::= AsyncWithoutES2017Support(from)
15271527
}
15281528

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

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

Lines changed: 32 additions & 5 deletions
< 17A7 td data-grid-cell-id="diff-1f099498bc837b2bea0a67952bd95f4a268d9611039f3e2001de391531e675be-778-803-2" data-line-anchor="diff-1f099498bc837b2bea0a67952bd95f4a268d9611039f3e2001de391531e675beL778" data-selected="false" role="gridcell" style="background-color:var(--diffBlob-deletionLine-bgColor, var(--diffBlob-deletion-bgColor-line));padding-right:24px" tabindex="-1" valign="top" class="focusable-grid-cell diff-text-cell left-side-diff-cell border-right left-side">-
if (flags.async)
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,9 @@ object Infos {
120120
final val FlagAccessedImportMeta = 1 << 2
121121
final val FlagUsedExponentOperator = 1 << 3
122122
final val FlagUsedAsync = 1 << 4
123-
final val FlagUsedClassSuperClass = 1 << 5
124-
final val FlagNeedsDesugaring = 1 << 6
123+
final val FlagUsedOrphanAwait = 1 << 5
124+
final val FlagUsedClassSuperClass = 1 << 6
125+
final val FlagNeedsDesugaring = 1 << 7
125126
}
126127

127128
/** Things from a given class that are reached by one method. */
@@ -402,6 +403,9 @@ object Infos {
402403
def addUsedAsync(): this.type =
403404
setFlag(ReachabilityInfo.FlagUsedAsync)
404405

406+
def addUsedOrphanAwait(): this.type =
407+
setFlag(ReachabilityInfo.FlagUsedOrphanAwait)
408+
405409
def addUsedIntLongDivModByMaybeZero(): this.type =
406410
addInstantiatedClass(ArithmeticExceptionClass, StringArgConstructorName)
407411

@@ -577,6 +581,13 @@ object Infos {
577581
private final class GenInfoTraverser(version: Version) extends Traverser {
578582
private val builder = new ReachabilityInfoBuilder(version)
579583

584+
/** Whether we are currently in the body of an `async` closure.
585+
*
586+
* If we encounter a `JSAwait` node while this is `false`, it is an
587+
* orphan await.
588+
*/
589+
private var inAsync: Boolean = false
590+
580591
def generateMethodInfo(methodDef: MethodDef): MethodInfo = {
581592
val methodName = methodDef.methodName
582593
methodName.paramTypeRefs.foreach(builder.maybeAddReferencedClass)
@@ -657,6 +668,22 @@ object Infos {
657668
}
658669
traverse(rhs)
659670

671+
/* Closure may have to adjust the inAsync flag before and after
672+
* traversing its body.
673+
*/
674+
case Closure(flags, _, _, _, _, body, captureValues) =>
675+
if (flags.async)
676+
builder.addUsedAsync()
677+
678+
// No point in using a try..finally. Instances of this class are single-use.
679+
val savedInAsync = inAsync
680+
inAsync = flags.async
681+
traverse(body)
682+
inAsync = savedInAsync
683+
684+
// Capture values are in the enclosing scope; not the scope of the closure
685+
captureValues.foreach(traverse(_))
686+
660687
// In all other cases, we'll have to call super.traverse()
661688
case _ =>
662689
tree match {
@@ -774,9 +801,9 @@ object Infos {
774801
case JSBinaryOp(JSBinaryOp.**, _, _) =>
775802
builder.addUsedExponentOperator()
776803

777-
case Closure(flags, _, _, _, _, _, _) =>
778
779-
builder.addUsedAsync()
804+
case JSAwait(_) =>
805+
if (!inAsync)
806+
builder.addUsedOrphanAwait()
780807

781808
case LoadJSConstructor(className) =>
782809
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
@@ -790,8 +790,6 @@ private final class ClassDefChecker(classDef: ClassDef,
790790
checkTree(default, env)
791791

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

797795
case Debugger() =>
@@ -1087,7 +1085,6 @@ private final class ClassDefChecker(classDef: ClassDef,
10871085
.fromParams(captureParams ++ params ++ restParam)
10881086
.withHasNewTarget(!flags.arrow)
10891087
.withMaybeThisType(!flags.arrow, AnyType)
1090-
.withInAsync(flags.async)
10911088
checkTree(body, bodyEnv)
10921089
}
10931090
}
@@ -1184,9 +1181,7 @@ object ClassDefChecker {
11841181
/** Return types by label. */
11851182
val returnLabels: Set[LabelName],
11861183
/** Whether usages of `this` are restricted in this scope. */
1187-
val isThisRestricted: Boolean,
1188-
/** Whether we are in an `async` closure, where `await` expressions are valid. */
1189-
val inAsync: Boolean
1184+
val isThisRestricted: Boolean
11901185
) {
11911186
import Env._
11921187

@@ -1209,17 +1204,13 @@ object ClassDefChecker {
12091204
def withIsThisRestricted(isThisRestricted: Boolean): Env =
12101205
copy(isThisRestricted = isThisRestricted)
12111206

1212-
def withInAsync(inAsync: Boolean): Env =
1213-
copy(inAsync = inAsync)
1214-
12151207
private def copy(
12161208
hasNewTarget: Boolean = hasNewTarget,
12171209
locals: Map[LocalName, LocalDef] = locals,
12181210
returnLabels: Set[LabelName] = returnLabels,
1219-
isThisRestricted: Boolean = isThisRestricted,
1220-
inAsync: Boolean = inAsync
1211+
isThisRestricted: Boolean = isThisRestricted
12211212
): Env = {
1222-
new Env(hasNewTarget, locals, returnLabels, isThisRestricted, inAsync)
1213+
new Env(hasNewTarget, locals, returnLabels, isThisRestricted)
12231214
}
12241215
}
12251216

@@ -1229,8 +1220,7 @@ object ClassDefChecker {
12291220
hasNewTarget = false,
12301221
locals = Map.empty,
12311222
returnLabels = Set.empty,
1232-
isThisRestricted = false,
1233-
inAsync = false
1223+
isThisRestricted = false
12341224
)
12351225
}
12361226

@@ -1243,8 +1233,7 @@ object ClassDefChecker {
12431233
hasNewTarget = false,
12441234
paramLocalDefs.toMap,
12451235
Set.empty,
1246-
isThisRestricted = false,
1247-
inAsync = false
1236+
isThisRestricted = false
12481237
)
12491238
}
12501239
}

0 commit comments

Comments
 (0)
0