Skip to content

Commit fc8870d

Browse files
committed
Replace the JS object given to jl.Class by primitive IR operations.
`java.lang.Class` requires primitive capabilities that only the linker can provide for most of its core methods. This commit changes the fundamental way to provide those. In the process, remove creation of multi-dimensional arrays from the IR, and move it to user-space instead. --- Previously, the contract was that the linker provided a JS object with specified properties and methods to the constructor of `jl.Class`. For example, it contained a `"name"` property in order to implement `jl.Class.getName()`. We happened to reuse the `$TypeData` instance as that object, for convenience, and to avoid more indirections. This design grew organically over the years, and was eventually specified in Scala.js IR 1.0.0. However, it always had some shortcomings that I was not happy about. First, the `getSuperclass()` method had to be special-cased in the Analyzer and Emitter: its very presence requires additional metadata. When it is not reachable, we do not actually provide the supposedly specified member `"getSuperclass"()` on the JS object. That assumes that only `jl.Class.getSuperclass()` actually calls that method. In turn, that assumes it never gets inlined (!), since doing so would make it unreachable, but the feature would still be required. Second, for optimization purposes, the emitter needs direct access to the `$TypeData` stored inside `jl.Class` (for some of its Transients). There again, that assumed a field with a particular name be present in `jl.Class` in which the JS object would be stored. This implicit requirement was already leaking in the way `jl.Class` was written, in order to prevent scalac from renaming the field. --- In this commit, we completely change the approach, and therefore the IR spec. We remove the JS object passed to `jl.Class`; its constructor now takes no arguments. Instead, we add primitives in the IR, in the form of new `UnaryOp`s and `BinaryOp`s. These new primitives operate on an argument of type `jl.Class!`, and possibly a second argument. For example, `UnaryOp.Class_name` replaces the functionality previously offered by the `"name"` field. `BinaryOp.Class_superClass` replaces `"getSuperclass"()`. It is up to the backend to ensure it can, somehow, implement those operations given an instance of `jl.Class`. We choose to add a magic field to `jl.Class` at the Emitter level to store the corresponding instance of `$TypeData`, and implement the operations in terms of that. The approach has several benefits: * It solves the two fundamental issues of the JS object, mentioned above. * It does not require "public" (unminifiable) property names for the features; since there is no JS object spec, we can consistently minify those members. * Several methods that were given an *intrinsic* treatment by the optimizer now fall, more naturally, under regular folding of operations. --- Since we are changing the spec anyway, we use the opportunity to switch what is primitive about `Array.newInstance`. Previously, the overload with multiple dimensions was primitive, and the one with a single dimension delegated to it. This was a waste, since usages of the multi-dimensional overload are basically non-existent otherwise. Similarly, the `NewArray` node was designed for multiple dimensions, and the single dimension was a special case. We now make the one-dimensional operations the only primitives. We implement the multi-dimensional overload of `newInstance` in terms of the other one. We also use that overload as a replacement for multi-dimensional `NewArray` nodes. This simplifies the treatment in the linker, and produces shorter and more efficient code at the end of the day. --- The change of approach comes with a non-negligible cost for backward compatibility. It is entirely done using deserialization hacks. The most glaring issue is the synthesization of the multi-dimensional overload of `Array.newInstance`. In this commit, we do not change the library/compiler yet, so that we can exercise the deserialization hacks.
1 parent a233bb2 commit fc8870d

33 files changed

+1011
-725
lines changed

ir/shared/src/main/scala/org/scalajs/ir/Printers.scala

+21-2
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,14 @@ object Printers {
371371
} else {
372372
print(lhs)
373373
print((op: @switch) match {
374-
case String_length => ".length"
375-
case CheckNotNull => ".notNull"
374+
case String_length => ".length"
375+
case CheckNotNull => ".notNull"
376+
case Class_name => ".name"
377+
case Class_isPrimitive => ".isPrimitive"
378+
case Class_isInterface => ".isInterface"
379+
case Class_isArray => ".isArray"
380+
case Class_componentType => ".componentType"
381+
case Class_superClass => ".superClass"
376382
})
377383
}
378384

@@ -413,6 +419,19 @@ object Printers {
413419
print(rhs)
414420
print(']')
415421

422+
case BinaryOp(op, lhs, rhs) if BinaryOp.isClassOp(op) =>
423+
import BinaryOp._
424+
print((op: @switch) match {
425+
case Class_isInstance => "isInstance("
426+
case Class_isAssignableFrom => "isAssignableFrom("
427+
case Class_cast => "cast("
428+
case Class_newArray => "newArray("
429+
})
430+
print(lhs)
431+
print(", ")
432+
print(rhs)
433+
print(')')
434+
416435
case BinaryOp(op, lhs, rhs) =>
417436
import BinaryOp._
418437
print('(')

ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala

+298-7
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import scala.collection.mutable
2222
import scala.concurrent._
2323

2424
import Names._
25+
import OriginalName.NoOriginalName
2526
import Position._
2627
import Trees._
2728
import Types._
@@ -1223,13 +1224,45 @@ object Serializers {
12231224
ApplyDynamicImport(readApplyFlags(), readClassName(),
12241225
readMethodIdent(), readTrees())
12251226

1226-
case TagUnaryOp => UnaryOp(readByte(), readTree())
1227-
case TagBinaryOp => BinaryOp(readByte(), readTree(), readTree())
1228-
case TagNewArray => NewArray(readArrayTypeRef(), readTrees())
1229-
case TagArrayValue => ArrayValue(readArrayTypeRef(), readTrees())
1230-
case TagArrayLength => ArrayLength(readTree())
1231-
case TagArraySelect => ArraySelect(readTree(), readTree())(readType())
1232-
case TagRecordValue => RecordValue(readType().asInstanceOf[RecordType], readTrees())
1227+
case TagUnaryOp => UnaryOp(readByte(), readTree())
1228+
case TagBinaryOp => BinaryOp(readByte(), readTree(), readTree())
1229+
1230+
case TagNewArray =>
1231+
val arrayTypeRef = readArrayTypeRef()
1232+
val lengths = readTrees()
1233+
lengths match {
1234+
case length :: Nil =>
1235+
NewArray(arrayTypeRef, lengths)
1236+
1237+
case _ =>
1238+
if (true /* hacks.use16 */) { // scalastyle:ignore
1239+
// Rewrite as a call to j.l.r.Array.newInstance
1240+
val ArrayTypeRef(base, origDims) = arrayTypeRef
1241+
val newDims = origDims - lengths.size
1242+
if (newDims < 0) {
1243+
throw new IOException(
1244+
s"Illegal legacy NewArray node with ${lengths.size} lengths but dimension $origDims at $pos")
1245+
}
1246+
val newBase =
1247+
if (newDims == 0) base
1248+
else ArrayTypeRef(base, newDims)
1249+
1250+
ApplyStatic(
1251+
ApplyFlags.empty,
1252+
HackNames.ReflectArrayClass,
1253+
MethodIdent(HackNames.newInstanceMultiName),
1254+
List(ClassOf(newBase), ArrayValue(ArrayTypeRef(IntRef, 1), lengths)))(
1255+
AnyType)
1256+
} else {
1257+
throw new IOException(
1258+
s"Illegal NewArray node with multiple lengths for IR version 1.17+ at $pos")
1259+
}
1260+
}
1261+
1262+
case TagArrayValue => ArrayValue(readArrayTypeRef(), readTrees())
1263+
case TagArrayLength => ArrayLength(readTree())
1264+
case TagArraySelect => ArraySelect(readTree(), readTree())(readType())
1265+
case TagRecordValue => RecordValue(readType().asInstanceOf[RecordType], readTrees())
12331266

12341267
case TagIsInstanceOf =>
12351268
val expr = readTree()
@@ -1471,6 +1504,10 @@ object Serializers {
14711504
if (hacks.use4 && kind.isJSClass) {
14721505
// #4409: Filter out abstract methods in non-native JS classes for version < 1.5
14731506
methods0.filter(_.body.isDefined)
1507+
} else if (true /*hacks.use16*/ && cls == ClassClass) { // scalastyle:ignore
1508+
jlClassMethodsHack16(methods0)
1509+
} else if (true /*hacks.use16*/ && cls == HackNames.ReflectArrayModClass) { // scalastyle:ignore
1510+
jlReflectArrayMethodsHack16(methods0)
14741511
} else {
14751512
methods0
14761513
}
@@ -1493,6 +1530,252 @@ object Serializers {
14931530
optimizerHints)
14941531
}
14951532

1533+
private def jlClassMethodsHack16(methods: List[MethodDef]): List[MethodDef] = {
1534+
for (method <- methods) yield {
1535+
implicit val pos = method.pos
1536+
1537+
val methodName = method.methodName
1538+
val methodSimpleNameString = methodName.simpleName.nameString
1539+
1540+
val thisJLClass = This()(ClassType(ClassClass, nullable = false))
1541+
1542+
if (methodName.isConstructor) {
1543+
val newName = MethodIdent(NoArgConstructorName)(method.name.pos)
1544+
val newBody = ApplyStatically(ApplyFlags.empty.withConstructor(true),
1545+
thisJLClass, ObjectClass, newName, Nil)(NoType)
1546+
MethodDef(method.flags, newName, method.originalName,
1547+
Nil, NoType, Some(newBody))(
1548+
method.optimizerHints, method.version)
1549+
} else {
1550+
def argRef = method.args.head.ref
1551+
1552+
var forceInline = true // reset to false in the `case _ =>`
1553+
1554+
val newBody: Tree = methodSimpleNameString match {
1555+
case "getName" => UnaryOp(UnaryOp.Class_name, thisJLClass)
1556+
case "isPrimitive" => UnaryOp(UnaryOp.Class_isPrimitive, thisJLClass)
1557+
case "isInterface" => UnaryOp(UnaryOp.Class_isInterface, thisJLClass)
1558+
case "isArray" => UnaryOp(UnaryOp.Class_isArray, thisJLClass)
1559+
case "getComponentType" => UnaryOp(UnaryOp.Class_componentType, thisJLClass)
1560+
case "getSuperclass" => UnaryOp(UnaryOp.Class_superClass, thisJLClass)
1561+
1562+
case "isInstance" => BinaryOp(BinaryOp.Class_isInstance, thisJLClass, argRef)
1563+
case "isAssignableFrom" => BinaryOp(BinaryOp.Class_isAssignableFrom, thisJLClass, argRef)
1564+
case "cast" => BinaryOp(BinaryOp.Class_cast, thisJLClass, argRef)
1565+
1566+
case _ =>
1567+
forceInline = false
1568+
1569+
/* Unfortunately, some of the other methods directly referred to
1570+
* `this.data["name"]`, instead of building on `this.getName()`.
1571+
* We must replace those occurrences with a `Class_name` as well.
1572+
*/
1573+
val transformer = new Transformers.Transformer {
1574+
override def transform(tree: Tree, isStat: Boolean): Tree = tree match {
1575+
case JSSelect(_, StringLiteral("name")) =>
1576+
implicit val pos = tree.pos
1577+
UnaryOp(UnaryOp.Class_name, thisJLClass)
1578+
case _ =>
1579+
super.transform(tree, isStat)
1580+
}
1581+
}
1582+
transformer.transform(method.body.get, isStat = method.resultType == NoType)
1583+
}
1584+
1585+
val newOptimizerHints =
1586+
if (forceInline) method.optimizerHints.withInline(true)
1587+
else method.optimizerHints
1588+
1589+
MethodDef(method.flags, method.name, method.originalName,
1590+
method.args, method.resultType, Some(newBody))(
1591+
newOptimizerHints, method.version)
1592+
}
1593+
}
1594+
}
1595+
1596+
private def jlReflectArrayMethodsHack16(methods: List[MethodDef]): List[MethodDef] = {
1597+
/* Basically this method hard-codes new implementations for the two
1598+
* overloads of newInstance.
1599+
* It is horrible, but better than pollute everything else in the linker.
1600+
*/
1601+
1602+
import HackNames._
1603+
1604+
def paramDef(name: String, ptpe: Type)(implicit pos: Position): ParamDef =
1605+
ParamDef(LocalIdent(LocalName(name)), NoOriginalName, ptpe, mutable = false)
1606+
1607+
def varDef(name: String, vtpe: Type, rhs: Tree, mutable: Boolean = false)(
1608+
implicit pos: Position): VarDef = {
1609+
VarDef(LocalIdent(LocalName(name)), NoOriginalName, vtpe, mutable, rhs)
1610+
}
1611+
1612+
val jlClassRef = ClassRef(ClassClass)
1613+
val intArrayTypeRef = ArrayTypeRef(IntRef, 1)
1614+
val objectRef = ClassRef(ObjectClass)
1615+
val objectArrayTypeRef = ArrayTypeRef(objectRef, 1)
1616+
1617+
val jlClassType = ClassType(ClassClass, nullable = true)
1618+
1619+
val newInstanceRecName = MethodName("newInstanceRec",
1620+
List(jlClassRef, intArrayTypeRef, IntRef), objectRef)
1621+
1622+
val EAF = ApplyFlags.empty
1623+
1624+
val newInstanceRecMethod = {
1625+
/* def newInstanceRec(componentType: jl.Class, dimensions: int[], offset: int): any = {
1626+
* val length: int = dimensions[offset]
1627+
* val result: any = newInstance(componentType, length)
1628+
* val innerOffset = offset + 1
1629+
* if (innerOffset < dimensions.length) {
1630+
* val result2: Object[] = result.asInstanceOf[Object[]]
1631+
* val innerComponentType: jl.Class = componentType.getComponentType()
1632+
* var i: Int = 0
1633+
* while (i != length)
1634+
* result2[i] = newInstanceRec(innerComponentType, dimensions, innerOffset)
1635+
* i = i + 1
1636+
* }
1637+
* }
1638+
* result
1639+
* }
1640+
*/
1641+
1642+
implicit val pos = Position.NoPosition
1643+
1644+
val getComponentTypeName = MethodName("getComponentType", Nil, jlClassRef)
1645+
1646+
val ths = This()(ClassType(ReflectArrayModClass, nullable = false))
1647+
1648+
val componentType = paramDef("componentType", jlClassType)
1649+
val dimensions = paramDef("dimensions", ArrayType(intArrayTypeRef, nullable = true))
1650+
val offset = paramDef("offset", IntType)
1651+
1652+
val length = varDef("length", IntType, ArraySelect(dimensions.ref, offset.ref)(IntType))
1653+
val result = varDef("result", AnyType,
1654+
Apply(EAF, ths, MethodIdent(newInstanceSingleName), List(componentType.ref, length.ref))(AnyType))
1655+
val innerOffset = varDef("innerOffset", IntType,
1656+
BinaryOp(BinaryOp.Int_+, offset.ref, IntLiteral(1)))
1657+
1658+
val result2 = varDef("result2", ArrayType(objectArrayTypeRef, nullable = true),
1659+
AsInstanceOf(result.ref, ArrayType(objectArrayTypeRef, nullable = true)))
1660+
val innerComponentType = varDef("innerComponentType", jlClassType,
1661+
Apply(EAF, componentType.ref, MethodIdent(getComponentTypeName), Nil)(jlClassType))
1662+
val i = varDef("i", IntType, IntLiteral(0), mutable = true)
1663+
1664+
val body = {
1665+
Block(
1666+
length,
1667+
result,
1668+
innerOffset,
1669+
If(BinaryOp(BinaryOp.Int_<, innerOffset.ref, ArrayLength(dimensions.ref)), {
1670+
Block(
1671+
result2,
1672+
innerComponentType,
1673+
i,
1674+
While(BinaryOp(BinaryOp.Int_!=, i.ref, length.ref), {
1675+
Block(
1676+
Assign(
1677+
ArraySelect(result2.ref, i.ref)(AnyType),
1678+
Apply(EAF, ths, MethodIdent(newInstanceRecName),
1679+
List(innerComponentType.ref, dimensions.ref, innerOffset.ref))(AnyType)
1680+
),
1681+
Assign(
1682+
i.ref,
1683+
BinaryOp(BinaryOp.Int_+, i.ref, IntLiteral(1))
1684+
)
1685+
)
1686+
})
1687+
)
1688+
}, Skip())(NoType),
1689+
result.ref
1690+
)
1691+
}
1692+
1693+
MethodDef(MemberFlags.empty, MethodIdent(newInstanceRecName),
1694+
NoOriginalName, List(componentType, dimensions, offset), AnyType,
1695+
Some(body))(
1696+
OptimizerHints.empty, Version.fromInt(1))
1697+
}
1698+
1699+
/* Only for the temporary commit where we always apply the hack, because
1700+
* the hack is applied in the JavalibIRCleaner and then a second time
1701+
* for the true deserialization.
1702+
*/
1703+
val oldMethodsTemp =
1704+
methods.filterNot(_.methodName == newInstanceRecName)
1705+
1706+
val newMethods = for (method <- oldMethodsTemp) yield {
1707+
method.methodName match {
1708+
case `newInstanceSingleName` =>
1709+
// newInstance(jl.Class, int) --> newArray(jlClass.notNull, length)
1710+
1711+
implicit val pos = method.pos
1712+
1713+
val List(jlClassParam, lengthParam) = method.args
1714+
1715+
val newBody = BinaryOp(BinaryOp.Class_newArray,
1716+
UnaryOp(UnaryOp.CheckNotNull, jlClassParam.ref),
1717+
lengthParam.ref)
1718+
1719+
MethodDef(method.flags, method.name, method.originalName,
1720+
method.args, method.resultType, Some(newBody))(
1721+
method.optimizerHints.withInline(true), method.version)
1722+
1723+
case `newInstanceMultiName` =>
1724+
/* newInstance(jl.Class, int[]) -->
1725+
* var outermostComponentType: jl.Class = jlClassParam
1726+
* var i: int = 1
1727+
* while (i != lengths.length) {
1728+
* outermostComponentType = getClass(this.newInstance(outermostComponentType, 0))
1729+
* i = i + 1
1730+
* }
1731+
* newInstanceRec(outermostComponentType, lengths, 0)
1732+
*/
1733+
1734+
implicit val pos = method.pos
1735+
1736+
val List(jlClassParam, lengthsParam) = method.args
1737+
1738+
val newBody = {
1739+
val outermostComponentType = varDef("outermostComponentType",
1740+
jlClassType, jlClassParam.ref, mutable = true)
1741+
val i = varDef("i", IntType, IntLiteral(1), mutable = true)
1742+
1743+
Block(
1744+
outermostComponentType,
1745+
i,
1746+
While(BinaryOp(BinaryOp.Int_!=, i.ref, ArrayLength(lengthsParam.ref)), {
1747+
Block(
1748+
Assign(
1749+
outermostComponentType.ref,
1750+
GetClass(Apply(EAF, This()(ClassType(ReflectArrayModClass, nullable = false)),
1751+
MethodIdent(newInstanceSingleName),
1752+
List(outermostComponentType.ref, IntLiteral(0)))(AnyType))
1753+
),
1754+
Assign(
1755+
i.ref,
1756+
BinaryOp(BinaryOp.Int_+, i.ref, IntLiteral(1))
1757+
)
1758+
)
1759+
}),
1760+
Apply(EAF, This()(ClassType(ReflectArrayModClass, nullable = false)),
1761+
MethodIdent(newInstanceRecName),
1762+
List(outermostComponentType.ref, lengthsParam.ref, IntLiteral(0)))(
1763+
AnyType)
1764+
)
1765+
}
1766+
1767+
MethodDef(method.flags, method.name, method.originalName,
1768+
method.args, method.resultType, Some(newBody))(
1769+
method.optimizerHints, method.version)
1770+
1771+
case _ =>
1772+
method
1773+
}
1774+
}
1775+
1776+
newInstanceRecMethod :: newMethods
1777+
}
1778+
14961779
private def jsConstructorHack(
14971780
jsMethodProps: List[JSMethodPropDef]): (Option[JSConstructorDef], List[JSMethodPropDef]) = {
14981781
val jsConstructorBuilder = new OptionBuilder[JSConstructorDef]
@@ -2159,11 +2442,19 @@ object Serializers {
21592442
ClassName("java.lang.CloneNotSupportedException")
21602443
val SystemModule: ClassName =
21612444
ClassName("java.lang.System$")
2445+
val ReflectArrayClass =
2446+
ClassName("java.lang.reflect.Array")
2447+
val ReflectArrayModClass =
2448+
ClassName("java.lang.reflect.Array$")
21622449

21632450
val cloneName: MethodName =
21642451
MethodName("clone", Nil, ClassRef(ObjectClass))
21652452
val identityHashCodeName: MethodName =
21662453
MethodName("identityHashCode", List(ClassRef(ObjectClass)), IntRef)
2454+
val newInstanceSingleName: MethodName =
2455+
MethodName("newInstance", List(ClassRef(ClassClass), IntRef), ClassRef(ObjectClass))
2456+
val newInstanceMultiName: MethodName =
2457+
MethodName("newInstance", List(ClassRef(ClassClass), ArrayTypeRef(IntRef, 1)), ClassRef(ObjectClass))
21672458
}
21682459

21692460
private class OptionBuilder[T] {

0 commit comments

Comments
 (0)