/*
* Copyright 2011 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.sharedpasses.opti;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import com.google.template.soy.base.IdGenerator;
import com.google.template.soy.data.SoyData;
import com.google.template.soy.data.SoyMapData;
import com.google.template.soy.data.restricted.BooleanData;
import com.google.template.soy.data.restricted.FloatData;
import com.google.template.soy.data.restricted.IntegerData;
import com.google.template.soy.data.restricted.NullData;
import com.google.template.soy.data.restricted.StringData;
import com.google.template.soy.exprtree.BooleanNode;
import com.google.template.soy.exprtree.ExprNode;
import com.google.template.soy.exprtree.ExprNode.ConstantNode;
import com.google.template.soy.exprtree.ExprRootNode;
import com.google.template.soy.exprtree.FloatNode;
import com.google.template.soy.exprtree.IntegerNode;
import com.google.template.soy.exprtree.StringNode;
import com.google.template.soy.sharedpasses.render.RenderException;
import com.google.template.soy.soytree.AbstractSoyNodeVisitor;
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.PrintDirectiveNode;
import com.google.template.soy.soytree.PrintNode;
import com.google.template.soy.soytree.RawTextNode;
import com.google.template.soy.soytree.SoyFileSetNode;
import com.google.template.soy.soytree.SoyNode;
import com.google.template.soy.soytree.SoyNode.BlockNode;
import com.google.template.soy.soytree.SoyNode.MsgBlockNode;
import com.google.template.soy.soytree.SoyNode.ParentSoyNode;
import com.google.template.soy.soytree.SoyNode.StandaloneNode;
import com.google.template.soy.soytree.SoyNode.SyntaxVersion;
import com.google.template.soy.soytree.SoytreeUtils;
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.TemplateRegistry;
import java.util.List;
import javax.inject.Inject;
/**
* Visitor for simplifying subtrees based on constant values known at compile time.
*
* <p> Important: Do not use outside of Soy code (treat as superpackage-private).
*
* <p> {@link #exec} should be called on a full Soy tree.
*
* @author Kai Huang
*/
public class SimplifyVisitor extends AbstractSoyNodeVisitor<Void> {
/** SimplifyExprVisitor instance used by this visitor. */
private final SimplifyExprVisitor simplifyExprVisitor;
/** Factory for creating an instance of PrerenderVisitor. */
private final PrerenderVisitorFactory prerenderVisitorFactory;
private IdGenerator nodeIdGen;
/** A registry of all templates in the Soy tree. */
private TemplateRegistry templateRegistry;
@Inject
public SimplifyVisitor(
SimplifyExprVisitor simplifyExprVisitor, PrerenderVisitorFactory prerenderVisitorFactory) {
this.simplifyExprVisitor = simplifyExprVisitor;
this.prerenderVisitorFactory = prerenderVisitorFactory;
}
@Override public Void exec(SoyNode node) {
Preconditions.checkArgument(node instanceof SoyFileSetNode);
SoyFileSetNode nodeAsRoot = (SoyFileSetNode) node;
// First simplify all expressions in the subtree.
SoytreeUtils.execOnAllV2Exprs(nodeAsRoot, simplifyExprVisitor);
// Setup.
nodeIdGen = nodeAsRoot.getNodeIdGenerator();
templateRegistry = new TemplateRegistry(nodeAsRoot);
// Simpify the subtree.
super.exec(nodeAsRoot);
return null;
}
// -----------------------------------------------------------------------------------------------
// Implementations for specific nodes.
@Override protected void visitPrintNode(PrintNode node) {
// We attempt to prerender this node if and only if it:
// (a) is in V2 syntax,
// (b) is not a child of a MsgBlockNode,
// (c) has a constant expression,
// (d) has constant expressions for all directive arguments (if any).
// The prerender attempt may fail due to other reasons not checked above.
if (node.getSyntaxVersion() != SyntaxVersion.V2) {
return;
}
BlockNode parent = node.getParent();
if (parent instanceof MsgBlockNode) {
return; // don't prerender
}
if (! isConstant(node.getExprUnion().getExpr())) {
return; // don't prerender
}
for (PrintDirectiveNode directive : node.getChildren()) {
for (ExprRootNode<?> arg : directive.getArgs()) {
if (! isConstant(arg)) {
return; // don't prerender
}
}
}
StringBuilder prerenderOutputSb = new StringBuilder();
try {
prerenderVisitorFactory.create(prerenderOutputSb, templateRegistry, new SoyMapData(), null)
.exec(node);
} catch (RenderException pe) {
return; // cannot prerender for some other reason not checked above
}
// Replace this node with a RawTextNode.
parent.replaceChild(node, new RawTextNode(nodeIdGen.genId(), prerenderOutputSb.toString()));
}
@Override protected void visitIfNode(IfNode node) {
// Recurse.
visitSoyNode(node);
// For each IfCondNode child:
// (a) If the condition is constant true: Replace the child with an IfElseNode and remove all
// children after it, if any. Can stop processing after doing this, because the new
// IfElseNode is now the last child.
// (b) If the condition is constant false: Remove the child.
for (SoyNode child : Lists.newArrayList(node.getChildren()) /*copy*/) {
if (child instanceof IfCondNode) {
IfCondNode condNode = (IfCondNode) child;
ExprRootNode<?> condExpr = condNode.getExprUnion().getExpr();
if (! isConstant(condExpr)) {
continue; // cannot simplify this child
}
if (getConstantOrNull(condExpr).toBoolean()) {
// ------ Constant true. ------
// Remove all children after this child.
int condIndex = node.getChildIndex(condNode);
for (int i = node.numChildren() - 1; i > condIndex; i--) {
node.removeChild(i);
}
// Replace this child with a new IfElseNode.
IfElseNode newElseNode = new IfElseNode(nodeIdGen.genId());
newElseNode.addChildren(condNode.getChildren());
node.replaceChild(condIndex, newElseNode);
// Stop processing.
break;
} else {
// ------ Constant false. ------
node.removeChild(condNode);
}
}
}
// If this IfNode:
// (a) Has no children left: Remove it.
// (b) Has only one child left, and it's an IfElseNode: Replace this IfNode with its
// grandchildren.
if (node.numChildren() == 0) {
node.getParent().removeChild(node);
}
if (node.numChildren() == 1 && node.getChild(0) instanceof IfElseNode) {
replaceNodeWithList(node, ((IfElseNode) node.getChild(0)).getChildren());
}
}
@Override protected void visitSwitchNode(SwitchNode node) {
// Recurse.
visitSoyNode(node);
// If the SwitchNode's expr is not constant, we can't simplify.
SoyData switchExprValue = getConstantOrNull(node.getExpr());
if (switchExprValue == null) {
return; // cannot simplify this node
}
// For each SwitchCaseNode child:
// (a) If the case has a constant expr that matches: Replace the child with a SwitchDefaultNode
// and remove all children after it, if any. Can stop processing after doing this, because
// the new SwitchDefaultNode is now the last child.
// (b) If the case has all constant exprs and none match: Remove the child.
for (SoyNode child : Lists.newArrayList(node.getChildren()) /*copy*/) {
if (child instanceof SwitchCaseNode) {
SwitchCaseNode caseNode = (SwitchCaseNode) child;
boolean hasMatchingConstant = false;
boolean hasAllNonmatchingConstants = true;
for (ExprRootNode<?> caseExpr : caseNode.getExprList()) {
SoyData caseExprValue = getConstantOrNull(caseExpr);
if (caseExprValue == null) {
hasAllNonmatchingConstants = false;
} else if (caseExprValue.equals(switchExprValue)) {
hasMatchingConstant = true;
hasAllNonmatchingConstants = false;
break;
}
}
if (hasMatchingConstant) {
// ------ Has a constant expr that matches. ------
// Remove all children after this child.
int caseIndex = node.getChildIndex(caseNode);
for (int i = node.numChildren() - 1; i > caseIndex; i--) {
node.removeChild(i);
}
// Replace this child with a new SwitchDefaultNode.
SwitchDefaultNode newDefaultNode = new SwitchDefaultNode(nodeIdGen.genId());
newDefaultNode.addChildren(caseNode.getChildren());
node.replaceChild(caseIndex, newDefaultNode);
// Stop processing.
break;
} else if (hasAllNonmatchingConstants) {
// ------ Has all contant exprs and none match. ------
node.removeChild(caseNode);
}
}
}
// If this SwitchNode:
// (a) Has no children left: Remove it.
// (b) Has only one child left, and it's a SwitchDefaultNode: Replace this SwitchNode with its
// grandchildren.
if (node.numChildren() == 1 && node.getChild(0) instanceof SwitchDefaultNode) {
replaceNodeWithList(node, ((SwitchDefaultNode) node.getChild(0)).getChildren());
}
}
// Note (Sep-2012): We removed prerendering of calls (visitCallBasicNode) due to development
// issues. We decided it was better to remove it than to add another rarely-used option to the Soy
// compiler.
// -----------------------------------------------------------------------------------------------
// Fallback implementation.
@Override protected void visitSoyNode(SoyNode node) {
if (node instanceof ParentSoyNode<?>) {
visitChildrenAllowingConcurrentModification((ParentSoyNode<?>) node);
}
if (! (node instanceof BlockNode)) {
return;
}
BlockNode nodeAsBlock = (BlockNode) node;
// Check whether there are any consecutive RawTextNode children.
boolean hasConsecRawTextNodes = false;
for (int i = 0; i <= nodeAsBlock.numChildren() - 2; i++) {
if (nodeAsBlock.getChild(i) instanceof RawTextNode &&
nodeAsBlock.getChild(i + 1) instanceof RawTextNode) {
hasConsecRawTextNodes = true;
break;
}
}
// If there aren't any consecutive RawTextNode children, we're done.
if (! hasConsecRawTextNodes) {
return;
}
// Rebuild the list of children, combining consecutive RawTextNodes into one.
List<StandaloneNode> copyOfOrigChildren = Lists.newArrayList(nodeAsBlock.getChildren());
nodeAsBlock.clearChildren();
List<RawTextNode> consecutiveRawTextNodes = Lists.newArrayList();
for (StandaloneNode origChild : copyOfOrigChildren) {
if (origChild instanceof RawTextNode) {
consecutiveRawTextNodes.add((RawTextNode) origChild);
} else {
// First add the preceding consecutive RawTextNodes, if any.
addConsecutiveRawTextNodesAsOneNodeHelper(nodeAsBlock, consecutiveRawTextNodes);
consecutiveRawTextNodes.clear();
// Then add the current new child.
nodeAsBlock.addChild(origChild);
}
}
// Add the final group of consecutive RawTextNodes, if any.
addConsecutiveRawTextNodesAsOneNodeHelper(nodeAsBlock, consecutiveRawTextNodes);
consecutiveRawTextNodes.clear();
}
// -----------------------------------------------------------------------------------------------
// Helpers.
private static boolean isConstant(ExprRootNode<?> exprRoot) {
return exprRoot != null && exprRoot.getChild(0) instanceof ConstantNode;
}
private static SoyData getConstantOrNull(ExprRootNode<?> exprRoot) {
if (exprRoot == null) {
return null;
}
ExprNode expr = exprRoot.getChild(0);
switch (expr.getKind()) {
case NULL_NODE: return NullData.INSTANCE;
case BOOLEAN_NODE: return BooleanData.forValue(((BooleanNode) expr).getValue());
case INTEGER_NODE: return IntegerData.forValue(((IntegerNode) expr).getValue());
case FLOAT_NODE: return FloatData.forValue(((FloatNode) expr).getValue());
case STRING_NODE: return StringData.forValue(((StringNode) expr).getValue());
default: return null;
}
}
/**
* Helper to add consecutive RawTextNodes as one child node (the raw text will be joined).
* If the consecutive RawTextNodes list actually only has one item, then adds that node instead
* of creating a new RawTextNode.
*
* Note: This function works closely with the above code. In particular, it assumes we're
* rebuilding the whole list (thus adding to the end of the parent) instead of fixing the old
* list in-place.
*
* @param parent The parent to add the new child to.
* @param consecutiveRawTextNodes The list of consecutive RawTextNodes.
*/
private void addConsecutiveRawTextNodesAsOneNodeHelper(
BlockNode parent, List<RawTextNode> consecutiveRawTextNodes) {
if (consecutiveRawTextNodes.size() == 0) {
return;
} else if (consecutiveRawTextNodes.size() == 1) {
// Simply add the one RawTextNode.
parent.addChild(consecutiveRawTextNodes.get(0));
} else {
// Create a new combined RawTextNode.
StringBuilder rawText = new StringBuilder();
for (RawTextNode rtn : consecutiveRawTextNodes) {
rawText.append(rtn.getRawText());
}
parent.addChild(new RawTextNode(nodeIdGen.genId(), rawText.toString()));
}
}
/**
* @param origNode The original node to replace.
* @param replacementNodes The list of nodes to put in place of the original node.
*/
private static void replaceNodeWithList(
StandaloneNode origNode, List<? extends StandaloneNode> replacementNodes) {
BlockNode parent = origNode.getParent();
int indexInParent = parent.getChildIndex(origNode);
parent.removeChild(indexInParent);
parent.addChildren(indexInParent, replacementNodes);
}
}