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

Fix #75: Use WebAssembly.JSTag as our exception tag. #103

Merged
merged 2 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -228,15 +228,14 @@ lazy val IgnoredTestNames: Set[String] = {
Set(
// reflective call: should be throw an exception when reflective proxy not found
"org.scalajs.testsuite.compiler.WasPublicBeforeTyperTestScala2",
// Various run-time errors and JS exceptions
"org.scalajs.testsuite.compiler.InteroperabilityTest",
// javaLangClassGetNameRenamedThroughSemantics failed: org.junit.ComparisonFailure:
// expected:<[renamed.test.]Class> but was:<[org.scalajs.testsuite.compiler.ReflectionTest$RenamedTest]Class>
"org.scalajs.testsuite.compiler.ReflectionTest",
"org.scalajs.testsuite.compiler.RegressionJSTest",
"org.scalajs.testsuite.jsinterop.FunctionTest",
"org.scalajs.testsuite.jsinterop.MiscInteropTest",
"org.scalajs.testsuite.jsinterop.NonNativeJSTypeTest",
"org.scalajs.testsuite.jsinterop.SpecialTest",
// wellKnownSymbolIterator/testToString failed: scala.scalajs.js.JavaScriptException: TypeError: Cannot convert a Symbol value to a string
"org.scalajs.testsuite.jsinterop.SymbolTest",
// Cannot call wasmObject.toString() from JavaScript:
// boxValueClassesGivenToJSInteropMethod failed: scala.scalajs.js.JavaScriptException: TypeError: vc.toString is not a function
"org.scalajs.testsuite.compiler.InteroperabilityTest",
// TypeError: WebAssembly objects are opaque
"org.scalajs.testsuite.javalib.lang.SystemJSTest",
// throwablesAreTrueErrors failed: org.junit.ComparisonFailure: expected:<[object [Error]]> but was:<[object [Object]]>
Expand Down
8 changes: 4 additions & 4 deletions wasm/src/main/scala/converters/WasmTextWriter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,8 @@ class WasmTextWriter {

private def writeInstr(instr: WasmInstr)(implicit b: WatBuilder): Unit = {
instr match {
case END | ELSE => b.deindent()
case _ => ()
case END | ELSE | _: CATCH | CATCH_ALL => b.deindent()
case _ => ()
}
b.newLine()
b.appendElement(instr.mnemonic)
Expand All @@ -333,8 +333,8 @@ class WasmTextWriter {
writeInstrImmediates(instr)

instr match {
case _: StructuredLabeledInstr | ELSE => b.indent()
case _ => ()
case _: StructuredLabeledInstr | ELSE | _: CATCH | CATCH_ALL => b.indent()
case _ => ()
}
}

Expand Down
1 change: 1 addition & 0 deletions wasm/src/main/scala/ir2wasm/HelperFunctions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -690,6 +690,7 @@ object HelperFunctions {
)
instrs += CALL(WasmFunctionName.jsArrayPush)
instrs += CALL(WasmFunctionName.jsNew)
instrs += EXTERN_CONVERT_ANY
instrs += THROW(ctx.exceptionTagName)
}
) { () =>
Expand Down
3 changes: 3 additions & 0 deletions wasm/src/main/scala/ir2wasm/LoaderContent.scala
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ const linkingInfo = Object.freeze({
});

const scalaJSHelpers = {
// JSTag
JSTag: WebAssembly.JSTag,

// BinaryOp.===
is: Object.is,

Expand Down
63 changes: 47 additions & 16 deletions wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,23 @@ import _root_.wasm4s.Defaults
import EmbeddedConstants._

object WasmExpressionBuilder {

/** Whether to use the legacy `try` instruction to implement `TryCatch`.
*
* Support for catching JS exceptions was only added to `try_table` in V8 12.5 from April 2024.
* While waiting for Node.js to catch up with V8, we use `try` to implement our `TryCatch`.
*
* We use this "fixed configuration option" to keep the code that implements `TryCatch` using
* `try_table` in the codebase, as code that is actually compiled, so that refactorings apply to
* it as well. It also makes it easier to manually experiment with the new `try_table` encoding,
* which will become available in Chrome v125.
*
* Note that we use `try_table` regardless to implement `TryFinally`. Its `catch_all_ref` handler
* is perfectly happy to catch and rethrow JavaScript exception in Node.js 22. Duplicating that
* implementation for `try` would be a nightmare, given how complex it is already.
*/
private final val UseLegacyExceptionsForTryCatch = true

def generateIRBody(tree: IRTrees.Tree, resultType: IRTypes.Type)(implicit
ctx: TypeDefinableWasmContext,
fctx: WasmFunctionContext
Expand Down Expand Up @@ -1690,26 +1707,39 @@ private class WasmExpressionBuilder private (
private def genTryCatch(t: IRTrees.TryCatch): IRTypes.Type = {
val resultType = TypeTransformer.transformResultType(t.tpe)(ctx)

fctx.block(resultType) { doneLabel =>
fctx.block(Types.WasmRefType.anyref) { catchLabel =>
/* We used to have `resultType` as result of the try_table, wich the
* `BR(doneLabel)` outside of the try_table. Unfortunately it seems
* V8 cannot handle try_table with a result type that is `(ref ...)`.
* The current encoding with `anyref` as result type (to match the
* enclosing block) and the `br` *inside* the `try_table` works.
*/
fctx.tryTable(Types.WasmRefType.anyref)(
List(CatchClause.Catch(ctx.exceptionTagName, catchLabel))
) {
genTree(t.block, t.tpe)
instrs += BR(doneLabel)
}
} // end block $catch
if (UseLegacyExceptionsForTryCatch) {
instrs += TRY(fctx.sigToBlockType(WasmFunctionSignature(Nil, resultType)))
genTree(t.block, t.tpe)
instrs += CATCH(ctx.exceptionTagName)
fctx.withNewLocal(t.errVar.name, Types.WasmRefType.anyref) { exceptionLocal =>
instrs += ANY_CONVERT_EXTERN
instrs += LOCAL_SET(exceptionLocal)
genTree(t.handler, t.tpe)
}
} // end block $done
instrs += END
} else {
fctx.block(resultType) { doneLabel =>
fctx.block(Types.WasmRefType.externref) { catchLabel =>
/* We used to have `resultType` as result of the try_table, with the
* `BR(doneLabel)` outside of the try_table. Unfortunately it seems
* V8 cannot handle try_table with a result type that is `(ref ...)`.
* The current encoding with `externref` as result type (to match the
* enclosing block) and the `br` *inside* the `try_table` works.
*/
fctx.tryTable(Types.WasmRefType.externref)(
List(CatchClause.Catch(ctx.exceptionTagName, catchLabel))
) {
genTree(t.block, t.tpe)
instrs += BR(doneLabel)
}
} // end block $catch
fctx.withNewLocal(t.errVar.name, Types.WasmRefType.anyref) { exceptionLocal =>
instrs += ANY_CONVERT_EXTERN
instrs += LOCAL_SET(exceptionLocal)
genTree(t.handler, t.tpe)
}
} // end block $done
}

if (t.tpe == IRTypes.NothingType)
instrs += UNREACHABLE
Expand Down Expand Up @@ -1762,6 +1792,7 @@ private class WasmExpressionBuilder private (

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

IRTypes.NothingType
Expand Down
13 changes: 13 additions & 0 deletions wasm/src/main/scala/wasm4s/Instructions.scala
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,16 @@ object WasmInstr {
extends WasmInstr("try_table", 0x1F)
with StructuredLabeledInstr

// Legacy exception system
case class TRY(i: BlockType, label: Option[WasmLabelName] = None)
extends WasmBlockTypeLabeledInstr("try", 0x06, i)
case class CATCH(i: WasmTagName) extends WasmTagInstr("catch", 0x07, i)
case object CATCH_ALL extends WasmSimpleInstr("catch_all", 0x19)
// case class DELEGATE(i: WasmLabelName) extends WasmLabelInstr("delegate", 0x18, i)
case class RETHROW(i: WasmLabelName)
extends WasmLabelInstr("rethrow", 0x09, i)
with StackPolymorphicInstr

// Parametric instructions
// https://webassembly.github.io/spec/core/syntax/instructions.html#parametric-instructions
case object DROP extends WasmSimpleInstr("drop", 0x1A)
Expand Down Expand Up @@ -295,6 +305,9 @@ object WasmInstr {
*/
case class REF_FUNC(i: WasmFunctionName) extends WasmFuncInstr("ref.func", 0xD2, i)

case object ANY_CONVERT_EXTERN extends WasmSimpleInstr("any.convert_extern", 0xFB1A)
case object EXTERN_CONVERT_ANY extends WasmSimpleInstr("extern.convert_any", 0xFB1B)

case object REF_I31 extends WasmSimpleInstr("ref.i31", 0xFB1C)
case object I31_GET_S extends WasmSimpleInstr("i31.get_s", 0xFB1D)
case object I31_GET_U extends WasmSimpleInstr("i31.get_u", 0xFB1E)
Expand Down
3 changes: 3 additions & 0 deletions wasm/src/main/scala/wasm4s/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ object Types {
/** `(ref extern)`. */
val extern: WasmRefType = apply(WasmHeapType.Extern)

/** `(ref null extern)`, i.e., `externref`. */
val externref: WasmRefType = nullable(WasmHeapType.Extern)

/** `(ref null exn)`, i.e., `exnref`. */
val exnref: WasmRefType = nullable(WasmHeapType.Exn)

Expand Down
8 changes: 5 additions & 3 deletions wasm/src/main/scala/wasm4s/WasmContext.scala
Original file line number Diff line number Diff line change
Expand Up @@ -335,9 +335,11 @@ class WasmContext(val module: WasmModule) extends TypeDefinableWasmContext {
val exceptionTagName: WasmTagName = WasmTagName("exception")

locally {
val exceptionSig = WasmFunctionSignature(List(anyref), Nil)
val exceptionFunType = addFunctionType(exceptionSig)
module.addTag(WasmTag(exceptionTagName, exceptionFunType))
val exceptionSig = WasmFunctionSignature(List(WasmRefType.externref), Nil)
val typ = WasmFunctionType(addFunctionType(exceptionSig), exceptionSig)
module.addImport(
WasmImport("__scalaJSHelpers", "JSTag", WasmImportDesc.Tag(exceptionTagName, typ))
)
}

private def addHelperImport(
Expand Down
2 changes: 1 addition & 1 deletion wasm/src/main/scala/wasm4s/WasmFunctionContext.scala
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ class WasmFunctionContext private (
while (nestingLevel >= 0 && iter.hasNext) {
val deadCodeInstr = iter.next()
deadCodeInstr match {
case END | ELSE if nestingLevel == 0 =>
case END | ELSE | _: CATCH | CATCH_ALL if nestingLevel == 0 =>
/* We have reached the end of the original block of dead code.
* Actually emit this END or ELSE and then drop `nestingLevel`
* below 0 to end the dead code processing loop.
Expand Down