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");
+      }
+    }
+  }
+}