Member value propagation in presence of class initialization side effects

Bug: 137908034
Change-Id: I55560a88861dd1f5fe4292bfded9c6ce1f4d9218
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
index 383f816..05feecc 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
@@ -7,7 +7,6 @@
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.ir.code.ConstInstruction;
 import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.optimize.info.DefaultFieldOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.FieldOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.MutableFieldOptimizationInfo;
@@ -150,36 +149,12 @@
    * <p>NOTE: It is the responsibility of the caller to check if this field is pinned or not.
    */
   public ConstInstruction valueAsConstInstruction(
-      IRCode code, Value dest, AppView<AppInfoWithLiveness> appView) {
-    // If it is a static field, we can only propagate the value if class initialization does not
-    // have side effects.
-    if (isStatic()) {
-      DexClass clazz = appView.definitionFor(field.holder);
-      if (clazz == null) {
-        return null;
-      }
-      DexType context = code.method.method.holder;
-      if (clazz.classInitializationMayHaveSideEffects(
-          appView,
-          // Types that are a super type of the current context are guaranteed to be initialized
-          // already.
-          type -> appView.isSubtype(context, type).isTrue())) {
-        // Ignore class initialization side-effects for dead proto extension fields to ensure that
-        // we force replace these field reads by null.
-        boolean ignore =
-            appView.withGeneratedExtensionRegistryShrinker(
-                shrinker -> shrinker.isDeadProtoExtensionField(field), false);
-        if (!ignore) {
-          return null;
-        }
-      }
-    }
-
+      IRCode code, DebugLocalInfo local, AppView<AppInfoWithLiveness> appView) {
     boolean isWritten = appView.appInfo().isFieldWrittenByFieldPutInstruction(this);
     if (!isWritten) {
       // Since the field is not written, we can simply return the default value for the type.
       DexValue value = isStatic() ? getStaticValue() : DexValue.defaultForType(field.type);
-      return value.asConstInstruction(code, dest, appView.options());
+      return value.asConstInstruction(appView, code, local);
     }
 
     // The only way to figure out whether the DexValue contains the final value is ensure the value
@@ -191,13 +166,38 @@
       }
       DexValue staticValue = getStaticValue();
       if (!staticValue.isDefault(field.type)) {
-        return staticValue.asConstInstruction(code, dest, appView.options());
+        return staticValue.asConstInstruction(appView, code, local);
       }
     }
 
     return null;
   }
 
+  public boolean mayTriggerClassInitializationSideEffects(
+      AppView<AppInfoWithLiveness> appView, DexType context) {
+    // Only static field matters when it comes to class initialization side effects.
+    if (!isStatic()) {
+      return false;
+    }
+    DexClass clazz = appView.definitionFor(field.holder);
+    if (clazz == null) {
+      return true;
+    }
+    if (clazz.classInitializationMayHaveSideEffects(
+        appView,
+        // Types that are a super type of the current context are guaranteed to be initialized
+        // already.
+        type -> appView.isSubtype(context, type).isTrue())) {
+      // Ignore class initialization side-effects for dead proto extension fields to ensure that
+      // we force replace these field reads by null.
+      boolean ignore =
+          appView.withGeneratedExtensionRegistryShrinker(
+              shrinker -> shrinker.isDeadProtoExtensionField(field), false);
+      return !ignore;
+    }
+    return false;
+  }
+
   public DexEncodedField toTypeSubstitutedField(DexField field) {
     if (this.field == field) {
       return this;
diff --git a/src/main/java/com/android/tools/r8/graph/DexValue.java b/src/main/java/com/android/tools/r8/graph/DexValue.java
index f9b12d3..f48a6a6 100644
--- a/src/main/java/com/android/tools/r8/graph/DexValue.java
+++ b/src/main/java/com/android/tools/r8/graph/DexValue.java
@@ -3,21 +3,23 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
+
 import com.android.tools.r8.dex.DexOutputBuffer;
 import com.android.tools.r8.dex.FileWriter;
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.code.BasicBlock.ThrowingInfo;
 import com.android.tools.r8.ir.code.ConstInstruction;
-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.IRCode;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.naming.dexitembasedstring.NameComputationInfo;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.EncodedValueUtils;
-import com.android.tools.r8.utils.InternalOptions;
 import java.util.Arrays;
 import org.objectweb.asm.Handle;
 import org.objectweb.asm.Type;
@@ -139,7 +141,8 @@
   public abstract Object getBoxedValue();
 
   /** Returns an instruction that can be used to materialize this {@link DexValue} (or null). */
-  public ConstInstruction asConstInstruction(IRCode code, Value dest, InternalOptions options) {
+  public ConstInstruction asConstInstruction(
+      AppView<? extends AppInfoWithSubtyping> appView, IRCode code, DebugLocalInfo local) {
     return null;
   }
 
@@ -214,7 +217,8 @@
     }
 
     @Override
-    public ConstInstruction asConstInstruction(IRCode code, Value dest, InternalOptions options) {
+    public ConstInstruction asConstInstruction(
+        AppView<? extends AppInfoWithSubtyping> appView, IRCode code, DebugLocalInfo local) {
       return null;
     }
   }
@@ -301,8 +305,9 @@
     }
 
     @Override
-    public ConstInstruction asConstInstruction(IRCode code, Value dest, InternalOptions options) {
-      return new ConstNumber(dest, value);
+    public ConstInstruction asConstInstruction(
+        AppView<? extends AppInfoWithSubtyping> appView, IRCode code, DebugLocalInfo local) {
+      return code.createIntConstant(value, local);
     }
   }
 
@@ -357,8 +362,9 @@
     }
 
     @Override
-    public ConstInstruction asConstInstruction(IRCode code, Value dest, InternalOptions options) {
-      return new ConstNumber(dest, value);
+    public ConstInstruction asConstInstruction(
+        AppView<? extends AppInfoWithSubtyping> appView, IRCode code, DebugLocalInfo local) {
+      return code.createIntConstant(value, local);
     }
   }
 
@@ -417,8 +423,9 @@
     }
 
     @Override
-    public ConstInstruction asConstInstruction(IRCode code, Value dest, InternalOptions options) {
-      return new ConstNumber(dest, value);
+    public ConstInstruction asConstInstruction(
+        AppView<? extends AppInfoWithSubtyping> appView, IRCode code, DebugLocalInfo local) {
+      return code.createIntConstant(value, local);
     }
   }
 
@@ -473,8 +480,9 @@
     }
 
     @Override
-    public ConstInstruction asConstInstruction(IRCode code, Value dest, InternalOptions options) {
-      return new ConstNumber(dest, value);
+    public ConstInstruction asConstInstruction(
+        AppView<? extends AppInfoWithSubtyping> appView, IRCode code, DebugLocalInfo local) {
+      return code.createIntConstant(value, local);
     }
   }
 
@@ -529,8 +537,9 @@
     }
 
     @Override
-    public ConstInstruction asConstInstruction(IRCode code, Value dest, InternalOptions options) {
-      return new ConstNumber(dest, value);
+    public ConstInstruction asConstInstruction(
+        AppView<? extends AppInfoWithSubtyping> appView, IRCode code, DebugLocalInfo local) {
+      return code.createLongConstant(value, local);
     }
   }
 
@@ -733,9 +742,12 @@
     }
 
     @Override
-    public ConstInstruction asConstInstruction(IRCode code, Value dest, InternalOptions options) {
+    public ConstInstruction asConstInstruction(
+        AppView<? extends AppInfoWithSubtyping> appView, IRCode code, DebugLocalInfo local) {
+      TypeLatticeElement type = TypeLatticeElement.stringClassType(appView, definitelyNotNull());
+      Value outValue = code.createValue(type, local);
       ConstString instruction =
-          new ConstString(dest, value, ThrowingInfo.defaultForConstString(options));
+          new ConstString(outValue, value, ThrowingInfo.defaultForConstString(appView.options()));
       if (!instruction.instructionInstanceCanThrow()) {
         return instruction;
       }
@@ -773,10 +785,16 @@
     }
 
     @Override
-    public ConstInstruction asConstInstruction(IRCode code, Value dest, InternalOptions options) {
+    public ConstInstruction asConstInstruction(
+        AppView<? extends AppInfoWithSubtyping> appView, IRCode code, DebugLocalInfo local) {
+      TypeLatticeElement type = TypeLatticeElement.stringClassType(appView, definitelyNotNull());
+      Value outValue = code.createValue(type, local);
       DexItemBasedConstString instruction =
           new DexItemBasedConstString(
-              dest, value, nameComputationInfo, ThrowingInfo.defaultForConstString(options));
+              outValue,
+              value,
+              nameComputationInfo,
+              ThrowingInfo.defaultForConstString(appView.options()));
       // DexItemBasedConstString cannot throw.
       assert !instruction.instructionInstanceCanThrow();
       return instruction;
@@ -1055,11 +1073,9 @@
     }
 
     @Override
-    public ConstInstruction asConstInstruction(IRCode code, Value dest, InternalOptions options) {
-      if (dest.getTypeLattice().isNullType()) {
-        return new ConstNumber(dest, 0);
-      }
-      return code.createConstNull(dest.getLocalInfo());
+    public ConstInstruction asConstInstruction(
+        AppView<? extends AppInfoWithSubtyping> appView, IRCode code, DebugLocalInfo local) {
+      return code.createConstNull(local);
     }
   }
 
@@ -1118,8 +1134,9 @@
     }
 
     @Override
-    public ConstInstruction asConstInstruction(IRCode code, Value dest, InternalOptions options) {
-      return new ConstNumber(dest, value ? 1 : 0);
+    public ConstInstruction asConstInstruction(
+        AppView<? extends AppInfoWithSubtyping> appView, IRCode code, DebugLocalInfo local) {
+      return code.createIntConstant(BooleanUtils.intValue(value), local);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index ce02bf6..f0c062b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -1008,15 +1008,29 @@
     return createValue(typeLattice, null);
   }
 
-  public Phi createPhi(BasicBlock block, TypeLatticeElement type) {
-    return new Phi(valueNumberGenerator.next(), block, type, null, RegisterReadType.NORMAL);
+  public ConstNumber createDoubleConstant(long value, DebugLocalInfo local) {
+    Value out = createValue(TypeLatticeElement.DOUBLE, local);
+    return new ConstNumber(out, value);
   }
 
   public ConstNumber createIntConstant(int value) {
-    Value out = createValue(TypeLatticeElement.INT);
+    return createIntConstant(value, null);
+  }
+
+  public ConstNumber createIntConstant(int value, DebugLocalInfo local) {
+    Value out = createValue(TypeLatticeElement.INT, local);
     return new ConstNumber(out, value);
   }
 
+  public ConstNumber createLongConstant(long value, DebugLocalInfo local) {
+    Value out = createValue(TypeLatticeElement.LONG, local);
+    return new ConstNumber(out, value);
+  }
+
+  public Phi createPhi(BasicBlock block, TypeLatticeElement type) {
+    return new Phi(valueNumberGenerator.next(), block, type, null, RegisterReadType.NORMAL);
+  }
+
   public final int getHighestBlockNumber() {
     return blocks.stream().max(Comparator.comparingInt(BasicBlock::getNumber)).get().getNumber();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
index 787cf33..62d5bd8 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
@@ -151,8 +151,8 @@
         return null;
       }
 
-      Value value = code.createValue(typeLattice, instruction.getLocalInfo());
-      ConstInstruction replacement = staticField.valueAsConstInstruction(code, value, appView);
+      ConstInstruction replacement =
+          staticField.valueAsConstInstruction(code, instruction.getLocalInfo(), appView);
       if (replacement == null) {
         reporter.warning(
             new StringDiagnostic(
@@ -369,10 +369,23 @@
       return;
     }
 
-    ConstInstruction replacement = target.valueAsConstInstruction(code, current.dest(), appView);
+    ConstInstruction replacement =
+        target.valueAsConstInstruction(code, current.outValue().getLocalInfo(), appView);
     if (replacement != null) {
       affectedValues.addAll(current.outValue().affectedValues());
-      iterator.replaceCurrentInstruction(replacement);
+      if (target.mayTriggerClassInitializationSideEffects(appView, code.method.method.holder)) {
+        // To preserve class initialization side effects, original static-get remains as-is, but its
+        // value is replaced with constant.
+        replacement.setPosition(current.getPosition());
+        current.outValue().replaceUsers(replacement.outValue());
+        if (current.getBlock().hasCatchHandlers()) {
+          iterator.split(code, blocks).listIterator(code).add(replacement);
+        } else {
+          iterator.add(replacement);
+        }
+      } else {
+        iterator.replaceCurrentInstruction(replacement);
+      }
       if (replacement.isDexItemBasedConstString()) {
         code.method.getMutableOptimizationInfo().markUseIdentifierNameString();
       }
@@ -398,7 +411,8 @@
     }
 
     // Check if a this value is known const.
-    ConstInstruction replacement = target.valueAsConstInstruction(code, current.dest(), appView);
+    ConstInstruction replacement =
+        target.valueAsConstInstruction(code, current.outValue().getLocalInfo(), appView);
     if (replacement != null) {
       affectedValues.add(replacement.outValue());
       iterator.replaceCurrentInstruction(replacement);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/MemberValuePropagationWithClassInitializationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/MemberValuePropagationWithClassInitializationTest.java
new file mode 100644
index 0000000..9c32e92
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/MemberValuePropagationWithClassInitializationTest.java
@@ -0,0 +1,133 @@
+// Copyright (c) 2019, 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.membervaluepropagation;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+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 com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FieldSubject;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+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 MemberValuePropagationWithClassInitializationTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public MemberValuePropagationWithClassInitializationTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(MemberValuePropagationWithClassInitializationTest.class)
+        .addKeepMainRule(TestClass.class)
+        .enableInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("A!", "B!");
+  }
+
+  private void inspect(CodeInspector inspector) {
+    // A.field is present.
+    ClassSubject aClassSubject = inspector.clazz(A.class);
+    assertThat(aClassSubject, isPresent());
+
+    FieldSubject fieldSubject = aClassSubject.uniqueFieldWithName("field");
+    assertThat(fieldSubject, isPresent());
+
+    // B.method() is present.
+    ClassSubject bClassSubject = inspector.clazz(B.class);
+    assertThat(bClassSubject, isPresent());
+
+    MethodSubject methodSubject = bClassSubject.uniqueMethodWithName("method");
+    assertThat(methodSubject, isPresent());
+
+    // TestClass.missingFieldValuePropagation() and TestClass.missingMethodValuePropagation() are
+    // absent.
+    ClassSubject testClassSubject = inspector.clazz(TestClass.class);
+    assertThat(testClassSubject, isPresent());
+    assertThat(
+        testClassSubject.uniqueMethodWithName("missingFieldValuePropagation"), not(isPresent()));
+    assertThat(
+        testClassSubject.uniqueMethodWithName("missingMethodValuePropagation"), not(isPresent()));
+
+    // TestClass.main() still accesses A.field and invokes B.method().
+    MethodSubject mainMethodSubject = testClassSubject.mainMethod();
+    assertThat(mainMethodSubject, isPresent());
+    assertTrue(
+        mainMethodSubject
+            .streamInstructions()
+            .filter(InstructionSubject::isStaticGet)
+            .anyMatch(x -> x.getField() == fieldSubject.getField().field));
+    assertTrue(
+        mainMethodSubject
+            .streamInstructions()
+            .filter(InstructionSubject::isInvokeStatic)
+            .anyMatch(x -> x.getMethod() == methodSubject.getMethod().method));
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      if (A.field) {
+        missingFieldValuePropagation();
+      }
+      if (B.method()) {
+        missingMethodValuePropagation();
+      }
+    }
+
+    @NeverInline
+    static void missingFieldValuePropagation() {
+      System.out.println("Missing field value propagation!");
+    }
+
+    @NeverInline
+    static void missingMethodValuePropagation() {
+      System.out.println("Missing method value propagation!");
+    }
+  }
+
+  static class A {
+
+    static boolean field = false;
+
+    static {
+      System.out.println("A!");
+    }
+  }
+
+  static class B {
+
+    static {
+      System.out.println("B!");
+    }
+
+    static boolean method() {
+      return false;
+    }
+  }
+}