From fdf8964773b9650099a8187615023aec41cdd27b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 21 Mar 2024 12:00:44 +0100 Subject: [PATCH 1/9] Add support for Tags in our Wasm structures and writers. We do not use the tags in any translation yet. --- .../scala/converters/WasmBinaryWriter.scala | 58 ++++++++++++++++++- .../scala/converters/WasmTextWriter.scala | 19 ++++++ wasm/src/main/scala/wasm4s/Instructions.scala | 2 +- wasm/src/main/scala/wasm4s/Names.scala | 7 +++ wasm/src/main/scala/wasm4s/Wasm.scala | 7 +++ 5 files changed, 89 insertions(+), 4 deletions(-) diff --git a/wasm/src/main/scala/converters/WasmBinaryWriter.scala b/wasm/src/main/scala/converters/WasmBinaryWriter.scala index 806fe9f1..ccc053ad 100644 --- a/wasm/src/main/scala/converters/WasmBinaryWriter.scala +++ b/wasm/src/main/scala/converters/WasmBinaryWriter.scala @@ -23,6 +23,15 @@ final class WasmBinaryWriter(module: WasmModule) { module.arrayTypes } + private val importTypeDefinitions: List[WasmFunctionType] = { + module.imports.map { imprt => + imprt.desc match { + case WasmImportDesc.Func(id, typ) => typ + case WasmImportDesc.Tag(id, typ) => typ + } + } + } + private val typeIdxValues: Map[WasmTypeName, Int] = allTypeDefinitions.map(_.name).zipWithIndex.toMap @@ -34,6 +43,14 @@ final class WasmBinaryWriter(module: WasmModule) { allNames.zipWithIndex.toMap } + private val tagIdxValues: Map[WasmTagName, Int] = { + val importedTagNames = module.imports.collect { + case WasmImport(_, _, WasmImportDesc.Tag(id, _)) => id + } + val allNames = importedTagNames ::: module.tags.map(_.name) + allNames.zipWithIndex.toMap + } + private val globalIdxValues: Map[WasmGlobalName, Int] = module.globals.map(_.name).zipWithIndex.toMap @@ -65,6 +82,7 @@ final class WasmBinaryWriter(module: WasmModule) { writeSection(fullOutput, SectionType)(writeTypeSection(_)) writeSection(fullOutput, SectionImport)(writeImportSection(_)) writeSection(fullOutput, SectionFunction)(writeFunctionSection(_)) + writeSection(fullOutput, SectionTag)(writeTagSection(_)) writeSection(fullOutput, SectionGlobal)(writeGlobalSection(_)) writeSection(fullOutput, SectionExport)(writeExportSection(_)) if (module.startFunction.isDefined) @@ -81,7 +99,10 @@ final class WasmBinaryWriter(module: WasmModule) { } private def writeTypeSection(buf: Buffer): Unit = { - buf.u32(1) // a single `rectype` + buf.u32(1 + importTypeDefinitions.size) // a single `rectype` + the import type definitions + + // the big rectype + buf.byte(0x4E) // `rectype` tag buf.u32(typeIdxValues.size) // number of `subtype`s in our single `rectype` @@ -106,6 +127,15 @@ final class WasmBinaryWriter(module: WasmModule) { writeResultType(buf, results) } } + + // the import type definitions, outside the rectype + + for (typeDef <- importTypeDefinitions) { + val WasmFunctionType(name, params, results) = typeDef + buf.byte(0x60) // func + writeResultType(buf, params) + writeResultType(buf, results) + } } private def writeImportSection(buf: Buffer): Unit = { @@ -113,10 +143,21 @@ final class WasmBinaryWriter(module: WasmModule) { buf.name(imprt.module) buf.name(imprt.name) + val indexBase = allTypeDefinitions.size + val importedFunTypeIdx = + importTypeDefinitions.map(_.name).zipWithIndex.map(kv => (kv._1, kv._2 + indexBase)).toMap + + def writeImportedTypeIdx(typeName: WasmTypeName.WasmFunctionTypeName): Unit = + buf.u32(importedFunTypeIdx(typeName)) + imprt.desc match { case WasmImportDesc.Func(id, typ) => buf.byte(0x00) // func - writeTypeIdx(buf, typ.name) + writeImportedTypeIdx(typ.name) + case WasmImportDesc.Tag(id, typ) => + buf.byte(0x04) // tag + buf.byte(0x00) // exception kind (that is the only valid kind for now) + writeImportedTypeIdx(typ.name) } } } @@ -127,6 +168,13 @@ final class WasmBinaryWriter(module: WasmModule) { } } + private def writeTagSection(buf: Buffer): Unit = { + buf.vec(module.tags) { tag => + buf.byte(0x00) // exception kind (that is the only valid kind for now) + writeTypeIdx(buf, tag.typ) + } + } + private def writeGlobalSection(buf: Buffer): Unit = { buf.vec(module.globals) { global => writeType(buf, global.typ) @@ -212,6 +260,9 @@ final class WasmBinaryWriter(module: WasmModule) { private def writeFuncIdx(buf: Buffer, funcName: WasmFunctionName): Unit = buf.u32(funcIdxValues(funcName)) + private def writeTagIdx(buf: Buffer, tagName: WasmTagName): Unit = + buf.u32(tagIdxValues(tagName)) + private def writeGlobalIdx(buf: Buffer, globalName: WasmGlobalName): Unit = buf.u32(globalIdxValues(globalName)) @@ -284,7 +335,7 @@ final class WasmBinaryWriter(module: WasmModule) { case LabelIdxVector(value) => buf.vec(value)(writeLabelIdx(buf, _)) case TypeIdx(value) => writeTypeIdx(buf, value) case TableIdx(value) => ??? - case TagIdx(value) => ??? + case TagIdx(value) => writeTagIdx(buf, value) case LocalIdx(value) => writeLocalIdx(buf, value) case GlobalIdx(value) => writeGlobalIdx(buf, value) case HeapType(value) => writeHeapType(buf, value) @@ -309,6 +360,7 @@ object WasmBinaryWriter { private final val SectionCode = 0x0A private final val SectionData = 0x0B private final val SectionDataCount = 0x0C + private final val SectionTag = 0x0D private final class Buffer { private val buf = new java.io.ByteArrayOutputStream() diff --git a/wasm/src/main/scala/converters/WasmTextWriter.scala b/wasm/src/main/scala/converters/WasmTextWriter.scala index c4dc33ae..2ff5e696 100644 --- a/wasm/src/main/scala/converters/WasmTextWriter.scala +++ b/wasm/src/main/scala/converters/WasmTextWriter.scala @@ -27,6 +27,7 @@ class WasmTextWriter { ) module.imports.foreach(writeImport) module.definedFunctions.foreach(writeFunction) + module.tags.foreach(writeTag) module.globals.foreach(writeGlobal) module.exports.foreach(writeExport) module.startFunction.foreach(writeStart) @@ -123,6 +124,13 @@ class WasmTextWriter { writeSig(typ.params, typ.results) } ) + case WasmImportDesc.Tag(id, typ) => + b.sameLineList( + "tag", { + b.appendElement(id.show) + writeSig(typ.params, typ.results) + } + ) } } ) @@ -173,6 +181,15 @@ class WasmTextWriter { ) } + private def writeTag(tag: WasmTag)(implicit b: WatBuilder): Unit = { + b.newLineList( + "tag", { + b.appendElement(tag.name.show) + b.sameLineListOne("type", tag.typ.show) + } + ) + } + private def writeGlobal(g: WasmGlobal)(implicit b: WatBuilder) = b.newLineList( "global", { @@ -259,6 +276,8 @@ class WasmTextWriter { case WasmImmediate.LabelIdx(i) => s"$$${i.toString}" // `loop 0` seems to be invalid case WasmImmediate.LabelIdxVector(indices) => indices.map(i => "$" + i.value).mkString(" ") + case WasmImmediate.TagIdx(name) => + name.show case i: WasmImmediate.CastFlags => throw new UnsupportedOperationException( s"CastFlags $i must be handled directly in the instruction $instr" diff --git a/wasm/src/main/scala/wasm4s/Instructions.scala b/wasm/src/main/scala/wasm4s/Instructions.scala index a12c22d4..2a131c56 100644 --- a/wasm/src/main/scala/wasm4s/Instructions.scala +++ b/wasm/src/main/scala/wasm4s/Instructions.scala @@ -339,7 +339,7 @@ object WasmImmediate { case class LabelIdxVector(val value: List[LabelIdx]) extends WasmImmediate case class TypeIdx(val value: WasmTypeName) extends WasmImmediate case class TableIdx(val value: Int) extends WasmImmediate - case class TagIdx(val value: Int) extends WasmImmediate + case class TagIdx(val value: WasmTagName) extends WasmImmediate case class LocalIdx(val value: WasmLocalName) extends WasmImmediate case class GlobalIdx(val value: WasmGlobalName) extends WasmImmediate case class HeapType(val value: WasmHeapType) extends WasmImmediate diff --git a/wasm/src/main/scala/wasm4s/Names.scala b/wasm/src/main/scala/wasm4s/Names.scala index 066b4eba..f708d3cf 100644 --- a/wasm/src/main/scala/wasm4s/Names.scala +++ b/wasm/src/main/scala/wasm4s/Names.scala @@ -17,6 +17,7 @@ object Names { case _: WasmGlobalName.WasmGlobalConstantStringName => "str_const" case _: WasmFunctionName => "fun" case _: WasmFieldName => "field" + case _: WasmTagName => "tag" case _: WasmExportName => "export" case _: WasmTypeName.WasmFunctionTypeName => "ty" case _: WasmTypeName.WasmStructTypeName => "struct" @@ -385,6 +386,12 @@ object Names { } + final case class WasmTagName private (override private[wasm4s] val name: String) + extends WasmName(name) + object WasmTagName { + def fromStr(str: String): WasmTagName = new WasmTagName(str) + } + final case class WasmExportName private (override private[wasm4s] val name: String) extends WasmName(name) object WasmExportName { diff --git a/wasm/src/main/scala/wasm4s/Wasm.scala b/wasm/src/main/scala/wasm4s/Wasm.scala index 9d92642d..2f4df42b 100644 --- a/wasm/src/main/scala/wasm4s/Wasm.scala +++ b/wasm/src/main/scala/wasm4s/Wasm.scala @@ -28,6 +28,7 @@ sealed abstract class WasmImportDesc object WasmImportDesc { final case class Func(id: WasmFunctionName, typ: WasmFunctionType) extends WasmImportDesc + final case class Tag(id: WasmTagName, typ: WasmFunctionType) extends WasmImportDesc } /** @see @@ -49,6 +50,9 @@ case class WasmLocal( val isParameter: Boolean // for text ) extends WasmNamedDefinitionField[WasmLocalName] +final case class WasmTag(val name: WasmTagName, val typ: WasmFunctionTypeName) + extends WasmNamedDefinitionField[WasmTagName] + case class WasmGlobal( val name: WasmGlobalName, val typ: WasmType, @@ -193,6 +197,7 @@ class WasmModule( private val _definedFunctions: mutable.ListBuffer[WasmFunction] = new mutable.ListBuffer(), // val tables: List[WasmTable] = Nil, // val memories: List[WasmMemory] = Nil, + private val _tags: mutable.ListBuffer[WasmTag] = new mutable.ListBuffer(), private val _globals: mutable.ListBuffer[WasmGlobal] = new mutable.ListBuffer(), private val _exports: mutable.ListBuffer[WasmExport[_]] = new mutable.ListBuffer(), private var _startFunction: Option[WasmFunctionName] = None, @@ -207,6 +212,7 @@ class WasmModule( def addArrayType(typ: WasmArrayType): Unit = _arrayTypes.addOne(typ) def addFunctionType(typ: WasmFunctionType): Unit = _functionTypes.addOne(typ) def addRecGroupType(typ: WasmStructType): Unit = _recGroupTypes.addOne(typ) + def addTag(tag: WasmTag): Unit = _tags.addOne(tag) def addGlobal(typ: WasmGlobal): Unit = _globals.addOne(typ) def addExport(exprt: WasmExport[_]) = _exports.addOne(exprt) def setStartFunction(startFunction: WasmFunctionName): Unit = _startFunction = Some(startFunction) @@ -217,6 +223,7 @@ class WasmModule( def arrayTypes = List(WasmArrayType.itables, WasmArrayType.u16Array) ++ _arrayTypes.toList def imports = _imports.toList def definedFunctions = _definedFunctions.toList + def tags: List[WasmTag] = _tags.toList def globals = _globals.toList def exports = _exports.toList def startFunction: Option[WasmFunctionName] = _startFunction From 57819dbfb74539a8f620bec2f386e2afba41fc5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 22 Mar 2024 00:23:29 +0100 Subject: [PATCH 2/9] Force usage of the leaner JavaScriptException for now. That one is not a case class, and therefore does not depend on plenty of things from the scalalib that we do not support yet. --- wasm/src/main/scala/ir2wasm/LibraryPatches.scala | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/wasm/src/main/scala/ir2wasm/LibraryPatches.scala b/wasm/src/main/scala/ir2wasm/LibraryPatches.scala index 73c0f35c..8ebf4e09 100644 --- a/wasm/src/main/scala/ir2wasm/LibraryPatches.scala +++ b/wasm/src/main/scala/ir2wasm/LibraryPatches.scala @@ -31,8 +31,13 @@ object LibraryPatches { } } + val leanerJSExceptionIRFile = + org.scalajs.linker.backend.emitter.PrivateLibHolder.files.find { irFile => + IRFileImpl.fromIRFile(irFile).path.contains("JavaScriptException") + }.get + derivedIRFiles.map { derived => - derived.flatten ++ Seq(StackTraceIRFile) ++ irFiles + derived.flatten ++ Seq(StackTraceIRFile, leanerJSExceptionIRFile) ++ irFiles } } From dcfb071f607618f0dce5d985e7c2042e18130e75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 26 Mar 2024 11:46:27 +0100 Subject: [PATCH 3/9] Implement WrapAsThrowable and UnwrapFromThrowable. --- cli/src/main/scala/TestSuites.scala | 3 +- .../src/main/scala/testsuite/Assert.scala | 5 +- .../core/WrapUnwrapThrowableTest.scala | 46 +++++++ .../src/main/scala/ir2wasm/SpecialNames.scala | 5 + .../scala/ir2wasm/WasmExpressionBuilder.scala | 121 ++++++++++++++---- wasm/src/main/scala/wasm4s/Types.scala | 4 + 6 files changed, 155 insertions(+), 29 deletions(-) create mode 100644 test-suite/src/main/scala/testsuite/core/WrapUnwrapThrowableTest.scala diff --git a/cli/src/main/scala/TestSuites.scala b/cli/src/main/scala/TestSuites.scala index 8b602903..7b2027d4 100644 --- a/cli/src/main/scala/TestSuites.scala +++ b/cli/src/main/scala/TestSuites.scala @@ -21,6 +21,7 @@ object TestSuites { TestSuite("testsuite.core.HijackedClassesUpcastTest"), TestSuite("testsuite.core.StaticMethodTest"), TestSuite("testsuite.core.ThrowablesTest"), - TestSuite("testsuite.core.ToStringTest") + TestSuite("testsuite.core.ToStringTest"), + TestSuite("testsuite.core.WrapUnwrapThrowableTest") ) } diff --git a/test-suite/src/main/scala/testsuite/Assert.scala b/test-suite/src/main/scala/testsuite/Assert.scala index 7add6af0..610994f4 100644 --- a/test-suite/src/main/scala/testsuite/Assert.scala +++ b/test-suite/src/main/scala/testsuite/Assert.scala @@ -13,8 +13,11 @@ package testsuite */ object Assert { def ok(cond: Boolean): Unit = - if (!cond) null.toString() // Apply to Null should compile to unreachable + if (!cond) fail() def assertSame(expected: Any, actual: Any): Unit = ok(expected.asInstanceOf[AnyRef] eq actual.asInstanceOf[AnyRef]) + + def fail(): Unit = + null.toString() // Apply to Null should compile to unreachable } diff --git a/test-suite/src/main/scala/testsuite/core/WrapUnwrapThrowableTest.scala b/test-suite/src/main/scala/testsuite/core/WrapUnwrapThrowableTest.scala new file mode 100644 index 00000000..10888d4d --- /dev/null +++ b/test-suite/src/main/scala/testsuite/core/WrapUnwrapThrowableTest.scala @@ -0,0 +1,46 @@ +package testsuite.core + +import scala.scalajs.js + +import testsuite.Assert.{assertSame, fail} + +object WrapUnwrapThrowableTest { + def main(): Unit = { + testWrapAsThrowable() + testUnwrapFromThrowable() + } + + def testWrapAsThrowable(): Unit = { + // Wraps a js.Object + val obj = new js.Object + val e1 = js.special.wrapAsThrowable(obj) + e1 match { + case e1: js.JavaScriptException => assertSame(obj, e1.exception) + case _ => fail() + } + + // Wraps null + val e2 = js.special.wrapAsThrowable(null) + e2 match { + case e2: js.JavaScriptException => assertSame(null, e2.exception) + case _ => fail() + } + + // Does not wrap a Throwable + val th = new IllegalArgumentException + assertSame(th, js.special.wrapAsThrowable(th)) + + // Does not double-wrap + assertSame(e1, js.special.wrapAsThrowable(e1)) + } + + def testUnwrapFromThrowable(): Unit = { + // Unwraps a JavaScriptException + val obj = new js.Object + assertSame(obj, js.special.unwrapFromThrowable(new js.JavaScriptException(obj))) + + // Does not unwrap a Throwable + val th = new IllegalArgumentException + assertSame(th, js.special.unwrapFromThrowable(th)) + } +} diff --git a/wasm/src/main/scala/ir2wasm/SpecialNames.scala b/wasm/src/main/scala/ir2wasm/SpecialNames.scala index a29e3ae1..d221979f 100644 --- a/wasm/src/main/scala/ir2wasm/SpecialNames.scala +++ b/wasm/src/main/scala/ir2wasm/SpecialNames.scala @@ -16,4 +16,9 @@ object SpecialNames { // The constructor of java.lang.Class val ClassCtor = MethodName.constructor(List(ClassRef(ObjectClass))) + + // js.JavaScriptException, for WrapAsThrowable and UnwrapFromThrowable + val JSExceptionClass = ClassName("scala.scalajs.js.JavaScriptException") + val JSExceptionCtor = MethodName.constructor(List(ClassRef(ObjectClass))) + val JSExceptionField = FieldName(JSExceptionClass, SimpleFieldName("exception")) } diff --git a/wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala b/wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala index e90b16eb..c7987c7f 100644 --- a/wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala +++ b/wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala @@ -100,31 +100,33 @@ private class WasmExpressionBuilder private ( def genTree(tree: IRTrees.Tree, expectedType: IRTypes.Type): Unit = { val generatedType: IRTypes.Type = tree match { - case t: IRTrees.Literal => genLiteral(t) - case t: IRTrees.UnaryOp => genUnaryOp(t) - case t: IRTrees.BinaryOp => genBinaryOp(t) - case t: IRTrees.VarRef => genVarRef(t) - case t: IRTrees.LoadModule => genLoadModule(t) - case t: IRTrees.StoreModule => genStoreModule(t) - case t: IRTrees.This => genThis(t) - case t: IRTrees.ApplyStatically => genApplyStatically(t) - case t: IRTrees.Apply => genApply(t) - case t: IRTrees.ApplyStatic => genApplyStatic(t) - case t: IRTrees.IsInstanceOf => genIsInstanceOf(t) - case t: IRTrees.AsInstanceOf => genAsInstanceOf(t) - case t: IRTrees.GetClass => genGetClass(t) - case t: IRTrees.Block => genBlock(t, expectedType) - case t: IRTrees.Labeled => genLabeled(t, expectedType) - case t: IRTrees.Return => genReturn(t) - case t: IRTrees.Select => genSelect(t) - case t: IRTrees.Assign => genAssign(t) - case t: IRTrees.VarDef => genVarDef(t) - case t: IRTrees.New => genNew(t) - case t: IRTrees.If => genIf(t, expectedType) - case t: IRTrees.While => genWhile(t) - case t: IRTrees.Debugger => IRTypes.NoType // ignore - case t: IRTrees.Skip => IRTypes.NoType - case t: IRTrees.IdentityHashCode => genIdentityHashCode(t) + case t: IRTrees.Literal => genLiteral(t) + case t: IRTrees.UnaryOp => genUnaryOp(t) + case t: IRTrees.BinaryOp => genBinaryOp(t) + case t: IRTrees.VarRef => genVarRef(t) + case t: IRTrees.LoadModule => genLoadModule(t) + case t: IRTrees.StoreModule => genStoreModule(t) + case t: IRTrees.This => genThis(t) + case t: IRTrees.ApplyStatically => genApplyStatically(t) + case t: IRTrees.Apply => genApply(t) + case t: IRTrees.ApplyStatic => genApplyStatic(t) + case t: IRTrees.IsInstanceOf => genIsInstanceOf(t) + case t: IRTrees.AsInstanceOf => genAsInstanceOf(t) + case t: IRTrees.GetClass => genGetClass(t) + case t: IRTrees.Block => genBlock(t, expectedType) + case t: IRTrees.Labeled => genLabeled(t, expectedType) + case t: IRTrees.Return => genReturn(t) + case t: IRTrees.Select => genSelect(t) + case t: IRTrees.Assign => genAssign(t) + case t: IRTrees.VarDef => genVarDef(t) + case t: IRTrees.New => genNew(t) + case t: IRTrees.If => genIf(t, expectedType) + case t: IRTrees.While => genWhile(t) + case t: IRTrees.Debugger => IRTypes.NoType // ignore + case t: IRTrees.Skip => IRTypes.NoType + case t: IRTrees.IdentityHashCode => genIdentityHashCode(t) + case t: IRTrees.WrapAsThrowable => genWrapAsThrowable(t) + case t: IRTrees.UnwrapFromThrowable => genUnwrapFromThrowable(t) // JavaScript expressions case t: IRTrees.JSNew => genJSNew(t) @@ -158,7 +160,6 @@ private class WasmExpressionBuilder private ( ??? // case select: IRTrees.JSPrivateSelect => ??? - // case v: IRTrees.UnwrapFromThrowable => ??? // case IRTrees.RecordValue(pos) => // case IRTrees.JSNewTarget(pos) => // case IRTrees.SelectStatic(tpe) => @@ -168,7 +169,6 @@ private class WasmExpressionBuilder private ( // case IRTrees.TryFinally(pos) => // case IRTrees.JSImportMeta(pos) => // case IRTrees.JSSuperSelect(pos) => - // case IRTrees.WrapAsThrowable(pos) => // case IRTrees.JSSuperConstructorCall(pos) => // case IRTrees.CreateJSClass(pos) => // case IRTrees.Transient(pos) => @@ -1434,6 +1434,73 @@ private class WasmExpressionBuilder private ( IRTypes.IntType } + private def genWrapAsThrowable(tree: IRTrees.WrapAsThrowable): IRTypes.Type = { + val throwableClassType = IRTypes.ClassType(IRNames.ThrowableClass) + val throwableTyp = TypeTransformer.transformType(throwableClassType)(ctx) + + val jsExceptionClassType = IRTypes.ClassType(SpecialNames.JSExceptionClass) + val jsExceptionTyp = TypeTransformer.transformType(jsExceptionClassType)(ctx) + + fctx.block(throwableTyp) { doneLabel => + genTree(tree.expr, IRTypes.AnyType) + + // if expr.isInstanceOf[Throwable], then br $done + instrs += BR_ON_CAST( + CastFlags(true, false), + doneLabel, + HeapType(Types.WasmHeapType.Simple.Any), + HeapType(Types.WasmHeapType.ThrowableType) + ) + + // otherwise, wrap in a new JavaScriptException + + val exprLocal = fctx.addSyntheticLocal(Types.WasmAnyRef) + val instanceLocal = fctx.addSyntheticLocal(jsExceptionTyp) + + instrs += LOCAL_SET(exprLocal) + instrs += CALL(FuncIdx(WasmFunctionName.newDefault(SpecialNames.JSExceptionClass))) + instrs += LOCAL_TEE(instanceLocal) + instrs += LOCAL_GET(exprLocal) + instrs += CALL( + FuncIdx( + WasmFunctionName( + IRTrees.MemberNamespace.Constructor, + SpecialNames.JSExceptionClass, + SpecialNames.JSExceptionCtor + ) + ) + ) + instrs += LOCAL_GET(instanceLocal) + } + + throwableClassType + } + + private def genUnwrapFromThrowable(tree: IRTrees.UnwrapFromThrowable): IRTypes.Type = { + fctx.block(Types.WasmAnyRef) { doneLabel => + genTree(tree.expr, IRTypes.ClassType(IRNames.ThrowableClass)) + + instrs += REF_AS_NOT_NULL + + // if !expr.isInstanceOf[js.JavaScriptException], then br $done + instrs += BR_ON_CAST_FAIL( + CastFlags(false, false), + doneLabel, + HeapType(Types.WasmHeapType.ThrowableType), + HeapType(Types.WasmHeapType.JSExceptionType) + ) + + // otherwise, unwrap the JavaScriptException by reading its field + + val idx = + ctx.getClassInfo(SpecialNames.JSExceptionClass).getFieldIdx(SpecialNames.JSExceptionField) + + instrs += STRUCT_GET(TypeIdx(WasmStructTypeName(SpecialNames.JSExceptionClass)), idx) + } + + IRTypes.AnyType + } + private def genJSNew(tree: IRTrees.JSNew): IRTypes.Type = { genTree(tree.ctor, IRTypes.AnyType) genJSArgsArray(tree.args) diff --git a/wasm/src/main/scala/wasm4s/Types.scala b/wasm/src/main/scala/wasm4s/Types.scala index 4fa88be9..73083e54 100644 --- a/wasm/src/main/scala/wasm4s/Types.scala +++ b/wasm/src/main/scala/wasm4s/Types.scala @@ -4,6 +4,8 @@ import org.scalajs.ir.{Names => IRNames} import Names._ import Names.WasmTypeName._ +import wasm.ir2wasm.SpecialNames + object Types { abstract sealed class WasmStorageType( private val name: String, @@ -78,5 +80,7 @@ object Types { val ObjectType = Type(WasmStructTypeName(IRNames.ObjectClass)) val ClassType = Type(WasmStructTypeName(IRNames.ClassClass)) + val ThrowableType = Type(WasmStructTypeName(IRNames.ThrowableClass)) + val JSExceptionType = Type(WasmStructTypeName(SpecialNames.JSExceptionClass)) } } From d115b9f1daf3f6d693522f3fbb4ab21bdb1cba66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 26 Mar 2024 12:05:11 +0100 Subject: [PATCH 4/9] Implement Throw. --- test-suite/src/main/scala/testsuite/Assert.scala | 14 ++------------ .../main/scala/ir2wasm/WasmExpressionBuilder.scala | 11 ++++++++--- wasm/src/main/scala/wasm4s/WasmContext.scala | 10 ++++++++++ 3 files changed, 20 insertions(+), 15 deletions(-) diff --git a/test-suite/src/main/scala/testsuite/Assert.scala b/test-suite/src/main/scala/testsuite/Assert.scala index 610994f4..0d363703 100644 --- a/test-suite/src/main/scala/testsuite/Assert.scala +++ b/test-suite/src/main/scala/testsuite/Assert.scala @@ -1,16 +1,6 @@ package testsuite -/** Temporary assertion method on Scala for Wasm. `ok` method generates `unreachable` if the given - * condition is false, trapping at runtime. - * - * While it's desirable to eventually utilize Scala's assertion, it's currently unavailable because - * we cannot compile Throwable to wasm yet, thus throw new (Throwable) is unusable. and making - * assert unavailable as well. - * - * Using JS's assert isn't feasible either; `console.assert` merely displays a message when - * assertion failure, and Node's assert module is unsupported for Wasm due to current - * unavailability of `JSImport` and module. - */ +/** Assertion helpers. */ object Assert { def ok(cond: Boolean): Unit = if (!cond) fail() @@ -19,5 +9,5 @@ object Assert { ok(expected.asInstanceOf[AnyRef] eq actual.asInstanceOf[AnyRef]) def fail(): Unit = - null.toString() // Apply to Null should compile to unreachable + throw new AssertionError("assertion failed") } diff --git a/wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala b/wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala index c7987c7f..17ca6821 100644 --- a/wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala +++ b/wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala @@ -122,6 +122,7 @@ private class WasmExpressionBuilder private ( case t: IRTrees.New => genNew(t) case t: IRTrees.If => genIf(t, expectedType) case t: IRTrees.While => genWhile(t) + case t: IRTrees.Throw => genThrow(t) case t: IRTrees.Debugger => IRTypes.NoType // ignore case t: IRTrees.Skip => IRTypes.NoType case t: IRTrees.IdentityHashCode => genIdentityHashCode(t) @@ -146,9 +147,6 @@ private class WasmExpressionBuilder private ( case t: IRTrees.JSLinkingInfo => genJSLinkingInfo(t) case t: IRTrees.Closure => genClosure(t) case t: IRTrees.Clone => genClone(t) - case _: IRTrees.Throw => - instrs += UNREACHABLE - IRTypes.NothingType // array case t: IRTrees.ArrayLength => genArrayLength(t) @@ -1328,6 +1326,13 @@ private class WasmExpressionBuilder private ( } } + private def genThrow(tree: IRTrees.Throw): IRTypes.Type = { + genTree(tree.expr, IRTypes.AnyType) + instrs += THROW(TagIdx(ctx.exceptionTagName)) + + IRTypes.NothingType + } + private def genBlock(t: IRTrees.Block, expectedType: IRTypes.Type): IRTypes.Type = { for (stat <- t.stats.init) genTree(stat, IRTypes.NoType) diff --git a/wasm/src/main/scala/wasm4s/WasmContext.scala b/wasm/src/main/scala/wasm4s/WasmContext.scala index 72206d0a..77431c7f 100644 --- a/wasm/src/main/scala/wasm4s/WasmContext.scala +++ b/wasm/src/main/scala/wasm4s/WasmContext.scala @@ -131,6 +131,8 @@ trait TypeDefinableWasmContext extends ReadOnlyWasmContext { this: WasmContext = ) ) + val exceptionTagName: WasmTagName + def addFunctionType(sig: WasmFunctionSignature): WasmFunctionTypeName = { functionSignatures.get(sig) match { case None => @@ -240,6 +242,14 @@ class WasmContext(val module: WasmModule) extends TypeDefinableWasmContext { def putClassInfo(name: IRNames.ClassName, info: WasmClassInfo): Unit = classInfo.put(name, info) + val exceptionTagName: WasmTagName = WasmTagName("exception") + + locally { + val exceptionSig = WasmFunctionSignature(List(WasmAnyRef), Nil) + val exceptionFunType = addFunctionType(exceptionSig) + module.addTag(WasmTag(exceptionTagName, exceptionFunType)) + } + private def addHelperImport( name: WasmFunctionName, params: List[WasmType], From c62ac8c8efe74f7185e607cae83533afce6feb8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 26 Mar 2024 10:53:42 +0100 Subject: [PATCH 5/9] Make it possible to run the sample from the browser. Because the latest nightly of Node.js does not support the latest specification for Wasm exceptions yet. --- loader.mjs | 16 +++++++++++----- testrun.html | 9 +++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 testrun.html diff --git a/loader.mjs b/loader.mjs index 1a41b08d..f456b73e 100644 --- a/loader.mjs +++ b/loader.mjs @@ -1,5 +1,3 @@ -import { readFileSync } from "node:fs"; - // Specified by java.lang.String.hashCode() function stringHashCode(s) { var res = 0; @@ -170,9 +168,17 @@ const scalaJSHelpers = { } export async function load(wasmFileName) { - const wasmBuffer = readFileSync(wasmFileName); - const wasmModule = await WebAssembly.instantiate(wasmBuffer, { + const importsObj = { "__scalaJSHelpers": scalaJSHelpers, - }); + }; + var wasmModulePromise; + if (typeof process !== "undefined") { + wasmModulePromise = import("node:fs").then((fs) => { + return WebAssembly.instantiate(fs.readFileSync(wasmFileName), importsObj); + }); + } else { + wasmModulePromise = WebAssembly.instantiateStreaming(fetch(wasmFileName), importsObj); + } + const wasmModule = await wasmModulePromise; return wasmModule.instance.exports; } diff --git a/testrun.html b/testrun.html new file mode 100644 index 00000000..47dd5645 --- /dev/null +++ b/testrun.html @@ -0,0 +1,9 @@ + + + Test run + + +

Look at the console

+ + + From 21881fddf76432f60eeacf06ffde7693f281cec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Thu, 21 Mar 2024 14:43:32 +0100 Subject: [PATCH 6/9] Remove old exception-related instructions from an outdated spec. --- wasm/src/main/scala/converters/WasmTextWriter.scala | 8 ++++---- wasm/src/main/scala/wasm4s/Instructions.scala | 5 ----- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/wasm/src/main/scala/converters/WasmTextWriter.scala b/wasm/src/main/scala/converters/WasmTextWriter.scala index 2ff5e696..037fe307 100644 --- a/wasm/src/main/scala/converters/WasmTextWriter.scala +++ b/wasm/src/main/scala/converters/WasmTextWriter.scala @@ -300,8 +300,8 @@ class WasmTextWriter { private def writeInstr(instr: WasmInstr)(implicit b: WatBuilder): Unit = { instr match { - case END | ELSE | _: CATCH => b.deindent() - case _ => () + case END | ELSE => b.deindent() + case _ => () } b.newLine() b.appendElement(instr.mnemonic) @@ -333,8 +333,8 @@ class WasmTextWriter { } instr match { - case _: BLOCK | _: LOOP | _: IF | ELSE | _: CATCH | _: TRY => b.indent() - case _ => () + case _: BLOCK | _: LOOP | _: IF | ELSE => b.indent() + case _ => () } } diff --git a/wasm/src/main/scala/wasm4s/Instructions.scala b/wasm/src/main/scala/wasm4s/Instructions.scala index 2a131c56..5464c7e5 100644 --- a/wasm/src/main/scala/wasm4s/Instructions.scala +++ b/wasm/src/main/scala/wasm4s/Instructions.scala @@ -204,12 +204,7 @@ object WasmInstr { case class CALL(i: FuncIdx) extends WasmInstr("call", 0x10, List(i)) case class CALL_INDIRECT(i: TableIdx, t: TypeIdx) extends WasmInstr("call_indirect", 0x11, List(i, t)) - case class TRY(i: BlockType) extends WasmInstr("try", 0x06, List(i)) - case class CATCH(i: TagIdx) extends WasmInstr("catch", 0x07, List(i)) - case object CATCH_ALL extends WasmInstr("catch_all", 0x19) - case class DELEGATE(i: LabelIdx) extends WasmInstr("delegate", 0x18, List(i)) case class THROW(i: TagIdx) extends WasmInstr("throw", 0x08, List(i)) - case class RETHROW(i: LabelIdx) extends WasmInstr("rethrow", 0x09) // Parametric instructions // https://webassembly.github.io/spec/core/syntax/instructions.html#parametric-instructions From e5f16393c309250fb593c8a1d98dd58269ae3a5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Fri, 22 Mar 2024 00:14:11 +0100 Subject: [PATCH 7/9] Make the logic for indenting instructions more generic. --- wasm/src/main/scala/converters/WasmTextWriter.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wasm/src/main/scala/converters/WasmTextWriter.scala b/wasm/src/main/scala/converters/WasmTextWriter.scala index 037fe307..6b577cfe 100644 --- a/wasm/src/main/scala/converters/WasmTextWriter.scala +++ b/wasm/src/main/scala/converters/WasmTextWriter.scala @@ -333,8 +333,8 @@ class WasmTextWriter { } instr match { - case _: BLOCK | _: LOOP | _: IF | ELSE => b.indent() - case _ => () + case _: StructuredLabeledInstr | ELSE => b.indent() + case _ => () } } From 25578616cfe5cc7fa6df562e7adb682d603a2bfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 26 Mar 2024 17:33:30 +0100 Subject: [PATCH 8/9] Implement TryCatch. Including adding all the missing instructions and types for the latest specification of exception handling. --- .../scala/converters/WasmBinaryWriter.scala | 7 +++++ .../scala/converters/WasmTextWriter.scala | 8 ++++++ .../scala/ir2wasm/WasmExpressionBuilder.scala | 22 ++++++++++++++- wasm/src/main/scala/wasm4s/Defaults.scala | 1 + wasm/src/main/scala/wasm4s/Instructions.scala | 19 +++++++++++++ wasm/src/main/scala/wasm4s/Types.scala | 3 ++ .../scala/wasm4s/WasmFunctionContext.scala | 28 +++++++++++++++++++ 7 files changed, 87 insertions(+), 1 deletion(-) diff --git a/wasm/src/main/scala/converters/WasmBinaryWriter.scala b/wasm/src/main/scala/converters/WasmBinaryWriter.scala index ccc053ad..cef49e56 100644 --- a/wasm/src/main/scala/converters/WasmBinaryWriter.scala +++ b/wasm/src/main/scala/converters/WasmBinaryWriter.scala @@ -341,6 +341,13 @@ final class WasmBinaryWriter(module: WasmModule) { case HeapType(value) => writeHeapType(buf, value) case StructFieldIdx(value) => buf.u32(value) + case CatchClauseVector(clauses) => + buf.vec(clauses) { clause => + buf.byte(clause.opcode.toByte) + for (imm <- clause.immediates) + writeImmediate(buf, imm) + } + case CastFlags(nullable1, nullable2) => buf.byte(((if (nullable1) 1 else 0) | (if (nullable2) 2 else 0)).toByte) } diff --git a/wasm/src/main/scala/converters/WasmTextWriter.scala b/wasm/src/main/scala/converters/WasmTextWriter.scala index 6b577cfe..f87697b5 100644 --- a/wasm/src/main/scala/converters/WasmTextWriter.scala +++ b/wasm/src/main/scala/converters/WasmTextWriter.scala @@ -278,6 +278,14 @@ class WasmTextWriter { indices.map(i => "$" + i.value).mkString(" ") case WasmImmediate.TagIdx(name) => name.show + case WasmImmediate.CatchClauseVector(clauses) => + for (clause <- clauses) { + b.appendElement("(" + clause.mnemonic) + for (imm <- clause.immediates) + writeImmediate(imm, instr) + b.appendElement(")") + } + "" case i: WasmImmediate.CastFlags => throw new UnsupportedOperationException( s"CastFlags $i must be handled directly in the instruction $instr" diff --git a/wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala b/wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala index 17ca6821..4d37c456 100644 --- a/wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala +++ b/wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala @@ -122,6 +122,7 @@ private class WasmExpressionBuilder private ( case t: IRTrees.New => genNew(t) case t: IRTrees.If => genIf(t, expectedType) case t: IRTrees.While => genWhile(t) + case t: IRTrees.TryCatch => genTryCatch(t) case t: IRTrees.Throw => genThrow(t) case t: IRTrees.Debugger => IRTypes.NoType // ignore case t: IRTrees.Skip => IRTypes.NoType @@ -171,7 +172,6 @@ private class WasmExpressionBuilder private ( // case IRTrees.CreateJSClass(pos) => // case IRTrees.Transient(pos) => // case IRTrees.ForIn(pos) => - // case tc: IRTrees.TryCatch => ??? // case IRTrees.JSImportCall(pos) => } @@ -1326,6 +1326,26 @@ 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.WasmAnyRef) { catchLabel => + fctx.tryTable(resultType)( + List(CatchClause.Catch(TagIdx(ctx.exceptionTagName), catchLabel)) + ) { + genTree(t.block, t.tpe) + } + instrs += BR(doneLabel) + } // end block $catch + val exceptionLocal = fctx.addLocal(t.errVar.name, Types.WasmAnyRef) + instrs += LOCAL_SET(exceptionLocal) + genTree(t.handler, t.tpe) + } // end block $done + + t.tpe + } + private def genThrow(tree: IRTrees.Throw): IRTypes.Type = { genTree(tree.expr, IRTypes.AnyType) instrs += THROW(TagIdx(ctx.exceptionTagName)) diff --git a/wasm/src/main/scala/wasm4s/Defaults.scala b/wasm/src/main/scala/wasm4s/Defaults.scala index eecc7a77..d0ce01c8 100644 --- a/wasm/src/main/scala/wasm4s/Defaults.scala +++ b/wasm/src/main/scala/wasm4s/Defaults.scala @@ -12,6 +12,7 @@ object Defaults { case WasmInt32 => I32_CONST(I32(0)) case WasmAnyRef => REF_NULL(HeapType(WasmHeapType.Simple.Any)) case WasmExternRef => REF_NULL(HeapType(WasmHeapType.Simple.NoExtern)) + case WasmExnRef => REF_NULL(HeapType(WasmHeapType.Simple.NoExn)) case WasmRefType(_) => nonDefaultable(t) case WasmFloat32 => F32_CONST(F32(0)) case WasmFuncRef => REF_NULL(HeapType(WasmHeapType.Simple.Func)) diff --git a/wasm/src/main/scala/wasm4s/Instructions.scala b/wasm/src/main/scala/wasm4s/Instructions.scala index 5464c7e5..b1777d6e 100644 --- a/wasm/src/main/scala/wasm4s/Instructions.scala +++ b/wasm/src/main/scala/wasm4s/Instructions.scala @@ -205,6 +205,9 @@ object WasmInstr { case class CALL_INDIRECT(i: TableIdx, t: TypeIdx) extends WasmInstr("call_indirect", 0x11, List(i, t)) case class THROW(i: TagIdx) extends WasmInstr("throw", 0x08, List(i)) + case object THROW_REF extends WasmInstr("throw_ref", 0x0A) + case class TRY_TABLE(i: BlockType, cs: CatchClauseVector, label: Option[LabelIdx] = None) + extends StructuredLabeledInstr("try_table", 0x1F, List(i, cs)) // Parametric instructions // https://webassembly.github.io/spec/core/syntax/instructions.html#parametric-instructions @@ -338,16 +341,32 @@ object WasmImmediate { case class LocalIdx(val value: WasmLocalName) extends WasmImmediate case class GlobalIdx(val value: WasmGlobalName) extends WasmImmediate case class HeapType(val value: WasmHeapType) extends WasmImmediate + case class StructFieldIdx(val value: Int) extends WasmImmediate object StructFieldIdx { val vtable = StructFieldIdx(0) val itables = StructFieldIdx(1) } + case class CatchClauseVector(val value: List[CatchClause]) extends WasmImmediate + /** `castflags` for `br_on_cast` and `br_on_cast_fail`. * * @see * https://webassembly.github.io/gc/core/binary/instructions.html#control-instructions */ case class CastFlags(nullable1: Boolean, nullable2: Boolean) extends WasmImmediate + + sealed abstract class CatchClause( + val mnemonic: String, + val opcode: Int, + val immediates: List[WasmImmediate] + ) + + object CatchClause { + case class Catch(x: TagIdx, l: LabelIdx) extends CatchClause("catch", 0x00, List(x, l)) + case class CatchRef(x: TagIdx, l: LabelIdx) extends CatchClause("catch_ref", 0x01, List(x, l)) + case class CatchAll(l: LabelIdx) extends CatchClause("catch_all", 0x02, List(l)) + case class CatchAllRef(l: LabelIdx) extends CatchClause("catch_all_ref", 0x03, List(l)) + } } diff --git a/wasm/src/main/scala/wasm4s/Types.scala b/wasm/src/main/scala/wasm4s/Types.scala index 73083e54..69a16bc4 100644 --- a/wasm/src/main/scala/wasm4s/Types.scala +++ b/wasm/src/main/scala/wasm4s/Types.scala @@ -35,6 +35,7 @@ object Types { // https://github.com/WebAssembly/gc/blob/main/proposals/gc/MVP.md#reference-types-1 case object WasmFuncRef extends WasmType("funcref", 0x70) case object WasmExternRef extends WasmType("externref", 0x6F) + case object WasmExnRef extends WasmType("exnref", 0x69) /** shorthand for (ref null any) */ case object WasmAnyRef extends WasmType("anyref", 0x6E) @@ -73,9 +74,11 @@ object Types { object Any extends Simple("any", 0x6E) object Eq extends Simple("eq", 0x6D) object Array extends Simple("array", 0x6A) + object Exn extends Simple("exn", 0x69) object Struct extends Simple("struct", 0x6B) object None extends Simple("none", 0x71) object NoExtern extends Simple("noextern", 0x72) + object NoExn extends Simple("noexn", 0x74) } val ObjectType = Type(WasmStructTypeName(IRNames.ObjectClass)) diff --git a/wasm/src/main/scala/wasm4s/WasmFunctionContext.scala b/wasm/src/main/scala/wasm4s/WasmFunctionContext.scala index e008d816..7d1ffb8c 100644 --- a/wasm/src/main/scala/wasm4s/WasmFunctionContext.scala +++ b/wasm/src/main/scala/wasm4s/WasmFunctionContext.scala @@ -167,6 +167,34 @@ class WasmFunctionContext private ( } } + def tryTable[A](blockType: BlockType)(clauses: List[WasmImmediate.CatchClause])(body: => A): A = { + instrs += TRY_TABLE(blockType, WasmImmediate.CatchClauseVector(clauses)) + val result = body + instrs += END + result + } + + def tryTable[A](resultType: WasmType)(clauses: List[WasmImmediate.CatchClause])(body: => A): A = + tryTable(BlockType.ValueType(resultType))(clauses)(body) + + def tryTable[A]()(clauses: List[WasmImmediate.CatchClause])(body: => A): A = + tryTable(BlockType.ValueType())(clauses)(body) + + def tryTable[A](sig: WasmFunctionSignature)(clauses: List[WasmImmediate.CatchClause])( + body: => A + ): A = + tryTable(BlockType.FunctionType(ctx.addFunctionType(sig)))(clauses)(body) + + def tryTable[A]( + resultTypes: List[WasmType] + )(clauses: List[WasmImmediate.CatchClause])(body: => A): A = { + resultTypes match { + case Nil => tryTable()(clauses)(body) + case single :: Nil => tryTable(single)(clauses)(body) + case _ => tryTable(WasmFunctionSignature(Nil, resultTypes))(clauses)(body) + } + } + // Final result def buildAndAddToContext(): WasmFunction = { From 452ffec87df8373a37e4efc37f8dcf8d72ef3f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Tue, 26 Mar 2024 17:36:18 +0100 Subject: [PATCH 9/9] Implement rudimentary support for TryFinally. Currently, it does not correctly handle `Labeled/Return` pairs that cross its boundary. --- .../scala/ir2wasm/WasmExpressionBuilder.scala | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala b/wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala index 4d37c456..649ad1b4 100644 --- a/wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala +++ b/wasm/src/main/scala/ir2wasm/WasmExpressionBuilder.scala @@ -123,6 +123,7 @@ private class WasmExpressionBuilder private ( case t: IRTrees.If => genIf(t, expectedType) case t: IRTrees.While => genWhile(t) case t: IRTrees.TryCatch => genTryCatch(t) + case t: IRTrees.TryFinally => genTryFinally(t) case t: IRTrees.Throw => genThrow(t) case t: IRTrees.Debugger => IRTypes.NoType // ignore case t: IRTrees.Skip => IRTypes.NoType @@ -165,7 +166,6 @@ private class WasmExpressionBuilder private ( // case IRTrees.JSSuperMethodCall(pos) => // case IRTrees.Match(tpe) => // case IRTrees.RecordSelect(tpe) => - // case IRTrees.TryFinally(pos) => // case IRTrees.JSImportMeta(pos) => // case IRTrees.JSSuperSelect(pos) => // case IRTrees.JSSuperConstructorCall(pos) => @@ -1346,6 +1346,46 @@ private class WasmExpressionBuilder private ( t.tpe } + private def genTryFinally(t: IRTrees.TryFinally): IRTypes.Type = { + /* This implementation is rudimentary. It does not handle `Labeled/Return` + * pairs that cross its boundary. In case there is a `Return` inside the + * `try` block targeting a `Labeled` block around this `TryFinally`, the + * `finally` block is by-passed. + */ + + val resultType = TypeTransformer.transformResultType(t.tpe)(ctx) + val resultLocals = resultType.map(fctx.addSyntheticLocal(_)) + + fctx.block() { doneLabel => + fctx.block(Types.WasmExnRef) { catchLabel => + fctx.tryTable()(List(CatchClause.CatchAllRef(catchLabel))) { + // try block + genTree(t.block, t.tpe) + + // store the result in locals during the finally block + for (resultLocal <- resultLocals.reverse) + instrs += LOCAL_SET(resultLocal) + } + + // on success, push a `null_ref exn` on the stack + instrs += REF_NULL(HeapType(Types.WasmHeapType.Simple.Exn)) + } // end block $catch + + // finally block (during which we leave the `(ref null exn)` on the stack) + genTree(t.finalizer, IRTypes.NoType) + + // if the `exnref` is non-null, rethrow it + instrs += BR_ON_NULL(doneLabel) + instrs += THROW_REF + } // end block $done + + // reload the result onto the stack + for (resultLocal <- resultLocals) + instrs += LOCAL_GET(resultLocal) + + t.tpe + } + private def genThrow(tree: IRTrees.Throw): IRTypes.Type = { genTree(tree.expr, IRTypes.AnyType) instrs += THROW(TagIdx(ctx.exceptionTagName))