Regression test for invalid optimization in D8

Bug: b/316744331
Change-Id: I3c5ea85c6a8aa32627d4c464a8b3ff475dc85cf5
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
index e043042..867d2a7 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -1572,6 +1572,14 @@
         return internalInstanceField(getMirror(), thisObjectId, fieldId);
       }
 
+      public void setFieldOnThis(String fieldName, String fieldSignature, Value value) {
+        long thisObjectId = getMirror().getThisObject(getThreadId(), getFrameId());
+        long classId = getMirror().getReferenceType(thisObjectId);
+        // TODO(zerny): Search supers too. This will only get the field if directly on the class.
+        long fieldId = findField(getMirror(), classId, fieldName, fieldSignature);
+        internalSetInstanceField(getMirror(), thisObjectId, fieldId, value);
+      }
+
       private long findField(VmMirror mirror, long classId, String fieldName,
           String fieldSignature) {
 
@@ -1633,6 +1641,19 @@
       }
     }
 
+    private static void internalSetInstanceField(
+        VmMirror mirror, long objectId, long fieldId, Value value) {
+      CommandPacket commandPacket =
+          new CommandPacket(
+              ObjectReferenceCommandSet.CommandSetID, ObjectReferenceCommandSet.SetValuesCommand);
+      commandPacket.setNextValueAsObjectID(objectId);
+      commandPacket.setNextValueAsInt(1); // field count.
+      commandPacket.setNextValueAsFieldID(fieldId);
+      commandPacket.setNextValueAsUntaggedValue(value);
+      ReplyPacket replyPacket = mirror.performCommand(commandPacket);
+      assert replyPacket.getErrorCode() == Error.NONE : "Error code: " + replyPacket.getErrorCode();
+    }
+
     private static Value internalInstanceField(VmMirror mirror, long objectId, long fieldId) {
       CommandPacket commandPacket =
           new CommandPacket(
diff --git a/src/test/java/com/android/tools/r8/regress/b316744331/Regress316744331Test.java b/src/test/java/com/android/tools/r8/regress/b316744331/Regress316744331Test.java
new file mode 100644
index 0000000..bf6229b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/regress/b316744331/Regress316744331Test.java
@@ -0,0 +1,112 @@
+// 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.regress.b316744331;
+
+import static junit.framework.TestCase.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import com.android.tools.r8.SingleTestRunResult;
+import com.android.tools.r8.TestBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.debug.DebugTestBase;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.StringUtils;
+import org.apache.harmony.jpda.tests.framework.jdwp.JDWPConstants.Tag;
+import org.apache.harmony.jpda.tests.framework.jdwp.Value;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class Regress316744331Test extends DebugTestBase {
+
+  static final String EXPECTED = StringUtils.lines("No null fields");
+
+  static final int ENTRY_LINE = 22;
+  static final int IN_STREAM_CHECK_LINE = 23;
+  static final int IN_STREAM_IS_NULL_LINE = 24;
+  static final int OUT_STREAM_CHECK_LINE = 29;
+  static final int OUT_STREAM_IS_NULL_LINE = 30;
+  static final int NORMAL_EXIT_LINE = 33;
+
+  static final Value NULL_VALUE = Value.createObjectValue(Tag.OBJECT_TAG, 0);
+
+  private final TestParameters parameters;
+  private final MethodReference fooMethod;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withDefaultCfRuntime()
+        .withDefaultDexRuntime()
+        .withAllApiLevels()
+        .build();
+  }
+
+  public Regress316744331Test(TestParameters parameters) {
+    this.parameters = parameters;
+    try {
+      this.fooMethod = Reference.methodFromMethod(Regress316744331TestClass.class.getMethod("foo"));
+    } catch (NoSuchMethodException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private TestBuilder<? extends SingleTestRunResult<?>, ?> getTestBuilder() {
+    return testForRuntime(parameters)
+        .addClasspathClasses(Regress316744331TestClass.class)
+        .addProgramClasses(Regress316744331TestClass.class);
+  }
+
+  @Test
+  public void testReference() throws Exception {
+    getTestBuilder()
+        .run(parameters.getRuntime(), Regress316744331TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testModifyInStreamField() throws Throwable {
+    runDebugTest(
+        getTestBuilder().debugConfig(parameters.getRuntime()),
+        Regress316744331TestClass.class,
+        breakpoint(fooMethod, ENTRY_LINE),
+        run(),
+        checkLine(ENTRY_LINE),
+        inspect(t -> assertEquals(NULL_VALUE, t.getFieldOnThis("m_instream", null))),
+        stepOver(),
+        checkLine(IN_STREAM_CHECK_LINE),
+        inspect(t -> assertNotEquals(NULL_VALUE, t.getFieldOnThis("m_instream", null))),
+        inspect(t -> t.setFieldOnThis("m_instream", null, NULL_VALUE)),
+        // Install a break point on the possible exits.
+        breakpoint(fooMethod, IN_STREAM_IS_NULL_LINE),
+        breakpoint(fooMethod, NORMAL_EXIT_LINE),
+        run(),
+        // TODO(b/316744331): D8 incorrectly optimizing out the code after the null check.
+        checkLine(parameters.isCfRuntime() ? IN_STREAM_IS_NULL_LINE : NORMAL_EXIT_LINE),
+        run());
+  }
+
+  @Test
+  public void testModifyOutStreamField() throws Throwable {
+    runDebugTest(
+        getTestBuilder().debugConfig(parameters.getRuntime()),
+        Regress316744331TestClass.class,
+        breakpoint(fooMethod, OUT_STREAM_CHECK_LINE),
+        run(),
+        checkLine(OUT_STREAM_CHECK_LINE),
+        inspect(t -> assertNotEquals(NULL_VALUE, t.getFieldOnThis("m_outstream", null))),
+        inspect(t -> t.setFieldOnThis("m_outstream", null, NULL_VALUE)),
+        // Install a break point on the possible exits.
+        breakpoint(fooMethod, OUT_STREAM_IS_NULL_LINE),
+        breakpoint(fooMethod, NORMAL_EXIT_LINE),
+        run(),
+        // TODO(b/316744331): D8 incorrectly optimizing out the code after the null check.
+        checkLine(parameters.isCfRuntime() ? OUT_STREAM_IS_NULL_LINE : NORMAL_EXIT_LINE),
+        run());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/regress/b316744331/Regress316744331TestClass.java b/src/test/java/com/android/tools/r8/regress/b316744331/Regress316744331TestClass.java
new file mode 100644
index 0000000..e7d2583
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/regress/b316744331/Regress316744331TestClass.java
@@ -0,0 +1,48 @@
+// 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.regress.b316744331;
+
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.PrintWriter;
+
+public class Regress316744331TestClass {
+
+  String filename;
+  FileReader m_instream;
+  PrintWriter m_outstream;
+
+  Regress316744331TestClass(String filename) {
+    this.filename = filename;
+  }
+
+  public void foo() throws IOException {
+    m_instream = new FileReader(filename);
+    if (null == m_instream) {
+      System.out.println("Reader is null!");
+      return;
+    }
+    m_outstream =
+        new PrintWriter(new java.io.BufferedWriter(new java.io.FileWriter(filename + ".java")));
+    if (null == m_outstream) {
+      System.out.println("Writer is null!");
+      return;
+    }
+    System.out.println("No null fields");
+  }
+
+  public static void main(String[] args) throws IOException {
+    // The debugger testing infra does not allow passing runtime arguments.
+    // Classpath should have an entry in normal and debugger runs so use it as the "file".
+    String cp = System.getProperty("java.class.path");
+    int jarIndex = cp.indexOf(".jar");
+    if (jarIndex < 0) {
+      jarIndex = cp.indexOf(".zip");
+    }
+    int start = cp.lastIndexOf(':', jarIndex);
+    String filename = cp.substring(Math.max(start, 0), jarIndex + 4);
+    new Regress316744331TestClass(filename).foo();
+  }
+}