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

Fix #32: Implement exception handling. #38

Merged
merged 9 commits into from
Mar 27, 2024
3 changes: 2 additions & 1 deletion cli/src/main/scala/TestSuites.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
}
16 changes: 11 additions & 5 deletions loader.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { readFileSync } from "node:fs";

// Specified by java.lang.String.hashCode()
function stringHashCode(s) {
var res = 0;
Expand Down Expand Up @@ -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;
}
17 changes: 5 additions & 12 deletions test-suite/src/main/scala/testsuite/Assert.scala
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
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) null.toString() // Apply to Null should compile to unreachable
tanishiking marked this conversation as resolved.
Show resolved Hide resolved
if (!cond) fail()

def assertSame(expected: Any, actual: Any): Unit =
ok(expected.asInstanceOf[AnyRef] eq actual.asInstanceOf[AnyRef])

def fail(): Unit =
throw new AssertionError("assertion failed")
}
Original file line number Diff line number Diff line change
@@ -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))
}
}
9 changes: 9 additions & 0 deletions testrun.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<html>
<head>
<title>Test run</title>
</head>
<body>
<p>Look at the console</p>
<script type="module" src="./run.mjs"></script>
</body>
</html>
65 changes: 62 additions & 3 deletions wasm/src/main/scala/converters/WasmBinaryWriter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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`

Expand All @@ -106,17 +127,37 @@ 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 = {
buf.vec(module.imports) { imprt =>
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)
}
}
}
Expand All @@ -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)
Expand Down Expand Up @@ -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))

Expand Down Expand Up @@ -284,12 +335,19 @@ 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)
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)
}
Expand All @@ -309,6 +367,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()
Expand Down
35 changes: 31 additions & 4 deletions wasm/src/main/scala/converters/WasmTextWriter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
)
}
}
)
Expand Down Expand Up @@ -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", {
Expand Down Expand Up @@ -259,6 +276,16 @@ 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 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"
Expand All @@ -281,8 +308,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)
Expand Down Expand Up @@ -314,8 +341,8 @@ class WasmTextWriter {
}

instr match {
case _: BLOCK | _: LOOP | _: IF | ELSE | _: CATCH | _: TRY => b.indent()
case _ => ()
case _: StructuredLabeledInstr | ELSE => b.indent()
case _ => ()
}
}

Expand Down
7 changes: 6 additions & 1 deletion wasm/src/main/scala/ir2wasm/LibraryPatches.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,13 @@ object LibraryPatches {
}
}

val leanerJSExceptionIRFile =
org.scalajs.linker.backend.emitter.PrivateLibHolder.files.find { irFile =>
tanishiking marked this conversation as resolved.
Show resolved Hide resolved
IRFileImpl.fromIRFile(irFile).path.contains("JavaScriptException")
}.get

derivedIRFiles.map { derived =>
derived.flatten ++ Seq(StackTraceIRFile) ++ irFiles
derived.flatten ++ Seq(StackTraceIRFile, leanerJSExceptionIRFile) ++ irFiles
}
}

Expand Down
5 changes: 5 additions & 0 deletions wasm/src/main/scala/ir2wasm/SpecialNames.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}
Loading