Skip to content

Commit b7e7c56

Browse files
committed
Fix scala-js#4688: Introduce IR nodes to wrap/unwrap JavaScript exceptions.
This allows to defer to linking the reference to `scala.scalajs.js.JavaScriptException`. For compatibility reasons, we must preserve that very class when it is available on the classpath. However, if it is not already on the classpath, the emitter provides an implementation through the linker-private-library. That implementation does not depend on the scalalib, as checked by the IR cleaner. We add a linker test in `RunTest` to make sure that we can indeed correctly link a codebase containing a `WrapAsThrowable` node, but not any implementation of `JavaScriptException`. In a hypothetical Scala.js 2.x, we would move `JavaScriptException` to the `java.lang` package.
1 parent 408a5ba commit b7e7c56

File tree

26 files changed

+347
-36
lines changed

26 files changed

+347
-36
lines changed

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

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2398,14 +2398,10 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
23982398
case Throw(expr) =>
23992399
val ex = genExpr(expr)
24002400
js.Throw {
2401-
if (!ex.isInstanceOf[js.Null] && isMaybeJavaScriptException(expr.tpe)) {
2402-
genApplyMethod(
2403-
genLoadModule(RuntimePackageModule),
2404-
Runtime_unwrapJavaScriptException,
2405-
List(ex))
2406-
} else {
2401+
if (!ex.isInstanceOf[js.Null] && isMaybeJavaScriptException(expr.tpe))
2402+
js.UnwrapFromThrowable(ex)
2403+
else
24072404
ex
2408-
}
24092405
}
24102406

24112407
/* !!! Copy-pasted from `CleanUp.scala` upstream and simplified with
@@ -2930,12 +2926,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
29302926

29312927
val (exceptValDef, exceptVar) = if (mightCatchJavaScriptException) {
29322928
val valDef = js.VarDef(freshLocalIdent("e"), NoOriginalName,
2933-
encodeClassType(ThrowableClass), mutable = false, {
2934-
genApplyMethod(
2935-
genLoadModule(RuntimePackageModule),
2936-
Runtime_wrapJavaScriptException,
2937-
List(origExceptVar))
2938-
})
2929+
encodeClassType(ThrowableClass), mutable = false, js.WrapAsThrowable(origExceptVar))
29392930
(valDef, valDef.ref)
29402931
} else {
29412932
(js.Skip(), origExceptVar)

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,6 @@ trait JSDefinitions {
110110
lazy val Special_debugger = getMemberMethod(SpecialPackageModule, newTermName("debugger"))
111111

112112
lazy val RuntimePackageModule = getPackageObject("scala.scalajs.runtime")
113-
lazy val Runtime_wrapJavaScriptException = getMemberMethod(RuntimePackageModule, newTermName("wrapJavaScriptException"))
114-
lazy val Runtime_unwrapJavaScriptException = getMemberMethod(RuntimePackageModule, newTermName("unwrapJavaScriptException"))
115113
lazy val Runtime_toScalaVarArgs = getMemberMethod(RuntimePackageModule, newTermName("toScalaVarArgs"))
116114
lazy val Runtime_toJSVarArgs = getMemberMethod(RuntimePackageModule, newTermName("toJSVarArgs"))
117115
lazy val Runtime_constructorOf = getMemberMethod(RuntimePackageModule, newTermName("constructorOf"))

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -517,8 +517,8 @@ class OptimizationTest extends JSASTTest {
517517
}
518518
}
519519
}
520-
""".hasNot("call to the scala.scalajs.runtime.package$ package module class") {
521-
case js.LoadModule(ScalaJSRuntimePackageClass) =>
520+
""".hasNot("WrapAsThrowable") {
521+
case js.WrapAsThrowable(_) =>
522522
}
523523

524524
// Confidence check
@@ -534,16 +534,15 @@ class OptimizationTest extends JSASTTest {
534534
}
535535
}
536536
}
537-
""".hasExactly(1, "call to the scala.scalajs.runtime.package$ package module class") {
538-
case js.LoadModule(ScalaJSRuntimePackageClass) =>
537+
""".hasExactly(1, "WrapAsThrowable") {
538+
case js.WrapAsThrowable(_) =>
539539
}
540540
}
541541
}
542542

543543
object OptimizationTest {
544544

545545
private val ArrayModuleClass = ClassName("scala.Array$")
546-
private val ScalaJSRuntimePackageClass = ClassName("scala.scalajs.runtime.package$")
547546

548547
private val applySimpleMethodName = SimpleMethodName("apply")
549548

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,14 @@ object Hashers {
345345
mixTag(TagIdentityHashCode)
346346
mixTree(expr)
347347

348+
case WrapAsThrowable(expr) =>
349+
mixTag(TagWrapAsThrowable)
350+
mixTree(expr)
351+
352+
case UnwrapFromThrowable(expr) =>
353+
mixTag(TagUnwrapFromThrowable)
354+
mixTree(expr)
355+
348356
case JSNew(ctor, args) =>
349357
mixTag(TagJSNew)
350358
mixTree(ctor)

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,13 @@ object Names {
486486
/** `java.io.Serializable`, which is an ancestor of array classes. */
487487
val SerializableClass: ClassName = ClassName("java.io.Serializable")
488488

489+
/** The superclass of all throwables.
490+
*
491+
* This is the result type of `WrapAsThrowable` nodes, as well as the input
492+
* type of `UnwrapFromThrowable`.
493+
*/
494+
val ThrowableClass = ClassName("java.lang.Throwable")
495+
489496
/** The exception thrown by a division by 0. */
490497
val ArithmeticExceptionClass: ClassName =
491498
ClassName("java.lang.ArithmeticException")

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,16 @@ object Printers {
558558
print(expr)
559559
print(')')
560560

561+
case WrapAsThrowable(expr) =>
562+
print("<wrapAsThrowable>(")
563+
print(expr)
564+
print(")")
565+
566+
case UnwrapFromThrowable(expr) =>
567+
print("<unwrapFromThrowable>(")
568+
print(expr)
569+
print(")")
570+
561571
// JavaScript expressions
562572

563573
case JSNew(ctor, args) =>

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,14 @@ object Serializers {
412412
writeTagAndPos(TagIdentityHashCode)
413413
writeTree(expr)
414414

415+
case WrapAsThrowable(expr) =>
416+
writeTagAndPos(TagWrapAsThrowable)
417+
writeTree(expr)
418+
419+
case UnwrapFromThrowable(expr) =>
420+
writeTagAndPos(TagUnwrapFromThrowable)
421+
writeTree(expr)
422+
415423
case JSNew(ctor, args) =>
416424
writeTagAndPos(TagJSNew)
417425
writeTree(ctor); writeTreeOrJSSpreads(args)
@@ -1142,6 +1150,11 @@ object Serializers {
11421150
case TagClone => Clone(readTree())
11431151
case TagIdentityHashCode => IdentityHashCode(readTree())
11441152

1153+
case TagWrapAsThrowable =>
1154+
WrapAsThrowable(readTree())
1155+
case TagUnwrapFromThrowable =>
1156+
UnwrapFromThrowable(readTree())
1157+
11451158
case TagJSNew => JSNew(readTree(), readTreeOrJSSpreads())
11461159
case TagJSPrivateSelect => JSPrivateSelect(readTree(), readClassName(), readFieldIdent())
11471160
case TagJSSelect => JSSelect(readTree(), readTree())

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@ private[ir] object Tags {
122122

123123
final val TagJSNewTarget = TagJSImportMeta + 1
124124

125+
// New in 1.11
126+
127+
final val TagWrapAsThrowable = TagJSNewTarget + 1
128+
final val TagUnwrapFromThrowable = TagWrapAsThrowable + 1
129+
125130
// Tags for member defs
126131

127132
final val TagFieldDef = 1

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ object Transformers {
149149
case IdentityHashCode(expr) =>
150150
IdentityHashCode(transformExpr(expr))
151151

152+
case WrapAsThrowable(expr) =>
153+
WrapAsThrowable(transformExpr(expr))
154+
155+
case UnwrapFromThrowable(expr) =>
156+
UnwrapFromThrowable(transformExpr(expr))
157+
152158
// JavaScript expressions
153159

154160
case JSNew(ctor, args) =>

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,12 @@ object Traversers {
142142
case IdentityHashCode(expr) =>
143143
traverse(expr)
144144

145+
case WrapAsThrowable(expr) =>
146+
traverse(expr)
147+
148+
case UnwrapFromThrowable(expr) =>
149+
traverse(expr)
150+
145151
// JavaScript expressions
146152

147153
case JSNew(ctor, args) =>

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,16 @@ object Trees {
493493
val tpe = IntType
494494
}
495495

496+
sealed case class WrapAsThrowable(expr: Tree)(implicit val pos: Position)
497+
extends Tree {
498+
val tpe = ClassType(ThrowableClass)
499+
}
500+
501+
sealed case class UnwrapFromThrowable(expr: Tree)(implicit val pos: Position)
502+
extends Tree {
503+
val tpe = AnyType
504+
}
505+
496506
// JavaScript expressions
497507

498508
sealed case class JSNew(ctor: Tree, args: List[TreeOrJSSpread])(

ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,15 @@ class PrintersTest {
599599
assertPrintEquals("<identityHashCode>(x)", IdentityHashCode(ref("x", AnyType)))
600600
}
601601

602+
@Test def printWrapAsThrowable(): Unit = {
603+
assertPrintEquals("<wrapAsThrowable>(e)", WrapAsThrowable(ref("e", AnyType)))
604+
}
605+
606+
@Test def printUnwrapFromThrowable(): Unit = {
607+
assertPrintEquals("<unwrapFromThrowable>(e)",
608+
UnwrapFromThrowable(ref("e", ClassType(ThrowableClass))))
609+
}
610+
602611
@Test def printJSNew(): Unit = {
603612
assertPrintEquals("new C()", JSNew(ref("C", AnyType), Nil))
604613
assertPrintEquals("new C(4, 5)", JSNew(ref("C", AnyType), List(i(4), i(5))))

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ package object runtime {
1818

1919
import scala.scalajs.runtime.Compat._
2020

21+
@deprecated("Unused by the codegen", "1.11.0")
2122
def wrapJavaScriptException(e: Any): Throwable = e match {
2223
case e: Throwable => e
2324
case _ => js.JavaScriptException(e)
2425
}
2526

27+
@deprecated("Unused by the codegen", "1.11.0")
2628
def unwrapJavaScriptException(th: Throwable): Any = th match {
2729
case js.JavaScriptException(e) => e
2830
case _ => th
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*
2+
* Scala.js (https://www.scala-js.org/)
3+
*
4+
* Copyright EPFL.
5+
*
6+
* Licensed under Apache License 2.0
7+
* (https://www.apache.org/licenses/LICENSE-2.0).
8+
*
9+
* See the NOTICE file distributed with this work for
10+
* additional information regarding copyright ownership.
11+
*/
12+
13+
package scala.scalajs.js
14+
15+
import scala.language.reflectiveCalls
16+
17+
final class JavaScriptException(val exception: scala.Any)
18+
extends RuntimeException {
19+
20+
override def getMessage(): String = exception.toString()
21+
22+
override def fillInStackTrace(): Throwable = {
23+
type JSExceptionEx = JavaScriptException {
24+
def setStackTraceStateInternal(e: scala.Any): Unit
25+
}
26+
this.asInstanceOf[JSExceptionEx].setStackTraceStateInternal(exception)
27+
this
28+
}
29+
}

linker/jvm/src/main/scala/org/scalajs/linker/backend/emitter/PrivateLibHolder.scala

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,18 @@ import org.scalajs.linker.interface.IRFile
1818
import org.scalajs.linker.standard.MemIRFileImpl
1919

2020
object PrivateLibHolder {
21-
private val relativeDir = "org/scalajs/linker/runtime/"
22-
private val sjsirNames = Seq(
23-
"RuntimeLong.sjsir",
24-
"RuntimeLong$.sjsir",
25-
"UndefinedBehaviorError.sjsir"
21+
private val sjsirPaths = Seq(
22+
"org/scalajs/linker/runtime/RuntimeLong.sjsir",
23+
"org/scalajs/linker/runtime/RuntimeLong$.sjsir",
24+
"org/scalajs/linker/runtime/UndefinedBehaviorError.sjsir",
25+
"scala/scalajs/js/JavaScriptException.sjsir"
2626
)
2727

2828
val files: Seq[IRFile] = {
29-
for (name <- sjsirNames) yield {
29+
for (path <- sjsirPaths) yield {
30+
val name = path.substring(path.lastIndexOf('/') + 1)
3031
new MemIRFileImpl(
31-
path = relativeDir + name,
32+
path = path,
3233
version = Some(""), // this indicates that the file never changes
3334
content = readResource(name)
3435
)

linker/jvm/src/test/scala/org/scalajs/linker/RunTest.scala

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import org.junit.{Rule, Test}
2121
import org.junit.Assert._
2222
import org.junit.rules.TemporaryFolder
2323

24+
import org.scalajs.ir.Names._
2425
import org.scalajs.ir.Trees._
26+
import org.scalajs.ir.Types._
2527

2628
import org.scalajs.junit.async._
2729

@@ -58,6 +60,37 @@ class RunTest {
5860
TestKit.InputKind.ESModule)
5961
}
6062

63+
@Test
64+
def wrapAsThrowable(): AsyncResult = await {
65+
// Check that WrapAsThrowable can link without js.JavaScriptException on the classpath
66+
67+
val getMessage = MethodName("getMessage", Nil, T)
68+
69+
val e = VarRef("e")(ClassType(ThrowableClass))
70+
71+
val classDefs = Seq(
72+
mainTestClassDef(Block(
73+
VarDef("e", NON, ClassType(ThrowableClass), mutable = false,
74+
WrapAsThrowable(JSNew(JSGlobalRef("RangeError"), List(str("boom"))))),
75+
genAssert(IsInstanceOf(e, ClassType("java.lang.Exception"))),
76+
genAssertEquals(str("RangeError: boom"), Apply(EAF, e, getMessage, Nil)(ClassType(BoxedStringClass)))
77+
))
78+
)
79+
80+
testLinkAndRun(classDefs, MainTestModuleInitializers, StandardConfig(),
81+
TestKit.InputKind.Script)
82+
}
83+
84+
private def genAssertEquals(expected: Tree, actual: Tree): Tree =
85+
genAssert(BinaryOp(BinaryOp.===, expected, actual))
86+
87+
private def genAssert(test: Tree): Tree = {
88+
If(UnaryOp(UnaryOp.Boolean_!, test),
89+
Throw(JSNew(JSGlobalRef("Error"), List(str("Assertion failed")))),
90+
Skip())(
91+
NoType)
92+
}
93+
6194
private def testLinkAndRun(classDefs: Seq[ClassDef],
6295
moduleInitializers: List[ModuleInitializer],
6396
linkerConfig: StandardConfig, inputKind: TestKit.InputKind): Future[Unit] = {

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ object Infos {
3131

3232
private val cloneMethodName = MethodName("clone", Nil, ClassRef(ObjectClass))
3333

34+
/* Elements of WrapAsThrowable and UnwrapFromThrowable used by the Emitter
35+
* In theory, these should be an implementation detail of the Emitter, and
36+
* requested through `symbolRequirements`. However, doing so would mean that
37+
* we would never be able to dead-code-eliminate JavaScriptException, which
38+
* would be annoying.
39+
*/
40+
private val JavaScriptExceptionClass = ClassName("scala.scalajs.js.JavaScriptException")
41+
private val AnyArgConstructorName = MethodName.constructor(List(ClassRef(ObjectClass)))
42+
3443
final case class NamespacedMethodName(
3544
namespace: MemberNamespace, methodName: MethodName)
3645

@@ -583,6 +592,13 @@ object Infos {
583592
case GetClass(_) =>
584593
builder.addAccessedClassClass()
585594

595+
case WrapAsThrowable(_) =>
596+
builder.addUsedInstanceTest(ThrowableClass)
597+
builder.addInstantiatedClass(JavaScriptExceptionClass, AnyArgConstructorName)
598+
599+
case UnwrapFromThrowable(_) =>
600+
builder.addUsedInstanceTest(JavaScriptExceptionClass)
601+
586602
case JSPrivateSelect(qualifier, className, field) =>
587603
builder.addPrivateJSFieldUsed(className, field.name)
588604

linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/EmitterNames.scala

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,19 @@ import org.scalajs.ir.Types._
1818
private[emitter] object EmitterNames {
1919
// Class names
2020

21+
val JavaScriptExceptionClass =
22+
ClassName("scala.scalajs.js.JavaScriptException")
23+
2124
val UndefinedBehaviorErrorClass =
2225
ClassName("org.scalajs.linker.runtime.UndefinedBehaviorError")
2326

24-
val ThrowableClass = ClassName("java.lang.Throwable")
27+
// Field names
28+
29+
val exceptionFieldName = FieldName("exception")
2530

2631
// Method names
2732

33+
val AnyArgConstructorName = MethodName.constructor(List(ClassRef(ObjectClass)))
2834
val StringArgConstructorName = MethodName.constructor(List(ClassRef(BoxedStringClass)))
2935
val ThrowableArgConsructorName = MethodName.constructor(List(ClassRef(ThrowableClass)))
3036

0 commit comments

Comments
 (0)