blob: 01aa824a045d59aa52a935e5d736302e715abfaf [file] [log] [blame]
/*
* Copyright 2019 The Bazel Authors. All rights reserved.
*
* 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.devtools.build.android.desugar.langmodel;
import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableTable;
import com.google.common.collect.Streams;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
/** A utility class for the desguaring of nest-based access control classes. */
public final class LangModelHelper {
/**
* The lookup table for dup instructional opcodes. The row key is the count of words on stack top
* to be duplicated. The column key is gap in word count between the original word section on
* stack top and the post-duplicated word section underneath. See from
*
* <p>https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-6.html#jvms-6.5.dup
*/
private static final ImmutableTable<Integer, Integer, Integer> DIRECT_DUP_OPCODES =
ImmutableTable.<Integer, Integer, Integer>builder()
.put(1, 0, Opcodes.DUP)
.put(2, 0, Opcodes.DUP2)
.put(1, 1, Opcodes.DUP_X1)
.put(2, 1, Opcodes.DUP2_X1)
.put(1, 2, Opcodes.DUP_X2)
.put(2, 2, Opcodes.DUP2_X2)
.build();
/**
* Returns the operation code for pop operations with a single instruction support by their type
* sizes on stack top
*/
public static int getTypeSizeAlignedPopOpcode(ImmutableList<Type> elementsToPop) {
int totalWordsToPop = getTotalWords(elementsToPop);
switch (totalWordsToPop) {
case 1:
return Opcodes.POP;
case 2:
return Opcodes.POP2;
default:
throw new IllegalStateException(
String.format(
"Expected either 1 or 2 words to be popped, but actually requested to pop (%d)"
+ " words from <top/>%s...<bottom/>",
totalWordsToPop, elementsToPop));
}
}
/**
* Returns the operation code for gap-free dup operations with a single instruction support by
* their type sizes on stack top.
*/
public static int getTypeSizeAlignedDupOpcode(ImmutableList<Type> elementsToDup) {
return getTypeSizeAlignedDupOpcode(elementsToDup, ImmutableList.of());
}
/**
* Returns the operation code for dup operations with a single instruction support by their type
* sizes on stack top and the underneath gap size in words.
*/
public static int getTypeSizeAlignedDupOpcode(
ImmutableList<Type> elementsToDup, ImmutableList<Type> elementsToSkipBeforeInsertion) {
int wordsToDup = getTotalWords(elementsToDup);
int wordsToSkip = getTotalWords(elementsToSkipBeforeInsertion);
Integer opCode = DIRECT_DUP_OPCODES.get(wordsToDup, wordsToSkip);
if (opCode != null) {
return opCode;
}
throw new IllegalStateException(
String.format(
"Expected either 1 or 2 words to be duplicated with a offset gap of {0, 1, 2} words,"
+ " but actually requested to duplicate (%d) words with an offset gap of (%d)"
+ " words, <top/>%s|%s...<bottom/>",
wordsToDup, wordsToSkip, elementsToDup, elementsToSkipBeforeInsertion));
}
/**
* Returns the word count summation of of the given type collection. It takes 1 word for category
* 1 computational type, and 2 words for category 2 computational type.
*
* <p>https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.11.1
*/
private static int getTotalWords(Collection<Type> types) {
return types.stream().mapToInt(Type::getSize).sum();
}
/**
* A checker on whether the give class is eligible as an inner class by its class internal name.
*
* <p>Note: The reliable source of truth is to check the InnerClasses attribute. However, the
* attribute may have not been visited yet.
*/
public static boolean isEligibleAsInnerClass(String className) {
return className.contains("$");
}
/**
* Whether the referenced class member is a in-nest distinct class access within the given
* enclosing method.
*/
public static boolean isCrossMateRefInNest(
ClassMemberKey<?> referencedMember, MethodKey enclosingMethod) {
String enclosingClassName = enclosingMethod.ownerName();
String referencedMemberName = referencedMember.ownerName();
return (isEligibleAsInnerClass(enclosingClassName)
|| isEligibleAsInnerClass(referencedMemberName))
&& !referencedMemberName.equals(enclosingClassName);
}
/** Emits efficient instructions for a given integer push operation. */
public static void visitPushInstr(MethodVisitor mv, final int value) {
if (value >= -1 && value <= 5) {
mv.visitInsn(Opcodes.ICONST_0 + value);
} else if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) {
mv.visitIntInsn(Opcodes.BIPUSH, value);
} else if (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE) {
mv.visitIntInsn(Opcodes.SIPUSH, value);
} else {
mv.visitLdcInsn(value);
}
}
/**
* Emits bytecode to allocate a new object array, stores the bottom values in the operand stack to
* the new array, and replaces the bottom values with a reference to the newly-allocated array.
*
* <p>Operand Stack:
* <li>Before instructions: [Stack Top]..., value_n, value_n-1, ..., value_2, value_1, value_0
* <li>After instructions: [Stack Top] ..., value_n, arrayref
*
* <p>where n is the size of {@code expectedTypesOnOperandStack} and is expected to be equal
* to array length referenced by arrayref
*
* @param mv The current method visitor that is visiting the class.
* @param mappers Applies to an operand stack value if tested positive on {@code filter}.
* @param expectedTypesOnOperandStack The expected types at the bottom of the operand stack. The
* @param extraConstants Extra constants to be stored in the object array.
*/
public static ImmutableList<ClassName> collapseStackValuesToObjectArray(
MethodVisitor mv,
ImmutableList<Function<ClassName, Optional<MethodInvocationSite>>> mappers,
ImmutableList<ClassName> expectedTypesOnOperandStack,
ImmutableList<Object> extraConstants) {
// Creates an array of java/lang/Object to store the values on top of the operand stack that
// are subject to string concatenation.
for (Object constant : extraConstants) {
mv.visitLdcInsn(constant);
}
int numOfValuesOnOperandStack = expectedTypesOnOperandStack.size() + extraConstants.size();
visitPushInstr(mv, numOfValuesOnOperandStack);
mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object");
// To preserve the order of the operands to be string-concatenated, we slot the values on
// the top of the stack to the end of the array.
List<ClassName> actualTypesInObjectArray =
Streams.concat(
expectedTypesOnOperandStack.stream(),
extraConstants.stream().map(Object::getClass).map(ClassName::create))
.collect(Collectors.toList());
for (int i = numOfValuesOnOperandStack - 1; i >= 0; i--) {
Type arrayElementType = actualTypesInObjectArray.get(i).toAsmObjectType();
// Pre-duplicates the array reference for next loop iteration use.
// Post-operation stack bottom to top:
// ..., value_i-1, arrayref, value_i, arrayref.
mv.visitInsn(
getTypeSizeAlignedDupOpcode(
ImmutableList.of(Type.getType(Object.class)), ImmutableList.of(arrayElementType)));
// Pushes the array index and adjusts the order of the values on stack top in the order
// of <bottom/> arrayref, index, value <top/> before emitting an aastore instruction.
// https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-6.html#jvms-6.5.aastore
// Post-operation stack bottom to top:
// ..., value_i-1, arrayref, value_i, arrayref, i.
visitPushInstr(mv, i);
// Cross-duplicates the array reference and index.
// Post-operation stack bottom to top:
// ..., value_i-1, arrayref, arrayref, i, value_i, arrayref, i.
mv.visitInsn(
getTypeSizeAlignedDupOpcode(
ImmutableList.of(Type.getType(Object.class), Type.getType(int.class)),
ImmutableList.of(arrayElementType)));
// Pops arrayref, index, leaving the stack top as value_i.
// Post-operation stack bottom to top:
// ..., value_i-1, arrayref, arrayref, i, value_i.
mv.visitInsn(
getTypeSizeAlignedPopOpcode(
ImmutableList.of(Type.getType(Object.class), Type.getType(int.class))));
int targetArrayIndex = i;
mappers.stream()
.map(mapper -> mapper.apply(actualTypesInObjectArray.get(targetArrayIndex)))
.flatMap(Streams::stream)
.forEach(
typeConversionSite -> {
typeConversionSite.accept(mv);
actualTypesInObjectArray.set(targetArrayIndex, typeConversionSite.returnTypeName());
});
// Post-operation stack bottom to top:
// ..., value_i-1, arrayref.
mv.visitInsn(Opcodes.AASTORE);
}
return ImmutableList.copyOf(actualTypesInObjectArray);
}
/**
* Emits bytecode to replace the object array reference at the operand stack bottom with its array
* element values.
*
* <p>Operand Stack:
* <li>Before instructions: [Stack Top] ..., value_n, arrayref
* <li>After instructions: [Stack Top]..., value_n, value_n-1, ..., value_2, value_1, value_0
*
* <p>where n is the array length referenced by arrayref and is expected to be equal to the
* size of {@code expectedTypesOnOperandStack} expanded on the operand stack.
*
* @param mv The current method visitor that is visiting the class.
* @param expectedTypesOnOperandStack The expected types at the bottom of the operand stack. The
* end of the list corresponds to the the bottom of the operand stack.
*/
public static void expandObjectArrayToStackValues(
MethodVisitor mv, ImmutableList<ClassName> expectedTypesOnOperandStack) {
int numOfValuesExpandedOnOperandStack = expectedTypesOnOperandStack.size();
for (int i = 0; i < numOfValuesExpandedOnOperandStack; i++) {
ClassName operandTypeName = expectedTypesOnOperandStack.get(i);
// Pre-duplicates the array reference for next loop iteration use.
// Post-operation stack bottom to top:
// ..., arrayref, arrayref
mv.visitInsn(Opcodes.DUP);
// Pushes the current array index on stack.
// Post-operation stack bottom to top:
// ..., arrayref, arrayref, i
visitPushInstr(mv, i);
// Post-operation stack bottom to top:
// ..., arrayref, obj_value_i
mv.visitInsn(Opcodes.AALOAD);
// Post-operation stack bottom to top:
// ..., arrayref, cast_and_unboxed_value_i
if (operandTypeName.isPrimitive()) {
ClassName boxedTypeName = operandTypeName.toBoxedType();
mv.visitTypeInsn(Opcodes.CHECKCAST, boxedTypeName.binaryName());
createBoxedTypeToPrimitiveInvocationSite(boxedTypeName).accept(mv);
} else if (!ClassName.create(Object.class).equals(operandTypeName)) {
mv.visitTypeInsn(Opcodes.CHECKCAST, operandTypeName.binaryName());
}
// ..., cast_and_unboxed_value_i, arrayref
if (operandTypeName.isWideType()) {
mv.visitInsn(Opcodes.DUP2_X1);
mv.visitInsn(Opcodes.POP2);
} else {
mv.visitInsn(Opcodes.SWAP);
}
}
// pops out the original arrayref.
mv.visitInsn(Opcodes.POP);
}
public static ImmutableList<ClassName> mapOperandStackValues(
MethodVisitor mv,
ImmutableList<Function<ClassName, Optional<MethodInvocationSite>>> mappers,
ImmutableList<ClassName> sourceTypesOnOperandStack,
ImmutableList<ClassName> targetTypesOnOperandStack,
String beginningMarker) {
ImmutableList<ClassName> classNames =
collapseStackValuesToObjectArray(
mv,
ImmutableList.<Function<ClassName, Optional<MethodInvocationSite>>>builder()
.addAll(mappers)
.add(LangModelHelper::anyPrimitiveToBoxedTypeInvocationSite)
.build(),
sourceTypesOnOperandStack,
ImmutableList.of(beginningMarker));
expandObjectArrayToStackValues(mv, targetTypesOnOperandStack);
return classNames;
}
public static Optional<MethodInvocationSite> anyPrimitiveToStringInvocationSite(
ClassName className) {
return className.isPrimitive()
? Optional.of(createPrimitiveToStringInvocationSite(className))
: Optional.empty();
}
/** Convenient factory method for converting a primitive type to string call site. */
private static MethodInvocationSite createPrimitiveToStringInvocationSite(
ClassName primitiveTypeName) {
checkArgument(
primitiveTypeName.isPrimitive(),
"Expected a primitive type for a type boxing call site, but got %s",
primitiveTypeName);
return MethodInvocationSite.builder()
.setInvocationKind(MemberUseKind.INVOKESTATIC)
.setMethod(
MethodKey.create(
primitiveTypeName.toBoxedType(),
"toString",
Type.getMethodDescriptor(
Type.getType(String.class), primitiveTypeName.toAsmObjectType())))
.setIsInterface(false)
.build();
}
public static Optional<MethodInvocationSite> anyObjectToStringInvocationSite(
ClassName className) {
return className.isPrimitive()
? Optional.empty()
: Optional.of(
MethodInvocationSite.builder()
.setInvocationKind(MemberUseKind.INVOKEVIRTUAL)
.setMethod(
MethodKey.create(
className,
"toString",
Type.getMethodDescriptor(Type.getType(String.class))))
.setIsInterface(false)
.build());
}
/** Convenient factory method for converting a primitive type to string call site. */
public static Optional<MethodInvocationSite> anyPrimitiveToBoxedTypeInvocationSite(
ClassName className) {
return className.isPrimitive()
? Optional.of(createPrimitiveToBoxedTypeInvocationSite(className))
: Optional.empty();
}
private static MethodInvocationSite createPrimitiveToBoxedTypeInvocationSite(
ClassName primitiveTypeName) {
checkArgument(
primitiveTypeName.isPrimitive(),
"Expected a primitive type for a type boxing call site, but got %s",
primitiveTypeName);
return MethodInvocationSite.builder()
.setInvocationKind(MemberUseKind.INVOKESTATIC)
.setMethod(
MethodKey.create(
primitiveTypeName.toBoxedType(),
"valueOf",
Type.getMethodDescriptor(
primitiveTypeName.toBoxedType().toAsmObjectType(),
primitiveTypeName.toAsmObjectType())))
.setIsInterface(false)
.build();
}
private static MethodInvocationSite createBoxedTypeToPrimitiveInvocationSite(
ClassName boxedType) {
String boxedTypeBinaryName = boxedType.binaryName();
final MethodKey typeUnboxingMethod;
if ("java/lang/Boolean".contentEquals(boxedTypeBinaryName)) {
typeUnboxingMethod = MethodKey.create(boxedType, "booleanValue", "()Z");
} else if ("java/lang/Character".contentEquals(boxedTypeBinaryName)) {
typeUnboxingMethod = MethodKey.create(boxedType, "charValue", "()C");
} else if ("java/lang/Byte".contentEquals(boxedTypeBinaryName)) {
typeUnboxingMethod = MethodKey.create(boxedType, "byteValue", "()B");
} else if ("java/lang/Short".contentEquals(boxedTypeBinaryName)) {
typeUnboxingMethod = MethodKey.create(boxedType, "shortValue", "()S");
} else if ("java/lang/Integer".contentEquals(boxedTypeBinaryName)) {
typeUnboxingMethod = MethodKey.create(boxedType, "intValue", "()I");
} else if ("java/lang/Float".contentEquals(boxedTypeBinaryName)) {
typeUnboxingMethod = MethodKey.create(boxedType, "floatValue", "()F");
} else if ("java/lang/Long".contentEquals(boxedTypeBinaryName)) {
typeUnboxingMethod = MethodKey.create(boxedType, "longValue", "()J");
} else if ("java/lang/Double".contentEquals(boxedTypeBinaryName)) {
typeUnboxingMethod = MethodKey.create(boxedType, "doubleValue", "()D");
} else {
throw new IllegalArgumentException(
String.format(
"Expected a boxed type to create a type boxing call site, but got %s", boxedType));
}
return MethodInvocationSite.builder()
.setInvocationKind(MemberUseKind.INVOKEVIRTUAL)
.setMethod(typeUnboxingMethod)
.setIsInterface(false)
.build();
}
private LangModelHelper() {}
}