summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
authorLukas Rytz <lukas.rytz@gmail.com>2015-01-17 15:29:49 +0100
committerLukas Rytz <lukas.rytz@gmail.com>2015-03-11 12:53:34 -0700
commitb34a452c0683d260ffb1644575a0e970559cae87 (patch)
tree62e359baa916e6df709a43adaed4deb3c599083e /test
parentff67161f946c515a3b0a719ce80531fa14a06a8f (diff)
downloadscala-b34a452c0683d260ffb1644575a0e970559cae87.tar.gz
scala-b34a452c0683d260ffb1644575a0e970559cae87.tar.bz2
scala-b34a452c0683d260ffb1644575a0e970559cae87.zip
Tools to perform inlining.
The method Inliner.inline clones the bytecode of a method and copies the new instructions to the callsite with the necessary modifications. See comments in the code. More tests are added in a later commit which integrates the inliner into the backend - tests are easier to write after that.
Diffstat (limited to 'test')
-rw-r--r--test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala193
1 files changed, 193 insertions, 0 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
new file mode 100644
index 0000000000..1aa7bd1391
--- /dev/null
+++ b/test/junit/scala/tools/nsc/backend/jvm/opt/InlinerTest.scala
@@ -0,0 +1,193 @@
+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._
+import scala.tools.asm.tree.analysis._
+import scala.tools.nsc.backend.jvm.opt.BytecodeUtils.BasicAnalyzer
+import scala.tools.testing.AssertUtil._
+
+import CodeGenTools._
+import scala.tools.partest.ASMConverters
+import ASMConverters._
+import AsmUtils._
+
+import scala.collection.convert.decorateAsScala._
+
+@RunWith(classOf[JUnit4])
+class InlinerTest {
+ val compiler = newCompiler(extraArgs = "-Ybackend:GenBCode -Yopt:l:none")
+ import compiler.genBCode.bTypes._
+ def addToRepo(cls: List[ClassNode]): List[ClassNode] = {
+ for (c <- cls) byteCodeRepository.classes(c.name) = (c, ByteCodeRepository.Classfile)
+ cls
+ }
+
+ // inline first invocation of f into g in class C
+ def inlineTest(code: String, mod: ClassNode => Unit = _ => ()): (MethodNode, Option[String]) = {
+ val List(cls) = addToRepo(compileClasses(compiler)(code))
+ mod(cls)
+ val clsBType = classBTypeFromParsedClassfile(cls.name)
+
+ 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()
+
+ val analyzer = new BasicAnalyzer(g, clsBType.internalName)
+
+ val r = inliner.inline(
+ fCall,
+ analyzer.frameAt(fCall).getStackSize,
+ g,
+ clsBType,
+ f,
+ clsBType,
+ receiverKnownNotNull = true,
+ keepLineNumbers = true)
+ (g, r)
+ }
+
+ @Test
+ def simpleInlineOK(): Unit = {
+ val code =
+ """class C {
+ | def f = 1
+ | def g = f + f
+ |}
+ """.stripMargin
+
+ val (g, _) = inlineTest(code)
+
+ val gConv = convertMethod(g)
+ assertSameCode(gConv.instructions.dropNonOp,
+ List(
+ VarOp(ALOAD, 0), VarOp(ASTORE, 1), // store this
+ Op(ICONST_1), VarOp(ISTORE, 2), Jump(GOTO, Label(10)), // store return value
+ Label(10), VarOp(ILOAD, 2), // load return value
+ VarOp(ALOAD, 0), Invoke(INVOKEVIRTUAL, "C", "f", "()I", false), Op(IADD), Op(IRETURN)))
+
+ // line numbers are kept, so there's a line 2 (from the inlined f)
+ assert(gConv.instructions exists {
+ case LineNumber(2, _) => true
+ case _ => false
+ }, gConv.instructions.filter(_.isInstanceOf[LineNumber]))
+
+ assert(gConv.localVars.map(_.name).sorted == List("f_this", "this"), gConv.localVars)
+ assert(g.maxStack == 2 && g.maxLocals == 3, s"${g.maxLocals} - ${g.maxStack}")
+ }
+
+ @Test
+ def nothingTypedOK(): Unit = {
+ val code =
+ """class C {
+ | def f: Nothing = ???
+ | def g: Int = { f; 1 }
+ |}
+ """.stripMargin
+
+ // On the bytecode level, methods of type Nothing have return type Nothing$.
+ // This can be treated like any other result object.
+
+ // 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 ???
+
+ assertSameCode(convertMethod(g).instructions.dropNonOp.take(4), expectedInlined)
+
+ localOpt.methodOptimizations(g, "C")
+ assertSameCode(convertMethod(g).instructions.dropNonOp,
+ expectedInlined ++ List(VarOp(ASTORE, 2), VarOp(ALOAD, 2), Op(ATHROW)))
+ }
+
+ @Test
+ def synchronizedNoInline(): Unit = {
+ val code =
+ """class C {
+ | def f: Int = 0
+ | def g: Int = f
+ |}
+ """.stripMargin
+
+ val (_, can) = inlineTest(code, cls => {
+ val f = cls.methods.asScala.find(_.name == "f").get
+ f.access |= ACC_SYNCHRONIZED
+ })
+ assert(can.get contains "synchronized", can)
+ }
+
+ @Test
+ def tryCatchOK(): Unit = {
+ val code =
+ """class C {
+ | def f: Int = try { 1 } catch { case _: Exception => 2 }
+ | def g = f + 1
+ |}
+ """.stripMargin
+ val (_, r) = inlineTest(code)
+ assert(r.isEmpty, r)
+ }
+
+ @Test
+ def tryCatchNoInline(): Unit = {
+ // cannot inline f: there's a value on g's stack. if f throws and enters the handler, all values
+ // on the stack are removed, including the one of g's stack that we still need.
+ val code =
+ """class C {
+ | def f: Int = try { 1 } catch { case _: Exception => 2 }
+ | def g = println(f)
+ |}
+ """.stripMargin
+ val (_, r) = inlineTest(code)
+ assert(r.get contains "operand stack at the callsite", r)
+ }
+
+ @Test
+ def illegalAccessNoInline(): Unit = {
+ val code =
+ """package a {
+ | class C {
+ | private def f: Int = 0
+ | def g: Int = f
+ | }
+ |}
+ |package b {
+ | class D {
+ | def h(c: a.C): Int = c.g + 1
+ | }
+ |}
+ """.stripMargin
+
+ val List(c, d) = addToRepo(compileClasses(compiler)(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 BasicAnalyzer(h, dTp.internalName)
+
+ val r = inliner.inline(
+ gCall,
+ analyzer.frameAt(gCall).getStackSize,
+ h,
+ dTp,
+ g,
+ cTp,
+ receiverKnownNotNull = true,
+ keepLineNumbers = true)
+
+ assert(r.get contains "would cause an IllegalAccessError", r)
+ }
+}