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

Commit 716d7d4

Browse files
committed
Fix static type tests for ancestors of hijacked classes.
This commit fixes the result of `x.isInstanceOf[C]` where `x` is a primitive and `C` is an ancestor of the hijacked class for `x`. For `jl.Number`, which is the only non-`Object` class that is an ancestor of a hijacked class, we add a special case in the codegen. For interfaces, we add generic code in their `genInstanceTest` function, based on their `specialInstanceTypes`. This means we have a second place where we need to compute `specialInstanceTypes`. We move it to the preprocessor, and also use it to generically compute `isAncestorOfHijackedClass`. This removes the hard-code set that we had before. Fix #74. Fix #84. Fix #92.
1 parent 11dcef3 commit 716d7d4

File tree

6 files changed

+128
-56
lines changed

6 files changed

+128
-56
lines changed

Diff for: build.sbt

-6
Original file line numberDiff line numberDiff line change
@@ -228,8 +228,6 @@ lazy val IgnoredTestNames: Set[String] = {
228228
Set(
229229
// reflective call: should be throw an exception when reflective proxy not found
230230
"org.scalajs.testsuite.compiler.WasPublicBeforeTyperTestScala2",
231-
// javaLangNumber failed: java.lang.AssertionError: 1, class java.lang.Number expected:<true> but was:<false>
232-
"org.scalajs.testsuite.compiler.RuntimeTypeTestsTest",
233231
// Various run-time errors and JS exceptions
234232
"org.scalajs.testsuite.compiler.InteroperabilityTest",
235233
"org.scalajs.testsuite.compiler.RegressionJSTest",
@@ -242,17 +240,13 @@ lazy val IgnoredTestNames: Set[String] = {
242240
"org.scalajs.testsuite.compiler.ReflectionTest",
243241
"org.scalajs.testsuite.compiler.RuntimeTypeTestsJSTest",
244242
"org.scalajs.testsuite.jsinterop.ModulesTest",
245-
// eqEqJLFloat/eqEqJLDouble failed: java.lang.AssertionError: null
246-
"org.scalajs.testsuite.compiler.RegressionTest",
247243
// TypeError: WebAssembly objects are opaque
248244
"org.scalajs.testsuite.javalib.lang.SystemJSTest",
249245
// throwablesAreTrueErrors failed: org.junit.ComparisonFailure: expected:<[object [Error]]> but was:<[object [Object]]>
250246
// throwablesAreJSErrors failed: java.lang.AssertionError: null
251247
"org.scalajs.testsuite.javalib.lang.ThrowableJSTest",
252248
// keepBreakToLabelWithinFinallyBlock_Issue2689 failed: java.lang.AssertionError: expected:<2> but was:<1>
253249
"org.scalajs.testsuite.compiler.OptimizerTest",
254-
// nonUnitBoxedPrimitiveValuesAreSerializable failed: java.lang.AssertionError: Boolean
255-
"org.scalajs.testsuite.javalib.io.SerializableTest",
256250
// No support for stack traces
257251
"org.scalajs.testsuite.library.StackTraceTest",
258252
)

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

+50-5
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,9 @@ object HelperFunctions {
690690
*
691691
* Since computing `jsValueType` is somewhat expensive, we first test
692692
* whether `specialInstanceTypes != 0` before calling `jsValueType`.
693+
*
694+
* There is a more elaborated concrete example of this algorithm in
695+
* `genInstanceTest`.
693696
*/
694697
instrs += LOCAL_GET(typeDataParam)
695698
instrs += STRUCT_GET(WasmStructTypeName.typeData, specialInstanceTypesIdx)
@@ -1636,6 +1639,8 @@ object HelperFunctions {
16361639
def genInstanceTest(clazz: LinkedClass)(implicit ctx: WasmContext): Unit = {
16371640
assert(clazz.kind == ClassKind.Interface)
16381641

1642+
val classInfo = ctx.getClassInfo(clazz.className)
1643+
16391644
val fctx = WasmFunctionContext(
16401645
Names.WasmFunctionName.instanceTest(clazz.name.name),
16411646
List("expr" -> WasmRefType.anyref),
@@ -1645,9 +1650,9 @@ object HelperFunctions {
16451650

16461651
import fctx.instrs
16471652

1648-
val itables = fctx.addSyntheticLocal(
1649-
Types.WasmRefType.nullable(WasmArrayType.itables.name)
1650-
)
1653+
val itables = fctx.addLocal("itables", WasmRefType.nullable(WasmArrayType.itables.name))
1654+
val exprNonNullLocal = fctx.addLocal("exprNonNull", WasmRefType.any)
1655+
16511656
val itableIdx = ctx.getItableIdx(clazz.name.name)
16521657
fctx.block(WasmRefType.anyref) { testFail =>
16531658
// if expr is not an instance of Object, return false
@@ -1677,8 +1682,48 @@ object HelperFunctions {
16771682
instrs += I32_CONST(1)
16781683
instrs += RETURN
16791684
} // test fail
1680-
instrs += DROP
1681-
instrs += I32_CONST(0) // false
1685+
1686+
if (classInfo.isAncestorOfHijackedClass) {
1687+
/* It could be a hijacked class instance that implements this interface.
1688+
* Test whether `jsValueType(expr)` is in the `specialInstanceTypes` bitset.
1689+
* In other words, return `((1 << jsValueType(expr)) & specialInstanceTypes) != 0`.
1690+
*
1691+
* For example, if this class is `Comparable`,
1692+
* `specialInstanceTypes == 0b00001111`, since `jl.Boolean`, `jl.String`
1693+
* and `jl.Double` implement `Comparable`, but `jl.Void` does not.
1694+
* If `expr` is a `number`, `jsValueType(expr) == 3`. We then test whether
1695+
* `(1 << 3) & 0b00001111 != 0`, which is true because `(1 << 3) == 0b00001000`.
1696+
* If `expr` is `undefined`, it would be `(1 << 4) == 0b00010000`, which
1697+
* would give `false`.
1698+
*/
1699+
val anyRefToVoidSig =
1700+
WasmFunctionSignature(List(WasmRefType.anyref), Nil)
1701+
1702+
fctx.block(anyRefToVoidSig) { isNullLabel =>
1703+
// exprNonNull := expr; branch to isNullLabel if it is null
1704+
instrs += BR_ON_NULL(isNullLabel)
1705+
instrs += LOCAL_SET(exprNonNullLocal)
1706+
1707+
// Load 1 << jsValueType(expr)
1708+
instrs += I32_CONST(1)
1709+
instrs += LOCAL_GET(exprNonNullLocal)
1710+
instrs += CALL(WasmFunctionName.jsValueType)
1711+
instrs += I32_SHL
1712+
1713+
// return (... & specialInstanceTypes) != 0
1714+
instrs += I32_CONST(classInfo.specialInstanceTypes)
1715+
instrs += I32_AND
1716+
instrs += I32_CONST(0)
1717+
instrs += I32_NE
1718+
instrs += RETURN
1719+
}
1720+
1721+
instrs += I32_CONST(0) // false
1722+
} else {
1723+
instrs += DROP
1724+
instrs += I32_CONST(0) // false
1725+
}
1726+
16821727
fctx.buildAndAddToContext()
16831728
}
16841729

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

+21
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import org.scalajs.ir.Traversers
1010

1111
import org.scalajs.linker.standard.LinkedClass
1212

13+
import EmbeddedConstants._
1314
import WasmContext._
1415

1516
object Preprocessor {
@@ -101,6 +102,26 @@ object Preprocessor {
101102
)
102103
)
103104

105+
// Update specialInstanceTypes for ancestors of hijacked classes
106+
if (clazz.kind == ClassKind.HijackedClass) {
107+
def addSpecialInstanceTypeOnAllAncestors(jsValueType: Int): Unit =
108+
clazz.ancestors.foreach(ctx.getClassInfo(_).addSpecialInstanceType(jsValueType))
109+
110+
clazz.className match {
111+
case IRNames.BoxedBooleanClass =>
112+
addSpecialInstanceTypeOnAllAncestors(JSValueTypeFalse)
113+
addSpecialInstanceTypeOnAllAncestors(JSValueTypeTrue)
114+
case IRNames.BoxedStringClass =>
115+
addSpecialInstanceTypeOnAllAncestors(JSValueTypeString)
116+
case IRNames.BoxedDoubleClass =>
117+
addSpecialInstanceTypeOnAllAncestors(JSValueTypeNumber)
118+
case IRNames.BoxedUnitClass =>
119+
addSpecialInstanceTypeOnAllAncestors(JSValueTypeUndefined)
120+
case _ =>
121+
()
122+
}
123+
}
124+
104125
/* Work around https://github.com/scala-js/scala-js/issues/4972
105126
* Manually mark all ancestors of instantiated classes as having instances.
106127
*/

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

+2-30
Original file line numberDiff line numberDiff line change
@@ -158,35 +158,7 @@ class WasmBuilder {
158158
val className = clazz.className
159159
val classInfo = ctx.getClassInfo(className)
160160

161-
/* See the `isInstance` helper. `specialInstanceTypes` is a bitset of the
162-
* `jsValueType`s corresponding to hijacked classes that extend this class.
163-
* For example, if this class is `Comparable`, we want the bitset to contain
164-
* the values for `boolean`, `string` and `number` (but not `undefined`),
165-
* because `jl.Boolean`, `jl.String` and `jl.Double` implement `Comparable`.
166-
*
167-
* When testing whether a `value` is a `Comparable`, `isInstance` will
168-
* compute the `jsValueType(value)` and test whether it is part of the bit
169-
* set, using `((1 << jsValueType(value)) & specialInstanceTypes) != 0`.
170-
*/
171-
val specialInstanceTypes = {
172-
if (!classInfo.isAncestorOfHijackedClass) {
173-
// fast path
174-
0
175-
} else {
176-
var bits = 0
177-
if (ctx.getClassInfo(IRNames.BoxedBooleanClass).ancestors.contains(className))
178-
bits |= ((1 << JSValueTypeFalse) | (1 << JSValueTypeTrue))
179-
if (ctx.getClassInfo(IRNames.BoxedStringClass).ancestors.contains(className))
180-
bits |= (1 << JSValueTypeString)
181-
if (ctx.getClassInfo(IRNames.BoxedDoubleClass).ancestors.contains(className))
182-
bits |= (1 << JSValueTypeNumber)
183-
if (ctx.getClassInfo(IRNames.BoxedUnitClass).ancestors.contains(className))
184-
bits |= (1 << JSValueTypeUndefined)
185-
bits
186-
}
187-
}
188-
189-
val kind = clazz.className match {
161+
val kind = className match {
190162
case IRNames.ObjectClass => KindObject
191163
case IRNames.BoxedUnitClass => KindBoxedUnit
192164
case IRNames.BoxedBooleanClass => KindBoxedBoolean
@@ -209,7 +181,7 @@ class WasmBuilder {
209181

210182
genTypeDataFieldValues(
211183
kind,
212-
specialInstanceTypes,
184+
classInfo.specialInstanceTypes,
213185
IRTypes.ClassRef(clazz.className),
214186
vtableElems
215187
)

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

+14
Original file line numberDiff line numberDiff line change
@@ -1300,6 +1300,20 @@ private class WasmExpressionBuilder private (
13001300
instrs += I32_CONST(1)
13011301
instrs += I32_XOR
13021302

1303+
case IRTypes.ClassType(JLNumberClass) =>
1304+
/* Special case: the only non-Object *class* that is an ancestor of a
1305+
* hijacked class. We need to accept `number` primitives here.
1306+
*/
1307+
val tempLocal = fctx.addSyntheticLocal(Types.WasmRefType.anyref)
1308+
instrs += LOCAL_TEE(tempLocal)
1309+
instrs += REF_TEST(Types.WasmRefType(WasmStructTypeName.forClass(JLNumberClass)))
1310+
fctx.ifThenElse(Types.WasmInt32) {
1311+
instrs += I32_CONST(1)
1312+
} {
1313+
instrs += LOCAL_GET(tempLocal)
1314+
instrs += CALL(WasmFunctionName.typeTest(IRTypes.DoubleRef))
1315+
}
1316+
13031317
case IRTypes.ClassType(testClassName) =>
13041318
IRTypes.BoxedClassToPrimType.get(testClassName) match {
13051319
case Some(primType) =>

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

+41-15
Original file line numberDiff line numberDiff line change
@@ -702,20 +702,6 @@ class WasmContext(val module: WasmModule) extends TypeDefinableWasmContext {
702702
object WasmContext {
703703
private val classFieldOffset = 2 // vtable, itables
704704

705-
private val AncestorsOfHijackedClasses: Set[IRNames.ClassName] = {
706-
// We hard-code this for now, but ideally we should derive it
707-
IRNames.HijackedClasses ++
708-
Set(
709-
IRNames.ObjectClass,
710-
IRNames.SerializableClass,
711-
IRNames.ClassName("java.lang.CharSequence"),
712-
IRNames.ClassName("java.lang.Comparable"),
713-
IRNames.ClassName("java.lang.Number"),
714-
IRNames.ClassName("java.lang.constant.Constable"),
715-
IRNames.ClassName("java.lang.constant.ConstantDesc")
716-
)
717-
}
718-
719705
final class WasmClassInfo(
720706
val name: IRNames.ClassName,
721707
val kind: ClassKind,
@@ -741,7 +727,47 @@ object WasmContext {
741727

742728
def hasInstances: Boolean = _hasInstances
743729

744-
def isAncestorOfHijackedClass: Boolean = AncestorsOfHijackedClasses.contains(name)
730+
private var _specialInstanceTypes: Int = 0
731+
732+
def addSpecialInstanceType(jsValueType: Int): Unit =
733+
_specialInstanceTypes |= (1 << jsValueType)
734+
735+
/** A bitset of the `jsValueType`s corresponding to hijacked classes that extend this class.
736+
*
737+
* This value is used for instance tests against this class. A JS value `x` is an instance of
738+
* this type iff `jsValueType(x)` is a member of this bitset. Because of how a bitset works,
739+
* this means testing the following formula:
740+
*
741+
* {{{
742+
* ((1 << jsValueType(x)) & specialInstanceTypes) != 0
743+
* }}}
744+
*
745+
* For example, if this class is `Comparable`, we want the bitset to contain the values for
746+
* `boolean`, `string` and `number` (but not `undefined`), because `jl.Boolean`, `jl.String`
747+
* and `jl.Double` implement `Comparable`.
748+
*
749+
* This field is initialized with 0, and augmented during preprocessing by calls to
750+
* `addSpecialInstanceType`.
751+
*
752+
* This technique is used both for static `isInstanceOf` tests as well as reflective tests
753+
* through `Class.isInstance`. For the latter, this value is stored in
754+
* `typeData.specialInstanceTypes`. For the former, it is embedded as a constant in the
755+
* generated code.
756+
*
757+
* See the `isInstance` and `genInstanceTest` helpers.
758+
*
759+
* Special cases: this value remains 0 for all the numeric hijacked classes except `jl.Double`,
760+
* since `jsValueType(x) == JSValueTypeNumber` is not enough to deduce that
761+
* `x.isInstanceOf[Int]`, for example.
762+
*/
763+
def specialInstanceTypes: Int = _specialInstanceTypes
764+
765+
/** Is this class an ancestor of any hijacked class?
766+
*
767+
* This includes but is not limited to the hijacked classes themselves, as well as `jl.Object`.
768+
*/
769+
def isAncestorOfHijackedClass: Boolean =
770+
specialInstanceTypes != 0 || kind == ClassKind.HijackedClass
745771

746772
def isInterface = kind == ClassKind.Interface
747773

0 commit comments

Comments
 (0)