
org.opalj.br.cfg.CFG.scala Maven / Gradle / Ivy
The newest version!
/* BSD 2-Clause License - see OPAL/LICENSE for details. */
package org.opalj
package br
package cfg
import scala.reflect.ClassTag
import java.util.Arrays
import scala.collection.{Set => SomeSet}
import scala.collection.AbstractIterator
import org.opalj.log.LogContext
import org.opalj.log.GlobalLogContext
import org.opalj.log.OPALLogger.info
import org.opalj.collection.immutable.IntTrieSet
import org.opalj.collection.immutable.IntTrieSet1
import org.opalj.collection.mutable.FixedSizedHashIDMap
import org.opalj.collection.mutable.IntArrayStack
import org.opalj.graphs.DefaultMutableNode
import org.opalj.graphs.DominatorTree
import org.opalj.graphs.Node
/**
* Represents the control flow graph of a method.
*
* To compute a `CFG` use the [[CFGFactory]].
*
* ==Thread-Safety==
* This class is thread-safe; all data is effectively immutable '''after construction''' time.
*
* @param code The code for which the CFG was build.
* @param normalReturnNode The unique exit node of the control flow graph if the
* method returns normally. If the method always throws an exception, this
* node will not have any predecessors.
* @param abnormalReturnNode The unique exit node of the control flow graph if the
* method returns abnormally (throws an exception). If the method is guaranteed
* to never throw an exception, this node will not have any predecessors.
* @param catchNodes List of all catch nodes. (Usually, we have one [[CatchNode]] per
* [[org.opalj.br.ExceptionHandler]], but if an exception handler does not catch
* anything, no [[CatchNode]] is created.)
* @param basicBlocks An implicit map between a program counter and its associated
* [[BasicBlock]]; it may be a sparse array!
*
* @author Erich Wittenbeck
* @author Michael Eichberg
*/
case class CFG[I <: AnyRef, C <: CodeSequence[I]](
code: C,
normalReturnNode: ExitNode,
abnormalReturnNode: ExitNode,
catchNodes: Seq[CatchNode],
private val basicBlocks: Array[BasicBlock]
) { cfg =>
if (CFG.Validate) {
val allBBs = basicBlocks.filter(_ != null)
val allBBsSet = allBBs.toSet
// 1. Check that each basic block has a lower start pc than the end pc
// i.e., startPC <= endPC.
check(
allBBs.forall(bb => bb.startPC <= bb.endPC),
allBBs.filter(bb => bb.startPC > bb.endPC).mkString
)
// 2. Check that each pc belonging to a basic block (bb) actually points to the respective bb
// i.e., pc in basicBlock : bb => basicBlock(pc) == bb.
check(
allBBsSet.forall { bb =>
(bb.startPC to bb.endPC).forall { pc =>
(basicBlocks(pc) eq null) || (basicBlocks(pc) eq bb)
}
},
basicBlocks.zipWithIndex.filter(_._1 != null).
map(bb => s"${bb._2}:${bb._1}#${System.identityHashCode(bb._1).toHexString}").
mkString("basic blocks mapping broken:\n\t", ",\n\t", "\n")
)
// 3. Check that the CFG is self-consistent; i.e., that no node references a node
// that does not occur in the BB.
check(
allBBsSet.forall { bb =>
bb.successors.forall { successorBB =>
(successorBB.isBasicBlock && {
val succBB = successorBB.asBasicBlock
(basicBlocks(succBB.startPC) eq succBB) && (basicBlocks(succBB.endPC) eq succBB)
}) ||
(successorBB.isCatchNode && catchNodes.contains(successorBB.asCatchNode)) ||
successorBB.isExitNode
}
},
allBBs.
map(bb => bb.toString+" => "+bb.successors.mkString(", ")).
mkString("unexpected successors:\n\t", "\n\t", "")
)
check(
allBBsSet.forall { bb =>
bb.predecessors.forall { predecessorBB =>
(
predecessorBB.isBasicBlock && {
val predBB = predecessorBB.asBasicBlock
(basicBlocks(predBB.startPC) eq predBB) && (basicBlocks(predBB.endPC) eq predBB)
}
) ||
(predecessorBB.isCatchNode && catchNodes.contains(predecessorBB.asCatchNode))
}
},
basicBlocks.zipWithIndex.filter(_._1 != null).map(_.swap).
map(bb => s"${bb._1}:${bb._2.toString} predecessors: ${bb._2.predecessors.mkString(", ")}").
mkString("unexpected predecessors:\n\t", "\n\t", s"\ncode:$code")
)
// 4. Check that all catch nodes referred to by the basic blocks are listed in the
// sequence of catch nodes
check(
allBBs.
filter(bb => bb.successors.exists { _.isCatchNode }).
flatMap(bb => bb.successors.collect { case cn: CatchNode => cn }).
forall(catchBB => catchNodes.contains(catchBB)),
catchNodes.mkString("the set of catch nodes {", ", ", "} is incomplete:\n") +
allBBs.collect {
case bb if bb.successors.exists(succBB => succBB.isCatchNode && !catchNodes.contains(succBB)) =>
s"$bb => ${bb.successors.collect { case cn: CatchNode => cn }.mkString(", ")}"
}.mkString("\n")
)
// 5. Check that predecessors and successors are consistent.
check(
allBBsSet.
forall(bb => bb.successors.forall { succBB => succBB.predecessors.contains(bb) }),
"successors and predecessors are inconsistent; e.g., "+
allBBsSet.
find(bb => !bb.successors.forall { succBB => succBB.predecessors.contains(bb) }).
map(bb => bb.successors.find(succBB => !succBB.predecessors.contains(bb)).map(succBB =>
s"$succBB is a successor of $bb, but does not list it as a predecessor").get).get
)
check(
allBBsSet.
forall(bb => bb.predecessors.forall { predBB => predBB.successors.contains(bb) }),
"predecessors and successors are inconsistent; e.g., "+
allBBsSet.
find(bb => !bb.predecessors.forall { predBB => predBB.successors.contains(bb) }).
map(bb => bb.predecessors.find(predBB => !predBB.successors.contains(bb)).map(predBB =>
s"predBB is a predecessor of $bb, but does not list it as a successor").get).get
)
}
/**
* Computes the maximum fixed point solution for forward data-flow analyses.
*
* @param seed The initial facts associated with the first instruction (pc = 0).
*
* @param t The transfer function which implements the analysis:
* `(Facts, I, PC, CFG.SuccessorId) => Facts`.
* The parameters are: 1. the current set of facts, 2. the current instruction,
* 3. the program counter of the current instruction and 4. the id of the successor.
*
* @param join The operation (typically a set intersection or set union) that is
* executed to join the results of the predecessors of a specific instruction.
* '''It is required that join returns the left (first) set as is if the set of facts
* didn't change.'''
* I.e., even if the left and right sets contain the same values and are
* `equal` (`==`) it is necessary to return the left set.
*/
final def performForwardDataFlowAnalysis[Facts >: Null <: AnyRef: ClassTag](
seed: Facts,
t: (Facts, I, PC, CFG.SuccessorId) => Facts,
join: (Facts, Facts) => Facts
): (Array[Facts], /*normal return*/ Facts, /*abnormal return*/ Facts) = {
implicit val logContext: LogContext = GlobalLogContext
val instructions = code.instructions
val codeSize = instructions.length
val entryFacts = new Array[Facts](codeSize) // the facts before instruction evaluation
entryFacts(0) = seed
var normalReturnFacts: Facts = null
var abnormalReturnFacts: Facts = null
val workList = new IntArrayStack(Math.min(codeSize, 10))
workList.push(0)
while (workList.nonEmpty) {
val pc = workList.pop()
val instruction = instructions(pc)
val facts = entryFacts(pc)
foreachLogicalSuccessor(pc) {
case CFG.NormalReturnId =>
val newFactsNoException = t(facts, instruction, pc, CFG.NormalReturnId)
normalReturnFacts =
if (normalReturnFacts == null) {
newFactsNoException
} else {
join(normalReturnFacts, newFactsNoException)
}
case CFG.AbnormalReturnId =>
val newFactsException = t(facts, instruction, pc, CFG.AbnormalReturnId)
abnormalReturnFacts =
if (abnormalReturnFacts == null) {
newFactsException
} else {
join(abnormalReturnFacts, newFactsException)
}
case succId =>
val newFacts = t(facts, instruction, pc, succId)
val effectiveSuccPC = if (succId < 0) -succId else succId
val succPCFacts = entryFacts(effectiveSuccPC)
if (succPCFacts == null) {
if (CFG.TraceDFSolver) {
info("progress - df solver", s"[initial] $pc -> $succId: $newFacts")
}
entryFacts(effectiveSuccPC) = newFacts
workList += effectiveSuccPC
} else {
val newSuccPCFacts = join(succPCFacts, newFacts)
if (newSuccPCFacts ne succPCFacts) {
if (CFG.TraceDFSolver) {
info("progress - df solver", s"[update] $pc -> $succId: $succPCFacts -> $newSuccPCFacts")
}
entryFacts(effectiveSuccPC) = newSuccPCFacts
workList += effectiveSuccPC
} else {
if (CFG.TraceDFSolver) {
info("progress - df solver", s"[no update] $pc -> $succId: $succPCFacts -> $newSuccPCFacts")
}
}
}
}
}
(entryFacts, normalReturnFacts, abnormalReturnFacts)
}
/**
* Computes the maximum fixed point solution for backward data-flow analyses.
*
* @param seed The initial facts associated with instructions which lead to (ab)normal
* returns.
*
* @param t The transfer function which implements the analysis:
* `(Facts, I, PC, CFG.PredecessorId) => Facts`.
* The parameters are: 1. the current set of facts, 2. the current instruction,
* 3. the program counter of the current instruction and 4. the id of the predecessor.
*
* @param join The operation (typically a set intersection or set union) that is
* executed to join the results of the successors of a specific instruction.
* '''It is required that join returns the left (first) set as is if the set of facts
* didn't change.'''
* I.e., even if the left and right sets contain the same values and are
* `equal` (`==`) it is necessary to return the left set.
*
* @note No facts will derived for stmts that are not reachable from an
* exit node; e.g., due to an infinite loop.
* That is, the returned array may contain `null` values and in an
* extreme case will only contain null values!
*/
final def performBackwardDataFlowAnalysis[Facts >: Null <: AnyRef: ClassTag](
seed: Facts,
t: (Facts, I, PC, CFG.PredecessorId) => Facts,
join: (Facts, Facts) => Facts
): (Array[Facts], /*init*/ Facts) = {
implicit val logContext: LogContext = GlobalLogContext
val instructions = code.instructions
val codeSize = instructions.length
val exitFacts = new Array[Facts](codeSize) // stores the facts after instruction evaluation
val workList = new IntArrayStack(Math.min(codeSize, 10))
normalReturnNode.predecessors.foreach { predBB =>
val returnPC = predBB.asBasicBlock.endPC
exitFacts(returnPC) = seed
workList.push(returnPC)
}
abnormalReturnNode.predecessors.foreach { predBB =>
val stmtPC = predBB.asBasicBlock.endPC
exitFacts(stmtPC) = seed
workList.push(stmtPC)
}
var initFacts: Facts = null
def handleTransition(pc: PC, predId: Int): Unit = {
val instruction = instructions(pc)
val facts = exitFacts(pc)
val newFacts = t(facts, instruction, pc, predId)
if (predId >= 0) {
val predPCFacts = exitFacts(predId)
if (predPCFacts == null) {
if (CFG.TraceDFSolver) {
info("progress - df solver", s"[initial] $pc -> $predId: $newFacts")
}
exitFacts(predId) = newFacts
workList += predId
} else {
val newPredPCFacts = join(predPCFacts, newFacts)
if (newPredPCFacts ne predPCFacts) {
if (CFG.TraceDFSolver) {
info(
"progress - df solver",
s"[update] $pc -> $predId: $predPCFacts -> $newPredPCFacts"
)
}
exitFacts(predId) = newPredPCFacts
workList += predId
} else {
if (CFG.TraceDFSolver) {
info(
"progress - df solver",
s"[no update] $pc -> $predId: $predPCFacts -> $newPredPCFacts"
)
}
}
}
} else {
if (initFacts == null) {
if (CFG.TraceDFSolver) {
info("progress - df solver", s"[initial] $pc -> -1: $newFacts")
}
initFacts = newFacts
} else {
val newInitFacts = join(initFacts, newFacts)
if (newInitFacts ne initFacts) {
if (CFG.TraceDFSolver) {
info(
"progress - df solver",
s"[update] $pc -> -1: $initFacts -> $newInitFacts"
)
}
initFacts = newFacts
} else {
if (CFG.TraceDFSolver) {
info(
"progress - df solver",
s"[no update] $pc -> -1: $initFacts -> $newFacts"
)
}
}
}
}
}
while (workList.nonEmpty) {
val pc = workList.pop()
foreachPredecessor(pc) { predPC => handleTransition(pc, predPC) }
if (pc == 0) handleTransition(pc, -1)
}
(exitFacts, initFacts)
}
/**
* The basic block associated with the very first instruction.
*/
final def startBlock: BasicBlock = basicBlocks(0)
/**
* Returns the basic block to which the instruction with the given `pc` belongs.
*
* @param pc A valid pc.
* @return The basic block associated with the given `pc`. If the `pc` is not valid,
* `null` is returned or an index out of bounds exception is thrown.
*/
final def bb(pc: Int): BasicBlock = basicBlocks(pc)
/**
* Returns the set of all reachable [[CFGNode]]s of the control flow graph.
*/
lazy val reachableBBs: SomeSet[CFGNode] = basicBlocks(0).reachable(reflexive = true)
/**
* Iterates over the set of all [[BasicBlock]]s. (I.e., the exit and catch nodes are
* not returned.) Always returns the basic block containing the first instruction first.
*/
def allBBs: Iterator[BasicBlock] = {
new AbstractIterator[BasicBlock] {
private[this] var currentBBPC = 0
def hasNext: Boolean = currentBBPC < basicBlocks.length
def next(): BasicBlock = {
val basicBlocks = cfg.basicBlocks
val current = basicBlocks(currentBBPC)
currentBBPC = current.endPC + 1
// jump to the end and check if the instruction directly following this bb
// actually belongs to a basic block
val maxPC = basicBlocks.length
while (currentBBPC < maxPC && (basicBlocks(currentBBPC) eq null)) {
currentBBPC += 1
}
current
}
}
}
def allNodes: Iterator[CFGNode] = {
allBBs ++ catchNodes.iterator ++ Iterator(normalReturnNode, abnormalReturnNode)
}
/**
* Returns all direct runtime successors of the instruction with the given pc.
*
* If the returned set is empty, then the instruction is either a return instruction or an
* instruction that always causes an exception to be thrown that is not handled by
* a handler of the respective method.
*
* @note If possible, the function `foreachSuccessor` should be used as it does not have
* to create comparatively expensive intermediate data structures.
*
* @param pc A valid pc of an instruction of the code block from which this cfg was derived.
*/
def successors(pc: Int): IntTrieSet = {
val bb = this.bb(pc)
if (bb.endPC > pc) {
// it must be - w.r.t. the code array - the next instruction
IntTrieSet1(code.pcOfNextInstruction(pc))
} else {
// the set of successor can be (at the same time) a RegularBB or an ExitNode
var successorPCs = IntTrieSet.empty
bb.successors foreach {
case bb: BasicBlock => successorPCs += bb.startPC
case cb: CatchNode => successorPCs += cb.handlerPC
case _ =>
}
successorPCs
}
}
/**
* Iterates over the direct successors of the instruction with the given pc and calls the given
* function `f` for each successor. `f` is guaranteed to be called only once for each successor
* instruction. (E.g., relevant in case of a switch where multiple cases are handled in the
* same way.)
*/
def foreachSuccessor(pc: Int)(f: PC => Unit): Unit = {
val bb = this.bb(pc)
if (bb.endPC > pc) {
// it must be - w.r.t. the code array - the next instruction
f(code.pcOfNextInstruction(pc))
} else {
// the set of successors can be (at the same time) a RegularBB or an ExitNode
var visited = IntTrieSet.empty
bb.successors foreach { bb =>
val nextPC =
if (bb.isBasicBlock) bb.asBasicBlock.startPC
else if (bb.isCatchNode) bb.asCatchNode.handlerPC
else -1
if (nextPC != -1 && !visited.contains(nextPC)) {
visited += nextPC
f(nextPC)
}
// else if (bb.isExitNode)... is not relevant
}
}
}
/**
* Iterates over the direct successors of the instruction with the given pc and calls the given
* function `f` for each successor. `f` is guaranteed to be called only once for each successor
* instruction. (E.g., relevant in case of a switch where multiple cases are handled in the
* same way.)
* The value passed to f will either be:
* - the pc of an instruction.
* - the value `CFG.AbnormalReturnId` (`Int.MinValue`) in case the evaluation of the
* instruction with the given pc throws an exception that leads to an abnormal return.
* - the value `CFG.NormalReturnId` (`Int.MaxValue`) in case the evaluation of the
* (return) instruction with the given `pc` leads to a normal return.
* - `-(successorPC)` if the evaluation leads to an exception that is caught and where the
* first instruction of the handler has the given `successorPC`.
*/
def foreachLogicalSuccessor(pc: Int)(f: Int => Unit): Unit = {
val bb = this.bb(pc)
if (bb.endPC > pc) {
// it must be - w.r.t. the code array - the next instruction
f(code.pcOfNextInstruction(pc))
} else {
// The set of successors can be (at the same time) a RegularBB, a CatchBB or an ExitNode
var visited = IntTrieSet.empty
bb.successors foreach {
case bb: BasicBlock =>
val nextPC = bb.startPC
if (!visited.contains(nextPC)) {
visited += nextPC
f(nextPC)
}
case cn: CatchNode =>
val nextPC = cn.handlerPC
if (!visited.contains(nextPC)) {
visited += nextPC
f(-nextPC)
}
case en: ExitNode =>
f(if (en.normalReturn) CFG.NormalReturnId else CFG.AbnormalReturnId)
}
}
}
def predecessors(pc: Int): IntTrieSet = {
if (pc == 0)
return IntTrieSet.empty;
val bb = this.bb(pc)
if (bb.startPC == pc) {
var predecessorPCs = IntTrieSet.empty
bb.predecessors foreach {
case bb: BasicBlock =>
predecessorPCs += bb.endPC
case cn: CatchNode =>
cn.predecessors.foreach { bb =>
predecessorPCs += bb.asBasicBlock.endPC
}
}
predecessorPCs
} else {
IntTrieSet1(code.pcOfPreviousInstruction(pc))
}
}
def foreachPredecessor(pc: Int)(f: Int => Unit): Unit = {
if (pc == 0)
return ;
val bb = this.bb(pc)
if (bb.startPC == pc) {
var visited = IntTrieSet.empty
bb.predecessors foreach { bb =>
if (bb.isBasicBlock) {
f(bb.asBasicBlock.endPC)
} else if (bb.isCatchNode) {
bb.asCatchNode.predecessors foreach { predBB =>
val nextPC = predBB.asBasicBlock.endPC
if (!visited.contains(nextPC)) {
visited += nextPC
f(nextPC)
}
}
}
}
} else {
f(code.pcOfPreviousInstruction(pc))
}
}
/**
* @return Returns the dominator tree of this CFG.
*
* @see [[DominatorTree.apply]]
*/
def dominatorTree: DominatorTree = {
DominatorTree(
0,
basicBlocks.head.predecessors.nonEmpty,
foreachSuccessor,
foreachPredecessor,
basicBlocks.last.endPC
)
}
/**
* Creates a new CFG where the boundaries of the basic blocks are updated given the `pcToIndex`
* mapping. The assumption is made that the indexes are continuous.
* If the first index (i.e., `pcToIndex(0)` is not 0, then a new basic block for the indexes
* in {0,pcToIndex(0)} is created if necessary.
*
* @param lastIndex The index of the last instruction of the underlying (non-empty) code array.
* I.e., if the instruction array contains one instruction then the `lastIndex` has
* to be `0`.
* @param singletonBBsExpander Function called for each basic block which encompasses a single
* instruction to expand the BB to encompass more instructions. This supports the
* case where an instruction was transformed in a way that resulted in multiple
* instructions/statements, but which all belong to the same basic block.
* ''This situation cannot be handled using pcToIndex.''
* This information is used to ensure that if a basic block, which currently just
* encompasses a single instruction, will encompass the new and the old instruction
* afterwards.
* The returned value will be used as the `endIndex.`
* `endIndex = singletonBBsExpander(pcToIndex(pc of singleton bb))`
* Hence, the function is given the mapped index has to return that value if the index
* does not belong to the expanded instruction.
*/
def mapPCsToIndexes[NewI <: AnyRef, NewC <: CodeSequence[NewI]](
newCode: NewC,
pcToIndex: Array[Int /*PC*/ ],
singletonBBsExpander: Int /*PC*/ => Int,
lastIndex: Int
): CFG[NewI, NewC] = {
/*
// [USED FOR DEBUGGING PURPOSES] *********************************************************
println(
basicBlocks.
filter(_ != null).
toSet.
map((bb: BasicBlock) => bb.toString+" => "+bb.successors.mkString(", ")).
mkString("Successors:\n", "\n", "\n")
)
println(
basicBlocks.
filter(_ != null).
toSet.
map((bb: BasicBlock) => bb.predecessors.mkString(", ")+" => "+bb.toString).
mkString("Predecessors:\n", "\n", "\n")
)
println(catchNodes.mkString("CatchNodes:", ",", "\n"))
println(pcToIndex.zipWithIndex.map(_.swap).mkString("Mapping:", ",", "\n"))
//
// ********************************************************* [USED FOR DEBUGGING PURPOSES]
*/
val bbsLength = basicBlocks.length
// Note, catch node ids have values in the range [-3-number of catch nodes,-3].
// Furthermore, we may have "dead" exception handlers and therefore the number
// of catch nodes does not necessarily reflect the smallest catch node id.
val leastCatchNodeId = if (catchNodes.isEmpty) 0 else catchNodes.iterator.map(_.nodeId).min
val bbMapping = FixedSizedHashIDMap[CFGNode, CFGNode](
minValue = Math.min(-2 /*the exit nodes*/ , leastCatchNodeId),
maxValue = code.instructions.length
)
val newBasicBlocks = new Array[BasicBlock](lastIndex + 1)
val newBasicBlocksArray = newBasicBlocks.asInstanceOf[Array[Object]]
val requiresNewStartBlock = pcToIndex(0) > 0
var lastNewBB: BasicBlock = null
if (requiresNewStartBlock) {
val endIndex = pcToIndex(0) - 1
// we have added instructions at the beginning which belong to a new start bb
lastNewBB = new BasicBlock(startPC = 0, _endPC = endIndex)
Arrays.fill(newBasicBlocksArray, 0, endIndex + 1, lastNewBB)
}
var startPC = 0
do {
val oldBB = basicBlocks(startPC)
val startIndex = pcToIndex(startPC)
val endIndex = {
val initialCandidate = pcToIndex(oldBB.endPC)
val endIndexCandidate =
if (initialCandidate == -1) { // There may be dead instructions
pcToIndex(
((oldBB.endPC - 1) to oldBB.startPC by -1).
find(pcToIndex(_) > -0).getOrElse(0)
)
} else initialCandidate
if (startIndex == endIndexCandidate) {
singletonBBsExpander(startIndex)
} else {
endIndexCandidate
}
}
lastNewBB = new BasicBlock(startIndex, endIndex)
bbMapping.put(oldBB, lastNewBB)
Arrays.fill(newBasicBlocksArray, startIndex, endIndex + 1, lastNewBB)
// let's advance startPC to the next instruction which is live (which has a BB)
startPC = oldBB.endPC + 1
var tempBB: BasicBlock = null
while (startPC < bbsLength && {
tempBB = basicBlocks(startPC)
(tempBB eq null) || pcToIndex(tempBB.startPC) < 0
}) {
assert(tempBB ne oldBB)
// This (index < 0) handles the case where the initial CFG was created using
// a simple algorithm that actually resulted in a CFG with detached basic blocks;
// we now kill these basic blocks by jumping over them!
// NOTE: This is indicative of dead code in the bytecode in the first place!
if (tempBB ne null) {
startPC = tempBB.endPC
}
startPC += 1
}
} while (startPC < bbsLength)
if (requiresNewStartBlock) {
val firstBB = newBasicBlocks(0)
val secondBB = newBasicBlocks(pcToIndex(0))
firstBB.addSuccessor(secondBB)
secondBB.addPredecessor(firstBB)
}
// add the catch nodes
val codeSize = code.instructions.length
catchNodes foreach { cn =>
val newCN = cn.copy(
startPC = pcToIndex(cn.startPC),
endPC = if (cn.endPC == codeSize) lastIndex + 1 else pcToIndex(cn.endPC),
handlerPC = pcToIndex(cn.handlerPC)
)
bbMapping.put(cn, newCN)
}
val newNormalReturnNode = new ExitNode(normalReturn = true)
bbMapping.put(normalReturnNode, newNormalReturnNode)
val newAbnormalReturnNode = new ExitNode(normalReturn = false)
bbMapping.put(abnormalReturnNode, newAbnormalReturnNode)
// rewire the graph
bbMapping iterate { (oldBB, newBB) =>
oldBB.successors foreach { oldSuccBB =>
val newSuccBB = bbMapping(oldSuccBB)
assert(newSuccBB ne null, s"no mapping for $oldSuccBB")
newBB.addSuccessor(newSuccBB)
// Instead of iterating over the predecessors, we just iterate over
// the successors; this way, we only include the nodes that are
// live; nodes that; e.g., are attached to the exit node but for
// which there is no path to reach them at all are dropped!
newSuccBB.addPredecessor(newBB)
}
}
val newCatchNodes = catchNodes.map(bbMapping(_).asInstanceOf[CatchNode])
assert(newCatchNodes.forall { _ ne null })
// let's see if we can merge the first two basic blocks
if (requiresNewStartBlock && basicBlocks(0).predecessors.isEmpty) {
val firstBB = newBasicBlocks(0)
val secondBB = firstBB.successors.head.asBasicBlock
val newFirstBB = secondBB.copy(startPC = 0, predecessors = Set.empty)
newFirstBB.successors.foreach(succBB => succBB.updatePredecessor(secondBB, newFirstBB))
Arrays.fill(newBasicBlocksArray, 0, secondBB._endPC + 1 /* (exclusive)*/ , newFirstBB)
}
CFG[NewI, NewC](
newCode, newNormalReturnNode, newAbnormalReturnNode, newCatchNodes, newBasicBlocks
)
}
// ---------------------------------------------------------------------------------------------
//
// Visualization & Debugging
//
// ---------------------------------------------------------------------------------------------
override def toString: String = {
// code: Code,
// normalReturnNode: ExitNode,
// abnormalReturnNode: ExitNode,
// catchNodes: Seq[CatchNode],
// private val basicBlocks: Array[BasicBlock]
val cfgNodes: Set[CFGNode] =
basicBlocks.filter(_ ne null).toSet[CFGNode] ++
catchNodes +
normalReturnNode +
abnormalReturnNode
val bbIds: Map[CFGNode, Int] = cfgNodes.zipWithIndex.toMap
bbIds.map { bbId =>
val (bb, id) = bbId
if (bb.isExitNode) {
s"BB_${id.toHexString}: $bb"
} else {
bb.successors.
map(succBB => "BB_"+bbIds(succBB).toHexString).
mkString(s"BB_${id.toHexString}: $bb -> {", ",", "}")
}
}.toList.sorted.mkString("CFG(\n\t", "\n\t", "\n)")
}
def toDot: String = {
val rootNodes = Set(startBlock) ++ catchNodes
org.opalj.graphs.toDot(rootNodes)
}
/**
* @return The pair:
* `('''Node for the start BB''', '''all Nodes (incl. the node for the start BB)''')`
*/
def toDot(
f: BasicBlock => String,
includeAbnormalReturns: Boolean = true
): (Node, Iterable[Node]) = {
// 1. create a node foreach cfg node
val bbsIterator = allBBs
val startBB = bbsIterator.next()
val startNodeVisualProperties = Map("fillcolor" -> "green", "style" -> "filled", "shape" -> "box")
val startBBNode = new DefaultMutableNode(
f(startBB),
theVisualProperties = startNodeVisualProperties
)
var cfgNodeToGNodes: Map[CFGNode, DefaultMutableNode[String]] = Map(startBB -> startBBNode)
cfgNodeToGNodes ++= bbsIterator.map(bb => (bb, new DefaultMutableNode(f(bb))))
cfgNodeToGNodes ++= catchNodes.map(cn => (cn, new DefaultMutableNode(cn.toString)))
cfgNodeToGNodes += (
abnormalReturnNode -> {
if (includeAbnormalReturns)
new DefaultMutableNode(
"abnormal return", theVisualProperties = abnormalReturnNode.visualProperties
)
else
null
}
)
cfgNodeToGNodes += (
normalReturnNode ->
new DefaultMutableNode(
"return", theVisualProperties = normalReturnNode.visualProperties
)
)
// 2. reconnect nodes
cfgNodeToGNodes foreach { cfgNodeToGNode =>
val (cfgNode, gNode) = cfgNodeToGNode
if (gNode != null)
cfgNode.successors foreach { cfgNode =>
val targetGNode = cfgNodeToGNodes(cfgNode)
if (targetGNode != null) gNode.addChild(targetGNode)
}
}
val nodes = cfgNodeToGNodes.values
(startBBNode, nodes)
}
}
object CFG {
final val NormalReturnId = Int.MaxValue
final val AbnormalReturnId = Int.MinValue
/**
* Identifies the successor of an instruction.
* The id is either:
* - `CFG.NormalReturnId` (`== Int.MaxValue`) if the successor is the unique exit
* node representing normal returns.
* - `CFG.AbnormalReturnId` (`== Int.MinValue`) if the successor is the unique exit
* node representing abnormal returns (i.e., an uncaught exception will be thrown
* by the instruction).
* - [-65535,-1] to identify the catch handler that handles the exception thrown
* by the current instruction. The pc of the first instruction of the catch handler
* is (`- successorId`).
* - the (valid) pc of the next instruction (the normal case.)
*/
final type SuccessorId = Int
/**
* Identifies the predecessor of an instruction.
* - -1 if the current instruction is the first instruction (`pc`/`index` = `0`)
* - a regular pc.
*/
final type PredecessorId = Int
final val ValidateKey = "org.opalj.br.cfg.CFG.Validate"
private[this] var validate: Boolean = {
val initialValidate = BaseConfig.getBoolean(ValidateKey)
updateValidate(initialValidate)
initialValidate
}
// We think of it as a runtime constant (which can be changed for testing purposes).
def Validate: Boolean = validate
def updateValidate(newValidate: Boolean): Unit = {
implicit val logContext: GlobalLogContext.type = GlobalLogContext
validate =
if (newValidate) {
info("OPAL", s"$ValidateKey: validation on")
true
} else {
info("OPAL", s"$ValidateKey: validation off")
false
}
}
final val TraceDFSolverKey = "org.opalj.br.cfg.CFG.DF.Solver.Trace"
final val TraceDFSolver: Boolean = {
val traceDFSolver = BaseConfig.getBoolean(TraceDFSolverKey)
info("OPAL", s"$TraceDFSolverKey: $traceDFSolver")(GlobalLogContext)
traceDFSolver
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy