Java tutorial
/* * Copyright 2016 Google Inc. * * 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.template.soy.jbcsrc; import static com.google.common.base.Preconditions.checkState; import com.google.common.base.Equivalence; import com.google.common.base.Equivalence.Wrapper; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.google.template.soy.basetree.CopyState; import com.google.template.soy.data.SoyValueProvider; import com.google.template.soy.exprtree.AbstractExprNodeVisitor; import com.google.template.soy.exprtree.DataAccessNode; import com.google.template.soy.exprtree.ExprEquivalence; import com.google.template.soy.exprtree.ExprNode; import com.google.template.soy.exprtree.ExprNode.OperatorNode; import com.google.template.soy.exprtree.ExprNode.PrimitiveNode; import com.google.template.soy.exprtree.ExprRootNode; import com.google.template.soy.exprtree.FunctionNode; import com.google.template.soy.exprtree.ListLiteralNode; import com.google.template.soy.exprtree.MapLiteralNode; import com.google.template.soy.exprtree.OperatorNodes.AndOpNode; import com.google.template.soy.exprtree.OperatorNodes.ConditionalOpNode; import com.google.template.soy.exprtree.OperatorNodes.NullCoalescingOpNode; import com.google.template.soy.exprtree.OperatorNodes.OrOpNode; import com.google.template.soy.exprtree.VarDefn; import com.google.template.soy.exprtree.VarRefNode; import com.google.template.soy.jbcsrc.api.RenderResult; import com.google.template.soy.msgs.internal.MsgUtils; import com.google.template.soy.msgs.restricted.SoyMsgPart; import com.google.template.soy.msgs.restricted.SoyMsgPart.Case; import com.google.template.soy.msgs.restricted.SoyMsgPlaceholderPart; import com.google.template.soy.msgs.restricted.SoyMsgPluralCaseSpec; import com.google.template.soy.msgs.restricted.SoyMsgPluralPart; import com.google.template.soy.msgs.restricted.SoyMsgPluralRemainderPart; import com.google.template.soy.msgs.restricted.SoyMsgRawTextPart; import com.google.template.soy.msgs.restricted.SoyMsgSelectPart; import com.google.template.soy.shared.internal.BuiltinFunction; import com.google.template.soy.soytree.AbstractSoyNodeVisitor; import com.google.template.soy.soytree.CallNode; import com.google.template.soy.soytree.CallParamContentNode; import com.google.template.soy.soytree.CallParamNode; import com.google.template.soy.soytree.CallParamValueNode; import com.google.template.soy.soytree.CssNode; import com.google.template.soy.soytree.DebuggerNode; import com.google.template.soy.soytree.ExprUnion; import com.google.template.soy.soytree.ForNode; import com.google.template.soy.soytree.ForNode.RangeArgs; import com.google.template.soy.soytree.ForeachIfemptyNode; import com.google.template.soy.soytree.ForeachNode; import com.google.template.soy.soytree.ForeachNonemptyNode; import com.google.template.soy.soytree.IfCondNode; import com.google.template.soy.soytree.IfElseNode; import com.google.template.soy.soytree.IfNode; import com.google.template.soy.soytree.LetContentNode; import com.google.template.soy.soytree.LetValueNode; import com.google.template.soy.soytree.LogNode; import com.google.template.soy.soytree.MsgFallbackGroupNode; import com.google.template.soy.soytree.MsgHtmlTagNode; import com.google.template.soy.soytree.MsgNode; import com.google.template.soy.soytree.MsgPlaceholderNode; import com.google.template.soy.soytree.PrintDirectiveNode; import com.google.template.soy.soytree.PrintNode; import com.google.template.soy.soytree.RawTextNode; import com.google.template.soy.soytree.SoyNode; import com.google.template.soy.soytree.SoyNode.ParentSoyNode; import com.google.template.soy.soytree.SoyNode.StandaloneNode; import com.google.template.soy.soytree.SwitchCaseNode; import com.google.template.soy.soytree.SwitchDefaultNode; import com.google.template.soy.soytree.SwitchNode; import com.google.template.soy.soytree.TemplateNode; import com.google.template.soy.soytree.XidNode; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * A static analyzer for how templates will access variables. * * <p>This class contains a generic method for constructing a control flow graph through a given Soy * template. We then use this graph to answer questions about the template. * * <p>Supported queries * <ul> * <li>{@link #isResolved(DataAccessNode)} can tell us whether or not a particular * variable or field reference has already been referenced at a given point and therefore * {@link SoyValueProvider#status()} has already returned {@link RenderResult#done()}. * </ul> * * <p>TODO(lukes): consider adding the following * <ul> * <li>Identify the last use of a variable. Currently we use variable scopes to decide when to * stop saving/restoring variables, but if we knew they were no longer in use we could save * generating save/restore logic. * <li>Identify the last render of a variable. We could use this to save temporary buffers. * <li>Identify the first use of a variable. We could use this to move {let...} definitions * closer to their uses. * </ul> */ final class TemplateAnalysis { static TemplateAnalysis analyze(TemplateNode node) { AccessGraph templateGraph = new PseudoEvaluatorVisitor().evaluate(node); return new TemplateAnalysis(templateGraph.getResolvedExpressions()); } private final ImmutableSet<ExprNode> resolvedExpressions; private TemplateAnalysis(Set<ExprNode> definitelyAccessedRefs) { this.resolvedExpressions = ImmutableSet.copyOf(definitelyAccessedRefs); } /** * Returns true if this variable reference is definitely not the first reference to the variable * within a given template. */ boolean isResolved(VarRefNode ref) { return resolvedExpressions.contains(ref); } /** * Returns true if this data access is definitely not the first reference to the field or item * within a given template. */ boolean isResolved(DataAccessNode ref) { return resolvedExpressions.contains(ref); } /** * This visitor (and the {@link PseudoEvaluatorExprVisitor}) visit every Soy node in the order * that the code generated from those node would execute and contructs an {@link AccessGraph}. */ private static final class PseudoEvaluatorVisitor extends AbstractSoyNodeVisitor<Void> { final Map<VarDefn, AccessGraph> letNodes = new HashMap<>(); final PseudoEvaluatorExprVisitor exprVisitor = new PseudoEvaluatorExprVisitor(letNodes); Block current; AccessGraph evaluate(TemplateNode node) { Block start = new Block(); Block finalNode = exec(start, node); checkState(finalNode.successors.isEmpty()); // annotate the graph with predecessor links addPredecessors(start); // TODO(lukes): due to the way we build the graph we may have lots of either empty nodes or // nodes with unconditional branches. We could reduce the size of the graph by trying to // eliminate such nodes. This may be worth it if we end up doing many traversals instead of // the current 1. return new AccessGraph(start, finalNode); } /** * Visit the given node and append all accesses to the given block. * * <p>Returns the ending block. */ Block exec(Block block, SoyNode node) { Block original = this.current; this.current = block; visit(node); Block rVal = current; this.current = original; return rVal; } @Override protected void visitPrintNode(PrintNode node) { evalInline(node.getExprUnion()); for (PrintDirectiveNode directive : node.getChildren()) { for (ExprRootNode arg : directive.getArgs()) { evalInline(arg); } } } @Override protected void visitRawTextNode(RawTextNode node) { // do nothing } @Override protected void visitLogNode(LogNode node) { visitChildren(node); } @Override protected void visitDebuggerNode(DebuggerNode node) { // do nothing } @Override protected void visitTemplateNode(TemplateNode node) { visitChildren(node); } @Override protected void visitForeachNode(ForeachNode node) { // the list is always evaluated first. evalInline(node.getExpr()); Block loopBegin = this.current; // create a branch for the loop body Block loopBody = loopBegin.addBranch(); Block loopEnd = exec(loopBody, node.getChild(0)); // If we can statically prove the list empty, use that information. StaticAnalysisResult isLoopEmpty = isListExpressionEmpty(node.getExpr()); if (node.numChildren() == 2) { // there is an {ifempty} block Block ifEmptyBlock = loopBegin.addBranch(); Block ifEmptyEnd = exec(ifEmptyBlock, node.getChild(1)); switch (isLoopEmpty) { case FALSE: this.current = loopEnd.addBranch(); break; case TRUE: this.current = ifEmptyEnd.addBranch(); break; case UNKNOWN: this.current = Block.merge(loopEnd, ifEmptyEnd); break; default: throw new AssertionError(); } } else { switch (isLoopEmpty) { case FALSE: this.current = loopEnd.addBranch(); break; case TRUE: this.current = loopBegin.addBranch(); break; case UNKNOWN: this.current = Block.merge(loopEnd, loopBegin); break; default: throw new AssertionError(); } } } @Override protected void visitForeachIfemptyNode(ForeachIfemptyNode node) { visitChildren(node); } @Override protected void visitForeachNonemptyNode(ForeachNonemptyNode node) { visitChildren(node); } @Override protected void visitForNode(ForNode node) { // The range(...) args of a For node are always evaluated first, in this order. // (see SoyNodeCompiler) RangeArgs rangeArgs = node.getRangeArgs(); if (rangeArgs.start().isPresent()) { evalInline(rangeArgs.start().get()); } if (rangeArgs.increment().isPresent()) { evalInline(rangeArgs.increment().get()); } evalInline(rangeArgs.limit()); Block loopBegin = this.current; // create a branch for the loop body Block loopBody = loopBegin.addBranch(); Block loopEnd = loopBody; for (StandaloneNode child : node.getChildren()) { loopEnd = exec(loopEnd, child); } // If the loop definitely executed we could merge results back up. There are actually a // surprising number of people using constants in their range args. if (rangeArgs.definitelyNotEmpty()) { this.current = loopEnd; } else { this.current = Block.merge(loopBegin, loopEnd); } } @Override protected void visitLetContentNode(LetContentNode node) { // Due to lazy evaluation we don't evaluate these at the declaration site. Instead we have // to fix them up after the fact. So calculate and store their basic blocks here. We will // wire them into the graph later. Block startBlock = new Block(); Block block = startBlock; for (StandaloneNode child : node.getChildren()) { block = exec(block, child); } letNodes.put(node.getVar(), new AccessGraph(startBlock, block)); } @Override protected void visitLetValueNode(LetValueNode node) { // see visitLetContentNode Block start = new Block(); Block end = exprVisitor.eval(start, node.getValueExpr()); letNodes.put(node.getVar(), new AccessGraph(start, end)); } @Override protected void visitSwitchNode(SwitchNode node) { // A few special cases. See SoyNodeCompiler.visitSwitchNode // * no children => don't evaluate the switch expression // * no cases => don't evaluate the switch expression List<SoyNode> children = node.getChildren(); if (children.isEmpty()) { return; } if (children.size() == 1 && children.get(0) instanceof SwitchDefaultNode) { visitChildren((SwitchDefaultNode) children.get(0)); return; } // otherwise we are in the normal case and this is much like an if-elseif-else statement // First eval the expr evalInline(node.getExpr()); // The one special case is that there are allowed to be multiple conditions per case. // For these, only the first one is necessarily evaluated. Block conditions = null; List<Block> branchEnds = new ArrayList<>(); boolean hasDefault = false; for (SoyNode child : node.getChildren()) { if (child instanceof SwitchCaseNode) { SwitchCaseNode scn = (SwitchCaseNode) child; Block caseBlockStart = new Block(); Block caseBlockEnd = exec(caseBlockStart, scn); branchEnds.add(caseBlockEnd); for (ExprUnion expr : scn.getAllExprUnions()) { if (conditions == null) { evalInline(expr); // the very first condition is always evaluated conditions = this.current; } else { // otherwise we are only maybe evaluating this condition Block condition = conditions.addBranch(); conditions = exprVisitor.eval(condition, expr.getExpr()); } conditions.successors.add(caseBlockStart); } } else { SwitchDefaultNode ien = (SwitchDefaultNode) child; Block defaultBlockStart = conditions.addBranch(); Block defaultBlockEnd = exec(defaultBlockStart, ien); branchEnds.add(defaultBlockEnd); hasDefault = true; } } if (!hasDefault) { branchEnds.add(conditions); } this.current = Block.merge(branchEnds); } @Override protected void visitSwitchCaseNode(SwitchCaseNode node) { visitChildren(node); } @Override protected void visitSwitchDefaultNode(SwitchDefaultNode node) { visitChildren(node); } @Override protected void visitIfNode(IfNode node) { // For ifs we always evaluate the first condition and if there is an 'else' clause at least // one of the branches. Things get trickier if we have multiple conditions. for example, // {if $p1}W{elseif $p2}X{else}Y{/if}Z // at position W $p1 has been ref'd // at position X $p1 and $p2 have been ref'd // at position Y $p1 and $p2 have been ref'd // at position Z only $p1 has definitely been ref'd // To handle all these cases we need to manage a bunch of forks Block conditionFork = null; List<Block> branchEnds = new ArrayList<>(); boolean hasElse = false; for (SoyNode child : node.getChildren()) { if (child instanceof IfCondNode) { IfCondNode icn = (IfCondNode) child; ExprRootNode conditionExpression = icn.getExprUnion().getExpr(); if (conditionFork == null) { // first condition is always evaluated evalInline(conditionExpression); conditionFork = this.current; } else { conditionFork = exprVisitor.eval(conditionFork.addBranch(), conditionExpression); } Block branch = conditionFork.addBranch(); branch = exec(branch, icn); branchEnds.add(branch); } else { IfElseNode ien = (IfElseNode) child; Block branch = conditionFork.addBranch(); branch = exec(branch, ien); branchEnds.add(branch); hasElse = true; } } if (!hasElse) { branchEnds.add(conditionFork); } this.current = Block.merge(branchEnds); } @Override protected void visitIfCondNode(IfCondNode node) { visitChildren(node); } @Override protected void visitIfElseNode(IfElseNode node) { visitChildren(node); } @Override protected void visitCssNode(CssNode node) { if (node.getComponentNameExpr() != null) { evalInline(node.getComponentNameExpr()); } } @Override protected void visitXidNode(XidNode node) { // do nothing, xids only contain constants. } @Override protected void visitCallNode(CallNode node) { // If there is a data="<expr>" this is always evaluated first. ExprRootNode dataExpr = node.dataAttribute().dataExpr(); if (dataExpr != null) { evalInline(dataExpr); } // template params are evaluated lazily in the callee, so we don't know whether or not any // of them will be evaluated. So treat each param like an optional branch. // Also we don't know anything about the relative ordering of each param. technically all // orderings are possible, but instead of that we just assume they are all mutually exclusive. // TODO(lukes): if we were modeling 'back-edges' we could treat params like a loop by putting // an optional back edge at the bottom of each one. this would achieve the result of // modelling all possible orderings. For our current usecases this is unnecessary. Block begin = this.current; List<Block> branchEnds = new ArrayList<>(); branchEnds.add(begin); // this ensures that there is a path that doesn't go through any of the params for (CallParamNode param : node.getChildren()) { Block paramBranch = begin.addBranch(); Block paramBranchEnd = exec(paramBranch, param); branchEnds.add(paramBranchEnd); } this.current = Block.merge(branchEnds); } @Override protected void visitCallParamValueNode(CallParamValueNode node) { // params are evaluated in their own fork. evalInline(node.getValueExprUnion()); } @Override protected void visitCallParamContentNode(CallParamContentNode node) { visitChildren(node); } @Override protected void visitMsgFallbackGroupNode(MsgFallbackGroupNode node) { if (node.numChildren() == 1) { // there is a single message, evaluate inline visit(node.getChild(0)); } else { // there is a fallback. then we have normal a branching structure Block normalBranch = this.current.addBranch(); Block fallbackBranch = this.current.addBranch(); normalBranch = exec(normalBranch, node.getChild(0)); fallbackBranch = exec(fallbackBranch, node.getChild(1)); this.current = Block.merge(normalBranch, fallbackBranch); } } @Override protected void visitMsgNode(MsgNode node) { // for {msg} nodes we always build a placeholder map of all the placeholders in the order // defined by the parsed 'msg parts'. See MsgCompiler. evaluateMsgParts(node, MsgUtils.buildMsgPartsAndComputeMsgIdForDualFormat(node).parts); } /** * Visits all the placeholders of a {msg} in the same order that the MsgCompiler generates code. */ private void evaluateMsgParts(MsgNode msgNode, Iterable<? extends SoyMsgPart> parts) { // Note: The same placeholder may show up multiple times. In the MsgCompiler we use a // separate data structure to dedup so we don't generate the same code for the same // placeholder multiple times. This isn't a concern here because our 'pseudo evaluation' // process is idempotent. for (SoyMsgPart part : parts) { if (part instanceof SoyMsgRawTextPart || part instanceof SoyMsgPluralRemainderPart) { // raw text and plural remainders don't have associated expressions continue; } if (part instanceof SoyMsgPluralPart) { SoyMsgPluralPart plural = (SoyMsgPluralPart) part; evalInline(msgNode.getRepPluralNode(plural.getPluralVarName()).getExpr()); // Recursively visit plural cases for (Case<SoyMsgPluralCaseSpec> caseOrDefault : plural.getCases()) { evaluateMsgParts(msgNode, caseOrDefault.parts()); } } else if (part instanceof SoyMsgSelectPart) { SoyMsgSelectPart select = (SoyMsgSelectPart) part; evalInline(msgNode.getRepSelectNode(select.getSelectVarName()).getExpr()); // Recursively visit select cases for (Case<String> caseOrDefault : select.getCases()) { evaluateMsgParts(msgNode, caseOrDefault.parts()); } } else if (part instanceof SoyMsgPlaceholderPart) { SoyMsgPlaceholderPart placeholder = (SoyMsgPlaceholderPart) part; visit(msgNode.getRepPlaceholderNode(placeholder.getPlaceholderName()).getChild(0)); } else { throw new AssertionError("unexpected part: " + part); } } } @Override protected void visitMsgPlaceholderNode(MsgPlaceholderNode node) { visitChildren(node); } @Override protected void visitMsgHtmlTagNode(MsgHtmlTagNode node) { visitChildren(node); } @Override protected void visitSoyNode(SoyNode node) { throw new UnsupportedOperationException("unsupported node type: " + node.getKind()); } // override to make it visible @Override protected void visitChildren(ParentSoyNode<?> node) { super.visitChildren(node); } /** Evaluates the given expression in the current block.*/ void evalInline(ExprUnion expr) { evalInline(expr.getExpr()); } /** Evaluates the given expression in the current block.*/ void evalInline(ExprNode expr) { Block begin = current; Block end = exprVisitor.eval(begin, expr); current = end; } } private enum StaticAnalysisResult { TRUE, FALSE, UNKNOWN; } private static StaticAnalysisResult isListExpressionEmpty(ExprNode node) { node = node instanceof ExprRootNode ? ((ExprRootNode) node).getRoot() : node; if (node instanceof ListLiteralNode) { return ((ListLiteralNode) node).numChildren() > 0 ? StaticAnalysisResult.FALSE : StaticAnalysisResult.TRUE; } return StaticAnalysisResult.UNKNOWN; } private static final class PseudoEvaluatorExprVisitor extends AbstractExprNodeVisitor<Void> { final Map<VarDefn, AccessGraph> letNodes; Block current; PseudoEvaluatorExprVisitor(Map<VarDefn, AccessGraph> letNodes) { this.letNodes = letNodes; } /** Evaluates the given expression in the given block. Returns the ending or 'exit' block. */ Block eval(Block block, ExprNode expr) { Block orig = this.current; this.current = block; visit(expr); Block rVal = this.current; this.current = orig; return rVal; } @Override protected void visitExprRootNode(ExprRootNode node) { visit(node.getRoot()); } @Override protected void visitVarRefNode(VarRefNode node) { AccessGraph letContent = letNodes.get(node.getDefnDecl()); if (letContent != null) { // because let nodes are lazily executed, we model this in the access graph as each let // varref branching to a copy of the implementation and back. This ensures that each // variable referenced by the {let} is referenced prior to the let variable. // Additionally we add a dead end branch from just prior the let to the canonical let // implementation block // // this means that the graph structure for this: // {let $foo : $p /} // {$foo} // // is // -> <foo-original> // <begin> // -> <foo copy> -> <$foo> -> <end> // This ensures that the variable references within the let are analyzed as being executed // immediately prior to any of the references. AccessGraph copy = letContent.copy(); // dead end branch to the canonical this.current.successors.add(letContent.start); // branch to and back from the copy this.current.successors.add(copy.start); this.current = copy.end.addBranch(); } current.add(node); } @Override protected void visitDataAccessNode(DataAccessNode node) { visitChildren(node); // visit subexpressions first current.add(node); } @Override protected void visitFunctionNode(FunctionNode node) { if (node.getSoyFunction() instanceof BuiltinFunction) { switch ((BuiltinFunction) node.getSoyFunction()) { case INDEX: case IS_FIRST: case IS_LAST: // early return for these. the AST looks like we are evaluating a var, but in fact we // generate alternate code to reference a synthetic variable. // See ExpressionCompiler return; case QUOTE_KEYS_IF_JS: case CHECK_NOT_NULL: // fall through break; default: throw new AssertionError("unexpected builtin function"); } } // eval all arguments in order visitChildren(node); } @Override protected void visitPrimitiveNode(PrimitiveNode node) { // do nothing. } @Override protected void visitListLiteralNode(ListLiteralNode node) { visitChildren(node); } @Override protected void visitMapLiteralNode(MapLiteralNode node) { visitChildren(node); } // Most operators always evaluate their arguments, the control flow operators are handled // separately @Override protected void visitOperatorNode(OperatorNode node) { visitChildren(node); } @Override protected void visitNullCoalescingOpNode(NullCoalescingOpNode node) { visit(node.getLeftChild()); // The right side may or may not be evaluated executeInBranch(node.getRightChild()); } @Override protected void visitOrOpNode(OrOpNode node) { visit(node.getChild(0)); executeInBranch(node.getChild(1)); } @Override protected void visitAndOpNode(AndOpNode node) { visit(node.getChild(0)); executeInBranch(node.getChild(1)); } /** * Evaluates the given node in an optional branch. */ private void executeInBranch(ExprNode expr) { Block prev = current; Block branch = prev.addBranch(); branch = eval(branch, expr); // Add a successor node to merge the branches back together. current = Block.merge(prev, branch); } @Override protected void visitConditionalOpNode(ConditionalOpNode node) { visit(node.getChild(0)); current = Block.merge(eval(current.addBranch(), node.getChild(1)), eval(current.addBranch(), node.getChild(2))); } } /** * Traverses the control flow graph reachable from {@code start} and adds all the predecessor * links. */ private static void addPredecessors(Block start) { Set<Block> visited = Sets.newIdentityHashSet(); addPredecessors(start, visited); } private static void addPredecessors(Block current, Set<Block> visited) { if (!visited.add(current)) { return; } for (Block successor : current.successors) { successor.predecessors.add(current); addPredecessors(successor, visited); } } /** * A graph of {@link Block blocks} for a given template showing how control flows through the * expressions of the template. * * <p>Note this is almost a classic Control Flow Graph * https://en.wikipedia.org/wiki/Control_flow_graph with the following exceptions * <ul> * <li>We aren't tracking 'back edges' (e.g. loops) accurately. * <li>We are tracking a small subset of the operations performed while evaluating a template. * Currently only {@link VarRefNode variable references} and {@link DataAccessNode data * access} operations. * </ul> * * Both of these limitations exist simply because we don't have usecases for tracking this data * yet. */ private static final class AccessGraph { final Block start; final Block end; AccessGraph(Block start, Block end) { this.start = start; this.end = end; } /** Creates a copy of the block. Note, this does not clone the {@code #exprs}. */ AccessGraph copy() { Map<Block, Block> originalToCopy = new IdentityHashMap<Block, Block>(); Block newStart = shallowCopyBlock(start, originalToCopy); Block newEnd = originalToCopy.get(end); return new AccessGraph(newStart, newEnd); } /** Returns a set of ExprNodes that have definitely already been resolved. */ Set<ExprNode> getResolvedExpressions() { // To implement we need to walk through the access graph and if we find any path to an // expression that doesn't go through another reference to the 'same expression' // The easiest way to do this is to calculate 'all paths' through the graph and then do a // search. Unfortunately, 'all paths through a DAG' is technically an exponential time // algorithm // But fortunately we can do better. We can do a DFS from every expression to the start node // and if we go through another reference to the same expression we can abort. If we find any // path to 'start' from the expression then we can remove it from the set. // Still, DFS from every node is technically O(N^2 + MN)). This too can be resolved with // dynamic programming to make it O(N + M). To do this we have to limit the number of paths // traversed by accumulating results. // If we do a Topological traversal from the start node then whenever we visit a node we will // have already visited all of its predecessors. The set of variables definitely accessed // prior to a node is simply the intersection of all the accessed variables from its // predecessors. So each node can be processed in time relative to the number of incoming // edges. Set<ExprNode> resolvedExprs = Sets.newIdentityHashSet(); Map<Block, Set<Equivalence.Wrapper<ExprNode>>> blockToAccessedExprs = new IdentityHashMap<>(); for (Block current : getTopologicalOrdering()) { // First calculate the set of exprs that were definitely accessed prior to this node Set<Equivalence.Wrapper<ExprNode>> currentBlockSet = mergePredecessors(blockToAccessedExprs, current); // Then figure out which nodes in this block were _already_ accessed. for (ExprNode expr : current.exprs) { Equivalence.Wrapper<ExprNode> wrapped = ExprEquivalence.get().wrap(expr); if (!currentBlockSet.add(wrapped)) { resolvedExprs.add(expr); } } blockToAccessedExprs.put(current, currentBlockSet); } return resolvedExprs; } static Set<Equivalence.Wrapper<ExprNode>> mergePredecessors( Map<Block, Set<Equivalence.Wrapper<ExprNode>>> blockToAccessedExprs, Block current) { Set<Equivalence.Wrapper<ExprNode>> currentBlockSet = null; for (Block predecessor : current.predecessors) { Set<Wrapper<ExprNode>> predecessorBlockSet = blockToAccessedExprs.get(predecessor); if (currentBlockSet == null) { currentBlockSet = new HashSet<>(predecessorBlockSet); } else { currentBlockSet.retainAll(predecessorBlockSet); } } if (currentBlockSet == null) { currentBlockSet = new HashSet<>(); } return currentBlockSet; } private List<Block> getTopologicalOrdering() { List<Block> ordering = new ArrayList<>(); Set<Block> visited = Sets.newIdentityHashSet(); Set<Block> discoveredButNotVisited = Sets.newIdentityHashSet(); discoveredButNotVisited.add(start); outer: while (!discoveredButNotVisited.isEmpty()) { for (Block notVisited : discoveredButNotVisited) { // If we have already visited all predecessors if (visited.containsAll(notVisited.predecessors)) { discoveredButNotVisited.remove(notVisited); visited.add(notVisited); discoveredButNotVisited.addAll(notVisited.successors); ordering.add(notVisited); continue outer; } } throw new AssertionError("failed to make progress"); } return ordering; } private static Block shallowCopyBlock(Block original, Map<Block, Block> originalToCopy) { if (originalToCopy.containsKey(original)) { return originalToCopy.get(original); } Block copy = new Block(); for (ExprNode expr : original.exprs) { copy.exprs.add(expr.copy(new CopyState())); } // update the map before recursing to avoid infinite loops originalToCopy.put(original, copy); for (Block successor : original.successors) { copy.successors.add(shallowCopyBlock(successor, originalToCopy)); } for (Block predecessor : original.predecessors) { copy.predecessors.add(shallowCopyBlock(predecessor, originalToCopy)); } return copy; } } /** * A block is a linear sequence of evaluations that happen with no branches. * * <p>Each block has an arbitrary number of {@link Block#predecessors} and * {@link Block#successors} representing all the blocks that come immediately prior to or after * this node. * * <p>This essentially a 'basic block' https://en.wikipedia.org/wiki/Basic_block with the caveat * that we are tracking expressions instead of instructions. */ private static final class Block { static Block merge(Block... preds) { return merge(Arrays.asList(preds)); } static Block merge(List<Block> preds) { Block end = new Block(); for (Block pred : preds) { pred.successors.add(end); } return end; } // This list will contain either DataAccessNode or VarRefNodes, eventually we may want to add // all 'leaf' nodes. final List<ExprNode> exprs = new ArrayList<>(); final Collection<Block> successors = new LinkedHashSet<>(); final Collection<Block> predecessors = new LinkedHashSet<>(); void add(VarRefNode var) { exprs.add(var); } void add(DataAccessNode dataAccess) { exprs.add(dataAccess); } // Returns a new block that is a successor to this one Block addBranch() { Block branch = new Block(); successors.add(branch); return branch; } @Override public String toString() { return getClass().getSimpleName() + ImmutableMap.of("exprs", exprs, "in_edges", predecessors.size(), "out_edges", successors.size()); } } }