Fix load elimination for System.out

Fixes: b/379347949
Change-Id: I635dbbccc0a6d4e6793694a74932c011820ec4c2
diff --git a/src/main/java/com/android/tools/r8/graph/DexClassAndField.java b/src/main/java/com/android/tools/r8/graph/DexClassAndField.java
index 1dc176f..1f1eff4 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClassAndField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClassAndField.java
@@ -61,7 +61,7 @@
     return this;
   }
 
-  public final boolean isFinalOrEffectivelyFinal(AppView<?> appView) {
+  public boolean isFinalOrEffectivelyFinal(AppView<?> appView) {
     return getAccessFlags().isFinal()
         || (appView.hasLiveness() && isEffectivelyFinal(appView.withLiveness()));
   }
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 a79ae60..2c0c8e4 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -773,6 +773,7 @@
           androidSystemOsConstantsMembers,
           androidViewViewMembers,
           // java.**
+          enumMembers,
           javaIoFileMembers,
           javaMathBigIntegerMembers,
           javaNioByteOrderMembers,
@@ -2114,7 +2115,7 @@
     private JavaIoPrintStreamMembers() {}
   }
 
-  public class EnumMembers {
+  public class EnumMembers extends LibraryMembers {
 
     public final DexField nameField = createField(enumType, stringType, "name");
     public final DexField ordinalField = createField(enumType, intType, "ordinal");
@@ -2169,6 +2170,12 @@
       fn.accept(ordinalField);
     }
 
+    @Override
+    public void forEachFinalField(Consumer<DexField> fn) {
+      fn.accept(nameField);
+      fn.accept(ordinalField);
+    }
+
     @SuppressWarnings("ReferenceEquality")
     public boolean isNameOrOrdinalField(DexField field) {
       return field == nameField || field == ordinalField;
diff --git a/src/main/java/com/android/tools/r8/graph/LibraryField.java b/src/main/java/com/android/tools/r8/graph/LibraryField.java
index 49ed565..8221f8d 100644
--- a/src/main/java/com/android/tools/r8/graph/LibraryField.java
+++ b/src/main/java/com/android/tools/r8/graph/LibraryField.java
@@ -32,4 +32,9 @@
   public boolean isLibraryMember() {
     return true;
   }
+
+  @Override
+  public boolean isFinalOrEffectivelyFinal(AppView<?> appView) {
+    return appView.libraryMethodOptimizer().isFinalLibraryField(getDefinition());
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
index 39b5c5c..7768041 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
@@ -157,12 +157,11 @@
     }
 
     @Override
-    @SuppressWarnings("EqualsGetClass")
     public boolean equals(Object other) {
       if (this == other) {
         return true;
       }
-      if (other == null || getClass() != other.getClass()) {
+      if (!(other instanceof ArraySlotWithConstantIndex)) {
         return false;
       }
       ArraySlotWithConstantIndex arraySlot = (ArraySlotWithConstantIndex) other;
@@ -190,12 +189,11 @@
     }
 
     @Override
-    @SuppressWarnings("EqualsGetClass")
     public boolean equals(Object other) {
       if (this == other) {
         return true;
       }
-      if (other == null || getClass() != other.getClass()) {
+      if (!(other instanceof ArraySlotWithValueIndex)) {
         return false;
       }
       ArraySlotWithValueIndex arraySlot = (ArraySlotWithValueIndex) other;
@@ -220,13 +218,15 @@
     }
 
     @Override
-    @SuppressWarnings("ReferenceEquality")
     public boolean equals(Object other) {
+      if (this == other) {
+        return true;
+      }
       if (!(other instanceof FieldAndObject)) {
         return false;
       }
       FieldAndObject o = (FieldAndObject) other;
-      return o.object == object && o.field == field;
+      return o.object == object && o.field.isIdenticalTo(field);
     }
   }
 
@@ -345,14 +345,13 @@
       return appView.libraryMethodOptimizer().isFinalLibraryField(field.getDefinition());
     }
 
-    @SuppressWarnings("ReferenceEquality")
     private DexClassAndField resolveField(DexField field) {
       if (appView.enableWholeProgramOptimizations()) {
         SingleFieldResolutionResult resolutionResult =
             appView.appInfo().withLiveness().resolveField(field).asSingleFieldResolutionResult();
         return resolutionResult != null ? resolutionResult.getResolutionPair() : null;
       }
-      if (field.getHolderType() == method.getHolderType()) {
+      if (field.getHolderType().isIdenticalTo(method.getHolderType())) {
         return method.getHolder().lookupProgramField(field);
       }
       return null;
@@ -614,10 +613,10 @@
       return activeState.markClassAsInitialized(type);
     }
 
-    @SuppressWarnings("ReferenceEquality")
     private void markMostRecentInitClassForRemoval(DexType initializedType) {
       InitClass mostRecentInitClass = activeState.getMostRecentInitClass();
-      if (mostRecentInitClass != null && mostRecentInitClass.getClassValue() == initializedType) {
+      if (mostRecentInitClass != null
+          && mostRecentInitClass.getClassValue().isIdenticalTo(initializedType)) {
         instructionsToRemove
             .computeIfAbsent(mostRecentInitClass.getBlock(), ignoreKey(Sets::newIdentityHashSet))
             .add(mostRecentInitClass);
@@ -1113,10 +1112,9 @@
       clearMostRecentStaticFieldWrites();
     }
 
-    @SuppressWarnings("ReferenceEquality")
     public void clearMostRecentInstanceFieldWrite(DexField field) {
       if (mostRecentInstanceFieldWrites != null) {
-        mostRecentInstanceFieldWrites.keySet().removeIf(key -> key.field == field);
+        mostRecentInstanceFieldWrites.keySet().removeIf(key -> key.field.isIdenticalTo(field));
       }
     }
 
@@ -1329,10 +1327,9 @@
       }
     }
 
-    @SuppressWarnings("ReferenceEquality")
     public void removeNonFinalInstanceFields(DexField field) {
       if (nonFinalInstanceFieldValues != null) {
-        nonFinalInstanceFieldValues.keySet().removeIf(key -> key.field == field);
+        nonFinalInstanceFieldValues.keySet().removeIf(key -> key.field.isIdenticalTo(field));
       }
     }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/redundantfieldloadelimination/RedundantLibraryFieldLoadEliminationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/redundantfieldloadelimination/RedundantLibraryFieldLoadEliminationTest.java
new file mode 100644
index 0000000..01593db
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/redundantfieldloadelimination/RedundantLibraryFieldLoadEliminationTest.java
@@ -0,0 +1,76 @@
+// 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.redundantfieldloadelimination;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.io.PrintStream;
+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 RedundantLibraryFieldLoadEliminationTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    parameters.assumeDexRuntime();
+    testForD8()
+        .addInnerClasses(getClass())
+        .release()
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters)
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello, world!");
+  }
+
+  private void inspect(CodeInspector inspector) {
+    MethodSubject mainMethodSubject = inspector.clazz(Main.class).mainMethod();
+    assertThat(mainMethodSubject, isPresent());
+    assertEquals(
+        2, mainMethodSubject.streamInstructions().filter(InstructionSubject::isStaticGet).count());
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      PrintStream out = System.out;
+      System.currentTimeMillis();
+      PrintStream out2 = System.out;
+      out.print("Hello");
+      out2.println(", world!");
+    }
+  }
+}