| // Copyright (c) 2020, 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.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_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_PUT; |
| |
| import com.android.tools.r8.graph.AccessFlags; |
| import com.android.tools.r8.graph.AppView; |
| import com.android.tools.r8.graph.DexCallSite; |
| import com.android.tools.r8.graph.DexClass; |
| import com.android.tools.r8.graph.DexClassAndMethod; |
| import com.android.tools.r8.graph.DexEncodedField; |
| import com.android.tools.r8.graph.DexEncodedMember; |
| 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.DexProgramClass; |
| import com.android.tools.r8.graph.DexType; |
| import com.android.tools.r8.graph.DirectMappedDexApplication; |
| 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.graph.ProgramPackageCollection; |
| import com.android.tools.r8.graph.ResolutionResult; |
| import com.android.tools.r8.graph.UseRegistry; |
| 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.InvokeMethod; |
| import com.android.tools.r8.ir.code.InvokeStatic; |
| import com.android.tools.r8.ir.code.MemberType; |
| import com.android.tools.r8.ir.code.Opcodes; |
| 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.PostMethodProcessor; |
| import com.android.tools.r8.ir.optimize.Inliner.Constraint; |
| 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.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.OptimizationFeedback.OptimizationInfoFixer; |
| import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackDelayed; |
| import com.android.tools.r8.ir.optimize.info.UpdatableMethodOptimizationInfo; |
| import com.android.tools.r8.shaking.AppInfoWithLiveness; |
| import com.android.tools.r8.shaking.FieldAccessInfoCollectionModifier; |
| 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.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.Function; |
| import java.util.function.Predicate; |
| |
| public class 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 final EnumUnboxingCandidateInfoCollection enumUnboxingCandidatesInfo; |
| private final ProgramPackageCollection enumsToUnboxWithPackageRequirement = |
| ProgramPackageCollection.createEmpty(); |
| private final Map<DexType, EnumStaticFieldValues> staticFieldValuesMap = |
| new ConcurrentHashMap<>(); |
| |
| private final DexEncodedField ordinalField; |
| |
| private EnumUnboxingRewriter enumUnboxerRewriter; |
| |
| private final boolean debugLogEnabled; |
| private final Map<DexType, List<Reason>> debugLogs; |
| |
| public EnumUnboxer(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; |
| enumUnboxingCandidatesInfo = new EnumUnboxingCandidateAnalysis(appView, this).findCandidates(); |
| |
| ordinalField = |
| appView.appInfo().resolveField(factory.enumMembers.ordinalField).getResolvedField(); |
| if (ordinalField == null) { |
| // This can happen when compiling for non standard libraries, in that case, this effectively |
| // disables the enum unboxer. |
| enumUnboxingCandidatesInfo.clear(); |
| } |
| } |
| |
| public static int ordinalToUnboxedInt(int ordinal) { |
| return ordinal + 1; |
| } |
| |
| /** |
| * 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. |
| enumUnboxingCandidatesInfo.removeCandidate(enumClass); |
| return true; |
| } |
| return false; |
| } |
| |
| 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) { |
| return enumUnboxingCandidatesInfo.getCandidateClassOrNull(type); |
| } |
| |
| public void analyzeEnums(IRCode code) { |
| 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 Opcodes.CONST_CLASS: |
| analyzeConstClass(instruction.asConstClass(), eligibleEnums, code.context()); |
| break; |
| case Opcodes.CHECK_CAST: |
| analyzeCheckCast(instruction.asCheckCast(), eligibleEnums); |
| break; |
| case INVOKE_STATIC: |
| analyzeInvokeStatic(instruction.asInvokeStatic(), eligibleEnums, code.context()); |
| break; |
| case Opcodes.STATIC_GET: |
| case Opcodes.INSTANCE_GET: |
| case Opcodes.STATIC_PUT: |
| case INSTANCE_PUT: |
| analyzeFieldInstruction(instruction.asFieldInstruction(), code); |
| 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()); |
| } |
| } |
| } |
| |
| private void analyzeFieldInstruction(FieldInstruction fieldInstruction, IRCode code) { |
| DexField field = fieldInstruction.getField(); |
| DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(field.holder); |
| if (enumClass != null) { |
| FieldResolutionResult resolutionResult = |
| appView.appInfo().resolveField(field, code.context()); |
| if (resolutionResult.isFailedOrUnknownResolution()) { |
| 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) { |
| // 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 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().uniqueUsers()) { |
| if (user.isInvokeVirtual() |
| && isUnboxableNameMethod(user.asInvokeVirtual().getInvokedMethod())) { |
| continue; |
| } |
| if (user.isInvokeStatic()) { |
| DexClassAndMethod singleTarget = user.asInvokeStatic().lookupSingleTarget(appView, context); |
| if (singleTarget != null && 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); |
| continue; |
| } |
| } |
| markEnumAsUnboxable(Reason.CONST_CLASS, enumClass); |
| return; |
| } |
| eligibleEnums.add(enumType); |
| } |
| |
| 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; |
| } |
| |
| public void unboxEnums( |
| PostMethodProcessor.Builder postBuilder, |
| ExecutorService executorService, |
| OptimizationFeedbackDelayed feedback) |
| throws ExecutionException { |
| EnumDataMap enumDataMap = finishAnalysis(); |
| // 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. |
| if (enumUnboxingCandidatesInfo.isEmpty()) { |
| return; |
| } |
| ImmutableSet<DexType> enumsToUnbox = enumUnboxingCandidatesInfo.candidates(); |
| ImmutableSet<DexProgramClass> enumClassesToUnbox = |
| enumUnboxingCandidatesInfo.candidateClasses(); |
| ProgramMethodSet dependencies = enumUnboxingCandidatesInfo.allMethodDependencies(); |
| enumUnboxingCandidatesInfo.clear(); |
| // Update keep info on any of the enum methods of the removed classes. |
| updateKeepInfo(enumsToUnbox); |
| DirectMappedDexApplication.Builder appBuilder = appView.appInfo().app().asDirect().builder(); |
| FieldAccessInfoCollectionModifier.Builder fieldAccessInfoCollectionModifierBuilder = |
| FieldAccessInfoCollectionModifier.builder(); |
| UnboxedEnumMemberRelocator relocator = |
| UnboxedEnumMemberRelocator.builder(appView) |
| .synthesizeEnumUnboxingUtilityClasses( |
| enumClassesToUnbox, |
| enumsToUnboxWithPackageRequirement, |
| appBuilder, |
| fieldAccessInfoCollectionModifierBuilder) |
| .build(); |
| fieldAccessInfoCollectionModifierBuilder.build().modify(appView); |
| enumUnboxerRewriter = new EnumUnboxingRewriter(appView, enumDataMap, relocator); |
| EnumUnboxingLens enumUnboxingLens = |
| new EnumUnboxingTreeFixer(appView, enumsToUnbox, relocator, enumUnboxerRewriter) |
| .fixupTypeReferences(); |
| enumUnboxerRewriter.setEnumUnboxingLens(enumUnboxingLens); |
| appView.setUnboxedEnums(enumDataMap); |
| GraphLens previousLens = appView.graphLens(); |
| appView.rewriteWithLensAndApplication(enumUnboxingLens, appBuilder.build()); |
| updateOptimizationInfos(executorService, feedback); |
| postBuilder.put(dependencies); |
| postBuilder.rewrittenWithLens(appView, previousLens); |
| } |
| |
| private void updateOptimizationInfos( |
| ExecutorService executorService, OptimizationFeedbackDelayed feedback) |
| throws ExecutionException { |
| feedback.fixupOptimizationInfos( |
| appView, |
| executorService, |
| new OptimizationInfoFixer() { |
| @Override |
| public void fixup(DexEncodedField field, MutableFieldOptimizationInfo optimizationInfo) { |
| optimizationInfo |
| .asMutableFieldOptimizationInfo() |
| .fixupClassTypeReferences(appView, appView.graphLens()) |
| .fixupAbstractValue(appView, appView.graphLens()); |
| } |
| |
| @Override |
| public void fixup( |
| DexEncodedMethod method, UpdatableMethodOptimizationInfo optimizationInfo) { |
| optimizationInfo |
| .fixupClassTypeReferences(appView, appView.graphLens()) |
| .fixupAbstractReturnValue(appView, appView.graphLens()) |
| .fixupInstanceInitializerInfo(appView, appView.graphLens()); |
| } |
| }); |
| } |
| |
| private void updateKeepInfo(Set<DexType> enumsToUnbox) { |
| KeepInfoCollection keepInfo = appView.appInfo().getKeepInfo(); |
| keepInfo.mutate(mutator -> mutator.removeKeepInfoForPrunedItems(enumsToUnbox)); |
| } |
| |
| public EnumDataMap finishAnalysis() { |
| analyzeInitializers(); |
| analyzeAccessibility(); |
| 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) { |
| 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) { |
| // 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(ordinalField); |
| if (field.isSingleNumberValue()) { |
| return OptionalInt.of(field.asSingleNumberValue().getIntValue()); |
| } |
| return OptionalInt.empty(); |
| } |
| |
| private void analyzeAccessibility() { |
| // Unboxing an enum will require to move its methods to a different class, which may impact |
| // accessibility. For a quick analysis we simply reuse the inliner analysis. |
| enumUnboxingCandidatesInfo.forEachCandidate( |
| enumClass -> { |
| Constraint classConstraint = analyzeAccessibilityInClass(enumClass); |
| if (classConstraint == Constraint.NEVER) { |
| markEnumAsUnboxable(Reason.ACCESSIBILITY, enumClass); |
| } else if (classConstraint == Constraint.PACKAGE) { |
| enumsToUnboxWithPackageRequirement.addProgramClass(enumClass); |
| } |
| }); |
| } |
| |
| private Constraint analyzeAccessibilityInClass(DexProgramClass enumClass) { |
| Constraint classConstraint = Constraint.ALWAYS; |
| EnumAccessibilityUseRegistry useRegistry = null; |
| for (DexEncodedMethod method : enumClass.methods()) { |
| // Enum initializer are analyzed in analyzeInitializers instead. |
| if (!method.isInitializer()) { |
| if (useRegistry == null) { |
| useRegistry = new EnumAccessibilityUseRegistry(factory); |
| } |
| Constraint methodConstraint = constraintForEnumUnboxing(method, useRegistry); |
| classConstraint = classConstraint.meet(methodConstraint); |
| if (classConstraint == Constraint.NEVER) { |
| return classConstraint; |
| } |
| } |
| } |
| return classConstraint; |
| } |
| |
| public Constraint constraintForEnumUnboxing( |
| DexEncodedMethod method, EnumAccessibilityUseRegistry useRegistry) { |
| return useRegistry.computeConstraint(method.asProgramMethod(appView)); |
| } |
| |
| 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 class EnumAccessibilityUseRegistry extends UseRegistry { |
| |
| private ProgramMethod context; |
| private Constraint constraint; |
| |
| public EnumAccessibilityUseRegistry(DexItemFactory factory) { |
| super(factory); |
| } |
| |
| public Constraint computeConstraint(ProgramMethod method) { |
| constraint = Constraint.ALWAYS; |
| context = method; |
| method.registerCodeReferences(this); |
| return constraint; |
| } |
| |
| public Constraint deriveConstraint(DexType targetHolder, AccessFlags<?> flags) { |
| DexProgramClass contextHolder = context.getHolder(); |
| if (targetHolder == contextHolder.type) { |
| return Constraint.ALWAYS; |
| } |
| if (flags.isPublic()) { |
| return Constraint.ALWAYS; |
| } |
| if (flags.isPrivate()) { |
| // Enum unboxing is currently happening only cf to dex, and no class should be in a nest |
| // at this point. If that is the case, we just don't unbox the enum, or we would need to |
| // support Constraint.SAMENEST in the enum unboxer. |
| assert !contextHolder.isInANest(); |
| // Only accesses within the enum are allowed since all enum methods and fields will be |
| // moved to the same class, and the enum itself becomes an integer, which is |
| // accessible everywhere. |
| return Constraint.NEVER; |
| } |
| assert flags.isProtected() || flags.isPackagePrivate(); |
| // Protected is in practice equivalent to package private in this analysis since we are |
| // accessing the member from an enum context where subclassing is limited. |
| // At this point we don't support unboxing enums with subclasses, so we assume either |
| // same package access, or we just don't unbox. |
| // The only protected methods in java.lang.Enum are clone, finalize and the constructor. |
| // Besides calls to the constructor in the instance initializer, Enums with calls to such |
| // methods cannot be unboxed. |
| return targetHolder.isSamePackage(contextHolder.type) ? Constraint.PACKAGE : Constraint.NEVER; |
| } |
| |
| @Override |
| public void registerTypeReference(DexType type) { |
| if (type.isArrayType()) { |
| registerTypeReference(type.toBaseType(factory)); |
| return; |
| } |
| |
| if (type.isPrimitiveType()) { |
| return; |
| } |
| |
| DexClass definition = appView.definitionFor(type); |
| if (definition == null) { |
| constraint = Constraint.NEVER; |
| return; |
| } |
| constraint = constraint.meet(deriveConstraint(type, definition.accessFlags)); |
| } |
| |
| @Override |
| public void registerInitClass(DexType type) { |
| registerTypeReference(type); |
| } |
| |
| @Override |
| public void registerInstanceOf(DexType type) { |
| registerTypeReference(type); |
| } |
| |
| @Override |
| public void registerNewInstance(DexType type) { |
| registerTypeReference(type); |
| } |
| |
| @Override |
| public void registerInvokeVirtual(DexMethod method) { |
| registerVirtualInvoke(method, false); |
| } |
| |
| @Override |
| public void registerInvokeInterface(DexMethod method) { |
| registerVirtualInvoke(method, true); |
| } |
| |
| private void registerVirtualInvoke(DexMethod method, boolean isInterface) { |
| if (method.holder.isArrayType()) { |
| return; |
| } |
| // Perform resolution and derive unboxing constraints based on the accessibility of the |
| // resolution result. |
| ResolutionResult resolutionResult = appView.appInfo().resolveMethod(method, isInterface); |
| if (!resolutionResult.isVirtualTarget()) { |
| constraint = Constraint.NEVER; |
| return; |
| } |
| registerTarget( |
| resolutionResult.getInitialResolutionHolder(), resolutionResult.getSingleTarget()); |
| } |
| |
| private void registerTarget(DexClass initialResolutionHolder, DexEncodedMember<?, ?> target) { |
| if (target == null) { |
| // This will fail at runtime. |
| constraint = Constraint.NEVER; |
| return; |
| } |
| DexType resolvedHolder = target.getHolderType(); |
| if (initialResolutionHolder == null) { |
| constraint = Constraint.NEVER; |
| return; |
| } |
| Constraint memberConstraint = deriveConstraint(resolvedHolder, target.getAccessFlags()); |
| // We also have to take the constraint of the initial resolution holder into account. |
| Constraint classConstraint = |
| deriveConstraint(initialResolutionHolder.type, initialResolutionHolder.accessFlags); |
| Constraint instructionConstraint = memberConstraint.meet(classConstraint); |
| constraint = instructionConstraint.meet(constraint); |
| } |
| |
| @Override |
| public void registerInvokeDirect(DexMethod method) { |
| registerSingleTargetInvoke(method, DexEncodedMethod::isDirectMethod); |
| } |
| |
| @Override |
| public void registerInvokeStatic(DexMethod method) { |
| registerSingleTargetInvoke(method, DexEncodedMethod::isStatic); |
| } |
| |
| private void registerSingleTargetInvoke( |
| DexMethod method, Predicate<DexEncodedMethod> methodValidator) { |
| if (method.holder.isArrayType()) { |
| return; |
| } |
| ResolutionResult resolutionResult = |
| appView.appInfo().unsafeResolveMethodDueToDexFormat(method); |
| DexEncodedMethod target = resolutionResult.getSingleTarget(); |
| if (target == null || !methodValidator.test(target)) { |
| constraint = Constraint.NEVER; |
| return; |
| } |
| registerTarget(resolutionResult.getInitialResolutionHolder(), target); |
| } |
| |
| @Override |
| public void registerInvokeSuper(DexMethod method) { |
| // Invoke-super can only target java.lang.Enum methods since we do not unbox enums with |
| // subclasses. Calls to java.lang.Object methods would have resulted in the enum to be marked |
| // as unboxable. The methods of java.lang.Enum called are already analyzed in the enum |
| // unboxer analysis, so invoke-super is always valid. |
| assert method.holder == factory.enumType; |
| } |
| |
| @Override |
| public void registerCallSite(DexCallSite callSite) { |
| // This is reached after lambda desugaring, so this should not be a lambda call site. |
| // We do not unbox enums with invoke custom since it's not clear the accessibility |
| // constraints would be correct if the method holding the invoke custom is moved to |
| // another class. |
| assert !factory.isLambdaMetafactoryMethod(callSite.bootstrapMethod.asMethod()); |
| constraint = Constraint.NEVER; |
| } |
| |
| private void registerFieldInstruction(DexField field) { |
| FieldResolutionResult fieldResolutionResult = appView.appInfo().resolveField(field, context); |
| registerTarget( |
| fieldResolutionResult.getInitialResolutionHolder(), |
| fieldResolutionResult.getResolvedField()); |
| } |
| |
| @Override |
| public void registerInstanceFieldRead(DexField field) { |
| registerFieldInstruction(field); |
| } |
| |
| @Override |
| public void registerInstanceFieldWrite(DexField field) { |
| registerFieldInstruction(field); |
| } |
| |
| @Override |
| public void registerStaticFieldRead(DexField field) { |
| registerFieldInstruction(field); |
| } |
| |
| @Override |
| public void registerStaticFieldWrite(DexField field) { |
| registerFieldInstruction(field); |
| } |
| } |
| |
| private void analyzeInitializers() { |
| enumUnboxingCandidatesInfo.forEachCandidate( |
| enumClass -> { |
| boolean hasInstanceInitializer = false; |
| for (DexEncodedMethod directMethod : enumClass.directMethods()) { |
| if (directMethod.isInstanceInitializer()) { |
| hasInstanceInitializer = true; |
| if (directMethod |
| .getOptimizationInfo() |
| .getContextInsensitiveInstanceInitializerInfo() |
| .mayHaveOtherSideEffectsThanInstanceFieldAssignments()) { |
| if (markEnumAsUnboxable(Reason.INVALID_INIT, enumClass)) { |
| break; |
| } |
| } |
| } |
| } |
| if (!hasInstanceInitializer) { |
| // This case typically happens when a programmer uses EnumSet/EnumMap without using the |
| // enum keep rules. The code is incorrect in this case (EnumSet/EnumMap won't work). |
| // We bail out. |
| if (markEnumAsUnboxable(Reason.NO_INIT, enumClass)) { |
| return; |
| } |
| } |
| |
| 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 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(); |
| |
| if (targetHolder.getType() != factory.enumType) { |
| // System.identityHashCode(Object) is supported for proto enums. |
| // Object#getClass without outValue and Objects.requireNonNull are supported since R8 |
| // rewrites explicit null checks to such instructions. |
| if (singleTargetReference == factory.javaLangSystemMethods.identityHashCode) { |
| return Reason.ELIGIBLE; |
| } |
| if (singleTargetReference == factory.stringMembers.valueOf) { |
| addRequiredNameData(enumClass); |
| return Reason.ELIGIBLE; |
| } |
| if (singleTargetReference == factory.objectMembers.getClass |
| && (!invoke.hasOutValue() || !invoke.outValue().hasAnyUsers())) { |
| // This is a hidden null check. |
| return Reason.ELIGIBLE; |
| } |
| if (singleTargetReference == factory.objectsMethods.requireNonNull |
| || singleTargetReference == factory.objectsMethods.requireNonNullWithMessage) { |
| return Reason.ELIGIBLE; |
| } |
| return new UnsupportedLibraryInvokeReason(singleTargetReference); |
| } |
| // TODO(b/147860220): EnumSet and EnumMap may be interesting to model. |
| if (singleTargetReference == factory.enumMembers.compareTo) { |
| 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); |
| } |
| |
| // 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; |
| } |
| |
| public Set<Phi> rewriteCode(IRCode code) { |
| // 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); |
| } |
| return Sets.newIdentityHashSet(); |
| } |
| |
| public void synthesizeUtilityMethods(IRConverter converter, ExecutorService executorService) |
| throws ExecutionException { |
| if (enumUnboxerRewriter != null) { |
| enumUnboxerRewriter.synthesizeEnumUnboxingUtilityMethods(converter, executorService); |
| } |
| } |
| } |