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

Commit 953adf2

Browse files
authored
Merge pull request #91 from sjrd/fix-hash-codes
Implement `IdentityHashCode` and fix the hashCode of primitives.
2 parents a822347 + 7dc4020 commit 953adf2

11 files changed

+240
-48
lines changed

Diff for: build.sbt

-9
Original file line numberDiff line numberDiff line change
@@ -247,15 +247,6 @@ lazy val IgnoredTestNames: Set[String] = {
247247
"org.scalajs.testsuite.javalib.lang.ObjectTest",
248248
// eqEqJLFloat/eqEqJLDouble failed: java.lang.AssertionError: null
249249
"org.scalajs.testsuite.compiler.RegressionTest",
250-
// hashCode of floats and doubles
251-
"org.scalajs.testsuite.javalib.lang.DoubleTest",
252-
"org.scalajs.testsuite.javalib.lang.FloatTest",
253-
"org.scalajs.testsuite.javalib.util.ArraysTest",
254-
"org.scalajs.testsuite.typedarray.ArraysTest",
255-
// hashCode of bigints and symbols
256-
"org.scalajs.testsuite.javalib.lang.ObjectJSTest",
257-
// Various issues with identityHashCode
258-
"org.scalajs.testsuite.javalib.lang.SystemTest",
259250
// TypeError: WebAssembly objects are opaque
260251
"org.scalajs.testsuite.javalib.lang.SystemJSTest",
261252
// throwablesAreTrueErrors failed: org.junit.ComparisonFailure: expected:<[object [Error]]> but was:<[object [Object]]>

Diff for: test-suite/src/main/scala/testsuite/core/HijackedClassesDispatchTest.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ object HijackedClassesDispatchTest {
2121
testHashCode(54321, 54321) &&
2222
testHashCode("foo", 101574) &&
2323
testHashCode(obj, 123) &&
24-
testHashCode(obj2, 42) &&
24+
testHashCode(obj2, 1) && // first object for which we ask idHashCode
25+
testHashCode(obj2, 1) && // it is also 1 the second time we ask
2526
testHashCode('A', 65) &&
2627
testIntValue(Int.box(5), 5) &&
2728
testIntValue(Long.box(6L), 6) &&

Diff for: wasm/src/main/scala/WebAssemblyLinkerBackend.scala

+5-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ final class WebAssemblyLinkerBackend(
4545
factory.multiple(
4646
factory.instantiateClass(ClassClass, ClassCtor),
4747
factory.instantiateClass(CharBoxClass, CharBoxCtor),
48-
factory.instantiateClass(LongBoxClass, LongBoxCtor)
48+
factory.instantiateClass(LongBoxClass, LongBoxCtor),
49+
50+
// See genIdentityHashCode in HelperFunctions
51+
factory.callMethodStatically(BoxedDoubleClass, hashCodeMethodName),
52+
factory.callMethodStatically(BoxedStringClass, hashCodeMethodName)
4953
)
5054
}
5155

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ object EmbeddedConstants {
2121
final val JSValueTypeString = 2
2222
final val JSValueTypeNumber = 3
2323
final val JSValueTypeUndefined = 4
24-
final val JSValueTypeOther = 5
24+
final val JSValueTypeBigInt = 5
25+
final val JSValueTypeSymbol = 6
26+
final val JSValueTypeOther = 7
2527

2628
// Values for `typeData.kind`
2729

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

+132
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ object HelperFunctions {
3333
genNewArrayOfThisClass()
3434
genAnyGetClass()
3535
genNewArrayObject()
36+
genIdentityHashCode()
3637
}
3738

3839
private def genStringLiteral()(implicit ctx: WasmContext): Unit = {
@@ -1383,6 +1384,137 @@ object HelperFunctions {
13831384
fctx.buildAndAddToContext()
13841385
}
13851386

1387+
/** `identityHashCode`: `anyref -> i32`.
1388+
*
1389+
* This is the implementation of `IdentityHashCode`. It is also used to compute the `hashCode()`
1390+
* of primitive values when dispatch is required (i.e., when the receiver type is not known to be
1391+
* a specific primitive or hijacked class), so it must be consistent with the implementations of
1392+
* `hashCode()` in hijacked classes.
1393+
*
1394+
* For `String` and `Double`, we actually call the hijacked class methods, as they are a bit
1395+
* involved. For `Boolean` and `Void`, we hard-code a copy here.
1396+
*/
1397+
def genIdentityHashCode()(implicit ctx: WasmContext): Unit = {
1398+
import IRTrees.MemberNamespace.Public
1399+
import SpecialNames.hashCodeMethodName
1400+
import WasmTypeName._
1401+
import WasmFieldIdx.typeData._
1402+
1403+
// A global exclusively used by this function
1404+
ctx.addGlobal(
1405+
WasmGlobal(
1406+
WasmGlobalName.lastIDHashCode,
1407+
WasmInt32,
1408+
WasmExpr(List(I32_CONST(0))),
1409+
isMutable = true
1410+
)
1411+
)
1412+
1413+
val fctx = WasmFunctionContext(
1414+
WasmFunctionName.identityHashCode,
1415+
List("obj" -> WasmRefType.anyref),
1416+
List(WasmInt32)
1417+
)
1418+
1419+
val List(objParam) = fctx.paramIndices
1420+
1421+
import fctx.instrs
1422+
1423+
val objNonNullLocal = fctx.addLocal("objNonNull", WasmRefType.any)
1424+
val resultLocal = fctx.addLocal("result", WasmInt32)
1425+
1426+
// If `obj` is `null`, return 0 (by spec)
1427+
fctx.block(WasmRefType.any) { nonNullLabel =>
1428+
instrs += LOCAL_GET(objParam)
1429+
instrs += BR_ON_NON_NULL(nonNullLabel)
1430+
instrs += I32_CONST(0)
1431+
instrs += RETURN
1432+
}
1433+
instrs += LOCAL_TEE(objNonNullLocal)
1434+
1435+
// If `obj` is one of our objects, skip all the jsValueType tests
1436+
instrs += REF_TEST(WasmRefType(WasmHeapType.ObjectType))
1437+
instrs += I32_EQZ
1438+
fctx.ifThen() {
1439+
fctx.switch() { () =>
1440+
instrs += LOCAL_GET(objNonNullLocal)
1441+
instrs += CALL(WasmFunctionName.jsValueType)
1442+
}(
1443+
List(JSValueTypeFalse) -> { () =>
1444+
instrs += I32_CONST(1237) // specified by jl.Boolean.hashCode()
1445+
instrs += RETURN
1446+
},
1447+
List(JSValueTypeTrue) -> { () =>
1448+
instrs += I32_CONST(1231) // specified by jl.Boolean.hashCode()
1449+
instrs += RETURN
1450+
},
1451+
List(JSValueTypeString) -> { () =>
1452+
instrs += LOCAL_GET(objNonNullLocal)
1453+
instrs += CALL(WasmFunctionName(Public, IRNames.BoxedStringClass, hashCodeMethodName))
1454+
instrs += RETURN
1455+
},
1456+
List(JSValueTypeNumber) -> { () =>
1457+
instrs += LOCAL_GET(objNonNullLocal)
1458+
instrs += CALL(WasmFunctionName.unbox(IRTypes.DoubleRef))
1459+
instrs += CALL(WasmFunctionName(Public, IRNames.BoxedDoubleClass, hashCodeMethodName))
1460+
instrs += RETURN
1461+
},
1462+
List(JSValueTypeUndefined) -> { () =>
1463+
instrs += I32_CONST(0) // specified by jl.Void.hashCode(), Scala.js only
1464+
instrs += RETURN
1465+
},
1466+
List(JSValueTypeBigInt) -> { () =>
1467+
instrs += LOCAL_GET(objNonNullLocal)
1468+
instrs += CALL(WasmFunctionName.bigintHashCode)
1469+
instrs += RETURN
1470+
},
1471+
List(JSValueTypeSymbol) -> { () =>
1472+
fctx.block() { descriptionIsNullLabel =>
1473+
instrs += LOCAL_GET(objNonNullLocal)
1474+
instrs += CALL(WasmFunctionName.symbolDescription)
1475+
instrs += BR_ON_NULL(descriptionIsNullLabel)
1476+
instrs += CALL(WasmFunctionName(Public, IRNames.BoxedStringClass, hashCodeMethodName))
1477+
instrs += RETURN
1478+
}
1479+
instrs += I32_CONST(0)
1480+
instrs += RETURN
1481+
}
1482+
) { () =>
1483+
// JSValueTypeOther -- fall through to using idHashCodeMap
1484+
()
1485+
}
1486+
}
1487+
1488+
// If we get here, use the idHashCodeMap
1489+
1490+
// Read the existing idHashCode, if one exists
1491+
instrs += GLOBAL_GET(WasmGlobalName.idHashCodeMap)
1492+
instrs += LOCAL_GET(objNonNullLocal)
1493+
instrs += CALL(WasmFunctionName.idHashCodeGet)
1494+
instrs += LOCAL_TEE(resultLocal)
1495+
1496+
// If it is 0, there was no recorded idHashCode yet; allocate a new one
1497+
instrs += I32_EQZ
1498+
fctx.ifThen() {
1499+
// Allocate a new idHashCode
1500+
instrs += GLOBAL_GET(WasmGlobalName.lastIDHashCode)
1501+
instrs += I32_CONST(1)
1502+
instrs += I32_ADD
1503+
instrs += LOCAL_TEE(resultLocal)
1504+
instrs += GLOBAL_SET(WasmGlobalName.lastIDHashCode)
1505+
1506+
// Store it for next time
1507+
instrs += GLOBAL_GET(WasmGlobalName.idHashCodeMap)
1508+
instrs += LOCAL_GET(objNonNullLocal)
1509+
instrs += LOCAL_GET(resultLocal)
1510+
instrs += CALL(WasmFunctionName.idHashCodeSet)
1511+
}
1512+
1513+
instrs += LOCAL_GET(resultLocal)
1514+
1515+
fctx.buildAndAddToContext()
1516+
}
1517+
13861518
/** Generate type inclusion test for interfaces.
13871519
*
13881520
* The expression `isInstanceOf[<interface>]` will be compiled to a CALL to the function

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

+21-21
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,15 @@ object LoaderContent {
1111

1212
private def stringContent: String = {
1313
raw"""
14-
// Specified by java.lang.String.hashCode()
15-
function stringHashCode(s) {
14+
// This implementation follows no particular specification, but is the same as the JS backend.
15+
// It happens to coincide with java.lang.Long.hashCode() for common values.
16+
function bigintHashCode(x) {
1617
var res = 0;
17-
var mul = 1;
18-
var i = (s.length - 1) | 0;
19-
while ((i >= 0)) {
20-
res = ((res + Math.imul(s.charCodeAt(i), mul)) | 0);
21-
mul = Math.imul(31, mul);
22-
i = (i - 1) | 0;
18+
if (x < 0n)
19+
x = ~x;
20+
while (x !== 0n) {
21+
res ^= Number(BigInt.asIntN(32, x));
22+
x >>= 32n;
2323
}
2424
return res;
2525
}
@@ -136,22 +136,21 @@ const scalaJSHelpers = {
136136
return x | 0; // JSValueTypeFalse or JSValueTypeTrue
137137
if (typeof x === 'undefined')
138138
return $JSValueTypeUndefined;
139+
if (typeof x === 'bigint')
140+
return $JSValueTypeBigInt;
141+
if (typeof x === 'symbol')
142+
return $JSValueTypeSymbol;
139143
return $JSValueTypeOther;
140144
},
141145

142-
// Hash code, because it is overridden in all hijacked classes
143-
// Specified by the hashCode() method of the corresponding hijacked classes
144-
jsValueHashCode: (x) => {
145-
if (typeof x === 'number')
146-
return x | 0; // TODO make this compliant for floats
147-
if (typeof x === 'string')
148-
return stringHashCode(x);
149-
if (typeof x === 'boolean')
150-
return x ? 1231 : 1237;
151-
if (typeof x === 'undefined')
152-
return 0;
153-
return 42; // for any JS object
146+
// Identity hash code
147+
bigintHashCode: bigintHashCode,
148+
symbolDescription: (x) => {
149+
var desc = x.description;
150+
return (desc === void 0) ? null : desc;
154151
},
152+
idHashCodeGet: (map, obj) => map.get(obj) | 0, // undefined becomes 0
153+
idHashCodeSet: (map, obj, value) => map.set(obj, value),
155154

156155
// JS interop
157156
jsGlobalRefGet: (globalRefName) => (new Function("return " + globalRefName))(),
@@ -280,8 +279,9 @@ const scalaJSHelpers = {
280279
}
281280

282281
export async function load(wasmFileURL, importedModules) {
282+
const myScalaJSHelpers = { ...scalaJSHelpers, idHashCodeMap: new WeakMap() };
283283
const importsObj = {
284-
"__scalaJSHelpers": scalaJSHelpers,
284+
"__scalaJSHelpers": myScalaJSHelpers,
285285
"__scalaJSImports": importedModules,
286286
};
287287
const resolvedURL = new URL(wasmFileURL, import.meta.url);

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

+2
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,6 @@ object SpecialNames {
2121
val JSExceptionClass = ClassName("scala.scalajs.js.JavaScriptException")
2222
val JSExceptionCtor = MethodName.constructor(List(ClassRef(ObjectClass)))
2323
val JSExceptionField = FieldName(JSExceptionClass, SimpleFieldName("exception"))
24+
25+
val hashCodeMethodName = MethodName("hashCode", Nil, IntRef)
2426
}

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

+29-13
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ object WasmExpressionBuilder {
4040
private val ObjectRef = IRTypes.ClassRef(IRNames.ObjectClass)
4141
private val BoxedStringRef = IRTypes.ClassRef(IRNames.BoxedStringClass)
4242
private val toStringMethodName = IRNames.MethodName("toString", Nil, BoxedStringRef)
43-
private val hashCodeMethodName = IRNames.MethodName("hashCode", Nil, IRTypes.IntRef)
4443
private val equalsMethodName = IRNames.MethodName("equals", List(ObjectRef), IRTypes.BooleanRef)
4544
private val compareToMethodName = IRNames.MethodName("compareTo", List(ObjectRef), IRTypes.IntRef)
4645

@@ -310,6 +309,12 @@ private class WasmExpressionBuilder private (
310309
}
311310
}
312311

312+
/** Generates the code an `Apply` call where the receiver's type is not statically known to be a
313+
* primitive or hijacked class.
314+
*
315+
* In that case, there is always at least a vtable/itable-based dispatch. It may also contain
316+
* primitive-based dispatch if the receiver's type is an ancestor of a hijacked class.
317+
*/
313318
private def genApplyNonPrim(t: IRTrees.Apply): IRTypes.Type = {
314319
implicit val pos: Position = t.pos
315320

@@ -378,7 +383,11 @@ private class WasmExpressionBuilder private (
378383
genArgs(t.args, t.method.name)
379384
genTableDispatch(receiverClassInfo, t.method.name, receiverLocalForDispatch)
380385
} else {
381-
/* Hijacked class dispatch codegen
386+
/* Here the receiver's type is an ancestor of a hijacked class (or `any`,
387+
* which is treated as `jl.Object`).
388+
*
389+
* We must emit additional dispatch for the possible primitive values.
390+
*
382391
* The overall structure of the generated code is as follows:
383392
*
384393
* block resultType $done
@@ -443,7 +452,17 @@ private class WasmExpressionBuilder private (
443452
argsLocals
444453
} // end block labelNotOurObject
445454

446-
// Now we have a value that is not one of our objects; the (ref any) is still on the stack
455+
/* Now we have a value that is not one of our objects, so it must be
456+
* a JavaScript value whose representative class extends/implements the
457+
* receiver class. It may be a primitive instance of a hijacked class, or
458+
* any other value (whose representative class is therefore `jl.Object`).
459+
*
460+
* It is also *not* `char` or `long`, since those would reach
461+
* `genApplyNonPrim` in their boxed form, and therefore they are
462+
* "ourObject".
463+
*
464+
* The (ref any) is still on the stack.
465+
*/
447466

448467
if (t.method.name == toStringMethodName) {
449468
// By spec, toString() is special
@@ -499,15 +518,16 @@ private class WasmExpressionBuilder private (
499518
}
500519
} else {
501520
/* It must be a method of j.l.Object and it can be any value.
502-
* hashCode() and equals() are overridden in all hijacked classes; we
503-
* use dedicated JavaScript helpers for those.
521+
* hashCode() and equals() are overridden in all hijacked classes.
522+
* We use `identityHashCode` for `hashCode` and `Object.is` for `equals`,
523+
* as they coincide with the respective specifications (on purpose).
504524
* The other methods are never overridden and can be statically
505525
* resolved to j.l.Object.
506526
*/
507527
pushArgs(argsLocals)
508528
t.method.name match {
509-
case `hashCodeMethodName` =>
510-
instrs += CALL(WasmFunctionName.jsValueHashCode)
529+
case SpecialNames.hashCodeMethodName =>
530+
instrs += CALL(WasmFunctionName.identityHashCode)
511531
case `equalsMethodName` =>
512532
instrs += CALL(WasmFunctionName.is)
513533
case _ =>
@@ -1759,13 +1779,9 @@ private class WasmExpressionBuilder private (
17591779
}
17601780

17611781
private def genIdentityHashCode(tree: IRTrees.IdentityHashCode): IRTypes.Type = {
1762-
/* TODO We should allocate ID hash codes and store them. We will probably
1763-
* have to store them as an additional field in all objects, together with
1764-
* the vtable and itable pointers.
1765-
*/
1782+
// TODO Avoid dispatch when we know a more precise type than any
17661783
genTree(tree.expr, IRTypes.AnyType)
1767-
instrs += DROP
1768-
instrs += I32_CONST(42)
1784+
instrs += CALL(WasmFunctionName.identityHashCode)
17691785

17701786
IRTypes.IntType
17711787
}

Diff for: wasm/src/main/scala/wasm4s/Names.scala

+11-1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ object Names {
7676

7777
val arrayClassITable: WasmGlobalName =
7878
new WasmGlobalName("itable.A")
79+
80+
val lastIDHashCode: WasmGlobalName =
81+
new WasmGlobalName("lastIDHashCode")
82+
83+
val idHashCodeMap: WasmGlobalName =
84+
new WasmGlobalName("idHashCodeMap")
7985
}
8086

8187
// final case class WasmGlobalName private (val name: String) extends WasmName(name) {
@@ -181,7 +187,10 @@ object Names {
181187
val isString = helper("isString")
182188

183189
val jsValueType = helper("jsValueType")
184-
val jsValueHashCode = helper("jsValueHashCode")
190+
val bigintHashCode = helper("bigintHashCode")
191+
val symbolDescription = helper("symbolDescription")
192+
val idHashCodeGet = helper("idHashCodeGet")
193+
val idHashCodeSet = helper("idHashCodeSet")
185194

186195
val jsGlobalRefGet = helper("jsGlobalRefGet")
187196
val jsGlobalRefSet = helper("jsGlobalRefSet")
@@ -268,6 +277,7 @@ object Names {
268277
val newArrayOfThisClass = helper("newArrayOfThisClass")
269278
val anyGetClass = helper("anyGetClass")
270279
val newArrayObject = helper("newArrayObject")
280+
val identityHashCode = helper("identityHashCode")
271281
}
272282

273283
final case class WasmFieldName private (override private[wasm4s] val name: String)

0 commit comments

Comments
 (0)