Add new @NoRedundantFieldLoadElimination testing annotation

Change-Id: I1d7bf5601ede3e4dfd28720c5360c92d14ff9bba
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 178c021..09fa213 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
@@ -648,8 +648,10 @@
     FieldAndObject fieldAndObject = new FieldAndObject(field.getReference(), object);
     FieldValue replacement = activeState.getInstanceFieldValue(fieldAndObject);
     if (replacement != null) {
-      markAssumeDynamicTypeUsersForRemoval(instanceGet, replacement, assumeRemover);
-      replacement.eliminateRedundantRead(it, instanceGet);
+      if (isRedundantFieldLoadEliminationAllowed(field)) {
+        markAssumeDynamicTypeUsersForRemoval(instanceGet, replacement, assumeRemover);
+        replacement.eliminateRedundantRead(it, instanceGet);
+      }
       return;
     }
 
@@ -658,6 +660,15 @@
     clearMostRecentInstanceFieldWrite(instanceGet, field);
   }
 
+  private boolean isRedundantFieldLoadEliminationAllowed(DexClassAndField field) {
+    // Always allowed in D8 since D8 does not support @NoRedundantFieldLoadElimination.
+    return !appView.enableWholeProgramOptimizations()
+        || !field.isProgramField()
+        || appView
+            .getKeepInfo(field.asProgramField())
+            .isRedundantFieldLoadEliminationAllowed(appView.options());
+  }
+
   private void handleNewInstance(NewInstance newInstance) {
     markClassAsInitialized(newInstance.getType());
     markMostRecentInitClassForRemoval(newInstance.getType());
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java
index 312b61e..db53845 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepFieldInfo.java
@@ -25,10 +25,12 @@
   }
 
   private final boolean allowFieldTypeStrengthening;
+  private final boolean allowRedundantFieldLoadElimination;
 
   private KeepFieldInfo(Builder builder) {
     super(builder);
     this.allowFieldTypeStrengthening = builder.isFieldTypeStrengtheningAllowed();
+    this.allowRedundantFieldLoadElimination = builder.isRedundantFieldLoadEliminationAllowed();
   }
 
   // This builder is not private as there are known instances where it is safe to modify keep info
@@ -46,6 +48,14 @@
     return allowFieldTypeStrengthening;
   }
 
+  public boolean isRedundantFieldLoadEliminationAllowed(GlobalKeepInfoConfiguration configuration) {
+    return internalIsRedundantFieldLoadEliminationAllowed();
+  }
+
+  boolean internalIsRedundantFieldLoadEliminationAllowed() {
+    return allowRedundantFieldLoadElimination;
+  }
+
   public Joiner joiner() {
     assert !isTop();
     return new Joiner(this);
@@ -64,6 +74,7 @@
   public static class Builder extends KeepInfo.Builder<Builder, KeepFieldInfo> {
 
     private boolean allowFieldTypeStrengthening;
+    private boolean allowRedundantFieldLoadElimination;
 
     private Builder() {
       super();
@@ -72,16 +83,20 @@
     private Builder(KeepFieldInfo original) {
       super(original);
       allowFieldTypeStrengthening = original.internalIsFieldTypeStrengtheningAllowed();
+      allowRedundantFieldLoadElimination =
+          original.internalIsRedundantFieldLoadEliminationAllowed();
     }
 
     @Override
     public Builder makeTop() {
-      return super.makeTop().disallowFieldTypeStrengthening();
+      return super.makeTop()
+          .disallowFieldTypeStrengthening()
+          .disallowRedundantFieldLoadElimination();
     }
 
     @Override
     public Builder makeBottom() {
-      return super.makeBottom().allowFieldTypeStrengthening();
+      return super.makeBottom().allowFieldTypeStrengthening().allowRedundantFieldLoadElimination();
     }
 
     public boolean isFieldTypeStrengtheningAllowed() {
@@ -101,6 +116,24 @@
       return setAllowFieldTypeStrengthening(false);
     }
 
+    public boolean isRedundantFieldLoadEliminationAllowed() {
+      return allowRedundantFieldLoadElimination;
+    }
+
+    public Builder setAllowRedundantFieldLoadElimination(
+        boolean allowRedundantFieldLoadElimination) {
+      this.allowRedundantFieldLoadElimination = allowRedundantFieldLoadElimination;
+      return self();
+    }
+
+    public Builder allowRedundantFieldLoadElimination() {
+      return setAllowRedundantFieldLoadElimination(true);
+    }
+
+    public Builder disallowRedundantFieldLoadElimination() {
+      return setAllowRedundantFieldLoadElimination(false);
+    }
+
     @Override
     public KeepFieldInfo getTopInfo() {
       return TOP;
@@ -124,7 +157,9 @@
     @Override
     boolean internalIsEqualTo(KeepFieldInfo other) {
       return super.internalIsEqualTo(other)
-          && isFieldTypeStrengtheningAllowed() == other.internalIsFieldTypeStrengtheningAllowed();
+          && isFieldTypeStrengtheningAllowed() == other.internalIsFieldTypeStrengtheningAllowed()
+          && isRedundantFieldLoadEliminationAllowed()
+              == other.internalIsRedundantFieldLoadEliminationAllowed();
     }
 
     @Override
@@ -144,6 +179,11 @@
       return self();
     }
 
+    public Joiner disallowRedundantFieldLoadElimination() {
+      builder.disallowRedundantFieldLoadElimination();
+      return self();
+    }
+
     @Override
     public Joiner asFieldJoiner() {
       return this;
@@ -155,7 +195,10 @@
       return super.merge(joiner)
           .applyIf(
               !joiner.builder.isFieldTypeStrengtheningAllowed(),
-              KeepFieldInfo.Joiner::disallowFieldTypeStrengthening);
+              KeepFieldInfo.Joiner::disallowFieldTypeStrengthening)
+          .applyIf(
+              !joiner.builder.isRedundantFieldLoadEliminationAllowed(),
+              KeepFieldInfo.Joiner::disallowRedundantFieldLoadElimination);
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/shaking/NoRedundantFieldLoadEliminationRule.java b/src/main/java/com/android/tools/r8/shaking/NoRedundantFieldLoadEliminationRule.java
new file mode 100644
index 0000000..87c5ee0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/NoRedundantFieldLoadEliminationRule.java
@@ -0,0 +1,85 @@
+// 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.shaking;
+
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+import java.util.List;
+
+public class NoRedundantFieldLoadEliminationRule
+    extends NoOptimizationBaseRule<NoRedundantFieldLoadEliminationRule> {
+
+  public static final String RULE_NAME = "noredundantfieldloadelimination";
+
+  public static class Builder
+      extends NoOptimizationBaseRule.Builder<NoRedundantFieldLoadEliminationRule, Builder> {
+
+    Builder() {
+      super();
+    }
+
+    @Override
+    public NoRedundantFieldLoadEliminationRule.Builder self() {
+      return this;
+    }
+
+    @Override
+    public NoRedundantFieldLoadEliminationRule build() {
+      return new NoRedundantFieldLoadEliminationRule(
+          origin,
+          getPosition(),
+          source,
+          buildClassAnnotations(),
+          classAccessFlags,
+          negatedClassAccessFlags,
+          classTypeNegated,
+          classType,
+          classNames,
+          buildInheritanceAnnotations(),
+          inheritanceClassName,
+          inheritanceIsExtends,
+          memberRules);
+    }
+  }
+
+  NoRedundantFieldLoadEliminationRule(
+      Origin origin,
+      Position position,
+      String source,
+      List<ProguardTypeMatcher> classAnnotations,
+      ProguardAccessFlags classAccessFlags,
+      ProguardAccessFlags negatedClassAccessFlags,
+      boolean classTypeNegated,
+      ProguardClassType classType,
+      ProguardClassNameList classNames,
+      List<ProguardTypeMatcher> inheritanceAnnotations,
+      ProguardTypeMatcher inheritanceClassName,
+      boolean inheritanceIsExtends,
+      List<ProguardMemberRule> memberRules) {
+    super(
+        origin,
+        position,
+        source,
+        classAnnotations,
+        classAccessFlags,
+        negatedClassAccessFlags,
+        classTypeNegated,
+        classType,
+        classNames,
+        inheritanceAnnotations,
+        inheritanceClassName,
+        inheritanceIsExtends,
+        memberRules);
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  String typeString() {
+    return RULE_NAME;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index c1de766..b050658 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -599,6 +599,12 @@
           configurationBuilder.addRule(rule);
           return true;
         }
+        if (acceptString(NoRedundantFieldLoadEliminationRule.RULE_NAME)) {
+          ProguardConfigurationRule rule =
+              parseNoOptimizationRule(optionStart, NoRedundantFieldLoadEliminationRule.builder());
+          configurationBuilder.addRule(rule);
+          return true;
+        }
         if (acceptString(NoReturnTypeStrengtheningRule.RULE_NAME)) {
           ProguardConfigurationRule rule =
               parseNoOptimizationRule(optionStart, NoReturnTypeStrengtheningRule.builder());
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
index 2b6c654..2b36aec 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetUtils.java
@@ -288,7 +288,8 @@
           markMatchingOverriddenMethods(clazz, memberKeepRules, rule, null, true, ifRule);
           markMatchingVisibleFields(clazz, memberKeepRules, rule, null, true, ifRule);
         }
-      } else if (rule instanceof NoFieldTypeStrengtheningRule) {
+      } else if (rule instanceof NoFieldTypeStrengtheningRule
+          || rule instanceof NoRedundantFieldLoadEliminationRule) {
         markMatchingFields(clazz, memberKeepRules, rule, null, ifRule);
       } else if (rule instanceof InlineRule
           || rule instanceof KeepConstantArgumentRule
@@ -1237,6 +1238,13 @@
             .asFieldJoiner()
             .disallowFieldTypeStrengthening();
         context.markAsUsed();
+      } else if (context instanceof NoRedundantFieldLoadEliminationRule) {
+        assert item.isProgramField();
+        dependentMinimumKeepInfo
+            .getOrCreateUnconditionalMinimumKeepInfoFor(item.getReference())
+            .asFieldJoiner()
+            .disallowRedundantFieldLoadElimination();
+        context.markAsUsed();
       } else if (context instanceof NoUnusedInterfaceRemovalRule) {
         noUnusedInterfaceRemoval.add(item.asClass().type);
         context.markAsUsed();
diff --git a/src/test/java/com/android/tools/r8/NoRedundantFieldLoadElimination.java b/src/test/java/com/android/tools/r8/NoRedundantFieldLoadElimination.java
new file mode 100644
index 0000000..16b6ebe
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/NoRedundantFieldLoadElimination.java
@@ -0,0 +1,10 @@
+// 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;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+@Target({ElementType.FIELD})
+public @interface NoRedundantFieldLoadElimination {}
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index b9c6c781..194d921 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -28,6 +28,7 @@
 import com.android.tools.r8.shaking.NoMethodStaticizingRule;
 import com.android.tools.r8.shaking.NoParameterReorderingRule;
 import com.android.tools.r8.shaking.NoParameterTypeStrengtheningRule;
+import com.android.tools.r8.shaking.NoRedundantFieldLoadEliminationRule;
 import com.android.tools.r8.shaking.NoReturnTypeStrengtheningRule;
 import com.android.tools.r8.shaking.NoUnusedInterfaceRemovalRule;
 import com.android.tools.r8.shaking.NoVerticalClassMergingRule;
@@ -573,6 +574,12 @@
             NoParameterTypeStrengtheningRule.RULE_NAME, NoParameterTypeStrengthening.class);
   }
 
+  public T enableNoRedundantFieldLoadEliminationAnnotations() {
+    return addNoRedundantFieldLoadEliminationAnnotation()
+        .addInternalMatchAnnotationOnFieldRule(
+            NoRedundantFieldLoadEliminationRule.RULE_NAME, NoRedundantFieldLoadElimination.class);
+  }
+
   public T enableNoReturnTypeStrengtheningAnnotations() {
     return addNoReturnTypeStrengtheningAnnotation()
         .addInternalMatchAnnotationOnMethodRule(
diff --git a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
index 704ce16..7f96c42 100644
--- a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
@@ -532,6 +532,10 @@
     return addTestingAnnotation(NoParameterTypeStrengthening.class);
   }
 
+  public final T addNoRedundantFieldLoadEliminationAnnotation() {
+    return addTestingAnnotation(NoRedundantFieldLoadElimination.class);
+  }
+
   public final T addNoReturnTypeStrengtheningAnnotation() {
     return addTestingAnnotation(NoReturnTypeStrengthening.class);
   }