Merging enum with subtypes

Change-Id: Iacfe18165347007b9f94dcbeba6ea2c21c3f29b0
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index 5ebfb20..7a7b8d6 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -771,7 +771,7 @@
   }
 
   public boolean validateUnboxedEnumsHaveBeenPruned() {
-    for (DexType unboxedEnum : unboxedEnums.getUnboxedEnums()) {
+    for (DexType unboxedEnum : unboxedEnums.computeAllUnboxedEnums()) {
       assert appInfo.definitionForWithoutExistenceAssert(unboxedEnum) == null
           : "Enum " + unboxedEnum + " has been unboxed but is still in the program.";
       assert appInfo().withLiveness().wasPruned(unboxedEnum)
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 4d6f31a..cfa8347 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -1992,8 +1992,17 @@
     }
 
     public boolean isEnumField(DexEncodedField staticField, DexType enumType) {
+      return isEnumField(staticField, enumType, ImmutableSet.of());
+    }
+
+    // In some case, the enum field may be respecialized to an enum subtype. In this case, one
+    // can pass the encoded field as well as the field with the super enum type for the checks.
+    public boolean isEnumField(
+        DexEncodedField staticField, DexType enumType, Set<DexType> subtypes) {
       assert staticField.isStatic();
-      return staticField.getType() == enumType && staticField.isEnum() && staticField.isFinal();
+      return (staticField.getType() == enumType || subtypes.contains(staticField.getType()))
+          && staticField.isEnum()
+          && staticField.isFinal();
     }
 
     public boolean isValuesFieldCandidate(DexEncodedField staticField, DexType enumType) {
diff --git a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
index 45936b7..97dfaf5 100644
--- a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
+++ b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
@@ -482,7 +482,7 @@
           (clazz, allocationSitesForClass) -> {
             DexType type = lens.lookupType(clazz.type);
             if (type.isPrimitiveType()) {
-              assert !objectAllocationInfos.hasInstantiatedStrictSubtype(clazz);
+              assert clazz.isEnum();
               return;
             }
             DexProgramClass rewrittenClass = asProgramClassOrNull(definitions.definitionFor(type));
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValues.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValues.java
index e57052f..0f40f03 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValues.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValues.java
@@ -4,12 +4,15 @@
 
 package com.android.tools.r8.ir.analysis.fieldvalueanalysis;
 
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.objectstate.ObjectState;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.google.common.collect.ImmutableMap;
 
 public abstract class StaticFieldValues {
@@ -47,6 +50,16 @@
       return new Builder();
     }
 
+    public EnumStaticFieldValues rewrittenWithLens(
+        AppView<AppInfoWithLiveness> appView, GraphLens lens, GraphLens codeLens) {
+      ImmutableMap.Builder<DexField, ObjectState> builder = ImmutableMap.builder();
+      enumAbstractValues.forEach(
+          (field, state) ->
+              builder.put(
+                  lens.lookupField(field), state.rewrittenWithLens(appView, lens, codeLens)));
+      return new EnumStaticFieldValues(builder.build());
+    }
+
     public static class Builder extends StaticFieldValues.Builder {
       private final ImmutableMap.Builder<DexField, ObjectState> enumObjectStateBuilder =
           ImmutableMap.builder();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumDataMap.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumDataMap.java
index 4a91dc0..aacdd3a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumDataMap.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumDataMap.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import java.util.ArrayList;
 import java.util.List;
@@ -21,13 +22,20 @@
 
 public class EnumDataMap {
   private final ImmutableMap<DexType, EnumData> map;
+  private final ImmutableMap<DexType, DexType> subEnumToSuperEnumMap;
 
   public static EnumDataMap empty() {
-    return new EnumDataMap(ImmutableMap.of());
+    return new EnumDataMap(ImmutableMap.of(), ImmutableMap.of());
   }
 
-  public EnumDataMap(ImmutableMap<DexType, EnumData> map) {
+  public EnumDataMap(
+      ImmutableMap<DexType, EnumData> map, ImmutableMap<DexType, DexType> subEnumToSuperEnumMap) {
     this.map = map;
+    this.subEnumToSuperEnumMap = subEnumToSuperEnumMap;
+  }
+
+  public boolean hasAnyEnumsWithSubtypes() {
+    return !subEnumToSuperEnumMap.isEmpty();
   }
 
   public void checkEnumsUnboxed(AppView<AppInfoWithLiveness> appView) {
@@ -47,47 +55,66 @@
     }
   }
 
-  public boolean isUnboxedEnum(DexProgramClass clazz) {
-    return isUnboxedEnum(clazz.getType());
-  }
-
-  public boolean isUnboxedEnum(DexType type) {
-    return map.containsKey(type);
+  public DexType representativeType(DexType type) {
+    return subEnumToSuperEnumMap.getOrDefault(type, type);
   }
 
   public boolean isEmpty() {
     return map.isEmpty();
   }
 
-  public EnumData get(DexProgramClass enumClass) {
-    EnumData enumData = map.get(enumClass.getType());
+  public boolean isSuperUnboxedEnum(DexType type) {
+    return map.containsKey(type);
+  }
+
+  public boolean isUnboxedEnum(DexProgramClass clazz) {
+    return isUnboxedEnum(clazz.getType());
+  }
+
+  public boolean isUnboxedEnum(DexType type) {
+    return map.containsKey(representativeType(type));
+  }
+
+  private EnumData get(DexType type) {
+    EnumData enumData = map.get(representativeType(type));
     assert enumData != null;
     return enumData;
   }
 
-  public Set<DexType> getUnboxedEnums() {
+  public EnumData get(DexProgramClass enumClass) {
+    return get(enumClass.getType());
+  }
+
+  public Set<DexType> getUnboxedSuperEnums() {
     return map.keySet();
   }
 
+  public Set<DexType> computeAllUnboxedEnums() {
+    Set<DexType> items = Sets.newIdentityHashSet();
+    items.addAll(map.keySet());
+    items.addAll(subEnumToSuperEnumMap.keySet());
+    return items;
+  }
+
   public EnumInstanceFieldKnownData getInstanceFieldData(
       DexType enumType, DexField enumInstanceField) {
-    assert map.containsKey(enumType);
-    return map.get(enumType).getInstanceFieldData(enumInstanceField);
+    assert isUnboxedEnum(enumType);
+    return get(enumType).getInstanceFieldData(enumInstanceField);
   }
 
   public boolean hasUnboxedValueFor(DexField enumStaticField) {
-    return isUnboxedEnum(enumStaticField.holder)
-        && map.get(enumStaticField.holder).hasUnboxedValueFor(enumStaticField);
+    return isUnboxedEnum(enumStaticField.getHolderType())
+        && get(enumStaticField.getHolderType()).hasUnboxedValueFor(enumStaticField);
   }
 
   public int getUnboxedValue(DexField enumStaticField) {
-    assert map.containsKey(enumStaticField.holder);
-    return map.get(enumStaticField.holder).getUnboxedValue(enumStaticField);
+    assert isUnboxedEnum(enumStaticField.getHolderType());
+    return get(enumStaticField.getHolderType()).getUnboxedValue(enumStaticField);
   }
 
   public int getValuesSize(DexType enumType) {
-    assert map.containsKey(enumType);
-    return map.get(enumType).getValuesSize();
+    assert isUnboxedEnum(enumType);
+    return get(enumType).getValuesSize();
   }
 
   public int getMaxValuesSize() {
@@ -101,8 +128,8 @@
   }
 
   public boolean matchesValuesField(DexField staticField) {
-    assert map.containsKey(staticField.holder);
-    return map.get(staticField.holder).matchesValuesField(staticField);
+    assert isUnboxedEnum(staticField.getHolderType());
+    return get(staticField.getHolderType()).matchesValuesField(staticField);
   }
 
   public static class EnumData {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
index c147322..ef10ff8 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerImpl.java
@@ -51,6 +51,7 @@
 import com.android.tools.r8.ir.analysis.type.ArrayTypeElement;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.analysis.type.DynamicType;
+import com.android.tools.r8.ir.analysis.type.ReferenceTypeElement;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.objectstate.EnumValuesObjectState;
@@ -116,9 +117,11 @@
 import com.google.common.collect.HashMultiset;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceArrayMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMaps;
 import it.unimi.dsi.fastutil.objects.Object2IntMap;
 import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
 import java.util.ArrayList;
@@ -683,8 +686,8 @@
 
     GraphLens previousLens = appView.graphLens();
     ImmutableSet<DexType> enumsToUnbox = enumUnboxingCandidatesInfo.candidates();
-    ImmutableSet<DexProgramClass> enumClassesToUnbox =
-        enumUnboxingCandidatesInfo.candidateClasses();
+    ImmutableMap<DexProgramClass, Set<DexProgramClass>> enumClassesToUnbox =
+        enumUnboxingCandidatesInfo.candidateClassesWithSubclasses();
     LongLivedProgramMethodSetBuilder<ProgramMethodSet> dependencies =
         enumUnboxingCandidatesInfo.allMethodDependencies();
     enumUnboxingCandidatesInfo.clear();
@@ -693,7 +696,7 @@
 
     EnumUnboxingUtilityClasses utilityClasses =
         EnumUnboxingUtilityClasses.builder(appView)
-            .synthesizeEnumUnboxingUtilityClasses(enumClassesToUnbox, enumDataMap)
+            .synthesizeEnumUnboxingUtilityClasses(enumClassesToUnbox.keySet(), enumDataMap)
             .build(converter, executorService);
 
     // Fixup the application.
@@ -729,7 +732,8 @@
         .merge(
             methodsDependingOnLibraryModelisation
                 .rewrittenWithLens(appView)
-                .removeAll(treeFixerResult.getPrunedItems().getRemovedMethods()));
+                .removeAll(treeFixerResult.getPrunedItems().getRemovedMethods()))
+        .addAll(treeFixerResult.getDispatchMethods(), appView.graphLens());
     methodsDependingOnLibraryModelisation.clear();
 
     updateOptimizationInfos(executorService, feedback, treeFixerResult, previousLens);
@@ -806,15 +810,18 @@
       debugLogs.keySet().forEach(enumUnboxingCandidatesInfo::removeCandidate);
       reportEnumsAnalysis();
     }
-    assert enumDataMap.getUnboxedEnums().size() == enumUnboxingCandidatesInfo.candidates().size();
+    assert enumDataMap.getUnboxedSuperEnums().size()
+        == enumUnboxingCandidatesInfo.candidates().size();
     return enumDataMap;
   }
 
   private EnumDataMap analyzeEnumInstances() {
+    ImmutableMap.Builder<DexType, DexType> enumSubtypes = ImmutableMap.builder();
     ImmutableMap.Builder<DexType, EnumData> builder = ImmutableMap.builder();
-    enumUnboxingCandidatesInfo.forEachCandidateAndRequiredInstanceFieldData(
-        (enumClass, instanceFields) -> {
-          EnumData data = buildData(enumClass, instanceFields);
+    enumUnboxingCandidatesInfo.forEachCandidateInfo(
+        info -> {
+          DexProgramClass enumClass = info.getEnumClass();
+          EnumData data = buildData(enumClass, info.getRequiredInstanceFieldData());
           if (data == null) {
             // Reason is already reported at this point.
             enumUnboxingCandidatesInfo.removeCandidate(enumClass);
@@ -822,10 +829,17 @@
           }
           if (!debugLogEnabled || !debugLogs.containsKey(enumClass.getType())) {
             builder.put(enumClass.type, data);
+            if (data.valuesTypes != null) {
+              for (DexType value : data.valuesTypes.values()) {
+                if (value != enumClass.type) {
+                  enumSubtypes.put(value, enumClass.type);
+                }
+              }
+            }
           }
         });
     staticFieldValuesMap.clear();
-    return new EnumDataMap(builder.build());
+    return new EnumDataMap(builder.build(), enumSubtypes.build());
   }
 
   private EnumData buildData(DexProgramClass enumClass, Set<DexField> instanceFields) {
@@ -854,13 +868,21 @@
       return null;
     }
 
+    enumStaticFieldValues =
+        enumStaticFieldValues.rewrittenWithLens(appView, appView.graphLens(), appView.codeLens());
+    Set<DexType> enumSubtypes = enumUnboxingCandidatesInfo.getSubtypes(enumClass.getType());
+
     // Step 1: We iterate over the field to find direct enum instance information and the values
     // fields.
     for (DexEncodedField staticField : enumClass.staticFields()) {
-      if (factory.enumMembers.isEnumField(staticField, enumClass.type)) {
+      // The field might be specialized while the data was recorded without the specialization.
+      if (factory.enumMembers.isEnumField(staticField, enumClass.type, enumSubtypes)) {
         ObjectState enumState =
             enumStaticFieldValues.getObjectStateForPossiblyPinnedField(staticField.getReference());
         if (enumState == null) {
+          assert enumStaticFieldValues.getObjectStateForPossiblyPinnedField(
+                  staticField.getReference().withType(enumClass.type, factory))
+              == null;
           if (staticField.getOptimizationInfo().isDead()) {
             // We don't care about unused field data.
             continue;
@@ -957,7 +979,7 @@
 
     return new EnumData(
         instanceFieldsData,
-        isEnumWithSubtypes ? valueTypes : null,
+        isEnumWithSubtypes ? valueTypes : Int2ReferenceMaps.emptyMap(),
         unboxedValues.build(),
         valuesField.build(),
         valuesContents == null ? EnumData.INVALID_VALUES_SIZE : valuesContents.getEnumValuesSize());
@@ -1051,26 +1073,44 @@
   }
 
   private void analyzeInitializers() {
-    enumUnboxingCandidatesInfo.forEachCandidate(
-        enumClass -> {
-          for (DexEncodedMethod directMethod : enumClass.directMethods()) {
-            if (directMethod.isInstanceInitializer()) {
-              if (directMethod
-                  .getOptimizationInfo()
-                  .getContextInsensitiveInstanceInitializerInfo()
-                  .mayHaveOtherSideEffectsThanInstanceFieldAssignments()) {
-                if (markEnumAsUnboxable(Reason.INVALID_INIT, enumClass)) {
-                  break;
-                }
-              }
+    enumUnboxingCandidatesInfo.forEachCandidateInfo(
+        (info) -> {
+          DexProgramClass enumClass = info.getEnumClass();
+          if (!instanceInitializersAllowUnboxing(enumClass)) {
+            if (markEnumAsUnboxable(Reason.INVALID_INIT, enumClass)) {
+              return;
             }
           }
           if (enumClass.classInitializationMayHaveSideEffects(appView)) {
-            markEnumAsUnboxable(Reason.INVALID_CLINIT, enumClass);
+            if (markEnumAsUnboxable(Reason.INVALID_CLINIT, enumClass)) {
+              return;
+            }
+          }
+          for (DexProgramClass subclass : info.getSubclasses()) {
+            if (!instanceInitializersAllowUnboxing(subclass)) {
+              if (markEnumAsUnboxable(Reason.INVALID_SUBTYPE_INIT, enumClass)) {
+                return;
+              }
+            }
+            if (subclass.hasClassInitializer()) {
+              if (markEnumAsUnboxable(Reason.SUBTYPE_CLINIT, enumClass)) {
+                return;
+              }
+            }
           }
         });
   }
 
+  private boolean instanceInitializersAllowUnboxing(DexProgramClass clazz) {
+    return !Iterables.any(
+        clazz.programInstanceInitializers(),
+        instanceInitializer ->
+            instanceInitializer
+                .getOptimizationInfo()
+                .getContextInsensitiveInstanceInitializerInfo()
+                .mayHaveOtherSideEffectsThanInstanceFieldAssignments());
+  }
+
   private Reason instructionAllowEnumUnboxing(
       Instruction instruction, IRCode code, DexProgramClass enumClass, Value enumValue) {
     ProgramMethod context = code.context();
@@ -1142,6 +1182,28 @@
     return Reason.ELIGIBLE;
   }
 
+  private ReferenceTypeElement getValueBaseType(Value value, TypeElement arrayType) {
+    TypeElement valueBaseType = value.getType();
+    if (valueBaseType.isArrayType()) {
+      assert valueBaseType.asArrayType().getBaseType().isClassType();
+      assert valueBaseType.asArrayType().getNesting() == arrayType.asArrayType().getNesting() - 1;
+      valueBaseType = valueBaseType.asArrayType().getBaseType();
+    }
+    assert valueBaseType.isClassType() || valueBaseType.isNullType();
+    return valueBaseType.asReferenceType();
+  }
+
+  private boolean areCompatibleArrayTypes(
+      ClassTypeElement arrayBaseType, ReferenceTypeElement valueBaseType) {
+    assert valueBaseType.isClassType() || valueBaseType.isNullType();
+    if (valueBaseType.isNullType()) {
+      // TODO(b/271385332): Allow nulls in enum arrays to be unboxed.
+      return false;
+    }
+    return enumUnboxingCandidatesInfo.isAssignableTo(
+        valueBaseType.asClassType().getClassType(), arrayBaseType.getClassType());
+  }
+
   private Reason analyzeArrayPutUser(
       ArrayPut arrayPut,
       IRCode code,
@@ -1157,14 +1219,8 @@
     assert arrayType.isArrayType();
     assert arrayType.asArrayType().getBaseType().isClassType();
     ClassTypeElement arrayBaseType = arrayType.asArrayType().getBaseType().asClassType();
-    TypeElement valueBaseType = arrayPut.value().getType();
-    if (valueBaseType.isArrayType()) {
-      assert valueBaseType.asArrayType().getBaseType().isClassType();
-      assert valueBaseType.asArrayType().getNesting() == arrayType.asArrayType().getNesting() - 1;
-      valueBaseType = valueBaseType.asArrayType().getBaseType();
-    }
-    if (arrayBaseType.equalUpToNullability(valueBaseType)
-        && arrayBaseType.getClassType() == enumClass.type) {
+    ReferenceTypeElement valueBaseType = getValueBaseType(arrayPut.value(), arrayType);
+    if (areCompatibleArrayTypes(arrayBaseType, valueBaseType)) {
       return Reason.ELIGIBLE;
     }
     return Reason.INVALID_ARRAY_PUT;
@@ -1191,13 +1247,8 @@
     }
 
     for (Value value : invokeNewArray.inValues()) {
-      TypeElement valueBaseType = value.getType();
-      if (valueBaseType.isArrayType()) {
-        assert valueBaseType.asArrayType().getBaseType().isClassType();
-        assert valueBaseType.asArrayType().getNesting() == arrayType.asArrayType().getNesting() - 1;
-        valueBaseType = valueBaseType.asArrayType().getBaseType();
-      }
-      if (!arrayBaseType.equalUpToNullability(valueBaseType)) {
+      ReferenceTypeElement valueBaseType = getValueBaseType(value, arrayType);
+      if (!areCompatibleArrayTypes(arrayBaseType, valueBaseType)) {
         return Reason.INVALID_INVOKE_NEW_ARRAY;
       }
     }
@@ -1305,7 +1356,9 @@
       if (targetHolder.isEnum() && singleTarget.getDefinition().isInstanceInitializer()) {
         // The enum instance initializer is only allowed to be called from an initializer of the
         // enum itself.
-        if (code.context().getHolder() != targetHolder || !code.method().isInitializer()) {
+        if (getEnumUnboxingCandidateOrNull(code.context().getHolder().getType())
+                != getEnumUnboxingCandidateOrNull(targetHolder.getType())
+            || !context.getDefinition().isInitializer()) {
           return Reason.INVALID_INIT;
         }
         if (code.method().isInstanceInitializer() && !invoke.getFirstArgument().isThis()) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
index abc0fde..8b82922 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
@@ -72,11 +72,12 @@
   }
 
   private void setEnumSubclassesOnCandidates() {
-    enumToUnboxCandidates.forEachCandidate(
-        candidate ->
-            enumToUnboxCandidates.setEnumSubclasses(
-                candidate.getType(),
-                enumSubclasses.getOrDefault(candidate.getType(), ImmutableSet.of())));
+    enumToUnboxCandidates.forEachCandidateInfo(
+        info -> {
+          DexType type = info.getEnumClass().getType();
+          enumToUnboxCandidates.setEnumSubclasses(
+              type, enumSubclasses.getOrDefault(type, ImmutableSet.of()));
+        });
   }
 
   private void removeIneligibleCandidates() {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateInfoCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateInfoCollection.java
index bfeb83b..3c2fc90 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateInfoCollection.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.ir.optimize.enums;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -12,20 +13,23 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.collections.LongLivedProgramMethodSetBuilder;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import java.util.IdentityHashMap;
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 
 public class EnumUnboxingCandidateInfoCollection {
 
   private final Map<DexType, EnumUnboxingCandidateInfo> enumTypeToInfo = new ConcurrentHashMap<>();
+  private final Map<DexType, DexType> subEnumToSuperEnumMap = new IdentityHashMap<>();
   private final Set<DexMethod> prunedMethods = Sets.newConcurrentHashSet();
 
   public void addCandidate(
@@ -42,8 +46,16 @@
     return !enumTypeToInfo.get(enumType).getSubclasses().isEmpty();
   }
 
+  public Set<DexType> getSubtypes(DexType enumType) {
+    return SetUtils.mapIdentityHashSet(
+        enumTypeToInfo.get(enumType).getSubclasses(), DexClass::getType);
+  }
+
   public void setEnumSubclasses(DexType superEnum, Set<DexProgramClass> subclasses) {
     enumTypeToInfo.get(superEnum).setSubclasses(subclasses);
+    for (DexProgramClass subclass : subclasses) {
+      subEnumToSuperEnumMap.put(subclass.getType(), superEnum);
+    }
   }
 
   public void addPrunedMethod(ProgramMethod method) {
@@ -70,16 +82,27 @@
     return ImmutableSet.copyOf(enumTypeToInfo.keySet());
   }
 
-  public ImmutableSet<DexProgramClass> candidateClasses() {
-    ImmutableSet.Builder<DexProgramClass> builder = ImmutableSet.builder();
+  public ImmutableMap<DexProgramClass, Set<DexProgramClass>> candidateClassesWithSubclasses() {
+    ImmutableMap.Builder<DexProgramClass, Set<DexProgramClass>> builder = ImmutableMap.builder();
     for (EnumUnboxingCandidateInfo info : enumTypeToInfo.values()) {
-      builder.add(info.getEnumClass());
+      builder.put(info.getEnumClass(), info.getSubclasses());
     }
     return builder.build();
   }
 
+  /** Answers true if both enums are identical, or if one inherit from the other. */
+  public boolean isAssignableTo(DexType subtype, DexType superType) {
+    assert superType != null;
+    assert subtype != null;
+    if (superType == subtype) {
+      return true;
+    }
+    return superType == subEnumToSuperEnumMap.get(subtype);
+  }
+
   public DexProgramClass getCandidateClassOrNull(DexType enumType) {
-    EnumUnboxingCandidateInfo info = enumTypeToInfo.get(enumType);
+    DexType superEnum = subEnumToSuperEnumMap.getOrDefault(enumType, enumType);
+    EnumUnboxingCandidateInfo info = enumTypeToInfo.get(superEnum);
     if (info == null) {
       return null;
     }
@@ -120,16 +143,8 @@
     info.addRequiredInstanceFieldData(field);
   }
 
-  public void forEachCandidate(Consumer<DexProgramClass> enumClassConsumer) {
-    enumTypeToInfo.values().forEach(info -> enumClassConsumer.accept(info.enumClass));
-  }
-
-  public void forEachCandidateAndRequiredInstanceFieldData(
-      BiConsumer<DexProgramClass, Set<DexField>> biConsumer) {
-    enumTypeToInfo
-        .values()
-        .forEach(
-            info -> biConsumer.accept(info.getEnumClass(), info.getRequiredInstanceFieldData()));
+  public void forEachCandidateInfo(Consumer<EnumUnboxingCandidateInfo> consumer) {
+    enumTypeToInfo.values().forEach(consumer);
   }
 
   public void clear() {
@@ -143,7 +158,7 @@
     return true;
   }
 
-  private static class EnumUnboxingCandidateInfo {
+  public static class EnumUnboxingCandidateInfo {
 
     private final DexProgramClass enumClass;
     private final LongLivedProgramMethodSetBuilder<ProgramMethodSet> methodDependencies;
@@ -164,6 +179,7 @@
     }
 
     public Set<DexProgramClass> getSubclasses() {
+      assert subclasses != null;
       return subclasses;
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
index 91e6e67..56a2d20 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
@@ -4,12 +4,15 @@
 
 package com.android.tools.r8.ir.optimize.enums;
 
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
 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.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.graph.lens.MethodLookupResult;
 import com.android.tools.r8.graph.lens.NestedGraphLens;
 import com.android.tools.r8.graph.proto.ArgumentInfoCollection;
 import com.android.tools.r8.graph.proto.RewrittenPrototypeDescription;
@@ -43,10 +46,11 @@
   EnumUnboxingLens(
       AppView<?> appView,
       BidirectionalOneToOneMap<DexField, DexField> fieldMap,
-      BidirectionalOneToManyRepresentativeMap<DexMethod, DexMethod> methodMap,
+      BidirectionalOneToManyRepresentativeMap<DexMethod, DexMethod> renamedSignatures,
       Map<DexType, DexType> typeMap,
+      Map<DexMethod, DexMethod> methodMap,
       Map<DexMethod, RewrittenPrototypeDescription> prototypeChangesPerMethod) {
-    super(appView, fieldMap, methodMap::getRepresentativeValue, typeMap, methodMap);
+    super(appView, fieldMap, methodMap, typeMap, renamedSignatures);
     assert !appView.unboxedEnums().isEmpty();
     this.abstractValueFactory = appView.abstractValueFactory();
     this.prototypeChangesPerMethod = prototypeChangesPerMethod;
@@ -69,6 +73,58 @@
   }
 
   @Override
+  public boolean isContextFreeForMethods(GraphLens codeLens) {
+    if (codeLens == this) {
+      return true;
+    }
+    return !unboxedEnums.hasAnyEnumsWithSubtypes()
+        && getPrevious().isContextFreeForMethods(codeLens);
+  }
+
+  @Override
+  public boolean verifyIsContextFreeForMethod(DexMethod method, GraphLens codeLens) {
+    if (codeLens == this) {
+      return true;
+    }
+    assert getPrevious().verifyIsContextFreeForMethod(getPreviousMethodSignature(method), codeLens);
+    DexMethod previous =
+        getPrevious()
+            .lookupMethod(getPreviousMethodSignature(method), null, null, codeLens)
+            .getReference();
+    assert unboxedEnums.representativeType(previous.getHolderType()) == previous.getHolderType();
+    return true;
+  }
+
+  @Override
+  public MethodLookupResult internalDescribeLookupMethod(
+      MethodLookupResult previous, DexMethod context, GraphLens codeLens) {
+    assert context != null || verifyIsContextFreeForMethod(previous.getReference(), codeLens);
+    assert context == null || previous.getType() != null;
+    DexMethod result;
+    if (previous.getType() == InvokeType.SUPER) {
+      assert context != null;
+      DexType superEnum = unboxedEnums.representativeType(context.getHolderType());
+      if (superEnum != context.getHolderType()) {
+        // TODO(b/271385332): implement this.
+        throw new Unreachable();
+      } else {
+        result = methodMap.apply(previous.getReference());
+      }
+    } else {
+      result = methodMap.apply(previous.getReference());
+    }
+    if (result == null) {
+      return previous;
+    }
+    return MethodLookupResult.builder(this)
+        .setReference(result)
+        .setPrototypeChanges(
+            internalDescribePrototypeChanges(previous.getPrototypeChanges(), result))
+        .setType(mapInvocationType(result, previous.getReference(), previous.getType()))
+        .build();
+  }
+
+  @Override
   protected RewrittenPrototypeDescription internalDescribePrototypeChanges(
       RewrittenPrototypeDescription prototypeChanges, DexMethod method) {
     // Rewrite the single value of the given RewrittenPrototypeDescription if it is referring to an
@@ -133,6 +189,7 @@
         new BidirectionalOneToOneHashMap<>();
     private final MutableBidirectionalOneToManyRepresentativeMap<DexMethod, DexMethod>
         newMethodSignatures = new BidirectionalOneToManyRepresentativeHashMap<>();
+    private final Map<DexMethod, DexMethod> methodMap = new IdentityHashMap<>();
 
     private Map<DexMethod, RewrittenPrototypeDescription> prototypeChangesPerMethod =
         new IdentityHashMap<>();
@@ -155,18 +212,59 @@
       newFieldSignatures.put(from, to);
     }
 
-    public void move(DexMethod from, DexMethod to, boolean fromStatic, boolean toStatic) {
-      move(from, to, fromStatic, toStatic, Collections.emptyList());
+    private RewrittenPrototypeDescription recordPrototypeChanges(
+        DexMethod from,
+        DexMethod to,
+        boolean fromStatic,
+        boolean toStatic,
+        boolean virtualReceiverAlreadyRemapped,
+        List<ExtraUnusedNullParameter> extraUnusedNullParameters) {
+      assert from != to;
+      RewrittenPrototypeDescription prototypeChanges =
+          computePrototypeChanges(
+              from,
+              to,
+              fromStatic,
+              toStatic,
+              virtualReceiverAlreadyRemapped,
+              extraUnusedNullParameters);
+      prototypeChangesPerMethod.put(to, prototypeChanges);
+      return prototypeChanges;
     }
 
-    public RewrittenPrototypeDescription move(
+    public void moveAndMap(DexMethod from, DexMethod to, boolean fromStatic) {
+      moveAndMap(from, to, fromStatic, true, Collections.emptyList());
+    }
+
+    public RewrittenPrototypeDescription moveVirtual(DexMethod from, DexMethod to) {
+      newMethodSignatures.put(from, to);
+      return recordPrototypeChanges(from, to, false, true, false, Collections.emptyList());
+    }
+
+    public RewrittenPrototypeDescription mapToDispatch(DexMethod from, DexMethod to) {
+      methodMap.put(from, to);
+      return recordPrototypeChanges(from, to, false, true, true, Collections.emptyList());
+    }
+
+    public RewrittenPrototypeDescription moveAndMap(
         DexMethod from,
         DexMethod to,
         boolean fromStatic,
         boolean toStatic,
         List<ExtraUnusedNullParameter> extraUnusedNullParameters) {
-      assert from != to;
       newMethodSignatures.put(from, to);
+      methodMap.put(from, to);
+      return recordPrototypeChanges(
+          from, to, fromStatic, toStatic, false, extraUnusedNullParameters);
+    }
+
+    private RewrittenPrototypeDescription computePrototypeChanges(
+        DexMethod from,
+        DexMethod to,
+        boolean fromStatic,
+        boolean toStatic,
+        boolean virtualReceiverAlreadyRemapped,
+        List<ExtraUnusedNullParameter> extraUnusedNullParameters) {
       int offsetDiff = 0;
       int toOffset = BooleanUtils.intValue(!toStatic);
       ArgumentInfoCollection.Builder builder =
@@ -175,14 +273,21 @@
       if (fromStatic != toStatic) {
         assert toStatic;
         offsetDiff = 1;
-        builder
-            .addArgumentInfo(
-                0,
-                RewrittenTypeInfo.builder()
-                    .setOldType(from.getHolderType())
-                    .setNewType(to.getParameter(0))
-                    .build())
-            .setIsConvertedToStaticMethod();
+        if (!virtualReceiverAlreadyRemapped) {
+          builder
+              .addArgumentInfo(
+                  0,
+                  RewrittenTypeInfo.builder()
+                      .setOldType(from.getHolderType())
+                      .setNewType(to.getParameter(0))
+                      .build())
+              .setIsConvertedToStaticMethod();
+        } else {
+          assert to.getParameter(0).isIntType();
+          assert !fromStatic;
+          assert toStatic;
+          assert from.getArity() == to.getArity() - 1;
+        }
       }
       for (int i = 0; i < from.getParameters().size(); i++) {
         DexType fromType = from.getParameter(i);
@@ -200,11 +305,8 @@
                   .setOldType(from.getReturnType())
                   .setNewType(to.getReturnType())
                   .build();
-      RewrittenPrototypeDescription prototypeChanges =
-          RewrittenPrototypeDescription.createForRewrittenTypes(returnInfo, builder.build())
-              .withExtraParameters(extraUnusedNullParameters);
-      prototypeChangesPerMethod.put(to, prototypeChanges);
-      return prototypeChanges;
+      return RewrittenPrototypeDescription.createForRewrittenTypes(returnInfo, builder.build())
+          .withExtraParameters(extraUnusedNullParameters);
     }
 
     void recordCheckNotZeroMethod(
@@ -227,6 +329,7 @@
           newFieldSignatures,
           newMethodSignatures,
           typeMap,
+          methodMap,
           ImmutableMap.copyOf(prototypeChangesPerMethod));
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
index 738ed1e..7d7ce1c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingTreeFixer.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.ir.optimize.enums;
 
 import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
+import static com.android.tools.r8.ir.optimize.enums.EnumUnboxerImpl.ordinalToUnboxedInt;
 
 import com.android.tools.r8.contexts.CompilationContext.ProcessorContext;
 import com.android.tools.r8.graph.AppView;
@@ -49,15 +50,22 @@
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackIgnore;
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfo;
+import com.android.tools.r8.ir.synthetic.EnumUnboxingCfCodeProvider.EnumUnboxingMethodDispatchCfCodeProvider;
+import com.android.tools.r8.ir.synthetic.EnumUnboxingCfCodeProvider.EnumUnboxingMethodDispatchCfCodeProvider.CfCodeWithLens;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.ImmutableArrayUtils;
 import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.collections.DexMethodSignatureSet;
 import com.android.tools.r8.utils.collections.ProgramMethodMap;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
+import com.google.common.collect.Sets;
+import it.unimi.dsi.fastutil.ints.Int2ObjectArrayMap;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.IdentityHashMap;
@@ -69,6 +77,7 @@
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
 import java.util.function.Predicate;
 
 class EnumUnboxingTreeFixer {
@@ -77,27 +86,45 @@
   private final AppView<AppInfoWithLiveness> appView;
   private final ProgramMethodMap<Set<DexProgramClass>> checkNotNullMethods;
   private final DexItemFactory factory;
+  // Provides information for each unboxed enum regarding the instance field values of each enum
+  // instance, the original instance class, and the ordinal value.
   private final EnumDataMap enumDataMap;
-  private final Set<DexProgramClass> unboxedEnums;
+  // Maps the superEnum to the enumSubtypes. This is already present in the enumDataMap as DexTypes,
+  // we duplicate that here as DexProgramClasses.
+  private final Map<DexProgramClass, Set<DexProgramClass>> unboxedEnumHierarchy;
   private final EnumUnboxingUtilityClasses utilityClasses;
+  private final ProgramMethodMap<CfCodeWithLens> dispatchMethods = ProgramMethodMap.create();
 
   EnumUnboxingTreeFixer(
       AppView<AppInfoWithLiveness> appView,
       ProgramMethodMap<Set<DexProgramClass>> checkNotNullMethods,
       EnumDataMap enumDataMap,
-      Set<DexProgramClass> unboxedEnums,
+      Map<DexProgramClass, Set<DexProgramClass>> unboxedEnums,
       EnumUnboxingUtilityClasses utilityClasses) {
     this.appView = appView;
     this.checkNotNullMethods = checkNotNullMethods;
     this.enumDataMap = enumDataMap;
     this.factory = appView.dexItemFactory();
+    this.unboxedEnumHierarchy = unboxedEnums;
     this.lensBuilder =
-        EnumUnboxingLens.enumUnboxingLensBuilder(appView)
-            .mapUnboxedEnums(enumDataMap.getUnboxedEnums());
-    this.unboxedEnums = unboxedEnums;
+        EnumUnboxingLens.enumUnboxingLensBuilder(appView).mapUnboxedEnums(getUnboxedEnums());
     this.utilityClasses = utilityClasses;
   }
 
+  private Set<DexProgramClass> computeUnboxedEnumClasses() {
+    Set<DexProgramClass> unboxedEnumClasses = Sets.newIdentityHashSet();
+    unboxedEnumHierarchy.forEach(
+        (superEnum, subEnums) -> {
+          unboxedEnumClasses.add(superEnum);
+          unboxedEnumClasses.addAll(subEnums);
+        });
+    return unboxedEnumClasses;
+  }
+
+  private Set<DexType> getUnboxedEnums() {
+    return enumDataMap.computeAllUnboxedEnums();
+  }
+
   Result fixupTypeReferences(IRConverter converter, ExecutorService executorService)
       throws ExecutionException {
     PrunedItems.Builder prunedItemsBuilder = PrunedItems.builder();
@@ -108,25 +135,27 @@
     // Fix all methods and fields using enums to unbox.
     // TODO(b/191617665): Parallelize this fixup.
     for (DexProgramClass clazz : appView.appInfo().classes()) {
-      if (enumDataMap.isUnboxedEnum(clazz)) {
+      if (enumDataMap.isSuperUnboxedEnum(clazz.getType())) {
+
         // Clear the initializers and move the other methods to the new location.
         LocalEnumUnboxingUtilityClass localUtilityClass =
             utilityClasses.getLocalUtilityClass(clazz);
         Collection<DexEncodedField> localUtilityFields =
             createLocalUtilityFields(clazz, localUtilityClass, prunedItemsBuilder);
         Collection<DexEncodedMethod> localUtilityMethods =
-            createLocalUtilityMethods(clazz, localUtilityClass, prunedItemsBuilder);
+            createLocalUtilityMethods(
+                clazz, unboxedEnumHierarchy.get(clazz), localUtilityClass, prunedItemsBuilder);
 
-        // Cleanup old class.
-        clazz.clearInstanceFields();
-        clazz.clearStaticFields();
-        clazz.getMethodCollection().clearDirectMethods();
-        clazz.getMethodCollection().clearVirtualMethods();
+        // Cleanup old classes.
+        cleanUpOldClass(clazz);
+        for (DexProgramClass subEnum : unboxedEnumHierarchy.get(clazz)) {
+          cleanUpOldClass(subEnum);
+        }
 
         // Update members on the local utility class.
         localUtilityClass.getDefinition().setDirectMethods(localUtilityMethods);
         localUtilityClass.getDefinition().setStaticFields(localUtilityFields);
-      } else {
+      } else if (!enumDataMap.isUnboxedEnum(clazz.getType())) {
         clazz.getMethodCollection().replaceMethods(this::fixupEncodedMethod);
         clazz.getFieldCollection().replaceFields(this::fixupEncodedField);
       }
@@ -143,7 +172,22 @@
     BiMap<DexMethod, DexMethod> checkNotNullToCheckNotZeroMapping =
         duplicateCheckNotNullMethods(converter, executorService);
 
-    return new Result(checkNotNullToCheckNotZeroMapping, lens, prunedItemsBuilder.build());
+    ProgramMethodSet dispatchMethodSet = ProgramMethodSet.create();
+    dispatchMethods.forEach(
+        (method, code) -> {
+          dispatchMethodSet.add(method);
+          code.setCodeLens(lens);
+        });
+
+    return new Result(
+        checkNotNullToCheckNotZeroMapping, dispatchMethodSet, lens, prunedItemsBuilder.build());
+  }
+
+  private void cleanUpOldClass(DexProgramClass clazz) {
+    clazz.clearInstanceFields();
+    clazz.clearStaticFields();
+    clazz.getMethodCollection().clearDirectMethods();
+    clazz.getMethodCollection().clearVirtualMethods();
   }
 
   private BiMap<DexMethod, DexMethod> duplicateCheckNotNullMethods(
@@ -154,10 +198,12 @@
     OneTimeMethodProcessor.Builder methodProcessorBuilder =
         OneTimeMethodProcessor.builder(eventConsumer, processorContext);
 
+    Set<DexProgramClass> unboxedEnumClasses = computeUnboxedEnumClasses();
+
     // Only duplicate checkNotNull() methods that are required for enum unboxing.
     checkNotNullMethods.removeIf(
         (checkNotNullMethod, dependentEnums) ->
-            !SetUtils.containsAnyOf(unboxedEnums, dependentEnums));
+            !SetUtils.containsAnyOf(unboxedEnumClasses, dependentEnums));
 
     // For each checkNotNull() method, synthesize a free flowing static checkNotZero() method that
     // takes an int instead of an Object with the same implementation.
@@ -230,7 +276,7 @@
             .resolveField(appView.dexItemFactory().enumMembers.ordinalField)
             .getResolvedField();
     ThreadUtils.processItems(
-        unboxedEnums,
+        unboxedEnumHierarchy.keySet(),
         unboxedEnum -> fixupEnumClassInitializer(converter, unboxedEnum, ordinalField),
         executorService);
   }
@@ -487,6 +533,7 @@
 
   private Collection<DexEncodedMethod> createLocalUtilityMethods(
       DexProgramClass unboxedEnum,
+      Set<DexProgramClass> subEnums,
       LocalEnumUnboxingUtilityClass localUtilityClass,
       PrunedItems.Builder prunedItemsBuilder) {
     Map<DexMethod, DexEncodedMethod> localUtilityMethods =
@@ -496,22 +543,157 @@
     localUtilityClass
         .getDefinition()
         .forEachMethod(method -> localUtilityMethods.put(method.getReference(), method));
+    // First generate all methods from the super enums, for any override, generate the dispatch
+    // method and both the move and super contextual move.
+    DexMethodSignatureSet methodsWithOverride = DexMethodSignatureSet.create();
+    forEachWhilePruningInstanceInitializers(
+        unboxedEnum,
+        prunedItemsBuilder,
+        method -> {
+          DexEncodedMethod superEnumUtilityMethod =
+              installLocalUtilityMethod(localUtilityClass, localUtilityMethods, method);
+          Map<DexMethod, DexMethod> overrideToUtilityMethods = new IdentityHashMap<>();
+          if (method.getDefinition().isNonPrivateVirtualMethod()) {
+            for (DexProgramClass subEnum : subEnums) {
+              ProgramMethod override = subEnum.lookupProgramMethod(method.getReference());
+              if (override != null) {
+                methodsWithOverride.add(method);
+                DexEncodedMethod subEnumLocalUtilityMethod =
+                    installLocalUtilityMethod(localUtilityClass, localUtilityMethods, override);
+                overrideToUtilityMethods.put(
+                    override.getReference(), subEnumLocalUtilityMethod.getReference());
+              }
+            }
+          }
+          if (overrideToUtilityMethods.isEmpty()) {
+            lensBuilder.moveAndMap(
+                method.getReference(),
+                superEnumUtilityMethod.getReference(),
+                method.getDefinition().isStatic());
+          } else {
+            DexMethod dispatch =
+                installDispatchMethod(
+                        localUtilityClass,
+                        localUtilityMethods,
+                        method,
+                        superEnumUtilityMethod,
+                        overrideToUtilityMethods)
+                    .getReference();
+            recordEmulatedDispatch(
+                method.getReference(), superEnumUtilityMethod.getReference(), dispatch);
+            for (DexMethod override : overrideToUtilityMethods.keySet()) {
+              recordEmulatedDispatch(override, overrideToUtilityMethods.get(override), dispatch);
+            }
+          }
+        });
+    // TODO(b/271385332): Check dispatch methods without implementation in the superEnum.
+    // Second for each subEnum generate the remaining methods if not already generated.
+    for (DexProgramClass subEnum : subEnums) {
+      forEachWhilePruningInstanceInitializers(
+          subEnum,
+          prunedItemsBuilder,
+          method -> {
+            if (!methodsWithOverride.contains(method.getReference())) {
+              DexEncodedMethod newLocalUtilityMethod =
+                  installLocalUtilityMethod(localUtilityClass, localUtilityMethods, method);
+              // We cannot remap the subtype <clinit> to the same method than the superEnum one,
+              // so we have to extend the design to support subtype <clinit> here.
+              assert !method.getDefinition().isClassInitializer();
+              lensBuilder.moveAndMap(
+                  method.getReference(),
+                  newLocalUtilityMethod.getReference(),
+                  method.getDefinition().isStatic());
+            }
+          });
+    }
+    return localUtilityMethods.values();
+  }
 
-    unboxedEnum.forEachProgramMethod(
+  public void recordEmulatedDispatch(DexMethod from, DexMethod move, DexMethod dispatch) {
+    // Move is used for getRenamedSignature and to remap invoke-super.
+    // Map is used to remap all the other invokes.
+    lensBuilder.moveVirtual(from, move);
+    lensBuilder.mapToDispatch(from, dispatch);
+  }
+
+  private DexEncodedMethod installDispatchMethod(
+      LocalEnumUnboxingUtilityClass localUtilityClass,
+      Map<DexMethod, DexEncodedMethod> localUtilityMethods,
+      ProgramMethod method,
+      DexEncodedMethod superEnumUtilityMethod,
+      Map<DexMethod, DexMethod> map) {
+    assert !map.isEmpty();
+    DexMethod newLocalUtilityMethodReference =
+        factory.createFreshMethodNameWithoutHolder(
+            "_dispatch_" + method.getName().toString(),
+            fixupProto(factory.prependHolderToProto(method.getReference())),
+            localUtilityClass.getType(),
+            newMethodSignature -> !localUtilityMethods.containsKey(newMethodSignature));
+    Int2ObjectMap<DexMethod> methodMap = new Int2ObjectArrayMap<>();
+    IdentityHashMap<DexType, DexMethod> typeToMethod = new IdentityHashMap<>();
+    map.forEach(
+        (methodReference, newMethodReference) ->
+            typeToMethod.put(methodReference.getHolderType(), newMethodReference));
+    DexProgramClass unboxedEnum = method.getHolder();
+    assert enumDataMap.get(unboxedEnum).valuesTypes != null;
+    enumDataMap
+        .get(unboxedEnum)
+        .valuesTypes
+        .forEach(
+            (i, type) -> {
+              if (typeToMethod.containsKey(type)) {
+                methodMap.put(ordinalToUnboxedInt(i), typeToMethod.get(type));
+              }
+            });
+    CfCodeWithLens codeWithLens =
+        new EnumUnboxingMethodDispatchCfCodeProvider(
+                appView,
+                localUtilityClass.getType(),
+                superEnumUtilityMethod.getReference(),
+                methodMap)
+            .generateCfCode();
+    DexEncodedMethod newLocalUtilityMethod =
+        DexEncodedMethod.builder()
+            .setMethod(newLocalUtilityMethodReference)
+            .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
+            .setCode(codeWithLens)
+            .setClassFileVersion(unboxedEnum.getInitialClassFileVersion())
+            .setApiLevelForDefinition(superEnumUtilityMethod.getApiLevelForDefinition())
+            .setApiLevelForCode(superEnumUtilityMethod.getApiLevelForCode())
+            .build();
+    dispatchMethods.put(
+        newLocalUtilityMethod.asProgramMethod(localUtilityClass.getDefinition()), codeWithLens);
+    assert !localUtilityMethods.containsKey(newLocalUtilityMethodReference);
+    localUtilityMethods.put(newLocalUtilityMethodReference, newLocalUtilityMethod);
+    return newLocalUtilityMethod;
+  }
+
+  private DexEncodedMethod installLocalUtilityMethod(
+      LocalEnumUnboxingUtilityClass localUtilityClass,
+      Map<DexMethod, DexEncodedMethod> localUtilityMethods,
+      ProgramMethod method) {
+    DexEncodedMethod newLocalUtilityMethod =
+        createLocalUtilityMethod(
+            method,
+            localUtilityClass,
+            newMethodSignature -> !localUtilityMethods.containsKey(newMethodSignature));
+    assert !localUtilityMethods.containsKey(newLocalUtilityMethod.getReference());
+    localUtilityMethods.put(newLocalUtilityMethod.getReference(), newLocalUtilityMethod);
+    return newLocalUtilityMethod;
+  }
+
+  private void forEachWhilePruningInstanceInitializers(
+      DexProgramClass clazz,
+      PrunedItems.Builder prunedItemsBuilder,
+      Consumer<? super ProgramMethod> consumer) {
+    clazz.forEachProgramMethod(
         method -> {
           if (method.getDefinition().isInstanceInitializer()) {
             prunedItemsBuilder.addRemovedMethod(method.getReference());
           } else {
-            DexEncodedMethod newLocalUtilityMethod =
-                createLocalUtilityMethod(
-                    method,
-                    localUtilityClass,
-                    newMethodSignature -> !localUtilityMethods.containsKey(newMethodSignature));
-            assert !localUtilityMethods.containsKey(newLocalUtilityMethod.getReference());
-            localUtilityMethods.put(newLocalUtilityMethod.getReference(), newLocalUtilityMethod);
+            consumer.accept(method);
           }
         });
-    return localUtilityMethods.values();
   }
 
   private DexEncodedMethod createLocalUtilityMethod(
@@ -534,9 +716,6 @@
                 localUtilityClass.getType(),
                 availableMethodSignatures);
 
-    // Record the move.
-    lensBuilder.move(methodReference, newMethod, method.getDefinition().isStatic(), true);
-
     return method
         .getDefinition()
         .toTypeSubstitutedMethod(
@@ -581,7 +760,7 @@
         ExtraUnusedNullParameter.computeExtraUnusedNullParameters(method.getReference(), newMethod);
     boolean isStatic = method.isStatic();
     RewrittenPrototypeDescription prototypeChanges =
-        lensBuilder.move(
+        lensBuilder.moveAndMap(
             method.getReference(), newMethod, isStatic, isStatic, extraUnusedNullParameters);
     return method.toTypeSubstitutedMethod(
         newMethod,
@@ -662,14 +841,17 @@
   public static class Result {
 
     private final BiMap<DexMethod, DexMethod> checkNotNullToCheckNotZeroMapping;
+    private final ProgramMethodSet dispatchMethods;
     private final EnumUnboxingLens lens;
     private final PrunedItems prunedItems;
 
     Result(
         BiMap<DexMethod, DexMethod> checkNotNullToCheckNotZeroMapping,
+        ProgramMethodSet dispatchMethods,
         EnumUnboxingLens lens,
         PrunedItems prunedItems) {
       this.checkNotNullToCheckNotZeroMapping = checkNotNullToCheckNotZeroMapping;
+      this.dispatchMethods = dispatchMethods;
       this.lens = lens;
       this.prunedItems = prunedItems;
     }
@@ -678,6 +860,10 @@
       return checkNotNullToCheckNotZeroMapping;
     }
 
+    public ProgramMethodSet getDispatchMethods() {
+      return dispatchMethods;
+    }
+
     EnumUnboxingLens getLens() {
       return lens;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/eligibility/Reason.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/eligibility/Reason.java
index bfd215d..56b05f6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/eligibility/Reason.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/eligibility/Reason.java
@@ -31,6 +31,8 @@
   public static final Reason NO_INIT = new StringReason("NO_INIT");
   public static final Reason INVALID_INIT = new StringReason("INVALID_INIT");
   public static final Reason INVALID_CLINIT = new StringReason("INVALID_CLINIT");
+  public static final Reason INVALID_SUBTYPE_INIT = new StringReason("INVALID_SUBTYPE_INIT");
+  public static final Reason SUBTYPE_CLINIT = new StringReason("SUBTYPE_CLINIT");
   public static final Reason INVALID_INVOKE = new StringReason("INVALID_INVOKE");
   public static final Reason INVALID_INVOKE_CLASSPATH =
       new StringReason("INVALID_INVOKE_CLASSPATH");
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
index 941248a..af9e41c 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java
@@ -16,6 +16,7 @@
 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.CfReturnVoid;
 import com.android.tools.r8.cf.code.CfStackInstruction;
 import com.android.tools.r8.cf.code.CfStackInstruction.Opcode;
 import com.android.tools.r8.cf.code.CfStaticFieldRead;
@@ -29,11 +30,13 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.code.IfType;
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.ir.optimize.enums.EnumDataMap.EnumData;
 import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldMappingData;
+import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
 import java.util.ArrayList;
 import java.util.List;
 import org.objectweb.asm.Opcodes;
@@ -65,6 +68,85 @@
     }
   }
 
+  public static class EnumUnboxingMethodDispatchCfCodeProvider extends EnumUnboxingCfCodeProvider {
+
+    private final GraphLens codeLens;
+    private final DexMethod superEnumMethod;
+    private final Int2ObjectMap<DexMethod> methodMap;
+
+    public EnumUnboxingMethodDispatchCfCodeProvider(
+        AppView<?> appView,
+        DexType holder,
+        DexMethod superEnumMethod,
+        Int2ObjectMap<DexMethod> methodMap) {
+      super(appView, holder);
+      this.codeLens = appView.codeLens();
+      this.superEnumMethod = superEnumMethod;
+      this.methodMap = methodMap;
+    }
+
+    @Override
+    public CfCodeWithLens generateCfCode() {
+      // TODO(b/167942775): Should use a table-switch for large enums (maybe same threshold in the
+      //  rewriter of switchmaps).
+
+      DexItemFactory factory = appView.dexItemFactory();
+      int returnInvokeSize = superEnumMethod.getParameters().size() + 2;
+      List<CfInstruction> instructions =
+          new ArrayList<>(methodMap.size() * (returnInvokeSize + 5) + returnInvokeSize);
+
+      CfFrame.Builder frameBuilder = CfFrame.builder();
+      for (DexType parameter : superEnumMethod.getParameters()) {
+        frameBuilder.appendLocal(FrameType.initialized(parameter));
+      }
+      methodMap.forEach(
+          (unboxedEnumValue, method) -> {
+            CfLabel dest = new CfLabel();
+            instructions.add(new CfLoad(ValueType.fromDexType(factory.intType), 0));
+            instructions.add(new CfConstNumber(unboxedEnumValue, ValueType.INT));
+            instructions.add(new CfIfCmp(IfType.NE, ValueType.INT, dest));
+            addReturnInvoke(instructions, method);
+            instructions.add(dest);
+            instructions.add(frameBuilder.build());
+          });
+
+      addReturnInvoke(instructions, superEnumMethod);
+      return new CfCodeWithLens(getHolder(), defaultMaxStack(), defaultMaxLocals(), instructions);
+    }
+
+    private void addReturnInvoke(List<CfInstruction> instructions, DexMethod method) {
+      int localIndex = 0;
+      for (DexType parameterType : method.getParameters()) {
+        instructions.add(new CfLoad(ValueType.fromDexType(parameterType), localIndex));
+        localIndex += parameterType.getRequiredRegisters();
+      }
+      instructions.add(new CfInvoke(Opcodes.INVOKESTATIC, method, false));
+      instructions.add(
+          method.getReturnType().isVoidType()
+              ? new CfReturnVoid()
+              : new CfReturn(ValueType.fromDexType(method.getReturnType())));
+    }
+
+    public static class CfCodeWithLens extends CfCode {
+      private GraphLens codeLens;
+
+      public void setCodeLens(GraphLens codeLens) {
+        this.codeLens = codeLens;
+      }
+
+      public CfCodeWithLens(
+          DexType originalHolder, int maxStack, int maxLocals, List<CfInstruction> instructions) {
+        super(originalHolder, maxStack, maxLocals, instructions);
+      }
+
+      @Override
+      public GraphLens getCodeLens(AppView<?> appView) {
+        assert codeLens != null;
+        return codeLens;
+      }
+    }
+  }
+
   public static class EnumUnboxingInstanceFieldCfCodeProvider extends EnumUnboxingCfCodeProvider {
 
     private final DexType returnType;
diff --git a/src/main/java/com/android/tools/r8/profile/art/ArtProfileCompletenessChecker.java b/src/main/java/com/android/tools/r8/profile/art/ArtProfileCompletenessChecker.java
index d7fde7d..06464c7 100644
--- a/src/main/java/com/android/tools/r8/profile/art/ArtProfileCompletenessChecker.java
+++ b/src/main/java/com/android/tools/r8/profile/art/ArtProfileCompletenessChecker.java
@@ -85,7 +85,9 @@
       ProgramDefinition definition,
       Set<CompletenessExceptions> completenessExceptions,
       List<DexReference> missing) {
-    if (completenessExceptions.contains(ALLOW_MISSING_ENUM_UNBOXING_UTILITY_METHODS)) {
+    // TODO(b/274030968): Fix profile for enum unboxing with subtypes.
+    if (appView.options().testing.enableEnumWithSubtypesUnboxing
+        || completenessExceptions.contains(ALLOW_MISSING_ENUM_UNBOXING_UTILITY_METHODS)) {
       DexType contextType = definition.getContextType();
       SyntheticItems syntheticItems = appView.getSyntheticItems();
       if (syntheticItems.isSynthetic(contextType)) {
diff --git a/src/main/java/com/android/tools/r8/utils/collections/LongLivedProgramMethodSetBuilder.java b/src/main/java/com/android/tools/r8/utils/collections/LongLivedProgramMethodSetBuilder.java
index 5bc7e56..81578f8 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/LongLivedProgramMethodSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/LongLivedProgramMethodSetBuilder.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexDefinitionSupplier;
+import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ProgramMethod;
@@ -166,7 +167,9 @@
       DexMethod rewrittenMethod =
           appView.graphLens().getRenamedMethodSignature(method, appliedGraphLens);
       DexProgramClass holder = appView.definitionForHolder(rewrittenMethod).asProgramClass();
-      result.createAndAdd(holder, holder.lookupMethod(rewrittenMethod));
+      DexEncodedMethod definition = holder.lookupMethod(rewrittenMethod);
+      assert definition != null;
+      result.createAndAdd(holder, definition);
     }
     return result;
   }
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
index 4690144..420b2e9 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
@@ -61,7 +61,7 @@
     options.enableEnumSwitchMapRemoval = enumValueOptimization;
   }
 
-  static List<Object[]> enumUnboxingTestParameters() {
+  public static List<Object[]> enumUnboxingTestParameters() {
     return enumUnboxingTestParameters(
         getTestParameters().withDexRuntimes().withAllApiLevels().build());
   }
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/enummerging/BasicEnumMergingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/BasicEnumMergingTest.java
new file mode 100644
index 0000000..56f3d9e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/BasicEnumMergingTest.java
@@ -0,0 +1,130 @@
+// Copyright (c) 2023, 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.enummerging;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.enumunboxing.EnumUnboxingTestBase;
+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 BasicEnumMergingTest extends EnumUnboxingTestBase {
+
+  private final TestParameters parameters;
+  private final boolean enumValueOptimization;
+  private final EnumKeepRules enumKeepRules;
+
+  @Parameters(name = "{0} valueOpt: {1} keep: {2}")
+  public static List<Object[]> data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public BasicEnumMergingTest(
+      TestParameters parameters, boolean enumValueOptimization, EnumKeepRules enumKeepRules) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+  }
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(BasicEnumMergingTest.class)
+        .addKeepMainRule(Main.class)
+        .addKeepRules(enumKeepRules.getKeepRules())
+        .addOptionsModification(opt -> opt.testing.enableEnumWithSubtypesUnboxing = true)
+        .addEnumUnboxingInspector(
+            inspector ->
+                inspector
+                    .assertUnboxed(
+                        EnumWithVirtualOverride.class,
+                        EnumWithVirtualOverrideAndPrivateMethod.class,
+                        EnumWithVirtualOverrideWide.class)
+                    .assertNotUnboxed(EnumWithVirtualOverrideAndPrivateField.class))
+        .enableInliningAnnotations()
+        .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+        .addOptionsModification(opt -> opt.testing.enableEnumUnboxingDebugLogs = true)
+        .setMinApi(parameters)
+        .allowDiagnosticInfoMessages()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("a", "B", "a", "B", "a", "B", "A 1 1.0 A", "B");
+  }
+
+  enum EnumWithVirtualOverride {
+    A {
+      public void method() {
+        System.out.println("a");
+      }
+    },
+    B;
+
+    public void method() {
+      System.out.println(name());
+    }
+  }
+
+  enum EnumWithVirtualOverrideAndPrivateMethod {
+    A {
+      @NeverInline
+      private void privateMethod() {
+        System.out.println("a");
+      }
+
+      public void methodpm() {
+        privateMethod();
+      }
+    },
+    B;
+
+    public void methodpm() {
+      System.out.println(name());
+    }
+  }
+
+  enum EnumWithVirtualOverrideWide {
+    A {
+      public void methodpmw(long l1, double d2, EnumWithVirtualOverrideWide itself) {
+        System.out.println("A " + l1 + " " + d2 + " " + itself);
+      }
+    },
+    B;
+
+    public void methodpmw(long l1, double d2, EnumWithVirtualOverrideWide itself) {
+      System.out.println(name());
+    }
+  }
+
+  enum EnumWithVirtualOverrideAndPrivateField {
+    A {
+      private String a = "a";
+
+      public void methodpf() {
+        System.out.println(a);
+      }
+    },
+    B;
+
+    public void methodpf() {
+      System.out.println(name());
+    }
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      EnumWithVirtualOverrideAndPrivateMethod.A.methodpm();
+      EnumWithVirtualOverrideAndPrivateMethod.B.methodpm();
+      EnumWithVirtualOverrideAndPrivateField.A.methodpf();
+      EnumWithVirtualOverrideAndPrivateField.B.methodpf();
+      EnumWithVirtualOverride.A.method();
+      EnumWithVirtualOverride.B.method();
+      EnumWithVirtualOverrideWide.A.methodpmw(1L, 1.0, EnumWithVirtualOverrideWide.A);
+      EnumWithVirtualOverrideWide.B.methodpmw(1L, 1.0, EnumWithVirtualOverrideWide.A);
+    }
+  }
+}