diff options
Diffstat (limited to 'test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala')
-rw-r--r-- | test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala | 1278 |
1 files changed, 1020 insertions, 258 deletions
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..7be88816d5 100644 --- a/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala +++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala @@ -2,72 +2,48 @@ package scala.tools.nsc package backend.jvm package opt +import org.junit.Assert._ +import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.JUnit4 -import org.junit.Test + +import scala.collection.JavaConverters._ 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.backend.jvm.BackendReporting._ import scala.tools.nsc.reporters.StoreReporter -import scala.tools.testing.AssertUtil._ +import scala.tools.partest.ASMConverters._ +import scala.tools.testing.BytecodeTesting +import scala.tools.testing.BytecodeTesting._ -import CodeGenTools._ -import scala.tools.partest.ASMConverters -import ASMConverters._ -import AsmUtils._ - -import BackendReporting._ - -import scala.collection.convert.decorateAsScala._ -import scala.tools.testing.ClearAfterClass +@RunWith(classOf[JUnit4]) +class InlinerTest extends BytecodeTesting { + override def compilerArgs = "-opt:l:classpath -opt-warnings" -object InlinerTest extends ClearAfterClass.Clearable { - val args = "-Ybackend:GenBCode -Yopt:l:classpath -Yopt-warnings" - var compiler = newCompiler(extraArgs = args) + val inlineOnlyCompiler = cached("inlineOnlyCompiler", () => newCompiler(extraArgs = "-opt:inline-project")) + import compiler._ + import global.genBCode.bTypes // 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 notPerRun: List[Clearable] = List( + bTypes.classBTypeFromInternalName, + bTypes.byteCodeRepository.compilingClasses, + bTypes.byteCodeRepository.parsedClasses, + bTypes.callGraph.callsites) + notPerRun foreach global.perRunCaches.unrecordCache - def assertNoInvoke(m: Method): Unit = assertNoInvoke(m.instructions) - def assertNoInvoke(ins: List[Instruction]): Unit = { - assert(!ins.exists(_.isInstanceOf[Invoke]), ins.stringLines) - } + import global.genBCode.bTypes.{byteCodeRepository, callGraph, inliner, inlinerHeuristics} + import inlinerHeuristics._ - 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 compiler = InlinerTest.compiler - import compiler.genBCode.bTypes._ def compile(scalaCode: String, javaCode: List[(String, String)] = Nil, allowMessage: StoreReporter#Info => Boolean = _ => false): List[ClassNode] = { - InlinerTest.notPerRun.foreach(_.clear()) - compileClasses(compiler)(scalaCode, javaCode, allowMessage) + notPerRun.foreach(_.clear()) + compileToBytes(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.map(_._1).toList.sortBy(_.name) } def checkCallsite(callsite: callGraph.Callsite, callee: MethodNode) = { @@ -79,27 +55,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 = getAsmMethod(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.canInlineCallsite(cs).map(_._1) + } - 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, null)) + gMethod } @Test @@ -111,10 +85,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 +119,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) + + 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).instructions.dropNonOp.take(4), expectedInlined) + 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))) + global.genBCode.bTypes.localOpt.methodOptimizations(g, "C") + assertSameCode(convertMethod(g), invokeQQQ :+ Op(ATHROW)) } @Test @@ -166,11 +147,11 @@ class InlinerTest extends ClearAfterClass { |} """.stripMargin - val (_, can) = inlineTest(code, cls => { - val f = cls.methods.asScala.find(_.name == "f").get + val can = canInlineTest(code, cls => { + val f = getAsmMethod(cls, "f") f.access |= ACC_SYNCHRONIZED }) - assert(can.get.isInstanceOf[SynchronizedMethod], can) + assert(can.nonEmpty && can.get.isInstanceOf[SynchronizedMethod], can) } @Test @@ -181,7 +162,7 @@ class InlinerTest extends ClearAfterClass { | def g = f + 1 |} """.stripMargin - val (_, r) = inlineTest(code) + val r = canInlineTest(code) assert(r.isEmpty, r) } @@ -195,8 +176,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 +197,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 = getAsmMethod(d, "h") + val gCall = getCallsite(hMeth, "g") + val r = inliner.canInlineCallsite(gCall) + assert(r.nonEmpty && r.get._1.isInstanceOf[IllegalAccessInstruction], r) } @Test @@ -252,7 +214,7 @@ class InlinerTest extends ClearAfterClass { |} """.stripMargin val List(cCls) = compile(code) - val instructions = getSingleMethod(cCls, "test").instructions + val instructions = getInstructions(cCls, "test") assert(instructions.contains(Op(ICONST_0)), instructions.stringLines) assert(!instructions.contains(Op(ICONST_1)), instructions) } @@ -273,7 +235,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 +257,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) } } @@ -318,7 +280,7 @@ class InlinerTest extends ClearAfterClass { |} """.stripMargin val List(c, _, _) = compile(code) - val ins = getSingleMethod(c, "f").instructions + val ins = getInstructions(c, "f") val invokeSysArraycopy = Invoke(INVOKESTATIC, "java/lang/System", "arraycopy", "(Ljava/lang/Object;ILjava/lang/Object;II)V", false) assert(ins contains invokeSysArraycopy, ins.stringLines) } @@ -336,7 +298,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,8 +311,8 @@ class InlinerTest extends ClearAfterClass { | def g(t: T) = t.f |} """.stripMargin - val List(c, t, tClass) = compile(code) - assertNoInvoke(getSingleMethod(c, "g")) + val List(c, t) = compile(code) + assertNoInvoke(getMethod(c, "g")) } @Test @@ -363,7 +325,7 @@ class InlinerTest extends ClearAfterClass { """.stripMargin val List(c) = compile(code) // no more invoke, f is inlined - assertNoInvoke(getSingleMethod(c, "g")) + assertNoInvoke(getMethod(c, "g")) } @Test @@ -375,26 +337,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 = getAsmMethod(c, "f") + val call = getCallsite(fMeth, "lowestOneBit") - assert(r.isEmpty, r) - val ins = instructionsFromMethod(f) + val warning = inliner.canInlineCallsite(call) + assert(warning.isEmpty, warning) + + inliner.inline(InlineRequest(call, Nil, null)) + val ins = instructionsFromMethod(fMeth) // no invocations, lowestOneBit is inlined assertNoInvoke(ins) @@ -425,7 +375,8 @@ class InlinerTest extends ClearAfterClass { |} """.stripMargin - val List(c) = compile(code) + // use a compiler without local optimizations (cleanups) + val c = inlineOnlyCompiler.compileClass(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,12 +416,12 @@ 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 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}) assert(c == 1, c) - val ins = getSingleMethod(b, "g").instructions + val ins = getInstructions(b, "g") val invokeFlop = Invoke(INVOKEVIRTUAL, "B", "flop", "()I", false) assert(ins contains invokeFlop, ins.stringLines) } @@ -488,23 +439,10 @@ 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")) + assertNoInvoke(getMethod(c, "t1")) + assertNoInvoke(getMethod(c, "t2")) } @Test @@ -521,9 +459,9 @@ class InlinerTest extends ClearAfterClass { | def t2 = g |} """.stripMargin - val List(c, t, tClass, u, uClass) = compile(code) - assertNoInvoke(getSingleMethod(c, "t1")) - assertNoInvoke(getSingleMethod(c, "t2")) + val List(c, t, u) = compile(code) + assertNoInvoke(getMethod(c, "t1")) + assertNoInvoke(getMethod(c, "t2")) } @Test @@ -537,14 +475,12 @@ class InlinerTest extends ClearAfterClass { | def t2 = this.f |} """.stripMargin - val warns = Set( - "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") + val warn = "::f()I is annotated @inline but could not be inlined:\nThe 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; i.msg contains warn}) assert(count == 2, count) - assertInvoke(getSingleMethod(c, "t1"), "T", "f") - assertInvoke(getSingleMethod(c, "t2"), "C", "f") + assertInvoke(getMethod(c, "t1"), "T", "f") + assertInvoke(getMethod(c, "t2"), "C", "f") } @Test @@ -557,8 +493,8 @@ class InlinerTest extends ClearAfterClass { | def t1(t: T) = t.f |} """.stripMargin - val List(c, t, tClass) = compile(code) - assertNoInvoke(getSingleMethod(c, "t1")) + val List(c, t) = compile(code) + assertNoInvoke(getMethod(c, "t1")) } @Test @@ -569,7 +505,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 @@ -577,16 +513,16 @@ class InlinerTest extends ClearAfterClass { | def t3(t: T) = t.f // no inlining here |} """.stripMargin - val warn = "T::f()I is annotated @inline but cannot be inlined: the method is not final and may be overridden" + val warn = "T::f()I is annotated @inline but could not be inlined:\nThe 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(getMethod(t, "f")) - assertNoInvoke(getSingleMethod(c, "t1")) - assertNoInvoke(getSingleMethod(c, "t2")) - assertInvoke(getSingleMethod(c, "t3"), "T", "f") + assertNoInvoke(getMethod(c, "t1")) + assertNoInvoke(getMethod(c, "t2")) + assertInvoke(getMethod(c, "t3"), "T", "f") } @Test @@ -598,24 +534,22 @@ 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(getMethod(t, "f")) - assertNoInvoke(getSingleMethod(assemblyClass, "n")) + assertNoInvoke(getMethod(assembly, "n")) - assertNoInvoke(getSingleMethod(c, "t1")) - assertNoInvoke(getSingleMethod(c, "t2")) + assertNoInvoke(getMethod(c, "t1")) + assertNoInvoke(getMethod(c, "t2")) } @Test @@ -647,30 +581,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,35 +613,29 @@ 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" + val warning = "T1::f()I is annotated @inline but could not be inlined:\nThe 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(getMethod(t2a, "g2a")) + assertInvoke(getMethod(t2b, "g2b"), "T1", "f") - assertInvoke(getSingleMethod(ca, "m1a"), "T1", "f") - assertNoInvoke(getSingleMethod(ca, "m2a")) // no invoke, see comment on def g2a - assertNoInvoke(getSingleMethod(ca, "m3a")) - assertInvoke(getSingleMethod(ca, "m4a"), "T1", "f") - assertNoInvoke(getSingleMethod(ca, "m5a")) + assertInvoke(getMethod(ca, "m1a"), "T1", "f") + assertNoInvoke(getMethod(ca, "m2a")) // no invoke, see comment on def g2a + assertNoInvoke(getMethod(ca, "m3a")) + assertInvoke(getMethod(ca, "m4a"), "T1", "f") + assertNoInvoke(getMethod(ca, "m5a")) - assertInvoke(getSingleMethod(cb, "m1b"), "T1", "f") - assertInvoke(getSingleMethod(cb, "m2b"), "T1", "f") // invoke, see comment on def g2b - assertNoInvoke(getSingleMethod(cb, "m3b")) - assertInvoke(getSingleMethod(cb, "m4b"), "T1", "f") - assertNoInvoke(getSingleMethod(cb, "m5b")) + assertInvoke(getMethod(cb, "m1b"), "T1", "f") + assertInvoke(getMethod(cb, "m2b"), "T1", "f") // invoke, see comment on def g2b + assertNoInvoke(getMethod(cb, "m3b")) + assertInvoke(getMethod(cb, "m4b"), "T1", "f") + assertNoInvoke(getMethod(cb, "m5b")) } @Test @@ -724,7 +652,7 @@ class InlinerTest extends ClearAfterClass { |} // so d.f can be resolved statically. same for E.f """.stripMargin val List(c, d, e, eModule, t) = compile(code) - assertNoInvoke(getSingleMethod(t, "t1")) + assertNoInvoke(getMethod(t, "t1")) } @Test @@ -732,16 +660,15 @@ 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) - assertNoInvoke(getSingleMethod(d, "m")) - assertNoInvoke(getSingleMethod(c, "m")) + val List(c, d, t) = compile(code) + assertNoInvoke(getMethod(d, "m")) + assertNoInvoke(getMethod(c, "m")) } @Test @@ -754,9 +681,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 = getMethod(t, "t1") + val t2 = getMethod(t, "t2") val cast = TypeOp(CHECKCAST, "C") Set(t1, t2).foreach(m => assert(m.instructions.contains(cast), m.instructions)) } @@ -771,7 +698,7 @@ class InlinerTest extends ClearAfterClass { | def t1(c: C) = c.foo |} """.stripMargin - val warn = "C::foo()I is annotated @inline but cannot be inlined: the method is not final and may be overridden" + val warn = "C::foo()I is annotated @inline but could not be inlined:\nThe method is not final and may be overridden." var c = 0 compile(code, allowMessage = i => {c += 1; i.msg contains warn}) assert(c == 1, c) @@ -799,12 +726,12 @@ class InlinerTest extends ClearAfterClass { """sealed trait T { | lazy val a = 0 | val b = 1 - | final lazy val c = 2 + | final lazy val c: Int = 2 // make sure it doesn't get a constant type | 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 @@ -813,12 +740,12 @@ class InlinerTest extends ClearAfterClass { |trait U { // not sealed | lazy val a = 0 | val b = 1 - | final lazy val c = 2 + | final lazy val c: Int = 2 // make sure it doesn't get a constant type | 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,30 +762,30 @@ 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 m1 = getSingleMethod(c, "m1") + val List(c, t, u) = compile(code, allowMessage = _.msg contains "::i()I is annotated @inline but could not be inlined:\nThe method is not final and may be overridden.") + val m1 = getMethod(c, "m1") assertInvoke(m1, "T", "a") assertInvoke(m1, "T", "b") - assertInvoke(m1, "T", "c") +// assertInvoke(m1, "T", "c") -- this lazy val is implemented purely in the trait, as it's constant, so it *can* be inlined - assertNoInvoke(getSingleMethod(c, "m2")) + assertNoInvoke(getMethod(c, "m2")) - val m3 = getSingleMethod(c, "m3") - assertInvoke(m3, "T$class", "f") - assertInvoke(m3, "T$class", "g") + val m3 = getMethod(c, "m3") + assertInvoke(m3, "T", "f") + assertInvoke(m3, "T", "g") assertInvoke(m3, "T", "h") assertInvoke(m3, "T", "i") - val m4 = getSingleMethod(c, "m4") + val m4 = getMethod(c, "m4") assertInvoke(m4, "U", "a") assertInvoke(m4, "U", "b") - assertInvoke(m4, "U", "c") +// assertInvoke(m4, "U", "c") -- this lazy val is implemented purely in the trait, as it's constant, so it *can* be inlined - assertNoInvoke(getSingleMethod(c, "m5")) + assertNoInvoke(getMethod(c, "m5")) - val m6 = getSingleMethod(c, "m6") + val m6 = getMethod(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 +819,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 class A$Inner could not be found on the classpath.""".stripMargin var c = 0 - compileClasses(newCompiler(extraArgs = InlinerTest.args + " -Yopt-warnings:_"))( + newCompiler(extraArgs = compilerArgs + " -opt-warnings:_").compileClasses( scalaCode, List((javaCode, "A.java")), allowMessage = i => {c += 1; i.msg contains warn}) @@ -906,11 +833,11 @@ class InlinerTest extends ClearAfterClass { @Test def inlineInvokeSpecial(): Unit = { val code = - """class Aa { + """class A { | def f1 = 0 |} - |class B extends Aa { - | @inline final override def f1 = 1 + super.f1 // invokespecial Aa.f1 + |class B extends A { + | @inline final override def f1 = 1 + super.f1 // invokespecial A.f1 | | private def f2m = 0 // public B$$f2m in bytecode | @inline final def f2 = f2m // invokevirtual B.B$$f2m @@ -934,21 +861,21 @@ class InlinerTest extends ClearAfterClass { val warn = """B::f1()I is annotated @inline but could not be inlined: - |The callee B::f1()I contains the instruction INVOKESPECIAL Aa.f1 ()I + |The callee B::f1()I contains the instruction INVOKESPECIAL A.f1 ()I |that would cause an IllegalAccessError when inlined into class T.""".stripMargin var c = 0 val List(a, b, t) = compile(code, allowMessage = i => {c += 1; i.msg contains warn}) assert(c == 1, c) - assertInvoke(getSingleMethod(b, "t1"), "Aa", "f1") - assertInvoke(getSingleMethod(b, "t2"), "B", "B$$f2m") - assertInvoke(getSingleMethod(b, "t3"), "B", "<init>") - assertInvoke(getSingleMethod(b, "t4"), "B", "<init>") + assertInvoke(getMethod(b, "t1"), "A", "f1") + assertInvoke(getMethod(b, "t2"), "B", "B$$f2m") + assertInvoke(getMethod(b, "t3"), "B", "<init>") + assertInvoke(getMethod(b, "t4"), "B", "<init>") - assertInvoke(getSingleMethod(t, "t1"), "B", "f1") - assertInvoke(getSingleMethod(t, "t2"), "B", "B$$f2m") - assertInvoke(getSingleMethod(t, "t3"), "B", "<init>") - assertInvoke(getSingleMethod(t, "t4"), "B", "<init>") + assertInvoke(getMethod(t, "t1"), "B", "f1") + assertInvoke(getMethod(t, "t2"), "B", "B$$f2m") + assertInvoke(getMethod(t, "t3"), "B", "<init>") + assertInvoke(getMethod(t, "t4"), "B", "<init>") } @Test @@ -958,8 +885,8 @@ 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) - assertInvoke(getSingleMethod(c, "t"), "java/lang/System", "arraycopy") + val c = newCompiler(extraArgs = compilerArgs + " -Yopt-inline-heuristics:everything").compileClass(code) + assertInvoke(getMethod(c, "t"), "java/lang/System", "arraycopy") } @Test @@ -973,7 +900,7 @@ class InlinerTest extends ClearAfterClass { """.stripMargin val List(c) = compile(code) - assertInvoke(getSingleMethod(c, "t"), "java/lang/Error", "<init>") + assertInvoke(getMethod(c, "t"), "java/lang/Error", "<init>") } @Test @@ -986,9 +913,844 @@ class InlinerTest extends ClearAfterClass { """.stripMargin val List(c) = compile(code) - val t = getSingleMethod(c, "t").instructions + val t = getInstructions(c, "t") 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 = getMethod(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", "$anonfun$m$2") + + val t2 = getMethod(c, "t2") + assertNoIndy(t2) + assertInvoke(t2, "M$", "$anonfun$m$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 = getAsmMethod(c, "h") + val gMeth = getAsmMethod(c, "g") + val iMeth = getAsmMethod(c, "i") + val fCall = getCallsite(gMeth, "f") + val gCall = getCallsite(hMeth, "g") + val hCall = getCallsite(iMeth, "h") + + val warning = inliner.canInlineCallsite(gCall) + assert(warning.isEmpty, warning) + + inliner.inline(InlineRequest(hCall, + post = List(InlineRequest(gCall, + post = List(InlineRequest(fCall, Nil, null)), null)), null)) + 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(getAsmMethod(cl, _)) + val aCall = getCallsite(b, "a") + val bCall = getCallsite(c, "b") + val cCall = getCallsite(d, "c") + + inliner.inline(InlineRequest(bCall, Nil, null)) + + val req = InlineRequest(cCall, + List(InlineRequest(bCall, + List(InlineRequest(aCall, Nil, null)), null)), null) + 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(getMethod(c, "t1"), "C", "$anonfun$t1$1") + assertInvoke(getMethod(c, "t2"), "C", "a") + assertInvoke(getMethod(c, "t3"), "C", "b") + assertNoInvoke(getMethod(c, "t4")) + assertNoInvoke(getMethod(c, "t5")) + assertNoInvoke(getMethod(c, "t6")) + assertInvoke(getMethod(c, "t7"), "C", "c") + assertInvoke(getMethod(c, "t8"), "scala/Predef$", "println") + assertNoInvoke(getMethod(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 // inlined + | 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(getMethod(c, "t1")) + assertInvoke(getMethod(c, "t2"), "C", "f2") + assertInvoke(getMethod(c, "t3"), "C", "f1") + assertNoInvoke(getMethod(c, "t4")) + assertNoInvoke(getMethod(c, "t5")) + assertInvoke(getMethod(c, "t6"), "C", "f3") + assertNoInvoke(getMethod(c, "t7")) + assertInvoke(getMethod(c, "t8"), "C", "f1") + assertNoInvoke(getMethod(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(getMethod(c, "t1"), "C", "$anonfun$t1$1") + assertInvoke(getMethod(c, "t2"), "C", "$anonfun$t2$1") + assertInvoke(getMethod(c, "t3"), "scala/Function1", "apply$mcII$sp") + assertInvoke(getMethod(c, "t4"), "scala/Function1", "apply$mcII$sp") + assertInvoke(getMethod(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(getMethod(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 INVOKESTATIC 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(getMethod(c, "h"), "C", "f$1") + assertInvoke(getMethod(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(getMethod(c, "g")) + assertNoInvoke(getMethod(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(getMethod(c, "t1"), List(BIPUSH, "$anonfun$t1$1", IRETURN)) + assertSameSummary(getMethod(c, "t1a"), List(LCONST_1, "$anonfun$t1a$1", IRETURN)) + assertSameSummary(getMethod(c, "t2"), List(ICONST_1, ICONST_2, "$anonfun$t2$1",IRETURN)) + + // val a = new ValKl(n); new ValKl(anonfun(a.x)).x + // value class instantiation-extraction should be optimized by boxing elim + assertSameSummary(getMethod(c, "t3"), List( + NEW, DUP, ICONST_1, "<init>", ASTORE, + NEW, DUP, ALOAD, "x", + "$anonfun$t3$1", + "<init>", + "x", IRETURN)) + + assertSameSummary(getMethod(c, "t4"), List(BIPUSH, "$anonfun$t4$1", "boxToInteger", ARETURN)) + assertSameSummary(getMethod(c, "t4a"), List(ICONST_1, LDC, "$anonfun$t4a$1", LRETURN)) + assertSameSummary(getMethod(c, "t5"), List(BIPUSH, ICONST_3, "$anonfun$t5$1", "boxToInteger", ARETURN)) + assertSameSummary(getMethod(c, "t5a"), List(BIPUSH, BIPUSH, I2B, "$anonfun$t5a$1", IRETURN)) + assertSameSummary(getMethod(c, "t6"), List(BIPUSH, "$anonfun$t6$1", RETURN)) + assertSameSummary(getMethod(c, "t7"), List(ICONST_1, "$anonfun$t7$1", RETURN)) + assertSameSummary(getMethod(c, "t8"), List(ICONST_1, LDC, "$anonfun$t8$1", LRETURN)) + assertSameSummary(getMethod(c, "t9"), List(ICONST_1, "boxToInteger", "$anonfun$t9$1", RETURN)) + + // t9a inlines Range.foreach, which is quite a bit of code, so just testing the core + assertInvoke(getMethod(c, "t9a"), "C", "$anonfun$t9a$1") + assertInvoke(getMethod(c, "t9a"), "scala/runtime/BoxesRunTime", "boxToInteger") + + assertSameSummary(getMethod(c, "t10"), List( + ICONST_1, ISTORE, + ALOAD, ILOAD, + "$anonfun$t10$1", RETURN)) + + // t10a inlines Range.foreach + assertInvoke(getMethod(c, "t10a"), "C", "$anonfun$t10a$1") + assertDoesNotInvoke(getMethod(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(getMethod(c, "t1"), List(Op(ICONST_0), Op(ICONST_1), Op(IADD), Op(IRETURN))) + assertEquals(getMethod(c, "t2").instructions collect { case i: Invoke => i.owner +"."+ i.name }, List( + "scala/runtime/IntRef.create", "C.$anonfun$t2$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(getMethod(c, "t1"), List(Op(ICONST_3), Op(ICONST_4), Op(IADD), Op(IRETURN))) + assertSameCode(getMethod(c, "t2"), List(Op(ICONST_1), Op(ICONST_2), Op(IADD), Op(IRETURN))) + assertSameCode(getMethod(c, "t3"), List(Op(ICONST_1), Op(ICONST_3), Op(ISUB), Op(IRETURN))) + assertNoInvoke(getMethod(c, "t4")) + assertNoInvoke(getMethod(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) = getInstructions(c, m) collect { case TypeOp(CHECKCAST, tp) => tp } + assertSameCode(getMethod(c, "t1"), List(VarOp(ALOAD, 1), Op(ARETURN))) + assertSameCode(getMethod(c, "t2"), List(VarOp(ALOAD, 1), Op(ARETURN))) + assertSameCode(getMethod(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 = findClass(cls, "Test$") + assertSameSummary(getMethod(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(getMethod(c, "t"), List( + ALOAD, "$anonfun$t$1", IFEQ /*A*/, + "$anonfun$t$2", IRETURN, + -1 /*A*/, "$anonfun$t$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 = "-opt:l:project -opt-warnings") + assertInvoke(getMethod(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) + // we never inline into mixin forwarders, see scala-dev#259 + assertInvoke(getMethod(c, "f"), "T2", "f$") + } + + @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(getMethod(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 = getMethod(c, "t") + assertNoIndy(t) + assertInvoke(t, "C", "$anonfun$t$1") + } + + @Test + def t9121(): Unit = { + val codes = List( + """package p1 + |object Implicits { + | class ScalaObservable(val underlying: Any) extends AnyVal { + | @inline def scMap[R](f: String): Any = f.toRx + | } + | implicit class RichFunction1[T1, R](val f: String) extends AnyVal { + | def toRx: Any = "" + | } + |} + """.stripMargin, + """ + |import p1.Implicits._ + |class C { + | def t(): Unit = new ScalaObservable("").scMap("") + |} + """.stripMargin) + val c :: _ = compileClassesSeparately(codes, extraArgs = compilerArgs) + assertInvoke(getMethod(c, "t"), "p1/Implicits$RichFunction1$", "toRx$extension") + } + + @Test + def keepLineNumbersPerCompilationUnit(): Unit = { + val code1 = + """class A { + | def fx(): Unit = () + | @inline final def ma = { + | fx() + | 1 + | } + |} + """.stripMargin + val code2 = + """class B extends A { + | @inline final def mb = { + | fx() + | 1 + | } + |} + |class C extends B { + | @inline final def mc = { + | fx() + | 1 + | } + | def t1 = ma // no lines, not the same source file + | def t2 = mb // lines + | def t3 = mc // lines + |} + """.stripMargin + notPerRun.foreach(_.clear()) + val run = compiler.newRun + run.compileSources(List(makeSourceFile(code1, "A.scala"), makeSourceFile(code2, "B.scala"))) + val List(_, _, c) = readAsmClasses(getGeneratedClassfiles(global.settings.outputDirs.getSingleOutput.get)) + def is(name: String) = getMethod(c, name).instructions.filterNot(_.isInstanceOf[FrameEntry]) + + assertSameCode(is("t1"), List( + Label(0), LineNumber(12, Label(0)), + VarOp(ALOAD, 0), Invoke(INVOKEVIRTUAL, "A", "fx", "()V", false), + Op(ICONST_1), Op(IRETURN), Label(6))) + + assertSameCode(is("t2"), List( + Label(0), LineNumber(3, Label(0)), VarOp(ALOAD, 0), Invoke(INVOKEVIRTUAL, "B", "fx", "()V", false), + Label(4), LineNumber(4, Label(4)), Op(ICONST_1), Op(IRETURN), Label(8))) + + assertSameCode(is("t3"), List( + Label(0), LineNumber(9, Label(0)), VarOp(ALOAD, 0), Invoke(INVOKEVIRTUAL, "C", "fx", "()V", false), + Label(4), LineNumber(10, Label(4)), Op(ICONST_1), Op(IRETURN), Label(8))) + } + + @Test + def traitHO(): Unit = { + val code = + """trait T { + | def foreach(f: Int => Unit): Unit = f(1) + |} + |final class C extends T { + | def cons(x: Int): Unit = () + | def t1 = foreach(cons) + |} + """.stripMargin + val List(c, t) = compile(code) + assertNoIndy(getMethod(c, "t1")) + } + + @Test + def limitInlinedLocalVariableNames(): Unit = { + val code = + """class C { + | def f(x: Int): Int = x + | @inline final def methodWithVeryVeryLongNameAlmostLikeAGermanWordOrAFrenchSentence(param: Int) = + | f(param) + | @inline final def anotherMethodWithVeryVeryLongNameAlmostLikeAGermanWordOrAFrenchSentence(param: Int) = + | methodWithVeryVeryLongNameAlmostLikeAGermanWordOrAFrenchSentence(f(param)) + | @inline final def oneMoreMethodWithVeryVeryLongNameAlmostLikeAGermanWordOrAFrenchSentence(param: Int) = + | anotherMethodWithVeryVeryLongNameAlmostLikeAGermanWordOrAFrenchSentence(f(param)) + | @inline final def yetAnotherMethodWithVeryVeryLongNameAlmostLikeAGermanWordOrAFrenchSentence(param: Int) = + | oneMoreMethodWithVeryVeryLongNameAlmostLikeAGermanWordOrAFrenchSentence(f(param)) + | @inline final def oneLastMethodWithVeryVeryLongNameAlmostLikeAGermanWordOrAFrenchSentence(param: Int) = + | yetAnotherMethodWithVeryVeryLongNameAlmostLikeAGermanWordOrAFrenchSentence(f(param)) + | def t(p: Int) = + | oneLastMethodWithVeryVeryLongNameAlmostLikeAGermanWordOrAFrenchSentence(f(p)) + + | oneLastMethodWithVeryVeryLongNameAlmostLikeAGermanWordOrAFrenchSentence(f(p)) + |} + """.stripMargin + + val List(c) = compile(code) + assertEquals(getAsmMethod(c, "t").localVariables.asScala.toList.map(l => (l.name, l.index)).sortBy(_._2),List( + ("this",0), + ("p",1), + ("oneLastMethodWithVeryVeryLongNameAlmostLikeAGermanWordOrAFrenchSentence_param",2), + ("oneLastMethodWithVeryVeryLongNameAlmostLikeAGermanWordOrAFrenchS_yetAnotherMethodWithVeryVeryLongNameAlmostLikeAGermanWordOrAFren_param",3), + ("oneLastMethodWithVeryVeryLongNameAlmostLik_yetAnotherMethodWithVeryVeryLongNameAlmost_oneMoreMethodWithVeryVeryLongNameAlmostLik_param",4), + ("oneLastMethodWithVeryVeryLongNam_yetAnotherMethodWithVeryVeryLong_oneMoreMethodWithVeryVeryLongNam_anotherMethodWithVeryVeryLongNam_param",5), + ("oneLastMethodWithVeryVery_yetAnotherMethodWithVeryV_oneMoreMethodWithVeryVery_anotherMethodWithVeryVery_methodWithVeryVeryLongNam_param",6), + ("oneLastMethodWithVeryVeryLongNameAlmostLikeAGermanWordOrAFrenchSentence_param",7), + ("oneLastMethodWithVeryVeryLongNameAlmostLikeAGermanWordOrAFrenchS_yetAnotherMethodWithVeryVeryLongNameAlmostLikeAGermanWordOrAFren_param",8), + ("oneLastMethodWithVeryVeryLongNameAlmostLik_yetAnotherMethodWithVeryVeryLongNameAlmost_oneMoreMethodWithVeryVeryLongNameAlmostLik_param",9), + ("oneLastMethodWithVeryVeryLongNam_yetAnotherMethodWithVeryVeryLong_oneMoreMethodWithVeryVeryLongNam_anotherMethodWithVeryVeryLongNam_param",10), + ("oneLastMethodWithVeryVery_yetAnotherMethodWithVeryV_oneMoreMethodWithVeryVery_anotherMethodWithVeryVery_methodWithVeryVeryLongNam_param",11))) + } + + @Test + def sd259(): Unit = { + // - trait methods are not inlined into their static super accessors, and also not into mixin forwarders. + // - inlining an invocation of a mixin forwarder also inlines the static accessor and the trait method body. + val code = + """trait T { + | def m1a = 1 + | final def m1b = 1 + | + | @inline def m2a = 2 + | @inline final def m2b = 2 + | + | def m3a(f: Int => Int) = f(1) + | final def m3b(f: Int => Int) = f(1) + |} + |final class A extends T + |class C { + | def t1(t: T) = t.m1a + | def t2(t: T) = t.m1b + | def t3(t: T) = t.m2a + | def t4(t: T) = t.m2b + | def t5(t: T) = t.m3a(x => x) + | def t6(t: T) = t.m3b(x => x) + | + | def t7(a: A) = a.m1a + | def t8(a: A) = a.m1b + | def t9(a: A) = a.m2a + | def t10(a: A) = a.m2b + | def t11(a: A) = a.m3a(x => x) + | def t12(a: A) = a.m3b(x => x) + |} + """.stripMargin + val warn = "T::m2a()I is annotated @inline but could not be inlined:\nThe method is not final and may be overridden." + var count = 0 + val List(a, c, t) = compile(code, allowMessage = i => {count += 1; i.msg contains warn}) + assert(count == 1) + + assertInvoke(getMethod(t, "m1a$"), "T", "m1a") + assertInvoke(getMethod(t, "m1b$"), "T", "m1b") + assertInvoke(getMethod(t, "m2a$"), "T", "m2a") + assertInvoke(getMethod(t, "m2b$"), "T", "m2b") + assertInvoke(getMethod(t, "m3a$"), "T", "m3a") + assertInvoke(getMethod(t, "m3b$"), "T", "m3b") + + assertInvoke(getMethod(a, "m1a"), "T", "m1a$") + assertInvoke(getMethod(a, "m1b"), "T", "m1b$") + assertInvoke(getMethod(a, "m2a"), "T", "m2a$") + assertInvoke(getMethod(a, "m2b"), "T", "m2b$") + assertInvoke(getMethod(a, "m3a"), "T", "m3a$") + assertInvoke(getMethod(a, "m3b"), "T", "m3b$") + + assertInvoke(getMethod(c, "t1"), "T", "m1a") + assertInvoke(getMethod(c, "t2"), "T", "m1b") + + assertInvoke(getMethod(c, "t3"), "T", "m2a") // could not inline + assertNoInvoke(getMethod(c, "t4")) + + assertInvoke(getMethod(c, "t5"), "T", "m3a") // could not inline + assertInvoke(getMethod(c, "t6"), "C", "$anonfun$t6$1") // both forwarders inlined, closure eliminated + + assertInvoke(getMethod(c, "t7"), "A", "m1a") + assertInvoke(getMethod(c, "t8"), "A", "m1b") + + assertNoInvoke(getMethod(c, "t9")) + assertNoInvoke(getMethod(c, "t10")) + + assertInvoke(getMethod(c, "t11"), "C", "$anonfun$t11$1") // both forwarders inlined, closure eliminated + assertInvoke(getMethod(c, "t12"), "C", "$anonfun$t12$1") // both forwarders inlined, closure eliminated + } + + @Test + def sd259b(): Unit = { + val code = + """trait T { + | def get = 1 + | @inline final def m = try { get } catch { case _: Throwable => 1 } + |} + |class A extends T + |class C { + | def t(a: A) = 1 + a.m // cannot inline a try block onto a non-empty stack + |} + """.stripMargin + val warn = + """T::m()I is annotated @inline but could not be inlined: + |The operand stack at the callsite in C::t(LA;)I contains more values than the + |arguments expected by the callee T::m()I. These values would be discarded + |when entering an exception handler declared in the inlined method.""".stripMargin + val List(a, c, t) = compile(code, allowMessage = _.msg contains warn) + + // inlinig of m$ is rolled back, because <invokespecial T.m> is not legal in class C. + assertInvoke(getMethod(c, "t"), "T", "m$") + } + + @Test + def sd259c(): Unit = { + val code = + """trait T { + | def bar = 1 + | @inline final def m = { + | def impl = bar // private, non-static method + | impl + | } + |} + |class A extends T + |class C { + | def t(a: A) = a.m + |} + """.stripMargin + val warn = + """T::m()I is annotated @inline but could not be inlined: + |The callee T::m()I contains the instruction INVOKESPECIAL T.impl$1 ()I + |that would cause an IllegalAccessError when inlined into class C.""".stripMargin + val List(a, c, t) = compile(code, allowMessage = _.msg contains warn) + assertInvoke(getMethod(c, "t"), "T", "m$") + } + + @Test + def sd259d(): Unit = { + val code = + """trait T { + | @inline final def m = 1 + |} + |class C extends T { + | def t = super.m // inline call to T.m$ here, we're not in the mixin forwarder C.m + |} + """.stripMargin + val List(c, t) = compileClasses(code) + assertNoInvoke(getMethod(c, "t")) + assertInvoke(getMethod(c, "m"), "T", "m$") + } } |