Enum unboxing: support Enum#valueOf
Bug: 147860220
Change-Id: I34c3395cebb207258f17b0a5e3c4669c180b5472
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 266b607..3e9ae7e 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -856,6 +856,7 @@
internal.enableVerticalClassMerging = false;
internal.enableClassStaticizer = false;
internal.outline.enabled = false;
+ internal.enableEnumUnboxing = false;
}
// Amend the proguard-map consumer with options from the proguard configuration.
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index a72549d..b0a86c6 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -158,6 +158,7 @@
public final DexString isEmptyMethodName = createString("isEmpty");
public final DexString lengthMethodName = createString("length");
+ public final DexString concatMethodName = createString("concat");
public final DexString containsMethodName = createString("contains");
public final DexString startsWithMethodName = createString("startsWith");
public final DexString endsWithMethodName = createString("endsWith");
@@ -256,6 +257,8 @@
public final DexString throwableDescriptor = createString(throwableDescriptorString);
public final DexString illegalAccessErrorDescriptor =
createString("Ljava/lang/IllegalAccessError;");
+ public final DexString illegalArgumentExceptionDescriptor =
+ createString("Ljava/lang/IllegalArgumentException;");
public final DexString icceDescriptor = createString("Ljava/lang/IncompatibleClassChangeError;");
public final DexString exceptionInInitializerErrorDescriptor =
createString("Ljava/lang/ExceptionInInitializerError;");
@@ -383,6 +386,8 @@
public final DexType throwableType = createStaticallyKnownType(throwableDescriptor);
public final DexType illegalAccessErrorType =
createStaticallyKnownType(illegalAccessErrorDescriptor);
+ public final DexType illegalArgumentExceptionType =
+ createStaticallyKnownType(illegalArgumentExceptionDescriptor);
public final DexType icceType = createStaticallyKnownType(icceDescriptor);
public final DexType exceptionInInitializerErrorType =
createStaticallyKnownType(exceptionInInitializerErrorDescriptor);
@@ -426,6 +431,8 @@
public final ConstructorMethods constructorMethods = new ConstructorMethods();
public final EnumMethods enumMethods = new EnumMethods();
public final NullPointerExceptionMethods npeMethods = new NullPointerExceptionMethods();
+ public final IllegalArgumentExceptionMethods illegalArgumentExceptionMethods =
+ new IllegalArgumentExceptionMethods();
public final PrimitiveTypesBoxedTypeFields primitiveTypesBoxedTypeFields =
new PrimitiveTypesBoxedTypeFields();
public final AtomicFieldUpdaterMethods atomicFieldUpdaterMethods =
@@ -976,6 +983,13 @@
createMethod(npeType, createProto(voidType, stringType), constructorMethodName);
}
+ public class IllegalArgumentExceptionMethods {
+
+ public final DexMethod initWithMessage =
+ createMethod(
+ illegalArgumentExceptionType, createProto(voidType, stringType), initMethodName);
+ }
+
/**
* All boxed types (Boolean, Byte, ...) have a field named TYPE which contains the Class object
* for the primitive type.
@@ -1062,6 +1076,7 @@
public final DexMethod isEmpty;
public final DexMethod length;
+ public final DexMethod concat;
public final DexMethod contains;
public final DexMethod startsWith;
public final DexMethod endsWith;
@@ -1094,6 +1109,7 @@
DexString[] needsOneObject = { objectDescriptor };
DexString[] needsOneInt = { intDescriptor };
+ concat = createMethod(stringDescriptor, concatMethodName, stringDescriptor, needsOneString);
contains = createMethod(
stringDescriptor, containsMethodName, booleanDescriptor, needsOneCharSequence);
startsWith = createMethod(
diff --git a/src/main/java/com/android/tools/r8/graph/EnumValueInfoMapCollection.java b/src/main/java/com/android/tools/r8/graph/EnumValueInfoMapCollection.java
index 4fd96f7..23bfb99 100644
--- a/src/main/java/com/android/tools/r8/graph/EnumValueInfoMapCollection.java
+++ b/src/main/java/com/android/tools/r8/graph/EnumValueInfoMapCollection.java
@@ -7,6 +7,7 @@
import com.google.common.collect.ImmutableMap;
import java.util.Map;
import java.util.Set;
+import java.util.function.BiConsumer;
public class EnumValueInfoMapCollection {
@@ -94,6 +95,10 @@
return map.get(field);
}
+ public void forEach(BiConsumer<DexField, EnumValueInfo> consumer) {
+ map.forEach(consumer);
+ }
+
EnumValueInfoMap rewrittenWithLens(GraphLense lens) {
ImmutableMap.Builder<DexField, EnumValueInfo> builder = ImmutableMap.builder();
map.forEach(
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index fbc9a62..e29a1c1 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -94,6 +94,7 @@
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.android.tools.r8.shaking.LibraryMethodOverrideAnalysis;
import com.android.tools.r8.shaking.MainDexClasses;
+import com.android.tools.r8.utils.Action;
import com.android.tools.r8.utils.CfgPrinter;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.ExceptionUtils;
@@ -181,7 +182,7 @@
OptimizationFeedbackSimple.getInstance();
private DexString highestSortingString;
- private List<com.android.tools.r8.utils.Action> onWaveDoneActions = null;
+ private List<Action> onWaveDoneActions = null;
private final List<DexString> neverMergePrefixes;
boolean seenNotNeverMergePrefix = false;
@@ -1147,6 +1148,10 @@
codeRewriter.simplifyDebugLocals(code);
}
+ if (enumUnboxer != null && methodProcessor.isPost()) {
+ enumUnboxer.rewriteCode(code);
+ }
+
if (appView.graphLense().hasCodeRewritings()) {
assert lensCodeRewriter != null;
timing.begin("Lens rewrite");
@@ -1154,10 +1159,6 @@
timing.end();
}
- if (enumUnboxer != null && methodProcessor.isPost()) {
- enumUnboxer.rewriteCode(code);
- }
-
if (method.isProcessed()) {
assert !appView.enableWholeProgramOptimizations()
|| !appView.appInfo().withLiveness().neverReprocess.contains(method.method);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index 91d2227..33d982a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -738,10 +738,10 @@
if (inliningIRProvider.shouldApplyCodeRewritings(code.method)) {
assert lensCodeRewriter != null;
- lensCodeRewriter.rewrite(code, target);
if (enumUnboxer != null) {
enumUnboxer.rewriteCode(code);
}
+ lensCodeRewriter.rewrite(code, target);
}
if (lambdaMerger != null) {
lambdaMerger.rewriteCodeForInlining(target, code, context, inliningIRProvider);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
index 7dcc31a..ea9141f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
@@ -4,6 +4,8 @@
package com.android.tools.r8.ir.optimize.enums;
+import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
+
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexApplication;
import com.android.tools.r8.graph.DexClass;
@@ -28,6 +30,7 @@
import com.android.tools.r8.ir.analysis.type.TypeElement;
import com.android.tools.r8.ir.code.ArrayPut;
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;
@@ -86,6 +89,7 @@
debugLogEnabled = false;
debugLogs = null;
}
+ assert !appView.options().debug;
enumsUnboxingCandidates = new EnumUnboxingCandidateAnalysis(appView, this).findCandidates();
}
@@ -140,11 +144,6 @@
if (enumClass != null) {
Reason reason = validateEnumUsages(code, outValue, enumClass);
if (reason == Reason.ELIGIBLE) {
- if (instruction.isCheckCast()) {
- // We are doing a type check, which typically means the in-value is of an upper
- // type and cannot be dealt with.
- markEnumAsUnboxable(Reason.DOWN_CAST, enumClass);
- }
eligibleEnums.add(enumClass.type);
}
}
@@ -152,15 +151,10 @@
addNullDependencies(outValue.uniqueUsers(), eligibleEnums);
}
}
- // If we have a ConstClass referencing directly an enum, it cannot be unboxed, except if
- // the constClass is in an enum valueOf method (in this case the valueOf method will be
- // removed or the enum will be marked as non unboxable).
if (instruction.isConstClass()) {
- ConstClass constClass = instruction.asConstClass();
- if (enumsUnboxingCandidates.containsKey(constClass.getValue())) {
- markEnumAsUnboxable(
- Reason.CONST_CLASS, appView.definitionForProgramType(constClass.getValue()));
- }
+ analyzeConstClass(instruction.asConstClass());
+ } else if (instruction.isCheckCast()) {
+ analyzeCheckCast(instruction.asCheckCast());
} else if (instruction.isInvokeStatic()) {
// TODO(b/150370354): Since we temporary allow enum unboxing on enums with values and
// valueOf static methods only if such methods are unused, such methods cannot be
@@ -204,6 +198,48 @@
}
}
+ private void analyzeCheckCast(CheckCast checkCast) {
+ // 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.
+ TypeElement objectType = checkCast.object().getDynamicUpperBoundType(appView);
+ if (objectType.equalUpToNullability(
+ TypeElement.fromDexType(checkCast.getType(), definitelyNotNull(), appView))) {
+ return;
+ }
+ DexProgramClass enumClass =
+ getEnumUnboxingCandidateOrNull(checkCast.getType().toBaseType(factory));
+ if (enumClass != null) {
+ markEnumAsUnboxable(Reason.DOWN_CAST, enumClass);
+ }
+ }
+
+ private void analyzeConstClass(ConstClass constClass) {
+ // We are using the ConstClass of an enum, which typically means the enum cannot be unboxed.
+ // We however allow unboxing if the ConstClass is only used as an argument to Enum#valueOf, to
+ // allow unboxing of: MyEnum a = Enum.valueOf(MyEnum.class, "A");.
+ if (!enumsUnboxingCandidates.containsKey(constClass.getValue())) {
+ return;
+ }
+ if (constClass.outValue() == null) {
+ return;
+ }
+ if (constClass.outValue().hasPhiUsers()) {
+ markEnumAsUnboxable(
+ Reason.CONST_CLASS, appView.definitionForProgramType(constClass.getValue()));
+ return;
+ }
+ for (Instruction user : constClass.outValue().uniqueUsers()) {
+ if (!(user.isInvokeStatic()
+ && user.asInvokeStatic().getInvokedMethod() == factory.enumMethods.valueOf)) {
+ markEnumAsUnboxable(
+ Reason.CONST_CLASS, appView.definitionForProgramType(constClass.getValue()));
+ return;
+ }
+ }
+ }
+
private void addNullDependencies(Set<Instruction> uses, Set<DexType> eligibleEnums) {
for (Instruction use : uses) {
if (use.isInvokeMethod()) {
@@ -359,14 +395,6 @@
return Reason.INVALID_INVOKE;
}
if (dexClass.isProgramClass()) {
- // All invokes in the program are generally valid, but specific care is required
- // for values() and valueOf().
- if (dexClass.isEnum() && factory.enumMethods.isValuesMethod(singleTarget, dexClass)) {
- return Reason.VALUES_INVOKE;
- }
- if (dexClass.isEnum() && factory.enumMethods.isValueOfMethod(singleTarget, dexClass)) {
- return Reason.VALUE_OF_INVOKE;
- }
int offset = BooleanUtils.intValue(!encodedSingleTarget.isStatic());
for (int i = 0; i < singleTarget.proto.parameters.size(); i++) {
if (invokeMethod.inValues().get(offset + i) == enumValue) {
@@ -384,8 +412,8 @@
if (dexClass.type != factory.enumType) {
return Reason.UNSUPPORTED_LIBRARY_CALL;
}
- // TODO(b/147860220): Methods toString(), name(), compareTo(), EnumSet and EnumMap may be
- // interesting to model. A the moment rewrite only Enum#ordinal().
+ // TODO(b/147860220): Methods toString(), name(), compareTo(), EnumSet and EnumMap may
+ // be interesting to model. A the moment rewrite only Enum#ordinal() and Enum#valueOf.
if (debugLogEnabled) {
if (singleTarget == factory.enumMethods.compareTo) {
return Reason.COMPARE_TO_INVOKE;
@@ -397,10 +425,10 @@
return Reason.TO_STRING_INVOKE;
}
}
- if (singleTarget != factory.enumMethods.ordinal) {
- return Reason.UNSUPPORTED_LIBRARY_CALL;
+ if (singleTarget == factory.enumMethods.ordinal) {
+ return Reason.ELIGIBLE;
}
- return Reason.ELIGIBLE;
+ return Reason.UNSUPPORTED_LIBRARY_CALL;
}
// A field put is valid only if the field is not on an enum, and the field type and the valuePut
@@ -603,7 +631,7 @@
for (DexProgramClass clazz : appView.appInfo().classes()) {
if (enumsToUnbox.contains(clazz.type)) {
assert clazz.instanceFields().size() == 0;
- clearEnumtoUnboxMethods(clazz);
+ clearEnumToUnboxMethods(clazz);
} else {
clazz.getMethodCollection().replaceMethods(this::fixupMethod);
fixupFields(clazz.staticFields(), clazz::setStaticField);
@@ -616,7 +644,7 @@
return lensBuilder.build(factory, appView.graphLense());
}
- private void clearEnumtoUnboxMethods(DexProgramClass clazz) {
+ private void clearEnumToUnboxMethods(DexProgramClass clazz) {
// The compiler may have references to the enum methods, but such methods will be removed
// and they cannot be reprocessed since their rewriting through the lensCodeRewriter/
// enumUnboxerRewriter will generate invalid code.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
index 08c848c..73eff15 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
@@ -24,27 +24,28 @@
import com.android.tools.r8.graph.ParameterAnnotationsList;
import com.android.tools.r8.ir.analysis.type.ArrayTypeElement;
import com.android.tools.r8.ir.analysis.type.DestructivePhiTypeUpdater;
-import com.android.tools.r8.ir.analysis.type.PrimitiveTypeElement;
import com.android.tools.r8.ir.analysis.type.TypeElement;
import com.android.tools.r8.ir.code.ArrayAccess;
-import com.android.tools.r8.ir.code.ConstNumber;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.InstructionListIterator;
import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
import com.android.tools.r8.ir.code.InvokeStatic;
import com.android.tools.r8.ir.code.MemberType;
-import com.android.tools.r8.ir.code.NumericType;
import com.android.tools.r8.ir.code.Phi;
import com.android.tools.r8.ir.code.StaticGet;
+import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.ir.synthetic.EnumUnboxingCfCodeProvider;
import com.android.tools.r8.origin.SynthesizedOrigin;
import com.android.tools.r8.shaking.AppInfoWithLiveness;
import com.google.common.collect.Sets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
@@ -56,6 +57,7 @@
private final AppView<AppInfoWithLiveness> appView;
private final DexItemFactory factory;
private final EnumValueInfoMapCollection enumsToUnbox;
+ private final Map<DexMethod, DexType> extraMethods = new ConcurrentHashMap<>();
private final DexType utilityClassType;
private final DexMethod ordinalUtilityMethod;
@@ -89,6 +91,7 @@
if (enumsToUnbox.isEmpty()) {
return;
}
+ assert code.isConsistentSSABeforeTypesAreCorrect();
Set<Phi> affectedPhis = Sets.newIdentityHashSet();
InstructionListIterator iterator = code.instructionListIterator();
while (iterator.hasNext()) {
@@ -99,14 +102,38 @@
InvokeMethodWithReceiver invokeMethod = instruction.asInvokeMethodWithReceiver();
DexMethod invokedMethod = invokeMethod.getInvokedMethod();
if (invokedMethod == factory.enumMethods.ordinal
- && invokeMethod.getReceiver().getType().isInt()) {
+ && isEnumToUnboxOrInt(invokeMethod.getReceiver().getType())) {
instruction =
new InvokeStatic(
ordinalUtilityMethod, invokeMethod.outValue(), invokeMethod.inValues());
iterator.replaceCurrentInstruction(instruction);
requiresOrdinalUtilityMethod = true;
+ continue;
}
// TODO(b/147860220): rewrite also other enum methods.
+ } else if (instruction.isInvokeStatic()) {
+ InvokeStatic invokeStatic = instruction.asInvokeStatic();
+ DexMethod invokedMethod = invokeStatic.getInvokedMethod();
+ if (invokedMethod == factory.enumMethods.valueOf
+ && invokeStatic.inValues().get(0).isConstClass()) {
+ DexType enumType =
+ invokeStatic.inValues().get(0).getConstInstruction().asConstClass().getValue();
+ if (enumsToUnbox.containsEnum(enumType)) {
+ DexMethod valueOfMethod = computeValueOfUtilityMethod(enumType);
+ Value outValue = invokeStatic.outValue();
+ Value rewrittenOutValue = null;
+ if (outValue != null) {
+ rewrittenOutValue = code.createValue(TypeElement.getInt());
+ affectedPhis.addAll(outValue.uniquePhiUsers());
+ }
+ iterator.replaceCurrentInstruction(
+ new InvokeStatic(
+ valueOfMethod,
+ rewrittenOutValue,
+ Collections.singletonList(invokeStatic.inValues().get(1))));
+ continue;
+ }
+ }
}
// Rewrites direct access to enum values into the corresponding int, $VALUES is not
// supported.
@@ -124,10 +151,8 @@
EnumValueInfo enumValueInfo = enumValueInfoMap.getEnumValueInfo(staticGet.getField());
assert enumValueInfo != null
: "Invalid read to " + staticGet.getField().name + ", error during enum analysis";
- instruction = new ConstNumber(staticGet.outValue(), enumValueInfo.convertToInt());
- staticGet.outValue().setType(PrimitiveTypeElement.fromNumericType(NumericType.INT));
- iterator.replaceCurrentInstruction(instruction);
affectedPhis.addAll(staticGet.outValue().uniquePhiUsers());
+ iterator.replaceCurrentInstruction(code.createIntConstant(enumValueInfo.convertToInt()));
}
}
// Rewrite array accesses from MyEnum[] (OBJECT) to int[] (INT).
@@ -137,8 +162,8 @@
instruction = arrayAccess.withMemberType(MemberType.INT);
iterator.replaceCurrentInstruction(instruction);
}
+ assert validateArrayAccess(instruction.asArrayAccess());
}
- assert validateEnumToUnboxRemoved(instruction);
}
if (!affectedPhis.isEmpty()) {
new DestructivePhiTypeUpdater(appView).recomputeAndPropagateTypes(code, affectedPhis);
@@ -146,32 +171,43 @@
assert code.isConsistentSSABeforeTypesAreCorrect();
}
- private boolean shouldRewriteArrayAccess(ArrayAccess arrayAccess) {
+ private boolean validateArrayAccess(ArrayAccess arrayAccess) {
ArrayTypeElement arrayType = arrayAccess.array().getType().asArrayType();
- return arrayAccess.getMemberType() == MemberType.OBJECT
- && arrayType.getNesting() == 1
- && arrayType.getBaseType().isInt();
+ assert arrayAccess.getMemberType() != MemberType.OBJECT
+ || arrayType.getNesting() > 1
+ || arrayType.getBaseType().isReferenceType();
+ return true;
}
- private boolean validateEnumToUnboxRemoved(Instruction instruction) {
- if (instruction.isArrayAccess()) {
- ArrayAccess arrayAccess = instruction.asArrayAccess();
- ArrayTypeElement arrayType = arrayAccess.array().getType().asArrayType();
- assert arrayAccess.getMemberType() != MemberType.OBJECT
- || arrayType.getNesting() > 1
- || arrayType.getBaseType().isReferenceType();
- }
- if (instruction.outValue() == null) {
+ private boolean isEnumToUnboxOrInt(TypeElement type) {
+ if (type.isInt()) {
return true;
}
- TypeElement type = instruction.getOutType();
- assert !type.isClassType() || !enumsToUnbox.containsEnum(type.asClassType().getClassType());
- if (type.isArrayType()) {
- TypeElement arrayBaseTypeLattice = type.asArrayType().getBaseType();
- assert !arrayBaseTypeLattice.isClassType()
- || !enumsToUnbox.containsEnum(arrayBaseTypeLattice.asClassType().getClassType());
+ if (!type.isClassType()) {
+ return false;
}
- return true;
+ return enumsToUnbox.containsEnum(type.asClassType().getClassType());
+ }
+
+ private DexMethod computeValueOfUtilityMethod(DexType type) {
+ assert enumsToUnbox.containsEnum(type);
+ DexMethod valueOf =
+ factory.createMethod(
+ utilityClassType,
+ factory.createProto(factory.intType, factory.stringType),
+ "valueOf" + type.toSourceString().replace('.', '$'));
+ extraMethods.putIfAbsent(valueOf, type);
+ return valueOf;
+ }
+
+ private boolean shouldRewriteArrayAccess(ArrayAccess arrayAccess) {
+ ArrayTypeElement arrayType = arrayAccess.array().getType().asArrayType();
+ if (arrayType.getNesting() != 1) {
+ return false;
+ }
+ TypeElement baseType = arrayType.getBaseType();
+ return baseType.isClassType()
+ && enumsToUnbox.containsEnum(baseType.asClassType().getClassType());
}
// TODO(b/150172351): Synthesize the utility class upfront in the enqueuer.
@@ -181,6 +217,11 @@
// Synthesize a class which holds various utility methods that may be called from the IR
// rewriting. If any of these methods are not used, they will be removed by the Enqueuer.
List<DexEncodedMethod> requiredMethods = new ArrayList<>();
+ extraMethods.forEach(
+ (method, enumType) -> {
+ requiredMethods.add(synthesizeValueOfUtilityMethod(method, enumType));
+ });
+ requiredMethods.sort((m1, m2) -> m1.method.name.slowCompareTo(m2.method.name));
if (requiresOrdinalUtilityMethod) {
requiredMethods.add(synthesizeOrdinalMethod());
}
@@ -214,6 +255,21 @@
converter.optimizeSynthesizedClass(utilityClass, executorService);
}
+ private DexEncodedMethod synthesizeValueOfUtilityMethod(DexMethod method, DexType enumType) {
+ CfCode cfCode =
+ new EnumUnboxingCfCodeProvider.EnumUnboxingValueOfCfCodeProvider(
+ appView, utilityClassType, enumType, enumsToUnbox.getEnumValueInfoMap(enumType))
+ .generateCfCode();
+ return new DexEncodedMethod(
+ method,
+ synthesizedMethodAccessFlags(),
+ DexAnnotationSet.empty(),
+ ParameterAnnotationsList.empty(),
+ cfCode,
+ REQUIRED_CLASS_FILE_VERSION,
+ true);
+ }
+
// TODO(b/150178516): Add a test for this case.
private boolean utilityClassInMainDexList() {
for (DexType toUnbox : enumsToUnbox.enumSet()) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/EnumMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/EnumMethodOptimizer.java
new file mode 100644
index 0000000..99851fe
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/EnumMethodOptimizer.java
@@ -0,0 +1,69 @@
+// 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.library;
+
+import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.code.Assume;
+import com.android.tools.r8.ir.code.Assume.DynamicTypeAssumption;
+import com.android.tools.r8.ir.code.ConstClass;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.Value;
+import java.util.Set;
+
+public class EnumMethodOptimizer implements LibraryMethodModelCollection {
+ private final AppView<?> appView;
+
+ EnumMethodOptimizer(AppView<?> appView) {
+ this.appView = appView;
+ }
+
+ @Override
+ public DexType getType() {
+ return appView.dexItemFactory().enumType;
+ }
+
+ @Override
+ public void optimize(
+ IRCode code,
+ InstructionListIterator instructionIterator,
+ InvokeMethod invoke,
+ DexEncodedMethod singleTarget,
+ Set<Value> affectedValues) {
+ if (singleTarget.method == appView.dexItemFactory().enumMethods.valueOf
+ && invoke.inValues().get(0).isConstClass()) {
+ insertAssumeDynamicType(code, instructionIterator, invoke);
+ }
+ }
+
+ private void insertAssumeDynamicType(
+ IRCode code, InstructionListIterator instructionIterator, InvokeMethod invoke) {
+ // TODO(b/152516470): Support unboxing enums with Enum#valueOf in try-catch.
+ if (invoke.getBlock().hasCatchHandlers()) {
+ return;
+ }
+ ConstClass constClass = invoke.inValues().get(0).getConstInstruction().asConstClass();
+ TypeElement dynamicUpperBoundType =
+ TypeElement.fromDexType(constClass.getValue(), definitelyNotNull(), appView);
+ Value outValue = invoke.outValue();
+ // Replace usages of out-value by the out-value of the AssumeDynamicType instruction.
+ Value specializedOutValue = code.createValue(outValue.getType(), outValue.getLocalInfo());
+ outValue.replaceUsers(specializedOutValue);
+
+ // Insert AssumeDynamicType instruction.
+ Assume<DynamicTypeAssumption> assumeInstruction =
+ Assume.createAssumeDynamicTypeInstruction(
+ dynamicUpperBoundType, null, specializedOutValue, outValue, invoke, appView);
+ assumeInstruction.setPosition(appView.options().debug ? invoke.getPosition() : Position.none());
+ instructionIterator.add(assumeInstruction);
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodOptimizer.java
index 531807e..3b27a72 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodOptimizer.java
@@ -37,6 +37,7 @@
register(new ObjectMethodOptimizer(appView));
register(new ObjectsMethodOptimizer(appView));
register(new StringMethodOptimizer(appView));
+ register(new EnumMethodOptimizer(appView));
if (LogMethodOptimizer.isEnabled(appView)) {
register(new LogMethodOptimizer(appView));
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java b/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java
new file mode 100644
index 0000000..b1e1747
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java
@@ -0,0 +1,107 @@
+// 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.synthetic;
+
+import com.android.tools.r8.cf.code.CfConstNumber;
+import com.android.tools.r8.cf.code.CfConstString;
+import com.android.tools.r8.cf.code.CfIf;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.cf.code.CfLabel;
+import com.android.tools.r8.cf.code.CfLoad;
+import com.android.tools.r8.cf.code.CfNew;
+import com.android.tools.r8.cf.code.CfReturn;
+import com.android.tools.r8.cf.code.CfStackInstruction;
+import com.android.tools.r8.cf.code.CfStackInstruction.Opcode;
+import com.android.tools.r8.cf.code.CfThrow;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.EnumValueInfoMapCollection.EnumValueInfoMap;
+import com.android.tools.r8.ir.code.If;
+import com.android.tools.r8.ir.code.ValueType;
+import java.util.ArrayList;
+import java.util.List;
+import org.objectweb.asm.Opcodes;
+
+public abstract class EnumUnboxingCfCodeProvider extends SyntheticCfCodeProvider {
+
+ EnumUnboxingCfCodeProvider(AppView<?> appView, DexType holder) {
+ super(appView, holder);
+ }
+
+ public static class EnumUnboxingValueOfCfCodeProvider extends EnumUnboxingCfCodeProvider {
+
+ private DexType enumType;
+ private EnumValueInfoMap map;
+
+ public EnumUnboxingValueOfCfCodeProvider(
+ AppView<?> appView, DexType holder, DexType enumType, EnumValueInfoMap map) {
+ super(appView, holder);
+ this.enumType = enumType;
+ this.map = map;
+ }
+
+ @Override
+ public CfCode generateCfCode() {
+ // Generated static method, for class com.x.MyEnum {A,B} would look like:
+ // int UtilityClass#com.x.MyEnum_valueOf(String s) {
+ // if (s == null) { throw npe(“Name is null”); }
+ // if (s.equals(“A”)) { return 1;}
+ // if (s.equals(“B”)) { return 2;}
+ // throw new IllegalArgumentException(
+ // "No enum constant com.x.MyEnum." + s);
+ DexItemFactory factory = appView.dexItemFactory();
+ List<CfInstruction> instructions = new ArrayList<>();
+
+ // if (s == null) { throw npe(“Name is null”); }
+ CfLabel nullDest = new CfLabel();
+ instructions.add(new CfLoad(ValueType.fromDexType(factory.stringType), 0));
+ instructions.add(new CfIf(If.Type.NE, ValueType.OBJECT, nullDest));
+ instructions.add(new CfNew(factory.npeType));
+ instructions.add(new CfStackInstruction(Opcode.Dup));
+ instructions.add(new CfConstString(appView.dexItemFactory().createString("Name is null")));
+ instructions.add(
+ new CfInvoke(Opcodes.INVOKESPECIAL, factory.npeMethods.initWithMessage, false));
+ instructions.add(new CfThrow());
+ instructions.add(nullDest);
+
+ // if (s.equals(“A”)) { return 1;}
+ // if (s.equals(“B”)) { return 2;}
+ map.forEach(
+ (field, enumValueInfo) -> {
+ CfLabel dest = new CfLabel();
+ instructions.add(new CfLoad(ValueType.fromDexType(factory.stringType), 0));
+ instructions.add(new CfConstString(field.name));
+ instructions.add(
+ new CfInvoke(Opcodes.INVOKEVIRTUAL, factory.stringMethods.equals, false));
+ instructions.add(new CfIf(If.Type.EQ, ValueType.INT, dest));
+ instructions.add(new CfConstNumber(enumValueInfo.convertToInt(), ValueType.INT));
+ instructions.add(new CfReturn(ValueType.INT));
+ instructions.add(dest);
+ });
+
+ // throw new IllegalArgumentException("No enum constant com.x.MyEnum." + s);
+ instructions.add(new CfNew(factory.illegalArgumentExceptionType));
+ instructions.add(new CfStackInstruction(Opcode.Dup));
+ instructions.add(
+ new CfConstString(
+ appView
+ .dexItemFactory()
+ .createString(
+ "No enum constant " + enumType.toSourceString().replace('$', '.') + ".")));
+ instructions.add(new CfLoad(ValueType.fromDexType(factory.stringType), 0));
+ instructions.add(new CfInvoke(Opcodes.INVOKEVIRTUAL, factory.stringMethods.concat, false));
+ instructions.add(
+ new CfInvoke(
+ Opcodes.INVOKESPECIAL,
+ factory.illegalArgumentExceptionMethods.initWithMessage,
+ false));
+ instructions.add(new CfThrow());
+ return standardCfCodeFromInstructions(instructions);
+ }
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/ValueOfEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/ValueOfEnumUnboxingTest.java
new file mode 100644
index 0000000..b4e1ef6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/ValueOfEnumUnboxingTest.java
@@ -0,0 +1,94 @@
+// 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.enumunboxing;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestParameters;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ValueOfEnumUnboxingTest extends EnumUnboxingTestBase {
+
+ private static final Class<?>[] SUCCESSES = {
+ EnumValueOf.class,
+ };
+
+ private final TestParameters parameters;
+ private final boolean enumValueOptimization;
+ private final KeepRule enumKeepRules;
+
+ @Parameters(name = "{0} valueOpt: {1} keep: {2}")
+ public static List<Object[]> data() {
+ return enumUnboxingTestParameters();
+ }
+
+ public ValueOfEnumUnboxingTest(
+ TestParameters parameters, boolean enumValueOptimization, KeepRule enumKeepRules) {
+ this.parameters = parameters;
+ this.enumValueOptimization = enumValueOptimization;
+ this.enumKeepRules = enumKeepRules;
+ }
+
+ @Test
+ public void testEnumUnboxing() throws Exception {
+ R8TestCompileResult compile =
+ testForR8(parameters.getBackend())
+ .addInnerClasses(ValueOfEnumUnboxingTest.class)
+ .addKeepMainRules(SUCCESSES)
+ .enableNeverClassInliningAnnotations()
+ .addKeepRules(enumKeepRules.getKeepRule())
+ .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+ .allowDiagnosticInfoMessages()
+ .setMinApi(parameters.getApiLevel())
+ .compile();
+ for (Class<?> success : SUCCESSES) {
+ R8TestRunResult run =
+ compile
+ .inspectDiagnosticMessages(
+ m ->
+ assertEnumIsUnboxed(
+ success.getDeclaredClasses()[0], success.getSimpleName(), m))
+ .run(parameters.getRuntime(), success)
+ .assertSuccess();
+ assertLines2By2Correct(run.getStdOut());
+ }
+ }
+
+ static class EnumValueOf {
+
+ @NeverClassInline
+ enum MyEnum {
+ A,
+ B
+ }
+
+ public static void main(String[] args) {
+ System.out.println(Enum.valueOf(EnumValueOf.MyEnum.class, "A").ordinal());
+ System.out.println(0);
+ System.out.println(Enum.valueOf(EnumValueOf.MyEnum.class, "B").ordinal());
+ System.out.println(1);
+ try {
+ Enum.valueOf(EnumValueOf.MyEnum.class, "C");
+ } catch (IllegalArgumentException argException) {
+ System.out.println(argException.getMessage());
+ System.out.println(
+ "No enum constant"
+ + " com.android.tools.r8.enumunboxing.ValueOfEnumUnboxingTest.EnumValueOf.MyEnum.C");
+ }
+ try {
+ Enum.valueOf(EnumValueOf.MyEnum.class, null);
+ } catch (NullPointerException npe) {
+ System.out.println(npe.getMessage());
+ System.out.println("Name is null");
+ }
+ }
+ }
+}