| // Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| package com.android.tools.r8.ir.optimize.enums; |
| |
| import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull; |
| import static com.android.tools.r8.ir.code.Opcodes.ARRAY_GET; |
| import static com.android.tools.r8.ir.code.Opcodes.ARRAY_LENGTH; |
| import static com.android.tools.r8.ir.code.Opcodes.ARRAY_PUT; |
| import static com.android.tools.r8.ir.code.Opcodes.ASSUME; |
| import static com.android.tools.r8.ir.code.Opcodes.CHECK_CAST; |
| import static com.android.tools.r8.ir.code.Opcodes.CONST_CLASS; |
| import static com.android.tools.r8.ir.code.Opcodes.IF; |
| import static com.android.tools.r8.ir.code.Opcodes.INSTANCE_GET; |
| import static com.android.tools.r8.ir.code.Opcodes.INSTANCE_PUT; |
| import static com.android.tools.r8.ir.code.Opcodes.INVOKE_CUSTOM; |
| import static com.android.tools.r8.ir.code.Opcodes.INVOKE_DIRECT; |
| import static com.android.tools.r8.ir.code.Opcodes.INVOKE_INTERFACE; |
| import static com.android.tools.r8.ir.code.Opcodes.INVOKE_STATIC; |
| import static com.android.tools.r8.ir.code.Opcodes.INVOKE_SUPER; |
| import static com.android.tools.r8.ir.code.Opcodes.INVOKE_VIRTUAL; |
| import static com.android.tools.r8.ir.code.Opcodes.RETURN; |
| import static com.android.tools.r8.ir.code.Opcodes.STATIC_GET; |
| import static com.android.tools.r8.ir.code.Opcodes.STATIC_PUT; |
| import static com.android.tools.r8.utils.MapUtils.ignoreKey; |
| |
| import com.android.tools.r8.graph.AppView; |
| import com.android.tools.r8.graph.DexClass; |
| import com.android.tools.r8.graph.DexClassAndField; |
| import com.android.tools.r8.graph.DexClassAndMethod; |
| import com.android.tools.r8.graph.DexEncodedField; |
| import com.android.tools.r8.graph.DexEncodedMethod; |
| import com.android.tools.r8.graph.DexField; |
| import com.android.tools.r8.graph.DexItemFactory; |
| import com.android.tools.r8.graph.DexMethod; |
| import com.android.tools.r8.graph.DexMethodHandle; |
| import com.android.tools.r8.graph.DexProgramClass; |
| import com.android.tools.r8.graph.DexProto; |
| import com.android.tools.r8.graph.DexType; |
| import com.android.tools.r8.graph.FieldResolutionResult; |
| import com.android.tools.r8.graph.GraphLens; |
| import com.android.tools.r8.graph.ProgramMethod; |
| import com.android.tools.r8.ir.analysis.fieldvalueanalysis.StaticFieldValues; |
| import com.android.tools.r8.ir.analysis.fieldvalueanalysis.StaticFieldValues.EnumStaticFieldValues; |
| import com.android.tools.r8.ir.analysis.type.ArrayTypeElement; |
| import com.android.tools.r8.ir.analysis.type.ClassTypeElement; |
| import com.android.tools.r8.ir.analysis.type.TypeElement; |
| import com.android.tools.r8.ir.analysis.value.AbstractValue; |
| import com.android.tools.r8.ir.analysis.value.EnumValuesObjectState; |
| import com.android.tools.r8.ir.analysis.value.ObjectState; |
| import com.android.tools.r8.ir.code.ArrayGet; |
| import com.android.tools.r8.ir.code.ArrayLength; |
| import com.android.tools.r8.ir.code.ArrayPut; |
| import com.android.tools.r8.ir.code.Assume; |
| import com.android.tools.r8.ir.code.BasicBlock; |
| import com.android.tools.r8.ir.code.CheckCast; |
| import com.android.tools.r8.ir.code.ConstClass; |
| import com.android.tools.r8.ir.code.FieldInstruction; |
| import com.android.tools.r8.ir.code.IRCode; |
| import com.android.tools.r8.ir.code.If; |
| import com.android.tools.r8.ir.code.InstanceGet; |
| import com.android.tools.r8.ir.code.Instruction; |
| import com.android.tools.r8.ir.code.InvokeCustom; |
| import com.android.tools.r8.ir.code.InvokeMethod; |
| import com.android.tools.r8.ir.code.InvokeStatic; |
| import com.android.tools.r8.ir.code.InvokeVirtual; |
| import com.android.tools.r8.ir.code.MemberType; |
| import com.android.tools.r8.ir.code.Phi; |
| import com.android.tools.r8.ir.code.Return; |
| import com.android.tools.r8.ir.code.Value; |
| import com.android.tools.r8.ir.conversion.IRConverter; |
| import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions; |
| import com.android.tools.r8.ir.conversion.MethodProcessor; |
| import com.android.tools.r8.ir.conversion.PostMethodProcessor.Builder; |
| import com.android.tools.r8.ir.optimize.enums.EnumDataMap.EnumData; |
| import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldKnownData; |
| import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldMappingData; |
| import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldOrdinalData; |
| import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldUnknownData; |
| import com.android.tools.r8.ir.optimize.enums.classification.CheckNotNullEnumUnboxerMethodClassification; |
| import com.android.tools.r8.ir.optimize.enums.classification.EnumUnboxerMethodClassification; |
| import com.android.tools.r8.ir.optimize.enums.eligibility.Reason; |
| import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.IllegalInvokeWithImpreciseParameterTypeReason; |
| import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.MissingContentsForEnumValuesArrayReason; |
| import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.MissingEnumStaticFieldValuesReason; |
| import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.MissingInstanceFieldValueForEnumInstanceReason; |
| import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.MissingObjectStateForEnumInstanceReason; |
| import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.UnsupportedInstanceFieldValueForEnumInstanceReason; |
| import com.android.tools.r8.ir.optimize.enums.eligibility.Reason.UnsupportedLibraryInvokeReason; |
| import com.android.tools.r8.ir.optimize.info.MutableFieldOptimizationInfo; |
| import com.android.tools.r8.ir.optimize.info.MutableMethodOptimizationInfo; |
| import com.android.tools.r8.ir.optimize.info.OptimizationFeedback.OptimizationInfoFixer; |
| import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackDelayed; |
| import com.android.tools.r8.shaking.AppInfoWithLiveness; |
| import com.android.tools.r8.shaking.KeepInfoCollection; |
| import com.android.tools.r8.utils.Reporter; |
| import com.android.tools.r8.utils.StringDiagnostic; |
| import com.android.tools.r8.utils.collections.ImmutableInt2ReferenceSortedMap; |
| import com.android.tools.r8.utils.collections.LongLivedClassSetBuilder; |
| import com.android.tools.r8.utils.collections.LongLivedProgramMethodMapBuilder; |
| import com.android.tools.r8.utils.collections.LongLivedProgramMethodSetBuilder; |
| import com.android.tools.r8.utils.collections.ProgramMethodMap; |
| import com.android.tools.r8.utils.collections.ProgramMethodSet; |
| import com.google.common.collect.HashMultiset; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Sets; |
| import it.unimi.dsi.fastutil.ints.Int2ReferenceArrayMap; |
| import it.unimi.dsi.fastutil.ints.Int2ReferenceMap; |
| import it.unimi.dsi.fastutil.objects.Object2IntMap; |
| import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.OptionalInt; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import java.util.concurrent.ConcurrentHashMap; |
| import java.util.concurrent.ExecutionException; |
| import java.util.concurrent.ExecutorService; |
| import java.util.function.Consumer; |
| import java.util.function.Function; |
| |
| public class EnumUnboxerImpl extends EnumUnboxer { |
| |
| private final AppView<AppInfoWithLiveness> appView; |
| private final DexItemFactory factory; |
| // Map the enum candidates with their dependencies, i.e., the methods to reprocess for the given |
| // enum if the optimization eventually decides to unbox it. |
| private EnumUnboxingCandidateInfoCollection enumUnboxingCandidatesInfo; |
| private final Set<DexProgramClass> candidatesToRemoveInWave = Sets.newConcurrentHashSet(); |
| private final Map<DexType, EnumStaticFieldValues> staticFieldValuesMap = |
| new ConcurrentHashMap<>(); |
| |
| // Methods depending on library modelisation need to be reprocessed so they are peephole |
| // optimized. |
| private LongLivedProgramMethodSetBuilder<ProgramMethodSet> methodsDependingOnLibraryModelisation; |
| |
| // Map from checkNotNull() methods to the enums that use the given method. |
| private LongLivedProgramMethodMapBuilder<LongLivedClassSetBuilder<DexProgramClass>> |
| checkNotNullMethodsBuilder; |
| |
| private final DexClassAndField ordinalField; |
| |
| private EnumUnboxingRewriter enumUnboxerRewriter; |
| |
| private final boolean debugLogEnabled; |
| private final Map<DexType, List<Reason>> debugLogs; |
| |
| EnumUnboxerImpl(AppView<AppInfoWithLiveness> appView) { |
| this.appView = appView; |
| this.factory = appView.dexItemFactory(); |
| if (appView.options().testing.enableEnumUnboxingDebugLogs) { |
| debugLogEnabled = true; |
| debugLogs = new ConcurrentHashMap<>(); |
| } else { |
| debugLogEnabled = false; |
| debugLogs = null; |
| } |
| assert !appView.options().debug; |
| ordinalField = |
| appView.appInfo().resolveField(factory.enumMembers.ordinalField).getResolutionPair(); |
| } |
| |
| public static int ordinalToUnboxedInt(int ordinal) { |
| return ordinal + 1; |
| } |
| |
| public DexClassAndField getOrdinalField() { |
| return ordinalField; |
| } |
| |
| @Override |
| public void updateEnumUnboxingCandidatesInfo() { |
| for (DexProgramClass candidate : candidatesToRemoveInWave) { |
| enumUnboxingCandidatesInfo.removeCandidate(candidate); |
| } |
| candidatesToRemoveInWave.clear(); |
| } |
| |
| /** |
| * Returns true if {@param enumClass} was marked as being unboxable. |
| * |
| * <p>Note that, if debug logging is enabled, {@param enumClass} is not marked unboxable until the |
| * enum unboxing analysis has finished. This is to ensure completeness of the reason reporting. |
| */ |
| private boolean markEnumAsUnboxable(Reason reason, DexProgramClass enumClass) { |
| assert enumClass.isEnum(); |
| if (!reportFailure(enumClass, reason)) { |
| // The failure was not reported, meaning debug logging is disabled. |
| candidatesToRemoveInWave.add(enumClass); |
| return true; |
| } |
| return false; |
| } |
| |
| private void markMethodDependsOnLibraryModelisation(ProgramMethod method) { |
| methodsDependingOnLibraryModelisation.add(method, appView.graphLens()); |
| } |
| |
| private DexProgramClass getEnumUnboxingCandidateOrNull(TypeElement lattice) { |
| if (lattice.isClassType()) { |
| DexType classType = lattice.asClassType().getClassType(); |
| return getEnumUnboxingCandidateOrNull(classType); |
| } |
| if (lattice.isArrayType()) { |
| ArrayTypeElement arrayType = lattice.asArrayType(); |
| if (arrayType.getBaseType().isClassType()) { |
| return getEnumUnboxingCandidateOrNull(arrayType.getBaseType()); |
| } |
| } |
| return null; |
| } |
| |
| private DexProgramClass getEnumUnboxingCandidateOrNull(DexType type) { |
| if (type.isArrayType()) { |
| return getEnumUnboxingCandidateOrNull(type.toBaseType(appView.dexItemFactory())); |
| } |
| if (type.isPrimitiveType() || type.isVoidType()) { |
| return null; |
| } |
| assert type.isClassType(); |
| return enumUnboxingCandidatesInfo.getCandidateClassOrNull(type); |
| } |
| |
| @Override |
| public void analyzeEnums(IRCode code, MutableMethodConversionOptions conversionOptions) { |
| Set<DexType> eligibleEnums = Sets.newIdentityHashSet(); |
| for (BasicBlock block : code.blocks) { |
| for (Instruction instruction : block.getInstructions()) { |
| Value outValue = instruction.outValue(); |
| if (outValue != null) { |
| DexProgramClass enumClass = |
| getEnumUnboxingCandidateOrNull(outValue.getDynamicUpperBoundType(appView)); |
| if (enumClass != null) { |
| Reason reason = validateEnumUsages(code, outValue, enumClass); |
| if (reason == Reason.ELIGIBLE) { |
| eligibleEnums.add(enumClass.type); |
| } |
| } |
| if (outValue.getType().isNullType()) { |
| addNullDependencies(code, outValue.uniqueUsers(), eligibleEnums); |
| } |
| } else { |
| if (instruction.isInvokeMethod()) { |
| DexProgramClass enumClass = |
| getEnumUnboxingCandidateOrNull(instruction.asInvokeMethod().getReturnType()); |
| if (enumClass != null) { |
| eligibleEnums.add(enumClass.type); |
| } |
| } |
| } |
| switch (instruction.opcode()) { |
| case CONST_CLASS: |
| analyzeConstClass(instruction.asConstClass(), eligibleEnums, code.context()); |
| break; |
| case CHECK_CAST: |
| analyzeCheckCast(instruction.asCheckCast(), eligibleEnums); |
| break; |
| case INVOKE_CUSTOM: |
| analyzeInvokeCustom(instruction.asInvokeCustom(), eligibleEnums); |
| break; |
| case INVOKE_STATIC: |
| analyzeInvokeStatic(instruction.asInvokeStatic(), eligibleEnums, code.context()); |
| break; |
| case STATIC_GET: |
| case INSTANCE_GET: |
| case STATIC_PUT: |
| case INSTANCE_PUT: |
| analyzeFieldInstruction( |
| instruction.asFieldInstruction(), eligibleEnums, code.context()); |
| break; |
| default: // Nothing to do for other instructions. |
| } |
| } |
| for (Phi phi : block.getPhis()) { |
| DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(phi.getType()); |
| if (enumClass != null) { |
| Reason reason = validateEnumUsages(code, phi, enumClass); |
| if (reason == Reason.ELIGIBLE) { |
| eligibleEnums.add(enumClass.type); |
| } |
| } |
| if (phi.getType().isNullType()) { |
| addNullDependencies(code, phi.uniqueUsers(), eligibleEnums); |
| } |
| } |
| } |
| if (!eligibleEnums.isEmpty()) { |
| for (DexType eligibleEnum : eligibleEnums) { |
| enumUnboxingCandidatesInfo.addMethodDependency(eligibleEnum, code.context()); |
| } |
| } |
| if (methodsDependingOnLibraryModelisation.contains(code.context(), appView.graphLens())) { |
| conversionOptions.disablePeepholeOptimizations(); |
| } |
| } |
| |
| private void analyzeInvokeCustom(InvokeCustom invoke, Set<DexType> eligibleEnums) { |
| Consumer<DexType> typeReferenceConsumer = |
| type -> { |
| DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(type); |
| if (enumClass != null) { |
| eligibleEnums.add(enumClass.getType()); |
| } |
| }; |
| invoke.getCallSite().getMethodProto().forEachType(typeReferenceConsumer); |
| invoke |
| .getCallSite() |
| .getBootstrapArgs() |
| .forEach( |
| bootstrapArgument -> { |
| if (bootstrapArgument.isDexValueMethodHandle()) { |
| DexMethodHandle methodHandle = |
| bootstrapArgument.asDexValueMethodHandle().getValue(); |
| if (methodHandle.isMethodHandle()) { |
| DexMethod method = methodHandle.asMethod(); |
| DexProgramClass enumClass = |
| getEnumUnboxingCandidateOrNull(method.getHolderType()); |
| if (enumClass != null) { |
| markEnumAsUnboxable(Reason.INVALID_INVOKE_CUSTOM, enumClass); |
| } else { |
| method.getProto().forEachType(typeReferenceConsumer); |
| } |
| } else { |
| assert methodHandle.isFieldHandle(); |
| DexField field = methodHandle.asField(); |
| DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(field.getHolderType()); |
| if (enumClass != null) { |
| markEnumAsUnboxable(Reason.INVALID_INVOKE_CUSTOM, enumClass); |
| } else { |
| typeReferenceConsumer.accept(field.getType()); |
| } |
| } |
| } else if (bootstrapArgument.isDexValueMethodType()) { |
| DexProto proto = bootstrapArgument.asDexValueMethodType().getValue(); |
| proto.forEachType(typeReferenceConsumer); |
| } |
| }); |
| } |
| |
| private void analyzeFieldInstruction( |
| FieldInstruction fieldInstruction, Set<DexType> eligibleEnums, ProgramMethod context) { |
| DexField field = fieldInstruction.getField(); |
| DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(field.holder); |
| if (enumClass != null) { |
| FieldResolutionResult resolutionResult = appView.appInfo().resolveField(field, context); |
| if (resolutionResult.isSuccessfulResolution()) { |
| eligibleEnums.add(enumClass.getType()); |
| } else { |
| markEnumAsUnboxable(Reason.UNRESOLVABLE_FIELD, enumClass); |
| } |
| } |
| } |
| |
| private void analyzeInvokeStatic( |
| InvokeStatic invokeStatic, Set<DexType> eligibleEnums, ProgramMethod context) { |
| DexMethod invokedMethod = invokeStatic.getInvokedMethod(); |
| DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(invokedMethod.holder); |
| if (enumClass != null) { |
| DexClassAndMethod method = invokeStatic.lookupSingleTarget(appView, context); |
| if (method != null) { |
| eligibleEnums.add(enumClass.type); |
| } else { |
| markEnumAsUnboxable(Reason.INVALID_INVOKE, enumClass); |
| } |
| } |
| } |
| |
| private void analyzeCheckCast(CheckCast checkCast, Set<DexType> eligibleEnums) { |
| // Casts to enum array types are fine as long all enum array creations are valid and have valid |
| // usages. Since creations of enum arrays are rewritten to primitive int arrays, enum array |
| // casts will continue to work after rewriting to int[] casts. Casts that failed with |
| // ClassCastException: "T[] cannot be cast to MyEnum[]" will continue to fail, but with "T[] |
| // cannot be cast to int[]". |
| // |
| // Note that strictly speaking, the rewriting from MyEnum[] to int[] could change the semantics |
| // of code that would fail with "int[] cannot be cast to MyEnum[]" in the input. However, javac |
| // does not allow such code ("incompatible types"), so we should generally not see such code. |
| if (checkCast.getType().isArrayType()) { |
| return; |
| } |
| |
| // We are doing a type check, which typically means the in-value is of an upper |
| // type and cannot be dealt with. |
| // If the cast is on a dynamically typed object, the checkCast can be simply removed. |
| // This allows enum array clone and valueOf to work correctly. |
| DexProgramClass enumClass = |
| getEnumUnboxingCandidateOrNull(checkCast.getType().toBaseType(factory)); |
| if (enumClass == null) { |
| return; |
| } |
| if (allowCheckCast(checkCast)) { |
| eligibleEnums.add(enumClass.type); |
| return; |
| } |
| markEnumAsUnboxable(Reason.DOWN_CAST, enumClass); |
| } |
| |
| private boolean allowCheckCast(CheckCast checkCast) { |
| TypeElement objectType = checkCast.object().getDynamicUpperBoundType(appView); |
| return objectType.equalUpToNullability( |
| TypeElement.fromDexType(checkCast.getType(), definitelyNotNull(), appView)); |
| } |
| |
| private void analyzeConstClass( |
| ConstClass constClass, Set<DexType> eligibleEnums, ProgramMethod context) { |
| // We are using the ConstClass of an enum, which typically means the enum cannot be unboxed. |
| // We however allow unboxing if the ConstClass is used only: |
| // - as an argument to java.lang.reflect.Array#newInstance(java.lang.Class, int[]), to allow |
| // unboxing of: |
| // MyEnum[][] a = new MyEnum[x][y]; |
| // - as an argument to Enum#valueOf, to allow unboxing of: |
| // MyEnum a = Enum.valueOf(MyEnum.class, "A"); |
| // - as a receiver for a name method, to allow unboxing of: |
| // MyEnum.class.getName(); |
| DexType enumType = constClass.getValue(); |
| if (!enumUnboxingCandidatesInfo.isCandidate(enumType)) { |
| return; |
| } |
| if (constClass.outValue() == null) { |
| eligibleEnums.add(enumType); |
| return; |
| } |
| DexProgramClass enumClass = appView.definitionFor(enumType).asProgramClass(); |
| if (constClass.outValue().hasPhiUsers()) { |
| markEnumAsUnboxable(Reason.CONST_CLASS, enumClass); |
| return; |
| } |
| for (Instruction user : constClass.outValue().aliasedUsers()) { |
| if (!isLegitimateConstClassUser(user, context, enumClass)) { |
| markEnumAsUnboxable(Reason.CONST_CLASS, enumClass); |
| return; |
| } |
| } |
| eligibleEnums.add(enumType); |
| } |
| |
| private boolean isLegitimateConstClassUser( |
| Instruction user, ProgramMethod context, DexProgramClass enumClass) { |
| if (user.isAssume()) { |
| if (user.outValue().hasPhiUsers()) { |
| return false; |
| } |
| return true; |
| } |
| |
| if (user.isInvokeStatic()) { |
| DexClassAndMethod singleTarget = user.asInvokeStatic().lookupSingleTarget(appView, context); |
| if (singleTarget == null) { |
| return false; |
| } |
| if (singleTarget.getReference() == factory.enumMembers.valueOf) { |
| // The name data is required for the correct mapping from the enum name to the ordinal |
| // in the valueOf utility method. |
| addRequiredNameData(enumClass); |
| markMethodDependsOnLibraryModelisation(context); |
| return true; |
| } |
| if (singleTarget.getReference() |
| == factory.javaLangReflectArrayMembers.newInstanceMethodWithDimensions) { |
| markMethodDependsOnLibraryModelisation(context); |
| return true; |
| } |
| } |
| |
| if (user.isInvokeVirtual()) { |
| InvokeVirtual invoke = user.asInvokeVirtual(); |
| DexMethod invokedMethod = invoke.getInvokedMethod(); |
| if (invokedMethod == factory.classMethods.desiredAssertionStatus) { |
| // Only valid in the enum's class initializer, since the class constant must be rewritten |
| // to LocalEnumUtility.class instead of int.class. |
| return context.getDefinition().isClassInitializer() && context.getHolder() == enumClass; |
| } |
| if (isUnboxableNameMethod(invokedMethod)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| private void addRequiredNameData(DexProgramClass enumClass) { |
| enumUnboxingCandidatesInfo.addRequiredEnumInstanceFieldData( |
| enumClass, factory.enumMembers.nameField); |
| } |
| |
| private boolean isUnboxableNameMethod(DexMethod method) { |
| return method == factory.classMethods.getName |
| || method == factory.classMethods.getCanonicalName |
| || method == factory.classMethods.getSimpleName; |
| } |
| |
| private void addNullDependencies(IRCode code, Set<Instruction> uses, Set<DexType> eligibleEnums) { |
| for (Instruction use : uses) { |
| if (use.isInvokeMethod()) { |
| InvokeMethod invokeMethod = use.asInvokeMethod(); |
| DexMethod invokedMethod = invokeMethod.getInvokedMethod(); |
| for (DexType paramType : invokedMethod.proto.parameters.values) { |
| if (enumUnboxingCandidatesInfo.isCandidate(paramType)) { |
| eligibleEnums.add(paramType); |
| } |
| } |
| if (invokeMethod.isInvokeMethodWithReceiver()) { |
| DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(invokedMethod.holder); |
| if (enumClass != null) { |
| markEnumAsUnboxable(Reason.ENUM_METHOD_CALLED_WITH_NULL_RECEIVER, enumClass); |
| } |
| } |
| } else if (use.isFieldPut()) { |
| DexType type = use.asFieldInstruction().getField().type; |
| if (enumUnboxingCandidatesInfo.isCandidate(type)) { |
| eligibleEnums.add(type); |
| } |
| } else if (use.isReturn()) { |
| DexType returnType = code.method().getReference().proto.returnType; |
| if (enumUnboxingCandidatesInfo.isCandidate(returnType)) { |
| eligibleEnums.add(returnType); |
| } |
| } |
| } |
| } |
| |
| private Reason validateEnumUsages(IRCode code, Value value, DexProgramClass enumClass) { |
| Reason result = Reason.ELIGIBLE; |
| for (Instruction user : value.uniqueUsers()) { |
| Reason reason = instructionAllowEnumUnboxing(user, code, enumClass, value); |
| if (reason != Reason.ELIGIBLE) { |
| if (markEnumAsUnboxable(reason, enumClass)) { |
| return reason; |
| } |
| // Record that the enum is ineligible, and continue analysis to collect all reasons for |
| // debugging. |
| result = reason; |
| } |
| } |
| for (Phi phi : value.uniquePhiUsers()) { |
| for (Value operand : phi.getOperands()) { |
| if (!operand.getType().isNullType() |
| && getEnumUnboxingCandidateOrNull(operand.getType()) != enumClass) { |
| // All reported reasons from here will be the same (INVALID_PHI), so just return |
| // immediately. |
| markEnumAsUnboxable(Reason.INVALID_PHI, enumClass); |
| return Reason.INVALID_PHI; |
| } |
| } |
| } |
| return result; |
| } |
| |
| @Override |
| public void prepareForPrimaryOptimizationPass(GraphLens graphLensForPrimaryOptimizationPass) { |
| assert appView.graphLens() == graphLensForPrimaryOptimizationPass; |
| initializeCheckNotNullMethods(graphLensForPrimaryOptimizationPass); |
| initializeEnumUnboxingCandidates(graphLensForPrimaryOptimizationPass); |
| } |
| |
| private void initializeCheckNotNullMethods(GraphLens graphLensForPrimaryOptimizationPass) { |
| assert checkNotNullMethodsBuilder == null; |
| checkNotNullMethodsBuilder = |
| LongLivedProgramMethodMapBuilder.createConcurrentBuilderForNonConcurrentMap( |
| graphLensForPrimaryOptimizationPass); |
| } |
| |
| private void initializeEnumUnboxingCandidates(GraphLens graphLensForPrimaryOptimizationPass) { |
| assert enumUnboxingCandidatesInfo == null; |
| enumUnboxingCandidatesInfo = |
| new EnumUnboxingCandidateAnalysis(appView, this) |
| .findCandidates(graphLensForPrimaryOptimizationPass); |
| methodsDependingOnLibraryModelisation = |
| LongLivedProgramMethodSetBuilder.createConcurrentForIdentitySet( |
| graphLensForPrimaryOptimizationPass); |
| } |
| |
| @Override |
| public void unboxEnums( |
| AppView<AppInfoWithLiveness> appView, |
| IRConverter converter, |
| Builder postMethodProcessorBuilder, |
| ExecutorService executorService, |
| OptimizationFeedbackDelayed feedback) |
| throws ExecutionException { |
| assert feedback.noUpdatesLeft(); |
| |
| assert candidatesToRemoveInWave.isEmpty(); |
| EnumDataMap enumDataMap = finishAnalysis(); |
| assert candidatesToRemoveInWave.isEmpty(); |
| |
| // At this point the enum unboxing candidates are no longer candidates, they will all be |
| // unboxed. We extract the now immutable enums to unbox information and clear the candidate |
| // info. |
| appView.setUnboxedEnums(enumDataMap); |
| |
| if (enumUnboxingCandidatesInfo.isEmpty()) { |
| assert enumDataMap.isEmpty(); |
| return; |
| } |
| |
| ImmutableSet<DexType> enumsToUnbox = enumUnboxingCandidatesInfo.candidates(); |
| ImmutableSet<DexProgramClass> enumClassesToUnbox = |
| enumUnboxingCandidatesInfo.candidateClasses(); |
| LongLivedProgramMethodSetBuilder<ProgramMethodSet> dependencies = |
| enumUnboxingCandidatesInfo.allMethodDependencies(); |
| enumUnboxingCandidatesInfo.clear(); |
| // Update keep info on any of the enum methods of the removed classes. |
| updateKeepInfo(enumsToUnbox); |
| |
| EnumUnboxingUtilityClasses utilityClasses = |
| EnumUnboxingUtilityClasses.builder(appView) |
| .synthesizeEnumUnboxingUtilityClasses(enumClassesToUnbox, enumDataMap) |
| .build(converter, executorService); |
| |
| // Fixup the application. |
| ProgramMethodMap<Set<DexProgramClass>> checkNotNullMethods = |
| checkNotNullMethodsBuilder |
| .rewrittenWithLens(appView, (enumClasses, appliedGraphLens) -> enumClasses) |
| .build(appView, builder -> builder.build(appView)); |
| EnumUnboxingTreeFixer.Result treeFixerResult = |
| new EnumUnboxingTreeFixer( |
| appView, checkNotNullMethods, enumDataMap, enumClassesToUnbox, utilityClasses) |
| .fixupTypeReferences(converter, executorService); |
| EnumUnboxingLens enumUnboxingLens = treeFixerResult.getLens(); |
| |
| // Update the graph lens. |
| appView.rewriteWithLens(enumUnboxingLens); |
| |
| // Enqueue the (lens rewritten) methods that require reprocessing. |
| // |
| // Note that the reprocessing set must be rewritten to the new enum unboxing lens before pruning |
| // the builders with the methods removed by the tree fixer (since these methods references are |
| // already fully lens rewritten). |
| postMethodProcessorBuilder |
| .getMethodsToReprocessBuilder() |
| .rewrittenWithLens(appView) |
| .merge(dependencies) |
| .merge(methodsDependingOnLibraryModelisation) |
| .removeAll(treeFixerResult.getPrunedItems().getRemovedMethods()); |
| methodsDependingOnLibraryModelisation.clear(); |
| |
| updateOptimizationInfos(executorService, feedback, treeFixerResult); |
| |
| enumUnboxerRewriter = |
| new EnumUnboxingRewriter( |
| appView, |
| treeFixerResult.getCheckNotNullToCheckNotZeroMapping(), |
| converter, |
| enumUnboxingLens, |
| enumDataMap, |
| utilityClasses); |
| } |
| |
| private void updateOptimizationInfos( |
| ExecutorService executorService, |
| OptimizationFeedbackDelayed feedback, |
| EnumUnboxingTreeFixer.Result treeFixerResult) |
| throws ExecutionException { |
| feedback.fixupOptimizationInfos( |
| appView, |
| executorService, |
| new OptimizationInfoFixer() { |
| @Override |
| public void fixup(DexEncodedField field, MutableFieldOptimizationInfo optimizationInfo) { |
| optimizationInfo |
| .fixupClassTypeReferences(appView, appView.graphLens()) |
| .fixupAbstractValue(appView, appView.graphLens()); |
| } |
| |
| @Override |
| public void fixup( |
| DexEncodedMethod method, MutableMethodOptimizationInfo optimizationInfo) { |
| optimizationInfo |
| .fixupClassTypeReferences(appView, appView.graphLens()) |
| .fixupAbstractReturnValue(appView, appView.graphLens()) |
| .fixupInstanceInitializerInfo( |
| appView, appView.graphLens(), treeFixerResult.getPrunedItems()); |
| |
| // Clear the enum unboxer method classification for check-not-null methods (these |
| // classifications are transferred to the synthesized check-not-zero methods by now). |
| if (!treeFixerResult |
| .getCheckNotNullToCheckNotZeroMapping() |
| .containsValue(method.getReference())) { |
| optimizationInfo.unsetEnumUnboxerMethodClassification(); |
| } |
| } |
| }); |
| } |
| |
| private void updateKeepInfo(Set<DexType> enumsToUnbox) { |
| KeepInfoCollection keepInfo = appView.appInfo().getKeepInfo(); |
| keepInfo.mutate(mutator -> mutator.removeKeepInfoForPrunedItems(enumsToUnbox)); |
| } |
| |
| public EnumDataMap finishAnalysis() { |
| analyzeInitializers(); |
| updateEnumUnboxingCandidatesInfo(); |
| EnumDataMap enumDataMap = analyzeEnumInstances(); |
| if (debugLogEnabled) { |
| // Remove all enums that have been reported as being unboxable. |
| debugLogs.keySet().forEach(enumUnboxingCandidatesInfo::removeCandidate); |
| reportEnumsAnalysis(); |
| } |
| assert enumDataMap.getUnboxedEnums().size() == enumUnboxingCandidatesInfo.candidates().size(); |
| return enumDataMap; |
| } |
| |
| private EnumDataMap analyzeEnumInstances() { |
| ImmutableMap.Builder<DexType, EnumData> builder = ImmutableMap.builder(); |
| enumUnboxingCandidatesInfo.forEachCandidateAndRequiredInstanceFieldData( |
| (enumClass, instanceFields) -> { |
| EnumData data = buildData(enumClass, instanceFields); |
| if (data == null) { |
| // Reason is already reported at this point. |
| enumUnboxingCandidatesInfo.removeCandidate(enumClass); |
| return; |
| } |
| if (!debugLogEnabled || !debugLogs.containsKey(enumClass.getType())) { |
| builder.put(enumClass.type, data); |
| } |
| }); |
| staticFieldValuesMap.clear(); |
| return new EnumDataMap(builder.build()); |
| } |
| |
| private EnumData buildData(DexProgramClass enumClass, Set<DexField> instanceFields) { |
| if (!enumClass.hasStaticFields()) { |
| return new EnumData(ImmutableMap.of(), ImmutableMap.of(), ImmutableSet.of(), -1); |
| } |
| |
| // This map holds all the accessible fields to their unboxed value, so we can remap the field |
| // read to the unboxed value. |
| ImmutableMap.Builder<DexField, Integer> unboxedValues = ImmutableMap.builder(); |
| // This maps the ordinal to the object state, note that some fields may have been removed, |
| // hence the entry is in this map but not the enumToOrdinalMap. |
| Int2ReferenceMap<ObjectState> ordinalToObjectState = new Int2ReferenceArrayMap<>(); |
| // Any fields matching the expected $VALUES content can be recorded here, they have however |
| // all the same content. |
| ImmutableSet.Builder<DexField> valuesField = ImmutableSet.builder(); |
| EnumValuesObjectState valuesContents = null; |
| |
| EnumStaticFieldValues enumStaticFieldValues = staticFieldValuesMap.get(enumClass.type); |
| if (enumStaticFieldValues == null) { |
| reportFailure(enumClass, new MissingEnumStaticFieldValuesReason()); |
| return null; |
| } |
| |
| // Step 1: We iterate over the field to find direct enum instance information and the values |
| // fields. |
| for (DexEncodedField staticField : enumClass.staticFields()) { |
| if (factory.enumMembers.isEnumField(staticField, enumClass.type)) { |
| ObjectState enumState = |
| enumStaticFieldValues.getObjectStateForPossiblyPinnedField(staticField.getReference()); |
| if (enumState == null) { |
| if (staticField.getOptimizationInfo().isDead()) { |
| // We don't care about unused field data. |
| continue; |
| } |
| // We could not track the content of that field. We bail out. |
| reportFailure( |
| enumClass, new MissingObjectStateForEnumInstanceReason(staticField.getReference())); |
| return null; |
| } |
| OptionalInt optionalOrdinal = getOrdinal(enumState); |
| if (!optionalOrdinal.isPresent()) { |
| reportFailure( |
| enumClass, |
| new MissingInstanceFieldValueForEnumInstanceReason( |
| staticField.getReference(), factory.enumMembers.ordinalField)); |
| return null; |
| } |
| int ordinal = optionalOrdinal.getAsInt(); |
| unboxedValues.put(staticField.getReference(), ordinalToUnboxedInt(ordinal)); |
| ordinalToObjectState.put(ordinal, enumState); |
| } else if (factory.enumMembers.isValuesFieldCandidate(staticField, enumClass.type)) { |
| ObjectState valuesState = |
| enumStaticFieldValues.getObjectStateForPossiblyPinnedField(staticField.getReference()); |
| if (valuesState == null) { |
| if (staticField.getOptimizationInfo().isDead()) { |
| // We don't care about unused field data. |
| continue; |
| } |
| // We could not track the content of that field. We bail out. |
| // We could not track the content of that field, and the field could be a values field. |
| // We conservatively bail out. |
| reportFailure( |
| enumClass, new MissingContentsForEnumValuesArrayReason(staticField.getReference())); |
| return null; |
| } |
| assert valuesState.isEnumValuesObjectState(); |
| assert valuesContents == null |
| || valuesContents.equals(valuesState.asEnumValuesObjectState()); |
| valuesContents = valuesState.asEnumValuesObjectState(); |
| valuesField.add(staticField.getReference()); |
| } |
| } |
| |
| // Step 2: We complete the information based on the values content, since some enum instances |
| // may be reachable only though the $VALUES field. |
| if (valuesContents != null) { |
| for (int ordinal = 0; ordinal < valuesContents.getEnumValuesSize(); ordinal++) { |
| if (!ordinalToObjectState.containsKey(ordinal)) { |
| ObjectState enumState = valuesContents.getObjectStateForOrdinal(ordinal); |
| if (enumState.isEmpty()) { |
| // If $VALUES is used, we need data for all enums, at least the ordinal. |
| return null; |
| } |
| assert getOrdinal(enumState).isPresent(); |
| assert getOrdinal(enumState).getAsInt() == ordinal; |
| ordinalToObjectState.put(ordinal, enumState); |
| } |
| } |
| } |
| |
| // The ordinalToObjectState map may have holes at this point, if some enum instances are never |
| // used ($VALUES unused or removed, and enum instance field unused or removed), it contains |
| // only data for reachable enum instance, that is what we're interested in. |
| ImmutableMap<DexField, EnumInstanceFieldKnownData> instanceFieldsData = |
| computeRequiredEnumInstanceFieldsData(enumClass, instanceFields, ordinalToObjectState); |
| if (instanceFieldsData == null) { |
| return null; |
| } |
| |
| return new EnumData( |
| instanceFieldsData, |
| unboxedValues.build(), |
| valuesField.build(), |
| valuesContents == null ? EnumData.INVALID_VALUES_SIZE : valuesContents.getEnumValuesSize()); |
| } |
| |
| private ImmutableMap<DexField, EnumInstanceFieldKnownData> computeRequiredEnumInstanceFieldsData( |
| DexProgramClass enumClass, |
| Set<DexField> instanceFields, |
| Int2ReferenceMap<ObjectState> ordinalToObjectState) { |
| ImmutableMap.Builder<DexField, EnumInstanceFieldKnownData> builder = ImmutableMap.builder(); |
| for (DexField instanceField : instanceFields) { |
| EnumInstanceFieldData fieldData = |
| computeRequiredEnumInstanceFieldData(instanceField, enumClass, ordinalToObjectState); |
| if (fieldData.isUnknown()) { |
| if (!debugLogEnabled) { |
| return null; |
| } |
| builder = null; |
| } |
| if (builder != null) { |
| builder.put(instanceField, fieldData.asEnumFieldKnownData()); |
| } |
| } |
| return builder != null ? builder.build() : null; |
| } |
| |
| private EnumInstanceFieldData computeRequiredEnumInstanceFieldData( |
| DexField instanceField, |
| DexProgramClass enumClass, |
| Int2ReferenceMap<ObjectState> ordinalToObjectState) { |
| DexEncodedField encodedInstanceField = |
| appView.appInfo().resolveFieldOn(enumClass, instanceField).getResolvedField(); |
| assert encodedInstanceField != null; |
| boolean canBeOrdinal = instanceField.type.isIntType(); |
| ImmutableInt2ReferenceSortedMap.Builder<AbstractValue> data = |
| ImmutableInt2ReferenceSortedMap.builder(); |
| for (Integer ordinal : ordinalToObjectState.keySet()) { |
| ObjectState state = ordinalToObjectState.get(ordinal); |
| AbstractValue fieldValue = state.getAbstractFieldValue(encodedInstanceField); |
| if (!fieldValue.isSingleValue()) { |
| reportFailure( |
| enumClass, new MissingInstanceFieldValueForEnumInstanceReason(ordinal, instanceField)); |
| return EnumInstanceFieldUnknownData.getInstance(); |
| } |
| if (!(fieldValue.isSingleNumberValue() || fieldValue.isSingleStringValue())) { |
| reportFailure( |
| enumClass, |
| new UnsupportedInstanceFieldValueForEnumInstanceReason(ordinal, instanceField)); |
| return EnumInstanceFieldUnknownData.getInstance(); |
| } |
| data.put(ordinalToUnboxedInt(ordinal), fieldValue); |
| if (canBeOrdinal) { |
| assert fieldValue.isSingleNumberValue(); |
| int computedValue = fieldValue.asSingleNumberValue().getIntValue(); |
| if (computedValue != ordinal) { |
| canBeOrdinal = false; |
| } |
| } |
| } |
| if (canBeOrdinal) { |
| return new EnumInstanceFieldOrdinalData(); |
| } |
| return new EnumInstanceFieldMappingData(data.build()); |
| } |
| |
| private OptionalInt getOrdinal(ObjectState state) { |
| AbstractValue field = state.getAbstractFieldValue(getOrdinalField().getDefinition()); |
| if (field.isSingleNumberValue()) { |
| return OptionalInt.of(field.asSingleNumberValue().getIntValue()); |
| } |
| return OptionalInt.empty(); |
| } |
| |
| @Override |
| public void recordEnumState(DexProgramClass clazz, StaticFieldValues staticFieldValues) { |
| if (staticFieldValues == null || !staticFieldValues.isEnumStaticFieldValues()) { |
| return; |
| } |
| assert clazz.isEnum(); |
| EnumStaticFieldValues enumStaticFieldValues = staticFieldValues.asEnumStaticFieldValues(); |
| if (getEnumUnboxingCandidateOrNull(clazz.type) != null) { |
| staticFieldValuesMap.put(clazz.type, enumStaticFieldValues); |
| } |
| } |
| |
| private void analyzeInitializers() { |
| enumUnboxingCandidatesInfo.forEachCandidate( |
| enumClass -> { |
| for (DexEncodedMethod directMethod : enumClass.directMethods()) { |
| if (directMethod.isInstanceInitializer()) { |
| if (directMethod |
| .getOptimizationInfo() |
| .getContextInsensitiveInstanceInitializerInfo() |
| .mayHaveOtherSideEffectsThanInstanceFieldAssignments()) { |
| if (markEnumAsUnboxable(Reason.INVALID_INIT, enumClass)) { |
| break; |
| } |
| } |
| } |
| } |
| if (enumClass.classInitializationMayHaveSideEffects(appView)) { |
| markEnumAsUnboxable(Reason.INVALID_CLINIT, enumClass); |
| } |
| }); |
| } |
| |
| private Reason instructionAllowEnumUnboxing( |
| Instruction instruction, IRCode code, DexProgramClass enumClass, Value enumValue) { |
| ProgramMethod context = code.context(); |
| switch (instruction.opcode()) { |
| case ASSUME: |
| return analyzeAssumeUser(instruction.asAssume(), code, context, enumClass, enumValue); |
| case ARRAY_GET: |
| return analyzeArrayGetUser(instruction.asArrayGet(), code, context, enumClass, enumValue); |
| case ARRAY_LENGTH: |
| return analyzeArrayLengthUser( |
| instruction.asArrayLength(), code, context, enumClass, enumValue); |
| case ARRAY_PUT: |
| return analyzeArrayPutUser(instruction.asArrayPut(), code, context, enumClass, enumValue); |
| case CHECK_CAST: |
| return analyzeCheckCastUser(instruction.asCheckCast(), code, context, enumClass, enumValue); |
| case IF: |
| return analyzeIfUser(instruction.asIf(), code, context, enumClass, enumValue); |
| case INSTANCE_GET: |
| return analyzeInstanceGetUser( |
| instruction.asInstanceGet(), code, context, enumClass, enumValue); |
| case INSTANCE_PUT: |
| return analyzeFieldPutUser( |
| instruction.asInstancePut(), code, context, enumClass, enumValue); |
| case INVOKE_DIRECT: |
| case INVOKE_INTERFACE: |
| case INVOKE_STATIC: |
| case INVOKE_SUPER: |
| case INVOKE_VIRTUAL: |
| return analyzeInvokeUser(instruction.asInvokeMethod(), code, context, enumClass, enumValue); |
| case RETURN: |
| return analyzeReturnUser(instruction.asReturn(), code, context, enumClass, enumValue); |
| case STATIC_PUT: |
| return analyzeFieldPutUser(instruction.asStaticPut(), code, context, enumClass, enumValue); |
| default: |
| return Reason.OTHER_UNSUPPORTED_INSTRUCTION; |
| } |
| } |
| |
| private Reason analyzeAssumeUser( |
| Assume assume, |
| IRCode code, |
| ProgramMethod context, |
| DexProgramClass enumClass, |
| Value enumValue) { |
| return validateEnumUsages(code, assume.outValue(), enumClass); |
| } |
| |
| private Reason analyzeArrayGetUser( |
| ArrayGet arrayGet, |
| IRCode code, |
| ProgramMethod context, |
| DexProgramClass enumClass, |
| Value enumValue) { |
| // MyEnum[] array = ...; array[0]; is valid. |
| return Reason.ELIGIBLE; |
| } |
| |
| private Reason analyzeArrayLengthUser( |
| ArrayLength arrayLength, |
| IRCode code, |
| ProgramMethod context, |
| DexProgramClass enumClass, |
| Value enumValue) { |
| // MyEnum[] array = ...; array.length; is valid. |
| return Reason.ELIGIBLE; |
| } |
| |
| private Reason analyzeArrayPutUser( |
| ArrayPut arrayPut, |
| IRCode code, |
| ProgramMethod context, |
| DexProgramClass enumClass, |
| Value enumValue) { |
| // MyEnum[] array; array[0] = MyEnum.A; is valid. |
| // MyEnum[][] array2d; MyEnum[] array; array2d[0] = array; is valid. |
| // MyEnum[]^N array; MyEnum[]^(N-1) element; array[0] = element; is valid. |
| // We need to prove that the value to put in and the array have correct types. |
| assert arrayPut.getMemberType() == MemberType.OBJECT; |
| TypeElement arrayType = arrayPut.array().getType(); |
| assert arrayType.isArrayType(); |
| assert arrayType.asArrayType().getBaseType().isClassType(); |
| ClassTypeElement arrayBaseType = arrayType.asArrayType().getBaseType().asClassType(); |
| TypeElement valueBaseType = arrayPut.value().getType(); |
| if (valueBaseType.isArrayType()) { |
| assert valueBaseType.asArrayType().getBaseType().isClassType(); |
| assert valueBaseType.asArrayType().getNesting() == arrayType.asArrayType().getNesting() - 1; |
| valueBaseType = valueBaseType.asArrayType().getBaseType(); |
| } |
| if (arrayBaseType.equalUpToNullability(valueBaseType) |
| && arrayBaseType.getClassType() == enumClass.type) { |
| return Reason.ELIGIBLE; |
| } |
| return Reason.INVALID_ARRAY_PUT; |
| } |
| |
| private Reason analyzeCheckCastUser( |
| CheckCast checkCast, |
| IRCode code, |
| ProgramMethod context, |
| DexProgramClass enumClass, |
| Value enumValue) { |
| if (allowCheckCast(checkCast)) { |
| return Reason.ELIGIBLE; |
| } |
| return Reason.DOWN_CAST; |
| } |
| |
| // A field put is valid only if the field is not on an enum, and the field type and the valuePut |
| // have identical enum type. |
| private Reason analyzeFieldPutUser( |
| FieldInstruction fieldPut, |
| IRCode code, |
| ProgramMethod context, |
| DexProgramClass enumClass, |
| Value enumValue) { |
| assert fieldPut.isInstancePut() || fieldPut.isStaticPut(); |
| DexEncodedField field = appView.appInfo().resolveField(fieldPut.getField()).getResolvedField(); |
| if (field == null) { |
| return Reason.INVALID_FIELD_PUT; |
| } |
| DexProgramClass dexClass = appView.programDefinitionFor(field.getHolderType(), code.context()); |
| if (dexClass == null) { |
| return Reason.INVALID_FIELD_PUT; |
| } |
| if (fieldPut.isInstancePut() && fieldPut.asInstancePut().object() == enumValue) { |
| return Reason.ELIGIBLE; |
| } |
| // The put value has to be of the field type. |
| if (field.getReference().type.toBaseType(factory) != enumClass.type) { |
| return Reason.TYPE_MISMATCH_FIELD_PUT; |
| } |
| return Reason.ELIGIBLE; |
| } |
| |
| // An If using enum as inValue is valid if it matches e == null |
| // or e == X with X of same enum type as e. Ex: if (e == MyEnum.A). |
| private Reason analyzeIfUser( |
| If theIf, IRCode code, ProgramMethod context, DexProgramClass enumClass, Value enumValue) { |
| assert (theIf.getType() == If.Type.EQ || theIf.getType() == If.Type.NE) |
| : "Comparing a reference with " + theIf.getType().toString(); |
| // e == null. |
| if (theIf.isZeroTest()) { |
| return Reason.ELIGIBLE; |
| } |
| // e == MyEnum.X |
| TypeElement leftType = theIf.lhs().getType(); |
| TypeElement rightType = theIf.rhs().getType(); |
| if (leftType.equalUpToNullability(rightType)) { |
| assert leftType.isClassType(); |
| assert leftType.asClassType().getClassType() == enumClass.type; |
| return Reason.ELIGIBLE; |
| } |
| return Reason.INVALID_IF_TYPES; |
| } |
| |
| private Reason analyzeInstanceGetUser( |
| InstanceGet instanceGet, |
| IRCode code, |
| ProgramMethod context, |
| DexProgramClass enumClass, |
| Value enumValue) { |
| assert instanceGet.getField().holder == enumClass.type; |
| DexField field = instanceGet.getField(); |
| enumUnboxingCandidatesInfo.addRequiredEnumInstanceFieldData(enumClass, field); |
| return Reason.ELIGIBLE; |
| } |
| |
| // All invokes in the library are invalid, besides a few cherry picked cases such as ordinal(). |
| private Reason analyzeInvokeUser( |
| InvokeMethod invoke, |
| IRCode code, |
| ProgramMethod context, |
| DexProgramClass enumClass, |
| Value enumValue) { |
| if (invoke.getInvokedMethod().holder.isArrayType()) { |
| // The only valid methods is clone for values() to be correct. |
| if (invoke.getInvokedMethod().name == factory.cloneMethodName) { |
| return Reason.ELIGIBLE; |
| } |
| return Reason.INVALID_INVOKE_ON_ARRAY; |
| } |
| DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, code.context()); |
| if (singleTarget == null) { |
| return Reason.INVALID_INVOKE; |
| } |
| DexMethod singleTargetReference = singleTarget.getReference(); |
| DexClass targetHolder = singleTarget.getHolder(); |
| if (targetHolder.isProgramClass()) { |
| if (targetHolder.isEnum() && singleTarget.getDefinition().isInstanceInitializer()) { |
| if (code.context().getHolder() == targetHolder && code.method().isClassInitializer()) { |
| // The enum instance initializer is allowed to be called only from the enum clinit. |
| return Reason.ELIGIBLE; |
| } else { |
| return Reason.INVALID_INIT; |
| } |
| } |
| |
| // Check if this is a checkNotNull() user. In this case, we can create a copy of the method |
| // that takes an int instead of java.lang.Object and call that method instead. |
| EnumUnboxerMethodClassification classification = |
| singleTarget.getOptimizationInfo().getEnumUnboxerMethodClassification(); |
| if (classification.isCheckNotNullClassification()) { |
| CheckNotNullEnumUnboxerMethodClassification checkNotNullClassification = |
| classification.asCheckNotNullClassification(); |
| if (checkNotNullClassification.isUseEligibleForUnboxing( |
| invoke.asInvokeStatic(), enumValue)) { |
| GraphLens graphLens = appView.graphLens(); |
| checkNotNullMethodsBuilder |
| .computeIfAbsent( |
| singleTarget.asProgramMethod(), |
| ignoreKey( |
| () -> |
| LongLivedClassSetBuilder.createConcurrentBuilderForIdentitySet( |
| graphLens)), |
| graphLens) |
| .add(enumClass, graphLens); |
| return Reason.ELIGIBLE; |
| } |
| } |
| |
| // Check that the enum-value only flows into parameters whose type exactly matches the |
| // enum's type. |
| for (int i = 0; i < singleTarget.getParameters().size(); i++) { |
| if (invoke.getArgumentForParameter(i) == enumValue |
| && singleTarget.getParameter(i).toBaseType(factory) != enumClass.getType()) { |
| return new IllegalInvokeWithImpreciseParameterTypeReason(singleTargetReference); |
| } |
| } |
| if (invoke.isInvokeMethodWithReceiver()) { |
| Value receiver = invoke.asInvokeMethodWithReceiver().getReceiver(); |
| if (receiver == enumValue && targetHolder.isInterface()) { |
| return Reason.DEFAULT_METHOD_INVOKE; |
| } |
| } |
| return Reason.ELIGIBLE; |
| } |
| |
| if (targetHolder.isClasspathClass()) { |
| return Reason.INVALID_INVOKE_CLASSPATH; |
| } |
| |
| assert targetHolder.isLibraryClass(); |
| |
| Reason reason = |
| analyzeLibraryInvoke( |
| invoke, code, context, enumClass, enumValue, singleTargetReference, targetHolder); |
| |
| if (reason == Reason.ELIGIBLE) { |
| markMethodDependsOnLibraryModelisation(context); |
| } |
| |
| return reason; |
| } |
| |
| private Reason analyzeLibraryInvoke( |
| InvokeMethod invoke, |
| IRCode code, |
| ProgramMethod context, |
| DexProgramClass enumClass, |
| Value enumValue, |
| DexMethod singleTargetReference, |
| DexClass targetHolder) { |
| // Calls to java.lang.Enum. |
| if (targetHolder.getType() == factory.enumType) { |
| // TODO(b/147860220): EnumSet and EnumMap may be interesting to model. |
| if (singleTargetReference == factory.enumMembers.compareTo |
| || singleTargetReference == factory.enumMembers.compareToWithObject) { |
| DexProgramClass otherEnumClass = |
| getEnumUnboxingCandidateOrNull(invoke.getLastArgument().getType()); |
| if (otherEnumClass == enumClass || invoke.getLastArgument().getType().isNullType()) { |
| return Reason.ELIGIBLE; |
| } |
| } else if (singleTargetReference == factory.enumMembers.equals) { |
| return Reason.ELIGIBLE; |
| } else if (singleTargetReference == factory.enumMembers.nameMethod |
| || singleTargetReference == factory.enumMembers.toString) { |
| assert invoke.asInvokeMethodWithReceiver().getReceiver() == enumValue; |
| addRequiredNameData(enumClass); |
| return Reason.ELIGIBLE; |
| } else if (singleTargetReference == factory.enumMembers.ordinalMethod) { |
| return Reason.ELIGIBLE; |
| } else if (singleTargetReference == factory.enumMembers.hashCode) { |
| return Reason.ELIGIBLE; |
| } else if (singleTargetReference == factory.enumMembers.constructor) { |
| // Enum constructor call is allowed only if called from an enum initializer. |
| if (code.method().isInstanceInitializer() && code.context().getHolder() == enumClass) { |
| return Reason.ELIGIBLE; |
| } |
| } |
| return new UnsupportedLibraryInvokeReason(singleTargetReference); |
| } |
| |
| // Calls to java.lang.Object. |
| if (targetHolder.getType() == factory.objectType) { |
| // Object#getClass without outValue is important since R8 rewrites explicit null checks to |
| // such instructions. |
| if (singleTargetReference == factory.objectMembers.getClass && invoke.hasUnusedOutValue()) { |
| // This is a hidden null check. |
| return Reason.ELIGIBLE; |
| } |
| return new UnsupportedLibraryInvokeReason(singleTargetReference); |
| } |
| |
| // Calls to java.lang.Objects. |
| if (targetHolder.getType() == factory.objectsType) { |
| // Objects#requireNonNull is important since R8 rewrites explicit null checks to such |
| // instructions. |
| if (singleTargetReference == factory.objectsMethods.requireNonNull |
| || singleTargetReference == factory.objectsMethods.requireNonNullWithMessage) { |
| return Reason.ELIGIBLE; |
| } |
| return new UnsupportedLibraryInvokeReason(singleTargetReference); |
| } |
| |
| // Calls to java.lang.String. |
| if (targetHolder.getType() == factory.stringType) { |
| if (singleTargetReference == factory.stringMembers.valueOf) { |
| addRequiredNameData(enumClass); |
| return Reason.ELIGIBLE; |
| } |
| return new UnsupportedLibraryInvokeReason(singleTargetReference); |
| } |
| |
| // Calls to java.lang.StringBuilder and java.lang.StringBuffer. |
| if (targetHolder.getType() == factory.stringBuilderType |
| || targetHolder.getType() == factory.stringBufferType) { |
| if (singleTargetReference == factory.stringBuilderMethods.appendObject |
| || singleTargetReference == factory.stringBufferMethods.appendObject) { |
| addRequiredNameData(enumClass); |
| return Reason.ELIGIBLE; |
| } |
| return new UnsupportedLibraryInvokeReason(singleTargetReference); |
| } |
| |
| // Calls to java.lang.System. |
| if (targetHolder.getType() == factory.javaLangSystemType) { |
| if (singleTargetReference == factory.javaLangSystemMethods.arraycopy) { |
| // Important for Kotlin 1.5 enums, which use arraycopy to create a copy of $VALUES instead |
| // of int[].clone(). |
| return Reason.ELIGIBLE; |
| } |
| if (singleTargetReference == factory.javaLangSystemMethods.identityHashCode) { |
| // Important for proto enum unboxing. |
| return Reason.ELIGIBLE; |
| } |
| return new UnsupportedLibraryInvokeReason(singleTargetReference); |
| } |
| |
| // Unsupported holder. |
| return new UnsupportedLibraryInvokeReason(singleTargetReference); |
| } |
| |
| // Return is used for valueOf methods. |
| private Reason analyzeReturnUser( |
| Return theReturn, |
| IRCode code, |
| ProgramMethod context, |
| DexProgramClass enumClass, |
| Value enumValue) { |
| DexType returnType = context.getReturnType(); |
| if (returnType != enumClass.type && returnType.toBaseType(factory) != enumClass.type) { |
| return Reason.IMPLICIT_UP_CAST_IN_RETURN; |
| } |
| return Reason.ELIGIBLE; |
| } |
| |
| private void reportEnumsAnalysis() { |
| assert debugLogEnabled; |
| Reporter reporter = appView.reporter(); |
| Set<DexType> candidates = enumUnboxingCandidatesInfo.candidates(); |
| reporter.info( |
| new StringDiagnostic( |
| "Unboxed " + candidates.size() + " enums: " + Arrays.toString(candidates.toArray()))); |
| |
| StringBuilder sb = |
| new StringBuilder("Unable to unbox ") |
| .append(debugLogs.size()) |
| .append(" enums.") |
| .append(System.lineSeparator()) |
| .append(System.lineSeparator()); |
| |
| // Sort by the number of reasons that prevent enum unboxing. |
| TreeMap<DexType, List<Reason>> sortedDebugLogs = |
| new TreeMap<>( |
| Comparator.<DexType>comparingInt(x -> debugLogs.get(x).size()) |
| .thenComparing(Function.identity())); |
| sortedDebugLogs.putAll(debugLogs); |
| |
| // Print the pinned enums and remove them from further reporting. |
| List<DexType> pinned = new ArrayList<>(); |
| Iterator<Entry<DexType, List<Reason>>> sortedDebugLogIterator = |
| sortedDebugLogs.entrySet().iterator(); |
| while (sortedDebugLogIterator.hasNext()) { |
| Entry<DexType, List<Reason>> entry = sortedDebugLogIterator.next(); |
| List<Reason> reasons = entry.getValue(); |
| if (reasons.size() > 1) { |
| break; |
| } |
| if (reasons.get(0) == Reason.PINNED) { |
| pinned.add(entry.getKey()); |
| sortedDebugLogIterator.remove(); |
| } |
| } |
| if (!pinned.isEmpty()) { |
| sb.append("Pinned: ").append(Arrays.toString(pinned.toArray())); |
| } |
| |
| // Print the reasons for each unboxable enum. |
| sortedDebugLogs.forEach( |
| (type, reasons) -> { |
| sb.append(type).append(" (").append(reasons.size()).append(" reasons):"); |
| HashMultiset.create(reasons) |
| .forEachEntry( |
| (reason, count) -> |
| sb.append(System.lineSeparator()) |
| .append(" - ") |
| .append(reason) |
| .append(" (") |
| .append(count) |
| .append(")")); |
| sb.append(System.lineSeparator()); |
| }); |
| |
| sb.append(System.lineSeparator()); |
| |
| // Print information about how often a given Reason kind prevents enum unboxing. |
| Object2IntMap<Object> reasonKindCount = new Object2IntOpenHashMap<>(); |
| debugLogs.forEach( |
| (type, reasons) -> |
| reasons.forEach( |
| reason -> |
| reasonKindCount.put(reason.getKind(), reasonKindCount.getInt(reason) + 1))); |
| List<Object> differentReasonKinds = new ArrayList<>(reasonKindCount.keySet()); |
| differentReasonKinds.sort( |
| (reasonKind, other) -> { |
| int freq = reasonKindCount.getInt(reasonKind) - reasonKindCount.getInt(other); |
| return freq != 0 |
| ? freq |
| : System.identityHashCode(reasonKind) - System.identityHashCode(other); |
| }); |
| differentReasonKinds.forEach( |
| reasonKind -> |
| sb.append(reasonKind) |
| .append(" (") |
| .append(reasonKindCount.getInt(reasonKind)) |
| .append(")") |
| .append(System.lineSeparator())); |
| |
| reporter.info(new StringDiagnostic(sb.toString())); |
| } |
| |
| boolean reportFailure(DexProgramClass enumClass, Reason reason) { |
| return reportFailure(enumClass.getType(), reason); |
| } |
| |
| /** Returns true if the failure was reported. */ |
| boolean reportFailure(DexType enumType, Reason reason) { |
| if (debugLogEnabled) { |
| debugLogs |
| .computeIfAbsent(enumType, ignore -> Collections.synchronizedList(new ArrayList<>())) |
| .add(reason); |
| return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public Set<Phi> rewriteCode(IRCode code, MethodProcessor methodProcessor) { |
| // This has no effect during primary processing since the enumUnboxerRewriter is set |
| // in between primary and post processing. |
| if (enumUnboxerRewriter != null) { |
| return enumUnboxerRewriter.rewriteCode(code, methodProcessor); |
| } |
| return Sets.newIdentityHashSet(); |
| } |
| |
| @Override |
| public void unsetRewriter() { |
| enumUnboxerRewriter = null; |
| } |
| } |