Reland "Fix enum merging with abstract method error"

This reverts commit 63507f057b56d22c7f9f3cdeae7652f3c5258f5e.

Change-Id: I95c1fce94f36a4fc5c7ca20f5abb27158706b921
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 32f0cbd..2db3697 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -327,6 +327,8 @@
       createString("Ljava/lang/IllegalAccessError;");
   public final DexString illegalArgumentExceptionDescriptor =
       createString("Ljava/lang/IllegalArgumentException;");
+  public final DexString abstractMethodErrorDescriptor =
+      createString("Ljava/lang/AbstractMethodError;");
   public final DexString icceDescriptor = createString("Ljava/lang/IncompatibleClassChangeError;");
   public final DexString exceptionInInitializerErrorDescriptor =
       createString("Ljava/lang/ExceptionInInitializerError;");
@@ -573,6 +575,8 @@
       createStaticallyKnownType(illegalAccessErrorDescriptor);
   public final DexType illegalArgumentExceptionType =
       createStaticallyKnownType(illegalArgumentExceptionDescriptor);
+  public final DexType abstractMethodErrorType =
+      createStaticallyKnownType(abstractMethodErrorDescriptor);
   public final DexType icceType = createStaticallyKnownType(icceDescriptor);
   public final DexType exceptionInInitializerErrorType =
       createStaticallyKnownType(exceptionInInitializerErrorDescriptor);
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 6ba6ad1..7449cee 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
@@ -7,6 +7,7 @@
 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.cf.CfVersion;
 import com.android.tools.r8.contexts.CompilationContext.ProcessorContext;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClassAndMethod;
@@ -60,8 +61,9 @@
 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.CfCodeWithLens;
 import com.android.tools.r8.ir.synthetic.EnumUnboxingCfCodeProvider.EnumUnboxingMethodDispatchCfCodeProvider;
-import com.android.tools.r8.ir.synthetic.EnumUnboxingCfCodeProvider.EnumUnboxingMethodDispatchCfCodeProvider.CfCodeWithLens;
+import com.android.tools.r8.ir.synthetic.ThrowCfCodeProvider;
 import com.android.tools.r8.profile.rewriting.ProfileCollectionAdditions;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.ImmutableArrayUtils;
@@ -654,10 +656,13 @@
     // and at least one override.
     DexMethod reference = nonPrivateVirtualMethod.withHolder(unboxedEnum.getType(), factory);
     ProgramMethodSet subimplementations = ProgramMethodSet.create();
+    boolean allImplements = true;
     for (DexProgramClass subEnum : subEnums) {
       ProgramMethod subMethod = subEnum.lookupProgramMethod(reference);
       if (subMethod != null) {
         subimplementations.add(subMethod);
+      } else {
+        allImplements = false;
       }
     }
     DexClassAndMethod superMethod = unboxedEnum.lookupProgramMethod(reference);
@@ -666,60 +671,73 @@
       superMethod = appView.appInfo().lookupSuperTarget(reference, unboxedEnum, appView);
       assert superMethod == null || superMethod.getReference() == factory.enumMembers.toString;
     }
-    if (superMethod == null || subimplementations.isEmpty()) {
-      // No emulated dispatch is required, just move everything.
-      // If an abstract method with no implementors is found, effectively don't do anything.
-      if (superMethod != null && !superMethod.getAccessFlags().isAbstract()) {
-        assert superMethod.isProgramMethod();
-        directMoveAndMap(localUtilityClass, localUtilityMethods, superMethod.asProgramMethod());
-      }
+    if (superMethod == null) {
+      // No effective virtual dispatch is required, just move each subimplementation.
       for (ProgramMethod override : subimplementations) {
+        assert !override.getAccessFlags().isAbstract();
         directMoveAndMap(localUtilityClass, localUtilityMethods, override);
       }
       return;
     }
-    if (superMethod.getDefinition().isAbstract() && subimplementations.size() == 1) {
-      // No emulated dispatch is required, just forward everything to the unique implementation.
-      ProgramMethod override = subimplementations.iterator().next();
-      DexMethod uniqueUtility = directMoveAndMap(localUtilityClass, localUtilityMethods, override);
-      lensBuilder.mapToDispatch(superMethod.getReference(), uniqueUtility);
+    if (superMethod.getAccessFlags().isAbstract()) {
+      if (subimplementations.isEmpty()) {
+        // Abstract method with no implementors: rewrite to abstract method error.
+        directMoveAndMap(
+            localUtilityClass, localUtilityMethods, superMethod.asProgramMethod(), true);
+      } else if (!allImplements) {
+        // The abstract method is missing implementors, so we need to remap all missing
+        // implementation to an abstract method error.
+        emulatedDispatchMoveAndMap(
+            localUtilityClass, localUtilityMethods, superMethod, subimplementations, true);
+      } else if (subimplementations.size() == 1) {
+        // Single implementor, no emulated dispatch is required, just forward everything to the
+        // unique implementation.
+        assert allImplements;
+        ProgramMethod override = subimplementations.iterator().next();
+        DexMethod uniqueUtility =
+            directMoveAndMap(localUtilityClass, localUtilityMethods, override);
+        lensBuilder.mapToDispatch(superMethod.getReference(), uniqueUtility);
+      } else {
+        // Multiple implementors, the abstract method is entirely implemented, no need to
+        // introduce the call to the abstract method error.
+        emulatedDispatchMoveAndMap(
+            localUtilityClass, localUtilityMethods, superMethod, subimplementations, false);
+      }
       return;
     }
-    // These methods require emulated dispatch.
+    assert !superMethod.getAccessFlags().isAbstract();
+    // No override, no effective virtual dispatch, just forward to the unique implementation.
+    if (subimplementations.isEmpty()) {
+      assert superMethod.isProgramMethod();
+      directMoveAndMap(localUtilityClass, localUtilityMethods, superMethod.asProgramMethod());
+      return;
+    }
+    // Emulated dispatch with a default case on the super enum.
     emulatedDispatchMoveAndMap(
-        localUtilityClass, localUtilityMethods, superMethod, subimplementations);
+        localUtilityClass, localUtilityMethods, superMethod, subimplementations, false);
   }
 
   private void emulatedDispatchMoveAndMap(
       LocalEnumUnboxingUtilityClass localUtilityClass,
       Map<DexMethod, DexEncodedMethod> localUtilityMethods,
       DexClassAndMethod superMethod,
-      ProgramMethodSet unorderedSubimplementations) {
+      ProgramMethodSet unorderedSubimplementations,
+      boolean needsAbstractMethodErrorCase) {
     assert !unorderedSubimplementations.isEmpty();
     DexMethod superUtilityMethod;
     List<ProgramMethod> sortedSubimplementations = new ArrayList<>(unorderedSubimplementations);
     sortedSubimplementations.sort(Comparator.comparing(ProgramMethod::getHolderType));
-    if (superMethod.isProgramMethod()) {
-      superUtilityMethod =
-          installLocalUtilityMethod(
-              localUtilityClass, localUtilityMethods, superMethod.asProgramMethod());
-    } else {
-      // All methods but toString() are final or non-virtual.
-      // We could support other cases by setting correctly the superUtilityMethod here.
-      assert superMethod.getReference().match(factory.enumMembers.toString);
-      ProgramMethod toString = localUtilityClass.ensureToStringMethod(appView);
-      superUtilityMethod = toString.getReference();
-      for (ProgramMethod context : sortedSubimplementations) {
-        // If the utility method is used only from the dispatch method, we have to process it and
-        // add it to the ArtProfile.
-        methodsToProcess.add(toString);
-        profileCollectionAdditions.addMethodIfContextIsInProfile(toString, context);
-      }
-    }
+    superUtilityMethod =
+        computeSuperUtilityMethod(
+            localUtilityClass,
+            localUtilityMethods,
+            superMethod,
+            sortedSubimplementations,
+            needsAbstractMethodErrorCase);
     Map<DexMethod, DexMethod> overrideToUtilityMethods = new IdentityHashMap<>();
     for (ProgramMethod subMethod : sortedSubimplementations) {
       DexMethod subEnumLocalUtilityMethod =
-          installLocalUtilityMethod(localUtilityClass, localUtilityMethods, subMethod);
+          installLocalUtilityMethod(localUtilityClass, localUtilityMethods, subMethod, false);
       assert subEnumLocalUtilityMethod != null;
       overrideToUtilityMethods.put(subMethod.getReference(), subEnumLocalUtilityMethod);
     }
@@ -748,13 +766,58 @@
     }
   }
 
+  private DexMethod computeSuperUtilityMethod(
+      LocalEnumUnboxingUtilityClass localUtilityClass,
+      Map<DexMethod, DexEncodedMethod> localUtilityMethods,
+      DexClassAndMethod superMethod,
+      List<ProgramMethod> sortedSubimplementations,
+      boolean needsAbstractMethodErrorCase) {
+    DexMethod superUtilityMethod;
+    if (superMethod.isProgramMethod()) {
+      if (needsAbstractMethodErrorCase) {
+        assert superMethod.getAccessFlags().isAbstract();
+        superUtilityMethod =
+            installLocalUtilityMethod(
+                localUtilityClass, localUtilityMethods, superMethod.asProgramMethod(), true);
+      } else if (!superMethod.getAccessFlags().isAbstract()) {
+        superUtilityMethod =
+            installLocalUtilityMethod(
+                localUtilityClass, localUtilityMethods, superMethod.asProgramMethod(), false);
+      } else {
+        assert superMethod.getAccessFlags().isAbstract();
+        superUtilityMethod = null;
+      }
+    } else {
+      // All methods but toString() are final or non-virtual.
+      // We could support other cases by setting correctly the superUtilityMethod here.
+      assert superMethod.getReference().match(factory.enumMembers.toString);
+      ProgramMethod toString = localUtilityClass.ensureToStringMethod(appView);
+      superUtilityMethod = toString.getReference();
+      for (ProgramMethod context : sortedSubimplementations) {
+        // If the utility method is used only from the dispatch method, we have to process it and
+        // add it to the ArtProfile.
+        methodsToProcess.add(toString);
+        profileCollectionAdditions.addMethodIfContextIsInProfile(toString, context);
+      }
+    }
+    return superUtilityMethod;
+  }
+
   private DexMethod directMoveAndMap(
       LocalEnumUnboxingUtilityClass localUtilityClass,
       Map<DexMethod, DexEncodedMethod> localUtilityMethods,
       ProgramMethod method) {
-    assert !method.getAccessFlags().isAbstract();
+    return directMoveAndMap(localUtilityClass, localUtilityMethods, method, false);
+  }
+
+  private DexMethod directMoveAndMap(
+      LocalEnumUnboxingUtilityClass localUtilityClass,
+      Map<DexMethod, DexEncodedMethod> localUtilityMethods,
+      ProgramMethod method,
+      boolean abstractMethodError) {
     DexMethod utilityMethod =
-        installLocalUtilityMethod(localUtilityClass, localUtilityMethods, method);
+        installLocalUtilityMethod(
+            localUtilityClass, localUtilityMethods, method, abstractMethodError);
     assert utilityMethod != null;
     lensBuilder.moveAndMap(method.getReference(), utilityMethod, method.getDefinition().isStatic());
     return utilityMethod;
@@ -829,15 +892,15 @@
   private DexMethod installLocalUtilityMethod(
       LocalEnumUnboxingUtilityClass localUtilityClass,
       Map<DexMethod, DexEncodedMethod> localUtilityMethods,
-      ProgramMethod method) {
-    if (method.getAccessFlags().isAbstract()) {
-      return null;
-    }
+      ProgramMethod method,
+      boolean abstractMethodError) {
+    assert abstractMethodError || !method.getAccessFlags().isAbstract();
+    Predicate<DexMethod> isFresh =
+        newMethodSignature -> !localUtilityMethods.containsKey(newMethodSignature);
     DexEncodedMethod newLocalUtilityMethod =
-        createLocalUtilityMethod(
-            method,
-            localUtilityClass,
-            newMethodSignature -> !localUtilityMethods.containsKey(newMethodSignature));
+        abstractMethodError
+            ? createAbstractMethodErrorLocalUtilityMethod(method, localUtilityClass, isFresh)
+            : createLocalUtilityMethod(method, localUtilityClass, isFresh);
     assert !localUtilityMethods.containsKey(newLocalUtilityMethod.getReference());
     localUtilityMethods.put(newLocalUtilityMethod.getReference(), newLocalUtilityMethod);
     return newLocalUtilityMethod.getReference();
@@ -847,40 +910,78 @@
       ProgramMethod method,
       LocalEnumUnboxingUtilityClass localUtilityClass,
       Predicate<DexMethod> availableMethodSignatures) {
-    DexMethod methodReference = method.getReference();
-
-    // Create a new, fresh method signature on the local utility class. We prefix the method by "_"
-    // such that this does not collide with the utility methods we synthesize for unboxing.
+    assert !method.getAccessFlags().isAbstract();
     DexMethod newMethod =
-        method.getDefinition().isClassInitializer()
-            ? factory.createClassInitializer(localUtilityClass.getType())
-            : factory.createFreshMethodNameWithoutHolder(
-                "_" + method.getName().toString(),
-                fixupProto(
-                    method.getAccessFlags().isStatic()
-                        ? method.getProto()
-                        : factory.prependHolderToProto(methodReference)),
-                localUtilityClass.getType(),
-                availableMethodSignatures);
-
+        createFreshMethodSignature(
+            method, localUtilityClass, availableMethodSignatures, method.getReference());
     return method
         .getDefinition()
         .toTypeSubstitutedMethod(
             newMethod,
             builder ->
-                builder
-                    .clearAllAnnotations()
-                    .modifyAccessFlags(
-                        accessFlags -> {
-                          if (method.getDefinition().isClassInitializer()) {
-                            assert accessFlags.isStatic();
-                          } else {
-                            accessFlags.promoteToPublic();
-                            accessFlags.promoteToStatic();
-                          }
-                        })
-                    .setCompilationState(method.getDefinition().getCompilationState())
-                    .unsetIsLibraryMethodOverride());
+                transformMethodForLocalUtility(builder, method)
+                    .setCompilationState(method.getDefinition().getCompilationState()));
+  }
+
+  private DexEncodedMethod createAbstractMethodErrorLocalUtilityMethod(
+      ProgramMethod method,
+      LocalEnumUnboxingUtilityClass localUtilityClass,
+      Predicate<DexMethod> availableMethodSignatures) {
+    assert method.getAccessFlags().isAbstract();
+    DexMethod newMethod =
+        createFreshMethodSignature(
+            method, localUtilityClass, availableMethodSignatures, method.getReference());
+    DexEncodedMethod dexEncodedMethod =
+        method
+            .getDefinition()
+            .toTypeSubstitutedMethod(
+                newMethod,
+                builder ->
+                    transformMethodForLocalUtility(builder, method)
+                        .modifyAccessFlags(MethodAccessFlags::unsetAbstract)
+                        .setCode(
+                            new ThrowCfCodeProvider(
+                                    appView, newMethod, factory.abstractMethodErrorType)
+                                .generateCfCode())
+                        .setClassFileVersion(CfVersion.V1_8)
+                        .setApiLevelForDefinition(appView.computedMinApiLevel())
+                        .setApiLevelForCode(appView.computedMinApiLevel()));
+    methodsToProcess.add(new ProgramMethod(localUtilityClass.getDefinition(), dexEncodedMethod));
+    return dexEncodedMethod;
+  }
+
+  private DexEncodedMethod.Builder transformMethodForLocalUtility(
+      DexEncodedMethod.Builder builder, ProgramMethod method) {
+    builder
+        .clearAllAnnotations()
+        .modifyAccessFlags(
+            accessFlags -> {
+              if (method.getDefinition().isClassInitializer()) {
+                assert accessFlags.isStatic();
+              } else {
+                accessFlags.promoteToPublic();
+                accessFlags.promoteToStatic();
+              }
+            })
+        .unsetIsLibraryMethodOverride();
+    return builder;
+  }
+
+  private DexMethod createFreshMethodSignature(
+      ProgramMethod method,
+      LocalEnumUnboxingUtilityClass localUtilityClass,
+      Predicate<DexMethod> availableMethodSignatures,
+      DexMethod methodReference) {
+    return method.getDefinition().isClassInitializer()
+        ? factory.createClassInitializer(localUtilityClass.getType())
+        : factory.createFreshMethodNameWithoutHolder(
+            "_" + method.getName().toString(),
+            fixupProto(
+                method.getAccessFlags().isStatic()
+                    ? method.getProto()
+                    : factory.prependHolderToProto(methodReference)),
+            localUtilityClass.getType(),
+            availableMethodSignatures);
   }
 
   private boolean isPrunedAfterEnumUnboxing(ProgramField field, EnumData enumData) {
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 112ffdd..f60fe64 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
@@ -169,25 +169,6 @@
               ? 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 {
@@ -322,4 +303,24 @@
       return standardCfCodeFromInstructions(instructions);
     }
   }
+
+  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;
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/ThrowCfCodeProvider.java b/src/main/java/com/android/tools/r8/ir/synthetic/ThrowCfCodeProvider.java
new file mode 100644
index 0000000..becf6d3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/ThrowCfCodeProvider.java
@@ -0,0 +1,46 @@
+// 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.ir.synthetic;
+
+import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
+
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.cf.code.CfNew;
+import com.android.tools.r8.cf.code.CfStackInstruction;
+import com.android.tools.r8.cf.code.CfStackInstruction.Opcode;
+import com.android.tools.r8.cf.code.CfThrow;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Generates a method that just throws a exception with empty <init></init> with *any* signature
+ * passed, so the method can be inserted in a hierarchy and be called with normal virtual dispatch.
+ */
+public class ThrowCfCodeProvider extends SyntheticCfCodeProvider {
+
+  private final DexMethod method;
+  private final DexType exceptionType;
+
+  public ThrowCfCodeProvider(AppView<?> appView, DexMethod method, DexType exceptionType) {
+    super(appView, method.getHolderType());
+    this.method = method;
+    this.exceptionType = exceptionType;
+  }
+
+  @Override
+  public CfCode generateCfCode() {
+    List<CfInstruction> instructions = new ArrayList<>();
+    instructions.add(new CfNew(exceptionType));
+    instructions.add(new CfStackInstruction(Opcode.Dup));
+    DexMethod init = appView.dexItemFactory().createInstanceInitializer(exceptionType);
+    instructions.add(new CfInvoke(INVOKESPECIAL, init, false));
+    instructions.add(new CfThrow());
+    return standardCfCodeFromInstructions(instructions);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/enummerging/AbstractMethodErrorEnumMergingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/AbstractMethodErrorEnumMergingTest.java
new file mode 100644
index 0000000..7937ef7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/enummerging/AbstractMethodErrorEnumMergingTest.java
@@ -0,0 +1,174 @@
+// 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 static com.android.tools.r8.ToolHelper.getClassFilesForInnerClasses;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.enumunboxing.EnumUnboxingTestBase;
+import com.android.tools.r8.utils.StringUtils;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+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 AbstractMethodErrorEnumMergingTest extends EnumUnboxingTestBase {
+
+  private final TestParameters parameters;
+  private final boolean enumValueOptimization;
+  private final EnumKeepRules enumKeepRules;
+  private final String EXPECTED_RESULT =
+      StringUtils.lines(
+          "class java.lang.AbstractMethodError",
+          "74",
+          "class java.lang.AbstractMethodError",
+          "44",
+          "class java.lang.AbstractMethodError",
+          "class java.lang.AbstractMethodError");
+
+  @Parameters(name = "{0} valueOpt: {1} keep: {2}")
+  public static List<Object[]> data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public AbstractMethodErrorEnumMergingTest(
+      TestParameters parameters, boolean enumValueOptimization, EnumKeepRules enumKeepRules) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    testForD8(parameters.getBackend())
+        .addProgramClassFileData(inputProgram())
+        .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(inputProgram())
+        .addKeepMainRule(Main.class)
+        .addKeepRules(enumKeepRules.getKeepRules())
+        .addEnumUnboxingInspector(
+            inspector -> inspector.assertUnboxed(MyEnum2Cases.class, MyEnum1Case.class))
+        .enableInliningAnnotations()
+        .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+        .setMinApi(parameters)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(EXPECTED_RESULT);
+  }
+
+  private List<byte[]> inputProgram() throws Exception {
+    Collection<Path> files = getClassFilesForInnerClasses(getClass());
+    List<byte[]> result = new ArrayList<>();
+    int changed = 0;
+    for (Path file : files) {
+      String fileName = file.getFileName().toString();
+      if (fileName.equals("AbstractMethodErrorEnumMergingTest$MyEnum1Case$1.class")
+          || fileName.equals("AbstractMethodErrorEnumMergingTest$MyEnum2Cases$1.class")) {
+        result.add(transformer(file, null).removeMethodsWithName("operate").transform());
+        changed++;
+      } else {
+        result.add(Files.readAllBytes(file));
+      }
+    }
+    assertEquals(2, changed);
+    return result;
+  }
+
+  enum MyEnum2Cases {
+    A(8) {
+      // Will be removed by transformation before compilation.
+      @NeverInline
+      @Override
+      public long operate(long another) {
+        throw new RuntimeException("Should have been removed");
+      }
+    },
+    B(32) {
+      @NeverInline
+      @Override
+      public long operate(long another) {
+        return num + another;
+      }
+    };
+    final long num;
+
+    MyEnum2Cases(long num) {
+      this.num = num;
+    }
+
+    public abstract long operate(long another);
+  }
+
+  enum MyEnum1Case {
+    A(8) {
+      // Will be removed by transformation before compilation.
+      @NeverInline
+      @Override
+      public long operate(long another) {
+        throw new RuntimeException("Should have been removed");
+      }
+    };
+    final long num;
+
+    MyEnum1Case(long num) {
+      this.num = num;
+    }
+
+    public abstract long operate(long another);
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      try {
+        System.out.println(MyEnum2Cases.A.operate(42));
+      } catch (Throwable t) {
+        System.out.println(t.getClass());
+      }
+      System.out.println(MyEnum2Cases.B.operate(42));
+      try {
+        System.out.println(indirect(MyEnum2Cases.A));
+      } catch (Throwable t) {
+        System.out.println(t.getClass());
+      }
+      System.out.println(indirect(MyEnum2Cases.B));
+
+      try {
+        System.out.println(MyEnum1Case.A.operate(42));
+      } catch (Throwable t) {
+        System.out.println(t.getClass());
+      }
+      try {
+        System.out.println(indirect(MyEnum1Case.A));
+      } catch (Throwable t) {
+        System.out.println(t.getClass());
+      }
+    }
+
+    @NeverInline
+    public static long indirect(MyEnum2Cases e) {
+      return e.operate(12);
+    }
+
+    @NeverInline
+    public static long indirect(MyEnum1Case e) {
+      return e.operate(7);
+    }
+  }
+}