/*
* 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.jbcsrc;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.template.soy.jbcsrc.BytecodeUtils.NULL_POINTER_EXCEPTION_TYPE;
import static com.google.template.soy.jbcsrc.BytecodeUtils.compare;
import static com.google.template.soy.jbcsrc.BytecodeUtils.constant;
import static com.google.template.soy.jbcsrc.BytecodeUtils.firstNonNull;
import static com.google.template.soy.jbcsrc.BytecodeUtils.logicalNot;
import static com.google.template.soy.jbcsrc.BytecodeUtils.ternary;
import com.google.common.base.Optional;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.Iterables;
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.exprtree.BooleanNode;
import com.google.template.soy.exprtree.DataAccessNode;
import com.google.template.soy.exprtree.ExprNode;
import com.google.template.soy.exprtree.ExprNode.ParentExprNode;
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.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.jbcsrc.ExpressionDetacher.BasicDetacher;
import com.google.template.soy.soytree.defn.InjectedParam;
import com.google.template.soy.soytree.defn.LocalVar;
import com.google.template.soy.soytree.defn.TemplateParam;
import com.google.template.soy.types.SoyType.Kind;
import com.google.template.soy.types.SoyTypes;
import com.google.template.soy.types.aggregate.ListType;
import com.google.template.soy.types.primitive.UnknownType;
import com.google.template.soy.types.proto.SoyProtoType;
import java.util.ArrayList;
import java.util.List;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
/**
* Compiles an {@link ExprNode} to a {@link SoyExpression}.
*
* <p>A note on how we use soy types. We generally try to limit the places where we read type
* information from the AST. This is because it tends to be not very accurate. Specifically, the
* places where we rely on type information from the AST are:
*
* <ul>
* <li>{@link VarRefNode}
* <li>{@link PrimitiveNode}
* <li>{@link DataAccessNode}
* </ul>
*
* <p>This is because these are the points that are most likely to be in direct control of the user.
* All other type information is derived directly from operations on SoyExpression objects.
*/
final class ExpressionCompiler {
static final class BasicExpressionCompiler {
private final CompilerVisitor compilerVisitor;
private BasicExpressionCompiler(
TemplateParameterLookup parameters, TemplateVariableManager varManager) {
this.compilerVisitor =
new CompilerVisitor(
parameters,
varManager,
new PluginFunctionCompiler(parameters),
Suppliers.ofInstance(BasicDetacher.INSTANCE));
}
private BasicExpressionCompiler(CompilerVisitor visitor) {
this.compilerVisitor = visitor;
}
/** Compile an expression. */
SoyExpression compile(ExprNode expr) {
return compilerVisitor.exec(expr);
}
/**
* Returns an expression that evaluates to a {@code List<SoyValue>} containing all the children.
*/
Expression compileToList(List<? extends ExprNode> children) {
List<SoyExpression> soyExprs = new ArrayList<>(children.size());
for (ExprNode expr : children) {
soyExprs.add(compile(expr));
}
return SoyExpression.asBoxedList(soyExprs);
}
}
/**
* Create an expression compiler that can implement complex detaching logic with the given {@link
* ExpressionDetacher.Factory}
*/
static ExpressionCompiler create(
ExpressionDetacher.Factory detacherFactory,
TemplateParameterLookup parameters,
TemplateVariableManager varManager) {
return new ExpressionCompiler(detacherFactory, parameters, varManager);
}
/**
* Create a basic compiler with trivial detaching logic.
*
* <p>All generated detach points are implemented as {@code return} statements and the returned
* value is boxed, so it is only valid for use by the {@link LazyClosureCompiler}.
*/
static BasicExpressionCompiler createBasicCompiler(
TemplateParameterLookup parameters, TemplateVariableManager varManager) {
return new BasicExpressionCompiler(parameters, varManager);
}
private final TemplateParameterLookup parameters;
private final TemplateVariableManager varManager;
private final ExpressionDetacher.Factory detacherFactory;
private ExpressionCompiler(
ExpressionDetacher.Factory detacherFactory,
TemplateParameterLookup parameters,
TemplateVariableManager varManager) {
this.detacherFactory = detacherFactory;
this.parameters = parameters;
this.varManager = varManager;
}
/**
* Compiles the given expression tree to a sequence of bytecode.
*
* <p>The reattachPoint should be {@link CodeBuilder#mark(Label) marked} by the caller at a
* location where the stack depth is 0 and will be used to 'reattach' execution if the compiled
* expression needs to perform a detach operation.
*/
SoyExpression compile(ExprNode node, Label reattachPoint) {
return asBasicCompiler(reattachPoint).compile(node);
}
/**
* Compiles the given expression tree to a sequence of bytecode if it can be done without
* generating any detach operations.
*/
Optional<SoyExpression> compileWithNoDetaches(ExprNode node) {
checkNotNull(node);
if (RequiresDetachVisitor.INSTANCE.exec(node)) {
return Optional.absent();
}
Supplier<ExpressionDetacher> throwingSupplier =
new Supplier<ExpressionDetacher>() {
@Override
public ExpressionDetacher get() {
throw new AssertionError();
}
};
return Optional.of(
new CompilerVisitor(
parameters, varManager, new PluginFunctionCompiler(parameters), throwingSupplier)
.exec(node));
}
/**
* Returns a {@link BasicExpressionCompiler} that can be used to compile multiple expressions all
* with the same detach logic.
*/
BasicExpressionCompiler asBasicCompiler(final Label reattachPoint) {
return new BasicExpressionCompiler(
new CompilerVisitor(
parameters,
varManager,
new PluginFunctionCompiler(parameters),
// Use a lazy supplier to allocate the expression detacher on demand. Allocating the
// detacher eagerly creates detach points so we want to delay until definitely
// neccesary.
Suppliers.memoize(
new Supplier<ExpressionDetacher>() {
@Override
public ExpressionDetacher get() {
return detacherFactory.createExpressionDetacher(reattachPoint);
}
})));
}
/**
* Compiles the given expression tree to a sequence of bytecode in the current method visitor.
*
* <p>The generated bytecode expects that the evaluation stack is empty when this method is called
* and it will generate code such that the stack contains a single SoyValue when it returns. The
* SoyValue object will have a runtime type equal to {@code node.getType().javaType()}.
*/
SoyExpression compile(ExprNode node) {
Label reattachPoint = new Label();
final SoyExpression exec = compile(node, reattachPoint);
return exec.withSource(exec.labelStart(reattachPoint));
}
private static final class CompilerVisitor
extends EnhancedAbstractExprNodeVisitor<SoyExpression> {
final Supplier<? extends ExpressionDetacher> detacher;
final TemplateParameterLookup parameters;
final TemplateVariableManager varManager;
final PluginFunctionCompiler functions;
CompilerVisitor(
TemplateParameterLookup parameters,
TemplateVariableManager varManager,
PluginFunctionCompiler functions,
Supplier<? extends ExpressionDetacher> detacher) {
this.detacher = detacher;
this.parameters = parameters;
this.varManager = varManager;
this.functions = functions;
}
@Override
protected final SoyExpression visitExprRootNode(ExprRootNode node) {
return visit(node.getRoot());
}
// Primitive value constants
@Override
protected final SoyExpression visitNullNode(NullNode node) {
return SoyExpression.NULL;
}
@Override
protected final SoyExpression visitFloatNode(FloatNode node) {
return SoyExpression.forFloat(constant(node.getValue()));
}
@Override
protected final SoyExpression visitStringNode(StringNode node) {
return SoyExpression.forString(constant(node.getValue(), varManager));
}
@Override
protected final SoyExpression visitBooleanNode(BooleanNode node) {
return node.getValue() ? SoyExpression.TRUE : SoyExpression.FALSE;
}
@Override
protected final SoyExpression visitIntegerNode(IntegerNode node) {
return SoyExpression.forInt(BytecodeUtils.constant(node.getValue()));
}
@Override
protected final SoyExpression visitGlobalNode(GlobalNode node) {
return visit(node.getValue());
}
// Collection literals
@Override
protected final SoyExpression visitListLiteralNode(ListLiteralNode node) {
// TODO(lukes): this should really box the children as SoyValueProviders, we are boxing them
// anyway and could additionally delay detach generation. Ditto for MapLiteralNode.
return SoyExpression.forList(
(ListType) node.getType(), SoyExpression.asBoxedList(visitChildren(node)));
}
@Override
protected final SoyExpression visitMapLiteralNode(MapLiteralNode node) {
// map literals are either records (if all the strings are literals) or maps if they aren't
// constants.
final int numItems = node.numChildren() / 2;
if (numItems == 0) {
return SoyExpression.forSoyValue(node.getType(), FieldRef.EMPTY_DICT.accessor());
}
boolean isRecord = node.getType().getKind() == Kind.RECORD;
List<Expression> keys = new ArrayList<>(numItems);
List<Expression> values = new ArrayList<>(numItems);
for (int i = 0; i < numItems; i++) {
// Keys are strings and values are boxed SoyValues
// Note: The soy grammar and type system both allow for maps to have arbitrary keys for
// types but none of the implementations support this. So we don't support it either.
// b/20468013
keys.add(visit(node.getChild(2 * i)).unboxAs(String.class));
values.add(visit(node.getChild(2 * i + 1)).box());
}
Expression soyDict =
MethodRef.DICT_IMPL_FOR_PROVIDER_MAP.invoke(BytecodeUtils.newLinkedHashMap(keys, values));
if (isRecord) {
return SoyExpression.forSoyValue(node.getType(), soyDict);
}
return SoyExpression.forSoyValue(node.getType(), soyDict);
}
// Comparison operators
@Override
protected final SoyExpression visitEqualOpNode(EqualOpNode node) {
return SoyExpression.forBool(
BytecodeUtils.compareSoyEquals(visit(node.getChild(0)), visit(node.getChild(1))));
}
@Override
protected final SoyExpression visitNotEqualOpNode(NotEqualOpNode node) {
return SoyExpression.forBool(
logicalNot(
BytecodeUtils.compareSoyEquals(visit(node.getChild(0)), visit(node.getChild(1)))));
}
// binary comparison operators. N.B. it is ok to coerce 'number' values to floats because that
// coercion preserves ordering
@Override
protected final SoyExpression visitLessThanOpNode(LessThanOpNode node) {
SoyExpression left = visit(node.getChild(0));
SoyExpression right = visit(node.getChild(1));
if (left.assignableToNullableInt() && right.assignableToNullableInt()) {
return SoyExpression.forBool(
compare(Opcodes.IFLT, left.unboxAs(long.class), right.unboxAs(long.class)));
}
if (left.assignableToNullableNumber() && right.assignableToNullableNumber()) {
return SoyExpression.forBool(
compare(Opcodes.IFLT, left.coerceToDouble(), right.coerceToDouble()));
}
return SoyExpression.forBool(MethodRef.RUNTIME_LESS_THAN.invoke(left.box(), right.box()));
}
@Override
protected final SoyExpression visitGreaterThanOpNode(GreaterThanOpNode node) {
SoyExpression left = visit(node.getChild(0));
SoyExpression right = visit(node.getChild(1));
if (left.assignableToNullableInt() && right.assignableToNullableInt()) {
return SoyExpression.forBool(
compare(Opcodes.IFGT, left.unboxAs(long.class), right.unboxAs(long.class)));
}
if (left.assignableToNullableNumber() && right.assignableToNullableNumber()) {
return SoyExpression.forBool(
compare(Opcodes.IFGT, left.coerceToDouble(), right.coerceToDouble()));
}
// Note the argument reversal
return SoyExpression.forBool(MethodRef.RUNTIME_LESS_THAN.invoke(right.box(), left.box()));
}
@Override
protected final SoyExpression visitLessThanOrEqualOpNode(LessThanOrEqualOpNode node) {
SoyExpression left = visit(node.getChild(0));
SoyExpression right = visit(node.getChild(1));
if (left.assignableToNullableInt() && right.assignableToNullableInt()) {
return SoyExpression.forBool(
compare(Opcodes.IFLE, left.unboxAs(long.class), right.unboxAs(long.class)));
}
if (left.assignableToNullableNumber() && right.assignableToNullableNumber()) {
return SoyExpression.forBool(
compare(Opcodes.IFLE, left.coerceToDouble(), right.coerceToDouble()));
}
return SoyExpression.forBool(
MethodRef.RUNTIME_LESS_THAN_OR_EQUAL.invoke(left.box(), right.box()));
}
@Override
protected final SoyExpression visitGreaterThanOrEqualOpNode(GreaterThanOrEqualOpNode node) {
SoyExpression left = visit(node.getChild(0));
SoyExpression right = visit(node.getChild(1));
if (left.assignableToNullableInt() && right.assignableToNullableInt()) {
return SoyExpression.forBool(
compare(Opcodes.IFGE, left.unboxAs(long.class), right.unboxAs(long.class)));
}
if (left.assignableToNullableNumber() && right.assignableToNullableNumber()) {
return SoyExpression.forBool(
compare(Opcodes.IFGE, left.coerceToDouble(), right.coerceToDouble()));
}
// Note the reversal of the arguments.
return SoyExpression.forBool(
MethodRef.RUNTIME_LESS_THAN_OR_EQUAL.invoke(right.box(), left.box()));
}
// Binary operators
// For the binary math operators we try to do unboxed arithmetic as much as possible.
// If both args are definitely ints -> do int math
// If both args are definitely numbers and at least one is definitely a float -> do float math
// otherwise use our boxed runtime methods.
@Override
protected final SoyExpression visitPlusOpNode(PlusOpNode node) {
SoyExpression left = visit(node.getChild(0));
SoyRuntimeType leftRuntimeType = left.soyRuntimeType();
SoyExpression right = visit(node.getChild(1));
SoyRuntimeType rightRuntimeType = right.soyRuntimeType();
// They are both definitely numbers
if (leftRuntimeType.assignableToNullableNumber()
&& rightRuntimeType.assignableToNullableNumber()) {
if (leftRuntimeType.assignableToNullableInt()
&& rightRuntimeType.assignableToNullableInt()) {
return applyBinaryIntOperator(Opcodes.LADD, left, right);
}
// if either is definitely a float, then we are definitely coercing so just do it now
if (leftRuntimeType.assignableToNullableFloat()
|| rightRuntimeType.assignableToNullableFloat()) {
return applyBinaryFloatOperator(Opcodes.DADD, left, right);
}
}
// '+' is overloaded for string arguments to mean concatenation.
if (leftRuntimeType.isKnownString() || rightRuntimeType.isKnownString()) {
SoyExpression leftString = left.coerceToString();
SoyExpression rightString = right.coerceToString();
return SoyExpression.forString(leftString.invoke(MethodRef.STRING_CONCAT, rightString));
}
return SoyExpression.forSoyValue(
SoyTypes.NUMBER_TYPE, MethodRef.RUNTIME_PLUS.invoke(left.box(), right.box()));
}
@Override
protected final SoyExpression visitMinusOpNode(MinusOpNode node) {
final SoyExpression left = visit(node.getChild(0));
final SoyExpression right = visit(node.getChild(1));
// They are both definitely numbers
if (left.assignableToNullableNumber() && right.assignableToNullableNumber()) {
if (left.assignableToNullableInt() && right.assignableToNullableInt()) {
return applyBinaryIntOperator(Opcodes.LSUB, left, right);
}
// if either is definitely a float, then we are definitely coercing so just do it now
if (left.assignableToNullableFloat() || right.assignableToNullableFloat()) {
return applyBinaryFloatOperator(Opcodes.DSUB, left, right);
}
}
return SoyExpression.forSoyValue(
SoyTypes.NUMBER_TYPE, MethodRef.RUNTIME_MINUS.invoke(left.box(), right.box()));
}
@Override
protected final SoyExpression visitTimesOpNode(TimesOpNode node) {
final SoyExpression left = visit(node.getChild(0));
final SoyExpression right = visit(node.getChild(1));
// They are both definitely numbers
if (left.assignableToNullableNumber() && right.assignableToNullableNumber()) {
if (left.assignableToNullableInt() && right.assignableToNullableInt()) {
return applyBinaryIntOperator(Opcodes.LMUL, left, right);
}
// if either is definitely a float, then we are definitely coercing so just do it now
if (left.assignableToNullableFloat() || right.assignableToNullableFloat()) {
return applyBinaryFloatOperator(Opcodes.DMUL, left, right);
}
}
return SoyExpression.forSoyValue(
SoyTypes.NUMBER_TYPE, MethodRef.RUNTIME_TIMES.invoke(left.box(), right.box()));
}
@Override
protected final SoyExpression visitDivideByOpNode(DivideByOpNode node) {
// Note: Soy always performs floating-point division, even on two integers (like JavaScript).
// Note that this *will* lose precision for longs.
return applyBinaryFloatOperator(
Opcodes.DDIV, visit(node.getChild(0)), visit(node.getChild(1)));
}
@Override
protected final SoyExpression visitModOpNode(ModOpNode node) {
// If the underlying expression is not an int, then this will throw a SoyDataExpression at
// runtime. This is how the current tofu works.
// If the expression is known not to be an int, then this will throw an exception at compile
// time. This should generally be handled by the type checker. See b/19833234
return applyBinaryIntOperator(Opcodes.LREM, visit(node.getChild(0)), visit(node.getChild(1)));
}
private static SoyExpression applyBinaryIntOperator(
final int operator, SoyExpression left, SoyExpression right) {
final SoyExpression leftInt = left.unboxAs(long.class);
final SoyExpression rightInt = right.unboxAs(long.class);
return SoyExpression.forInt(
new Expression(Type.LONG_TYPE) {
@Override
void doGen(CodeBuilder mv) {
leftInt.gen(mv);
rightInt.gen(mv);
mv.visitInsn(operator);
}
});
}
private static SoyExpression applyBinaryFloatOperator(
final int operator, SoyExpression left, SoyExpression right) {
final SoyExpression leftFloat = left.coerceToDouble();
final SoyExpression rightFloat = right.coerceToDouble();
return SoyExpression.forFloat(
new Expression(Type.DOUBLE_TYPE) {
@Override
void doGen(CodeBuilder mv) {
leftFloat.gen(mv);
rightFloat.gen(mv);
mv.visitInsn(operator);
}
});
}
// Unary negation
@Override
protected final SoyExpression visitNegativeOpNode(NegativeOpNode node) {
final SoyExpression child = visit(node.getChild(0));
if (child.assignableToNullableInt()) {
final SoyExpression intExpr = child.unboxAs(long.class);
return SoyExpression.forInt(
new Expression(Type.LONG_TYPE, child.features()) {
@Override
void doGen(CodeBuilder mv) {
intExpr.gen(mv);
mv.visitInsn(Opcodes.LNEG);
}
});
}
if (child.assignableToNullableFloat()) {
final SoyExpression floatExpr = child.unboxAs(double.class);
return SoyExpression.forFloat(
new Expression(Type.DOUBLE_TYPE, child.features()) {
@Override
void doGen(CodeBuilder mv) {
floatExpr.gen(mv);
mv.visitInsn(Opcodes.DNEG);
}
});
}
return SoyExpression.forSoyValue(
SoyTypes.NUMBER_TYPE, MethodRef.RUNTIME_NEGATIVE.invoke(child.box()));
}
// Boolean operators
@Override
protected final SoyExpression visitNotOpNode(NotOpNode node) {
// All values are convertible to boolean
return SoyExpression.forBool(logicalNot(visit(node.getChild(0)).coerceToBoolean()));
}
@Override
protected final SoyExpression visitAndOpNode(AndOpNode node) {
SoyExpression left = visit(node.getChild(0)).coerceToBoolean();
SoyExpression right = visit(node.getChild(1)).coerceToBoolean();
return SoyExpression.forBool(BytecodeUtils.logicalAnd(left, right));
}
@Override
protected final SoyExpression visitOrOpNode(OrOpNode node) {
SoyExpression left = visit(node.getChild(0)).coerceToBoolean();
SoyExpression right = visit(node.getChild(1)).coerceToBoolean();
return SoyExpression.forBool(BytecodeUtils.logicalOr(left, right));
}
// Null coalescing operator
@Override
protected SoyExpression visitNullCoalescingOpNode(NullCoalescingOpNode node) {
final SoyExpression left = visit(node.getLeftChild());
if (left.isNonNullable()) {
// This would be for when someone writes '1 ?: 2', we just compile that to '1'
// This case is insane and should potentially be a compiler error, for now we just assume
// it is dead code.
return left;
}
// It is extremely common for a user to write '<complex-expression> ?: <primitive-expression>
// so try to generate code that doesn't involve unconditionally boxing the right hand side.
final SoyExpression right = visit(node.getRightChild());
if (SoyTypes.removeNull(left.soyType()).equals(right.soyType())) {
SoyExpression result;
if (left.isBoxed() == right.isBoxed()) {
// no conversions!
result = right.withSource(firstNonNull(left, right));
} else {
SoyExpression boxedRight = right.box();
result = boxedRight.withSource(firstNonNull(left.box(), boxedRight));
}
if (Expression.areAllCheap(left, right)) {
result = result.asCheap();
}
return result;
}
// Now we need to do some boxing conversions. soy expression boxes null -> null so this is
// safe (and I assume that the jit can eliminate the resulting redundant branches)
Type runtimeType = SoyRuntimeType.getBoxedType(node.getType()).runtimeType();
return SoyExpression.forSoyValue(
node.getType(),
firstNonNull(left.box().checkedCast(runtimeType), right.box().checkedCast(runtimeType)));
}
// Ternary operator
@Override
protected final SoyExpression visitConditionalOpNode(ConditionalOpNode node) {
final SoyExpression condition = visit(node.getChild(0)).coerceToBoolean();
SoyExpression trueBranch = visit(node.getChild(1));
SoyExpression falseBranch = visit(node.getChild(2));
// If types are == and they are both boxed (or both not boxed) then we can just use them
// directly.
// Otherwise we need to do boxing conversions.
// In the past there have been several attempts to eliminate unnecessary boxing operations
// in these conditions however it is too difficult given the type information we have
// available to us and the primitive operations available on SoyExpression. For example, the
// expressions may have non-nullable types and yet take on null values at runtime, if we were
// to introduce aggressive unboxing operations it could result in unexpected
// NullPointerExceptions at runtime. To fix these issues we would need to have a better
// notion of what expressions are nullable (or really, non-nullable) at runtime.
// TODO(lukes): Simple ideas that could help the above:
// 1. expose the 'non-null' prover from ResolveExpressionTypesVisitor, this can in fact be
// relied on. However it is currently mixed in with other parts of the type system which
// cannot be trusted
// 2. compute a least common upper bound for these types. At least that way we would preserve
// more type information
boolean typesEqual = trueBranch.soyType().equals(falseBranch.soyType());
if (typesEqual) {
if (trueBranch.isBoxed() == falseBranch.isBoxed()) {
return trueBranch.withSource(ternary(condition, trueBranch, falseBranch));
}
SoyExpression boxedTrue = trueBranch.box();
return boxedTrue.withSource(ternary(condition, boxedTrue, falseBranch.box()));
}
return SoyExpression.forSoyValue(
UnknownType.getInstance(),
ternary(
condition,
trueBranch.box().checkedCast(SoyValue.class),
falseBranch.box().checkedCast(SoyValue.class)));
}
// For loop variables
@Override
SoyExpression visitForLoopIndex(VarRefNode varRef, LocalVar local) {
// an index variable in a {for $index in range(...)} statement
// These are special because they do not need any attaching/detaching logic and are
// always unboxed ints
return SoyExpression.forInt(
BytecodeUtils.numericConversion(parameters.getLocal(local), Type.LONG_TYPE));
}
@Override
SoyExpression visitForeachLoopVar(VarRefNode varRef, LocalVar local) {
Expression expression = parameters.getLocal(local);
expression = detacher.get().resolveSoyValueProvider(expression);
return SoyExpression.forSoyValue(
varRef.getType(),
expression.checkedCast(SoyRuntimeType.getBoxedType(varRef.getType()).runtimeType()));
}
// Params
@Override
SoyExpression visitParam(VarRefNode varRef, TemplateParam param) {
// TODO(lukes): It would be nice not to generate a detach for every param access, since
// after the first successful 'resolve()' we know that all later ones will also resolve
// successfully. This means that we will generate a potentially large amount of dead
// branches/states/calls to SoyValueProvider.status(). We could eliminate these by doing
// some kind of definite assignment analysis to know whether or not a particular varref is
// _not_ the first one. This would be super awesome and would save bytecode/branches/states
// and technically be useful for all varrefs. For the time being we do the naive thing and
// just assume that the jit can handle all the dead branches effectively.
Expression paramExpr = detacher.get().resolveSoyValueProvider(parameters.getParam(param));
// This inserts a CHECKCAST instruction (aka runtime type checking). However, it is limited
// since we do not have good checking for unions (or nullability)
// TODO(lukes): Where/how should we implement type checking. For the time being type errors
// will show up here, and in the unboxing conversions performed during expression
// manipulation. And, presumably, in NullPointerExceptions.
return SoyExpression.forSoyValue(
varRef.getType(),
paramExpr.checkedCast(SoyRuntimeType.getBoxedType(varRef.getType()).runtimeType()));
}
@Override
SoyExpression visitIjParam(VarRefNode varRef, InjectedParam param) {
Expression ij =
MethodRef.RUNTIME_GET_FIELD_PROVIDER.invoke(
parameters.getIjRecord(), constant(param.name()));
return SoyExpression.forSoyValue(
varRef.getType(),
detacher
.get()
.resolveSoyValueProvider(ij)
.checkedCast(SoyRuntimeType.getBoxedType(varRef.getType()).runtimeType()));
}
// Let vars
@Override
SoyExpression visitLetNodeVar(VarRefNode varRef, LocalVar local) {
Expression expression = parameters.getLocal(local);
expression = detacher.get().resolveSoyValueProvider(expression);
return SoyExpression.forSoyValue(
varRef.getType(),
expression.checkedCast(SoyRuntimeType.getBoxedType(varRef.getType()).runtimeType()));
}
// Data access
@Override
protected SoyExpression visitDataAccessNode(DataAccessNode node) {
return new NullSafeAccessVisitor().visit(node);
}
// Field access
@Override
protected SoyExpression visitFieldAccessNode(FieldAccessNode node) {
return new NullSafeAccessVisitor().visit(node);
}
// Builtin functions
@Override
SoyExpression visitIsFirstFunction(FunctionNode node, SyntheticVarName indexVar) {
final Expression expr = parameters.getLocal(indexVar);
return SoyExpression.forBool(
new Expression(Type.BOOLEAN_TYPE) {
@Override
void doGen(CodeBuilder adapter) {
// implements index == 0 ? true : false
expr.gen(adapter);
Label ifFirst = new Label();
adapter.ifZCmp(Opcodes.IFEQ, ifFirst);
adapter.pushBoolean(false);
Label end = new Label();
adapter.goTo(end);
adapter.mark(ifFirst);
adapter.pushBoolean(true);
adapter.mark(end);
}
});
}
@Override
SoyExpression visitIsLastFunction(
FunctionNode node, SyntheticVarName indexVar, SyntheticVarName lengthVar) {
final Expression index = parameters.getLocal(indexVar);
final Expression length = parameters.getLocal(lengthVar);
// basically 'index + 1 == length'
return SoyExpression.forBool(
new Expression(Type.BOOLEAN_TYPE) {
@Override
void doGen(CodeBuilder adapter) {
// 'index + 1 == length ? true : false'
index.gen(adapter);
adapter.pushInt(1);
adapter.visitInsn(Opcodes.IADD);
length.gen(adapter);
Label ifLast = new Label();
adapter.ifICmp(Opcodes.IFEQ, ifLast);
adapter.pushBoolean(false);
Label end = new Label();
adapter.goTo(end);
adapter.mark(ifLast);
adapter.pushBoolean(true);
adapter.mark(end);
}
});
}
@Override
SoyExpression visitIndexFunction(FunctionNode node, SyntheticVarName indexVar) {
// '(long) index'
return SoyExpression.forInt(
BytecodeUtils.numericConversion(parameters.getLocal(indexVar), Type.LONG_TYPE));
}
@Override
SoyExpression visitCheckNotNullFunction(FunctionNode node) {
// there is only ever a single child
final ExprNode childNode = Iterables.getOnlyElement(node.getChildren());
final SoyExpression childExpr = visit(childNode);
return childExpr
.withSource(
new Expression(childExpr.resultType(), childExpr.features()) {
@Override
void doGen(CodeBuilder adapter) {
childExpr.gen(adapter);
adapter.dup();
Label end = new Label();
adapter.ifNonNull(end);
adapter.throwException(
NULL_POINTER_EXCEPTION_TYPE,
"'" + childNode.toSourceString() + "' evaluates to null");
adapter.mark(end);
}
})
.asNonNullable();
}
// Non-builtin functions
// TODO(lukes): For plugins we simply add the Map<String, SoyJavaFunction> map to RenderContext
// and pull it out of there. However, it seems like we should be able to turn some of those
// calls into static method calls (maybe be stashing instances in static fields in our
// template). We would probably need to introduce a new mechanism for registering functions.
// Or we should just 'intrinsify' a number of extra function (isNonnull for example)
@Override
SoyExpression visitPluginFunction(FunctionNode node) {
return functions.callPluginFunction(node, visitChildren(node));
}
// Proto initialization calls
@Override
protected final SoyExpression visitProtoInitNode(ProtoInitNode node) {
List<SoyExpression> args = visitChildren(node);
return ProtoUtils.createProto(
node, args, parameters.getRenderContext(), detacher, varManager);
}
// Catch-all for unimplemented nodes
@Override
protected final SoyExpression visitExprNode(ExprNode node) {
throw new UnsupportedOperationException(
"Support for " + node.getKind() + " has not been added yet");
}
/**
* A helper for generating code for null safe access expressions.
*
* <p>A null safe access {@code $foo?.bar?.baz} is syntactic sugar for {@code $foo == null ?
* null : ($foo.bar == null ? null : $foo.bar.baz)}. So to generate code for it we need to have
* a way to 'exit' the full access chain as soon as we observe a failed null safety check.
*/
private final class NullSafeAccessVisitor {
Label nullSafeExit;
Label getNullSafeExit() {
Label local = nullSafeExit;
return local == null ? nullSafeExit = new Label() : local;
}
SoyExpression visit(DataAccessNode node) {
SoyExpression dataAccess = visitNullSafeNodeRecurse(node);
if (nullSafeExit == null) {
return dataAccess;
}
if (BytecodeUtils.isPrimitive(dataAccess.resultType())) {
// proto accessors will return primitives, so in order to allow it to be compatible with
// a nullable expression we need to box.
dataAccess = dataAccess.box();
}
return dataAccess.asNullable().labelEnd(nullSafeExit);
}
SoyExpression addNullSafetyCheck(final SoyExpression baseExpr) {
// need to check if baseExpr == null
final Label nullSafeExit = getNullSafeExit();
return baseExpr
.withSource(
new Expression(baseExpr.resultType(), baseExpr.features()) {
@Override
void doGen(CodeBuilder adapter) {
baseExpr.gen(adapter);
BytecodeUtils.nullCoalesce(adapter, nullSafeExit);
}
})
.asNonNullable();
}
SoyExpression visitNullSafeNodeRecurse(ExprNode node) {
switch (node.getKind()) {
case FIELD_ACCESS_NODE:
case ITEM_ACCESS_NODE:
SoyExpression baseExpr =
visitNullSafeNodeRecurse(((DataAccessNode) node).getBaseExprChild());
if (((DataAccessNode) node).isNullSafe()) {
baseExpr = addNullSafetyCheck(baseExpr);
} else {
// Mark non nullable.
// Dereferencing for access below may require unboxing and there is no point in
// adding null safety checks to the unboxing code. So we just mark non nullable. In
// otherwords, if we are going to hit an NPE while dereferencing this expression, it
// makes no difference if it is due to the unboxing or the actual dereference.
baseExpr = baseExpr.asNonNullable();
}
if (node.getKind() == ExprNode.Kind.FIELD_ACCESS_NODE) {
return visitNullSafeFieldAccess(baseExpr, (FieldAccessNode) node);
} else {
return visitNullSafeItemAccess(baseExpr, (ItemAccessNode) node);
}
default:
return CompilerVisitor.this.visit(node);
}
}
SoyExpression visitNullSafeFieldAccess(SoyExpression baseExpr, FieldAccessNode node) {
switch (baseExpr.soyType().getKind()) {
case PROTO:
SoyProtoType protoType = (SoyProtoType) baseExpr.soyType();
return ProtoUtils.accessField(protoType, baseExpr, node, parameters.getRenderContext());
case UNKNOWN:
case UNION:
case RECORD:
// Always fall back to SoyRecord. All known object and record types implement this
// interface.
Expression fieldProvider =
MethodRef.RUNTIME_GET_FIELD_PROVIDER.invoke(
baseExpr.box().checkedCast(SoyRecord.class), constant(node.getFieldName()));
return SoyExpression.forSoyValue(
node.getType(),
detacher
.get()
.resolveSoyValueProvider(fieldProvider)
.checkedCast(SoyRuntimeType.getBoxedType(node.getType()).runtimeType()));
default:
throw new AssertionError("unexpected field access operation");
}
}
SoyExpression visitNullSafeItemAccess(SoyExpression baseExpr, ItemAccessNode node) {
// KeyExprs never participate in the current null access chain.
SoyExpression keyExpr = CompilerVisitor.this.visit(node.getKeyExprChild());
Expression soyValueProvider;
// Special case index lookups on lists to avoid boxing the int key. Maps cannot be
// optimized the same way because there is no real way to 'unbox' a SoyMap.
if (baseExpr.soyRuntimeType().isKnownList()) {
soyValueProvider =
MethodRef.RUNTIME_GET_LIST_ITEM.invoke(
baseExpr.unboxAs(List.class), keyExpr.unboxAs(long.class));
} else {
// Box and do a map style lookup.
soyValueProvider =
MethodRef.RUNTIME_GET_MAP_ITEM.invoke(
baseExpr.box().checkedCast(SoyMap.class), keyExpr.box());
}
Expression soyValue =
detacher
.get()
.resolveSoyValueProvider(soyValueProvider)
// Just like javac, we insert cast operations when removing from a collection.
.checkedCast(SoyRuntimeType.getBoxedType(node.getType()).runtimeType());
return SoyExpression.forSoyValue(node.getType(), soyValue);
}
}
}
/**
* A visitor that scans an expression to see if it has any subexpression that may require detach
* operations. Should be kept in sync with {@link CompilerVisitor}.
*/
private static final class RequiresDetachVisitor
extends EnhancedAbstractExprNodeVisitor<Boolean> {
static final RequiresDetachVisitor INSTANCE = new RequiresDetachVisitor();
@Override
Boolean visitForeachLoopVar(VarRefNode varRef, LocalVar local) {
return true;
}
@Override
Boolean visitParam(VarRefNode varRef, TemplateParam param) {
return true;
}
@Override
Boolean visitLetNodeVar(VarRefNode node, LocalVar local) {
return true;
}
@Override
protected Boolean visitDataAccessNode(DataAccessNode node) {
return true;
}
@Override
Boolean visitIjParam(VarRefNode node, InjectedParam param) {
return true;
}
@Override
protected Boolean visitProtoInitNode(ProtoInitNode node) {
for (Boolean i : visitChildren(node)) {
if (i) {
return true;
}
}
// Proto init calls require detach if any of the specified fields are repeated.
SoyProtoType protoType = (SoyProtoType) node.getType();
for (String paramName : node.getParamNames()) {
if (protoType.getFieldDescriptor(paramName).isRepeated()) {
return true;
}
}
return false;
}
@Override
protected Boolean visitExprNode(ExprNode node) {
if (node instanceof ParentExprNode) {
for (Boolean i : visitChildren((ParentExprNode) node)) {
if (i) {
return true;
}
}
}
return false;
}
}
}