Merge changes Ieff6fd20,I16a59957

* changes:
  Use original static modifier when evaluating -if rules
  Use original final modifier when evaluating -if rules
diff --git a/build.gradle b/build.gradle
index 00b8a46..c205b2d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -676,9 +676,6 @@
 task testJar(type: ShadowJar, dependsOn: testClasses) {
     baseName = "r8tests"
     from sourceSets.test.output
-    if (!project.hasProperty('exclude_deps')) {
-        relocate('org.objectweb.asm', 'com.android.tools.r8.org.objectweb.asm')
-    }
 }
 
 task testJarNoDeps(type: ShadowJar, dependsOn: testClasses) {
diff --git a/src/main/java/com/android/tools/r8/GenerateMainDexList.java b/src/main/java/com/android/tools/r8/GenerateMainDexList.java
index 1be9b3a..14dd1ae 100644
--- a/src/main/java/com/android/tools/r8/GenerateMainDexList.java
+++ b/src/main/java/com/android/tools/r8/GenerateMainDexList.java
@@ -75,7 +75,7 @@
 
       if (!mainDexRootSet.checkDiscarded.isEmpty()) {
         new DiscardedChecker(
-            mainDexRootSet, mainDexClasses.getClasses(), appView.appInfo(), options)
+                mainDexRootSet, mainDexClasses.getClasses(), appView.appInfo(), options)
             .run();
       }
       // Print -whyareyoukeeping results if any.
diff --git a/src/main/java/com/android/tools/r8/code/FillArrayData.java b/src/main/java/com/android/tools/r8/code/FillArrayData.java
index 4569897..a49a31d 100644
--- a/src/main/java/com/android/tools/r8/code/FillArrayData.java
+++ b/src/main/java/com/android/tools/r8/code/FillArrayData.java
@@ -44,4 +44,9 @@
   public String toSmaliString(ClassNameMapper naming) {
     return formatSmaliString("v" + AA + ", :label_" + (getOffset() + BBBBBBBB));
   }
+
+  @Override
+  public boolean canThrow() {
+    return true;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java
index 73f7921..5e71072 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -441,7 +441,7 @@
     dest.putShort((short) code.incomingRegisterSize);
     dest.putShort((short) code.outgoingRegisterSize);
     dest.putShort((short) code.tries.length);
-    dest.putInt(mixedSectionOffsets.getOffsetFor(code.getDebugInfo()));
+    dest.putInt(mixedSectionOffsets.getOffsetFor(code.getDebugInfoForWriting()));
     // Jump over the size.
     int insnSizeOffset = dest.position();
     dest.forward(4);
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index b754c9a..e02869d 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -40,6 +40,7 @@
 
   public DexString highestSortingString;
   private DexDebugInfo debugInfo;
+  private DexDebugInfoForWriting debugInfoForWriting;
 
   public DexCode(
       int registerSize,
@@ -97,6 +98,9 @@
 
   public void setDebugInfo(DexDebugInfo debugInfo) {
     this.debugInfo = debugInfo;
+    if (debugInfoForWriting != null) {
+      debugInfoForWriting = null;
+    }
   }
 
   public DexDebugInfo debugInfoWithAdditionalFirstParameter(DexString name) {
@@ -382,7 +386,7 @@
       }
     }
     if (debugInfo != null) {
-      debugInfo.collectIndexedItems(indexedItems);
+      getDebugInfoForWriting().collectIndexedItems(indexedItems);
     }
     if (handlers != null) {
       for (TryHandler handler : handlers) {
@@ -391,6 +395,17 @@
     }
   }
 
+  public DexDebugInfoForWriting getDebugInfoForWriting() {
+    if (debugInfo == null) {
+      return null;
+    }
+    if (debugInfoForWriting == null) {
+      debugInfoForWriting = new DexDebugInfoForWriting(debugInfo);
+    }
+
+    return debugInfoForWriting;
+  }
+
   private void updateHighestSortingString(DexString candidate) {
     assert candidate != null;
     if (highestSortingString == null || highestSortingString.slowCompareTo(candidate) < 0) {
@@ -406,7 +421,7 @@
   void collectMixedSectionItems(MixedSectionCollection mixedItems) {
     if (mixedItems.add(this)) {
       if (debugInfo != null) {
-        debugInfo.collectMixedSectionItems(mixedItems);
+        getDebugInfoForWriting().collectMixedSectionItems(mixedItems);
       }
     }
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugInfoForWriting.java b/src/main/java/com/android/tools/r8/graph/DexDebugInfoForWriting.java
new file mode 100644
index 0000000..0777b85
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugInfoForWriting.java
@@ -0,0 +1,23 @@
+// Copyright (c) 2019, 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.graph;
+
+import com.android.tools.r8.graph.DexDebugEvent.SetInlineFrame;
+import java.util.Arrays;
+
+/**
+ * Wraps DexDebugInfo to make comparison and hashcode not consider
+ * the SetInlineFrames
+ */
+public class DexDebugInfoForWriting extends DexDebugInfo {
+
+  public DexDebugInfoForWriting(DexDebugInfo dexDebugInfo) {
+    super(dexDebugInfo.startLine, dexDebugInfo.parameters,
+        Arrays.stream(dexDebugInfo.events)
+            .filter(d -> !(d instanceof SetInlineFrame))
+            .toArray(DexDebugEvent[]::new));
+  }
+
+}
diff --git a/src/main/java/com/android/tools/r8/graph/GraphLense.java b/src/main/java/com/android/tools/r8/graph/GraphLense.java
index 50c91eb..898afed 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLense.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLense.java
@@ -300,23 +300,38 @@
     private final BiMap<DexMethod, DexMethod> originalMethodSignatures = HashBiMap.create();
 
     public void map(DexType from, DexType to) {
+      if (from == to) {
+        return;
+      }
       typeMap.put(from, to);
     }
 
     public void map(DexMethod from, DexMethod to) {
+      if (from == to) {
+        return;
+      }
       methodMap.put(from, to);
     }
 
     public void map(DexField from, DexField to) {
+      if (from == to) {
+        return;
+      }
       fieldMap.put(from, to);
     }
 
     public void move(DexMethod from, DexMethod to) {
+      if (from == to) {
+        return;
+      }
       map(from, to);
       originalMethodSignatures.put(to, from);
     }
 
     public void move(DexField from, DexField to) {
+      if (from == to) {
+        return;
+      }
       fieldMap.put(from, to);
       originalFieldSignatures.put(to, from);
     }
@@ -840,9 +855,11 @@
     @Override
     public String toString() {
       StringBuilder builder = new StringBuilder();
-      for (Map.Entry<DexType, DexType> entry : typeMap.entrySet()) {
-        builder.append(entry.getKey().toSourceString()).append(" -> ");
-        builder.append(entry.getValue().toSourceString()).append(System.lineSeparator());
+      if (typeMap != null) {
+        for (Map.Entry<DexType, DexType> entry : typeMap.entrySet()) {
+          builder.append(entry.getKey().toSourceString()).append(" -> ");
+          builder.append(entry.getValue().toSourceString()).append(System.lineSeparator());
+        }
       }
       for (Map.Entry<DexMethod, DexMethod> entry : methodMap.entrySet()) {
         builder.append(entry.getKey().toSourceString()).append(" -> ");
diff --git a/src/main/java/com/android/tools/r8/graph/JarCode.java b/src/main/java/com/android/tools/r8/graph/JarCode.java
index 85cde7c..3ce1467 100644
--- a/src/main/java/com/android/tools/r8/graph/JarCode.java
+++ b/src/main/java/com/android/tools/r8/graph/JarCode.java
@@ -118,6 +118,10 @@
     return true;
   }
 
+  public boolean hasLocalVariableTable() {
+    return getNode().localVariables != null && !getNode().localVariables.isEmpty();
+  }
+
   @Override
   public IRCode buildIR(
       DexEncodedMethod encodedMethod,
@@ -214,9 +218,13 @@
     triggerDelayedParsingIfNeccessary();
     node.instructions.accept(
         new JarRegisterEffectsVisitor(method.getHolder(), registry, application));
-    node.tryCatchBlocks.forEach(tryCatchBlockNode ->
+    for (TryCatchBlockNode tryCatchBlockNode : node.tryCatchBlocks) {
+      // Exception type can be null for "catch all" used for try/finally.
+      if (tryCatchBlockNode.type != null) {
         registry.registerTypeReference(application.getTypeFromDescriptor(
-            DescriptorUtils.getDescriptorFromClassBinaryName(tryCatchBlockNode.type))));
+            DescriptorUtils.getDescriptorFromClassBinaryName(tryCatchBlockNode.type)));
+      }
+    }
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeLatticeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeLatticeElement.java
index 1544570..9c3bcc3 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/TypeLatticeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/TypeLatticeElement.java
@@ -107,7 +107,6 @@
     throw new Unreachable("unless a new type lattice is introduced.");
   }
 
-
   public static TypeLatticeElement join(
       Iterable<TypeLatticeElement> typeLattices, AppInfo appInfo) {
     TypeLatticeElement result = BOTTOM;
diff --git a/src/main/java/com/android/tools/r8/ir/code/DebugLocalsChange.java b/src/main/java/com/android/tools/r8/ir/code/DebugLocalsChange.java
index 77f7fb8..9b8a00e 100644
--- a/src/main/java/com/android/tools/r8/ir/code/DebugLocalsChange.java
+++ b/src/main/java/com/android/tools/r8/ir/code/DebugLocalsChange.java
@@ -27,17 +27,6 @@
     assert !ending.isEmpty() || !starting.isEmpty();
     this.ending = ending;
     this.starting = starting;
-    super.setPosition(Position.none());
-  }
-
-  @Override
-  public void setPosition(Position position) {
-    throw new Unreachable();
-  }
-
-  @Override
-  public boolean verifyValidPositionInfo(boolean debug) {
-    return true;
   }
 
   public Int2ReferenceMap<DebugLocalInfo> getEnding() {
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index 8e0d572..cb93a5e 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -662,13 +662,9 @@
           }
           // After the throwing instruction only debug instructions and the final jump
           // instruction is allowed.
-          // TODO(ager): For now allow const instructions due to the way consts are pushed
-          // towards their use
           if (seenThrowing) {
             assert instruction.isDebugInstruction()
                 || instruction.isJumpInstruction()
-                || instruction.isConstInstruction()
-                || instruction.isNewArrayFilledData()
                 || instruction.isStore()
                 || instruction.isPop();
           }
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionsIterator.java b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionsIterator.java
index b98662c..cdb40bf 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionsIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionsIterator.java
@@ -19,10 +19,7 @@
 
   @Override
   public boolean hasNext() {
-    if (instructionIterator.hasNext()) {
-      return true;
-    }
-    return blockIterator.hasNext();
+    return instructionIterator.hasNext() || blockIterator.hasNext();
   }
 
   @Override
@@ -39,6 +36,35 @@
   }
 
   @Override
+  public boolean hasPrevious() {
+    return instructionIterator.hasPrevious() || blockIterator.hasPrevious();
+  }
+
+  @Override
+  public Instruction previous() {
+    if (instructionIterator.hasPrevious()) {
+      return instructionIterator.previous();
+    }
+    if (!blockIterator.hasPrevious()) {
+      throw new NoSuchElementException();
+    }
+    BasicBlock block = blockIterator.previous();
+    instructionIterator = block.listIterator(block.getInstructions().size());
+    assert instructionIterator.hasPrevious();
+    return instructionIterator.previous();
+  }
+
+  @Override
+  public int nextIndex() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public int previousIndex() {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
   public void add(Instruction instruction) {
     instructionIterator.add(instruction);
   }
@@ -49,6 +75,11 @@
   }
 
   @Override
+  public void set(Instruction instruction) {
+    instructionIterator.set(instruction);
+  }
+
+  @Override
   public void replaceCurrentInstruction(Instruction newInstruction) {
     instructionIterator.replaceCurrentInstruction(newInstruction);
   }
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 2979039..25d1fc4 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
@@ -142,6 +142,7 @@
     return super.toString()
         + " "
         + type
+        + (isZeroTest() ? "Z" : " ")
         + " block "
         + getTrueTarget().getNumberAsString()
         + " (fallthrough "
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionIterator.java b/src/main/java/com/android/tools/r8/ir/code/InstructionIterator.java
index 2f2b3f1..72aa3ee 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionIterator.java
@@ -4,7 +4,10 @@
 
 package com.android.tools.r8.ir.code;
 
-public interface InstructionIterator extends NextUntilIterator<Instruction> {
+import java.util.ListIterator;
+
+public interface InstructionIterator
+    extends ListIterator<Instruction>, NextUntilIterator<Instruction> {
   /**
    * Replace the current instruction (aka the {@link Instruction} returned by the previous call to
    * {@link #next} with the passed in <code>newInstruction</code>.
@@ -22,15 +25,6 @@
    */
   void replaceCurrentInstruction(Instruction newInstruction);
 
-  /**
-   * Adds an instruction. The instruction will be added just before the current
-   * cursor position.
-   *
-   * The instruction will be assigned to the block it is added to.
-   *
-   * @param instruction The instruction to add.
-   */
-  void add(Instruction instruction);
 
   /**
    * Safe removal function that will insert a DebugLocalRead to take over the debug values if any
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
index a5e5d78..c232d3f 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
@@ -11,9 +11,7 @@
 import java.util.ListIterator;
 
 public interface InstructionListIterator
-    extends ListIterator<Instruction>,
-        NextUntilIterator<Instruction>,
-        PreviousUntilIterator<Instruction> {
+    extends InstructionIterator, PreviousUntilIterator<Instruction> {
 
   /**
    * Peek the previous instruction.
@@ -50,27 +48,6 @@
   }
 
   /**
-   * Safe removal function that will insert a DebugLocalRead to take over the debug values if any
-   * are associated with the current instruction.
-   */
-  void removeOrReplaceByDebugLocalRead();
-
-  /**
-   * Replace the current instruction (aka the {@link Instruction} returned by the previous call to
-   * {@link #next} with the passed in <code>newInstruction</code>.
-   *
-   * The current instruction will be completely detached from the instruction stream with uses
-   * of its in-values removed.
-   *
-   * If the current instruction produces an out-value the new instruction must also produce
-   * an out-value, and all uses of the current instructions out-value will be replaced by the
-   * new instructions out-value.
-   *
-   * @param newInstruction the instruction to insert instead of the current.
-   */
-  void replaceCurrentInstruction(Instruction newInstruction);
-
-  /**
    * Split the block into two blocks at the point of the {@link ListIterator} cursor. The existing
    * block will have all the instructions before the cursor, and the new block all the
    * instructions after the cursor.
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewArrayFilledData.java b/src/main/java/com/android/tools/r8/ir/code/NewArrayFilledData.java
index de0f48c..ed51913 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NewArrayFilledData.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NewArrayFilledData.java
@@ -8,7 +8,6 @@
 import com.android.tools.r8.code.FillArrayDataPayload;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
@@ -74,9 +73,8 @@
   }
 
   @Override
-  public boolean canBeDeadCode(AppInfo appInfo, IRCode code) {
-    // Side-effects its input values.
-    return false;
+  public boolean instructionTypeCanThrow() {
+    return true;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/code/Value.java b/src/main/java/com/android/tools/r8/ir/code/Value.java
index f95b988..374acd5 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Value.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Value.java
@@ -797,7 +797,17 @@
   }
 
   public boolean knownToBeBoolean() {
-    return knownToBeBoolean;
+    if (knownToBeBoolean) {
+      return true;
+    }
+    if (getTypeLattice().isInt()) {
+      Value aliasedValue = getAliasedValue();
+      if (!aliasedValue.isPhi() && aliasedValue.definition.isConstNumber()) {
+        ConstNumber definition = aliasedValue.definition.asConstNumber();
+        return definition.isZero() || definition.getRawValue() == 1;
+      }
+    }
+    return false;
   }
 
   public void markAsThis(boolean receiverCanBeNull) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
index 6e5e5d7..4dcc839 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -149,7 +149,9 @@
     computeInitializers();
     typeVerificationHelper = new TypeVerificationHelper(code, factory, appInfo);
     typeVerificationHelper.computeVerificationTypes();
-    splitExceptionalBlocks();
+    if (!options.testing.noSplittingExceptionalEdges) {
+      splitExceptionalBlocks();
+    }
     rewriter.converter.deadCodeRemover.run(code);
     rewriteNots();
     LoadStoreHelper loadStoreHelper = new LoadStoreHelper(code, typeVerificationHelper, appInfo);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
index 4b2fabb..ec2fe4c 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexBuilder.java
@@ -377,8 +377,11 @@
             unresolvedPosition = instruction.asDebugPosition();
             localsAtUnresolvedPosition = new Int2ReferenceOpenHashMap<>(locals);
           }
-        } else if (instruction.getPosition().isSome()) {
-          if (!isNopInstruction(instruction, nextBlock)) {
+        } else {
+          assert instruction.getPosition().isSome();
+          if (instruction.isDebugLocalsChange()) {
+            instruction.asDebugLocalsChange().apply(locals);
+          } else if (!isNopInstruction(instruction, nextBlock)) {
             if (unresolvedPosition != null) {
               if (unresolvedPosition.getPosition() == instruction.getPosition()
                   && locals.equals(localsAtUnresolvedPosition)) {
@@ -389,12 +392,6 @@
             }
             currentMaterializedPosition = instruction.getPosition();
           }
-        } else {
-          // Only local-change instructions don't have positions in debug mode but fail gracefully.
-          assert instruction.isDebugLocalsChange();
-          if (instruction.isDebugLocalsChange()) {
-            instruction.asDebugLocalsChange().apply(locals);
-          }
         }
       }
     }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java b/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
index 78e5e7e..881b255 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/DexSourceCode.java
@@ -356,6 +356,10 @@
       return index;
     }
     if (dex.canThrow()) {
+      // TODO(zerny): Remove this from block computation.
+      if (dex.hasPayload()) {
+        arrayFilledDataPayloadResolver.addPayloadUser((FillArrayData) dex);
+      }
       // If the instruction can throw and is in a try block, add edges to its catch successors.
       Try tryRange = getTryForOffset(offset);
       if (tryRange != null) {
@@ -398,10 +402,6 @@
       builder.ensureNormalSuccessorBlock(offset, offset + dex.getSize());
       return index;
     }
-    // TODO(zerny): Remove this from block computation.
-    if (dex.hasPayload()) {
-      arrayFilledDataPayloadResolver.addPayloadUser((FillArrayData) dex);
-    }
     // This instruction does not close the block.
     return -1;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index 4035611..045ba3e 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -1661,9 +1661,11 @@
   }
 
   public void addNewArrayFilledData(int arrayRef, int elementWidth, long size, short[] data) {
-    add(
+    NewArrayFilledData instruction =
         new NewArrayFilledData(
-            readRegister(arrayRef, ValueTypeConstraint.OBJECT), elementWidth, size, data));
+            readRegister(arrayRef, ValueTypeConstraint.OBJECT), elementWidth, size, data);
+    assert instruction.instructionTypeCanThrow();
+    addInstruction(instruction);
   }
 
   public void addNewInstance(int dest, DexType type) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index e96acfb..7b55543 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -879,6 +879,10 @@
     printC1VisualizerHeader(method);
     String previous = printMethod(code, "Initial IR (SSA)", null);
 
+    if (options.testing.irModifier != null) {
+      options.testing.irModifier.accept(code);
+    }
+
     if (options.canHaveArtStringNewInitBug()) {
       CodeRewriter.ensureDirectStringNewToInit(code);
     }
@@ -993,6 +997,7 @@
     codeRewriter.rewriteSwitch(code);
     codeRewriter.processMethodsNeverReturningNormally(code);
     codeRewriter.simplifyIf(code);
+    codeRewriter.redundantConstNumberRemoval(code);
     new RedundantFieldLoadElimination(appInfo, code, enableWholeProgramOptimizations).run();
 
     if (options.testing.invertConditionals) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
index 62cddc9..cf88f0f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
@@ -210,7 +210,15 @@
             iterator.replaceCurrentInstruction(newInvoke);
 
             if (constantReturnMaterializingInstruction != null) {
-              iterator.add(constantReturnMaterializingInstruction);
+              if (block.hasCatchHandlers()) {
+                // Split the block to ensure no instructions after throwing instructions.
+                iterator
+                    .split(code, blocks)
+                    .listIterator()
+                    .add(constantReturnMaterializingInstruction);
+              } else {
+                iterator.add(constantReturnMaterializingInstruction);
+              }
             }
 
             DexType actualReturnType = actualTarget.proto.returnType;
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 5938781..3227d45 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
@@ -26,6 +26,7 @@
 import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer.TrivialInstanceInitializer;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexItemFactory.ThrowableMethods;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexString;
@@ -120,6 +121,8 @@
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntIterator;
 import it.unimi.dsi.fastutil.ints.IntList;
+import it.unimi.dsi.fastutil.longs.Long2ReferenceMap;
+import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap;
 import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap;
 import it.unimi.dsi.fastutil.objects.Object2IntMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
@@ -2701,6 +2704,9 @@
           if (contents == null) {
             continue;
           }
+          if (block.hasCatchHandlers()) {
+            continue;
+          }
           int arraySize = newArray.size().getConstInstruction().asConstNumber().getIntValue();
           NewArrayFilledData fillArray =
               new NewArrayFilledData(newArray.outValue(), elementSize, arraySize, contents);
@@ -3141,6 +3147,210 @@
     simplifyIfWithKnownCondition(code, block, theIf, theIf.targetFromCondition(cond));
   }
 
+  /**
+   * This optimization exploits that we can sometimes learn the constant value of an SSA value that
+   * flows into an if-eq of if-neq instruction.
+   *
+   * <p>Consider the following example:
+   *
+   * <pre>
+   * 1. if (obj != null) {
+   * 2.  return doStuff();
+   * 3. }
+   * 4. return null;
+   * </pre>
+   *
+   * <p>Since we know that `obj` is null in all blocks that are dominated by the false-target of the
+   * if-instruction in line 1, we can safely replace the null-constant in line 4 by `obj`, and
+   * thereby save a const-number instruction.
+   */
+  public void redundantConstNumberRemoval(IRCode code) {
+    Supplier<Long2ReferenceMap<List<ConstNumber>>> constantsByValue =
+        Suppliers.memoize(() -> getConstantsByValue(code));
+    Supplier<DominatorTree> dominatorTree = Suppliers.memoize(() -> new DominatorTree(code));
+
+    boolean changed = false;
+    for (BasicBlock block : code.blocks) {
+      Instruction lastInstruction = block.getInstructions().getLast();
+      if (!lastInstruction.isIf()) {
+        continue;
+      }
+
+      If ifInstruction = lastInstruction.asIf();
+      Type type = ifInstruction.getType();
+
+      Value lhs = ifInstruction.inValues().get(0);
+      Value rhs = !ifInstruction.isZeroTest() ? ifInstruction.inValues().get(1) : null;
+
+      if (!ifInstruction.isZeroTest() && !lhs.isConstNumber() && !rhs.isConstNumber()) {
+        // We can only conclude anything from an if-instruction if it is a zero-test or if one of
+        // the two operands is a constant.
+        continue;
+      }
+
+      // If the type is neither EQ nor NE, we cannot conclude anything about any of the in-values
+      // of the if-instruction from the outcome of the if-instruction.
+      if (type != Type.EQ && type != Type.NE) {
+        continue;
+      }
+
+      BasicBlock trueTarget, falseTarget;
+      if (type == Type.EQ) {
+        trueTarget = ifInstruction.getTrueTarget();
+        falseTarget = ifInstruction.fallthroughBlock();
+      } else {
+        falseTarget = ifInstruction.getTrueTarget();
+        trueTarget = ifInstruction.fallthroughBlock();
+      }
+
+      if (ifInstruction.isZeroTest()) {
+        changed |=
+            replaceDominatedConstNumbers(0, lhs, trueTarget, constantsByValue, dominatorTree);
+        if (lhs.knownToBeBoolean()) {
+          changed |=
+              replaceDominatedConstNumbers(1, lhs, falseTarget, constantsByValue, dominatorTree);
+        }
+      } else {
+        assert rhs != null;
+        if (lhs.isConstNumber()) {
+          ConstNumber lhsAsNumber = lhs.getConstInstruction().asConstNumber();
+          changed |=
+              replaceDominatedConstNumbers(
+                  lhsAsNumber.getRawValue(), rhs, trueTarget, constantsByValue, dominatorTree);
+          if (lhs.knownToBeBoolean() && rhs.knownToBeBoolean()) {
+            changed |=
+                replaceDominatedConstNumbers(
+                    negateBoolean(lhsAsNumber), rhs, falseTarget, constantsByValue, dominatorTree);
+          }
+        } else {
+          assert rhs.isConstNumber();
+          ConstNumber rhsAsNumber = rhs.getConstInstruction().asConstNumber();
+          changed |=
+              replaceDominatedConstNumbers(
+                  rhsAsNumber.getRawValue(), lhs, trueTarget, constantsByValue, dominatorTree);
+          if (lhs.knownToBeBoolean() && rhs.knownToBeBoolean()) {
+            changed |=
+                replaceDominatedConstNumbers(
+                    negateBoolean(rhsAsNumber), lhs, falseTarget, constantsByValue, dominatorTree);
+          }
+        }
+      }
+
+      if (constantsByValue.get().isEmpty()) {
+        break;
+      }
+    }
+
+    if (changed) {
+      code.removeAllTrivialPhis();
+    }
+    assert code.isConsistentSSA();
+  }
+
+  private static Long2ReferenceMap<List<ConstNumber>> getConstantsByValue(IRCode code) {
+    // A map from the raw value of constants in `code` to the const number instructions that define
+    // the given raw value (irrespective of the type of the raw value).
+    Long2ReferenceMap<List<ConstNumber>> constantsByValue = new Long2ReferenceOpenHashMap<>();
+
+    // Initialize `constantsByValue`.
+    Iterable<Instruction> instructions = code::instructionIterator;
+    for (Instruction instruction : instructions) {
+      if (instruction.isConstNumber()) {
+        ConstNumber constNumber = instruction.asConstNumber();
+        if (constNumber.outValue().hasLocalInfo()) {
+          // Not necessarily constant, because it could be changed in the debugger.
+          continue;
+        }
+        long rawValue = constNumber.getRawValue();
+        if (constantsByValue.containsKey(rawValue)) {
+          constantsByValue.get(rawValue).add(constNumber);
+        } else {
+          List<ConstNumber> list = new ArrayList<>();
+          list.add(constNumber);
+          constantsByValue.put(rawValue, list);
+        }
+      }
+    }
+    return constantsByValue;
+  }
+
+  private static int negateBoolean(ConstNumber number) {
+    assert number.outValue().knownToBeBoolean();
+    return number.getRawValue() == 0 ? 1 : 0;
+  }
+
+  private boolean replaceDominatedConstNumbers(
+      long withValue,
+      Value newValue,
+      BasicBlock dominator,
+      Supplier<Long2ReferenceMap<List<ConstNumber>>> constantsByValueSupplier,
+      Supplier<DominatorTree> dominatorTree) {
+    if (newValue.hasLocalInfo()) {
+      // We cannot replace a constant with a value that has local info, because that could change
+      // debugging behavior.
+      return false;
+    }
+
+    Long2ReferenceMap<List<ConstNumber>> constantsByValue = constantsByValueSupplier.get();
+    List<ConstNumber> constantsWithValue = constantsByValue.get(withValue);
+    if (constantsWithValue == null || constantsWithValue.isEmpty()) {
+      return false;
+    }
+
+    boolean changed = false;
+
+    ListIterator<ConstNumber> constantWithValueIterator = constantsWithValue.listIterator();
+    while (constantWithValueIterator.hasNext()) {
+      ConstNumber constNumber = constantWithValueIterator.next();
+      Value value = constNumber.outValue();
+      assert !value.hasLocalInfo();
+      assert constNumber.getRawValue() == withValue;
+
+      BasicBlock block = constNumber.getBlock();
+
+      // If the following condition does not hold, then the if-instruction does not dominate the
+      // block containing the constant, although the true or false target does.
+      if (block == dominator && block.getPredecessors().size() != 1) {
+        // This should generally not happen, but it is possible to write bytecode where it does.
+        assert false;
+        continue;
+      }
+
+      if (value.knownToBeBoolean() && !newValue.knownToBeBoolean()) {
+        // We cannot replace a boolean by a none-boolean since that can lead to verification
+        // errors. For example, the following code fails with "register v1 has type Imprecise
+        // Constant: 127 but expected Boolean return-1nr".
+        //
+        //   public boolean convertIntToBoolean(int v1) {
+        //       const/4 v0, 0x1
+        //       if-eq v1, v0, :eq_true
+        //       const/4 v1, 0x0
+        //     :eq_true
+        //       return v1
+        //   }
+        continue;
+      }
+
+      if (dominatorTree.get().dominatedBy(block, dominator)) {
+        if (newValue.getTypeLattice().lessThanOrEqual(value.getTypeLattice(), appInfo)) {
+          value.replaceUsers(newValue);
+          block.listIterator(constNumber).removeOrReplaceByDebugLocalRead();
+          constantWithValueIterator.remove();
+          changed = true;
+        } else if (value.getTypeLattice().isNullType()) {
+          // TODO(b/120257211): Need a mechanism to determine if `newValue` can be used at all of
+          // the use sites of `value` without introducing a type error.
+        }
+      }
+    }
+
+    if (constantsWithValue.isEmpty()) {
+      constantsByValue.remove(withValue);
+    }
+
+    return changed;
+  }
+
   // Find all method invocations that never returns normally, split the block
   // after each such invoke instruction and follow it with a block throwing a
   // null value (which should result in NPE). Note that this throw is not
@@ -3436,7 +3646,7 @@
   // Note that addSuppressed() and getSuppressed() methods are final in
   // Throwable, so these changes don't have to worry about overrides.
   public void rewriteThrowableAddAndGetSuppressed(IRCode code) {
-    DexItemFactory.ThrowableMethods throwableMethods = dexItemFactory.throwableMethods;
+    ThrowableMethods throwableMethods = dexItemFactory.throwableMethods;
 
     for (BasicBlock block : code.blocks) {
       InstructionListIterator iterator = block.listIterator();
@@ -3576,7 +3786,7 @@
       } else {
         // Insert "if (argument != null) ...".
         successor = block.unlinkSingleSuccessor();
-        If theIf = new If(If.Type.NE, argument);
+        If theIf = new If(Type.NE, argument);
         theIf.setPosition(position);
         BasicBlock ifBlock = BasicBlock.createIfBlock(code.blocks.size(), theIf);
         code.blocks.add(ifBlock);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
index 4da9f8d..99f9731 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
@@ -17,11 +17,13 @@
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
+import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InstancePut;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionIterator;
+import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.StaticPut;
@@ -29,6 +31,7 @@
 import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.ProguardMemberRule;
 import com.google.common.collect.Sets;
+import java.util.ListIterator;
 import java.util.Set;
 import java.util.function.Predicate;
 
@@ -133,151 +136,160 @@
   public void rewriteWithConstantValues(
       IRCode code, DexType callingContext, Predicate<DexEncodedMethod> isProcessedConcurrently) {
     Set<Value> affectedValues = Sets.newIdentityHashSet();
-    InstructionIterator iterator = code.instructionIterator();
-    while (iterator.hasNext()) {
-      Instruction current = iterator.next();
-      if (current.isInvokeMethod()) {
-        InvokeMethod invoke = current.asInvokeMethod();
-        DexMethod invokedMethod = invoke.getInvokedMethod();
-        DexType invokedHolder = invokedMethod.getHolder();
-        if (!invokedHolder.isClassType()) {
-          continue;
-        }
-        // TODO(70550443): Maybe check all methods here.
-        DexEncodedMethod definition = appInfo
-            .lookup(invoke.getType(), invokedMethod, callingContext);
+    ListIterator<BasicBlock> blocks = code.blocks.listIterator();
+    while (blocks.hasNext()) {
+      BasicBlock block = blocks.next();
+      InstructionListIterator iterator = block.listIterator();
+      while (iterator.hasNext()) {
+        Instruction current = iterator.next();
+        if (current.isInvokeMethod()) {
+          InvokeMethod invoke = current.asInvokeMethod();
+          DexMethod invokedMethod = invoke.getInvokedMethod();
+          DexType invokedHolder = invokedMethod.getHolder();
+          if (!invokedHolder.isClassType()) {
+            continue;
+          }
+          // TODO(70550443): Maybe check all methods here.
+          DexEncodedMethod definition = appInfo
+              .lookup(invoke.getType(), invokedMethod, callingContext);
 
-        boolean invokeReplaced = false;
-        ProguardMemberRuleLookup lookup = lookupMemberRule(definition);
-        if (lookup != null) {
-          if (lookup.type == RuleType.ASSUME_NO_SIDE_EFFECTS
-              && (invoke.outValue() == null || !invoke.outValue().isUsed())) {
-            // Remove invoke if marked as having no side effects and the return value is not used.
-            iterator.remove();
-            invokeReplaced = true;
-          } else if (invoke.outValue() != null && invoke.outValue().isUsed()) {
-            // Check to see if a constant value can be assumed.
-            Instruction replacement =
-                constantReplacementFromProguardRule(lookup.rule, code, invoke);
-            if (replacement != null) {
-              affectedValues.add(replacement.outValue());
-              replaceInstructionFromProguardRule(lookup.type, iterator, current, replacement);
+          boolean invokeReplaced = false;
+          ProguardMemberRuleLookup lookup = lookupMemberRule(definition);
+          if (lookup != null) {
+            if (lookup.type == RuleType.ASSUME_NO_SIDE_EFFECTS
+                && (invoke.outValue() == null || !invoke.outValue().isUsed())) {
+              // Remove invoke if marked as having no side effects and the return value is not used.
+              iterator.remove();
               invokeReplaced = true;
-            } else {
-              // Check to see if a value range can be assumed.
-              setValueRangeFromProguardRule(lookup.rule, current.outValue());
+            } else if (invoke.outValue() != null && invoke.outValue().isUsed()) {
+              // Check to see if a constant value can be assumed.
+              Instruction replacement =
+                  constantReplacementFromProguardRule(lookup.rule, code, invoke);
+              if (replacement != null) {
+                affectedValues.add(replacement.outValue());
+                replaceInstructionFromProguardRule(lookup.type, iterator, current, replacement);
+                invokeReplaced = true;
+              } else {
+                // Check to see if a value range can be assumed.
+                setValueRangeFromProguardRule(lookup.rule, current.outValue());
+              }
             }
           }
-        }
 
-        // If no Proguard rule could replace the instruction check for knowledge about the
-        // return value.
-        if (!invokeReplaced && invoke.outValue() != null) {
-          DexEncodedMethod target = invoke.lookupSingleTarget(appInfo, callingContext);
-          if (target != null) {
-            if (target.getOptimizationInfo().neverReturnsNull() && invoke.outValue().canBeNull()) {
-              Value knownToBeNonNullValue = invoke.outValue();
-              knownToBeNonNullValue.markNeverNull();
-              TypeLatticeElement typeLattice = knownToBeNonNullValue.getTypeLattice();
-              assert typeLattice.isNullable() && typeLattice.isReference();
-              knownToBeNonNullValue.narrowing(appInfo, typeLattice.asNonNullable());
-              affectedValues.addAll(knownToBeNonNullValue.affectedValues());
-            }
-            if (target.getOptimizationInfo().returnsConstant()) {
-              long constant = target.getOptimizationInfo().getReturnedConstant();
-              ConstNumber replacement = createConstNumberReplacement(
-                  code, constant, invoke.outValue().getTypeLattice(), invoke.getLocalInfo());
-              affectedValues.add(replacement.outValue());
-              invoke.outValue().replaceUsers(replacement.outValue());
-              invoke.setOutValue(null);
-              replacement.setPosition(invoke.getPosition());
-              invoke.moveDebugValues(replacement);
-              iterator.add(replacement);
-            }
-          }
-        }
-      } else if (current.isInstancePut()) {
-        InstancePut instancePut = current.asInstancePut();
-        DexField field = instancePut.getField();
-        DexEncodedField target = appInfo.lookupInstanceTarget(field.getHolder(), field);
-        if (target != null) {
-          // Remove writes to dead (i.e. never read) fields.
-          if (!isFieldRead(target, false) && instancePut.object().isNeverNull()) {
-            iterator.remove();
-          }
-        }
-      } else if (current.isStaticGet()) {
-        StaticGet staticGet = current.asStaticGet();
-        DexField field = staticGet.getField();
-        DexEncodedField target = appInfo.lookupStaticTarget(field.getHolder(), field);
-        ProguardMemberRuleLookup lookup = null;
-        if (target != null) {
-          // Check if a this value is known const.
-          Instruction replacement = target.valueAsConstInstruction(appInfo, staticGet.dest());
-          if (replacement == null) {
-            lookup = lookupMemberRule(target);
-            if (lookup != null) {
-              replacement = constantReplacementFromProguardRule(lookup.rule, code, staticGet);
-            }
-          }
-          if (replacement == null) {
-            // If no const replacement was found, at least store the range information.
-            if (lookup != null) {
-              setValueRangeFromProguardRule(lookup.rule, staticGet.dest());
-            }
-          }
-          if (replacement != null) {
-            affectedValues.add(replacement.outValue());
-            // Ignore assumenosideeffects for fields.
-            if (lookup != null && lookup.type == RuleType.ASSUME_VALUES) {
-              replaceInstructionFromProguardRule(lookup.type, iterator, current, replacement);
-            } else {
-              iterator.replaceCurrentInstruction(replacement);
-            }
-          } else if (staticGet.dest() != null) {
-            // In case the class holder of this static field satisfying following criteria:
-            //   -- cannot trigger other static initializer except for its own
-            //   -- is final
-            //   -- has a class initializer which is classified as trivial
-            //      (see CodeRewriter::computeClassInitializerInfo) and
-            //      initializes the field being accessed
-            //
-            // ... and the field itself is not pinned by keep rules (in which case it might
-            // be updated outside the class constructor, e.g. via reflections), it is safe
-            // to assume that the static-get instruction reads the value it initialized value
-            // in class initializer and is never null.
-            //
-            DexClass holderDefinition = appInfo.definitionFor(field.getHolder());
-            if (holderDefinition != null
-                && holderDefinition.accessFlags.isFinal()
-                && !appInfo.canTriggerStaticInitializer(field.getHolder(), true)) {
-              Value outValue = staticGet.dest();
-              DexEncodedMethod classInitializer = holderDefinition.getClassInitializer();
-              if (classInitializer != null && !isProcessedConcurrently.test(classInitializer)) {
-                TrivialInitializer info =
-                    classInitializer.getOptimizationInfo().getTrivialInitializerInfo();
-                if (info != null
-                    && ((TrivialClassInitializer) info).field == field
-                    && !appInfo.isPinned(field)
-                    && outValue.canBeNull()) {
-                  outValue.markNeverNull();
-                  TypeLatticeElement typeLattice = outValue.getTypeLattice();
-                  assert typeLattice.isNullable() && typeLattice.isReference();
-                  outValue.narrowing(appInfo, typeLattice.asNonNullable());
-                  affectedValues.addAll(outValue.affectedValues());
+          // If no Proguard rule could replace the instruction check for knowledge about the
+          // return value.
+          if (!invokeReplaced && invoke.outValue() != null) {
+            DexEncodedMethod target = invoke.lookupSingleTarget(appInfo, callingContext);
+            if (target != null) {
+              if (target.getOptimizationInfo().neverReturnsNull() && invoke.outValue()
+                  .canBeNull()) {
+                Value knownToBeNonNullValue = invoke.outValue();
+                knownToBeNonNullValue.markNeverNull();
+                TypeLatticeElement typeLattice = knownToBeNonNullValue.getTypeLattice();
+                assert typeLattice.isNullable() && typeLattice.isReference();
+                knownToBeNonNullValue.narrowing(appInfo, typeLattice.asNonNullable());
+                affectedValues.addAll(knownToBeNonNullValue.affectedValues());
+              }
+              if (target.getOptimizationInfo().returnsConstant()) {
+                long constant = target.getOptimizationInfo().getReturnedConstant();
+                ConstNumber replacement = createConstNumberReplacement(
+                    code, constant, invoke.outValue().getTypeLattice(), invoke.getLocalInfo());
+                affectedValues.add(replacement.outValue());
+                invoke.outValue().replaceUsers(replacement.outValue());
+                invoke.setOutValue(null);
+                replacement.setPosition(invoke.getPosition());
+                invoke.moveDebugValues(replacement);
+                if (current.getBlock().hasCatchHandlers()) {
+                  iterator.split(code, blocks).listIterator().add(replacement);
+                } else {
+                  iterator.add(replacement);
                 }
               }
             }
           }
-        }
-      } else if (current.isStaticPut()) {
-        StaticPut staticPut = current.asStaticPut();
-        DexField field = staticPut.getField();
-        DexEncodedField target = appInfo.lookupStaticTarget(field.getHolder(), field);
-        if (target != null) {
-          // Remove writes to dead (i.e. never read) fields.
-          if (!isFieldRead(target, true)) {
-            iterator.removeOrReplaceByDebugLocalRead();
+        } else if (current.isInstancePut()) {
+          InstancePut instancePut = current.asInstancePut();
+          DexField field = instancePut.getField();
+          DexEncodedField target = appInfo.lookupInstanceTarget(field.getHolder(), field);
+          if (target != null) {
+            // Remove writes to dead (i.e. never read) fields.
+            if (!isFieldRead(target, false) && instancePut.object().isNeverNull()) {
+              iterator.remove();
+            }
+          }
+        } else if (current.isStaticGet()) {
+          StaticGet staticGet = current.asStaticGet();
+          DexField field = staticGet.getField();
+          DexEncodedField target = appInfo.lookupStaticTarget(field.getHolder(), field);
+          ProguardMemberRuleLookup lookup = null;
+          if (target != null) {
+            // Check if a this value is known const.
+            Instruction replacement = target.valueAsConstInstruction(appInfo, staticGet.dest());
+            if (replacement == null) {
+              lookup = lookupMemberRule(target);
+              if (lookup != null) {
+                replacement = constantReplacementFromProguardRule(lookup.rule, code, staticGet);
+              }
+            }
+            if (replacement == null) {
+              // If no const replacement was found, at least store the range information.
+              if (lookup != null) {
+                setValueRangeFromProguardRule(lookup.rule, staticGet.dest());
+              }
+            }
+            if (replacement != null) {
+              affectedValues.add(replacement.outValue());
+              // Ignore assumenosideeffects for fields.
+              if (lookup != null && lookup.type == RuleType.ASSUME_VALUES) {
+                replaceInstructionFromProguardRule(lookup.type, iterator, current, replacement);
+              } else {
+                iterator.replaceCurrentInstruction(replacement);
+              }
+            } else if (staticGet.dest() != null) {
+              // In case the class holder of this static field satisfying following criteria:
+              //   -- cannot trigger other static initializer except for its own
+              //   -- is final
+              //   -- has a class initializer which is classified as trivial
+              //      (see CodeRewriter::computeClassInitializerInfo) and
+              //      initializes the field being accessed
+              //
+              // ... and the field itself is not pinned by keep rules (in which case it might
+              // be updated outside the class constructor, e.g. via reflections), it is safe
+              // to assume that the static-get instruction reads the value it initialized value
+              // in class initializer and is never null.
+              //
+              DexClass holderDefinition = appInfo.definitionFor(field.getHolder());
+              if (holderDefinition != null
+                  && holderDefinition.accessFlags.isFinal()
+                  && !appInfo.canTriggerStaticInitializer(field.getHolder(), true)) {
+                Value outValue = staticGet.dest();
+                DexEncodedMethod classInitializer = holderDefinition.getClassInitializer();
+                if (classInitializer != null && !isProcessedConcurrently.test(classInitializer)) {
+                  TrivialInitializer info =
+                      classInitializer.getOptimizationInfo().getTrivialInitializerInfo();
+                  if (info != null
+                      && ((TrivialClassInitializer) info).field == field
+                      && !appInfo.isPinned(field)
+                      && outValue.canBeNull()) {
+                    outValue.markNeverNull();
+                    TypeLatticeElement typeLattice = outValue.getTypeLattice();
+                    assert typeLattice.isNullable() && typeLattice.isReference();
+                    outValue.narrowing(appInfo, typeLattice.asNonNullable());
+                    affectedValues.addAll(outValue.affectedValues());
+                  }
+                }
+              }
+            }
+          }
+        } else if (current.isStaticPut()) {
+          StaticPut staticPut = current.asStaticPut();
+          DexField field = staticPut.getField();
+          DexEncodedField target = appInfo.lookupStaticTarget(field.getHolder(), field);
+          if (target != null) {
+            // Remove writes to dead (i.e. never read) fields.
+            if (!isFieldRead(target, true)) {
+              iterator.removeOrReplaceByDebugLocalRead();
+            }
           }
         }
       }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java
index c46504c..5e32629 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/PeepholeOptimizer.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.ConstNumber;
+import com.android.tools.r8.ir.code.DebugLocalsChange;
 import com.android.tools.r8.ir.code.Goto;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
@@ -22,12 +23,14 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.IdentityHashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 
 public class PeepholeOptimizer {
   /**
@@ -36,10 +39,159 @@
   public static void optimize(IRCode code, LinearScanRegisterAllocator allocator) {
     removeIdenticalPredecessorBlocks(code, allocator);
     removeRedundantInstructions(code, allocator);
+    shareIdenticalBlockPrefix(code, allocator);
     shareIdenticalBlockSuffix(code, allocator, 0);
     assert code.isConsistentGraph();
   }
 
+  /** Identify common prefixes in successor blocks and share them. */
+  private static void shareIdenticalBlockPrefix(IRCode code, RegisterAllocator allocator) {
+    InstructionEquivalence equivalence = new InstructionEquivalence(allocator);
+    Set<BasicBlock> blocksToBeRemoved = new HashSet<>();
+    for (BasicBlock block : code.blocks) {
+      if (blocksToBeRemoved.contains(block)) {
+        // This block has already been removed entirely.
+        continue;
+      }
+
+      List<BasicBlock> normalSuccessors = block.getNormalSuccessors();
+      if (normalSuccessors.size() != 2) {
+        continue;
+      }
+
+      // Exactly two normal successors.
+      BasicBlock normalSuccessor = normalSuccessors.get(0);
+      BasicBlock otherNormalSuccessor = normalSuccessors.get(1);
+
+      // Ensure that the current block is on all paths to the two successor blocks.
+      if (normalSuccessor.getPredecessors().size() != 1
+          || otherNormalSuccessor.getPredecessors().size() != 1) {
+        continue;
+      }
+
+      // Only try to share instructions if the two successors have the same locals state on entry.
+      if (!Objects.equals(
+          normalSuccessor.getLocalsAtEntry(), otherNormalSuccessor.getLocalsAtEntry())) {
+        continue;
+      }
+
+      // Share instructions from the two normal successors one-by-one.
+      while (true) {
+        // Check if all instructions were already removed from the two successors.
+        if (normalSuccessor.isEmpty() || otherNormalSuccessor.isEmpty()) {
+          assert blocksToBeRemoved.contains(normalSuccessor);
+          assert blocksToBeRemoved.contains(otherNormalSuccessor);
+          break;
+        }
+
+        Instruction instruction = normalSuccessor.entry();
+        Instruction otherInstruction = otherNormalSuccessor.entry();
+
+        // If the two instructions are not the same, we cannot merge them into the predecessor.
+        if (!equivalence.doEquivalent(instruction, otherInstruction)) {
+          break;
+        }
+
+        // Each block with one or more catch handlers may have at most one throwing instruction.
+        if (instruction.instructionTypeCanThrow() && block.hasCatchHandlers()) {
+          break;
+        }
+
+        // If the instruction can throw and one of the normal successor blocks has a catch handler,
+        // then we cannot merge the instruction into the predecessor, since this would change the
+        // exceptional control flow.
+        if (instruction.instructionInstanceCanThrow()
+            && (normalSuccessor.hasCatchHandlers() || otherNormalSuccessor.hasCatchHandlers())) {
+          break;
+        }
+
+        // Check for commutativity (semantics). If the instruction writes to a register, then we
+        // need to make sure that the instruction commutes with the last instruction in the
+        // predecessor. Consider the following example.
+        //
+        //                    <Block A>
+        //   if-eqz r0 then goto Block B else goto Block C
+        //           /                         \
+        //      <Block B>                  <Block C>
+        //     const r0, 1                const r0, 1
+        //       ...                        ...
+        //
+        // In the example, it is not possible to change the order of "if-eqz r0" and
+        // "const r0, 1" without changing the semantics.
+        if (instruction.outValue() != null && instruction.outValue().needsRegister()) {
+          int destinationRegister =
+              allocator.getRegisterForValue(instruction.outValue(), instruction.getNumber());
+          boolean commutative =
+              block.exit().inValues().stream()
+                  .allMatch(
+                      inValue -> {
+                        int operandRegister =
+                            allocator.getRegisterForValue(inValue, block.exit().getNumber());
+                        for (int i = 0; i < instruction.outValue().requiredRegisters(); i++) {
+                          for (int j = 0; j < inValue.requiredRegisters(); j++) {
+                            if (destinationRegister + i == operandRegister + j) {
+                              // Overlap detected, the two instructions do not commute.
+                              return false;
+                            }
+                          }
+                        }
+                        return true;
+                      });
+          if (!commutative) {
+            break;
+          }
+        }
+
+        // Check for commutativity (debug info).
+        if (!instruction.getPosition().equals(block.exit().getPosition())
+            && !(block.exit().getPosition().isNone() && !block.exit().getDebugValues().isEmpty())) {
+          break;
+        }
+
+        // Remove the instruction from the two normal successors.
+        normalSuccessor.getInstructions().removeFirst();
+        otherNormalSuccessor.getInstructions().removeFirst();
+
+        // Move the instruction into the predecessor.
+        if (instruction.isJumpInstruction()) {
+          // Replace jump instruction in predecessor with the jump instruction from the two normal
+          // successors.
+          LinkedList<Instruction> instructions = block.getInstructions();
+          instructions.removeLast();
+          instructions.add(instruction);
+          instruction.setBlock(block);
+
+          // Update successors of predecessor block.
+          block.detachAllSuccessors();
+          for (BasicBlock newNormalSuccessor : normalSuccessor.getNormalSuccessors()) {
+            block.link(newNormalSuccessor);
+          }
+
+          // Detach the two normal successors from the rest of the graph.
+          normalSuccessor.detachAllSuccessors();
+          otherNormalSuccessor.detachAllSuccessors();
+
+          // Record that the two normal successors should be removed entirely.
+          blocksToBeRemoved.add(normalSuccessor);
+          blocksToBeRemoved.add(otherNormalSuccessor);
+        } else {
+          // Insert instruction before the jump instruction in the predecessor.
+          block.getInstructions().listIterator(block.getInstructions().size() - 1).add(instruction);
+          instruction.setBlock(block);
+
+          // Update locals-at-entry if needed.
+          if (instruction.isDebugLocalsChange()) {
+            DebugLocalsChange localsChange = instruction.asDebugLocalsChange();
+            localsChange.apply(normalSuccessor.getLocalsAtEntry());
+            localsChange.apply(otherNormalSuccessor.getLocalsAtEntry());
+          }
+        }
+      }
+    }
+
+    code.blocks.removeAll(blocksToBeRemoved);
+  }
+
   /** Identify common suffixes in predecessor blocks and share them. */
   public static void shareIdenticalBlockSuffix(
       IRCode code, RegisterAllocator allocator, int overhead) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java
index 9d2bb32..061907f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java
@@ -211,7 +211,9 @@
     int argumentCount =
         method.method.proto.parameters.size() + (method.accessFlags.isStatic() ? 0 : 1);
     // TODO(65810338): Implement for virtual methods as well.
-    if (method.accessFlags.isPrivate() || method.accessFlags.isStatic()) {
+    if (method.accessFlags.isPrivate()
+        || method.accessFlags.isStatic()
+        || method.isInstanceInitializer()) {
       CollectUsedArguments collector = new CollectUsedArguments();
       if (!method.accessFlags.isStatic()) {
         // TODO(65810338): The receiver cannot be removed without transforming the method to being
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
index 59d8717..f8ddcb6 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
@@ -291,7 +291,7 @@
   //    invoke-virtual { s1, ... } mtd1
   //    goto Exit
   //  b2:
-  //    s2 <- static-get singleoton
+  //    s2 <- static-get singleton
   //    ...
   //    invoke-virtual { s2, ... } mtd1
   //    goto Exit
@@ -503,7 +503,7 @@
       }
     }
 
-    if (!methodMapping.isEmpty() || fieldMapping.isEmpty()) {
+    if (!methodMapping.isEmpty() || !fieldMapping.isEmpty()) {
       classStaticizer.converter.appView.setGraphLense(
           new ClassStaticizerGraphLense(
               classStaticizer.converter.graphLense(),
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
index 1ac8555..a453346 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
@@ -28,6 +28,7 @@
 import com.android.tools.r8.ir.code.NumericType;
 import com.android.tools.r8.ir.code.Or;
 import com.android.tools.r8.ir.code.Phi;
+import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.StackValue;
 import com.android.tools.r8.ir.code.StackValues;
 import com.android.tools.r8.ir.code.Sub;
@@ -423,7 +424,8 @@
         // Compute the final change in locals and insert it before nextInstruction.
         boolean localsChanged = !ending.isEmpty() || !starting.isEmpty();
         if (localsChanged) {
-          DebugLocalsChange change = createLocalsChange(ending, starting);
+          DebugLocalsChange change =
+              createLocalsChange(ending, starting, instruction.getPosition());
           if (change != null) {
             // Insert the DebugLocalsChange instruction before nextInstruction.
             instructionIterator.add(change);
@@ -506,36 +508,43 @@
         starting.put(finalLocal.getIntKey(), finalLocal.getValue());
       }
     }
-    DebugLocalsChange change = createLocalsChange(ending, starting);
+    DebugLocalsChange change = createLocalsChange(ending, starting, block.getPosition());
     if (change != null) {
       instructionIterator.add(change);
     }
   }
 
   private static DebugLocalsChange createLocalsChange(
-      Int2ReferenceMap<DebugLocalInfo> ending, Int2ReferenceMap<DebugLocalInfo> starting) {
+      Int2ReferenceMap<DebugLocalInfo> ending,
+      Int2ReferenceMap<DebugLocalInfo> starting,
+      Position position) {
+    assert position.isSome();
     if (ending.isEmpty() && starting.isEmpty()) {
       return null;
     }
+    DebugLocalsChange localsChange;
     if (ending.isEmpty() || starting.isEmpty()) {
-      return new DebugLocalsChange(ending, starting);
-    }
-    IntSet unneeded = new IntArraySet(Math.min(ending.size(), starting.size()));
-    for (Entry<DebugLocalInfo> entry : ending.int2ReferenceEntrySet()) {
-      if (starting.get(entry.getIntKey()) == entry.getValue()) {
-        unneeded.add(entry.getIntKey());
+      localsChange = new DebugLocalsChange(ending, starting);
+    } else {
+      IntSet unneeded = new IntArraySet(Math.min(ending.size(), starting.size()));
+      for (Entry<DebugLocalInfo> entry : ending.int2ReferenceEntrySet()) {
+        if (starting.get(entry.getIntKey()) == entry.getValue()) {
+          unneeded.add(entry.getIntKey());
+        }
       }
+      if (unneeded.size() == ending.size() && unneeded.size() == starting.size()) {
+        return null;
+      }
+      IntIterator iterator = unneeded.iterator();
+      while (iterator.hasNext()) {
+        int key = iterator.nextInt();
+        ending.remove(key);
+        starting.remove(key);
+      }
+      localsChange = new DebugLocalsChange(ending, starting);
     }
-    if (unneeded.size() == ending.size() && unneeded.size() == starting.size()) {
-      return null;
-    }
-    IntIterator iterator = unneeded.iterator();
-    while (iterator.hasNext()) {
-      int key = iterator.nextInt();
-      ending.remove(key);
-      starting.remove(key);
-    }
-    return new DebugLocalsChange(ending, starting);
+    localsChange.setPosition(position);
+    return localsChange;
   }
 
   private void clearState() {
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index 47d3743..709d27a 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -218,7 +218,7 @@
       }
       TextPosition optionStart = getPosition();
       expectChar('-');
-      if (parseIgnoredOption() ||
+      if (parseIgnoredOption(optionStart) ||
           parseIgnoredOptionAndWarn(optionStart) ||
           parseUnsupportedOptionAndErr(optionStart)) {
         // Intentionally left empty.
@@ -401,13 +401,22 @@
             && (unknownOption.equals("forceinline") || unknownOption.equals("neverinline"))) {
           devMessage = ", this option needs to be turned on explicitly if used for tests.";
         }
-        reporter.error(new StringDiagnostic(
-            "Unknown option \"-" + unknownOption + "\"" + devMessage,
-            origin, getPosition(optionStart)));
+        unknownOption(unknownOption, optionStart, devMessage);
       }
       return true;
     }
 
+    private void unknownOption(String unknownOption, TextPosition optionStart) {
+      unknownOption(unknownOption, optionStart, "");
+    }
+
+    private void unknownOption(
+        String unknownOption, TextPosition optionStart, String additionalMessage) {
+      throw reporter.fatalError((new StringDiagnostic(
+          "Unknown option \"-" + unknownOption + "\"" + additionalMessage,
+          origin, getPosition(optionStart))));
+    }
+
     private boolean parseUnsupportedOptionAndErr(TextPosition optionStart) {
       String option = Iterables.find(UNSUPPORTED_FLAG_OPTIONS, this::skipFlag, null);
       if (option != null) {
@@ -438,13 +447,13 @@
       return true;
     }
 
-    private boolean parseIgnoredOption() {
+    private boolean parseIgnoredOption(TextPosition optionStart) {
       return Iterables.any(IGNORED_SINGLE_ARG_OPTIONS, this::skipOptionWithSingleArg)
           || Iterables.any(IGNORED_OPTIONAL_SINGLE_ARG_OPTIONS,
                            this::skipOptionWithOptionalSingleArg)
           || Iterables.any(IGNORED_FLAG_OPTIONS, this::skipFlag)
           || Iterables.any(IGNORED_CLASS_DESCRIPTOR_OPTIONS, this::skipOptionWithClassSpec)
-          || parseOptimizationOption();
+          || parseOptimizationOption(optionStart);
     }
 
     private void parseInclude() throws ProguardRuleParserException {
@@ -533,10 +542,11 @@
 
     }
 
-    private boolean parseOptimizationOption() {
+    private boolean parseOptimizationOption(TextPosition optionStart) {
       if (!acceptString("optimizations")) {
         return false;
       }
+      warnIgnoringOptions("optimizations", optionStart);
       do {
         skipWhitespace();
         skipOptimizationName();
@@ -768,14 +778,19 @@
           TextPosition start = getPosition();
           acceptString("-");
           String unknownOption = acceptString();
-          throw reporter.fatalError(new StringDiagnostic(
-              "Unknown option \"-" + unknownOption + "\"",
-              origin,
-              start));
+          unknownOption(unknownOption, start);
         }
       } else {
         builder.setType(ProguardKeepRuleType.KEEP);
       }
+      if (!eof() && !Character.isWhitespace(peekChar()) && peekChar() != ',') {
+        // The only path to here is through "-keep" with an unsupported suffix.
+        unacceptString("-keep");
+        TextPosition start = getPosition();
+        acceptString("-");
+        String unknownOption = acceptString();
+        unknownOption(unknownOption, start);
+      }
       parseRuleModifiers(builder);
     }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
index 46d3a68..898d06c 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -885,6 +885,8 @@
     if (context instanceof ProguardKeepRule) {
       if (item.isDexEncodedMethod() && item.asDexEncodedMethod().accessFlags.isSynthetic()) {
         // Don't keep synthetic methods (see b/120971047 for additional details).
+        // TODO(b/122819537): need to distinguish lambda desugared synthetic methods v.s. kotlinc
+        // synthetic methods?
         return;
       }
 
diff --git a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
index 56dc071..2499ccb 100644
--- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -288,6 +288,7 @@
    * @return a class descriptor i.e. "Ljava/lang/Object;"
    */
   public static String getDescriptorFromClassBinaryName(String typeBinaryName) {
+    assert typeBinaryName != null;
     return ('L' + typeBinaryName + ';');
   }
 
diff --git a/src/main/java/com/android/tools/r8/utils/ExceptionDiagnostic.java b/src/main/java/com/android/tools/r8/utils/ExceptionDiagnostic.java
index 3e4a44d..78d47c2 100644
--- a/src/main/java/com/android/tools/r8/utils/ExceptionDiagnostic.java
+++ b/src/main/java/com/android/tools/r8/utils/ExceptionDiagnostic.java
@@ -9,6 +9,8 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
 import java.io.FileNotFoundException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
 import java.nio.file.FileAlreadyExistsException;
 import java.nio.file.NoSuchFileException;
 
@@ -45,6 +47,11 @@
     if (e instanceof FileAlreadyExistsException) {
       return "File already exists: " + e.getMessage();
     }
-    return e.getMessage();
+    StringWriter stack = new StringWriter();
+    e.printStackTrace(new PrintWriter(stack));
+    String message = e.getMessage();
+    return message != null
+        ? StringUtils.joinLines(message, "Stack trace:", stack.toString())
+        : StringUtils.joinLines(stack.toString());
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/ExceptionUtils.java b/src/main/java/com/android/tools/r8/utils/ExceptionUtils.java
index ee11bcf..d77adfd 100644
--- a/src/main/java/com/android/tools/r8/utils/ExceptionUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ExceptionUtils.java
@@ -11,8 +11,6 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
 import java.io.IOException;
-import java.io.PrintWriter;
-import java.io.StringWriter;
 import java.nio.file.FileSystemException;
 import java.nio.file.Paths;
 import java.util.function.Consumer;
@@ -67,14 +65,7 @@
       } catch (ResourceException e) {
         throw reporter.fatalError(new ExceptionDiagnostic(e, e.getOrigin()));
       } catch (AssertionError e) {
-        // Most of our assertions don't have a message, create a wrapper that has the stack as the
-        // message.
-        if (e.getMessage() == null) {
-          StringWriter stack = new StringWriter();
-          e.printStackTrace(new PrintWriter(stack));
-          e = new AssertionError(stack.toString(), e);
-        }
-        throw reporter.fatalError(new ExceptionDiagnostic(e, Origin.unknown()), e);
+        throw reporter.fatalError(new ExceptionDiagnostic(e, Origin.unknown()));
       }
       reporter.failIfPendingErrors();
     } catch (AbortException e) {
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 4921c79..f34e059 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.optimize.Inliner;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.ProguardConfiguration;
@@ -532,6 +533,8 @@
     public boolean noLocalsTableOnInput = false;
     public boolean forceNameReflectionOptimization = false;
     public boolean disallowLoadStoreOptimization = false;
+    public Consumer<IRCode> irModifier = null;
+    public boolean noSplittingExceptionalEdges = false;
   }
 
   private boolean hasMinApi(AndroidApiLevel level) {
diff --git a/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java b/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
index 1daaad3..9dfb30d 100644
--- a/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8;
 
+import static com.android.tools.r8.ToolHelper.CLASSPATH_SEPARATOR;
 import static com.android.tools.r8.ToolHelper.getJavaExecutable;
 import static org.junit.Assert.assertEquals;
 
@@ -76,7 +77,9 @@
 
       String classPath =
           addR8ExternalDeps
-              ? r8jar.toAbsolutePath().toString() + ":" + ToolHelper.DEPS_NOT_RELOCATED
+              ? r8jar.toAbsolutePath().toString()
+                  + CLASSPATH_SEPARATOR
+                  + ToolHelper.DEPS_NOT_RELOCATED
               : r8jar.toAbsolutePath().toString();
 
       List<String> command = new ArrayList<>();
diff --git a/src/test/java/com/android/tools/r8/GenerateMainDexListResult.java b/src/test/java/com/android/tools/r8/GenerateMainDexListResult.java
new file mode 100644
index 0000000..392a72b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/GenerateMainDexListResult.java
@@ -0,0 +1,17 @@
+// Copyright (c) 2019, 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;
+
+public class GenerateMainDexListResult
+    extends TestBaseResult<GenerateMainDexListResult, GenerateMainDexListRunResult> {
+
+  GenerateMainDexListResult(TestState state) {
+    super(state);
+  }
+
+  @Override
+  public GenerateMainDexListResult self() {
+    return this;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/GenerateMainDexListRunResult.java b/src/test/java/com/android/tools/r8/GenerateMainDexListRunResult.java
new file mode 100644
index 0000000..b9842f2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/GenerateMainDexListRunResult.java
@@ -0,0 +1,22 @@
+// Copyright (c) 2019, 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;
+
+import java.util.List;
+
+public class GenerateMainDexListRunResult extends TestRunResult<GenerateMainDexListRunResult> {
+
+  List<String> mainDexList;
+
+  public GenerateMainDexListRunResult(List<String> mainDexList) {
+    super(null, null);
+    this.mainDexList = mainDexList;
+  }
+
+  @Override
+  protected GenerateMainDexListRunResult self() {
+    return this;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/GenerateMainDexListTestBuilder.java b/src/test/java/com/android/tools/r8/GenerateMainDexListTestBuilder.java
new file mode 100644
index 0000000..6eae670
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/GenerateMainDexListTestBuilder.java
@@ -0,0 +1,64 @@
+// Copyright (c) 2019, 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;
+
+import com.android.tools.r8.GenerateMainDexListCommand.Builder;
+import com.android.tools.r8.debug.DebugTestConfig;
+import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.origin.Origin;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+
+public class GenerateMainDexListTestBuilder
+    extends TestBaseBuilder<
+        GenerateMainDexListCommand,
+        Builder,
+        GenerateMainDexListResult,
+        GenerateMainDexListRunResult,
+        GenerateMainDexListTestBuilder> {
+
+  private GenerateMainDexListTestBuilder(TestState state, Builder builder) {
+    super(state, builder);
+  }
+
+  public static GenerateMainDexListTestBuilder create(TestState state) {
+    return new GenerateMainDexListTestBuilder(state, GenerateMainDexListCommand.builder());
+  }
+
+  @Override
+  GenerateMainDexListTestBuilder self() {
+    return this;
+  }
+
+  @Override
+  public GenerateMainDexListRunResult run(String mainClass)
+      throws IOException, CompilationFailedException {
+    throw new Unimplemented("No support for running with a main class");
+  }
+
+  @Override
+  public GenerateMainDexListRunResult run(Class mainClass)
+      throws IOException, CompilationFailedException {
+    throw new Unimplemented("No support for running with a main class");
+  }
+
+  public DebugTestConfig debugConfig() {
+    throw new Unimplemented("No support for debug configuration");
+  }
+
+  public GenerateMainDexListRunResult run() throws CompilationFailedException {
+    return new GenerateMainDexListRunResult(GenerateMainDexList.run(builder.build()));
+  }
+
+  public GenerateMainDexListTestBuilder addMainDexRules(Collection<String> rules) {
+    builder.addMainDexRules(new ArrayList<>(rules), Origin.unknown());
+    return self();
+  }
+
+  public GenerateMainDexListTestBuilder addMainDexRules(String... rules) {
+    return addMainDexRules(Arrays.asList(rules));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index e0baab1..1df6808 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -103,6 +103,10 @@
     return ProguardTestBuilder.create(new TestState(temp));
   }
 
+  public static GenerateMainDexListTestBuilder testForMainDexListGenerator(TemporaryFolder temp) {
+    return GenerateMainDexListTestBuilder.create(new TestState(temp));
+  }
+
   public R8TestBuilder testForR8(Backend backend) {
     return testForR8(temp, backend);
   }
@@ -131,6 +135,10 @@
     return testForProguard(temp);
   }
 
+  public GenerateMainDexListTestBuilder testForMainDexListGenerator() {
+    return testForMainDexListGenerator(temp);
+  }
+
   public enum Backend {
     CF,
     DEX
@@ -163,6 +171,8 @@
 
   /**
    * Write lines of text to a temporary file.
+   *
+   * The file will include a line separator after the last line.
    */
   protected Path writeTextToTempFile(String... lines) throws IOException {
     return writeTextToTempFile(System.lineSeparator(), Arrays.asList(lines));
@@ -170,11 +180,28 @@
 
   /**
    * Write lines of text to a temporary file, along with the specified line separator.
+   *
+   * The file will include a line separator after the last line.
    */
   protected Path writeTextToTempFile(String lineSeparator, List<String> lines)
       throws IOException {
+    return writeTextToTempFile(lineSeparator, lines, true);
+  }
+
+  /**
+   * Write lines of text to a temporary file, along with the specified line separator.
+   *
+   * The argument <code>includeTerminatingLineSeparator</code> control if the file will include
+   * a line separator after the last line.
+   */
+  protected Path writeTextToTempFile(
+      String lineSeparator, List<String> lines, boolean includeTerminatingLineSeparator)
+      throws IOException {
     Path file = temp.newFile().toPath();
-    String contents = String.join(lineSeparator, lines) + lineSeparator;
+    String contents = String.join(lineSeparator, lines);
+    if (includeTerminatingLineSeparator) {
+      contents += lineSeparator;
+    }
     Files.write(file, contents.getBytes(StandardCharsets.UTF_8));
     return file;
   }
diff --git a/src/test/java/com/android/tools/r8/TestBaseBuilder.java b/src/test/java/com/android/tools/r8/TestBaseBuilder.java
new file mode 100644
index 0000000..5fcd1b5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/TestBaseBuilder.java
@@ -0,0 +1,50 @@
+// Copyright (c) 2019, 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;
+
+import com.android.tools.r8.origin.Origin;
+import java.nio.file.Path;
+import java.util.Collection;
+
+public abstract class TestBaseBuilder<
+        C extends BaseCommand,
+        B extends BaseCommand.Builder<C, B>,
+        CR extends TestBaseResult<CR, RR>,
+        RR extends TestRunResult,
+        T extends TestBaseBuilder<C, B, CR, RR, T>>
+    extends TestBuilder<RR, T> {
+
+  final B builder;
+
+  TestBaseBuilder(TestState state, B builder) {
+    super(state);
+    this.builder = builder;
+  }
+
+  @Override
+  public T addProgramClassFileData(Collection<byte[]> classes) {
+    for (byte[] clazz : classes) {
+      builder.addClassProgramData(clazz, Origin.unknown());
+    }
+    return self();
+  }
+
+  @Override
+  public T addProgramFiles(Collection<Path> files) {
+    builder.addProgramFiles(files);
+    return self();
+  }
+
+  @Override
+  public T addLibraryFiles(Collection<Path> files) {
+    builder.addLibraryFiles(files);
+    return self();
+  }
+
+  public T addMainDexListFiles(Collection<Path> files) {
+    builder.addMainDexListFiles(files);
+    return self();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestBaseResult.java b/src/test/java/com/android/tools/r8/TestBaseResult.java
new file mode 100644
index 0000000..e7623a3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/TestBaseResult.java
@@ -0,0 +1,15 @@
+// Copyright (c) 2019, 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;
+
+public abstract class TestBaseResult<CR extends TestBaseResult<CR, RR>, RR extends TestRunResult> {
+  final TestState state;
+
+  TestBaseResult(TestState state) {
+    this.state = state;
+  }
+
+  public abstract CR self();
+}
diff --git a/src/test/java/com/android/tools/r8/TestBuilder.java b/src/test/java/com/android/tools/r8/TestBuilder.java
index 61bf1b3..c67c05c 100644
--- a/src/test/java/com/android/tools/r8/TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestBuilder.java
@@ -3,16 +3,12 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
-import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
-
 import com.android.tools.r8.debug.DebugTestConfig;
 import com.android.tools.r8.utils.ListUtils;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.HashSet;
-import java.util.Set;
 
 public abstract class TestBuilder<RR extends TestRunResult, T extends TestBuilder<RR, T>> {
 
@@ -92,14 +88,6 @@
   }
 
   static Collection<Path> getFilesForInnerClasses(Collection<Class<?>> classes) throws IOException {
-    Set<Path> paths = new HashSet<>();
-    for (Class clazz : classes) {
-      Path path = ToolHelper.getClassFileForTestClass(clazz);
-      String prefix = path.toString().replace(CLASS_EXTENSION, "$");
-      paths.addAll(
-          ToolHelper.getClassFilesForTestDirectory(
-              path.getParent(), p -> p.toString().startsWith(prefix)));
-    }
-    return paths;
+    return ToolHelper.getClassFilesForInnerClasses(classes);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/TestCompileResult.java b/src/test/java/com/android/tools/r8/TestCompileResult.java
index 0c84e7c..d74fab5 100644
--- a/src/test/java/com/android/tools/r8/TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/TestCompileResult.java
@@ -4,9 +4,6 @@
 package com.android.tools.r8;
 
 import static com.android.tools.r8.TestBase.Backend.DEX;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.fail;
 
 import com.android.tools.r8.TestBase.Backend;
 import com.android.tools.r8.ToolHelper.DexVm;
@@ -29,19 +26,17 @@
 import org.hamcrest.Matcher;
 
 public abstract class TestCompileResult<
-    CR extends TestCompileResult<CR, RR>, RR extends TestRunResult> {
+        CR extends TestCompileResult<CR, RR>, RR extends TestRunResult>
+    extends TestBaseResult<CR, RR> {
 
-  final TestState state;
   public final AndroidApp app;
   final List<Path> additionalRunClassPath = new ArrayList<>();
 
   TestCompileResult(TestState state, AndroidApp app) {
-    this.state = state;
+    super(state);
     this.app = app;
   }
 
-  public abstract CR self();
-
   public abstract Backend getBackend();
 
   public abstract TestDiagnosticMessages getDiagnosticMessages();
@@ -83,57 +78,27 @@
   }
 
   public CR assertNoMessages() {
-    assertEquals(0, getDiagnosticMessages().getInfos().size());
-    assertEquals(0, getDiagnosticMessages().getWarnings().size());
-    assertEquals(0, getDiagnosticMessages().getErrors().size());
+    getDiagnosticMessages().assertNoMessages();
     return self();
   }
 
   public CR assertOnlyInfos() {
-    assertNotEquals(0, getDiagnosticMessages().getInfos().size());
-    assertEquals(0, getDiagnosticMessages().getWarnings().size());
-    assertEquals(0, getDiagnosticMessages().getErrors().size());
+    getDiagnosticMessages().assertOnlyInfos();
     return self();
   }
 
   public CR assertOnlyWarnings() {
-    assertEquals(0, getDiagnosticMessages().getInfos().size());
-    assertNotEquals(0, getDiagnosticMessages().getWarnings().size());
-    assertEquals(0, getDiagnosticMessages().getErrors().size());
+    getDiagnosticMessages().assertOnlyWarnings();
     return self();
   }
 
   public CR assertWarningMessageThatMatches(Matcher<String> matcher) {
-    assertNotEquals(0, getDiagnosticMessages().getWarnings().size());
-    for (int i = 0; i < getDiagnosticMessages().getWarnings().size(); i++) {
-      if (matcher.matches(getDiagnosticMessages().getWarnings().get(i).getDiagnosticMessage())) {
-        return self();
-      }
-    }
-    StringBuilder builder = new StringBuilder("No warning matches " + matcher.toString());
-    builder.append(System.lineSeparator());
-    if (getDiagnosticMessages().getWarnings().size() == 0) {
-      builder.append("There where no warnings.");
-    } else {
-      builder.append("There where " + getDiagnosticMessages().getWarnings().size() + " warnings:");
-      builder.append(System.lineSeparator());
-      for (int i = 0; i < getDiagnosticMessages().getWarnings().size(); i++) {
-        builder.append(getDiagnosticMessages().getWarnings().get(i).getDiagnosticMessage());
-        builder.append(System.lineSeparator());
-      }
-    }
-    fail(builder.toString());
+    getDiagnosticMessages().assertWarningMessageThatMatches(matcher);
     return self();
   }
 
   public CR assertNoWarningMessageThatMatches(Matcher<String> matcher) {
-    assertNotEquals(0, getDiagnosticMessages().getWarnings().size());
-    for (int i = 0; i < getDiagnosticMessages().getWarnings().size(); i++) {
-      String message = getDiagnosticMessages().getWarnings().get(i).getDiagnosticMessage();
-      if (matcher.matches(message)) {
-        fail("The warning: \"" + message + "\" + matches " + matcher + ".");
-      }
-    }
+    getDiagnosticMessages().assertNoWarningMessageThatMatches(matcher);
     return self();
   }
 
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index cbd0cb9..77dc0df 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -5,7 +5,6 @@
 
 import com.android.tools.r8.TestBase.Backend;
 import com.android.tools.r8.debug.DebugTestConfig;
-import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.AndroidAppConsumers;
@@ -24,7 +23,7 @@
         CR extends TestCompileResult<CR, RR>,
         RR extends TestRunResult,
         T extends TestCompilerBuilder<C, B, CR, RR, T>>
-    extends TestBuilder<RR, T> {
+    extends TestBaseBuilder<C, B, CR, RR, T> {
 
   public static final Consumer<InternalOptions> DEFAULT_OPTIONS =
       new Consumer<InternalOptions>() {
@@ -32,7 +31,6 @@
         public void accept(InternalOptions options) {}
       };
 
-  final B builder;
   final Backend backend;
 
   // Default initialized setup. Can be overwritten if needed.
@@ -44,8 +42,7 @@
   private PrintStream stdout = null;
 
   TestCompilerBuilder(TestState state, B builder, Backend backend) {
-    super(state);
-    this.builder = builder;
+    super(state, builder);
     this.backend = backend;
     defaultLibrary = TestBase.runtimeJar(backend);
     programConsumer = TestBase.emptyConsumer(backend);
@@ -135,30 +132,10 @@
     return self();
   }
 
-  public T addMainDexListFiles(Collection<Path> files) {
-    builder.addMainDexListFiles(files);
-    return self();
-  }
-
-  @Override
-  public T addProgramClassFileData(Collection<byte[]> classes) {
-    for (byte[] clazz : classes) {
-      builder.addClassProgramData(clazz, Origin.unknown());
-    }
-    return self();
-  }
-
-  @Override
-  public T addProgramFiles(Collection<Path> files) {
-    builder.addProgramFiles(files);
-    return self();
-  }
-
   @Override
   public T addLibraryFiles(Collection<Path> files) {
     defaultLibrary = null;
-    builder.addLibraryFiles(files);
-    return self();
+    return super.addLibraryFiles(files);
   }
 
   public T noDesugaring() {
diff --git a/src/test/java/com/android/tools/r8/TestDiagnosticMessages.java b/src/test/java/com/android/tools/r8/TestDiagnosticMessages.java
index 3e734cc..d1e55f4 100644
--- a/src/test/java/com/android/tools/r8/TestDiagnosticMessages.java
+++ b/src/test/java/com/android/tools/r8/TestDiagnosticMessages.java
@@ -5,6 +5,8 @@
 package com.android.tools.r8;
 
 import java.util.List;
+import org.hamcrest.Matcher;
+
 
 public interface TestDiagnosticMessages {
 
@@ -13,4 +15,20 @@
   public List<Diagnostic> getWarnings();
 
   public List<Diagnostic> getErrors();
+
+  public TestDiagnosticMessages assertNoMessages();
+
+  public TestDiagnosticMessages assertOnlyInfos();
+
+  public TestDiagnosticMessages assertOnlyWarnings();
+
+  public TestDiagnosticMessages assertInfosCount(int count);
+
+  public TestDiagnosticMessages assertWarningsCount(int count);
+
+  public TestDiagnosticMessages assertErrorsCount(int count);
+
+  public TestDiagnosticMessages assertWarningMessageThatMatches(Matcher<String> matcher);
+
+  public TestDiagnosticMessages assertNoWarningMessageThatMatches(Matcher<String> matcher);
 }
diff --git a/src/test/java/com/android/tools/r8/TestDiagnosticMessagesImpl.java b/src/test/java/com/android/tools/r8/TestDiagnosticMessagesImpl.java
index d0476b9..15c03dc 100644
--- a/src/test/java/com/android/tools/r8/TestDiagnosticMessagesImpl.java
+++ b/src/test/java/com/android/tools/r8/TestDiagnosticMessagesImpl.java
@@ -4,8 +4,13 @@
 
 package com.android.tools.r8;
 
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.fail;
+
 import java.util.ArrayList;
 import java.util.List;
+import org.hamcrest.Matcher;
 
 public class TestDiagnosticMessagesImpl implements DiagnosticsHandler, TestDiagnosticMessages {
   private final List<Diagnostic> infos = new ArrayList<>();
@@ -38,4 +43,75 @@
   public List<Diagnostic> getErrors() {
     return errors;
   }
+
+
+  public TestDiagnosticMessages assertNoMessages() {
+    assertEquals(0, getInfos().size());
+    assertEquals(0, getWarnings().size());
+    assertEquals(0, getErrors().size());
+    return this;
+  }
+
+  public TestDiagnosticMessages assertOnlyInfos() {
+    assertNotEquals(0, getInfos().size());
+    assertEquals(0, getWarnings().size());
+    assertEquals(0, getErrors().size());
+    return this;
+  }
+
+  public TestDiagnosticMessages assertOnlyWarnings() {
+    assertEquals(0, getInfos().size());
+    assertNotEquals(0, getWarnings().size());
+    assertEquals(0, getErrors().size());
+    return this;
+  }
+
+  public TestDiagnosticMessages assertInfosCount(int count) {
+    assertEquals(count, getInfos().size());
+    return this;
+  }
+
+  public TestDiagnosticMessages assertWarningsCount(int count) {
+    assertEquals(count, getWarnings().size());
+    return this;
+  }
+
+  public TestDiagnosticMessages assertErrorsCount(int count) {
+    assertEquals(count, getErrors().size());
+    return this;
+  }
+
+  public TestDiagnosticMessages assertWarningMessageThatMatches(Matcher<String> matcher) {
+    assertNotEquals(0, getWarnings().size());
+    for (int i = 0; i < getWarnings().size(); i++) {
+      if (matcher.matches(getWarnings().get(i).getDiagnosticMessage())) {
+        return this;
+      }
+    }
+    StringBuilder builder = new StringBuilder("No warning matches " + matcher.toString());
+    builder.append(System.lineSeparator());
+    if (getWarnings().size() == 0) {
+      builder.append("There were no warnings.");
+    } else {
+      builder.append("There were " + getWarnings().size() + " warnings:");
+      builder.append(System.lineSeparator());
+      for (int i = 0; i < getWarnings().size(); i++) {
+        builder.append(getWarnings().get(i).getDiagnosticMessage());
+        builder.append(System.lineSeparator());
+      }
+    }
+    fail(builder.toString());
+    return this;
+  }
+
+  public TestDiagnosticMessages assertNoWarningMessageThatMatches(Matcher<String> matcher) {
+    assertNotEquals(0, getWarnings().size());
+    for (int i = 0; i < getWarnings().size(); i++) {
+      String message = getWarnings().get(i).getDiagnosticMessage();
+      if (matcher.matches(message)) {
+        fail("The warning: \"" + message + "\" + matches " + matcher + ".");
+      }
+    }
+    return this;
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/TestRunResult.java b/src/test/java/com/android/tools/r8/TestRunResult.java
index 65a57e6..a032fd6 100644
--- a/src/test/java/com/android/tools/r8/TestRunResult.java
+++ b/src/test/java/com/android/tools/r8/TestRunResult.java
@@ -29,6 +29,10 @@
 
   abstract RR self();
 
+  public AndroidApp app() {
+    return app;
+  }
+
   public String getStdOut() {
     return result.stdout;
   }
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index c9fe628..bab857c 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
 import static com.android.tools.r8.utils.FileUtils.isDexFile;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -57,6 +58,7 @@
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -95,7 +97,8 @@
   public static final String SMALI_BUILD_DIR = TESTS_BUILD_DIR + "smali/";
 
   public static final String LINE_SEPARATOR = StringUtils.LINE_SEPARATOR;
-  public static final String PATH_SEPARATOR = File.pathSeparator;
+  public static final String CLASSPATH_SEPARATOR = File.pathSeparator;
+
   public static final String DEFAULT_DEX_FILENAME = "classes.dex";
   public static final String DEFAULT_PROGUARD_MAP_FILE = "proguard.map";
 
@@ -856,6 +859,28 @@
         Paths.get("", parts.toArray(new String[parts.size() - 1])));
   }
 
+  public static Collection<Path> getClassFilesForInnerClasses(Path path) throws IOException {
+    Set<Path> paths = new HashSet<>();
+    String prefix = path.toString().replace(CLASS_EXTENSION, "$");
+    paths.addAll(
+        ToolHelper.getClassFilesForTestDirectory(
+            path.getParent(), p -> p.toString().startsWith(prefix)));
+    return paths;
+  }
+
+  public static Collection<Path> getClassFilesForInnerClasses(Collection<Class<?>> classes)
+      throws IOException {
+    Set<Path> paths = new HashSet<>();
+    for (Class clazz : classes) {
+      Path path = ToolHelper.getClassFileForTestClass(clazz);
+      String prefix = path.toString().replace(CLASS_EXTENSION, "$");
+      paths.addAll(
+          ToolHelper.getClassFilesForTestDirectory(
+              path.getParent(), p -> p.toString().startsWith(prefix)));
+    }
+    return paths;
+  }
+
   public static Path getFileNameForTestClass(Class clazz) {
     List<String> parts = getNamePartsForTestClass(clazz);
     return Paths.get("", parts.toArray(new String[parts.size() - 1]));
@@ -1062,7 +1087,8 @@
   }
 
   public static ProcessResult runJava(List<Path> classpath, String... args) throws IOException {
-    String cp = classpath.stream().map(Path::toString).collect(Collectors.joining(PATH_SEPARATOR));
+    String cp =
+        classpath.stream().map(Path::toString).collect(Collectors.joining(CLASSPATH_SEPARATOR));
     List<String> cmdline = new ArrayList<String>(Arrays.asList(getJavaExecutable(), "-cp", cp));
     cmdline.addAll(Arrays.asList(args));
     ProcessBuilder builder = new ProcessBuilder(cmdline);
@@ -1082,7 +1108,8 @@
 
   public static ProcessResult runJavaNoVerify(
       List<Path> classpath, String mainClass, List<String> args) throws IOException {
-    String cp = classpath.stream().map(Path::toString).collect(Collectors.joining(PATH_SEPARATOR));
+    String cp =
+        classpath.stream().map(Path::toString).collect(Collectors.joining(CLASSPATH_SEPARATOR));
     ArrayList<String> cmdline = Lists.newArrayList(
         getJavaExecutable(), "-cp", cp, "-noverify", mainClass);
     cmdline.addAll(args);
diff --git a/src/test/java/com/android/tools/r8/cf/TryRangeTestLimitRange.java b/src/test/java/com/android/tools/r8/cf/TryRangeTestLimitRange.java
new file mode 100644
index 0000000..5e97ab0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/TryRangeTestLimitRange.java
@@ -0,0 +1,29 @@
+// Copyright (c) 2019, 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.cf;
+
+import com.android.tools.r8.NeverInline;
+
+public class TryRangeTestLimitRange {
+
+  @NeverInline
+  public static float doSomething(int x) throws Exception {
+    if (x == 42) {
+      throw new Exception("is 42");
+    } else {
+      return 1;
+    }
+  }
+
+  public static void main(String[] args) {
+    int x = args.length;
+    int y = x + 1;
+    try {
+      doSomething(y);
+    } catch (Exception ex) {
+      System.out.println(x + ": " + y);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/TryRangeTestRunner.java b/src/test/java/com/android/tools/r8/cf/TryRangeTestRunner.java
index 98542ff..e4c7098 100644
--- a/src/test/java/com/android/tools/r8/cf/TryRangeTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/TryRangeTestRunner.java
@@ -4,8 +4,14 @@
 
 package com.android.tools.r8.cf;
 
+import static org.junit.Assert.assertTrue;
+
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.TestBase;
+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 java.util.ListIterator;
 import org.junit.Test;
 
 /**
@@ -18,7 +24,7 @@
 public class TryRangeTestRunner extends TestBase {
 
   @Test
-  public void test() throws Exception {
+  public void testRegisterAllocationLimitTrailingRange() throws Exception {
     testForR8(Backend.CF)
         .addProgramClasses(TryRangeTest.class)
         .addKeepMainRule(TryRangeTest.class)
@@ -26,8 +32,55 @@
         .minification(false)
         .noTreeShaking()
         .enableInliningAnnotations()
-        .addOptionsModification(o -> o.testing.disallowLoadStoreOptimization = true)
+        .addOptionsModification(
+            o -> {
+              o.testing.disallowLoadStoreOptimization = true;
+            })
         .run(TryRangeTest.class)
         .assertSuccess();
   }
+
+  @Test
+  public void testRegisterAllocationLimitLeadingRange() throws Exception {
+    testForR8(Backend.CF)
+        .addProgramClasses(TryRangeTestLimitRange.class)
+        .addKeepMainRule(TryRangeTestLimitRange.class)
+        .setMode(CompilationMode.RELEASE)
+        .minification(false)
+        .noTreeShaking()
+        .enableInliningAnnotations()
+        .addOptionsModification(
+            o -> {
+              o.testing.disallowLoadStoreOptimization = true;
+              o.testing.irModifier = this::processIR;
+              // TODO(mkroghj) Remove this option entirely when splittingExceptionalEdges is moved.
+              o.testing.noSplittingExceptionalEdges = true;
+            })
+        .run(TryRangeTestLimitRange.class)
+        .assertFailure();
+  }
+
+  private void processIR(IRCode code) {
+    if (!code.method.qualifiedName().equals(TryRangeTestLimitRange.class.getName() + ".main")) {
+      return;
+    }
+    BasicBlock entryBlock = code.blocks.get(0);
+    BasicBlock tryBlock = code.blocks.get(1);
+    assertTrue(tryBlock.hasCatchHandlers());
+    ListIterator<Instruction> it = entryBlock.getInstructions().listIterator();
+    Instruction constNumber = it.next();
+    while (!constNumber.isConstNumber()) {
+      constNumber = it.next();
+    }
+    it.remove();
+    Instruction add = it.next();
+    while (!add.isAdd()) {
+      add = it.next();
+    }
+    it.remove();
+    constNumber.setBlock(tryBlock);
+    add.setBlock(tryBlock);
+    tryBlock.getInstructions().add(0, add);
+    tryBlock.getInstructions().add(0, constNumber);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/debuginfo/CannonicalizeWithInline.java b/src/test/java/com/android/tools/r8/debuginfo/CannonicalizeWithInline.java
new file mode 100644
index 0000000..5d6e151
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/debuginfo/CannonicalizeWithInline.java
@@ -0,0 +1,78 @@
+// Copyright (c) 2019, 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.debuginfo;
+
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.dex.DexParser;
+import com.android.tools.r8.dex.DexSection;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class CannonicalizeWithInline extends TestBase {
+
+  private int getNumberOfDebugInfos(Path file) throws IOException {
+    DexSection[] dexSections = DexParser.parseMapFrom(file);
+    for (DexSection dexSection : dexSections) {
+      if (dexSection.type == Constants.TYPE_DEBUG_INFO_ITEM) {
+        return dexSection.length;
+      }
+    }
+    return 0;
+  }
+
+  @Test
+  public void testCannonicalize() throws Exception {
+    Class clazzA = ClassA.class;
+    Class clazzB = ClassB.class;
+
+    R8TestCompileResult result = testForR8(Backend.DEX)
+        .addProgramClasses(clazzA, clazzB)
+        .addKeepRules(
+            "-keepattributes SourceFile,LineNumberTable",
+            "-keep class ** {\n" +
+                "public void call(int);\n" +
+            "}"
+        )
+        .compile();
+    Path classesPath = temp.getRoot().toPath();
+    result.app.write(classesPath, OutputMode.DexIndexed);
+    int numberOfDebugInfos = getNumberOfDebugInfos(
+        Paths.get(temp.getRoot().getCanonicalPath(), "classes.dex"));
+    Assert.assertEquals(1, numberOfDebugInfos);
+  }
+
+  // Two classes which has debug info that looks exactly the same, except for SetInlineFrame.
+  // R8 will inline the call to foobar in both classes, causing us to store a SetInlineFrame in the
+  // debug info.
+  // Ensure that we still canonicalize when writing.
+  public static class ClassA {
+
+    public void call(int a) {
+        foobar(a);
+    }
+
+    private String foobar(int a) {
+      String s = "aFoobar" + a;
+      return s;
+    }
+  }
+
+  public static class ClassB {
+
+    public void call(int a) {
+      foobar(a);
+    }
+
+    private String foobar(int a) {
+      String s = "bFoobar" + a;
+      return s;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/BasicTestDependenciesDesugaringTest.java b/src/test/java/com/android/tools/r8/desugar/BasicTestDependenciesDesugaringTest.java
index 97047e8..8af2f8e 100644
--- a/src/test/java/com/android/tools/r8/desugar/BasicTestDependenciesDesugaringTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/BasicTestDependenciesDesugaringTest.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.desugar;
 
+import static com.android.tools.r8.ToolHelper.CLASSPATH_SEPARATOR;
+
 import com.android.tools.r8.D8Command;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.errors.CompilationError;
@@ -30,8 +32,6 @@
 @RunWith(Parameterized.class)
 public class BasicTestDependenciesDesugaringTest {
 
-  private static final String CLASSPATH_SEPARATOR = File.pathSeparator;
-
   private static final String[] allLibs;
   static {
     try {
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/type/ConstrainedPrimitiveTypeTest.java b/src/test/java/com/android/tools/r8/ir/analysis/type/ConstrainedPrimitiveTypeTest.java
index c140610..798f1b0 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/type/ConstrainedPrimitiveTypeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/type/ConstrainedPrimitiveTypeTest.java
@@ -10,10 +10,12 @@
 import static com.android.tools.r8.ir.analysis.type.TypeLatticeElement.LONG;
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.ir.analysis.AnalysisTestBase;
 import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.Streams;
 import java.util.function.Consumer;
 import org.junit.Test;
@@ -25,6 +27,21 @@
   }
 
   @Test
+  public void testOutput() throws Exception {
+    String expectedOutput =
+        StringUtils.lines("1", "2", "3", "1.0", "1.0", "2.0", "1", "1", "2", "1.0", "1.0", "2.0");
+
+    testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expectedOutput);
+
+    testForR8(Backend.DEX)
+        .addInnerClasses(ConstrainedPrimitiveTypeTest.class)
+        .addKeepMainRule(TestClass.class)
+        .enableInliningAnnotations()
+        .run(TestClass.class)
+        .assertSuccessWithOutput(expectedOutput);
+  }
+
+  @Test
   public void testIntWithInvokeUser() throws Exception {
     buildAndCheckIR("intWithInvokeUserTest", testInspector(INT, 1));
   }
@@ -83,26 +100,47 @@
 
   static class TestClass {
 
-    public static void intWithInvokeUserTest() {
-      int x = 1;
-      Integer.toString(x);
+    public static void main(String[] args) {
+      boolean unknownButTrue = args.length >= 0;
+      boolean unknownButFalse = args.length < 0;
+      intWithInvokeUserTest();
+      intWithIndirectInvokeUserTest(unknownButTrue);
+      intWithIndirectInvokeUserTest(unknownButFalse);
+      floatWithInvokeUserTest();
+      floatWithIndirectInvokeUserTest(unknownButTrue);
+      floatWithIndirectInvokeUserTest(unknownButFalse);
+      longWithInvokeUserTest();
+      longWithIndirectInvokeUserTest(unknownButTrue);
+      longWithIndirectInvokeUserTest(unknownButFalse);
+      doubleWithInvokeUserTest();
+      doubleWithIndirectInvokeUserTest(unknownButTrue);
+      doubleWithIndirectInvokeUserTest(unknownButFalse);
     }
 
+    @NeverInline
+    public static void intWithInvokeUserTest() {
+      int x = 1;
+      System.out.println(Integer.toString(x));
+    }
+
+    @NeverInline
     public static void intWithIndirectInvokeUserTest(boolean unknown) {
       int x;
       if (unknown) {
-        x = 1;
-      } else {
         x = 2;
+      } else {
+        x = 3;
       }
-      Integer.toString(x);
+      System.out.println(Integer.toString(x));
     }
 
+    @NeverInline
     public static void floatWithInvokeUserTest() {
       float x = 1f;
-      Float.toString(x);
+      System.out.println(Float.toString(x));
     }
 
+    @NeverInline
     public static void floatWithIndirectInvokeUserTest(boolean unknown) {
       float x;
       if (unknown) {
@@ -110,14 +148,16 @@
       } else {
         x = 2f;
       }
-      Float.toString(x);
+      System.out.println(Float.toString(x));
     }
 
+    @NeverInline
     public static void longWithInvokeUserTest() {
       long x = 1L;
-      Long.toString(x);
+      System.out.println(Long.toString(x));
     }
 
+    @NeverInline
     public static void longWithIndirectInvokeUserTest(boolean unknown) {
       long x;
       if (unknown) {
@@ -125,14 +165,16 @@
       } else {
         x = 2L;
       }
-      Long.toString(x);
+      System.out.println(Long.toString(x));
     }
 
+    @NeverInline
     public static void doubleWithInvokeUserTest() {
       double x = 1.0;
-      Double.toString(x);
+      System.out.println(Double.toString(x));
     }
 
+    @NeverInline
     public static void doubleWithIndirectInvokeUserTest(boolean unknown) {
       double x;
       if (unknown) {
@@ -140,7 +182,7 @@
       } else {
         x = 2f;
       }
-      Double.toString(x);
+      System.out.println(Double.toString(x));
     }
   }
 }
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
new file mode 100644
index 0000000..fc382ee
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/RedundantConstNumberRemovalTest.java
@@ -0,0 +1,248 @@
+// Copyright (c) 2019, 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;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestBase;
+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.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.Streams;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class RedundantConstNumberRemovalTest extends TestBase {
+
+  private final Backend backend;
+
+  @Parameters(name = "Backend: {0}")
+  public static Backend[] data() {
+    return Backend.values();
+  }
+
+  public RedundantConstNumberRemovalTest(Backend backend) {
+    this.backend = backend;
+  }
+
+  @Test
+  public void test() throws Exception {
+    String expectedOutput =
+        StringUtils.lines(
+            "true", "true", "true", "true", "true", "true", "true", "true", "true", "true", "true",
+            "true", "true", "true", "true", "true");
+
+    if (backend == Backend.CF) {
+      testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expectedOutput);
+    }
+
+    R8TestRunResult result =
+        testForR8(backend)
+            .addInnerClasses(RedundantConstNumberRemovalTest.class)
+            .addKeepMainRule(TestClass.class)
+            .enableInliningAnnotations()
+            .run(TestClass.class)
+            .assertSuccessWithOutput(expectedOutput);
+
+    ClassSubject classSubject = result.inspector().clazz(TestClass.class);
+    verifyBooleanCheckTest(classSubject.uniqueMethodWithName("booleanCheckTest"));
+    verifyBooleanCheckTest(classSubject.uniqueMethodWithName("negateBooleanCheckTest"));
+    verifyIntCheckTest(classSubject.uniqueMethodWithName("intCheckTest"));
+    verifyIntCheckTest(classSubject.uniqueMethodWithName("negateIntCheckTest"));
+    verifyNullCheckTest(classSubject.uniqueMethodWithName("nullCheckTest"));
+    verifyNullCheckTest(classSubject.uniqueMethodWithName("invertedNullCheckTest"));
+    verifyNullCheckTest(classSubject.uniqueMethodWithName("nonNullCheckTest"));
+    verifyNullCheckWithWrongTypeTest(
+        classSubject.uniqueMethodWithName("nullCheckWithWrongTypeTest"));
+  }
+
+  private void verifyBooleanCheckTest(MethodSubject methodSubject) {
+    assertThat(methodSubject, isPresent());
+
+    if (backend == Backend.DEX) {
+      // Check that the generated code for booleanCheckTest() only has a return instruction.
+      assertEquals(1, methodSubject.streamInstructions().count());
+      assertTrue(methodSubject.iterateInstructions().next().isReturn());
+    } else {
+      assert backend == Backend.CF;
+      // Check that the generated code for booleanCheckTest() only has return instructions that
+      // return the argument.
+      // TODO(christofferqa): CF backend does not share identical prefix of successors.
+      IRCode code = methodSubject.buildIR();
+      assertTrue(
+          Streams.stream(code.instructionIterator())
+              .filter(Instruction::isReturn)
+              .allMatch(
+                  instruction -> instruction.asReturn().returnValue().definition.isArgument()));
+    }
+  }
+
+  private void verifyIntCheckTest(MethodSubject methodSubject) {
+    assertThat(methodSubject, isPresent());
+    IRCode code = methodSubject.buildIR();
+
+    if (backend == Backend.DEX) {
+      // Only a single basic block.
+      assertEquals(1, code.blocks.size());
+      // The block only has three instructions.
+      BasicBlock entryBlock = code.blocks.get(0);
+      assertEquals(3, 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());
+      assertTrue(ret.asReturn().returnValue().definition.isArgument());
+    } else {
+      // Check that the generated code for intCheckTest() only has return instructions that
+      // return the argument.
+      // TODO(christofferqa): CF backend does not share identical prefix of successors.
+      assertTrue(
+          Streams.stream(code.instructionIterator())
+              .filter(Instruction::isReturn)
+              .allMatch(
+                  instruction -> instruction.asReturn().returnValue().definition.isArgument()));
+    }
+  }
+
+  private void verifyNullCheckTest(MethodSubject methodSubject) {
+    // Check that the generated code for nullCheckTest() only has a single `const-null` instruction.
+    assertThat(methodSubject, isPresent());
+    assertEquals(
+        1, methodSubject.streamInstructions().filter(InstructionSubject::isConstNull).count());
+
+    // Also check that one of the return instructions actually returns the argument.
+    IRCode code = methodSubject.buildIR();
+    assertEquals(1, code.collectArguments().size());
+    // TODO(b/120257211): D8 should replace `return null` by `return arg`.
+    assertFalse(
+        code.collectArguments().get(0).uniqueUsers().stream().anyMatch(Instruction::isReturn));
+  }
+
+  private void verifyNullCheckWithWrongTypeTest(MethodSubject methodSubject) {
+    // Check that the generated code for nullCheckWithWrongTypeTest() still has a `return null`
+    // instruction.
+    assertThat(methodSubject, isPresent());
+    IRCode code = methodSubject.buildIR();
+
+    // Check that the code returns null.
+    assertTrue(
+        Streams.stream(code.instructionIterator())
+            .anyMatch(
+                instruction ->
+                    instruction.isReturn()
+                        && instruction.asReturn().returnValue().definition.isConstNumber()));
+
+    // Also check that none of the return instructions actually returns the argument.
+    assertEquals(1, code.collectArguments().size());
+    assertTrue(
+        code.collectArguments().get(0).uniqueUsers().stream().noneMatch(Instruction::isReturn));
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      System.out.println(booleanCheckTest(true) == true);
+      System.out.println(booleanCheckTest(false) == false);
+      System.out.println(negateBooleanCheckTest(true) == true);
+      System.out.println(negateBooleanCheckTest(false) == false);
+      System.out.println(intCheckTest(0) == 0);
+      System.out.println(intCheckTest(42) == 42);
+      System.out.println(negateIntCheckTest(0) == 0);
+      System.out.println(negateIntCheckTest(42) == 42);
+      System.out.println(nullCheckTest(new Object()) != null);
+      System.out.println(nullCheckTest(null) == null);
+      System.out.println(invertedNullCheckTest(new Object()) != null);
+      System.out.println(invertedNullCheckTest(null) == null);
+      System.out.println(nonNullCheckTest(new Object()) != null);
+      System.out.println(nonNullCheckTest(null) == null);
+      System.out.println(nullCheckWithWrongTypeTest(new Object()) != null);
+      System.out.println(nullCheckWithWrongTypeTest(null) == null);
+    }
+
+    @NeverInline
+    private static boolean booleanCheckTest(boolean x) {
+      if (x) {
+        return true; // should be replaced by `x`.
+      }
+      return false; // should be replaced by `x`.
+    }
+
+    @NeverInline
+    private static boolean negateBooleanCheckTest(boolean x) {
+      if (!x) {
+        return false; // should be replaced by `x`
+      }
+      return true; // should be replaced by `x`
+    }
+
+    @NeverInline
+    private static int intCheckTest(int x) {
+      if (x == 42) {
+        return 42; // should be replaced by `x`.
+      }
+      return x;
+    }
+
+    @NeverInline
+    private static int negateIntCheckTest(int x) {
+      if (x != 42) {
+        return x;
+      }
+      return 42; // should be replaced by `x`
+    }
+
+    @NeverInline
+    private static Object nullCheckTest(Object x) {
+      if (x != null) {
+        return new Object();
+      }
+      return null; // should be replaced by `x`.
+    }
+
+    @NeverInline
+    private static Object invertedNullCheckTest(Object x) {
+      if (null == x) {
+        return null; // should be replaced by `x`.
+      }
+      return new Object();
+    }
+
+    @NeverInline
+    private static Object nonNullCheckTest(Object x) {
+      if (x == null) {
+        return null; // should be replaced by `x`
+      }
+      return new Object();
+    }
+
+    @NeverInline
+    private static Throwable nullCheckWithWrongTypeTest(Object x) {
+      if (x != null) {
+        return new Throwable();
+      }
+      return null; // cannot be replaced by `x` because Object is not a subtype of Throwable.
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
index df797ad..7b3ed4a 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
@@ -29,11 +29,14 @@
 import com.android.tools.r8.ir.optimize.staticizer.movetohost.CandidateConflictField;
 import com.android.tools.r8.ir.optimize.staticizer.movetohost.CandidateConflictMethod;
 import com.android.tools.r8.ir.optimize.staticizer.movetohost.CandidateOk;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.CandidateOkFieldOnly;
 import com.android.tools.r8.ir.optimize.staticizer.movetohost.CandidateOkSideEffects;
 import com.android.tools.r8.ir.optimize.staticizer.movetohost.HostConflictField;
 import com.android.tools.r8.ir.optimize.staticizer.movetohost.HostConflictMethod;
 import com.android.tools.r8.ir.optimize.staticizer.movetohost.HostOk;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.HostOkFieldOnly;
 import com.android.tools.r8.ir.optimize.staticizer.movetohost.HostOkSideEffects;
+import com.android.tools.r8.ir.optimize.staticizer.movetohost.MoveToHostFieldOnlyTestClass;
 import com.android.tools.r8.ir.optimize.staticizer.movetohost.MoveToHostTestClass;
 import com.android.tools.r8.ir.optimize.staticizer.trivial.Simple;
 import com.android.tools.r8.ir.optimize.staticizer.trivial.SimpleWithGetter;
@@ -150,6 +153,36 @@
   }
 
   @Test
+  public void testMoveToHost_fieldOnly() throws Exception {
+    assumeTrue("b/112831361", backend == Backend.DEX);
+    Class<?> main = MoveToHostFieldOnlyTestClass.class;
+    Class<?>[] classes = {
+        NeverInline.class,
+        MoveToHostFieldOnlyTestClass.class,
+        HostOkFieldOnly.class,
+        CandidateOkFieldOnly.class
+    };
+    TestRunResult result = testForR8(backend)
+        .addProgramClasses(classes)
+        .enableProguardTestOptions()
+        .enableInliningAnnotations()
+        .addKeepMainRule(main)
+        .noMinification()
+        .addKeepRules("-allowaccessmodification")
+        .addOptionsModification(this::configure)
+        .run(main);
+
+    CodeInspector inspector = result.inspector();
+    ClassSubject clazz = inspector.clazz(main);
+
+    assertEquals(
+        Lists.newArrayList(),
+        references(clazz, "testOk_fieldOnly", "void"));
+
+    assertFalse(inspector.clazz(CandidateOkFieldOnly.class).isPresent());
+  }
+
+  @Test
   public void testMoveToHost() throws Exception {
     assumeTrue("b/112831361", backend == Backend.DEX);
     Class<?> main = MoveToHostTestClass.class;
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOkFieldOnly.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOkFieldOnly.java
new file mode 100644
index 0000000..48963f4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateOkFieldOnly.java
@@ -0,0 +1,8 @@
+// Copyright (c) 2019, 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.staticizer.movetohost;
+
+public class CandidateOkFieldOnly {
+  // No instance methods.
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostOkFieldOnly.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostOkFieldOnly.java
new file mode 100644
index 0000000..e132627
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/HostOkFieldOnly.java
@@ -0,0 +1,8 @@
+// Copyright (c) 2019, 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.staticizer.movetohost;
+
+public class HostOkFieldOnly {
+  static CandidateOkFieldOnly INSTANCE = new CandidateOkFieldOnly();
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostFieldOnlyTestClass.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostFieldOnlyTestClass.java
new file mode 100644
index 0000000..f725d21
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostFieldOnlyTestClass.java
@@ -0,0 +1,29 @@
+// Copyright (c) 2019, 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.staticizer.movetohost;
+
+import com.android.tools.r8.NeverInline;
+
+public class MoveToHostFieldOnlyTestClass {
+
+  public static void main(String[] args) {
+    MoveToHostFieldOnlyTestClass test = new MoveToHostFieldOnlyTestClass();
+    test.testOk_fieldOnly();
+  }
+
+  @NeverInline
+  private void testOk_fieldOnly() {
+    // Any instance method call whose target holder is not the candidate will invalidate candidacy,
+    // for example, toString() without overriding, getClass(), etc.
+    // Note that having instance methods in the candidate class guarantees that method mappings will
+    // exist when field mappings do so.
+    // Any other uses other than invoke-virtual or invoke-direct (to either <init> or private) are
+    // not allowed, e.g., System.out.println(INSTANCE), null check, or static-put to somewhere else.
+    // Therefore, it's merely dead code, and thus it has not been harmful to forget to create a
+    // staticizer lense when there is no method mapping (for instance methods to staticized ones)
+    // while there are field mappings as shown in this example.
+    Object x = HostOkFieldOnly.INSTANCE;
+  }
+}
+
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsInstanceConstructorTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsInstanceConstructorTest.java
new file mode 100644
index 0000000..20d7bae
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedarguments/UnusedArgumentsInstanceConstructorTest.java
@@ -0,0 +1,95 @@
+// Copyright (c) 2019, 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.unusedarguments;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class UnusedArgumentsInstanceConstructorTest extends TestBase {
+
+  private final Backend backend;
+
+  @Parameters(name = "Backend: {0}")
+  public static Backend[] data() {
+    return Backend.values();
+  }
+
+  public UnusedArgumentsInstanceConstructorTest(Backend backend) {
+    this.backend = backend;
+  }
+
+  @Test
+  public void test() throws Exception {
+    String expectedOutput = StringUtils.lines("Hello world");
+
+    if (backend == Backend.CF) {
+      testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expectedOutput);
+    }
+
+    CodeInspector inspector =
+        testForR8(backend)
+            .addInnerClasses(UnusedArgumentsInstanceConstructorTest.class)
+            .addKeepMainRule(TestClass.class)
+            .enableInliningAnnotations()
+            .enableClassInliningAnnotations()
+            .run(TestClass.class)
+            .assertSuccessWithOutput(expectedOutput)
+            .inspector();
+
+    ClassSubject classSubject = inspector.clazz(A.class);
+    assertThat(classSubject, isPresent());
+
+    MethodSubject methodSubject = classSubject.uniqueMethodWithName("<init>");
+    assertThat(methodSubject, isPresent());
+    assertTrue(methodSubject.getMethod().method.proto.parameters.isEmpty());
+
+    assertThat(inspector.clazz(B.class), not(isPresent()));
+    assertThat(inspector.clazz(C.class), not(isPresent()));
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      new A(null, new C()).doSomething();
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+
+    public A(B uninstantiated, C unused) {
+      System.out.print("Hello");
+      if (uninstantiated != null) {
+        throw new RuntimeException("Unreachable");
+      } else {
+        System.out.print(" ");
+      }
+    }
+
+    @NeverInline
+    public void doSomething() {
+      System.out.println("world");
+    }
+  }
+
+  static class B {}
+
+  static class C {}
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java
new file mode 100644
index 0000000..700e2d4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java
@@ -0,0 +1,66 @@
+// Copyright (c) 2019, 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.kotlin;
+
+import com.android.tools.r8.KotlinTestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ProcessKotlinReflectionLibTest extends KotlinTestBase {
+  private final Backend backend;
+
+  public ProcessKotlinReflectionLibTest(Backend backend, KotlinTargetVersion targetVersion) {
+    super(targetVersion);
+    this.backend = backend;
+  }
+
+  @Parameterized.Parameters(name = "Backend: {0} target: {1}")
+  public static Collection<Object[]> data() {
+    return buildParameters(Backend.values(), KotlinTargetVersion.values());
+  }
+
+  private void test(String... rules) throws Exception {
+    testForR8(backend)
+        .addLibraryFiles(ToolHelper.getDefaultAndroidJar(), ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(ToolHelper.getKotlinReflectJar())
+        .addKeepRules(rules)
+        .compile();
+  }
+
+  @Test
+  public void testAsIs() throws Exception {
+    test("-dontshrink", "-dontoptimize", "-dontobfuscate");
+  }
+
+  @Test
+  public void testDontShrinkAndDontOptimize() throws Exception {
+    test("-dontshrink", "-dontoptimize");
+  }
+
+  @Test
+  public void testDontShrinkAndDontObfuscate() throws Exception {
+    test("-dontshrink", "-dontobfuscate");
+  }
+
+  @Test
+  public void testDontShrink() throws Exception {
+    test("-dontshrink");
+  }
+
+  @Test
+  public void testDontOptimize() throws Exception {
+    test("-dontoptimize");
+  }
+
+  @Test
+  public void testDontObfuscate() throws Exception {
+    test("-dontobfuscate");
+  }
+
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinStdlibTest.java b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinStdlibTest.java
new file mode 100644
index 0000000..f19b089
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinStdlibTest.java
@@ -0,0 +1,73 @@
+// Copyright (c) 2019, 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.kotlin;
+
+import com.android.tools.r8.KotlinTestBase;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
+import java.util.Collection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ProcessKotlinStdlibTest extends KotlinTestBase {
+  private final Backend backend;
+
+  public ProcessKotlinStdlibTest(Backend backend, KotlinTargetVersion targetVersion) {
+    super(targetVersion);
+    this.backend = backend;
+  }
+
+  @Parameterized.Parameters(name = "Backend: {0} target: {1}")
+  public static Collection<Object[]> data() {
+    return buildParameters(Backend.values(), KotlinTargetVersion.values());
+  }
+
+  private void test(String... rules) throws Exception {
+    testForR8(backend)
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addKeepRules(rules)
+        .compile();
+  }
+
+  @Test
+  public void testAsIs() throws Exception {
+    test("-dontshrink", "-dontoptimize", "-dontobfuscate");
+  }
+
+  @Test
+  public void testDontShrinkAndDontOptimize() throws Exception {
+    // TODO(b/122819537): need to trace kotlinc-generated synthetic methods.
+    if (backend == Backend.DEX) {
+      return;
+    }
+    test("-dontshrink", "-dontoptimize");
+  }
+
+  @Test
+  public void testDontShrinkAndDontObfuscate() throws Exception {
+    test("-dontshrink", "-dontobfuscate");
+  }
+
+  @Test
+  public void testDontShrink() throws Exception {
+    // TODO(b/122819537): need to trace kotlinc-generated synthetic methods.
+    if (backend == Backend.DEX) {
+      return;
+    }
+    test("-dontshrink");
+  }
+
+  @Test
+  public void testDontOptimize() throws Exception {
+    test("-dontoptimize");
+  }
+
+  @Test
+  public void testDontObfuscate() throws Exception {
+    test("-dontobfuscate");
+  }
+
+}
diff --git a/src/test/java/com/android/tools/r8/maindexlist/checkdiscard/MainDexListCheckDiscard.java b/src/test/java/com/android/tools/r8/maindexlist/checkdiscard/MainDexListCheckDiscard.java
index 16958a6..125c364 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/checkdiscard/MainDexListCheckDiscard.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/checkdiscard/MainDexListCheckDiscard.java
@@ -5,16 +5,10 @@
 package com.android.tools.r8.maindexlist.checkdiscard;
 
 import com.android.tools.r8.CompilationFailedException;
-import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.GenerateMainDexList;
-import com.android.tools.r8.GenerateMainDexListCommand;
-import com.android.tools.r8.OutputMode;
-import com.android.tools.r8.R8Command;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.google.common.collect.ImmutableList;
 import java.util.List;
 import org.junit.Assert;
@@ -54,33 +48,24 @@
     this.command = command;
   }
 
-
   public void runTestWithR8(String checkDiscardRule) throws Exception {
-    R8Command command =
-        ToolHelper.prepareR8CommandBuilder(
-                readClasses(HelloWorldMain.class, MainDexClass.class, NonMainDexClass.class))
-            .addMainDexRules(
-                ImmutableList.of(keepMainProguardConfiguration(HelloWorldMain.class)),
-                Origin.unknown())
-            .addMainDexRules(ImmutableList.of(checkDiscardRule), Origin.unknown())
-            .setOutput(temp.getRoot().toPath(), OutputMode.DexIndexed)
-            .setMode(CompilationMode.RELEASE)
-            .setDisableTreeShaking(true)
-            .setDisableMinification(true)
-            .build();
-    ToolHelper.runR8(command);
+    testForR8(Backend.DEX)
+        .addProgramClasses(CLASSES)
+        .setMinApi(AndroidApiLevel.K)
+        .addMainDexRules(keepMainProguardConfiguration(HelloWorldMain.class))
+        .addMainDexRules(checkDiscardRule)
+        .noTreeShaking()
+        .noMinification()
+        .compile();
   }
 
   public void runTestWithGenerator(String checkDiscardRule) throws Exception {
-    GenerateMainDexListCommand.Builder builder =
-        GenerateMainDexListCommand.builder()
-            .addProgramFiles(ListUtils.map(CLASSES, ToolHelper::getClassFileForTestClass))
-            .addLibraryFiles(ToolHelper.getDefaultAndroidJar())
-            .addMainDexRules(
-                ImmutableList.of(keepMainProguardConfiguration(HelloWorldMain.class)),
-                Origin.unknown())
-            .addMainDexRules(ImmutableList.of(checkDiscardRule), Origin.unknown());
-    GenerateMainDexList.run(builder.build());
+    testForMainDexListGenerator()
+        .addProgramClasses(CLASSES)
+        .addLibraryFiles(ToolHelper.getDefaultAndroidJar())
+        .addMainDexRules(keepMainProguardConfiguration(HelloWorldMain.class))
+        .addMainDexRules(checkDiscardRule)
+        .run();
   }
 
   public void runTest(String checkDiscardRule, boolean shouldFail) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingTest.java
index 1038f5c..b175785 100644
--- a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingTest.java
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingTest.java
@@ -35,6 +35,7 @@
 import java.util.Iterator;
 import java.util.concurrent.ExecutionException;
 import org.junit.Before;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -75,6 +76,7 @@
     out = temp.newFolder("out").toPath();
   }
 
+  @Ignore("b/121305642")
   @Test
   public void test044_obfuscate_and_apply() throws Exception {
     // keep rules that allow obfuscations while keeping everything.
@@ -110,7 +112,7 @@
     AndroidApp instrApp =
         runR8(
             ToolHelper.addProguardConfigurationConsumer(
-                    getCommandForInstrumentation(instrOut, flag, NAMING044_JAR, APPLYMAPPING044_JAR)
+                    getCommandForInstrumentation(instrOut, flag, out, APPLYMAPPING044_JAR)
                         .setDisableMinification(true),
                     pgConfig -> pgConfig.setApplyMappingFile(proguardMap))
                 .build());
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/RetraceTestBase.java b/src/test/java/com/android/tools/r8/naming/retrace/RetraceTestBase.java
index 2258d67..f7b95f9 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/RetraceTestBase.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/RetraceTestBase.java
@@ -56,6 +56,10 @@
             .assertFailure();
 
     // Extract actual stack trace and retraced stack trace from failed run result.
+    // TODO(122940268): Remove test code when fixed.
+    System.out.println("<--- TEST RESULT START --->");
+    System.out.println(result);
+    System.out.println("<--- TEST RESULT END --->");
     StackTrace actualStackTrace = StackTrace.extractFromArt(result.getStdErr());
     StackTrace retracedStackTrace =
         actualStackTrace.retrace(result.proguardMap(), temp.newFolder().toPath());
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java b/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java
index 0ecd3e4..dd8bc9c 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java
@@ -165,9 +165,27 @@
     }
     // Take all lines from the bottom starting with "\tat ".
     int first = last;
+    // TODO(122940268): Remove test code when fixed.
+    System.out.println("TOTAL STDERR LINES: " + stderrLines.size());
+    for (int i = 0; i < last; i++) {
+      System.out.print("LINE " + i + ": " + stderrLines.get(i));
+      if (stderrLines.get(i).length() > 3) {
+        System.out.print(" (" + ((int) stderrLines.get(i).charAt(0)));
+        System.out.print(", " + ((int) stderrLines.get(i).charAt(1)));
+        System.out.print(", " + ((int) stderrLines.get(i).charAt(2) + ")"));
+      } else {
+        System.out.print(" (less than three chars)");
+      }
+     if (stderrLines.get(i).startsWith(TAB_AT_PREFIX)) {
+        System.out.println(" IS STACKTRACE LINE");
+      } else {
+        System.out.println(" IS NOT STACKTRACE LINE");
+      }
+    }
     while (first - 1 >= 0 && stderrLines.get(first - 1).startsWith(TAB_AT_PREFIX)) {
       first--;
     }
+    System.out.println("STACKTRACE LINES ARE " + first + " to " + (last - 1));
     for (int i = first; i < last; i++) {
       stackTraceLines.add(StackTraceLine.parse(stderrLines.get(i)));
     }
diff --git a/src/test/java/com/android/tools/r8/regress/b122887884/Regress122887884.java b/src/test/java/com/android/tools/r8/regress/b122887884/Regress122887884.java
new file mode 100644
index 0000000..23650c9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/regress/b122887884/Regress122887884.java
@@ -0,0 +1,28 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.regress.b122887884;
+
+public class Regress122887884 {
+
+  public static int[] foo(String[] args) {
+    if (args.length == 0) {
+      return new int[] {0, 0};
+    }
+    String first = args[0];
+    try {
+      String[] split = first.split("");
+      if (split.length != 2) {
+        // This results in a new-array v2 v2 int[] at which point the exception handler must split.
+        return new int[] {0, 0};
+      }
+      return new int[] {1, 1};
+    } catch (Throwable t) {
+      return new int[] {0, 0};
+    }
+  }
+
+  public static void main(String[] args) {
+    System.out.println(foo(args)[0]);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/regress/b122887884/Regress122887884Runner.java b/src/test/java/com/android/tools/r8/regress/b122887884/Regress122887884Runner.java
new file mode 100644
index 0000000..54f32fd
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/regress/b122887884/Regress122887884Runner.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.regress.b122887884;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.utils.StringUtils;
+import org.junit.Test;
+
+public class Regress122887884Runner extends TestBase {
+
+  private final Class<?> CLASS = Regress122887884.class;
+  private final String EXPECTED = StringUtils.lines("0");
+
+  @Test
+  public void test() throws Exception {
+    testForD8().addProgramClasses(CLASS).run(CLASS).assertSuccessWithOutput(EXPECTED);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
index ff84486..26e5356 100644
--- a/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ProguardConfigurationParserTest.java
@@ -1053,8 +1053,10 @@
             "-optimizations   !  xxx,  !  yyy",
             "-optimizations !code/simplification/advanced,code/simplification/*")) {
       reset();
-      parser.parse(createConfigurationForTesting(ImmutableList.of(option)));
-      verifyParserEndsCleanly();
+      Path proguardConfig = writeTextToTempFile(option);
+      parser.parse(proguardConfig);
+      assertEquals(1, handler.warnings.size());
+      checkDiagnostics(handler.warnings, proguardConfig, 1, 1, "Ignoring", "-optimizations");
     }
   }
 
@@ -1070,15 +1072,35 @@
       ProguardConfigurationParser parser =
           new ProguardConfigurationParser(new DexItemFactory(), reporter);
       parser.parse(proguardConfig);
-      checkDiagnostics(handler.warnings, proguardConfig, 2, 1,
-          "Ignoring", "-optimizationpasses");
-      Position p = handler.warnings.get(0).getPosition();
-      assertTrue(p instanceof TextRange);
-      TextRange r = (TextRange) p;
-      assertEquals(2, r.getStart().getLine());
-      assertEquals(1, r.getStart().getColumn());
-      assertEquals(2, r.getEnd().getLine());
-      assertEquals(22, r.getEnd().getColumn());
+      assertEquals(3, handler.warnings.size());
+
+      checkDiagnostics(handler.warnings, 0, proguardConfig, 1, 1, "Ignoring", "-optimizations");
+      Position p1 = handler.warnings.get(0).getPosition();
+      assertTrue(p1 instanceof TextRange);
+      TextRange r1 = (TextRange) p1;
+      assertEquals(1, r1.getStart().getLine());
+      assertEquals(1, r1.getStart().getColumn());
+      assertEquals(1, r1.getEnd().getLine());
+      assertEquals(15, r1.getEnd().getColumn());
+
+      checkDiagnostics(
+          handler.warnings, 1, proguardConfig, 2, 1, "Ignoring", "-optimizationpasses");
+      Position p2 = handler.warnings.get(1).getPosition();
+      assertTrue(p2 instanceof TextRange);
+      TextRange r2 = (TextRange) p2;
+      assertEquals(2, r2.getStart().getLine());
+      assertEquals(1, r2.getStart().getColumn());
+      assertEquals(2, r2.getEnd().getLine());
+      assertEquals(22, r2.getEnd().getColumn());
+
+      checkDiagnostics(handler.warnings, 2, proguardConfig, 3, 1, "Ignoring", "-optimizations");
+      Position p3 = handler.warnings.get(2).getPosition();
+      assertTrue(p3 instanceof TextRange);
+      TextRange r3 = (TextRange) p3;
+      assertEquals(3, r3.getStart().getLine());
+      assertEquals(1, r3.getStart().getColumn());
+      assertEquals(3, r3.getEnd().getLine());
+      assertEquals(15, r3.getEnd().getColumn());
     }
   }
 
@@ -1113,6 +1135,39 @@
   }
 
   @Test
+  public void parseInvalidKeepOption() throws Exception {
+    Path proguardConfig = writeTextToTempFile(
+        "-keepx public class * {       ",
+        "  native <methods>;           ",
+        "}                             "
+    );
+    try {
+      ProguardConfigurationParser parser =
+          new ProguardConfigurationParser(new DexItemFactory(), reporter);
+      parser.parse(proguardConfig);
+      fail();
+    } catch (AbortException e) {
+      checkDiagnostics(handler.errors, proguardConfig, 1, 1,
+          "Unknown option", "-keepx");
+    }
+  }
+
+  @Test
+  public void parseKeepOptionEOF() throws Exception {
+    Path proguardConfig = writeTextToTempFile(
+        System.lineSeparator(), ImmutableList.of("-keep"), false);
+    try {
+      ProguardConfigurationParser parser =
+          new ProguardConfigurationParser(new DexItemFactory(), reporter);
+      parser.parse(proguardConfig);
+      fail();
+    } catch (AbortException e) {
+      checkDiagnostics(handler.errors, proguardConfig, 1, 6,
+          "Expected [!]interface|@interface|class|enum");
+    }
+  }
+
+  @Test
   public void parseInvalidKeepClassOption() throws Exception {
     Path proguardConfig = writeTextToTempFile(
         "-keepclassx public class * {  ",
@@ -1493,7 +1548,7 @@
         parser.parse(createConfigurationForTesting(ImmutableList.of(option + " class A { *; }")));
         fail("Expect to fail due to testing option being turned off.");
       } catch (AbortException e) {
-        assertEquals(2, handler.errors.size());
+        assertEquals(1, handler.errors.size());
         checkDiagnostics(handler.errors, 0, null, 1, 1, "Unknown option \"" + option + "\"");
       }
     }
diff --git a/src/test/java/com/android/tools/r8/shaking/synthetic/KotlinCollectionDump.java b/src/test/java/com/android/tools/r8/shaking/synthetic/KotlinCollectionDump.java
new file mode 100644
index 0000000..d104272
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/synthetic/KotlinCollectionDump.java
@@ -0,0 +1,113 @@
+// Copyright (c) 2019, 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.shaking.synthetic;
+
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+// Generated by running tools/asmifier.py on the following code snippet:
+//
+//  class Base {
+//    public static void foo() {
+//      System.out.println("Base::foo");
+//    }
+//    static void bar() {
+//      Sub.foo();
+//    }
+//  }
+//
+// then make bar _synthetic_ as `kotlinc` does.
+class Base implements Opcodes {
+
+  public static byte[] dump () throws Exception {
+
+    ClassWriter classWriter = new ClassWriter(0);
+    MethodVisitor methodVisitor;
+
+    classWriter.visit(V1_8, ACC_SUPER, "Base", null, "java/lang/Object", null);
+
+    classWriter.visitSource("Arrays.kt", null);
+
+    {
+      methodVisitor = classWriter.visitMethod(0, "<init>", "()V", null, null);
+      methodVisitor.visitCode();
+      Label label0 = new Label();
+      methodVisitor.visitLabel(label0);
+      methodVisitor.visitLineNumber(3, label0);
+      methodVisitor.visitVarInsn(ALOAD, 0);
+      methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+      methodVisitor.visitInsn(RETURN);
+      methodVisitor.visitMaxs(1, 1);
+      methodVisitor.visitEnd();
+    }
+    {
+      methodVisitor = classWriter.visitMethod(ACC_PUBLIC | ACC_STATIC, "foo", "()V", null, null);
+      methodVisitor.visitCode();
+      Label label0 = new Label();
+      methodVisitor.visitLabel(label0);
+      methodVisitor.visitLineNumber(5, label0);
+      methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+      methodVisitor.visitLdcInsn("Base::foo");
+      methodVisitor.visitMethodInsn(
+          INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
+      Label label1 = new Label();
+      methodVisitor.visitLabel(label1);
+      methodVisitor.visitLineNumber(6, label1);
+      methodVisitor.visitInsn(RETURN);
+      methodVisitor.visitMaxs(2, 0);
+      methodVisitor.visitEnd();
+    }
+    {
+      methodVisitor = classWriter.visitMethod(ACC_STATIC | ACC_SYNTHETIC, "bar", "()V", null, null);
+      methodVisitor.visitCode();
+      Label label0 = new Label();
+      methodVisitor.visitLabel(label0);
+      methodVisitor.visitLineNumber(9, label0);
+      methodVisitor.visitMethodInsn(INVOKESTATIC, "Sub", "foo", "()V", false);
+      Label label1 = new Label();
+      methodVisitor.visitLabel(label1);
+      methodVisitor.visitLineNumber(10, label1);
+      methodVisitor.visitInsn(RETURN);
+      methodVisitor.visitMaxs(0, 0);
+      methodVisitor.visitEnd();
+    }
+    classWriter.visitEnd();
+
+    return classWriter.toByteArray();
+  }
+}
+
+// Generated by running tools/asmifier.py on the following code snippet:
+//  class Sub extends Base {}
+class Sub implements Opcodes {
+
+  public static byte[] dump () throws Exception {
+
+    ClassWriter classWriter = new ClassWriter(0);
+    MethodVisitor methodVisitor;
+
+    classWriter.visit(V1_8, ACC_SUPER, "Sub", null, "Base", null);
+
+    classWriter.visitSource("Arrays.kt", null);
+
+    {
+      methodVisitor = classWriter.visitMethod(0, "<init>", "()V", null, null);
+      methodVisitor.visitCode();
+      Label label0 = new Label();
+      methodVisitor.visitLabel(label0);
+      methodVisitor.visitLineNumber(13, label0);
+      methodVisitor.visitVarInsn(ALOAD, 0);
+      methodVisitor.visitMethodInsn(INVOKESPECIAL, "Base", "<init>", "()V", false);
+      methodVisitor.visitInsn(RETURN);
+      methodVisitor.visitMaxs(1, 1);
+      methodVisitor.visitEnd();
+    }
+    classWriter.visitEnd();
+
+    return classWriter.toByteArray();
+  }
+}
+
diff --git a/src/test/java/com/android/tools/r8/shaking/synthetic/StaticCallInSyntheticMethodAsmTest.java b/src/test/java/com/android/tools/r8/shaking/synthetic/StaticCallInSyntheticMethodAsmTest.java
new file mode 100644
index 0000000..cad2410
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/synthetic/StaticCallInSyntheticMethodAsmTest.java
@@ -0,0 +1,46 @@
+// Copyright (c) 2019, 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.shaking.synthetic;
+
+import com.android.tools.r8.AsmTestBase;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class StaticCallInSyntheticMethodAsmTest extends AsmTestBase {
+  private final Backend backend;
+
+  @Parameterized.Parameters(name = "backend: {0}")
+  public static Backend[] data() {
+    return Backend.values();
+  }
+
+  public StaticCallInSyntheticMethodAsmTest(Backend backend) {
+    this.backend = backend;
+  }
+
+  // class Base {
+  //   static void foo() { ... }
+  //   static synthetic void bar() { Sub.foo(); }
+  // }
+  // class Sub extends Base {}
+  //
+  // As per b/120971047, we do not add synthetic methods to the root set.
+  // When running the above example with -dontshrink, the static call in the synthetic method is not
+  // traced, so no chance to rebind that call to Base#foo. Then, at the end of dex writing, it hits
+  // assertion error in the naming lense that checks if call targets are eligible for renaming.
+  @Test
+  public void test() throws Exception {
+    // TODO(b/122819537): need to trace kotlinc-generated synthetic methods.
+    if (backend == Backend.DEX) {
+      return;
+    }
+    testForR8(backend)
+        .addProgramClassFileData(Base.dump(), Sub.dump())
+        .addKeepRules("-dontshrink")
+        .compile();
+  }
+
+}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java
index c2ced01..8e24088 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentMethodSubject.java
@@ -6,12 +6,18 @@
 
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.naming.MemberNaming.Signature;
 
 public class AbsentMethodSubject extends MethodSubject {
 
   @Override
+  public IRCode buildIR() {
+    throw new Unreachable("Cannot build IR for an absent method");
+  }
+
+  @Override
   public boolean isPresent() {
     return false;
   }
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 924ec2d..ee2155e 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
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.utils.codeinspector;
 
-import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.cf.code.CfArithmeticBinop;
 import com.android.tools.r8.cf.code.CfCheckCast;
@@ -31,7 +30,6 @@
 import com.android.tools.r8.cf.code.CfStackInstruction;
 import com.android.tools.r8.cf.code.CfSwitch;
 import com.android.tools.r8.cf.code.CfThrow;
-import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.ir.code.Monitor.Type;
@@ -167,6 +165,11 @@
   }
 
   @Override
+  public boolean isReturn() {
+    return instruction instanceof CfReturn;
+  }
+
+  @Override
   public boolean isReturnVoid() {
     return instruction instanceof CfReturnVoid;
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
index 1f520ff..9983320 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeInspector.java
@@ -58,7 +58,7 @@
 
 public class CodeInspector {
 
-  private final DexApplication application;
+  final DexApplication application;
   final DexItemFactory dexItemFactory;
   private final ClassNameMapper mapping;
   final Map<String, String> originalToObfuscatedMapping;
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 6d463da..7051226 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
@@ -70,6 +70,7 @@
 import com.android.tools.r8.code.NewInstance;
 import com.android.tools.r8.code.Nop;
 import com.android.tools.r8.code.PackedSwitch;
+import com.android.tools.r8.code.Return;
 import com.android.tools.r8.code.ReturnObject;
 import com.android.tools.r8.code.ReturnVoid;
 import com.android.tools.r8.code.Sget;
@@ -275,6 +276,11 @@
   }
 
   @Override
+  public boolean isReturn() {
+    return instruction instanceof Return;
+  }
+
+  @Override
   public boolean isReturnVoid() {
     return instruction instanceof ReturnVoid;
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java
index d878a86..796532d 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.code.Instruction;
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DexCode;
@@ -17,10 +18,14 @@
 import com.android.tools.r8.graph.DexDebugPositionState;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.GraphLense;
 import com.android.tools.r8.graph.JarCode;
+import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.naming.MemberNaming;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.naming.signature.GenericSignatureParser;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.InternalOptions;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
 import java.util.Arrays;
@@ -41,6 +46,19 @@
   }
 
   @Override
+  public IRCode buildIR() {
+    DexEncodedMethod method = getMethod();
+    return method
+        .getCode()
+        .buildIR(
+            method,
+            new AppInfo(codeInspector.application),
+            GraphLense.getIdentityLense(),
+            new InternalOptions(),
+            Origin.unknown());
+  }
+
+  @Override
   public boolean isPresent() {
     return true;
   }
@@ -202,8 +220,7 @@
       return !code.asCfCode().getLocalVariables().isEmpty();
     }
     if (code.isJarCode()) {
-      return code.asJarCode().getNode().localVariables != null
-          && !code.asJarCode().getNode().localVariables.isEmpty();
+      return code.asJarCode().hasLocalVariableTable();
     }
     throw new Unreachable("Unexpected code type: " + code.getClass().getSimpleName());
   }
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 6abeacf..294cc8c 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
@@ -58,6 +58,8 @@
 
   boolean isIfEqz();
 
+  boolean isReturn();
+
   boolean isReturnVoid();
 
   boolean isReturnObject();
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java
index b0d2c9b..8fb4001 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/MethodSubject.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.utils.codeinspector;
 
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.google.common.collect.Streams;
 import java.util.Iterator;
@@ -13,6 +14,8 @@
 
 public abstract class MethodSubject extends MemberSubject {
 
+  public abstract IRCode buildIR();
+
   public abstract boolean isAbstract();
 
   public abstract boolean isBridge();
diff --git a/tools/apk-masseur.py b/tools/apk_masseur.py
similarity index 85%
rename from tools/apk-masseur.py
rename to tools/apk_masseur.py
index 4f24f0f..6013e6f 100755
--- a/tools/apk-masseur.py
+++ b/tools/apk_masseur.py
@@ -36,10 +36,6 @@
   if len(args) != 1:
     parser.error('Expected <apk> argument, got: ' + ' '.join(args))
   apk = args[0]
-  if not options.out:
-    options.out = os.path.basename(apk)
-  if not options.keystore:
-    options.keystore = findKeystore()
   return (options, apk)
 
 def findKeystore():
@@ -81,28 +77,36 @@
   subprocess.check_call(cmd)
   return signed_apk
 
-def main():
-  (options, apk) = parse_options()
+def masseur(
+    apk, dex=None, out=None, adb_options=None, keystore=None, install=False):
+  if not out:
+    out = os.path.basename(apk)
+  if not keystore:
+    keystore = findKeystore()
   with utils.TempDir() as temp:
     processed_apk = None
-    if options.dex:
-      processed_apk = repack(options.dex, apk, temp)
+    if dex:
+      processed_apk = repack(dex, apk, temp)
     else:
       print 'Signing original APK without modifying dex files'
       processed_apk = os.path.join(temp, 'processed.apk')
       shutil.copyfile(apk, processed_apk)
-    signed_apk = sign(processed_apk, options.keystore, temp)
+    signed_apk = sign(processed_apk, keystore, temp)
     aligned_apk = align(signed_apk, temp)
-    print 'Writing result to', options.out
-    shutil.copyfile(aligned_apk, options.out)
+    print 'Writing result to', out
+    shutil.copyfile(aligned_apk, out)
     adb_cmd = ['adb']
-    if options.adb_options:
+    if adb_options:
       adb_cmd.extend(
-          [option for option in options.adb_options.split(' ') if option])
-    if options.install:
-      adb_cmd.extend(['install', '-t', '-r', '-d', options.out]);
+          [option for option in adb_options.split(' ') if option])
+    if install:
+      adb_cmd.extend(['install', '-t', '-r', '-d', out]);
       utils.PrintCmd(adb_cmd)
       subprocess.check_call(adb_cmd)
+
+def main():
+  (options, apk) = parse_options()
+  masseur(apk, **vars(options))
   return 0
 
 if __name__ == '__main__':
diff --git a/tools/as_utils.py b/tools/as_utils.py
index d5db8a3..65ab470 100644
--- a/tools/as_utils.py
+++ b/tools/as_utils.py
@@ -4,6 +4,7 @@
 # BSD-style license that can be found in the LICENSE file.
 
 from distutils.version import LooseVersion
+from HTMLParser import HTMLParser
 import os
 import shutil
 
@@ -56,7 +57,8 @@
 
 def remove_r8_dependency(checkout_dir):
   build_file = os.path.join(checkout_dir, 'build.gradle')
-  assert os.path.isfile(build_file), 'Expected a file to be present at {}'.format(build_file)
+  assert os.path.isfile(build_file), (
+      'Expected a file to be present at {}'.format(build_file))
   with open(build_file) as f:
     lines = f.readlines()
   with open(build_file, 'w') as f:
@@ -64,6 +66,54 @@
       if (utils.R8_JAR not in line) and (utils.R8LIB_JAR not in line):
         f.write(line)
 
+def IsGradleTaskName(x):
+  # Check that it is non-empty.
+  if not x:
+    return False
+  # Check that there is no whitespace.
+  for c in x:
+    if c.isspace():
+      return False
+  # Check that the first character following an optional ':' is a lower-case
+  # alphabetic character.
+  c = x[0]
+  if c == ':' and len(x) >= 2:
+    c = x[1]
+  return c.isalpha() and c.islower()
+
+def IsGradleCompilerTask(x, shrinker):
+  if 'r8' in shrinker:
+    assert 'transformClassesWithDexBuilderFor' not in x
+    assert 'transformDexArchiveWithDexMergerFor' not in x
+    return 'transformClassesAndResourcesWithR8For' in x
+
+  assert shrinker == 'proguard'
+  return ('transformClassesAndResourcesWithProguard' in x
+      or 'transformClassesWithDexBuilderFor' in x
+      or 'transformDexArchiveWithDexMergerFor' in x)
+
+def SetPrintConfigurationDirective(app, config, checkout_dir, destination):
+  proguard_config_file = FindProguardConfigurationFile(
+      app, config, checkout_dir)
+  with open(proguard_config_file) as f:
+    lines = f.readlines()
+  with open(proguard_config_file, 'w') as f:
+    for line in lines:
+      if '-printconfiguration' not in line:
+        f.write(line)
+    f.write('-printconfiguration {}\n'.format(destination))
+
+def FindProguardConfigurationFile(app, config, checkout_dir):
+  app_module = config.get('app_module', 'app')
+  candidates = ['proguard-rules.pro', 'proguard-rules.txt', 'proguard.cfg']
+  for candidate in candidates:
+    proguard_config_file = os.path.join(checkout_dir, app_module, candidate)
+    if os.path.isfile(proguard_config_file):
+      return proguard_config_file
+  # Currently assuming that the Proguard configuration file can be found at
+  # one of the predefined locations.
+  assert False
+
 def Move(src, dst):
   print('Moving `{}` to `{}`'.format(src, dst))
   dst_parent = os.path.dirname(dst)
@@ -103,3 +153,60 @@
   html_dir = os.path.dirname(html_file)
   for dir_name in ['css', 'js']:
     MoveDir(os.path.join(html_dir, dir_name), os.path.join(dest_dir, dir_name))
+
+def ParseProfileReport(profile_dir):
+  html_file = os.path.join(profile_dir, 'index.html')
+  assert os.path.isfile(html_file)
+
+  parser = ProfileReportParser()
+  with open(html_file) as f:
+    for line in f.readlines():
+      parser.feed(line)
+  return parser.result
+
+# A simple HTML parser that recognizes the following pattern:
+#
+# <tr>
+# <td class="indentPath">:app:transformClassesAndResourcesWithR8ForRelease</td>
+# <td class="numeric">3.490s</td>
+# <td></td>
+# </tr>
+class ProfileReportParser(HTMLParser):
+  entered_table_row = False
+  entered_task_name_cell = False
+  entered_duration_cell = False
+
+  current_task_name = None
+  current_duration = None
+
+  result = {}
+
+  def handle_starttag(self, tag, attrs):
+    entered_table_row_before = self.entered_table_row
+    entered_task_name_cell_before = self.entered_task_name_cell
+
+    self.entered_table_row = (tag == 'tr')
+    self.entered_task_name_cell = (tag == 'td' and entered_table_row_before)
+    self.entered_duration_cell = (
+        self.current_task_name
+            and tag == 'td'
+            and entered_task_name_cell_before)
+
+  def handle_endtag(self, tag):
+    if tag == 'tr':
+      if self.current_task_name and self.current_duration:
+        self.result[self.current_task_name] = self.current_duration
+      self.current_task_name = None
+      self.current_duration = None
+    self.entered_table_row = False
+
+  def handle_data(self, data):
+    stripped = data.strip()
+    if not stripped:
+      return
+    if self.entered_task_name_cell:
+      if IsGradleTaskName(stripped):
+        self.current_task_name = stripped
+    elif self.entered_duration_cell and stripped.endswith('s'):
+      self.current_duration = float(stripped[:-1])
+    self.entered_table_row = False
diff --git a/tools/compare_apk_sizes.py b/tools/compare_apk_sizes.py
index e477320..a8137c2 100755
--- a/tools/compare_apk_sizes.py
+++ b/tools/compare_apk_sizes.py
@@ -23,6 +23,9 @@
   result = optparse.OptionParser(usage=USAGE)
   result.add_option('--temp',
                     help='Temporary directory to store extracted classes in')
+  result.add_option('--use_code_size',
+      help='Use the size of code segments instead of the full size of the dex.',
+      default=False, action='store_true')
   result.add_option('--report',
                     help='Print comparison to this location instead of stdout')
   return result.parse_args()
@@ -50,20 +53,37 @@
   if toolhelper.run('d8', args) is not 0:
     raise Exception('Failed running d8')
 
+def get_code_size(path):
+  segments = toolhelper.run('dexsegments',
+                            [path],
+                            build=False,
+                            return_stdout=True)
+  for line in segments.splitlines():
+    if 'Code' in line:
+      # The code size line looks like:
+      #  - Code: 264 / 4
+      splits = line.split(' ')
+      return int(splits[3])
+  # Some classes has no code.
+  return 0
+
 class FileInfo:
-  def __init__(self, path, root):
+  def __init__(self, path, root, use_code_size):
     self.path = path
     self.full_path = os.path.join(root, path)
-    self.size = os.path.getsize(self.full_path)
+    if use_code_size:
+      self.size = get_code_size(self.full_path)
+    else:
+      self.size = os.path.getsize(self.full_path)
 
-def generate_file_info(path):
+def generate_file_info(path, options):
   file_info_map = {}
   with utils.ChangedWorkingDirectory(path):
     for root, dirs, files in os.walk('.'):
       for f in files:
         assert f.endswith('dex')
         file_path = os.path.join(root, f)
-        entry = FileInfo(file_path, path)
+        entry = FileInfo(file_path, path, use_code_size=options.use_code_size)
         file_info_map[file_path] = entry
   return file_info_map
 
@@ -85,9 +105,9 @@
   output.write('\n\n')
 
 
-def compare(app1_classes_dir, app2_classes_dir, app1, app2, report):
-  app1_files = generate_file_info(app1_classes_dir)
-  app2_files = generate_file_info(app2_classes_dir)
+def compare(app1_classes_dir, app2_classes_dir, app1, app2, options):
+  app1_files = generate_file_info(app1_classes_dir, options)
+  app2_files = generate_file_info(app2_classes_dir, options)
   only_in_app1 = [k for k in app1_files if k not in app2_files]
   only_in_app2 = [k for k in app2_files if k not in app1_files]
   in_both = [k for k in app2_files if k in app1_files]
@@ -105,12 +125,12 @@
       bigger_in_app2[f] = app2_entry.size - app1_entry.size
     else:
       same_size.append(f)
-  output = open(report, 'w') if report else sys.stdout
+  output = open(options.report, 'w') if options.report else sys.stdout
   print_info(app1, app1_files, only_in_app1, bigger_in_app1, output)
   print_info(app2, app2_files, only_in_app2, bigger_in_app2, output)
   output.write('Same size\n')
   output.write('\n'.join(['  %s' % x for x in same_size]))
-  if report:
+  if options.report:
     output.close()
 
 def Main():
@@ -137,7 +157,7 @@
 
     extract_classes(app1_input, app1_classes_dir)
     extract_classes(app2_input, app2_classes_dir)
-    compare(app1_classes_dir, app2_classes_dir, app1, app2, options.report)
+    compare(app1_classes_dir, app2_classes_dir, app1, app2, options)
 
 if __name__ == '__main__':
   sys.exit(Main())
diff --git a/tools/dex2oat.py b/tools/dex2oat.py
index 1e2cffd..e21dc7d 100755
--- a/tools/dex2oat.py
+++ b/tools/dex2oat.py
@@ -14,6 +14,8 @@
 
 VERSIONS = [
   'default',
+  '9.0.0',
+  '8.1.0',
   '7.0.0',
   '6.0.1',
   '5.1.1',
@@ -21,6 +23,8 @@
 
 DIRS = {
   'default': 'art',
+  '9.0.0': 'art-9.0.0',
+  '8.1.0': 'art-8.1.0',
   '7.0.0': 'art-7.0.0',
   '6.0.1': 'art-6.0.1',
   '5.1.1': 'art-5.1.1',
@@ -28,6 +32,8 @@
 
 PRODUCTS = {
   'default': 'angler',
+  '9.0.0': 'marlin',
+  '8.1.0': 'marlin',
   '7.0.0': 'angler',
   '6.0.1': 'angler',
   '5.1.1': 'mako',
@@ -35,11 +41,23 @@
 
 ARCHS = {
   'default': 'arm64',
+  '9.0.0': 'arm64',
+  '8.1.0': 'arm64',
   '7.0.0': 'arm64',
   '6.0.1': 'arm64',
   '5.1.1': 'arm',
 }
 
+VERBOSE_OPTIONS = [
+  'verifier',
+  'compiler',
+  'gc',
+  'jit',
+  'jni',
+  'class',
+  'all',
+]
+
 def ParseOptions():
   parser = optparse.OptionParser()
   parser.add_option('--version',
@@ -53,6 +71,10 @@
   parser.add_option('--output',
                     help='Where to place the output oat (defaults to no output / temp file).',
                     default=None)
+  parser.add_option('--verbose',
+                    help='Enable verbose dex2oat logging.',
+                    choices=VERBOSE_OPTIONS,
+                    default=None)
   return parser.parse_args()
 
 def Main():
@@ -66,12 +88,15 @@
   dexfile = args[0]
   oatfile = options.output
   versions = VERSIONS if options.all else [options.version]
+  verbose = [options.verbose] if options.verbose else []
+  if 'all' in verbose:
+    verbose = [x for x in VERBOSE_OPTIONS if x is not 'all']
   for version in versions:
-    run(dexfile, oatfile, version)
+    run(dexfile, oatfile, version, verbose)
     print
   return 0
 
-def run(dexfile, oatfile=None, version='default'):
+def run(dexfile, oatfile=None, version='default', verbose=[]):
   # dex2oat accepts non-existent dex files, check here instead
   if not os.path.exists(dexfile):
     raise Exception('DEX file not found: "{}"'.format(dexfile))
@@ -90,6 +115,8 @@
       '--oat-file=' + oatfile,
       '--instruction-set=' + arch,
     ]
+    for flag in verbose:
+      cmd += ['--runtime-arg', '-verbose:' + flag]
     env = {"LD_LIBRARY_PATH": os.path.join(base, 'lib')}
     utils.PrintCmd(cmd)
     subprocess.check_call(cmd, env = env)
diff --git a/tools/internal_test.py b/tools/internal_test.py
index be9779a..5bf1da2 100755
--- a/tools/internal_test.py
+++ b/tools/internal_test.py
@@ -67,6 +67,7 @@
 
 def log(str):
   print("%s: %s" % (time.strftime("%c"), str))
+  sys.stdout.flush()
 
 def ParseOptions():
   result = optparse.OptionParser()
@@ -90,6 +91,9 @@
 
 def restart_if_new_version(original_content):
   new_content = get_own_file_content()
+  log('Lengths %s %s' % (len(original_content), len(new_content)))
+  log('is master %s ' % utils.is_master())
+  # Restart if the script got updated.
   if new_content != original_content:
     log('Restarting tools/internal_test.py, content changed')
     os.execv(sys.argv[0], sys.argv)
diff --git a/tools/retrace.py b/tools/retrace.py
index 3b8da1d..1b77e9a 100755
--- a/tools/retrace.py
+++ b/tools/retrace.py
@@ -3,13 +3,58 @@
 # 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.
 
+import archive
+import argparse
 import subprocess
 import sys
+import tempfile
 import utils
 
+def parse_arguments():
+  parser = argparse.ArgumentParser(
+      description = 'R8lib wrapper for retrace tool.')
+  parser.add_argument(
+      '-c',
+      '--commit_hash',
+      help='Commit hash to download r8lib map file for.',
+      default=None)
+  parser.add_argument(
+      '--version',
+      help='Version to download r8lib map file for.',
+      default=None)
+  parser.add_argument(
+      '--map',
+      help='Path to r8lib map.',
+      default=utils.R8LIB_JAR + '.map')
+  parser.add_argument(
+      '--stacktrace',
+      help='Path to stacktrace file.',
+      default=None)
+  return parser.parse_args()
+
+
 def main():
-  # Run the retrace tool with standard r8lib arguments.
-  subprocess.call(['java', '-jar', utils.RETRACE_JAR, '-verbose', utils.R8LIB_JAR + '.map'])
+  args = parse_arguments()
+  r8lib_map_path = args.map
+  hashOrVersion = args.commit_hash or args.version
+  if hashOrVersion:
+    download_path = archive.GetUploadDestination(
+        hashOrVersion,
+        'r8lib.jar.map',
+        args.commit_hash is not None)
+    if utils.file_exists_on_cloud_storage(download_path):
+      r8lib_map_path = tempfile.NamedTemporaryFile().name
+      utils.download_file_from_cloud_storage(download_path, r8lib_map_path)
+    else:
+      print('Could not find map file from argument: %s.' % hashOrVersion)
+      return 1
+
+  retrace_args = ['java', '-jar', utils.RETRACE_JAR, r8lib_map_path]
+  if args.stacktrace:
+    retrace_args.append(args.stacktrace)
+
+  return subprocess.call(retrace_args)
+
 
 if __name__ == '__main__':
   sys.exit(main())
diff --git a/tools/run_on_as_app.py b/tools/run_on_as_app.py
index 7c96506..5dc42ec 100755
--- a/tools/run_on_as_app.py
+++ b/tools/run_on_as_app.py
@@ -3,19 +3,21 @@
 # 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.
 
+import apk_masseur
 import apk_utils
 import gradle
 import os
 import optparse
 import subprocess
 import sys
+import tempfile
 import time
 import utils
 import zipfile
 
 import as_utils
 
-SHRINKERS = ['r8', 'r8full', 'r8-minified', 'r8full-minified', 'proguard']
+SHRINKERS = ['r8', 'r8-minified', 'r8full', 'r8full-minified', 'proguard']
 WORKING_DIR = utils.BUILD
 
 if 'R8_BENCHMARK_DIR' in os.environ and os.path.isdir(os.environ['R8_BENCHMARK_DIR']):
@@ -77,6 +79,14 @@
       'flavor': 'standard',
       'releaseTarget': 'app:assembleRelease',
   },
+  'tivi': {
+      'app_id': 'app.tivi',
+      # Forked from https://github.com/chrisbanes/tivi.git removing
+      # signingConfigs.
+      'git_repo': 'https://github.com/sgjesse/tivi.git',
+      # TODO(123047413): Fails with R8.
+      'skip': True,
+  },
   # This does not build yet.
   'muzei': {
       'git_repo': 'https://github.com/sgjesse/muzei.git',
@@ -128,10 +138,35 @@
   subprocess.check_call(
       ['adb', '-s', emulator_id, 'install', '-r', '-d', apk_dest])
 
+def PercentageDiffAsString(before, after):
+  if after < before:
+    return '-' + str(round((1.0 - after / before) * 100)) + '%'
+  else:
+    return '+' + str(round((after - before) / before * 100)) + '%'
+
+def UninstallApkOnEmulator(app, config):
+  app_id = config.get('app_id')
+  process = subprocess.Popen(
+      ['adb', 'uninstall', app_id],
+      stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+  stdout, stderr = process.communicate()
+
+  if stdout.strip() == 'Success':
+    # Successfully uninstalled
+    return
+
+  if 'Unknown package: {}'.format(app_id) in stderr:
+    # Application not installed
+    return
+
+  raise Exception(
+      'Unexpected result from `adb uninstall {}\nStdout: {}\nStderr: {}'.format(
+          app_id, stdout, stderr))
+
 def WaitForEmulator():
   stdout = subprocess.check_output(['adb', 'devices'])
   if '{}\tdevice'.format(emulator_id) in stdout:
-    return
+    return True
 
   print('Emulator \'{}\' not connected; waiting for connection'.format(
       emulator_id))
@@ -144,9 +179,9 @@
     if '{}\tdevice'.format(emulator_id) not in stdout:
       print('... still waiting for connection')
       if time_waited >= 5 * 60:
-        raise Exception('No emulator connected for 5 minutes')
+        return False
     else:
-      return
+      return True
 
 def GetResultsForApp(app, config, options):
   git_repo = config['git_repo']
@@ -190,34 +225,63 @@
       apk_dest = None
       result = {}
       try:
-        (apk_dest, profile_dest_dir) = BuildAppWithShrinker(
-          app, config, shrinker, checkout_dir, options)
+        (apk_dest, profile_dest_dir, proguard_config_file) = \
+            BuildAppWithShrinker(app, config, shrinker, checkout_dir, options)
         dex_size = ComputeSizeOfDexFilesInApk(apk_dest)
         result['apk_dest'] = apk_dest,
         result['build_status'] = 'success'
         result['dex_size'] = dex_size
         result['profile_dest_dir'] = profile_dest_dir
+
+        profile = as_utils.ParseProfileReport(profile_dest_dir)
+        result['profile'] = {
+            task_name:duration for task_name, duration in profile.iteritems()
+            if as_utils.IsGradleCompilerTask(task_name, shrinker)}
       except Exception as e:
         warn('Failed to build {} with {}'.format(app, shrinker))
         if e:
           print('Error: ' + str(e))
         result['build_status'] = 'failed'
 
-      if options.monkey:
-        if result.get('build_status') == 'success':
+      if result.get('build_status') == 'success':
+        if options.monkey:
           result['monkey_status'] = 'success' if RunMonkey(
-              app, config, apk_dest) else 'failed'
+              app, config, options, apk_dest) else 'failed'
+
+        if 'r8' in shrinker and options.r8_compilation_steps > 1:
+          recompilation_results = []
+          previous_apk = apk_dest
+          for i in range(1, options.r8_compilation_steps):
+            try:
+              recompiled_apk_dest = os.path.join(
+                  checkout_dir, 'out', shrinker, 'app-release-{}.apk'.format(i))
+              RebuildAppWithShrinker(
+                  previous_apk, recompiled_apk_dest, proguard_config_file, shrinker)
+              recompilation_result = {
+                'apk_dest': recompiled_apk_dest,
+                'build_status': 'success',
+                'dex_size': ComputeSizeOfDexFilesInApk(recompiled_apk_dest)
+              }
+              if options.monkey:
+                recompilation_result['monkey_status'] = 'success' if RunMonkey(
+                    app, config, options, recompiled_apk_dest) else 'failed'
+              recompilation_results.append(recompilation_result)
+              previous_apk = recompiled_apk_dest
+            except Exception as e:
+              warn('Failed to recompile {} with {}'.format(app, shrinker))
+              recompilation_results.append({ 'build_status': 'failed' })
+              break
+          result['recompilation_results'] = recompilation_results
 
       result_per_shrinker[shrinker] = result
 
-    if IsTrackedByGit('gradle.properties'):
-      GitCheckout('gradle.properties')
-
   return result_per_shrinker
 
 def BuildAppWithShrinker(app, config, shrinker, checkout_dir, options):
+  print()
   print('Building {} with {}'.format(app, shrinker))
 
+  # Add/remove 'r8.jar' from top-level build.gradle.
   if options.disable_tot:
     as_utils.remove_r8_dependency(checkout_dir)
   else:
@@ -227,23 +291,16 @@
   archives_base_name = config.get(' archives_base_name', app_module)
   flavor = config.get('flavor')
 
-  # Ensure that gradle.properties are not modified before modifying it to
-  # select shrinker.
-  if IsTrackedByGit('gradle.properties'):
-    GitCheckout('gradle.properties')
-  with open("gradle.properties", "a") as gradle_properties:
-    if 'r8' in shrinker:
-      gradle_properties.write('\nandroid.enableR8=true\n')
-      if shrinker == 'r8full' or shrinker == 'r8full-minified':
-        gradle_properties.write('android.enableR8.fullMode=true\n')
-    else:
-      assert shrinker == 'proguard'
-      gradle_properties.write('\nandroid.enableR8=false\n')
-
   out = os.path.join(checkout_dir, 'out', shrinker)
   if not os.path.exists(out):
     os.makedirs(out)
 
+  # Set -printconfiguration in Proguard rules.
+  proguard_config_dest = os.path.abspath(
+      os.path.join(out, 'proguard-rules.pro'))
+  as_utils.SetPrintConfigurationDirective(
+      app, config, checkout_dir, proguard_config_dest)
+
   env = os.environ.copy()
   env['ANDROID_HOME'] = android_home
   env['JAVA_OPTS'] = '-ea'
@@ -252,8 +309,17 @@
     releaseTarget = app_module + ':' + 'assemble' + (
         flavor.capitalize() if flavor else '') + 'Release'
 
-  cmd = ['./gradlew', '--no-daemon', 'clean', releaseTarget, '--profile',
-      '--stacktrace']
+  # Value for property android.enableR8.
+  enableR8 = 'r8' in shrinker
+  # Value for property android.enableR8.fullMode.
+  enableR8FullMode = shrinker == 'r8full' or shrinker == 'r8full-minified'
+  # Build gradlew command line.
+  cmd = ['./gradlew', '--no-daemon', 'clean', releaseTarget,
+         '--profile', '--stacktrace',
+         '-Pandroid.enableR8=' + str(enableR8).lower(),
+         '-Pandroid.enableR8.fullMode=' + str(enableR8FullMode).lower()]
+  if options.gradle_flags:
+    cmd.extend(options.gradle_flags.split(' '))
 
   utils.PrintCmd(cmd)
   build_process = subprocess.Popen(cmd, env=env, stdout=subprocess.PIPE)
@@ -307,16 +373,43 @@
   profile_dest_dir = os.path.join(out, 'profile')
   as_utils.MoveProfileReportTo(profile_dest_dir, stdout)
 
-  return (apk_dest, profile_dest_dir)
+  return (apk_dest, profile_dest_dir, proguard_config_dest)
 
-def RunMonkey(app, config, apk_dest):
-  WaitForEmulator()
+def RebuildAppWithShrinker(apk, apk_dest, proguard_config_file, shrinker):
+  assert 'r8' in shrinker
+  assert apk_dest.endswith('.apk')
+
+  # Compile given APK with shrinker to temporary zip file.
+  api = 28 # TODO(christofferqa): Should be the one from build.gradle
+  android_jar = os.path.join(utils.REPO_ROOT, utils.ANDROID_JAR.format(api=api))
+  r8_jar = utils.R8LIB_JAR if IsMinifiedR8(shrinker) else utils.R8_JAR
+  zip_dest = apk_dest[:-3] + '.zip'
+
+  cmd = ['java', '-ea', '-jar', r8_jar, '--release', '--pg-conf',
+      proguard_config_file, '--lib', android_jar, '--output', zip_dest, apk]
+  utils.PrintCmd(cmd)
+
+  subprocess.check_output(cmd)
+
+  # Make a copy of the given APK, move the newly generated dex files into the
+  # copied APK, and then sign the APK.
+  apk_masseur.masseur(apk, dex=zip_dest, out=apk_dest)
+
+def RunMonkey(app, config, options, apk_dest):
+  if not WaitForEmulator():
+    return False
+
+  UninstallApkOnEmulator(app, config)
   InstallApkOnEmulator(apk_dest)
 
   app_id = config.get('app_id')
-  number_of_events_to_generate = 50
+  number_of_events_to_generate = options.monkey_events
 
-  cmd = ['adb', 'shell', 'monkey', '-p', app_id,
+  # Intentionally using a constant seed such that the monkey generates the same
+  # event sequence for each shrinker.
+  random_seed = 42
+
+  cmd = ['adb', 'shell', 'monkey', '-p', app_id, '-s', str(random_seed),
       str(number_of_events_to_generate)]
   utils.PrintCmd(cmd)
 
@@ -336,8 +429,10 @@
       print('  skipped ({})'.format(error_message))
       continue
 
-    baseline = float(
-        result_per_shrinker.get('proguard', {}).get('dex_size', -1))
+    proguard_result = result_per_shrinker.get('proguard', {})
+    proguard_dex_size = float(proguard_result.get('dex_size', -1))
+    proguard_duration = sum(proguard_result.get('profile', {}).values())
+
     for shrinker in SHRINKERS:
       if shrinker not in result_per_shrinker:
         continue
@@ -348,23 +443,50 @@
       else:
         print('  {}:'.format(shrinker))
         dex_size = result.get('dex_size')
-        if dex_size != baseline and baseline >= 0:
-          if dex_size < baseline:
-            success('    dex size: {} ({}, -{}%)'.format(
-              dex_size, dex_size - baseline,
-              round((1.0 - dex_size / baseline) * 100), 1))
-          elif dex_size >= baseline:
-            warn('    dex size: {} ({}, +{}%)'.format(
-              dex_size, dex_size - baseline,
-              round((baseline - dex_size) / dex_size * 100, 1)))
+        msg = '    dex size: {}'.format(dex_size)
+        if dex_size != proguard_dex_size and proguard_dex_size >= 0:
+          msg = '{} ({}, {})'.format(
+              msg, dex_size - proguard_dex_size,
+              PercentageDiffAsString(proguard_dex_size, dex_size))
+          success(msg) if dex_size < proguard_dex_size else warn(msg)
         else:
-          print('    dex size: {}'.format(dex_size))
+          print(msg)
+
+        profile = result.get('profile')
+        duration = sum(profile.values())
+        msg = '    performance: {}s'.format(duration)
+        if duration != proguard_duration and proguard_duration > 0:
+          msg = '{} ({}s, {})'.format(
+              msg, duration - proguard_duration,
+              PercentageDiffAsString(proguard_duration, duration))
+          success(msg) if duration < proguard_duration else warn(msg)
+        else:
+          print(msg)
+        if len(profile) >= 2:
+          for task_name, task_duration in profile.iteritems():
+            print('      {}: {}s'.format(task_name, task_duration))
+
         if options.monkey:
           monkey_status = result.get('monkey_status')
           if monkey_status != 'success':
             warn('    monkey: {}'.format(monkey_status))
           else:
             success('    monkey: {}'.format(monkey_status))
+        recompilation_results = result.get('recompilation_results', [])
+        i = 1
+        for recompilation_result in recompilation_results:
+          build_status = recompilation_result.get('build_status')
+          if build_status != 'success':
+            print('    recompilation #{}: {}'.format(i, build_status))
+          else:
+            dex_size = recompilation_result.get('dex_size')
+            print('    recompilation #{}'.format(i))
+            print('      dex size: {}'.format(dex_size))
+            if options.monkey:
+              monkey_status = recompilation_result.get('monkey_status')
+              msg = '      monkey: {}'.format(monkey_status)
+              success(msg) if monkey_status == 'success' else warn(msg)
+          i += 1
 
 def ParseOptions(argv):
   result = optparse.OptionParser()
@@ -375,6 +497,10 @@
                     help='Whether to install and run app(s) with monkey',
                     default=False,
                     action='store_true')
+  result.add_option('--monkey_events',
+                    help='Number of events that the monkey should trigger',
+                    default=250,
+                    type=int)
   result.add_option('--pull',
                     help='Whether to pull the latest version of each app',
                     default=False,
@@ -386,6 +512,10 @@
   result.add_option('--shrinker',
                     help='The shrinkers to use (by default, all are run)',
                     action='append')
+  result.add_option('--r8-compilation-steps', '--r8_compilation_steps',
+                    help='Number of times R8 should be run on each app',
+                    default=2,
+                    type=int)
   result.add_option('--disable-tot', '--disable_tot',
                     help='Whether to disable the use of the ToT version of R8',
                     default=False,
@@ -394,7 +524,12 @@
                     help='Run without building ToT first (only when using ToT)',
                     default=False,
                     action='store_true')
+  result.add_option('--gradle-flags', '--gradle_flags',
+                    help='Flags to pass in to gradle')
   (options, args) = result.parse_args(argv)
+  if options.disable_tot:
+    # r8.jar is required for recompiling the generated APK
+    options.r8_compilation_steps = 1
   if options.shrinker:
     for shrinker in options.shrinker:
       assert shrinker in SHRINKERS
diff --git a/tools/toolhelper.py b/tools/toolhelper.py
index 86787be..d7cf222 100644
--- a/tools/toolhelper.py
+++ b/tools/toolhelper.py
@@ -10,7 +10,7 @@
 
 def run(tool, args, build=None, debug=True,
         profile=False, track_memory_file=None, extra_args=None,
-        stderr=None, stdout=None):
+        stderr=None, stdout=None, return_stdout=False):
   if build is None:
     build, args = extract_build_from_args(args)
   if build:
@@ -36,6 +36,8 @@
     cmd.extend(["--lib", lib])
   cmd.extend(args)
   utils.PrintCmd(cmd)
+  if return_stdout:
+    return subprocess.check_output(cmd)
   return subprocess.call(cmd, stdout=stdout, stderr=stderr)
 
 def run_in_tests(tool, args, build=None, debug=True, extra_args=None):
diff --git a/tools/utils.py b/tools/utils.py
index bd54dbc..f8b4469 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -87,6 +87,11 @@
       sha1.update(chunk)
   return sha1.hexdigest()
 
+def is_master():
+  remotes = subprocess.check_output(['git', 'branch', '-r', '--contains',
+                                     'HEAD'])
+  return 'origin/master' in remotes
+
 def get_HEAD_sha1():
   cmd = ['git', 'rev-parse', 'HEAD']
   PrintCmd(cmd)