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

Commit

Permalink
Merge pull request #13 from sjrd/upcast-downcast-hijacked-classes
Browse files Browse the repository at this point in the history
Fix #12: Implement the behavior of hijacked classes.
  • Loading branch information
tanishiking authored Mar 13, 2024
2 parents e956e49 + 5acf283 commit 334446d
Show file tree
Hide file tree
Showing 22 changed files with 1,435 additions and 227 deletions.
17 changes: 16 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import org.scalajs.linker.interface.OutputPatterns

val scalaV = "2.13.12"

inThisBuild(Def.settings(
scalacOptions ++= Seq(
"-encoding",
"utf-8",
"-feature",
"-deprecation",
"-Xfatal-warnings",
)
))

lazy val cli = project
.in(file("cli"))
.enablePlugins(ScalaJSPlugin)
Expand Down Expand Up @@ -86,7 +98,10 @@ lazy val tests = project
"org.scala-js" %%% "scala-js-macrotask-executor" % "1.1.1" % Test
),
scalaJSLinkerConfig ~= {
_.withModuleKind(ModuleKind.CommonJSModule),
// Generate CoreTests as an ES module so that it can import the loader.mjs
// Give it an `.mjs` extension so that Node.js actually interprets it as an ES module
_.withModuleKind(ModuleKind.ESModule)
.withOutputPatterns(OutputPatterns.fromJSFile("%s.mjs")),
},
test := Def.sequential(
(testSuite / Compile / run).toTask(""),
Expand Down
5 changes: 4 additions & 1 deletion cli/src/main/scala/TestSuites.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ object TestSuites {
TestSuite("testsuite.core.virtualdispatch.VirtualDispatch", "virtualDispatch"),
TestSuite("testsuite.core.interfacecall.InterfaceCall", "interfaceCall"),
TestSuite("testsuite.core.asinstanceof.AsInstanceOfTest", "asInstanceOf"),
TestSuite("testsuite.core.hijackedclassesmono.HijackedClassesMonoTest", "hijackedClassesMono")
TestSuite("testsuite.core.hijackedclassesdispatch.HijackedClassesDispatchTest", "hijackedClassesDispatch"),
TestSuite("testsuite.core.hijackedclassesmono.HijackedClassesMonoTest", "hijackedClassesMono"),
TestSuite("testsuite.core.hijackedclassesupcast.HijackedClassesUpcastTest", "hijackedClassesUpcast"),
TestSuite("testsuite.core.tostring.ToStringTest", "toStringConversions")
)
}
90 changes: 90 additions & 0 deletions loader.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { readFileSync } from "node:fs";

// Specified by java.lang.String.hashCode()
function stringHashCode(s) {
var res = 0;
var mul = 1;
var i = (s.length - 1) | 0;
while ((i >= 0)) {
res = ((res + Math.imul(s.charCodeAt(i), mul)) | 0);
mul = Math.imul(31, mul);
i = (i - 1) | 0;
}
return res;
}

const scalaJSHelpers = {
// BinaryOp.===
is: Object.is,

// undefined
undef: () => void 0,
isUndef: (x) => x === (void 0),

// Boxes (upcast) -- most are identity at the JS level but with different types in Wasm
bZ: (x) => x !== 0,
bB: (x) => x,
bS: (x) => x,
bI: (x) => x,
bF: (x) => x,
bD: (x) => x,

// Unboxes (downcast, null is converted to the zero of the type)
uZ: (x) => x | 0,
uB: (x) => (x << 24) >> 24,
uS: (x) => (x << 16) >> 16,
uI: (x) => x | 0,
uF: (x) => Math.fround(x),
uD: (x) => +x,

// Unboxes to primitive or null (downcast to the boxed classes)
uNZ: (x) => (x !== null) ? (x | 0) : null,
uNB: (x) => (x !== null) ? ((x << 24) >> 24) : null,
uNS: (x) => (x !== null) ? ((x << 16) >> 16) : null,
uNI: (x) => (x !== null) ? (x | 0) : null,
uNF: (x) => (x !== null) ? Math.fround(x) : null,
uND: (x) => (x !== null) ? +x : null,

// Type tests
tZ: (x) => typeof x === 'boolean',
tB: (x) => typeof x === 'number' && Object.is((x << 24) >> 24, x),
tS: (x) => typeof x === 'number' && Object.is((x << 16) >> 16, x),
tI: (x) => typeof x === 'number' && Object.is(x | 0, x),
tF: (x) => typeof x === 'number' && (Math.fround(x) === x || x !== x),
tD: (x) => typeof x === 'number',

// Strings
emptyString: () => "",
stringLength: (s) => s.length,
stringCharAt: (s, i) => s.charCodeAt(i),
jsValueToString: (x) => "" + x,
booleanToString: (b) => b ? "true" : "false",
charToString: (c) => String.fromCharCode(c),
intToString: (i) => "" + i,
longToString: (l) => "" + l, // l must be a bigint here
doubleToString: (d) => "" + d,
stringConcat: (x, y) => ("" + x) + y, // the added "" is for the case where x === y === null
isString: (x) => typeof x === 'string',

// Hash code, because it is overridden in all hijacked classes
// Specified by the hashCode() method of the corresponding hijacked classes
jsValueHashCode: (x) => {
if (typeof x === 'number')
return x | 0; // TODO make this compliant for floats
if (typeof x === 'string')
return stringHashCode(x);
if (typeof x === 'boolean')
return x ? 1231 : 1237;
if (typeof x === 'undefined')
return 0;
return 42; // for any JS object
},
}

export async function load(wasmFileName) {
const wasmBuffer = readFileSync(wasmFileName);
const wasmModule = await WebAssembly.instantiate(wasmBuffer, {
"__scalaJSHelpers": scalaJSHelpers,
});
return wasmModule.instance.exports;
}
7 changes: 3 additions & 4 deletions run.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { readFileSync } from "node:fs";
const wasmBuffer = readFileSync("./target/output.wasm");
const wasmModule = await WebAssembly.instantiate(wasmBuffer);
const { test } = wasmModule.instance.exports;
import { load } from "./loader.mjs";

const { test } = await load("./target/output.wasm");
const o = test();
console.log(o);
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package testsuite.core.hijackedclassesdispatch

import scala.scalajs.js.annotation._

object HijackedClassesDispatchTest {
def main(): Unit = { val _ = test() }

@JSExportTopLevel("hijackedClassesDispatch")
def test(): Boolean = {
val obj = new Test()
val otherObj = new Test()
val obj2 = new Test2()
val otherObj2 = new Test2()
testToString(true, "true") &&
testToString(54321, "54321") &&
testToString(obj, "Test class") &&
testToString(obj2, "[object]") &&
testToString('A', "A") &&
testHashCode(true, 1231) &&
testHashCode(54321, 54321) &&
testHashCode("foo", 101574) &&
testHashCode(obj, 123) &&
testHashCode(obj2, 42) &&
testHashCode('A', 65) &&
testIntValue(Int.box(5), 5) &&
testIntValue(Long.box(6L), 6) &&
testIntValue(Double.box(7.5), 7) &&
testIntValue(new CustomNumber(), 789) &&
testLength("foo", 3) &&
testLength(new CustomCharSeq(), 54) &&
testCharAt("foobar", 3, 'b') &&
testCharAt(new CustomCharSeq(), 3, 'A') &&
testEquals(true, 1, false) &&
testEquals(1.0, 1, true) &&
testEquals("foo", "foo", true) &&
testEquals("foo", "bar", false) &&
testEquals(obj, obj2, false) &&
testEquals(obj, otherObj, true) &&
testEquals(obj2, otherObj2, false) &&
testNotifyAll(true) &&
testNotifyAll(obj)
}

def testToString(x: Any, expected: String): Boolean =
x.toString() == expected

def testHashCode(x: Any, expected: Int): Boolean =
x.hashCode() == expected

def testIntValue(x: Number, expected: Int): Boolean =
x.intValue() == expected

def testLength(x: CharSequence, expected: Int): Boolean =
x.length() == expected

def testCharAt(x: CharSequence, i: Int, expected: Char): Boolean =
x.charAt(i) == expected

def testEquals(x: Any, y: Any, expected: Boolean): Boolean =
x.asInstanceOf[AnyRef].equals(y) == expected

def testNotifyAll(x: Any): Boolean = {
// This is just to test that the call validates and does not trap
x.asInstanceOf[AnyRef].notifyAll()
true
}

class Test {
override def toString(): String = "Test class"

override def hashCode(): Int = 123

override def equals(that: Any): Boolean =
that.isInstanceOf[Test]
}

class Test2

class CustomNumber() extends Number {
def value(): Int = 789
def intValue(): Int = value()
def longValue(): Long = 789L
def floatValue(): Float = 789.0f
def doubleValue(): Double = 789.0
}

class CustomCharSeq extends CharSequence {
def length(): Int = 54
override def toString(): String = "CustomCharSeq"
def charAt(index: Int): Char = 'A'
def subSequence(start: Int, end: Int): CharSequence = this
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package testsuite.core.hijackedclassesupcast

import scala.scalajs.js.annotation._

object HijackedClassesUpcastTest {
def main(): Unit = { val _ = test() }

@JSExportTopLevel("hijackedClassesUpcast")
def test(): Boolean = {
testBoolean(true) &&
testInteger(5) &&
testIntegerNull(null) &&
testString("foo") &&
testStringNull(null) &&
testCharacter('A')
}

def testBoolean(x: Boolean): Boolean = {
val x1 = identity(x)
x1 && {
val x2: Any = x1
x2 match {
case x3: Boolean => x3
case _ => false
}
}
}

def testInteger(x: Int): Boolean = {
val x1 = identity(x)
x1 == 5 && {
val x2: Any = x1
x2 match {
case x3: Int => x3 + 1 == 6
case _ => false
}
}
}

def testIntegerNull(x: Any): Boolean = {
!x.isInstanceOf[Int] &&
!x.isInstanceOf[java.lang.Integer] &&
(x.asInstanceOf[Int] == 0) && {
val x2 = x.asInstanceOf[java.lang.Integer]
x2 == null
}
}

def testString(x: String): Boolean = {
val x1 = identity(x)
x1.length() == 3 && {
val x2: Any = x1
x2 match {
case x3: String => x3.length() == 3
case _ => false
}
}
}

def testStringNull(x: Any): Boolean = {
!x.isInstanceOf[String] && {
val x2 = x.asInstanceOf[String]
x2 == null
}
}

def testCharacter(x: Char): Boolean = {
val x1 = identity(x)
x1 == 'A' && {
val x2: Any = x1
x2 match {
case x3: Char => (x3 + 1).toChar == 'B'
case _ => false
}
}
}

@noinline
def identity[A](x: A): A = x
}
Loading

0 comments on commit 334446d

Please sign in to comment.