Extend StringBuilder optimization to potentially dead code.

Bug: 114002137, 137038659
Change-Id: Iade63e99f2fc67a2f269edde7d61dee07d3d317f
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 7ef8149..13ae8f7 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
@@ -186,6 +186,7 @@
     // Inspired by {@link JumboStringTest}. Some code intentionally may have too many append(...).
     private static final int CONCATENATION_THRESHOLD = 200;
     private static final String ANY_STRING = "*";
+    private static final String DUMMY = "$dummy$";
 
     private final IRCode code;
 
@@ -553,6 +554,7 @@
     // StringBuilders that are simplified by this analysis or simply dead (e.g., after applying
     // -assumenosideeffects to logging calls, then logging messages built by builders are dead).
     // Will be used to clean up uses of the builders, such as creation, <init>, and append calls.
+    final Set<Value> deadBuilders = Sets.newIdentityHashSet();
     final Set<Value> simplifiedBuilders = Sets.newIdentityHashSet();
 
     private StringConcatenationAnalysis applyConcatenationResults(Set<Value> candidateBuilders) {
@@ -566,12 +568,22 @@
         assert invoke.inValues().size() == 1;
         Value builder = invoke.getReceiver().getAliasedValue();
         assert invoke.hasOutValue();
-        // TODO(b/114002137): Can be extended to potentially dead code.
-        if (!invoke.outValue().isUsed()) {
-          it.removeOrReplaceByDebugLocalRead();
+        Value outValue = invoke.outValue();
+        if (outValue.isDead(appView, code)) {
+          // If the out value is still used but potentially dead, replace it with a dummy string.
+          if (outValue.isUsed()) {
+            Value dummy =
+                code.createValue(
+                    TypeLatticeElement.stringClassType(appView, definitelyNotNull()),
+                    invoke.getLocalInfo());
+            it.replaceCurrentInstruction(
+                new ConstString(dummy, factory.createString(DUMMY), throwingInfo));
+          } else {
+            it.removeOrReplaceByDebugLocalRead();
+          }
           // Although this builder is not simplified by this analysis, add that to the set so that
           // it can be removed at the final clean-up phase.
-          simplifiedBuilders.add(builder);
+          deadBuilders.add(builder);
           numberOfDeadBuilders++;
           continue;
         }
@@ -612,8 +624,7 @@
       }
       // If the result of toString() is no longer used, computing the compile-time constant is
       // even not necessary.
-      // TODO(b/114002137): Can be extended to potentially dead code.
-      if (!invoke.outValue().isUsed()) {
+      if (invoke.outValue().isDead(appView, code)) {
         return true;
       }
       Map<Instruction, BuilderState> perInstrState = builderStates.get(builder);
@@ -682,35 +693,47 @@
     }
 
     void removeTrivialBuilders() {
-      if (simplifiedBuilders.isEmpty()) {
+      if (deadBuilders.isEmpty() && simplifiedBuilders.isEmpty()) {
         return;
       }
-      // All instructions that refer to simplified builders are dead.
-      // Here, we remove append(...) calls, <init>, and new-instance in order.
+      Set<Value> buildersToRemove = Sets.union(deadBuilders, simplifiedBuilders);
+      // 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();
       while (it.hasNext()) {
         Instruction instr = it.next();
         if (instr.isInvokeVirtual()) {
           InvokeVirtual invoke = instr.asInvokeVirtual();
           DexMethod invokedMethod = invoke.getInvokedMethod();
-          if (optimizationConfiguration.isAppendMethod(invokedMethod)
-              && simplifiedBuilders.contains(invoke.getReceiver())) {
+          if (optimizationConfiguration.isToStringMethod(invokedMethod)
+              && buildersToRemove.contains(invoke.getReceiver())) {
             it.removeOrReplaceByDebugLocalRead();
           }
         }
       }
+      // append(...) and <init> don't have out values, so removing them won't bother each other.
       it = code.instructionListIterator();
       while (it.hasNext()) {
         Instruction instr = it.next();
+        if (instr.isInvokeVirtual()) {
+          InvokeVirtual invoke = instr.asInvokeVirtual();
+          DexMethod invokedMethod = invoke.getInvokedMethod();
+          if (optimizationConfiguration.isAppendMethod(invokedMethod)
+              && buildersToRemove.contains(invoke.getReceiver())) {
+            it.removeOrReplaceByDebugLocalRead();
+          }
+        }
         if (instr.isInvokeDirect()) {
           InvokeDirect invoke = instr.asInvokeDirect();
           DexMethod invokedMethod = invoke.getInvokedMethod();
           if (optimizationConfiguration.isBuilderInit(invokedMethod)
-              && simplifiedBuilders.contains(invoke.getReceiver())) {
+              && buildersToRemove.contains(invoke.getReceiver())) {
             it.removeOrReplaceByDebugLocalRead();
           }
         }
       }
+      // new-instance should be removed at last, since it will check the out value, builder, is not
+      // used anywhere, which we've removed so far.
       it = code.instructionListIterator();
       while (it.hasNext()) {
         Instruction instr = it.next();
@@ -721,6 +744,7 @@
           it.removeOrReplaceByDebugLocalRead();
         }
       }
+      assert code.isConsistentSSA();
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderStoredToDeadFieldTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderStoredToDeadFieldTest.java
index 13e58e1..b57dc6b 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderStoredToDeadFieldTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringBuilderStoredToDeadFieldTest.java
@@ -5,7 +5,7 @@
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.TestBase;
@@ -14,6 +14,7 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject.JumboStringMode;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -56,11 +57,13 @@
     MethodSubject mainMethod = main.mainMethod();
     assertThat(main, isPresent());
 
-    // TODO(b/114002137): can remove builder usage, which is linked to potentially dead code.
-    assertFalse(
+    assertTrue(
         mainMethod.streamInstructions().noneMatch(
             i -> i.isInvoke()
                 && i.getMethod().holder.toDescriptorString().contains("StringBuilder")));
+    assertTrue(
+        mainMethod.streamInstructions().noneMatch(
+            i -> i.isConstString(JumboStringMode.ALLOW)));
   }
 
   // TODO(b/114002137): Once enabled, remove this test-specific setting.
@@ -75,8 +78,7 @@
     public static void main(String... args) {
       StringBuilder b = new StringBuilder();
       b.append("CurrentTimeMillis: ");
-      // TODO(b/114002137): switch to use Long value as-is.
-      b.append(String.valueOf(System.currentTimeMillis()));
+      b.append(System.currentTimeMillis());
       neverRead = b.toString();
     }
   }