Improve unused toString() removal

Change-Id: I6db043d4cd781c2a587fc062f9f881b3c4a16a4b
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index 6bedacf..531c78a 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -122,9 +122,9 @@
       this.callSiteOptimizationInfoPropagator = null;
     }
 
-    this.libraryMemberOptimizer = new LibraryMemberOptimizer(this);
     this.libraryMethodSideEffectModelCollection =
         new LibraryMethodSideEffectModelCollection(dexItemFactory());
+    this.libraryMemberOptimizer = new LibraryMemberOptimizer(this);
 
     if (enableWholeProgramOptimizations() && options().protoShrinking().isProtoShrinkingEnabled()) {
       this.protoShrinker = new ProtoShrinker(withLiveness());
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index a93000e..385fb58 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -23,7 +23,6 @@
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.ReferenceTypeElement;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
-import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.desugar.NestBasedAccessDesugaring;
@@ -453,13 +452,16 @@
   public final StringBuildingMethods stringBufferMethods =
       new StringBuildingMethods(stringBufferType);
   public final BooleanMembers booleanMembers = new BooleanMembers();
+  public final ByteMembers byteMembers = new ByteMembers();
+  public final CharMembers charMembers = new CharMembers();
   public final FloatMembers floatMembers = new FloatMembers();
   public final IntegerMembers integerMembers = new IntegerMembers();
+  public final LongMembers longMembers = new LongMembers();
   public final ObjectsMethods objectsMethods = new ObjectsMethods();
   public final ObjectMembers objectMembers = new ObjectMembers();
+  public final ShortMembers shortMembers = new ShortMembers();
   public final StringMembers stringMembers = new StringMembers();
-  public final LongMembers longMembers = new LongMembers();
-  public final DoubleMethods doubleMethods = new DoubleMethods();
+  public final DoubleMembers doubleMembers = new DoubleMembers();
   public final ThrowableMethods throwableMethods = new ThrowableMethods();
   public final AssertionErrorMethods assertionErrorMethods = new AssertionErrorMethods();
   public final ClassMethods classMethods = new ClassMethods();
@@ -715,7 +717,22 @@
 
   public Set<DexType> libraryClassesWithoutStaticInitialization =
       ImmutableSet.of(
-          boxedBooleanType, enumType, npeType, objectType, stringBufferType, stringBuilderType);
+          boxedBooleanType,
+          boxedByteType,
+          boxedCharType,
+          boxedDoubleType,
+          boxedFloatType,
+          boxedIntType,
+          boxedLongType,
+          boxedNumberType,
+          boxedShortType,
+          boxedVoidType,
+          enumType,
+          npeType,
+          objectType,
+          stringBufferType,
+          stringBuilderType,
+          stringType);
 
   private boolean skipNameValidationForTesting = false;
 
@@ -731,35 +748,12 @@
     return dexMethod == metafactoryMethod || dexMethod == metafactoryAltMethod;
   }
 
-  public interface LibraryMembers {
+  public abstract static class LibraryMembers {
 
-    void forEachFinalField(Consumer<DexField> consumer);
+    public void forEachFinalField(Consumer<DexField> consumer) {}
   }
 
-  public class BooleanMembers implements LibraryMembers {
-
-    public final DexField FALSE = createField(boxedBooleanType, boxedBooleanType, "FALSE");
-    public final DexField TRUE = createField(boxedBooleanType, boxedBooleanType, "TRUE");
-    public final DexField TYPE = createField(boxedBooleanType, classType, "TYPE");
-
-    public final DexMethod booleanValue =
-        createMethod(boxedBooleanType, createProto(booleanType), "booleanValue");
-    public final DexMethod parseBoolean =
-        createMethod(boxedBooleanType, createProto(booleanType, stringType), "parseBoolean");
-    public final DexMethod valueOf =
-        createMethod(boxedBooleanType, createProto(boxedBooleanType, booleanType), "valueOf");
-
-    private BooleanMembers() {}
-
-    @Override
-    public void forEachFinalField(Consumer<DexField> consumer) {
-      consumer.accept(FALSE);
-      consumer.accept(TRUE);
-      consumer.accept(TYPE);
-    }
-  }
-
-  public class AndroidOsBuildMembers implements LibraryMembers {
+  public class AndroidOsBuildMembers extends LibraryMembers {
 
     public final DexField BOOTLOADER = createField(androidOsBuildType, stringType, "BOOTLOADER");
     public final DexField BRAND = createField(androidOsBuildType, stringType, "BRAND");
@@ -805,7 +799,7 @@
     }
   }
 
-  public class AndroidOsBuildVersionMembers implements LibraryMembers {
+  public class AndroidOsBuildVersionMembers extends LibraryMembers {
 
     public final DexField CODENAME = createField(androidOsBuildVersionType, stringType, "CODENAME");
     public final DexField RELEASE = createField(androidOsBuildVersionType, stringType, "RELEASE");
@@ -824,7 +818,7 @@
     }
   }
 
-  public class AndroidOsBundleMembers implements LibraryMembers {
+  public class AndroidOsBundleMembers extends LibraryMembers {
 
     public final DexField CREATOR =
         createField(androidOsBundleType, androidOsParcelableCreatorType, "CREATOR");
@@ -837,7 +831,7 @@
     }
   }
 
-  public class AndroidSystemOsConstantsMembers implements LibraryMembers {
+  public class AndroidSystemOsConstantsMembers extends LibraryMembers {
 
     public final DexField S_IRUSR = createField(androidSystemOsConstantsType, intType, "S_IRUSR");
     public final DexField S_IXUSR = createField(androidSystemOsConstantsType, intType, "S_IXUSR");
@@ -849,7 +843,7 @@
     }
   }
 
-  public class AndroidViewViewMembers implements LibraryMembers {
+  public class AndroidViewViewMembers extends LibraryMembers {
 
     public final DexField TRANSLATION_Z =
         createField(androidViewViewType, androidUtilPropertyType, "TRANSLATION_Z");
@@ -872,10 +866,54 @@
     }
   }
 
-  public class FloatMembers implements LibraryMembers {
+  public class BooleanMembers extends LibraryMembers {
+
+    public final DexField FALSE = createField(boxedBooleanType, boxedBooleanType, "FALSE");
+    public final DexField TRUE = createField(boxedBooleanType, boxedBooleanType, "TRUE");
+    public final DexField TYPE = createField(boxedBooleanType, classType, "TYPE");
+
+    public final DexMethod booleanValue =
+        createMethod(boxedBooleanType, createProto(booleanType), "booleanValue");
+    public final DexMethod parseBoolean =
+        createMethod(boxedBooleanType, createProto(booleanType, stringType), "parseBoolean");
+    public final DexMethod valueOf =
+        createMethod(boxedBooleanType, createProto(boxedBooleanType, booleanType), "valueOf");
+    public final DexMethod toString =
+        createMethod(boxedBooleanType, createProto(stringType), "toString");
+
+    private BooleanMembers() {}
+
+    @Override
+    public void forEachFinalField(Consumer<DexField> consumer) {
+      consumer.accept(FALSE);
+      consumer.accept(TRUE);
+      consumer.accept(TYPE);
+    }
+  }
+
+  public class ByteMembers extends LibraryMembers {
+
+    public final DexMethod toString =
+        createMethod(boxedByteType, createProto(stringType), "toString");
+
+    private ByteMembers() {}
+  }
+
+  public class CharMembers extends LibraryMembers {
+
+    public final DexMethod toString =
+        createMethod(boxedCharType, createProto(stringType), "toString");
+
+    private CharMembers() {}
+  }
+
+  public class FloatMembers extends LibraryMembers {
 
     public final DexField TYPE = createField(boxedFloatType, classType, "TYPE");
 
+    public final DexMethod toString =
+        createMethod(boxedFloatType, createProto(stringType), "toString");
+
     private FloatMembers() {}
 
     @Override
@@ -884,7 +922,7 @@
     }
   }
 
-  public class JavaIoFileMembers implements LibraryMembers {
+  public class JavaIoFileMembers extends LibraryMembers {
 
     public final DexField pathSeparator = createField(javaIoFileType, stringType, "pathSeparator");
     public final DexField separator = createField(javaIoFileType, stringType, "separator");
@@ -896,7 +934,7 @@
     }
   }
 
-  public class JavaMathBigIntegerMembers implements LibraryMembers {
+  public class JavaMathBigIntegerMembers extends LibraryMembers {
 
     public final DexField ONE = createField(javaMathBigIntegerType, javaMathBigIntegerType, "ONE");
     public final DexField ZERO =
@@ -909,7 +947,7 @@
     }
   }
 
-  public class JavaNioByteOrderMembers implements LibraryMembers {
+  public class JavaNioByteOrderMembers extends LibraryMembers {
 
     public final DexField LITTLE_ENDIAN =
         createField(javaNioByteOrderType, javaNioByteOrderType, "LITTLE_ENDIAN");
@@ -937,7 +975,7 @@
     }
   }
 
-  public class JavaUtilComparatorMembers implements LibraryMembers {
+  public class JavaUtilComparatorMembers extends LibraryMembers {
 
     public final DexField EMPTY_LIST =
         createField(javaUtilCollectionsType, javaUtilListType, "EMPTY_LIST");
@@ -951,7 +989,7 @@
     }
   }
 
-  public class JavaUtilConcurrentTimeUnitMembers implements LibraryMembers {
+  public class JavaUtilConcurrentTimeUnitMembers extends LibraryMembers {
 
     public final DexField DAYS =
         createField(javaUtilConcurrentTimeUnitType, javaUtilConcurrentTimeUnitType, "DAYS");
@@ -980,7 +1018,7 @@
     }
   }
 
-  public class JavaUtilLocaleMembers implements LibraryMembers {
+  public class JavaUtilLocaleMembers extends LibraryMembers {
 
     public final DexField ENGLISH = createField(javaUtilLocaleType, javaUtilLocaleType, "ENGLISH");
     public final DexField ROOT = createField(javaUtilLocaleType, javaUtilLocaleType, "ROOT");
@@ -994,7 +1032,7 @@
     }
   }
 
-  public class JavaUtilLoggingLevelMembers implements LibraryMembers {
+  public class JavaUtilLoggingLevelMembers extends LibraryMembers {
 
     public final DexField CONFIG =
         createField(javaUtilLoggingLevelType, javaUtilLoggingLevelType, "CONFIG");
@@ -1020,11 +1058,13 @@
     }
   }
 
-  public class LongMembers implements LibraryMembers {
+  public class LongMembers extends LibraryMembers {
 
     public final DexField TYPE = createField(boxedLongType, classType, "TYPE");
 
     public final DexMethod compare;
+    public final DexMethod toString =
+        createMethod(boxedLongType, createProto(stringType), "toString");
 
     private LongMembers() {
       compare = createMethod(boxedLongDescriptor,
@@ -1037,11 +1077,14 @@
     }
   }
 
-  public class DoubleMethods {
+  public class DoubleMembers {
 
     public final DexMethod isNaN;
 
-    private DoubleMethods() {
+    public final DexMethod toString =
+        createMethod(boxedDoubleType, createProto(stringType), "toString");
+
+    private DoubleMembers() {
       isNaN =
           createMethod(
               boxedDoubleDescriptor,
@@ -1051,10 +1094,13 @@
     }
   }
 
-  public class IntegerMembers implements LibraryMembers {
+  public class IntegerMembers extends LibraryMembers {
 
     public final DexField TYPE = createField(boxedIntType, classType, "TYPE");
 
+    public final DexMethod toString =
+        createMethod(boxedIntType, createProto(stringType), "toString");
+
     @Override
     public void forEachFinalField(Consumer<DexField> consumer) {
       consumer.accept(TYPE);
@@ -1445,7 +1491,15 @@
     }
   }
 
-  public class StringMembers implements LibraryMembers {
+  public class ShortMembers extends LibraryMembers {
+
+    public final DexMethod toString =
+        createMethod(boxedShortType, createProto(stringType), "toString");
+
+    private ShortMembers() {}
+  }
+
+  public class StringMembers extends LibraryMembers {
 
     public final DexField CASE_INSENSITIVE_ORDER =
         createField(stringType, javaUtilComparatorType, "CASE_INSENSITIVE_ORDER");
@@ -1454,6 +1508,8 @@
     public final DexMethod length;
 
     public final DexMethod concat;
+    public final DexMethod constructor =
+        createMethod(stringType, createProto(voidType, stringType), constructorMethodName);
     public final DexMethod contains;
     public final DexMethod startsWith;
     public final DexMethod endsWith;
@@ -1625,22 +1681,22 @@
       return constructorMethods.contains(method);
     }
 
-    public boolean constructorInvokeIsSideEffectFree(InvokeMethod invoke) {
-      DexMethod invokedMethod = invoke.getInvokedMethod();
-      if (invokedMethod == charSequenceConstructor) {
-        // Performs callbacks on the given CharSequence, which may have side effects.
-        TypeElement charSequenceType = invoke.getArgument(1).getType();
-        return charSequenceType.isClassType()
-            && charSequenceType.asClassType().getClassType() == stringType;
-      }
-
+    public boolean constructorInvokeIsSideEffectFree(
+        DexMethod invokedMethod, List<Value> arguments) {
       if (invokedMethod == defaultConstructor) {
         return true;
       }
 
+      if (invokedMethod == charSequenceConstructor) {
+        // Performs callbacks on the given CharSequence, which may have side effects.
+        TypeElement charSequenceType = arguments.get(1).getType();
+        return charSequenceType.isClassType()
+            && charSequenceType.asClassType().getClassType() == stringType;
+      }
+
       if (invokedMethod == intConstructor) {
         // NegativeArraySizeException - if the capacity argument is less than 0.
-        Value capacityValue = invoke.inValues().get(1);
+        Value capacityValue = arguments.get(1);
         if (capacityValue.hasValueRange()) {
           return capacityValue.getValueRange().getMin() >= 0;
         }
@@ -1649,7 +1705,7 @@
 
       if (invokedMethod == stringConstructor) {
         // NullPointerException - if str is null.
-        Value strValue = invoke.inValues().get(1);
+        Value strValue = arguments.get(1);
         return !strValue.getType().isNullable();
       }
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeElement.java
index 095333f..a2c8d57 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeElement.java
@@ -274,6 +274,15 @@
     return false;
   }
 
+  public final boolean isClassType(DexType type) {
+    assert type.isClassType();
+    return isClassType() && asClassType().getClassType() == type;
+  }
+
+  public final boolean isStringType(DexItemFactory dexItemFactory) {
+    return isClassType(dexItemFactory.stringType);
+  }
+
   public ClassTypeElement asClassType() {
     return null;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 7fc0d77..e8ba6bd 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -129,6 +129,10 @@
     return outValue != null;
   }
 
+  public boolean hasUnusedOutValue() {
+    return !hasOutValue() || !outValue().hasAnyUsers();
+  }
+
   public Value outValue() {
     return outValue;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index 520a408..fabcd3e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -2460,13 +2460,35 @@
     assert code.isConsistentSSA();
   }
 
+  static class ControlFlowSimplificationResult {
+    private boolean anyAffectedValues;
+    private boolean anySimplifications;
+
+    private ControlFlowSimplificationResult(boolean anyAffectedValues, boolean anySimplifications) {
+      assert !anyAffectedValues || anySimplifications;
+      this.anyAffectedValues = anyAffectedValues;
+      this.anySimplifications = anySimplifications;
+    }
+
+    public boolean anyAffectedValues() {
+      return anyAffectedValues;
+    }
+
+    public boolean anySimplifications() {
+      return anySimplifications;
+    }
+  }
+
   public boolean simplifyControlFlow(IRCode code) {
     boolean anyAffectedValues = rewriteSwitch(code);
-    anyAffectedValues |= simplifyIf(code);
+    anyAffectedValues |= simplifyIf(code).anyAffectedValues();
     return anyAffectedValues;
   }
 
-  public boolean simplifyIf(IRCode code) {
+  public ControlFlowSimplificationResult simplifyIf(IRCode code) {
+    BasicBlockBehavioralSubsumption behavioralSubsumption =
+        new BasicBlockBehavioralSubsumption(appView, code);
+    boolean simplified = false;
     for (BasicBlock block : code.blocks) {
       // Skip removed (= unreachable) blocks.
       if (block.getNumber() != 0 && block.getPredecessors().isEmpty()) {
@@ -2474,114 +2496,34 @@
       }
       if (block.exit().isIf()) {
         flipIfBranchesIfNeeded(code, block);
-        rewriteIfWithConstZero(code, block);
+        if (rewriteIfWithConstZero(code, block)) {
+          simplified = true;
+        }
 
         if (simplifyKnownBooleanCondition(code, block)) {
+          simplified = true;
           continue;
         }
 
         // Simplify if conditions when possible.
         If theIf = block.exit().asIf();
-
         if (theIf.isZeroTest()) {
-          simplifyIfZeroTest(code, block, theIf);
-          continue;
+          if (simplifyIfZeroTest(code, block, theIf)) {
+            simplified = true;
+            continue;
+          }
+        } else {
+          if (simplifyNonIfZeroTest(code, block, theIf)) {
+            simplified = true;
+            continue;
+          }
         }
 
-        Value lhs = theIf.lhs();
-        Value lhsRoot = lhs.getAliasedValue();
-        Value rhs = theIf.rhs();
-        Value rhsRoot = rhs.getAliasedValue();
-
-        if (lhsRoot == rhsRoot) {
-          // Comparing the same value.
-          simplifyIfWithKnownCondition(code, block, theIf, theIf.targetFromCondition(0));
-        } else if (lhsRoot.isDefinedByInstructionSatisfying(Instruction::isCreatingInstanceOrArray)
-            && rhsRoot.isDefinedByInstructionSatisfying(Instruction::isCreatingInstanceOrArray)) {
-          // Comparing two newly created objects.
-          assert theIf.getType() == Type.EQ || theIf.getType() == Type.NE;
-          simplifyIfWithKnownCondition(code, block, theIf, theIf.targetFromCondition(1));
-        } else if (lhsRoot.isConstNumber() && rhsRoot.isConstNumber()) {
-          // Zero test with a constant of comparison between between two constants.
-          ConstNumber left = lhsRoot.getConstInstruction().asConstNumber();
-          ConstNumber right = rhsRoot.getConstInstruction().asConstNumber();
-          BasicBlock target = theIf.targetFromCondition(left, right);
-          simplifyIfWithKnownCondition(code, block, theIf, target);
-        } else if (lhs.hasValueRange() && rhs.hasValueRange()) {
-          // Zero test with a value range, or comparison between between two values,
-          // each with a value ranges.
-          LongInterval leftRange = lhs.getValueRange();
-          LongInterval rightRange = rhs.getValueRange();
-          // Two overlapping ranges. Check for single point overlap.
-          if (!leftRange.overlapsWith(rightRange)) {
-            // No overlap.
-            int cond = Long.signum(leftRange.getMin() - rightRange.getMin());
-            simplifyIfWithKnownCondition(code, block, theIf, cond);
-          } else {
-            // The two intervals overlap. We can simplify if they overlap at the end points.
-            switch (theIf.getType()) {
-              case LT:
-              case GE:
-                // [a, b] < [c, d] is always false when a == d.
-                // [a, b] >= [c, d] is always true when a == d.
-                // In both cases 0 condition will choose the right branch.
-                if (leftRange.getMin() == rightRange.getMax()) {
-                  simplifyIfWithKnownCondition(code, block, theIf, 0);
-                }
-                break;
-              case GT:
-              case LE:
-                // [a, b] > [c, d] is always false when b == c.
-                // [a, b] <= [c, d] is always true when b == c.
-                // In both cases 0 condition will choose the right branch.
-                if (leftRange.getMax() == rightRange.getMin()) {
-                  simplifyIfWithKnownCondition(code, block, theIf, 0);
-                }
-                break;
-              case EQ:
-              case NE:
-                // Since there is overlap EQ and NE cannot be determined.
-                break;
-            }
-          }
-        } else if (theIf.getType() == Type.EQ || theIf.getType() == Type.NE) {
-          ProgramMethod context = code.context();
-          AbstractValue abstractValue = lhs.getAbstractValue(appView, context);
-          if (abstractValue.isSingleConstClassValue()) {
-            AbstractValue otherAbstractValue = rhs.getAbstractValue(appView, context);
-            if (otherAbstractValue.isSingleConstClassValue()) {
-              SingleConstClassValue singleConstClassValue = abstractValue.asSingleConstClassValue();
-              SingleConstClassValue otherSingleConstClassValue =
-                  otherAbstractValue.asSingleConstClassValue();
-              simplifyIfWithKnownCondition(
-                  code,
-                  block,
-                  theIf,
-                  BooleanUtils.intValue(
-                      singleConstClassValue.getType() != otherSingleConstClassValue.getType()));
-            }
-          } else if (abstractValue.isSingleFieldValue()) {
-            AbstractValue otherAbstractValue = rhs.getAbstractValue(appView, context);
-            if (otherAbstractValue.isSingleFieldValue()) {
-              SingleFieldValue singleFieldValue = abstractValue.asSingleFieldValue();
-              SingleFieldValue otherSingleFieldValue = otherAbstractValue.asSingleFieldValue();
-              if (singleFieldValue.getField() == otherSingleFieldValue.getField()) {
-                simplifyIfWithKnownCondition(code, block, theIf, 0);
-              } else {
-                DexClass holder = appView.definitionForHolder(singleFieldValue.getField());
-                DexEncodedField field = singleFieldValue.getField().lookupOnClass(holder);
-                if (field != null && field.isEnum()) {
-                  DexClass otherHolder =
-                      appView.definitionForHolder(otherSingleFieldValue.getField());
-                  DexEncodedField otherField =
-                      otherSingleFieldValue.getField().lookupOnClass(otherHolder);
-                  if (otherField != null && otherField.isEnum()) {
-                    simplifyIfWithKnownCondition(code, block, theIf, 1);
-                  }
-                }
-              }
-            }
-          }
+        // Unable to determine which branch will be taken. Check if the true target can safely be
+        // rewritten to the false target.
+        if (behavioralSubsumption.isSubsumedBy(theIf.getTrueTarget(), theIf.fallthroughBlock())) {
+          simplifyIfWithKnownCondition(code, block, theIf, theIf.fallthroughBlock());
+          simplified = true;
         }
       }
     }
@@ -2590,17 +2532,17 @@
       new TypeAnalysis(appView).narrowing(affectedValues);
     }
     assert code.isConsistentSSA();
-    return !affectedValues.isEmpty();
+    return new ControlFlowSimplificationResult(!affectedValues.isEmpty(), simplified);
   }
 
-  private void simplifyIfZeroTest(IRCode code, BasicBlock block, If theIf) {
+  private boolean simplifyIfZeroTest(IRCode code, BasicBlock block, If theIf) {
     Value lhs = theIf.lhs();
     Value lhsRoot = lhs.getAliasedValue();
     if (lhsRoot.isConstNumber()) {
       ConstNumber cond = lhsRoot.getConstInstruction().asConstNumber();
       BasicBlock target = theIf.targetFromCondition(cond);
       simplifyIfWithKnownCondition(code, block, theIf, target);
-      return;
+      return true;
     }
 
     if (theIf.isNullTest()) {
@@ -2608,12 +2550,12 @@
 
       if (lhs.isAlwaysNull(appView)) {
         simplifyIfWithKnownCondition(code, block, theIf, theIf.targetFromNullObject());
-        return;
+        return true;
       }
 
       if (lhs.isNeverNull()) {
         simplifyIfWithKnownCondition(code, block, theIf, theIf.targetFromNonNullObject());
-        return;
+        return true;
       }
     }
 
@@ -2623,7 +2565,7 @@
         // Interval doesn't contain zero at all.
         int sign = Long.signum(interval.getMin());
         simplifyIfWithKnownCondition(code, block, theIf, sign);
-        return;
+        return true;
       }
 
       // Interval contains zero.
@@ -2635,7 +2577,7 @@
           // In both cases a zero condition takes the right branch.
           if (interval.getMin() == 0) {
             simplifyIfWithKnownCondition(code, block, theIf, 0);
-            return;
+            return true;
           }
           break;
 
@@ -2646,7 +2588,7 @@
           // In both cases a zero condition takes the right branch.
           if (interval.getMax() == 0) {
             simplifyIfWithKnownCondition(code, block, theIf, 0);
-            return;
+            return true;
           }
           break;
 
@@ -2658,6 +2600,125 @@
           break;
       }
     }
+    return false;
+  }
+
+  private boolean simplifyNonIfZeroTest(IRCode code, BasicBlock block, If theIf) {
+    Value lhs = theIf.lhs();
+    Value lhsRoot = lhs.getAliasedValue();
+    Value rhs = theIf.rhs();
+    Value rhsRoot = rhs.getAliasedValue();
+    if (lhsRoot == rhsRoot) {
+      // Comparing the same value.
+      simplifyIfWithKnownCondition(code, block, theIf, theIf.targetFromCondition(0));
+      return true;
+    }
+
+    if (lhsRoot.isDefinedByInstructionSatisfying(Instruction::isCreatingInstanceOrArray)
+        && rhsRoot.isDefinedByInstructionSatisfying(Instruction::isCreatingInstanceOrArray)) {
+      // Comparing two newly created objects.
+      assert theIf.getType() == Type.EQ || theIf.getType() == Type.NE;
+      simplifyIfWithKnownCondition(code, block, theIf, theIf.targetFromCondition(1));
+      return true;
+    }
+
+    if (lhsRoot.isConstNumber() && rhsRoot.isConstNumber()) {
+      // Zero test with a constant of comparison between between two constants.
+      ConstNumber left = lhsRoot.getConstInstruction().asConstNumber();
+      ConstNumber right = rhsRoot.getConstInstruction().asConstNumber();
+      BasicBlock target = theIf.targetFromCondition(left, right);
+      simplifyIfWithKnownCondition(code, block, theIf, target);
+      return true;
+    }
+
+    if (lhs.hasValueRange() && rhs.hasValueRange()) {
+      // Zero test with a value range, or comparison between between two values,
+      // each with a value ranges.
+      LongInterval leftRange = lhs.getValueRange();
+      LongInterval rightRange = rhs.getValueRange();
+      // Two overlapping ranges. Check for single point overlap.
+      if (!leftRange.overlapsWith(rightRange)) {
+        // No overlap.
+        int cond = Long.signum(leftRange.getMin() - rightRange.getMin());
+        simplifyIfWithKnownCondition(code, block, theIf, cond);
+        return true;
+      }
+
+      // The two intervals overlap. We can simplify if they overlap at the end points.
+      switch (theIf.getType()) {
+        case LT:
+        case GE:
+          // [a, b] < [c, d] is always false when a == d.
+          // [a, b] >= [c, d] is always true when a == d.
+          // In both cases 0 condition will choose the right branch.
+          if (leftRange.getMin() == rightRange.getMax()) {
+            simplifyIfWithKnownCondition(code, block, theIf, 0);
+            return true;
+          }
+          break;
+        case GT:
+        case LE:
+          // [a, b] > [c, d] is always false when b == c.
+          // [a, b] <= [c, d] is always true when b == c.
+          // In both cases 0 condition will choose the right branch.
+          if (leftRange.getMax() == rightRange.getMin()) {
+            simplifyIfWithKnownCondition(code, block, theIf, 0);
+            return true;
+          }
+          break;
+        case EQ:
+        case NE:
+          // Since there is overlap EQ and NE cannot be determined.
+          break;
+      }
+    }
+
+    if (theIf.getType() == Type.EQ || theIf.getType() == Type.NE) {
+      ProgramMethod context = code.context();
+      AbstractValue abstractValue = lhs.getAbstractValue(appView, context);
+      if (abstractValue.isSingleConstClassValue()) {
+        AbstractValue otherAbstractValue = rhs.getAbstractValue(appView, context);
+        if (otherAbstractValue.isSingleConstClassValue()) {
+          SingleConstClassValue singleConstClassValue = abstractValue.asSingleConstClassValue();
+          SingleConstClassValue otherSingleConstClassValue =
+              otherAbstractValue.asSingleConstClassValue();
+          simplifyIfWithKnownCondition(
+              code,
+              block,
+              theIf,
+              BooleanUtils.intValue(
+                  singleConstClassValue.getType() != otherSingleConstClassValue.getType()));
+          return true;
+        }
+        return false;
+      }
+
+      if (abstractValue.isSingleFieldValue()) {
+        AbstractValue otherAbstractValue = rhs.getAbstractValue(appView, context);
+        if (otherAbstractValue.isSingleFieldValue()) {
+          SingleFieldValue singleFieldValue = abstractValue.asSingleFieldValue();
+          SingleFieldValue otherSingleFieldValue = otherAbstractValue.asSingleFieldValue();
+          if (singleFieldValue.getField() == otherSingleFieldValue.getField()) {
+            simplifyIfWithKnownCondition(code, block, theIf, 0);
+            return true;
+          }
+
+          DexClass holder = appView.definitionForHolder(singleFieldValue.getField());
+          DexEncodedField field = singleFieldValue.getField().lookupOnClass(holder);
+          if (field != null && field.isEnum()) {
+            DexClass otherHolder = appView.definitionForHolder(otherSingleFieldValue.getField());
+            DexEncodedField otherField =
+                otherSingleFieldValue.getField().lookupOnClass(otherHolder);
+            if (otherField != null && otherField.isEnum()) {
+              simplifyIfWithKnownCondition(code, block, theIf, 1);
+              return true;
+            }
+          }
+        }
+      }
+    }
+
+    return false;
   }
 
   private void simplifyIfWithKnownCondition(
@@ -3181,30 +3242,31 @@
     assert block.exit().asGoto().getTarget() == target;
   }
 
-  private void rewriteIfWithConstZero(IRCode code, BasicBlock block) {
+  private boolean rewriteIfWithConstZero(IRCode code, BasicBlock block) {
     If theIf = block.exit().asIf();
     if (theIf.isZeroTest()) {
-      return;
+      return false;
     }
 
-    List<Value> inValues = theIf.inValues();
-    Value leftValue = inValues.get(0);
-    Value rightValue = inValues.get(1);
+    Value leftValue = theIf.lhs();
+    Value rightValue = theIf.rhs();
     if (leftValue.isConstNumber() || rightValue.isConstNumber()) {
       if (leftValue.isConstNumber()) {
         if (leftValue.getConstInstruction().asConstNumber().isZero()) {
           If ifz = new If(theIf.getType().forSwappedOperands(), rightValue);
           block.replaceLastInstruction(ifz, code);
           assert block.exit() == ifz;
+          return true;
         }
-      } else {
-        if (rightValue.getConstInstruction().asConstNumber().isZero()) {
-          If ifz = new If(theIf.getType(), leftValue);
-          block.replaceLastInstruction(ifz, code);
-          assert block.exit() == ifz;
-        }
+      } else if (rightValue.getConstInstruction().asConstNumber().isZero()) {
+        If ifz = new If(theIf.getType(), leftValue);
+        block.replaceLastInstruction(ifz, code);
+        assert block.exit() == ifz;
+        return true;
       }
     }
+
+    return false;
   }
 
   private boolean flipIfBranchesIfNeeded(IRCode code, BasicBlock block) {
@@ -3736,7 +3798,7 @@
                 && value.definition.asNumberConversion().to == NumericType.DOUBLE) {
               InvokeStatic invokeIsNaN =
                   new InvokeStatic(
-                      dexItemFactory.doubleMethods.isNaN, null, ImmutableList.of(value));
+                      dexItemFactory.doubleMembers.isNaN, null, ImmutableList.of(value));
               invokeIsNaN.setPosition(instruction.getPosition());
 
               // Insert the invoke before the current instruction.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java b/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
index 26aa681..0ecfc91 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DeadCodeRemover.java
@@ -53,7 +53,8 @@
         removeDeadInstructions(worklist, code, block);
         removeDeadPhis(worklist, code, block);
       }
-    } while (removeUnneededCatchHandlers(code));
+    } while (codeRewriter.simplifyIf(code).anySimplifications()
+        || removeUnneededCatchHandlers(code));
     assert code.isConsistentSSA();
 
     timing.end();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
index ed477a6..e62b2c1 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
@@ -122,7 +122,7 @@
 
   @Override
   public void methodMayNotHaveSideEffects(DexEncodedMethod method) {
-    // Ignored.
+    method.getMutableOptimizationInfo().markMayNotHaveSideEffects();
   }
 
   @Override
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 25916f9..01c4cf7 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
@@ -135,6 +135,13 @@
         continue;
       }
 
+      if (invoke.hasUnusedOutValue()
+          && !singleTarget.getDefinition().isInstanceInitializer()
+          && !invoke.instructionMayHaveSideEffects(appView, code.context())) {
+        instructionIterator.removeOrReplaceByDebugLocalRead();
+        continue;
+      }
+
       LibraryMethodModelCollection.State optimizationState =
           optimizationStates.computeIfAbsent(
               optimizer,
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java
index 70ace2b..071108d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java
@@ -4,41 +4,41 @@
 
 package com.android.tools.r8.ir.optimize.library;
 
-import static com.google.common.base.Predicates.alwaysFalse;
-import static com.google.common.base.Predicates.alwaysTrue;
-
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.LibraryMethod;
 import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.utils.BiPredicateUtils;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.function.Predicate;
+import java.util.function.BiPredicate;
+import java.util.function.Consumer;
 
 public class LibraryMethodSideEffectModelCollection {
 
-  private final Map<DexMethod, Predicate<InvokeMethod>> finalMethodsWithoutSideEffects;
+  private final Map<DexMethod, BiPredicate<DexMethod, List<Value>>> finalMethodsWithoutSideEffects;
+  private final Set<DexMethod> unconditionalFinalMethodsWithoutSideEffects;
+
   private final Set<DexMethod> nonFinalMethodsWithoutSideEffects;
 
   public LibraryMethodSideEffectModelCollection(DexItemFactory dexItemFactory) {
     finalMethodsWithoutSideEffects = buildFinalMethodsWithoutSideEffects(dexItemFactory);
+    unconditionalFinalMethodsWithoutSideEffects =
+        buildUnconditionalFinalMethodsWithoutSideEffects(dexItemFactory);
     nonFinalMethodsWithoutSideEffects = buildNonFinalMethodsWithoutSideEffects(dexItemFactory);
   }
 
-  private static Map<DexMethod, Predicate<InvokeMethod>> buildFinalMethodsWithoutSideEffects(
-      DexItemFactory dexItemFactory) {
-    ImmutableMap.Builder<DexMethod, Predicate<InvokeMethod>> builder =
-        ImmutableMap.<DexMethod, Predicate<InvokeMethod>>builder()
-            .put(dexItemFactory.enumMembers.constructor, alwaysTrue())
-            .put(dexItemFactory.npeMethods.init, alwaysTrue())
-            .put(dexItemFactory.npeMethods.initWithMessage, alwaysTrue())
-            .put(dexItemFactory.objectMembers.constructor, alwaysTrue())
-            .put(dexItemFactory.objectMembers.getClass, alwaysTrue())
-            .put(dexItemFactory.stringBuilderMethods.toString, alwaysTrue())
-            .put(dexItemFactory.stringMembers.hashCode, alwaysTrue());
-    putAll(builder, dexItemFactory.classMethods.getNames, alwaysTrue());
+  private static Map<DexMethod, BiPredicate<DexMethod, List<Value>>>
+      buildFinalMethodsWithoutSideEffects(DexItemFactory dexItemFactory) {
+    ImmutableMap.Builder<DexMethod, BiPredicate<DexMethod, List<Value>>> builder =
+        ImmutableMap.<DexMethod, BiPredicate<DexMethod, List<Value>>>builder()
+            .put(
+                dexItemFactory.stringMembers.constructor,
+                (method, arguments) -> arguments.get(0).isNeverNull());
     putAll(
         builder,
         dexItemFactory.stringBufferMethods.constructorMethods,
@@ -47,10 +47,34 @@
         builder,
         dexItemFactory.stringBuilderMethods.constructorMethods,
         dexItemFactory.stringBuilderMethods::constructorInvokeIsSideEffectFree);
-    putAll(builder, dexItemFactory.boxedValueOfMethods(), alwaysTrue());
     return builder.build();
   }
 
+  private static Set<DexMethod> buildUnconditionalFinalMethodsWithoutSideEffects(
+      DexItemFactory dexItemFactory) {
+    return ImmutableSet.<DexMethod>builder()
+        .add(dexItemFactory.booleanMembers.toString)
+        .add(dexItemFactory.byteMembers.toString)
+        .add(dexItemFactory.charMembers.toString)
+        .add(dexItemFactory.doubleMembers.toString)
+        .add(dexItemFactory.enumMembers.constructor)
+        .add(dexItemFactory.floatMembers.toString)
+        .add(dexItemFactory.integerMembers.toString)
+        .add(dexItemFactory.longMembers.toString)
+        .add(dexItemFactory.npeMethods.init)
+        .add(dexItemFactory.npeMethods.initWithMessage)
+        .add(dexItemFactory.objectMembers.constructor)
+        .add(dexItemFactory.objectMembers.getClass)
+        .add(dexItemFactory.shortMembers.toString)
+        .add(dexItemFactory.stringBufferMethods.toString)
+        .add(dexItemFactory.stringBuilderMethods.toString)
+        .add(dexItemFactory.stringMembers.hashCode)
+        .add(dexItemFactory.stringMembers.toString)
+        .addAll(dexItemFactory.classMethods.getNames)
+        .addAll(dexItemFactory.boxedValueOfMethods())
+        .build();
+  }
+
   private static Set<DexMethod> buildNonFinalMethodsWithoutSideEffects(
       DexItemFactory dexItemFactory) {
     return ImmutableSet.of(
@@ -59,25 +83,31 @@
         dexItemFactory.objectMembers.toString);
   }
 
-  private static void putAll(
-      ImmutableMap.Builder<DexMethod, Predicate<InvokeMethod>> builder,
-      Iterable<DexMethod> methods,
-      Predicate<InvokeMethod> predicate) {
-    for (DexMethod method : methods) {
-      builder.put(method, predicate);
+  private static <K, V> void putAll(ImmutableMap.Builder<K, V> builder, Iterable<K> keys, V value) {
+    for (K key : keys) {
+      builder.put(key, value);
     }
   }
 
+  public void forEachSideEffectFreeFinalMethod(Consumer<DexMethod> consumer) {
+    unconditionalFinalMethodsWithoutSideEffects.forEach(consumer);
+  }
+
   public boolean isCallToSideEffectFreeFinalMethod(InvokeMethod invoke) {
-    return finalMethodsWithoutSideEffects
-        .getOrDefault(invoke.getInvokedMethod(), alwaysFalse())
-        .test(invoke);
+    return isSideEffectFreeFinalMethod(invoke.getInvokedMethod(), invoke.arguments());
+  }
+
+  public boolean isSideEffectFreeFinalMethod(DexMethod method, List<Value> arguments) {
+    return unconditionalFinalMethodsWithoutSideEffects.contains(method)
+        || finalMethodsWithoutSideEffects
+            .getOrDefault(method, BiPredicateUtils.alwaysFalse())
+            .test(method, arguments);
   }
 
   // This intentionally takes the invoke instruction since the determination of whether a library
   // method has side effects may depend on the arguments.
   public boolean isSideEffectFree(InvokeMethod invoke, LibraryMethod singleTarget) {
-    return isCallToSideEffectFreeFinalMethod(invoke)
+    return isSideEffectFreeFinalMethod(singleTarget.getReference(), invoke.arguments())
         || nonFinalMethodsWithoutSideEffects.contains(singleTarget.getReference());
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryOptimizationInfoInitializer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryOptimizationInfoInitializer.java
index 9b41f18..ecbb3e4 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryOptimizationInfoInitializer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryOptimizationInfoInitializer.java
@@ -47,6 +47,7 @@
     modelStaticFinalLibraryFields(finalLibraryFields);
     modelLibraryMethodsReturningNonNull();
     modelLibraryMethodsReturningReceiver();
+    modelLibraryMethodsWithoutSideEffects();
     modelRequireNonNullMethods();
   }
 
@@ -113,6 +114,18 @@
     }
   }
 
+  private void modelLibraryMethodsWithoutSideEffects() {
+    appView
+        .getLibraryMethodSideEffectModelCollection()
+        .forEachSideEffectFreeFinalMethod(
+            method -> {
+              DexEncodedMethod definition = lookupMethod(method);
+              if (definition != null) {
+                feedback.methodMayNotHaveSideEffects(definition);
+              }
+            });
+  }
+
   private void modelRequireNonNullMethods() {
     for (DexMethod requireNonNullMethod : dexItemFactory.objectsMethods.requireNonNullMethods()) {
       DexEncodedMethod definition = lookupMethod(requireNonNullMethod);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectsMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectsMethodOptimizer.java
index 5fc8d62..a303082 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectsMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectsMethodOptimizer.java
@@ -5,15 +5,19 @@
 package com.android.tools.r8.ir.optimize.library;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexItemFactory.ObjectsMethods;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ResolutionResult.SingleResolutionResult;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.IRCode;
 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.utils.ValueUtils;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.Set;
 
 public class ObjectsMethodOptimizer extends StatelessLibraryMethodModelCollection {
@@ -67,9 +71,11 @@
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
       Set<Value> affectedValues) {
-    // Optimize Objects.toString(null) into "null".
     Value object = invoke.getFirstArgument();
-    if (object.getType().isDefinitelyNull()) {
+    TypeElement type = object.getType();
+
+    // Optimize Objects.toString(null) into "null".
+    if (type.isDefinitelyNull()) {
       instructionIterator.replaceCurrentInstructionWithConstString(appView, code, "null");
       if (invoke.hasOutValue()) {
         affectedValues.addAll(invoke.outValue().affectedValues());
@@ -77,10 +83,50 @@
       return;
     }
 
-    // Remove Objects.toString(stringBuilder) if unused.
-    if (ValueUtils.isStringBuilder(invoke.getFirstArgument(), dexItemFactory)) {
-      if (!invoke.hasOutValue() || !invoke.outValue().hasNonDebugUsers()) {
+    // Optimize Objects.toString(nonNullString) into nonNullString.
+    if (type.isDefinitelyNotNull() && type.isStringType(dexItemFactory)) {
+      if (invoke.hasOutValue()) {
+        affectedValues.addAll(invoke.outValue().affectedValues());
+        invoke.outValue().replaceUsers(object);
+      }
+      instructionIterator.removeOrReplaceByDebugLocalRead();
+      return;
+    }
+
+    // Remove Objects.toString() if it is unused and does not have side effects.
+    if (!invoke.hasOutValue() || !invoke.outValue().hasNonDebugUsers()) {
+      // Calling toString() on an array does not call toString() on the array elements.
+      if (type.isArrayType()) {
         instructionIterator.removeOrReplaceByDebugLocalRead();
+        return;
+      }
+
+      assert type.isClassType();
+
+      // Check if this is a library class with a toString() method that does not have side effects.
+      DexType classType = type.asClassType().getClassType();
+      DexMethod toStringMethodReference =
+          dexItemFactory.objectMembers.toString.withHolder(classType, dexItemFactory);
+      if (appView
+          .getLibraryMethodSideEffectModelCollection()
+          .isSideEffectFreeFinalMethod(toStringMethodReference, invoke.arguments())) {
+        instructionIterator.removeOrReplaceByDebugLocalRead();
+        return;
+      }
+
+      // Check if this is a program class with a toString() method that does not have side effects.
+      AppInfoWithLiveness appInfo = appView.appInfo().withLiveness();
+      if (appInfo != null) {
+        DexClass clazz = appInfo.definitionFor(classType, code.context());
+        if (clazz != null && clazz.isEffectivelyFinal(appView)) {
+          SingleResolutionResult resolutionResult =
+              appInfo.resolveMethodOn(clazz, toStringMethodReference).asSingleResolution();
+          if (resolutionResult != null
+              && !resolutionResult.getResolvedMethod().getOptimizationInfo().mayHaveSideEffects()) {
+            instructionIterator.removeOrReplaceByDebugLocalRead();
+            return;
+          }
+        }
       }
     }
   }
diff --git a/src/main/java/com/android/tools/r8/utils/BiPredicateUtils.java b/src/main/java/com/android/tools/r8/utils/BiPredicateUtils.java
new file mode 100644
index 0000000..f470d35
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/BiPredicateUtils.java
@@ -0,0 +1,18 @@
+// 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.utils;
+
+import java.util.function.BiPredicate;
+
+public class BiPredicateUtils {
+
+  public static <S, T> BiPredicate<S, T> alwaysFalse() {
+    return (s, t) -> false;
+  }
+
+  public static <S, T> BiPredicate<S, T> alwaysTrue() {
+    return (s, t) -> true;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/RedundantConstNumberRemovalTest.java b/src/test/java/com/android/tools/r8/ir/optimize/RedundantConstNumberRemovalTest.java
index 4813749..27f097b 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/RedundantConstNumberRemovalTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/RedundantConstNumberRemovalTest.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -39,6 +40,7 @@
     return getTestParameters()
         .withCfRuntimes()
         .withDexRuntimesStartingFromExcluding(Version.V4_4_4)
+        .withApiLevelsStartingAtIncluding(AndroidApiLevel.L)
         .build();
   }
 
@@ -64,7 +66,7 @@
             .enableInliningAnnotations()
             .addOptionsModification(
                 internalOptions -> internalOptions.enableRedundantConstNumberOptimization = true)
-            .setMinApi(parameters.getRuntime())
+            .setMinApi(parameters.getApiLevel())
             .run(TestClass.class)
             .assertSuccessWithOutput(expectedOutput);
 
@@ -112,15 +114,10 @@
       assertEquals(1, code.blocks.size());
       // The block only has three instructions.
       BasicBlock entryBlock = code.entryBlock();
-      assertEquals(3, entryBlock.getInstructions().size());
+      assertEquals(2, entryBlock.getInstructions().size());
       // The first one is the `argument` instruction.
       Instruction argument = entryBlock.getInstructions().getFirst();
       assertTrue(argument.isArgument());
-      // The next one is a `const-number` instruction is not used for anything.
-      // TODO(christofferqa): D8 should be able to get rid of the unused const-number instruction.
-      Instruction unused = entryBlock.getInstructions().get(1);
-      assertTrue(unused.isConstNumber());
-      assertEquals(0, unused.outValue().numberOfAllUsers());
       // The `return` instruction returns the argument.
       Instruction ret = entryBlock.getInstructions().getLast();
       assertTrue(ret.isReturn());
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/fields/FieldInitializedByConstantInOneConstructorTest.java b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/fields/FieldInitializedByConstantInOneConstructorTest.java
index 8dcd7bc..0902756 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/fields/FieldInitializedByConstantInOneConstructorTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/fields/FieldInitializedByConstantInOneConstructorTest.java
@@ -51,11 +51,7 @@
     ClassSubject testClassSubject = inspector.clazz(TestClass.class);
     assertThat(testClassSubject, isPresent());
     assertThat(testClassSubject.uniqueMethodWithName("live"), isPresent());
-    if (parameters.isCfRuntime()) {
-      assertThat(testClassSubject.uniqueMethodWithName("dead"), isPresent());
-    } else {
-      assertThat(testClassSubject.uniqueMethodWithName("dead"), not(isPresent()));
-    }
+    assertThat(testClassSubject.uniqueMethodWithName("dead"), not(isPresent()));
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/fields/FieldInitializedByDifferentConstantsInMultipleConstructorsTest.java b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/fields/FieldInitializedByDifferentConstantsInMultipleConstructorsTest.java
index 9bedfb1..c3a453d 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/fields/FieldInitializedByDifferentConstantsInMultipleConstructorsTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/membervaluepropagation/fields/FieldInitializedByDifferentConstantsInMultipleConstructorsTest.java
@@ -51,11 +51,7 @@
     ClassSubject testClassSubject = inspector.clazz(TestClass.class);
     assertThat(testClassSubject, isPresent());
     assertThat(testClassSubject.uniqueMethodWithName("live"), isPresent());
-    if (parameters.isCfRuntime()) {
-      assertThat(testClassSubject.uniqueMethodWithName("dead"), isPresent());
-    } else {
-      assertThat(testClassSubject.uniqueMethodWithName("dead"), not(isPresent()));
-    }
+    assertThat(testClassSubject.uniqueMethodWithName("dead"), not(isPresent()));
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/ObjectsToStringOnLibraryClassTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/ObjectsToStringOnLibraryClassTest.java
new file mode 100644
index 0000000..3e852df
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/ObjectsToStringOnLibraryClassTest.java
@@ -0,0 +1,100 @@
+// 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 static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+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.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.util.Objects;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ObjectsToStringOnLibraryClassTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ObjectsToStringOnLibraryClassTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              MethodSubject mainMethodSubject = inspector.clazz(Main.class).mainMethod();
+              assertThat(mainMethodSubject, isPresent());
+              assertTrue(
+                  mainMethodSubject
+                      .streamInstructions()
+                      .filter(InstructionSubject::isInvoke)
+                      .allMatch(
+                          x ->
+                              x.getMethod()
+                                  .getName()
+                                  .toSourceString()
+                                  .equals("currentTimeMillis")));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      boolean unknown = System.currentTimeMillis() > 0;
+
+      // Boolean.
+      Objects.toString(unknown ? Boolean.FALSE : null);
+      Objects.toString(unknown ? Boolean.TRUE : null);
+
+      // Byte.
+      Objects.toString(unknown ? Byte.valueOf((byte) 0) : null);
+
+      // Char.
+      Objects.toString(unknown ? Character.valueOf((char) 0) : null);
+
+      // Double.
+      Objects.toString(unknown ? Double.valueOf(0) : null);
+
+      // Float.
+      Objects.toString(unknown ? Float.valueOf(0) : null);
+
+      // Integer.
+      Objects.toString(unknown ? Integer.valueOf(0) : null);
+
+      // Long.
+      Objects.toString(unknown ? Long.valueOf(0) : null);
+
+      // Short.
+      Objects.toString(unknown ? Short.valueOf((short) 0) : null);
+
+      // String.
+      Objects.toString(unknown ? "null" : null);
+      Objects.toString(unknown ? new String("null") : null);
+
+      // StringBuffer, StringBuilder.
+      Objects.toString(unknown ? new StringBuffer() : null);
+      Objects.toString(unknown ? new StringBuilder() : null);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/movestringconstants/MoveStringConstantsTest.java b/src/test/java/com/android/tools/r8/movestringconstants/MoveStringConstantsTest.java
index d9b3cd4..61387ea 100644
--- a/src/test/java/com/android/tools/r8/movestringconstants/MoveStringConstantsTest.java
+++ b/src/test/java/com/android/tools/r8/movestringconstants/MoveStringConstantsTest.java
@@ -91,31 +91,37 @@
     assumeTrue(backend == Backend.DEX);
     validateSequence(
         methodThrowToBeInlined.iterateInstructions(),
+
         // 'if' with "foo#1" is flipped.
         InstructionSubject::isIfEqz,
 
         // 'if' with "foo#2" is removed along with the constant.
 
-        // 'if' with "foo#3" is removed so now we have unconditional call.
-        insn -> insn.isConstString("StringConstants::foo#3", JumboStringMode.DISALLOW),
-        InstructionSubject::isInvokeStatic,
-        InstructionSubject::isThrow,
+        // 'if' with "foo#3" is removed so now we have an unconditional call inside the branch.
+        InstructionSubject::isIfEq,
 
-        // 'if's with "foo#4" and "foo#5" are flipped, but their throwing branches
-        // are not moved to the end of the code (area for improvement?).
+        // 'if' with "foo#4" is flipped, but the throwing branch is not moved to the end of the code
+        // (area for improvement?).
         insn -> insn.isConstString("StringConstants::foo#4", JumboStringMode.DISALLOW),
         InstructionSubject::isIfEqz, // Flipped if
         InstructionSubject::isGoto, // Jump around throwing branch.
         InstructionSubject::isInvokeStatic, // Throwing branch.
         InstructionSubject::isThrow,
+
+        // 'if's with "foo#5" are flipped.
         insn -> insn.isConstString("StringConstants::foo#5", JumboStringMode.DISALLOW),
         InstructionSubject::isIfEqz, // Flipped if
         InstructionSubject::isReturnVoid, // Final return statement.
         InstructionSubject::isInvokeStatic, // Throwing branch.
         InstructionSubject::isThrow,
 
-        // After 'if' with "foo#1" flipped, always throwing branch
-        // moved here along with the constant.
+        // 'if' with "foo#3" is removed so now we have an unconditional call.
+        insn -> insn.isConstString("StringConstants::foo#3", JumboStringMode.DISALLOW),
+        InstructionSubject::isInvokeStatic,
+        InstructionSubject::isThrow,
+
+        // After 'if' with "foo#1" flipped, the always throwing branch is moved here along with the
+        // constant.
         insn -> insn.isConstString("StringConstants::foo#1", JumboStringMode.DISALLOW),
         InstructionSubject::isInvokeStatic,
         InstructionSubject::isThrow);
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
index 4e6a3f5..3a9976f 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CfInstructionSubject.java
@@ -196,6 +196,11 @@
   }
 
   @Override
+  public boolean isIfEq() {
+    return instruction instanceof CfIf && ((CfIf) instruction).getOpcode() == Opcodes.IF_ICMPEQ;
+  }
+
+  @Override
   public boolean isIfEqz() {
     return instruction instanceof CfIf && ((CfIf) instruction).getOpcode() == Opcodes.IFEQ;
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
index 26944f9..4b9f733 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/DexInstructionSubject.java
@@ -316,6 +316,11 @@
   }
 
   @Override
+  public boolean isIfEq() {
+    return instruction instanceof IfEq;
+  }
+
+  @Override
   public boolean isIfEqz() {
     return instruction instanceof IfEqz;
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
index e6e26fd..56436e9 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/InstructionSubject.java
@@ -75,6 +75,8 @@
 
   boolean isIfNez();
 
+  boolean isIfEq();
+
   boolean isIfEqz();
 
   boolean isReturn();