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

Commit 334446d

Browse files
authored
Merge pull request #13 from sjrd/upcast-downcast-hijacked-classes
Fix #12: Implement the behavior of hijacked classes.
2 parents e956e49 + 5acf283 commit 334446d

22 files changed

+1435
-227
lines changed

Diff for: build.sbt

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
1+
import org.scalajs.linker.interface.OutputPatterns
2+
13
val scalaV = "2.13.12"
24

5+
inThisBuild(Def.settings(
6+
scalacOptions ++= Seq(
7+
"-encoding",
8+
"utf-8",
9+
"-feature",
10+
"-deprecation",
11+
"-Xfatal-warnings",
12+
)
13+
))
14+
315
lazy val cli = project
416
.in(file("cli"))
517
.enablePlugins(ScalaJSPlugin)
@@ -86,7 +98,10 @@ lazy val tests = project
8698
"org.scala-js" %%% "scala-js-macrotask-executor" % "1.1.1" % Test
8799
),
88100
scalaJSLinkerConfig ~= {
89-
_.withModuleKind(ModuleKind.CommonJSModule),
101+
// Generate CoreTests as an ES module so that it can import the loader.mjs
102+
// Give it an `.mjs` extension so that Node.js actually interprets it as an ES module
103+
_.withModuleKind(ModuleKind.ESModule)
104+
.withOutputPatterns(OutputPatterns.fromJSFile("%s.mjs")),
90105
},
91106
test := Def.sequential(
92107
(testSuite / Compile / run).toTask(""),

Diff for: cli/src/main/scala/TestSuites.scala

+4-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ object TestSuites {
99
TestSuite("testsuite.core.virtualdispatch.VirtualDispatch", "virtualDispatch"),
1010
TestSuite("testsuite.core.interfacecall.InterfaceCall", "interfaceCall"),
1111
TestSuite("testsuite.core.asinstanceof.AsInstanceOfTest", "asInstanceOf"),
12-
TestSuite("testsuite.core.hijackedclassesmono.HijackedClassesMonoTest", "hijackedClassesMono")
12+
TestSuite("testsuite.core.hijackedclassesdispatch.HijackedClassesDispatchTest", "hijackedClassesDispatch"),
13+
TestSuite("testsuite.core.hijackedclassesmono.HijackedClassesMonoTest", "hijackedClassesMono"),
14+
TestSuite("testsuite.core.hijackedclassesupcast.HijackedClassesUpcastTest", "hijackedClassesUpcast"),
15+
TestSuite("testsuite.core.tostring.ToStringTest", "toStringConversions")
1316
)
1417
}

Diff for: loader.mjs

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { readFileSync } from "node:fs";
2+
3+
// Specified by java.lang.String.hashCode()
4+
function stringHashCode(s) {
5+
var res = 0;
6+
var mul = 1;
7+
var i = (s.length - 1) | 0;
8+
while ((i >= 0)) {
9+
res = ((res + Math.imul(s.charCodeAt(i), mul)) | 0);
10+
mul = Math.imul(31, mul);
11+
i = (i - 1) | 0;
12+
}
13+
return res;
14+
}
15+
16+
const scalaJSHelpers = {
17+
// BinaryOp.===
18+
is: Object.is,
19+
20+
// undefined
21+
undef: () => void 0,
22+
isUndef: (x) => x === (void 0),
23+
24+
// Boxes (upcast) -- most are identity at the JS level but with different types in Wasm
25+
bZ: (x) => x !== 0,
26+
bB: (x) => x,
27+
bS: (x) => x,
28+
bI: (x) => x,
29+
bF: (x) => x,
30+
bD: (x) => x,
31+
32+
// Unboxes (downcast, null is converted to the zero of the type)
33+
uZ: (x) => x | 0,
34+
uB: (x) => (x << 24) >> 24,
35+
uS: (x) => (x << 16) >> 16,
36+
uI: (x) => x | 0,
37+
uF: (x) => Math.fround(x),
38+
uD: (x) => +x,
39+
40+
// Unboxes to primitive or null (downcast to the boxed classes)
41+
uNZ: (x) => (x !== null) ? (x | 0) : null,
42+
uNB: (x) => (x !== null) ? ((x << 24) >> 24) : null,
43+
uNS: (x) => (x !== null) ? ((x << 16) >> 16) : null,
44+
uNI: (x) => (x !== null) ? (x | 0) : null,
45+
uNF: (x) => (x !== null) ? Math.fround(x) : null,
46+
uND: (x) => (x !== null) ? +x : null,
47+
48+
// Type tests
49+
tZ: (x) => typeof x === 'boolean',
50+
tB: (x) => typeof x === 'number' && Object.is((x << 24) >> 24, x),
51+
tS: (x) => typeof x === 'number' && Object.is((x << 16) >> 16, x),
52+
tI: (x) => typeof x === 'number' && Object.is(x | 0, x),
53+
tF: (x) => typeof x === 'number' && (Math.fround(x) === x || x !== x),
54+
tD: (x) => typeof x === 'number',
55+
56+
// Strings
57+
emptyString: () => "",
58+
stringLength: (s) => s.length,
59+
stringCharAt: (s, i) => s.charCodeAt(i),
60+
jsValueToString: (x) => "" + x,
61+
booleanToString: (b) => b ? "true" : "false",
62+
charToString: (c) => String.fromCharCode(c),
63+
intToString: (i) => "" + i,
64+
longToString: (l) => "" + l, // l must be a bigint here
65+
doubleToString: (d) => "" + d,
66+
stringConcat: (x, y) => ("" + x) + y, // the added "" is for the case where x === y === null
67+
isString: (x) => typeof x === 'string',
68+
69+
// Hash code, because it is overridden in all hijacked classes
70+
// Specified by the hashCode() method of the corresponding hijacked classes
71+
jsValueHashCode: (x) => {
72+
if (typeof x === 'number')
73+
return x | 0; // TODO make this compliant for floats
74+
if (typeof x === 'string')
75+
return stringHashCode(x);
76+
if (typeof x === 'boolean')
77+
return x ? 1231 : 1237;
78+
if (typeof x === 'undefined')
79+
return 0;
80+
return 42; // for any JS object
81+
},
82+
}
83+
84+
export async function load(wasmFileName) {
85+
const wasmBuffer = readFileSync(wasmFileName);
86+
const wasmModule = await WebAssembly.instantiate(wasmBuffer, {
87+
"__scalaJSHelpers": scalaJSHelpers,
88+
});
89+
return wasmModule.instance.exports;
90+
}

Diff for: run.mjs

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { readFileSync } from "node:fs";
2-
const wasmBuffer = readFileSync("./target/output.wasm");
3-
const wasmModule = await WebAssembly.instantiate(wasmBuffer);
4-
const { test } = wasmModule.instance.exports;
1+
import { load } from "./loader.mjs";
2+
3+
const { test } = await load("./target/output.wasm");
54
const o = test();
65
console.log(o);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package testsuite.core.hijackedclassesdispatch
2+
3+
import scala.scalajs.js.annotation._
4+
5+
object HijackedClassesDispatchTest {
6+
def main(): Unit = { val _ = test() }
7+
8+
@JSExportTopLevel("hijackedClassesDispatch")
9+
def test(): Boolean = {
10+
val obj = new Test()
11+
val otherObj = new Test()
12+
val obj2 = new Test2()
13+
val otherObj2 = new Test2()
14+
testToString(true, "true") &&
15+
testToString(54321, "54321") &&
16+
testToString(obj, "Test class") &&
17+
testToString(obj2, "[object]") &&
18+
testToString('A', "A") &&
19+
testHashCode(true, 1231) &&
20+
testHashCode(54321, 54321) &&
21+
testHashCode("foo", 101574) &&
22+
testHashCode(obj, 123) &&
23+
testHashCode(obj2, 42) &&
24+
testHashCode('A', 65) &&
25+
testIntValue(Int.box(5), 5) &&
26+
testIntValue(Long.box(6L), 6) &&
27+
testIntValue(Double.box(7.5), 7) &&
28+
testIntValue(new CustomNumber(), 789) &&
29+
testLength("foo", 3) &&
30+
testLength(new CustomCharSeq(), 54) &&
31+
testCharAt("foobar", 3, 'b') &&
32+
testCharAt(new CustomCharSeq(), 3, 'A') &&
33+
testEquals(true, 1, false) &&
34+
testEquals(1.0, 1, true) &&
35+
testEquals("foo", "foo", true) &&
36+
testEquals("foo", "bar", false) &&
37+
testEquals(obj, obj2, false) &&
38+
testEquals(obj, otherObj, true) &&
39+
testEquals(obj2, otherObj2, false) &&
40+
testNotifyAll(true) &&
41+
testNotifyAll(obj)
42+
}
43+
44+
def testToString(x: Any, expected: String): Boolean =
45+
x.toString() == expected
46+
47+
def testHashCode(x: Any, expected: Int): Boolean =
48+
x.hashCode() == expected
49+
50+
def testIntValue(x: Number, expected: Int): Boolean =
51+
x.intValue() == expected
52+
53+
def testLength(x: CharSequence, expected: Int): Boolean =
54+
x.length() == expected
55+
56+
def testCharAt(x: CharSequence, i: Int, expected: Char): Boolean =
57+
x.charAt(i) == expected
58+
59+
def testEquals(x: Any, y: Any, expected: Boolean): Boolean =
60+
x.asInstanceOf[AnyRef].equals(y) == expected
61+
62+
def testNotifyAll(x: Any): Boolean = {
63+
// This is just to test that the call validates and does not trap
64+
x.asInstanceOf[AnyRef].notifyAll()
65+
true
66+
}
67+
68+
class Test {
69+
override def toString(): String = "Test class"
70+
71+
override def hashCode(): Int = 123
72+
73+
override def equals(that: Any): Boolean =
74+
that.isInstanceOf[Test]
75+
}
76+
77+
class Test2
78+
79+
class CustomNumber() extends Number {
80+
def value(): Int = 789
81+
def intValue(): Int = value()
82+
def longValue(): Long = 789L
83+
def floatValue(): Float = 789.0f
84+
def doubleValue(): Double = 789.0
85+
}
86+
87+
class CustomCharSeq extends CharSequence {
88+
def length(): Int = 54
89+
override def toString(): String = "CustomCharSeq"
90+
def charAt(index: Int): Char = 'A'
91+
def subSequence(start: Int, end: Int): CharSequence = this
92+
}
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package testsuite.core.hijackedclassesupcast
2+
3+
import scala.scalajs.js.annotation._
4+
5+
object HijackedClassesUpcastTest {
6+
def main(): Unit = { val _ = test() }
7+
8+
@JSExportTopLevel("hijackedClassesUpcast")
9+
def test(): Boolean = {
10+
testBoolean(true) &&
11+
testInteger(5) &&
12+
testIntegerNull(null) &&
13+
testString("foo") &&
14+
testStringNull(null) &&
15+
testCharacter('A')
16+
}
17+
18+
def testBoolean(x: Boolean): Boolean = {
19+
val x1 = identity(x)
20+
x1 && {
21+
val x2: Any = x1
22+
x2 match {
23+
case x3: Boolean => x3
24+
case _ => false
25+
}
26+
}
27+
}
28+
29+
def testInteger(x: Int): Boolean = {
30+
val x1 = identity(x)
31+
x1 == 5 && {
32+
val x2: Any = x1
33+
x2 match {
34+
case x3: Int => x3 + 1 == 6
35+
case _ => false
36+
}
37+
}
38+
}
39+
40+
def testIntegerNull(x: Any): Boolean = {
41+
!x.isInstanceOf[Int] &&
42+
!x.isInstanceOf[java.lang.Integer] &&
43+
(x.asInstanceOf[Int] == 0) && {
44+
val x2 = x.asInstanceOf[java.lang.Integer]
45+
x2 == null
46+
}
47+
}
48+
49+
def testString(x: String): Boolean = {
50+
val x1 = identity(x)
51+
x1.length() == 3 && {
52+
val x2: Any = x1
53+
x2 match {
54+
case x3: String => x3.length() == 3
55+
case _ => false
56+
}
57+
}
58+
}
59+
60+
def testStringNull(x: Any): Boolean = {
61+
!x.isInstanceOf[String] && {
62+
val x2 = x.asInstanceOf[String]
63+
x2 == null
64+
}
65+
}
66+
67+
def testCharacter(x: Char): Boolean = {
68+
val x1 = identity(x)
69+
x1 == 'A' && {
70+
val x2: Any = x1
71+
x2 match {
72+
case x3: Char => (x3 + 1).toChar == 'B'
73+
case _ => false
74+
}
75+
}
76+
}
77+
78+
@noinline
79+
def identity[A](x: A): A = x
80+
}

0 commit comments

Comments
 (0)