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 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
def assertEliminateDead(code: (Instruction, Boolean)*): Unit = {
val method = genMethod()(code.map(_._1): _*)
LocalOptImpls.removeUnreachableCodeImpl(method, "C")
val nonEliminated = instructionsFromMethod(method)
val expectedLive = code.filter(_._2).map(_._1).toList
assertSameCode(nonEliminated, expectedLive)
}
@Test
def basicElimination(): Unit = {
assertEliminateDead(
Op(ACONST_NULL),
Op(ATHROW),
Op(RETURN).dead
)
assertEliminateDead(
Op(RETURN)
)
assertEliminateDead(
Op(RETURN),
Op(ACONST_NULL).dead,
Op(ATHROW).dead
)
}
@Test
def eliminateNop(): Unit = {
assertEliminateDead(
// reachable, but removed anyway.
Op(NOP).dead,
Op(RETURN),
Op(NOP).dead
)
}
@Test
def eliminateBranchOver(): Unit = {
assertEliminateDead(
Jump(GOTO, Label(1)),
Op(ACONST_NULL).dead,
Op(ATHROW).dead,
Label(1),
Op(RETURN)
)
assertEliminateDead(
Jump(GOTO, Label(1)),
Label(1),
Op(RETURN)
)
}
@Test
def deadLabelsRemain(): Unit = {
assertEliminateDead(
Op(RETURN),
Jump(GOTO, Label(1)).dead,
// not dead - labels may be referenced from other places in a classfile (eg exceptions table).
// will need a different opt to get rid of them
Label(1)
)
}
@Test
def pushPopNotEliminated(): Unit = {
assertEliminateDead(
// not dead, visited by data flow analysis.
Op(ACONST_NULL),
Op(POP),
Op(RETURN)
)
}
@Test
def nullnessNotConsidered(): Unit = {
assertEliminateDead(
Op(ACONST_NULL),
Jump(IFNULL, Label(1)),
Op(RETURN), // not dead
Label(1),
Op(RETURN)
)
}
@Test
def basicEliminationCompiler(): Unit = {
val code = "def f: Int = { return 1; 2 }"
val withDce = singleMethodInstructions(dceCompiler)(code)
assertSameCode(withDce.dropNonOp, List(Op(ICONST_1), Op(IRETURN)))
val noDce = singleMethodInstructions(noOptCompiler)(code)
// The emitted code is ICONST_1, IRETURN, ICONST_2, IRETURN. The latter two are dead.
//
// GenBCode puts the last IRETURN into a new basic block: it emits a label before the second
// IRETURN. This is an implementation detail, it may change; it affects the outcome of this test.
//
// During classfile writing with COMPUTE_FAMES (-target:jvm-1.6 or larger), the ClassfileWriter
// puts the ICONST_2 into a new basic block, because the preceding operation (IRETURN) ends
// the current block. We get something like
//
// L1: ICONST_1; IRETURN
// L2: ICONST_2 << dead
// L3: IRETURN << dead
//
// 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
def eliminateDeadCatchBlocks(): Unit = {
// the Label(1) is live: it's used in the local variable descriptor table (local variable "this" has a range from 0 to 1).
def wrapInDefault(code: Instruction*) = List(Label(0), LineNumber(1, Label(0))) ::: code.toList ::: List(Label(1))
val code = "def f: Int = { return 0; try { 1 } catch { case _: Exception => 2 } }"
val m = singleMethod(dceCompiler)(code)
assertTrue(m.handlers.isEmpty) // redundant (if code is gone, handler is gone), but done once here for extra safety
assertSameCode(m.instructions,
wrapInDefault(Op(ICONST_0), Op(IRETURN)))
val code2 = "def f: Unit = { try { } catch { case _: Exception => () }; () }"
// requires fixpoint optimization of methodOptCompiler (dce alone is not enough): first the handler is eliminated, then it's dead catch block.
assertSameCode(singleMethodInstructions(methodOptCompiler)(code2), wrapInDefault(Op(RETURN)))
val code3 = "def f: Unit = { try { } catch { case _: Exception => try { } catch { case _: Exception => () } }; () }"
assertSameCode(singleMethodInstructions(methodOptCompiler)(code3), wrapInDefault(Op(RETURN)))
// this example requires two iterations to get rid of the outer handler.
// the first iteration of DCE cannot remove the inner handler. then the inner (empty) handler is removed.
// then the second iteration of DCE removes the inner catch block, and then the outer handler is removed.
val code4 = "def f: Unit = { try { try { } catch { case _: Exception => () } } catch { case _: Exception => () }; () }"
assertSameCode(singleMethodInstructions(methodOptCompiler)(code4), wrapInDefault(Op(RETURN)))
}
@Test // test the dce-testing tools
def metaTest(): Unit = {
assertThrows[AssertionError](
assertEliminateDead(Op(RETURN).dead),
_.contains("Expected: List()\nActual : List(Op(RETURN))")
)
assertThrows[AssertionError](
assertEliminateDead(Op(RETURN), Op(RETURN)),
_.contains("Expected: List(Op(RETURN), Op(RETURN))\nActual : List(Op(RETURN))")
)
}
@Test
def bytecodeEquivalence: Unit = {
assertTrue(List(VarOp(ILOAD, 1)) ===
List(VarOp(ILOAD, 2)))
assertTrue(List(VarOp(ILOAD, 1), VarOp(ISTORE, 1)) ===
List(VarOp(ILOAD, 2), VarOp(ISTORE, 2)))
// the first Op will associate 1->2, then the 2->2 will fail
assertFalse(List(VarOp(ILOAD, 1), VarOp(ISTORE, 2)) ===
List(VarOp(ILOAD, 2), VarOp(ISTORE, 2)))
// will associate 1->2 and 2->1, which is OK
assertTrue(List(VarOp(ILOAD, 1), VarOp(ISTORE, 2)) ===
List(VarOp(ILOAD, 2), VarOp(ISTORE, 1)))
assertTrue(List(Label(1), Label(2), Label(1)) ===
List(Label(2), Label(4), Label(2)))
assertTrue(List(LineNumber(1, Label(1)), Label(1)) ===
List(LineNumber(1, Label(3)), Label(3)))
assertFalse(List(LineNumber(1, Label(1)), Label(1)) ===
List(LineNumber(1, Label(3)), Label(1)))
assertTrue(List(TableSwitch(TABLESWITCH, 1, 3, Label(4), List(Label(5), Label(6))), Label(4), Label(5), Label(6)) ===
List(TableSwitch(TABLESWITCH, 1, 3, Label(9), List(Label(3), Label(4))), Label(9), Label(3), Label(4)))
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)))
}
}