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

Java tutorial

Introduction

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

import com.google.template.soy.basicfunctions.AugmentMapFunction;
import com.google.template.soy.basicfunctions.CeilingFunction;
import com.google.template.soy.basicfunctions.FloorFunction;
import com.google.template.soy.basicfunctions.KeysFunction;
import com.google.template.soy.basicfunctions.MaxFunction;
import com.google.template.soy.basicfunctions.MinFunction;
import com.google.template.soy.basicfunctions.RandomIntFunction;
import com.google.template.soy.basicfunctions.RoundFunction;
import com.google.template.soy.data.SoyDict;
import com.google.template.soy.data.SoyList;
import com.google.template.soy.data.SoyMap;
import com.google.template.soy.data.SoyValue;
import com.google.template.soy.exprtree.FunctionNode;
import com.google.template.soy.types.SoyType;
import com.google.template.soy.types.aggregate.ListType;
import com.google.template.soy.types.aggregate.MapType;
import com.google.template.soy.types.aggregate.UnionType;
import com.google.template.soy.types.primitive.FloatType;
import com.google.template.soy.types.primitive.IntType;
import com.google.template.soy.types.primitive.StringType;
import com.google.template.soy.types.primitive.UnknownType;

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

import java.util.List;

/**
 * Implements intrinsics for built in functions.
 *
 * <p>Many soy functions are very simple but the Tofu calling convention that is used requires us to
 * <ul>
 *     <li>Box all arguments
 *     <li>Pass the arguments as a {@code List}
 *     <li>Map {@code null} -> {@code NullData} and back
 * </ul>
 *
 * <p>A lot of this cost could be avoided by swapping out an intrinsic implementation.  For example,
 * {@code isNonnull($foo)} requires code that looks like: <pre>{@code
 * 
 *   List<SoyValue> args = new ArrayList<1>();
 *   args.put(<$foo>.box());
 *   SoyValue ret = Runtime.callSoyFunction(
 *       renderContext.getFunction("isNonnull"),
 *       args);
 *
 * }</pre>
 *
 * <p>But that could be easily replaced with a simpler implementation of just
 * {@code <$foo> == null}.  This class provides intrinsic implementations of all functions that are
 * built into {@code soy}.
 */
final class PluginFunctionCompiler {
    private static final SoyType NUMBER_TYPE = UnionType.of(IntType.getInstance(), FloatType.getInstance());

    private static final MethodRef AUGMENT_MAP_FN = MethodRef
            .create(AugmentMapFunction.class, "augmentMap", SoyDict.class, SoyDict.class).asNonNullable();
    private static final MethodRef CEIL_FN = MethodRef.create(CeilingFunction.class, "ceil", SoyValue.class)
            .asNonNullable();
    private static final MethodRef FLOOR_FN = MethodRef.create(FloorFunction.class, "floor", SoyValue.class)
            .asNonNullable();
    private static final MethodRef KEYS_FN = MethodRef.create(KeysFunction.class, "keys", SoyMap.class);
    private static final MethodRef LIST_SIZE = MethodRef.create(List.class, "size");
    private static final MethodRef MATH_CEIL = MethodRef.create(Math.class, "ceil", double.class).asCheap();
    private static final MethodRef MATH_FLOOR = MethodRef.create(Math.class, "floor", double.class).asCheap();
    private static final MethodRef MATH_MAX_DOUBLE = MethodRef.create(Math.class, "max", double.class, double.class)
            .asCheap();
    private static final MethodRef MATH_MAX_LONG = MethodRef.create(Math.class, "max", long.class, long.class)
            .asCheap();
    private static final MethodRef MATH_MIN_DOUBLE = MethodRef.create(Math.class, "min", double.class, double.class)
            .asCheap();
    private static final MethodRef MATH_MIN_LONG = MethodRef.create(Math.class, "min", long.class, long.class)
            .asCheap();
    private static final MethodRef MATH_ROUND = MethodRef.create(Math.class, "round", double.class).asCheap();
    private static final MethodRef MAX_FN = MethodRef
            .create(MaxFunction.class, "max", SoyValue.class, SoyValue.class).asNonNullable();
    private static final MethodRef MIN_FN = MethodRef
            .create(MinFunction.class, "min", SoyValue.class, SoyValue.class).asNonNullable();
    private static final MethodRef RANDOM_INT_FN = MethodRef
            .create(RandomIntFunction.class, "randomInt", long.class).asCheap();
    private static final MethodRef ROUND_FN = MethodRef.create(RoundFunction.class, "round", SoyValue.class)
            .asNonNullable();
    private static final MethodRef ROUND_WITH_NUM_DIGITS_AFTER_POINT_FN = MethodRef
            .create(RoundFunction.class, "round", SoyValue.class, int.class).asNonNullable();
    private static final MethodRef SOYLIST_LENGTH = MethodRef.create(SoyList.class, "length");
    private static final MethodRef STRING_CONTAINS = MethodRef.create(String.class, "contains", CharSequence.class);
    private static final MethodRef STRING_INDEX_OF = MethodRef.create(String.class, "indexOf", String.class);
    private static final MethodRef STRING_LENGTH = MethodRef.create(String.class, "length");
    private static final MethodRef STRING_SUBSTR_START = MethodRef.create(String.class, "substring", int.class)
            .asNonNullable();
    private static final MethodRef STRING_SUBSTR_START_END = MethodRef.create(String.class, "substring", int.class,
            int.class);

    private final VariableLookup variables;

    PluginFunctionCompiler(VariableLookup variables) {
        this.variables = variables;
    }

    /** Returns an expression that evaluates the given soy function. */
    SoyExpression callPluginFunction(FunctionNode node, List<SoyExpression> args) {
        switch (node.getFunctionName()) {
        case "augmentMap":
            return invokeAugmentMapFunction(args.get(0), args.get(1));
        case "ceiling":
            return invokeCeilingFunction(args.get(0));
        case "floor":
            return invokeFloorFunction(args.get(0));
        case "isNonnull":
            return invokeIsNonnullFunction(args.get(0));
        case "keys":
            return invokeKeysFunction(args.get(0));
        case "length":
            return invokeLengthFunction(args.get(0));
        case "max":
            return invokeMaxFunction(args.get(0), args.get(1));
        case "min":
            return invokeMinFunction(args.get(0), args.get(1));
        case "randomInt":
            return invokeRandomIntFunction(args.get(0));
        case "round":
            if (args.size() == 1) {
                return invokeRoundFunction(args.get(0));
            }
            return invokeRoundFunction(args.get(0), args.get(1));
        case "strContains":
            return invokeStrContainsFunction(args.get(0), args.get(1));
        case "strIndexOf":
            return invokeStrIndexOfFunction(args.get(0), args.get(1));
        case "strLen":
            return invokeStrLenFunction(args.get(0));
        case "strSub":
            if (args.size() == 2) {
                return invokeStrSubFunction(args.get(0), args.get(1));
            }
            return invokeStrSubFunction(args.get(0), args.get(1), args.get(2));
        default:
            // TODO(lukes): add support for the BidiFunctions
            return invokeSoyFunction(node, args);
        }
    }

    /**
     * @see com.google.template.soy.basicfunctions.AugmentMapFunction
     */
    private SoyExpression invokeAugmentMapFunction(SoyExpression arg0, SoyExpression arg1) {
        Expression first = arg0.cast(SoyDict.class);
        Expression second = arg1.cast(SoyDict.class);
        // TODO(lukes): this logic should move into the ResolveExpressionTypesVisitor
        MapType mapType = MapType.of(StringType.getInstance(),
                UnionType.of(getMapValueType(arg0.soyType()), getMapValueType(arg1.soyType())));
        return SoyExpression.forSoyValue(mapType, AUGMENT_MAP_FN.invoke(first, second));
    }

    private SoyType getMapValueType(SoyType type) {
        if (type instanceof MapType) {
            return ((MapType) type).getValueType();
        }
        return UnknownType.getInstance();
    }

    /**
     * @see com.google.template.soy.basicfunctions.CeilingFunction
     */
    private SoyExpression invokeCeilingFunction(SoyExpression argument) {
        switch (argument.resultType().getSort()) {
        case Type.LONG:
            return argument;
        case Type.DOUBLE:
            return SoyExpression
                    .forInt(BytecodeUtils.numericConversion(MATH_CEIL.invoke(argument), Type.LONG_TYPE));
        default:
            return SoyExpression.forSoyValue(IntType.getInstance(), CEIL_FN.invoke(argument.box()));
        }
    }

    /**
     * @see com.google.template.soy.basicfunctions.FloorFunction
     */
    private SoyExpression invokeFloorFunction(SoyExpression argument) {
        switch (argument.resultType().getSort()) {
        case Type.LONG:
            return argument;
        case Type.DOUBLE:
            return SoyExpression
                    .forInt(BytecodeUtils.numericConversion(MATH_FLOOR.invoke(argument), Type.LONG_TYPE));
        default:
            return SoyExpression.forSoyValue(IntType.getInstance(), FLOOR_FN.invoke(argument.box()));
        }
    }

    /**
     * @see com.google.template.soy.basicfunctions.IsNonnullFunction
     */
    private SoyExpression invokeIsNonnullFunction(final SoyExpression soyExpression) {
        if (BytecodeUtils.isPrimitive(soyExpression.resultType())) {
            return SoyExpression.TRUE;
        }
        return SoyExpression.forBool(new Expression(Type.BOOLEAN_TYPE, soyExpression.features()) {
            @Override
            void doGen(CodeBuilder adapter) {
                soyExpression.gen(adapter);
                Label isNull = new Label();
                adapter.ifNull(isNull);
                // non-null
                adapter.pushBoolean(true);
                Label end = new Label();
                adapter.goTo(end);
                adapter.mark(isNull);
                adapter.pushBoolean(false);
                adapter.mark(end);
            }
        });
    }

    /**
     * @see com.google.template.soy.basicfunctions.KeysFunction
     */
    private SoyExpression invokeKeysFunction(SoyExpression soyExpression) {
        SoyType argType = soyExpression.soyType();
        // TODO(lukes): this logic should live in ResolveExpressionTypesVisitor
        SoyType listElementType;
        if (argType instanceof MapType) {
            listElementType = ((MapType) argType).getKeyType(); // pretty much just string
        } else if (argType instanceof ListType) {
            listElementType = IntType.getInstance();
        } else {
            listElementType = UnknownType.getInstance();
        }
        return SoyExpression.forList(ListType.of(listElementType),
                KEYS_FN.invoke(soyExpression.box().cast(SoyMap.class)));
    }

    /**
     * @see com.google.template.soy.basicfunctions.LengthFunction
     */
    private SoyExpression invokeLengthFunction(SoyExpression soyExpression) {
        Expression lengthExpressionAsInt;
        if (soyExpression.isBoxed()) {
            lengthExpressionAsInt = soyExpression.cast(SoyList.class).invoke(SOYLIST_LENGTH);
        } else {
            lengthExpressionAsInt = soyExpression.cast(List.class).invoke(LIST_SIZE);
        }
        return SoyExpression.forInt(BytecodeUtils.numericConversion(lengthExpressionAsInt, Type.LONG_TYPE));
    }

    /**
     * @see com.google.template.soy.basicfunctions.MaxFunction
     */
    private SoyExpression invokeMaxFunction(SoyExpression left, SoyExpression right) {
        if (left.assignableToNullableInt() && right.assignableToNullableInt()) {
            return SoyExpression.forInt(MATH_MAX_LONG.invoke(left.unboxAs(long.class), right.unboxAs(long.class)));
        } else if (left.assignableToNullableFloat() && right.assignableToNullableFloat()) {
            return SoyExpression
                    .forFloat(MATH_MAX_DOUBLE.invoke(left.unboxAs(double.class), right.unboxAs(double.class)));
        } else {
            return SoyExpression.forSoyValue(NUMBER_TYPE, MAX_FN.invoke(left.box(), right.box()));
        }
    }

    /**
     * @see com.google.template.soy.basicfunctions.MinFunction
     */
    private SoyExpression invokeMinFunction(SoyExpression left, SoyExpression right) {
        if (left.assignableToNullableInt() && right.assignableToNullableInt()) {
            return SoyExpression.forInt(MATH_MIN_LONG.invoke(left.unboxAs(long.class), right.unboxAs(long.class)));
        } else if (left.assignableToNullableFloat() && right.assignableToNullableFloat()) {
            return SoyExpression
                    .forFloat(MATH_MIN_DOUBLE.invoke(left.unboxAs(double.class), right.unboxAs(double.class)));
        } else {
            return SoyExpression.forSoyValue(NUMBER_TYPE, MIN_FN.invoke(left.box(), right.box()));
        }
    }

    /**
     * @see com.google.template.soy.basicfunctions.RandomIntFunction
     */
    private SoyExpression invokeRandomIntFunction(SoyExpression soyExpression) {
        return SoyExpression.forInt(RANDOM_INT_FN.invoke(soyExpression.unboxAs(long.class)));
    }

    /**
     * @see com.google.template.soy.basicfunctions.RoundFunction
     */
    private SoyExpression invokeRoundFunction(SoyExpression soyExpression) {
        if (soyExpression.assignableToNullableInt()) {
            return soyExpression;
        }
        if (soyExpression.assignableToNullableFloat()) {
            return SoyExpression.forInt(MATH_ROUND.invoke(soyExpression.unboxAs(double.class)));
        }
        return SoyExpression.forInt(ROUND_FN.invoke(soyExpression.box()));
    }

    /**
     * @see com.google.template.soy.basicfunctions.RoundFunction
     */
    private SoyExpression invokeRoundFunction(SoyExpression value, SoyExpression digitsAfterPoint) {
        return SoyExpression.forSoyValue(NUMBER_TYPE, ROUND_WITH_NUM_DIGITS_AFTER_POINT_FN.invoke(value.box(),
                BytecodeUtils.numericConversion(digitsAfterPoint.unboxAs(long.class), Type.INT_TYPE)));
    }

    private SoyExpression invokeSoyFunction(FunctionNode node, List<SoyExpression> args) {
        Expression soyJavaFunctionExpr = MethodRef.RENDER_CONTEXT_GET_FUNCTION.invoke(variables.getRenderContext(),
                constant(node.getFunctionName()));
        Expression list = SoyExpression.asBoxedList(args);
        return SoyExpression.forSoyValue(UnknownType.getInstance(),
                MethodRef.RUNTIME_CALL_SOY_FUNCTION.invoke(soyJavaFunctionExpr, list));
    }

    /**
     * @see com.google.template.soy.basicfunctions.StrContainsFunction
     */
    private SoyExpression invokeStrContainsFunction(SoyExpression left, SoyExpression right) {
        return SoyExpression
                .forBool(left.unboxAs(String.class).invoke(STRING_CONTAINS, right.unboxAs(String.class)));
    }

    /**
     * @see com.google.template.soy.basicfunctions.StrIndexOfFunction
     */
    private SoyExpression invokeStrIndexOfFunction(SoyExpression left, SoyExpression right) {
        return SoyExpression.forInt(BytecodeUtils.numericConversion(
                left.unboxAs(String.class).invoke(STRING_INDEX_OF, right.unboxAs(String.class)), Type.LONG_TYPE));
    }

    /**
     * @see com.google.template.soy.basicfunctions.StrIndexOfFunction
     */
    private SoyExpression invokeStrLenFunction(SoyExpression str) {
        return SoyExpression.forInt(
                BytecodeUtils.numericConversion(str.unboxAs(String.class).invoke(STRING_LENGTH), Type.LONG_TYPE));
    }

    /**
     * @see com.google.template.soy.basicfunctions.StrSubFunction
     */
    private SoyExpression invokeStrSubFunction(SoyExpression str, SoyExpression startIndex) {
        return SoyExpression.forString(str.unboxAs(String.class).invoke(STRING_SUBSTR_START,
                BytecodeUtils.numericConversion(startIndex.unboxAs(long.class), Type.INT_TYPE)));
    }

    /**
     * @see com.google.template.soy.basicfunctions.StrSubFunction
     */
    private SoyExpression invokeStrSubFunction(SoyExpression str, SoyExpression startIndex,
            SoyExpression endIndex) {
        return SoyExpression.forString(str.unboxAs(String.class).invoke(STRING_SUBSTR_START_END,
                BytecodeUtils.numericConversion(startIndex.unboxAs(long.class), Type.INT_TYPE),
                BytecodeUtils.numericConversion(endIndex.unboxAs(long.class), Type.INT_TYPE)));
    }
}