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.restricted; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import com.google.common.base.Optional; import com.google.common.base.Utf8; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.primitives.Ints; import com.google.protobuf.Message; import com.google.template.soy.base.internal.SanitizedContentKind; import com.google.template.soy.data.Dir; import com.google.template.soy.data.LoggingAdvisingAppendable; import com.google.template.soy.data.SanitizedContent; import com.google.template.soy.data.SanitizedContent.ContentKind; import com.google.template.soy.data.SoyLegacyObjectMap; import com.google.template.soy.data.SoyList; import com.google.template.soy.data.SoyMap; import com.google.template.soy.data.SoyProtoValue; import com.google.template.soy.data.SoyRecord; import com.google.template.soy.data.SoyValue; import com.google.template.soy.data.SoyValueProvider; import com.google.template.soy.data.restricted.IntegerData; import com.google.template.soy.data.restricted.SoyString; import com.google.template.soy.jbcsrc.api.RenderResult; import com.google.template.soy.jbcsrc.restricted.Expression.Feature; import com.google.template.soy.jbcsrc.restricted.Expression.Features; import com.google.template.soy.jbcsrc.shared.CompiledTemplate; import com.google.template.soy.jbcsrc.shared.Names; import com.google.template.soy.jbcsrc.shared.RenderContext; import java.io.Closeable; import java.lang.reflect.Array; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.annotation.Nullable; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.Label; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.commons.Method; import org.objectweb.asm.util.Printer; /** A set of utilities for generating simple expressions in bytecode */ public final class BytecodeUtils { private static final String LARGE_STRING_CONSTANT_NAME = "$const_string"; // https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.4.7 private static final int MAX_CONSTANT_STRING_LENGTH = 65535; public static final TypeInfo OBJECT = TypeInfo.create(Object.class); private static final Type OBJECT_ARRAY_TYPE = Type.getType(Object[].class); public static final Type LOGGING_ADVISING_APPENDABLE_TYPE = Type.getType(LoggingAdvisingAppendable.class); public static final Type LOGGING_ADVISING_BUILDER_TYPE = Type .getType(LoggingAdvisingAppendable.BufferingAppendable.class); public static final Type ARRAY_LIST_TYPE = Type.getType(ArrayList.class); public static final Type COMPILED_TEMPLATE_TYPE = Type.getType(CompiledTemplate.class); public static final Type CONTENT_KIND_TYPE = Type.getType(ContentKind.class); public static final Type CLOSEABLE_TYPE = Type.getType(Closeable.class); public static final Type DIR_TYPE = Type.getType(Dir.class); public static final Type HASH_MAP_TYPE = Type.getType(HashMap.class); public static final Type INTEGER_DATA_TYPE = Type.getType(IntegerData.class); public static final Type LINKED_HASH_MAP_TYPE = Type.getType(LinkedHashMap.class); public static final Type LIST_TYPE = Type.getType(List.class); public static final Type MAP_TYPE = Type.getType(Map.class); public static final Type MAP_ENTRY_TYPE = Type.getType(Map.Entry.class); public static final Type MESSAGE_TYPE = Type.getType(Message.class); public static final Type NULL_POINTER_EXCEPTION_TYPE = Type.getType(NullPointerException.class); public static final Type RENDER_CONTEXT_TYPE = Type.getType(RenderContext.class); public static final Type RENDER_RESULT_TYPE = Type.getType(RenderResult.class); public static final Type SANITIZED_CONTENT_TYPE = Type.getType(SanitizedContent.class); public static final Type SOY_LIST_TYPE = Type.getType(SoyList.class); public static final Type SOY_LEGACY_OBJECT_MAP_TYPE = Type.getType(SoyLegacyObjectMap.class); public static final Type SOY_MAP_TYPE = Type.getType(SoyMap.class); public static final Type SOY_PROTO_VALUE_TYPE = Type.getType(SoyProtoValue.class); public static final Type SOY_RECORD_TYPE = Type.getType(SoyRecord.class); public static final Type SOY_VALUE_TYPE = Type.getType(SoyValue.class); public static final Type SOY_VALUE_PROVIDER_TYPE = Type.getType(SoyValueProvider.class); public static final Type SOY_STRING_TYPE = Type.getType(SoyString.class); public static final Type STRING_TYPE = Type.getType(String.class); public static final Type THROWABLE_TYPE = Type.getType(Throwable.class); public static final Type ILLEGAL_STATE_EXCEPTION_TYPE = Type.getType(IllegalStateException.class); public static final Method CLASS_INIT = Method.getMethod("void <clinit>()"); public static final Method NULLARY_INIT = Method.getMethod("void <init>()"); private static final LoadingCache<Type, Optional<Class<?>>> objectTypeToClassCache = CacheBuilder.newBuilder() .build(new CacheLoader<Type, Optional<Class<?>>>() { @Override public Optional<Class<?>> load(Type key) throws Exception { switch (key.getSort()) { case Type.ARRAY: Optional<Class<?>> elementType = objectTypeToClassCache.getUnchecked(key.getElementType()); if (elementType.isPresent()) { // The easiest way to generically get an array class. return Optional.<Class<?>>of(Array.newInstance(elementType.get(), 0).getClass()); } return Optional.absent(); case Type.VOID: return Optional.<Class<?>>of(void.class); case Type.BOOLEAN: return Optional.<Class<?>>of(boolean.class); case Type.BYTE: return Optional.<Class<?>>of(byte.class); case Type.CHAR: return Optional.<Class<?>>of(char.class); case Type.DOUBLE: return Optional.<Class<?>>of(double.class); case Type.INT: return Optional.<Class<?>>of(int.class); case Type.SHORT: return Optional.<Class<?>>of(short.class); case Type.LONG: return Optional.<Class<?>>of(long.class); case Type.FLOAT: return Optional.<Class<?>>of(float.class); case Type.OBJECT: try { String className = key.getClassName(); if (className.startsWith(Names.CLASS_PREFIX)) { // if the class is generated, don't try to look it up. // It might actually succeed in a case where we have the class on our // classpath already! return Optional.absent(); } return Optional.<Class<?>>of( Class.forName(className, false, BytecodeUtils.class.getClassLoader())); } catch (ClassNotFoundException e) { return Optional.absent(); } default: throw new IllegalArgumentException("unsupported type: " + key); } } }); private BytecodeUtils() { } /** * Returns {@code true} if {@code left} is possibly assignable from {@code right}. * * <p>Analogous to {@code right instanceof left}. */ public static boolean isPossiblyAssignableFrom(Type left, Type right) { return doIsAssignableFrom(left, right, true); } /** * Returns {@code true} if {@code left} is definitely assignable from {@code right}. * * <p>Analogous to {@code right instanceof left}. */ public static boolean isDefinitelyAssignableFrom(Type left, Type right) { return doIsAssignableFrom(left, right, false); } /** * Checks if {@code left} is assignable from {@code right}, however if we don't have information * about one of the types then this returns {@code failOpen}. */ private static boolean doIsAssignableFrom(Type left, Type right, boolean failOpen) { if (left.equals(right)) { return true; } if (left.getSort() != right.getSort()) { return false; } if (left.getSort() != Type.OBJECT) { return false; // all other sorts require exact equality (even arrays) } // for object types we really need to know type hierarchy information to test for whether // right is assignable to left. Optional<Class<?>> leftClass = objectTypeToClassCache.getUnchecked(left); Optional<Class<?>> rightClass = objectTypeToClassCache.getUnchecked(right); if (!leftClass.isPresent() || !rightClass.isPresent()) { // This means one of the types being compared is a generated object. So we can't easily check // it. Just delegate responsibility to the verifier. return failOpen; } return leftClass.get().isAssignableFrom(rightClass.get()); } /** * Returns the runtime class represented by the given type. * * @throws IllegalArgumentException if the class cannot be found. It is expected that this method * will only be called for types that have a runtime on the compilers classpath. */ public static Class<?> classFromAsmType(Type type) { Optional<Class<?>> maybeClass = objectTypeToClassCache.getUnchecked(type); if (!maybeClass.isPresent()) { throw new IllegalArgumentException("Could not load: " + type); } return maybeClass.get(); } private static final Expression FALSE = new Expression(Type.BOOLEAN_TYPE, Feature.CHEAP) { @Override protected void doGen(CodeBuilder mv) { mv.pushBoolean(false); } }; private static final Expression TRUE = new Expression(Type.BOOLEAN_TYPE, Feature.CHEAP) { @Override protected void doGen(CodeBuilder mv) { mv.pushBoolean(true); } }; /** Returns an {@link Expression} that can load the given boolean constant. */ public static Expression constant(boolean value) { return value ? TRUE : FALSE; } /** Returns an {@link Expression} that can load the given int constant. */ public static Expression constant(final int value) { return new Expression(Type.INT_TYPE, Feature.CHEAP) { @Override protected void doGen(CodeBuilder mv) { mv.pushInt(value); } }; } /** Returns an {@link Expression} that can load the given char constant. */ public static Expression constant(final char value) { return new Expression(Type.CHAR_TYPE, Feature.CHEAP) { @Override protected void doGen(CodeBuilder mv) { mv.pushInt(value); } }; } /** Returns an {@link Expression} that can load the given long constant. */ public static Expression constant(final long value) { return new Expression(Type.LONG_TYPE, Feature.CHEAP) { @Override protected void doGen(CodeBuilder mv) { mv.pushLong(value); } }; } /** Returns an {@link Expression} that can load the given double constant. */ public static Expression constant(final double value) { return new Expression(Type.DOUBLE_TYPE, Feature.CHEAP) { @Override protected void doGen(CodeBuilder mv) { mv.pushDouble(value); } }; } /** Returns an {@link Expression} that can load the given String constant. */ public static Expression constant(final String value) { checkNotNull(value); checkArgument(Utf8.encodedLength(value) <= MAX_CONSTANT_STRING_LENGTH, "String is too long when encoded in utf8"); return stringConstant(value); } /** * Returns an {@link Expression} that can load the given String constant. * * <p>Unlike {@link #constant(String)} this can handle strings larger than 65K bytes. */ public static Expression constant(String value, ClassFieldManager manager) { int encodedLength = Utf8.encodedLength(value); if (encodedLength <= MAX_CONSTANT_STRING_LENGTH) { return stringConstant(value); } // else it is too big for a single constant pool entry so split it into a small number of // entries and generate a static final field to hold the cat'ed value. int startIndex = 0; Expression stringExpression = null; int length = value.length(); do { int endIndex = offsetOf65KUtf8Bytes(value, startIndex, length); // N.B. we may end up splitting the string at a surrogate pair, but the class format uses // modified utf8 which is forgiving about such things. Expression substringConstant = stringConstant(value.substring(startIndex, endIndex)); startIndex = endIndex; if (stringExpression == null) { stringExpression = substringConstant; } else { stringExpression = stringExpression.invoke(MethodRef.STRING_CONCAT, substringConstant); } } while (startIndex < length); FieldRef fieldRef = manager.addStaticField(LARGE_STRING_CONSTANT_NAME, stringExpression); return fieldRef.accessor(); } /** * Returns the largest index between {@code startIndex} and {@code endIdex} such that the UTF8 * encoded bytes of {@code str.substring(startIndex, returnValue}} is less than or equal to 65K. */ private static int offsetOf65KUtf8Bytes(String str, int startIndex, int endIndex) { // This implementation is based off of Utf8.encodedLength int utf8Length = 0; int i = startIndex; for (; i < endIndex; i++) { char c = str.charAt(i); utf8Length++; if (c < 0x800) { utf8Length += (0x7f - c) >>> 31; // branch free! } else { utf8Length += Character.isSurrogate(c) ? 1 : 2; } if (utf8Length == MAX_CONSTANT_STRING_LENGTH) { return i + 1; } else if (utf8Length > MAX_CONSTANT_STRING_LENGTH) { return i; } } return endIndex; } private static Expression stringConstant(final String value) { return new Expression(STRING_TYPE, Feature.CHEAP, Feature.NON_NULLABLE) { @Override protected void doGen(CodeBuilder mv) { mv.pushString(value); } }; } /** Returns an {@link Expression} that evaluates to the given ContentKind, or null. */ public static Expression constant(@Nullable ContentKind kind) { return (kind == null) ? BytecodeUtils.constantNull(CONTENT_KIND_TYPE) : FieldRef.enumReference(kind).accessor(); } /** * Returns an {@link Expression} that evaluates to the {@link ContentKind} value that is * equivalent to the given {@link SanitizedContentKind}, or null. */ public static Expression constantSanitizedContentKindAsContentKind(@Nullable SanitizedContentKind kind) { return (kind == null) ? BytecodeUtils.constantNull(CONTENT_KIND_TYPE) : FieldRef.enumReference(ContentKind.valueOf(kind.name())).accessor(); } /** Returns an {@link Expression} that evaluates to the given Dir, or null. */ public static Expression constant(@Nullable Dir dir) { return (dir == null) ? BytecodeUtils.constantNull(DIR_TYPE) : FieldRef.enumReference(dir).accessor(); } /** Returns an {@link Expression} with the given type that always returns null. */ public static Expression constantNull(Type type) { checkArgument(type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY, "%s is not a reference type", type); return new Expression(type, Feature.CHEAP) { @Override protected void doGen(CodeBuilder mv) { mv.visitInsn(Opcodes.ACONST_NULL); } }; } /** * Returns an expression that does a numeric conversion cast from the given expression to the * given type. * * @throws IllegalArgumentException if either the expression or the target type is not a numeric * primitive */ public static Expression numericConversion(final Expression expr, final Type to) { if (to.equals(expr.resultType())) { return expr; } if (!isNumericPrimitive(to) || !isNumericPrimitive(expr.resultType())) { throw new IllegalArgumentException("Cannot convert from " + expr.resultType() + " to " + to); } return new Expression(to, expr.features()) { @Override protected void doGen(CodeBuilder adapter) { expr.gen(adapter); adapter.cast(expr.resultType(), to); } }; } private static boolean isNumericPrimitive(Type type) { int sort = type.getSort(); switch (sort) { case Type.OBJECT: case Type.ARRAY: case Type.VOID: case Type.METHOD: case Type.BOOLEAN: return false; case Type.BYTE: case Type.CHAR: case Type.DOUBLE: case Type.INT: case Type.SHORT: case Type.LONG: case Type.FLOAT: return true; default: throw new AssertionError("unexpected type " + type); } } /** Returns {@code true} if {@link Type} is a primitive type. */ public static boolean isPrimitive(Type type) { switch (type.getSort()) { case Type.OBJECT: case Type.ARRAY: return false; case Type.BOOLEAN: case Type.BYTE: case Type.CHAR: case Type.DOUBLE: case Type.INT: case Type.SHORT: case Type.LONG: case Type.FLOAT: return true; case Type.VOID: case Type.METHOD: throw new IllegalArgumentException("Invalid type: " + type); default: throw new AssertionError("unexpected type " + type); } } /** * Generates a default nullary public constructor for the given type on the {@link ClassVisitor}. * * <p>For java classes this is normally generated by the compiler and looks like: * * <pre>{@code * public Foo() { * super(); * } * }</pre> */ public static void defineDefaultConstructor(ClassVisitor cv, TypeInfo ownerType) { CodeBuilder mg = new CodeBuilder(Opcodes.ACC_PUBLIC, NULLARY_INIT, null, cv); mg.visitCode(); Label start = mg.mark(); Label end = mg.newLabel(); LocalVariable thisVar = LocalVariable.createThisVar(ownerType, start, end); thisVar.gen(mg); mg.invokeConstructor(OBJECT.type(), NULLARY_INIT); mg.returnValue(); mg.mark(end); thisVar.tableEntry(mg); mg.endMethod(); } // TODO(lukes): some of these branch operators are a little too branchy. For example, the // expression a == b || a == c, could be implemented by // logicalOr(compare(Opcodes.IFEQ, a, b), compare(Opcodes.IFEQ, a, c)), but that is not optimal // instead we could allow compare to take an expression for what to do when the comparison fails // that way we could save a branch. Maybe these operators are a failed abstraction? /** Compares the two primitive valued expressions using the provided comparison operation. */ public static Expression compare(final int comparisonOpcode, final Expression left, final Expression right) { checkArgument(left.resultType().equals(right.resultType()), "left and right must have matching types, found %s and %s", left.resultType(), right.resultType()); checkIntComparisonOpcode(left.resultType(), comparisonOpcode); Features features = Expression.areAllCheap(left, right) ? Features.of(Feature.CHEAP) : Features.of(); return new Expression(Type.BOOLEAN_TYPE, features) { @Override protected void doGen(CodeBuilder mv) { left.gen(mv); right.gen(mv); Label ifTrue = mv.newLabel(); Label end = mv.newLabel(); mv.ifCmp(left.resultType(), comparisonOpcode, ifTrue); mv.pushBoolean(false); mv.goTo(end); mv.mark(ifTrue); mv.pushBoolean(true); mv.mark(end); } }; } private static void checkIntComparisonOpcode(Type comparisonType, int opcode) { switch (opcode) { case Opcodes.IFEQ: case Opcodes.IFNE: return; case Opcodes.IFGT: case Opcodes.IFGE: case Opcodes.IFLT: case Opcodes.IFLE: if (comparisonType.getSort() == Type.ARRAY || comparisonType.getSort() == Type.OBJECT) { throw new IllegalArgumentException( "Type: " + comparisonType + " cannot be compared via " + Printer.OPCODES[opcode]); } return; default: throw new IllegalArgumentException("Unsupported opcode for comparison operation: " + opcode); } } /** * Returns an expression that evaluates to the logical negation of the given boolean valued * expression. */ public static Expression logicalNot(final Expression baseExpr) { baseExpr.checkAssignableTo(Type.BOOLEAN_TYPE); checkArgument(baseExpr.resultType().equals(Type.BOOLEAN_TYPE), "not a boolean expression"); return new Expression(Type.BOOLEAN_TYPE, baseExpr.features()) { @Override protected void doGen(CodeBuilder mv) { baseExpr.gen(mv); // Surprisingly, java bytecode uses a branch (instead of 'xor 1' or something) to implement // this. This is most likely useful for allowing true to be represented by any non-zero // number. Label ifTrue = mv.newLabel(); Label end = mv.newLabel(); mv.ifZCmp(Opcodes.IFNE, ifTrue); // if not 0 goto ifTrue mv.pushBoolean(true); mv.goTo(end); mv.mark(ifTrue); mv.pushBoolean(false); mv.mark(end); } }; } /** Compares two {@link SoyExpression}s for equality using soy == semantics. */ public static Expression compareSoyEquals(final SoyExpression left, final SoyExpression right) { // We can special case when we know the types. // If either is a string, we run special logic so test for that first // otherwise we special case primitives and eventually fall back to our runtime. SoyRuntimeType leftRuntimeType = left.soyRuntimeType(); SoyRuntimeType rightRuntimeType = right.soyRuntimeType(); if (leftRuntimeType.isKnownString()) { return doEqualsString(left.unboxAs(String.class), right); } if (rightRuntimeType.isKnownString()) { // TODO(lukes): we are changing the order of evaluation here. return doEqualsString(right.unboxAs(String.class), left); } if (leftRuntimeType.isKnownInt() && rightRuntimeType.isKnownInt() && left.isNonNullable() && right.isNonNullable()) { return compare(Opcodes.IFEQ, left.unboxAs(long.class), right.unboxAs(long.class)); } if (leftRuntimeType.isKnownNumber() && rightRuntimeType.isKnownNumber() && left.isNonNullable() && right.isNonNullable() && (leftRuntimeType.isKnownFloat() || rightRuntimeType.isKnownFloat())) { return compare(Opcodes.IFEQ, left.coerceToDouble(), right.coerceToDouble()); } return MethodRef.RUNTIME_EQUAL.invoke(left.box(), right.box()); } /** * Compare a string valued expression to another expression using soy == semantics. * * @param stringExpr An expression that is known to be an unboxed string * @param other An expression to compare it to. */ private static Expression doEqualsString(SoyExpression stringExpr, SoyExpression other) { // This is compatible with SharedRuntime.compareString, which interestingly makes == break // transitivity. See b/21461181 SoyRuntimeType otherRuntimeType = other.soyRuntimeType(); if (otherRuntimeType.isKnownStringOrSanitizedContent()) { if (stringExpr.isNonNullable()) { return stringExpr.invoke(MethodRef.EQUALS, other.unboxAs(String.class)); } else { return MethodRef.OBJECTS_EQUALS.invoke(stringExpr, other.unboxAs(String.class)); } } if (otherRuntimeType.isKnownNumber() && other.isNonNullable()) { // in this case, we actually try to convert stringExpr to a number return MethodRef.RUNTIME_STRING_EQUALS_AS_NUMBER.invoke(stringExpr, other.coerceToDouble()); } // We don't know what other is, assume the worst and call out to our boxed implementation for // string comparisons. return MethodRef.RUNTIME_COMPARE_NULLABLE_STRING.invoke(stringExpr, other.box()); } /** * Returns an expression that evaluates to {@code left} if left is non null, and evaluates to * {@code right} otherwise. */ public static Expression firstNonNull(final Expression left, final Expression right) { checkArgument(left.resultType().getSort() == Type.OBJECT); checkArgument(right.resultType().getSort() == Type.OBJECT); Features features = Features.of(); if (Expression.areAllCheap(left, right)) { features = features.plus(Feature.CHEAP); } if (right.isNonNullable()) { features = features.plus(Feature.NON_NULLABLE); } return new Expression(left.resultType(), features) { @Override protected void doGen(CodeBuilder cb) { Label leftIsNonNull = new Label(); left.gen(cb); // Stack: L cb.dup(); // Stack: L, L cb.ifNonNull(leftIsNonNull); // Stack: L // pop the extra copy of left cb.pop(); // Stack: right.gen(cb); // Stack: R cb.mark(leftIsNonNull); // At this point the stack has an instance of L or R } }; } /** * Returns an expression that evaluates equivalently to a java ternary expression: {@code * condition ? left : right} */ public static Expression ternary(final Expression condition, final Expression trueBranch, final Expression falseBranch) { checkArgument(condition.resultType().equals(Type.BOOLEAN_TYPE)); checkArgument(trueBranch.resultType().getSort() == falseBranch.resultType().getSort()); Features features = Features.of(); if (Expression.areAllCheap(condition, trueBranch, falseBranch)) { features = features.plus(Feature.CHEAP); } if (trueBranch.isNonNullable() && falseBranch.isNonNullable()) { features = features.plus(Feature.NON_NULLABLE); } return new Expression(trueBranch.resultType(), features) { @Override protected void doGen(CodeBuilder mv) { condition.gen(mv); Label ifFalse = new Label(); Label end = new Label(); mv.visitJumpInsn(Opcodes.IFEQ, ifFalse); // if 0 goto ifFalse trueBranch.gen(mv); // eval true branch mv.visitJumpInsn(Opcodes.GOTO, end); // jump to the end mv.visitLabel(ifFalse); falseBranch.gen(mv); // eval false branch mv.visitLabel(end); } }; } /** * Implements the short circuiting logical or ({@code ||}) operator over the list of boolean * expressions. */ public static Expression logicalOr(Expression... expressions) { return logicalOr(ImmutableList.copyOf(expressions)); } /** * Implements the short circuiting logical or ({@code ||}) operator over the list of boolean * expressions. */ public static Expression logicalOr(List<? extends Expression> expressions) { return doShortCircuitingLogicalOperator(ImmutableList.copyOf(expressions), true); } /** * Implements the short circuiting logical and ({@code &&}) operator over the list of boolean * expressions. */ public static Expression logicalAnd(Expression... expressions) { return logicalAnd(ImmutableList.copyOf(expressions)); } /** * Implements the short circuiting logical and ({@code &&}) operator over the list of boolean * expressions. */ public static Expression logicalAnd(List<? extends Expression> expressions) { return doShortCircuitingLogicalOperator(ImmutableList.copyOf(expressions), false); } private static Expression doShortCircuitingLogicalOperator( final ImmutableList<? extends Expression> expressions, final boolean isOrOperator) { checkArgument(!expressions.isEmpty()); for (Expression expr : expressions) { expr.checkAssignableTo(Type.BOOLEAN_TYPE); } if (expressions.size() == 1) { return expressions.get(0); } return new Expression(Type.BOOLEAN_TYPE, Expression.areAllCheap(expressions) ? Features.of(Feature.CHEAP) : Features.of()) { @Override protected void doGen(CodeBuilder adapter) { Label end = new Label(); Label shortCircuit = new Label(); for (int i = 0; i < expressions.size(); i++) { Expression expr = expressions.get(i); expr.gen(adapter); if (i == expressions.size() - 1) { // if we are the last one, just goto end. Whatever the result of the last expression is // determines the result of the whole expression (when all prior tests fail). adapter.goTo(end); } else { adapter.ifZCmp(isOrOperator ? Opcodes.IFNE : Opcodes.IFEQ, shortCircuit); } } adapter.mark(shortCircuit); adapter.pushBoolean(isOrOperator); // default for || is true && is false adapter.mark(end); } }; } /** * Returns an expression that returns a new {@code ImmutableList} containing the given items. * * <p>NOTE: {@code ImmutableList} rejects null elements. */ public static Expression asImmutableList(Iterable<? extends Expression> items) { ImmutableList<Expression> copy = ImmutableList.copyOf(items); if (copy.size() < MethodRef.IMMUTABLE_LIST_OF.size()) { return MethodRef.IMMUTABLE_LIST_OF.get(copy.size()).invoke(copy); } ImmutableList<Expression> explicit = copy.subList(0, MethodRef.IMMUTABLE_LIST_OF.size()); Expression remainder = asArray(OBJECT_ARRAY_TYPE, copy.subList(MethodRef.IMMUTABLE_LIST_OF.size(), copy.size())); return MethodRef.IMMUTABLE_LIST_OF_ARRAY.invoke(Iterables.concat(explicit, ImmutableList.of(remainder))); } private static Expression asArray(final Type arrayType, final ImmutableList<? extends Expression> elements) { final Type elementType = arrayType.getElementType(); return new Expression(arrayType, Feature.NON_NULLABLE) { @Override protected void doGen(CodeBuilder adapter) { adapter.pushInt(elements.size()); adapter.newArray(elementType); for (int i = 0; i < elements.size(); i++) { adapter.dup(); // dup the array adapter.pushInt(i); // the index to store into elements.get(i).gen(adapter); // the element to store adapter.arrayStore(elementType); } } }; } /** Returns an expression that returns a new {@link ArrayList} containing all the given items. */ public static Expression asList(Iterable<? extends Expression> items) { final ImmutableList<Expression> copy = ImmutableList.copyOf(items); if (copy.isEmpty()) { return MethodRef.IMMUTABLE_LIST_OF.get(0).invoke(); } // Note, we cannot necessarily use ImmutableList for anything besides the empty list because // we may need to put a null in it. final Expression construct = ConstructorRef.ARRAY_LIST_SIZE.construct(constant(copy.size())); return new Expression(ARRAY_LIST_TYPE, Feature.NON_NULLABLE) { @Override protected void doGen(CodeBuilder mv) { construct.gen(mv); for (Expression child : copy) { mv.dup(); child.gen(mv); MethodRef.ARRAY_LIST_ADD.invokeUnchecked(mv); mv.pop(); // pop the bool result of arraylist.add } } }; } /** * Outputs bytecode that will test the item at the top of the stack for null, and branch to {@code * nullExit} if it is {@code null}. At {@code nullSafeExit} there will be a null value at the top * of the stack. */ public static void nullCoalesce(CodeBuilder builder, Label nullExit) { builder.dup(); Label nonNull = new Label(); builder.ifNonNull(nonNull); // See http://mail.ow2.org/wws/arc/asm/2016-02/msg00001.html for a discussion of this pattern // but even though the value at the top of the stack here is null, its type isn't. So we need // to pop and push. This is the idiomatic pattern. builder.pop(); builder.pushNull(); builder.goTo(nullExit); builder.mark(nonNull); } /** * Outputs bytecode that unboxes the current top element of the stack as {@code asType}. Top of * stack must not be null. * * <p>Always prefer using {@link SoyExpression#unboxAs} over this method, whenever possible. * * <p>Guarantees: * Bytecode output will not change stack height * Output will only change the top * element, and nothing below that * * @return the type of the result of the unbox operation */ public static Type unboxUnchecked(CodeBuilder cb, SoyRuntimeType soyType, Class<?> asType) { checkArgument(soyType.isBoxed(), "Expected %s to be a boxed type", soyType); Type fromType = soyType.runtimeType(); checkArgument(!SoyValue.class.isAssignableFrom(asType), "Can't use unboxUnchecked() to convert from %s to a SoyValue: %s.", fromType, asType); // No-op conversion if (isDefinitelyAssignableFrom(Type.getType(asType), fromType)) { return fromType; } if (asType.equals(boolean.class)) { MethodRef.SOY_VALUE_BOOLEAN_VALUE.invokeUnchecked(cb); return Type.BOOLEAN_TYPE; } if (asType.equals(long.class)) { MethodRef.SOY_VALUE_LONG_VALUE.invokeUnchecked(cb); return Type.LONG_TYPE; } if (asType.equals(double.class)) { MethodRef.SOY_VALUE_FLOAT_VALUE.invokeUnchecked(cb); return Type.DOUBLE_TYPE; } if (asType.equals(String.class)) { MethodRef.SOY_VALUE_STRING_VALUE.invokeUnchecked(cb); return STRING_TYPE; } if (asType.equals(List.class)) { cb.checkCast(SOY_LIST_TYPE); MethodRef.SOY_LIST_AS_JAVA_LIST.invokeUnchecked(cb); return LIST_TYPE; } if (asType.equals(Message.class)) { if (!isDefinitelyAssignableFrom(SOY_PROTO_VALUE_TYPE, fromType)) { cb.checkCast(SOY_PROTO_VALUE_TYPE); } MethodRef.SOY_PROTO_VALUE_GET_PROTO.invokeUnchecked(cb); return MESSAGE_TYPE; } throw new UnsupportedOperationException("Can't unbox top of stack from " + fromType + " to " + asType); } /** Returns an expression that returns a new {@link HashMap} containing all the given entries. */ public static Expression newHashMap(Iterable<? extends Expression> keys, Iterable<? extends Expression> values) { return newMap(keys, values, ConstructorRef.HASH_MAP_CAPACITY, HASH_MAP_TYPE); } /** * Returns an expression that returns a new {@link LinkedHashMap} containing all the given * entries. */ public static Expression newLinkedHashMap(Iterable<? extends Expression> keys, Iterable<? extends Expression> values) { return newMap(keys, values, ConstructorRef.LINKED_HASH_MAP_CAPACITY, LINKED_HASH_MAP_TYPE); } private static Expression newMap(Iterable<? extends Expression> keys, Iterable<? extends Expression> values, ConstructorRef constructorRef, Type mapType) { final ImmutableList<Expression> keysCopy = ImmutableList.copyOf(keys); final ImmutableList<Expression> valuesCopy = ImmutableList.copyOf(values); checkArgument(keysCopy.size() == valuesCopy.size()); for (int i = 0; i < keysCopy.size(); i++) { checkArgument(keysCopy.get(i).resultType().getSort() == Type.OBJECT); checkArgument(valuesCopy.get(i).resultType().getSort() == Type.OBJECT); } final Expression construct = constructorRef.construct(constant(hashMapCapacity(keysCopy.size()))); return new Expression(mapType, Feature.NON_NULLABLE) { @Override protected void doGen(CodeBuilder mv) { construct.gen(mv); for (int i = 0; i < keysCopy.size(); i++) { Expression key = keysCopy.get(i); Expression value = valuesCopy.get(i); mv.dup(); key.gen(mv); value.gen(mv); MethodRef.MAP_PUT.invokeUnchecked(mv); mv.pop(); // pop the Object result of map.put } } }; } private static int hashMapCapacity(int expectedSize) { if (expectedSize < 3) { return expectedSize + 1; } if (expectedSize < Ints.MAX_POWER_OF_TWO) { // This is the calculation used in JDK8 to resize when a putAll // happens; it seems to be the most conservative calculation we // can make. 0.75 is the default load factor. return (int) (expectedSize / 0.75F + 1.0F); } return Integer.MAX_VALUE; // any large value } /** * Returns a {@link SoyExpression} that evaluates to true if the expression evaluated to a * non-null value. */ public static SoyExpression isNonNull(final Expression expr) { if (BytecodeUtils.isPrimitive(expr.resultType())) { return SoyExpression.TRUE; } return SoyExpression.forBool(new Expression(Type.BOOLEAN_TYPE, expr.features()) { @Override protected void doGen(CodeBuilder adapter) { expr.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); } }); } /** Returns a {@link SoyExpression} that evaluates to true if the expression evaluated to null. */ public static SoyExpression isNull(final Expression expr) { if (BytecodeUtils.isPrimitive(expr.resultType())) { return SoyExpression.FALSE; } // This is what javac generates for 'someObject == null' return SoyExpression.forBool(new Expression(Type.BOOLEAN_TYPE, expr.features()) { @Override protected void doGen(CodeBuilder adapter) { expr.gen(adapter); Label isNull = new Label(); adapter.ifNull(isNull); // non-null adapter.pushBoolean(false); Label end = new Label(); adapter.goTo(end); adapter.mark(isNull); adapter.pushBoolean(true); adapter.mark(end); } }); } }