com.google.template.soy.jbcsrc.Expression.java Source code

Java tutorial

Introduction

Here is the source code for com.google.template.soy.jbcsrc.Expression.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;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;

import org.objectweb.asm.Label;
import org.objectweb.asm.Type;

import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;

/**
 * An expression has a {@link #resultType()} and can {@link #gen generate} code to evaluate the
 * expression.
 *
 * <p>Expressions should be side effect free and also should not <em>consume</em> stack items.
 */
abstract class Expression extends BytecodeProducer {
    /** 
     * Expression features track additional metadata for expressions.
     * 
     * <p>Features should be defined such that not setting a feature on an expression is a safe 
     * default.  That way if they get accidentally dropped in a transformation we simply generate
     * less efficient code, not incorrect code.
     */
    enum Feature {
        /** The expression is guaranteed to not return null. */
        NON_NULLABLE,
        /** 
         * The expression is 'cheap'.  As a rule of thumb, if it involves allocation, it is not cheap.
         * 
         * <p>Cheapness is useful when deciding if it would be reasonable to evaluate an expression more
         * than once if the alternative is generating additional fields and save/restore code.
         */
        CHEAP;
        // TODO(lukes): an idempotent feature would be useful some expressions are not safe to gen more
        // than once.
    }

    /** An immutable wrapper of an EnumSet of {@link Feature}. */
    static final class Features {
        private static final Features EMPTY = new Features(EnumSet.noneOf(Feature.class));

        static Features of() {
            return EMPTY;
        }

        static Features of(Feature first, Feature... rest) {
            EnumSet<Feature> set = EnumSet.of(first);
            Collections.addAll(set, rest);
            return new Features(set);
        }

        private static Features forType(Type expressionType, Features features) {
            switch (expressionType.getSort()) {
            case Type.OBJECT:
            case Type.ARRAY:
                return features;
            case Type.BOOLEAN:
            case Type.BYTE:
            case Type.CHAR:
            case Type.DOUBLE:
            case Type.INT:
            case Type.SHORT:
            case Type.LONG:
            case Type.FLOAT:
                // primitives are never null
                return features.plus(Feature.NON_NULLABLE);
            case Type.VOID:
            case Type.METHOD:
                throw new IllegalArgumentException("Invalid type: " + expressionType);
            default:
                throw new AssertionError("unexpected type " + expressionType);
            }
        }

        private final EnumSet<Feature> set;

        private Features(EnumSet<Feature> set) {
            this.set = checkNotNull(set);
        }

        boolean has(Feature feature) {
            return set.contains(feature);
        }

        Features plus(Feature feature) {
            if (set.contains(feature)) {
                return this;
            }
            EnumSet<Feature> newSet = copyFeatures();
            newSet.add(feature);
            return new Features(newSet);
        }

        Features minus(Feature feature) {
            if (!set.contains(feature)) {
                return this;
            }
            EnumSet<Feature> newSet = copyFeatures();
            newSet.remove(feature);
            return new Features(newSet);
        }

        private EnumSet<Feature> copyFeatures() {
            // Can't use EnumSet.copyOf() because it throws on empty collections!
            EnumSet<Feature> newSet = EnumSet.noneOf(Feature.class);
            newSet.addAll(set);
            return newSet;
        }
    }

    /** Returns true if all referenced expressions are {@linkplain #isCheap() cheap}. */
    static boolean areAllCheap(Iterable<? extends Expression> args) {
        for (Expression arg : args) {
            if (!arg.isCheap()) {
                return false;
            }
        }
        return true;
    }

    /** Returns true if all referenced expressions are {@linkplain #isCheap() cheap}. */
    static boolean areAllCheap(Expression first, Expression... rest) {
        return areAllCheap(ImmutableList.<Expression>builder().add(first).add(rest).build());
    }

    /**
     * Checks that the given expressions are compatible with the given types.
     */
    static void checkTypes(ImmutableList<Type> types, Expression... exprs) {
        checkTypes(types, Arrays.asList(exprs));
    }

    /**
     * Checks that the given expressions are compatible with the given types.
     */
    static void checkTypes(ImmutableList<Type> types, Iterable<? extends Expression> exprs) {
        int size = Iterables.size(exprs);
        checkArgument(size == types.size(), "Supplied the wrong number of parameters. Expected %s, got %s",
                types.size(), size);
        int i = 0;
        for (Expression expr : exprs) {
            expr.checkAssignableTo(types.get(i), "Parameter %s", i);
            i++;
        }
    }

    private final Features features;
    private final Type resultType;

    Expression(Type resultType) {
        this(resultType, Features.of());
    }

    Expression(Type resultType, Feature first, Feature... rest) {
        this(resultType, Features.of(first, rest));
    }

    Expression(Type resultType, Features features) {
        this.resultType = checkNotNull(resultType);
        this.features = Features.forType(resultType, features);
    }

    /** 
     * Generate code to evaluate the expression.
     *   
     * <p>The generated code satisfies the invariant that the top of the runtime stack will contain a
     * value with this {@link #resultType()} immediately after evaluation of the code. 
     */
    @Override
    abstract void doGen(CodeBuilder adapter);

    /** The type of the expression. */
    final Type resultType() {
        return resultType;
    }

    /** Whether or not this expression is {@link Feature#CHEAP cheap}. */
    boolean isCheap() {
        return features.has(Feature.CHEAP);
    }

    /** Whether or not this expression is {@link Feature#NON_NULLABLE non nullable}. */
    boolean isNonNullable() {
        return features.has(Feature.NON_NULLABLE);
    }

    /**
     * Returns all the feature bits. 
     * Typically, users will want to invoke one of the convenience accessors {@link #isCheap()} or 
     * {@link #isNonNullable()}. 
     */
    Features features() {
        return features;
    }

    /**
     * Check that this expression is assignable to {@code expected}. 
     */
    final void checkAssignableTo(Type expected) {
        checkAssignableTo(expected, "");
    }

    /**
     * Check that this expression is assignable to {@code expected}. 
     */
    final void checkAssignableTo(Type expected, String fmt, Object... args) {
        if (resultType().equals(expected)) {
            return;
        }
        if (expected.getSort() == resultType().getSort() && expected.getSort() == Type.OBJECT) {
            // for class types we really need to know type hierarchy information to test for 
            // whether actualType is assignable to expectedType.
            // This test is mostly optimistic so we just assume that they match. The verifier will tell
            // us ultimately if we screw up.
            // TODO(lukes): see if we can do something better here,  special case the SoyValue 
            // hierarchy?  We could use BytecodeUtils.classFromAsmType, but that might be overly expensive
            return;
        }
        String message = String.format("Type mismatch. Expected %s, got %s.", expected, resultType());
        if (!fmt.isEmpty()) {
            message = String.format(fmt, args) + ". " + message;
        }
        throw new IllegalArgumentException(message);
    }

    /** 
     * Convert this expression to a statement, by executing it and throwing away the result.
     * 
     * <p>This is useful for invoking non-void methods when we don't care about the result.
     */
    Statement toStatement() {
        return new Statement() {
            @Override
            void doGen(CodeBuilder adapter) {
                Expression.this.gen(adapter);
                switch (resultType().getSize()) {
                case 0:
                    throw new AssertionError("void expressions are not allowed");
                case 1:
                    adapter.pop();
                    break;
                case 2:
                    adapter.pop2();
                    break;
                }
            }
        };
    }

    /** Returns an equivalent expression where {@link #isCheap()} returns {@code true}. */
    Expression asCheap() {
        if (isCheap()) {
            return this;
        }
        return new Expression(resultType, features.plus(Feature.CHEAP)) {
            @Override
            void doGen(CodeBuilder adapter) {
                Expression.this.gen(adapter);
            }
        };
    }

    /** Returns an equivalent expression where {@link #isNonNullable()} returns {@code true}. */
    Expression asNonNullable() {
        if (isNonNullable()) {
            return this;
        }
        return new Expression(resultType, features.plus(Feature.NON_NULLABLE)) {
            @Override
            void doGen(CodeBuilder adapter) {
                Expression.this.gen(adapter);
            }
        };
    }

    /**
     * Returns an expression that performs a checked cast from the current type to the target type.
     *
     * @throws IllegalArgumentException if either type is not a reference type.
     */
    Expression cast(final Type target) {
        checkArgument(target.getSort() == Type.OBJECT, "cast targets must be reference types.");
        checkArgument(resultType().getSort() == Type.OBJECT, "you may only cast from reference types.");
        if (target.equals(resultType())) {
            return this;
        }
        return new Expression(target, features()) {
            @Override
            void doGen(CodeBuilder adapter) {
                Expression.this.gen(adapter);
                adapter.checkCast(resultType());
            }
        };
    }

    /**
     * Returns an expression that performs a checked cast from the current type to the target type.
     *
     * @throws IllegalArgumentException if either type is not a reference type.
     */
    Expression cast(Class<?> target) {
        return cast(Type.getType(target));
    }

    /**
     * A simple helper that calls through to {@link MethodRef#invoke(Expression...)}, but allows a
     * more natural fluent call style.
     */
    Expression invoke(MethodRef method, Expression... args) {
        return method.invoke(ImmutableList.<Expression>builder().add(this).add(args).build());
    }

    /**
     * A simple helper that calls through to {@link MethodRef#invokeVoid(Expression...)}, but allows a
     * more natural fluent call style.
     */
    Statement invokeVoid(MethodRef method, Expression... args) {
        return method.invokeVoid(ImmutableList.<Expression>builder().add(this).add(args).build());
    }

    /**
     * Returns a new expression identical to this one but with the given label applied at the start
     * of the expression.
     */
    Expression labelStart(final Label label) {
        return new Expression(resultType(), features) {
            @Override
            void doGen(CodeBuilder adapter) {
                adapter.mark(label);
                Expression.this.gen(adapter);
            }
        };
    }

    /**
     * Returns a new expression identical to this one but with the given label applied at the end
     * of the expression.
     */
    Expression labelEnd(final Label label) {
        return new Expression(resultType(), features) {
            @Override
            void doGen(CodeBuilder adapter) {
                Expression.this.gen(adapter);
                adapter.mark(label);
            }
        };
    }

    @Override
    public String toString() {
        String name = getClass().getSimpleName();
        if (name.isEmpty()) {
            // provide a default for anonymous subclasses
            name = "Expression";
        }
        name = name + "(" + resultType + "){";
        boolean needsLeadingSpace = false;
        if (features.has(Feature.CHEAP)) {
            name += "cheap";
            needsLeadingSpace = true;
        }
        if (features.has(Feature.NON_NULLABLE) && !BytecodeUtils.isPrimitive(resultType)) {
            name += (needsLeadingSpace ? " " : "") + "non-null";
        }
        return name + "}<" + resultType() + ">:\n" + trace();
    }
}