Support for Objects#hashCode in enum unboxer

Bug: b/326085657
Change-Id: I208618b5256edf71df8f0201c46108cbd7acbef5
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 4e2f94e..00f2da3 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -438,6 +438,7 @@
   public final DexType classArrayType = createStaticallyKnownType(classArrayDescriptor);
   public final DexType enumType = createStaticallyKnownType(enumDescriptor);
   public final DexType annotationType = createStaticallyKnownType(annotationDescriptor);
+  public final DexType arraysType = createStaticallyKnownType(arraysDescriptor);
   public final DexType objectsType = createStaticallyKnownType(objectsDescriptor);
   public final DexType collectionsType = createStaticallyKnownType(collectionsDescriptor);
   public final DexType iterableType = createStaticallyKnownType(iterableDescriptor);
@@ -1407,6 +1408,8 @@
   public class JavaUtilArraysMethods {
 
     public final DexMethod asList;
+    public final DexMethod hashCode =
+        createMethod(arraysType, createProto(intType, objectArrayType), "hashCode");
     public final DexMethod equalsObjectArray;
 
     private JavaUtilArraysMethods() {
@@ -1794,6 +1797,8 @@
 
     public final DexMethod equals =
         createMethod(objectsType, createProto(booleanType, objectType, objectType), "equals");
+    public final DexMethod hash =
+        createMethod(objectsType, createProto(intType, objectArrayType), "hash");
     public final DexMethod hashCode =
         createMethod(objectsType, createProto(intType, objectType), "hashCode");
     public final DexMethod isNull =
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
index 45a4545..9e1a36e 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
@@ -66,8 +66,7 @@
 public class PolicyScheduler {
 
   public static List<Policy> getPolicies(
-      AppView<?> appView,
-      RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
+      AppView<?> appView, RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
     if (appView.hasClassHierarchy()) {
       return getPoliciesForR8(appView.withClassHierarchy(), runtimeTypeCheckInfo);
     } else {
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
index dbb9d1a..1eb69e7 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
@@ -179,7 +179,7 @@
   public InstructionListIterator addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
       IRCode code,
       BasicBlockIterator blockIterator,
-      Collection<Instruction> instructionsToAdd,
+      Collection<? extends Instruction> instructionsToAdd,
       InternalOptions options) {
     // Assert that we are not inserting after the final jump, and also store peekNext() for later.
     Instruction origNext = null;
@@ -196,14 +196,14 @@
       BasicBlockInstructionListIterator dstIterator,
       IRCode code,
       BasicBlockIterator blockIterator,
-      Collection<Instruction> instructionsToAdd,
+      Collection<? extends Instruction> instructionsToAdd,
       InternalOptions options) {
     if (!dstIterator.block.hasCatchHandlers() || instructionsToAdd.isEmpty()) {
       dstIterator.addAll(instructionsToAdd);
       return dstIterator;
     }
 
-    Iterator<Instruction> srcIterator = instructionsToAdd.iterator();
+    Iterator<? extends Instruction> srcIterator = instructionsToAdd.iterator();
 
     // If the throwing instruction is before the cursor, then we must split the block first.
     // If there is one afterwards, we can add instructions and when we split, the throwing one
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
index 0b2be34..6b33cca 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
@@ -238,7 +238,7 @@
   public InstructionListIterator addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
       IRCode code,
       BasicBlockIterator blockIterator,
-      Collection<Instruction> instructionsToAdd,
+      Collection<? extends Instruction> instructionsToAdd,
       InternalOptions options) {
     return instructionIterator.addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
         code, blockIterator, instructionsToAdd, options);
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
index e612141..19c675b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
@@ -36,13 +36,13 @@
     }
   }
 
-  default void addAll(Collection<Instruction> instructions) {
+  default void addAll(Collection<? extends Instruction> instructions) {
     for (Instruction instruction : instructions) {
       add(instruction);
     }
   }
 
-  default boolean addUntilThrowing(Iterator<Instruction> srcIterator) {
+  default boolean addUntilThrowing(Iterator<? extends Instruction> srcIterator) {
     while (srcIterator.hasNext()) {
       // Add all non-throwing instructions up until the first throwing instruction.
       Instruction instruction = srcIterator.next();
@@ -57,7 +57,7 @@
   InstructionListIterator addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
       IRCode code,
       BasicBlockIterator blockIterator,
-      Collection<Instruction> instructionsToAdd,
+      Collection<? extends Instruction> instructionsToAdd,
       InternalOptions options);
 
   default InstructionListIterator addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
diff --git a/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
index 0bb697e..e3ac085 100644
--- a/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
@@ -199,7 +199,7 @@
   public InstructionListIterator addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
       IRCode code,
       BasicBlockIterator blockIterator,
-      Collection<Instruction> instructionsToAdd,
+      Collection<? extends Instruction> instructionsToAdd,
       InternalOptions options) {
     return currentBlockIterator.addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
         code, blockIterator, instructionsToAdd, options);
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewArrayFilled.java b/src/main/java/com/android/tools/r8/ir/code/NewArrayFilled.java
index 11dce59..c5d6c05 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NewArrayFilled.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NewArrayFilled.java
@@ -38,6 +38,10 @@
     this.type = type;
   }
 
+  public static Builder builder() {
+    return new Builder();
+  }
+
   @Override
   public int opcode() {
     return Opcodes.NEW_ARRAY_FILLED;
@@ -241,4 +245,30 @@
   public void buildLir(LirBuilder<Value, ?> builder) {
     builder.addNewArrayFilled(getArrayType(), arguments());
   }
+
+  public static class Builder extends BuilderBase<Builder, NewArrayFilled> {
+
+    private List<Value> elements;
+    private DexType type;
+
+    public Builder setElements(List<Value> elements) {
+      this.elements = elements;
+      return this;
+    }
+
+    public Builder setType(DexType type) {
+      this.type = type;
+      return this;
+    }
+
+    @Override
+    public NewArrayFilled build() {
+      return amend(new NewArrayFilled(type, outValue, elements));
+    }
+
+    @Override
+    public Builder self() {
+      return this;
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/backports/ObjectsMethodRewrites.java b/src/main/java/com/android/tools/r8/ir/desugar/backports/ObjectsMethodRewrites.java
index 20998db..96be152 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/backports/ObjectsMethodRewrites.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/backports/ObjectsMethodRewrites.java
@@ -9,7 +9,6 @@
 import com.android.tools.r8.cf.code.CfStackInstruction;
 import com.android.tools.r8.cf.code.CfStackInstruction.Opcode;
 import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.desugar.BackportedMethodRewriter.FullMethodInvokeRewriter;
 import com.android.tools.r8.ir.desugar.BackportedMethodRewriter.MethodInvokeRewriter;
 import com.android.tools.r8.ir.desugar.LocalStackAllocator;
@@ -20,13 +19,8 @@
 public final class ObjectsMethodRewrites {
 
   public static MethodInvokeRewriter rewriteToArraysHashCode() {
-    return (invoke, factory) -> {
-      DexType arraysType = factory.createType(factory.arraysDescriptor);
-      return new CfInvoke(
-          Opcodes.INVOKESTATIC,
-          factory.createMethod(arraysType, invoke.getMethod().proto, "hashCode"),
-          false);
-    };
+    return (invoke, factory) ->
+        new CfInvoke(Opcodes.INVOKESTATIC, factory.javaUtilArraysMethods.hashCode, false);
   }
 
   public static MethodInvokeRewriter rewriteRequireNonNull() {
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 3470908..4734657 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
@@ -1268,16 +1268,17 @@
       assert false;
       return Reason.INVALID_INVOKE_NEW_ARRAY;
     }
-    if (enumClass.type.isNotIdenticalTo(arrayBaseType.getClassType())) {
+    if (enumClass.getType().isIdenticalTo(arrayBaseType.getClassType())) {
+      if (Iterables.all(
+          newArrayFilled.arguments(), argument -> isAssignableToArray(argument, arrayBaseType))) {
+        return Reason.ELIGIBLE;
+      }
       return Reason.INVALID_INVOKE_NEW_ARRAY;
     }
-
-    for (Value value : newArrayFilled.inValues()) {
-      if (!isAssignableToArray(value, arrayBaseType)) {
-        return Reason.INVALID_INVOKE_NEW_ARRAY;
-      }
+    if (EnumUnboxerUtils.isArrayUsedOnlyForHashCode(newArrayFilled, factory)) {
+      return Reason.ELIGIBLE;
     }
-    return Reason.ELIGIBLE;
+    return Reason.INVALID_INVOKE_NEW_ARRAY;
   }
 
   private Reason analyzeCheckCastUser(CheckCast checkCast) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerUtils.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerUtils.java
new file mode 100644
index 0000000..77d819f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxerUtils.java
@@ -0,0 +1,28 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.optimize.enums;
+
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.code.NewArrayFilled;
+import com.android.tools.r8.ir.code.Value;
+
+public class EnumUnboxerUtils {
+
+  public static boolean isArrayUsedOnlyForHashCode(
+      NewArrayFilled newArrayFilled, DexItemFactory factory) {
+    Value array = newArrayFilled.outValue();
+    if (!array.hasSingleUniqueUser() || array.hasPhiUsers()) {
+      return false;
+    }
+    InvokeStatic invoke = array.singleUniqueUser().asInvokeStatic();
+    if (invoke == null) {
+      return false;
+    }
+    DexMethod invokedMethod = invoke.getInvokedMethod();
+    return invokedMethod.isIdenticalTo(factory.javaUtilArraysMethods.hashCode)
+        || invokedMethod.isIdenticalTo(factory.objectsMethods.hash);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCfMethods.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCfMethods.java
index 53e7b32..f5eeee2 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCfMethods.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCfMethods.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.cf.code.CfArithmeticBinop;
 import com.android.tools.r8.cf.code.CfArrayLength;
 import com.android.tools.r8.cf.code.CfArrayStore;
+import com.android.tools.r8.cf.code.CfConstNull;
 import com.android.tools.r8.cf.code.CfConstNumber;
 import com.android.tools.r8.cf.code.CfFrame;
 import com.android.tools.r8.cf.code.CfGoto;
@@ -43,9 +44,52 @@
 public final class EnumUnboxingCfMethods {
 
   public static void registerSynthesizedCodeReferences(DexItemFactory factory) {
+    factory.createSynthesizedType("Ljava/lang/Integer;");
     factory.createSynthesizedType("Ljava/lang/NullPointerException;");
   }
 
+  public static CfCode EnumUnboxingMethods_boxedOrdinalOrNull(
+      DexItemFactory factory, DexMethod method) {
+    CfLabel label0 = new CfLabel();
+    CfLabel label1 = new CfLabel();
+    CfLabel label2 = new CfLabel();
+    CfLabel label3 = new CfLabel();
+    return new CfCode(
+        method.holder,
+        2,
+        1,
+        ImmutableList.of(
+            label0,
+            new CfLoad(ValueType.INT, 0),
+            new CfIf(IfType.NE, ValueType.INT, label1),
+            new CfConstNull(),
+            new CfGoto(label2),
+            label1,
+            new CfFrame(
+                new Int2ObjectAVLTreeMap<>(new int[] {0}, new FrameType[] {FrameType.intType()})),
+            new CfLoad(ValueType.INT, 0),
+            new CfConstNumber(1, ValueType.INT),
+            new CfArithmeticBinop(CfArithmeticBinop.Opcode.Sub, NumericType.INT),
+            new CfInvoke(
+                184,
+                factory.createMethod(
+                    factory.createType("Ljava/lang/Integer;"),
+                    factory.createProto(factory.createType("Ljava/lang/Integer;"), factory.intType),
+                    factory.createString("valueOf")),
+                false),
+            label2,
+            new CfFrame(
+                new Int2ObjectAVLTreeMap<>(new int[] {0}, new FrameType[] {FrameType.intType()}),
+                new ArrayDeque<>(
+                    Arrays.asList(
+                        FrameType.initializedNonNullReference(
+                            factory.createType("Ljava/lang/Integer;"))))),
+            new CfReturn(ValueType.OBJECT),
+            label3),
+        ImmutableList.of(),
+        ImmutableList.of());
+  }
+
   public static CfCode EnumUnboxingMethods_compareTo(DexItemFactory factory, DexMethod method) {
     CfLabel label0 = new CfLabel();
     CfLabel label1 = new CfLabel();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
index da575cd..aabff86 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
 import static com.android.tools.r8.ir.analysis.type.Nullability.maybeNull;
+import static com.android.tools.r8.ir.optimize.enums.EnumUnboxerUtils.isArrayUsedOnlyForHashCode;
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClassAndMethod;
@@ -40,6 +41,7 @@
 import com.android.tools.r8.ir.code.NewArrayFilled;
 import com.android.tools.r8.ir.code.NewUnboxedEnumInstance;
 import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
@@ -56,6 +58,7 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.IdentityHashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -232,11 +235,61 @@
         }
       }
     }
+    if (code.metadata().mayHaveNewArrayFilled()) {
+      fixupPrimitiveElementsOfNonPrimitiveNewArrayFilled(code, convertedEnums, eventConsumer);
+    }
     code.removeRedundantBlocks();
     assert code.isConsistentSSABeforeTypesAreCorrect(appView);
     return affectedPhis;
   }
 
+  private void fixupPrimitiveElementsOfNonPrimitiveNewArrayFilled(
+      IRCode code,
+      Map<Instruction, DexType> convertedEnums,
+      EnumUnboxerMethodProcessorEventConsumer eventConsumer) {
+    BasicBlockIterator blocks = code.listIterator();
+    while (blocks.hasNext()) {
+      BasicBlock block = blocks.next();
+      InstructionListIterator instructionIterator = block.listIterator(code);
+      NewArrayFilled newArrayFilled;
+      while ((newArrayFilled = instructionIterator.nextUntil(Instruction::isNewArrayFilled))
+          != null) {
+        if (isArrayUsedOnlyForHashCode(newArrayFilled, factory)) {
+          // Insert a box operation.
+          Map<Value, InvokeStatic> argumentsToBox = new LinkedHashMap<>();
+          for (int argumentIndex = 0;
+              argumentIndex < newArrayFilled.arguments().size();
+              argumentIndex++) {
+            Value argument = newArrayFilled.getArgument(argumentIndex);
+            Position position = newArrayFilled.getPosition();
+            if (getEnumClassTypeOrNull(argument, convertedEnums) != null) {
+              InvokeStatic boxInstruction =
+                  argumentsToBox.computeIfAbsent(
+                      argument,
+                      a ->
+                          InvokeStatic.builder()
+                              .setFreshOutValue(code, factory.boxedIntType.toTypeElement(appView))
+                              .setSingleArgument(a)
+                              .setMethod(
+                                  getSharedUtilityClass()
+                                      .ensureBoxedOrdinalOrNullMethod(
+                                          appView, code.context(), eventConsumer))
+                              .setPosition(position)
+                              .build());
+              newArrayFilled.replaceValue(argumentIndex, boxInstruction.outValue());
+            }
+          }
+          if (!argumentsToBox.isEmpty()) {
+            instructionIterator.previous();
+            instructionIterator =
+                instructionIterator.addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
+                    code, blocks, argumentsToBox.values(), options);
+          }
+        }
+      }
+    }
+  }
+
   private void rewriteArrayAccess(
       IRCode code,
       Set<Phi> affectedPhis,
@@ -498,6 +551,19 @@
       Map<Instruction, DexType> convertedEnums,
       InstructionListIterator instructionIterator) {
     DexType arrayBaseType = newArrayFilled.getArrayType().toBaseType(factory);
+    if (isArrayUsedOnlyForHashCode(newArrayFilled, factory)) {
+      DexType rewrittenArrayType =
+          newArrayFilled.getArrayType().replaceBaseType(factory.objectType, factory);
+      NewArrayFilled rewrittenNewArrayFilled =
+          NewArrayFilled.builder()
+              .setElements(newArrayFilled.arguments())
+              .setFreshOutValue(
+                  code, rewrittenArrayType.toTypeElement(appView, definitelyNotNull()))
+              .setType(rewrittenArrayType)
+              .build();
+      instructionIterator.replaceCurrentInstruction(rewrittenNewArrayFilled);
+      return;
+    }
     if (!unboxedEnumsData.isUnboxedEnum(arrayBaseType)) {
       return;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/SharedEnumUnboxingUtilityClass.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/SharedEnumUnboxingUtilityClass.java
index 37c814d..07dfc5e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/SharedEnumUnboxingUtilityClass.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/SharedEnumUnboxingUtilityClass.java
@@ -74,6 +74,7 @@
     ensureEqualsMethod(appView);
     ensureObjectsEqualsMethod(appView);
     ensureOrdinalMethod(appView);
+    ensureBoxedOrdinalOrNullMethod(appView);
   }
 
   public ProgramMethod ensureCheckNotZeroMethod(
@@ -189,6 +190,25 @@
         method -> EnumUnboxingCfMethods.EnumUnboxingMethods_ordinal(dexItemFactory, method));
   }
 
+  public ProgramMethod ensureBoxedOrdinalOrNullMethod(
+      AppView<AppInfoWithLiveness> appView,
+      ProgramMethod context,
+      EnumUnboxerMethodProcessorEventConsumer eventConsumer) {
+    ProgramMethod method = ensureBoxedOrdinalOrNullMethod(appView);
+    eventConsumer.acceptEnumUnboxerSharedUtilityClassMethodContext(method, context);
+    return method;
+  }
+
+  private ProgramMethod ensureBoxedOrdinalOrNullMethod(AppView<AppInfoWithLiveness> appView) {
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    return internalEnsureMethod(
+        appView,
+        dexItemFactory.createString("boxedOrdinalOrNull"),
+        dexItemFactory.createProto(dexItemFactory.boxedIntType, dexItemFactory.intType),
+        method ->
+            EnumUnboxingCfMethods.EnumUnboxingMethods_boxedOrdinalOrNull(dexItemFactory, method));
+  }
+
   private ProgramMethod internalEnsureMethod(
       AppView<AppInfoWithLiveness> appView,
       DexString methodName,
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EnumObjectsHashCodeTest.java b/src/test/java/com/android/tools/r8/enumunboxing/EnumObjectsHashCodeTest.java
new file mode 100644
index 0000000..ebca151
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumObjectsHashCodeTest.java
@@ -0,0 +1,122 @@
+// Copyright (c) 2024, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.enumunboxing;
+
+import com.android.tools.r8.KeepConstantArguments;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import java.util.Objects;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class EnumObjectsHashCodeTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addEnumUnboxingInspector(inspector -> inspector.assertUnboxed(MyEnum.class))
+        .enableConstantArgumentAnnotations()
+        .enableInliningAnnotations()
+        .setMinApi(parameters)
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("31", "70154", "31", "null", "31", "31", "31", "961");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      testFromGetter();
+      testMixed();
+      testNull(MyEnum.getNull());
+      testNullUnboxed();
+      testParam(MyEnum.getA());
+      testPhi();
+      testRepeated();
+    }
+
+    @NeverInline
+    static void testFromGetter() {
+      System.out.println(Objects.hash(MyEnum.getA()));
+    }
+
+    @NeverInline
+    static void testMixed() {
+      System.out.println(Objects.hash(new A(), MyEnum.A, MyEnum.B));
+    }
+
+    @NeverInline
+    static void testNull(MyEnum e) {
+      System.out.println(Objects.hash(e));
+    }
+
+    @NeverInline
+    static void testNullUnboxed() {
+      MyEnum e = null;
+      MyEnum.print(e);
+      System.out.println(Objects.hash(e));
+    }
+
+    @NeverInline
+    static void testParam(MyEnum e) {
+      System.out.println(Objects.hash(e));
+    }
+
+    @NeverInline
+    static void testPhi() {
+      MyEnum e = System.currentTimeMillis() > 0 ? MyEnum.A : MyEnum.B;
+      System.out.println(Objects.hash(e));
+    }
+
+    @NeverInline
+    static void testRepeated() {
+      System.out.println(Objects.hash(MyEnum.A, MyEnum.A));
+    }
+  }
+
+  static class A {
+
+    @Override
+    public int hashCode() {
+      return 42;
+    }
+  }
+
+  enum MyEnum {
+    A,
+    B;
+
+    @NeverInline
+    static MyEnum getA() {
+      return System.currentTimeMillis() > 0 ? A : B;
+    }
+
+    @NeverInline
+    static MyEnum getNull() {
+      return System.currentTimeMillis() > 0 ? null : A;
+    }
+
+    @KeepConstantArguments
+    @NeverInline
+    static void print(MyEnum e) {
+      System.out.println(e != null ? e.name() : "null");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingMethods.java b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingMethods.java
index 48818ec..c56421a 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingMethods.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingMethods.java
@@ -9,6 +9,10 @@
 // are different on each Enum implementation.
 public class EnumUnboxingMethods {
 
+  public static Integer boxedOrdinalOrNull(int unboxedEnum) {
+    return unboxedEnum == 0 ? null : unboxedEnum - 1;
+  }
+
   // An enum is unboxed to ordinal + 1.
   // For example, enum E {A,B}, is unboxed to null -> 0, A -> 1, B-> 2.
   // Computing the ordinal of an unboxed enum throws a null pointer exception on 0,
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
index 02dc773..9dc6503 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
@@ -205,7 +205,7 @@
     public InstructionListIterator addPossiblyThrowingInstructionsToPossiblyThrowingBlock(
         IRCode code,
         BasicBlockIterator blockIterator,
-        Collection<Instruction> instructionsToAdd,
+        Collection<? extends Instruction> instructionsToAdd,
         InternalOptions options) {
       throw new Unimplemented();
     }