diff --git a/build.sbt b/build.sbt index e98d277e..fec682de 100644 --- a/build.sbt +++ b/build.sbt @@ -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]]> diff --git a/wasm/src/main/scala/converters/WasmTextWriter.scala b/wasm/src/main/scala/converters/WasmTextWriter.scala index 6b57a69d..6ac6a0ba 100644 --- a/wasm/src/main/scala/converters/WasmTextWriter.scala +++ b/wasm/src/main/scala/converters/WasmTextWriter.scala @@ -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) @@ -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 _ => () } } diff --git a/wasm/src/main/scala/ir2wasm/HelperFunctions.scala b/wasm/src/main/scala/ir2wasm/HelperFunctions.scala index 87b488ba..b59456fb 100644 --- a/wasm/src/main/scala/ir2wasm/HelperFunctions.scala +++ b/wasm/src/main/scala/ir2wasm/HelperFunctions.scala @@ -690,6 +690,7 @@ object HelperFunctions { ) instrs += CALL(WasmFunctionName.jsArrayPush) instrs += CALL(WasmFunctionName.jsNew) + instrs += EXTERN_CONVERT_ANY instrs += THROW(ctx.exceptionTagName) } ) { () => diff --git a/wasm/src/main/scala/ir2wasm/LoaderContent.scala b/wasm/src/main/scala/ir2wasm/LoaderContent.scala index e60e79b5..28c21dfa 100644 --- a/wasm/src/main/scala/ir2wasm/LoaderContent.scala +++ b/wasm/src/main/scala/ir2wasm/LoaderContent.scala @@ -65,6 +65,9 @@ const linkingInfo = Object.freeze({ }); const scalaJSHelpers = { + // JSTag + JSTag: WebAssembly.JSTag, + // BinaryOp.=== is: Object.is, diff --git a/wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala b/wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala index 5d6e5b24..d84d1586 100644 --- a/wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala +++ b/wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala @@ -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 @@ -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 @@ -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 diff --git a/wasm/src/main/scala/wasm4s/Instructions.scala b/wasm/src/main/scala/wasm4s/Instructions.scala index 465876d6..6c3c175d 100644 --- a/wasm/src/main/scala/wasm4s/Instructions.scala +++ b/wasm/src/main/scala/wasm4s/Instructions.scala @@ -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) @@ -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) diff --git a/wasm/src/main/scala/wasm4s/Types.scala b/wasm/src/main/scala/wasm4s/Types.scala index dfd500d8..403c1db6 100644 --- a/wasm/src/main/scala/wasm4s/Types.scala +++ b/wasm/src/main/scala/wasm4s/Types.scala @@ -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) diff --git a/wasm/src/main/scala/wasm4s/WasmContext.scala b/wasm/src/main/scala/wasm4s/WasmContext.scala index 76a1fdf0..07f3c729 100644 --- a/wasm/src/main/scala/wasm4s/WasmContext.scala +++ b/wasm/src/main/scala/wasm4s/WasmContext.scala @@ -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( diff --git a/wasm/src/main/scala/wasm4s/WasmFunctionContext.scala b/wasm/src/main/scala/wasm4s/WasmFunctionContext.scala index e640732e..116cc523 100644 --- a/wasm/src/main/scala/wasm4s/WasmFunctionContext.scala +++ b/wasm/src/main/scala/wasm4s/WasmFunctionContext.scala @@ -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.