Skip to content
This repository was archived by the owner on Jul 12, 2024. It is now read-only.

Commit c2b2ec7

Browse files
authored
Merge pull request #103 from sjrd/catch-js-exceptions
Fix #75: Use WebAssembly.JSTag as our exception tag.
2 parents 7adcb8d + e2a87b4 commit c2b2ec7

9 files changed

+83
-31
lines changed

Diff for: build.sbt

+6-7
Original file line numberDiff line numberDiff line change
@@ -228,15 +228,14 @@ lazy val IgnoredTestNames: Set[String] = {
228228
Set(
229229
// reflective call: should be throw an exception when reflective proxy not found
230230
"org.scalajs.testsuite.compiler.WasPublicBeforeTyperTestScala2",
231-
// Various run-time errors and JS exceptions
232-
"org.scalajs.testsuite.compiler.InteroperabilityTest",
231+
// javaLangClassGetNameRenamedThroughSemantics failed: org.junit.ComparisonFailure:
232+
// expected:<[renamed.test.]Class> but was:<[org.scalajs.testsuite.compiler.ReflectionTest$RenamedTest]Class>
233233
"org.scalajs.testsuite.compiler.ReflectionTest",
234-
"org.scalajs.testsuite.compiler.RegressionJSTest",
235-
"org.scalajs.testsuite.jsinterop.FunctionTest",
236-
"org.scalajs.testsuite.jsinterop.MiscInteropTest",
237-
"org.scalajs.testsuite.jsinterop.NonNativeJSTypeTest",
238-
"org.scalajs.testsuite.jsinterop.SpecialTest",
234+
// wellKnownSymbolIterator/testToString failed: scala.scalajs.js.JavaScriptException: TypeError: Cannot convert a Symbol value to a string
239235
"org.scalajs.testsuite.jsinterop.SymbolTest",
236+
// Cannot call wasmObject.toString() from JavaScript:
237+
// boxValueClassesGivenToJSInteropMethod failed: scala.scalajs.js.JavaScriptException: TypeError: vc.toString is not a function
238+
"org.scalajs.testsuite.compiler.InteroperabilityTest",
240239
// TypeError: WebAssembly objects are opaque
241240
"org.scalajs.testsuite.javalib.lang.SystemJSTest",
242241
// throwablesAreTrueErrors failed: org.junit.ComparisonFailure: expected:<[object [Error]]> but was:<[object [Object]]>

Diff for: wasm/src/main/scala/converters/WasmTextWriter.scala

+4-4
Original file line numberDiff line numberDiff line change
@@ -318,8 +318,8 @@ class WasmTextWriter {
318318

319319
private def writeInstr(instr: WasmInstr)(implicit b: WatBuilder): Unit = {
320320
instr match {
321-
case END | ELSE => b.deindent()
322-
case _ => ()
321+
case END | ELSE | _: CATCH | CATCH_ALL => b.deindent()
322+
case _ => ()
323323
}
324324
b.newLine()
325325
b.appendElement(instr.mnemonic)
@@ -333,8 +333,8 @@ class WasmTextWriter {
333333
writeInstrImmediates(instr)
334334

335335
instr match {
336-
case _: StructuredLabeledInstr | ELSE => b.indent()
337-
case _ => ()
336+
case _: StructuredLabeledInstr | ELSE | _: CATCH | CATCH_ALL => b.indent()
337+
case _ => ()
338338
}
339339
}
340340

Diff for: wasm/src/main/scala/ir2wasm/HelperFunctions.scala

+1
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,7 @@ object HelperFunctions {
690690
)
691691
instrs += CALL(WasmFunctionName.jsArrayPush)
692692
instrs += CALL(WasmFunctionName.jsNew)
693+
instrs += EXTERN_CONVERT_ANY
693694
instrs += THROW(ctx.exceptionTagName)
694695
}
695696
) { () =>

Diff for: wasm/src/main/scala/ir2wasm/LoaderContent.scala

+3
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ const linkingInfo = Object.freeze({
6565
});
6666

6767
const scalaJSHelpers = {
68+
// JSTag
69+
JSTag: WebAssembly.JSTag,
70+
6871
// BinaryOp.===
6972
is: Object.is,
7073

Diff for: wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala

+47-16
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,23 @@ import _root_.wasm4s.Defaults
2121
import EmbeddedConstants._
2222

2323
object WasmExpressionBuilder {
24+
25+
/** Whether to use the legacy `try` instruction to implement `TryCatch`.
26+
*
27+
* Support for catching JS exceptions was only added to `try_table` in V8 12.5 from April 2024.
28+
* While waiting for Node.js to catch up with V8, we use `try` to implement our `TryCatch`.
29+
*
30+
* We use this "fixed configuration option" to keep the code that implements `TryCatch` using
31+
* `try_table` in the codebase, as code that is actually compiled, so that refactorings apply to
32+
* it as well. It also makes it easier to manually experiment with the new `try_table` encoding,
33+
* which will become available in Chrome v125.
34+
*
35+
* Note that we use `try_table` regardless to implement `TryFinally`. Its `catch_all_ref` handler
36+
* is perfectly happy to catch and rethrow JavaScript exception in Node.js 22. Duplicating that
37+
* implementation for `try` would be a nightmare, given how complex it is already.
38+
*/
39+
private final val UseLegacyExceptionsForTryCatch = true
40+
2441
def generateIRBody(tree: IRTrees.Tree, resultType: IRTypes.Type)(implicit
2542
ctx: TypeDefinableWasmContext,
2643
fctx: WasmFunctionContext
@@ -1690,26 +1707,39 @@ private class WasmExpressionBuilder private (
16901707
private def genTryCatch(t: IRTrees.TryCatch): IRTypes.Type = {
16911708
val resultType = TypeTransformer.transformResultType(t.tpe)(ctx)
16921709

1693-
fctx.block(resultType) { doneLabel =>
1694-
fctx.block(Types.WasmRefType.anyref) { catchLabel =>
1695-
/* We used to have `resultType` as result of the try_table, wich the
1696-
* `BR(doneLabel)` outside of the try_table. Unfortunately it seems
1697-
* V8 cannot handle try_table with a result type that is `(ref ...)`.
1698-
* The current encoding with `anyref` as result type (to match the
1699-
* enclosing block) and the `br` *inside* the `try_table` works.
1700-
*/
1701-
fctx.tryTable(Types.WasmRefType.anyref)(
1702-
List(CatchClause.Catch(ctx.exceptionTagName, catchLabel))
1703-
) {
1704-
genTree(t.block, t.tpe)
1705-
instrs += BR(doneLabel)
1706-
}
1707-
} // end block $catch
1710+
if (UseLegacyExceptionsForTryCatch) {
1711+
instrs += TRY(fctx.sigToBlockType(WasmFunctionSignature(Nil, resultType)))
1712+
genTree(t.block, t.tpe)
1713+
instrs += CATCH(ctx.exceptionTagName)
17081714
fctx.withNewLocal(t.errVar.name, Types.WasmRefType.anyref) { exceptionLocal =>
1715+
instrs += ANY_CONVERT_EXTERN
17091716
instrs += LOCAL_SET(exceptionLocal)
17101717
genTree(t.handler, t.tpe)
17111718
}
1712-
} // end block $done
1719+
instrs += END
1720+
} else {
1721+
fctx.block(resultType) { doneLabel =>
1722+
fctx.block(Types.WasmRefType.externref) { catchLabel =>
1723+
/* We used to have `resultType` as result of the try_table, with the
1724+
* `BR(doneLabel)` outside of the try_table. Unfortunately it seems
1725+
* V8 cannot handle try_table with a result type that is `(ref ...)`.
1726+
* The current encoding with `externref` as result type (to match the
1727+
* enclosing block) and the `br` *inside* the `try_table` works.
1728+
*/
1729+
fctx.tryTable(Types.WasmRefType.externref)(
1730+
List(CatchClause.Catch(ctx.exceptionTagName, catchLabel))
1731+
) {
1732+
genTree(t.block, t.tpe)
1733+
instrs += BR(doneLabel)
1734+
}
1735+
} // end block $catch
1736+
fctx.withNewLocal(t.errVar.name, Types.WasmRefType.anyref) { exceptionLocal =>
1737+
instrs += ANY_CONVERT_EXTERN
1738+
instrs += LOCAL_SET(exceptionLocal)
1739+
genTree(t.handler, t.tpe)
1740+
}
1741+
} // end block $done
1742+
}
17131743

17141744
if (t.tpe == IRTypes.NothingType)
17151745
instrs += UNREACHABLE
@@ -1762,6 +1792,7 @@ private class WasmExpressionBuilder private (
17621792

17631793
private def genThrow(tree: IRTrees.Throw): IRTypes.Type = {
17641794
genTree(tree.expr, IRTypes.AnyType)
1795+
instrs += EXTERN_CONVERT_ANY
17651796
instrs += THROW(ctx.exceptionTagName)
17661797

17671798
IRTypes.NothingType

Diff for: wasm/src/main/scala/wasm4s/Instructions.scala

+13
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,16 @@ object WasmInstr {
259259
extends WasmInstr("try_table", 0x1F)
260260
with StructuredLabeledInstr
261261

262+
// Legacy exception system
263+
case class TRY(i: BlockType, label: Option[WasmLabelName] = None)
264+
extends WasmBlockTypeLabeledInstr("try", 0x06, i)
265+
case class CATCH(i: WasmTagName) extends WasmTagInstr("catch", 0x07, i)
266+
case object CATCH_ALL extends WasmSimpleInstr("catch_all", 0x19)
267+
// case class DELEGATE(i: WasmLabelName) extends WasmLabelInstr("delegate", 0x18, i)
268+
case class RETHROW(i: WasmLabelName)
269+
extends WasmLabelInstr("rethrow", 0x09, i)
270+
with StackPolymorphicInstr
271+
262272
// Parametric instructions
263273
// https://webassembly.github.io/spec/core/syntax/instructions.html#parametric-instructions
264274
case object DROP extends WasmSimpleInstr("drop", 0x1A)
@@ -295,6 +305,9 @@ object WasmInstr {
295305
*/
296306
case class REF_FUNC(i: WasmFunctionName) extends WasmFuncInstr("ref.func", 0xD2, i)
297307

308+
case object ANY_CONVERT_EXTERN extends WasmSimpleInstr("any.convert_extern", 0xFB1A)
309+
case object EXTERN_CONVERT_ANY extends WasmSimpleInstr("extern.convert_any", 0xFB1B)
310+
298311
case object REF_I31 extends WasmSimpleInstr("ref.i31", 0xFB1C)
299312
case object I31_GET_S extends WasmSimpleInstr("i31.get_s", 0xFB1D)
300313
case object I31_GET_U extends WasmSimpleInstr("i31.get_u", 0xFB1E)

Diff for: wasm/src/main/scala/wasm4s/Types.scala

+3
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ object Types {
5454
/** `(ref extern)`. */
5555
val extern: WasmRefType = apply(WasmHeapType.Extern)
5656

57+
/** `(ref null extern)`, i.e., `externref`. */
58+
val externref: WasmRefType = nullable(WasmHeapType.Extern)
59+
5760
/** `(ref null exn)`, i.e., `exnref`. */
5861
val exnref: WasmRefType = nullable(WasmHeapType.Exn)
5962

Diff for: wasm/src/main/scala/wasm4s/WasmContext.scala

+5-3
Original file line numberDiff line numberDiff line change
@@ -335,9 +335,11 @@ class WasmContext(val module: WasmModule) extends TypeDefinableWasmContext {
335335
val exceptionTagName: WasmTagName = WasmTagName("exception")
336336

337337
locally {
338-
val exceptionSig = WasmFunctionSignature(List(anyref), Nil)
339-
val exceptionFunType = addFunctionType(exceptionSig)
340-
module.addTag(WasmTag(exceptionTagName, exceptionFunType))
338+
val exceptionSig = WasmFunctionSignature(List(WasmRefType.externref), Nil)
339+
val typ = WasmFunctionType(addFunctionType(exceptionSig), exceptionSig)
340+
module.addImport(
341+
WasmImport("__scalaJSHelpers", "JSTag", WasmImportDesc.Tag(exceptionTagName, typ))
342+
)
341343
}
342344

343345
private def addHelperImport(

Diff for: wasm/src/main/scala/wasm4s/WasmFunctionContext.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ class WasmFunctionContext private (
388388
while (nestingLevel >= 0 && iter.hasNext) {
389389
val deadCodeInstr = iter.next()
390390
deadCodeInstr match {
391-
case END | ELSE if nestingLevel == 0 =>
391+
case END | ELSE | _: CATCH | CATCH_ALL if nestingLevel == 0 =>
392392
/* We have reached the end of the original block of dead code.
393393
* Actually emit this END or ELSE and then drop `nestingLevel`
394394
* below 0 to end the dead code processing loop.

0 commit comments

Comments
 (0)