Synthesize toStringIfNotNull() to optimize StringBuilder.append(Object)

Change-Id: I4a59c54c4a18f31248fb63364fe370496b6df24d
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UtilityMethodsForCodeOptimizations.java b/src/main/java/com/android/tools/r8/ir/optimize/UtilityMethodsForCodeOptimizations.java
index 247e2e3..1b55f13 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/UtilityMethodsForCodeOptimizations.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/UtilityMethodsForCodeOptimizations.java
@@ -19,6 +19,35 @@
 
 public class UtilityMethodsForCodeOptimizations {
 
+  public static UtilityMethodForCodeOptimizations synthesizeToStringIfNotNullMethod(
+      AppView<?> appView, ProgramMethod context, MethodProcessingId methodProcessingId) {
+    InternalOptions options = appView.options();
+    if (options.isGeneratingClassFiles()) {
+      // TODO(b/172194277): Allow synthetics when generating CF.
+      return null;
+    }
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    DexProto proto = dexItemFactory.createProto(dexItemFactory.voidType, dexItemFactory.objectType);
+    SyntheticItems syntheticItems = appView.getSyntheticItems();
+    ProgramMethod syntheticMethod =
+        syntheticItems.createMethod(
+            context,
+            dexItemFactory,
+            builder ->
+                builder
+                    .setProto(proto)
+                    .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
+                    .setCode(method -> getToStringIfNotNullCodeTemplate(method, options)),
+            methodProcessingId);
+    return new UtilityMethodForCodeOptimizations(syntheticMethod);
+  }
+
+  private static CfCode getToStringIfNotNullCodeTemplate(
+      DexMethod method, InternalOptions options) {
+    return CfUtilityMethodsForCodeOptimizations
+        .CfUtilityMethodsForCodeOptimizationsTemplates_toStringIfNotNull(options, method);
+  }
+
   public static UtilityMethodForCodeOptimizations synthesizeThrowClassCastExceptionIfNotNullMethod(
       AppView<?> appView, ProgramMethod context, MethodProcessingId methodProcessingId) {
     InternalOptions options = appView.options();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
index 5c38b07..25916f9 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
@@ -119,34 +119,33 @@
         new IdentityHashMap<>();
     while (instructionIterator.hasNext()) {
       Instruction instruction = instructionIterator.next();
-      if (instruction.isInvokeMethod()) {
-        InvokeMethod invoke = instruction.asInvokeMethod();
-        DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, code.context());
-        if (singleTarget != null) {
-          optimizeInvoke(
-              code, instructionIterator, invoke, singleTarget, affectedValues, optimizationStates);
-        }
+      if (!instruction.isInvokeMethod()) {
+        continue;
       }
+
+      InvokeMethod invoke = instruction.asInvokeMethod();
+      DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, code.context());
+      if (singleTarget == null) {
+        continue;
+      }
+
+      LibraryMethodModelCollection<?> optimizer =
+          libraryMethodModelCollections.get(singleTarget.getHolderType());
+      if (optimizer == null) {
+        continue;
+      }
+
+      LibraryMethodModelCollection.State optimizationState =
+          optimizationStates.computeIfAbsent(
+              optimizer,
+              libraryMethodModelCollection ->
+                  libraryMethodModelCollection.createInitialState(
+                      methodProcessor, methodProcessingId));
+      optimizer.optimize(
+          code, instructionIterator, invoke, singleTarget, affectedValues, optimizationState);
     }
     if (!affectedValues.isEmpty()) {
       new TypeAnalysis(appView).narrowing(affectedValues);
     }
   }
-
-  private void optimizeInvoke(
-      IRCode code,
-      InstructionListIterator instructionIterator,
-      InvokeMethod invoke,
-      DexClassAndMethod singleTarget,
-      Set<Value> affectedValues,
-      Map<LibraryMethodModelCollection<?>, LibraryMethodModelCollection.State> optimizationStates) {
-    LibraryMethodModelCollection<?> optimizer =
-        libraryMethodModelCollections.getOrDefault(
-            singleTarget.getHolderType(), NopLibraryMethodModelCollection.getInstance());
-    LibraryMethodModelCollection.State optimizationState =
-        optimizationStates.computeIfAbsent(
-            optimizer, LibraryMethodModelCollection::createInitialState);
-    optimizer.optimize(
-        code, instructionIterator, invoke, singleTarget, affectedValues, optimizationState);
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodModelCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodModelCollection.java
index 463f7e7..5cb30b3 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodModelCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodModelCollection.java
@@ -10,13 +10,16 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessingId;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.optimize.library.LibraryMethodModelCollection.State;
 import java.util.Set;
 
 /** Used to model the behavior of library methods for optimization purposes. */
 public interface LibraryMethodModelCollection<T extends State> {
 
-  default T createInitialState() {
+  default T createInitialState(
+      MethodProcessor methodProcessor, MethodProcessingId methodProcessingId) {
     return null;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/StatelessLibraryMethodModelCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/library/StatelessLibraryMethodModelCollection.java
index 6754170..8b36204 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/StatelessLibraryMethodModelCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/StatelessLibraryMethodModelCollection.java
@@ -9,6 +9,8 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessingId;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.optimize.library.StatelessLibraryMethodModelCollection.State;
 import java.util.Set;
 
@@ -16,7 +18,8 @@
     implements LibraryMethodModelCollection<State> {
 
   @Override
-  public final State createInitialState() {
+  public final State createInitialState(
+      MethodProcessor methodProcessor, MethodProcessingId methodProcessingId) {
     return null;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/StringBuilderMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/StringBuilderMethodOptimizer.java
index a88bae9..9cccb5d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/StringBuilderMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/StringBuilderMethodOptimizer.java
@@ -23,6 +23,10 @@
 import com.android.tools.r8.ir.code.InvokeStatic;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessingId;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations;
+import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations.UtilityMethodForCodeOptimizations;
 import com.android.tools.r8.ir.optimize.library.StringBuilderMethodOptimizer.State;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.WorkList;
@@ -33,20 +37,23 @@
 
 public class StringBuilderMethodOptimizer implements LibraryMethodModelCollection<State> {
 
+  private final AppView<?> appView;
   private final DexItemFactory dexItemFactory;
   private final InternalOptions options;
   private final StringBuildingMethods stringBuilderMethods;
 
   StringBuilderMethodOptimizer(AppView<?> appView) {
     DexItemFactory dexItemFactory = appView.dexItemFactory();
+    this.appView = appView;
     this.dexItemFactory = dexItemFactory;
     this.options = appView.options();
     this.stringBuilderMethods = dexItemFactory.stringBuilderMethods;
   }
 
   @Override
-  public State createInitialState() {
-    return new State();
+  public State createInitialState(
+      MethodProcessor methodProcessor, MethodProcessingId methodProcessingId) {
+    return new State(methodProcessor, methodProcessingId);
   }
 
   @Override
@@ -65,12 +72,13 @@
     if (invoke.isInvokeMethodWithReceiver()) {
       InvokeMethodWithReceiver invokeWithReceiver = invoke.asInvokeMethodWithReceiver();
       if (stringBuilderMethods.isAppendMethod(singleTarget.getReference())) {
-        optimizeAppend(instructionIterator, invokeWithReceiver, singleTarget, state);
+        optimizeAppend(code, instructionIterator, invokeWithReceiver, singleTarget, state);
       }
     }
   }
 
   private void optimizeAppend(
+      IRCode code,
       InstructionListIterator instructionIterator,
       InvokeMethodWithReceiver invoke,
       DexClassAndMethod singleTarget,
@@ -88,27 +96,52 @@
     } else if (stringBuilderMethods.isAppendObjectMethod(appendMethod)) {
       Value object = invoke.getArgument(1);
       if (object.isNeverNull()) {
+        // Replace the instruction by java.lang.Object.toString().
         instructionIterator.replaceCurrentInstruction(
             InvokeVirtual.builder()
-                .setSingleArgument(object)
                 .setMethod(dexItemFactory.objectMembers.toString)
+                .setSingleArgument(object)
                 .build());
       } else if (options.canUseJavaUtilObjects()) {
+        // Replace the instruction by java.util.Objects.toString().
         instructionIterator.replaceCurrentInstruction(
             InvokeStatic.builder()
-                .setSingleArgument(object)
                 .setMethod(dexItemFactory.objectsMethods.toStringWithObject)
+                .setSingleArgument(object)
                 .build());
         // Allow the java.util.Objects optimizer to optimize the newly added toString().
         instructionIterator.previous();
+      } else {
+        // Replace the instruction by toStringIfNotNull().
+        UtilityMethodForCodeOptimizations toStringIfNotNullMethod =
+            UtilityMethodsForCodeOptimizations.synthesizeToStringIfNotNullMethod(
+                appView, code.context(), state.methodProcessingId);
+        // TODO(b/172194277): Allow synthetics when generating CF.
+        if (toStringIfNotNullMethod != null) {
+          toStringIfNotNullMethod.optimize(state.methodProcessor);
+          InvokeStatic replacement =
+              InvokeStatic.builder()
+                  .setMethod(toStringIfNotNullMethod.getMethod())
+                  .setSingleArgument(object)
+                  .build();
+          instructionIterator.replaceCurrentInstruction(replacement);
+        }
       }
     }
   }
 
   class State implements LibraryMethodModelCollection.State {
 
+    final MethodProcessor methodProcessor;
+    final MethodProcessingId methodProcessingId;
+
     final Reference2BooleanMap<Value> unusedBuilders = new Reference2BooleanOpenHashMap<>();
 
+    State(MethodProcessor methodProcessor, MethodProcessingId methodProcessingId) {
+      this.methodProcessor = methodProcessor;
+      this.methodProcessingId = methodProcessingId;
+    }
+
     boolean isUnusedBuilder(Value value) {
       if (!unusedBuilders.containsKey(value)) {
         computeIsUnusedBuilder(value);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizations.java b/src/main/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizations.java
index 9484007..9625b93 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizations.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizations.java
@@ -72,4 +72,40 @@
         ImmutableList.of(),
         ImmutableList.of());
   }
+
+  public static CfCode CfUtilityMethodsForCodeOptimizationsTemplates_toStringIfNotNull(
+      InternalOptions options, DexMethod method) {
+    CfLabel label0 = new CfLabel();
+    CfLabel label1 = new CfLabel();
+    CfLabel label2 = new CfLabel();
+    CfLabel label3 = new CfLabel();
+    return new CfCode(
+        method.holder,
+        1,
+        1,
+        ImmutableList.of(
+            label0,
+            new CfLoad(ValueType.OBJECT, 0),
+            new CfIf(If.Type.EQ, ValueType.OBJECT, label2),
+            label1,
+            new CfLoad(ValueType.OBJECT, 0),
+            new CfInvoke(
+                182,
+                options.itemFactory.createMethod(
+                    options.itemFactory.objectType,
+                    options.itemFactory.createProto(options.itemFactory.stringType),
+                    options.itemFactory.createString("toString")),
+                false),
+            new CfStackInstruction(CfStackInstruction.Opcode.Pop),
+            label2,
+            new CfFrame(
+                new Int2ReferenceAVLTreeMap<>(
+                    new int[] {0},
+                    new FrameType[] {FrameType.initialized(options.itemFactory.objectType)}),
+                new ArrayDeque<>(Arrays.asList())),
+            new CfReturnVoid(),
+            label3),
+        ImmutableList.of(),
+        ImmutableList.of());
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendMaybeNullObjectTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendMaybeNullObjectTest.java
index 7d23755..78c0a10 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendMaybeNullObjectTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendMaybeNullObjectTest.java
@@ -42,7 +42,9 @@
               MethodSubject mainMethod = inspector.clazz(Main.class).mainMethod();
               assertThat(
                   mainMethod,
-                  notIf(instantiatesClass(StringBuilder.class), canUseJavaUtilObjects(parameters)));
+                  notIf(
+                      instantiatesClass(StringBuilder.class),
+                      canUseJavaUtilObjects(parameters) || parameters.isDexRuntime()));
             })
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithEmptyOutput();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendObjectTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendObjectTest.java
index 72c3721..d8ed11c 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendObjectTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendObjectTest.java
@@ -42,7 +42,9 @@
               MethodSubject mainMethod = inspector.clazz(Main.class).mainMethod();
               assertThat(
                   mainMethod,
-                  notIf(instantiatesClass(StringBuilder.class), canUseJavaUtilObjects(parameters)));
+                  notIf(
+                      instantiatesClass(StringBuilder.class),
+                      canUseJavaUtilObjects(parameters) || parameters.isDexRuntime()));
             })
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithEmptyOutput();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizationsTemplates.java b/src/test/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizationsTemplates.java
index d8d7c85..8f7c579 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizationsTemplates.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizationsTemplates.java
@@ -6,6 +6,12 @@
 
 public class CfUtilityMethodsForCodeOptimizationsTemplates {
 
+  public static void toStringIfNotNull(Object o) {
+    if (o != null) {
+      o.toString();
+    }
+  }
+
   public static void throwClassCastExceptionIfNotNull(Object o) {
     if (o != null) {
       throw new ClassCastException();