/*
* 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.internal.IdGenerator;
import com.google.template.soy.basetree.SyntaxVersion;
import com.google.template.soy.data.SoyValue;
import com.google.template.soy.exprtree.ExprNode;
import com.google.template.soy.exprtree.ExprRootNode;
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.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.annotation.Nullable;
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).
*
*/
public final class SimplifyVisitor {
private final SimplifyExprVisitor simplifyExprVisitor;
private final PrerenderVisitorFactory prerenderVisitorFactory;
@Inject
public SimplifyVisitor(
SimplifyExprVisitor simplifyExprVisitor, PrerenderVisitorFactory prerenderVisitorFactory) {
this.simplifyExprVisitor = simplifyExprVisitor;
this.prerenderVisitorFactory = prerenderVisitorFactory;
}
/** Simplifies the given file set. */
public void simplify(SoyFileSetNode fileSet, TemplateRegistry registry) {
new Impl(registry, fileSet.getNodeIdGenerator()).exec(fileSet);
}
private final class Impl extends AbstractSoyNodeVisitor<Void> {
final TemplateRegistry templateRegistry;
final IdGenerator nodeIdGen;
Impl(TemplateRegistry templateRegistry, IdGenerator idGenerator) {
this.templateRegistry = templateRegistry;
this.nodeIdGen = idGenerator;
}
@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);
// 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.couldHaveSyntaxVersionAtLeast(SyntaxVersion.V2_0)) {
return;
}
ParentSoyNode<StandaloneNode> parent = node.getParent();
if (parent instanceof MsgBlockNode) {
return; // don't prerender
}
if (!isConstant(node.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 {
PrerenderVisitor prerenderer =
prerenderVisitorFactory.create(prerenderOutputSb, templateRegistry);
prerenderer.exec(node);
} catch (RenderException pe) {
return; // cannot prerender for some other reason not checked above
}
// Replace this node with a RawTextNode.
String string = prerenderOutputSb.toString();
if (string.isEmpty()) {
parent.removeChild(node);
} else {
parent.replaceChild(
node, new RawTextNode(nodeIdGen.genId(), string, node.getSourceLocation()));
}
}
@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.getExpr();
if (!isConstant(condExpr)) {
continue; // cannot simplify this child
}
if (getConstantOrNull(condExpr).coerceToBoolean()) {
// ------ 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(), condNode.getSourceLocation());
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.
SoyValue 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()) {
SoyValue 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(), caseNode.getSourceLocation());
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();
}
/**
* 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.
*
* <p>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.isEmpty()) {
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(), parent.getSourceLocation()));
}
}
}
// -----------------------------------------------------------------------------------------------
// Helpers.
private static boolean isConstant(@Nullable ExprRootNode exprRoot) {
return exprRoot != null && SimplifyExprVisitor.isConstant(exprRoot.getRoot());
}
private static SoyValue getConstantOrNull(ExprRootNode exprRoot) {
if (exprRoot == null) {
return null;
}
ExprNode expr = exprRoot.getRoot();
return SimplifyExprVisitor.getConstantOrNull(expr);
}
/**
* @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) {
ParentSoyNode<StandaloneNode> parent = origNode.getParent();
int indexInParent = parent.getChildIndex(origNode);
parent.removeChild(indexInParent);
parent.addChildren(indexInParent, replacementNodes);
}
}