This repository has been archived by the owner on Jul 12, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #13 from sjrd/upcast-downcast-hijacked-classes
Fix #12: Implement the behavior of hijacked classes.
- Loading branch information
Showing
22 changed files
with
1,435 additions
and
227 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
93 changes: 93 additions & 0 deletions
93
test-suite/src/main/scala/testsuite/core/HijackedClassesDispatchTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
80 changes: 80 additions & 0 deletions
80
test-suite/src/main/scala/testsuite/core/HijackedClassesUpcastTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.