Extend canonicalizer to constant, non-throwing fields

Bug: b/236648397
Bug: b/236661949
Bug: b/237372251
Change-Id: I7dc761ea342a30efb8b243678cd2424645ba9624
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index b785927..51a4a4a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -447,10 +447,6 @@
     return isInstanceInitializer() || isClassInitializer();
   }
 
-  public boolean isInitializer(boolean isStatic) {
-    return isStatic ? isClassInitializer() : isInstanceInitializer();
-  }
-
   public boolean isInstanceInitializer() {
     checkIfObsolete();
     return accessFlags.isConstructor() && !accessFlags.isStatic();
diff --git a/src/main/java/com/android/tools/r8/ir/code/FieldGet.java b/src/main/java/com/android/tools/r8/ir/code/FieldGet.java
index ec663fb..4d4a55b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/FieldGet.java
+++ b/src/main/java/com/android/tools/r8/ir/code/FieldGet.java
@@ -13,6 +13,10 @@
 
   TypeElement getOutType();
 
+  boolean isInstanceGet();
+
+  boolean isStaticGet();
+
   boolean hasUsedOutValue();
 
   Value outValue();
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstanceGet.java b/src/main/java/com/android/tools/r8/ir/code/InstanceGet.java
index b88555d..6f571d0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstanceGet.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstanceGet.java
@@ -39,6 +39,25 @@
     super(field, dest, object);
   }
 
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static InstanceGet copyOf(IRCode code, InstanceGet original) {
+    Value newValue =
+        new Value(code.valueNumberGenerator.next(), original.getOutType(), original.getLocalInfo());
+    return copyOf(newValue, original);
+  }
+
+  public static InstanceGet copyOf(Value newValue, InstanceGet original) {
+    assert newValue != original.outValue();
+    return InstanceGet.builder()
+        .setField(original.getField())
+        .setObject(original.object())
+        .setOutValue(newValue)
+        .build();
+  }
+
   @Override
   public int opcode() {
     return Opcodes.INSTANCE_GET;
@@ -234,4 +253,35 @@
   public boolean instructionMayTriggerMethodInvocation(AppView<?> appView, ProgramMethod context) {
     return false;
   }
+
+  @Override
+  public boolean instructionTypeCanBeCanonicalized() {
+    return true;
+  }
+
+  public static class Builder extends BuilderBase<Builder, InstanceGet> {
+
+    private DexField field;
+    private Value object;
+
+    public Builder setField(DexField field) {
+      this.field = field;
+      return this;
+    }
+
+    public Builder setObject(Value object) {
+      this.object = object;
+      return this;
+    }
+
+    @Override
+    public InstanceGet build() {
+      return amend(new InstanceGet(outValue, object, field));
+    }
+
+    @Override
+    public Builder self() {
+      return this;
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 276a72c..06b2d1c 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -128,7 +128,6 @@
   private final CfgPrinter printer;
   public final CodeRewriter codeRewriter;
   private final NaturalIntLoopRemover naturalIntLoopRemover = new NaturalIntLoopRemover();
-  private final ConstantCanonicalizer constantCanonicalizer;
   public final MemberValuePropagation<?> memberValuePropagation;
   private final LensCodeRewriter lensCodeRewriter;
   private final Inliner inliner;
@@ -176,7 +175,6 @@
     this.options = appView.options();
     this.printer = printer;
     this.codeRewriter = new CodeRewriter(appView);
-    this.constantCanonicalizer = new ConstantCanonicalizer(codeRewriter);
     this.classInitializerDefaultsOptimization =
         new ClassInitializerDefaultsOptimization(appView, this);
     this.stringOptimizer = new StringOptimizer(appView);
@@ -1430,7 +1428,7 @@
     // TODO(mkroghj) Test if shorten live ranges is worth it.
     if (!options.isGeneratingClassFiles()) {
       timing.begin("Canonicalize constants");
-      constantCanonicalizer.canonicalize(appView, code);
+      new ConstantCanonicalizer(appView, codeRewriter, context, code).canonicalize();
       timing.end();
       previous = printMethod(code, "IR after constant canonicalization (SSA)", previous);
       timing.begin("Create constants for literal instructions");
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
index 7082cc6..7a0a731 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
@@ -7,13 +7,17 @@
 import static com.android.tools.r8.ir.code.Opcodes.CONST_NUMBER;
 import static com.android.tools.r8.ir.code.Opcodes.CONST_STRING;
 import static com.android.tools.r8.ir.code.Opcodes.DEX_ITEM_BASED_CONST_STRING;
+import static com.android.tools.r8.ir.code.Opcodes.INSTANCE_GET;
 import static com.android.tools.r8.ir.code.Opcodes.STATIC_GET;
 
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.FieldAccessFlags;
+import com.android.tools.r8.graph.FieldResolutionResult.SingleFieldResolutionResult;
+import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.SingleFieldValue;
@@ -22,7 +26,10 @@
 import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.ConstString;
 import com.android.tools.r8.ir.code.DexItemBasedConstString;
+import com.android.tools.r8.ir.code.FieldGet;
+import com.android.tools.r8.ir.code.FieldInstruction;
 import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstanceGet;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.StaticGet;
@@ -39,17 +46,44 @@
  * Canonicalize constants.
  */
 public class ConstantCanonicalizer {
+
   // Threshold to limit the number of constant canonicalization.
   private static final int MAX_CANONICALIZED_CONSTANT = 22;
 
+  private final AppView<?> appView;
   private final CodeRewriter codeRewriter;
+  private final ProgramMethod context;
+  private final IRCode code;
+  private final boolean isAccessingVolatileField;
 
-  public ConstantCanonicalizer(CodeRewriter codeRewriter) {
+  public ConstantCanonicalizer(
+      AppView<?> appView, CodeRewriter codeRewriter, ProgramMethod context, IRCode code) {
+    this.appView = appView;
     this.codeRewriter = codeRewriter;
+    this.context = context;
+    this.code = code;
+    this.isAccessingVolatileField = computeIsAccessingVolatileField(appView, code);
   }
 
-  public void canonicalize(AppView<?> appView, IRCode code) {
-    ProgramMethod context = code.context();
+  private static boolean computeIsAccessingVolatileField(AppView<?> appView, IRCode code) {
+    if (!appView.hasClassHierarchy()) {
+      // Conservatively return true.
+      return true;
+    }
+    AppInfoWithClassHierarchy appInfo = appView.appInfoWithClassHierarchy();
+    for (FieldInstruction fieldGet :
+        code.<FieldInstruction>instructions(Instruction::isFieldInstruction)) {
+      SingleFieldResolutionResult<?> resolutionResult =
+          appInfo.resolveField(fieldGet.getField()).asSingleFieldResolutionResult();
+      if (resolutionResult == null
+          || resolutionResult.getResolvedField().getAccessFlags().isVolatile()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public void canonicalize() {
     Object2ObjectLinkedOpenCustomHashMap<Instruction, List<Value>> valuesDefinedByConstant =
         new Object2ObjectLinkedOpenCustomHashMap<>(
             new Strategy<Instruction>() {
@@ -67,8 +101,9 @@
                     return candidate.asConstString().getValue().hashCode();
                   case DEX_ITEM_BASED_CONST_STRING:
                     return candidate.asDexItemBasedConstString().getItem().hashCode();
+                  case INSTANCE_GET:
                   case STATIC_GET:
-                    return candidate.asStaticGet().getField().hashCode();
+                    return candidate.asFieldGet().getField().hashCode();
                   default:
                     throw new Unreachable();
                 }
@@ -76,71 +111,26 @@
 
               @Override
               public boolean equals(Instruction a, Instruction b) {
-                // Constants with local info must not be canonicalized and must be filtered.
-                assert a == null || !a.outValue().hasLocalInfo();
-                assert b == null || !b.outValue().hasLocalInfo();
-                return a == b || (a != null && b != null && a.identicalNonValueNonPositionParts(b));
+                if (a == b) {
+                  return true;
+                }
+                if (a == null || b == null || a.getClass() != b.getClass()) {
+                  return false;
+                }
+                if (a.isInstanceGet() && a.getFirstOperand() != b.getFirstOperand()) {
+                  return false;
+                }
+                return a.identicalNonValueNonPositionParts(b);
               }
             });
 
     // Collect usages of constants that can be canonicalized.
     for (Instruction current : code.instructions()) {
-      // Interested only in instructions types that can be canonicalized, i.e., ConstClass,
-      // ConstNumber, (DexItemBased)?ConstString, and StaticGet.
-      if (!current.instructionTypeCanBeCanonicalized()) {
-        continue;
+      if (isConstantCanonicalizationCandidate(current)) {
+        List<Value> oldValuesDefinedByConstant =
+            valuesDefinedByConstant.computeIfAbsent(current, k -> new ArrayList<>());
+        oldValuesDefinedByConstant.add(current.outValue());
       }
-      // Do not canonicalize ConstClass that may have side effects. Its original instructions
-      // will not be removed by dead code remover due to the side effects.
-      if (current.isConstClass() && current.instructionMayHaveSideEffects(appView, context)) {
-        continue;
-      }
-      // Do not canonicalize ConstString instructions if there are monitor operations in the code.
-      // That could lead to unbalanced locking and could lead to situations where OOM exceptions
-      // could leave a synchronized method without unlocking the monitor.
-      if ((current.isConstString() || current.isDexItemBasedConstString())
-          && code.metadata().mayHaveMonitorInstruction()) {
-        continue;
-      }
-      // Canonicalize effectively final fields that are guaranteed to be written before they are
-      // read. This is only OK if the instruction cannot have side effects.
-      if (current.isStaticGet()) {
-        AbstractValue abstractValue = current.outValue().getAbstractValue(appView, context);
-        if (!abstractValue.isSingleFieldValue()) {
-          continue;
-        }
-        SingleFieldValue singleFieldValue = abstractValue.asSingleFieldValue();
-        DexType fieldHolderType = singleFieldValue.getField().getHolderType();
-        if (context.getDefinition().isClassInitializer()
-            && context.getHolderType() == fieldHolderType) {
-          // Avoid that canonicalization inserts a read before the unique write in the class
-          // initializer, as that would change the program behavior.
-          continue;
-        }
-        DexClass fieldHolder = appView.definitionFor(fieldHolderType);
-        DexEncodedField field = singleFieldValue.getField().lookupOnClass(fieldHolder);
-        if (field == null
-            || !field.isEnum()
-            || current.instructionMayHaveSideEffects(appView, context)) {
-          // Only allow canonicalization of enums.
-          continue;
-        }
-      }
-      // Constants with local info must not be canonicalized and must be filtered.
-      if (current.outValue().hasLocalInfo()) {
-        continue;
-      }
-      // Constants that are used by invoke range are not canonicalized to be compliant with the
-      // optimization splitRangeInvokeConstant that gives the register allocator more freedom in
-      // assigning register to ranged invokes which can greatly reduce the number of register
-      // needed (and thereby code size as well). Thus no need to do a transformation that should
-      // be removed later by another optimization.
-      if (constantUsedByInvokeRange(current)) {
-        continue;
-      }
-      List<Value> oldValuesDefinedByConstant =
-          valuesDefinedByConstant.computeIfAbsent(current, k -> new ArrayList<>());
-      oldValuesDefinedByConstant.add(current.outValue());
     }
 
     if (valuesDefinedByConstant.isEmpty()) {
@@ -189,13 +179,16 @@
                 DexItemBasedConstString.copyOf(
                     code, canonicalizedConstant.asDexItemBasedConstString());
             break;
+          case INSTANCE_GET:
+            newConst = InstanceGet.copyOf(code, canonicalizedConstant.asInstanceGet());
+            break;
           case STATIC_GET:
             newConst = StaticGet.copyOf(code, canonicalizedConstant.asStaticGet());
             break;
           default:
             throw new Unreachable();
         }
-        insertCanonicalizedConstant(code, newConst);
+        insertCanonicalizedConstant(newConst);
       }
       for (Value outValue : entry.getValue()) {
         outValue.replaceUsers(newConst.outValue());
@@ -212,7 +205,152 @@
     assert code.isConsistentSSA(appView);
   }
 
-  private static void insertCanonicalizedConstant(IRCode code, Instruction canonicalizedConstant) {
+  public boolean isConstantCanonicalizationCandidate(Instruction instruction) {
+    // Interested only in instructions types that can be canonicalized, i.e., ConstClass,
+    // ConstNumber, (DexItemBased)?ConstString, InstanceGet and StaticGet.
+    switch (instruction.opcode()) {
+      case CONST_CLASS:
+        // Do not canonicalize ConstClass that may have side effects. Its original instructions
+        // will not be removed by dead code remover due to the side effects.
+        if (instruction.instructionMayHaveSideEffects(appView, context)) {
+          return false;
+        }
+        break;
+      case CONST_NUMBER:
+        break;
+      case CONST_STRING:
+      case DEX_ITEM_BASED_CONST_STRING:
+        // Do not canonicalize ConstString instructions if there are monitor operations in the code.
+        // That could lead to unbalanced locking and could lead to situations where OOM exceptions
+        // could leave a synchronized method without unlocking the monitor.
+        if (code.metadata().mayHaveMonitorInstruction()) {
+          return false;
+        }
+        break;
+      case INSTANCE_GET:
+        {
+          InstanceGet instanceGet = instruction.asInstanceGet();
+          if (!instanceGet.object().isThis()) {
+            // TODO(b/236661949): Generalize this to more receivers. For canonicalization we need
+            //  the receiver to be non-null (or the instruction can throw) and we also currently
+            //  require the receiver to be defined on-entry, since we always hoist constants to the
+            //  entry block.
+            return false;
+          }
+          if (!isReadOfFinalFieldOutsideInitializer(instanceGet)) {
+            return false;
+          }
+          break;
+        }
+      case STATIC_GET:
+        {
+          // Canonicalize effectively final fields that are guaranteed to be written before they are
+          // read. This is only OK if the instruction cannot have side effects.
+          StaticGet staticGet = instruction.asStaticGet();
+          if (staticGet.instructionMayHaveSideEffects(appView, context)) {
+            return false;
+          }
+          if (!isReadOfFinalFieldOutsideInitializer(staticGet)
+              && !isEffectivelyFinalField(staticGet)) {
+            return false;
+          }
+          break;
+        }
+      default:
+        assert !instruction.instructionTypeCanBeCanonicalized() : instruction.toString();
+        return false;
+    }
+    // Constants with local info must not be canonicalized and must be filtered.
+    if (instruction.outValue().hasLocalInfo()) {
+      return false;
+    }
+    // Constants that are used by invoke range are not canonicalized to be compliant with the
+    // optimization splitRangeInvokeConstant that gives the register allocator more freedom in
+    // assigning register to ranged invokes which can greatly reduce the number of register
+    // needed (and thereby code size as well). Thus no need to do a transformation that should
+    // be removed later by another optimization.
+    if (constantUsedByInvokeRange(instruction)) {
+      return false;
+    }
+    return true;
+  }
+
+  private boolean isReadOfFinalFieldOutsideInitializer(FieldGet fieldGet) {
+    if (isAccessingVolatileField) {
+      // A final field may be initialized concurrently. A requirement for this is that the field is
+      // volatile. However, the reading or writing of another volatile field also allows for
+      // concurrently initializing a non-volatile field. See also redundant field load elimination.
+      return false;
+    }
+    AppView<? extends AppInfoWithClassHierarchy> appViewWithClassHierarchy =
+        appView.withClassHierarchy();
+    SingleFieldResolutionResult<?> resolutionResult =
+        appViewWithClassHierarchy
+            .appInfo()
+            .resolveField(fieldGet.getField())
+            .asSingleFieldResolutionResult();
+    if (resolutionResult == null) {
+      // Not known to be final.
+      return false;
+    }
+    if (!resolutionResult.isSingleProgramFieldResolutionResult()) {
+      // We can't rely on the final flag of non-program fields.
+      return false;
+    }
+    ProgramField resolvedField = resolutionResult.getSingleProgramField();
+    FieldAccessFlags accessFlags = resolvedField.getAccessFlags();
+    assert !accessFlags.isVolatile();
+    // TODO(b/236661949): Add support for effectively final fields so that this also works well
+    //  without -allowaccessmodification.
+    if (!accessFlags.isFinal()) {
+      return false;
+    }
+    if (appView.getKeepInfo(resolvedField).isPinned(appView.options())) {
+      // The final flag could be unset using reflection.
+      return false;
+    }
+    if (context.getDefinition().isInitializer()
+        && context.getAccessFlags().isStatic() == fieldGet.isStaticGet()) {
+      if (context.getHolder() == resolvedField.getHolder()) {
+        // If this is an initializer on the field's holder, then bail out, since the field value is
+        // only known to be final after object/class creation.
+        return false;
+      }
+      if (fieldGet.isInstanceGet()
+          && appViewWithClassHierarchy
+              .appInfo()
+              .isSubtype(context.getHolder(), resolvedField.getHolder())) {
+        // If an instance initializer reads a final instance field declared in a super class, we
+        // cannot hoist the read above the parent constructor call.
+        return false;
+      }
+    }
+    if (!resolutionResult.getInitialResolutionHolder().isResolvable(appView)) {
+      // If this field read is guarded by an API level check, hoisting of this field could lead to
+      // a ClassNotFoundException on some API levels.
+      return false;
+    }
+    return true;
+  }
+
+  private boolean isEffectivelyFinalField(StaticGet staticGet) {
+    AbstractValue abstractValue = staticGet.outValue().getAbstractValue(appView, context);
+    if (!abstractValue.isSingleFieldValue()) {
+      return false;
+    }
+    SingleFieldValue singleFieldValue = abstractValue.asSingleFieldValue();
+    DexType fieldHolderType = singleFieldValue.getField().getHolderType();
+    if (context.getDefinition().isClassInitializer()
+        && context.getHolderType() == fieldHolderType) {
+      // Avoid that canonicalization inserts a read before the unique write in the class
+      // initializer, as that would change the program behavior.
+      return false;
+    }
+    DexClass fieldHolder = appView.definitionFor(fieldHolderType);
+    return singleFieldValue.getField().lookupOnClass(fieldHolder) != null;
+  }
+
+  private void insertCanonicalizedConstant(Instruction canonicalizedConstant) {
     BasicBlock entryBlock = code.entryBlock();
     // Insert the constant instruction at the start of the block right after the argument
     // instructions. It is important that the const instruction is put before any instruction
diff --git a/src/main/java/com/android/tools/r8/optimize/AccessModifier.java b/src/main/java/com/android/tools/r8/optimize/AccessModifier.java
index d8b5946..c1ee380 100644
--- a/src/main/java/com/android/tools/r8/optimize/AccessModifier.java
+++ b/src/main/java/com/android/tools/r8/optimize/AccessModifier.java
@@ -148,7 +148,8 @@
         && !accessInfo.isWrittenFromMethodHandle()
         && accessInfo.isWrittenOnlyInMethodSatisfying(
             method ->
-                method.getDefinition().isInitializer(flags.isStatic())
+                method.getDefinition().isInitializer()
+                    && method.getAccessFlags().isStatic() == flags.isStatic()
                     && method.getHolder() == field.getHolder())
         && !flags.isFinal()
         && !flags.isVolatile()) {