/*
* 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.checkArgument;
import static com.google.template.soy.jbcsrc.BytecodeUtils.OBJECT;
import static com.google.template.soy.jbcsrc.BytecodeUtils.RENDER_CONTEXT_TYPE;
import static com.google.template.soy.jbcsrc.BytecodeUtils.SOY_LIST_TYPE;
import static com.google.template.soy.jbcsrc.BytecodeUtils.SOY_VALUE_TYPE;
import static com.google.template.soy.jbcsrc.BytecodeUtils.constant;
import static com.google.template.soy.jbcsrc.BytecodeUtils.logicalNot;
import com.google.common.base.Optional;
import com.google.protobuf.Message;
import com.google.template.soy.data.SanitizedContent.ContentKind;
import com.google.template.soy.data.SoyValue;
import com.google.template.soy.soytree.CallNode;
import com.google.template.soy.soytree.MsgNode;
import com.google.template.soy.soytree.PrintNode;
import com.google.template.soy.types.SoyType;
import com.google.template.soy.types.SoyType.Kind;
import com.google.template.soy.types.aggregate.ListType;
import com.google.template.soy.types.primitive.BoolType;
import com.google.template.soy.types.primitive.FloatType;
import com.google.template.soy.types.primitive.IntType;
import com.google.template.soy.types.primitive.NullType;
import com.google.template.soy.types.primitive.SanitizedType;
import com.google.template.soy.types.primitive.StringType;
import com.google.template.soy.types.primitive.UnknownType;
import java.util.ArrayList;
import java.util.List;
import org.objectweb.asm.Label;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
/**
* An Expression involving a soy value.
*
* <p>SoyExpressions can be {@link #box() boxed} into SoyValue subtypes and they also support some
* implicit conversions.
*
* <p>All soy expressions are convertable to {@code boolean} or {@link String} valued expressions,
* but depending on the type they may also support additional unboxing conversions.
*/
class SoyExpression extends Expression {
static SoyExpression forSoyValue(SoyType type, Expression delegate) {
return new SoyExpression(SoyRuntimeType.getBoxedType(type), delegate);
}
static SoyExpression forBool(Expression delegate) {
return new SoyExpression(getUnboxedType(BoolType.getInstance()), delegate);
}
static SoyExpression forFloat(Expression delegate) {
return new SoyExpression(getUnboxedType(FloatType.getInstance()), delegate);
}
static SoyExpression forInt(Expression delegate) {
return new SoyExpression(getUnboxedType(IntType.getInstance()), delegate);
}
static SoyExpression forString(Expression delegate) {
return new SoyExpression(getUnboxedType(StringType.getInstance()), delegate);
}
static SoyExpression forSanitizedString(Expression delegate, ContentKind kind) {
return new SoyExpression(getUnboxedType(SanitizedType.getTypeForContentKind(kind)), delegate);
}
static SoyExpression forList(ListType listType, Expression delegate) {
return new SoyExpression(getUnboxedType(listType), delegate);
}
static SoyExpression forProto(
SoyRuntimeType type, Expression delegate, Expression renderContext) {
checkArgument(renderContext.resultType().equals(RENDER_CONTEXT_TYPE));
return new SoyExpression(type, delegate, Optional.of(renderContext));
}
/**
* Returns an Expression that evaluates to a list containing all the items as boxed soy values.
*/
static Expression asBoxedList(List<SoyExpression> items) {
List<Expression> childExprs = new ArrayList<>(items.size());
for (SoyExpression child : items) {
childExprs.add(child.box());
}
return BytecodeUtils.asList(childExprs);
}
static final SoyExpression NULL =
new SoyExpression(
getUnboxedType(NullType.getInstance()),
new Expression(OBJECT.type(), Feature.CHEAP) {
@Override
void doGen(CodeBuilder cb) {
cb.pushNull();
}
});
static final SoyExpression NULL_BOXED =
new SoyExpression(
SoyRuntimeType.getBoxedType(NullType.getInstance()),
new Expression(SOY_VALUE_TYPE, Feature.CHEAP) {
@Override
void doGen(CodeBuilder cb) {
cb.pushNull();
}
});
static final SoyExpression TRUE =
new SoyExpression(getUnboxedType(BoolType.getInstance()), BytecodeUtils.constant(true)) {
@Override
SoyExpression box() {
return new DefaultBoxed(
SoyRuntimeType.getBoxedType(BoolType.getInstance()),
this,
FieldRef.BOOLEAN_DATA_TRUE.accessor(),
Optional.<Expression>absent());
}
};
static final SoyExpression FALSE =
new SoyExpression(getUnboxedType(BoolType.getInstance()), BytecodeUtils.constant(false)) {
@Override
SoyExpression box() {
return new DefaultBoxed(
SoyRuntimeType.getBoxedType(BoolType.getInstance()),
this,
FieldRef.BOOLEAN_DATA_FALSE.accessor(),
Optional.<Expression>absent());
}
};
private static SoyRuntimeType getUnboxedType(SoyType soyType) {
return SoyRuntimeType.getUnboxedType(soyType).get();
}
private final SoyRuntimeType soyRuntimeType;
private final Expression delegate;
private final Optional<Expression> renderContext;
private SoyExpression(SoyRuntimeType soyRuntimeType, Expression delegate) {
this(soyRuntimeType, delegate, Optional.<Expression>absent());
}
private SoyExpression(
SoyRuntimeType soyRuntimeType, Expression delegate, Optional<Expression> renderContext) {
super(delegate.resultType(), delegate.features());
checkArgument(
BytecodeUtils.isPossiblyAssignableFrom(soyRuntimeType.runtimeType(), delegate.resultType()),
"Expecting SoyExpression type of %s, found delegate with type of %s",
soyRuntimeType.runtimeType(),
delegate.resultType());
this.soyRuntimeType = soyRuntimeType;
this.delegate = delegate;
this.renderContext = renderContext;
}
/** Returns the {@link SoyType} of the expression. */
final SoyType soyType() {
return soyRuntimeType.soyType();
}
/** Returns the {@link SoyRuntimeType} of the expression. */
final SoyRuntimeType soyRuntimeType() {
return soyRuntimeType;
}
@Override
final void doGen(CodeBuilder adapter) {
delegate.gen(adapter);
}
boolean assignableToNullableInt() {
return soyRuntimeType.assignableToNullableInt();
}
boolean assignableToNullableFloat() {
return soyRuntimeType.assignableToNullableFloat();
}
boolean assignableToNullableNumber() {
return soyRuntimeType.assignableToNullableNumber();
}
boolean isBoxed() {
return soyRuntimeType.isBoxed();
}
/** Returns a SoyExpression that evaluates to a subtype of {@link SoyValue}. */
SoyExpression box() {
if (isBoxed()) {
return this;
}
if (soyType().equals(NullType.getInstance())) {
return NULL_BOXED;
}
// If null is expected and it is a reference type we want to propagate null through the boxing
// operation
if (!delegate.isNonNullable()) {
// now prefix with a null check and then box so null is preserved via 'boxing'
// TODO(lukes): this violates the expression contract since we jump to a label outside the
// scope of the expression
final Label end = new Label();
return withSource(
new Expression(resultType(), features()) {
@Override
void doGen(CodeBuilder adapter) {
delegate.gen(adapter);
BytecodeUtils.nullCoalesce(adapter, end);
}
})
.asNonNullable()
.box()
.asNullable()
.labelEnd(end);
}
if (soyRuntimeType.isKnownBool()) {
return asBoxed(MethodRef.BOOLEAN_DATA_FOR_VALUE.invoke(delegate));
}
if (soyRuntimeType.isKnownInt()) {
return asBoxed(MethodRef.INTEGER_DATA_FOR_VALUE.invoke(delegate));
}
if (soyRuntimeType.isKnownFloat()) {
return asBoxed(MethodRef.FLOAT_DATA_FOR_VALUE.invoke(delegate));
}
if (soyRuntimeType.isKnownSanitizedContent()) {
return asBoxed(
MethodRef.ORDAIN_AS_SAFE.invoke(
delegate,
FieldRef.enumReference(((SanitizedType) soyType()).getContentKind()).accessor()));
}
if (soyRuntimeType.isKnownString()) {
return asBoxed(MethodRef.STRING_DATA_FOR_VALUE.invoke(delegate));
}
if (soyRuntimeType.isKnownList()) {
return asBoxed(MethodRef.LIST_IMPL_FOR_PROVIDER_LIST.invoke(delegate));
}
if (soyRuntimeType.isKnownProto()) {
// dereference the context early in case our caller screwed up and we don't have one.
final Expression context = renderContext.get();
return asBoxed(
new Expression(MethodRef.RENDER_CONTEXT_BOX.returnType(), delegate.features()) {
@Override
void doGen(CodeBuilder adapter) {
delegate.gen(adapter);
context.gen(adapter);
// swap the two top items of the stack.
// This ensures that the base expression is gen'd at a stack depth of zero, which is
// important if it contains a label for a reattach point or if this is prefixed with a
// null check.
// TODO(lukes): this is super lame and is due to the fact that we generate expressions
// with complex control flow that isn't completely encapsulated. Ideas: inline all
// the null checks so that the control flow is more encapsulated (though this will add
// a lot of redundancy), promote the concept of an expression that must be gen()'d at
// a depth of zero to a top level concept (an Expression Feature), so that we can flag
// it more eagerly, something else?
adapter.swap();
MethodRef.RENDER_CONTEXT_BOX.invokeUnchecked(adapter);
}
});
}
throw new IllegalStateException("Can't box soy expression of type " + soyRuntimeType);
}
private DefaultBoxed asBoxed(Expression expr) {
return new DefaultBoxed(soyRuntimeType.box(), this, expr, renderContext);
}
/** Coerce this expression to a boolean value. */
SoyExpression coerceToBoolean() {
// First deal with primitives which don't have to care about null.
if (BytecodeUtils.isPrimitive(resultType())) {
return coercePrimitiveToBoolean();
}
if (soyType().equals(NullType.getInstance())) {
return FALSE;
}
if (delegate.isNonNullable()) {
return coerceNonNullableReferenceTypeToBoolean();
} else {
// If we are potentially nullable, then map null to false and run the normal logic recursively
// for the non-nullable branch.
final Label end = new Label();
return withSource(
new Expression(delegate.resultType(), delegate.features()) {
@Override
void doGen(CodeBuilder adapter) {
delegate.gen(adapter);
adapter.dup();
Label nonNull = new Label();
adapter.ifNonNull(nonNull);
adapter.pop();
adapter.pushBoolean(false);
adapter.goTo(end);
adapter.mark(nonNull);
}
})
.asNonNullable()
.coerceToBoolean()
.labelEnd(end);
}
}
private SoyExpression coercePrimitiveToBoolean() {
if (resultType().equals(Type.BOOLEAN_TYPE)) {
return this;
} else if (resultType().equals(Type.DOUBLE_TYPE)) {
return forBool(MethodRef.RUNTIME_COERCE_DOUBLE_TO_BOOLEAN.invoke(delegate));
} else if (resultType().equals(Type.LONG_TYPE)) {
return forBool(BytecodeUtils.compare(Opcodes.IFNE, delegate, BytecodeUtils.constant(0L)));
} else {
throw new AssertionError(
"resultType(): " + resultType() + " is not a valid type for a SoyExpression");
}
}
private SoyExpression coerceNonNullableReferenceTypeToBoolean() {
if (isBoxed()) {
// If we are boxed, just call the SoyValue method
return forBool(delegate.invoke(MethodRef.SOY_VALUE_COERCE_TO_BOOLEAN));
}
// unboxed non-primitive types. This would be strings, protos or lists
if (soyRuntimeType.isKnownString()) {
return forBool(logicalNot(delegate.invoke(MethodRef.STRING_IS_EMPTY)));
}
// All other types are always truthy, but we still need to eval the delegate in case it has
// side effects or contains a null exit branch.
return forBool(
new Expression(Type.BOOLEAN_TYPE, delegate.features()) {
@Override
void doGen(CodeBuilder adapter) {
delegate.gen(adapter);
adapter.pop();
adapter.pushBoolean(true);
}
});
}
/** Coerce this expression to a string value. */
SoyExpression coerceToString() {
if (soyRuntimeType.isKnownString() && !isBoxed()) {
return this;
}
if (BytecodeUtils.isPrimitive(resultType())) {
if (resultType().equals(Type.BOOLEAN_TYPE)) {
return forString(MethodRef.BOOLEAN_TO_STRING.invoke(delegate));
} else if (resultType().equals(Type.DOUBLE_TYPE)) {
return forString(MethodRef.DOUBLE_TO_STRING.invoke(delegate));
} else if (resultType().equals(Type.LONG_TYPE)) {
return forString(MethodRef.LONG_TO_STRING.invoke(delegate));
} else {
throw new AssertionError(
"resultType(): " + resultType() + " is not a valid type for a SoyExpression");
}
}
if (!isBoxed()) {
// this is for unboxed reference types (strings, lists, protos) String.valueOf handles null
// implicitly
return forString(MethodRef.STRING_VALUE_OF.invoke(delegate));
}
return forString(MethodRef.RUNTIME_COERCE_TO_STRING.invoke(delegate));
}
/** Coerce this expression to a double value. Useful for float-int comparisons. */
SoyExpression coerceToDouble() {
if (!isBoxed()) {
if (soyRuntimeType.isKnownFloat()) {
return this;
}
if (soyRuntimeType.isKnownInt()) {
return forFloat(BytecodeUtils.numericConversion(delegate, Type.DOUBLE_TYPE));
}
throw new UnsupportedOperationException("Can't convert " + resultType() + " to a double");
}
if (soyRuntimeType.isKnownFloat()) {
return forFloat(delegate.invoke(MethodRef.SOY_VALUE_FLOAT_VALUE));
}
return forFloat(delegate.invoke(MethodRef.SOY_VALUE_NUMBER_VALUE));
}
// TODO(lukes): split this into a set of specialized methods, one per target type like we did
// for the 'coerce' methods.
/**
* Unboxes this to a {@link SoyExpression} with a runtime type of {@code asType}.
*
* <p>This method is appropriate when you know (likely via inspection of the {@link #soyType()},
* or other means) that the value does have the appropriate type but you prefer to interact with
* it as its unboxed representation. If you simply want to 'coerce' the given value to a new type
* consider {@link #coerceToBoolean()} {@link #coerceToDouble()} or {@link #coerceToString()}
* which are designed for that use case.
*/
SoyExpression unboxAs(Class<?> asType) {
checkArgument(
!SoyValue.class.isAssignableFrom(asType),
"Cannot use unboxAs() to convert to a SoyValue: %s, use .box() instead",
asType);
// No-op conversion, always allow.
// SoyExpressions that are already unboxed fall into this case.
if (BytecodeUtils.isDefinitelyAssignableFrom(
Type.getType(asType), soyRuntimeType.runtimeType())) {
return this;
}
// Attempting to unbox an unboxed proto
if (asType.equals(Message.class) && soyRuntimeType.isKnownProto() && !isBoxed()) {
return this;
}
if (!isBoxed()) {
throw new IllegalStateException(
"Trying to unbox an unboxed value ("
+ soyRuntimeType
+ ") into "
+ asType
+ " doesn't make sense. Should you be using a type coercion? e.g. .coerceToBoolean()");
}
if (asType.equals(boolean.class)) {
return forBool(delegate.invoke(MethodRef.SOY_VALUE_BOOLEAN_VALUE));
}
if (asType.equals(long.class)) {
return forInt(delegate.invoke(MethodRef.SOY_VALUE_LONG_VALUE));
}
if (asType.equals(double.class)) {
return forFloat(delegate.invoke(MethodRef.SOY_VALUE_FLOAT_VALUE));
}
if (delegate.isNonNullable()) {
if (asType.equals(String.class)) {
Expression unboxedString = delegate.invoke(MethodRef.SOY_VALUE_STRING_VALUE);
// We need to ensure that santized types don't lose their content kinds
return soyRuntimeType.isKnownSanitizedContent()
? forSanitizedString(unboxedString, ((SanitizedType) soyType()).getContentKind())
: forString(unboxedString);
}
if (asType.equals(List.class)) {
return unboxAsList();
}
if (asType.equals(Message.class)) {
SoyRuntimeType runtimeType = getUnboxedType(soyType());
return forProto(
runtimeType,
delegate
.invoke(MethodRef.SOY_PROTO_VALUE_GET_PROTO)
.checkedCast(runtimeType.runtimeType()),
renderContext.get());
}
} else {
// else it must be a List/Proto/String all of which must preserve null through the unboxing
// operation
// TODO(lukes): this violates the expression contract since we jump to a label outside the
// scope of the expression
final Label end = new Label();
Expression nonNullDelegate =
new Expression(resultType(), features()) {
@Override
void doGen(CodeBuilder adapter) {
delegate.gen(adapter);
BytecodeUtils.nullCoalesce(adapter, end);
}
};
return withSource(nonNullDelegate).asNonNullable().unboxAs(asType).asNullable().labelEnd(end);
}
throw new UnsupportedOperationException("Can't unbox " + soyRuntimeType + " as " + asType);
}
private SoyExpression unboxAsList() {
ListType asListType;
if (soyRuntimeType.isKnownList()) {
asListType = (ListType) soyType();
} else {
Kind kind = soyType().getKind();
if (kind == Kind.UNKNOWN) {
asListType = ListType.of(UnknownType.getInstance());
} else {
// The type checker should have already rejected all of these
throw new UnsupportedOperationException("Can't convert " + soyRuntimeType + " to List");
}
}
return forList(
asListType, delegate.checkedCast(SOY_LIST_TYPE).invoke(MethodRef.SOY_LIST_AS_JAVA_LIST));
}
/** Returns a new {@link SoyExpression} with the same type but a new delegate expression. */
SoyExpression withSource(Expression expr) {
return new SoyExpression(soyRuntimeType, expr, renderContext);
}
SoyExpression withRenderContext(Expression renderContext) {
checkArgument(renderContext.resultType().equals(RENDER_CONTEXT_TYPE));
return new SoyExpression(soyRuntimeType, delegate, Optional.of(renderContext));
}
/**
* Applies a print directive to the soyValue, only useful for parameterless print directives such
* as those applied to {@link MsgNode msg nodes} and {@link CallNode call nodes} for autoescaping.
* For {@link PrintNode print nodes}, the directives may be parameterized by arbitrary soy
* expressions.
*/
SoyExpression applyPrintDirective(Expression renderContext, String directive) {
return applyPrintDirective(renderContext, directive, MethodRef.IMMUTABLE_LIST_OF.invoke());
}
/** Applies a print directive to the soyValue. */
SoyExpression applyPrintDirective(
Expression renderContext, String directive, Expression argsList) {
// Technically the type is either StringData or SanitizedContent depending on this type, but
// boxed. Consider propagating the type more accurately, currently there isn't (afaict) much
// benefit (and strangely there is no common super type for SanitizedContent and String), this
// is probably because after escaping, the only thing you would ever do is convert to a string.
return SoyExpression.forSoyValue(
UnknownType.getInstance(),
MethodRef.RUNTIME_APPLY_PRINT_DIRECTIVE.invoke(
renderContext.invoke(MethodRef.RENDER_CONTEXT_GET_PRINT_DIRECTIVE, constant(directive)),
this.box(),
argsList));
}
@Override
SoyExpression asCheap() {
return withSource(delegate.asCheap());
}
@Override
SoyExpression asNonNullable() {
return new SoyExpression(
soyRuntimeType.asNonNullable(), delegate.asNonNullable(), renderContext);
}
@Override
SoyExpression asNullable() {
return new SoyExpression(soyRuntimeType.asNullable(), delegate.asNullable(), renderContext);
}
@Override
SoyExpression labelStart(Label label) {
return withSource(delegate.labelStart(label));
}
@Override
SoyExpression labelEnd(Label label) {
return withSource(delegate.labelEnd(label));
}
/** Default subtype of {@link SoyExpression} used by our core expression implementations. */
private static final class DefaultBoxed extends SoyExpression {
private final SoyExpression unboxed;
DefaultBoxed(
SoyRuntimeType soyRuntimeType,
SoyExpression unboxed,
Expression delegate,
Optional<Expression> renderContext) {
super(soyRuntimeType, delegate, renderContext);
this.unboxed = unboxed;
}
@Override
final SoyExpression unboxAs(Class<?> asType) {
return unboxed.unboxAs(asType);
}
@Override
SoyExpression coerceToBoolean() {
return unboxed.coerceToBoolean();
}
@Override
SoyExpression coerceToString() {
return unboxed.coerceToString();
}
@Override
SoyExpression coerceToDouble() {
return unboxed.coerceToDouble();
}
@Override
final SoyExpression box() {
return this;
}
}
}