scala.tools.nsc.backend.opt.InlineExceptionHandlers.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of scala-compiler Show documentation
Show all versions of scala-compiler Show documentation
Compiler for the Scala Programming Language
/* NSC -- new scala compiler
* Copyright 2005-2013 LAMP/EPFL
*/
package scala.tools.nsc
package backend.opt
import scala.util.control.Breaks._
/**
* This optimization phase inlines the exception handlers so that further phases can optimize the code better
*
* {{{
* try {
* ...
* if (condition)
* throw IllegalArgumentException("sth")
* } catch {
* case e: IllegalArgumentException =>
* case e: ... => ...
* }
* }}}
*
* will inline the exception handler code to:
*
* {{{
* try {
* ...
* if (condition)
* // + jump to the end of the catch statement
* } catch {
* case e: IllegalArgumentException =>
* case e: ... => ...
* }
* }}}
*
* Q: How does the inlining work, ICode level?
* A: if a block contains a THROW(A) instruction AND there is a handler that takes A or a superclass of A we do:
* 1. We duplicate the handler code such that we can transform THROW into a JUMP
* 2. We analyze the handler to see what local it expects the exception to be placed in
* 3. We place the exception that is thrown in the correct "local variable" slot and clean up the stack
* 4. We finally JUMP to the duplicate handler
* All the above logic is implemented in InlineExceptionHandlersPhase.apply(bblock: BasicBlock)
*
* Q: Why do we need to duplicate the handler?
* A: An exception might be thrown in a method that we invoke in the function and we cannot see that THROW command
* directly. In order to catch such exceptions, we keep the exception handler in place and duplicate it in order
* to inline its code.
*
* @author Vlad Ureche
*/
abstract class InlineExceptionHandlers extends SubComponent {
import global._
import icodes._
import icodes.opcodes._
val phaseName = "inlineExceptionHandlers"
/** Create a new phase */
override def newPhase(p: Phase) = new InlineExceptionHandlersPhase(p)
/**
* Inlining Exception Handlers
*/
class InlineExceptionHandlersPhase(prev: Phase) extends ICodePhase(prev) {
def name = phaseName
/* This map is used to keep track of duplicated exception handlers
* explanation: for each exception handler basic block, there is a copy of it
* -some exception handler basic blocks might not be duplicated because they have an unknown format => Option[(...)]
* -some exception handler duplicates expect the exception on the stack while others expect it in a local
* => Option[Local]
*/
private val handlerCopies = perRunCaches.newMap[BasicBlock, Option[(Option[Local], BasicBlock)]]
/* This map is the inverse of handlerCopies, used to compute the stack of duplicate blocks */
private val handlerCopiesInverted = perRunCaches.newMap[BasicBlock, (BasicBlock, TypeKind)]
private def handlerLocal(bb: BasicBlock): Option[Local] =
for (v <- handlerCopies get bb ; (local, block) <- v ; l <- local) yield l
/* Type Flow Analysis */
private val tfa: analysis.MethodTFA = new analysis.MethodTFA()
private var tfaCache: Map[Int, tfa.lattice.Elem] = Map.empty
private var analyzedMethod: IMethod = NoIMethod
/* Blocks that need to be analyzed */
private var todoBlocks: List[BasicBlock] = Nil
/* Used only for warnings */
private var currentClass: IClass = null
/** Apply exception handler inlining to a class */
override def apply(c: IClass): Unit =
if (settings.inlineHandlers.value) {
val startTime = System.currentTimeMillis
currentClass = c
debuglog("Starting InlineExceptionHandlers on " + c)
c.methods foreach applyMethod
debuglog("Finished InlineExceptionHandlers on " + c + "... " + (System.currentTimeMillis - startTime) + "ms")
currentClass = null
}
/**
* Apply exception handler inlining to a method
*
* Note: for each exception handling block, we (might) create duplicates. Therefore we iterate until we get to a
* fixed point where all the possible handlers have been inlined.
*
* TODO: Should we have an inlining depth limit? A nested sequence of n try-catch blocks can lead to at most 2n
* inlined blocks, so worst case scenario we double the size of the code
*/
private def applyMethod(method: IMethod): Unit = {
if (method.hasCode) {
// create the list of starting blocks
todoBlocks = global.icodes.linearizer.linearize(method)
while (todoBlocks.nonEmpty) {
val levelBlocks = todoBlocks
todoBlocks = Nil
levelBlocks foreach applyBasicBlock // new blocks will be added to todoBlocks
}
}
// Cleanup the references after we finished the file
handlerCopies.clear()
handlerCopiesInverted.clear()
todoBlocks = Nil
// Type flow analysis cleanup
analyzedMethod = NoIMethod
tfaCache = Map.empty
//TODO: Need a way to clear tfa structures
}
/** Apply exception handler inlining to a basic block */
private def applyBasicBlock(bblock: BasicBlock): Unit = {
/*
* The logic of this entire method:
* - for each basic block, we look at each instruction until we find a THROW instruction
* - once we found a THROW instruction, we decide if it is DECIDABLE which of handler will catch the exception
* (see method findExceptionHandler for more details)
* - if we decided there is a handler that will catch the exception, we need to replace the THROW instruction by
* a set of equivalent instructions:
* * we need to compute the static types of the stack slots
* * we need to clear the stack, everything but the exception instance on top (or in a local variable slot)
* * we need to JUMP to the duplicate exception handler
* - we compute the static types of the stack slots in function getTypesAtInstruction
* - we duplicate the exception handler (and we get back the information of whether the duplicate expects the
* exception instance on top of the stack or in a local variable slot)
* - we compute the necessary code to put the exception in its place, clear the stack and JUMP
* - we change the THROW exception to the new Clear stack + JUMP code
*/
for {
(instr @ THROW(clazz), index) <- bblock.iterator.zipWithIndex
// Decide if any handler fits this exception
// If not, then nothing to do, we cannot determine statically which handler will catch the exception
(handler, caughtException) <- findExceptionHandler(toTypeKind(clazz.tpe), bblock.exceptionSuccessors)
} {
log(" Replacing " + instr + " in " + bblock + " to new handler")
// Solve the stack and drop the element that we already stored, which should be the exception
// needs to be done here to be the first thing before code becomes altered
val typeInfo = getTypesAtInstruction(bblock, index)
// Duplicate exception handler
duplicateExceptionHandlerCache(handler) match {
case None =>
log(" Could not duplicate handler for " + instr + " in " + bblock)
case Some((exceptionLocalOpt, newHandler)) =>
val onStackException = typeInfo.head
val thrownException = toTypeKind(clazz.tpe)
// A couple of sanity checks, to make sure we don't touch code we can't safely handle
val canReplaceHandler = (
typeInfo.nonEmpty
&& (index == bblock.length - 1)
&& (onStackException <:< thrownException)
)
// in other words: what's on the stack MUST conform to what's in the THROW(..)!
if (!canReplaceHandler) {
currentClass.cunit.warning(NoPosition, "Unable to inline the exception handler inside incorrect" +
" block:\n" + bblock.iterator.mkString("\n") + "\nwith stack: " + typeInfo + " just " +
"before instruction index " + index)
}
else {
// Prepare the new code to replace the THROW instruction
val newCode = exceptionLocalOpt match {
// the handler duplicate expects the exception in a local: easy one :)
case Some(local) =>
// in the first cycle we remove the exception Type
STORE_LOCAL(local) +: typeInfo.tail.map(x => DROP(x)) :+ JUMP(newHandler)
// we already have the exception on top of the stack, only need to JUMP
case None if typeInfo.length == 1 =>
JUMP(newHandler) :: Nil
// we have the exception on top of the stack but we have other stuff on the stack
// create a local, load exception, clear the stack and finally store the exception on the stack
case _ =>
val exceptionType = typeInfo.head
// Here we could create a single local for all exceptions of a certain type. TODO: try that.
val localName = currentClass.cunit.freshTermName("exception$")
val localType = exceptionType
val localSymbol = bblock.method.symbol.newValue(localName).setInfo(localType.toType)
val local = new Local(localSymbol, localType, false)
bblock.method.addLocal(local)
// Save the exception, drop the stack and place back the exception
STORE_LOCAL(local) :: typeInfo.tail.map(x => DROP(x)) ::: List(LOAD_LOCAL(local), JUMP(newHandler))
}
// replace THROW by the new code
bblock.replaceInstruction(instr, newCode)
// notify the successors changed for the current block
// notify the predecessors changed for the inlined handler block
bblock.touched = true
newHandler.touched = true
log(" Replaced " + instr + " in " + bblock + " to new handler")
log("OPTIMIZED class " + currentClass + " method " +
bblock.method + " block " + bblock + " newhandler " +
newHandler + ":\n\t\t" + onStackException + " <:< " +
thrownException + " <:< " + caughtException)
}
}
}
}
/**
* Gets the types on the stack at a certain point in the program. Note that we want to analyze the method lazily
* and therefore use the analyzedMethod variable
*/
private def getTypesAtInstruction(bblock: BasicBlock, index: Int): List[TypeKind] = {
// get the stack at the block entry
var typeInfo = getTypesAtBlockEntry(bblock)
// perform tfa to the current instruction
log(" stack at the beginning of block " + bblock + " in function " +
bblock.method + ": " + typeInfo.stack)
for (i <- 0 to (index - 1)) {
typeInfo = tfa.interpret(typeInfo, bblock(i))
log(" stack after interpret: " + typeInfo.stack + " after instruction " +
bblock(i))
}
log(" stack before instruction " + index + " of block " + bblock + " in function " +
bblock.method + ": " + typeInfo.stack)
// return the result
typeInfo.stack.types
}
/**
* Gets the stack at the block entry. Normally the typeFlowAnalysis should be run again, but we know how to compute
* the stack for handler duplicates. For the locals, it's safe to assume the info from the original handler is
* still valid (a more precise analysis can be done, but it's not necessary)
*/
private def getTypesAtBlockEntry(bblock: BasicBlock): tfa.lattice.Elem = {
// lazily perform tfa, because it's expensive
// cache results by block label, as rewriting the code messes up the block's hashCode
if (analyzedMethod eq NoIMethod) {
analyzedMethod = bblock.method
tfa.init(bblock.method)
tfa.run
log(" performed tfa on method: " + bblock.method)
for (block <- bblock.method.blocks.sortBy(_.label))
tfaCache += block.label -> tfa.in(block)
}
log(" getting typeinfo at the beginning of block " + bblock)
tfaCache.getOrElse(bblock.label, {
// this block was not analyzed, but it's a copy of some other block so its stack should be the same
log(" getting typeinfo at the beginning of block " + bblock + " as a copy of " +
handlerCopiesInverted(bblock))
val (origBlock, exception) = handlerCopiesInverted(bblock)
val typeInfo = getTypesAtBlockEntry(origBlock)
val stack =
if (handlerLocal(origBlock).nonEmpty) Nil // empty stack, the handler copy expects an empty stack
else List(exception) // one slot on the stack for the exception
// If we use the mutability property, it crashes the analysis
tfa.lattice.IState(new analysis.VarBinding(typeInfo.vars), new icodes.TypeStack(stack))
})
}
/**
* Finds the first exception handler that matches the current exception
*
* Note the following code:
* {{{
* try {
* throw new IllegalArgumentException("...")
* } catch {
* case e: RuntimeException => log("RuntimeException")
* case i: IllegalArgumentException => log("IllegalArgumentException")
* }
* }}}
*
* will print "RuntimeException" => we need the *first* valid handler
*
* There's a hidden catch here: say we have the following code:
* {{{
* try {
* val exception: Throwable =
* if (scala.util.Random.nextInt % 2 == 0)
* new IllegalArgumentException("even")
* else
* new StackOverflowError("odd")
* throw exception
* } catch {
* case e: IllegalArgumentException =>
* println("Correct, IllegalArgumentException")
* case e: StackOverflowError =>
* println("Correct, StackOverflowException")
* case t: Throwable =>
* println("WROOOONG, not Throwable!")
* }
* }}}
*
* We don't want to select a handler if there's at least one that's more specific!
*/
def findExceptionHandler(thrownException: TypeKind, handlers: List[BasicBlock]): Option[(BasicBlock, TypeKind)] = {
for (handler <- handlers ; LOAD_EXCEPTION(clazz) <- handler take 1) {
val caughtException = toTypeKind(clazz.tpe)
// we'll do inlining here: createdException <:< thrownException <:< caughtException, good!
if (thrownException <:< caughtException)
return Some((handler, caughtException))
// we can't do inlining here, the handling mechanism is more precise than we can reason about
if (caughtException <:< thrownException)
return None
// no result yet, look deeper in the handler stack
}
None
}
/**
* This function takes care of duplicating the basic block code for inlining the handler
*
* Note: This function does not duplicate the same basic block twice. It wil contain a map of the duplicated
* basic blocks
*/
private def duplicateExceptionHandlerCache(handler: BasicBlock) =
handlerCopies.getOrElseUpdate(handler, duplicateExceptionHandler(handler))
/** This function takes care of actual duplication */
private def duplicateExceptionHandler(handler: BasicBlock): Option[(Option[Local], BasicBlock)] = {
log(" duplicating handler block " + handler)
handler take 2 match {
case Seq(LOAD_EXCEPTION(caughtClass), next) =>
val (dropCount, exceptionLocal) = next match {
case STORE_LOCAL(local) => (2, Some(local)) // we drop both LOAD_EXCEPTION and STORE_LOCAL
case _ => (1, None) // we only drop the LOAD_EXCEPTION and expect the exception on the stack
}
val caughtException = toTypeKind(caughtClass.tpe)
// copy the exception handler code once again, dropping the LOAD_EXCEPTION
val copy = handler.code.newBlock
copy.emitOnly((handler.iterator drop dropCount).toSeq: _*)
// extend the handlers of the handler to the copy
for (parentHandler <- handler.method.exh ; if parentHandler covers handler) {
parentHandler.addCoveredBlock(copy)
// notify the parent handler that the successors changed
parentHandler.startBlock.touched = true
}
// notify the successors of the inlined handler might have changed
copy.touched = true
handler.touched = true
log(" duplicated handler block " + handler + " to " + copy)
// announce the duplicate handler
handlerCopiesInverted(copy) = ((handler, caughtException))
todoBlocks ::= copy
Some((exceptionLocal, copy))
case _ =>
currentClass.cunit.warning(NoPosition, "Unable to inline the exception handler due to incorrect format:\n" +
handler.iterator.mkString("\n"))
None
}
}
}
}