com.google.template.soy.jbcsrc.restricted.SoyExpression.java Source code

Java tutorial

Introduction

Here is the source code for com.google.template.soy.jbcsrc.restricted.SoyExpression.java

Source

/*
 * 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.restricted;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.template.soy.jbcsrc.restricted.BytecodeUtils.OBJECT;
import static com.google.template.soy.jbcsrc.restricted.BytecodeUtils.SOY_LIST_TYPE;
import static com.google.template.soy.jbcsrc.restricted.BytecodeUtils.SOY_VALUE_TYPE;
import static com.google.template.soy.jbcsrc.restricted.BytecodeUtils.logicalNot;

import com.google.protobuf.Message;
import com.google.template.soy.base.SourceLocation;
import com.google.template.soy.base.internal.SanitizedContentKind;
import com.google.template.soy.data.SanitizedContent.ContentKind;
import com.google.template.soy.data.SoyValue;
import com.google.template.soy.data.SoyValueProvider;
import com.google.template.soy.data.internal.RuntimeMapTypeTracker;
import com.google.template.soy.types.BoolType;
import com.google.template.soy.types.FloatType;
import com.google.template.soy.types.IntType;
import com.google.template.soy.types.LegacyObjectMapType;
import com.google.template.soy.types.ListType;
import com.google.template.soy.types.MapType;
import com.google.template.soy.types.NullType;
import com.google.template.soy.types.SanitizedType;
import com.google.template.soy.types.SoyType;
import com.google.template.soy.types.SoyType.Kind;
import com.google.template.soy.types.StringType;
import com.google.template.soy.types.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 convertible to {@code boolean} or {@link String} valued expressions,
 * but depending on the type they may also support additional unboxing conversions.
 */
public final class SoyExpression extends Expression {

    public static SoyExpression forSoyValue(SoyType type, Expression delegate) {
        return new SoyExpression(SoyRuntimeType.getBoxedType(type), delegate);
    }

    public static SoyExpression forBool(Expression delegate) {
        return new SoyExpression(getUnboxedType(BoolType.getInstance()), delegate);
    }

    public static SoyExpression forFloat(Expression delegate) {
        return new SoyExpression(getUnboxedType(FloatType.getInstance()), delegate);
    }

    public static SoyExpression forInt(Expression delegate) {
        return new SoyExpression(getUnboxedType(IntType.getInstance()), delegate);
    }

    public static SoyExpression forString(Expression delegate) {
        return new SoyExpression(getUnboxedType(StringType.getInstance()), delegate);
    }

    public static SoyExpression forSanitizedString(Expression delegate, SanitizedContentKind kind) {
        return new SoyExpression(getUnboxedType(SanitizedType.getTypeForContentKind(kind)), delegate);
    }

    public static SoyExpression forList(ListType listType, Expression delegate) {
        return new SoyExpression(getUnboxedType(listType), delegate);
    }

    public static SoyExpression forLegacyObjectMap(LegacyObjectMapType mapType, Expression delegate) {
        return new SoyExpression(SoyRuntimeType.getBoxedType(mapType), delegate);
    }

    public static SoyExpression forMap(MapType mapType, Expression delegate) {
        return new SoyExpression(SoyRuntimeType.getBoxedType(mapType), delegate);
    }

    public static SoyExpression forProto(SoyRuntimeType type, Expression delegate) {
        checkArgument(type.soyType().getKind() == Kind.PROTO);
        return new SoyExpression(type, delegate);
    }

    /**
     * Returns an Expression that evaluates to a list containing all the items as boxed soy values.
     */
    public 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);
    }

    public static final SoyExpression NULL = new SoyExpression(getUnboxedType(NullType.getInstance()),
            new Expression(OBJECT.type(), Feature.CHEAP) {
                @Override
                protected void doGen(CodeBuilder cb) {
                    cb.pushNull();
                }
            });

    public static final SoyExpression NULL_BOXED = new SoyExpression(
            SoyRuntimeType.getBoxedType(NullType.getInstance()), new Expression(SOY_VALUE_TYPE, Feature.CHEAP) {
                @Override
                protected void doGen(CodeBuilder cb) {
                    cb.pushNull();
                }
            });

    public static final SoyExpression TRUE = forBool(BytecodeUtils.constant(true));

    public static final SoyExpression FALSE = forBool(BytecodeUtils.constant(false));

    private static SoyRuntimeType getUnboxedType(SoyType soyType) {
        return SoyRuntimeType.getUnboxedType(soyType).get();
    }

    private final SoyRuntimeType soyRuntimeType;
    private final Expression delegate;

    private SoyExpression(SoyRuntimeType soyRuntimeType, Expression delegate) {
        super(delegate.resultType(), delegate.features());
        checkArgument(BytecodeUtils.isPossiblyAssignableFrom(soyRuntimeType.runtimeType(), delegate.resultType()),
                "Expecting SoyExpression type of %s for soy type %s, found delegate with type of %s",
                soyRuntimeType.runtimeType(), soyRuntimeType.soyType(), delegate.resultType());
        this.soyRuntimeType = soyRuntimeType;
        this.delegate = delegate;
    }

    /** Returns the {@link SoyType} of the expression. */
    public final SoyType soyType() {
        return soyRuntimeType.soyType();
    }

    /** Returns the {@link SoyRuntimeType} of the expression. */
    public final SoyRuntimeType soyRuntimeType() {
        return soyRuntimeType;
    }

    @Override
    protected final void doGen(CodeBuilder adapter) {
        delegate.gen(adapter);
    }

    @Override
    public SoyExpression withSourceLocation(SourceLocation location) {
        return withSource(delegate.withSourceLocation(location));
    }

    public boolean assignableToNullableInt() {
        return soyRuntimeType.assignableToNullableInt();
    }

    public boolean assignableToNullableFloat() {
        return soyRuntimeType.assignableToNullableFloat();
    }

    public boolean assignableToNullableNumber() {
        return soyRuntimeType.assignableToNullableNumber();
    }

    public boolean assignableToNullableString() {
        return soyRuntimeType.assignableToNullableString();
    }

    public boolean isBoxed() {
        return soyRuntimeType.isBoxed();
    }

    /** Returns an Expression of a non-null {@link SoyValueProvider} providing this value. */
    public Expression boxAsSoyValueProvider() {
        if (soyType().equals(NullType.getInstance())) {
            if (delegate == NULL || delegate == NULL_BOXED) {
                return FieldRef.NULL_PROVIDER.accessor();
            }
            // otherwise this expression might have side effects,  evaluate it as a statement then return
            // the NULL_PROVIDER
            return toStatement().then(FieldRef.NULL_PROVIDER.accessor());
        }
        if (delegate.isNonNullable()) {
            // Every SoyValue is-a SoyValueProvider, so if it is non-null
            return box();
        }
        if (isBoxed()) {
            return new Expression(BytecodeUtils.SOY_VALUE_PROVIDER_TYPE,
                    delegate.features().plus(Feature.NON_NULLABLE)) {
                @Override
                protected void doGen(CodeBuilder adapter) {
                    Label end = new Label();
                    delegate.gen(adapter);
                    adapter.dup();
                    adapter.ifNonNull(end);
                    adapter.pop();
                    FieldRef.NULL_PROVIDER.accessStaticUnchecked(adapter);
                    adapter.mark(end);
                }
            };
        }
        return new Expression(BytecodeUtils.SOY_VALUE_PROVIDER_TYPE,
                delegate.features().plus(Feature.NON_NULLABLE)) {
            @Override
            protected void doGen(CodeBuilder adapter) {
                Label end = new Label();
                delegate.gen(adapter);
                adapter.dup();
                Label nonNull = new Label();
                adapter.ifNonNull(nonNull);
                adapter.pop(); // pop the null value and replace with the nullprovider
                FieldRef.NULL_PROVIDER.accessStaticUnchecked(adapter);
                adapter.goTo(end);
                adapter.mark(nonNull);
                doBox(adapter, soyRuntimeType);
                adapter.mark(end);
            }
        };
    }

    /** Returns a SoyExpression that evaluates to a subtype of {@link SoyValue}. */
    public SoyExpression box() {
        if (isBoxed()) {
            return this;
        }
        if (soyType().equals(NullType.getInstance())) {
            if (delegate == NULL) {
                return NULL_BOXED;
            }
            return asBoxed(toStatement().then(NULL_BOXED));
        }
        // since we aren't boxed and these must be primitives so we don't need to worry about
        // nullability
        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 null is expected and it is a reference type we want to propagate null through the boxing
        // operation
        final boolean isNonNullable = delegate.isNonNullable();
        return asBoxed(new Expression(soyRuntimeType.box().runtimeType(), features()) {
            @Override
            protected void doGen(CodeBuilder adapter) {
                Label end = null;
                delegate.gen(adapter);
                if (!isNonNullable) {
                    end = new Label();
                    BytecodeUtils.nullCoalesce(adapter, end);
                }
                doBox(adapter, soyRuntimeType.asNonNullable());
                if (end != null) {
                    adapter.mark(end);
                }
            }
        });
    }

    /**
     * Generates code to box the expression assuming that it is non-nullable and on the top of the
     * stack.
     */
    private static void doBox(CodeBuilder adapter, SoyRuntimeType type) {
        if (type.isKnownSanitizedContent()) {
            FieldRef.enumReference(ContentKind.valueOf(((SanitizedType) type.soyType()).getContentKind().name()))
                    .accessStaticUnchecked(adapter);
            MethodRef.ORDAIN_AS_SAFE.invokeUnchecked(adapter);
        } else if (type.isKnownString()) {
            MethodRef.STRING_DATA_FOR_VALUE.invokeUnchecked(adapter);
        } else if (type.isKnownListOrUnionOfLists()) {
            MethodRef.LIST_IMPL_FOR_PROVIDER_LIST.invokeUnchecked(adapter);
        } else if (type.isKnownLegacyObjectMapOrUnionOfMaps()) {
            FieldRef.enumReference(RuntimeMapTypeTracker.Type.LEGACY_OBJECT_MAP_OR_RECORD).putUnchecked(adapter);
            MethodRef.DICT_IMPL_FOR_PROVIDER_MAP.invokeUnchecked(adapter);
        } else if (type.isKnownMapOrUnionOfMaps()) {
            MethodRef.MAP_IMPL_FOR_PROVIDER_MAP.invokeUnchecked(adapter);
        } else if (type.isKnownProtoOrUnionOfProtos()) {
            MethodRef.SOY_PROTO_VALUE_CREATE.invokeUnchecked(adapter);
        } else {
            throw new IllegalStateException("Can't box soy expression of type " + type);
        }
    }

    private SoyExpression asBoxed(Expression expr) {
        return new SoyExpression(soyRuntimeType.box(), expr);
    }

    /** Coerce this expression to a boolean value. */
    public 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
                protected 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
            protected void doGen(CodeBuilder adapter) {
                delegate.gen(adapter);
                adapter.pop();
                adapter.pushBoolean(true);
            }
        });
    }

    /** Coerce this expression to a string value. */
    public 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. */
    public 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.
     */
    public 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.isKnownProtoOrUnionOfProtos() && !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()));
            }
        } 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
                protected 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.isKnownListOrUnionOfLists()) {
            asListType = soyRuntimeType.asListType();
        } 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. */
    public SoyExpression withSource(Expression expr) {
        if (expr == delegate) {
            return this;
        }
        return new SoyExpression(soyRuntimeType, expr);
    }

    @Override
    public SoyExpression asCheap() {
        return withSource(delegate.asCheap());
    }

    @Override
    public SoyExpression asNonNullable() {
        return new SoyExpression(soyRuntimeType.asNonNullable(), delegate.asNonNullable());
    }

    @Override
    public SoyExpression asNullable() {
        return new SoyExpression(soyRuntimeType.asNullable(), delegate.asNullable());
    }

    @Override
    public SoyExpression labelStart(Label label) {
        return withSource(delegate.labelStart(label));
    }

    @Override
    public SoyExpression labelEnd(Label label) {
        return withSource(delegate.labelEnd(label));
    }
}