blob: 2e81b4af4ac35392c3a498ac5b8c89d9719f334f [file] [log] [blame]
package org.checkerframework.dataflow.analysis;
/*>>>
import org.checkerframework.checker.nullness.qual.Nullable;
*/
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.PriorityQueue;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
import org.checkerframework.dataflow.cfg.ControlFlowGraph;
import org.checkerframework.dataflow.cfg.UnderlyingAST;
import org.checkerframework.dataflow.cfg.UnderlyingAST.CFGLambda;
import org.checkerframework.dataflow.cfg.UnderlyingAST.CFGMethod;
import org.checkerframework.dataflow.cfg.UnderlyingAST.Kind;
import org.checkerframework.dataflow.cfg.block.Block;
import org.checkerframework.dataflow.cfg.block.ConditionalBlock;
import org.checkerframework.dataflow.cfg.block.ExceptionBlock;
import org.checkerframework.dataflow.cfg.block.RegularBlock;
import org.checkerframework.dataflow.cfg.block.SpecialBlock;
import org.checkerframework.dataflow.cfg.node.AssignmentNode;
import org.checkerframework.dataflow.cfg.node.LocalVariableNode;
import org.checkerframework.dataflow.cfg.node.Node;
import org.checkerframework.dataflow.cfg.node.ReturnNode;
import org.checkerframework.javacutil.ElementUtils;
import org.checkerframework.javacutil.Pair;
/**
* An implementation of an iterative algorithm to solve a org.checkerframework.dataflow problem,
* given a control flow graph and a transfer function.
*
* @author Stefan Heule
* @param <A> the abstract value type to be tracked by the analysis
* @param <S> the store type used in the analysis
* @param <T> the transfer function type that is used to approximated runtime behavior
*/
public class Analysis<
A extends AbstractValue<A>, S extends Store<S>, T extends TransferFunction<A, S>> {
/** Is the analysis currently running? */
protected boolean isRunning = false;
/** The transfer function for regular nodes. */
protected T transferFunction;
/** The control flow graph to perform the analysis on. */
protected ControlFlowGraph cfg;
/** The associated processing environment */
protected final ProcessingEnvironment env;
/** Instance of the types utility. */
protected final Types types;
/** Then stores before every basic block (assumed to be 'no information' if not present). */
protected IdentityHashMap<Block, S> thenStores;
/** Else stores before every basic block (assumed to be 'no information' if not present). */
protected IdentityHashMap<Block, S> elseStores;
/**
* Number of times every block has been analyzed since the last time widening was applied. Null,
* if maxCountBeforeWidening is -1 which implies widening isn't used for this analysis.
*/
protected IdentityHashMap<Block, Integer> blockCount;
/**
* Number of times a block can be analyzed before widening. -1 implies that widening shouldn't
* be used.
*/
protected final int maxCountBeforeWidening;
/**
* The transfer inputs before every basic block (assumed to be 'no information' if not present).
*/
protected IdentityHashMap<Block, TransferInput<A, S>> inputs;
/** The stores after every return statement. */
protected IdentityHashMap<ReturnNode, TransferResult<A, S>> storesAtReturnStatements;
/** The worklist used for the fix-point iteration. */
protected Worklist worklist;
/** Abstract values of nodes. */
protected IdentityHashMap<Node, A> nodeValues;
/** Map from (effectively final) local variable elements to their abstract value. */
public HashMap<Element, A> finalLocalValues;
/**
* The node that is currently handled in the analysis (if it is running). The following
* invariant holds:
*
* <pre>
* !isRunning &rArr; (currentNode == null)
* </pre>
*/
protected Node currentNode;
/**
* The tree that is currently being looked at. The transfer function can set this tree to make
* sure that calls to {@code getValue} will not return information for this given tree.
*/
protected Tree currentTree;
/** The current transfer input when the analysis is running. */
protected TransferInput<A, S> currentInput;
public Tree getCurrentTree() {
return currentTree;
}
public void setCurrentTree(Tree currentTree) {
this.currentTree = currentTree;
}
/**
* Construct an object that can perform a org.checkerframework.dataflow analysis over a control
* flow graph. The transfer function is set later using {@code setTransferFunction}.
*/
public Analysis(ProcessingEnvironment env) {
this(env, null, -1);
}
/**
* Construct an object that can perform a org.checkerframework.dataflow analysis over a control
* flow graph, given a transfer function.
*/
public Analysis(ProcessingEnvironment env, T transfer) {
this(env, transfer, -1);
}
/**
* Construct an object that can perform a org.checkerframework.dataflow analysis over a control
* flow graph, given a transfer function.
*/
public Analysis(ProcessingEnvironment env, T transfer, int maxCountBeforeWidening) {
this.env = env;
types = env.getTypeUtils();
this.transferFunction = transfer;
this.maxCountBeforeWidening = maxCountBeforeWidening;
this.transferFunction = transfer;
}
public void setTransferFunction(T transfer) {
this.transferFunction = transfer;
}
public T getTransferFunction() {
return transferFunction;
}
public Types getTypes() {
return types;
}
public ProcessingEnvironment getEnv() {
return env;
}
/**
* Perform the actual analysis. Should only be called once after the object has been created.
*/
public void performAnalysis(ControlFlowGraph cfg) {
assert isRunning == false;
isRunning = true;
init(cfg);
while (!worklist.isEmpty()) {
Block b = worklist.poll();
switch (b.getType()) {
case REGULAR_BLOCK:
{
RegularBlock rb = (RegularBlock) b;
// apply transfer function to contents
TransferInput<A, S> inputBefore = getInputBefore(rb);
currentInput = inputBefore.copy();
TransferResult<A, S> transferResult = null;
Node lastNode = null;
boolean addToWorklistAgain = false;
for (Node n : rb.getContents()) {
transferResult = callTransferFunction(n, currentInput);
addToWorklistAgain |= updateNodeValues(n, transferResult);
currentInput = new TransferInput<>(n, this, transferResult);
lastNode = n;
}
// loop will run at least one, making transferResult non-null
// propagate store to successors
Block succ = rb.getSuccessor();
assert succ != null
: "regular basic block without non-exceptional successor unexpected";
propagateStoresTo(
succ, lastNode, currentInput, rb.getFlowRule(), addToWorklistAgain);
break;
}
case EXCEPTION_BLOCK:
{
ExceptionBlock eb = (ExceptionBlock) b;
// apply transfer function to content
TransferInput<A, S> inputBefore = getInputBefore(eb);
currentInput = inputBefore.copy();
Node node = eb.getNode();
TransferResult<A, S> transferResult =
callTransferFunction(node, currentInput);
boolean addToWorklistAgain = updateNodeValues(node, transferResult);
// propagate store to successor
Block succ = eb.getSuccessor();
if (succ != null) {
currentInput = new TransferInput<>(node, this, transferResult);
// TODO? Variable wasn't used.
// Store.FlowRule storeFlow = eb.getFlowRule();
propagateStoresTo(
succ, node, currentInput, eb.getFlowRule(), addToWorklistAgain);
}
// propagate store to exceptional successors
for (Entry<TypeMirror, Set<Block>> e :
eb.getExceptionalSuccessors().entrySet()) {
TypeMirror cause = e.getKey();
S exceptionalStore = transferResult.getExceptionalStore(cause);
if (exceptionalStore != null) {
for (Block exceptionSucc : e.getValue()) {
addStoreBefore(
exceptionSucc,
node,
exceptionalStore,
Store.Kind.BOTH,
addToWorklistAgain);
}
} else {
for (Block exceptionSucc : e.getValue()) {
addStoreBefore(
exceptionSucc,
node,
inputBefore.copy().getRegularStore(),
Store.Kind.BOTH,
addToWorklistAgain);
}
}
}
break;
}
case CONDITIONAL_BLOCK:
{
ConditionalBlock cb = (ConditionalBlock) b;
// get store before
TransferInput<A, S> inputBefore = getInputBefore(cb);
TransferInput<A, S> input = inputBefore.copy();
// propagate store to successor
Block thenSucc = cb.getThenSuccessor();
Block elseSucc = cb.getElseSuccessor();
propagateStoresTo(thenSucc, null, input, cb.getThenFlowRule(), false);
propagateStoresTo(elseSucc, null, input, cb.getElseFlowRule(), false);
break;
}
case SPECIAL_BLOCK:
{
// special basic blocks are empty and cannot throw exceptions,
// thus there is no need to perform any analysis.
SpecialBlock sb = (SpecialBlock) b;
Block succ = sb.getSuccessor();
if (succ != null) {
propagateStoresTo(
succ, null, getInputBefore(b), sb.getFlowRule(), false);
}
break;
}
default:
assert false;
break;
}
}
assert isRunning == true;
isRunning = false;
}
/**
* Propagate the stores in currentInput to the successor block, succ, according to the flowRule.
*/
protected void propagateStoresTo(
Block succ,
Node node,
TransferInput<A, S> currentInput,
Store.FlowRule flowRule,
boolean addToWorklistAgain) {
switch (flowRule) {
case EACH_TO_EACH:
if (currentInput.containsTwoStores()) {
addStoreBefore(
succ,
node,
currentInput.getThenStore(),
Store.Kind.THEN,
addToWorklistAgain);
addStoreBefore(
succ,
node,
currentInput.getElseStore(),
Store.Kind.ELSE,
addToWorklistAgain);
} else {
addStoreBefore(
succ,
node,
currentInput.getRegularStore(),
Store.Kind.BOTH,
addToWorklistAgain);
}
break;
case THEN_TO_BOTH:
addStoreBefore(
succ,
node,
currentInput.getThenStore(),
Store.Kind.BOTH,
addToWorklistAgain);
break;
case ELSE_TO_BOTH:
addStoreBefore(
succ,
node,
currentInput.getElseStore(),
Store.Kind.BOTH,
addToWorklistAgain);
break;
case THEN_TO_THEN:
addStoreBefore(
succ,
node,
currentInput.getThenStore(),
Store.Kind.THEN,
addToWorklistAgain);
break;
case ELSE_TO_ELSE:
addStoreBefore(
succ,
node,
currentInput.getElseStore(),
Store.Kind.ELSE,
addToWorklistAgain);
break;
}
}
/**
* Updates the value of node {@code node} to the value of the {@code transferResult}. Returns
* true if the node's value changed, or a store was updated.
*/
protected boolean updateNodeValues(Node node, TransferResult<A, S> transferResult) {
A newVal = transferResult.getResultValue();
boolean nodeValueChanged = false;
if (newVal != null) {
A oldVal = nodeValues.get(node);
nodeValues.put(node, newVal);
nodeValueChanged = !Objects.equals(oldVal, newVal);
}
return nodeValueChanged || transferResult.storeChanged();
}
/**
* Call the transfer function for node {@code node}, and set that node as current node first.
*/
protected TransferResult<A, S> callTransferFunction(Node node, TransferInput<A, S> store) {
if (node.isLValue()) {
// TODO: should the default behavior be to return either a regular
// transfer result or a conditional transfer result (depending on
// store.hasTwoStores()), or is the following correct?
return new RegularTransferResult<A, S>(null, store.getRegularStore());
}
store.node = node;
currentNode = node;
TransferResult<A, S> transferResult = node.accept(transferFunction, store);
currentNode = null;
if (node instanceof ReturnNode) {
// save a copy of the store to later check if some property held at
// a given return statement
storesAtReturnStatements.put((ReturnNode) node, transferResult);
}
if (node instanceof AssignmentNode) {
// store the flow-refined value for effectively final local variables
AssignmentNode assignment = (AssignmentNode) node;
Node lhst = assignment.getTarget();
if (lhst instanceof LocalVariableNode) {
LocalVariableNode lhs = (LocalVariableNode) lhst;
Element elem = lhs.getElement();
if (ElementUtils.isEffectivelyFinal(elem)) {
finalLocalValues.put(elem, transferResult.getResultValue());
}
}
}
return transferResult;
}
/** Initialize the analysis with a new control flow graph. */
protected void init(ControlFlowGraph cfg) {
this.cfg = cfg;
thenStores = new IdentityHashMap<>();
elseStores = new IdentityHashMap<>();
blockCount = maxCountBeforeWidening == -1 ? null : new IdentityHashMap<Block, Integer>();
inputs = new IdentityHashMap<>();
storesAtReturnStatements = new IdentityHashMap<>();
worklist = new Worklist(cfg);
nodeValues = new IdentityHashMap<>();
finalLocalValues = new HashMap<>();
worklist.add(cfg.getEntryBlock());
List<LocalVariableNode> parameters = null;
UnderlyingAST underlyingAST = cfg.getUnderlyingAST();
if (underlyingAST.getKind() == Kind.METHOD) {
MethodTree tree = ((CFGMethod) underlyingAST).getMethod();
parameters = new ArrayList<>();
for (VariableTree p : tree.getParameters()) {
LocalVariableNode var = new LocalVariableNode(p);
parameters.add(var);
// TODO: document that LocalVariableNode has no block that it
// belongs to
}
} else if (underlyingAST.getKind() == Kind.LAMBDA) {
LambdaExpressionTree lambda = ((CFGLambda) underlyingAST).getLambdaTree();
parameters = new ArrayList<>();
for (VariableTree p : lambda.getParameters()) {
LocalVariableNode var = new LocalVariableNode(p);
parameters.add(var);
// TODO: document that LocalVariableNode has no block that it
// belongs to
}
} else {
// nothing to do
}
S initialStore = transferFunction.initialStore(underlyingAST, parameters);
Block entry = cfg.getEntryBlock();
thenStores.put(entry, initialStore);
elseStores.put(entry, initialStore);
inputs.put(entry, new TransferInput<>(null, this, initialStore));
}
/**
* Add a basic block to the worklist. If {@code b} is already present, the method does nothing.
*/
protected void addToWorklist(Block b) {
// TODO: use a more efficient way to check if b is already present
if (!worklist.contains(b)) {
worklist.add(b);
}
}
/**
* Add a store before the basic block {@code b} by merging with the existing stores for that
* location.
*/
protected void addStoreBefore(
Block b, Node node, S s, Store.Kind kind, boolean addBlockToWorklist) {
S thenStore = getStoreBefore(b, Store.Kind.THEN);
S elseStore = getStoreBefore(b, Store.Kind.ELSE);
boolean shouldWiden = false;
if (blockCount != null) {
Integer count = blockCount.get(b);
if (count == null) {
count = 0;
}
shouldWiden = count >= maxCountBeforeWidening;
if (shouldWiden) {
blockCount.put(b, 0);
} else {
blockCount.put(b, count + 1);
}
}
switch (kind) {
case THEN:
{
// Update the then store
S newThenStore = mergeStores(s, thenStore, shouldWiden);
if (!newThenStore.equals(thenStore)) {
thenStores.put(b, newThenStore);
if (elseStore != null) {
inputs.put(b, new TransferInput<>(node, this, newThenStore, elseStore));
addBlockToWorklist = true;
}
}
break;
}
case ELSE:
{
// Update the else store
S newElseStore = mergeStores(s, elseStore, shouldWiden);
if (!newElseStore.equals(elseStore)) {
elseStores.put(b, newElseStore);
if (thenStore != null) {
inputs.put(b, new TransferInput<>(node, this, thenStore, newElseStore));
addBlockToWorklist = true;
}
}
break;
}
case BOTH:
if (thenStore == elseStore) {
// Currently there is only one regular store
S newStore = mergeStores(s, thenStore, shouldWiden);
if (!newStore.equals(thenStore)) {
thenStores.put(b, newStore);
elseStores.put(b, newStore);
inputs.put(b, new TransferInput<>(node, this, newStore));
addBlockToWorklist = true;
}
} else {
boolean storeChanged = false;
S newThenStore = mergeStores(s, thenStore, shouldWiden);
if (!newThenStore.equals(thenStore)) {
thenStores.put(b, newThenStore);
storeChanged = true;
}
S newElseStore = mergeStores(s, elseStore, shouldWiden);
if (!newElseStore.equals(elseStore)) {
elseStores.put(b, newElseStore);
storeChanged = true;
}
if (storeChanged) {
inputs.put(b, new TransferInput<>(node, this, newThenStore, newElseStore));
addBlockToWorklist = true;
}
}
}
if (addBlockToWorklist) {
addToWorklist(b);
}
}
private S mergeStores(S newStore, S previousStore, boolean shouldWiden) {
if (previousStore == null) {
return newStore;
} else if (shouldWiden) {
return newStore.widenedUpperBound(previousStore);
} else {
return newStore.leastUpperBound(previousStore);
}
}
/**
* A worklist is a priority queue of blocks in which the order is given by depth-first ordering
* to place non-loop predecessors ahead of successors.
*/
protected static class Worklist {
/** Map all blocks in the CFG to their depth-first order. */
protected IdentityHashMap<Block, Integer> depthFirstOrder;
/** Comparator to allow priority queue to order blocks by their depth-first order. */
public class DFOComparator implements Comparator<Block> {
@Override
public int compare(Block b1, Block b2) {
return depthFirstOrder.get(b1) - depthFirstOrder.get(b2);
}
}
/** The backing priority queue. */
protected PriorityQueue<Block> queue;
public Worklist(ControlFlowGraph cfg) {
depthFirstOrder = new IdentityHashMap<>();
int count = 1;
for (Block b : cfg.getDepthFirstOrderedBlocks()) {
depthFirstOrder.put(b, count++);
}
queue = new PriorityQueue<Block>(11, new DFOComparator());
}
public boolean isEmpty() {
return queue.isEmpty();
}
public boolean contains(Block block) {
return queue.contains(block);
}
public void add(Block block) {
queue.add(block);
}
public Block poll() {
return queue.poll();
}
@Override
public String toString() {
return "Worklist(" + queue + ")";
}
}
/**
* Read the {@link TransferInput} for a particular basic block (or {@code null} if none exists
* yet).
*/
public /*@Nullable*/ TransferInput<A, S> getInput(Block b) {
return getInputBefore(b);
}
/**
* @return the transfer input corresponding to the location right before the basic block {@code
* b}.
*/
protected /*@Nullable*/ TransferInput<A, S> getInputBefore(Block b) {
return inputs.get(b);
}
/** @return the store corresponding to the location right before the basic block {@code b}. */
protected /*@Nullable*/ S getStoreBefore(Block b, Store.Kind kind) {
switch (kind) {
case THEN:
return readFromStore(thenStores, b);
case ELSE:
return readFromStore(elseStores, b);
default:
assert false;
return null;
}
}
/**
* Read the {@link Store} for a particular basic block from a map of stores (or {@code null} if
* none exists yet).
*/
protected static <S> /*@Nullable*/ S readFromStore(Map<Block, S> stores, Block b) {
return stores.get(b);
}
/** Is the analysis currently running? */
public boolean isRunning() {
return isRunning;
}
/**
* @return the abstract value for {@link Node} {@code n}, or {@code null} if no information is
* available. Note that if the analysis has not finished yet, this value might not represent
* the final value for this node.
*/
public /*@Nullable*/ A getValue(Node n) {
if (isRunning) {
// we do not yet have a org.checkerframework.dataflow fact about the current node
if (currentNode == null
|| currentNode == n
|| (currentTree != null && currentTree == n.getTree())) {
return null;
}
// check that 'n' is a subnode of 'node'. Check immediate operands
// first for efficiency.
assert !n.isLValue() : "Did not expect an lvalue, but got " + n;
if (!(currentNode != n
&& (currentNode.getOperands().contains(n)
|| currentNode.getTransitiveOperands().contains(n)))) {
return null;
}
return nodeValues.get(n);
}
return nodeValues.get(n);
}
/**
* @return the abstract value for {@link Tree} {@code t}, or {@code null} if no information is
* available. Note that if the analysis has not finished yet, this value might not represent
* the final value for this node.
*/
public /*@Nullable*/ A getValue(Tree t) {
// we do not yet have a org.checkerframework.dataflow fact about the current node
if (t == currentTree) {
return null;
}
Node nodeCorrespondingToTree = getNodeForTree(t);
if (nodeCorrespondingToTree == null || nodeCorrespondingToTree.isLValue()) {
return null;
}
return getValue(nodeCorrespondingToTree);
}
/** Get the {@link Node} for a given {@link Tree}. */
public Node getNodeForTree(Tree t) {
return cfg.getNodeCorrespondingToTree(t);
}
/**
* Get the {@link MethodTree} of the current CFG if the argument {@link Tree} maps to a {@link
* Node} in the CFG or null otherwise.
*/
public /*@Nullable*/ MethodTree getContainingMethod(Tree t) {
return cfg.getContainingMethod(t);
}
/**
* Get the {@link ClassTree} of the current CFG if the argument {@link Tree} maps to a {@link
* Node} in the CFG or null otherwise.
*/
public /*@Nullable*/ ClassTree getContainingClass(Tree t) {
return cfg.getContainingClass(t);
}
public List<Pair<ReturnNode, TransferResult<A, S>>> getReturnStatementStores() {
List<Pair<ReturnNode, TransferResult<A, S>>> result = new ArrayList<>();
for (ReturnNode returnNode : cfg.getReturnNodes()) {
TransferResult<A, S> store = storesAtReturnStatements.get(returnNode);
result.add(Pair.of(returnNode, store));
}
return result;
}
public AnalysisResult<A, S> getResult() {
assert !isRunning;
IdentityHashMap<Tree, Node> treeLookup = cfg.getTreeLookup();
return new AnalysisResult<>(nodeValues, inputs, treeLookup, finalLocalValues);
}
/**
* @return the regular exit store, or {@code null}, if there is no such store (because the
* method cannot exit through the regular exit block).
*/
public /*@Nullable*/ S getRegularExitStore() {
SpecialBlock regularExitBlock = cfg.getRegularExitBlock();
if (inputs.containsKey(regularExitBlock)) {
S regularExitStore = inputs.get(regularExitBlock).getRegularStore();
return regularExitStore;
} else {
return null;
}
}
public S getExceptionalExitStore() {
S exceptionalExitStore = inputs.get(cfg.getExceptionalExitBlock()).getRegularStore();
return exceptionalExitStore;
}
}