| /* |
| * 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() {} |
| } |