Java tutorial
/* * 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))); } }