blob: 0b47a530458bf3c645ac9403d987024dcf68ca44 [file] [log] [blame]
// 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.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.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) {
// 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 (user.isAssume()) {
continue;
}
if (user.isInvokeVirtual()
&& isUnboxableNameMethod(user.asInvokeVirtual().getInvokedMethod())) {
continue;
}
if (user.isInvokeStatic()) {
DexClassAndMethod singleTarget = user.asInvokeStatic().lookupSingleTarget(appView, context);
if (singleTarget != null) {
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);
continue;
}
if (singleTarget.getReference()
== factory.javaLangReflectArrayMembers.newInstanceMethodWithDimensions) {
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
.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());
}
});
}
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) {
// 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(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 appView.options().isGeneratingClassFiles()
|| !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 -> {
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 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);
}
}
}