Skip to content
This repository was 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
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
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
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 =>
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