diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala index 674ffc3ba6..a64d6a8656 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSCode.scala @@ -2572,9 +2572,10 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) ex match { case js.New(cls, _, _) if cls != JavaScriptExceptionClassName => // Common case where ex is neither null nor a js.JavaScriptException - js.Throw(ex) + js.UnaryOp(js.UnaryOp.Throw, ex) case _ => - js.Throw(js.UnwrapFromThrowable(ex)) + js.UnaryOp(js.UnaryOp.Throw, + js.UnaryOp(js.UnaryOp.UnwrapFromThrowable, js.UnaryOp(js.UnaryOp.CheckNotNull, ex))) } /* !!! Copy-pasted from `CleanUp.scala` upstream and simplified with @@ -3108,13 +3109,14 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) val (exceptValDef, exceptVar) = if (mightCatchJavaScriptException) { val valDef = js.VarDef(freshLocalIdent("e"), NoOriginalName, - encodeClassType(ThrowableClass), mutable = false, js.WrapAsThrowable(origExceptVar)) + encodeClassType(ThrowableClass), mutable = false, + js.UnaryOp(js.UnaryOp.WrapAsThrowable, origExceptVar)) (valDef, valDef.ref) } else { (js.Skip(), origExceptVar) } - val elseHandler: js.Tree = js.Throw(origExceptVar) + val elseHandler: js.Tree = js.UnaryOp(js.UnaryOp.Throw, origExceptVar) val handler = catches.foldRight(elseHandler) { (caseDef, elsep) => implicit val pos = caseDef.pos @@ -3323,7 +3325,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) private def genThrowClassCastException()(implicit pos: Position): js.Tree = { val ctor = ClassCastExceptionClass.info.member( nme.CONSTRUCTOR).suchThat(_.tpe.params.isEmpty) - js.Throw(genNew(ClassCastExceptionClass, ctor, Nil)) + js.UnaryOp(js.UnaryOp.Throw, genNew(ClassCastExceptionClass, ctor, Nil)) } /** Gen JS code for a super call, of the form Class.super[mix].fun(args). @@ -4891,7 +4893,8 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) js.Assign(genSelect(fun.tpe.paramTypes(1)), arguments(1)) } else { // length of the array - js.ArrayLength(arrayValue) + js.UnaryOp(js.UnaryOp.Array_length, + js.UnaryOp(js.UnaryOp.CheckNotNull, arrayValue)) } } @@ -5326,7 +5329,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) case IDENTITY_HASH_CODE => // runtime.identityHashCode(arg) val arg = genArgs1 - js.IdentityHashCode(arg) + js.UnaryOp(js.UnaryOp.IdentityHashCode, arg) case DEBUGGER => // js.special.debugger() @@ -5463,7 +5466,7 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) case JS_THROW => // js.special.throw(arg) - js.Throw(genArgs1) + js.UnaryOp(js.UnaryOp.Throw, genArgs1) case JS_TRY_CATCH => /* js.special.tryCatch(arg1, arg2) @@ -5502,11 +5505,12 @@ abstract class GenJSCode[G <: Global with Singleton](val global: G) case WRAP_AS_THROWABLE => // js.special.wrapAsThrowable(arg) - js.WrapAsThrowable(genArgs1) + js.UnaryOp(js.UnaryOp.WrapAsThrowable, genArgs1) case UNWRAP_FROM_THROWABLE => // js.special.unwrapFromThrowable(arg) - js.UnwrapFromThrowable(genArgs1) + js.UnaryOp(js.UnaryOp.UnwrapFromThrowable, + js.UnaryOp(js.UnaryOp.CheckNotNull, genArgs1)) case LINKTIME_PROPERTY => // LinkingInfo.linkTimePropertyXXX("...") diff --git a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSExports.scala b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSExports.scala index 603a6286d3..4cdc4369c3 100644 --- a/compiler/src/main/scala/org/scalajs/nscplugin/GenJSExports.scala +++ b/compiler/src/main/scala/org/scalajs/nscplugin/GenJSExports.scala @@ -1022,7 +1022,7 @@ trait GenJSExports[G <: Global with Singleton] extends SubComponent { private def genThrowTypeError(msg: String = "No matching overload")( implicit pos: Position): js.Tree = { - js.Throw(js.StringLiteral(msg)) + js.UnaryOp(js.UnaryOp.Throw, js.StringLiteral(msg)) } class FormalArgsRegistry(minArgc: Int, needsRestParam: Boolean) { diff --git a/compiler/src/test/scala/org/scalajs/nscplugin/test/OptimizationTest.scala b/compiler/src/test/scala/org/scalajs/nscplugin/test/OptimizationTest.scala index 24914ca07b..b10bef4b95 100644 --- a/compiler/src/test/scala/org/scalajs/nscplugin/test/OptimizationTest.scala +++ b/compiler/src/test/scala/org/scalajs/nscplugin/test/OptimizationTest.scala @@ -519,7 +519,7 @@ class OptimizationTest extends JSASTTest { } } """.hasNot("WrapAsThrowable") { - case js.WrapAsThrowable(_) => + case js.UnaryOp(js.UnaryOp.WrapAsThrowable, _) => } // Confidence check @@ -536,7 +536,7 @@ class OptimizationTest extends JSASTTest { } } """.hasExactly(1, "WrapAsThrowable") { - case js.WrapAsThrowable(_) => + case js.UnaryOp(js.UnaryOp.WrapAsThrowable, _) => } } diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala index a653c15fce..770d83a7c7 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Hashers.scala @@ -232,10 +232,6 @@ object Hashers { mixTree(finalizer) mixType(tree.tpe) - case Throw(expr) => - mixTag(TagThrow) - mixTree(expr) - case Match(selector, cases, default) => mixTag(TagMatch) mixTree(selector) @@ -331,10 +327,6 @@ object Hashers { mixArrayTypeRef(typeRef) mixTrees(elems) - case ArrayLength(array) => - mixTag(TagArrayLength) - mixTree(array) - case ArraySelect(array, index) => mixTag(TagArraySelect) mixTree(array) @@ -362,26 +354,6 @@ object Hashers { mixTree(expr) mixType(tpe) - case GetClass(expr) => - mixTag(TagGetClass) - mixTree(expr) - - case Clone(expr) => - mixTag(TagClone) - mixTree(expr) - - case IdentityHashCode(expr) => - mixTag(TagIdentityHashCode) - mixTree(expr) - - case WrapAsThrowable(expr) => - mixTag(TagWrapAsThrowable) - mixTree(expr) - - case UnwrapFromThrowable(expr) => - mixTag(TagUnwrapFromThrowable) - mixTree(expr) - case JSNew(ctor, args) => mixTag(TagJSNew) mixTree(ctor) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala index 4c4401a406..d6a490b634 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala @@ -260,10 +260,6 @@ object Printers { print(" finally ") printBlock(finalizer) - case Throw(expr) => - print("throw ") - print(expr) - case Match(selector, cases, default) => print("match (") print(selector) @@ -347,40 +343,47 @@ object Printers { case UnaryOp(op, lhs) => import UnaryOp._ - if (op < String_length) { - print('(') - print((op: @switch) match { - case Boolean_! => - "!" - case IntToChar => - "(char)" - case IntToByte => - "(byte)" - case IntToShort => - "(short)" - case CharToInt | ByteToInt | ShortToInt | LongToInt | DoubleToInt => - "(int)" - case IntToLong | DoubleToLong => - "(long)" - case DoubleToFloat | LongToFloat => - "(float)" - case IntToDouble | LongToDouble | FloatToDouble => - "(double)" - }) - print(lhs) - print(')') - } else { + def p(prefix: String, suffix: String): Unit = { + print(prefix) print(lhs) - print((op: @switch) match { - case String_length => ".length" - case CheckNotNull => ".notNull" - case Class_name => ".name" - case Class_isPrimitive => ".isPrimitive" - case Class_isInterface => ".isInterface" - case Class_isArray => ".isArray" - case Class_componentType => ".componentType" - case Class_superClass => ".superClass" - }) + print(suffix) + } + + (op: @switch) match { + case Boolean_! => + p("(!", ")") + case IntToChar => + p("((char)", ")") + case IntToByte => + p("((byte)", ")") + case IntToShort => + p("((short)", ")") + case CharToInt | ByteToInt | ShortToInt | LongToInt | DoubleToInt => + p("((int)", ")") + case IntToLong | DoubleToLong => + p("((long)", ")") + case DoubleToFloat | LongToFloat => + p("((float)", ")") + case IntToDouble | LongToDouble | FloatToDouble => + p("((double)", ")") + + case String_length => p("", ".length") + case CheckNotNull => p("", ".notNull") + case Class_name => p("", ".name") + case Class_isPrimitive => p("", ".isPrimitive") + case Class_isInterface => p("", ".isInterface") + case Class_isArray => p("", ".isArray") + case Class_componentType => p("", ".componentType") + case Class_superClass => p("", ".superClass") + case Array_length => p("", ".length") + case GetClass => p("", ".getClass()") + + case Clone => p("(", ")") + case IdentityHashCode => p("(", ")") + case WrapAsThrowable => p("(", ")") + case UnwrapFromThrowable => p("(", ")") + + case Throw => p("throw ", "") } case BinaryOp(BinaryOp.Int_-, IntLiteral(0), rhs) => @@ -525,10 +528,6 @@ object Printers { print(typeRef) printArgs(elems) - case ArrayLength(array) => - print(array) - print(".length") - case ArraySelect(array, index) => print(array) print('[') @@ -564,30 +563,6 @@ object Printers { print(tpe) print(']') - case GetClass(expr) => - print(expr) - print(".getClass()") - - case Clone(expr) => - print("(") - print(expr) - print(')') - - case IdentityHashCode(expr) => - print("(") - print(expr) - print(')') - - case WrapAsThrowable(expr) => - print("(") - print(expr) - print(")") - - case UnwrapFromThrowable(expr) => - print("(") - print(expr) - print(")") - // JavaScript expressions case JSNew(ctor, args) => diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala index da0dc4d01e..66c3249f9e 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala @@ -299,10 +299,6 @@ object Serializers { writeTagAndPos(TagTryFinally) writeTree(block); writeTree(finalizer) - case Throw(expr) => - writeTagAndPos(TagThrow) - writeTree(expr) - case Match(selector, cases, default) => writeTagAndPos(TagMatch) writeTree(selector) @@ -377,10 +373,6 @@ object Serializers { writeTagAndPos(TagArrayValue) writeArrayTypeRef(tpe); writeTrees(elems) - case ArrayLength(array) => - writeTagAndPos(TagArrayLength) - writeTree(array) - case ArraySelect(array, index) => writeTagAndPos(TagArraySelect) writeTree(array); writeTree(index) @@ -403,26 +395,6 @@ object Serializers { writeTagAndPos(TagAsInstanceOf) writeTree(expr); writeType(tpe) - case GetClass(expr) => - writeTagAndPos(TagGetClass) - writeTree(expr) - - case Clone(expr) => - writeTagAndPos(TagClone) - writeTree(expr) - - case IdentityHashCode(expr) => - writeTagAndPos(TagIdentityHashCode) - writeTree(expr) - - case WrapAsThrowable(expr) => - writeTagAndPos(TagWrapAsThrowable) - writeTree(expr) - - case UnwrapFromThrowable(expr) => - writeTagAndPos(TagUnwrapFromThrowable) - writeTree(expr) - case JSNew(ctor, args) => writeTagAndPos(TagJSNew) writeTree(ctor); writeTreeOrJSSpreads(args) @@ -1139,8 +1111,8 @@ object Serializers { * (throw qual.field[null]) = rhs --> qual.field[null] = rhs */ lhs0 match { - case Throw(sel: Select) if sel.tpe == NullType => sel - case _ => lhs0 + case UnaryOp(UnaryOp.Throw, sel: Select) if sel.tpe == NullType => sel + case _ => lhs0 } } else { lhs0 @@ -1170,13 +1142,6 @@ object Serializers { case TagTryFinally => TryFinally(readTree(), readTree()) - case TagThrow => - val expr = readTree() - val patchedExpr = - if (hacks.use8) throwArgumentHack8(expr) - else expr - Throw(patchedExpr) - case TagMatch => Match(readTree(), List.fill(readInt()) { (readTrees().map(_.asInstanceOf[MatchableLiteral]), readTree()) @@ -1207,7 +1172,7 @@ object Serializers { /* Note [Nothing FieldDef rewrite] * qual.field[nothing] --> throw qual.field[null] */ - Throw(Select(qualifier, field)(NullType)) + UnaryOp(UnaryOp.Throw, Select(qualifier, field)(NullType)) } else { Select(qualifier, field)(tpe) } @@ -1246,6 +1211,36 @@ object Serializers { case TagUnaryOp => UnaryOp(readByte(), readTree()) case TagBinaryOp => BinaryOp(readByte(), readTree(), readTree()) + case TagArrayLength | TagGetClass | TagClone | TagIdentityHashCode | + TagWrapAsThrowable | TagUnwrapFromThrowable | TagThrow => + if (!hacks.use17) { + throw new IOException( + s"Illegal legacy node $tag found in class ${enclosingClassName.nameString}") + } + + val lhs = readTree() + def checkNotNullLhs: Tree = UnaryOp(UnaryOp.CheckNotNull, lhs) + + (tag: @switch) match { + case TagArrayLength => + UnaryOp(UnaryOp.Array_length, checkNotNullLhs) + case TagGetClass => + UnaryOp(UnaryOp.GetClass, checkNotNullLhs) + case TagClone => + UnaryOp(UnaryOp.Clone, checkNotNullLhs) + case TagIdentityHashCode => + UnaryOp(UnaryOp.IdentityHashCode, lhs) + case TagWrapAsThrowable => + UnaryOp(UnaryOp.WrapAsThrowable, lhs) + case TagUnwrapFromThrowable => + UnaryOp(UnaryOp.UnwrapFromThrowable, checkNotNullLhs) + case TagThrow => + val patchedLhs = + if (hacks.use8) throwArgumentHack8(lhs) + else lhs + UnaryOp(UnaryOp.Throw, patchedLhs) + } + case TagNewArray => val arrayTypeRef = readArrayTypeRef() val lengths = readTrees() @@ -1279,7 +1274,6 @@ object Serializers { } case TagArrayValue => ArrayValue(readArrayTypeRef(), readTrees()) - case TagArrayLength => ArrayLength(readTree()) case TagArraySelect => ArraySelect(readTree(), readTree())(readType()) case TagRecordValue => RecordValue(readType().asInstanceOf[RecordType], readTrees()) @@ -1299,14 +1293,6 @@ object Serializers { IsInstanceOf(expr, testType) case TagAsInstanceOf => AsInstanceOf(readTree(), readType()) - case TagGetClass => GetClass(readTree()) - case TagClone => Clone(readTree()) - case TagIdentityHashCode => IdentityHashCode(readTree()) - - case TagWrapAsThrowable => - WrapAsThrowable(readTree()) - case TagUnwrapFromThrowable => - UnwrapFromThrowable(readTree()) case TagJSNew => JSNew(readTree(), readTreeOrJSSpreads()) case TagJSPrivateSelect => JSPrivateSelect(readTree(), readFieldIdent()) @@ -1462,10 +1448,13 @@ object Serializers { * `runtime.package$.unwrapJavaScriptException(x)`. */ private def throwArgumentHack8(expr: Tree)(implicit pos: Position): Tree = { + def unwrapFromThrowable(t: Tree): Tree = + UnaryOp(UnaryOp.UnwrapFromThrowable, t) + expr.tpe match { case NullType => // Evaluate the expression then definitely run into an NPE UB - UnwrapFromThrowable(expr) + unwrapFromThrowable(expr) case ClassType(_, _) => expr match { @@ -1476,7 +1465,7 @@ object Serializers { /* Common case (explicit re-throw of the form `throw th`) where we don't need the IIFE. * if (expr === null) unwrapFromThrowable(null) else expr */ - If(BinaryOp(BinaryOp.===, expr, Null()), UnwrapFromThrowable(Null()), expr)(AnyType) + If(BinaryOp(BinaryOp.===, expr, Null()), unwrapFromThrowable(Null()), expr)(AnyType) case _ => /* General case where we need to avoid evaluating `expr` twice. * ((x) => if (x === null) unwrapFromThrowable(null) else x)(expr) @@ -1485,7 +1474,7 @@ object Serializers { val xParamDef = ParamDef(x, OriginalName.NoOriginalName, AnyType, mutable = false) val xRef = xParamDef.ref val closure = Closure(arrow = true, Nil, List(xParamDef), None, { - If(BinaryOp(BinaryOp.===, xRef, Null()), UnwrapFromThrowable(Null()), xRef)(AnyType) + If(BinaryOp(BinaryOp.===, xRef, Null()), unwrapFromThrowable(Null()), xRef)(AnyType) }, Nil) JSFunctionApply(closure, List(expr)) } @@ -1681,6 +1670,12 @@ object Serializers { VarDef(LocalIdent(LocalName(name)), NoOriginalName, vtpe, mutable, rhs) } + def arrayLength(t: Tree)(implicit pos: Position): Tree = + UnaryOp(UnaryOp.Array_length, t) + + def getClass(t: Tree)(implicit pos: Position): Tree = + UnaryOp(UnaryOp.GetClass, t) + val jlClassRef = ClassRef(ClassClass) val intArrayTypeRef = ArrayTypeRef(IntRef, 1) val objectRef = ClassRef(ObjectClass) @@ -1738,7 +1733,7 @@ object Serializers { length, result, innerOffset, - If(BinaryOp(BinaryOp.Int_<, innerOffset.ref, ArrayLength(dimensions.ref)), { + If(BinaryOp(BinaryOp.Int_<, innerOffset.ref, arrayLength(dimensions.ref)), { Block( result2, innerComponentType, @@ -1808,11 +1803,11 @@ object Serializers { Block( outermostComponentType, i, - While(BinaryOp(BinaryOp.Int_!=, i.ref, ArrayLength(lengthsParam.ref)), { + While(BinaryOp(BinaryOp.Int_!=, i.ref, arrayLength(lengthsParam.ref)), { Block( Assign( outermostComponentType.ref, - GetClass(Apply(EAF, This()(ClassType(ReflectArrayModClass, nullable = false)), + getClass(Apply(EAF, This()(ClassType(ReflectArrayModClass, nullable = false)), MethodIdent(newInstanceSingleName), List(outermostComponentType.ref, IntLiteral(0)))(AnyType)) ), @@ -1942,7 +1937,7 @@ object Serializers { */ assert(args.size == 1) - val patchedBody = Some(IdentityHashCode(args(0).ref)) + val patchedBody = Some(UnaryOp(UnaryOp.IdentityHashCode, args(0).ref)) val patchedOptimizerHints = OptimizerHints.empty.withInline(true) MethodDef(flags, name, originalName, args, resultType, patchedBody)( @@ -1967,11 +1962,13 @@ object Serializers { val patchedBody = Some { If(IsInstanceOf(thisValue, cloneableClassType.toNonNullable), - Clone(AsInstanceOf(thisValue, cloneableClassType)), - Throw(New( - HackNames.CloneNotSupportedExceptionClass, - MethodIdent(NoArgConstructorName), - Nil)))(cloneableClassType) + UnaryOp(UnaryOp.Clone, + UnaryOp(UnaryOp.CheckNotNull, AsInstanceOf(thisValue, cloneableClassType))), + UnaryOp(UnaryOp.Throw, + New( + HackNames.CloneNotSupportedExceptionClass, + MethodIdent(NoArgConstructorName), + Nil)))(cloneableClassType) } val patchedOptimizerHints = OptimizerHints.empty.withInline(true) diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala index 5aa07f32ff..7be426e058 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Transformers.scala @@ -73,9 +73,6 @@ object Transformers { case TryFinally(block, finalizer) => TryFinally(transform(block), transform(finalizer)) - case Throw(expr) => - Throw(transform(expr)) - case Match(selector, cases, default) => Match(transform(selector), cases.map(c => (c._1, transform(c._2))), transform(default))(tree.tpe) @@ -114,9 +111,6 @@ object Transformers { case ArrayValue(tpe, elems) => ArrayValue(tpe, transformTrees(elems)) - case ArrayLength(array) => - ArrayLength(transform(array)) - case ArraySelect(array, index) => ArraySelect(transform(array), transform(index))(tree.tpe) @@ -132,21 +126,6 @@ object Transformers { case AsInstanceOf(expr, tpe) => AsInstanceOf(transform(expr), tpe) - case GetClass(expr) => - GetClass(transform(expr)) - - case Clone(expr) => - Clone(transform(expr)) - - case IdentityHashCode(expr) => - IdentityHashCode(transform(expr)) - - case WrapAsThrowable(expr) => - WrapAsThrowable(transform(expr)) - - case UnwrapFromThrowable(expr) => - UnwrapFromThrowable(transform(expr)) - // JavaScript expressions case JSNew(ctor, args) => diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala b/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala index ec39bb1086..d242bbfce7 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Traversers.scala @@ -64,9 +64,6 @@ object Traversers { traverse(block) traverse(finalizer) - case Throw(expr) => - traverse(expr) - case Match(selector, cases, default) => traverse(selector) cases foreach (c => (c._1 map traverse, traverse(c._2))) @@ -107,9 +104,6 @@ object Traversers { case ArrayValue(tpe, elems) => elems foreach traverse - case ArrayLength(array) => - traverse(array) - case ArraySelect(array, index) => traverse(array) traverse(index) @@ -126,21 +120,6 @@ object Traversers { case AsInstanceOf(expr, _) => traverse(expr) - case GetClass(expr) => - traverse(expr) - - case Clone(expr) => - traverse(expr) - - case IdentityHashCode(expr) => - traverse(expr) - - case WrapAsThrowable(expr) => - traverse(expr) - - case UnwrapFromThrowable(expr) => - traverse(expr) - // JavaScript expressions case JSNew(ctor, args) => diff --git a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala index 917df781ed..6cd4db23c7 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala @@ -188,10 +188,6 @@ object Trees { val tpe = block.tpe } - sealed case class Throw(expr: Tree)(implicit val pos: Position) extends Tree { - val tpe = NothingType - } - /** A break-free switch (without fallthrough behavior). * * Unlike a JavaScript switch, it can be used in expression position. @@ -280,12 +276,24 @@ object Trees { } /** Unary operation. + * + * All unary operations follow common evaluation steps: + * + * 1. Let `lhsValue` be the result of evaluating `lhs`. + * 2. Perform an operation that depends on `op` and `lhsValue`. * * The `Class_x` operations take a `jl.Class!` argument, i.e., a * non-nullable `jl.Class`. * + * Likewise, `Array_length`, `GetClass`, `Clone` and `UnwrapFromThrowable` + * take arguments of non-nullable types. + * * `CheckNotNull` throws NPEs subject to UB. * + * `Throw` always throws, obviously. + * + * `Clone` and `WrapAsThrowable` are side-effect-free but not pure. + * * Otherwise, unary operations preserve pureness. */ sealed case class UnaryOp(op: UnaryOp.Code, lhs: Tree)( @@ -337,9 +345,26 @@ object Trees { final val Class_componentType = 23 final val Class_superClass = 24 + // Misc ops introduced in 1.18, which used to have dedicated IR nodes + final val Array_length = 25 + final val GetClass = 26 + final val Clone = 27 + final val IdentityHashCode = 28 + final val WrapAsThrowable = 29 + final val UnwrapFromThrowable = 30 + final val Throw = 31 + def isClassOp(op: Code): Boolean = op >= Class_name && op <= Class_superClass + def isPureOp(op: Code): Boolean = (op: @switch) match { + case CheckNotNull | Clone | WrapAsThrowable | Throw => false + case _ => true + } + + def isSideEffectFreeOp(op: Code): Boolean = + op != CheckNotNull && op != Throw + def resultTypeOf(op: Code, argType: Type): Type = (op: @switch) match { case Boolean_! | Class_isPrimitive | Class_isInterface | Class_isArray => BooleanType @@ -349,7 +374,8 @@ object Trees { ByteType case IntToShort => ShortType - case CharToInt | ByteToInt | ShortToInt | LongToInt | DoubleToInt | String_length => + case CharToInt | ByteToInt | ShortToInt | LongToInt | DoubleToInt | + String_length | Array_length | IdentityHashCode => IntType case IntToLong | DoubleToLong => LongType @@ -357,12 +383,18 @@ object Trees { FloatType case IntToDouble | LongToDouble | FloatToDouble => DoubleType - case CheckNotNull => + case CheckNotNull | Clone => argType.toNonNullable case Class_name => StringType - case Class_componentType | Class_superClass => + case Class_componentType | Class_superClass | GetClass => ClassType(ClassClass, nullable = true) + case WrapAsThrowable => + ClassType(ThrowableClass, nullable = false) + case UnwrapFromThrowable => + AnyType + case Throw => + NothingType } } @@ -526,11 +558,6 @@ object Trees { val tpe = ArrayType(typeRef, nullable = false) } - sealed case class ArrayLength(array: Tree)(implicit val pos: Position) - extends Tree { - val tpe = IntType - } - sealed case class ArraySelect(array: Tree, index: Tree)(val tpe: Type)( implicit val pos: Position) extends AssignLhs @@ -552,32 +579,6 @@ object Trees { implicit val pos: Position) extends Tree - sealed case class GetClass(expr: Tree)(implicit val pos: Position) - extends Tree { - val tpe = ClassType(ClassClass, nullable = true) - } - - sealed case class Clone(expr: Tree)(implicit val pos: Position) - extends Tree { - // this is OK because our type system does not have singleton types - val tpe: Type = expr.tpe.toNonNullable - } - - sealed case class IdentityHashCode(expr: Tree)(implicit val pos: Position) - extends Tree { - val tpe = IntType - } - - sealed case class WrapAsThrowable(expr: Tree)(implicit val pos: Position) - extends Tree { - val tpe = ClassType(ThrowableClass, nullable = false) - } - - sealed case class UnwrapFromThrowable(expr: Tree)(implicit val pos: Position) - extends Tree { - val tpe = AnyType - } - // JavaScript expressions sealed case class JSNew(ctor: Tree, args: List[TreeOrJSSpread])( @@ -826,11 +827,7 @@ object Trees { val tpe = VoidType } - /** Unary operation (always preserves pureness). - * - * Operations which do not preserve pureness are not allowed in this tree. - * These are notably ++ and -- - */ + /** JavaScript unary operation. */ sealed case class JSUnaryOp(op: JSUnaryOp.Code, lhs: Tree)( implicit val pos: Position) extends Tree { val tpe = JSUnaryOp.resultTypeOf(op) @@ -851,11 +848,7 @@ object Trees { AnyType } - /** Binary operation (always preserves pureness). - * - * Operations which do not preserve pureness are not allowed in this tree. - * These are notably +=, -=, *=, /= and %= - */ + /** JavaScript binary operation. */ sealed case class JSBinaryOp(op: JSBinaryOp.Code, lhs: Tree, rhs: Tree)( implicit val pos: Position) extends Tree { val tpe = JSBinaryOp.resultTypeOf(op) diff --git a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala index 052db4b9da..0b2cb5a9b6 100644 --- a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala +++ b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala @@ -267,10 +267,6 @@ class PrintersTest { TryFinally(TryCatch(i(5), "e", NON, i(6))(IntType), i(7))) } - @Test def printThrow(): Unit = { - assertPrintEquals("throw null", Throw(Null())) - } - @Test def printMatch(): Unit = { assertPrintEquals( """ @@ -403,6 +399,14 @@ class PrintersTest { assertPrintEquals("x.isArray", UnaryOp(Class_isArray, classVarRef)) assertPrintEquals("x.componentType", UnaryOp(Class_componentType, classVarRef)) assertPrintEquals("x.superClass", UnaryOp(Class_superClass, classVarRef)) + + assertPrintEquals("x.length", UnaryOp(Array_length, ref("x", arrayType(IntRef, 1)))) + assertPrintEquals("x.getClass()", UnaryOp(GetClass, ref("x", AnyType))) + assertPrintEquals("(x)", UnaryOp(Clone, ref("x", arrayType(ObjectClass, 1)))) + assertPrintEquals("(x)", UnaryOp(IdentityHashCode, ref("x", AnyType))) + assertPrintEquals("(e)", UnaryOp(WrapAsThrowable, ref("e", AnyType))) + assertPrintEquals("(e)", + UnaryOp(UnwrapFromThrowable, ref("e", ClassType(ThrowableClass, nullable = true)))) } @Test def printPseudoUnaryOp(): Unit = { @@ -574,10 +578,6 @@ class PrintersTest { ArrayValue(ArrayTypeRef(IntRef, 2), List(Null()))) } - @Test def printArrayLength(): Unit = { - assertPrintEquals("x.length", ArrayLength(ref("x", arrayType(IntRef, 1)))) - } - @Test def printArraySelect(): Unit = { assertPrintEquals("x[3]", ArraySelect(ref("x", arrayType(IntRef, 1)), i(3))(IntType)) @@ -606,27 +606,6 @@ class PrintersTest { AsInstanceOf(ref("x", AnyType), IntType)) } - @Test def printGetClass(): Unit = { - assertPrintEquals("x.getClass()", GetClass(ref("x", AnyType))) - } - - @Test def printClone(): Unit = { - assertPrintEquals("(x)", Clone(ref("x", arrayType(ObjectClass, 1)))) - } - - @Test def printIdentityHashCode(): Unit = { - assertPrintEquals("(x)", IdentityHashCode(ref("x", AnyType))) - } - - @Test def printWrapAsThrowable(): Unit = { - assertPrintEquals("(e)", WrapAsThrowable(ref("e", AnyType))) - } - - @Test def printUnwrapFromThrowable(): Unit = { - assertPrintEquals("(e)", - UnwrapFromThrowable(ref("e", ClassType(ThrowableClass, nullable = true)))) - } - @Test def printJSNew(): Unit = { assertPrintEquals("new C()", JSNew(ref("C", AnyType), Nil)) assertPrintEquals("new C(4, 5)", JSNew(ref("C", AnyType), List(i(4), i(5)))) diff --git a/linker/jvm/src/test/scala/org/scalajs/linker/RunTest.scala b/linker/jvm/src/test/scala/org/scalajs/linker/RunTest.scala index 0f7f6a628a..5c49da7dee 100644 --- a/linker/jvm/src/test/scala/org/scalajs/linker/RunTest.scala +++ b/linker/jvm/src/test/scala/org/scalajs/linker/RunTest.scala @@ -71,7 +71,7 @@ class RunTest { val classDefs = Seq( mainTestClassDef(Block( VarDef("e", NON, ClassType(ThrowableClass, nullable = true), mutable = false, - WrapAsThrowable(JSNew(JSGlobalRef("RangeError"), List(str("boom"))))), + UnaryOp(UnaryOp.WrapAsThrowable, JSNew(JSGlobalRef("RangeError"), List(str("boom"))))), genAssert(IsInstanceOf(e, ClassType("java.lang.Exception", nullable = false))), genAssertEquals(str("RangeError: boom"), Apply(EAF, e, getMessage, Nil)(ClassType(BoxedStringClass, nullable = true))) @@ -87,7 +87,7 @@ class RunTest { private def genAssert(test: Tree): Tree = { If(UnaryOp(UnaryOp.Boolean_!, test), - Throw(JSNew(JSGlobalRef("Error"), List(str("Assertion failed")))), + UnaryOp(UnaryOp.Throw, JSNew(JSGlobalRef("Error"), List(str("Assertion failed")))), Skip())( VoidType) } diff --git a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala index 84a8d8a630..1458e107f4 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/analyzer/Infos.scala @@ -684,6 +684,13 @@ object Infos { op match { case Class_superClass => builder.addUsedClassSuperClass() + case GetClass => + builder.addAccessedClassClass() + case WrapAsThrowable => + builder.addUsedInstanceTest(ThrowableClass) + builder.addInstantiatedClass(JavaScriptExceptionClass, AnyArgConstructorName) + case UnwrapFromThrowable => + builder.addUsedInstanceTest(JavaScriptExceptionClass) case _ => // do nothing } @@ -727,16 +734,6 @@ object Infos { builder.maybeAddAccessedClassData(cls) builder.addAccessedClassClass() - case GetClass(_) => - builder.addAccessedClassClass() - - case WrapAsThrowable(_) => - builder.addUsedInstanceTest(ThrowableClass) - builder.addInstantiatedClass(JavaScriptExceptionClass, AnyArgConstructorName) - - case UnwrapFromThrowable(_) => - builder.addUsedInstanceTest(JavaScriptExceptionClass) - case JSPrivateSelect(_, field) => builder.addStaticallyReferencedClass(field.name.className) // for the private name of the field builder.addFieldRead(field.name) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala index 77e47c1fb4..467e73d68b 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/CoreJSLib.scala @@ -712,23 +712,16 @@ private[emitter] object CoreJSLib { Return(constantClassResult(BoxedUnitClass)) } ), { - If(instance === Null(), { - if (nullPointers == CheckedBehavior.Unchecked) - Return(genApply(instance, getClassMethodName, Nil)) - else - genCallHelper(VarField.throwNullPointerException) + If(genIsInstanceOfHijackedClass(instance, BoxedLongClass), { + Return(constantClassResult(BoxedLongClass)) }, { - If(genIsInstanceOfHijackedClass(instance, BoxedLongClass), { - Return(constantClassResult(BoxedLongClass)) + If(genIsInstanceOfHijackedClass(instance, BoxedCharacterClass), { + Return(constantClassResult(BoxedCharacterClass)) }, { - If(genIsInstanceOfHijackedClass(instance, BoxedCharacterClass), { - Return(constantClassResult(BoxedCharacterClass)) + If(genIsScalaJSObject(instance), { + Return(scalaObjectResult(instance)) }, { - If(genIsScalaJSObject(instance), { - Return(scalaObjectResult(instance)) - }, { - Return(jsObjectResult) - }) + Return(jsObjectResult) }) }) }) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala index 89516298bb..12af2ad9ab 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/FunctionEmitter.scala @@ -1031,8 +1031,10 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { } result - case UnaryOp(op, lhs) if op != UnaryOp.CheckNotNull || noExtractYet => + case arg @ UnaryOp(op, lhs) + if canUnaryOpBeExpression(arg) && (UnaryOp.isPureOp(op) || noExtractYet) => UnaryOp(op, rec(lhs)) + case BinaryOp(op, lhs, rhs) => val newRhs = rec(rhs) BinaryOp(op, rec(lhs), newRhs) @@ -1083,8 +1085,6 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { ApplyStatic(flags, className, method, recs(args))(arg.tpe) case ApplyDynamicImport(flags, className, method, args) if noExtractYet => ApplyDynamicImport(flags, className, method, recs(args)) - case ArrayLength(array) if noExtractYet => - ArrayLength(rec(array)) case ArraySelect(array, index) if noExtractYet => val newIndex = rec(index) ArraySelect(rec(array), newIndex)(arg.tpe) @@ -1219,6 +1219,22 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { } } + private def canUnaryOpBeExpression(tree: UnaryOp): Boolean = { + import UnaryOp._ + + tree.op match { + case Throw => + false + case WrapAsThrowable | UnwrapFromThrowable => + tree.lhs match { + case VarRef(_) | Transient(JSVarRef(_, _)) => true + case _ => false + } + case _ => + true + } + } + /** Common implementation for the functions below. * A pure expression can be moved around or executed twice, because it * will always produce the same result and never have side-effects. @@ -1255,6 +1271,16 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case Transient(JSVarRef(_, mutable)) => allowUnpure || !mutable + case tree @ UnaryOp(op, lhs) if canUnaryOpBeExpression(tree) => + if (op == UnaryOp.CheckNotNull) + testNPE(lhs) + else if (UnaryOp.isPureOp(op)) + test(lhs) + else if (UnaryOp.isSideEffectFreeOp(op)) + allowUnpure && test(lhs) + else + allowSideEffects && test(lhs) + // Division and modulo, preserve pureness unless they can divide by 0 case BinaryOp(BinaryOp.Int_/ | BinaryOp.Int_%, lhs, rhs) if !allowSideEffects => rhs match { @@ -1281,16 +1307,8 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case Block(trees) => trees forall test case If(cond, thenp, elsep) => test(cond) && test(thenp) && test(elsep) case BinaryOp(_, lhs, rhs) => test(lhs) && test(rhs) - case UnaryOp(op, lhs) => if (op == UnaryOp.CheckNotNull) testNPE(lhs) else test(lhs) - case ArrayLength(array) => testNPE(array) case RecordSelect(record, _) => test(record) case IsInstanceOf(expr, _) => test(expr) - case IdentityHashCode(expr) => test(expr) - case GetClass(arg) => testNPE(arg) - - // Expressions preserving pureness (modulo NPE) but requiring that expr be a var - case WrapAsThrowable(expr @ (VarRef(_) | Transient(JSVarRef(_, _)))) => test(expr) - case UnwrapFromThrowable(expr @ (VarRef(_) | Transient(JSVarRef(_, _)))) => testNPE(expr) // Transients preserving pureness (modulo NPE) case Transient(Cast(expr, _)) => @@ -1298,7 +1316,7 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case Transient(ZeroOf(runtimeClass)) => test(runtimeClass) // ZeroOf *assumes* that `runtimeClass ne null` case Transient(ObjectClassName(obj)) => - testNPE(obj) + test(obj) // Expressions preserving side-effect freedom (modulo NPE) case Select(qualifier, _) => @@ -1307,8 +1325,6 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { allowUnpure case ArrayValue(tpe, elems) => allowUnpure && (elems forall test) - case Clone(arg) => - allowUnpure && testNPE(arg) case JSArrayConstr(items) => allowUnpure && (items.forall(testJSArg)) case tree @ JSObjectConstr(items) => @@ -1685,9 +1701,6 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { js.TryFinally(newBlock, newFinalizer) } - case Throw(expr) => - pushLhsInto(Lhs.Throw, expr, tailPosLabels) - /** Matches are desugared into switches * * A match is different from a switch in two respects, both linked @@ -1757,8 +1770,22 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { } case UnaryOp(op, lhs) => - unnest(lhs) { (newLhs, env) => - redo(UnaryOp(op, newLhs))(env) + op match { + case UnaryOp.Throw => + pushLhsInto(Lhs.Throw, lhs, tailPosLabels) + + case UnaryOp.WrapAsThrowable | UnaryOp.UnwrapFromThrowable => + unnest(lhs) { (newLhs, newEnv) => + implicit val env = newEnv + withTempJSVar(newLhs) { varRef => + redo(UnaryOp(op, varRef)) + } + } + + case _ => + unnest(lhs) { (newLhs, env) => + redo(UnaryOp(op, newLhs))(env) + } } case BinaryOp(op, lhs, rhs) => @@ -1776,11 +1803,6 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { redo(ArrayValue(tpe, newElems))(env) } - case ArrayLength(array) => - unnest(array) { (newArray, env) => - redo(ArrayLength(newArray))(env) - } - case ArraySelect(array, index) => unnest(checkNotNull(array), index) { (newArray, newIndex, env) => redo(ArraySelect(newArray, newIndex)(rhs.tpe))(env) @@ -1801,37 +1823,6 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { redo(AsInstanceOf(newExpr, tpe))(env) } - case GetClass(expr) => - unnest(expr) { (newExpr, env) => - redo(GetClass(newExpr))(env) - } - - case Clone(expr) => - unnest(expr) { (newExpr, env) => - redo(Clone(newExpr))(env) - } - - case IdentityHashCode(expr) => - unnest(expr) { (newExpr, env) => - redo(IdentityHashCode(newExpr))(env) - } - - case WrapAsThrowable(expr) => - unnest(expr) { (newExpr, newEnv) => - implicit val env = newEnv - withTempJSVar(newExpr) { varRef => - redo(WrapAsThrowable(varRef)) - } - } - - case UnwrapFromThrowable(expr) => - unnest(expr) { (newExpr, newEnv) => - implicit val env = newEnv - withTempJSVar(newExpr) { varRef => - redo(UnwrapFromThrowable(varRef)) - } - } - case Transient(Cast(expr, tpe)) => unnest(expr) { (newExpr, env) => redo(Transient(Cast(newExpr, tpe)))(env) @@ -2429,6 +2420,66 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { js.Apply(genGetDataOf(newLhs) DOT cpn.getComponentType, Nil) case Class_superClass => js.Apply(genGetDataOf(newLhs) DOT cpn.getSuperclass, Nil) + + case Array_length => + genIdentBracketSelect( + genSyntheticPropSelect(newLhs, SyntheticProperty.u), + "length") + + case GetClass => + genCallHelper(VarField.objectGetClass, newLhs) + + case Clone => + lhs.tpe match { + /* If the argument is known to be an array, directly call its + * `clone__O` method. + * This happens all the time when calling `clone()` on an array, + * since the optimizer will inline `java.lang.Object.clone()` in + * those cases, leaving a `Clone()` node an array. + */ + case _: ArrayType => + genApply(newLhs, cloneMethodName, Nil) + + /* Otherwise, if it might be an array, use the full dispatcher. + * In theory, only the `CloneableClass` case is required, since + * `Clone` only accepts values of type `Cloneable`. However, since + * the inliner does not always refine the type of receivers, we + * also account for other supertypes of array types. There is a + * similar issue for CharSequenceClass in `Apply` nodes. + * + * TODO Is the above comment still relevant now that the optimizer + * is type-preserving? + * + * In practice, this only happens in the (non-inlined) definition + * of `java.lang.Object.clone()` itself, since everywhere else it + * is inlined in contexts where the receiver has a more precise + * type. + */ + case ClassType(CloneableClass, _) | ClassType(SerializableClass, _) | + ClassType(ObjectClass, _) | AnyType | AnyNotNullType => + genCallHelper(VarField.objectOrArrayClone, newLhs) + + // Otherwise, it is known not to be an array. + case _ => + genCallHelper(VarField.objectClone, newLhs) + } + + case IdentityHashCode => + genCallHelper(VarField.systemIdentityHashCode, newLhs) + + case WrapAsThrowable => + val newLhsVar = newLhs.asInstanceOf[js.VarRef] + js.If( + genIsInstanceOfClass(newLhsVar, ThrowableClass), + newLhsVar, + genScalaClassNew(JavaScriptExceptionClass, AnyArgConstructorName, newLhsVar)) + + case UnwrapFromThrowable => + val newLhsVar = newLhs.asInstanceOf[js.VarRef] + js.If( + genIsInstanceOfClass(newLhsVar, JavaScriptExceptionClass), + genSelect(newLhsVar, FieldIdent(exceptionFieldName)), + newLhsVar) } case BinaryOp(op, lhs, rhs) => @@ -2739,12 +2790,6 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { extractWithGlobals( genArrayValue(typeRef, elems.map(transformExpr(_, preserveChar)))) - case ArrayLength(array) => - val newArray = transformExprNoChar(checkNotNull(array)) - genIdentBracketSelect( - genSyntheticPropSelect(newArray, SyntheticProperty.u), - "length") - case ArraySelect(array, index) => val newArray = transformExprNoChar(checkNotNull(array)) val newIndex = transformExprNoChar(index) @@ -2764,62 +2809,6 @@ private[emitter] class FunctionEmitter(sjsGen: SJSGen) { case AsInstanceOf(expr, tpe) => extractWithGlobals(genAsInstanceOf(transformExprNoChar(expr), tpe)) - case GetClass(expr) => - genCallHelper(VarField.objectGetClass, transformExprNoChar(expr)) - - case Clone(expr) => - val newExpr = transformExprNoChar(checkNotNull(expr)) - expr.tpe match { - /* If the argument is known to be an array, directly call its - * `clone__O` method. - * This happens all the time when calling `clone()` on an array, - * since the optimizer will inline `java.lang.Object.clone()` in - * those cases, leaving a `Clone()` node an array. - */ - case _: ArrayType => - genApply(newExpr, cloneMethodName, Nil) - - /* Otherwise, if it might be an array, use the full dispatcher. - * In theory, only the `CloneableClass` case is required, since - * `Clone` only accepts values of type `Cloneable`. However, since - * the inliner does not always refine the type of receivers, we - * also account for other supertypes of array types. There is a - * similar issue for CharSequenceClass in `Apply` nodes. - * - * TODO Is the above comment still relevant now that the optimizer - * is type-preserving? - * - * In practice, this only happens in the (non-inlined) definition - * of `java.lang.Object.clone()` itself, since everywhere else it - * is inlined in contexts where the receiver has a more precise - * type. - */ - case ClassType(CloneableClass, _) | ClassType(SerializableClass, _) | - ClassType(ObjectClass, _) | AnyType | AnyNotNullType => - genCallHelper(VarField.objectOrArrayClone, newExpr) - - // Otherwise, it is known not to be an array. - case _ => - genCallHelper(VarField.objectClone, newExpr) - } - - case IdentityHashCode(expr) => - genCallHelper(VarField.systemIdentityHashCode, transformExprNoChar(expr)) - - case WrapAsThrowable(expr) => - val newExpr = transformExprNoChar(expr).asInstanceOf[js.VarRef] - js.If( - genIsInstanceOfClass(newExpr, ThrowableClass), - newExpr, - genScalaClassNew(JavaScriptExceptionClass, AnyArgConstructorName, newExpr)) - - case UnwrapFromThrowable(expr) => - val newExpr = transformExprNoChar(expr).asInstanceOf[js.VarRef] - js.If( - genIsInstanceOfClass(newExpr, JavaScriptExceptionClass), - genSelect(newExpr, FieldIdent(exceptionFieldName)), - genCheckNotNull(newExpr)) - case prop: LinkTimeProperty => transformExpr( config.coreSpec.linkTimeProperties.transformLinkTimeProperty(prop), diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Transients.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Transients.scala index 61f9ece247..00301771cc 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Transients.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/emitter/Transients.scala @@ -137,9 +137,7 @@ object Transients { /** Intrinsic for `obj.getClass().getName()`. * - * This node accepts any value for `obj`, including `null`. Its - * implementation takes care of throwing `NullPointerException`s as - * required. + * The argument's type must conform to `AnyNotNullType`. */ final case class ObjectClassName(obj: Tree) extends Transient.Value { val tpe: Type = StringType diff --git a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala index 22332110ac..bf0e54adc5 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/backend/wasmemitter/FunctionEmitter.scala @@ -537,7 +537,6 @@ private class FunctionEmitter private ( case t: ApplyDynamicImport => genApplyDynamicImport(t) case t: IsInstanceOf => genIsInstanceOf(t) case t: AsInstanceOf => genAsInstanceOf(t) - case t: GetClass => genGetClass(t) case t: Block => genBlock(t, expectedType) case t: Labeled => unwinding.genLabeled(t, expectedType) case t: Return => unwinding.genReturn(t) @@ -551,14 +550,9 @@ private class FunctionEmitter private ( case t: ForIn => genForIn(t) case t: TryCatch => genTryCatch(t, expectedType) case t: TryFinally => unwinding.genTryFinally(t, expectedType) - case t: Throw => genThrow(t) case t: Match => genMatch(t, expectedType) case t: Debugger => VoidType // ignore case t: Skip => VoidType - case t: Clone => genClone(t) - case t: IdentityHashCode => genIdentityHashCode(t) - case t: WrapAsThrowable => genWrapAsThrowable(t) - case t: UnwrapFromThrowable => genUnwrapFromThrowable(t) case t: LinkTimeProperty => genLinkTimeProperty(t) // JavaScript expressions @@ -581,7 +575,6 @@ private class FunctionEmitter private ( case t: Closure => genClosure(t) // array - case t: ArrayLength => genArrayLength(t) case t: NewArray => genNewArray(t) case t: ArraySelect => genArraySelect(t) case t: ArrayValue => genArrayValue(t) @@ -1370,11 +1363,27 @@ private class FunctionEmitter private ( } private def genUnaryOp(tree: UnaryOp): Type = { + // scalastyle:off return + import UnaryOp._ val UnaryOp(op, lhs) = tree - genTreeAuto(lhs) + /* Touch of peephole optimization; useful so that the various operators can + * assume the NothingType case away (e.g., `Array_length`, `Clone`). + */ + if (lhs.tpe == NothingType) { + genTreeAuto(lhs) + return NothingType + } + + // scalastyle:on return + + genTree(lhs, op match { + case IdentityHashCode => AnyNotNullType + case WrapAsThrowable | Throw => AnyType + case _ => lhs.tpe + }) markPosition(tree) @@ -1451,6 +1460,107 @@ private class FunctionEmitter private ( fb += wa.Call(genFunctionID.getComponentType) case Class_superClass => fb += wa.Call(genFunctionID.getSuperClass) + + case Array_length => + val ArrayType(arrayTypeRef, _) = lhs.tpe: @unchecked + fb += wa.StructGet( + genTypeID.forArrayClass(arrayTypeRef), + genFieldID.objStruct.arrayUnderlying + ) + fb += wa.ArrayLen + + case GetClass => + val needHijackedClassDispatch = lhs.tpe match { + case ClassType(className, _) => + ctx.getClassInfo(className).isAncestorOfHijackedClass + case ArrayType(_, _) => + false + case _ => + true + } + + if (!needHijackedClassDispatch) { + fb += wa.StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.vtable) + fb += wa.Call(genFunctionID.getClassOf) + } else { + genAdapt(lhs.tpe, AnyNotNullType) // no-op when the optimizer is enabled + fb += wa.Call(genFunctionID.anyGetClass) + } + + case Clone => + val lhsLocal = addSyntheticLocal(watpe.RefType(genTypeID.ObjectStruct)) + fb += wa.LocalTee(lhsLocal) + fb += wa.LocalGet(lhsLocal) + fb += wa.StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.vtable) + fb += wa.StructGet(genTypeID.typeData, genFieldID.typeData.cloneFunction) + // cloneFunction: (ref jl.Object) -> (ref jl.Object) + fb += wa.CallRef(genTypeID.cloneFunctionType) + + // cast the (ref jl.Object) back down to the result type + transformSingleType(lhs.tpe) match { + case watpe.RefType(_, watpe.HeapType.Type(genTypeID.ObjectStruct)) => + // no need to cast to (ref null? jl.Object) + case wasmType: watpe.RefType => + fb += wa.RefCast(wasmType.toNonNullable) + case wasmType => + // Since no hijacked class extends jl.Cloneable, this case cannot happen + throw new AssertionError( + s"Unexpected type for Clone: ${lhs.tpe} (Wasm: $wasmType)") + } + + case IdentityHashCode => + // TODO Avoid dispatch when we know a more precise type than any + fb += wa.Call(genFunctionID.identityHashCode) + + case WrapAsThrowable => + val nonNullThrowableType = watpe.RefType(genTypeID.ThrowableStruct) + val jsExceptionType = watpe.RefType(genTypeID.JSExceptionStruct) + + val anyRefToNonNullThrowable = + Sig(List(watpe.RefType.anyref), List(nonNullThrowableType)) + fb.block(anyRefToNonNullThrowable) { doneLabel => + // if expr.isInstanceOf[Throwable], then br $done + fb += wa.BrOnCast(doneLabel, watpe.RefType.anyref, nonNullThrowableType) + + // otherwise, wrap in a new JavaScriptException + + val lhsLocal = addSyntheticLocal(watpe.RefType.anyref) + val instanceLocal = addSyntheticLocal(jsExceptionType) + + fb += wa.LocalSet(lhsLocal) + fb += wa.Call(genFunctionID.newDefault(SpecialNames.JSExceptionClass)) + fb += wa.LocalTee(instanceLocal) + fb += wa.LocalGet(lhsLocal) + fb += wa.Call( + genFunctionID.forMethod( + MemberNamespace.Constructor, + SpecialNames.JSExceptionClass, + SpecialNames.AnyArgConstructorName + ) + ) + fb += wa.LocalGet(instanceLocal) + } + + case UnwrapFromThrowable => + val nonNullThrowableToAnyRef = + Sig(List(watpe.RefType(genTypeID.ThrowableStruct)), List(watpe.RefType.anyref)) + fb.block(nonNullThrowableToAnyRef) { doneLabel => + // if !expr.isInstanceOf[js.JavaScriptException], then br $done + fb += wa.BrOnCastFail( + doneLabel, + watpe.RefType(genTypeID.ThrowableStruct), + watpe.RefType(genTypeID.JSExceptionStruct) + ) + // otherwise, unwrap the JavaScriptException by reading its field + fb += wa.StructGet( + genTypeID.JSExceptionStruct, + genFieldID.forClassInstanceField(SpecialNames.exceptionFieldName) + ) + } + + case Throw => + fb += wa.ExternConvertAny + fb += wa.Throw(genTagID.exception) } tree.tpe @@ -2287,41 +2397,6 @@ private class FunctionEmitter private ( } } - private def genGetClass(tree: GetClass): Type = { - /* Unlike in `genApply` or `genStringConcat`, here we make no effort to - * optimize known-primitive receivers. In practice, such cases would be - * useless. - */ - - val GetClass(expr) = tree - - val needHijackedClassDispatch = expr.tpe match { - case ClassType(className, _) => - ctx.getClassInfo(className).isAncestorOfHijackedClass - case ArrayType(_, _) | NothingType | NullType => - false - case _ => - true - } - - if (!needHijackedClassDispatch) { - val typeDataLocal = addSyntheticLocal(watpe.RefType(genTypeID.typeData)) - - genTreeAuto(expr) - markPosition(tree) - genCheckNonNullFor(expr) - fb += wa.StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.vtable) - fb += wa.Call(genFunctionID.getClassOf) - } else { - genTreeToAny(expr) - markPosition(tree) - genAsNonNullOrNPEFor(expr) - fb += wa.Call(genFunctionID.anyGetClass) - } - - tree.tpe - } - private def genReadStorage(storage: VarStorage): Unit = { storage match { case VarStorage.Local(localID) => @@ -2491,17 +2566,6 @@ private class FunctionEmitter private ( expectedType } - private def genThrow(tree: Throw): Type = { - val Throw(expr) = tree - - genTree(expr, AnyType) - markPosition(tree) - fb += wa.ExternConvertAny - fb += wa.Throw(genTagID.exception) - - NothingType - } - private def genBlock(tree: Block, expectedType: Type): Type = { val Block(stats) = tree @@ -2595,81 +2659,6 @@ private class FunctionEmitter private ( ClassType(boxClassName, nullable = false) } - private def genIdentityHashCode(tree: IdentityHashCode): Type = { - val IdentityHashCode(expr) = tree - - // TODO Avoid dispatch when we know a more precise type than any - genTree(expr, AnyType) - - markPosition(tree) - fb += wa.Call(genFunctionID.identityHashCode) - - IntType - } - - private def genWrapAsThrowable(tree: WrapAsThrowable): Type = { - val WrapAsThrowable(expr) = tree - - val nonNullThrowableType = watpe.RefType(genTypeID.ThrowableStruct) - val jsExceptionType = watpe.RefType(genTypeID.JSExceptionStruct) - - fb.block(nonNullThrowableType) { doneLabel => - genTree(expr, AnyType) - - markPosition(tree) - - // if expr.isInstanceOf[Throwable], then br $done - fb += wa.BrOnCast(doneLabel, watpe.RefType.anyref, nonNullThrowableType) - - // otherwise, wrap in a new JavaScriptException - - val exprLocal = addSyntheticLocal(watpe.RefType.anyref) - val instanceLocal = addSyntheticLocal(jsExceptionType) - - fb += wa.LocalSet(exprLocal) - fb += wa.Call(genFunctionID.newDefault(SpecialNames.JSExceptionClass)) - fb += wa.LocalTee(instanceLocal) - fb += wa.LocalGet(exprLocal) - fb += wa.Call( - genFunctionID.forMethod( - MemberNamespace.Constructor, - SpecialNames.JSExceptionClass, - SpecialNames.AnyArgConstructorName - ) - ) - fb += wa.LocalGet(instanceLocal) - } - - tree.tpe - } - - private def genUnwrapFromThrowable(tree: UnwrapFromThrowable): Type = { - val UnwrapFromThrowable(expr) = tree - - fb.block(watpe.RefType.anyref) { doneLabel => - genTreeAuto(expr) - - markPosition(tree) - - genAsNonNullOrNPEFor(expr) - - // if !expr.isInstanceOf[js.JavaScriptException], then br $done - fb += wa.BrOnCastFail( - doneLabel, - watpe.RefType(genTypeID.ThrowableStruct), - watpe.RefType(genTypeID.JSExceptionStruct) - ) - - // otherwise, unwrap the JavaScriptException by reading its field - fb += wa.StructGet( - genTypeID.JSExceptionStruct, - genFieldID.forClassInstanceField(SpecialNames.exceptionFieldName) - ) - } - - AnyType - } - private def genLinkTimeProperty(tree: LinkTimeProperty): Type = { val lit = ctx.coreSpec.linkTimeProperties.transformLinkTimeProperty(tree) genLiteral(lit, lit.tpe) @@ -2943,37 +2932,6 @@ private class FunctionEmitter private ( AnyType } - private def genArrayLength(tree: ArrayLength): Type = { - val ArrayLength(array) = tree - - genTreeAuto(array) - - markPosition(tree) - - array.tpe match { - case ArrayType(arrayTypeRef, _) => - // Get the underlying array - genCheckNonNullFor(array) - fb += wa.StructGet( - genTypeID.forArrayClass(arrayTypeRef), - genFieldID.objStruct.arrayUnderlying - ) - // Get the length - fb += wa.ArrayLen - IntType - - case NothingType => - // unreachable - NothingType - case NullType => - genNPE() - NothingType - case _ => - throw new IllegalArgumentException( - s"ArraySelect.array must be an array type, but has type ${tree.array.tpe}") - } - } - private def genNewArray(tree: NewArray): Type = { val NewArray(arrayTypeRef, length) = tree @@ -3166,51 +3124,6 @@ private class FunctionEmitter private ( AnyNotNullType } - private def genClone(tree: Clone): Type = { - val Clone(expr) = tree - - expr.tpe match { - case NothingType => - genTree(expr, NothingType) - NothingType - - case NullType => - genTree(expr, NullType) - genNPE() - NothingType - - case exprType => - val exprLocal = addSyntheticLocal(watpe.RefType(genTypeID.ObjectStruct)) - - genTreeAuto(expr) - - markPosition(tree) - - genAsNonNullOrNPEFor(expr) - fb += wa.LocalTee(exprLocal) - - fb += wa.LocalGet(exprLocal) - fb += wa.StructGet(genTypeID.ObjectStruct, genFieldID.objStruct.vtable) - fb += wa.StructGet(genTypeID.typeData, genFieldID.typeData.cloneFunction) - // cloneFunction: (ref jl.Object) -> (ref jl.Object) - fb += wa.CallRef(genTypeID.cloneFunctionType) - - // cast the (ref jl.Object) back down to the result type - transformSingleType(exprType) match { - case watpe.RefType(_, watpe.HeapType.Type(genTypeID.ObjectStruct)) => - // no need to cast to (ref null? jl.Object) - case wasmType: watpe.RefType => - fb += wa.RefCast(wasmType.toNonNullable) - case wasmType => - // Since no hijacked class extends jl.Cloneable, this case cannot happen - throw new AssertionError( - s"Unexpected type for Clone: $exprType (Wasm: $wasmType)") - } - - exprType - } - } - private def genMatch(tree: Match, expectedType: Type): Type = { val Match(selector, cases, defaultBody) = tree @@ -3410,7 +3323,6 @@ private class FunctionEmitter private ( case Transients.ObjectClassName(obj) => genTreeToAny(obj) markPosition(tree) - genAsNonNullOrNPEFor(obj) fb += wa.Call(genFunctionID.anyGetClassName) StringType diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala index 763255f2c1..1e75d4db31 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/ClassDefChecker.scala @@ -747,9 +747,6 @@ private final class ClassDefChecker(classDef: ClassDef, checkTree(block, env) checkTree(finalizer, env) - case Throw(expr) => - checkTree(expr, env) - case Match(selector, cases, default) => checkTree(selector, env) for ((alts, body) <- cases) { @@ -816,12 +813,7 @@ private final class ClassDefChecker(classDef: ClassDef, checkArrayTypeRef(typeRef) checkTrees(elems, env) - case ArrayLength(array) => - checkArrayReceiverType(array.tpe) - checkTree(array, env) - case ArraySelect(array, index) => - checkArrayReceiverType(array.tpe) checkTree(array, env) checkTree(index, env) @@ -857,21 +849,6 @@ private final class ClassDefChecker(classDef: ClassDef, // ok } - case GetClass(expr) => - checkTree(expr, env) - - case Clone(expr) => - checkTree(expr, env) - - case IdentityHashCode(expr) => - checkTree(expr, env) - - case WrapAsThrowable(expr) => - checkTree(expr, env) - - case UnwrapFromThrowable(expr) => - checkTree(expr, env) - case LinkTimeProperty(name) => // JavaScript expressions @@ -1029,13 +1006,6 @@ private final class ClassDefChecker(classDef: ClassDef, reportError("invalid transient tree") } - private def checkArrayReceiverType(tpe: Type)( - implicit ctx: ErrorContext): Unit = tpe match { - case tpe: ArrayType => checkArrayType(tpe) - case NullType | NothingType => // ok - case _ => reportError(i"Array type expected but $tpe found") - } - private def checkArrayType(tpe: ArrayType)( implicit ctx: ErrorContext): Unit = { checkArrayTypeRef(tpe.arrayTypeRef) diff --git a/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala index dcd87e7c5a..4c2c4f8464 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/checker/IRChecker.scala @@ -330,9 +330,6 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter, typecheckExpect(block, env, tpe) typecheck(finalizer, env) - case Throw(expr) => - typecheckAny(expr, env) - case Match(selector, cases, default) => // Typecheck the selector as an int or a java.lang.String typecheck(selector, env) @@ -466,6 +463,16 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter, i"with non-object result type: $resultType") } + case UnaryOp(UnaryOp.Array_length, lhs) => + // Array_length is a bit special because it allows any non-nullable array type + typecheck(lhs, env) + lhs.tpe match { + case NothingType | ArrayType(_, false) => + // ok + case other => + reportError(i"Array type expected but $other found") + } + case UnaryOp(op, lhs) => import UnaryOp._ val expectedArgType = (op: @switch) match { @@ -487,11 +494,17 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter, DoubleType case String_length => StringType - case CheckNotNull => + case CheckNotNull | IdentityHashCode | WrapAsThrowable | Throw => AnyType case Class_name | Class_isPrimitive | Class_isInterface | Class_isArray | Class_componentType | Class_superClass => ClassType(ClassClass, nullable = false) + case GetClass => + AnyNotNullType + case Clone => + ClassType(CloneableClass, nullable = false) + case UnwrapFromThrowable => + ClassType(ThrowableClass, nullable = false) } typecheckExpect(lhs, env, expectedArgType) @@ -541,13 +554,6 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter, for (elem <- elems) typecheckExpect(elem, env, elemType) - case ArrayLength(array) => - typecheckExpr(array, env) - if (array.tpe != NullType && - array.tpe != NothingType && - !array.tpe.isInstanceOf[ArrayType]) - reportError(i"Array type expected but ${array.tpe} found") - case ArraySelect(array, index) => typecheckExpect(index, env, IntType) typecheckExpr(array, env) @@ -569,21 +575,6 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter, typecheckAny(expr, env) checkIsAsInstanceTargetType(tpe) - case GetClass(expr) => - typecheckAny(expr, env) - - case Clone(expr) => - typecheckExpect(expr, env, ClassType(CloneableClass, nullable = true)) - - case IdentityHashCode(expr) => - typecheckAny(expr, env) - - case WrapAsThrowable(expr) => - typecheckAny(expr, env) - - case UnwrapFromThrowable(expr) => - typecheckExpect(expr, env, ClassType(ThrowableClass, nullable = true)) - case LinkTimeProperty(name) => // JavaScript expressions diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala index 5b2cf7ad5e..589220ef0f 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/IncOptimizer.scala @@ -882,8 +882,8 @@ final class IncOptimizer private[optimizer] (config: CommonPhaseConfig, collOps: case UnaryOp(UnaryOp.CheckNotNull, expr) => config.coreSpec.semantics.nullPointers == CheckedBehavior.Unchecked && isTriviallySideEffectFree(expr) - case GetClass(expr) => // Before 1.17, we used GetClass as CheckNotNull - config.coreSpec.semantics.nullPointers == CheckedBehavior.Unchecked && + case UnaryOp(op, expr) => // Before 1.17, we used GetClass as CheckNotNull + UnaryOp.isSideEffectFreeOp(op) && isTriviallySideEffectFree(expr) case New(className, _, args) => diff --git a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala index 94a294a4cc..61612d9783 100644 --- a/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala +++ b/linker/shared/src/main/scala/org/scalajs/linker/frontend/optimizer/OptimizerCore.scala @@ -456,9 +456,6 @@ private[optimizer] abstract class OptimizerCore( val newFinalizer = transformStat(finalizer) TryFinally(newBlock, newFinalizer) - case Throw(expr) => - Throw(transformExpr(expr)) - case Match(selector, cases, default) => val newSelector = transformExpr(selector) newSelector match { @@ -533,9 +530,6 @@ private[optimizer] abstract class OptimizerCore( case ArrayValue(tpe, elems) => ArrayValue(tpe, elems map transformExpr) - case ArrayLength(array) => - ArrayLength(transformExpr(array)) - case ArraySelect(array, index) => val newArray = transformExpr(array) @@ -588,44 +582,6 @@ private[optimizer] abstract class OptimizerCore( } } - case GetClass(expr) => - trampoline { - pretransformExpr(expr) { texpr => - def constant(typeRef: TypeRef): TailRec[Tree] = - TailCalls.done(Block(checkNotNullStatement(texpr), ClassOf(typeRef))) - - texpr.tpe match { - case RefinedType(ClassType(LongImpl.RuntimeLongClass, false), true) => - constant(ClassRef(BoxedLongClass)) - case RefinedType(ClassType(className, false), true) => - constant(ClassRef(className)) - case RefinedType(ArrayType(arrayTypeRef, false), true) => - constant(arrayTypeRef) - case RefinedType(AnyType | AnyNotNullType | ClassType(ObjectClass, _), _) => - // The result can be anything, including null - TailCalls.done(GetClass(finishTransformExpr(texpr))) - case _ => - /* If texpr.tpe is neither AnyType nor j.l.Object, it cannot be - * a JS object, so its getClass() cannot be null. Cast away - * nullability to help downstream optimizations. - */ - val newGetClass = GetClass(finishTransformExpr(texpr)) - TailCalls.done(makeCast(newGetClass, newGetClass.tpe.toNonNullable)) - } - } - } - - case Clone(expr) => - Clone(transformExpr(expr)) - - case IdentityHashCode(expr) => - IdentityHashCode(transformExpr(expr)) - - case _:WrapAsThrowable | _:UnwrapFromThrowable => - trampoline { - pretransformExpr(tree)(finishTransform(isStat)) - } - case prop: LinkTimeProperty => config.coreSpec.linkTimeProperties.transformLinkTimeProperty(prop) @@ -993,42 +949,6 @@ private[optimizer] abstract class OptimizerCore( case tree: BinaryOp => pretransformBinaryOp(tree)(cont) - case WrapAsThrowable(expr) => - pretransformExpr(expr) { texpr => - if (isSubtype(texpr.tpe.base, ThrowableClassType.toNonNullable)) { - cont(texpr) - } else { - if (texpr.tpe.isExact) { - pretransformNew(AllocationSite.Tree(tree), JavaScriptExceptionClass, - MethodIdent(AnyArgConstructorName), texpr :: Nil)(cont) - } else { - cont(PreTransTree(WrapAsThrowable(finishTransformExpr(texpr)))) - } - } - } - - case UnwrapFromThrowable(expr) => - pretransformExpr(expr) { texpr => - def default = - cont(PreTransTree(UnwrapFromThrowable(finishTransformExpr(texpr)))) - - val baseTpe = texpr.tpe.base - - if (baseTpe == NothingType) { - cont(texpr) - } else if (baseTpe == NullType) { - cont(checkNotNull(texpr)) - } else if (isSubtype(baseTpe, JavaScriptExceptionClassType)) { - pretransformSelectCommon(AnyType, texpr, optQualDeclaredType = None, - FieldIdent(exceptionFieldName), isLhsOfAssign = false)(cont) - } else { - if (texpr.tpe.isExact || !isSubtype(JavaScriptExceptionClassType.toNonNullable, baseTpe)) - cont(checkNotNull(texpr)) - else - default - } - } - case tree: JSSelect => pretransformJSSelect(tree, isLhsOfAssign = false)(cont) @@ -1565,7 +1485,7 @@ private[optimizer] abstract class OptimizerCore( finishTransformBindings(bindingsAndStats, finishTransformStat(result)) case PreTransUnaryOp(op, lhs) => - if (op == UnaryOp.CheckNotNull) + if (!UnaryOp.isSideEffectFreeOp(op)) finishTransformExpr(stat) else finishTransformStat(lhs) @@ -1689,15 +1609,13 @@ private[optimizer] abstract class OptimizerCore( keepOnlySideEffects(length) case ArrayValue(_, elems) => Block(elems.map(keepOnlySideEffects(_)))(stat.pos) - case ArrayLength(array) => - checkNotNullStatement(array)(stat.pos) case ArraySelect(array, index) if semantics.arrayIndexOutOfBounds == CheckedBehavior.Unchecked => Block(checkNotNullStatement(array)(stat.pos), keepOnlySideEffects(index))(stat.pos) case Select(qualifier, _) => checkNotNullStatement(qualifier)(stat.pos) case Closure(_, _, _, _, _, captureValues) => Block(captureValues.map(keepOnlySideEffects))(stat.pos) - case UnaryOp(op, arg) if op != UnaryOp.CheckNotNull => + case UnaryOp(op, arg) if UnaryOp.isSideEffectFreeOp(op) => keepOnlySideEffects(arg) case If(cond, thenp, elsep) => (keepOnlySideEffects(thenp), keepOnlySideEffects(elsep)) match { @@ -1716,14 +1634,6 @@ private[optimizer] abstract class OptimizerCore( Block(elems.map(keepOnlySideEffects))(stat.pos) case RecordSelect(record, _) => keepOnlySideEffects(record) - case GetClass(expr) => - checkNotNullStatement(expr)(stat.pos) - case Clone(expr) => - checkNotNullStatement(expr)(stat.pos) - case WrapAsThrowable(expr) => - keepOnlySideEffects(expr) - case UnwrapFromThrowable(expr) => - checkNotNullStatement(expr)(stat.pos) /* By definition, a failed cast is always UB, so it cannot have side effects. * However, if the target type is `nothing`, we keep the cast not to lose @@ -1873,9 +1783,6 @@ private[optimizer] abstract class OptimizerCore( case If(cond, thenp, elsep) => rec(cond).mapOrFailed(If(_, thenp, elsep)(body.tpe)) - case Throw(expr) => - rec(expr).mapOrFailed(Throw(_)) - case Match(selector, cases, default) => rec(selector).mapOrFailed(Match(_, cases, default)(body.tpe)) @@ -1914,7 +1821,7 @@ private[optimizer] abstract class OptimizerCore( recs(args).mapOrFailed(ApplyStatic(flags, className, method, _)(body.tpe)) case UnaryOp(op, arg) => - rec(arg).mapOrKeepGoingIf(UnaryOp(op, _))(keepGoingIf = op != UnaryOp.CheckNotNull) + rec(arg).mapOrKeepGoingIf(UnaryOp(op, _))(keepGoingIf = UnaryOp.isPureOp(op)) case BinaryOp(op, lhs, rhs) => import BinaryOp._ @@ -1941,9 +1848,6 @@ private[optimizer] abstract class OptimizerCore( case ArrayValue(typeRef, elems) => recs(elems).mapOrKeepGoing(ArrayValue(typeRef, _)) - case ArrayLength(array) => - rec(array).mapOrKeepGoingIf(ArrayLength(_))(keepGoingIf = isNotNull(array)) - case ArraySelect(array, index) => rec(array) match { case Success(newArray) => @@ -1973,12 +1877,6 @@ private[optimizer] abstract class OptimizerCore( case Transient(Cast(expr, tpe)) => rec(expr).mapOrKeepGoing(newExpr => makeCast(newExpr, tpe)) - case GetClass(expr) => - rec(expr).mapOrKeepGoingIf(GetClass(_))(keepGoingIf = isNotNull(expr)) - - case Clone(expr) => - rec(expr).mapOrFailed(Clone(_)) - case JSUnaryOp(op, arg) => rec(arg).mapOrFailed(JSUnaryOp(op, _)) @@ -2911,8 +2809,7 @@ private[optimizer] abstract class OptimizerCore( val tarray = targs.head tarray.tpe.base match { case _: ArrayType => - val array = finishTransformExpr(tarray) - contTree(Trees.ArrayLength(array)) + cont(foldUnaryOp(UnaryOp.Array_length, checkNotNull(tarray))) case _ => default } @@ -3094,7 +2991,8 @@ private[optimizer] abstract class OptimizerCore( If( Transient(WasmBinaryOp(WasmBinaryOp.I32GtU, cpLocalDef.newReplacement, IntLiteral(Character.MAX_CODE_POINT))), - Throw(New(IllegalArgumentExceptionClass, MethodIdent(NoArgConstructorName), Nil)), + UnaryOp(UnaryOp.Throw, + New(IllegalArgumentExceptionClass, MethodIdent(NoArgConstructorName), Nil)), Skip() )(VoidType), Transient(WasmStringFromCodePoint(cpLocalDef.newReplacement)) @@ -3187,10 +3085,16 @@ private[optimizer] abstract class OptimizerCore( case ClassGetName => optTReceiver.get match { case PreTransMaybeBlock(bindingsAndStats, - PreTransTree(MaybeCast(GetClass(expr)), _)) => + PreTransTree(MaybeCast(UnaryOp(UnaryOp.GetClass, expr)), _)) => contTree(finishTransformBindings( bindingsAndStats, Transient(ObjectClassName(expr)))) + // Same thing, but the argument stayed as a PreTransUnaryOp + case PreTransMaybeBlock(bindingsAndStats, + PreTransUnaryOp(UnaryOp.GetClass, texpr)) => + contTree(finishTransformBindings( + bindingsAndStats, Transient(ObjectClassName(finishTransformExpr(texpr))))) + case _ => default } @@ -3425,7 +3329,7 @@ private[optimizer] abstract class OptimizerCore( * coming from Scala.js < 1.15.1 (since 1.15.1, we intercept that shape * already in the compiler back-end). */ - case If(cond, th: Throw, Assign(Select(This(), _), value)) :: rest => + case If(cond, th @ UnaryOp(UnaryOp.Throw, _), Assign(Select(This(), _), value)) :: rest => // work around a bug of the compiler (these should be @-bindings) val stat = stats.head.asInstanceOf[If] val ass = stat.elsep.asInstanceOf[Assign] @@ -3567,7 +3471,42 @@ private[optimizer] abstract class OptimizerCore( val UnaryOp(op, arg) = tree pretransformExpr(arg) { tlhs => - expandLongOps(foldUnaryOp(op, tlhs))(cont) + def folded: PreTransform = + foldUnaryOp(op, tlhs) + + op match { + case UnaryOp.WrapAsThrowable => + if (isSubtype(tlhs.tpe.base, ThrowableClassType.toNonNullable)) { + cont(tlhs) + } else { + if (tlhs.tpe.isExact) { + pretransformNew(AllocationSite.Tree(tree), JavaScriptExceptionClass, + MethodIdent(AnyArgConstructorName), tlhs :: Nil)(cont) + } else { + cont(folded) + } + } + + case UnaryOp.UnwrapFromThrowable => + val baseTpe = tlhs.tpe.base + + if (baseTpe == NothingType) { + cont(tlhs) + } else if (baseTpe == NullType) { + cont(checkNotNull(tlhs)) + } else if (isSubtype(baseTpe, JavaScriptExceptionClassType)) { + pretransformSelectCommon(AnyType, tlhs, optQualDeclaredType = None, + FieldIdent(exceptionFieldName), isLhsOfAssign = false)(cont) + } else { + if (tlhs.tpe.isExact || !isSubtype(JavaScriptExceptionClassType.toNonNullable, baseTpe)) + cont(checkNotNull(tlhs)) + else + cont(folded) + } + + case _ => + expandLongOps(folded)(cont) + } } } @@ -3949,6 +3888,28 @@ private[optimizer] abstract class OptimizerCore( default } + case GetClass => + def constant(typeRef: TypeRef): PreTransform = + PreTransTree(Block(finishTransformStat(arg), ClassOf(typeRef))) + + arg.tpe match { + case RefinedType(ClassType(LongImpl.RuntimeLongClass, false), true) => + constant(ClassRef(BoxedLongClass)) + case RefinedType(ClassType(className, false), true) => + constant(ClassRef(className)) + case RefinedType(ArrayType(arrayTypeRef, false), true) => + constant(arrayTypeRef) + case RefinedType(AnyType | AnyNotNullType | ClassType(ObjectClass, _), _) => + // The result can be anything, including null + default + case _ => + /* If texpr.tpe is neither AnyType nor j.l.Object, it cannot be + * a JS object, so its getClass() cannot be null. Cast away + * nullability to help downstream optimizations. + */ + foldCast(default, ClassType(ClassClass, nullable = false)) + } + case _ => default } @@ -6919,7 +6880,6 @@ private[optimizer] object OptimizerCore { case ApplyStatically(_, receiver, _, _, Nil) => isTrivialArg(receiver) case ApplyStatic(_, _, _, Nil) => true - case ArrayLength(array) => isTrivialArg(array) case ArraySelect(array, index) => isTrivialArg(array) && isTrivialArg(index) case AsInstanceOf(inner, _) => isSimpleArg(inner) diff --git a/linker/shared/src/test/scala/org/scalajs/linker/IRCheckerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/IRCheckerTest.scala index a0333eccf1..0fe3988e97 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/IRCheckerTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/IRCheckerTest.scala @@ -31,6 +31,7 @@ import org.scalajs.linker.interface._ import org.scalajs.linker.interface.unstable.IRFileImpl import org.scalajs.linker.standard._ import org.scalajs.linker.frontend.Refiner +import org.scalajs.linker.backend.emitter.PrivateLibHolder import org.scalajs.linker.testutils._ import org.scalajs.linker.testutils.TestIRBuilder._ @@ -83,7 +84,7 @@ class IRCheckerTest { callMethOn(ApplyStatic(EAF, MainTestClassName, nullBarMethodName, Nil)(ClassType("Bar", nullable = true))), callMethOn(Null()), - callMethOn(Throw(Null())) + callMethOn(UnaryOp(UnaryOp.Throw, Null())) )) ) ) @@ -255,15 +256,45 @@ class IRCheckerTest { } } + @Test + def unaryOpNonNullableArguments(): AsyncResult = await { + import UnaryOp._ + + // List of ops that take non-nullable reference types as argument + val ops = List( + Class_name, + Class_isPrimitive, + Class_isInterface, + Class_isArray, + Class_componentType, + Class_superClass, + Array_length, + GetClass, + Clone, + UnwrapFromThrowable + ) + + val results = for (op <- ops) yield { + val classDefs = Seq( + mainTestClassDef(UnaryOp(op, Null())) + ) + + for (log <- testLinkIRErrors(classDefs, MainTestModuleInitializers)) yield { + log.assertContainsError("expected but null found") + } + } + + Future.sequence(results) + } + @Test def arrayOpsNullOrNothing(): AsyncResult = await { val classDefs = Seq( mainTestClassDef( Block( ArraySelect(Null(), int(1))(NothingType), - ArrayLength(Null()), - ArraySelect(Throw(Null()), int(1))(NothingType), - ArrayLength(Throw(Null())) + ArraySelect(UnaryOp(UnaryOp.Throw, Null()), int(1))(NothingType), + UnaryOp(UnaryOp.Array_length, UnaryOp(UnaryOp.Throw, Null())) ) ) ) @@ -453,7 +484,8 @@ object IRCheckerTest { val linkerFrontend = StandardLinkerFrontend(config) val irFiles = ( stdLibFiles ++ - classDefs.map(MemClassDefIRFile(_)) + classDefs.map(MemClassDefIRFile(_)) ++ + PrivateLibHolder.files ) linkerFrontend.link(irFiles, moduleInitializers, noSymbolRequirements, logger) } diff --git a/linker/shared/src/test/scala/org/scalajs/linker/OptimizerTest.scala b/linker/shared/src/test/scala/org/scalajs/linker/OptimizerTest.scala index 7b37ab5648..3954dfe278 100644 --- a/linker/shared/src/test/scala/org/scalajs/linker/OptimizerTest.scala +++ b/linker/shared/src/test/scala/org/scalajs/linker/OptimizerTest.scala @@ -76,7 +76,7 @@ class OptimizerTest { // @noinline def witness(): AnyRef = throw null MethodDef(EMF, witnessMethodName, NON, Nil, AnyType, Some { - Throw(Null()) + UnaryOp(UnaryOp.Throw, Null()) })(EOH.withNoinline(true), UNV), // @noinline def reachClone(): Object = clone() @@ -124,10 +124,10 @@ class OptimizerTest { } }.hasExactly(if (inlinedWhenOnObject) 1 else 0, "IsInstanceOf node") { case IsInstanceOf(_, _) => true - }.hasExactly(if (inlinedWhenOnObject) 1 else 0, "Throw node") { - case Throw(_) => true + }.hasExactly(if (inlinedWhenOnObject) 1 else 0, "throw operation") { + case UnaryOp(UnaryOp.Throw, _) => true }.hasExactly(if (inlinedWhenOnObject) 3 else 2, "built-in () operations") { - case Clone(_) => true + case UnaryOp(UnaryOp.Clone, _) => true }.hasExactly(if (inlinedWhenOnObject) 0 else 1, "call to clone() (not inlined)") { case Apply(_, _, MethodIdent(`cloneMethodName`), _) => true } @@ -289,7 +289,7 @@ class OptimizerTest { )), Return(voidReturnArgument, matchResult1) )), - Throw(New("java.lang.Exception", NoArgConstructorName, Nil)) + UnaryOp(UnaryOp.Throw, New("java.lang.Exception", NoArgConstructorName, Nil)) )) )) ) diff --git a/project/BinaryIncompatibilities.scala b/project/BinaryIncompatibilities.scala index ec5063ac59..305e97d428 100644 --- a/project/BinaryIncompatibilities.scala +++ b/project/BinaryIncompatibilities.scala @@ -7,13 +7,27 @@ object BinaryIncompatibilities { val IR = Seq( // !!! Breaking, OK in minor release ProblemFilters.exclude[DirectMissingMethodProblem]("org.scalajs.ir.Printers#IRTreePrinter.print"), + ProblemFilters.exclude[MissingClassProblem]("org.scalajs.ir.Trees$ArrayLength"), + ProblemFilters.exclude[MissingClassProblem]("org.scalajs.ir.Trees$ArrayLength$"), + ProblemFilters.exclude[MissingClassProblem]("org.scalajs.ir.Trees$Clone"), + ProblemFilters.exclude[MissingClassProblem]("org.scalajs.ir.Trees$Clone$"), + ProblemFilters.exclude[MissingClassProblem]("org.scalajs.ir.Trees$GetClass"), + ProblemFilters.exclude[MissingClassProblem]("org.scalajs.ir.Trees$GetClass$"), + ProblemFilters.exclude[MissingClassProblem]("org.scalajs.ir.Trees$IdentityHashCode"), + ProblemFilters.exclude[MissingClassProblem]("org.scalajs.ir.Trees$IdentityHashCode$"), ProblemFilters.exclude[MissingClassProblem]("org.scalajs.ir.Trees$JSLinkingInfo"), ProblemFilters.exclude[MissingClassProblem]("org.scalajs.ir.Trees$JSLinkingInfo$"), ProblemFilters.exclude[MissingClassProblem]("org.scalajs.ir.Trees$LabelIdent"), ProblemFilters.exclude[MissingClassProblem]("org.scalajs.ir.Trees$LabelIdent$"), ProblemFilters.exclude[Problem]("org.scalajs.ir.Trees#Labeled.*"), ProblemFilters.exclude[Problem]("org.scalajs.ir.Trees#Return.*"), + ProblemFilters.exclude[MissingClassProblem]("org.scalajs.ir.Trees$Throw"), + ProblemFilters.exclude[MissingClassProblem]("org.scalajs.ir.Trees$Throw$"), + ProblemFilters.exclude[MissingClassProblem]("org.scalajs.ir.Trees$UnwrapFromThrowable"), + ProblemFilters.exclude[MissingClassProblem]("org.scalajs.ir.Trees$UnwrapFromThrowable$"), ProblemFilters.exclude[Problem]("org.scalajs.ir.Trees#VarRef.*"), + ProblemFilters.exclude[MissingClassProblem]("org.scalajs.ir.Trees$WrapAsThrowable"), + ProblemFilters.exclude[MissingClassProblem]("org.scalajs.ir.Trees$WrapAsThrowable$"), ProblemFilters.exclude[IncompatibleResultTypeProblem]("org.scalajs.ir.Trees#*.tpe"), ProblemFilters.exclude[MissingClassProblem]("org.scalajs.ir.Types$NoType$"), diff --git a/project/Build.scala b/project/Build.scala index 487e8482e7..2fc1510e58 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -2066,7 +2066,7 @@ object Build { if (!useMinifySizes) { Some(ExpectedSizes( fastLink = 449000 to 450000, - fullLink = 95000 to 96000, + fullLink = 94000 to 95000, fastLinkGz = 58000 to 59000, fullLinkGz = 25000 to 26000, )) diff --git a/project/JavaLangObject.scala b/project/JavaLangObject.scala index eb3eedabef..1b32bac8ff 100644 --- a/project/JavaLangObject.scala +++ b/project/JavaLangObject.scala @@ -62,7 +62,7 @@ object JavaLangObject { Nil, ClassType(ClassClass, nullable = true), Some { - GetClass(This()(ThisType)) + UnaryOp(UnaryOp.GetClass, This()(ThisType)) })(OptimizerHints.empty.withInline(true), Unversioned), /* def hashCode(): Int = (this) */ @@ -73,7 +73,7 @@ object JavaLangObject { Nil, IntType, Some { - IdentityHashCode(This()(ThisType)) + UnaryOp(UnaryOp.IdentityHashCode, This()(ThisType)) })(OptimizerHints.empty.withInline(true), Unversioned), /* def equals(that: Object): Boolean = this eq that */ @@ -102,9 +102,10 @@ object JavaLangObject { AnyType, Some { If(IsInstanceOf(This()(ThisType), ClassType(CloneableClass, nullable = false)), { - Clone(AsInstanceOf(This()(ThisType), ClassType(CloneableClass, nullable = true))) + UnaryOp(UnaryOp.Clone, UnaryOp(UnaryOp.CheckNotNull, + AsInstanceOf(This()(ThisType), ClassType(CloneableClass, nullable = true)))) }, { - Throw(New(ClassName("java.lang.CloneNotSupportedException"), + UnaryOp(UnaryOp.Throw, New(ClassName("java.lang.CloneNotSupportedException"), MethodIdent(NoArgConstructorName), Nil)) })(AnyType) })(OptimizerHints.empty.withInline(true), Unversioned),