Skip to content

Commit 994a598

Browse files
committed
Add js.special primitives for JS exception handling.
* `js.special.throw(e)` throws any value `e`. * `js.special.tryCatch(() => body)(e => handler)` catches any exception thrown by the body and handles it in the handler. * `js.special.wrapAsThrowable(e)` wraps `e` in a `JavaScriptException` if it is not a `Throwable`. * `js.special.unwrapFromThrowable(th)` unwraps `JavaScriptException`s and returns other values as is. These primitives can be used to implement some fundamental operations in a more direct way.
1 parent b7e7c56 commit 994a598

File tree

11 files changed

+330
-46
lines changed

11 files changed

+330
-46
lines changed

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5288,6 +5288,53 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G)
52885288
js.ForIn(objVarDef.ref, keyVarIdent, NoOriginalName, {
52895289
js.JSFunctionApply(fVarDef.ref, List(keyVarRef))
52905290
}))
5291+
5292+
case JS_THROW =>
5293+
// js.special.throw(arg)
5294+
js.Throw(genArgs1)
5295+
5296+
case JS_TRY_CATCH =>
5297+
/* js.special.tryCatch(arg1, arg2)
5298+
*
5299+
* We must generate:
5300+
*
5301+
* val body = arg1
5302+
* val handler = arg2
5303+
* try {
5304+
* body()
5305+
* } catch (e) {
5306+
* handler(e)
5307+
* }
5308+
*
5309+
* with temporary vals, because `arg2` must be evaluated before
5310+
* `body` executes. Moreover, exceptions thrown while evaluating
5311+
* the function values `arg1` and `arg2` must not be caught.
5312+
*/
5313+
val (arg1, arg2) = genArgs2
5314+
val bodyVarDef = js.VarDef(freshLocalIdent("body"), NoOriginalName,
5315+
jstpe.AnyType, mutable = false, arg1)
5316+
val handlerVarDef = js.VarDef(freshLocalIdent("handler"), NoOriginalName,
5317+
jstpe.AnyType, mutable = false, arg2)
5318+
val exceptionVarIdent = freshLocalIdent("e")
5319+
val exceptionVarRef = js.VarRef(exceptionVarIdent)(jstpe.AnyType)
5320+
js.Block(
5321+
bodyVarDef,
5322+
handlerVarDef,
5323+
js.TryCatch(
5324+
js.JSFunctionApply(bodyVarDef.ref, Nil),
5325+
exceptionVarIdent,
5326+
NoOriginalName,
5327+
js.JSFunctionApply(handlerVarDef.ref, List(exceptionVarRef))
5328+
)(jstpe.AnyType)
5329+
)
5330+
5331+
case WRAP_AS_THROWABLE =>
5332+
// js.special.wrapAsThrowable(arg)
5333+
js.WrapAsThrowable(genArgs1)
5334+
5335+
case UNWRAP_FROM_THROWABLE =>
5336+
// js.special.unwrapFromThrowable(arg)
5337+
js.UnwrapFromThrowable(genArgs1)
52915338
}
52925339
}
52935340

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ trait JSDefinitions {
107107
lazy val Special_instanceof = getMemberMethod(SpecialPackageModule, newTermName("instanceof"))
108108
lazy val Special_delete = getMemberMethod(SpecialPackageModule, newTermName("delete"))
109109
lazy val Special_forin = getMemberMethod(SpecialPackageModule, newTermName("forin"))
110+
lazy val Special_throw = getMemberMethod(SpecialPackageModule, newTermName("throw"))
111+
lazy val Special_tryCatch = getMemberMethod(SpecialPackageModule, newTermName("tryCatch"))
112+
lazy val Special_wrapAsThrowable = getMemberMethod(SpecialPackageModule, newTermName("wrapAsThrowable"))
113+
lazy val Special_unwrapFromThrowable = getMemberMethod(SpecialPackageModule, newTermName("unwrapFromThrowable"))
110114
lazy val Special_debugger = getMemberMethod(SpecialPackageModule, newTermName("debugger"))
111115

112116
lazy val RuntimePackageModule = getPackageObject("scala.scalajs.runtime")

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

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,16 @@ abstract class JSPrimitives {
5959
final val IDENTITY_HASH_CODE = LINKING_INFO + 1 // runtime.identityHashCode
6060
final val DYNAMIC_IMPORT = IDENTITY_HASH_CODE + 1 // runtime.dynamicImport
6161

62-
final val STRICT_EQ = DYNAMIC_IMPORT + 1 // js.special.strictEquals
63-
final val IN = STRICT_EQ + 1 // js.special.in
64-
final val INSTANCEOF = IN + 1 // js.special.instanceof
65-
final val DELETE = INSTANCEOF + 1 // js.special.delete
66-
final val FORIN = DELETE + 1 // js.special.forin
67-
final val DEBUGGER = FORIN + 1 // js.special.debugger
62+
final val STRICT_EQ = DYNAMIC_IMPORT + 1 // js.special.strictEquals
63+
final val IN = STRICT_EQ + 1 // js.special.in
64+
final val INSTANCEOF = IN + 1 // js.special.instanceof
65+
final val DELETE = INSTANCEOF + 1 // js.special.delete
66+
final val FORIN = DELETE + 1 // js.special.forin
67+
final val JS_THROW = FORIN + 1 // js.special.throw
68+
final val JS_TRY_CATCH = JS_THROW + 1 // js.special.tryCatch
69+
final val WRAP_AS_THROWABLE = JS_TRY_CATCH + 1 // js.special.wrapAsThrowable
70+
final val UNWRAP_FROM_THROWABLE = WRAP_AS_THROWABLE + 1 // js.special.unwrapFromThrowable
71+
final val DEBUGGER = UNWRAP_FROM_THROWABLE + 1 // js.special.debugger
6872

6973
final val LastJSPrimitiveCode = DEBUGGER
7074

@@ -114,6 +118,10 @@ abstract class JSPrimitives {
114118
addPrimitive(Special_instanceof, INSTANCEOF)
115119
addPrimitive(Special_delete, DELETE)
116120
addPrimitive(Special_forin, FORIN)
121+
addPrimitive(Special_throw, JS_THROW)
122+
addPrimitive(Special_tryCatch, JS_TRY_CATCH)
123+
addPrimitive(Special_wrapAsThrowable, WRAP_AS_THROWABLE)
124+
addPrimitive(Special_unwrapFromThrowable, UNWRAP_FROM_THROWABLE)
117125
addPrimitive(Special_debugger, DEBUGGER)
118126
}
119127

library/src/main/scala-new-collections/scala/scalajs/js/JSConverters.scala

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,7 @@ object JSConverters extends JSConvertersLowPrioImplicits {
172172
resolve(value)
173173

174174
case scala.util.Failure(th) =>
175-
reject(th match {
176-
case js.JavaScriptException(e) => e
177-
case _ => th
178-
})
175+
reject(js.special.unwrapFromThrowable(th))
179176
}
180177
})
181178
}

library/src/main/scala-old-collections/scala/scalajs/js/JSConverters.scala

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -182,10 +182,7 @@ object JSConverters extends js.JSConvertersLowPrioImplicits {
182182
resolve(value)
183183

184184
case scala.util.Failure(th) =>
185-
reject(th match {
186-
case js.JavaScriptException(e) => e
187-
case _ => th
188-
})
185+
reject(js.special.unwrapFromThrowable(th))
189186
}
190187
})
191188
}

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,7 @@ object Thenable {
5959
(): Unit | js.Thenable[Unit]
6060
},
6161
js.defined { (e: scala.Any) =>
62-
p2.failure(e match {
63-
case th: Throwable => th
64-
case _ => js.JavaScriptException(e)
65-
})
62+
p2.failure(js.special.wrapAsThrowable(e))
6663
(): Unit | js.Thenable[Unit]
6764
})
6865
p2.future

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,50 @@ package object special {
135135
def forin(obj: scala.Any)(f: js.Function1[scala.Any, scala.Any]): Unit =
136136
throw new java.lang.Error("stub")
137137

138+
/** Throw an arbitrary value, which will be caught as is by a JavaScript
139+
* `try..catch` statement.
140+
*
141+
* Usually, a Scala `throw` expression is more appropriate. Even if you
142+
* want to throw a JS error type such as [[js.Error]], it is more idiomatic
143+
* to wrap it in a [[js.JavaScriptException]] and throw that one.
144+
*
145+
* However, if you hold a value of an arbitrary type, which was caught by a
146+
* JavaScript `try..catch` statement (sometimes indirectly, such as with
147+
* [[js.Promise]]s), it is appropriate to use `js.special.throw` to rethrow
148+
* it.
149+
*/
150+
def `throw`(ex: scala.Any): Nothing =
151+
throw new java.lang.Error("stub")
152+
153+
/** Performs a JavaScript `try..catch`, which can catch any type of value.
154+
*
155+
* Usually, a Scala `try..catch` expression catching [[Throwable]] is more
156+
* appropriate. Values that are not instances of [[Throwable]], such as JS
157+
* error values, are then wrapped in a [[js.JavaScriptException]].
158+
*
159+
* However, if you need to get the originally thrown value, for example to
160+
* pass it on to a JavaScript error handler, it is appropriate to use
161+
* `js.special.tryCatch`.
162+
*/
163+
def tryCatch[A](body: js.Function0[A])(handler: js.Function1[scala.Any, A]): A =
164+
throw new java.lang.Error("stub")
165+
166+
/** Wrap any value so that it can be assigned to a [[Throwable]].
167+
*
168+
* Instances of [[Throwable]] are returned as is. Other values are wrapped
169+
* in a [[js.JavaScriptException]].
170+
*/
171+
def wrapAsThrowable(ex: scala.Any): Throwable =
172+
throw new java.lang.Error("stub")
173+
174+
/** Unwrap an exception value wrapped with `wrapAsThrowable`.
175+
*
176+
* Instances of [[js.JavaScriptException]] are unwrapped to return the
177+
* underlying value. Other values are returned as is.
178+
*/
179+
def unwrapFromThrowable(th: Throwable): scala.Any =
180+
throw new java.lang.Error("stub")
181+
138182
/** The value of the JavaScript `this` at the top-level of the generated
139183
* file.
140184
*

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

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,13 @@ package object runtime {
1818

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

21-
@deprecated("Unused by the codegen", "1.11.0")
22-
def wrapJavaScriptException(e: Any): Throwable = e match {
23-
case e: Throwable => e
24-
case _ => js.JavaScriptException(e)
25-
}
21+
@deprecated("Unused by the codegen; use js.special.wrapAsThrowable instead", "1.11.0")
22+
@inline def wrapJavaScriptException(e: Any): Throwable =
23+
js.special.wrapAsThrowable(e)
2624

27-
@deprecated("Unused by the codegen", "1.11.0")
28-
def unwrapJavaScriptException(th: Throwable): Any = th match {
29-
case js.JavaScriptException(e) => e
30-
case _ => th
31-
}
25+
@deprecated("Unused by the codegen; use js.special.unwrapFromThrowable instead", "1.11.0")
26+
@inline def unwrapJavaScriptException(th: Throwable): Any =
27+
js.special.unwrapFromThrowable(th)
3228

3329
@inline def toScalaVarArgs[A](array: js.Array[A]): Seq[A] =
3430
toScalaVarArgsImpl(array)

test-suite/js/src/test/scala/org/scalajs/testsuite/compiler/OptimizerTest.scala

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,29 @@ class OptimizerTest {
157157
// scalastyle:on return
158158
}
159159

160+
@Test def preserveSideEffectsInWrapAsThrowable(): Unit = {
161+
var i: Int = 1
162+
val x =
163+
if (i > 0) js.special.wrapAsThrowable({ i += 1; i })
164+
else 42
165+
166+
x match {
167+
case js.JavaScriptException(y) =>
168+
assertEquals(2, y)
169+
}
170+
assertEquals(2, i)
171+
}
172+
160173
@Test def preserveSideEffectsInUnwrapFromThrowable(): Unit = {
161-
/* This is also indirectly serves as a test for WrapAsThrowable. It's not
162-
* possible to write user-level Scala code that produces a WrapAsThrowable
163-
* with anything but a VarRef. But since WrapAsThrowable is handled exactly
164-
* like UnwrapFromThrowable in the linker, this test somewhat covers both.
165-
*/
174+
var i: Int = 1
175+
val x =
176+
if (i > 0) js.special.unwrapFromThrowable({ i += 1; new js.JavaScriptException(i) })
177+
else 42
178+
assertEquals(2, x)
179+
assertEquals(2, i)
180+
}
166181

182+
@Test def preserveSideEffectsInUnwrapFromThrowableInThrow(): Unit = {
167183
var i: Int = 1
168184
try {
169185
if (i > 0)

test-suite/js/src/test/scala/org/scalajs/testsuite/jsinterop/SpecialTest.scala

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,130 @@ class SpecialTest {
105105
js.special.delete(a[js.Object]("foo"), kh.key)
106106
}
107107

108+
// js.special.tryCatch
109+
110+
@Test def jsThrow(): Unit = {
111+
val e = assertThrows(classOf[js.JavaScriptException], js.special.`throw`("foo"))
112+
assertEquals("foo", e.exception)
113+
114+
assertThrows(classOf[IllegalArgumentException], js.special.`throw`(new IllegalArgumentException))
115+
}
116+
117+
@Test def jsTryCatch(): Unit = {
118+
@noinline def interrupt(): Unit = throw new IllegalStateException
119+
120+
// No exception
121+
locally {
122+
var order = "0"
123+
js.special.tryCatch({
124+
order += "1"
125+
126+
{ () => order += "3" }
127+
})({
128+
order += "2"
129+
130+
{ (e: Any) => fail("no exception should be thrown and caught") }
131+
})
132+
assertEquals("0123", order)
133+
}
134+
135+
// Exception thrown during execution of the body
136+
locally {
137+
var order = "0"
138+
js.special.tryCatch({
139+
order += "1"
140+
141+
{ () =>
142+
order += "3"
143+
interrupt()
144+
}
145+
})({
146+
order += "2"
147+
148+
{ (e: Any) =>
149+
order += "4"
150+
assertTrue(e.isInstanceOf[IllegalStateException])
151+
}
152+
})
153+
assertEquals("01234", order)
154+
}
155+
156+
// Exception thrown when computing the body
157+
locally {
158+
var order = "0"
159+
assertThrows(classOf[IllegalStateException], {
160+
js.special.tryCatch({
161+
order += "1"
162+
interrupt()
163+
164+
{ () => fail("unreachable 1") }
165+
})({
166+
fail("unreachable 2")
167+
168+
{ (e: Any) => fail("unreachable 3") }
169+
})
170+
})
171+
assertEquals("01", order)
172+
}
173+
174+
// Exception thrown when computing the handler
175+
locally {
176+
var order = "0"
177+
assertThrows(classOf[IllegalStateException], {
178+
js.special.tryCatch({
179+
order += "1"
180+
181+
{ () => fail("unreachable 1") }
182+
})({
183+
order += "2"
184+
interrupt()
185+
186+
{ (e: Any) => fail("unreachable 2") }
187+
})
188+
})
189+
assertEquals("012", order)
190+
}
191+
}
192+
193+
// js.special.wrapAsThrowable
194+
195+
@Test def wrapAsThrowable(): Unit = {
196+
// Wraps a js.Object
197+
val obj = new js.Object
198+
val e1 = js.special.wrapAsThrowable(obj)
199+
e1 match {
200+
case js.JavaScriptException(o) => assertSame(obj, o)
201+
}
202+
203+
// Wraps null
204+
val e2 = js.special.wrapAsThrowable(null)
205+
e2 match {
206+
case js.JavaScriptException(v) => assertNull(v)
207+
}
208+
209+
// Does not wrap a Throwable
210+
val th = new IllegalArgumentException
211+
assertSame(th, js.special.wrapAsThrowable(th))
212+
213+
// Does not double-wrap
214+
assertSame(e1, js.special.wrapAsThrowable(e1))
215+
}
216+
217+
// js.special.unwrapFromThrowable
218+
219+
@Test def unwrapFromThrowable(): Unit = {
220+
// Unwraps a JavaScriptException
221+
val obj = new js.Object
222+
assertSame(obj, js.special.unwrapFromThrowable(js.JavaScriptException(obj)))
223+
224+
// Does not unwrap a Throwable
225+
val th = new IllegalArgumentException
226+
assertSame(th, js.special.unwrapFromThrowable(th))
227+
228+
// Does not unwrap null
229+
assertNull(null, js.special.unwrapFromThrowable(null))
230+
}
231+
108232
// js.special.fileLevelThis
109233

110234
@Test def fileLevelThisCanBeUsedToDetectTheGlobalObject(): Unit = {

0 commit comments

Comments
 (0)