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

Commit 4e46099

Browse files
committed
Implement method dispatch for hijacked classes.
When the type of the receiver of an `Apply` is an ancestor of any hijacked class, it might contain a JS value instead of one of our own objects. In that case, we must perform a more elaborate dispatch that tests whether we have a true object, or whether we must dispatch to a hijacked class method. This change also surfaces that the methods of `j.l.Object` can receive an arbitrary non-null value, i.e., a `(ref any)`. This has to be reflected in vtables and itables, unfortunately.
1 parent 9908c04 commit 4e46099

File tree

12 files changed

+469
-65
lines changed

12 files changed

+469
-65
lines changed

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

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ 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.hijackedclassesdispatch.HijackedClassesDispatchTest", "hijackedClassesDispatch"),
1213
TestSuite("testsuite.core.hijackedclassesmono.HijackedClassesMonoTest", "hijackedClassesMono"),
1314
TestSuite("testsuite.core.hijackedclassesupcast.HijackedClassesUpcastTest", "hijackedClassesUpcast"),
1415
TestSuite("testsuite.core.tostring.ToStringTest", "toStringConversions")

Diff for: loader.mjs

+25
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
import { readFileSync } from "node:fs";
22

3+
function stringHashCode(s) {
4+
var res = 0;
5+
var mul = 1;
6+
var i = (s.length - 1) | 0;
7+
while ((i >= 0)) {
8+
res = ((res + Math.imul(s.charCodeAt(i), mul)) | 0);
9+
mul = Math.imul(31, mul);
10+
i = (i - 1) | 0;
11+
}
12+
return res;
13+
}
14+
315
const scalaJSHelpers = {
416
// BinaryOp.===
517
is: Object.is,
@@ -51,6 +63,19 @@ const scalaJSHelpers = {
5163
doubleToString: (d) => "" + d,
5264
stringConcat: (x, y) => ("" + x) + y, // the added "" is for the case where x === y === null
5365
isString: (x) => typeof x === 'string',
66+
67+
// Hash code, because it is overridden in all hijacked classes
68+
jsValueHashCode: (x) => {
69+
if (typeof x === 'number')
70+
return x | 0; // TODO make this compliant for floats
71+
if (typeof x === 'string')
72+
return stringHashCode(x);
73+
if (typeof x === 'boolean')
74+
return x ? 1231 : 1237;
75+
if (typeof x === 'undefined')
76+
return 0;
77+
return 42; // for any JS object
78+
},
5479
}
5580

5681
export async function load(wasmFileName) {
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+
}

Diff for: wasm/src/main/scala/converters/WasmBinaryWriter.scala

+3
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,9 @@ final class WasmBinaryWriter(module: WasmModule) {
266266
case GlobalIdx(value) => writeGlobalIdx(buf, value)
267267
case HeapType(value) => writeHeapType(buf, value)
268268
case StructFieldIdx(value) => buf.u32(value)
269+
270+
case CastFlags(nullable1, nullable2) =>
271+
buf.byte(((if (nullable1) 1 else 0) | (if (nullable2) 2 else 0)).toByte)
269272
}
270273
}
271274
}

Diff for: wasm/src/main/scala/converters/WasmTextWriter.scala

+28-1
Original file line numberDiff line numberDiff line change
@@ -217,13 +217,22 @@ class WasmTextWriter {
217217
case WasmImmediate.BlockType.ValueType(optTy) =>
218218
optTy.fold("") { ty => s"(result ${ty.show})" }
219219
case WasmImmediate.LabelIdx(i) => s"$$${i.toString}" // `loop 0` seems to be invalid
220+
case i: WasmImmediate.CastFlags =>
221+
throw new UnsupportedOperationException(s"CastFlags $i must be handled directly in the instruction $instr")
220222
case _ =>
221223
println(i)
222224
???
223225
}
224226
b.appendElement(str)
225227
}
226228

229+
private def writeRefTypeImmediate(i: WasmImmediate.HeapType, nullable: Boolean)(implicit b: WatBuilder): Unit = {
230+
if (nullable)
231+
b.appendElement(s"(ref null ${i.value.show})")
232+
else
233+
b.appendElement(s"(ref ${i.value.show})")
234+
}
235+
227236
private def writeInstr(instr: WasmInstr)(implicit b: WatBuilder): Unit = {
228237
instr match {
229238
case END | ELSE | _: CATCH => b.deindent()
@@ -237,7 +246,25 @@ class WasmTextWriter {
237246
case _ =>
238247
()
239248
}
240-
instr.immediates.foreach { i => writeImmediate(i, instr) }
249+
250+
def writeBrOnCastImmediates(
251+
castFlags: WasmImmediate.CastFlags, label: WasmImmediate.LabelIdx,
252+
from: WasmImmediate.HeapType, to: WasmImmediate.HeapType
253+
): Unit = {
254+
writeImmediate(label, instr)
255+
writeRefTypeImmediate(from, castFlags.nullable1)
256+
writeRefTypeImmediate(to, castFlags.nullable2)
257+
}
258+
259+
instr match {
260+
case BR_ON_CAST(castFlags, label, from, to) =>
261+
writeBrOnCastImmediates(castFlags, label, from, to)
262+
case BR_ON_CAST_FAIL(castFlags, label, from, to) =>
263+
writeBrOnCastImmediates(castFlags, label, from, to)
264+
case _ =>
265+
instr.immediates.foreach { i => writeImmediate(i, instr) }
266+
}
267+
241268
instr match {
242269
case _: BLOCK | _: LOOP | _: IF | ELSE | _: CATCH | _: TRY => b.indent()
243270
case _ => ()

Diff for: wasm/src/main/scala/ir2wasm/Preprocessor.scala

+1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ object Preprocessor {
2828
collectMethods(clazz)
2929
case ClassKind.JSClass | ClassKind.JSModuleClass | ClassKind.NativeJSModuleClass |
3030
ClassKind.AbstractJSType | ClassKind.NativeJSClass =>
31+
println(s"${clazz.kind} ${clazz.fullName}")
3132
???
3233
}
3334
}

Diff for: wasm/src/main/scala/ir2wasm/TypeTransformer.scala

+5-5
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import wasm4s._
1010
object TypeTransformer {
1111

1212
val makeReceiverType: Types.WasmType =
13-
Types.WasmRefNullType(Types.WasmHeapType.ObjectType)
13+
Types.WasmRefType.any
1414

1515
def transformFunctionType(
1616
// clazz: WasmContext.WasmClassInfo,
@@ -62,12 +62,12 @@ object TypeTransformer {
6262
className match {
6363
case _ =>
6464
val info = ctx.getClassInfo(clazz.className)
65-
if (info.isInterface)
66-
Types.WasmRefNullType(Types.WasmHeapType.ObjectType)
67-
else if (info.kind == ClassKind.HijackedClass)
65+
if (info.isAncestorOfHijackedClass)
6866
Types.WasmAnyRef
67+
else if (info.isInterface)
68+
Types.WasmRefNullType(Types.WasmHeapType.ObjectType)
6969
else
70-
Types.WasmRefType(
70+
Types.WasmRefNullType(
7171
Types.WasmHeapType.Type(Names.WasmTypeName.WasmStructTypeName(className))
7272
)
7373
}

Diff for: wasm/src/main/scala/ir2wasm/WasmBuilder.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -396,15 +396,15 @@ class WasmBuilder {
396396
)(implicit ctx: WasmContext): WasmFunction = {
397397
val receiver = WasmLocal(
398398
Names.WasmLocalName.receiver,
399-
// Receiver type for non-constructor methods needs to be Object type because params are invariant
399+
// Receiver type for non-constructor methods needs to be `(ref any)` because params are invariant
400400
// Otherwise, vtable can't be a subtype of the supertype's subtype
401401
// Constructor can use the exact type because it won't be registered to vtables.
402402
if (clazz.kind == ClassKind.HijackedClass)
403403
transformType(IRTypes.BoxedClassToPrimType(clazz.name.name))
404404
else if (method.flags.namespace.isConstructor)
405405
WasmRefNullType(WasmHeapType.Type(WasmTypeName.WasmStructTypeName(clazz.name.name)))
406406
else
407-
WasmRefNullType(WasmHeapType.ObjectType),
407+
WasmRefType.any,
408408
isParameter = true
409409
)
410410
val paramTys = receiver.typ +:

0 commit comments

Comments
 (0)