diff options
Diffstat (limited to 'test/junit/scala/tools/nsc/backend/jvm/opt')
17 files changed, 1996 insertions, 498 deletions
diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/AnalyzerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/AnalyzerTest.scala new file mode 100644 index 0000000000..09675870f0 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/AnalyzerTest.scala @@ -0,0 +1,57 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import org.junit.Assert._ + +import scala.tools.asm.tree._ +import scala.tools.asm.tree.analysis._ +import scala.tools.nsc.backend.jvm.analysis.{AliasingFrame, AliasingAnalyzer} + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ +import BackendReporting._ +import BytecodeUtils._ + +import scala.collection.JavaConverters._ +import scala.tools.testing.ClearAfterClass + +@RunWith(classOf[JUnit4]) +class AnalyzerTest extends ClearAfterClass { + val noOptCompiler = cached("compiler", () => newCompiler(extraArgs = "-Yopt:l:none")) + + @Test + def aliasingOfPrimitives(): Unit = { + val code = + """class C { + | def f(a: Int, b: Long) = { + | val c = a - b // a is converted with i2l + | val d = c + | val e = a + | // locals: 0 this -- 1 a -- 2-3 b -- 4-5 c -- 6-7 d -- 8 e + | e + d // e is converted with i2l + | } + |} + """.stripMargin + + val List(c) = compileClasses(noOptCompiler)(code) + val a = new AliasingAnalyzer(new BasicInterpreter) + val f = findAsmMethod(c, "f") + a.analyze("C", f) + + val List(_, i2l) = findInstr(f, "I2L") + val aliasesAtI2l = a.frameAt(i2l, f).asInstanceOf[AliasingFrame[_]].aliases + assertEquals(aliasesAtI2l(1).iterator.toList, List(1, 8, 9)) // a, e and stack top + assertEquals(aliasesAtI2l(4).iterator.toList, List(4, 6)) + + val List(add) = findInstr(f, "LADD") + val aliasesAtAdd = a.frameAt(add, f).asInstanceOf[AliasingFrame[_]].aliases + assertEquals(aliasesAtAdd(1).iterator.toList, List(1, 8)) // after i2l the value on the stack is no longer an alias + assertEquals(aliasesAtAdd(4).iterator.toList, List(4, 6, 10)) // c, d and stack top + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala index 1b6c080234..aba0aab038 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/BTypesFromClassfileTest.scala @@ -17,12 +17,12 @@ import ASMConverters._ import BackendReporting._ -import scala.collection.convert.decorateAsScala._ +import scala.collection.JavaConverters._ @RunWith(classOf[JUnit4]) class BTypesFromClassfileTest { // inliner enabled -> inlineInfos are collected (and compared) in ClassBTypes - val compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:inline-global") + val compiler = newCompiler(extraArgs = "-Yopt:inline-global") import compiler._ import definitions._ diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala index 9fda034a04..9a27c42cac 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/CallGraphTest.scala @@ -6,6 +6,7 @@ import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.junit.Test import scala.collection.generic.Clearable +import scala.collection.immutable.IntMap import scala.tools.asm.Opcodes._ import org.junit.Assert._ @@ -20,26 +21,51 @@ import ASMConverters._ import AsmUtils._ import BackendReporting._ -import scala.collection.convert.decorateAsScala._ +import scala.collection.JavaConverters._ +import scala.tools.testing.ClearAfterClass @RunWith(classOf[JUnit4]) -class CallGraphTest { - val compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:inline-global -Yopt-warnings") - import compiler.genBCode.bTypes._ - - // allows inspecting the caches after a compilation run - val notPerRun: List[Clearable] = List(classBTypeFromInternalName, byteCodeRepository.classes, callGraph.callsites) +class CallGraphTest extends ClearAfterClass { + val compiler = cached("compiler", () => newCompiler(extraArgs = "-Yopt:inline-global -Yopt-warnings") + ) + import compiler.genBCode.bTypes + val notPerRun: List[Clearable] = List( + bTypes.classBTypeFromInternalName, + bTypes.byteCodeRepository.compilingClasses, + bTypes.byteCodeRepository.parsedClasses, + bTypes.callGraph.callsites) notPerRun foreach compiler.perRunCaches.unrecordCache - def compile(code: String, allowMessage: StoreReporter#Info => Boolean): List[ClassNode] = { + import compiler.genBCode.bTypes._ + import callGraph._ + + def compile(code: String, allowMessage: StoreReporter#Info => Boolean = _ => false): List[ClassNode] = { notPerRun.foreach(_.clear()) - compileClasses(compiler)(code, allowMessage = allowMessage) + compileClasses(compiler)(code, allowMessage = allowMessage).map(c => byteCodeRepository.classNode(c.name).get) } def callsInMethod(methodNode: MethodNode): List[MethodInsnNode] = methodNode.instructions.iterator.asScala.collect({ case call: MethodInsnNode => call }).toList + def checkCallsite(call: MethodInsnNode, callsiteMethod: MethodNode, target: MethodNode, calleeDeclClass: ClassBType, + safeToInline: Boolean, atInline: Boolean, atNoInline: Boolean, argInfos: IntMap[ArgInfo] = IntMap.empty) = { + val callsite = callGraph.callsites(callsiteMethod)(call) + try { + assert(callsite.callsiteInstruction == call) + assert(callsite.callsiteMethod == callsiteMethod) + val callee = callsite.callee.get + assert(callee.callee == target) + assert(callee.calleeDeclarationClass == calleeDeclClass) + assert(callee.safeToInline == safeToInline) + assert(callee.annotatedInline == atInline) + assert(callee.annotatedNoInline == atNoInline) + assert(callsite.argInfos == argInfos) + } catch { + case e: Throwable => println(callsite); throw e + } + } + @Test def callGraphStructure(): Unit = { val code = @@ -83,70 +109,107 @@ class CallGraphTest { msgCount += 1 ok exists (m.msg contains _) } - val List(cCls, cMod, dCls, testCls) = compile(code, checkMsg).map(c => byteCodeRepository.classNode(c.name).get) + val List(cCls, cMod, dCls, testCls) = compile(code, checkMsg) assert(msgCount == 6, msgCount) - val List(cf1, cf2, cf3, cf4, cf5, cf6, cf7) = cCls.methods.iterator.asScala.filter(_.name.startsWith("f")).toList.sortBy(_.name) - val List(df1, df3) = dCls.methods.iterator.asScala.filter(_.name.startsWith("f")).toList.sortBy(_.name) - val g1 = cMod.methods.iterator.asScala.find(_.name == "g1").get - val List(t1, t2) = testCls.methods.iterator.asScala.filter(_.name.startsWith("t")).toList.sortBy(_.name) + val List(cf1, cf2, cf3, cf4, cf5, cf6, cf7) = findAsmMethods(cCls, _.startsWith("f")) + val List(df1, df3) = findAsmMethods(dCls, _.startsWith("f")) + val g1 = findAsmMethod(cMod, "g1") + val List(t1, t2) = findAsmMethods(testCls, _.startsWith("t")) val List(cf1Call, cf2Call, cf3Call, cf4Call, cf5Call, cf6Call, cf7Call, cg1Call) = callsInMethod(t1) val List(df1Call, df2Call, df3Call, df4Call, df5Call, df6Call, df7Call, dg1Call) = callsInMethod(t2) - def checkCallsite(callsite: callGraph.Callsite, - call: MethodInsnNode, callsiteMethod: MethodNode, target: MethodNode, calleeDeclClass: ClassBType, - safeToInline: Boolean, atInline: Boolean, atNoInline: Boolean) = try { - assert(callsite.callsiteInstruction == call) - assert(callsite.callsiteMethod == callsiteMethod) - val callee = callsite.callee.get - assert(callee.callee == target) - assert(callee.calleeDeclarationClass == calleeDeclClass) - assert(callee.safeToInline == safeToInline) - assert(callee.annotatedInline == atInline) - assert(callee.annotatedNoInline == atNoInline) - - assert(callsite.argInfos == List()) // not defined yet - } catch { - case e: Throwable => println(callsite); throw e - } - val cClassBType = classBTypeFromClassNode(cCls) val cMClassBType = classBTypeFromClassNode(cMod) val dClassBType = classBTypeFromClassNode(dCls) - checkCallsite(callGraph.callsites(cf1Call), - cf1Call, t1, cf1, cClassBType, false, false, false) - checkCallsite(callGraph.callsites(cf2Call), - cf2Call, t1, cf2, cClassBType, true, false, false) - checkCallsite(callGraph.callsites(cf3Call), - cf3Call, t1, cf3, cClassBType, false, true, false) - checkCallsite(callGraph.callsites(cf4Call), - cf4Call, t1, cf4, cClassBType, true, true, false) - checkCallsite(callGraph.callsites(cf5Call), - cf5Call, t1, cf5, cClassBType, false, false, true) - checkCallsite(callGraph.callsites(cf6Call), - cf6Call, t1, cf6, cClassBType, true, false, true) - checkCallsite(callGraph.callsites(cf7Call), - cf7Call, t1, cf7, cClassBType, false, true, true) - checkCallsite(callGraph.callsites(cg1Call), - cg1Call, t1, g1, cMClassBType, true, false, false) - - checkCallsite(callGraph.callsites(df1Call), - df1Call, t2, df1, dClassBType, false, true, false) - checkCallsite(callGraph.callsites(df2Call), - df2Call, t2, cf2, cClassBType, true, false, false) - checkCallsite(callGraph.callsites(df3Call), - df3Call, t2, df3, dClassBType, true, false, false) - checkCallsite(callGraph.callsites(df4Call), - df4Call, t2, cf4, cClassBType, true, true, false) - checkCallsite(callGraph.callsites(df5Call), - df5Call, t2, cf5, cClassBType, false, false, true) - checkCallsite(callGraph.callsites(df6Call), - df6Call, t2, cf6, cClassBType, true, false, true) - checkCallsite(callGraph.callsites(df7Call), - df7Call, t2, cf7, cClassBType, false, true, true) - checkCallsite(callGraph.callsites(dg1Call), - dg1Call, t2, g1, cMClassBType, true, false, false) + checkCallsite(cf1Call, t1, cf1, cClassBType, false, false, false) + checkCallsite(cf2Call, t1, cf2, cClassBType, true, false, false) + checkCallsite(cf3Call, t1, cf3, cClassBType, false, true, false) + checkCallsite(cf4Call, t1, cf4, cClassBType, true, true, false) + checkCallsite(cf5Call, t1, cf5, cClassBType, false, false, true) + checkCallsite(cf6Call, t1, cf6, cClassBType, true, false, true) + checkCallsite(cf7Call, t1, cf7, cClassBType, false, true, true) + checkCallsite(cg1Call, t1, g1, cMClassBType, true, false, false) + + checkCallsite(df1Call, t2, df1, dClassBType, false, true, false) + checkCallsite(df2Call, t2, cf2, cClassBType, true, false, false) + checkCallsite(df3Call, t2, df3, dClassBType, true, false, false) + checkCallsite(df4Call, t2, cf4, cClassBType, true, true, false) + checkCallsite(df5Call, t2, cf5, cClassBType, false, false, true) + checkCallsite(df6Call, t2, cf6, cClassBType, true, false, true) + checkCallsite(df7Call, t2, cf7, cClassBType, false, true, true) + checkCallsite(dg1Call, t2, g1, cMClassBType, true, false, false) + } + + @Test + def callerSensitiveNotSafeToInline(): Unit = { + val code = + """class C { + | def m = java.lang.Class.forName("C") + |} + """.stripMargin + val List(c) = compile(code) + val m = findAsmMethod(c, "m") + val List(fn) = callsInMethod(m) + val forNameMeth = byteCodeRepository.methodNode("java/lang/Class", "forName", "(Ljava/lang/String;)Ljava/lang/Class;").get._1 + val classTp = classBTypeFromInternalName("java/lang/Class") + val r = callGraph.callsites(m)(fn) + checkCallsite(fn, m, forNameMeth, classTp, safeToInline = false, atInline = false, atNoInline = false) + } + + @Test + def checkArgInfos(): Unit = { + val code = + """abstract class C { + | def h(f: Int => Int): Int = f(1) + | def t1 = h(x => x + 1) + | def t2(i: Int, f: Int => Int, z: Int) = h(f) + i - z + | def t3(f: Int => Int) = h(x => f(x + 1)) + |} + |trait D { + | def iAmASam(x: Int): Int + | def selfSamCall = iAmASam(10) + |} + |""".stripMargin + val List(c, d) = compile(code) + + def callIn(m: String) = callGraph.callsites.find(_._1.name == m).get._2.values.head + val t1h = callIn("t1") + assertEquals(t1h.argInfos.toList, List((1, FunctionLiteral))) + + val t2h = callIn("t2") + assertEquals(t2h.argInfos.toList, List((1, ForwardedParam(2)))) + + val t3h = callIn("t3") + assertEquals(t3h.argInfos.toList, List((1, FunctionLiteral))) + + val selfSamCall = callIn("selfSamCall") + assertEquals(selfSamCall.argInfos.toList, List((0,ForwardedParam(0)))) + } + + @Test + def argInfoAfterInlining(): Unit = { + val code = + """class C { + | def foo(f: Int => Int) = f(1) // not inlined + | @inline final def bar(g: Int => Int) = foo(g) // forwarded param 1 + | @inline final def baz = foo(x => x + 1) // literal + | + | def t1 = bar(x => x + 1) // call to foo should have argInfo literal + | def t2(x: Int, f: Int => Int) = x + bar(f) // call to foo should have argInfo forwarded param 2 + | def t3 = baz // call to foo should have argInfo literal + | def someFun: Int => Int = null + | def t4(x: Int) = x + bar(someFun) // call to foo has empty argInfo + |} + """.stripMargin + + compile(code) + def callIn(m: String) = callGraph.callsites.find(_._1.name == m).get._2.values.head + assertEquals(callIn("t1").argInfos.toList, List((1, FunctionLiteral))) + assertEquals(callIn("t2").argInfos.toList, List((1, ForwardedParam(2)))) + assertEquals(callIn("t3").argInfos.toList, List((1, FunctionLiteral))) + assertEquals(callIn("t4").argInfos.toList, Nil) } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala new file mode 100644 index 0000000000..e8530af4e0 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/ClosureOptimizerTest.scala @@ -0,0 +1,101 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.collection.generic.Clearable +import scala.collection.mutable.ListBuffer +import scala.reflect.internal.util.BatchSourceFile +import scala.tools.asm.Opcodes._ +import org.junit.Assert._ + +import scala.tools.asm.tree._ +import scala.tools.asm.tree.analysis._ +import scala.tools.nsc.io._ +import scala.tools.nsc.reporters.StoreReporter +import scala.tools.testing.AssertUtil._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import AsmUtils._ + +import BackendReporting._ + +import scala.collection.JavaConverters._ +import scala.tools.testing.ClearAfterClass + +@RunWith(classOf[JUnit4]) +class ClosureOptimizerTest extends ClearAfterClass { + val compiler = cached("compiler", () => newCompiler(extraArgs = "-Yopt:l:classpath -Yopt-warnings:_")) + + @Test + def nothingTypedClosureBody(): Unit = { + val code = + """abstract class C { + | def isEmpty: Boolean + | @inline final def getOrElse[T >: C](f: => T) = if (isEmpty) f else this + | def t = getOrElse(throw new Error("")) + |} + """.stripMargin + + val List(c) = compileClasses(compiler)(code) + val t = findAsmMethod(c, "t") + val List(bodyCall) = findInstr(t, "INVOKESTATIC C.C$$$anonfun$1 ()Lscala/runtime/Nothing$") + assert(bodyCall.getNext.getOpcode == ATHROW) + } + + @Test + def nullTypedClosureBody(): Unit = { + val code = + """abstract class C { + | def isEmpty: Boolean + | @inline final def getOrElse[T >: C](f: => T) = if (isEmpty) f else this + | def t = getOrElse(null) + |} + """.stripMargin + + val List(c) = compileClasses(compiler)(code) + val t = findAsmMethod(c, "t") + val List(bodyCall) = findInstr(t, "INVOKESTATIC C.C$$$anonfun$1 ()Lscala/runtime/Null$") + assert(bodyCall.getNext.getOpcode == POP) + assert(bodyCall.getNext.getNext.getOpcode == ACONST_NULL) + } + + @Test + def makeLMFCastExplicit(): Unit = { + val code = + """class C { + | def t(l: List[String]) = { + | val fun: String => String = s => s + | fun(l.head) + | } + |} + """.stripMargin + val List(c) = compileClasses(compiler)(code) + assertSameCode(getSingleMethod(c, "t"), + List(VarOp(ALOAD, 1), Invoke(INVOKEVIRTUAL, "scala/collection/immutable/List", "head", "()Ljava/lang/Object;", false), + TypeOp(CHECKCAST, "java/lang/String"), Invoke(INVOKESTATIC, "C", "C$$$anonfun$1", "(Ljava/lang/String;)Ljava/lang/String;", false), + Op(ARETURN))) + } + + @Test + def closureOptWithUnreachableCode(): Unit = { + // this example used to crash the ProdCons analysis in the closure optimizer - ProdCons + // expects no unreachable code. + val code = + """class C { + | @inline final def m = throw new Error("") + | def t = { + | val f = (x: Int) => x + 1 + | m + | f(10) // unreachable after inlining m + | } + |} + """.stripMargin + val List(c) = compileClasses(compiler)(code) + assertSameSummary(getSingleMethod(c, "t"), List(NEW, DUP, LDC, "<init>", ATHROW)) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala index 76492cfa23..ac1b759fe2 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/CompactLocalVariablesTest.scala @@ -17,8 +17,8 @@ class CompactLocalVariablesTest { // recurse-unreachable-jumps is required for eliminating catch blocks, in the first dce round they // are still live.only after eliminating the empty handler the catch blocks become unreachable. - val methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code,compact-locals") - val noCompactVarsCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code") + val methodOptCompiler = newCompiler(extraArgs = "-Yopt:unreachable-code,compact-locals") + val noCompactVarsCompiler = newCompiler(extraArgs = "-Yopt:unreachable-code") @Test def compactUnused(): Unit = { diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala index cb01f3d164..6d566c722f 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/EmptyExceptionHandlersTest.scala @@ -13,21 +13,11 @@ import scala.tools.partest.ASMConverters import ASMConverters._ import scala.tools.testing.ClearAfterClass -object EmptyExceptionHandlersTest extends ClearAfterClass.Clearable { - var noOptCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none") - var dceCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:unreachable-code") - def clear(): Unit = { - noOptCompiler = null - dceCompiler = null - } -} @RunWith(classOf[JUnit4]) class EmptyExceptionHandlersTest extends ClearAfterClass { - ClearAfterClass.stateToClear = EmptyExceptionHandlersTest - - val noOptCompiler = EmptyExceptionHandlersTest.noOptCompiler - val dceCompiler = EmptyExceptionHandlersTest.dceCompiler + val noOptCompiler = cached("noOptCompiler", () => newCompiler(extraArgs = "-Yopt:l:none")) + val dceCompiler = cached("dceCompiler", () => newCompiler(extraArgs = "-Yopt:unreachable-code")) val exceptionDescriptor = "java/lang/Exception" diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala index 57088bdd2f..5cb1aab4a9 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineInfoTest.scala @@ -16,22 +16,21 @@ import scala.tools.testing.ClearAfterClass import BackendReporting._ -import scala.collection.convert.decorateAsScala._ +import scala.collection.JavaConverters._ -object InlineInfoTest extends ClearAfterClass.Clearable { - var compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:classpath") - def clear(): Unit = { compiler = null } +@RunWith(classOf[JUnit4]) +class InlineInfoTest extends ClearAfterClass { + val compiler = cached("compiler", () => newCompiler(extraArgs = "-Yopt:l:classpath")) - def notPerRun: List[Clearable] = List(compiler.genBCode.bTypes.classBTypeFromInternalName, compiler.genBCode.bTypes.byteCodeRepository.classes) + import compiler.genBCode.bTypes + def notPerRun: List[Clearable] = List( + bTypes.classBTypeFromInternalName, + bTypes.byteCodeRepository.compilingClasses, + bTypes.byteCodeRepository.parsedClasses) notPerRun foreach compiler.perRunCaches.unrecordCache -} - -@RunWith(classOf[JUnit4]) -class InlineInfoTest { - val compiler = InlineInfoTest.compiler def compile(code: String) = { - InlineInfoTest.notPerRun.foreach(_.clear()) + notPerRun.foreach(_.clear()) compileClasses(compiler)(code) } @@ -55,6 +54,7 @@ class InlineInfoTest { |class C extends T with U """.stripMargin val classes = compile(code) + val fromSyms = classes.map(c => compiler.genBCode.bTypes.classBTypeFromInternalName(c.name).info.get.inlineInfo) val fromAttrs = classes.map(c => { diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala index 029caa995c..6dd0a33289 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlineWarningTest.scala @@ -13,7 +13,6 @@ import org.junit.Assert._ import scala.tools.asm.tree._ import scala.tools.asm.tree.analysis._ -import scala.tools.nsc.backend.jvm.opt.BytecodeUtils.AsmAnalyzer import scala.tools.nsc.io._ import scala.tools.nsc.reporters.StoreReporter import scala.tools.testing.AssertUtil._ @@ -25,23 +24,17 @@ import AsmUtils._ import BackendReporting._ -import scala.collection.convert.decorateAsScala._ +import scala.collection.JavaConverters._ import scala.tools.testing.ClearAfterClass -object InlineWarningTest extends ClearAfterClass.Clearable { - val argsNoWarn = "-Ybackend:GenBCode -Yopt:l:classpath" - val args = argsNoWarn + " -Yopt-warnings" - var compiler = newCompiler(extraArgs = args) - def clear(): Unit = { compiler = null } -} - @RunWith(classOf[JUnit4]) class InlineWarningTest extends ClearAfterClass { - ClearAfterClass.stateToClear = InlineWarningTest - - val compiler = InlineWarningTest.compiler + val argsNoWarn = "-Yopt:l:classpath" + val args = argsNoWarn + " -Yopt-warnings" + val compiler = cached("compiler", () => newCompiler(extraArgs = args)) + val compilerWarnAll = cached("compilerWarnAll", () => newCompiler(extraArgs = argsNoWarn + " -Yopt-warnings:_")) - def compile(scalaCode: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[ClassNode] = { + def compile(scalaCode: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false, compiler: Global = compiler): List[ClassNode] = { compileClasses(compiler)(scalaCode, javaCode, allowMessage) } @@ -70,29 +63,6 @@ class InlineWarningTest extends ClearAfterClass { } @Test - def traitMissingImplClass(): Unit = { - val codeA = "trait T { @inline final def f = 1 }" - val codeB = "class C { def t1(t: T) = t.f }" - - val removeImpl = (outDir: AbstractFile) => { - val f = outDir.lookupName("T$class.class", directory = false) - if (f != null) f.delete() - } - - val warn = - """T::f()I is annotated @inline but cannot be inlined: the trait method call could not be rewritten to the static implementation method. Possible reason: - |The method f(LT;)I could not be found in the class T$class or any of its parents. - |Note that the following parent classes could not be found on the classpath: T$class""".stripMargin - - var c = 0 - compileSeparately(List(codeA, codeB), extraArgs = InlineWarningTest.args, afterEach = removeImpl, allowMessage = i => {c += 1; i.msg contains warn}) - assert(c == 1, c) - - // only summary here - compileSeparately(List(codeA, codeB), extraArgs = InlineWarningTest.argsNoWarn, afterEach = removeImpl, allowMessage = _.msg contains "there was one inliner warning") - } - - @Test def handlerNonEmptyStack(): Unit = { val code = """class C { @@ -125,22 +95,22 @@ class InlineWarningTest extends ClearAfterClass { val warns = List( """failed to determine if bar should be inlined: |The method bar()I could not be found in the class A or any of its parents. - |Note that the following parent classes are defined in Java sources (mixed compilation), no bytecode is available: A""".stripMargin, + |Note that the parent class A is defined in a Java source (mixed compilation), no bytecode is available.""".stripMargin, """B::flop()I is annotated @inline but could not be inlined: |Failed to check if B::flop()I can be safely inlined to B without causing an IllegalAccessError. Checking instruction INVOKESTATIC A.bar ()I failed: |The method bar()I could not be found in the class A or any of its parents. - |Note that the following parent classes are defined in Java sources (mixed compilation), no bytecode is available: A""".stripMargin) + |Note that the parent class A is defined in a Java source (mixed compilation), no bytecode is available.""".stripMargin) var c = 0 val List(b) = compile(scalaCode, List((javaCode, "A.java")), allowMessage = i => {c += 1; warns.tail.exists(i.msg contains _)}) assert(c == 1, c) // no warnings here - compileClasses(newCompiler(extraArgs = InlineWarningTest.argsNoWarn + " -Yopt-warnings:none"))(scalaCode, List((javaCode, "A.java"))) + compileClasses(newCompiler(extraArgs = argsNoWarn + " -Yopt-warnings:none"))(scalaCode, List((javaCode, "A.java"))) c = 0 - compileClasses(newCompiler(extraArgs = InlineWarningTest.argsNoWarn + " -Yopt-warnings:no-inline-mixed"))(scalaCode, List((javaCode, "A.java")), allowMessage = i => {c += 1; warns.exists(i.msg contains _)}) + compileClasses(newCompiler(extraArgs = argsNoWarn + " -Yopt-warnings:no-inline-mixed"))(scalaCode, List((javaCode, "A.java")), allowMessage = i => {c += 1; warns.exists(i.msg contains _)}) assert(c == 2, c) } @@ -172,6 +142,33 @@ class InlineWarningTest extends ClearAfterClass { } @Test + def dontWarnWhenNotIlnineAnnotated(): Unit = { + val code = + """class M { + | final def f(t: Int => Int) = { + | @noinline def nested = 0 + | nested + t(1) + | } + | def t = f(x => x + 1) + |} + | + |class N { + | def t(a: M) = a.f(x => x + 1) + |} + """.stripMargin + compile(code, allowMessage = _ => false) // no warnings allowed + + val warn = + """M::f(Lscala/Function1;)I could not be inlined: + |The callee M::f(Lscala/Function1;)I contains the instruction INVOKESPECIAL M.nested$1 ()I + |that would cause an IllegalAccessError when inlined into class N""".stripMargin + + var c = 0 + compile(code, compiler = compilerWarnAll, allowMessage = i => { c += 1; i.msg contains warn }) + assert(c == 1, c) + } + + @Test def cannotMixStrictfp(): Unit = { val code = """import annotation.strictfp diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala index 7ed0e13226..ab1aef47cd 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerIllegalAccessTest.scala @@ -16,19 +16,12 @@ import scala.tools.partest.ASMConverters import ASMConverters._ import AsmUtils._ -import scala.collection.convert.decorateAsScala._ +import scala.collection.JavaConverters._ import scala.tools.testing.ClearAfterClass -object InlinerIllegalAccessTest extends ClearAfterClass.Clearable { - var compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none") - def clear(): Unit = { compiler = null } -} - @RunWith(classOf[JUnit4]) class InlinerIllegalAccessTest extends ClearAfterClass { - ClearAfterClass.stateToClear = InlinerIllegalAccessTest - - val compiler = InlinerIllegalAccessTest.compiler + val compiler = cached("compiler", () => newCompiler(extraArgs = "-Yopt:l:none")) import compiler.genBCode.bTypes._ def addToRepo(cls: List[ClassNode]): Unit = for (c <- cls) byteCodeRepository.add(c, ByteCodeRepository.Classfile) @@ -67,7 +60,7 @@ class InlinerIllegalAccessTest extends ClearAfterClass { check(dClass, assertEmpty) check(eClass, assertEmpty) // C is public, so accessible in E - byteCodeRepository.classes.clear() + byteCodeRepository.parsedClasses.clear() classBTypeFromInternalName.clear() cClass.access &= ~ACC_PUBLIC // ftw diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala index 5c9bd1c188..075513a2b7 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerSeparateCompilationTest.scala @@ -13,16 +13,15 @@ import scala.tools.partest.ASMConverters import ASMConverters._ import AsmUtils._ -import scala.collection.convert.decorateAsScala._ +import scala.collection.JavaConverters._ object InlinerSeparateCompilationTest { - val args = "-Ybackend:GenBCode -Yopt:l:classpath" + val args = "-Yopt:l:classpath" } @RunWith(classOf[JUnit4]) class InlinerSeparateCompilationTest { import InlinerSeparateCompilationTest._ - import InlinerTest.{listStringLines, assertInvoke, assertNoInvoke} @Test def inlnieMixedinMember(): Unit = { @@ -44,7 +43,7 @@ class InlinerSeparateCompilationTest { """.stripMargin val warn = "T::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden" - val List(c, o, oMod, t, tCls) = compileClassesSeparately(List(codeA, codeB), args + " -Yopt-warnings", _.msg contains warn) + val List(c, o, oMod, t) = compileClassesSeparately(List(codeA, codeB), args + " -Yopt-warnings", _.msg contains warn) assertInvoke(getSingleMethod(c, "t1"), "T", "f") assertNoInvoke(getSingleMethod(c, "t2")) assertNoInvoke(getSingleMethod(c, "t3")) @@ -64,7 +63,7 @@ class InlinerSeparateCompilationTest { |} """.stripMargin - val List(c, t, tCls) = compileClassesSeparately(List(codeA, codeB), args) + val List(c, t) = compileClassesSeparately(List(codeA, codeB), args) assertNoInvoke(getSingleMethod(c, "t1")) } @@ -87,7 +86,7 @@ class InlinerSeparateCompilationTest { |} """.stripMargin - val List(c, t, tCls, u, uCls) = compileClassesSeparately(List(codeA, codeB), args) + val List(c, t, u) = compileClassesSeparately(List(codeA, codeB), args) for (m <- List("t1", "t2", "t3")) assertNoInvoke(getSingleMethod(c, m)) } @@ -108,8 +107,8 @@ class InlinerSeparateCompilationTest { |$assembly """.stripMargin - val List(a, aCls, t, tCls) = compileClassesSeparately(List(codeA, assembly), args) - assertNoInvoke(getSingleMethod(tCls, "f")) - assertNoInvoke(getSingleMethod(aCls, "n")) + val List(a, t) = compileClassesSeparately(List(codeA, assembly), args) + assertNoInvoke(getSingleMethod(t, "f")) + assertNoInvoke(getSingleMethod(a, "n")) } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala index 0309bb97cc..b7641b5ec7 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -6,17 +6,11 @@ import org.junit.runner.RunWith import org.junit.runners.JUnit4 import org.junit.Test import scala.collection.generic.Clearable -import scala.collection.mutable.ListBuffer -import scala.reflect.internal.util.BatchSourceFile import scala.tools.asm.Opcodes._ import org.junit.Assert._ import scala.tools.asm.tree._ -import scala.tools.asm.tree.analysis._ -import scala.tools.nsc.backend.jvm.opt.BytecodeUtils.AsmAnalyzer -import scala.tools.nsc.io._ import scala.tools.nsc.reporters.StoreReporter -import scala.tools.testing.AssertUtil._ import CodeGenTools._ import scala.tools.partest.ASMConverters @@ -25,49 +19,35 @@ import AsmUtils._ import BackendReporting._ -import scala.collection.convert.decorateAsScala._ +import scala.collection.JavaConverters._ import scala.tools.testing.ClearAfterClass -object InlinerTest extends ClearAfterClass.Clearable { - val args = "-Ybackend:GenBCode -Yopt:l:classpath -Yopt-warnings" - var compiler = newCompiler(extraArgs = args) - - // allows inspecting the caches after a compilation run - def notPerRun: List[Clearable] = List(compiler.genBCode.bTypes.classBTypeFromInternalName, compiler.genBCode.bTypes.byteCodeRepository.classes, compiler.genBCode.bTypes.callGraph.callsites) - notPerRun foreach compiler.perRunCaches.unrecordCache - - def clear(): Unit = { compiler = null } - - implicit class listStringLines[T](val l: List[T]) extends AnyVal { - def stringLines = l.mkString("\n") - } - - def assertNoInvoke(m: Method): Unit = assertNoInvoke(m.instructions) - def assertNoInvoke(ins: List[Instruction]): Unit = { - assert(!ins.exists(_.isInstanceOf[Invoke]), ins.stringLines) - } - - def assertInvoke(m: Method, receiver: String, method: String): Unit = assertInvoke(m.instructions, receiver, method) - def assertInvoke(l: List[Instruction], receiver: String, method: String): Unit = { - assert(l.exists { - case Invoke(_, `receiver`, `method`, _, _) => true - case _ => false - }, l.stringLines) - } -} - @RunWith(classOf[JUnit4]) class InlinerTest extends ClearAfterClass { - ClearAfterClass.stateToClear = InlinerTest - - import InlinerTest.{listStringLines, assertInvoke, assertNoInvoke} + val args = "-Yopt:l:classpath -Yopt-warnings" + val compiler = cached("compiler", () => newCompiler(extraArgs = args)) + val inlineOnlyCompiler = cached("inlineOnlyCompiler", () => newCompiler(extraArgs = "-Yopt:inline-project")) + import compiler.genBCode.bTypes + // allows inspecting the caches after a compilation run + def notPerRun: List[Clearable] = List( + bTypes.classBTypeFromInternalName, + bTypes.byteCodeRepository.compilingClasses, + bTypes.byteCodeRepository.parsedClasses, + bTypes.callGraph.callsites) + notPerRun foreach compiler.perRunCaches.unrecordCache - val compiler = InlinerTest.compiler import compiler.genBCode.bTypes._ + import compiler.genBCode.bTypes.backendUtils._ + import inlinerHeuristics._ + def compile(scalaCode: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[ClassNode] = { - InlinerTest.notPerRun.foreach(_.clear()) + notPerRun.foreach(_.clear()) compileClasses(compiler)(scalaCode, javaCode, allowMessage) + // Use the class nodes stored in the byteCodeRepository. The ones returned by compileClasses are not the same, + // these are created new from the classfile byte array. They are completely separate instances which cannot + // be used to look up methods / callsites in the callGraph hash maps for example. + byteCodeRepository.compilingClasses.valuesIterator.toList.sortBy(_.name) } def checkCallsite(callsite: callGraph.Callsite, callee: MethodNode) = { @@ -79,27 +59,25 @@ class InlinerTest extends ClearAfterClass { assert(callsite.callee.get.callee == callee, callsite.callee.get.callee.name) } - // inline first invocation of f into g in class C - def inlineTest(code: String, mod: ClassNode => Unit = _ => ()): (MethodNode, Option[CannotInlineWarning]) = { - val List(cls) = compile(code) - mod(cls) - val clsBType = classBTypeFromParsedClassfile(cls.name) + def getCallsite(method: MethodNode, calleeName: String) = callGraph.callsites(method).valuesIterator.find(_.callee.get.callee.name == calleeName).get - val List(f, g) = cls.methods.asScala.filter(m => Set("f", "g")(m.name)).toList.sortBy(_.name) - val fCall = g.instructions.iterator.asScala.collect({ case i: MethodInsnNode if i.name == "f" => i }).next() + def gMethAndFCallsite(code: String, mod: ClassNode => Unit = _ => ()) = { + val List(c) = compile(code) + mod(c) + val gMethod = findAsmMethod(c, "g") + val fCall = getCallsite(gMethod, "f") + (gMethod, fCall) + } - val analyzer = new AsmAnalyzer(g, clsBType.internalName) + def canInlineTest(code: String, mod: ClassNode => Unit = _ => ()): Option[OptimizerWarning] = { + val cs = gMethAndFCallsite(code, mod)._2 + inliner.earlyCanInlineCheck(cs) orElse inliner.canInlineBody(cs) + } - val r = inliner.inline( - fCall, - analyzer.frameAt(fCall).getStackSize, - g, - clsBType, - f, - clsBType, - receiverKnownNotNull = true, - keepLineNumbers = true) - (g, r) + def inlineTest(code: String, mod: ClassNode => Unit = _ => ()): MethodNode = { + val (gMethod, fCall) = gMethAndFCallsite(code, mod) + inliner.inline(InlineRequest(fCall, Nil)) + gMethod } @Test @@ -111,10 +89,10 @@ class InlinerTest extends ClearAfterClass { |} """.stripMargin - val (g, _) = inlineTest(code) + val g = inlineTest(code) val gConv = convertMethod(g) - assertSameCode(gConv.instructions.dropNonOp, + assertSameCode(gConv, List( VarOp(ALOAD, 0), VarOp(ASTORE, 1), // store this Op(ICONST_1), VarOp(ISTORE, 2), Jump(GOTO, Label(10)), // store return value @@ -145,16 +123,23 @@ class InlinerTest extends ClearAfterClass { // See also discussion around ATHROW in BCodeBodyBuilder - val (g, _) = inlineTest(code) - val expectedInlined = List( - VarOp(ALOAD, 0), VarOp(ASTORE, 1), // store this - Field(GETSTATIC, "scala/Predef$", "MODULE$", "Lscala/Predef$;"), Invoke(INVOKEVIRTUAL, "scala/Predef$", "$qmark$qmark$qmark", "()Lscala/runtime/Nothing$;", false)) // inlined call to ??? + val g = inlineTest(code) - assertSameCode(convertMethod(g).instructions.dropNonOp.take(4), expectedInlined) + val invokeQQQ = List( + Field(GETSTATIC, "scala/Predef$", "MODULE$", "Lscala/Predef$;"), + Invoke(INVOKEVIRTUAL, "scala/Predef$", "$qmark$qmark$qmark", "()Lscala/runtime/Nothing$;", false)) + + val gBeforeLocalOpt = VarOp(ALOAD, 0) :: VarOp(ASTORE, 1) :: invokeQQQ ::: List( + VarOp(ASTORE, 2), + Jump(GOTO, Label(11)), + Label(11), + VarOp(ALOAD, 2), + Op(ATHROW)) + + assertSameCode(convertMethod(g), gBeforeLocalOpt) compiler.genBCode.bTypes.localOpt.methodOptimizations(g, "C") - assertSameCode(convertMethod(g).instructions.dropNonOp, - expectedInlined ++ List(VarOp(ASTORE, 2), VarOp(ALOAD, 2), Op(ATHROW))) + assertSameCode(convertMethod(g), invokeQQQ :+ Op(ATHROW)) } @Test @@ -166,11 +151,11 @@ class InlinerTest extends ClearAfterClass { |} """.stripMargin - val (_, can) = inlineTest(code, cls => { + val can = canInlineTest(code, cls => { val f = cls.methods.asScala.find(_.name == "f").get f.access |= ACC_SYNCHRONIZED }) - assert(can.get.isInstanceOf[SynchronizedMethod], can) + assert(can.nonEmpty && can.get.isInstanceOf[SynchronizedMethod], can) } @Test @@ -181,7 +166,7 @@ class InlinerTest extends ClearAfterClass { | def g = f + 1 |} """.stripMargin - val (_, r) = inlineTest(code) + val r = canInlineTest(code) assert(r.isEmpty, r) } @@ -195,8 +180,8 @@ class InlinerTest extends ClearAfterClass { | def g = println(f) |} """.stripMargin - val (_, r) = inlineTest(code) - assert(r.get.isInstanceOf[MethodWithHandlerCalledOnNonEmptyStack], r) + val r = canInlineTest(code) + assert(r.nonEmpty && r.get.isInstanceOf[MethodWithHandlerCalledOnNonEmptyStack], r) } @Test @@ -216,29 +201,10 @@ class InlinerTest extends ClearAfterClass { """.stripMargin val List(c, d) = compile(code) - - val cTp = classBTypeFromParsedClassfile(c.name) - val dTp = classBTypeFromParsedClassfile(d.name) - - val g = c.methods.asScala.find(_.name == "g").get - val h = d.methods.asScala.find(_.name == "h").get - val gCall = h.instructions.iterator.asScala.collect({ - case m: MethodInsnNode if m.name == "g" => m - }).next() - - val analyzer = new AsmAnalyzer(h, dTp.internalName) - - val r = inliner.inline( - gCall, - analyzer.frameAt(gCall).getStackSize, - h, - dTp, - g, - cTp, - receiverKnownNotNull = true, - keepLineNumbers = true) - - assert(r.get.isInstanceOf[IllegalAccessInstruction], r) + val hMeth = findAsmMethod(d, "h") + val gCall = getCallsite(hMeth, "g") + val r = inliner.canInlineBody(gCall) + assert(r.nonEmpty && r.get.isInstanceOf[IllegalAccessInstruction], r) } @Test @@ -273,7 +239,7 @@ class InlinerTest extends ClearAfterClass { assert(gIns contains invokeG, gIns) // f is inlined into g, g invokes itself recursively assert(callGraph.callsites.size == 3, callGraph.callsites) - for (callsite <- callGraph.callsites.values if methods.contains(callsite.callsiteMethod)) { + for (callsite <- callGraph.callsites.valuesIterator.flatMap(_.valuesIterator) if methods.contains(callsite.callsiteMethod)) { checkCallsite(callsite, g) } } @@ -295,8 +261,8 @@ class InlinerTest extends ClearAfterClass { assert(gIns.count(_ == invokeG) == 2, gIns) assert(hIns.count(_ == invokeG) == 2, hIns) - assert(callGraph.callsites.size == 7, callGraph.callsites) - for (callsite <- callGraph.callsites.values if methods.contains(callsite.callsiteMethod)) { + assert(callGraph.callsites.valuesIterator.flatMap(_.valuesIterator).size == 7, callGraph.callsites) + for (callsite <- callGraph.callsites.valuesIterator.flatMap(_.valuesIterator) if methods.contains(callsite.callsiteMethod)) { checkCallsite(callsite, g) } } @@ -336,7 +302,7 @@ class InlinerTest extends ClearAfterClass { |} """.stripMargin val List(c) = compile(code) - assert(callGraph.callsites.values exists (_.callsiteInstruction.name == "clone")) + assert(callGraph.callsites.valuesIterator.flatMap(_.valuesIterator) exists (_.callsiteInstruction.name == "clone")) } @Test @@ -349,7 +315,7 @@ class InlinerTest extends ClearAfterClass { | def g(t: T) = t.f |} """.stripMargin - val List(c, t, tClass) = compile(code) + val List(c, t) = compile(code) assertNoInvoke(getSingleMethod(c, "g")) } @@ -375,26 +341,14 @@ class InlinerTest extends ClearAfterClass { """.stripMargin val List(c) = compile(code) - val f = c.methods.asScala.find(_.name == "f").get - val callsiteIns = f.instructions.iterator().asScala.collect({ case c: MethodInsnNode => c }).next() - val clsBType = classBTypeFromParsedClassfile(c.name) - val analyzer = new AsmAnalyzer(f, clsBType.internalName) - - val integerClassBType = classBTypeFromInternalName("java/lang/Integer") - val lowestOneBitMethod = byteCodeRepository.methodNode(integerClassBType.internalName, "lowestOneBit", "(I)I").get._1 - - val r = inliner.inline( - callsiteIns, - analyzer.frameAt(callsiteIns).getStackSize, - f, - clsBType, - lowestOneBitMethod, - integerClassBType, - receiverKnownNotNull = false, - keepLineNumbers = false) + val fMeth = findAsmMethod(c, "f") + val call = getCallsite(fMeth, "lowestOneBit") - assert(r.isEmpty, r) - val ins = instructionsFromMethod(f) + val warning = inliner.canInlineBody(call) + assert(warning.isEmpty, warning) + + inliner.inline(InlineRequest(call, Nil)) + val ins = instructionsFromMethod(fMeth) // no invocations, lowestOneBit is inlined assertNoInvoke(ins) @@ -425,7 +379,8 @@ class InlinerTest extends ClearAfterClass { |} """.stripMargin - val List(c) = compile(code) + // use a compiler without local optimizations (cleanups) + val List(c) = compileClasses(inlineOnlyCompiler)(code) val ms @ List(f1, f2, g1, g2) = c.methods.asScala.filter(_.name.length == 2).toList // stack height at callsite of f1 is 1, so max of g1 after inlining is max of f1 + 1 @@ -465,7 +420,7 @@ class InlinerTest extends ClearAfterClass { """B::flop()I is annotated @inline but could not be inlined: |Failed to check if B::flop()I can be safely inlined to B without causing an IllegalAccessError. Checking instruction INVOKESTATIC A.bar ()I failed: |The method bar()I could not be found in the class A or any of its parents. - |Note that the following parent classes are defined in Java sources (mixed compilation), no bytecode is available: A""".stripMargin + |Note that the parent class A is defined in a Java source (mixed compilation), no bytecode is available.""".stripMargin var c = 0 val List(b) = compile(scalaCode, List((javaCode, "A.java")), allowMessage = i => {c += 1; i.msg contains warn}) @@ -488,26 +443,13 @@ class InlinerTest extends ClearAfterClass { | def t2(c: C) = c.f |} """.stripMargin - val List(c, t, tClass) = compile(code) + val List(c, t) = compile(code) // both are just `return 1`, no more calls assertNoInvoke(getSingleMethod(c, "t1")) assertNoInvoke(getSingleMethod(c, "t2")) } @Test - def inlineMixinMethods(): Unit = { - val code = - """trait T { - | @inline final def f = 1 - |} - |class C extends T - """.stripMargin - val List(c, t, tClass) = compile(code) - // the static implementation method is inlined into the mixin, so there's no invocation in the mixin - assertNoInvoke(getSingleMethod(c, "f")) - } - - @Test def inlineTraitInherited(): Unit = { val code = """trait T { @@ -521,7 +463,7 @@ class InlinerTest extends ClearAfterClass { | def t2 = g |} """.stripMargin - val List(c, t, tClass, u, uClass) = compile(code) + val List(c, t, u) = compile(code) assertNoInvoke(getSingleMethod(c, "t1")) assertNoInvoke(getSingleMethod(c, "t2")) } @@ -541,7 +483,7 @@ class InlinerTest extends ClearAfterClass { "C::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden", "T::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden") var count = 0 - val List(c, t, tClass) = compile(code, allowMessage = i => {count += 1; warns.exists(i.msg contains _)}) + val List(c, t) = compile(code, allowMessage = i => {count += 1; warns.exists(i.msg contains _)}) assert(count == 2, count) assertInvoke(getSingleMethod(c, "t1"), "T", "f") assertInvoke(getSingleMethod(c, "t2"), "C", "f") @@ -557,7 +499,7 @@ class InlinerTest extends ClearAfterClass { | def t1(t: T) = t.f |} """.stripMargin - val List(c, t, tClass) = compile(code) + val List(c, t) = compile(code) assertNoInvoke(getSingleMethod(c, "t1")) } @@ -569,7 +511,7 @@ class InlinerTest extends ClearAfterClass { |} |object O extends T { | @inline def g = 1 - | // mixin generates `def f = T$class.f(this)`, which is inlined here (we get ICONST_0) + | // mixin generates `def f = super[T].f`, which is inlined here (we get ICONST_0) |} |class C { | def t1 = O.f // the mixin method of O is inlined, so we directly get the ICONST_0 @@ -579,10 +521,10 @@ class InlinerTest extends ClearAfterClass { """.stripMargin val warn = "T::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden" var count = 0 - val List(c, oMirror, oModule, t, tClass) = compile(code, allowMessage = i => {count += 1; i.msg contains warn}) + val List(c, oMirror, oModule, t) = compile(code, allowMessage = i => {count += 1; i.msg contains warn}) assert(count == 1, count) - assertNoInvoke(getSingleMethod(oModule, "f")) + assertNoInvoke(getSingleMethod(t, "f")) assertNoInvoke(getSingleMethod(c, "t1")) assertNoInvoke(getSingleMethod(c, "t2")) @@ -598,21 +540,19 @@ class InlinerTest extends ClearAfterClass { |} |trait Assembly extends T { | @inline final def g = 1 - | @inline final def n = m // inlined. (*) - | // (*) the declaration class of m is T. the signature of T$class.m is m(LAssembly;)I. so we need the self type to build the - | // signature. then we can look up the MethodNode of T$class.m and then rewrite the INVOKEINTERFACE to INVOKESTATIC. + | @inline final def n = m // inlined (m is final) |} |class C { - | def t1(a: Assembly) = a.f // like above, decl class is T, need self-type of T to rewrite the interface call to static. + | def t1(a: Assembly) = a.f // inlined (f is final) | def t2(a: Assembly) = a.n |} """.stripMargin - val List(assembly, assemblyClass, c, t, tClass) = compile(code) + val List(assembly, c, t) = compile(code) - assertNoInvoke(getSingleMethod(tClass, "f")) + assertNoInvoke(getSingleMethod(t, "f")) - assertNoInvoke(getSingleMethod(assemblyClass, "n")) + assertNoInvoke(getSingleMethod(assembly, "n")) assertNoInvoke(getSingleMethod(c, "t1")) assertNoInvoke(getSingleMethod(c, "t2")) @@ -647,30 +587,30 @@ class InlinerTest extends ClearAfterClass { val code = """trait T1 { | @inline def f: Int = 0 - | @inline def g1 = f // not inlined: f not final, so T1$class.g1 has an interface call T1.f + | @inline def g1 = f // not inlined: f not final |} | - |// erased self-type (used in impl class for `self` parameter): T1 + |// erased self-type: T1 |trait T2a { self: T1 with T2a => | @inline override final def f = 1 - | @inline def g2a = f // inlined: resolved as T2a.f, which is re-written to T2a$class.f, so T2a$class.g2a has ICONST_1 + | @inline def g2a = f // inlined: resolved as T2a.f |} | |final class Ca extends T1 with T2a { - | // mixin generates accessors like `def g1 = T1$class.g1`, the impl class method call is inlined into the accessor. + | // mixin generates accessors like `def g1 = super[T1].g1`, the impl super call is inlined into the accessor. | | def m1a = g1 // call to accessor, inlined, we get the interface call T1.f | def m2a = g2a // call to accessor, inlined, we get ICONST_1 | def m3a = f // call to accessor, inlined, we get ICONST_1 | - | def m4a(t: T1) = t.f // T1.f is not final, so not inlined, interface call to T1.f - | def m5a(t: T2a) = t.f // re-written to T2a$class.f, inlined, ICONST_1 + | def m4a(t: T1) = t.f // T1.f is not final, so not inlined, we get an interface call T1.f + | def m5a(t: T2a) = t.f // inlined, we get ICONST_1 |} | |// erased self-type: T2b |trait T2b { self: T2b with T1 => | @inline override final def f = 1 - | @inline def g2b = f // not inlined: resolved as T1.f, so T2b$class.g2b has an interface call T1.f + | @inline def g2b = f // not inlined: resolved as T1.f, we get an interface call T1.f |} | |final class Cb extends T1 with T2b { @@ -679,23 +619,17 @@ class InlinerTest extends ClearAfterClass { | def m3b = f // inlined, we get ICONST_1 | | def m4b(t: T1) = t.f // T1.f is not final, so not inlined, interface call to T1.f - | def m5b(t: T2b) = t.f // re-written to T2b$class.f, inlined, ICONST_1 + | def m5b(t: T2b) = t.f // inlined, ICONST_1 |} """.stripMargin val warning = "T1::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden" var count = 0 - val List(ca, cb, t1, t1C, t2a, t2aC, t2b, t2bC) = compile(code, allowMessage = i => {count += 1; i.msg contains warning}) + val List(ca, cb, t1, t2a, t2b) = compile(code, allowMessage = i => {count += 1; i.msg contains warning}) assert(count == 4, count) // see comments, f is not inlined 4 times - val t2aCfDesc = t2aC.methods.asScala.find(_.name == "f").get.desc - assert(t2aCfDesc == "(LT1;)I", t2aCfDesc) // self-type of T2a is T1 - - val t2bCfDesc = t2bC.methods.asScala.find(_.name == "f").get.desc - assert(t2bCfDesc == "(LT2b;)I", t2bCfDesc) // self-type of T2b is T2b - - assertNoInvoke(getSingleMethod(t2aC, "g2a")) - assertInvoke(getSingleMethod(t2bC, "g2b"), "T1", "f") + assertNoInvoke(getSingleMethod(t2a, "g2a")) + assertInvoke(getSingleMethod(t2b, "g2b"), "T1", "f") assertInvoke(getSingleMethod(ca, "m1a"), "T1", "f") assertNoInvoke(getSingleMethod(ca, "m2a")) // no invoke, see comment on def g2a @@ -732,14 +666,13 @@ class InlinerTest extends ClearAfterClass { val code = """class C { | trait T { @inline final def f = 1 } - | class D extends T{ + | class D extends T { | def m(t: T) = t.f | } - | | def m(d: D) = d.f |} """.stripMargin - val List(c, d, t, tC) = compile(code) + val List(c, d, t) = compile(code) assertNoInvoke(getSingleMethod(d, "m")) assertNoInvoke(getSingleMethod(c, "m")) } @@ -754,9 +687,9 @@ class InlinerTest extends ClearAfterClass { | def t2(t: T) = t.f(2) |} """.stripMargin - val List(c, t, tc) = compile(code) - val t1 = getSingleMethod(tc, "t1") - val t2 = getSingleMethod(tc, "t2") + val List(c, t) = compile(code) + val t1 = getSingleMethod(t, "t1") + val t2 = getSingleMethod(t, "t2") val cast = TypeOp(CHECKCAST, "C") Set(t1, t2).foreach(m => assert(m.instructions.contains(cast), m.instructions)) } @@ -803,8 +736,8 @@ class InlinerTest extends ClearAfterClass { | final val d = 3 | final val d1: Int = 3 | - | @noinline def f = 5 // re-written to T$class - | @noinline final def g = 6 // re-written + | @noinline def f = 5 + | @noinline final def g = 6 | | @noinline def h: Int | @inline def i: Int @@ -817,8 +750,8 @@ class InlinerTest extends ClearAfterClass { | final val d = 3 | final val d1: Int = 3 | - | @noinline def f = 5 // not re-written (not final) - | @noinline final def g = 6 // re-written + | @noinline def f = 5 + | @noinline final def g = 6 | | @noinline def h: Int | @inline def i: Int @@ -835,7 +768,7 @@ class InlinerTest extends ClearAfterClass { |} """.stripMargin - val List(c, t, tClass, u, uClass) = compile(code, allowMessage = _.msg contains "i()I is annotated @inline but cannot be inlined") + val List(c, t, u) = compile(code, allowMessage = _.msg contains "i()I is annotated @inline but cannot be inlined") val m1 = getSingleMethod(c, "m1") assertInvoke(m1, "T", "a") assertInvoke(m1, "T", "b") @@ -844,8 +777,8 @@ class InlinerTest extends ClearAfterClass { assertNoInvoke(getSingleMethod(c, "m2")) val m3 = getSingleMethod(c, "m3") - assertInvoke(m3, "T$class", "f") - assertInvoke(m3, "T$class", "g") + assertInvoke(m3, "T", "f") + assertInvoke(m3, "T", "g") assertInvoke(m3, "T", "h") assertInvoke(m3, "T", "i") @@ -858,7 +791,7 @@ class InlinerTest extends ClearAfterClass { val m6 = getSingleMethod(c, "m6") assertInvoke(m6, "U", "f") - assertInvoke(m6, "U$class", "g") + assertInvoke(m6, "U", "g") assertInvoke(m6, "U", "h") assertInvoke(m6, "U", "i") } @@ -892,11 +825,11 @@ class InlinerTest extends ClearAfterClass { val warn = """failed to determine if <init> should be inlined: |The method <init>()V could not be found in the class A$Inner or any of its parents. - |Note that the following parent classes could not be found on the classpath: A$Inner""".stripMargin + |Note that the parent class A$Inner could not be found on the classpath.""".stripMargin var c = 0 - compileClasses(newCompiler(extraArgs = InlinerTest.args + " -Yopt-warnings:_"))( + compileClasses(newCompiler(extraArgs = args + " -Yopt-warnings:_"))( scalaCode, List((javaCode, "A.java")), allowMessage = i => {c += 1; i.msg contains warn}) @@ -958,7 +891,7 @@ class InlinerTest extends ClearAfterClass { | def t = System.arraycopy(null, 0, null, 0, 0) |} """.stripMargin - val List(c) = compileClasses(newCompiler(extraArgs = InlinerTest.args + " -Yopt-inline-heuristics:everything"))(code) + val List(c) = compileClasses(newCompiler(extraArgs = args + " -Yopt-inline-heuristics:everything"))(code) assertInvoke(getSingleMethod(c, "t"), "java/lang/System", "arraycopy") } @@ -988,7 +921,588 @@ class InlinerTest extends ClearAfterClass { val List(c) = compile(code) val t = getSingleMethod(c, "t").instructions assertNoInvoke(t) - assert(2 == t.collect({case Ldc(_, "hai!") => }).size) // twice the body of f + assert(1 == t.collect({case Ldc(_, "hai!") => }).size) // push-pop eliminates the first LDC("hai!") assert(1 == t.collect({case Jump(IFNONNULL, _) => }).size) // one single null check } + + @Test + def inlineIndyLambda(): Unit = { + val code = + """object M { + | @inline def m(s: String) = { + | val f = (x: String) => x.trim + | f(s) + | } + |} + |class C { + | @inline final def m(s: String) = { + | val f = (x: String) => x.trim + | f(s) + | } + | def t1 = m("foo") + | def t2 = M.m("bar") + |} + """.stripMargin + + val List(c, _, _) = compile(code) + + val t1 = getSingleMethod(c, "t1") + assertNoIndy(t1) + // the indy call is inlined into t, and the closure elimination rewrites the closure invocation to the body method + assertInvoke(t1, "C", "C$$$anonfun$2") + + val t2 = getSingleMethod(c, "t2") + assertNoIndy(t2) + assertInvoke(t2, "M$", "M$$$anonfun$1") + } + + @Test + def inlinePostRequests(): Unit = { + val code = + """class C { + | final def f = 10 + | final def g = f + 19 + | final def h = g + 29 + | final def i = h + 39 + |} + """.stripMargin + + val List(c) = compile(code) + val hMeth = findAsmMethod(c, "h") + val gMeth = findAsmMethod(c, "g") + val iMeth = findAsmMethod(c, "i") + val fCall = getCallsite(gMeth, "f") + val gCall = getCallsite(hMeth, "g") + val hCall = getCallsite(iMeth, "h") + + val warning = inliner.canInlineBody(gCall) + assert(warning.isEmpty, warning) + + inliner.inline(InlineRequest(hCall, + post = List(InlineRequest(gCall, + post = List(InlineRequest(fCall, Nil)))))) + assertNoInvoke(convertMethod(iMeth)) // no invoke in i: first h is inlined, then the inlined call to g is also inlined, etc for f + assertInvoke(convertMethod(gMeth), "C", "f") // g itself still has the call to f + } + + @Test + def postRequestSkipAlreadyInlined(): Unit = { + val code = + """class C { + | final def a = 10 + | final def b = a + 20 + | final def c = b + 30 + | final def d = c + 40 + |} + """.stripMargin + + val List(cl) = compile(code) + val List(b, c, d) = List("b", "c", "d").map(findAsmMethod(cl, _)) + val aCall = getCallsite(b, "a") + val bCall = getCallsite(c, "b") + val cCall = getCallsite(d, "c") + + inliner.inline(InlineRequest(bCall, Nil)) + + val req = InlineRequest(cCall, + List(InlineRequest(bCall, + List(InlineRequest(aCall, Nil))))) + inliner.inline(req) + + assertNoInvoke(convertMethod(d)) + } + + @Test + def inlineAnnotatedCallsite(): Unit = { + val code = + """class C { + | final def a(x: Int, f: Int => Int): Int = f(x) + | final def b(x: Int) = x + | final def c = 1 + | final def d[T] = 2 + | final def e[T](x: T) = c + | final def f[T](x: T) = println(x) + | final def g(x: Int)(y: Int) = x + | + | def t1 = a(10, _ + 1) + | def t2 = a(10, _ + 1): @noinline + | def t3 = b(3) + | def t4 = b(3): @inline + | def t5 = c: @inline + | def t6 = d[Int]: @inline + | def t7 = e[Int](2): @inline + | def t8 = f[Int](2): @inline + | def t9 = g(1)(2): @inline + |} + """.stripMargin + + val List(c) = compile(code) + assertInvoke(getSingleMethod(c, "t1"), "C", "C$$$anonfun$1") + assertInvoke(getSingleMethod(c, "t2"), "C", "a") + assertInvoke(getSingleMethod(c, "t3"), "C", "b") + assertNoInvoke(getSingleMethod(c, "t4")) + assertNoInvoke(getSingleMethod(c, "t5")) + assertNoInvoke(getSingleMethod(c, "t6")) + assertInvoke(getSingleMethod(c, "t7"), "C", "c") + assertInvoke(getSingleMethod(c, "t8"), "scala/Predef$", "println") + assertNoInvoke(getSingleMethod(c, "t9")) + } + + @Test + def inlineNoInlineOverride(): Unit = { + val code = + """class C { + | @inline final def f1(x: Int) = x + | @noinline final def f2(x: Int) = x + | final def f3(x: Int) = x + | + | def t1 = f1(1) // inlined + | def t2 = f2(1) // not inlined + | def t3 = f1(1): @noinline // not inlined + | def t4 = f2(1): @inline // not inlined (cannot override the def-site @noinline) + | def t5 = f3(1): @inline // inlined + | def t6 = f3(1): @noinline // not inlined + | + | def t7 = f1(1) + (f3(1): @inline) // without parenthesis, the ascription encloses the entire expression.. + | def t8 = f1(1) + (f1(1): @noinline) + | def t9 = f1(1) + f1(1) : @noinline // the ascription goes on the entire expression, so on the + invocation.. both f1 are inlined + |} + """.stripMargin + + val List(c) = compile(code) + assertNoInvoke(getSingleMethod(c, "t1")) + assertInvoke(getSingleMethod(c, "t2"), "C", "f2") + assertInvoke(getSingleMethod(c, "t3"), "C", "f1") + assertInvoke(getSingleMethod(c, "t4"), "C", "f2") + assertNoInvoke(getSingleMethod(c, "t5")) + assertInvoke(getSingleMethod(c, "t6"), "C", "f3") + assertNoInvoke(getSingleMethod(c, "t7")) + assertInvoke(getSingleMethod(c, "t8"), "C", "f1") + assertNoInvoke(getSingleMethod(c, "t9")) + } + + @Test + def inlineHigherOrder(): Unit = { + val code = + """class C { + | final def h(f: Int => Int): Int = f(0) + | def t1 = h(x => x + 1) + | def t2 = { + | val fun = (x: Int) => x + 1 + | h(fun) + | } + | def t3(f: Int => Int) = h(f) + | def t4(f: Int => Int) = { + | val fun = f + | h(fun) + | } + | def t5 = h(Map(0 -> 10)) // not currently inlined + |} + """.stripMargin + + val List(c) = compile(code) + assertInvoke(getSingleMethod(c, "t1"), "C", "C$$$anonfun$1") + assertInvoke(getSingleMethod(c, "t2"), "C", "C$$$anonfun$2") + assertInvoke(getSingleMethod(c, "t3"), "scala/Function1", "apply$mcII$sp") + assertInvoke(getSingleMethod(c, "t4"), "scala/Function1", "apply$mcII$sp") + assertInvoke(getSingleMethod(c, "t5"), "C", "h") + } + + @Test + def twoStepNoInlineHandler(): Unit = { + val code = + """class C { + | @inline final def f = try 1 catch { case _: Throwable => 2 } + | @inline final def g = f + | def t = println(g) // cannot inline g onto non-empty stack once that f was inlined into g + |} + """.stripMargin + + val warn = + """C::g()I is annotated @inline but could not be inlined: + |The operand stack at the callsite in C::t()V contains more values than the + |arguments expected by the callee C::g()I. These values would be discarded + |when entering an exception handler declared in the inlined method.""".stripMargin + + val List(c) = compile(code, allowMessage = _.msg contains warn) + assertInvoke(getSingleMethod(c, "t"), "C", "g") + } + + @Test + def twoStepNoInlinePrivate(): Unit = { + val code = + """class C { + | @inline final def g = { + | @noinline def f = 0 + | f + | } + | @inline final def h = g // after inlining g, h has an invocate of private method f$1 + |} + |class D { + | def t(c: C) = c.h // cannot inline + |} + """.stripMargin + + val warn = + """C::h()I is annotated @inline but could not be inlined: + |The callee C::h()I contains the instruction INVOKESPECIAL C.f$1 ()I + |that would cause an IllegalAccessError when inlined into class D.""".stripMargin + + val List(c, d) = compile(code, allowMessage = _.msg contains warn) + assertInvoke(getSingleMethod(c, "h"), "C", "f$1") + assertInvoke(getSingleMethod(d, "t"), "C", "h") + } + + @Test + def twoStepInlinePrivate(): Unit = { + val code = + """class C { + | @inline final def g = { // initially, g invokes the private method f$1, but then f$1 is inlined + | @inline def f = 0 + | f + | } + |} + |class D { + | def t(c: C) = c.g // can inline + |} + """.stripMargin + + val List(c, d) = compile(code) + assertNoInvoke(getSingleMethod(c, "g")) + assertNoInvoke(getSingleMethod(d, "t")) + } + + @Test + def optimizeSpecializedClosures(): Unit = { + val code = + """class ValKl(val x: Int) extends AnyVal + | + |class C { + | def t1 = { + | // IndyLambda: SAM type is JFunction1$mcII$sp, SAM is apply$mcII$sp(I)I, body method is $anonfun(I)I + | val f = (x: Int) => x + 1 + | // invocation of apply$mcII$sp(I)I, matches the SAM in IndyLambda. no boxing / unboxing needed. + | f(10) + | // opt: re-write the invocation to the body method + | } + | + | @inline final def m1a(f: Long => Int) = f(1l) + | def t1a = m1a(l => l.toInt) // after inlining m1a, we have the same situation as in t1 + | + | def t2 = { + | // there is no specialized variant of Function2 for this combination of types, so the IndyLambda has to create a generic Function2. + | // IndyLambda: SAM type is JFunction2, SAM is apply(ObjectObject)Object, body method is $anonfun$adapted(ObjectObject)Object + | val f = (b: Byte, i: Int) => i + b + | // invocation of apply(ObjectOjbect)Object, matches SAM in IndyLambda. arguments are boxed, result unboxed. + | f(1, 2) + | // opt: re-wrtie to $anonfun$adapted + | // inline that call, then we get box-unbox pairs (can be eliminated) and a call to $anonfun(BI)I + | } + | + | def t3 = { + | // similar to t2: for functions with value class parameters, IndyLambda always uses the generic Function version. + | // IndyLambda: SAM type is JFunction1, SAM is apply(Object)Object, body method is $anonfun$adapted(Object)Object + | val f = (a: ValKl) => a + | // invocation of apply(Object)Object, ValKl instance is created, result extracted + | f(new ValKl(1)) + | // opt: re-write to $anonfun$adapted. + | // inline that call, then we get value class instantiation-extraction pairs and a call to $anonfun(I)I + | } + | + | def t4 = { + | // IndyLambda: SAM type is JFunction1$mcII$sp, SAM is apply$mcII$sp(I)I, body method is $anonfun(I)I + | val f: Int => Any = (x: Int) => 1 + | // invocation of apply(Object)Object, argument is boxed. method name and type doesn't match IndyLambda. + | f(10) + | // opt: rewriting to the body method requires inserting an unbox operation for the argument, and a box operation for the result + | // that produces a box-unbox pair and a call to $anonfun(I)I + | } + | + | + | @inline final def m4a[T, U, V](f: (T, U) => V, x: T, y: U) = f(x, y) // invocation to generic apply(ObjectObject)Object + | def t4a = m4a((x: Int, y: Double) => 1l + x + y.toLong, 1, 2d) // IndyLambda uses specilized JFunction2$mcJID$sp. after inlining m4a, similar to t4. + | + | def t5 = { + | // no specialization for the comibnation of primitives + | // IndyLambda: SAM type is JFunction2, SAM is generic apply, body method is $anonfun$adapted + | val f: (Int, Byte) => Any = (x: Int, b: Byte) => 1 + | // invocation of generic apply. + | f(10, 3) + | // opt: re-write to $anonfun$adapted, inline that method. generates box-unbox pairs and a call to $anonfun(IB)I + | } + | + | def t5a = m4a((x: Int, y: Byte) => 1, 12, 31.toByte) // similar to t5 after inlining m4a + | + | // m6$mIVc$sp invokes apply$mcVI$sp + | @inline final def m6[@specialized(Int) T, @specialized(Unit) U](f: T => U, x: T): Unit = f(x) + | // IndyLambda: JFunction1$mcVI$sp, SAM is apply$mcVI$sp, body method $anonfun(I)V + | // invokes m6$mIVc$sp (Lscala/Function1;I)V + | def t6 = m6((x: Int) => (), 10) + | // opt: after inlining m6, the closure method invocation (apply$mcVI$sp) matches the IndyLambda, the call can be rewritten, no boxing + | + | // m7 invokes apply + | @inline final def m7[@specialized(Boolean) T, @specialized(Int) U](f: T => U, x: T): Unit = f(x) + | // IndyLambda: JFunction1, SAM is apply(Object)Object, body method is $anonfun$adapted(Obj)Obj + | // `true` is boxed before passing to m7 + | def t7 = m7((x: Boolean) => (), true) + | // opt: after inlining m7, the apply call is re-written to $anonfun$adapted, which is then inlined. + | // we get a box-unbox pair and a call to $anonfun(Z)V + | + | + | // invokes the generic apply(ObjObj)Obj + | @inline final def m8[T, U, V](f: (T, U) => V, x: T, y: U) = f(x, y) + | // IndyLambda: JFunction2$mcJID$sp, SAM is apply$mcJID$sp, body method $anonfun(ID)J + | // boxes the int and double arguments and calls m8, unboxToLong the result + | def t8 = m8((x: Int, y: Double) => 1l + x + y.toLong, 1, 2d) + | // opt: after inlining m8, rewrite to the body method $anonfun(ID)J, which requires inserting unbox operations for the params, box for the result + | // the box-unbox pairs can then be optimized away + | + | // m9$mVc$sp invokes apply$mcVI$sp + | @inline final def m9[@specialized(Unit) U](f: Int => U): Unit = f(1) + | // IndyLambda: JFunction1, SAM is apply(Obj)Obj, body method $anonfun$adapted(Ojb)Obj + | // invocation of m9$mVc$sp + | def t9 = m9(println) + | // opt: after inlining m9, rewrite to $anonfun$adapted(Ojb)Obj, which requires inserting a box operation for the parameter. + | // then we inline $adapted, which has signature (Obj)V. the `BoxedUnit.UNIT` from the body of $anonfun$adapted is eliminated by push-pop + | + | def t9a = (1 to 10) foreach println // similar to t9 + | + | def intCons(i: Int): Unit = () + | // IndyLambda: JFunction1$mcVI$sp, SAM is apply$mcVI$sp, body method $anonfun(I)V + | def t10 = m9(intCons) + | // after inlining m9, rewrite the apply$mcVI$sp call to the body method, no adaptations required + | + | def t10a = (1 to 10) foreach intCons // similar to t10 + |} + """.stripMargin + val List(c, _, _) = compile(code) + + assertSameSummary(getSingleMethod(c, "t1"), List(BIPUSH, "C$$$anonfun$1", IRETURN)) + assertSameSummary(getSingleMethod(c, "t1a"), List(LCONST_1, "C$$$anonfun$2", IRETURN)) + assertSameSummary(getSingleMethod(c, "t2"), List(ICONST_1, ICONST_2, "C$$$anonfun$3",IRETURN)) + + // val a = new ValKl(n); new ValKl(anonfun(a.x)).x + // value class instantiation-extraction should be optimized by boxing elim + assertSameSummary(getSingleMethod(c, "t3"), List( + NEW, DUP, ICONST_1, "<init>", ASTORE, + NEW, DUP, ALOAD, "x", + "C$$$anonfun$4", + "<init>", + "x", IRETURN)) + + assertSameSummary(getSingleMethod(c, "t4"), List(BIPUSH, "C$$$anonfun$5", "boxToInteger", ARETURN)) + assertSameSummary(getSingleMethod(c, "t4a"), List(ICONST_1, LDC, "C$$$anonfun$6", LRETURN)) + assertSameSummary(getSingleMethod(c, "t5"), List(BIPUSH, ICONST_3, "C$$$anonfun$7", "boxToInteger", ARETURN)) + assertSameSummary(getSingleMethod(c, "t5a"), List(BIPUSH, BIPUSH, I2B, "C$$$anonfun$8", IRETURN)) + assertSameSummary(getSingleMethod(c, "t6"), List(BIPUSH, "C$$$anonfun$9", RETURN)) + assertSameSummary(getSingleMethod(c, "t7"), List(ICONST_1, "C$$$anonfun$10", RETURN)) + assertSameSummary(getSingleMethod(c, "t8"), List(ICONST_1, LDC, "C$$$anonfun$11", LRETURN)) + assertSameSummary(getSingleMethod(c, "t9"), List(ICONST_1, "boxToInteger", "C$$$anonfun$12", RETURN)) + + // t9a inlines Range.foreach, which is quite a bit of code, so just testing the core + assertInvoke(getSingleMethod(c, "t9a"), "C", "C$$$anonfun$13") + assertInvoke(getSingleMethod(c, "t9a"), "scala/runtime/BoxesRunTime", "boxToInteger") + + assertSameSummary(getSingleMethod(c, "t10"), List( + ICONST_1, ISTORE, + ALOAD, ILOAD, + "C$$$anonfun$14", RETURN)) + + // t10a inlines Range.foreach + assertInvoke(getSingleMethod(c, "t10a"), "C", "C$$$anonfun$15") + assertDoesNotInvoke(getSingleMethod(c, "t10a"), "boxToInteger") + } + + @Test + def refElimination(): Unit = { + val code = + """class C { + | def t1 = { + | var i = 0 + | @inline def inner() = i += 1 + | inner() + | i + | } + | + | final def m(f: Int => Unit) = f(10) + | def t2 = { + | var x = -1 // IntRef not yet eliminated: closure elimination does not + | m(i => if (i == 10) x = 1) // yet inline the anonfun method, need to improve the heuristsics + | x + | } + |} + """.stripMargin + val List(c) = compile(code) + assertSameCode(getSingleMethod(c, "t1"), List(Op(ICONST_0), Op(ICONST_1), Op(IADD), Op(IRETURN))) + assertEquals(getSingleMethod(c, "t2").instructions collect { case i: Invoke => i.owner +"."+ i.name }, List( + "scala/runtime/IntRef.create", "C.C$$$anonfun$1")) + } + + @Test + def tupleElimination(): Unit = { + val code = + """class C { + | @inline final def tpl[A, B](a: A, b: B) = (a, b) + | @inline final def t_1[A, B](t: (A, B)) = t._1 + | @inline final def t_2[A, B](t: (A, B)) = t._2 + | + | def t1 = { + | val t = (3, 4) // specialized tuple + | t_1(t) + t_2(t) // invocations to generic _1 / _2, box operation inserted when eliminated + | } + | + | def t2 = { + | val t = tpl(1, 2) // generic Tuple2[Integer, Integer] created + | t._1 + t._2 // invokes the specialized _1$mcI$sp, eliminating requires adding an unbox operation + | } + | + | @inline final def m = (1, 3) + | def t3 = { + | val (a, b) = m + | a - b + | } + | + | def t4 = { + | val ((a, b), (c, d)) = (m, m) + | a + b + c + d + | } + | + | def t5 = m match { + | case (1, y) => y + | case (x, y) => x * y + | } + |} + """.stripMargin + val List(c) = compile(code) + assertSameCode(getSingleMethod(c, "t1"), List(Op(ICONST_3), Op(ICONST_4), Op(IADD), Op(IRETURN))) + assertSameCode(getSingleMethod(c, "t2"), List(Op(ICONST_1), Op(ICONST_2), Op(IADD), Op(IRETURN))) + assertSameCode(getSingleMethod(c, "t3"), List(Op(ICONST_1), Op(ICONST_3), Op(ISUB), Op(IRETURN))) + assertNoInvoke(getSingleMethod(c, "t4")) + assertNoInvoke(getSingleMethod(c, "t5")) + } + + @Test + def redundantCasts(): Unit = { + + // we go through the hoop of inlining the casts because erasure eliminates `asInstanceOf` calls + // that are statically known to succeed. For example the following cast is removed by erasure: + // `(if (b) c else d).asInstanceOf[C]` + + val code = + """class C { + | @inline final def asO(a: Any) = a.asInstanceOf[Object] + | @inline final def asC(a: Any) = a.asInstanceOf[C] + | @inline final def asD(a: Any) = a.asInstanceOf[D] + | + | def t1(c: C) = asC(c) // eliminated + | def t2(c: C) = asO(c) // eliminated + | def t3(c: Object) = asC(c) // not elimianted + | def t4(c: C, d: D, b: Boolean) = asC(if (b) c else d) // not eliminated: lub of two non-equal reference types approximated with Object + | def t5(c: C, d: D, b: Boolean) = asO(if (b) c else d) + | def t6(c: C, cs: Array[C], b: Boolean) = asO(if (b) c else cs) + |} + |class D extends C + """.stripMargin + val List(c, _) = compile(code) + def casts(m: String) = getSingleMethod(c, m).instructions collect { case TypeOp(CHECKCAST, tp) => tp } + assertSameCode(getSingleMethod(c, "t1"), List(VarOp(ALOAD, 1), Op(ARETURN))) + assertSameCode(getSingleMethod(c, "t2"), List(VarOp(ALOAD, 1), Op(ARETURN))) + assertSameCode(getSingleMethod(c, "t3"), List(VarOp(ALOAD, 1), TypeOp(CHECKCAST, "C"), Op(ARETURN))) + assertEquals(casts("t4"), List("C")) + assertEquals(casts("t5"), Nil) + assertEquals(casts("t6"), Nil) + } + + @Test + def inlineFromSealed(): Unit = { + val code = + """sealed abstract class Foo { + | @inline def bar(x: Int) = x + 1 + |} + |object Foo { + | def mkFoo(): Foo = new Baz2 + |} + | + |object Baz1 extends Foo + |final class Baz2 extends Foo + | + |object Test { + | def f = Foo.mkFoo() bar 10 + |} + """.stripMargin + + val cls = compile(code) + val test = cls.find(_.name == "Test$").get + assertSameSummary(getSingleMethod(test, "f"), List( + GETSTATIC, "mkFoo", + BIPUSH, ISTORE, + IFNONNULL, ACONST_NULL, ATHROW, -1 /*label*/, + ILOAD, ICONST_1, IADD, IRETURN)) + } + + @Test // a test taken from the test suite for the 2.11 inliner + def oldInlineHigherOrderTest(): Unit = { + val code = + """class C { + | private var debug = false + | @inline private def ifelse[T](cond: => Boolean, ifPart: => T, elsePart: => T): T = if (cond) ifPart else elsePart + | final def t = ifelse(debug, 1, 2) + |} + """.stripMargin + val List(c) = compile(code) + + // box-unbox will clean it up + assertSameSummary(getSingleMethod(c, "t"), List( + ALOAD, "C$$$anonfun$1", IFEQ /*A*/, + "C$$$anonfun$2", IRETURN, + -1 /*A*/, "C$$$anonfun$3", IRETURN)) + } + + @Test + def inlineProject(): Unit = { + val codeA = "final class A { @inline def f = 1 }" + val codeB = "class B { def t(a: A) = a.f }" + // tests that no warning is emitted + val List(a, b) = compileClassesSeparately(List(codeA, codeB), extraArgs = "-Yopt:l:project -Yopt-warnings") + assertInvoke(getSingleMethod(b, "t"), "A", "f") + } + + @Test + def sd86(): Unit = { + val code = + """trait T1 { @inline def f = 999 } + |trait T2 { self: T1 => @inline override def f = 1 } // note that f is not final + |class C extends T1 with T2 + """.stripMargin + val List(c, t1, t2) = compile(code, allowMessage = _ => true) + // the forwarder C.f is inlined, so there's no invocation + assertSameSummary(getSingleMethod(c, "f"), List(ICONST_1, IRETURN)) + } + + @Test + def sd140(): Unit = { + val code = + """trait T { @inline def f = 0 } + |trait U extends T { @inline override def f = 1 } + |trait V extends T { def m = 0 } + |final class K extends V with U { override def m = super[V].m } + |class C { def t = (new K).f } + """.stripMargin + val c :: _ = compile(code) + assertSameSummary(getSingleMethod(c, "t"), List(NEW, "<init>", ICONST_1, IRETURN)) // ICONST_1, U.f is inlined (not T.f) + } + + @Test + def inlineArrayForeach(): Unit = { + val code = + """class C { + | def consume(x: Int) = () + | def t(a: Array[Int]): Unit = a foreach consume + |} + """.stripMargin + val List(c) = compile(code) + val t = getSingleMethod(c, "t") + assertNoIndy(t) + assertInvoke(t, "C", "C$$$anonfun$1") + } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala deleted file mode 100644 index 5ef2458c0a..0000000000 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOpts.scala +++ /dev/null @@ -1,92 +0,0 @@ -package scala.tools.nsc -package backend.jvm -package opt - -import org.junit.runner.RunWith -import org.junit.runners.JUnit4 -import org.junit.Test -import scala.tools.asm.Opcodes._ -import org.junit.Assert._ - -import scala.tools.testing.AssertUtil._ - -import CodeGenTools._ -import scala.tools.partest.ASMConverters -import ASMConverters._ -import scala.tools.testing.ClearAfterClass - -object MethodLevelOpts extends ClearAfterClass.Clearable { - var methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:method") - def clear(): Unit = { methodOptCompiler = null } -} - -@RunWith(classOf[JUnit4]) -class MethodLevelOpts extends ClearAfterClass { - ClearAfterClass.stateToClear = MethodLevelOpts - - val methodOptCompiler = MethodLevelOpts.methodOptCompiler - - def wrapInDefault(code: Instruction*) = List(Label(0), LineNumber(1, Label(0))) ::: code.toList ::: List(Label(1)) - - @Test - def eliminateEmptyTry(): Unit = { - val code = "def f = { try {} catch { case _: Throwable => 0; () }; 1 }" - val warn = "a pure expression does nothing in statement position" - assertSameCode(singleMethodInstructions(methodOptCompiler)(code, allowMessage = _.msg contains warn), wrapInDefault(Op(ICONST_1), Op(IRETURN))) - } - - @Test - def cannotEliminateLoadBoxedUnit(): Unit = { - // the compiler inserts a boxed into the try block. it's therefore non-empty (and live) and not eliminated. - val code = "def f = { try {} catch { case _: Throwable => 0 }; 1 }" - val m = singleMethod(methodOptCompiler)(code) - assertTrue(m.handlers.length == 1) - assertSameCode(m.instructions.take(3), List(Label(0), LineNumber(1, Label(0)), Field(GETSTATIC, "scala/runtime/BoxedUnit", "UNIT", "Lscala/runtime/BoxedUnit;"))) - } - - @Test - def inlineThrowInCatchNotTry(): Unit = { - // the try block does not contain the `ATHROW` instruction, but in the catch block, `ATHROW` is inlined - val code = "def f(e: Exception) = throw { try e catch { case _: Throwable => e } }" - val m = singleMethod(methodOptCompiler)(code) - assertHandlerLabelPostions(m.handlers.head, m.instructions, 0, 3, 5) - assertSameCode(m.instructions, - wrapInDefault(VarOp(ALOAD, 1), Label(3), Op(ATHROW), Label(5), FrameEntry(4, List(), List("java/lang/Throwable")), Op(POP), VarOp(ALOAD, 1), Op(ATHROW)) - ) - } - - @Test - def inlineReturnInCatchNotTry(): Unit = { - val code = "def f: Int = return { try 1 catch { case _: Throwable => 2 } }" - // cannot inline the IRETURN into the try block (because RETURN may throw IllegalMonitorState) - val m = singleMethod(methodOptCompiler)(code) - assertHandlerLabelPostions(m.handlers.head, m.instructions, 0, 3, 5) - assertSameCode(m.instructions, - wrapInDefault(Op(ICONST_1), Label(3), Op(IRETURN), Label(5), FrameEntry(4, List(), List("java/lang/Throwable")), Op(POP), Op(ICONST_2), Op(IRETURN))) - } - - @Test - def simplifyJumpsInTryCatchFinally(): Unit = { - val code = - """def f: Int = - | try { - | return 1 - | } catch { - | case _: Throwable => - | return 2 - | } finally { - | return 2 - | // dead - | val x = try 10 catch { case _: Throwable => 11 } - | println(x) - | } - """.stripMargin - val m = singleMethod(methodOptCompiler)(code) - assertTrue(m.handlers.length == 2) - assertSameCode(m.instructions.dropNonOp, // drop line numbers and labels that are only used by line numbers - - // one single label left :-) - List(Op(ICONST_1), VarOp(ISTORE, 2), Jump(GOTO, Label(20)), Op(POP), Op(ICONST_2), VarOp(ISTORE, 2), Jump(GOTO, Label(20)), VarOp(ASTORE, 3), Op(ICONST_2), Op(IRETURN), Label(20), Op(ICONST_2), Op(IRETURN)) - ) - } -} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala new file mode 100644 index 0000000000..003b2d4880 --- /dev/null +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/MethodLevelOptsTest.scala @@ -0,0 +1,755 @@ +package scala.tools.nsc +package backend.jvm +package opt + +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.junit.Test +import scala.tools.asm.Opcodes._ +import org.junit.Assert._ + +import scala.tools.asm.tree.ClassNode +import scala.tools.nsc.backend.jvm.AsmUtils._ +import scala.tools.testing.AssertUtil._ + +import CodeGenTools._ +import scala.tools.partest.ASMConverters +import ASMConverters._ +import scala.tools.testing.ClearAfterClass +import scala.collection.JavaConverters._ + +@RunWith(classOf[JUnit4]) +class MethodLevelOptsTest extends ClearAfterClass { + val methodOptCompiler = cached("methodOptCompiler", () => newCompiler(extraArgs = "-Yopt:l:method")) + + def wrapInDefault(code: Instruction*) = List(Label(0), LineNumber(1, Label(0))) ::: code.toList ::: List(Label(1)) + + def locals(c: ClassNode, m: String) = findAsmMethod(c, m).localVariables.asScala.toList.map(l => (l.name, l.index)).sortBy(_._2) + + @Test + def eliminateEmptyTry(): Unit = { + val code = "def f = { try {} catch { case _: Throwable => 0; () }; 1 }" + val warn = "a pure expression does nothing in statement position" + assertSameCode(singleMethodInstructions(methodOptCompiler)(code, allowMessage = _.msg contains warn), wrapInDefault(Op(ICONST_1), Op(IRETURN))) + } + + @Test + def eliminateLoadBoxedUnit(): Unit = { + // the compiler inserts a boxed into the try block. it's therefore non-empty (and live) and not eliminated. + val code = "def f = { try {} catch { case _: Throwable => 0 }; 1 }" + val m = singleMethod(methodOptCompiler)(code) + assertTrue(m.handlers.length == 0) + assertSameCode(m, List(Op(ICONST_1), Op(IRETURN))) + } + + @Test + def inlineThrowInCatchNotTry(): Unit = { + // the try block does not contain the `ATHROW` instruction, but in the catch block, `ATHROW` is inlined + val code = "def f(e: Exception) = throw { try e catch { case _: Throwable => e } }" + val m = singleMethod(methodOptCompiler)(code) + assertHandlerLabelPostions(m.handlers.head, m.instructions, 0, 3, 5) + assertSameCode(m.instructions, + wrapInDefault(VarOp(ALOAD, 1), Label(3), Op(ATHROW), Label(5), FrameEntry(4, List(), List("java/lang/Throwable")), Op(POP), VarOp(ALOAD, 1), Op(ATHROW)) + ) + } + + @Test + def inlineReturnInCatchNotTry(): Unit = { + val code = "def f: Int = return { try 1 catch { case _: Throwable => 2 } }" + // cannot inline the IRETURN into the try block (because RETURN may throw IllegalMonitorState) + val m = singleMethod(methodOptCompiler)(code) + assertHandlerLabelPostions(m.handlers.head, m.instructions, 0, 3, 5) + assertSameCode(m.instructions, + wrapInDefault(Op(ICONST_1), Label(3), Op(IRETURN), Label(5), FrameEntry(4, List(), List("java/lang/Throwable")), Op(POP), Op(ICONST_2), Op(IRETURN))) + } + + @Test + def simplifyJumpsInTryCatchFinally(): Unit = { + val code = + """def f: Int = + | try { + | return 1 + | } catch { + | case _: Throwable => + | return 2 + | } finally { + | return 3 + | // dead + | val x = try 10 catch { case _: Throwable => 11 } + | println(x) + | } + """.stripMargin + val m = singleMethod(methodOptCompiler)(code) + assertTrue(m.handlers.isEmpty) + assertSameCode(m, List(Op(ICONST_3), Op(IRETURN))) + } + + @Test + def nullStoreLoadElim(): Unit = { + // point of this test: we have two cleanups + // - remove `ACONST_NULL; ASTORE x` if x is otherwise not live + // - remove `ASTORE x; ALOAD x` if x is otherwise not live + // in the example below, we have `ACONST_NULL; ASTORE x; ALOAD x`. in this case the store-load + // should be removed (even though it looks like a null-store at first). + val code = + """class C { + | def t = { + | val x = null + | x.toString + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertSameCode(getSingleMethod(c, "t"), List( + Op(ACONST_NULL), Invoke(INVOKEVIRTUAL, "java/lang/Object", "toString", "()Ljava/lang/String;", false), Op(ARETURN))) + } + + @Test + def deadStoreReferenceElim(): Unit = { + val code = + """class C { + | def t = { + | var a = "a" // assign to non-initialized, directly removed by dead store + | a = "b" // assign to initialized, replaced by null-store, which is then removed: the var is not live, the uses are null-store or store-load + | a = "c" + | a // store-load pair will be eliminated + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertSameCode( + getSingleMethod(c, "t"), List(Ldc(LDC, "c"), Op(ARETURN))) + } + + @Test + def deadStoreReferenceKeepNull(): Unit = { + val code = + """class C { + | def t = { + | var a = "el" // this store is live, used in the println. + | println(a) + | a = "met" // since it's an ASTORE to a live variable, cannot elim the store (SI-5313), but store null instead. + | // so we get `LDC met; POP; ACONST_NULL; ASTORE 1`. the `LDC met; POP` is eliminated by push-pop. + | a = "zit" // this store is live, so we get `LDC zit; ASOTRE 1; ALOAD 1; ARETURN`. + | // we cannot eliminated the store-load sequence, because the local is live (again SI-5313). + | a + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + + assertSameCode(getSingleMethod(c, "t"), List( + Ldc(LDC, "el"), VarOp(ASTORE, 1), + Field(GETSTATIC, "scala/Predef$", "MODULE$", "Lscala/Predef$;"), VarOp(ALOAD, 1), Invoke(INVOKEVIRTUAL, "scala/Predef$", "println", "(Ljava/lang/Object;)V", false), + Op(ACONST_NULL), VarOp(ASTORE, 1), + Ldc(LDC, "zit"), VarOp(ASTORE, 1), VarOp(ALOAD, 1), Op(ARETURN))) + } + + @Test + def elimUnusedTupleObjectStringBox(): Unit = { + val code = + """class C { + | def t(x: Int, y: Int): Int = { + | val a = (x, y) // Tuple2$mcII$sp + | val b = (a, y) // Tuple2 + | val c = (new Object, "krik", new String) // unused java/lang/Object, java/lang/String allocation and string constant is also eliminated + | val d = new java.lang.Integer(x) + | val e = new String(new Array[Char](23)) // array allocation not eliminated, as it may throw (negative size, SI-8601) + | val f = new scala.runtime.IntRef(11) + | x + y + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertSameCode(getSingleMethod(c, "t"), List( + IntOp(BIPUSH, 23), IntOp(NEWARRAY, 5), Op(POP), VarOp(ILOAD, 1), VarOp(ILOAD, 2), Op(IADD), Op(IRETURN))) + } + + @Test + def noElimImpureConstructor(): Unit = { + val code = + """class C { + | def t(x: Int, y: Int): Int = { + | val a = new java.lang.Integer("nono") + | x + y + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertSameCode(getSingleMethod(c, "t"), List( + TypeOp(NEW, "java/lang/Integer"), Ldc(LDC, "nono"), Invoke(INVOKESPECIAL, "java/lang/Integer", "<init>", "(Ljava/lang/String;)V", false), + VarOp(ILOAD, 1), VarOp(ILOAD, 2), Op(IADD), Op(IRETURN))) + } + + @Test + def elimUnusedBoxUnbox(): Unit = { + val code = + """class C { + | def t(a: Long): Int = { + | val t = 3 + a + | val u = a + t + | val v: Any = u // scala/runtime/BoxesRunTime.boxToLong + | + | val w = (v, a) // a Tuple2 (not specialized because first value is Any) + | // so calls scala/runtime/BoxesRunTime.boxToLong on the second value + | + | val x = v.asInstanceOf[Long] // scala/runtime/BoxesRunTime.unboxToLong + | + | val z = (java.lang.Long.valueOf(a), t) // java box call on the left, scala/runtime/BoxesRunTime.boxToLong on the right + | + | 0 + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertSameCode(getSingleMethod(c, "t"), List(Op(ICONST_0), Op(IRETURN))) + } + + @Test + def elimUnusedClosure(): Unit = { + val code = + """class C { + | def t(x: Int, y: Int): Int = { + | val f = (a: Int) => a + x + y + | val g = (b: Int) => b - x + | val h = (s: String) => println(s) + | f(30) + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertSameCode(getSingleMethod(c, "t"), List( + IntOp(BIPUSH, 30), VarOp(ISTORE, 3), // no constant propagation, so we keep the store (and load below) of a const + VarOp(ILOAD, 1), + VarOp(ILOAD, 2), + VarOp(ILOAD, 3), + Invoke(INVOKESTATIC, "C", "C$$$anonfun$1", "(III)I", false), Op(IRETURN))) + } + + @Test + def rewriteSpecializedClosureCall(): Unit = { + val code = + """class C { + | def t = { + | val f1 = (x: Int) => println(x) // int-unit specialization + | val f2 = (x: Int, y: Long) => x == y // int-long-boolean + | f1(1) + | f2(3, 4) + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + val t = getSingleMethod(c, "t") + assert(!t.instructions.exists(_.opcode == INVOKEDYNAMIC), t) + } + + @Test + def boxUnboxPrimitive(): Unit = { + val code = + """class C { + | def t1 = { + | val a: Any = runtime.BoxesRunTime.boxToInteger(1) + | runtime.BoxesRunTime.unboxToInt(a) + 1 + | } + | + | // two box and two unbox operations + | def t2(b: Boolean) = { + | val a = if (b) (3l: Any) else 2l + | a.asInstanceOf[Long] + 1 + a.asInstanceOf[Long] + | } + | + | def t3(i: Integer): Int = i.asInstanceOf[Int] + | + | def t4(l: Long): Any = l + | + | def t5(i: Int): Int = { + | val b = Integer.valueOf(i) + | val c: Integer = i + | b.asInstanceOf[Int] + c.intValue + | } + | + | def t6: Long = { + | val y = new java.lang.Boolean(true) + | val i: Integer = if (y) new Integer(10) else 13 + | val j: java.lang.Long = 3l + | j + i + | } + | + | def t7: Int = { + | val a: Any = 3 + | a.asInstanceOf[Int] + a.asInstanceOf[Int] + | } + | + | def t8 = null.asInstanceOf[Int] + | + | def t9: Int = { + | val a = Integer.valueOf(10) + | val b = runtime.BoxesRunTime.unboxToInt(a) + | a + b + | } + | + | @noinline def escape(a: Any) = () + | + | // example E4 in BoxUnbox doc comment + | def t10: Int = { + | val a = Integer.valueOf(10) // int 10 is stored into local + | escape(a) + | a // no unbox, 10 is read from local + | } + | + | // the boxes here cannot be eliminated. see doc comment in BoxUnbox, example E1. + | def t11(b: Boolean): Int = { + | val i = Integer.valueOf(10) + | val j = Integer.valueOf(41) + | escape(i) // force rewrite method M1 (see doc in BoxUnbox) + | val res: Integer = if (b) i else j + | res.toInt // cannot be re-written to a local variable read - we don't know which local to read + | } + | + | // both boxes have a single unboxing consumer, and the escape. note that the escape does + | // NOT put the two boxes into the same set of rewrite operations: we can rewrite both + | // boxes with their unbox individually. in both cases the box also escapes, so method + | // M1 will keep the box around. + | def t12(b: Boolean): Int = { + | val i = Integer.valueOf(10) + | val j = Integer.valueOf(32) + | escape(if (b) i else j) // force method M1. the escape here is a consumer for both boxes + | if (b) i.toInt else j.toInt // both boxes (i, j) have their own unboxing consumer + | } + |} + """.stripMargin + + val List(c) = compileClasses(methodOptCompiler)(code) + + assertNoInvoke(getSingleMethod(c, "t1")) + assertNoInvoke(getSingleMethod(c, "t2")) + assertInvoke(getSingleMethod(c, "t3"), "scala/runtime/BoxesRunTime", "unboxToInt") + assertInvoke(getSingleMethod(c, "t4"), "scala/runtime/BoxesRunTime", "boxToLong") + assertNoInvoke(getSingleMethod(c, "t5")) + assertNoInvoke(getSingleMethod(c, "t6")) + assertNoInvoke(getSingleMethod(c, "t7")) + assertSameSummary(getSingleMethod(c, "t8"), List(ICONST_0, IRETURN)) + assertNoInvoke(getSingleMethod(c, "t9")) + // t10: no invocation of unbox + assertEquals(getSingleMethod(c, "t10").instructions collect { case Invoke(_, owner, name, _, _) => (owner, name) }, List( + ("java/lang/Integer", "valueOf"), + ("C", "escape"))) + + assertSameSummary(getSingleMethod(c, "t11"), List( + BIPUSH, "valueOf", ASTORE /*2*/, + BIPUSH, "valueOf", ASTORE /*3*/, + ALOAD /*0*/, ALOAD /*2*/, "escape", + ILOAD /*1*/, IFEQ /*L1*/, ALOAD /*2*/, GOTO /*L2*/, /*Label L1*/ -1, ALOAD /*3*/, /*Label L2*/ -1, + ASTORE /*4*/, GETSTATIC /*Predef*/, ALOAD /*4*/, "Integer2int", IRETURN)) + + // no unbox invocations + assertEquals(getSingleMethod(c, "t12").instructions collect { case Invoke(_, owner, name, _, _) => (owner, name) }, List( + ("java/lang/Integer", "valueOf"), + ("java/lang/Integer", "valueOf"), + ("C", "escape"))) + } + + @Test + def refEliminiation(): Unit = { + val code = + """class C { + | import runtime._ + | @noinline def escape(a: Any) = () + | + | def t1 = { // box eliminated + | val r = new IntRef(0) + | r.elem + | } + | + | def t2(b: Boolean) = { + | val r1 = IntRef.zero() // both eliminated + | val r2 = IntRef.create(1) + | val res: IntRef = if (b) r1 else r2 + | res.elem + | } + | + | def t3 = { + | val r = LongRef.create(10l) // eliminated + | r.elem += 3 + | r.elem + | } + | + | def t4(b: Boolean) = { + | val x = BooleanRef.create(false) // eliminated + | if (b) x.elem = true + | if (x.elem) "a" else "b" + | } + | + | def t5 = { + | val r = IntRef.create(10) // not eliminated: the box might be modified in the escape + | escape(r) + | r.elem + | } + | + | def t6(b: Boolean) = { + | val r1 = IntRef.zero() + | val r2 = IntRef.create(1) + | r1.elem = 39 + | val res: IntRef = if (b) r1 else r2 + | res.elem // boxes remain: can't rewrite this read, don't know which local + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertSameSummary(getSingleMethod(c, "t1"), List(ICONST_0, IRETURN)) + assertNoInvoke(getSingleMethod(c, "t2")) + assertSameSummary(getSingleMethod(c, "t3"), List(LDC, LDC, LADD, LRETURN)) + assertNoInvoke(getSingleMethod(c, "t4")) + assertEquals(getSingleMethod(c, "t5").instructions collect { case Field(_, owner, name, _) => s"$owner.$name" }, + List("scala/runtime/IntRef.elem")) + assertEquals(getSingleMethod(c, "t6").instructions collect { case Field(op, owner, name, _) => s"$op $owner.$name" }, + List(s"$PUTFIELD scala/runtime/IntRef.elem", s"$GETFIELD scala/runtime/IntRef.elem")) + } + + @Test + def tupleElimination(): Unit = { + val code = + """class C { + | def t1(b: Boolean) = { + | val t = ("hi", "fish") + | if (b) t._1 else t._2 + | } + | + | def t2 = { + | val t = (1, 3) // specialized tuple + | t._1 + t._2 // specialized accessors (_1$mcII$sp) + | } + | + | def t3 = { + | // boxed before tuple creation, a non-specialized tuple is created + | val t = (new Integer(3), Integer.valueOf(4)) + | t._1 + t._2 // invokes the generic `_1` / `_2` getters, both values unboxed by Integer2int + | } + | + | def t4: Any = { + | val t = (3, 3) // specialized tuple is created, ints are not boxed + | (t: Tuple2[Any, Any])._1 // when eliminating the _1 call, need to insert a boxing operation + | } + | + | // the inverse of t4 also happens: an Tuple[Integer] where _1$mcI$sp is invoked. In this + | // case, an unbox operation needs to be added when eliminating the extraction. The only + | // way I found to test this is with an inlined generic method, see InlinerTest.tupleElimination. + | def tpl[A, B](a: A, b: B) = (a, b) + | def t5: Int = tpl(1, 2)._1 // invokes _1$mcI$sp + | + | def t6 = { + | val (a, b) = (1, 2) + | a - b + | } + | + | def t7 = { + | // this example is more tricky to handle than it looks, see doc comment in BoxUnbox. + | val ((a, b), c) = ((1, 2), 3) + | a + b + c + | } + | + | def t8 = { + | val ((a, b), (c, d)) = ((1, 2), (3, Integer.valueOf(10))) + | a + b + c + d + | } + | + | def t9(a: Int, b: Int) = (a, b) match { // tuple is optimized away + | case (x, y) if x == y => 0 + | case (x, y) => x + y + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertNoInvoke(getSingleMethod(c, "t1")) + assertSameSummary(getSingleMethod(c, "t2"), List(ICONST_1, ICONST_3, IADD, IRETURN)) + assertSameSummary(getSingleMethod(c, "t3"), List(ICONST_3, ICONST_4, IADD, IRETURN)) + assertSameSummary(getSingleMethod(c, "t4"), List(ICONST_3, "boxToInteger", ARETURN)) + assertEquals(getSingleMethod(c, "t5").instructions collect { case Invoke(_, owner, name, _, _) => (owner, name) }, List( + ("scala/runtime/BoxesRunTime", "boxToInteger"), + ("scala/runtime/BoxesRunTime", "boxToInteger"), + ("C", "tpl"), + ("scala/Tuple2", "_1$mcI$sp"))) + assertSameSummary(getSingleMethod(c, "t6"), List(ICONST_1, ICONST_2, ISUB, IRETURN)) + assertSameSummary(getSingleMethod(c, "t7"), List( + ICONST_1, ICONST_2, ISTORE, ISTORE, + ICONST_3, ISTORE, + ILOAD, ILOAD, IADD, ILOAD, IADD, IRETURN)) + assertNoInvoke(getSingleMethod(c, "t8")) + assertNoInvoke(getSingleMethod(c, "t9")) + } + + @Test + def nullnessOpts(): Unit = { + val code = + """class C { + | def t1 = { + | val a = new C + | if (a == null) + | println() // eliminated + | a + | } + | + | def t2 = null.asInstanceOf[Long] // replaced by zero value + | + | def t3 = { + | val t = (1, 3) + | val a = null + | if (t ne a) t._1 + | else throw new Error() + | } + | + | def t4 = { + | val i = Integer.valueOf(1) + | val a = null + | if (i eq a) throw new Error() + | else i.toInt + | } + | + | def t5 = { + | val i = runtime.DoubleRef.zero() + | if (i == null) throw new Error() + | else i.elem + | } + | + | def t6 = { + | var a = null + | var i = null + | a = i // eliminated (store of null to variable that is already null) + | a // replaced by ACONST_NULL (load of variable that is known null) + | } + | + | def t7 = { + | val a = null + | a.isInstanceOf[String] // eliminated, replaced by 0 (null.isInstanceOf is always false) + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertSameSummary(getSingleMethod(c, "t1"), List(NEW, DUP, "<init>", ARETURN)) + assertSameCode(getSingleMethod(c, "t2"), List(Op(LCONST_0), Op(LRETURN))) + assertSameCode(getSingleMethod(c, "t3"), List(Op(ICONST_1), Op(IRETURN))) + assertSameCode(getSingleMethod(c, "t4"), List(Op(ICONST_1), Op(IRETURN))) + assertSameCode(getSingleMethod(c, "t5"), List(Op(DCONST_0), Op(DRETURN))) + assertSameCode(getSingleMethod(c, "t6"), List(Op(ACONST_NULL), Op(ARETURN))) + assertSameCode(getSingleMethod(c, "t7"), List(Op(ICONST_0), Op(IRETURN))) + } + + @Test + def elimRedundantNullCheck(): Unit = { + val code = + """class C { + | def t(x: Object) = { + | val bool = x == null + | if (x != null) 1 else 0 + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertSameCode( + getSingleMethod(c, "t"), List( + VarOp(ALOAD, 1), Jump(IFNULL, Label(6)), Op(ICONST_1), Op(IRETURN), Label(6), Op(ICONST_0), Op(IRETURN))) + } + + @Test + def t5313(): Unit = { + val code = + """class C { + | def randomBoolean = scala.util.Random.nextInt % 2 == 0 + | + | // 3 stores to kept1 (slot 1), 1 store to result (slot 2) + | def t1 = { + | var kept1 = new Object + | val result = new java.lang.ref.WeakReference(kept1) + | kept1 = null // we can't eliminate this assignment because result can observe + | // when the object has no more references. See SI-5313 + | kept1 = new Object // could eliminate this one with a more elaborate analysis (we know it contains null) + | // however, such is not implemented: if a var is live, then stores are kept. + | result + | } + | + | // only two variables are live: kept2 and kept3. they end up on slots 1 and 2. + | // kept2 has 2 stores, kept3 has 1 store. + | def t2 = { + | var erased2 = null // we can eliminate this store because it's never used + | val erased3 = erased2 // and this + | var erased4 = erased2 // and this + | val erased5 = erased4 // and this + | var kept2: Object = new Object // ultimately can't be eliminated + | while(randomBoolean) { + | val kept3 = kept2 + | kept2 = null // this can't, because it clobbers kept2, which is used + | erased4 = null // safe to eliminate + | println(kept3) + | } + | 0 + | } + | + | def t3 = { + | var kept4 = new Object // have to keep, it's used + | try + | println(kept4) + | catch { + | case _ : Throwable => kept4 = null // have to keep, it clobbers kept4 which is used + | } + | 0 + | } + | + | def t4 = { + | var kept5 = new Object + | print(kept5) + | kept5 = null // can't eliminate it's a clobber and it's used + | print(kept5) + | kept5 = null // eliminated by nullness analysis (store null to a local that is known to be null) + | 0 + | } + | + | def t5 = { + | while(randomBoolean) { + | var kept6: AnyRef = null // not used, but have to keep because it clobbers the next used store + | // on the back edge of the loop + | kept6 = new Object // used + | println(kept6) + | } + | 0 + | } + |} + """.stripMargin + + val List(c) = compileClasses(methodOptCompiler)(code) + def stores(m: String) = getSingleMethod(c, m).instructions.filter(_.opcode == ASTORE) + + assertEquals(locals(c, "t1"), List(("this",0), ("kept1",1), ("result",2))) + assert(stores("t1") == List(VarOp(ASTORE, 1), VarOp(ASTORE, 2), VarOp(ASTORE, 1), VarOp(ASTORE, 1)), + textify(findAsmMethod(c, "t1"))) + + assertEquals(locals(c, "t2"), List(("this",0), ("kept2",1), ("kept3",2))) + assert(stores("t2") == List(VarOp(ASTORE, 1), VarOp(ASTORE, 2), VarOp(ASTORE, 1)), + textify(findAsmMethod(c, "t2"))) + + assertEquals(locals(c, "t3"), List(("this",0), ("kept4",1))) + assert(stores("t3") == List(VarOp(ASTORE, 1), VarOp(ASTORE, 1)), + textify(findAsmMethod(c, "t3"))) + + assertEquals(locals(c, "t4"), List(("this",0), ("kept5",1))) + assert(stores("t4") == List(VarOp(ASTORE, 1), VarOp(ASTORE, 1)), + textify(findAsmMethod(c, "t4"))) + + assertEquals(locals(c, "t5"), List(("this",0), ("kept6",1))) + assert(stores("t5") == List(VarOp(ASTORE, 1), VarOp(ASTORE, 1)), + textify(findAsmMethod(c, "t5"))) + } + + @Test + def testCpp(): Unit = { + // copied from an old test (run/test-cpp.scala) + val code = + """class C { + | import scala.util.Random._ + | + | def t1(x: Int) = { + | val y = x + | println(y) + | } + | + | def t2 = { + | val x = 2 + | val y = x + | println(y) + | } + | + | def t3 = { + | val x = this + | val y = x + | println(y) + | } + | + | def f = nextInt + | + | def t4 = { + | val x = f + | val y = x + | println(y) + | } + | + | def t5 = { + | var x = nextInt + | var y = x + | println(y) + | + | y = nextInt + | x = y + | println(x) + | } + |} + """.stripMargin + + val List(c) = compileClasses(methodOptCompiler)(code) + assertEquals(locals(c, "t1"), List(("this", 0), ("x", 1))) + + assertEquals(locals(c, "t2"), List(("this", 0), ("x", 1))) + // we don't have constant propagation (yet). + // the local var can't be optimized as a store;laod sequence, there's a GETSTATIC between the two + assertSameSummary(getSingleMethod(c, "t2"), List( + ICONST_2, ISTORE, GETSTATIC, ILOAD, "boxToInteger", "println", RETURN)) + + assertEquals(locals(c, "t3"), List(("this", 0))) + assertEquals(locals(c, "t4"), List(("this", 0), ("x", 1))) + assertEquals(locals(c, "t5"), List(("this", 0), ("x", 1))) + } + + @Test + def t7006(): Unit = { + val code = + """class C { + | def t: Unit = { + | try { + | val x = 3 + | } finally { + | print("hello") + | } + | while(true) { } + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + val t = getSingleMethod(c, "t") + assertEquals(t.handlers, Nil) + assertEquals(locals(c, "t"), List(("this", 0))) + assertSameSummary(t, List(GETSTATIC, LDC, "print", -1, GOTO)) + } + + @Test + def booleanOrderingCompare(): Unit = { + val code = + """class C { + | def compare(x: Boolean, y: Boolean) = (x, y) match { + | case (false, true) => -1 + | case (true, false) => 1 + | case _ => 0 + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + assertNoInvoke(getSingleMethod(c, "compare")) + } + + @Test + def t8790(): Unit = { + val code = + """class C { + | def t(x: Int, y: Int): String = (x, y) match { + | case (7, 8) => "a" + | case _ => "b" + | } + |} + """.stripMargin + val List(c) = compileClasses(methodOptCompiler)(code) + + assertSameSummary(getSingleMethod(c, "t"), List( + BIPUSH, ILOAD, IF_ICMPNE, + BIPUSH, ILOAD, IF_ICMPNE, + LDC, ASTORE, GOTO, + -1, LDC, ASTORE, + -1, ALOAD, ARETURN)) + } +} diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/ScalaInlineInfoTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/ScalaInlineInfoTest.scala index f8e887426b..6cb3fd3bba 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/ScalaInlineInfoTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/ScalaInlineInfoTest.scala @@ -9,26 +9,37 @@ import scala.tools.asm.Opcodes._ import org.junit.Assert._ import CodeGenTools._ +import scala.tools.asm.tree.ClassNode import scala.tools.nsc.backend.jvm.BTypes.{MethodInlineInfo, InlineInfo} import scala.tools.partest.ASMConverters import ASMConverters._ -import scala.collection.convert.decorateAsScala._ - -object ScalaInlineInfoTest { - var compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none") - def clear(): Unit = { compiler = null } -} +import scala.collection.JavaConverters._ +import scala.tools.testing.ClearAfterClass @RunWith(classOf[JUnit4]) -class ScalaInlineInfoTest { - val compiler = newCompiler() +class ScalaInlineInfoTest extends ClearAfterClass { + val compiler = cached("compiler", () => newCompiler(extraArgs = "-Yopt:l:none")) + + def inlineInfo(c: ClassNode): InlineInfo = c.attrs.asScala.collect({ case a: InlineInfoAttribute => a.inlineInfo }).head + + def mapDiff[A, B](a: Map[A, B], b: Map[A, B]) = { + val r = new StringBuilder + for ((a, av) <- a) { + if (!b.contains(a)) r.append(s"missing in b: $a\n") + else if (av != b(a)) r.append(s"different for $a: $av != ${b(a)}\n") + } + for (b <- b.keys.toList diff a.keys.toList) { + r.append(s"missing in a: $b\n") + } + r.toString + } @Test def traitMembersInlineInfo(): Unit = { val code = """trait T { | def f1 = 1 // concrete method - | private def f2 = 1 // implOnly method (does not end up in the interface) + | private def f2 = 1 // default method only (not in subclass) | def f3 = { | def nest = 0 // nested method (does not end up in the interface) | nest @@ -38,13 +49,13 @@ class ScalaInlineInfoTest { | def f4 = super.toString // super accessor | | object O // module accessor (method is generated) - | def f5 = { + | final def f5 = { | object L { val x = 0 } // nested module (just flattened out) | L.x | } | | @noinline - | def f6: Int // abstract method (not in impl class) + | def f6: Int // abstract method | | // fields | @@ -55,31 +66,111 @@ class ScalaInlineInfoTest { | | final val x5 = 0 |} + |class C extends T { + | def f6 = 0 + | var x3 = 0 + |} """.stripMargin - val cs @ List(t, tl, to, tCls) = compileClasses(compiler)(code) - val List(info) = t.attrs.asScala.collect({ case a: InlineInfoAttribute => a.inlineInfo }).toList - val expect = InlineInfo( - None, // self type + val cs @ List(c, t, tl, to) = compileClasses(compiler)(code) + val infoT = inlineInfo(t) + val expectT = InlineInfo ( false, // final class + None, // not a sam Map( - ("O()LT$O$;", MethodInlineInfo(true, false,false,false)), - ("T$$super$toString()Ljava/lang/String;",MethodInlineInfo(false,false,false,false)), - ("T$_setter_$x1_$eq(I)V", MethodInlineInfo(false,false,false,false)), - ("f1()I", MethodInlineInfo(false,true, false,false)), - ("f3()I", MethodInlineInfo(false,true, false,false)), - ("f4()Ljava/lang/String;", MethodInlineInfo(false,true, true, false)), - ("f5()I", MethodInlineInfo(false,true, false,false)), - ("f6()I", MethodInlineInfo(false,false,false,true )), - ("x1()I", MethodInlineInfo(false,false,false,false)), - ("x3()I", MethodInlineInfo(false,false,false,false)), - ("x3_$eq(I)V", MethodInlineInfo(false,false,false,false)), - ("x4()I", MethodInlineInfo(false,false,false,false)), - ("x5()I", MethodInlineInfo(true, false,false,false)), - ("y2()I", MethodInlineInfo(false,false,false,false)), - ("y2_$eq(I)V", MethodInlineInfo(false,false,false,false))), + ("O()LT$O$;", MethodInlineInfo(true ,false,false)), // the accessor is abstract in bytecode, but still effectivelyFinal because there's no (late)DEFERRED flag, https://github.com/scala/scala-dev/issues/126 + ("T$$super$toString()Ljava/lang/String;", MethodInlineInfo(true ,false,false)), + ("T$_setter_$x1_$eq(I)V", MethodInlineInfo(false,false,false)), + ("f1()I", MethodInlineInfo(false,false,false)), + ("f2()I", MethodInlineInfo(true, false,false)), + ("f3()I", MethodInlineInfo(false,false,false)), + ("f4()Ljava/lang/String;", MethodInlineInfo(false,true, false)), + ("f5()I", MethodInlineInfo(true ,false,false)), + ("f6()I", MethodInlineInfo(false,false,true )), + ("x1()I", MethodInlineInfo(false,false,false)), + ("y2()I", MethodInlineInfo(false,false,false)), + ("y2_$eq(I)V", MethodInlineInfo(false,false,false)), + ("x3()I", MethodInlineInfo(false,false,false)), + ("x3_$eq(I)V", MethodInlineInfo(false,false,false)), + ("x4()I", MethodInlineInfo(false,false,false)), + ("x5()I", MethodInlineInfo(true, false,false)), + ("L$lzycompute$1(Lscala/runtime/VolatileObjectRef;)LT$L$2$;", MethodInlineInfo(true, false,false)), + ("L$1(Lscala/runtime/VolatileObjectRef;)LT$L$2$;", MethodInlineInfo(true ,false,false)), + ("nest$1()I", MethodInlineInfo(true, false,false)), + ("$init$()V", MethodInlineInfo(false,false,false))), None // warning ) - assert(info == expect, info) + + assert(infoT == expectT, mapDiff(expectT.methodInfos, infoT.methodInfos) + infoT) + + val infoC = inlineInfo(c) + val expectC = InlineInfo(false, None, Map( + "O()LT$O$;" -> MethodInlineInfo(true ,false,false), + "O$lzycompute()LT$O$;" -> MethodInlineInfo(true, false,false), + "f6()I" -> MethodInlineInfo(false,false,false), + "x1()I" -> MethodInlineInfo(false,false,false), + "T$_setter_$x1_$eq(I)V" -> MethodInlineInfo(false,false,false), + "y2()I" -> MethodInlineInfo(false,false,false), + "y2_$eq(I)V" -> MethodInlineInfo(false,false,false), + "x3()I" -> MethodInlineInfo(false,false,false), + "x3_$eq(I)V" -> MethodInlineInfo(false,false,false), + "x4$lzycompute()I" -> MethodInlineInfo(true ,false,false), + "x4()I" -> MethodInlineInfo(false,false,false), + "x5()I" -> MethodInlineInfo(true ,false,false), + "T$$super$toString()Ljava/lang/String;" -> MethodInlineInfo(true ,false,false), + "<init>()V" -> MethodInlineInfo(false,false,false)), + None) + + assert(infoC == expectC, mapDiff(expectC.methodInfos, infoC.methodInfos) + infoC) + } + + @Test + def inlineInfoSam(): Unit = { + val code = + """trait C { // expected to be seen as sam: g(I)I + | def f = 0 + | def g(x: Int): Int + | val foo = "hi" + |} + |abstract class D { + | val biz: Int + |} + |trait T { // expected to be seen as sam: h(Ljava/lang/String;)I + | def h(a: String): Int + |} + |trait E extends T { // expected to be seen as sam: h(Ljava/lang/String;)I + | def hihi(x: Int) = x + |} + |class F extends T { + | def h(a: String) = 0 + |} + |trait U { + | def conc() = 10 + | def nullary: Int + |} + """.stripMargin + val cs = compileClasses(compiler)(code) + val sams = cs.map(c => (c.name, inlineInfo(c).sam)) + assertEquals(sams, + List( + ("C",Some("g(I)I")), + ("D",None), + ("E",Some("h(Ljava/lang/String;)I")), + ("F",None), + ("T",Some("h(Ljava/lang/String;)I")), + ("U",None))) + + } + + @Test + def lzyComputeInlineInfo(): Unit = { + val code = "class C { object O }" + val List(c, om) = compileClasses(compiler)(code) + val infoC = inlineInfo(c) + val expected = Map( + "<init>()V" -> MethodInlineInfo(false,false,false), + "O$lzycompute()LC$O$;" -> MethodInlineInfo(true,false,false), + "O()LC$O$;" -> MethodInlineInfo(true,false,false)) + assert(infoC.methodInfos == expected, mapDiff(infoC.methodInfos, expected)) } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/SimplifyJumpsTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/SimplifyJumpsTest.scala index a685ae7dd5..99acb318de 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/SimplifyJumpsTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/SimplifyJumpsTest.scala @@ -96,10 +96,22 @@ class SimplifyJumpsTest { instructionsFromMethod(method), List(VarOp(ILOAD, 1), Jump(IFLT, Label(3))) ::: rest.tail ) - // no label allowed between begin and rest. if there's another label, then there could be a - // branch that label. eliminating the GOTO would change the behavior. - val nonOptMethod = genMethod()(begin ::: Label(22) :: rest: _*) - assertFalse(LocalOptImpls.simplifyJumps(nonOptMethod)) + // branch over goto is OK even if there's a label in between, if that label is not a jump target + val withNonJumpTargetLabel = genMethod()(begin ::: Label(22) :: rest: _*) + assertTrue(LocalOptImpls.simplifyJumps(withNonJumpTargetLabel)) + assertSameCode( + instructionsFromMethod(withNonJumpTargetLabel), + List(VarOp(ILOAD, 1), Jump(IFLT, Label(3)), Label(22)) ::: rest.tail ) + + // if the Label(22) between IFGE and GOTO is the target of some jump, we cannot rewrite the IFGE + // and remove the GOTO: removing the GOTO would change semantics. However, the jump that targets + // Label(22) will be re-written (jump-chain collapsing), so in a second round, the IFGE is still + // rewritten to IFLT + val twoRounds = genMethod()(List(VarOp(ILOAD, 1), Jump(IFLE, Label(22))) ::: begin ::: Label(22) :: rest: _*) + assertTrue(LocalOptImpls.simplifyJumps(twoRounds)) + assertSameCode( + instructionsFromMethod(twoRounds), + List(VarOp(ILOAD, 1), Jump(IFLE, Label(3)), VarOp(ILOAD, 1), Jump(IFLT, Label(3)), Label(22)) ::: rest.tail ) } @Test @@ -167,6 +179,9 @@ class SimplifyJumpsTest { VarOp(ILOAD, 1), Jump(IFGE, Label(target)), + VarOp(ILOAD, 1), // some code to prevent rewriting the conditional jump + Op(IRETURN), + Label(4), Jump(GOTO, Label(3)), diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala index 902af7b7fa..46f06d1d39 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/UnreachableCodeTest.scala @@ -15,36 +15,17 @@ import scala.tools.partest.ASMConverters import ASMConverters._ import scala.tools.testing.ClearAfterClass -object UnreachableCodeTest extends ClearAfterClass.Clearable { - // jvm-1.6 enables emitting stack map frames, which impacts the code generation wrt dead basic blocks, - // see comment in BCodeBodyBuilder - var methodOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:method") - var dceCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:unreachable-code") - var noOptCompiler = newCompiler(extraArgs = "-target:jvm-1.6 -Ybackend:GenBCode -Yopt:l:none") - - // jvm-1.5 disables computing stack map frames, and it emits dead code as-is. note that this flag triggers a deprecation warning - var noOptNoFramesCompiler = newCompiler(extraArgs = "-target:jvm-1.5 -Ybackend:GenBCode -Yopt:l:none -deprecation") - - def clear(): Unit = { - methodOptCompiler = null - dceCompiler = null - noOptCompiler = null - noOptNoFramesCompiler = null - } -} - @RunWith(classOf[JUnit4]) class UnreachableCodeTest extends ClearAfterClass { - ClearAfterClass.stateToClear = UnreachableCodeTest - - val methodOptCompiler = UnreachableCodeTest.methodOptCompiler - val dceCompiler = UnreachableCodeTest.dceCompiler - val noOptCompiler = UnreachableCodeTest.noOptCompiler - val noOptNoFramesCompiler = UnreachableCodeTest.noOptNoFramesCompiler + // jvm-1.6 enables emitting stack map frames, which impacts the code generation wrt dead basic blocks, + // see comment in BCodeBodyBuilder + val methodOptCompiler = cached("methodOptCompiler", () => newCompiler(extraArgs = "-Yopt:l:method")) + val dceCompiler = cached("dceCompiler", () => newCompiler(extraArgs = "-Yopt:unreachable-code")) + val noOptCompiler = cached("noOptCompiler", () => newCompiler(extraArgs = "-Yopt:l:none")) def assertEliminateDead(code: (Instruction, Boolean)*): Unit = { val method = genMethod()(code.map(_._1): _*) - LocalOptImpls.removeUnreachableCodeImpl(method, "C") + dceCompiler.genBCode.bTypes.localOpt.removeUnreachableCodeImpl(method, "C") val nonEliminated = instructionsFromMethod(method) val expectedLive = code.filter(_._2).map(_._1).toList assertSameCode(nonEliminated, expectedLive) @@ -152,11 +133,6 @@ class UnreachableCodeTest extends ClearAfterClass { // Finally, instructions in the dead basic blocks are replaced by ATHROW, as explained in // a comment in BCodeBodyBuilder. assertSameCode(noDce.dropNonOp, List(Op(ICONST_1), Op(IRETURN), Op(ATHROW), Op(ATHROW))) - - // when NOT computing stack map frames, ASM's ClassWriter does not replace dead code by NOP/ATHROW - val warn = "target:jvm-1.5 is deprecated" - val noDceNoFrames = singleMethodInstructions(noOptNoFramesCompiler)(code, allowMessage = _.msg contains warn) - assertSameCode(noDceNoFrames.dropNonOp, List(Op(ICONST_1), Op(IRETURN), Op(ICONST_2), Op(IRETURN))) } @Test @@ -225,4 +201,50 @@ class UnreachableCodeTest extends ClearAfterClass { assertTrue(List(FrameEntry(F_FULL, List(INTEGER, DOUBLE, Label(3)), List("java/lang/Object", Label(4))), Label(3), Label(4)) === List(FrameEntry(F_FULL, List(INTEGER, DOUBLE, Label(1)), List("java/lang/Object", Label(3))), Label(1), Label(3))) } + + @Test + def loadNullNothingBytecode(): Unit = { + val code = + """class C { + | def nl: Null = null + | def nt: Nothing = throw new Error("") + | def cons(a: Any) = () + | + | def t1 = cons(null) + | def t2 = cons(nl) + | def t3 = cons(throw new Error("")) + | def t4 = cons(nt) + |} + """.stripMargin + val List(c) = compileClasses(noOptCompiler)(code) + + assertSameSummary(getSingleMethod(c, "nl"), List(ACONST_NULL, ARETURN)) + + assertSameSummary(getSingleMethod(c, "nt"), List( + NEW, DUP, LDC, "<init>", ATHROW)) + + assertSameSummary(getSingleMethod(c, "t1"), List( + ALOAD, ACONST_NULL, "cons", RETURN)) + + // GenBCode introduces POP; ACONST_NULL after loading an expression of type scala.runtime.Null$, + // see comment in BCodeBodyBuilder.adapt + assertSameSummary(getSingleMethod(c, "t2"), List( + ALOAD, ALOAD, "nl", POP, ACONST_NULL, "cons", RETURN)) + + // the bytecode generated by GenBCode is ... ATHROW; INVOKEVIRTUAL C.cons; RETURN + // the ASM classfile writer creates a new basic block (creates a label) right after the ATHROW + // and replaces all instructions by NOP*; ATHROW, see comment in BCodeBodyBuilder.adapt + // NOTE: DCE is enabled by default and gets rid of the redundant code (tested below) + assertSameSummary(getSingleMethod(c, "t3"), List( + ALOAD, NEW, DUP, LDC, "<init>", ATHROW, NOP, NOP, NOP, ATHROW)) + + // GenBCode introduces an ATHROW after the invocation of C.nt, see BCodeBodyBuilder.adapt + // NOTE: DCE is enabled by default and gets rid of the redundant code (tested below) + assertSameSummary(getSingleMethod(c, "t4"), List( + ALOAD, ALOAD, "nt", ATHROW, NOP, NOP, NOP, ATHROW)) + + val List(cDCE) = compileClasses(dceCompiler)(code) + assertSameSummary(getSingleMethod(cDCE, "t3"), List(ALOAD, NEW, DUP, LDC, "<init>", ATHROW)) + assertSameSummary(getSingleMethod(cDCE, "t4"), List(ALOAD, ALOAD, "nt", ATHROW)) + } } diff --git a/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala b/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala index 769736669b..77e73e64b9 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/UnusedLocalVariablesTest.scala @@ -14,16 +14,9 @@ import scala.tools.partest.ASMConverters import ASMConverters._ import scala.tools.testing.ClearAfterClass -object UnusedLocalVariablesTest extends ClearAfterClass.Clearable { - var dceCompiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:unreachable-code") - def clear(): Unit = { dceCompiler = null } -} - @RunWith(classOf[JUnit4]) class UnusedLocalVariablesTest extends ClearAfterClass { - ClearAfterClass.stateToClear = UnusedLocalVariablesTest - - val dceCompiler = UnusedLocalVariablesTest.dceCompiler + val dceCompiler = cached("dceCompiler", () => newCompiler(extraArgs = "-Yopt:unreachable-code")) @Test def removeUnusedVar(): Unit = { |