From c1646d14fb01e915766215a7ab5450c08e7153e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 15 Dec 2024 13:44:52 +0100 Subject: [PATCH 1/2] 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. --- .../main/scala/org/scalajs/ir/Printers.scala | 73 ++-- .../scala/org/scalajs/ir/Serializers.scala | 87 +++-- .../src/main/scala/org/scalajs/ir/Trees.scala | 54 ++- .../scala/org/scalajs/ir/PrintersTest.scala | 8 + .../scala/org/scalajs/linker/RunTest.scala | 4 +- .../org/scalajs/linker/analyzer/Infos.scala | 17 +- .../linker/backend/emitter/CoreJSLib.scala | 21 +- .../backend/emitter/FunctionEmitter.scala | 223 ++++++------ .../linker/backend/emitter/Transients.scala | 4 +- .../backend/wasmemitter/FunctionEmitter.scala | 329 +++++++----------- .../linker/checker/ClassDefChecker.scala | 34 +- .../scalajs/linker/checker/IRChecker.scala | 47 ++- .../frontend/optimizer/IncOptimizer.scala | 4 +- .../frontend/optimizer/OptimizerCore.scala | 184 ++++------ .../org/scalajs/linker/IRCheckerTest.scala | 42 ++- .../org/scalajs/linker/OptimizerTest.scala | 10 +- project/Build.scala | 2 +- 17 files changed, 533 insertions(+), 610 deletions(-) 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..3008148c71 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Printers.scala @@ -347,40 +347,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) => 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..369cb5990c 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Serializers.scala @@ -1139,8 +1139,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 +1170,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 +1200,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 +1239,36 @@ object Serializers { case TagUnaryOp => UnaryOp(readByte(), readTree()) case TagBinaryOp => BinaryOp(readByte(), readTree(), readTree()) + case TagArrayLength | TagGetClass | TagClone | TagIdentityHashCode | + TagWrapAsThrowable | TagUnwrapFromThrowable | TagThrow => + if (false /*!hacks.use17*/) { // scalastyle:ignore + 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 +1302,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 +1321,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 +1476,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 +1493,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 +1502,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 +1698,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 +1761,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 +1831,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 +1965,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 +1990,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/Trees.scala b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala index 917df781ed..031970c895 100644 --- a/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala +++ b/ir/shared/src/main/scala/org/scalajs/ir/Trees.scala @@ -280,12 +280,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 +349,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 +378,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 +387,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 } } @@ -826,11 +862,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 +883,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..06c2d0de56 100644 --- a/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala +++ b/ir/shared/src/test/scala/org/scalajs/ir/PrintersTest.scala @@ -403,6 +403,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 = { 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..78f0b2ec39 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) @@ -602,6 +595,11 @@ private class FunctionEmitter private ( case _: JSSuperConstructorCall => throw new AssertionError(s"Invalid tree: $tree") + + case _:Throw | _:ArrayLength | _:GetClass | _:Clone | _:IdentityHashCode | + _:WrapAsThrowable | _:UnwrapFromThrowable => + throw new AssertionError( + s"illegal legacy node of class ${tree.getClass().getSimpleName()}") } genAdapt(generatedType, expectedType) @@ -1370,11 +1368,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 +1465,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 +2402,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 +2571,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 +2664,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 +2937,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 +3129,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 +3328,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..c6dd5e7fd8 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 @@ -1019,6 +996,10 @@ private final class ClassDefChecker(classDef: ClassDef, transient.traverse(new Traversers.Traverser { override def traverse(tree: Tree): Unit = checkTree(tree, env) }) + + case _:Throw | _:ArrayLength | _:GetClass | _:Clone | _:IdentityHashCode | + _:WrapAsThrowable | _:UnwrapFromThrowable => + reportError(i"illegal legacy node of class ${tree.getClass().getSimpleName()}") } newEnv @@ -1029,13 +1010,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..fd17c06147 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 @@ -768,6 +759,10 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter, case _:RecordSelect | _:RecordValue | _:Transient | _:JSSuperConstructorCall => reportError("invalid tree") + + case _:Throw | _:ArrayLength | _:GetClass | _:Clone | _:IdentityHashCode | + _:WrapAsThrowable | _:UnwrapFromThrowable => + reportError(i"illegal legacy node of class ${tree.getClass().getSimpleName()}") } } 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/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, )) From e4bdf168d46637db211d87bf9312a79740f8ec04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Doeraene?= Date: Sun, 15 Dec 2024 15:19:05 +0100 Subject: [PATCH 2/2] Remove the IR nodes that were merged in `UnaryOp`. And update the compiler to directly generate the new `UnaryOp`s. We only enable the deserialization hack when reading IR <= 1.17. --- .../org/scalajs/nscplugin/GenJSCode.scala | 24 +++++++------ .../org/scalajs/nscplugin/GenJSExports.scala | 2 +- .../nscplugin/test/OptimizationTest.scala | 4 +-- .../main/scala/org/scalajs/ir/Hashers.scala | 28 --------------- .../main/scala/org/scalajs/ir/Printers.scala | 32 ----------------- .../scala/org/scalajs/ir/Serializers.scala | 30 +--------------- .../scala/org/scalajs/ir/Transformers.scala | 21 ----------- .../scala/org/scalajs/ir/Traversers.scala | 21 ----------- .../src/main/scala/org/scalajs/ir/Trees.scala | 35 ------------------- .../scala/org/scalajs/ir/PrintersTest.scala | 29 --------------- .../backend/wasmemitter/FunctionEmitter.scala | 5 --- .../linker/checker/ClassDefChecker.scala | 4 --- .../scalajs/linker/checker/IRChecker.scala | 4 --- project/BinaryIncompatibilities.scala | 14 ++++++++ project/JavaLangObject.scala | 9 ++--- 15 files changed, 37 insertions(+), 225 deletions(-) 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 3008148c71..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) @@ -532,10 +528,6 @@ object Printers { print(typeRef) printArgs(elems) - case ArrayLength(array) => - print(array) - print(".length") - case ArraySelect(array, index) => print(array) print('[') @@ -571,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 369cb5990c..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) @@ -1241,7 +1213,7 @@ object Serializers { case TagArrayLength | TagGetClass | TagClone | TagIdentityHashCode | TagWrapAsThrowable | TagUnwrapFromThrowable | TagThrow => - if (false /*!hacks.use17*/) { // scalastyle:ignore + if (!hacks.use17) { throw new IOException( s"Illegal legacy node $tag found in class ${enclosingClassName.nameString}") } 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 031970c895..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. @@ -562,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 @@ -588,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])( 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 06c2d0de56..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( """ @@ -582,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)) @@ -614,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/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 78f0b2ec39..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 @@ -595,11 +595,6 @@ private class FunctionEmitter private ( case _: JSSuperConstructorCall => throw new AssertionError(s"Invalid tree: $tree") - - case _:Throw | _:ArrayLength | _:GetClass | _:Clone | _:IdentityHashCode | - _:WrapAsThrowable | _:UnwrapFromThrowable => - throw new AssertionError( - s"illegal legacy node of class ${tree.getClass().getSimpleName()}") } genAdapt(generatedType, expectedType) 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 c6dd5e7fd8..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 @@ -996,10 +996,6 @@ private final class ClassDefChecker(classDef: ClassDef, transient.traverse(new Traversers.Traverser { override def traverse(tree: Tree): Unit = checkTree(tree, env) }) - - case _:Throw | _:ArrayLength | _:GetClass | _:Clone | _:IdentityHashCode | - _:WrapAsThrowable | _:UnwrapFromThrowable => - reportError(i"illegal legacy node of class ${tree.getClass().getSimpleName()}") } newEnv 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 fd17c06147..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 @@ -759,10 +759,6 @@ private final class IRChecker(unit: LinkingUnit, reporter: ErrorReporter, case _:RecordSelect | _:RecordValue | _:Transient | _:JSSuperConstructorCall => reportError("invalid tree") - - case _:Throw | _:ArrayLength | _:GetClass | _:Clone | _:IdentityHashCode | - _:WrapAsThrowable | _:UnwrapFromThrowable => - reportError(i"illegal legacy node of class ${tree.getClass().getSimpleName()}") } } 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/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),