/*
* Copyright 2008 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.render;
import com.google.common.collect.Maps;
import com.google.template.soy.data.SoyData;
import com.google.template.soy.data.SoyListData;
import com.google.template.soy.data.SoyMapData;
import com.google.template.soy.data.restricted.BooleanData;
import com.google.template.soy.data.restricted.CollectionData;
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.data.restricted.UndefinedData;
import com.google.template.soy.exprtree.AbstractReturningExprNodeVisitor;
import com.google.template.soy.exprtree.BooleanNode;
import com.google.template.soy.exprtree.DataRefAccessIndexNode;
import com.google.template.soy.exprtree.DataRefAccessKeyNode;
import com.google.template.soy.exprtree.DataRefAccessNode;
import com.google.template.soy.exprtree.DataRefNode;
import com.google.template.soy.exprtree.ExprNode;
import com.google.template.soy.exprtree.ExprRootNode;
import com.google.template.soy.exprtree.FloatNode;
import com.google.template.soy.exprtree.FunctionNode;
import com.google.template.soy.exprtree.IntegerNode;
import com.google.template.soy.exprtree.ListLiteralNode;
import com.google.template.soy.exprtree.MapLiteralNode;
import com.google.template.soy.exprtree.NullNode;
import com.google.template.soy.exprtree.OperatorNodes.AndOpNode;
import com.google.template.soy.exprtree.OperatorNodes.ConditionalOpNode;
import com.google.template.soy.exprtree.OperatorNodes.DivideByOpNode;
import com.google.template.soy.exprtree.OperatorNodes.EqualOpNode;
import com.google.template.soy.exprtree.OperatorNodes.GreaterThanOpNode;
import com.google.template.soy.exprtree.OperatorNodes.GreaterThanOrEqualOpNode;
import com.google.template.soy.exprtree.OperatorNodes.LessThanOpNode;
import com.google.template.soy.exprtree.OperatorNodes.LessThanOrEqualOpNode;
import com.google.template.soy.exprtree.OperatorNodes.MinusOpNode;
import com.google.template.soy.exprtree.OperatorNodes.ModOpNode;
import com.google.template.soy.exprtree.OperatorNodes.NegativeOpNode;
import com.google.template.soy.exprtree.OperatorNodes.NotEqualOpNode;
import com.google.template.soy.exprtree.OperatorNodes.NotOpNode;
import com.google.template.soy.exprtree.OperatorNodes.OrOpNode;
import com.google.template.soy.exprtree.OperatorNodes.PlusOpNode;
import com.google.template.soy.exprtree.OperatorNodes.TimesOpNode;
import com.google.template.soy.exprtree.StringNode;
import com.google.template.soy.shared.internal.NonpluginFunction;
import com.google.template.soy.shared.restricted.SoyJavaRuntimeFunction;
import java.util.Deque;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
/**
* Visitor for evaluating the expression rooted at a given ExprNode.
*
* <p> Important: Do not use outside of Soy code (treat as superpackage-private).
*
* <p> {@link #exec} may be called on any expression. The result of evaluating the expression (in
* the context of the {@code data} and {@code env} passed into the constructor) is returned as a
* {@code SoyData} object.
*
* @author Kai Huang
*/
public class EvalVisitor extends AbstractReturningExprNodeVisitor<SoyData> {
/**
* Interface for a factory that creates an EvalVisitor.
*/
public static interface EvalVisitorFactory {
/**
* Creates an EvalVisitor.
* @param data The current template data.
* @param ijData The current injected data.
* @param env The current environment.
* @return The newly created EvalVisitor instance.
*/
public EvalVisitor create(
SoyMapData data, @Nullable SoyMapData ijData, Deque<Map<String, SoyData>> env);
}
/** Map of all SoyJavaRuntimeFunctions (name to function). */
private final Map<String, SoyJavaRuntimeFunction> soyJavaRuntimeFunctionsMap;
/** The current template data. */
private final SoyMapData data;
/** The current injected data. */
private final SoyMapData ijData;
/** The current environment. */
private final Deque<Map<String, SoyData>> env;
/**
* @param soyJavaRuntimeFunctionsMap Map of all SoyJavaRuntimeFunctions (name to function). Can be
* null if the subclass that is calling this constructor plans to override the default
* implementation of {@code computeFunction()}.
* @param data The current template data.
* @param ijData The current injected data.
* @param env The current environment.
*/
protected EvalVisitor(
@Nullable Map<String, SoyJavaRuntimeFunction> soyJavaRuntimeFunctionsMap, SoyMapData data,
@Nullable SoyMapData ijData, Deque<Map<String, SoyData>> env) {
this.soyJavaRuntimeFunctionsMap = soyJavaRuntimeFunctionsMap;
this.data = data;
this.ijData = ijData;
this.env = env;
}
// -----------------------------------------------------------------------------------------------
// Implementation for a dummy root node.
@Override protected SoyData visitExprRootNode(ExprRootNode<?> node) {
return visit(node.getChild(0));
}
// -----------------------------------------------------------------------------------------------
// Implementations for primitives.
@Override protected SoyData visitNullNode(NullNode node) {
return NullData.INSTANCE;
}
@Override protected SoyData visitBooleanNode(BooleanNode node) {
return convertResult(node.getValue());
}
@Override protected SoyData visitIntegerNode(IntegerNode node) {
return convertResult(node.getValue());
}
@Override protected SoyData visitFloatNode(FloatNode node) {
return convertResult(node.getValue());
}
@Override protected SoyData visitStringNode(StringNode node) {
return convertResult(node.getValue());
}
// -----------------------------------------------------------------------------------------------
// Implementations for collections.
@Override protected SoyData visitListLiteralNode(ListLiteralNode node) {
return new SoyListData(visitChildren(node));
}
@Override protected SoyData visitMapLiteralNode(MapLiteralNode node) {
Map<String, SoyData> map = Maps.newHashMap();
for (int i = 0, n = node.numChildren(); i < n; i += 2) {
SoyData key = visit(node.getChild(i));
if (! (key instanceof StringData)) {
throw new RenderException(
"Maps must have string keys (key \"" + node.getChild(i).toSourceString() + "\"" +
" in map " + node.toSourceString() + " does not evaluate to a string).");
}
SoyData value = visit(node.getChild(i + 1));
map.put(key.stringValue(), value);
}
return new SoyMapData(map);
}
// -----------------------------------------------------------------------------------------------
// Implementations for data references.
@Override protected SoyData visitDataRefNode(DataRefNode node) {
// First resolve the first key, which may reference a variable, data, or injected data.
SoyData value0 = resolveDataRefFirstKey(node);
// Case 1: There is only one key. We already have the final value of the data reference.
if (node.numChildren() == 0) {
return value0;
}
// Case 2: There are more keys.
SoyData value = value0;
for (ExprNode child : node.getChildren()) {
DataRefAccessNode accessNode = (DataRefAccessNode) child;
// We expect 'value' to be a CollectionData during every iteration.
if (! (value instanceof CollectionData)) {
if (accessNode.isNullSafe()) {
if (value == null || value instanceof UndefinedData || value instanceof NullData) {
return NullData.INSTANCE;
} else {
throw new RenderException(
"While evaluating \"" + node.toSourceString() + "\", encountered non-collection" +
" just before accessing \"" + accessNode.toSourceString() + "\".");
}
} else {
// This behavior is not ideal, but needed for compatibility with existing code.
return UndefinedData.INSTANCE;
// TODO: If feasible, find and fix existing instances, then enable this exception.
//if (value == null || value instanceof UndefinedData) {
// throw new RenderException(
// "While evaluating \"" + node.toSourceString() + "\", encountered undefined LHS" +
// " just before accessing \"" + accessNode.toSourceString() + "\".");
//}
//value = UndefinedData.INSTANCE;
//continue;
}
}
// Extract either a string key or integer index from the child access node.
String key = null;
int index = -1;
switch (accessNode.getKind()) {
case DATA_REF_ACCESS_KEY_NODE:
key = ((DataRefAccessKeyNode) accessNode).getKey();
break;
case DATA_REF_ACCESS_INDEX_NODE:
index = ((DataRefAccessIndexNode) accessNode).getIndex();
break;
case DATA_REF_ACCESS_EXPR_NODE:
SoyData keyData = visit(accessNode.getChild(0));
if (keyData instanceof IntegerData) {
index = ((IntegerData) keyData).getValue();
} else {
key = keyData.toString();
}
break;
default:
throw new AssertionError();
}
// Get the data at the extracted key or index.
if (key != null) {
value = ((CollectionData) value).getSingle(key);
} else {
if (! (value instanceof SoyListData)) {
throw new RenderException(
"While evaluating \"" + node.toSourceString() + "\", encountered non-list" +
" just before accessing \"" + accessNode.toSourceString() + "\".");
}
value = ((SoyListData) value).get(index);
}
}
return (value != null) ? value : UndefinedData.INSTANCE;
}
// -----------------------------------------------------------------------------------------------
// Implementations for operators.
@Override protected SoyData visitNegativeOpNode(NegativeOpNode node) {
SoyData operand = visit(node.getChild(0));
if (operand instanceof IntegerData) {
return convertResult( - operand.integerValue() );
} else {
return convertResult( - operand.floatValue() );
}
}
@Override protected SoyData visitNotOpNode(NotOpNode node) {
SoyData operand = visit(node.getChild(0));
return convertResult( ! operand.toBoolean() );
}
@Override protected SoyData visitTimesOpNode(TimesOpNode node) {
SoyData operand0 = visit(node.getChild(0));
SoyData operand1 = visit(node.getChild(1));
if (operand0 instanceof IntegerData && operand1 instanceof IntegerData) {
return convertResult(operand0.integerValue() * operand1.integerValue());
} else {
return convertResult(operand0.numberValue() * operand1.numberValue());
}
}
@Override protected SoyData visitDivideByOpNode(DivideByOpNode node) {
SoyData operand0 = visit(node.getChild(0));
SoyData operand1 = visit(node.getChild(1));
// Note: Soy always performs floating-point division, even on two integers (like JavaScript).
return convertResult(operand0.numberValue() / operand1.numberValue());
}
@Override protected SoyData visitModOpNode(ModOpNode node) {
SoyData operand0 = visit(node.getChild(0));
SoyData operand1 = visit(node.getChild(1));
return convertResult(operand0.integerValue() % operand1.integerValue());
}
@Override protected SoyData visitPlusOpNode(PlusOpNode node) {
SoyData operand0 = visit(node.getChild(0));
SoyData operand1 = visit(node.getChild(1));
if (operand0 instanceof IntegerData && operand1 instanceof IntegerData) {
return convertResult(operand0.integerValue() + operand1.integerValue());
} else if (operand0 instanceof StringData || operand1 instanceof StringData) {
// String concatenation. Note we're calling toString() instead of stringValue() in case one
// of the operands needs to be coerced to a string.
return convertResult(operand0.toString() + operand1.toString());
} else {
return convertResult(operand0.numberValue() + operand1.numberValue());
}
}
@Override protected SoyData visitMinusOpNode(MinusOpNode node) {
SoyData operand0 = visit(node.getChild(0));
SoyData operand1 = visit(node.getChild(1));
if (operand0 instanceof IntegerData && operand1 instanceof IntegerData) {
return convertResult(operand0.integerValue() - operand1.integerValue());
} else {
return convertResult(operand0.numberValue() - operand1.numberValue());
}
}
@Override protected SoyData visitLessThanOpNode(LessThanOpNode node) {
SoyData operand0 = visit(node.getChild(0));
SoyData operand1 = visit(node.getChild(1));
if (operand0 instanceof IntegerData && operand1 instanceof IntegerData) {
return convertResult(operand0.integerValue() < operand1.integerValue());
} else {
return convertResult(operand0.numberValue() < operand1.numberValue());
}
}
@Override protected SoyData visitGreaterThanOpNode(GreaterThanOpNode node) {
SoyData operand0 = visit(node.getChild(0));
SoyData operand1 = visit(node.getChild(1));
if (operand0 instanceof IntegerData && operand1 instanceof IntegerData) {
return convertResult(operand0.integerValue() > operand1.integerValue());
} else {
return convertResult(operand0.numberValue() > operand1.numberValue());
}
}
@Override protected SoyData visitLessThanOrEqualOpNode(LessThanOrEqualOpNode node) {
SoyData operand0 = visit(node.getChild(0));
SoyData operand1 = visit(node.getChild(1));
if (operand0 instanceof IntegerData && operand1 instanceof IntegerData) {
return convertResult(operand0.integerValue() <= operand1.integerValue());
} else {
return convertResult(operand0.numberValue() <= operand1.numberValue());
}
}
@Override protected SoyData visitGreaterThanOrEqualOpNode(GreaterThanOrEqualOpNode node) {
SoyData operand0 = visit(node.getChild(0));
SoyData operand1 = visit(node.getChild(1));
if (operand0 instanceof IntegerData && operand1 instanceof IntegerData) {
return convertResult(operand0.integerValue() >= operand1.integerValue());
} else {
return convertResult(operand0.numberValue() >= operand1.numberValue());
}
}
@Override protected SoyData visitEqualOpNode(EqualOpNode node) {
SoyData operand0 = visit(node.getChild(0));
SoyData operand1 = visit(node.getChild(1));
return convertResult(operand0.equals(operand1));
}
@Override protected SoyData visitNotEqualOpNode(NotEqualOpNode node) {
SoyData operand0 = visit(node.getChild(0));
SoyData operand1 = visit(node.getChild(1));
return convertResult(!operand0.equals(operand1));
}
@Override protected SoyData visitAndOpNode(AndOpNode node) {
// Note: Short-circuit evaluation.
SoyData operand0 = visit(node.getChild(0));
if (!operand0.toBoolean()) {
return convertResult(false);
} else {
SoyData operand1 = visit(node.getChild(1));
return convertResult(operand1.toBoolean());
}
}
@Override protected SoyData visitOrOpNode(OrOpNode node) {
// Note: Short-circuit evaluation.
SoyData operand0 = visit(node.getChild(0));
if (operand0.toBoolean()) {
return convertResult(true);
} else {
SoyData operand1 = visit(node.getChild(1));
return convertResult(operand1.toBoolean());
}
}
@Override protected SoyData visitConditionalOpNode(ConditionalOpNode node) {
// Note: We only evaluate the part that we need.
SoyData operand0 = visit(node.getChild(0));
if (operand0.toBoolean()) {
return visit(node.getChild(1));
} else {
return visit(node.getChild(2));
}
}
// -----------------------------------------------------------------------------------------------
// Implementations for functions.
@Override protected SoyData visitFunctionNode(FunctionNode node) {
String fnName = node.getFunctionName();
// Handle nonplugin functions.
NonpluginFunction nonpluginFn = NonpluginFunction.forFunctionName(fnName);
if (nonpluginFn != null) {
switch (nonpluginFn) {
case IS_FIRST:
return visitIsFirstFunction(node);
case IS_LAST:
return visitIsLastFunction(node);
case INDEX:
return visitIndexFunction(node);
case QUOTE_KEYS_IF_JS:
return visitMapLiteralNode((MapLiteralNode) node.getChild(0));
default:
throw new AssertionError();
}
}
// Handle plugin functions.
List<SoyData> args = visitChildren(node);
return computeFunction(fnName, args, node);
}
/**
* Protected helper for visitFunctionNode() to compute a function.
*
* <p> This default implementation can be overridden by subclasses (such as TofuEvalVisitor) that
* have access to a potentially larger set of functions.
*
* @param fnName The name of the function.
* @param args The arguments to the function.
* @param fnNode The function node. Only used for error reporting.
* @return The result of the function called on the given arguments.
*/
protected SoyData computeFunction(String fnName, List<SoyData> args, FunctionNode fnNode) {
SoyJavaRuntimeFunction fn = soyJavaRuntimeFunctionsMap.get(fnName);
if (fn == null) {
throw new RenderException(
"Failed to find Soy function with name '" + fnName + "'" +
" (function call \"" + fnNode.toSourceString() + "\").");
}
// Arity has already been checked by CheckFunctionCallsVisitor.
return computeFunctionHelper(fn, args, fnNode);
}
/**
* Protected helper for {@code computeFunction}. This helper exists so that subclasses can
* override it.
* @param fn The function object.
* @param args The arguments to the function.
* @param fnNode The function node. Only used for error reporting.
* @return The result of the function called on the given arguments.
*/
protected SoyData computeFunctionHelper(
SoyJavaRuntimeFunction fn, List<SoyData> args, FunctionNode fnNode) {
try {
return fn.compute(args);
} catch (Exception e) {
throw new RenderException(
"While computing function \"" + fnNode.toSourceString() + "\": " + e.getMessage(), e);
}
}
private SoyData visitIsFirstFunction(FunctionNode node) {
int localVarIndex;
try {
DataRefNode dataRef = (DataRefNode) node.getChild(0);
String localVarName = dataRef.getFirstKey();
localVarIndex = getLocalVar(localVarName + "__index").integerValue();
} catch (Exception e) {
throw new RenderException("Failed to evaluate function call " + node.toSourceString() + ".");
}
return convertResult(localVarIndex == 0);
}
private SoyData visitIsLastFunction(FunctionNode node) {
int localVarIndex, localVarLastIndex;
try {
DataRefNode dataRef = (DataRefNode) node.getChild(0);
String localVarName = dataRef.getFirstKey();
localVarIndex = getLocalVar(localVarName + "__index").integerValue();
localVarLastIndex = getLocalVar(localVarName + "__lastIndex").integerValue();
} catch (Exception e) {
throw new RenderException("Failed to evaluate function call " + node.toSourceString() + ".");
}
return convertResult(localVarIndex == localVarLastIndex);
}
private SoyData visitIndexFunction(FunctionNode node) {
int localVarIndex;
try {
DataRefNode dataRef = (DataRefNode) node.getChild(0);
String localVarName = dataRef.getFirstKey();
localVarIndex = getLocalVar(localVarName + "__index").integerValue();
} catch (Exception e) {
throw new RenderException("Failed to evaluate function call " + node.toSourceString() + ".");
}
return convertResult(localVarIndex);
}
// -----------------------------------------------------------------------------------------------
// Private helpers.
/**
* Private helper to convert a boolean result.
* @param b The boolean to convert.
*/
private SoyData convertResult(boolean b) {
return BooleanData.forValue(b);
}
/**
* Private helper to convert an integer result.
* @param i The integer to convert.
*/
private SoyData convertResult(int i) {
return IntegerData.forValue(i);
}
/**
* Private helper to convert a float result.
* @param f The float to convert.
*/
private SoyData convertResult(double f) {
return FloatData.forValue(f);
}
/**
* Private helper to convert a string result.
* @param s The string to convert.
*/
private SoyData convertResult(String s) {
return StringData.forValue(s);
}
/**
* Private helper to get the value of a local variable (from the environment).
* Note: Throws an AssertionError if the given name is not defined in the environment.
* @param localVarName The name of the local var to retrieve.
* @return The value of the local var.
*/
private SoyData getLocalVar(String localVarName) {
for (Map<String, SoyData> envFrame : env) {
SoyData value = envFrame.get(localVarName);
if (value != null) {
return value;
}
}
throw new AssertionError();
}
/**
* Private helper to get the value of the first part of a data ref.
* @param dataRefNode The data ref whose first key we want to retrieve.
* @return The value of the first key, or UndefinedData if it is not defined in the environment
* nor the template data.
*/
protected SoyData resolveDataRefFirstKey(DataRefNode dataRefNode) {
String firstKey = dataRefNode.getFirstKey();
SoyData value = null;
if (dataRefNode.isIjDataRef()) {
if (ijData != null) {
value = ijData.getSingle(firstKey);
} else {
if (dataRefNode.isNullSafeIjDataRef()) {
return NullData.INSTANCE;
} else {
throw new RenderException(
"Injected data not provided, yet referenced (" + dataRefNode.toSourceString() + ").");
}
}
} else {
Boolean isLocalVarDataRef = dataRefNode.isLocalVarDataRef(); // null if unknown
// Retrieve from the environment. Do this when (a) we know it's a local var data ref or (b) we
// don't know either way.
if (isLocalVarDataRef == Boolean.TRUE || isLocalVarDataRef == null) {
if (env != null) {
for (Map<String, SoyData> envFrame : env) {
value = envFrame.get(firstKey);
if (value != null) {
break;
}
}
}
}
// Retrieve from the data. Do this when (a) we know it's not a local var data ref or (b) we
// don't know either way, but we failed to retrieve a nonnull value from the environment.
if (isLocalVarDataRef == Boolean.FALSE || (isLocalVarDataRef == null && value == null)) {
if (data != null) {
value = data.getSingle(firstKey);
}
}
}
return (value != null) ? value : UndefinedData.INSTANCE;
}
}