/*
* 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 static com.google.common.base.Preconditions.checkNotNull;
import static com.google.template.soy.shared.internal.SharedRuntime.dividedBy;
import static com.google.template.soy.shared.internal.SharedRuntime.equal;
import static com.google.template.soy.shared.internal.SharedRuntime.lessThan;
import static com.google.template.soy.shared.internal.SharedRuntime.lessThanOrEqual;
import static com.google.template.soy.shared.internal.SharedRuntime.minus;
import static com.google.template.soy.shared.internal.SharedRuntime.negative;
import static com.google.template.soy.shared.internal.SharedRuntime.plus;
import static com.google.template.soy.shared.internal.SharedRuntime.times;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.template.soy.data.SoyAbstractValue;
import com.google.template.soy.data.SoyDataException;
import com.google.template.soy.data.SoyMap;
import com.google.template.soy.data.SoyRecord;
import com.google.template.soy.data.SoyValue;
import com.google.template.soy.data.SoyValueConverter;
import com.google.template.soy.data.internal.DictImpl;
import com.google.template.soy.data.internal.ListImpl;
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.data.restricted.UndefinedData;
import com.google.template.soy.exprtree.AbstractReturningExprNodeVisitor;
import com.google.template.soy.exprtree.BooleanNode;
import com.google.template.soy.exprtree.DataAccessNode;
import com.google.template.soy.exprtree.ExprNode;
import com.google.template.soy.exprtree.ExprRootNode;
import com.google.template.soy.exprtree.FieldAccessNode;
import com.google.template.soy.exprtree.FloatNode;
import com.google.template.soy.exprtree.FunctionNode;
import com.google.template.soy.exprtree.GlobalNode;
import com.google.template.soy.exprtree.IntegerNode;
import com.google.template.soy.exprtree.ItemAccessNode;
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.NullCoalescingOpNode;
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.ProtoInitNode;
import com.google.template.soy.exprtree.StringNode;
import com.google.template.soy.exprtree.VarRefNode;
import com.google.template.soy.shared.internal.BuiltinFunction;
import com.google.template.soy.shared.restricted.SoyFunction;
import com.google.template.soy.shared.restricted.SoyJavaFunction;
import com.google.template.soy.soytree.defn.LoopVar;
import com.google.template.soy.types.proto.SoyProtoType;
import com.google.template.soy.types.proto.SoyProtoValueImpl;
import java.io.IOException;
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
* SoyValue} object.
*
*/
public class EvalVisitor extends AbstractReturningExprNodeVisitor<SoyValue> {
/** Interface for a factory that creates an EvalVisitor. */
public interface EvalVisitorFactory {
/**
* Creates an EvalVisitor.
*
* @param ijData The current injected data.
* @param env The current environment.
* @return The newly created EvalVisitor instance.
*/
EvalVisitor create(@Nullable SoyRecord ijData, Environment env);
}
/** Instance of SoyValueConverter to use. */
private final SoyValueConverter valueConverter;
/** The current injected data. */
private final SoyRecord ijData;
/** The current environment. */
private final Environment env;
/**
* @param ijData The current injected data.
* @param env The current environment.
*/
protected EvalVisitor(
SoyValueConverter valueConverter, @Nullable SoyRecord ijData, Environment env) {
this.valueConverter = valueConverter;
this.ijData = ijData;
this.env = checkNotNull(env);
}
// -----------------------------------------------------------------------------------------------
// Implementation for a dummy root node.
@Override
protected SoyValue visitExprRootNode(ExprRootNode node) {
return visit(node.getRoot());
}
// -----------------------------------------------------------------------------------------------
// Implementations for primitives.
@Override
protected SoyValue visitNullNode(NullNode node) {
return NullData.INSTANCE;
}
@Override
protected SoyValue visitBooleanNode(BooleanNode node) {
return convertResult(node.getValue());
}
@Override
protected SoyValue visitIntegerNode(IntegerNode node) {
return convertResult(node.getValue());
}
@Override
protected SoyValue visitFloatNode(FloatNode node) {
return convertResult(node.getValue());
}
@Override
protected SoyValue visitStringNode(StringNode node) {
return convertResult(node.getValue());
}
@Override
protected SoyValue visitGlobalNode(GlobalNode node) {
return visit(node.getValue());
}
// -----------------------------------------------------------------------------------------------
// Implementations for collections.
@Override
protected SoyValue visitListLiteralNode(ListLiteralNode node) {
List<SoyValue> values = this.visitChildren(node);
return ListImpl.forProviderList(values);
}
@Override
protected SoyValue visitMapLiteralNode(MapLiteralNode node) {
int numItems = node.numChildren() / 2;
boolean isStringKeyed = true;
ExprNode firstNonstringKeyNode = null;
List<SoyValue> keys = Lists.newArrayListWithCapacity(numItems);
List<SoyValue> values = Lists.newArrayListWithCapacity(numItems);
for (int i = 0; i < numItems; i++) {
SoyValue key = visit(node.getChild(2 * i));
if (isStringKeyed && !(key instanceof StringData)) {
isStringKeyed = false;
firstNonstringKeyNode = node.getChild(2 * i); // temporary until we support nonstring key
}
keys.add(key);
values.add(visit(node.getChild(2 * i + 1)));
}
if (isStringKeyed) {
// Not an ImmutableMap, because map literals allow duplicate keys (last one wins).
Map<String, SoyValue> map = Maps.newLinkedHashMap();
for (int i = 0; i < numItems; i++) {
map.put(keys.get(i).stringValue(), values.get(i));
}
return DictImpl.forProviderMap(map);
} else {
// TODO: Support map literals with nonstring keys.
throw RenderException.create(
String.format(
"Currently, map literals must have string keys (key \"%s\" in map %s does not "
+ "evaluate to a string). Support for nonstring keys is a todo.",
firstNonstringKeyNode.toSourceString(), node.toSourceString()));
}
}
// -----------------------------------------------------------------------------------------------
// Implementations for data references.
@Override
protected SoyValue visitVarRefNode(VarRefNode node) {
return visitNullSafeNode(node);
}
@Override
protected SoyValue visitDataAccessNode(DataAccessNode node) {
return visitNullSafeNode(node);
}
/**
* Helper function which ensures that {@link NullSafetySentinel} instances don't escape from this
* visitor.
*
* @param node The node to evaluate.
* @return The result of evaluating the node.
*/
private SoyValue visitNullSafeNode(ExprNode node) {
SoyValue value = visitNullSafeNodeRecurse(node);
// Transform null sentinel into a normal null value.
if (value == NullSafetySentinel.INSTANCE) {
return NullData.INSTANCE;
}
return value;
}
/**
* Helper function which recursively evaluates data references. This bypasses the normal visitor
* mechanism as follows: As soon as the EvalVisitor sees a node which is a data reference, it
* calls this function which evaluates that data reference and any descendant data references,
* returning either the result of the evaluation, or a special sentinel value which indicates that
* a null-safety check failed. Internally this sentinel value is used to short-circuit evaluations
* that would otherwise fail because of the null value.
*
* <p>If any descendant node is not a data reference, then this uses the normal visitor mechanism
* to evaluate that node.
*
* <p>The reason for bypassing the normal visitor mechanism is that we want to detect the
* transition between data-reference nodes and non-data-reference nodes. So for example, if a
* FieldAccessNode has a parent node which is a data reference, we want to propagate the sentinel
* value upward, whereas if the parent is not a data reference, then we want to convert the
* sentinel value into a regular null value.
*
* @param node The node to evaluate.
* @return The result of evaluating the node.
*/
private SoyValue visitNullSafeNodeRecurse(ExprNode node) {
switch (node.getKind()) {
case VAR_REF_NODE:
return visitNullSafeVarRefNode((VarRefNode) node);
case FIELD_ACCESS_NODE:
return visitNullSafeFieldAccessNode((FieldAccessNode) node);
case ITEM_ACCESS_NODE:
return visitNullSafeItemAccessNode((ItemAccessNode) node);
default:
return visit(node);
}
}
private SoyValue visitNullSafeVarRefNode(VarRefNode varRef) {
SoyValue result = null;
if (varRef.isDollarSignIjParameter()) {
// TODO(lukes): it would be nice to move this logic into Environment or even eliminate the
// ijData == null case. It seems like this case is mostly for prerendering, though im not
// sure.
if (ijData != null) {
result = ijData.getField(varRef.getName());
} else {
throw RenderException.create(
"Injected data not provided, yet referenced (" + varRef.toSourceString() + ").");
}
} else {
return env.getVar(varRef.getDefnDecl());
}
return (result != null) ? result : UndefinedData.INSTANCE;
}
private SoyValue visitNullSafeFieldAccessNode(FieldAccessNode fieldAccess) {
SoyValue base = visitNullSafeNodeRecurse(fieldAccess.getBaseExprChild());
// attempting field access on non-SoyRecord
if (!(base instanceof SoyRecord)) {
if (base == NullSafetySentinel.INSTANCE) {
// Bail out if base expression failed a null-safety check.
return NullSafetySentinel.INSTANCE;
}
if (fieldAccess.isNullSafe()) {
if (isNullOrUndefinedBase(base)) {
// Return the sentinel value that indicates that a null-safety check failed.
return NullSafetySentinel.INSTANCE;
} else {
throw RenderException.create(
String.format(
"While evaluating \"%s\", encountered non-record just before accessing \"%s\".",
fieldAccess.toSourceString(), fieldAccess.getSourceStringSuffix()));
}
}
// This behavior is not ideal, but needed for compatibility with existing code.
// TODO: If feasible, find and fix existing instances, then throw RenderException here.
return UndefinedData.INSTANCE;
}
// base is a valid SoyRecord: get value
SoyValue value = ((SoyRecord) base).getField(fieldAccess.getFieldName());
// Note that this code treats value of null and value of NullData differently. Only the latter
// will trigger this check, which is partly why places like
// SoyProtoValueImpl.getFieldProviderInternal() and AbstractDict.getField() return null instead
// of NullData.
// TODO(user): Consider cleaning up the null / NullData inconsistencies.
if (value != null && !fieldAccess.getType().isInstance(value)) {
throw RenderException.create(
String.format(
"Expected value of type '%s', but actual type was '%s'.",
fieldAccess.getType(), value.getClass().getSimpleName()));
}
return (value != null) ? value : UndefinedData.INSTANCE;
}
private SoyValue visitNullSafeItemAccessNode(ItemAccessNode itemAccess) {
SoyValue base = visitNullSafeNodeRecurse(itemAccess.getBaseExprChild());
// attempting item access on non-SoyMap
if (!(base instanceof SoyMap)) {
if (base == NullSafetySentinel.INSTANCE) {
// Bail out if base expression failed a null-safety check.
return NullSafetySentinel.INSTANCE;
}
if (itemAccess.isNullSafe()) {
if (isNullOrUndefinedBase(base)) {
// Return the sentinel value that indicates that a null-safety check failed.
return NullSafetySentinel.INSTANCE;
} else {
throw RenderException.create(
String.format(
"While evaluating \"%s\", encountered non-map/list just before accessing \"%s\".",
itemAccess.toSourceString(), itemAccess.getSourceStringSuffix()));
}
}
// This behavior is not ideal, but needed for compatibility with existing code.
// TODO: If feasible, find and fix existing instances, then throw RenderException here.
return UndefinedData.INSTANCE;
}
// base is a valid SoyMap: get value
SoyValue key = visit(itemAccess.getKeyExprChild());
SoyValue value = ((SoyMap) base).getItem(key);
if (value != null && !itemAccess.getType().isInstance(value)) {
throw RenderException.create(
String.format(
"Expected value of type '%s', but actual type was '%s'.",
itemAccess.getType(), value.getClass().getSimpleName()));
}
return (value != null) ? value : UndefinedData.INSTANCE;
}
// Returns true if the base SoyValue of a data access chain is null or undefined.
private static boolean isNullOrUndefinedBase(SoyValue base) {
return base == null
|| base instanceof NullData
|| base instanceof UndefinedData
|| base == NullSafetySentinel.INSTANCE;
}
// -----------------------------------------------------------------------------------------------
// Implementations for operators.
@Override
protected SoyValue visitNegativeOpNode(NegativeOpNode node) {
return negative(visit(node.getChild(0)));
}
@Override
protected SoyValue visitNotOpNode(NotOpNode node) {
SoyValue operand = visit(node.getChild(0));
return convertResult(!operand.coerceToBoolean());
}
@Override
protected SoyValue visitTimesOpNode(TimesOpNode node) {
return times(visit(node.getChild(0)), visit(node.getChild(1)));
}
@Override
protected SoyValue visitDivideByOpNode(DivideByOpNode node) {
return FloatData.forValue(dividedBy(visit(node.getChild(0)), visit(node.getChild(1))));
}
@Override
protected SoyValue visitModOpNode(ModOpNode node) {
SoyValue operand0 = visit(node.getChild(0));
SoyValue operand1 = visit(node.getChild(1));
return convertResult(operand0.longValue() % operand1.longValue());
}
@Override
protected SoyValue visitPlusOpNode(PlusOpNode node) {
return plus(visit(node.getChild(0)), visit(node.getChild(1)));
}
@Override
protected SoyValue visitMinusOpNode(MinusOpNode node) {
return minus(visit(node.getChild(0)), visit(node.getChild(1)));
}
@Override
protected SoyValue visitLessThanOpNode(LessThanOpNode node) {
return BooleanData.forValue(lessThan(visit(node.getChild(0)), visit(node.getChild(1))));
}
@Override
protected SoyValue visitGreaterThanOpNode(GreaterThanOpNode node) {
// note the argument reversal
return BooleanData.forValue(lessThan(visit(node.getChild(1)), visit(node.getChild(0))));
}
@Override
protected SoyValue visitLessThanOrEqualOpNode(LessThanOrEqualOpNode node) {
return BooleanData.forValue(lessThanOrEqual(visit(node.getChild(0)), visit(node.getChild(1))));
}
@Override
protected SoyValue visitGreaterThanOrEqualOpNode(GreaterThanOrEqualOpNode node) {
// note the argument reversal
return BooleanData.forValue(lessThanOrEqual(visit(node.getChild(1)), visit(node.getChild(0))));
}
@Override
protected SoyValue visitEqualOpNode(EqualOpNode node) {
return convertResult(equal(visit(node.getChild(0)), visit(node.getChild(1))));
}
@Override
protected SoyValue visitNotEqualOpNode(NotEqualOpNode node) {
return convertResult(!equal(visit(node.getChild(0)), visit(node.getChild(1))));
}
@Override
protected SoyValue visitAndOpNode(AndOpNode node) {
// Note: Short-circuit evaluation.
SoyValue operand0 = visit(node.getChild(0));
if (!operand0.coerceToBoolean()) {
return convertResult(false);
} else {
SoyValue operand1 = visit(node.getChild(1));
return convertResult(operand1.coerceToBoolean());
}
}
@Override
protected SoyValue visitOrOpNode(OrOpNode node) {
// Note: Short-circuit evaluation.
SoyValue operand0 = visit(node.getChild(0));
if (operand0.coerceToBoolean()) {
return convertResult(true);
} else {
SoyValue operand1 = visit(node.getChild(1));
return convertResult(operand1.coerceToBoolean());
}
}
@Override
protected SoyValue visitConditionalOpNode(ConditionalOpNode node) {
// Note: We only evaluate the part that we need.
SoyValue operand0 = visit(node.getChild(0));
if (operand0.coerceToBoolean()) {
return visit(node.getChild(1));
} else {
return visit(node.getChild(2));
}
}
@Override
protected SoyValue visitNullCoalescingOpNode(NullCoalescingOpNode node) {
SoyValue operand0 = visit(node.getChild(0));
// identical to the implementation of isNonnull
if (operand0 instanceof NullData || operand0 instanceof UndefinedData) {
return visit(node.getChild(1));
}
return operand0;
}
// -----------------------------------------------------------------------------------------------
// Implementations for functions.
@Override
protected SoyValue visitFunctionNode(FunctionNode node) {
SoyFunction soyFunction = node.getSoyFunction();
// Handle nonplugin functions.
if (soyFunction instanceof BuiltinFunction) {
BuiltinFunction nonpluginFn = (BuiltinFunction) soyFunction;
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));
case CHECK_NOT_NULL:
return visitCheckNotNullFunction(node.getChild(0));
case V1_EXPRESSION:
throw new UnsupportedOperationException(
"the v1Expression function can't be used in templates compiled to Java");
default:
throw new AssertionError();
}
} else if (soyFunction instanceof SoyJavaFunction) {
List<SoyValue> args = this.visitChildren(node);
SoyJavaFunction fn = (SoyJavaFunction) soyFunction;
// Note: Arity has already been checked by CheckFunctionCallsVisitor.
return computeFunctionHelper(fn, args, node);
} else {
throw RenderException.create(
"Failed to find Soy function with name '"
+ node.getFunctionName()
+ "'"
+ " (function call \""
+ node.toSourceString()
+ "\").");
}
}
@Override
protected SoyValue visitProtoInitNode(ProtoInitNode node) {
// The downcast is safe because if it was anything else, compilation would have already failed.
SoyProtoType soyProto = (SoyProtoType) node.getType();
ImmutableList<String> paramNames = node.getParamNames();
SoyProtoValueImpl.Builder builder = new SoyProtoValueImpl.Builder(valueConverter, soyProto);
for (int i = 0; i < node.numChildren(); i++) {
SoyValue visit = visit(node.getChild(i));
// null means don't assign
if (visit instanceof NullData || visit instanceof UndefinedData) {
continue;
}
builder.setField(paramNames.get(i), visit);
}
return builder.build();
}
private SoyValue visitCheckNotNullFunction(ExprNode child) {
SoyValue childValue = visit(child);
if (childValue instanceof NullData || childValue instanceof UndefinedData) {
throw new SoyDataException(child.toSourceString() + " is null");
}
return childValue;
}
/**
* 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 SoyValue computeFunctionHelper(
SoyJavaFunction fn, List<SoyValue> args, FunctionNode fnNode) {
try {
return fn.computeForJava(args);
} catch (Exception e) {
throw RenderException.create(
"While computing function \"" + fnNode.toSourceString() + "\": " + e.getMessage(), e);
}
}
private SoyValue visitIsFirstFunction(FunctionNode node) {
int localVarIndex;
try {
VarRefNode dataRef = (VarRefNode) node.getChild(0);
localVarIndex = env.getIndex((LoopVar) dataRef.getDefnDecl());
} catch (Exception e) {
throw RenderException.create(
"Failed to evaluate function call " + node.toSourceString() + ".", e);
}
return convertResult(localVarIndex == 0);
}
private SoyValue visitIsLastFunction(FunctionNode node) {
boolean isLast;
try {
VarRefNode dataRef = (VarRefNode) node.getChild(0);
isLast = env.isLast((LoopVar) dataRef.getDefnDecl());
} catch (Exception e) {
throw RenderException.create(
"Failed to evaluate function call " + node.toSourceString() + ".", e);
}
return convertResult(isLast);
}
private SoyValue visitIndexFunction(FunctionNode node) {
int localVarIndex;
try {
VarRefNode dataRef = (VarRefNode) node.getChild(0);
localVarIndex = env.getIndex((LoopVar) dataRef.getDefnDecl());
} catch (Exception e) {
throw RenderException.create(
"Failed to evaluate function call " + node.toSourceString() + ".", e);
}
return convertResult(localVarIndex);
}
// -----------------------------------------------------------------------------------------------
// Private helpers.
/**
* Private helper to convert a boolean result.
*
* @param b The boolean to convert.
*/
private SoyValue convertResult(boolean b) {
return BooleanData.forValue(b);
}
/**
* Private helper to convert an integer result.
*
* @param i The integer to convert.
*/
private SoyValue convertResult(long i) {
return IntegerData.forValue(i);
}
/**
* Private helper to convert a float result.
*
* @param f The float to convert.
*/
private SoyValue convertResult(double f) {
return FloatData.forValue(f);
}
/**
* Private helper to convert a string result.
*
* @param s The string to convert.
*/
private SoyValue convertResult(String s) {
return StringData.forValue(s);
}
/**
* Class that represents a sentinel value indicating that a null-safety check failed. This value
* should never "leak" outside this class, in other words, no code outside of this class should
* ever see a value of this type.
*/
private static final class NullSafetySentinel extends SoyAbstractValue {
/** Static singleton instance of SafeNullData. */
public static final NullSafetySentinel INSTANCE = new NullSafetySentinel();
private NullSafetySentinel() {}
@Override
public boolean equals(Object other) {
return other == this;
}
@Override
public int hashCode() {
return System.identityHashCode(this);
}
@Override
public boolean coerceToBoolean() {
return false;
}
@Override
public String coerceToString() {
return "null";
}
@Override
public void render(Appendable appendable) throws IOException {
appendable.append(coerceToString());
}
}
}