/* * Copyright 2008 The Closure Compiler Authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.javascript.jscomp; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.javascript.jscomp.ControlFlowGraph.Branch; import com.google.javascript.jscomp.DataFlowAnalysis.BranchedFlowState; import com.google.javascript.jscomp.DataFlowAnalysis.BranchedForwardDataFlowAnalysis; import com.google.javascript.jscomp.DataFlowAnalysis.FlowState; import com.google.javascript.jscomp.DataFlowAnalysis.MaxIterationsExceededException; import com.google.javascript.jscomp.JoinOp.BinaryJoinOp; import com.google.javascript.jscomp.graph.DiGraph.DiGraphEdge; import com.google.javascript.jscomp.graph.GraphNode; import com.google.javascript.jscomp.graph.LatticeElement; import junit.framework.TestCase; import java.util.Comparator; import java.util.List; import java.util.Map; /** * A test suite with a very small programming language that has two types of * instructions: {@link BranchInstruction} and {@link ArithmeticInstruction}. * Test cases must construct a small program with these instructions and * manually put each instruction in a {@code ControlFlowGraph}. * */ public class DataFlowAnalysisTest extends TestCase { /** * Operations supported by ArithmeticInstruction. */ enum Operation { ADD("+"), SUB("-"), DIV("/"), MUL("*"); private final String stringRep; private Operation(String stringRep) { this.stringRep = stringRep; } @Override public String toString() { return stringRep; } } /** * A simple value. */ abstract static class Value { boolean isNumber() { return this instanceof Number; } boolean isVariable() { return this instanceof Variable; } } /** * A variable. */ static class Variable extends Value { private String name; /** * Constructor. * * @param n Name of the variable. */ Variable(String n) { name = n; } String getName() { return name; } @Override public boolean equals(Object other) { // Use the String's .equals() if (!(other instanceof Variable)) { return false; } return ((Variable) other).name.equals(name); } @Override public int hashCode() { return name.hashCode(); } @Override public String toString() { return this.name; } } /** * A number constant. */ static class Number extends Value { private int value; /** * Constructor * * @param v Value */ Number(int v) { value = v; } int getValue() { return value; } @Override public String toString() { return "" + value; } @Override public int hashCode() { return value; } } /** * An instruction of the dummy program. */ abstract static class Instruction { int order = 0; /** * Check whether this is an arithmetic instruction. * * @return {@code true} if it is an arithmetic instruction. */ boolean isArithmetic() { return this instanceof ArithmeticInstruction; } /** * Check whether this is a branch instruction. * * @return {@code true} if it is a branch instruction. */ boolean isBranch() { return this instanceof BranchInstruction; } } /** * Basic arithmetic instruction that only takes the form of: * *
   * Result = Operand1 operator Operand2
   * 
*/ static class ArithmeticInstruction extends Instruction { private Operation operation; private Value operand1; private Value operand2; private Variable result; /** * Constructor * * @param res Result. * @param op1 First Operand. * @param o Operator. * @param op2 Second Operand. */ ArithmeticInstruction(Variable res, int op1, Operation o, int op2) { this(res, new Number(op1), o, new Number(op2)); } /** * Constructor * * @param res Result. * @param op1 First Operand. * @param o Operator. * @param op2 Second Operand. */ ArithmeticInstruction(Variable res, Value op1, Operation o, int op2) { this(res, op1, o, new Number(op2)); } /** * Constructor * * @param res Result. * @param op1 First Operand. * @param o Operator. * @param op2 Second Operand. */ ArithmeticInstruction(Variable res, int op1, Operation o, Value op2) { this(res, new Number(op1), o, op2); } /** * Constructor * * @param res Result. * @param op1 First Operand. * @param o Operator. * @param op2 Second Operand. */ ArithmeticInstruction(Variable res, Value op1, Operation o, Value op2) { result = res; operand1 = op1; operand2 = op2; operation = o; } Operation getOperator() { return operation; } void setOperator(Operation op) { this.operation = op; } Value getOperand1() { return operand1; } void setOperand1(Value operand1) { this.operand1 = operand1; } Value getOperand2() { return operand2; } void setOperand2(Value operand2) { this.operand2 = operand2; } Variable getResult() { return result; } void setResult(Variable result) { this.result = result; } @Override public String toString() { StringBuilder out = new StringBuilder(); out.append(result); out.append(" = "); out.append(operand1); out.append(operation); out.append(operand2); return out.toString(); } @Override public int hashCode() { return toString().hashCode(); } } public static ArithmeticInstruction newAssignNumberToVariableInstruction(Variable res, int num) { return new ArithmeticInstruction(res, num, Operation.ADD, 0); } public static ArithmeticInstruction newAssignVariableToVariableInstruction(Variable lhs, Variable rhs) { return new ArithmeticInstruction(lhs, rhs, Operation.ADD, 0); } /** * Branch instruction based on a {@link Value} as a condition. */ static class BranchInstruction extends Instruction { private Value condition; BranchInstruction(Value cond) { condition = cond; } Value getCondition() { return condition; } void setCondition(Value condition) { this.condition = condition; } } /** * A lattice to represent constant states. Each variable of the program will * have a lattice defined as: * *
   *        TOP
   *   / / |         \
   *  0  1 2 3 ..... MAX_VALUE
   *  \  \ |         /
   *       BOTTOM
   * 
* * Where BOTTOM represents the variable is not a constant. *

* This class will represent a product lattice of each variable's lattice. The * whole lattice is store in a {@code HashMap}. If variable {@code x} is * defined to be constant 10. The map will contain the value 10 with the * variable {@code x} as key. Otherwise, {@code x} is not a constant. */ private static class ConstPropLatticeElement implements LatticeElement { private final Map constMap; private final boolean isTop; /** * Constructor. * * @param isTop To define if the lattice is top. */ ConstPropLatticeElement(boolean isTop) { this.isTop = isTop; this.constMap = Maps.newHashMap(); } /** * Create a lattice where every variable is defined to be not constant. */ ConstPropLatticeElement() { this(false); } ConstPropLatticeElement(ConstPropLatticeElement other) { this.isTop = other.isTop; this.constMap = Maps.newHashMap(other.constMap); } @Override public String toString() { if (isTop) { return "TOP"; } StringBuilder out = new StringBuilder(); out.append("{"); for (Variable var : constMap.keySet()) { out.append(var); out.append("="); out.append(constMap.get(var)); out.append(" "); } out.append("}"); return out.toString(); } @Override public boolean equals(Object other) { if (other instanceof ConstPropLatticeElement) { ConstPropLatticeElement otherLattice = (ConstPropLatticeElement) other; return (this.isTop == otherLattice.isTop) && this.constMap.equals(otherLattice.constMap); } return false; } } private static class ConstPropJoinOp extends BinaryJoinOp { @Override public ConstPropLatticeElement apply(ConstPropLatticeElement a, ConstPropLatticeElement b) { ConstPropLatticeElement result = new ConstPropLatticeElement(); // By the definition of TOP of the lattice. if (a.isTop) { return new ConstPropLatticeElement(a); } if (b.isTop) { return new ConstPropLatticeElement(b); } // Do the join for each variable's lattice. for (Variable var : a.constMap.keySet()) { if (b.constMap.containsKey(var)) { Integer number = b.constMap.get(var); // The result will contain that variable as a known constant // if both lattice has that variable the same constant. if (a.constMap.get(var).equals(number)) { result.constMap.put(var, number); } } } return result; } } /** * A simple forward constant propagation. */ static class DummyConstPropagation extends DataFlowAnalysis { /** * Constructor. * * @param targetCfg Control Flow Graph. */ DummyConstPropagation(ControlFlowGraph targetCfg) { super(targetCfg, new ConstPropJoinOp()); } @Override boolean isForward() { return true; } @Override ConstPropLatticeElement flowThrough(Instruction node, ConstPropLatticeElement input) { if (node.isBranch()) { return new ConstPropLatticeElement(input); } else { return flowThroughArithmeticInstruction((ArithmeticInstruction) node, input); } } @Override ConstPropLatticeElement createEntryLattice() { return new ConstPropLatticeElement(); } @Override ConstPropLatticeElement createInitialEstimateLattice() { return new ConstPropLatticeElement(true); } } static ConstPropLatticeElement flowThroughArithmeticInstruction( ArithmeticInstruction aInst, ConstPropLatticeElement input) { ConstPropLatticeElement out = new ConstPropLatticeElement(input); // Try to see if left is a number. If it is a variable, it might already // be a constant coming in. Integer leftConst = null; if (aInst.operand1.isNumber()) { leftConst = ((Number) aInst.operand1).value; } else { if (input.constMap.containsKey(aInst.operand1)) { leftConst = input.constMap.get(aInst.operand1); } } // Do the same thing to the right. Integer rightConst = null; if (aInst.operand2.isNumber()) { rightConst = ((Number) aInst.operand2).value; } else { if (input.constMap.containsKey(aInst.operand2)) { rightConst = input.constMap.get(aInst.operand2); } } // If both are known constant we can perform the operation. if (leftConst != null && rightConst != null) { Integer constResult = null; if (aInst.operation == Operation.ADD) { constResult = leftConst.intValue() + rightConst.intValue(); } else if (aInst.operation == Operation.SUB) { constResult = leftConst.intValue() - rightConst.intValue(); } else if (aInst.operation == Operation.MUL) { constResult = leftConst.intValue() * rightConst.intValue(); } else if (aInst.operation == Operation.DIV) { constResult = leftConst.intValue() / rightConst.intValue(); } // Put it in the map. (Possibly replacing the existing constant value) out.constMap.put(aInst.result, constResult); } else { // If we cannot find a constant for it out.constMap.remove(aInst.result); } return out; } public void testSimpleIf() { // if (a) { b = 1; } else { b = 1; } c = b; Variable a = new Variable("a"); Variable b = new Variable("b"); Variable c = new Variable("c"); Instruction inst1 = new BranchInstruction(a); Instruction inst2 = newAssignNumberToVariableInstruction(b, 1); Instruction inst3 = newAssignNumberToVariableInstruction(b, 1); Instruction inst4 = newAssignVariableToVariableInstruction(c, b); ControlFlowGraph cfg = new ControlFlowGraph(inst1, true, true); GraphNode n1 = cfg.createNode(inst1); GraphNode n2 = cfg.createNode(inst2); GraphNode n3 = cfg.createNode(inst3); GraphNode n4 = cfg.createNode(inst4); cfg.connect(inst1, ControlFlowGraph.Branch.ON_FALSE, inst2); cfg.connect(inst1, ControlFlowGraph.Branch.ON_TRUE, inst3); cfg.connect(inst2, ControlFlowGraph.Branch.UNCOND, inst4); cfg.connect(inst3, ControlFlowGraph.Branch.UNCOND, inst4); DummyConstPropagation constProp = new DummyConstPropagation(cfg); constProp.analyze(); // We cannot conclude anything from if (a). verifyInHas(n1, a, null); verifyInHas(n1, b, null); verifyInHas(n1, c, null); verifyOutHas(n1, a, null); verifyOutHas(n1, b, null); verifyOutHas(n1, c, null); // We can conclude b = 1 after the instruction. verifyInHas(n2, a, null); verifyInHas(n2, b, null); verifyInHas(n2, c, null); verifyOutHas(n2, a, null); verifyOutHas(n2, b, 1); verifyOutHas(n2, c, null); // Same as above. verifyInHas(n3, a, null); verifyInHas(n3, b, null); verifyInHas(n3, c, null); verifyOutHas(n3, a, null); verifyOutHas(n3, b, 1); verifyOutHas(n3, c, null); // After the merge we should still have b = 1. verifyInHas(n4, a, null); verifyInHas(n4, b, 1); verifyInHas(n4, c, null); verifyOutHas(n4, a, null); // After the instruction both b and c are 1. verifyOutHas(n4, b, 1); verifyOutHas(n4, c, 1); } public void testSimpleLoop() { // a = 0; do { a = a + 1 } while (b); c = a; Variable a = new Variable("a"); Variable b = new Variable("b"); Variable c = new Variable("c"); Instruction inst1 = newAssignNumberToVariableInstruction(a, 0); Instruction inst2 = new ArithmeticInstruction(a, a, Operation.ADD, 1); Instruction inst3 = new BranchInstruction(b); Instruction inst4 = newAssignVariableToVariableInstruction(c, a); ControlFlowGraph cfg = new ControlFlowGraph(inst1, true, true); GraphNode n1 = cfg.createNode(inst1); GraphNode n2 = cfg.createNode(inst2); GraphNode n3 = cfg.createNode(inst3); GraphNode n4 = cfg.createNode(inst4); cfg.connect(inst1, ControlFlowGraph.Branch.UNCOND, inst2); cfg.connect(inst2, ControlFlowGraph.Branch.UNCOND, inst3); cfg.connect(inst3, ControlFlowGraph.Branch.ON_TRUE, inst2); cfg.connect(inst3, ControlFlowGraph.Branch.ON_FALSE, inst4); DummyConstPropagation constProp = new DummyConstPropagation(cfg); // This will also show that the framework terminates properly. constProp.analyze(); // a = 0 is the only thing we know. verifyInHas(n1, a, null); verifyInHas(n1, b, null); verifyInHas(n1, c, null); verifyOutHas(n1, a, 0); verifyOutHas(n1, b, null); verifyOutHas(n1, c, null); // Nothing is provable in this program, so confirm that we haven't // erroneously "proven" something. verifyInHas(n2, a, null); verifyInHas(n2, b, null); verifyInHas(n2, c, null); verifyOutHas(n2, a, null); verifyOutHas(n2, b, null); verifyOutHas(n2, c, null); verifyInHas(n3, a, null); verifyInHas(n3, b, null); verifyInHas(n3, c, null); verifyOutHas(n3, a, null); verifyOutHas(n3, b, null); verifyOutHas(n3, c, null); verifyInHas(n4, a, null); verifyInHas(n4, b, null); verifyInHas(n4, c, null); verifyOutHas(n4, a, null); verifyOutHas(n4, b, null); verifyOutHas(n4, c, null); } public void testLatticeArrayMinimizationWhenMidpointIsEven() { assertEquals(6, JoinOp.BinaryJoinOp.computeMidPoint(12)); } public void testLatticeArrayMinimizationWhenMidpointRoundsDown() { assertEquals(8, JoinOp.BinaryJoinOp.computeMidPoint(18)); } public void testLatticeArrayMinimizationWithTwoElements() { assertEquals(1, JoinOp.BinaryJoinOp.computeMidPoint(2)); } /** * A simple forward constant propagation. */ static class BranchedDummyConstPropagation extends BranchedForwardDataFlowAnalysis { BranchedDummyConstPropagation(ControlFlowGraph targetCfg) { super(targetCfg, new ConstPropJoinOp()); } @Override ConstPropLatticeElement flowThrough(Instruction node, ConstPropLatticeElement input) { if (node.isArithmetic()) { return flowThroughArithmeticInstruction( (ArithmeticInstruction) node, input); } else { return new ConstPropLatticeElement(input); } } @Override List branchedFlowThrough(Instruction node, ConstPropLatticeElement input) { List result = Lists.newArrayList(); List> outEdges = getCfg().getOutEdges(node); if (node.isArithmetic()) { assertTrue(outEdges.size() < 2); ConstPropLatticeElement aResult = flowThroughArithmeticInstruction( (ArithmeticInstruction) node, input); for (DiGraphEdge _ : outEdges) { result.add(aResult); } } else { BranchInstruction branchInst = (BranchInstruction) node; for (DiGraphEdge branch : outEdges) { ConstPropLatticeElement edgeResult = new ConstPropLatticeElement(input); if (branch.getValue() == Branch.ON_FALSE && branchInst.getCondition().isVariable()) { edgeResult.constMap.put((Variable) branchInst.getCondition(), 0); } result.add(edgeResult); } } return result; } @Override ConstPropLatticeElement createEntryLattice() { return new ConstPropLatticeElement(); } @Override ConstPropLatticeElement createInitialEstimateLattice() { return new ConstPropLatticeElement(true); } } public void testBranchedSimpleIf() { // if (a) { a = 0; } else { b = 0; } c = b; Variable a = new Variable("a"); Variable b = new Variable("b"); Variable c = new Variable("c"); Instruction inst1 = new BranchInstruction(a); Instruction inst2 = newAssignNumberToVariableInstruction(a, 0); Instruction inst3 = newAssignNumberToVariableInstruction(b, 0); Instruction inst4 = newAssignVariableToVariableInstruction(c, b); ControlFlowGraph cfg = new ControlFlowGraph(inst1, true, true); GraphNode n1 = cfg.createNode(inst1); GraphNode n2 = cfg.createNode(inst2); GraphNode n3 = cfg.createNode(inst3); GraphNode n4 = cfg.createNode(inst4); cfg.connect(inst1, ControlFlowGraph.Branch.ON_TRUE, inst2); cfg.connect(inst1, ControlFlowGraph.Branch.ON_FALSE, inst3); cfg.connect(inst2, ControlFlowGraph.Branch.UNCOND, inst4); cfg.connect(inst3, ControlFlowGraph.Branch.UNCOND, inst4); BranchedDummyConstPropagation constProp = new BranchedDummyConstPropagation(cfg); constProp.analyze(); // We cannot conclude anything from if (a). verifyBranchedInHas(n1, a, null); verifyBranchedInHas(n1, b, null); verifyBranchedInHas(n1, c, null); // Nothing is known on the true branch. verifyBranchedInHas(n2, a, null); verifyBranchedInHas(n2, b, null); verifyBranchedInHas(n2, c, null); // Verify that we have a = 0 on the false branch. verifyBranchedInHas(n3, a, 0); verifyBranchedInHas(n3, b, null); verifyBranchedInHas(n3, c, null); // After the merge we should still have a = 0. verifyBranchedInHas(n4, a, 0); } public void testMaxIterationsExceededException() { final int MAX_STEP = 10; Variable a = new Variable("a"); Instruction inst1 = new ArithmeticInstruction(a, a, Operation.ADD, a); ControlFlowGraph cfg = new ControlFlowGraph(inst1, true, true) { @Override public Comparator> getOptionalNodeComparator(boolean isForward) { return new Comparator>() { @Override public int compare(DiGraphNode o1, DiGraphNode o2) { return o1.getValue().order - o2.getValue().order; } }; } }; cfg.createNode(inst1); // We have MAX_STEP + 1 nodes, it is impossible to finish the analysis with // MAX_STEP number of steps. for (int i = 0; i < MAX_STEP + 1; i++) { Instruction inst2 = new ArithmeticInstruction(a, a, Operation.ADD, a); cfg.createNode(inst2); inst2.order = i + 1; cfg.connect(inst1, ControlFlowGraph.Branch.UNCOND, inst2); inst1 = inst2; } DummyConstPropagation constProp = new DummyConstPropagation(cfg); try { constProp.analyze(MAX_STEP); fail("Expected MaxIterationsExceededException to be thrown."); } catch (MaxIterationsExceededException e) { assertEquals(e.getMessage(), "Analysis did not terminate after " + MAX_STEP + " iterations"); } } static void verifyInHas(GraphNode node, Variable var, Integer constant) { FlowState fState = node.getAnnotation(); assertEquals(constant, fState.getIn().constMap.get(var)); } static void verifyOutHas(GraphNode node, Variable var, Integer constant) { FlowState fState = node.getAnnotation(); assertEquals(constant, fState.getOut().constMap.get(var)); } static void verifyBranchedInHas(GraphNode node, Variable var, Integer constant) { BranchedFlowState fState = node.getAnnotation(); assertEquals(constant, fState.getIn().constMap.get(var)); } }