Introduce EnumUnboxing analysis

Bug:147860220
Change-Id: I7f732157156065cab37e7c9ee2f73ec31b2a630a
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 1837037..435b746 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 trimName = createString("trim");
 
   public final DexString valueOfMethodName = createString("valueOf");
+  public final DexString valuesMethodName = createString("values");
   public final DexString toStringMethodName = createString("toString");
   public final DexString internMethodName = createString("intern");
 
@@ -778,6 +779,7 @@
     public final DexMethod ordinal;
     public final DexMethod name;
     public final DexMethod toString;
+    public final DexMethod compareTo;
 
     public final DexMethod constructor =
         createMethod(enumType, createProto(voidType, stringType, intType), constructorMethodName);
@@ -809,6 +811,29 @@
               toStringMethodName,
               stringDescriptor,
               DexString.EMPTY_ARRAY);
+      compareTo =
+          createMethod(
+              enumDescriptor,
+              compareToMethodName,
+              stringDescriptor,
+              new DexString[] {enumDescriptor});
+    }
+
+    public boolean isValuesMethod(DexMethod method, DexClass enumClass) {
+      assert enumClass.isEnum();
+      return method.holder == enumClass.type
+          && method.proto.returnType == enumClass.type.toArrayType(1, DexItemFactory.this)
+          && method.proto.parameters.size() == 0
+          && method.name == valuesMethodName;
+    }
+
+    public boolean isValueOfMethod(DexMethod method, DexClass enumClass) {
+      assert enumClass.isEnum();
+      return method.holder == enumClass.type
+          && method.proto.returnType == enumClass.type
+          && method.proto.parameters.size() == 1
+          && method.proto.parameters.values[0] == stringType
+          && method.name == valueOfMethodName;
     }
   }
 
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 51e7743..b3a62b7 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
@@ -56,6 +56,7 @@
 import com.android.tools.r8.ir.optimize.DeadCodeRemover;
 import com.android.tools.r8.ir.optimize.Devirtualizer;
 import com.android.tools.r8.ir.optimize.DynamicTypeOptimization;
+import com.android.tools.r8.ir.optimize.EnumUnboxer;
 import com.android.tools.r8.ir.optimize.IdempotentFunctionCallCanonicalizer;
 import com.android.tools.r8.ir.optimize.Inliner;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
@@ -156,6 +157,7 @@
   private final TypeChecker typeChecker;
   private final DesugaredLibraryAPIConverter desugaredLibraryAPIConverter;
   private final ServiceLoaderRewriter serviceLoaderRewriter;
+  private final EnumUnboxer enumUnboxer;
 
   // Assumers that will insert Assume instructions.
   public final Collection<Assumer> assumers = new ArrayList<>();
@@ -244,6 +246,7 @@
       this.stringSwitchRemover = null;
       this.serviceLoaderRewriter = null;
       this.methodOptimizationInfoCollector = null;
+      this.enumUnboxer = null;
       return;
     }
     this.lambdaRewriter =
@@ -316,12 +319,13 @@
           options.enableUninstantiatedTypeOptimization
               ? new UninstantiatedTypeOptimization(appViewWithLiveness)
               : null;
-      this.typeChecker = new TypeChecker(appView.withLiveness());
+      this.typeChecker = new TypeChecker(appViewWithLiveness);
       this.d8NestBasedAccessDesugaring = null;
       this.serviceLoaderRewriter =
           options.enableServiceLoaderRewriting
-              ? new ServiceLoaderRewriter(appView.withLiveness())
+              ? new ServiceLoaderRewriter(appViewWithLiveness)
               : null;
+      this.enumUnboxer = options.enableEnumUnboxing ? new EnumUnboxer(appViewWithLiveness) : null;
     } else {
       this.classInliner = null;
       this.classStaticizer = null;
@@ -342,6 +346,7 @@
           options.shouldDesugarNests() ? new D8NestBasedAccessDesugaring(appView) : null;
       this.serviceLoaderRewriter = null;
       this.methodOptimizationInfoCollector = null;
+      this.enumUnboxer = null;
     }
     this.stringSwitchRemover =
         options.isStringSwitchConversionEnabled()
@@ -661,6 +666,10 @@
       libraryMethodOverrideAnalysis.finish();
     }
 
+    if (enumUnboxer != null) {
+      enumUnboxer.finishEnumAnalysis();
+    }
+
     // Post processing:
     //   1) Second pass for methods whose collected call site information become more precise.
     //   2) Second inlining pass for dealing with double inline callers.
@@ -1388,6 +1397,10 @@
 
     previous = printMethod(code, "IR after interface method rewriting (SSA)", previous);
 
+    if (enumUnboxer != null && methodProcessor.isPost()) {
+      enumUnboxer.unboxEnums(code);
+    }
+
     // This pass has to be after interfaceMethodRewriter and BackportedMethodRewriter.
     if (desugaredLibraryAPIConverter != null
         && (!appView.enableWholeProgramOptimizations() || methodProcessor.isPrimary())) {
@@ -1464,6 +1477,10 @@
       timing.end();
     }
 
+    if (enumUnboxer != null && methodProcessor.isPrimary()) {
+      enumUnboxer.analyzeEnums(code);
+    }
+
     assert code.verifyTypes(appView);
 
     if (appView.enableWholeProgramOptimizations()) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java
index b2bf648..34609c9 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java
@@ -19,6 +19,10 @@
     return getPhase() == Phase.PRIMARY;
   }
 
+  default boolean isPost() {
+    return getPhase() == Phase.POST;
+  }
+
   default CallSiteInformation getCallSiteInformation() {
     return CallSiteInformation.empty();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/EnumUnboxer.java b/src/main/java/com/android/tools/r8/ir/optimize/EnumUnboxer.java
new file mode 100644
index 0000000..8e46dd3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/EnumUnboxer.java
@@ -0,0 +1,296 @@
+// 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;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+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.ir.analysis.type.ArrayTypeLatticeElement;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.AppInfoWithLiveness.EnumValueInfo;
+import com.android.tools.r8.utils.Reporter;
+import com.android.tools.r8.utils.StringDiagnostic;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class EnumUnboxer {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final Set<DexType> enumsToUnbox;
+
+  private final boolean debugLogEnabled;
+  private final Map<DexType, Reason> debugLogs;
+  private final DexItemFactory factory;
+
+  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;
+    }
+    enumsToUnbox = new EnumUnboxingCandidateAnalysis(appView, this).findCandidates();
+  }
+
+  public void unboxEnums(IRCode code) {
+    // TODO(b/147860220): To implement.
+    // Do not forget static get, which is implicitly valid (no inValue).
+  }
+
+  public void analyzeEnums(IRCode code) {
+    // Enum <clinit> and <init> are analyzed in between the two processing phases using optimization
+    // feedback.
+    DexClass dexClass = appView.definitionFor(code.method.method.holder);
+    if (dexClass.isEnum() && code.method.isInitializer()) {
+      return;
+    }
+    analyzeEnumsInMethod(code);
+  }
+
+  private void markEnumAsUnboxable(Reason reason, DexProgramClass enumClass) {
+    assert enumClass.isEnum();
+    reportFailure(enumClass.type, reason);
+    enumsToUnbox.remove(enumClass.type);
+  }
+
+  private DexProgramClass getEnumUnboxingCandidateOrNull(TypeLatticeElement lattice) {
+    if (lattice.isClassType()) {
+      DexType classType = lattice.asClassTypeLatticeElement().getClassType();
+      return getEnumUnboxingCandidateOrNull(classType);
+    }
+    if (lattice.isArrayType()) {
+      ArrayTypeLatticeElement arrayLattice = lattice.asArrayTypeLatticeElement();
+      if (arrayLattice.getArrayBaseTypeLattice().isClassType()) {
+        DexType classType =
+            arrayLattice.getArrayBaseTypeLattice().asClassTypeLatticeElement().getClassType();
+        return getEnumUnboxingCandidateOrNull(classType);
+      }
+    }
+    return null;
+  }
+
+  private DexProgramClass getEnumUnboxingCandidateOrNull(DexType anyType) {
+    if (!enumsToUnbox.contains(anyType)) {
+      return null;
+    }
+    return appView.definitionForProgramType(anyType);
+  }
+
+  private void analyzeEnumsInMethod(IRCode code) {
+    for (BasicBlock block : code.blocks) {
+      for (Instruction instruction : block.getInstructions()) {
+        Value outValue = instruction.outValue();
+        DexProgramClass enumClass =
+            outValue == null ? null : getEnumUnboxingCandidateOrNull(outValue.getTypeLattice());
+        if (enumClass != null) {
+          validateEnumUsages(code, outValue.uniqueUsers(), outValue.uniquePhiUsers(), enumClass);
+        }
+      }
+      for (Phi phi : block.getPhis()) {
+        DexProgramClass enumClass = getEnumUnboxingCandidateOrNull(phi.getTypeLattice());
+        if (enumClass != null) {
+          validateEnumUsages(code, phi.uniqueUsers(), phi.uniquePhiUsers(), enumClass);
+        }
+      }
+    }
+  }
+
+  private Reason validateEnumUsages(
+      IRCode code, Set<Instruction> uses, Set<Phi> phiUses, DexProgramClass enumClass) {
+    for (Instruction user : uses) {
+      Reason reason = instructionAllowEnumUnboxing(user, code, enumClass);
+      if (reason != Reason.ELIGIBLE) {
+        markEnumAsUnboxable(reason, enumClass);
+        return reason;
+      }
+    }
+    for (Phi phi : phiUses) {
+      for (Value operand : phi.getOperands()) {
+        if (getEnumUnboxingCandidateOrNull(operand.getTypeLattice()) != enumClass) {
+          markEnumAsUnboxable(Reason.INVALID_PHI, enumClass);
+          return Reason.INVALID_PHI;
+        }
+      }
+    }
+    return Reason.ELIGIBLE;
+  }
+
+  public void finishEnumAnalysis() {
+    for (DexType toUnbox : enumsToUnbox) {
+      DexProgramClass enumClass = appView.definitionForProgramType(toUnbox);
+      assert enumClass != null;
+
+      DexEncodedMethod initializer = enumClass.lookupDirectMethod(factory.enumMethods.constructor);
+      assert initializer != null;
+      if (initializer.getOptimizationInfo().mayHaveSideEffects()) {
+        markEnumAsUnboxable(Reason.INVALID_INIT, enumClass);
+        continue;
+      }
+
+      if (enumClass.classInitializationMayHaveSideEffects(appView)) {
+        markEnumAsUnboxable(Reason.INVALID_CLINIT, enumClass);
+        continue;
+      }
+
+      Map<DexField, EnumValueInfo> enumValueInfoMapFor =
+          appView.appInfo().withLiveness().getEnumValueInfoMapFor(enumClass.type);
+      if (enumValueInfoMapFor == null) {
+        markEnumAsUnboxable(Reason.MISSING_INFO_MAP, enumClass);
+        continue;
+      }
+      if (enumValueInfoMapFor.size() != enumClass.staticFields().size() - 1) {
+        markEnumAsUnboxable(Reason.UNEXPECTED_STATIC_FIELD, enumClass);
+      }
+    }
+    if (debugLogEnabled) {
+      reportEnumsAnalysis();
+    }
+  }
+
+  private Reason instructionAllowEnumUnboxing(
+      Instruction instruction, IRCode code, DexProgramClass enumClass) {
+
+    // All invokes in the library are invalid, besides a few cherry picked cases such as ordinal().
+    if (instruction.isInvokeMethod()) {
+      InvokeMethod invokeMethod = instruction.asInvokeMethod();
+      if (invokeMethod.getInvokedMethod().holder.isArrayType()) {
+        // The only valid methods is clone for values() to be correct.
+        if (invokeMethod.getInvokedMethod().name == factory.cloneMethodName) {
+          return Reason.ELIGIBLE;
+        }
+        return Reason.INVALID_INVOKE_ON_ARRAY;
+      }
+      DexEncodedMethod invokedEncodedMethod =
+          invokeMethod.lookupSingleTarget(appView, code.method.method.holder);
+      if (invokedEncodedMethod == null) {
+        return Reason.INVALID_INVOKE;
+      }
+      DexMethod invokedMethod = invokedEncodedMethod.method;
+      DexClass dexClass = appView.definitionFor(invokedMethod.holder);
+      if (dexClass == null) {
+        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(invokedMethod, dexClass)) {
+          return Reason.VALUES_INVOKE;
+        }
+        if (dexClass.isEnum() && factory.enumMethods.isValueOfMethod(invokedMethod, dexClass)) {
+          return Reason.VALUE_OF_INVOKE;
+        }
+        return Reason.ELIGIBLE;
+      }
+      if (dexClass.isClasspathClass()) {
+        return Reason.INVALID_INVOKE;
+      }
+      assert dexClass.isLibraryClass();
+      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().
+      if (debugLogEnabled) {
+        if (invokedMethod == factory.enumMethods.compareTo) {
+          return Reason.COMPARE_TO_INVOKE;
+        }
+        if (invokedMethod == factory.enumMethods.name) {
+          return Reason.NAME_INVOKE;
+        }
+        if (invokedMethod == factory.enumMethods.toString) {
+          return Reason.TO_STRING_INVOKE;
+        }
+      }
+      if (invokedMethod != factory.enumMethods.ordinal) {
+        return Reason.UNSUPPORTED_LIBRARY_CALL;
+      }
+      return Reason.ELIGIBLE;
+    }
+
+    if (instruction.isAssume()) {
+      Value outValue = instruction.outValue();
+      return validateEnumUsages(code, outValue.uniqueUsers(), outValue.uniquePhiUsers(), enumClass);
+    }
+
+    // Return is used for valueOf methods.
+    if (instruction.isReturn()) {
+      DexType returnType = code.method.method.proto.returnType;
+      if (returnType != enumClass.type && returnType.toBaseType(factory) != enumClass.type) {
+        return Reason.IMPLICIT_UP_CAST_IN_RETURN;
+      }
+      return Reason.ELIGIBLE;
+    }
+
+    return Reason.OTHER_UNSUPPORTED_INSTRUCTION;
+  }
+
+  private void reportEnumsAnalysis() {
+    assert debugLogEnabled;
+    Reporter reporter = appView.options().reporter;
+    reporter.info(
+        new StringDiagnostic(
+            "Unboxed enums (Unboxing succeeded "
+                + enumsToUnbox.size()
+                + "): "
+                + Arrays.toString(enumsToUnbox.toArray())));
+    StringBuilder sb = new StringBuilder();
+    sb.append("Boxed enums (Unboxing failed ").append(debugLogs.size()).append("):\n");
+    for (DexType enumType : debugLogs.keySet()) {
+      sb.append("- ")
+          .append(enumType)
+          .append(": ")
+          .append(debugLogs.get(enumType).toString())
+          .append('\n');
+    }
+    reporter.info(new StringDiagnostic(sb.toString()));
+  }
+
+  void reportFailure(DexType enumType, Reason reason) {
+    if (debugLogEnabled) {
+      debugLogs.put(enumType, reason);
+    }
+  }
+
+  public enum Reason {
+    ELIGIBLE,
+    SUBTYPES,
+    INTERFACE,
+    INSTANCE_FIELD,
+    UNEXPECTED_STATIC_FIELD,
+    VIRTUAL_METHOD,
+    UNEXPECTED_DIRECT_METHOD,
+    INVALID_PHI,
+    INVALID_INIT,
+    INVALID_CLINIT,
+    INVALID_INVOKE,
+    INVALID_INVOKE_ON_ARRAY,
+    IMPLICIT_UP_CAST_IN_RETURN,
+    VALUE_OF_INVOKE,
+    VALUES_INVOKE,
+    COMPARE_TO_INVOKE,
+    TO_STRING_INVOKE,
+    NAME_INVOKE,
+    UNSUPPORTED_LIBRARY_CALL,
+    MISSING_INFO_MAP,
+    OTHER_UNSUPPORTED_INSTRUCTION;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/EnumUnboxingCandidateAnalysis.java b/src/main/java/com/android/tools/r8/ir/optimize/EnumUnboxingCandidateAnalysis.java
new file mode 100644
index 0000000..3fb84e1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/EnumUnboxingCandidateAnalysis.java
@@ -0,0 +1,114 @@
+// 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;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.optimize.EnumUnboxer.Reason;
+import com.google.common.collect.Sets;
+import java.util.Set;
+
+class EnumUnboxingCandidateAnalysis {
+
+  private final AppView<?> appView;
+  private final EnumUnboxer enumUnboxer;
+  private final DexItemFactory factory;
+
+  EnumUnboxingCandidateAnalysis(AppView<?> appView, EnumUnboxer enumUnboxer) {
+    this.appView = appView;
+    this.enumUnboxer = enumUnboxer;
+    factory = appView.dexItemFactory();
+  }
+
+  Set<DexType> findCandidates() {
+    Set<DexType> enums = Sets.newConcurrentHashSet();
+    for (DexProgramClass clazz : appView.appInfo().classes()) {
+      if (isEnumUnboxingCandidate(clazz)) {
+        enums.add(clazz.type);
+      }
+    }
+    return enums;
+  }
+
+  private boolean isEnumUnboxingCandidate(DexProgramClass clazz) {
+    if (!clazz.isEnum()) {
+      return false;
+    }
+    if (!clazz.isEffectivelyFinal(appView)) {
+      enumUnboxer.reportFailure(clazz.type, Reason.SUBTYPES);
+      return false;
+    }
+    // TODO(b/147860220): interfaces without default methods should be acceptable if the build setup
+    // is correct (all abstract methods are implemented).
+    if (!clazz.interfaces.isEmpty()) {
+      enumUnboxer.reportFailure(clazz.type, Reason.INTERFACE);
+      return false;
+    }
+    if (!clazz.instanceFields().isEmpty()) {
+      enumUnboxer.reportFailure(clazz.type, Reason.INSTANCE_FIELD);
+      return false;
+    }
+    if (!enumHasBasicStaticFields(clazz)) {
+      enumUnboxer.reportFailure(clazz.type, Reason.UNEXPECTED_STATIC_FIELD);
+      return false;
+    }
+    if (!clazz.virtualMethods().isEmpty()) {
+      enumUnboxer.reportFailure(clazz.type, Reason.VIRTUAL_METHOD);
+      return false;
+    }
+    // Methods values, valueOf, init, clinit are present on each enum.
+    // Methods init and clinit are required if the enum is used.
+    // Methods valueOf and values are normally kept by the commonly used/recommended enum keep rule
+    // -keepclassmembers,allowoptimization enum * {
+    //     public static **[] values();
+    //     public static ** valueOf(java.lang.String);
+    // }
+    // In general there will be 4 methods, unless the enum keep rule is not present.
+    if (clazz.directMethods().size() > 4) {
+      enumUnboxer.reportFailure(clazz.type, Reason.UNEXPECTED_DIRECT_METHOD);
+      return false;
+    }
+    for (DexEncodedMethod directMethod : clazz.directMethods()) {
+      if (!(factory.enumMethods.isValuesMethod(directMethod.method, clazz)
+          || factory.enumMethods.isValueOfMethod(directMethod.method, clazz)
+          || isStandardEnumInitializer(directMethod)
+          || directMethod.isClassInitializer())) {
+        enumUnboxer.reportFailure(clazz.type, Reason.UNEXPECTED_DIRECT_METHOD);
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private boolean isStandardEnumInitializer(DexEncodedMethod method) {
+    return method.isInstanceInitializer()
+        && method.method.proto == factory.enumMethods.constructor.proto;
+  }
+
+  // The enum should have the $VALUES static field and only fields directly referencing the enum
+  // instances.
+  private boolean enumHasBasicStaticFields(DexProgramClass clazz) {
+    for (DexEncodedField staticField : clazz.staticFields()) {
+      if (staticField.field.type == clazz.type
+          && staticField.accessFlags.isEnum()
+          && staticField.accessFlags.isFinal()) {
+        // Enum field, valid, do nothing.
+      } else if (staticField.field.type.isArrayType()
+          && staticField.field.type.toArrayElementType(factory) == clazz.type
+          && staticField.accessFlags.isSynthetic()
+          && staticField.accessFlags.isFinal()
+          && staticField.field.name == factory.enumValuesFieldName) {
+        // Field $VALUES, valid, do nothing.
+      } else {
+        return false;
+      }
+    }
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 22df0a3..9eb8b6c 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -204,6 +204,7 @@
   public boolean enableInlining =
       !Version.isDevelopmentVersion()
           || System.getProperty("com.android.tools.r8.disableinlining") == null;
+  public boolean enableEnumUnboxing = false;
   // TODO(b/141451716): Evaluate the effect of allowing inlining in the inlinee.
   public boolean applyInliningToInlinee =
       System.getProperty("com.android.tools.r8.applyInliningToInlinee") != null;
@@ -1010,6 +1011,7 @@
     public boolean enableCheckCastAndInstanceOfRemoval = true;
     public boolean enableDeadSwitchCaseElimination = true;
     public boolean enableSwitchToIfRewriting = true;
+    public boolean enableEnumUnboxingDebugLogs = false;
     public boolean forceRedundantConstNumberRemoval = false;
     public boolean forceAssumeNoneInsertion = false;
     public boolean invertConditionals = false;
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
new file mode 100644
index 0000000..332ad2e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
@@ -0,0 +1,66 @@
+// 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 static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertTrue;
+
+import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.ToolHelper.DexVm;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.List;
+
+public class EnumUnboxingTestBase extends TestBase {
+
+  static final String KEEP_ENUM =
+      "-keepclassmembers enum * { public static **[] values(); public static **"
+          + " valueOf(java.lang.String); }";
+
+  public void assertLines2By2Correct(String string) {
+    List<String> lines = StringUtils.splitLines(string);
+    assert lines.size() % 2 == 0;
+    for (int i = 0; i < lines.size(); i += 2) {
+      assertEquals(
+          "Different lines: " + lines.get(i) + " || " + lines.get(i + 1) + "\n" + string,
+          lines.get(i),
+          lines.get(i + 1));
+    }
+  }
+
+  void enableEnumOptions(InternalOptions options) {
+    options.enableEnumUnboxing = true;
+    options.testing.enableEnumUnboxingDebugLogs = true;
+  }
+
+  void assertEnumIsUnboxed(Class<?> enumClass, String testName, TestDiagnosticMessages m) {
+    Diagnostic diagnostic = m.getInfos().get(0);
+    assertTrue(diagnostic.getDiagnosticMessage().startsWith("Unboxed enums"));
+    assertTrue(
+        "Expected enum to be removed (" + testName + "): ",
+        diagnostic.getDiagnosticMessage().contains(enumClass.getSimpleName()));
+  }
+
+  void assertEnumIsBoxed(Class<?> enumClass, String testName, TestDiagnosticMessages m) {
+    Diagnostic diagnostic = m.getInfos().get(1);
+    assertTrue(diagnostic.getDiagnosticMessage().startsWith("Boxed enums"));
+    assertTrue(
+        "Expected enum NOT to be removed (" + testName + "): ",
+        diagnostic.getDiagnosticMessage().contains(enumClass.getSimpleName()));
+  }
+
+  static TestParametersCollection enumUnboxingTestParameters() {
+    return getTestParameters()
+        .withCfRuntime(CfVm.JDK9)
+        .withDexRuntime(DexVm.Version.first())
+        .withDexRuntime(DexVm.Version.last())
+        .withAllApiLevels()
+        .build();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/FailingEnumUnboxingAnalysisTest.java b/src/test/java/com/android/tools/r8/enumunboxing/FailingEnumUnboxingAnalysisTest.java
new file mode 100644
index 0000000..247e8a1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/FailingEnumUnboxingAnalysisTest.java
@@ -0,0 +1,191 @@
+// 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 static junit.framework.TestCase.assertTrue;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.enumunboxing.FailingEnumUnboxingAnalysisTest.EnumInstanceFieldMain.EnumInstanceField;
+import com.android.tools.r8.enumunboxing.FailingEnumUnboxingAnalysisTest.EnumInterfaceMain.EnumInterface;
+import com.android.tools.r8.enumunboxing.FailingEnumUnboxingAnalysisTest.EnumStaticFieldMain.EnumStaticField;
+import com.android.tools.r8.enumunboxing.FailingEnumUnboxingAnalysisTest.EnumStaticMethodMain.EnumStaticMethod;
+import com.android.tools.r8.enumunboxing.FailingEnumUnboxingAnalysisTest.EnumVirtualMethodMain.EnumVirtualMethod;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 FailingEnumUnboxingAnalysisTest extends EnumUnboxingTestBase {
+
+  private static final Class<?>[] FAILURES = {
+    EnumInterface.class,
+    EnumStaticField.class,
+    EnumInstanceField.class,
+    EnumStaticMethod.class,
+    EnumVirtualMethod.class
+  };
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public FailingEnumUnboxingAnalysisTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testEnumUnboxingFailure() throws Exception {
+    R8FullTestBuilder r8FullTestBuilder =
+        testForR8(parameters.getBackend()).addInnerClasses(FailingEnumUnboxingAnalysisTest.class);
+    for (Class<?> failure : FAILURES) {
+      r8FullTestBuilder.addKeepMainRule(failure.getEnclosingClass());
+    }
+    R8TestCompileResult compile =
+        r8FullTestBuilder
+            .noTreeShaking() // Disabled to avoid merging Itf into EnumInterface.
+            .enableInliningAnnotations()
+            .addKeepRules(KEEP_ENUM)
+            .addOptionsModification(this::enableEnumOptions)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspect(this::assertEnumsAsExpected);
+    for (Class<?> failure : FAILURES) {
+      R8TestRunResult run =
+          compile
+              .inspectDiagnosticMessages(
+                  m -> assertEnumIsBoxed(failure, failure.getSimpleName(), m))
+              .run(parameters.getRuntime(), failure.getEnclosingClass())
+              .assertSuccess();
+      assertLines2By2Correct(run.getStdOut());
+    }
+  }
+
+  private void assertEnumsAsExpected(CodeInspector inspector) {
+    assertEquals(1, inspector.clazz(EnumInterface.class).getDexClass().interfaces.size());
+
+    assertTrue(inspector.clazz(EnumStaticField.class).uniqueFieldWithName("X").isPresent());
+    assertTrue(inspector.clazz(EnumInstanceField.class).uniqueFieldWithName("a").isPresent());
+
+    assertEquals(5, inspector.clazz(EnumStaticMethod.class).getDexClass().directMethods().size());
+    assertEquals(1, inspector.clazz(EnumVirtualMethod.class).virtualMethods().size());
+  }
+
+  static class EnumInterfaceMain {
+
+    public static void main(String[] args) {
+      System.out.println(EnumInterface.A.ordinal());
+      System.out.println(0);
+    }
+
+    enum EnumInterface implements Itf {
+      A,
+      B,
+      C
+    }
+
+    interface Itf {
+
+      default int ordinal() {
+        return -1;
+      }
+    }
+  }
+
+  static class EnumStaticFieldMain {
+
+    public static void main(String[] args) {
+      System.out.println(EnumStaticField.A.ordinal());
+      System.out.println(0);
+      System.out.println(EnumStaticField.X.ordinal());
+      System.out.println(0);
+    }
+
+    enum EnumStaticField {
+      A,
+      B,
+      C;
+      static EnumStaticField X = A;
+    }
+  }
+
+  static class EnumInstanceFieldMain {
+
+    enum EnumInstanceField {
+      A(10),
+      B(20),
+      C(30);
+      private int a;
+
+      EnumInstanceField(int i) {
+        this.a = i;
+      }
+    }
+
+    public static void main(String[] args) {
+      System.out.println(EnumInstanceField.A.ordinal());
+      System.out.println(0);
+      System.out.println(EnumInstanceField.A.a);
+      System.out.println(10);
+    }
+  }
+
+  static class EnumStaticMethodMain {
+
+    enum EnumStaticMethod {
+      A,
+      B,
+      C;
+
+      // Enum cannot be unboxed if it has a static method, we do not inline so the method is
+      // present.
+      @NeverInline
+      static int foo() {
+        return Math.addExact(-1, 0);
+      }
+    }
+
+    public static void main(String[] args) {
+      System.out.println(EnumStaticMethod.A.ordinal());
+      System.out.println(0);
+      System.out.println(EnumStaticMethod.foo());
+      System.out.println(-1);
+    }
+  }
+
+  static class EnumVirtualMethodMain {
+
+    public static void main(String[] args) {
+      EnumVirtualMethod e1 = EnumVirtualMethod.A;
+      System.out.println(e1.ordinal());
+      System.out.println(0);
+      System.out.println(e1.valueOf());
+      System.out.println(-1);
+    }
+
+    enum EnumVirtualMethod {
+      A,
+      B,
+      C;
+
+      // Enum cannot be unboxed if it has a virtual method, we do not inline so the method is
+      // present.
+      @NeverInline
+      int valueOf() {
+        return Math.addExact(-1, 0);
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/FailingMethodEnumUnboxingAnalysisTest.java b/src/test/java/com/android/tools/r8/enumunboxing/FailingMethodEnumUnboxingAnalysisTest.java
new file mode 100644
index 0000000..57bbc5c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/FailingMethodEnumUnboxingAnalysisTest.java
@@ -0,0 +1,238 @@
+// 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 static junit.framework.TestCase.assertTrue;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.EnumSet;
+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 FailingMethodEnumUnboxingAnalysisTest extends EnumUnboxingTestBase {
+
+  private static final Class<?>[] FAILURES = {
+    NullCheck.class,
+    Check.class,
+    FieldPut.class,
+    FieldPutObject.class,
+    ToString.class,
+    EnumSetTest.class,
+    FailingPhi.class
+  };
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public FailingMethodEnumUnboxingAnalysisTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testEnumUnboxingFailure() throws Exception {
+    R8FullTestBuilder r8FullTestBuilder =
+        testForR8(parameters.getBackend())
+            .addInnerClasses(FailingMethodEnumUnboxingAnalysisTest.class);
+    for (Class<?> failure : FAILURES) {
+      r8FullTestBuilder.addKeepMainRule(failure);
+    }
+    R8TestCompileResult compile =
+        r8FullTestBuilder
+            .addKeepRules(KEEP_ENUM)
+            .addOptionsModification(this::enableEnumOptions)
+            .enableInliningAnnotations()
+            .addOptionsModification(
+                // Disabled to avoid toString() being removed.
+                opt -> opt.enableEnumValueOptimization = false)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspect(this::assertEnumsAsExpected);
+    for (Class<?> failure : FAILURES) {
+      R8TestRunResult run =
+          compile
+              .inspectDiagnosticMessages(
+                  m ->
+                      assertEnumIsBoxed(
+                          failure.getDeclaredClasses()[0], failure.getSimpleName(), m))
+              .run(parameters.getRuntime(), failure)
+              .assertSuccess();
+      assertLines2By2Correct(run.getStdOut());
+    }
+  }
+
+  private void assertEnumsAsExpected(CodeInspector inspector) {
+    // Check all as expected (else we test nothing)
+    assertTrue(inspector.clazz(NullCheck.class).uniqueMethodWithName("nullCheck").isPresent());
+    assertTrue(inspector.clazz(Check.class).uniqueMethodWithName("check").isPresent());
+
+    assertEquals(1, inspector.clazz(FieldPut.class).allInstanceFields().size());
+    assertEquals(1, inspector.clazz(FieldPutObject.class).allInstanceFields().size());
+
+    assertTrue(inspector.clazz(FailingPhi.class).uniqueMethodWithName("switchOn").isPresent());
+  }
+
+  @SuppressWarnings("ConstantConditions")
+  static class NullCheck {
+
+    enum MyEnum {
+      A,
+      B,
+      C
+    }
+
+    public static void main(String[] args) {
+      System.out.println(nullCheck(MyEnum.A));
+      System.out.println(false);
+      System.out.println(nullCheck(null));
+      System.out.println(true);
+    }
+
+    // Do not resolve the == with constants after inlining.
+    @NeverInline
+    static boolean nullCheck(MyEnum e) {
+      return e == null;
+    }
+  }
+
+  static class Check {
+
+    enum MyEnum {
+      A,
+      B,
+      C
+    }
+
+    public static void main(String[] args) {
+      MyEnum e1 = MyEnum.A;
+      System.out.println(check(e1));
+      System.out.println(false);
+    }
+
+    // Do not resolve the == with constants after inlining.
+    @NeverInline
+    static boolean check(MyEnum e) {
+      return e == MyEnum.B;
+    }
+  }
+
+  static class FieldPut {
+
+    enum MyEnum {
+      A,
+      B,
+      C
+    }
+
+    MyEnum e;
+
+    public static void main(String[] args) {
+      FieldPut fieldPut = new FieldPut();
+      fieldPut.setA();
+      System.out.println(fieldPut.e.ordinal());
+      System.out.println(0);
+    }
+
+    void setA() {
+      e = MyEnum.A;
+    }
+  }
+
+  static class FieldPutObject {
+
+    enum MyEnum {
+      A,
+      B,
+      C
+    }
+
+    Object e;
+
+    public static void main(String[] args) {
+      FieldPutObject fieldPut = new FieldPutObject();
+      fieldPut.setA();
+      Object obj = new Object();
+      fieldPut.e = obj;
+      System.out.println(fieldPut.e);
+      System.out.println(obj);
+    }
+
+    void setA() {
+      e = MyEnum.A;
+    }
+  }
+
+  static class ToString {
+
+    enum MyEnum {
+      A,
+      B,
+      C
+    }
+
+    public static void main(String[] args) {
+      MyEnum e1 = MyEnum.A;
+      System.out.println(e1.toString());
+      System.out.println("A");
+    }
+  }
+
+  static class EnumSetTest {
+
+    enum MyEnum {
+      A,
+      B,
+      C
+    }
+
+    public static void main(String[] args) {
+      EnumSet<MyEnum> es = EnumSet.allOf(MyEnum.class);
+      System.out.println(es.size());
+      System.out.println("3");
+    }
+  }
+
+  static class FailingPhi {
+
+    enum MyEnum {
+      A,
+      B,
+      C
+    }
+
+    public static void main(String[] args) {
+      System.out.println(switchOn(1));
+      System.out.println("B");
+      System.out.println(switchOn(2));
+      System.out.println("class java.lang.Object");
+    }
+
+    // Avoid removing the switch entirely.
+    @NeverInline
+    static Object switchOn(int i) {
+      switch (i) {
+        case 0:
+          return MyEnum.A;
+        case 1:
+          return MyEnum.B;
+        default:
+          return Object.class;
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/OrdinalEnumUnboxingAnalysisTest.java b/src/test/java/com/android/tools/r8/enumunboxing/OrdinalEnumUnboxingAnalysisTest.java
new file mode 100644
index 0000000..079c9f7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/OrdinalEnumUnboxingAnalysisTest.java
@@ -0,0 +1,62 @@
+// 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.R8TestRunResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+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 OrdinalEnumUnboxingAnalysisTest extends EnumUnboxingTestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public OrdinalEnumUnboxingAnalysisTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  private static final Class<?> ENUM_CLASS = MyEnum.class;
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    Class<Ordinal> classToTest = Ordinal.class;
+    R8TestRunResult run =
+        testForR8(parameters.getBackend())
+            .addProgramClasses(classToTest, ENUM_CLASS)
+            .addKeepMainRule(classToTest)
+            .addKeepRules(KEEP_ENUM)
+            .addOptionsModification(this::enableEnumOptions)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspectDiagnosticMessages(
+                m -> assertEnumIsUnboxed(ENUM_CLASS, classToTest.getSimpleName(), m))
+            .run(parameters.getRuntime(), classToTest)
+            .assertSuccess();
+    assertLines2By2Correct(run.getStdOut());
+  }
+
+  enum MyEnum {
+    A,
+    B,
+    C
+  }
+
+  static class Ordinal {
+
+    public static void main(String[] args) {
+      System.out.println(MyEnum.A.ordinal());
+      System.out.println(0);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/PhiEnumUnboxingAnalysisTest.java b/src/test/java/com/android/tools/r8/enumunboxing/PhiEnumUnboxingAnalysisTest.java
new file mode 100644
index 0000000..87cacd5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/PhiEnumUnboxingAnalysisTest.java
@@ -0,0 +1,79 @@
+// 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.NeverInline;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+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 PhiEnumUnboxingAnalysisTest extends EnumUnboxingTestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public PhiEnumUnboxingAnalysisTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  private static final Class<?> ENUM_CLASS = MyEnum.class;
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    Class<?> classToTest = Phi.class;
+    R8TestRunResult run =
+        testForR8(parameters.getBackend())
+            .addProgramClasses(classToTest, ENUM_CLASS)
+            .addKeepMainRule(classToTest)
+            .addKeepRules(KEEP_ENUM)
+            .enableInliningAnnotations()
+            .addOptionsModification(this::enableEnumOptions)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspectDiagnosticMessages(
+                m -> assertEnumIsUnboxed(ENUM_CLASS, classToTest.getSimpleName(), m))
+            .run(parameters.getRuntime(), classToTest)
+            .assertSuccess();
+    assertLines2By2Correct(run.getStdOut());
+  }
+
+  enum MyEnum {
+    A,
+    B,
+    C
+  }
+
+  static class Phi {
+
+    public static void main(String[] args) {
+      System.out.println(switchOn(1).ordinal());
+      System.out.println(1);
+      System.out.println(switchOn(2).ordinal());
+      System.out.println(2);
+    }
+
+    // Avoid removing the switch entirely.
+    @NeverInline
+    static MyEnum switchOn(int i) {
+      switch (i) {
+        case 0:
+          return MyEnum.A;
+        case 1:
+          return MyEnum.B;
+        default:
+          return MyEnum.C;
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/SwitchEnumUnboxingAnalysisTest.java b/src/test/java/com/android/tools/r8/enumunboxing/SwitchEnumUnboxingAnalysisTest.java
new file mode 100644
index 0000000..699c72d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/SwitchEnumUnboxingAnalysisTest.java
@@ -0,0 +1,79 @@
+// 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.NeverInline;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+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 SwitchEnumUnboxingAnalysisTest extends EnumUnboxingTestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public SwitchEnumUnboxingAnalysisTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  private static final Class<?> ENUM_CLASS = MyEnum.class;
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    Class<Switch> classToTest = Switch.class;
+    R8TestRunResult run =
+        testForR8(parameters.getBackend())
+            .addInnerClasses(SwitchEnumUnboxingAnalysisTest.class)
+            .addKeepMainRule(classToTest)
+            .addKeepRules(KEEP_ENUM)
+            .enableInliningAnnotations()
+            .addOptionsModification(this::enableEnumOptions)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspectDiagnosticMessages(
+                m -> assertEnumIsBoxed(ENUM_CLASS, classToTest.getSimpleName(), m))
+            .run(parameters.getRuntime(), classToTest)
+            .assertSuccess();
+    assertLines2By2Correct(run.getStdOut());
+  }
+
+  enum MyEnum {
+    A,
+    B,
+    C
+  }
+
+  static class Switch {
+
+    public static void main(String[] args) {
+      System.out.println(switchOnEnum(MyEnum.A));
+      System.out.println(0xC0FFEE);
+      System.out.println(switchOnEnum(MyEnum.B));
+      System.out.println(0xBABE);
+    }
+
+    // Avoid removing the switch entirely.
+    @NeverInline
+    static int switchOnEnum(MyEnum e) {
+      switch (e) {
+        case A:
+          return 0xC0FFEE;
+        case B:
+          return 0xBABE;
+        default:
+          return 0xDEADBEEF;
+      }
+    }
+  }
+}