Reland "Relax the dead condition of field reads."

PS1 is same as go/r8g/33900.

PS2/4 re-enable the test in question on 6.0.1 / for R8 only.

Bug: 70295684, 124039115
Change-Id: I2d21a10829bb1f11f8080040a3869a63b3ba8f18
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 40756ae..5f3c869 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
@@ -4,6 +4,8 @@
 
 package com.android.tools.r8.ir.code;
 
+import static com.android.tools.r8.optimize.MemberRebindingAnalysis.isVisibleFromOriginalContext;
+
 import com.android.tools.r8.cf.LoadStoreHelper;
 import com.android.tools.r8.cf.TypeVerificationHelper;
 import com.android.tools.r8.cf.code.CfFieldInstruction;
@@ -19,6 +21,7 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -31,6 +34,7 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import org.objectweb.asm.Opcodes;
 
 public class InstanceGet extends FieldInstruction {
@@ -98,6 +102,29 @@
   }
 
   @Override
+  public boolean canBeDeadCode(
+      AppView<? extends AppInfoWithLiveness> appView, AppInfo appInfo, IRCode code) {
+    // Not applicable for D8.
+    if (appView == null || !appView.enableWholeProgramOptimizations()) {
+      return false;
+    }
+    // instance-get can be dead code as long as it cannot have any of the following:
+    // * NoSuchFieldError (resolution failure)
+    // * IllegalAccessError (not visible from the access context)
+    // * NullPointerException (null receiver).
+    // TODO(b/123857022): Should be possible to use definitionFor().
+    DexEncodedField resolvedField = appInfo.resolveFieldOn(getField().getHolder(), getField());
+    if (resolvedField == null) {
+      return false;
+    }
+    if (code == null
+        || !isVisibleFromOriginalContext(appInfo, code.method.method.getHolder(), resolvedField)) {
+      return false;
+    }
+    return object().getTypeLattice().nullability().isDefinitelyNotNull();
+  }
+
+  @Override
   public int maxInValueRegister() {
     return Constants.U4BIT_MAX;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/StaticGet.java b/src/main/java/com/android/tools/r8/ir/code/StaticGet.java
index b79d076..f27ebd6 100644
--- a/src/main/java/com/android/tools/r8/ir/code/StaticGet.java
+++ b/src/main/java/com/android/tools/r8/ir/code/StaticGet.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.code;
 
+import static com.android.tools.r8.optimize.MemberRebindingAnalysis.isVisibleFromOriginalContext;
+
 import com.android.tools.r8.cf.LoadStoreHelper;
 import com.android.tools.r8.cf.TypeVerificationHelper;
 import com.android.tools.r8.cf.code.CfFieldInstruction;
@@ -18,6 +20,7 @@
 import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis;
@@ -29,6 +32,7 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import org.objectweb.asm.Opcodes;
 
 public class StaticGet extends FieldInstruction {
@@ -91,6 +95,33 @@
   }
 
   @Override
+  public boolean canBeDeadCode(
+      AppView<? extends AppInfoWithLiveness> appView, AppInfo appInfo, IRCode code) {
+    // Not applicable for D8.
+    if (appView == null || !appView.enableWholeProgramOptimizations()) {
+      return false;
+    }
+    // static-get can be dead as long as it cannot have any of the following:
+    // * NoSuchFieldError (resolution failure)
+    // * IllegalAccessError (not visible from the access context)
+    // * side-effects in <clinit>
+    // TODO(b/123857022): Should be possible to use definitionFor().
+    DexEncodedField resolvedField = appInfo.resolveFieldOn(getField().getHolder(), getField());
+    if (resolvedField == null) {
+      return false;
+    }
+    if (code == null
+        || !isVisibleFromOriginalContext(appInfo, code.method.method.getHolder(), resolvedField)) {
+      return false;
+    }
+    DexType context = code.method.method.holder;
+    return !getField().clazz.classInitializationMayHaveSideEffects(
+        appInfo,
+        // Types that are a super type of `context` are guaranteed to be initialized already.
+        type -> context.isSubtypeOf(type, appInfo));
+  }
+
+  @Override
   public int maxInValueRegister() {
     return Constants.U8BIT_MAX;
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
index 3ff88b9..a2bed1d 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.optimize;
 
+import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedField;
@@ -290,14 +291,15 @@
       Set<DexEncodedMethod> contexts = fieldsWithContexts.get(field);
       if (target != null && target.field != field
           && contexts.stream().allMatch(context ->
-              isVisibleFromOriginalContext(context.method.getHolder(), target))) {
+              isVisibleFromOriginalContext(appInfo, context.method.getHolder(), target))) {
         builder.map(field,
             lense.lookupField(validTargetFor(target.field, field, lookupTargetOnClass)));
       }
     }
   }
 
-  private boolean isVisibleFromOriginalContext(DexType context, DexEncodedField field) {
+  public static boolean isVisibleFromOriginalContext(
+      AppInfo appInfo, DexType context, DexEncodedField field) {
     DexType holderType = field.field.getHolder();
     DexClass holder = appInfo.definitionFor(holderType);
     if (holder == null) {
diff --git a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
index 5efc26d..6d34f44 100644
--- a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
@@ -508,7 +508,6 @@
             "156-register-dex-file-multi-loader",
             "412-new-array",
             "530-checker-lse2",
-            "550-new-instance-clinit",
             "580-checker-round",
             "594-invoke-super",
             "625-checker-licm-regressions",
@@ -670,6 +669,12 @@
           // lib64 libarttest.so: wrong ELF class ELFCLASS64.
           .put("543-env-long-ref",
               TestCondition.match(TestCondition.runtimesUpTo(DexVm.Version.V4_4_4)))
+          // Leaving two static-get triggers LSE bug on 6.0.1 (b/25735083).
+          // R8, with subtyping, knows the first sget is dead, and removing it avoids the bug.
+          // Due to the lack of subtype hierarchy, D8 can't guarantee <clinit> side effects.
+          .put("550-new-instance-clinit",
+              TestCondition.match(
+                  TestCondition.D8_COMPILER, TestCondition.runtimes(DexVm.Version.V6_0_1)))
           // Regression test for an issue that is not fixed on version 5.1.1. Throws an Exception
           // instance instead of the expected NullPointerException. This bug is only tickled when
           // running the R8 generated code when starting from jar or from dex code generated with
diff --git a/src/test/java/com/android/tools/r8/jasmin/MemberResolutionTest.java b/src/test/java/com/android/tools/r8/jasmin/MemberResolutionTest.java
index 33f28fc..ea8ed0b 100644
--- a/src/test/java/com/android/tools/r8/jasmin/MemberResolutionTest.java
+++ b/src/test/java/com/android/tools/r8/jasmin/MemberResolutionTest.java
@@ -462,7 +462,7 @@
     main.addMainMethod(
         ".limit stack 2",
         ".limit locals 1",
-        "  getstatic Empty/aMethod I",
+        "  getstatic Empty/aField I",
         "  return");
 
     ensureRuntimeException(builder, NoSuchFieldError.class);
diff --git a/src/test/java/com/android/tools/r8/shaking/FieldReadsJasminTest.java b/src/test/java/com/android/tools/r8/shaking/FieldReadsJasminTest.java
new file mode 100644
index 0000000..f306e04
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/FieldReadsJasminTest.java
@@ -0,0 +1,286 @@
+// 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.shaking;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isRenamed;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.jasmin.JasminBuilder;
+import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder;
+import com.android.tools.r8.jasmin.JasminTestBase;
+import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+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 com.google.common.collect.ImmutableList;
+import java.util.Iterator;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class FieldReadsJasminTest extends JasminTestBase {
+  private static final String CLS = "Empty";
+  private static final String MAIN = "Main";
+  private final Backend backend;
+
+  @Parameterized.Parameters(name = "Backend: {0}")
+  public static Object[] data() {
+    return Backend.values();
+  }
+
+  public FieldReadsJasminTest(Backend backend) {
+    this.backend = backend;
+  }
+
+  @Test
+  public void testInstanceGet_nonNullReceiver() throws Exception {
+    JasminBuilder builder = new JasminBuilder();
+
+    ClassBuilder empty = builder.addClass(CLS);
+    empty.addField("protected", "aField", "I", null);
+
+    ClassBuilder main = builder.addClass(MAIN);
+    main.addMainMethod(
+        ".limit stack 2",
+        ".limit locals 1",
+        "  new Empty",
+        "  dup",
+        "  invokespecial Empty/<init>()V",
+        "  getfield Empty/aField I",
+        "  return");
+
+    ensureNoFields(builder, empty);
+  }
+
+  @Test
+  public void testStaticGet_noSideEffect() throws Exception {
+    JasminBuilder builder = new JasminBuilder();
+
+    ClassBuilder empty = builder.addClass(CLS);
+    empty.addStaticField("sField", "I");
+
+    ClassBuilder main = builder.addClass(MAIN);
+    main.addMainMethod(
+        ".limit stack 2",
+        ".limit locals 1",
+        "  getstatic Empty/sField I",
+        "  return");
+
+    ensureNoFields(builder, empty);
+  }
+
+  private void ensureNoFields(JasminBuilder app, ClassBuilder clazz) throws Exception {
+    testForR8(backend)
+        .addProgramClassFileData(app.buildClasses())
+        .addKeepRules("-keep class * { <methods>; }")
+        .compile()
+        .inspect(inspector -> {
+          ClassSubject classSubject = inspector.clazz(clazz.name);
+          assertThat(classSubject, isPresent());
+          classSubject.forAllFields(foundFieldSubject -> {
+            fail("Expect not to see any fields.");
+          });
+        });
+  }
+
+  @Test
+  public void testStaticGet_nonTrivialClinit_yetSameHolder() throws Exception {
+    JasminBuilder builder = new JasminBuilder();
+
+    ClassBuilder main = builder.addClass(MAIN);
+    // static int sField = System.currentTimeMillis() >=0 ? 42 : 0;
+    main.addStaticField("sField", "I", null);
+    main.addClassInitializer(
+        ".limit stack 4",
+        ".limit locals 0",
+        "  invokestatic java/lang/System/currentTimeMillis()J",
+        "  lconst_0",
+        "  lcmp",
+        "  iflt l",
+        "  bipush 42",
+        "  goto p",
+        "l:",
+        "  iconst_0",
+        "p:",
+        "  putstatic Main/sField I",
+        "  return");
+    MethodSignature mainMethod = main.addMainMethod(
+        ".limit stack 2",
+        ".limit locals 1",
+        "  getstatic Main/sField I",
+        "  return");
+
+    ensureFieldExistsButNoRead(builder, main, mainMethod, main, "sField");
+  }
+
+  private void ensureFieldExistsButNoRead(
+      JasminBuilder app,
+      ClassBuilder clazz,
+      MethodSignature method,
+      ClassBuilder fieldHolder,
+      String fieldName)
+      throws Exception {
+    testForR8(backend)
+        .addProgramClassFileData(app.buildClasses())
+        .addKeepRules("-keep class * { <methods>; }")
+        .compile()
+        .inspect(inspector -> {
+          FieldSubject fld = inspector.clazz(fieldHolder.name).uniqueFieldWithName(fieldName);
+          assertThat(fld, isRenamed());
+
+          ClassSubject classSubject = inspector.clazz(clazz.name);
+          assertThat(classSubject, isPresent());
+          MethodSubject methodSubject = classSubject.uniqueMethodWithName(method.name);
+          assertThat(methodSubject, isPresent());
+          Iterator<InstructionSubject> it =
+              methodSubject.iterateInstructions(InstructionSubject::isFieldAccess);
+          assertFalse(it.hasNext());
+        });
+  }
+
+  @Test
+  public void testInstanceGet_nullableReceiver() throws Exception {
+    JasminBuilder builder = new JasminBuilder();
+
+    ClassBuilder empty = builder.addClass(CLS);
+    empty.addDefaultConstructor();
+    empty.addField("protected", "aField", "I", null);
+    MethodSignature foo = empty.addStaticMethod("foo", ImmutableList.of("L" + CLS + ";"), "V",
+        ".limit stack 2",
+        ".limit locals 1",
+        "  aload 0",
+        "  getfield Empty/aField I",
+        "  return");
+
+    ClassBuilder main = builder.addClass(MAIN);
+    main.addMainMethod(
+        ".limit stack 2",
+        ".limit locals 1",
+        "  aconst_null",
+        "  invokestatic Empty/foo(L" + CLS + ";)V",
+        "  return");
+
+    ensureFieldExistsAndReadOnlyOnce(builder, empty, foo, empty, "aField");
+  }
+
+  @Test
+  public void testStaticGet_nonTrivialClinit() throws Exception {
+    JasminBuilder builder = new JasminBuilder();
+
+    ClassBuilder empty = builder.addClass(CLS);
+    empty.addDefaultConstructor();
+    empty.addStaticField("sField", "I");
+    empty.addClassInitializer(
+        ".limit stack 3",
+        ".limit locals 1",
+        "  getstatic java/lang/System/out Ljava/io/PrintStream;",
+        "  ldc \"hello\"",
+        "  invokevirtual java/io/PrintStream/print(Ljava/lang/String;)V",
+        "  return");
+
+    ClassBuilder main = builder.addClass(MAIN);
+    MethodSignature mainMethod = main.addMainMethod(
+        ".limit stack 2",
+        ".limit locals 1",
+        "  getstatic Empty/sField I",
+        "  return");
+
+    ensureFieldExistsAndReadOnlyOnce(builder, main, mainMethod, empty, "sField");
+  }
+
+  @Test
+  public void testStaticGet_allocation() throws Exception {
+    JasminBuilder builder = new JasminBuilder();
+
+    ClassBuilder empty = builder.addClass(CLS);
+    empty.addDefaultConstructor();
+    empty.addStaticField("sField", "Ljava/lang/String;", "\"8\"");
+
+    ClassBuilder main = builder.addClass(MAIN);
+    MethodSignature mainMethod = main.addMainMethod(
+        ".limit stack 2",
+        ".limit locals 1",
+        "  getstatic Empty/sField Ljava/lang/String;",
+        "  return");
+
+    ensureFieldExistsAndReadOnlyOnce(builder, main, mainMethod, empty, "sField");
+  }
+
+  @Test
+  public void b124039115() throws Exception {
+    JasminBuilder builder = new JasminBuilder();
+
+    ClassBuilder empty = builder.addClass(CLS);
+    empty.addDefaultConstructor();
+    empty.addClassInitializer(
+        ".limit stack 2",
+        ".limit locals 0",
+        "  getstatic Main/sField I",
+        "  iconst_1",
+        "  iadd",
+        "  putstatic Main/sField I",
+        "  return");
+
+    ClassBuilder main = builder.addClass(MAIN);
+    main.addDefaultConstructor();
+    main.addStaticField("sField", "I", null);
+    main.addClassInitializer(
+        ".limit stack 2",
+        ".limit locals 0",
+        "  bipush 1",
+        "  putstatic Main/sField I",
+        "  return");
+    MethodSignature mainMethod = main.addMainMethod(
+        ".limit stack 3",
+        ".limit locals 2",
+        "  getstatic Main/sField I",
+        "  new Empty",
+        "  dup",
+        "  invokespecial Empty/<init>()V",
+        "  getstatic Main/sField I",
+        "  bipush 2",
+        "  if_icmpeq r",
+        "  aconst_null",
+        "  athrow",
+        "r:",
+        "  return");
+
+    ensureFieldExistsAndReadOnlyOnce(builder, main, mainMethod, main, "sField");
+  }
+
+  private void ensureFieldExistsAndReadOnlyOnce(
+      JasminBuilder app,
+      ClassBuilder clazz,
+      MethodSignature method,
+      ClassBuilder fieldHolder,
+      String fieldName)
+      throws Exception {
+    testForR8(backend)
+        .addProgramClassFileData(app.buildClasses())
+        .addKeepRules("-keep class * { <methods>; }")
+        .compile()
+        .inspect(inspector -> {
+          FieldSubject fld = inspector.clazz(fieldHolder.name).uniqueFieldWithName(fieldName);
+          assertThat(fld, isRenamed());
+
+          ClassSubject classSubject = inspector.clazz(clazz.name);
+          assertThat(classSubject, isPresent());
+          MethodSubject methodSubject = classSubject.uniqueMethodWithName(method.name);
+          assertThat(methodSubject, isPresent());
+          Iterator<InstructionSubject> it =
+              methodSubject.iterateInstructions(InstructionSubject::isFieldAccess);
+          assertTrue(it.hasNext());
+          assertEquals(fld.getFinalName(), it.next().getField().name.toString());
+          assertFalse(it.hasNext());
+        });
+  }
+
+}