Handle if-instructions in string builder optimization

Change-Id: I56fb479bcb18956f3e0696dfb857264aba0bd03d
diff --git a/src/main/java/com/android/tools/r8/ir/code/If.java b/src/main/java/com/android/tools/r8/ir/code/If.java
index 706d470..62cb5a2 100644
--- a/src/main/java/com/android/tools/r8/ir/code/If.java
+++ b/src/main/java/com/android/tools/r8/ir/code/If.java
@@ -224,6 +224,12 @@
     return targetFromCondition(1);
   }
 
+  public BasicBlock targetFromNullObject() {
+    assert isZeroTest();
+    assert inValues.get(0).outType().isObject();
+    return targetFromCondition(0);
+  }
+
   public BasicBlock targetFromCondition(int cond) {
     assert Integer.signum(cond) == cond;
     switch (type) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizer.java
index 4dc7926..3864fb8 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizer.java
@@ -23,7 +23,9 @@
 import com.android.tools.r8.ir.code.ConstString;
 import com.android.tools.r8.ir.code.DominatorTree;
 import com.android.tools.r8.ir.code.DominatorTree.Assumption;
+import com.android.tools.r8.ir.code.Goto;
 import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeDirect;
@@ -35,6 +37,7 @@
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.logging.Log;
+import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableSet;
@@ -731,13 +734,34 @@
       if (deadBuilders.isEmpty() && simplifiedBuilders.isEmpty()) {
         return;
       }
-      Set<Value> buildersToRemove = Sets.union(deadBuilders, simplifiedBuilders);
+      Set<Value> affectedValues = Sets.newIdentityHashSet();
+      Set<Value> buildersToRemove = SetUtils.newIdentityHashSet(deadBuilders, simplifiedBuilders);
+      Set<Value> buildersUsedInIf = Sets.newIdentityHashSet();
       // All instructions that refer to dead/simplified builders are dead.
       // Here, we remove toString() calls, append(...) calls, <init>, and new-instance in order.
       InstructionListIterator it = code.instructionListIterator();
+      boolean shouldRemoveUnreachableBlocks = false;
       while (it.hasNext()) {
         Instruction instr = it.next();
-        if (instr.isInvokeMethod()) {
+        if (instr.isIf()) {
+          If theIf = instr.asIf();
+          Value lhs = theIf.lhs().getAliasedValue();
+          if (theIf.isZeroTest()) {
+            if (buildersToRemove.contains(lhs)) {
+              theIf.targetFromNullObject().unlinkSinglePredecessorSiblingsAllowed();
+              it.replaceCurrentInstruction(new Goto());
+              shouldRemoveUnreachableBlocks = true;
+            }
+          } else {
+            Value rhs = theIf.rhs().getAliasedValue();
+            if (buildersToRemove.contains(lhs)) {
+              buildersUsedInIf.add(lhs);
+            }
+            if (buildersToRemove.contains(rhs)) {
+              buildersUsedInIf.add(rhs);
+            }
+          }
+        } else if (instr.isInvokeMethod()) {
           InvokeMethod invoke = instr.asInvokeMethod();
           DexMethod invokedMethod = invoke.getInvokedMethod();
           if (optimizationConfiguration.isToStringMethod(invokedMethod)
@@ -746,6 +770,10 @@
           }
         }
       }
+      if (shouldRemoveUnreachableBlocks) {
+        affectedValues.addAll(code.removeUnreachableBlocks());
+      }
+      buildersToRemove.removeAll(buildersUsedInIf);
       // append(...) and <init> don't have out values, so removing them won't bother each other.
       it = code.instructionListIterator();
       while (it.hasNext()) {
@@ -787,6 +815,9 @@
           it.removeOrReplaceByDebugLocalRead();
         }
       }
+      if (!affectedValues.isEmpty()) {
+        new TypeAnalysis(appView).narrowing(affectedValues);
+      }
       assert code.isConsistentSSA();
     }
   }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIfNullUserTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIfNullUserTest.java
new file mode 100644
index 0000000..2d4b414
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIfNullUserTest.java
@@ -0,0 +1,49 @@
+// Copyright (c) 2021, 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.string;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class StringBuilderWithIfNullUserTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public StringBuilderWithIfNullUserTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(StringBuilderWithIfNullUserTest.class)
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello world!");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      StringBuilder builder = new StringBuilder();
+      builder.append("Hello world!");
+      if (builder != null) {
+        System.out.println(builder.toString());
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIfUserTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIfUserTest.java
index 0b733bd..14ba197 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIfUserTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderWithIfUserTest.java
@@ -7,7 +7,6 @@
 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.AssertUtils;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -28,17 +27,13 @@
 
   @Test
   public void test() throws Exception {
-    AssertUtils.assertFailsCompilationIf(
-        parameters.isDexRuntime(),
-        () -> {
-          testForR8(parameters.getBackend())
-              .addInnerClasses(StringBuilderWithIfUserTest.class)
-              .addKeepMainRule(Main.class)
-              .setMinApi(parameters.getApiLevel())
-              .compile()
-              .run(parameters.getRuntime(), Main.class)
-              .assertSuccessWithOutputLines("Hello world!");
-        });
+    testForR8(parameters.getBackend())
+        .addInnerClasses(StringBuilderWithIfUserTest.class)
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello world!");
   }
 
   static class Main {