Skip to content

Commit c1646d1

Browse files
committed
Merge several IR nodes into UnaryOp.
`ArrayLength`, `GetClass`, `Clone`, `IdentityHashCode`, `WrapAsThrowable`, `UnwrapFromThrowable` and `Throw` are now ops of `UnaryOp`, rather than having their own IR nodes. They all follow the general contract of how a `UnaryOp` must behave: first evaluate the argument, then perform an operation that only depends on the value of the argument (and not, for example, on the scope). It makes the JS `FunctionEmitter` a bit messier. However, everything else gets simpler, as we get rid of dedicated paths for those 7 IR nodes. In order to better fit with the other `UnaryOp`s, notably the ones for `jl.Class` operations, we demand non-nullable arguments. An explicit `CheckNotNull` must be used for arguments that are nullable. --- In this commit, we do not update the compiler yet. It still emits the old IR nodes, so that we can test the deserialiation hacks. That also means the case classes still exist for now.
1 parent 4878731 commit c1646d1

File tree

17 files changed

+533
-610
lines changed

17 files changed

+533
-610
lines changed

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

Lines changed: 40 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -347,40 +347,47 @@ object Printers {
347347
case UnaryOp(op, lhs) =>
348348
import UnaryOp._
349349

350-
if (op < String_length) {
351-
print('(')
352-
print((op: @switch) match {
353-
case Boolean_! =>
354-
"!"
355-
case IntToChar =>
356-
"(char)"
357-
case IntToByte =>
358-
"(byte)"
359-
case IntToShort =>
360-
"(short)"
361-
case CharToInt | ByteToInt | ShortToInt | LongToInt | DoubleToInt =>
362-
"(int)"
363-
case IntToLong | DoubleToLong =>
364-
"(long)"
365-
case DoubleToFloat | LongToFloat =>
366-
"(float)"
367-
case IntToDouble | LongToDouble | FloatToDouble =>
368-
"(double)"
369-
})
370-
print(lhs)
371-
print(')')
372-
} else {
350+
def p(prefix: String, suffix: String): Unit = {
351+
print(prefix)
373352
print(lhs)
374-
print((op: @switch) match {
375-
case String_length => ".length"
376-
case CheckNotNull => ".notNull"
377-
case Class_name => ".name"
378-
case Class_isPrimitive => ".isPrimitive"
379-
case Class_isInterface => ".isInterface"
380-
case Class_isArray => ".isArray"
381-
case Class_componentType => ".componentType"
382-
case Class_superClass => ".superClass"
383-
})
353+
print(suffix)
354+
}
355+
356+
(op: @switch) match {
357+
case Boolean_! =>
358+
p("(!", ")")
359+
case IntToChar =>
360+
p("((char)", ")")
361+
case IntToByte =>
362+
p("((byte)", ")")
363+
case IntToShort =>
364+
p("((short)", ")")
365+
case CharToInt | ByteToInt | ShortToInt | LongToInt | DoubleToInt =>
366+
p("((int)", ")")
367+
case IntToLong | DoubleToLong =>
368+
p("((long)", ")")
369+
case DoubleToFloat | LongToFloat =>
370+
p("((float)", ")")
371+
case IntToDouble | LongToDouble | FloatToDouble =>
372+
p("((double)", ")")
373+
374+
case String_length => p("", ".length")
375+
case CheckNotNull => p("", ".notNull")
376+
case Class_name => p("", ".name")
377+
case Class_isPrimitive => p("", ".isPrimitive")
378+
case Class_isInterface => p("", ".isInterface")
379+
case Class_isArray => p("", ".isArray")
380+
case Class_componentType => p("", ".componentType")
381+
case Class_superClass => p("", ".superClass")
382+
case Array_length => p("", ".length")
383+
case GetClass => p("", ".getClass()")
384+
385+
case Clone => p("<clone>(", ")")
386+
case IdentityHashCode => p("<identityHashCode>(", ")")
387+
case WrapAsThrowable => p("<wrapAsThrowable>(", ")")
388+
case UnwrapFromThrowable => p("<unwrapFromThrowable>(", ")")
389+
390+
case Throw => p("throw ", "")
384391
}
385392

386393
case BinaryOp(BinaryOp.Int_-, IntLiteral(0), rhs) =>

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

Lines changed: 56 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,8 +1139,8 @@ object Serializers {
11391139
* (throw qual.field[null]) = rhs --> qual.field[null] = rhs
11401140
*/
11411141
lhs0 match {
1142-
case Throw(sel: Select) if sel.tpe == NullType => sel
1143-
case _ => lhs0
1142+
case UnaryOp(UnaryOp.Throw, sel: Select) if sel.tpe == NullType => sel
1143+
case _ => lhs0
11441144
}
11451145
} else {
11461146
lhs0
@@ -1170,13 +1170,6 @@ object Serializers {
11701170
case TagTryFinally =>
11711171
TryFinally(readTree(), readTree())
11721172

1173-
case TagThrow =>
1174-
val expr = readTree()
1175-
val patchedExpr =
1176-
if (hacks.use8) throwArgumentHack8(expr)
1177-
else expr
1178-
Throw(patchedExpr)
1179-
11801173
case TagMatch =>
11811174
Match(readTree(), List.fill(readInt()) {
11821175
(readTrees().map(_.asInstanceOf[MatchableLiteral]), readTree())
@@ -1207,7 +1200,7 @@ object Serializers {
12071200
/* Note [Nothing FieldDef rewrite]
12081201
* qual.field[nothing] --> throw qual.field[null]
12091202
*/
1210-
Throw(Select(qualifier, field)(NullType))
1203+
UnaryOp(UnaryOp.Throw, Select(qualifier, field)(NullType))
12111204
} else {
12121205
Select(qualifier, field)(tpe)
12131206
}
@@ -1246,6 +1239,36 @@ object Serializers {
12461239
case TagUnaryOp => UnaryOp(readByte(), readTree())
12471240
case TagBinaryOp => BinaryOp(readByte(), readTree(), readTree())
12481241

1242+
case TagArrayLength | TagGetClass | TagClone | TagIdentityHashCode |
1243+
TagWrapAsThrowable | TagUnwrapFromThrowable | TagThrow =>
1244+
if (false /*!hacks.use17*/) { // scalastyle:ignore
1245+
throw new IOException(
1246+
s"Illegal legacy node $tag found in class ${enclosingClassName.nameString}")
1247+
}
1248+
1249+
val lhs = readTree()
1250+
def checkNotNullLhs: Tree = UnaryOp(UnaryOp.CheckNotNull, lhs)
1251+
1252+
(tag: @switch) match {
1253+
case TagArrayLength =>
1254+
UnaryOp(UnaryOp.Array_length, checkNotNullLhs)
1255+
case TagGetClass =>
1256+
UnaryOp(UnaryOp.GetClass, checkNotNullLhs)
1257+
case TagClone =>
1258+
UnaryOp(UnaryOp.Clone, checkNotNullLhs)
1259+
case TagIdentityHashCode =>
1260+
UnaryOp(UnaryOp.IdentityHashCode, lhs)
1261+
case TagWrapAsThrowable =>
1262+
UnaryOp(UnaryOp.WrapAsThrowable, lhs)
1263+
case TagUnwrapFromThrowable =>
1264+
UnaryOp(UnaryOp.UnwrapFromThrowable, checkNotNullLhs)
1265+
case TagThrow =>
1266+
val patchedLhs =
1267+
if (hacks.use8) throwArgumentHack8(lhs)
1268+
else lhs
1269+
UnaryOp(UnaryOp.Throw, patchedLhs)
1270+
}
1271+
12491272
case TagNewArray =>
12501273
val arrayTypeRef = readArrayTypeRef()
12511274
val lengths = readTrees()
@@ -1279,7 +1302,6 @@ object Serializers {
12791302
}
12801303

12811304
case TagArrayValue => ArrayValue(readArrayTypeRef(), readTrees())
1282-
case TagArrayLength => ArrayLength(readTree())
12831305
case TagArraySelect => ArraySelect(readTree(), readTree())(readType())
12841306
case TagRecordValue => RecordValue(readType().asInstanceOf[RecordType], readTrees())
12851307

@@ -1299,14 +1321,6 @@ object Serializers {
12991321
IsInstanceOf(expr, testType)
13001322

13011323
case TagAsInstanceOf => AsInstanceOf(readTree(), readType())
1302-
case TagGetClass => GetClass(readTree())
1303-
case TagClone => Clone(readTree())
1304-
case TagIdentityHashCode => IdentityHashCode(readTree())
1305-
1306-
case TagWrapAsThrowable =>
1307-
WrapAsThrowable(readTree())
1308-
case TagUnwrapFromThrowable =>
1309-
UnwrapFromThrowable(readTree())
13101324

13111325
case TagJSNew => JSNew(readTree(), readTreeOrJSSpreads())
13121326
case TagJSPrivateSelect => JSPrivateSelect(readTree(), readFieldIdent())
@@ -1462,10 +1476,13 @@ object Serializers {
14621476
* `runtime.package$.unwrapJavaScriptException(x)`.
14631477
*/
14641478
private def throwArgumentHack8(expr: Tree)(implicit pos: Position): Tree = {
1479+
def unwrapFromThrowable(t: Tree): Tree =
1480+
UnaryOp(UnaryOp.UnwrapFromThrowable, t)
1481+
14651482
expr.tpe match {
14661483
case NullType =>
14671484
// Evaluate the expression then definitely run into an NPE UB
1468-
UnwrapFromThrowable(expr)
1485+
unwrapFromThrowable(expr)
14691486

14701487
case ClassType(_, _) =>
14711488
expr match {
@@ -1476,7 +1493,7 @@ object Serializers {
14761493
/* Common case (explicit re-throw of the form `throw th`) where we don't need the IIFE.
14771494
* if (expr === null) unwrapFromThrowable(null) else expr
14781495
*/
1479-
If(BinaryOp(BinaryOp.===, expr, Null()), UnwrapFromThrowable(Null()), expr)(AnyType)
1496+
If(BinaryOp(BinaryOp.===, expr, Null()), unwrapFromThrowable(Null()), expr)(AnyType)
14801497
case _ =>
14811498
/* General case where we need to avoid evaluating `expr` twice.
14821499
* ((x) => if (x === null) unwrapFromThrowable(null) else x)(expr)
@@ -1485,7 +1502,7 @@ object Serializers {
14851502
val xParamDef = ParamDef(x, OriginalName.NoOriginalName, AnyType, mutable = false)
14861503
val xRef = xParamDef.ref
14871504
val closure = Closure(arrow = true, Nil, List(xParamDef), None, {
1488-
If(BinaryOp(BinaryOp.===, xRef, Null()), UnwrapFromThrowable(Null()), xRef)(AnyType)
1505+
If(BinaryOp(BinaryOp.===, xRef, Null()), unwrapFromThrowable(Null()), xRef)(AnyType)
14891506
}, Nil)
14901507
JSFunctionApply(closure, List(expr))
14911508
}
@@ -1681,6 +1698,12 @@ object Serializers {
16811698
VarDef(LocalIdent(LocalName(name)), NoOriginalName, vtpe, mutable, rhs)
16821699
}
16831700

1701+
def arrayLength(t: Tree)(implicit pos: Position): Tree =
1702+
UnaryOp(UnaryOp.Array_length, t)
1703+
1704+
def getClass(t: Tree)(implicit pos: Position): Tree =
1705+
UnaryOp(UnaryOp.GetClass, t)
1706+
16841707
val jlClassRef = ClassRef(ClassClass)
16851708
val intArrayTypeRef = ArrayTypeRef(IntRef, 1)
16861709
val objectRef = ClassRef(ObjectClass)
@@ -1738,7 +1761,7 @@ object Serializers {
17381761
length,
17391762
result,
17401763
innerOffset,
1741-
If(BinaryOp(BinaryOp.Int_<, innerOffset.ref, ArrayLength(dimensions.ref)), {
1764+
If(BinaryOp(BinaryOp.Int_<, innerOffset.ref, arrayLength(dimensions.ref)), {
17421765
Block(
17431766
result2,
17441767
innerComponentType,
@@ -1808,11 +1831,11 @@ object Serializers {
18081831
Block(
18091832
outermostComponentType,
18101833
i,
1811-
While(BinaryOp(BinaryOp.Int_!=, i.ref, ArrayLength(lengthsParam.ref)), {
1834+
While(BinaryOp(BinaryOp.Int_!=, i.ref, arrayLength(lengthsParam.ref)), {
18121835
Block(
18131836
Assign(
18141837
outermostComponentType.ref,
1815-
GetClass(Apply(EAF, This()(ClassType(ReflectArrayModClass, nullable = false)),
1838+
getClass(Apply(EAF, This()(ClassType(ReflectArrayModClass, nullable = false)),
18161839
MethodIdent(newInstanceSingleName),
18171840
List(outermostComponentType.ref, IntLiteral(0)))(AnyType))
18181841
),
@@ -1942,7 +1965,7 @@ object Serializers {
19421965
*/
19431966
assert(args.size == 1)
19441967

1945-
val patchedBody = Some(IdentityHashCode(args(0).ref))
1968+
val patchedBody = Some(UnaryOp(UnaryOp.IdentityHashCode, args(0).ref))
19461969
val patchedOptimizerHints = OptimizerHints.empty.withInline(true)
19471970

19481971
MethodDef(flags, name, originalName, args, resultType, patchedBody)(
@@ -1967,11 +1990,13 @@ object Serializers {
19671990

19681991
val patchedBody = Some {
19691992
If(IsInstanceOf(thisValue, cloneableClassType.toNonNullable),
1970-
Clone(AsInstanceOf(thisValue, cloneableClassType)),
1971-
Throw(New(
1972-
HackNames.CloneNotSupportedExceptionClass,
1973-
MethodIdent(NoArgConstructorName),
1974-
Nil)))(cloneableClassType)
1993+
UnaryOp(UnaryOp.Clone,
1994+
UnaryOp(UnaryOp.CheckNotNull, AsInstanceOf(thisValue, cloneableClassType))),
1995+
UnaryOp(UnaryOp.Throw,
1996+
New(
1997+
HackNames.CloneNotSupportedExceptionClass,
1998+
MethodIdent(NoArgConstructorName),
1999+
Nil)))(cloneableClassType)
19752000
}
19762001
val patchedOptimizerHints = OptimizerHints.empty.withInline(true)
19772002

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

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -280,12 +280,24 @@ object Trees {
280280
}
281281

282282
/** Unary operation.
283+
*
284+
* All unary operations follow common evaluation steps:
285+
*
286+
* 1. Let `lhsValue` be the result of evaluating `lhs`.
287+
* 2. Perform an operation that depends on `op` and `lhsValue`.
283288
*
284289
* The `Class_x` operations take a `jl.Class!` argument, i.e., a
285290
* non-nullable `jl.Class`.
286291
*
292+
* Likewise, `Array_length`, `GetClass`, `Clone` and `UnwrapFromThrowable`
293+
* take arguments of non-nullable types.
294+
*
287295
* `CheckNotNull` throws NPEs subject to UB.
288296
*
297+
* `Throw` always throws, obviously.
298+
*
299+
* `Clone` and `WrapAsThrowable` are side-effect-free but not pure.
300+
*
289301
* Otherwise, unary operations preserve pureness.
290302
*/
291303
sealed case class UnaryOp(op: UnaryOp.Code, lhs: Tree)(
@@ -337,9 +349,26 @@ object Trees {
337349
final val Class_componentType = 23
338350
final val Class_superClass = 24
339351

352+
// Misc ops introduced in 1.18, which used to have dedicated IR nodes
353+
final val Array_length = 25
354+
final val GetClass = 26
355+
final val Clone = 27
356+
final val IdentityHashCode = 28
357+
final val WrapAsThrowable = 29
358+
final val UnwrapFromThrowable = 30
359+
final val Throw = 31
360+
340361
def isClassOp(op: Code): Boolean =
341362
op >= Class_name && op <= Class_superClass
342363

364+
def isPureOp(op: Code): Boolean = (op: @switch) match {
365+
case CheckNotNull | Clone | WrapAsThrowable | Throw => false
366+
case _ => true
367+
}
368+
369+
def isSideEffectFreeOp(op: Code): Boolean =
370+
op != CheckNotNull && op != Throw
371+
343372
def resultTypeOf(op: Code, argType: Type): Type = (op: @switch) match {
344373
case Boolean_! | Class_isPrimitive | Class_isInterface | Class_isArray =>
345374
BooleanType
@@ -349,20 +378,27 @@ object Trees {
349378
ByteType
350379
case IntToShort =>
351380
ShortType
352-
case CharToInt | ByteToInt | ShortToInt | LongToInt | DoubleToInt | String_length =>
381+
case CharToInt | ByteToInt | ShortToInt | LongToInt | DoubleToInt |
382+
String_length | Array_length | IdentityHashCode =>
353383
IntType
354384
case IntToLong | DoubleToLong =>
355385
LongType
356386
case DoubleToFloat | LongToFloat =>
357387
FloatType
358388
case IntToDouble | LongToDouble | FloatToDouble =>
359389
DoubleType
360-
case CheckNotNull =>
390+
case CheckNotNull | Clone =>
361391
argType.toNonNullable
362392
case Class_name =>
363393
StringType
364-
case Class_componentType | Class_superClass =>
394+
case Class_componentType | Class_superClass | GetClass =>
365395
ClassType(ClassClass, nullable = true)
396+
case WrapAsThrowable =>
397+
ClassType(ThrowableClass, nullable = false)
398+
case UnwrapFromThrowable =>
399+
AnyType
400+
case Throw =>
401+
NothingType
366402
}
367403
}
368404

@@ -826,11 +862,7 @@ object Trees {
826862
val tpe = VoidType
827863
}
828864

829-
/** Unary operation (always preserves pureness).
830-
*
831-
* Operations which do not preserve pureness are not allowed in this tree.
832-
* These are notably ++ and --
833-
*/
865+
/** JavaScript unary operation. */
834866
sealed case class JSUnaryOp(op: JSUnaryOp.Code, lhs: Tree)(
835867
implicit val pos: Position) extends Tree {
836868
val tpe = JSUnaryOp.resultTypeOf(op)
@@ -851,11 +883,7 @@ object Trees {
851883
AnyType
852884
}
853885

854-
/** Binary operation (always preserves pureness).
855-
*
856-
* Operations which do not preserve pureness are not allowed in this tree.
857-
* These are notably +=, -=, *=, /= and %=
858-
*/
886+
/** JavaScript binary operation. */
859887
sealed case class JSBinaryOp(op: JSBinaryOp.Code, lhs: Tree, rhs: Tree)(
860888
implicit val pos: Position) extends Tree {
861889
val tpe = JSBinaryOp.resultTypeOf(op)

ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,14 @@ class PrintersTest {
403403
assertPrintEquals("x.isArray", UnaryOp(Class_isArray, classVarRef))
404404
assertPrintEquals("x.componentType", UnaryOp(Class_componentType, classVarRef))
405405
assertPrintEquals("x.superClass", UnaryOp(Class_superClass, classVarRef))
406+
407+
assertPrintEquals("x.length", UnaryOp(Array_length, ref("x", arrayType(IntRef, 1))))
408+
assertPrintEquals("x.getClass()", UnaryOp(GetClass, ref("x", AnyType)))
409+
assertPrintEquals("<clone>(x)", UnaryOp(Clone, ref("x", arrayType(ObjectClass, 1))))
410+
assertPrintEquals("<identityHashCode>(x)", UnaryOp(IdentityHashCode, ref("x", AnyType)))
411+
assertPrintEquals("<wrapAsThrowable>(e)", UnaryOp(WrapAsThrowable, ref("e", AnyType)))
412+
assertPrintEquals("<unwrapFromThrowable>(e)",
413+
UnaryOp(UnwrapFromThrowable, ref("e", ClassType(ThrowableClass, nullable = true))))
406414
}
407415

408416
@Test def printPseudoUnaryOp(): Unit = {

0 commit comments

Comments
 (0)