/*
* Copyright 2015 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.pysrc.internal;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.template.soy.error.ErrorReporter;
import com.google.template.soy.error.SoyErrorKind;
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.ExprNode.OperatorNode;
import com.google.template.soy.exprtree.ExprNode.PrimitiveNode;
import com.google.template.soy.exprtree.ExprRootNode;
import com.google.template.soy.exprtree.FieldAccessNode;
import com.google.template.soy.exprtree.FunctionNode;
import com.google.template.soy.exprtree.GlobalNode;
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.Operator;
import com.google.template.soy.exprtree.Operator.Operand;
import com.google.template.soy.exprtree.Operator.SyntaxElement;
import com.google.template.soy.exprtree.OperatorNodes.ConditionalOpNode;
import com.google.template.soy.exprtree.OperatorNodes.EqualOpNode;
import com.google.template.soy.exprtree.OperatorNodes.NotEqualOpNode;
import com.google.template.soy.exprtree.OperatorNodes.NullCoalescingOpNode;
import com.google.template.soy.exprtree.OperatorNodes.PlusOpNode;
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.pysrc.restricted.PyExpr;
import com.google.template.soy.pysrc.restricted.PyExprUtils;
import com.google.template.soy.pysrc.restricted.PyFunctionExprBuilder;
import com.google.template.soy.pysrc.restricted.PyStringExpr;
import com.google.template.soy.pysrc.restricted.SoyPySrcFunction;
import com.google.template.soy.shared.internal.BuiltinFunction;
import com.google.template.soy.shared.restricted.SoyFunction;
import com.google.template.soy.types.SoyType;
import com.google.template.soy.types.SoyType.Kind;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Visitor for translating a Soy expression (in the form of an {@link ExprNode}) into an equivalent
* Python expression.
*
*/
public final class TranslateToPyExprVisitor extends AbstractReturningExprNodeVisitor<PyExpr> {
private static final SoyErrorKind PROTO_ACCESS_NOT_SUPPORTED =
SoyErrorKind.of("Proto accessors are not supported in pysrc.");
private static final SoyErrorKind PROTO_INIT_NOT_SUPPORTED =
SoyErrorKind.of("Proto init is not supported in pysrc.");
private static final SoyErrorKind SOY_PY_SRC_FUNCTION_NOT_FOUND =
SoyErrorKind.of("Failed to find SoyPySrcFunction ''{0}''.");
/**
* Errors in this visitor generate Python source that immediately explodes. Users of Soy are
* expected to check the error reporter before using the gencode; if they don't, this should
* apprise them. TODO(brndn): consider changing the visitor to return {@code Optional<PyExpr>} and
* returning {@link Optional#absent()} on error.
*/
private static final PyExpr ERROR =
new PyExpr("raise Exception('Soy compilation failed')", Integer.MAX_VALUE);
private final LocalVariableStack localVarExprs;
private final ErrorReporter errorReporter;
TranslateToPyExprVisitor(LocalVariableStack localVarExprs, ErrorReporter errorReporter) {
this.errorReporter = errorReporter;
this.localVarExprs = localVarExprs;
}
// -----------------------------------------------------------------------------------------------
// Implementation for a dummy root node.
@Override
protected PyExpr visitExprRootNode(ExprRootNode node) {
return visit(node.getRoot());
}
// -----------------------------------------------------------------------------------------------
// Implementations for primitives.
@Override
protected PyExpr visitPrimitiveNode(PrimitiveNode node) {
// Note: ExprNode.toSourceString() technically returns a Soy expression. In the case of
// primitives, the result is usually also the correct Python expression.
return new PyExpr(node.toSourceString(), Integer.MAX_VALUE);
}
@Override
protected PyExpr visitStringNode(StringNode node) {
return new PyStringExpr(node.toSourceString());
}
@Override
protected PyExpr visitNullNode(NullNode node) {
// Nulls are represented as 'None' in Python.
return new PyExpr("None", Integer.MAX_VALUE);
}
@Override
protected PyExpr visitBooleanNode(BooleanNode node) {
// Specifically set booleans to 'True' and 'False' given python's strict naming for booleans.
return new PyExpr(node.getValue() ? "True" : "False", Integer.MAX_VALUE);
}
// -----------------------------------------------------------------------------------------------
// Implementations for collections.
@Override
protected PyExpr visitListLiteralNode(ListLiteralNode node) {
return PyExprUtils.convertIterableToPyListExpr(
Iterables.transform(
node.getChildren(),
new Function<ExprNode, PyExpr>() {
@Override
public PyExpr apply(ExprNode node) {
return visit(node);
}
}));
}
@Override
protected PyExpr visitMapLiteralNode(MapLiteralNode node) {
Preconditions.checkArgument(node.numChildren() % 2 == 0);
Map<PyExpr, PyExpr> dict = new LinkedHashMap<>();
for (int i = 0, n = node.numChildren(); i < n; i += 2) {
ExprNode keyNode = node.getChild(i);
ExprNode valueNode = node.getChild(i + 1);
dict.put(visit(keyNode), visit(valueNode));
}
return PyExprUtils.convertMapToOrderedDict(dict);
}
// -----------------------------------------------------------------------------------------------
// Implementations for data references.
@Override
protected PyExpr visitVarRefNode(VarRefNode node) {
return visitNullSafeNode(node);
}
@Override
protected PyExpr visitDataAccessNode(DataAccessNode node) {
return visitNullSafeNode(node);
}
private PyExpr visitNullSafeNode(ExprNode node) {
StringBuilder nullSafetyPrefix = new StringBuilder();
String refText = visitNullSafeNodeRecurse(node, nullSafetyPrefix);
if (nullSafetyPrefix.length() == 0) {
return new PyExpr(refText, Integer.MAX_VALUE);
} else {
return new PyExpr(
nullSafetyPrefix + refText, PyExprUtils.pyPrecedenceForOperator(Operator.CONDITIONAL));
}
}
private String visitNullSafeNodeRecurse(ExprNode node, StringBuilder nullSafetyPrefix) {
switch (node.getKind()) {
case VAR_REF_NODE:
{
VarRefNode varRef = (VarRefNode) node;
if (varRef.isInjected()) {
// Case 1: Injected data reference.
return genCodeForLiteralKeyAccess("ijData", varRef.getName());
} else {
PyExpr translation = localVarExprs.getVariableExpression(varRef.getName());
if (translation != null) {
// Case 2: In-scope local var.
return translation.getText();
} else {
// Case 3: Data reference.
return genCodeForLiteralKeyAccess("data", varRef.getName());
}
}
}
case FIELD_ACCESS_NODE:
case ITEM_ACCESS_NODE:
{
DataAccessNode dataAccess = (DataAccessNode) node;
// First recursively visit base expression.
String refText =
visitNullSafeNodeRecurse(dataAccess.getBaseExprChild(), nullSafetyPrefix);
// Generate null safety check for base expression.
if (dataAccess.isNullSafe()) {
nullSafetyPrefix.append("None if ").append(refText).append(" is None else ");
}
// Generate access to field
if (node.getKind() == ExprNode.Kind.FIELD_ACCESS_NODE) {
FieldAccessNode fieldAccess = (FieldAccessNode) node;
return genCodeForFieldAccess(
fieldAccess,
fieldAccess.getBaseExprChild().getType(),
refText,
fieldAccess.getFieldName());
} else {
ItemAccessNode itemAccess = (ItemAccessNode) node;
Kind baseKind = itemAccess.getBaseExprChild().getType().getKind();
PyExpr keyPyExpr = visit(itemAccess.getKeyExprChild());
if (baseKind == Kind.MAP || baseKind == Kind.RECORD) {
return genCodeForKeyAccess(refText, keyPyExpr.getText());
} else {
return new PyFunctionExprBuilder("runtime.key_safe_data_access")
.addArg(new PyExpr(refText, Integer.MAX_VALUE))
.addArg(keyPyExpr)
.build();
}
}
}
default:
{
PyExpr value = visit(node);
return PyExprUtils.maybeProtect(value, Integer.MAX_VALUE).getText();
}
}
}
@Override
protected PyExpr visitGlobalNode(GlobalNode node) {
return visit(node.getValue());
}
// -----------------------------------------------------------------------------------------------
// Implementations for operators.
@Override
protected PyExpr visitOperatorNode(OperatorNode node) {
return genPyExprUsingSoySyntax(node);
}
@Override
protected PyExpr visitNullCoalescingOpNode(NullCoalescingOpNode node) {
List<PyExpr> children = visitChildren(node);
PyExpr conditionalExpr = PyExprUtils.genPyNotNullCheck(children.get(0));
PyExpr trueExpr = children.get(0);
PyExpr falseExpr = children.get(1);
// TODO(dcphillips): unlike jssrc,Tofu and jbcsrc pysrc evaluates the condition twice. It would
// be nice to avoid that. Obvious solutions include.
// 1. Introduce a local variable:
// tmp = <left hand side>
// if tmp is None:
// tmp = <right hand side>
//
// 2. Use a lambda to defer evaluation of the right hand side.
// lambda x=<left hand side> : <right hand side> if x is None else x
return genTernaryConditional(conditionalExpr, trueExpr, falseExpr);
}
@Override
protected PyExpr visitEqualOpNode(EqualOpNode node) {
// Python has stricter type casting rules during equality comparison. To get around this we
// use our custom utility to emulate the behavior of Soy/JS.
List<PyExpr> operandPyExprs = visitChildren(node);
return new PyExpr(
"runtime.type_safe_eq("
+ operandPyExprs.get(0).getText()
+ ", "
+ operandPyExprs.get(1).getText()
+ ")",
Integer.MAX_VALUE);
}
@Override
protected PyExpr visitNotEqualOpNode(NotEqualOpNode node) {
// Invert type_safe_eq.
List<PyExpr> operandPyExprs = visitChildren(node);
return new PyExpr(
"not runtime.type_safe_eq("
+ operandPyExprs.get(0).getText()
+ ", "
+ operandPyExprs.get(1).getText()
+ ")",
PyExprUtils.pyPrecedenceForOperator(Operator.NOT));
}
@Override
protected PyExpr visitPlusOpNode(PlusOpNode node) {
// Python has stricter type casting between strings and other primitives than Soy, so addition
// must be sent through the type_safe_add utility to emulate that behavior.
List<PyExpr> operandPyExprs = visitChildren(node);
return new PyExpr(
"runtime.type_safe_add("
+ operandPyExprs.get(0).getText()
+ ", "
+ operandPyExprs.get(1).getText()
+ ")",
Integer.MAX_VALUE);
}
@Override
protected PyExpr visitConditionalOpNode(ConditionalOpNode node) {
// Retrieve the operands.
Operator op = Operator.CONDITIONAL;
List<SyntaxElement> syntax = op.getSyntax();
List<PyExpr> operandExprs = visitChildren(node);
Operand conditionalOperand = ((Operand) syntax.get(0));
PyExpr conditionalExpr = operandExprs.get(conditionalOperand.getIndex());
Operand trueOperand = ((Operand) syntax.get(4));
PyExpr trueExpr = operandExprs.get(trueOperand.getIndex());
Operand falseOperand = ((Operand) syntax.get(8));
PyExpr falseExpr = operandExprs.get(falseOperand.getIndex());
return genTernaryConditional(conditionalExpr, trueExpr, falseExpr);
}
/**
* {@inheritDoc}
*
* <p>The source of available functions is a look-up map provided by Guice in {@link
* SharedModule#provideSoyFunctionsMap}.
*
* @see BuiltinFunction
* @see SoyPySrcFunction
*/
@Override
protected PyExpr visitFunctionNode(FunctionNode node) {
SoyFunction soyFunction = node.getSoyFunction();
if (soyFunction instanceof BuiltinFunction) {
return visitNonPluginFunction(node, (BuiltinFunction) soyFunction);
} else if (soyFunction instanceof SoyPySrcFunction) {
List<PyExpr> args = visitChildren(node);
return ((SoyPySrcFunction) soyFunction).computeForPySrc(args);
} else {
errorReporter.report(
node.getSourceLocation(), SOY_PY_SRC_FUNCTION_NOT_FOUND, node.getFunctionName());
return ERROR;
}
}
private PyExpr visitNonPluginFunction(FunctionNode node, BuiltinFunction nonpluginFn) {
switch (nonpluginFn) {
case IS_FIRST:
return visitForEachFunction(node, "__isFirst");
case IS_LAST:
return visitForEachFunction(node, "__isLast");
case INDEX:
return visitForEachFunction(node, "__index");
case QUOTE_KEYS_IF_JS:
// 'quoteKeysIfJs' is ignored in Python.
return visitMapLiteralNode((MapLiteralNode) node.getChild(0));
case CHECK_NOT_NULL:
return visitCheckNotNullFunction(node);
case V1_EXPRESSION:
throw new UnsupportedOperationException(
"the v1Expression function can't be used in templates compiled to Python");
default:
throw new AssertionError();
}
}
private PyExpr visitCheckNotNullFunction(FunctionNode node) {
PyExpr childExpr = visit(node.getChild(0));
return new PyFunctionExprBuilder("runtime.check_not_null").addArg(childExpr).asPyExpr();
}
private PyExpr visitForEachFunction(FunctionNode node, String suffix) {
String varName = ((VarRefNode) node.getChild(0)).getName();
return localVarExprs.getVariableExpression(varName + suffix);
}
/**
* Generates the code for key access given a key literal, e.g. {@code .get('key')}.
*
* @param key the String literal value to be used as a key
*/
private static String genCodeForLiteralKeyAccess(String containerExpr, String key) {
return genCodeForKeyAccess(containerExpr, "'" + key + "'");
}
/**
* Generates the code for key access given the name of a variable to be used as a key, e.g. {@code
* .get(key)}.
*
* @param keyName the variable name to be used as a key
*/
private static String genCodeForKeyAccess(String containerExpr, String keyName) {
return containerExpr + ".get(" + keyName + ")";
}
/**
* Generates the code for a field name access, e.g. ".foo" or "['bar']".
*
* @param node the field access source node
* @param baseType the type of the object that contains the field
* @param containerExpr an expression that evaluates to the container of the named field. This
* expression may have any operator precedence that binds more tightly than exponentiation.
* @param fieldName the field name
*/
private String genCodeForFieldAccess(
ExprNode node, SoyType baseType, String containerExpr, String fieldName) {
if (baseType != null && baseType.getKind() == SoyType.Kind.PROTO) {
errorReporter.report(node.getSourceLocation(), PROTO_ACCESS_NOT_SUPPORTED);
return ".ERROR";
}
return genCodeForLiteralKeyAccess(containerExpr, fieldName);
}
/**
* Generates a Python expression for the given OperatorNode's subtree assuming that the Python
* expression for the operator uses the same syntax format as the Soy operator.
*
* @param opNode the OperatorNode whose subtree to generate a Python expression for
* @return the generated Python expression
*/
private PyExpr genPyExprUsingSoySyntax(OperatorNode opNode) {
List<PyExpr> operandPyExprs = visitChildren(opNode);
String newExpr = PyExprUtils.genExprWithNewToken(opNode.getOperator(), operandPyExprs, null);
return new PyExpr(newExpr, PyExprUtils.pyPrecedenceForOperator(opNode.getOperator()));
}
/**
* Generates a ternary conditional Python expression given the conditional and true/false
* expressions.
*
* @param conditionalExpr the conditional expression
* @param trueExpr the expression to execute if the conditional executes to true
* @param falseExpr the expression to execute if the conditional executes to false
* @return a ternary conditional expression
*/
private PyExpr genTernaryConditional(PyExpr conditionalExpr, PyExpr trueExpr, PyExpr falseExpr) {
// Python's ternary operator switches the order from <conditional> ? <true> : <false> to
// <true> if <conditional> else <false>.
int conditionalPrecedence = PyExprUtils.pyPrecedenceForOperator(Operator.CONDITIONAL);
StringBuilder exprSb =
new StringBuilder()
.append(PyExprUtils.maybeProtect(trueExpr, conditionalPrecedence).getText())
.append(" if ")
.append(PyExprUtils.maybeProtect(conditionalExpr, conditionalPrecedence).getText())
.append(" else ")
.append(PyExprUtils.maybeProtect(falseExpr, conditionalPrecedence).getText());
return new PyExpr(exprSb.toString(), conditionalPrecedence);
}
@Override
protected PyExpr visitProtoInitNode(ProtoInitNode node) {
errorReporter.report(node.getSourceLocation(), PROTO_INIT_NOT_SUPPORTED);
return ERROR;
}
}