Merge "Enable running tests on r8lib with dependencies"
diff --git a/build.gradle b/build.gradle
index bb36ac5..5bd5c9e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1927,6 +1927,7 @@
 
 task copyMavenDeps(type: Copy) {
     from configurations.compile into "$buildDir/deps"
+    from configurations.compileClasspath into "$buildDir/deps"
     from configurations.testCompile into "$buildDir/deps"
 }
 
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 3cf4664..228c0e5 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -445,9 +445,6 @@
         appViewWithLiveness.setAppInfo(new SwitchMapCollector(appViewWithLiveness, options).run());
         appViewWithLiveness.setAppInfo(
             new EnumOrdinalMapCollector(appViewWithLiveness, options).run());
-
-        // TODO(b/79143143): re-enable once fixed.
-        // graphLense = new BridgeMethodAnalysis(graphLense, appInfo.withLiveness()).run();
       }
 
       timing.begin("Create IR");
diff --git a/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java b/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
index e4f1c8c..012c4a4 100644
--- a/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
@@ -7,6 +7,7 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.ProgramResourceProvider;
+import com.android.tools.r8.graph.LazyLoadedDexApplication.AllClasses;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.utils.ProgramClassCollection;
 import com.android.tools.r8.utils.Timing;
@@ -21,9 +22,11 @@
 
 public class DirectMappedDexApplication extends DexApplication {
 
+  private final AllClasses allClasses;
   private final ImmutableMap<DexType, DexLibraryClass> libraryClasses;
 
   private DirectMappedDexApplication(ClassNameMapper proguardMap,
+      AllClasses allClasses,
       ProgramClassCollection programClasses,
       ImmutableList<ProgramResourceProvider> programResourceProviders,
       ImmutableMap<DexType, DexLibraryClass> libraryClasses,
@@ -32,6 +35,7 @@
       Timing timing) {
     super(proguardMap, programClasses, programResourceProviders, mainDexList, deadCode,
         dexItemFactory, highestSortingString, timing);
+    this.allClasses = allClasses;
     this.libraryClasses = libraryClasses;
   }
 
@@ -92,17 +96,21 @@
 
   public static class Builder extends DexApplication.Builder<Builder> {
 
+    private final AllClasses allClasses;
     private final List<DexLibraryClass> libraryClasses = new ArrayList<>();
 
     Builder(LazyLoadedDexApplication application) {
       super(application);
       // As a side-effect, this will force-load all classes.
-      Map<DexType, DexClass> allClasses = application.getFullClassMap();
+      this.allClasses = application.loadAllClasses();
+      Map<DexType, DexClass> allClasses = this.allClasses.getClasses();
+      // TODO(120884788): This filter will only add library classes which are not program classes.
       Iterables.filter(allClasses.values(), DexLibraryClass.class).forEach(libraryClasses::add);
     }
 
     private Builder(DirectMappedDexApplication application) {
       super(application);
+      this.allClasses = application.allClasses;
       this.libraryClasses.addAll(application.libraryClasses.values());
     }
 
@@ -116,6 +124,7 @@
       // Rebuild the map. This will fail if keys are not unique.
       return new DirectMappedDexApplication(
           proguardMap,
+          allClasses,
           ProgramClassCollection.create(
               programClasses, ProgramClassCollection::resolveClassConflictImpl),
           ImmutableList.copyOf(programResourceProviders),
diff --git a/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java b/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java
index b648e41..6dea825 100644
--- a/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java
@@ -55,31 +55,67 @@
     return clazz;
   }
 
-  private Map<DexType, DexClass> forceLoadAllClasses() {
-    Map<DexType, DexClass> loaded = new IdentityHashMap<>();
+  static class AllClasses {
+    private Map<DexType, DexClass> libraryClasses;
+    private Map<DexType, DexClass> classpathClasses;
+    private Map<DexType, DexClass> programClasses;
+    private Map<DexType, DexClass> classes;
 
-    // Program classes are supposed to be loaded, but force-loading them is no-op.
-    programClasses.forceLoad(type -> true);
-    programClasses.getAllClasses().forEach(clazz -> loaded.put(clazz.type, clazz));
+    AllClasses(
+        LibraryClassCollection libraryClasses,
+        ClasspathClassCollection classpathClasses,
+        ProgramClassCollection programClasses) {
+      load(libraryClasses, classpathClasses, programClasses);
 
-    if (classpathClasses != null) {
-      classpathClasses.forceLoad(type -> !loaded.containsKey(type));
-      classpathClasses.getAllClasses().forEach(clazz -> loaded.putIfAbsent(clazz.type, clazz));
+      // Collect loaded classes in the precedence order program classes, class path classes and
+      // library classes.
+      // TODO(b/120884788): Change this.
+      classes = new IdentityHashMap<>();
+      classes.putAll(this.programClasses);
+      if (classpathClasses != null) {
+        classpathClasses.getAllClasses().forEach(clazz -> classes.putIfAbsent(clazz.type, clazz));
+      }
+      if (libraryClasses != null) {
+        libraryClasses.getAllClasses().forEach(clazz -> classes.putIfAbsent(clazz.type, clazz));
+      }
     }
 
-    if (libraryClasses != null) {
-      libraryClasses.forceLoad(type -> !loaded.containsKey(type));
-      libraryClasses.getAllClasses().forEach(clazz -> loaded.putIfAbsent(clazz.type, clazz));
+    public Map<DexType, DexClass> getLibraryClasses() {
+      return libraryClasses;
     }
 
-    return loaded;
+    public Map<DexType, DexClass> getClasspathClasses() {
+      return classpathClasses;
+    }
+
+    public Map<DexType, DexClass> getClasses() {
+      return classes;
+    }
+
+    private void load(
+        LibraryClassCollection libraryClasses,
+        ClasspathClassCollection classpathClasses,
+        ProgramClassCollection programClasses) {
+      if (libraryClasses != null) {
+        libraryClasses.forceLoad(type -> true);
+        this.libraryClasses = libraryClasses.getAllClassesInMap();
+      }
+      if (classpathClasses != null) {
+        classpathClasses.forceLoad(type -> true);
+        this.classpathClasses = classpathClasses.getAllClassesInMap();
+      }
+      assert programClasses != null;
+      // Program classes are supposed to be loaded, but force-loading them is no-op.
+      programClasses.forceLoad(type -> true);
+      this.programClasses = programClasses.getAllClassesInMap();
+    }
   }
 
   /**
    * Force load all classes and return type -> class map containing all the classes.
    */
-  public Map<DexType, DexClass> getFullClassMap() {
-    return forceLoadAllClasses();
+  public AllClasses loadAllClasses() {
+    return new AllClasses(libraryClasses, classpathClasses, programClasses);
   }
 
   public static class Builder extends DexApplication.Builder<Builder> {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/ReferenceTypeLatticeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/ReferenceTypeLatticeElement.java
index e7282e1..0db0ca4 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/ReferenceTypeLatticeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/ReferenceTypeLatticeElement.java
@@ -43,13 +43,13 @@
   }
 
   @Override
-  public boolean isNull() {
+  public boolean isNullType() {
     return type == DexItemFactory.nullValueType;
   }
 
   @Override
   public TypeLatticeElement asNullable() {
-    assert isNull();
+    assert isNullType();
     return this;
   }
 
@@ -88,7 +88,7 @@
 
   @Override
   public int hashCode() {
-    assert isNull();
+    assert isNullType();
     return System.identityHashCode(this);
   }
 }
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 b29cd31..ad8edd3 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
@@ -29,7 +29,6 @@
   public static final ReferenceTypeLatticeElement NULL =
       ReferenceTypeLatticeElement.getNullTypeLatticeElement();
 
-
   // TODO(b/72693244): Switch to NullLatticeElement.
   private final boolean isNullable;
 
@@ -42,7 +41,7 @@
   }
 
   public NullLatticeElement nullElement() {
-    if (isNull()) {
+    if (isNullType()) {
       return NullLatticeElement.definitelyNull();
     }
     if (!isNullable()) {
@@ -91,10 +90,10 @@
     if (isTop() || other.isTop()) {
       return TOP;
     }
-    if (isNull()) {
+    if (isNullType()) {
       return other.asNullable();
     }
-    if (other.isNull()) {
+    if (other.isNullType()) {
       return asNullable();
     }
     if (isPrimitive()) {
@@ -270,7 +269,7 @@
   public boolean isPreciseType() {
     return isArrayType()
         || isClassType()
-        || isNull()
+        || isNullType()
         || isInt()
         || isFloat()
         || isLong()
@@ -286,21 +285,13 @@
   }
 
   /**
-   * Should use {@link #isConstantNull()} or {@link #isDefinitelyNull()} instead.
-   */
-  @Deprecated
-  public boolean isNull() {
-    return false;
-  }
-
-  /**
    * Determines if this type only includes null values that are defined by a const-number
    * instruction in the same enclosing method.
    *
    * These null values can be assigned to any type.
    */
-  public boolean isConstantNull() {
-    return isNull();
+  public boolean isNullType() {
+    return false;
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
index c68baf9..596fdb8 100644
--- a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
+++ b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
@@ -148,7 +148,7 @@
       assert inType.nullElement().lessThanOrEqual(outType.nullElement());
 
       // Since we cannot remove the cast the in-value must be different from null.
-      assert !inType.isNull();
+      assert !inType.isNullType();
 
       // TODO(b/72693244): Consider checking equivalence. This requires that the types are always
       // as precise as possible, though, meaning that almost all changes to the IR must be followed
diff --git a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
index 7a6a9a4..3976889 100644
--- a/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
+++ b/src/main/java/com/android/tools/r8/ir/code/ConstNumber.java
@@ -302,7 +302,7 @@
     assert super.verifyTypes(appInfo, graphLense);
     assert !isZero()
         || outValue().getTypeLattice().isPrimitive()
-        || outValue().getTypeLattice().isConstantNull();
+        || outValue().getTypeLattice().isNullType();
     return true;
   }
 
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 cde197c..48b6c9b 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,8 +11,10 @@
 import java.util.ListIterator;
 import java.util.function.Predicate;
 
-public interface InstructionListIterator extends ListIterator<Instruction>,
-    NextUntilIterator<Instruction> {
+public interface InstructionListIterator
+    extends ListIterator<Instruction>,
+        NextUntilIterator<Instruction>,
+        PreviousUntilIterator<Instruction> {
 
   /**
    * Peek the previous instruction.
diff --git a/src/main/java/com/android/tools/r8/ir/code/PreviousUntilIterator.java b/src/main/java/com/android/tools/r8/ir/code/PreviousUntilIterator.java
new file mode 100644
index 0000000..4d0f790
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/code/PreviousUntilIterator.java
@@ -0,0 +1,26 @@
+// 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.code;
+
+import java.util.ListIterator;
+import java.util.function.Predicate;
+
+public interface PreviousUntilIterator<T> extends ListIterator<T> {
+
+  /**
+   * Continue to call {@link #previous} while {@code predicate} tests {@code false}.
+   *
+   * @returns the item that matched the predicate or {@code null} if all items fail the predicate
+   *     test
+   */
+  default T previousUntil(Predicate<T> predicate) {
+    while (hasPrevious()) {
+      T item = previous();
+      if (predicate.test(item)) {
+        return item;
+      }
+    }
+    return null;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/code/Throw.java b/src/main/java/com/android/tools/r8/ir/code/Throw.java
index c9b12f2..23bb76c 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Throw.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Throw.java
@@ -82,7 +82,7 @@
       return true;
     }
     TypeLatticeElement exceptionType = exception().getTypeLattice();
-    if (exceptionType.isConstantNull()) {
+    if (exceptionType.isNullType()) {
       // throw null
       return true;
     }
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 72e2887..8c16d04 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
@@ -5,7 +5,7 @@
 
 import static com.android.tools.r8.ir.desugar.InterfaceMethodRewriter.Flavor.ExcludeDexResources;
 import static com.android.tools.r8.ir.desugar.InterfaceMethodRewriter.Flavor.IncludeAllResources;
-import static com.android.tools.r8.ir.optimize.CodeRewriter.checksNullReceiverBeforeSideEffect;
+import static com.android.tools.r8.ir.optimize.CodeRewriter.checksNullBeforeSideEffect;
 
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfo;
@@ -35,7 +35,6 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
-import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeStatic;
 import com.android.tools.r8.ir.code.NumericType;
 import com.android.tools.r8.ir.code.Value;
@@ -880,10 +879,6 @@
     printC1VisualizerHeader(method);
     String previous = printMethod(code, "Initial IR (SSA)", null);
 
-    if (method.getCode() != null && method.getCode().isJarCode()) {
-      computeKotlinNonNullParamHints(feedback, method, code);
-    }
-
     if (options.canHaveArtStringNewInitBug()) {
       CodeRewriter.ensureDirectStringNewToInit(code);
     }
@@ -1110,11 +1105,10 @@
 
     codeRewriter.identifyReturnsArgument(method, code, feedback);
     if (options.enableInlining && inliner != null) {
-      codeRewriter.identifyInvokeSemanticsForInlining(method, code, feedback);
+      codeRewriter.identifyInvokeSemanticsForInlining(method, code, graphLense(), feedback);
     }
 
-    // If hints from Kotlin metadata or use of Kotlin Intrinsics were not available, track usage of
-    // parameters and compute their nullability and possibility of NPE.
+    // Track usage of parameters and compute their nullability and possibility of NPE.
     if (method.getOptimizationInfo().getNonNullParamOrThrow() == null) {
       computeNonNullParamHints(feedback, method, code);
     }
@@ -1159,33 +1153,25 @@
     finalizeIR(method, code, feedback);
   }
 
-  private void computeKotlinNonNullParamHints(
-      OptimizationFeedback feedback, DexEncodedMethod method, IRCode code) {
+  private void computeNonNullParamHints(
+    OptimizationFeedback feedback, DexEncodedMethod method, IRCode code) {
     List<Value> arguments = code.collectArguments(true);
     BitSet paramsCheckedForNull = new BitSet();
-    DexMethod checkParameterIsNotNull =
-        code.options.itemFactory.kotlin.intrinsics.checkParameterIsNotNull;
     for (int index = 0; index < arguments.size(); index++) {
       Value argument = arguments.get(index);
-      for (Instruction user : argument.uniqueUsers()) {
-        // To enforce parameter non-null requirement Kotlin uses intrinsic:
-        //    kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull(param, message)
-        //
-        // with the following we simply look if the parameter is ever passed
-        // to the mentioned intrinsic as the first argument. We do it only for
-        // code coming from Java classfile, since after the method is rewritten
-        // by R8 this call gets inlined.
-        if (!user.isInvokeStatic()) {
-          continue;
-        }
-        InvokeMethod invoke = user.asInvokeMethod();
-        DexMethod invokedMethod =
-            graphLense().getOriginalMethodSignature(invoke.getInvokedMethod());
-        // TODO(b/121377154): Make sure there is no other side-effect before argument's null check.
-        // E.g., is this the first method invocation inside the method?
-        if (invokedMethod == checkParameterIsNotNull && user.inValues().indexOf(argument) == 0) {
-          paramsCheckedForNull.set(index);
-        }
+      // This handles cases where the parameter is checked via Kotlin Intrinsics:
+      //
+      //   kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull(param, message)
+      //
+      // or its inlined version:
+      //
+      //   if (param != null) return;
+      //   invoke-static throwParameterIsNullException(msg)
+      //
+      // or some other variants, e.g., throw null or NPE after the direct null check.
+      if (argument.isUsed()
+          && checksNullBeforeSideEffect(code, appInfo, graphLense(), argument)) {
+        paramsCheckedForNull.set(index);
       }
     }
     if (paramsCheckedForNull.length() > 0) {
@@ -1213,28 +1199,6 @@
     }
   }
 
-  // TODO(b/121377154): Consider merging compute(Kotlin)?NonNullParamHints into one.
-  private void computeNonNullParamHints(
-    OptimizationFeedback feedback, DexEncodedMethod method, IRCode code) {
-    List<Value> arguments = code.collectArguments(true);
-    BitSet paramsCheckedForNull = new BitSet();
-    for (int index = 0; index < arguments.size(); index++) {
-      Value argument = arguments.get(index);
-      if (argument.isUsed()
-          && checksNullReceiverBeforeSideEffect(code, appInfo.dexItemFactory, argument)) {
-        paramsCheckedForNull.set(index);
-      }
-      // The above one only catches something like:
-      //   if (param != null) return;
-      //   invoke-static throwParameterIsNullException(msg)
-      // This is good enough to handle checkParameterIsNotNull(param, msg), which is generated by
-      // kotlinc for arguments that are not nullable.
-    }
-    if (paramsCheckedForNull.length() > 0) {
-      feedback.setNonNullParamOrThrow(method, paramsCheckedForNull);
-    }
-  }
-
   private void finalizeIR(DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
     code.traceBlocks();
     if (options.isGeneratingClassFiles()) {
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 2244653..40a7692 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
@@ -38,6 +38,7 @@
 import com.android.tools.r8.graph.DexValue.DexValueNull;
 import com.android.tools.r8.graph.DexValue.DexValueShort;
 import com.android.tools.r8.graph.DexValue.DexValueString;
+import com.android.tools.r8.graph.GraphLense;
 import com.android.tools.r8.graph.ParameterUsagesInfo;
 import com.android.tools.r8.graph.ParameterUsagesInfo.ParameterUsage;
 import com.android.tools.r8.graph.ParameterUsagesInfo.ParameterUsageBuilder;
@@ -1003,7 +1004,7 @@
   }
 
   public void identifyInvokeSemanticsForInlining(
-      DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
+      DexEncodedMethod method, IRCode code, GraphLense graphLense, OptimizationFeedback feedback) {
     if (method.isStatic()) {
       // Identifies if the method preserves class initialization after inlining.
       feedback.markTriggerClassInitBeforeAnySideEffect(method,
@@ -1013,7 +1014,7 @@
       final Value receiver = code.getThis();
       feedback.markCheckNullReceiverBeforeAnySideEffect(method,
           receiver.isUsed()
-              && checksNullReceiverBeforeSideEffect(code, appInfo.dexItemFactory, receiver));
+              && checksNullBeforeSideEffect(code, appInfo, graphLense, receiver));
     }
   }
 
@@ -1342,55 +1343,66 @@
   }
 
   /**
-   * Returns true if the given code unconditionally throws if receiver is null before any other
-   * side effect instruction.
+   * Returns true if the given code unconditionally throws if value is null before any other side
+   * effect instruction.
    *
    * Note: we do not track phis so we may return false negative. This is a conservative approach.
    */
-  public static boolean checksNullReceiverBeforeSideEffect(
-      IRCode code, DexItemFactory factory, Value receiver) {
-    Wrapper<DexMethod> throwParamIsNullException =
-        MethodSignatureEquivalence.get()
-            .wrap(factory.kotlin.intrinsics.throwParameterIsNullException);
+  public static boolean checksNullBeforeSideEffect(
+      IRCode code, AppInfo appInfo, GraphLense graphLense, Value value) {
     return alwaysTriggerExpectedEffectBeforeAnythingElse(
         code,
         (instr, it) -> {
           BasicBlock currentBlock = instr.getBlock();
-          // If the code explicitly checks the nullability of receiver, we should visit the next
-          // block that corresponds to the null receiver where NPE semantic could be preserved.
-          if (!currentBlock.hasCatchHandlers() && isNullCheck(instr, receiver)) {
+          // If the code explicitly checks the nullability of the value, we should visit the next
+          // block that corresponds to the null value where NPE semantic could be preserved.
+          if (!currentBlock.hasCatchHandlers() && isNullCheck(instr, value)) {
             return InstructionEffect.CONDITIONAL_EFFECT;
           }
-          // Kotlin specific way of throwing NPE: throwParameterIsNullException.
-          // Similarly, combined with the above CONDITIONAL_EFFECT, the code checks on NPE on
-          // receiver.
-          if (instr.isInvokeStatic()) {
-            DexMethod method = instr.asInvokeStatic().getInvokedMethod();
-            if (MethodSignatureEquivalence.get().wrap(method).equals(throwParamIsNullException)) {
+          if (isKotlinNullCheck(appInfo, graphLense, instr, value)) {
+            DexMethod invokedMethod = instr.asInvokeStatic().getInvokedMethod();
+            // Kotlin specific way of throwing NPE: throwParameterIsNullException.
+            // Similarly, combined with the above CONDITIONAL_EFFECT, the code checks on NPE on
+            // the value.
+            if (invokedMethod.name
+                == appInfo.dexItemFactory.kotlin.intrinsics.throwParameterIsNullException.name) {
               // We found a NPE (or similar exception) throwing code.
-              // Combined with the above CONDITIONAL_EFFECT, the code checks NPE on receiver.
+              // Combined with the above CONDITIONAL_EFFECT, the code checks NPE on the value.
               for (BasicBlock predecessor : currentBlock.getPredecessors()) {
                 Instruction last =
                     predecessor.listIterator(predecessor.getInstructions().size()).previous();
-                if (isNullCheck(last, receiver)) {
+                if (isNullCheck(last, value)) {
                   return InstructionEffect.DESIRED_EFFECT;
                 }
               }
+              // Hitting here means that this call might be used for other parameters. If we don't
+              // bail out, it will be regarded as side effects for the current value.
+              return InstructionEffect.NO_EFFECT;
+            } else {
+              // Kotlin specific way of checking parameter nullness: checkParameterIsNotNull.
+              assert invokedMethod.name
+                  == appInfo.dexItemFactory.kotlin.intrinsics.checkParameterIsNotNull.name;
+              return InstructionEffect.DESIRED_EFFECT;
             }
           }
-          if (isInstantiationOfNullPointerException(instr, it, factory)) {
+          if (isInstantiationOfNullPointerException(instr, it, appInfo.dexItemFactory)) {
             it.next(); // Skip call to NullPointerException.<init>.
             return InstructionEffect.NO_EFFECT;
-          } else if (instr.throwsNpeIfValueIsNull(receiver, factory)) {
+          } else if (instr.throwsNpeIfValueIsNull(value, appInfo.dexItemFactory)) {
             // In order to preserve NPE semantic, the exception must not be caught by any handler.
             // Therefore, we must ignore this instruction if it is covered by a catch handler.
             // Note: this is a conservative approach where we consider that any catch handler could
             // catch the exception, even if it cannot catch a NullPointerException.
             if (!currentBlock.hasCatchHandlers()) {
-              // We found a NPE check on receiver.
+              // We found a NPE check on the value.
               return InstructionEffect.DESIRED_EFFECT;
             }
           } else if (instructionHasSideEffects(instr)) {
+            // If the current instruction is const-string, this could load the parameter name.
+            // Just make sure it is indeed not throwing.
+            if (instr.isConstString() && !instr.instructionInstanceCanThrow()) {
+              return InstructionEffect.NO_EFFECT;
+            }
             // We found a side effect before a NPE check.
             return InstructionEffect.OTHER_EFFECT;
           }
@@ -1398,9 +1410,36 @@
         });
   }
 
-  private static boolean isNullCheck(Instruction instr, Value receiver) {
+  // Note that this method may have false positives, since the application could in principle
+  // declare a method called checkParameterIsNotNull(parameter, message) or
+  // throwParameterIsNullException(parameterName) in a package that starts with "kotlin".
+  private static boolean isKotlinNullCheck(
+      AppInfo appInfo, GraphLense graphLense, Instruction instr, Value value) {
+    if (!instr.isInvokeStatic()) {
+      return false;
+    }
+    // We need to strip the holder, since Kotlin adds different versions of null-check machinery,
+    // e.g., kotlin.collections.ArraysKt___ArraysKt... or kotlin.jvm.internal.ArrayIteratorKt...
+    MethodSignatureEquivalence wrapper = MethodSignatureEquivalence.get();
+    Wrapper<DexMethod> checkParameterIsNotNull =
+        wrapper.wrap(appInfo.dexItemFactory.kotlin.intrinsics.checkParameterIsNotNull);
+    Wrapper<DexMethod> throwParamIsNullException =
+        wrapper.wrap(appInfo.dexItemFactory.kotlin.intrinsics.throwParameterIsNullException);
+    DexMethod invokedMethod =
+        graphLense.getOriginalMethodSignature(instr.asInvokeStatic().getInvokedMethod());
+    Wrapper<DexMethod> methodWrap = wrapper.wrap(invokedMethod);
+    if (methodWrap.equals(throwParamIsNullException)
+        || (methodWrap.equals(checkParameterIsNotNull) && instr.inValues().get(0).equals(value))) {
+      if (invokedMethod.getHolder().getPackageDescriptor().startsWith("kotlin")) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private static boolean isNullCheck(Instruction instr, Value value) {
     return instr.isIf() && instr.asIf().isZeroTest()
-        && instr.inValues().get(0).equals(receiver)
+        && instr.inValues().get(0).equals(value)
         && (instr.asIf().getType() == Type.EQ || instr.asIf().getType() == Type.NE);
   }
 
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 b82c743..e08e9c4 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
@@ -250,20 +250,23 @@
             // in class initializer and is never null.
             //
             DexClass holderDefinition = appInfo.definitionFor(field.getHolder());
-            if (holderDefinition != null &&
-                holderDefinition.accessFlags.isFinal() &&
-                !appInfo.canTriggerStaticInitializer(field.getHolder(), true)) {
-
+            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()) {
+                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());
                 }
               }
             }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java b/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java
index 2094584..b417c48 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/NonNullTracker.java
@@ -343,7 +343,7 @@
         // redundant
         !knownToBeNonNullValue.isNeverNull()
         // v <- non-null NULL ?!
-        && !typeLattice.isConstantNull()
+        && !typeLattice.isNullType()
         // v <- non-null known-to-be-non-null // redundant
         && typeLattice.isNullable()
         // e.g., v <- non-null INT ?!
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 47b9289..9d2bb32 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
@@ -36,6 +36,7 @@
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import java.util.stream.Collectors;
 
 public class UnusedArgumentsCollector {
 
@@ -89,7 +90,9 @@
         Streams.stream(appView.appInfo().classes())
             .map(this::runnableForClass)
             .map(executorService::submit)
-            .iterator());
+            // Materialize list such that all runnables are submitted to the executor service
+            // before calling awaitFutures().
+            .collect(Collectors.toList()));
 
     if (!methodMapping.isEmpty()) {
       return new UnusedArgumentsGraphLense(
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/peepholes/PeepholeHelper.java b/src/main/java/com/android/tools/r8/ir/optimize/peepholes/PeepholeHelper.java
index c1e911f..d656455 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/peepholes/PeepholeHelper.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/peepholes/PeepholeHelper.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.StackValues;
 import com.android.tools.r8.ir.code.Value;
+import java.util.List;
 import java.util.function.Predicate;
 
 public class PeepholeHelper {
@@ -19,18 +20,6 @@
             && (t.outValue() == null || !t.outValue().hasLocalInfo());
   }
 
-  public static void swapNextTwoInstructions(InstructionListIterator it) {
-    assert it.hasNext();
-    Instruction moveForward = it.next();
-    Instruction moveBack = it.next();
-    it.set(moveForward);
-    // Two calls to previous is needed because the iterator moves between elements.
-    it.previous();
-    it.previous();
-    it.set(moveBack);
-    it.next();
-  }
-
   public static void resetNext(InstructionListIterator it, int count) {
     for (int i = 0; i < count; i++) {
       it.previous();
@@ -62,4 +51,23 @@
     }
     return count;
   }
+
+  public static void moveInstructionsUpToCurrentPosition(
+      InstructionListIterator it, List<Instruction> instructions) {
+    assert !instructions.isEmpty();
+    for (Instruction instruction : instructions) {
+      for (Value inValue : instruction.inValues()) {
+        inValue.addUser(instruction);
+      }
+      it.add(instruction);
+    }
+    Instruction current = it.nextUntil(i -> i == instructions.get(0));
+    for (int i = 0; i < instructions.size(); i++) {
+      assert current == instructions.get(i);
+      it.removeOrReplaceByDebugLocalRead();
+      current = it.next();
+    }
+    it.previousUntil(i -> i == instructions.get(instructions.size() - 1));
+    it.next();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/peepholes/StoreLoadToDupStorePeephole.java b/src/main/java/com/android/tools/r8/ir/optimize/peepholes/StoreLoadToDupStorePeephole.java
index a0f1c8c..58c50d8 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/peepholes/StoreLoadToDupStorePeephole.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/peepholes/StoreLoadToDupStorePeephole.java
@@ -11,7 +11,6 @@
 import com.android.tools.r8.ir.code.StackValue;
 import com.android.tools.r8.ir.code.StackValues;
 import com.android.tools.r8.ir.code.Store;
-import com.android.tools.r8.ir.code.Value;
 import java.util.List;
 
 /**
@@ -45,63 +44,40 @@
     if (match == null) {
       return false;
     }
-    Store oldStore = storeExp.get(match).asStore();
+    Store store = storeExp.get(match).asStore();
     Load load = loadExp.get(match).asLoad();
-    if (load.src() != oldStore.outValue() || oldStore.outValue().numberOfAllUsers() <= 1) {
+    if (load.src() != store.outValue() || store.outValue().numberOfAllUsers() <= 1) {
       return false;
     }
     List<Instruction> dups = dupsExp.get(match);
-    Instruction lastDup = dups.isEmpty() ? null : dups.get(dups.size() - 1);
-    StackValue oldStoreSrc = (StackValue) oldStore.src();
-    Value loadOut = load.swapOutValue(null);
-    if (lastDup == null) {
-      // Replace
-      //     Store                     v0 <- oldStoreSrc
-      //     Load                 loadOut <- v0
-      // with
-      //     Dup   [loadOut, newStoreSrc] <- oldStoreSrc
-      //     Store                     v0 <- newStoreSrc
-
-      StackValue newStoreSrc = oldStoreSrc.duplicate(oldStoreSrc.getHeight() + 1);
-      Dup dup = new Dup((StackValue) loadOut, newStoreSrc, oldStoreSrc);
-      dup.setPosition(oldStore.getPosition());
-      oldStore.replaceValue(0, newStoreSrc);
-      it.add(dup);
-      PeepholeHelper.resetPrevious(it, 2);
-      it.removeOrReplaceByDebugLocalRead();
+    StackValue oldStoreSrc = (StackValue) store.src();
+    StackValue dupIn;
+    StackValue lastOut;
+    // Store and load may be in two different basic blocks (see b/122445224) so move all
+    // instructions to the block with the store.
+    if (dups.isEmpty()) {
+      lastOut = (StackValue) load.outValue();
+      dupIn = oldStoreSrc;
     } else {
-      // Replace
-      //     Store                         storeOut <- oldStoreSrc
-      //     Load                           loadOut <- storeOut
-      //     Dup                           [sa, sb] <- loadOut
-      //     (Dup/Dup2)*
-      //     Dup/Dup2           [..., topAfterDups] <- [...]           # lastDup
-      // with
-      //     Dup                           [sa, sb] <- oldStoreSrc
-      //     (Dup/Dup2)*
-      //     Dup/Dup2        [..., newTopAfterDups] <- [...]           # lastDup
-      //     Dup        [topAfterDups, newStoreSrc] <- newTopAfterDups
-      //     Store                         storeOut <- newStoreSrc
-      Value storeOut = oldStore.swapOutValue(null);
-      it.removeOrReplaceByDebugLocalRead(); // Remove Store.
-      it.next();
-      it.removeOrReplaceByDebugLocalRead(); // Remove Load.
-      assert dups.get(0).isDup() && dups.get(0).inValues().get(0) == loadOut;
+      assert dups.get(0).isDup() && dups.get(0).inValues().get(0) == load.outValue();
       dups.get(0).replaceValue(0, oldStoreSrc);
-      StackValues lastDupOut = (StackValues) lastDup.outValue();
-      StackValue topAfterDups = lastDupOut.getStackValues()[lastDupOut.getStackValues().length - 1];
-      StackValue newTopAfterDups = topAfterDups.duplicate(topAfterDups.getHeight());
-      lastDupOut.getStackValues()[lastDupOut.getStackValues().length - 1] = newTopAfterDups;
-      newTopAfterDups.definition = lastDup;
-      StackValue newStoreSrc = oldStoreSrc.duplicate(topAfterDups.getHeight() + 1);
-      Dup newDup = new Dup(topAfterDups, newStoreSrc, newTopAfterDups);
-      newDup.setPosition(lastDup.getPosition());
-      PeepholeHelper.resetPrevious(it, dups.size());
-      it.add(newDup);
-      Store newStore = new Store(storeOut, newStoreSrc);
-      newStore.setPosition(lastDup.getPosition());
-      it.add(newStore);
+      PeepholeHelper.moveInstructionsUpToCurrentPosition(it, dups);
+      StackValue[] lastDupOutValues =
+          ((StackValues) dups.get(dups.size() - 1).outValue()).getStackValues();
+      lastOut = lastDupOutValues[lastDupOutValues.length - 1];
+      dupIn = lastOut;
     }
+    StackValue newLastOut = lastOut.duplicate(lastOut.getHeight());
+    StackValue newStoreSrc = lastOut.duplicate(lastOut.getHeight() + 1);
+    lastOut.replaceUsers(newLastOut);
+    assert !load.outValue().isUsed();
+    Dup dup = new Dup(newLastOut, newStoreSrc, dupIn);
+    dup.setPosition(store.getPosition());
+    it.add(dup);
+    store.replaceValue(0, newStoreSrc);
+    it.nextUntil(i -> i == load);
+    it.removeOrReplaceByDebugLocalRead();
+    PeepholeHelper.resetNext(it, dups.size() + 1);
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
index bd4f2b1..cdd85e4 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringOptimizer.java
@@ -305,7 +305,7 @@
           continue;
         }
         TypeLatticeElement inType = in.getTypeLattice();
-        if (inType.isConstantNull()) {
+        if (inType.isNullType()) {
           Value nullStringValue =
               code.createValue(TypeLatticeElement.stringClassType(appInfo), invoke.getLocalInfo());
           ConstString nullString = new ConstString(
diff --git a/src/main/java/com/android/tools/r8/optimize/BridgeMethodAnalysis.java b/src/main/java/com/android/tools/r8/optimize/BridgeMethodAnalysis.java
deleted file mode 100644
index 86065d9..0000000
--- a/src/main/java/com/android/tools/r8/optimize/BridgeMethodAnalysis.java
+++ /dev/null
@@ -1,160 +0,0 @@
-// Copyright (c) 2017, 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.optimize;
-
-import com.android.tools.r8.errors.Unimplemented;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexField;
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.GraphLense;
-import com.android.tools.r8.ir.code.Invoke.Type;
-import com.android.tools.r8.logging.Log;
-import com.android.tools.r8.optimize.InvokeSingleTargetExtractor.InvokeKind;
-import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
-import java.util.IdentityHashMap;
-import java.util.Map;
-
-public class BridgeMethodAnalysis {
-
-  private final GraphLense lense;
-  private final AppInfoWithLiveness appInfo;
-  private final Map<DexMethod, DexMethod> bridgeTargetToBridgeMap = new IdentityHashMap<>();
-
-  public BridgeMethodAnalysis(GraphLense lense, AppInfoWithLiveness appInfo) {
-    this.lense = lense;
-    this.appInfo = appInfo;
-  }
-
-  public GraphLense run() {
-    for (DexClass clazz : appInfo.classes()) {
-      clazz.forEachMethod(this::identifyBridgeMethod);
-    }
-    return new BridgeLense(lense, bridgeTargetToBridgeMap);
-  }
-
-  private void identifyBridgeMethod(DexEncodedMethod method) {
-    // The tree pruner can mark bridge methods abstract if they are not reachable but cannot
-    // be removed.
-    if (method.accessFlags.isBridge() && !method.accessFlags.isAbstract()) {
-      InvokeSingleTargetExtractor targetExtractor =
-          new InvokeSingleTargetExtractor(appInfo.dexItemFactory);
-      method.getCode().registerCodeReferences(targetExtractor);
-      DexMethod target = targetExtractor.getTarget();
-      InvokeKind kind = targetExtractor.getKind();
-      if (target != null && target.getArity() == method.method.getArity()) {
-        assert !method.accessFlags.isPrivate() && !method.accessFlags.isConstructor();
-        if (kind == InvokeKind.STATIC) {
-          assert method.accessFlags.isStatic();
-          DexMethod actualTarget = lense.lookupMethod(target, method, Type.STATIC).getMethod();
-          DexEncodedMethod targetMethod = appInfo.lookupStaticTarget(actualTarget);
-          if (targetMethod != null) {
-            addForwarding(method, targetMethod);
-          }
-        } else if (kind == InvokeKind.VIRTUAL) {
-          // TODO(herhut): Add support for bridges with multiple targets.
-          DexMethod actualTarget = lense.lookupMethod(target, method, Type.VIRTUAL).getMethod();
-          DexEncodedMethod targetMethod = appInfo.lookupSingleVirtualTarget(actualTarget);
-          if (targetMethod != null) {
-            addForwarding(method, targetMethod);
-          }
-        }
-      }
-    }
-  }
-
-  private void addForwarding(DexEncodedMethod method, DexEncodedMethod target) {
-    // This is a single target bridge we can inline.
-    if (Log.ENABLED) {
-      Log.info(getClass(), "Adding bridge forwarding %s -> %s.", method.method,
-          target.method);
-    }
-    // If we manage to rewrite all invocations, the bridge will be the only invocation of the target
-    // of the bridge and the target will get inlined. This should happen in most cases. For the few
-    // other cases, we might have inserted some extra checkcast instructions for the return type.
-    bridgeTargetToBridgeMap.put(target.method, method.method);
-  }
-
-
-  private static class BridgeLense extends GraphLense {
-
-    private final GraphLense previousLense;
-    private final Map<DexMethod, DexMethod> bridgeTargetToBridgeMap;
-
-    private BridgeLense(GraphLense previousLense,
-        Map<DexMethod, DexMethod> bridgeTargetToBridgeMap) {
-      this.previousLense = previousLense;
-      this.bridgeTargetToBridgeMap = bridgeTargetToBridgeMap;
-    }
-
-    @Override
-    public DexField getOriginalFieldSignature(DexField field) {
-      return previousLense.getOriginalFieldSignature(field);
-    }
-
-    @Override
-    public DexMethod getOriginalMethodSignature(DexMethod method) {
-      // TODO(b/79143143): implement this when re-enable bridge analysis.
-      throw new Unimplemented("BridgeLense.getOriginalMethodSignature() not implemented");
-    }
-
-    @Override
-    public DexField getRenamedFieldSignature(DexField originalField) {
-      return previousLense.getRenamedFieldSignature(originalField);
-    }
-
-    @Override
-    public DexMethod getRenamedMethodSignature(DexMethod originalMethod) {
-      // TODO(b/79143143): implement this when re-enabling bridge analysis.
-      throw new Unimplemented("BridgeLense.getRenamedMethodSignature() not implemented");
-    }
-
-    @Override
-    public DexType lookupType(DexType type) {
-      return previousLense.lookupType(type);
-    }
-
-    @Override
-    public GraphLenseLookupResult lookupMethod(
-        DexMethod method, DexEncodedMethod context, Type type) {
-      GraphLenseLookupResult previous = previousLense.lookupMethod(method, context, type);
-      DexMethod bridge = bridgeTargetToBridgeMap.get(previous.getMethod());
-      // Do not forward calls from a bridge method to itself while the bridge method is still
-      // a bridge.
-      if (bridge == null || (context.accessFlags.isBridge() && bridge == context.method)) {
-        return previous;
-      }
-      return new GraphLenseLookupResult(bridge, type);
-    }
-
-    @Override
-    public RewrittenPrototypeDescription lookupPrototypeChanges(DexMethod method) {
-      return previousLense.lookupPrototypeChanges(method);
-    }
-
-    @Override
-    public DexField lookupField(DexField field) {
-      return previousLense.lookupField(field);
-    }
-
-    @Override
-    public boolean isContextFreeForMethods() {
-      return false;
-    }
-
-    @Override
-    public String toString() {
-      StringBuilder builder = new StringBuilder();
-      builder.append("------ BridgeMap ------").append(System.lineSeparator());
-      for (Map.Entry<DexMethod, DexMethod> entry : bridgeTargetToBridgeMap.entrySet()) {
-        builder.append(entry.getKey().toSourceString()).append(" -> ");
-        builder.append(entry.getValue().toSourceString()).append(System.lineSeparator());
-      }
-      builder.append("-----------------------").append(System.lineSeparator());
-      builder.append(previousLense.toString());
-      return builder.toString();
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 279d381..f10d000 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -297,11 +297,11 @@
     this.options = options;
   }
 
-  private void enqueueRootItems(Map<DexDefinition, ProguardKeepRule> items) {
+  private void enqueueRootItems(Map<DexDefinition, Set<ProguardKeepRule>> items) {
     items.entrySet().forEach(this::enqueueRootItem);
   }
 
-  private void enqueueRootItem(Entry<DexDefinition, ProguardKeepRule> root) {
+  private void enqueueRootItem(Entry<DexDefinition, Set<ProguardKeepRule>> root) {
     enqueueRootItem(root.getKey(), root.getValue());
   }
 
@@ -309,7 +309,26 @@
     enqueueRootItem(item, KeepReason.dueToKeepRule(rule));
   }
 
+  private void enqueueRootItem(DexDefinition item, Set<ProguardKeepRule> rules) {
+    assert !rules.isEmpty();
+    if (keptGraphConsumer != null) {
+      GraphNode node = getGraphNode(item);
+      for (ProguardKeepRule rule : rules) {
+        registerEdge(node, KeepReason.dueToKeepRule(rule));
+      }
+    }
+    internalEnqueueRootItem(item, KeepReason.dueToKeepRule(rules.iterator().next()));
+  }
+
   private void enqueueRootItem(DexDefinition item, KeepReason reason) {
+    if (keptGraphConsumer != null) {
+      registerEdge(getGraphNode(item), reason);
+    }
+    internalEnqueueRootItem(item, reason);
+  }
+
+  private void internalEnqueueRootItem(DexDefinition item, KeepReason reason) {
+    // TODO(b/120959039): do we need to propagate the reason to the action now?
     if (item.isDexClass()) {
       DexClass clazz = item.asDexClass();
       workList.add(Action.markInstantiated(clazz, reason));
@@ -349,11 +368,11 @@
   }
 
   private void enqueueHolderIfDependentNonStaticMember(
-      DexClass holder, Map<DexDefinition, ProguardKeepRule> dependentItems) {
+      DexClass holder, Map<DexDefinition, Set<ProguardKeepRule>> dependentItems) {
     // Check if any dependent members are not static, and in that case enqueue the class as well.
     // Having a dependent rule like -keepclassmembers with non static items indicates that class
     // instances will be present even if tracing do not find any instantiation. See b/115867670.
-    for (Entry<DexDefinition, ProguardKeepRule> entry : dependentItems.entrySet()) {
+    for (Entry<DexDefinition, Set<ProguardKeepRule>> entry : dependentItems.entrySet()) {
       DexDefinition dependentItem = entry.getKey();
       if (dependentItem.isDexClass()) {
         continue;
@@ -791,7 +810,7 @@
         annotations.forEach(this::handleAnnotationOfLiveType);
       }
 
-      Map<DexDefinition, ProguardKeepRule> dependentItems = rootSet.getDependentItems(holder);
+      Map<DexDefinition, Set<ProguardKeepRule>> dependentItems = rootSet.getDependentItems(holder);
       enqueueHolderIfDependentNonStaticMember(holder, dependentItems);
       // Add all dependent members to the workqueue.
       enqueueRootItems(dependentItems);
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 2f7f54a..e86966f 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -61,7 +61,7 @@
   private final AppView<? extends AppInfo> appView;
   private final DirectMappedDexApplication application;
   private final Collection<ProguardConfigurationRule> rules;
-  private final Map<DexDefinition, ProguardKeepRule> noShrinking = new IdentityHashMap<>();
+  private final Map<DexDefinition, Set<ProguardKeepRule>> noShrinking = new IdentityHashMap<>();
   private final Set<DexDefinition> noOptimization = Sets.newIdentityHashSet();
   private final Set<DexDefinition> noObfuscation = Sets.newIdentityHashSet();
   private final LinkedHashMap<DexDefinition, DexDefinition> reasonAsked = new LinkedHashMap<>();
@@ -76,7 +76,7 @@
   private final Set<DexMethod> keepUnusedArguments = Sets.newIdentityHashSet();
   private final Set<DexType> neverClassInline = Sets.newIdentityHashSet();
   private final Set<DexType> neverMerge = Sets.newIdentityHashSet();
-  private final Map<DexDefinition, Map<DexDefinition, ProguardKeepRule>> dependentNoShrinking =
+  private final Map<DexDefinition, Map<DexDefinition, Set<ProguardKeepRule>>> dependentNoShrinking =
       new IdentityHashMap<>();
   private final Map<DexDefinition, ProguardMemberRule> noSideEffects = new IdentityHashMap<>();
   private final Map<DexDefinition, ProguardMemberRule> assumedValues = new IdentityHashMap<>();
@@ -854,8 +854,10 @@
       return;
     }
     // Keep the type if the item is also kept.
-    dependentNoShrinking.computeIfAbsent(item, x -> new IdentityHashMap<>())
-        .put(definition, context);
+    dependentNoShrinking
+        .computeIfAbsent(item, x -> new IdentityHashMap<>())
+        .computeIfAbsent(definition, k -> new HashSet<>())
+        .add(context);
     // Unconditionally add to no-obfuscation, as that is only checked for surviving items.
     noObfuscation.add(definition);
   }
@@ -890,10 +892,12 @@
       ProguardKeepRuleModifiers modifiers = keepRule.getModifiers();
       if (!modifiers.allowsShrinking) {
         if (precondition != null) {
-          dependentNoShrinking.computeIfAbsent(precondition, x -> new IdentityHashMap<>())
-              .put(item, keepRule);
+          dependentNoShrinking
+              .computeIfAbsent(precondition, x -> new IdentityHashMap<>())
+              .computeIfAbsent(item, i -> new HashSet<>())
+              .add(keepRule);
         } else {
-          noShrinking.put(item, keepRule);
+          noShrinking.computeIfAbsent(item, i -> new HashSet<>()).add(keepRule);
         }
       }
       if (!modifiers.allowsOptimization) {
@@ -970,7 +974,7 @@
 
   public static class RootSet {
 
-    public final Map<DexDefinition, ProguardKeepRule> noShrinking;
+    public final Map<DexDefinition, Set<ProguardKeepRule>> noShrinking;
     public final Set<DexDefinition> noOptimization;
     public final Set<DexDefinition> noObfuscation;
     public final ImmutableList<DexDefinition> reasonAsked;
@@ -985,12 +989,13 @@
     public final Set<DexType> neverMerge;
     public final Map<DexDefinition, ProguardMemberRule> noSideEffects;
     public final Map<DexDefinition, ProguardMemberRule> assumedValues;
-    private final Map<DexDefinition, Map<DexDefinition, ProguardKeepRule>> dependentNoShrinking;
+    private final Map<DexDefinition, Map<DexDefinition, Set<ProguardKeepRule>>>
+        dependentNoShrinking;
     public final Set<DexReference> identifierNameStrings;
     public final Set<ProguardIfRule> ifRules;
 
     private RootSet(
-        Map<DexDefinition, ProguardKeepRule> noShrinking,
+        Map<DexDefinition, Set<ProguardKeepRule>> noShrinking,
         Set<DexDefinition> noOptimization,
         Set<DexDefinition> noObfuscation,
         ImmutableList<DexDefinition> reasonAsked,
@@ -1005,7 +1010,7 @@
         Set<DexType> neverMerge,
         Map<DexDefinition, ProguardMemberRule> noSideEffects,
         Map<DexDefinition, ProguardMemberRule> assumedValues,
-        Map<DexDefinition, Map<DexDefinition, ProguardKeepRule>> dependentNoShrinking,
+        Map<DexDefinition, Map<DexDefinition, Set<ProguardKeepRule>>> dependentNoShrinking,
         Set<DexReference> identifierNameStrings,
         Set<ProguardIfRule> ifRules) {
       this.noShrinking = Collections.unmodifiableMap(noShrinking);
@@ -1030,7 +1035,7 @@
 
     // Add dependent items that depend on -if rules.
     void addDependentItems(
-        Map<DexDefinition, Map<DexDefinition, ProguardKeepRule>> dependentItems) {
+        Map<DexDefinition, Map<DexDefinition, Set<ProguardKeepRule>>> dependentItems) {
       dependentItems.forEach(
           (def, dependence) ->
               dependentNoShrinking
@@ -1038,7 +1043,7 @@
                   .putAll(dependence));
     }
 
-    Map<DexDefinition, ProguardKeepRule> getDependentItems(DexDefinition item) {
+    Map<DexDefinition, Set<ProguardKeepRule>> getDependentItems(DexDefinition item) {
       return Collections
           .unmodifiableMap(dependentNoShrinking.getOrDefault(item, Collections.emptyMap()));
     }
@@ -1224,18 +1229,18 @@
   static class ConsequentRootSet {
     final Set<DexMethod> neverInline;
     final Set<DexType> neverClassInline;
-    final Map<DexDefinition, ProguardKeepRule> noShrinking;
+    final Map<DexDefinition, Set<ProguardKeepRule>> noShrinking;
     final Set<DexDefinition> noOptimization;
     final Set<DexDefinition> noObfuscation;
-    final Map<DexDefinition, Map<DexDefinition, ProguardKeepRule>> dependentNoShrinking;
+    final Map<DexDefinition, Map<DexDefinition, Set<ProguardKeepRule>>> dependentNoShrinking;
 
     private ConsequentRootSet(
         Set<DexMethod> neverInline,
         Set<DexType> neverClassInline,
-        Map<DexDefinition, ProguardKeepRule> noShrinking,
+        Map<DexDefinition, Set<ProguardKeepRule>> noShrinking,
         Set<DexDefinition> noOptimization,
         Set<DexDefinition> noObfuscation,
-        Map<DexDefinition, Map<DexDefinition, ProguardKeepRule>> dependentNoShrinking) {
+        Map<DexDefinition, Map<DexDefinition, Set<ProguardKeepRule>>> dependentNoShrinking) {
       this.neverInline = Collections.unmodifiableSet(neverInline);
       this.neverClassInline = Collections.unmodifiableSet(neverClassInline);
       this.noShrinking = Collections.unmodifiableMap(noShrinking);
diff --git a/src/main/java/com/android/tools/r8/utils/ClassMap.java b/src/main/java/com/android/tools/r8/utils/ClassMap.java
index 0ebfb9d..33c1bf3 100644
--- a/src/main/java/com/android/tools/r8/utils/ClassMap.java
+++ b/src/main/java/com/android/tools/r8/utils/ClassMap.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.graph.ClassKind;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexType;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Iterator;
@@ -134,6 +135,18 @@
     return loadedClasses;
   }
 
+  public Map<DexType, DexClass> getAllClassesInMap() {
+    if (classProvider.get() != null) {
+      throw new Unreachable("Getting all classes from not fully loaded collection.");
+    }
+    ImmutableMap.Builder<DexType, DexClass> builder = ImmutableMap.builder();
+    // This is fully loaded, so the class map will no longer change.
+    for (Map.Entry<DexType, Supplier<T>> entry : classes.entrySet()) {
+      builder.put(entry.getKey(), entry.getValue().get());
+    }
+    return builder.build();
+  }
+
   public Iterable<DexType> getAllTypes() {
     return classes.keySet();
   }
diff --git a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
index 91bb786..ec12f82 100644
--- a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
@@ -10,14 +10,12 @@
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.graph.DexDebugEvent;
-import com.android.tools.r8.graph.DexDebugEvent.AdvanceLine;
 import com.android.tools.r8.graph.DexDebugEvent.AdvancePC;
 import com.android.tools.r8.graph.DexDebugEvent.Default;
 import com.android.tools.r8.graph.DexDebugEvent.EndLocal;
 import com.android.tools.r8.graph.DexDebugEvent.RestartLocal;
 import com.android.tools.r8.graph.DexDebugEvent.SetEpilogueBegin;
 import com.android.tools.r8.graph.DexDebugEvent.SetFile;
-import com.android.tools.r8.graph.DexDebugEvent.SetInlineFrame;
 import com.android.tools.r8.graph.DexDebugEvent.SetPrologueEnd;
 import com.android.tools.r8.graph.DexDebugEvent.StartLocal;
 import com.android.tools.r8.graph.DexDebugEventBuilder;
@@ -401,56 +399,45 @@
         new PositionEventEmitter(application.dexItemFactory, method.method, processedEvents);
 
     // Debug event visitor to map line numbers.
-    // TODO(117268618): Cleanup the duplicate pc tracking.
     DexDebugEventVisitor visitor =
-        new DexDebugEventVisitor() {
-          DexDebugPositionState state =
-              new DexDebugPositionState(debugInfo.startLine, method.method);
-          int currentPc = 0;
+        new DexDebugPositionState(debugInfo.startLine, method.method) {
 
+          // Keep track of what PC has been emitted.
+          private int emittedPc = 0;
+
+          // Force the current PC to emitted.
           private void flushPc() {
-            if (currentPc != state.getCurrentPc()) {
-              positionEventEmitter.emitAdvancePc(state.getCurrentPc());
-              currentPc = state.getCurrentPc();
+            if (emittedPc != getCurrentPc()) {
+              positionEventEmitter.emitAdvancePc(getCurrentPc());
+              emittedPc = getCurrentPc();
             }
           }
 
-          @Override
-          public void visit(AdvancePC advancePC) {
-            state.visit(advancePC);
-          }
-
-          @Override
-          public void visit(AdvanceLine advanceLine) {
-            state.visit(advanceLine);
-          }
-
-          @Override
-          public void visit(SetInlineFrame setInlineFrame) {
-            state.visit(setInlineFrame);
-          }
-
+          // A default event denotes a line table entry and must always be emitted. Remap its line.
           @Override
           public void visit(Default defaultEvent) {
-            state.visit(defaultEvent);
-            int currentLine = state.getCurrentLine();
-            assert currentLine >= 0;
+            super.visit(defaultEvent);
+            assert getCurrentLine() >= 0;
             Position position =
                 positionRemapper.createRemappedPosition(
-                    state.getCurrentLine(),
-                    state.getCurrentFile(),
-                    state.getCurrentMethod(),
-                    state.getCurrentCallerPosition());
+                    getCurrentLine(),
+                    getCurrentFile(),
+                    getCurrentMethod(),
+                    getCurrentCallerPosition());
             mappedPositions.add(
                 new MappedPosition(
-                    state.getCurrentMethod(),
-                    currentLine,
-                    state.getCurrentCallerPosition(),
+                    getCurrentMethod(),
+                    getCurrentLine(),
+                    getCurrentCallerPosition(),
                     position.line));
-            positionEventEmitter.emitPositionEvents(state.getCurrentPc(), position);
-            currentPc = state.getCurrentPc();
+            positionEventEmitter.emitPositionEvents(getCurrentPc(), position);
+            emittedPc = getCurrentPc();
           }
 
+          // Non-materializing events use super, ie, AdvancePC, AdvanceLine and SetInlineFrame.
+
+          // Materializing events are just amended to the stream.
+
           @Override
           public void visit(SetFile setFile) {
             processedEvents.add(setFile);
@@ -466,6 +453,8 @@
             processedEvents.add(setEpilogueBegin);
           }
 
+          // Local changes must force flush the PC ensuing they pertain to the correct point.
+
           @Override
           public void visit(StartLocal startLocal) {
             flushPc();
diff --git a/src/main/java/com/android/tools/r8/utils/ThreadUtils.java b/src/main/java/com/android/tools/r8/utils/ThreadUtils.java
index bde7016..fb4f409 100644
--- a/src/main/java/com/android/tools/r8/utils/ThreadUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ThreadUtils.java
@@ -15,11 +15,7 @@
 
   public static void awaitFutures(Iterable<? extends Future<?>> futures)
       throws ExecutionException {
-    awaitFutures(futures.iterator());
-  }
-
-  public static void awaitFutures(Iterator<? extends Future<?>> futureIterator)
-      throws ExecutionException {
+    Iterator<? extends Future<?>> futureIterator = futures.iterator();
     try {
       while (futureIterator.hasNext()) {
         futureIterator.next().get();
diff --git a/src/test/java/com/android/tools/r8/D8TestCompileResult.java b/src/test/java/com/android/tools/r8/D8TestCompileResult.java
index 79fb293..1e40a0b 100644
--- a/src/test/java/com/android/tools/r8/D8TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/D8TestCompileResult.java
@@ -7,12 +7,17 @@
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.utils.AndroidApp;
 
-public class D8TestCompileResult extends TestCompileResult<D8TestRunResult> {
+public class D8TestCompileResult extends TestCompileResult<D8TestCompileResult, D8TestRunResult> {
   D8TestCompileResult(TestState state, AndroidApp app) {
     super(state, app);
   }
 
   @Override
+  public D8TestCompileResult self() {
+    return this;
+  }
+
+  @Override
   public Backend getBackend() {
     return Backend.DEX;
   }
@@ -23,7 +28,7 @@
   }
 
   @Override
-  public D8TestRunResult createRunResult(AndroidApp app, ProcessResult result) {
+  public D8TestRunResult createRunResult(ProcessResult result) {
     return new D8TestRunResult(app, result);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/DXTestCompileResult.java b/src/test/java/com/android/tools/r8/DXTestCompileResult.java
index da6a17a..e339440 100644
--- a/src/test/java/com/android/tools/r8/DXTestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/DXTestCompileResult.java
@@ -7,13 +7,18 @@
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.utils.AndroidApp;
 
-public class DXTestCompileResult extends TestCompileResult<DXTestRunResult> {
+public class DXTestCompileResult extends TestCompileResult<DXTestCompileResult, DXTestRunResult> {
 
   DXTestCompileResult(TestState state, AndroidApp app) {
     super(state, app);
   }
 
   @Override
+  public DXTestCompileResult self() {
+    return this;
+  }
+
+  @Override
   public Backend getBackend() {
     return Backend.DEX;
   }
@@ -24,7 +29,7 @@
   }
 
   @Override
-  public DXTestRunResult createRunResult(AndroidApp app, ProcessResult result) {
+  public DXTestRunResult createRunResult(ProcessResult result) {
     return new DXTestRunResult(app, result);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java b/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
new file mode 100644
index 0000000..3b5ddd8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ExternalR8TestBuilder.java
@@ -0,0 +1,184 @@
+// 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 static com.android.tools.r8.ToolHelper.getJavaExecutable;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.R8Command.Builder;
+import com.android.tools.r8.TestBase.Backend;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.base.Charsets;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.stream.Collectors;
+
+// The type arguments R8Command, Builder is not relevant for running external R8.
+public class ExternalR8TestBuilder
+    extends TestShrinkerBuilder<
+        R8Command,
+        Builder,
+        ExternalR8TestCompileResult,
+        ExternalR8TestRunResult,
+        ExternalR8TestBuilder> {
+
+  // The r8.jar to run.
+  private Path r8jar = ToolHelper.R8_JAR;
+
+  // Ordered list of program jar entries.
+  private final List<Path> programJars = new ArrayList<>();
+
+  // Ordered list of library jar entries.
+  private final List<Path> libJars = new ArrayList<>();
+
+  // Proguard configuration file lines.
+  private final List<String> config = new ArrayList<>();
+
+  // Additional Proguard configuration files.
+  private List<Path> proguardConfigFiles = new ArrayList<>();
+
+  private ExternalR8TestBuilder(TestState state, Builder builder, Backend backend) {
+    super(state, builder, backend);
+  }
+
+  public static ExternalR8TestBuilder create(TestState state, Backend backend) {
+    return new ExternalR8TestBuilder(state, R8Command.builder(), backend);
+  }
+
+  @Override
+  ExternalR8TestBuilder self() {
+    return this;
+  }
+
+  @Override
+  ExternalR8TestCompileResult internalCompile(
+      Builder builder, Consumer<InternalOptions> optionsConsumer, Supplier<AndroidApp> app)
+      throws CompilationFailedException {
+    try {
+      Path outputFolder = getState().getNewTempFolder();
+      Path outputJar = outputFolder.resolve("output.jar");
+      Path proguardMapFile = outputFolder.resolve("output.jar.map");
+
+      List<String> command = new ArrayList<>();
+      Collections.addAll(
+          command,
+          getJavaExecutable(),
+          "-cp",
+          r8jar.toAbsolutePath().toString(),
+          R8.class.getTypeName(),
+          "--output",
+          outputJar.toAbsolutePath().toString(),
+          "--pg-map-output",
+          proguardMapFile.toString(),
+          backend == Backend.CF ? "--classfile" : "--dex",
+          builder.getMode() == CompilationMode.DEBUG ? "--debug" : "--release");
+      if (!config.isEmpty()) {
+        Path proguardConfigFile = outputFolder.resolve("proguard-config.txt");
+        FileUtils.writeTextFile(proguardConfigFile, config);
+        command.add("--pg-conf");
+        command.add(proguardConfigFile.toAbsolutePath().toString());
+      }
+      for (Path proguardConfigFile : proguardConfigFiles) {
+        command.add("--pg-conf");
+        command.add(proguardConfigFile.toAbsolutePath().toString());
+      }
+      if (libJars.isEmpty()) {
+        command.add("--lib");
+        command.add(TestBase.runtimeJar(backend).toAbsolutePath().toString());
+      } else {
+        for (Path libJar : libJars) {
+          command.add("--lib");
+          command.add(libJar.toAbsolutePath().toString());
+        }
+      }
+      command.addAll(programJars.stream().map(Path::toString).collect(Collectors.toList()));
+
+      ProcessBuilder processBuilder = new ProcessBuilder(command);
+      ProcessResult processResult = ToolHelper.runProcess(processBuilder);
+      assertEquals(processResult.stderr, 0, processResult.exitCode);
+      String proguardMap =
+          proguardMapFile.toFile().exists()
+              ? FileUtils.readTextFile(proguardMapFile, Charsets.UTF_8)
+              : "";
+      return new ExternalR8TestCompileResult(getState(), outputJar, processResult, proguardMap);
+    } catch (IOException e) {
+      throw new CompilationFailedException(e);
+    }
+  }
+
+  @Override
+  public ExternalR8TestBuilder addProgramClasses(Collection<Class<?>> classes) {
+    // Adding a collection of classes will build a jar of exactly those classes so that no other
+    // classes are made available via a too broad classpath directory.
+    try {
+      Path programJar = getState().getNewTempFolder().resolve("input.jar");
+      ClassFileConsumer inputConsumer = new ClassFileConsumer.ArchiveConsumer(programJar);
+      for (Class<?> clazz : classes) {
+        String descriptor = DescriptorUtils.javaTypeToDescriptor(clazz.getName());
+        inputConsumer.accept(ByteDataView.of(ToolHelper.getClassAsBytes(clazz)), descriptor, null);
+      }
+      inputConsumer.finished(null);
+      programJars.add(programJar);
+      return self();
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  public ExternalR8TestBuilder addProgramFiles(Collection<Path> files) {
+    for (Path file : files) {
+      if (FileUtils.isJarFile(file)) {
+        programJars.add(file);
+      } else {
+        throw new Unimplemented("No support for adding paths directly");
+      }
+    }
+    return self();
+  }
+
+  @Override
+  public ExternalR8TestBuilder addProgramClassFileData(Collection<byte[]> classes) {
+    throw new Unimplemented("No support for adding classfile data directly");
+  }
+
+  @Override
+  public ExternalR8TestBuilder addLibraryFiles(Collection<Path> files) {
+    libJars.addAll(files);
+    return self();
+  }
+
+  @Override
+  public ExternalR8TestBuilder addKeepRuleFiles(List<Path> proguardConfigFiles) {
+    this.proguardConfigFiles.addAll(proguardConfigFiles);
+    return self();
+  }
+
+  @Override
+  public ExternalR8TestBuilder addKeepRules(Collection<String> rules) {
+    config.addAll(rules);
+    return self();
+  }
+
+  public ExternalR8TestBuilder useR8WithRelocatedDeps() {
+    return useProvidedR8(ToolHelper.R8_WITH_RELOCATED_DEPS_JAR);
+  }
+
+  public ExternalR8TestBuilder useProvidedR8(Path r8jar) {
+    this.r8jar = r8jar;
+    return self();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ExternalR8TestCompileResult.java b/src/test/java/com/android/tools/r8/ExternalR8TestCompileResult.java
new file mode 100644
index 0000000..f2a8839
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ExternalR8TestCompileResult.java
@@ -0,0 +1,67 @@
+// 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.TestBase.Backend;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.concurrent.ExecutionException;
+
+public class ExternalR8TestCompileResult
+    extends TestCompileResult<ExternalR8TestCompileResult, ExternalR8TestRunResult> {
+
+  private final Path outputJar;
+  private final ProcessResult processResult;
+  private final String proguardMap;
+
+  protected ExternalR8TestCompileResult(
+      TestState state, Path outputJar, ProcessResult processResult, String proguardMap) {
+    super(state, AndroidApp.builder().addProgramFiles(outputJar).build());
+    assert processResult.exitCode == 0;
+    this.outputJar = outputJar;
+    this.processResult = processResult;
+    this.proguardMap = proguardMap;
+  }
+
+  public Path outputJar() {
+    return outputJar;
+  }
+
+  public String stdout() {
+    return processResult.stdout;
+  }
+
+  public String stderr() {
+    return processResult.stdout;
+  }
+
+  @Override
+  public ExternalR8TestCompileResult self() {
+    return this;
+  }
+
+  @Override
+  public Backend getBackend() {
+    return null;
+  }
+
+  @Override
+  public TestDiagnosticMessages getDiagnosticMessages() {
+    throw new UnsupportedOperationException("No diagnostics messages from external R8");
+  }
+
+  @Override
+  public CodeInspector inspector() throws IOException, ExecutionException {
+    return new CodeInspector(app, proguardMap);
+  }
+
+  @Override
+  protected ExternalR8TestRunResult createRunResult(ProcessResult result) {
+    return new ExternalR8TestRunResult(app, outputJar, proguardMap, result);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ExternalR8TestRunResult.java b/src/test/java/com/android/tools/r8/ExternalR8TestRunResult.java
new file mode 100644
index 0000000..f301b78
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ExternalR8TestRunResult.java
@@ -0,0 +1,44 @@
+// 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 static org.junit.Assert.assertNotNull;
+
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.concurrent.ExecutionException;
+
+public class ExternalR8TestRunResult extends TestRunResult<ExternalR8TestRunResult> {
+
+  private final Path outputJar;
+  private final String proguardMap;
+
+  public ExternalR8TestRunResult(
+      AndroidApp app, Path outputJar, String proguardMap, ProcessResult result) {
+    super(app, result);
+    this.outputJar = outputJar;
+    this.proguardMap = proguardMap;
+  }
+
+  public Path outputJar() {
+    return outputJar;
+  }
+
+  @Override
+  protected ExternalR8TestRunResult self() {
+    return this;
+  }
+
+  @Override
+  public CodeInspector inspector() throws IOException, ExecutionException {
+    // See comment in base class.
+    assertSuccess();
+    assertNotNull(app);
+    return new CodeInspector(app, proguardMap);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ForceInline.java b/src/test/java/com/android/tools/r8/ForceInline.java
index 4a74233..dfeace7 100644
--- a/src/test/java/com/android/tools/r8/ForceInline.java
+++ b/src/test/java/com/android/tools/r8/ForceInline.java
@@ -3,5 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
-public @interface ForceInline {
-}
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+@Target({ElementType.METHOD})
+public @interface ForceInline {}
diff --git a/src/test/java/com/android/tools/r8/ProguardTestBuilder.java b/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
index 2e4b1bd..ea482fa 100644
--- a/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
@@ -98,9 +98,13 @@
 
   // Ordered list of injar entries.
   private List<Path> injars = new ArrayList<>();
+
   // Proguard configuration file lines.
   private List<String> config = new ArrayList<>();
 
+  // Additional Proguard configuration files.
+  private List<Path> proguardConfigFiles = new ArrayList<>();
+
   private ProguardTestBuilder(TestState state, Builder builder, Backend backend) {
     super(state, builder, backend);
   }
@@ -138,6 +142,10 @@
       command.add(ToolHelper.getJava8RuntimeJar().toString());
       command.add("-include");
       command.add(configFile.toString());
+      for (Path proguardConfigFile : proguardConfigFiles) {
+        command.add("-include");
+        command.add(proguardConfigFile.toAbsolutePath().toString());
+      }
       command.add("-outjar");
       command.add(outJar.toString());
       command.add("-printmapping");
@@ -153,11 +161,9 @@
       if (result.exitCode != 0) {
         throw new CompilationFailedException(result.toString());
       }
-      AndroidApp.Builder aaabuilder = AndroidApp.builder();
-      aaabuilder.addProgramFiles(outJar);
       String proguardMap =
           Files.exists(mapFile) ? FileUtils.readTextFile(mapFile, Charsets.UTF_8) : "";
-      return new ProguardTestCompileResult(getState(), aaabuilder.build(), proguardMap);
+      return new ProguardTestCompileResult(getState(), outJar, proguardMap);
     } catch (IOException e) {
       throw new CompilationFailedException(e);
     }
@@ -203,14 +209,8 @@
   }
 
   @Override
-  public ProguardTestBuilder addKeepRuleFiles(List<Path> files) {
-    try {
-      for (Path file : files) {
-        config.addAll(FileUtils.readAllLines(file));
-      }
-    } catch (IOException e) {
-      throw new RuntimeException(e);
-    }
+  public ProguardTestBuilder addKeepRuleFiles(List<Path> proguardConfigFiles) {
+    this.proguardConfigFiles.addAll(proguardConfigFiles);
     return self();
   }
 
diff --git a/src/test/java/com/android/tools/r8/ProguardTestCompileResult.java b/src/test/java/com/android/tools/r8/ProguardTestCompileResult.java
index 4ea8953..6f47adb 100644
--- a/src/test/java/com/android/tools/r8/ProguardTestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/ProguardTestCompileResult.java
@@ -8,17 +8,30 @@
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import java.io.IOException;
+import java.nio.file.Path;
 import java.util.concurrent.ExecutionException;
 
-public class ProguardTestCompileResult extends TestCompileResult<ProguardTestRunResult> {
+public class ProguardTestCompileResult
+    extends TestCompileResult<ProguardTestCompileResult, ProguardTestRunResult> {
 
+  private final Path outputJar;
   private final String proguardMap;
 
-  ProguardTestCompileResult(TestState state, AndroidApp app, String proguardMap) {
-    super(state, app);
+  ProguardTestCompileResult(TestState state, Path outputJar, String proguardMap) {
+    super(state, AndroidApp.builder().addProgramFiles(outputJar).build());
+    this.outputJar = outputJar;
     this.proguardMap = proguardMap;
   }
 
+  public Path outputJar() {
+    return outputJar;
+  }
+
+  @Override
+  public ProguardTestCompileResult self() {
+    return this;
+  }
+
   @Override
   public Backend getBackend() {
     return Backend.CF;
@@ -26,7 +39,7 @@
 
   @Override
   public TestDiagnosticMessages getDiagnosticMessages() {
-    throw new UnsupportedOperationException("No diagnostics messages from dx");
+    throw new UnsupportedOperationException("No diagnostics messages from Proguard");
   }
 
   @Override
@@ -35,7 +48,7 @@
   }
 
   @Override
-  public ProguardTestRunResult createRunResult(AndroidApp app, ProcessResult result) {
+  public ProguardTestRunResult createRunResult(ProcessResult result) {
     return new ProguardTestRunResult(app, result, proguardMap);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java b/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
index 195e01c..7d6af00 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesCommon.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
 import com.android.tools.r8.utils.TestDescriptionWatcher;
 import com.google.common.collect.ImmutableList;
@@ -80,8 +81,8 @@
   private final CompilationMode mode;
   private final String pkg;
   private final String mainClass;
-  private final Frontend frontend;
-  private final Output output;
+  protected final Frontend frontend;
+  protected final Output output;
 
   public R8RunExamplesCommon(
       String pkg,
@@ -174,16 +175,7 @@
                   .setDisableMinification(true)
                   .addProguardConfiguration(ImmutableList.of("-keepattributes *"), Origin.unknown())
                   .build();
-          ToolHelper.runR8(
-              command,
-              options -> {
-                options.lineNumberOptimization = LineNumberOptimization.OFF;
-                options.enableCfFrontend = frontend == Frontend.CF;
-                if (output == Output.CF) {
-                  // Class inliner is not supported with CF backend yet.
-                  options.enableClassInlining = false;
-                }
-              });
+          ToolHelper.runR8(command, this::configure);
         break;
       }
       default:
@@ -191,6 +183,15 @@
     }
   }
 
+  protected void configure(InternalOptions options) {
+    options.lineNumberOptimization = LineNumberOptimization.OFF;
+    options.enableCfFrontend = frontend == Frontend.CF;
+    if (output == Output.CF) {
+      // Class inliner is not supported with CF backend yet.
+      options.enableClassInlining = false;
+    }
+  }
+
   private boolean shouldCompileFail() {
     if (output == Output.CF && getFailingCompileCf().contains(mainClass)) {
       return true;
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesKotlinTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesKotlinTest.java
index 4451910..c2c0869 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesKotlinTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesKotlinTest.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8;
 
 import com.android.tools.r8.R8RunArtTestsTest.CompilerUnderTest;
+import com.android.tools.r8.utils.InternalOptions;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -17,6 +18,15 @@
 @RunWith(Parameterized.class)
 public class R8RunExamplesKotlinTest extends R8RunExamplesCommon {
 
+  @Override
+  protected void configure(InternalOptions options) {
+    options.enableCfFrontend = frontend == Frontend.CF;
+    if (output == Output.CF) {
+      // Class inliner is not supported with CF backend yet.
+      options.enableClassInlining = false;
+    }
+  }
+
   @Parameters(name = "{0}_{1}_{2}_{3}_{5}_{6}")
   public static Collection<String[]> data() {
     String[] tests = {
diff --git a/src/test/java/com/android/tools/r8/R8TestCompileResult.java b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
index ef5bfb1..8a4c44d 100644
--- a/src/test/java/com/android/tools/r8/R8TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
@@ -12,7 +12,7 @@
 import java.io.IOException;
 import java.util.concurrent.ExecutionException;
 
-public class R8TestCompileResult extends TestCompileResult<R8TestRunResult> {
+public class R8TestCompileResult extends TestCompileResult<R8TestCompileResult, R8TestRunResult> {
 
   private final Backend backend;
   private final String proguardMap;
@@ -31,6 +31,11 @@
   }
 
   @Override
+  public R8TestCompileResult self() {
+    return this;
+  }
+
+  @Override
   public Backend getBackend() {
     return backend;
   }
@@ -51,7 +56,7 @@
   }
 
   @Override
-  public R8TestRunResult createRunResult(AndroidApp app, ProcessResult result) {
+  public R8TestRunResult createRunResult(ProcessResult result) {
     return new R8TestRunResult(app, result, proguardMap, this::graphInspector);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 0783413..e0baab1 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -83,6 +83,10 @@
     return R8TestBuilder.create(new TestState(temp), backend, R8Mode.Compat);
   }
 
+  public static ExternalR8TestBuilder testForExternalR8(TemporaryFolder temp, Backend backend) {
+    return ExternalR8TestBuilder.create(new TestState(temp), backend);
+  }
+
   public static D8TestBuilder testForD8(TemporaryFolder temp) {
     return D8TestBuilder.create(new TestState(temp));
   }
@@ -107,6 +111,10 @@
     return testForR8Compat(temp, backend);
   }
 
+  public ExternalR8TestBuilder testForExternalR8(Backend backend) {
+    return testForExternalR8(temp, backend);
+  }
+
   public D8TestBuilder testForD8() {
     return testForD8(temp);
   }
diff --git a/src/test/java/com/android/tools/r8/TestCompileResult.java b/src/test/java/com/android/tools/r8/TestCompileResult.java
index 4a31320..5d5229b 100644
--- a/src/test/java/com/android/tools/r8/TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/TestCompileResult.java
@@ -24,7 +24,8 @@
 import java.util.concurrent.ExecutionException;
 import org.hamcrest.Matcher;
 
-public abstract class TestCompileResult<RR extends TestRunResult> {
+public abstract class TestCompileResult<
+    CR extends TestCompileResult<CR, RR>, RR extends TestRunResult> {
 
   final TestState state;
   public final AndroidApp app;
@@ -34,11 +35,13 @@
     this.app = app;
   }
 
+  public abstract CR self();
+
   public abstract Backend getBackend();
 
   public abstract TestDiagnosticMessages getDiagnosticMessages();
 
-  protected abstract RR createRunResult(AndroidApp add, ProcessResult result);
+  protected abstract RR createRunResult(ProcessResult result);
 
   public RR run(Class<?> mainClass) throws IOException {
     return run(mainClass.getTypeName());
@@ -55,47 +58,46 @@
     }
   }
 
-  public TestCompileResult writeToZip(Path file) throws IOException {
+  public CR writeToZip(Path file) throws IOException {
     app.writeToZip(file, getBackend() == DEX ? OutputMode.DexIndexed : OutputMode.ClassFile);
-    return this;
+    return self();
   }
 
   public CodeInspector inspector() throws IOException, ExecutionException {
     return new CodeInspector(app);
   }
 
-  public TestCompileResult<RR> inspect(Consumer<CodeInspector> consumer)
-      throws IOException, ExecutionException {
+  public CR inspect(Consumer<CodeInspector> consumer) throws IOException, ExecutionException {
     consumer.accept(inspector());
-    return this;
+    return self();
   }
 
-  public TestCompileResult<RR> assertNoMessages() {
+  public CR assertNoMessages() {
     assertEquals(0, getDiagnosticMessages().getInfos().size());
     assertEquals(0, getDiagnosticMessages().getWarnings().size());
     assertEquals(0, getDiagnosticMessages().getErrors().size());
-    return this;
+    return self();
   }
 
-  public TestCompileResult<RR> assertOnlyInfos() {
+  public CR assertOnlyInfos() {
     assertNotEquals(0, getDiagnosticMessages().getInfos().size());
     assertEquals(0, getDiagnosticMessages().getWarnings().size());
     assertEquals(0, getDiagnosticMessages().getErrors().size());
-    return this;
+    return self();
   }
 
-  public TestCompileResult<RR> assertOnlyWarnings() {
+  public CR assertOnlyWarnings() {
     assertEquals(0, getDiagnosticMessages().getInfos().size());
     assertNotEquals(0, getDiagnosticMessages().getWarnings().size());
     assertEquals(0, getDiagnosticMessages().getErrors().size());
-    return this;
+    return self();
   }
 
-  public TestCompileResult<RR> assertWarningMessageThatMatches(Matcher<String> matcher) {
+  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 this;
+        return self();
       }
     }
     StringBuilder builder = new StringBuilder("No warning matches " + matcher.toString());
@@ -111,10 +113,10 @@
       }
     }
     fail(builder.toString());
-    return this;
+    return self();
   }
 
-  public TestCompileResult<RR> assertNoWarningMessageThatMatches(Matcher<String> matcher) {
+  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();
@@ -122,15 +124,15 @@
         fail("The warning: \"" + message + "\" + matches " + matcher + ".");
       }
     }
-    return this;
+    return self();
   }
 
-  public TestCompileResult<RR> disassemble(PrintStream ps) throws IOException, ExecutionException {
+  public CR disassemble(PrintStream ps) throws IOException, ExecutionException {
     ToolHelper.disassemble(app, ps);
-    return this;
+    return self();
   }
 
-  public TestCompileResult<RR> disassemble() throws IOException, ExecutionException {
+  public CR disassemble() throws IOException, ExecutionException {
     return disassemble(System.out);
   }
 
@@ -162,14 +164,14 @@
     Path out = state.getNewTempFolder().resolve("out.zip");
     app.writeToZip(out, OutputMode.ClassFile);
     ProcessResult result = ToolHelper.runJava(out, mainClass);
-    return createRunResult(app, result);
+    return createRunResult(result);
   }
 
   private RR runArt(String mainClass) throws IOException {
     Path out = state.getNewTempFolder().resolve("out.zip");
     app.writeToZip(out, OutputMode.DexIndexed);
     ProcessResult result = ToolHelper.runArtRaw(out.toString(), mainClass);
-    return createRunResult(app, result);
+    return createRunResult(result);
   }
 
   public Dex2OatTestRunResult runDex2Oat() throws IOException {
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index e9ebd4e..8276a06 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -21,7 +21,7 @@
 public abstract class TestCompilerBuilder<
         C extends BaseCompilerCommand,
         B extends BaseCompilerCommand.Builder<C, B>,
-        CR extends TestCompileResult<RR>,
+        CR extends TestCompileResult<CR, RR>,
         RR extends TestRunResult,
         T extends TestCompilerBuilder<C, B, CR, RR, T>>
     extends TestBuilder<RR, T> {
diff --git a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
index 81f0407..9d8e03c 100644
--- a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
@@ -16,7 +16,7 @@
 public abstract class TestShrinkerBuilder<
         C extends BaseCompilerCommand,
         B extends BaseCompilerCommand.Builder<C, B>,
-        CR extends TestCompileResult<RR>,
+        CR extends TestCompileResult<CR, RR>,
         RR extends TestRunResult,
         T extends TestCompilerBuilder<C, B, CR, RR, T>>
     extends TestCompilerBuilder<C, B, CR, RR, T> {
diff --git a/src/test/java/com/android/tools/r8/cf/BootstrapCurrentEqualityTest.java b/src/test/java/com/android/tools/r8/cf/BootstrapCurrentEqualityTest.java
index a0cd03d..aeeb0ec 100644
--- a/src/test/java/com/android/tools/r8/cf/BootstrapCurrentEqualityTest.java
+++ b/src/test/java/com/android/tools/r8/cf/BootstrapCurrentEqualityTest.java
@@ -10,14 +10,13 @@
 
 import com.android.tools.r8.ArchiveClassFileProvider;
 import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.ExternalR8TestCompileResult;
 import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.R8;
 import com.android.tools.r8.R8Command;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
-import com.android.tools.r8.utils.FileUtils;
-import com.google.common.base.Charsets;
 import java.io.BufferedInputStream;
 import java.io.FileInputStream;
 import java.io.IOException;
@@ -34,12 +33,11 @@
 import org.junit.rules.TemporaryFolder;
 
 /**
- * This test relies on a freshly built builds/libs/r8lib_with_deps.jar. If this test fails
- * remove build directory and rebuild r8lib_with_deps by calling test.py or gradle r8libWithdeps.
+ * This test relies on a freshly built build/libs/r8lib_with_deps.jar. If this test fails remove
+ * build directory and rebuild r8lib_with_deps by calling test.py or gradle r8libWithdeps.
  */
 public class BootstrapCurrentEqualityTest extends TestBase {
 
-  private static final String R8_NAME = "com.android.tools.r8.R8";
   private static final Path MAIN_KEEP = Paths.get("src/main/keep.txt");
 
   private static final String HELLO_NAME = "hello.Hello";
@@ -47,24 +45,6 @@
     "-keep class " + HELLO_NAME + " {", "  public static void main(...);", "}",
   };
 
-  private static class R8Result {
-
-    final ProcessResult processResult;
-    final Path outputJar;
-    final String pgMap;
-
-    R8Result(ProcessResult processResult, Path outputJar, String pgMap) {
-      this.processResult = processResult;
-      this.outputJar = outputJar;
-      this.pgMap = pgMap;
-    }
-
-    @Override
-    public String toString() {
-      return processResult.toString() + "\n\n" + pgMap;
-    }
-  }
-
   private static Path r8R8Debug;
   private static Path r8R8Release;
 
@@ -74,22 +54,22 @@
 
   @BeforeClass
   public static void beforeAll() throws Exception {
-    r8R8Debug = compileR8("--debug");
-    r8R8Release = compileR8("--release");
+    r8R8Debug = compileR8(CompilationMode.DEBUG);
+    r8R8Release = compileR8(CompilationMode.RELEASE);
   }
 
-  private static Path compileR8(String mode) throws Exception {
+  private static Path compileR8(CompilationMode mode) throws Exception {
     // Run R8 on r8.jar.
     Path jar;
     if (testExternal) {
-      R8Result output =
-          runExternalR8(
-              ToolHelper.R8_WITH_RELOCATED_DEPS_JAR,
-              ToolHelper.R8_WITH_RELOCATED_DEPS_JAR,
-              testFolder.newFolder().toPath(),
-              MAIN_KEEP,
-              mode);
-      jar = output.outputJar;
+      jar =
+          testForExternalR8(newTempFolder(), Backend.CF)
+              .useR8WithRelocatedDeps()
+              .addProgramFiles(ToolHelper.R8_WITH_RELOCATED_DEPS_JAR)
+              .addKeepRuleFiles(MAIN_KEEP)
+              .setMode(mode)
+              .compile()
+              .outputJar();
     } else {
       jar = testFolder.newFolder().toPath().resolve("out.zip");
       R8.run(
@@ -124,66 +104,48 @@
 
   private void compareR8(Path program, ProcessResult runResult, String[] keep, String... args)
       throws Exception {
-    R8Result runR8Debug =
-        runExternalR8(
-            ToolHelper.R8_WITH_RELOCATED_DEPS_JAR,
-            program,
-            temp.newFolder().toPath(),
-            keep,
-            "--debug");
-    assertEquals(runResult.toString(), ToolHelper.runJava(runR8Debug.outputJar, args).toString());
-    R8Result runR8Release =
-        runExternalR8(
-            ToolHelper.R8_WITH_RELOCATED_DEPS_JAR,
-            program,
-            temp.newFolder().toPath(),
-            keep,
-            "--release");
-    assertEquals(runResult.toString(), ToolHelper.runJava(runR8Release.outputJar, args).toString());
-    RunR8AndCheck(r8R8Debug, program, runR8Debug, keep, "--debug");
-    RunR8AndCheck(r8R8Debug, program, runR8Release, keep, "--release");
-    RunR8AndCheck(r8R8Release, program, runR8Debug, keep, "--debug");
-    RunR8AndCheck(r8R8Release, program, runR8Release, keep, "--release");
+    ExternalR8TestCompileResult runR8Debug =
+        testForExternalR8(newTempFolder(), Backend.CF)
+            .useR8WithRelocatedDeps()
+            .addProgramFiles(program)
+            .addKeepRules(keep)
+            .setMode(CompilationMode.DEBUG)
+            .compile();
+    assertEquals(runResult.toString(), ToolHelper.runJava(runR8Debug.outputJar(), args).toString());
+    ExternalR8TestCompileResult runR8Release =
+        testForExternalR8(newTempFolder(), Backend.CF)
+            .useR8WithRelocatedDeps()
+            .addProgramFiles(program)
+            .addKeepRules(keep)
+            .setMode(CompilationMode.RELEASE)
+            .compile();
+    assertEquals(
+        runResult.toString(), ToolHelper.runJava(runR8Release.outputJar(), args).toString());
+    RunR8AndCheck(r8R8Debug, program, runR8Debug, keep, CompilationMode.DEBUG);
+    RunR8AndCheck(r8R8Debug, program, runR8Release, keep, CompilationMode.RELEASE);
+    RunR8AndCheck(r8R8Release, program, runR8Debug, keep, CompilationMode.DEBUG);
+    RunR8AndCheck(r8R8Release, program, runR8Release, keep, CompilationMode.RELEASE);
   }
 
-  private void RunR8AndCheck(Path r8, Path program, R8Result result, String[] keep, String mode)
+  private void RunR8AndCheck(
+      Path r8,
+      Path program,
+      ExternalR8TestCompileResult result,
+      String[] keep,
+      CompilationMode mode)
       throws Exception {
-    R8Result runR8R8 = runExternalR8(r8, program, temp.newFolder().toPath(), keep, mode);
+    ExternalR8TestCompileResult runR8R8 =
+        testForExternalR8(newTempFolder(), Backend.CF)
+            .useProvidedR8(r8)
+            .addProgramFiles(program)
+            .addKeepRules(keep)
+            .setMode(mode)
+            .compile();
     // Check that the process outputs (exit code, stdout, stderr) are the same.
-    assertEquals(result.toString(), runR8R8.toString());
+    assertEquals(result.stdout(), runR8R8.stdout());
+    assertEquals(result.stderr(), runR8R8.stderr());
     // Check that the output jars are the same.
-    assertProgramsEqual(result.outputJar, runR8R8.outputJar);
-  }
-
-  private static R8Result runExternalR8(
-      Path r8Jar, Path inputJar, Path output, String[] keepRules, String mode) throws Exception {
-    Path pgConfigFile = output.resolve("keep.rules");
-    FileUtils.writeTextFile(pgConfigFile, keepRules);
-    return runExternalR8(r8Jar, inputJar, output, pgConfigFile, mode);
-  }
-
-  private static R8Result runExternalR8(
-      Path r8Jar, Path inputJar, Path output, Path keepRules, String mode) throws Exception {
-    Path outputJar = output.resolve("output.jar");
-    Path pgMapFile = output.resolve("output.jar.map");
-    ProcessResult processResult =
-        ToolHelper.runJava(
-            r8Jar,
-            R8_NAME,
-            "--lib",
-            ToolHelper.JAVA_8_RUNTIME,
-            "--classfile",
-            inputJar.toString(),
-            "--output",
-            outputJar.toString(),
-            "--pg-conf",
-            keepRules.toString(),
-            mode,
-            "--pg-map-output",
-            pgMapFile.toString());
-    assertEquals(processResult.stderr, 0, processResult.exitCode);
-    String pgMap = FileUtils.readTextFile(pgMapFile, Charsets.UTF_8);
-    return new R8Result(processResult, outputJar, pgMap);
+    assertProgramsEqual(result.outputJar(), runR8R8.outputJar());
   }
 
   private static void assertProgramsEqual(Path expectedJar, Path actualJar) throws Exception {
@@ -234,4 +196,10 @@
       throws Exception {
     return toByteArray(inputJar.getProgramResource(descriptor).getByteStream());
   }
+
+  private static TemporaryFolder newTempFolder() throws IOException {
+    TemporaryFolder tempFolder = new TemporaryFolder(testFolder.newFolder());
+    tempFolder.create();
+    return tempFolder;
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/cf/CloserTest.java b/src/test/java/com/android/tools/r8/cf/CloserTest.java
new file mode 100644
index 0000000..874eb8a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/CloserTest.java
@@ -0,0 +1,37 @@
+// 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;
+import java.io.IOException;
+import java.io.OutputStream;
+
+public class CloserTest {
+
+  @NeverInline
+  public static CloserTest create() {
+    return new CloserTest();
+  }
+
+  OutputStream register() {
+    return System.out;
+  }
+
+  public void doSomething(String message) throws IOException {
+    System.out.println(message);
+  }
+
+  public static void main(String... args) throws IOException {
+    CloserTest closer = CloserTest.create();
+    try {
+      OutputStream out = closer.register();
+      out.write(args[0].getBytes());
+    } catch (Throwable e) {
+      closer.doSomething(e.getMessage());
+    } finally {
+      closer.doSomething("FINISHED");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/CloserTestRunner.java b/src/test/java/com/android/tools/r8/cf/CloserTestRunner.java
new file mode 100644
index 0000000..cac41cc
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/CloserTestRunner.java
@@ -0,0 +1,54 @@
+// 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.CompilationMode;
+import com.android.tools.r8.TestBase;
+import org.junit.Test;
+
+/**
+ * This tests that we produce valid code when having normal-flow with exceptional edges in blocks.
+ * We might perform optimizations that add stack-operations (dup, swap, etc.) before and after
+ * instructions that lie on the boundary of the exception table that is generated for a basic block:
+ *
+ * <pre>
+ *   Code:
+ *      0: invokestatic  #16                 // Method create:()Lcom/android/tools/r8/cf/CloserTest;
+ *      3: dup
+ *      4: dup
+ *      5: astore_1
+ *      ...
+ *   Exception table:
+ *      from    to  target type
+ *      3       9   24     Class java/lang/Throwable
+ *      ...
+ *   StackMap table:
+ *   StackMapTable: number_of_entries = 4
+ *         frame_type = 255
+ *         offset_delta=21
+ *         locals=[top, class com/android/tools/r8/cf/CloserTest]
+ *         stack=[class java/lang/Throwable]
+ * </pre>
+ *
+ * If we produce something like this, the JVM verifier will throw a VerifyError on @bci: 3 since we
+ * have not stored CloserTest in locals[1] (that happens in @bci 5), as described in the
+ * StackMapTable. This is because the exception handler starts at @bci 3 and not later.
+ */
+public class CloserTestRunner extends TestBase {
+
+  @Test
+  public void test() throws Exception {
+    testForR8(Backend.CF)
+        .addProgramClasses(CloserTest.class)
+        .addKeepMainRule(CloserTest.class)
+        .setMode(CompilationMode.RELEASE)
+        .minification(false)
+        .noTreeShaking()
+        .enableInliningAnnotations()
+        .compile()
+        .run(CloserTest.class)
+        .assertSuccess();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/DebugInfoTestRunner.java b/src/test/java/com/android/tools/r8/cf/DebugInfoTestRunner.java
index f8a654a..3a99d39 100644
--- a/src/test/java/com/android/tools/r8/cf/DebugInfoTestRunner.java
+++ b/src/test/java/com/android/tools/r8/cf/DebugInfoTestRunner.java
@@ -3,62 +3,45 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.cf;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 
-import com.android.tools.r8.ClassFileConsumer;
 import com.android.tools.r8.CompilationFailedException;
-import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.ProgramConsumer;
-import com.android.tools.r8.R8Command;
-import com.android.tools.r8.R8Command.Builder;
+import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ToolHelper.ProcessResult;
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.utils.ThrowingConsumer;
 import java.nio.file.Path;
 import org.junit.Test;
 
 public class DebugInfoTestRunner extends TestBase {
-  static final Class CLASS = DebugInfoTest.class;
+  private static final Class<?> CLASS = DebugInfoTest.class;
+  private static final String EXPECTED = "";
 
   @Test
   public void test() throws Exception {
-    ProcessResult runInput =
-        ToolHelper.runJava(ToolHelper.getClassPathForTests(), CLASS.getCanonicalName());
-    assertEquals(0, runInput.exitCode);
+    testForJvm().addProgramClasses(CLASS).run(CLASS).assertSuccessWithOutput(EXPECTED);
+
     Path out1 = temp.getRoot().toPath().resolve("out1.zip");
-    build(
-        builder -> builder.addClassProgramData(ToolHelper.getClassAsBytes(CLASS), Origin.unknown()),
-        new ClassFileConsumer.ArchiveConsumer(out1));
-    ProcessResult run1 = ToolHelper.runJava(out1, CLASS.getCanonicalName());
-    assertEquals(runInput.toString(), run1.toString());
-    Path out2 = temp.getRoot().toPath().resolve("out2.zip");
-    boolean invalidDebugInfo = false;
+    builder()
+        .addProgramClasses(CLASS)
+        .compile()
+        .writeToZip(out1)
+        .run(CLASS)
+        .assertSuccessWithOutput(EXPECTED);
+
     try {
-      build(builder -> builder.addProgramFiles(out1), new ClassFileConsumer.ArchiveConsumer(out2));
+      builder().addProgramFiles(out1).run(CLASS).assertSuccessWithOutput(EXPECTED);
+      // TODO(b/77522100): Remove once fixed.
+      fail();
     } catch (CompilationFailedException e) {
-      invalidDebugInfo = e.getCause().getMessage().contains("Invalid debug info");
-    }
-    // TODO(b/77522100): Change to assertFalse when fixed.
-    assertTrue(invalidDebugInfo);
-    if (!invalidDebugInfo) {
-      ProcessResult run2 = ToolHelper.runJava(out2, CLASS.getCanonicalName());
-      assertEquals(runInput.toString(), run2.toString());
+      // TODO(b/77522100): Remove one fixed.
+      assert e.getCause().getMessage().contains("Invalid debug info");
     }
   }
 
-  private void build(ThrowingConsumer<Builder, Exception> input, ProgramConsumer consumer)
-      throws Exception {
-    Builder builder =
-        R8Command.builder()
-            .setMode(CompilationMode.DEBUG)
-            .setDisableTreeShaking(true)
-            .setDisableMinification(true)
-            .addLibraryFiles(ToolHelper.getJava8RuntimeJar())
-            .setProgramConsumer(consumer);
-    input.accept(builder);
-    ToolHelper.runR8(builder.build(), o -> o.invalidDebugInfoFatal = true);
+  private R8TestBuilder builder() {
+    return testForR8(Backend.CF)
+        .debug()
+        .noTreeShaking()
+        .noMinification()
+        .addOptionsModification(o -> o.invalidDebugInfoFatal = true);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/classlookup/LibraryClassExtendsProgramClassTest.java b/src/test/java/com/android/tools/r8/classlookup/LibraryClassExtendsProgramClassTest.java
index a33a5ad..0b5a60b 100644
--- a/src/test/java/com/android/tools/r8/classlookup/LibraryClassExtendsProgramClassTest.java
+++ b/src/test/java/com/android/tools/r8/classlookup/LibraryClassExtendsProgramClassTest.java
@@ -9,9 +9,8 @@
 import static org.junit.Assert.fail;
 
 import com.android.tools.r8.CompilationFailedException;
-import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestCompileResult;
 import com.android.tools.r8.jasmin.JasminBuilder;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import java.util.List;
@@ -45,7 +44,7 @@
 
   @Test
   public void testCompatibilityModeWarning() throws Exception {
-    TestCompileResult<R8TestRunResult> result = testForR8Compat(Backend.DEX)
+    R8TestCompileResult result = testForR8Compat(Backend.DEX)
         .setMinApi(AndroidApiLevel.O)
         .addProgramClassFileData(junitClasses)
         .addKeepAllClassesRule()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/NonNullParamTest.java b/src/test/java/com/android/tools/r8/ir/optimize/NonNullParamTest.java
index 10b9a63..518ea8d 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/NonNullParamTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/NonNullParamTest.java
@@ -92,7 +92,7 @@
 
     MethodSubject selfCheck = mainSubject.method("void", "selfCheck", ImmutableList.of());
     assertThat(selfCheck, isPresent());
-    assertEquals(0, countCallToParamNullCheck(selfCheck));
+    assertEquals(1, countCallToParamNullCheck(selfCheck));
     assertEquals(1, countPrintCall(selfCheck));
     assertEquals(0, countThrow(selfCheck));
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
index 9380115..19c931e 100644
--- a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
+++ b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
@@ -146,16 +146,16 @@
   protected MethodSubject checkMethodIsKept(ClassSubject classSubject,
       MethodSignature methodSignature) {
     checkMethodPresenceInInput(classSubject.getOriginalName(), methodSignature, true);
-    return checkMethodisKeptOrRemoved(classSubject, methodSignature, true);
+    return checkMethodIsKeptOrRemoved(classSubject, methodSignature, true);
   }
 
   protected void checkMethodIsRemoved(ClassSubject classSubject,
       MethodSignature methodSignature) {
     checkMethodPresenceInInput(classSubject.getOriginalName(), methodSignature, true);
-    checkMethodisKeptOrRemoved(classSubject, methodSignature, false);
+    checkMethodIsKeptOrRemoved(classSubject, methodSignature, false);
   }
 
-  protected MethodSubject checkMethodisKeptOrRemoved(ClassSubject classSubject,
+  protected MethodSubject checkMethodIsKeptOrRemoved(ClassSubject classSubject,
       MethodSignature methodSignature, boolean isPresent) {
     checkMethodPresenceInInput(classSubject.getOriginalName(), methodSignature, true);
     return checkMethodPresenceInOutput(classSubject, methodSignature, isPresent);
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
index 1d3f25e..1a690e2 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
+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.InstructionSubject;
@@ -212,7 +213,13 @@
   protected void runTest(String folder, String mainClass,
       boolean enabled, AndroidAppInspector inspector) throws Exception {
     runTest(
-        folder, mainClass, null,
+        folder, mainClass,
+        // TODO(jsjeon): Introduce @NeverInline to kotlinR8TestResources
+        StringUtils.lines(
+            "-neverinline class * { void test*State*(...); }",
+            "-neverinline class * { void testBigExtraMethod(...); }",
+            "-neverinline class * { void testBigExtraMethodReturningLambda(...); }"
+        ),
         options -> {
           options.enableInlining = true;
           options.enableClassInlining = enabled;
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
index 58eae89..9224405 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
@@ -49,8 +49,8 @@
 
       // Getters should be removed after inlining, which is possible only if access is relaxed.
       final boolean areGetterPresent = !allowAccessModification;
-      checkMethodisKeptOrRemoved(dataClass, NAME_GETTER_METHOD, areGetterPresent);
-      checkMethodisKeptOrRemoved(dataClass, AGE_GETTER_METHOD, areGetterPresent);
+      checkMethodIsKeptOrRemoved(dataClass, NAME_GETTER_METHOD, areGetterPresent);
+      checkMethodIsKeptOrRemoved(dataClass, AGE_GETTER_METHOD, areGetterPresent);
 
       // No use of componentN functions.
       checkMethodIsRemoved(dataClass, COMPONENT1_METHOD);
@@ -85,8 +85,8 @@
       // ComponentN functions should be removed after inlining, which is possible only if access
       // is relaxed.
       final boolean areComponentMethodsPresent = !allowAccessModification;
-      checkMethodisKeptOrRemoved(dataClass, COMPONENT1_METHOD, areComponentMethodsPresent);
-      checkMethodisKeptOrRemoved(dataClass, COMPONENT2_METHOD, areComponentMethodsPresent);
+      checkMethodIsKeptOrRemoved(dataClass, COMPONENT1_METHOD, areComponentMethodsPresent);
+      checkMethodIsKeptOrRemoved(dataClass, COMPONENT2_METHOD, areComponentMethodsPresent);
 
       // No use of getter.
       checkMethodIsRemoved(dataClass, NAME_GETTER_METHOD);
@@ -118,7 +118,7 @@
       ClassSubject dataClass = checkClassIsKept(codeInspector, TEST_DATA_CLASS.getClassName());
 
       boolean component2IsPresent = !allowAccessModification;
-      checkMethodisKeptOrRemoved(dataClass, COMPONENT2_METHOD, component2IsPresent);
+      checkMethodIsKeptOrRemoved(dataClass, COMPONENT2_METHOD, component2IsPresent);
 
       // Function component1 is not used.
       checkMethodIsRemoved(dataClass, COMPONENT1_METHOD);
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
index c004828..ac75230 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
@@ -735,10 +735,8 @@
       MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
       MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
 
-      // Getter and setter cannot be inlined when we don't know if null check semantic is
-      // preserved.
-      checkMethodIsKept(objectClass, getter);
-      checkMethodIsKept(objectClass, setter);
+      checkMethodIsRemoved(objectClass, getter);
+      checkMethodIsRemoved(objectClass, setter);
       assertTrue(fieldSubject.getField().accessFlags.isPublic());
     });
   }
@@ -758,10 +756,8 @@
       MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
       MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
 
-      // Getter and setter cannot be inlined when we don't know if null check semantic is
-      // preserved.
-      checkMethodIsKept(objectClass, getter);
-      checkMethodIsKept(objectClass, setter);
+      checkMethodIsRemoved(objectClass, getter);
+      checkMethodIsRemoved(objectClass, setter);
       assertTrue(fieldSubject.getField().accessFlags.isPublic());
     });
   }
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/DesugarLambdaRetraceTest.java b/src/test/java/com/android/tools/r8/naming/retrace/DesugarLambdaRetraceTest.java
index 1868940..4c43c7c 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/DesugarLambdaRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/DesugarLambdaRetraceTest.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.naming.retrace;
 
 import static com.android.tools.r8.naming.retrace.StackTrace.isSameExceptForFileName;
+import static com.android.tools.r8.naming.retrace.StackTrace.isSameExceptForFileNameAndLineNumber;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertThat;
 
@@ -12,6 +13,7 @@
 import com.android.tools.r8.naming.retrace.StackTrace.StackTraceLine;
 import com.google.common.collect.ImmutableList;
 import java.util.Collection;
+import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -40,6 +42,8 @@
   }
 
   private int expectedActualStackTraceHeight() {
+    // In debug mode the expected stack trace height differs since there is no lambda desugaring
+    // for CF.
     return mode == CompilationMode.RELEASE ? 2 : (backend == Backend.CF ? 4 : 5);
   }
 
@@ -47,31 +51,117 @@
     return line.className.contains("-$$Lambda$");
   }
 
+  private void checkLambdaFrame(StackTrace retracedStackTrace) {
+    StackTrace lambdaFrames = retracedStackTrace.filter(this::isSynthesizedLambdaFrame);
+    assertEquals(1, lambdaFrames.size());
+    if (lambdaFrames.get(0).hasLineNumber()) {
+      assertEquals(mode == CompilationMode.RELEASE ? 0 : 2, lambdaFrames.get(0).lineNumber);
+    }
+    // Proguard retrace will take the class name until the first $ to construct the file
+    // name, so for "-$$Lambda$...", the file name becomes "-.java".
+    assertEquals("-.java", lambdaFrames.get(0).fileName);
+  }
+
+  private void checkIsSameExceptForFileName(
+      StackTrace actualStackTrace, StackTrace retracedStackTrace) {
+    // Even when SourceFile is present retrace replaces the file name in the stack trace.
+    if (backend == Backend.CF) {
+      // TODO(122440196): Additional code to locate issue.
+      if (!isSameExceptForFileName(expectedStackTrace).matches(retracedStackTrace)) {
+        System.out.println("Expected original:");
+        System.out.println(expectedStackTrace.getOriginalStderr());
+        System.out.println("Actual original:");
+        System.out.println(retracedStackTrace.getOriginalStderr());
+        System.out.println("Parsed original:");
+        System.out.println(expectedStackTrace);
+        System.out.println("Parsed retraced:");
+        System.out.println(retracedStackTrace);
+      }
+      assertThat(retracedStackTrace, isSameExceptForFileName(expectedStackTrace));
+    } else {
+      // TODO(122440196): Additional code to locate issue.
+      if (!isSameExceptForFileName(expectedStackTrace)
+          .matches(retracedStackTrace.filter(line -> !isSynthesizedLambdaFrame(line)))) {
+        System.out.println("Expected original:");
+        System.out.println(expectedStackTrace.getOriginalStderr());
+        System.out.println("Actual original:");
+        System.out.println(retracedStackTrace.getOriginalStderr());
+        System.out.println("Parsed original:");
+        System.out.println(expectedStackTrace);
+        System.out.println("Parsed retraced:");
+        System.out.println(retracedStackTrace);
+      }
+      // With the frame from the lambda class filtered out the stack trace is the same.
+      assertThat(
+          retracedStackTrace.filter(line -> !isSynthesizedLambdaFrame(line)),
+          isSameExceptForFileName(expectedStackTrace));
+      // Check the frame from the lambda class.
+      checkLambdaFrame(retracedStackTrace);
+    }
+    assertEquals(expectedActualStackTraceHeight(), actualStackTrace.size());
+  }
+
+  private void checkIsSameExceptForFileNameAndLineNumber(
+      StackTrace actualStackTrace, StackTrace retracedStackTrace) {
+    // Even when SourceFile is present retrace replaces the file name in the stack trace.
+    if (backend == Backend.CF) {
+      // TODO(122440196): Additional code to locate issue.
+      if (!isSameExceptForFileNameAndLineNumber(expectedStackTrace).matches(retracedStackTrace)) {
+        System.out.println("Expected original:");
+        System.out.println(expectedStackTrace.getOriginalStderr());
+        System.out.println("Actual original:");
+        System.out.println(retracedStackTrace.getOriginalStderr());
+        System.out.println("Parsed original:");
+        System.out.println(expectedStackTrace);
+        System.out.println("Parsed retraced:");
+        System.out.println(retracedStackTrace);
+      }
+      assertThat(retracedStackTrace, isSameExceptForFileNameAndLineNumber(expectedStackTrace));
+    } else {
+      // TODO(122440196): Additional code to locate issue.
+      if (!isSameExceptForFileNameAndLineNumber(expectedStackTrace)
+          .matches(retracedStackTrace.filter(line -> !isSynthesizedLambdaFrame(line)))) {
+        System.out.println("Expected original:");
+        System.out.println(expectedStackTrace.getOriginalStderr());
+        System.out.println("Actual original:");
+        System.out.println(retracedStackTrace.getOriginalStderr());
+        System.out.println("Parsed original:");
+        System.out.println(expectedStackTrace);
+        System.out.println("Parsed retraced:");
+        System.out.println(retracedStackTrace);
+      }
+      // With the frame from the lambda class filtered out the stack trace is the same.
+      assertThat(
+          retracedStackTrace.filter(line -> !isSynthesizedLambdaFrame(line)),
+          isSameExceptForFileNameAndLineNumber(expectedStackTrace));
+      // Check the frame from the lambda class.
+      checkLambdaFrame(retracedStackTrace);
+    }
+    assertEquals(expectedActualStackTraceHeight(), actualStackTrace.size());
+  }
+
   @Test
+  @Ignore("b/122440196")
   public void testSourceFileAndLineNumberTable() throws Exception {
     runTest(
-        "-keepattributes SourceFile,LineNumberTable\n-printmapping",
-        (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
-          // Even when SourceFile is present retrace replaces the file name in the stack trace.
-          if (backend == Backend.CF) {
-            assertThat(retracedStackTrace, isSameExceptForFileName(expectedStackTrace));
-          } else {
-            // With the frame from the lambda class filtered out the stack trace is teh same.
-            assertThat(
-                retracedStackTrace.filter(line -> !isSynthesizedLambdaFrame(line)),
-                isSameExceptForFileName(expectedStackTrace));
-            // Check the frame from the lambda class.
-            StackTrace lambdaFrames = retracedStackTrace.filter(this::isSynthesizedLambdaFrame);
-            assertEquals(1, lambdaFrames.size());
-            if (lambdaFrames.get(0).hasLineNumber()) {
-              assertEquals(mode == CompilationMode.RELEASE ? 0 : 2, lambdaFrames.get(0).lineNumber);
-            }
-            // Proguard retrace will take the class name until the first $ to construct the file
-            // name, so for "-$$Lambda$...", the file name becomes "-.java".
-            assertEquals("-.java", lambdaFrames.get(0).fileName);
-          }
-          assertEquals(expectedActualStackTraceHeight(), actualStackTrace.size());
-        });
+        ImmutableList.of("-keepattributes SourceFile,LineNumberTable"),
+        this::checkIsSameExceptForFileName);
+  }
+
+  @Test
+  @Ignore("b/122440196")
+  public void testLineNumberTableOnly() throws Exception {
+    runTest(
+        ImmutableList.of("-keepattributes LineNumberTable"),
+        this::checkIsSameExceptForFileName);
+  }
+
+  @Test
+  @Ignore("b/122440196")
+  public void testNoLineNumberTable() throws Exception {
+    runTest(
+        ImmutableList.of(),
+        this::checkIsSameExceptForFileNameAndLineNumber);
   }
 }
 
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/DesugarStaticInterfaceMethodsRetraceTest.java b/src/test/java/com/android/tools/r8/naming/retrace/DesugarStaticInterfaceMethodsRetraceTest.java
new file mode 100644
index 0000000..8114108
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/retrace/DesugarStaticInterfaceMethodsRetraceTest.java
@@ -0,0 +1,76 @@
+// 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.naming.retrace;
+
+import static com.android.tools.r8.naming.retrace.StackTrace.isSameExceptForFileName;
+import static org.junit.Assert.assertThat;
+
+import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.NeverInline;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+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 DesugarStaticInterfaceMethodsRetraceTest extends RetraceTestBase {
+
+  @Parameters(name = "Backend: {0}, mode: {1}")
+  public static Collection<Object[]> data() {
+    return buildParameters(Backend.values(), CompilationMode.values());
+  }
+
+  public DesugarStaticInterfaceMethodsRetraceTest(Backend backend, CompilationMode mode) {
+    super(backend, mode);
+  }
+
+  @Override
+  public Collection<Class<?>> getClasses() {
+    return ImmutableList.of(
+        getMainClass(), InterfaceWithStaticMethod1.class, InterfaceWithStaticMethod2.class);
+  }
+
+  @Override
+  public Class<?> getMainClass() {
+    return MainDesugarStaticInterfaceMethodsRetraceTest.class;
+  }
+
+  @Test
+  public void testSourceFileAndLineNumberTable() throws Exception {
+    runTest(
+        ImmutableList.of("-keepattributes SourceFile,LineNumberTable"),
+        // For the desugaring to companion classes the retrace stacktrace is still the same
+        // as the mapping file has a fully qualified class name in the method mapping, e.g.:
+        //
+        //   com.android.tools.r8.naming.retrace.InterfaceWithDefaultMethod1$-CC -> com.android.tools.r8.naming.retrace.a:
+        //       1:1:void com.android.tools.r8.naming.retrace.InterfaceWithDefaultMethod1.defaultMethod1():80:80 -> a
+        (StackTrace actualStackTrace, StackTrace retracedStackTrace) ->
+          assertThat(retracedStackTrace, isSameExceptForFileName(expectedStackTrace)));
+  }
+}
+
+interface InterfaceWithStaticMethod2 {
+
+  static void staticMethod1() {
+    throw null;
+  }
+}
+
+interface InterfaceWithStaticMethod1 {
+
+  @NeverInline
+  static void staticMethod2() {
+    InterfaceWithStaticMethod2.staticMethod1();
+  }
+}
+
+class MainDesugarStaticInterfaceMethodsRetraceTest {
+
+  public static void main(String[] args) {
+    InterfaceWithStaticMethod1.staticMethod2();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/InliningRetraceTest.java b/src/test/java/com/android/tools/r8/naming/retrace/InliningRetraceTest.java
index 6d7ce25..c252426 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/InliningRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/InliningRetraceTest.java
@@ -11,6 +11,7 @@
 
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.ForceInline;
+import com.google.common.collect.ImmutableList;
 import java.util.Collection;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -41,7 +42,7 @@
   @Test
   public void testSourceFileAndLineNumberTable() throws Exception {
     runTest(
-        "-keepattributes SourceFile,LineNumberTable",
+        ImmutableList.of("-keepattributes SourceFile,LineNumberTable"),
         (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
           // Even when SourceFile is present retrace replaces the file name in the stack trace.
           assertThat(retracedStackTrace, isSameExceptForFileName(expectedStackTrace));
@@ -52,7 +53,7 @@
   @Test
   public void testLineNumberTableOnly() throws Exception {
     runTest(
-        "-keepattributes LineNumberTable",
+        ImmutableList.of("-keepattributes LineNumberTable"),
         (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
           assertThat(retracedStackTrace, isSameExceptForFileName(expectedStackTrace));
           assertEquals(expectedActualStackTraceHeight(), actualStackTrace.size());
@@ -62,7 +63,7 @@
   @Test
   public void testNoLineNumberTable() throws Exception {
     runTest(
-        "",
+        ImmutableList.of(),
         (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
           assertThat(retracedStackTrace, isSameExceptForFileNameAndLineNumber(expectedStackTrace));
           assertEquals(expectedActualStackTraceHeight(), actualStackTrace.size());
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 6243e26..2258d67 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
@@ -9,6 +9,7 @@
 import com.android.tools.r8.TestBase;
 import com.google.common.collect.ImmutableList;
 import java.util.Collection;
+import java.util.List;
 import java.util.function.BiConsumer;
 import org.junit.Before;
 
@@ -40,7 +41,7 @@
             .map(StackTrace::extractFromJvm);
   }
 
-  public void runTest(String keepRule, BiConsumer<StackTrace, StackTrace> checker)
+  public void runTest(List<String> keepRules, BiConsumer<StackTrace, StackTrace> checker)
       throws Exception {
 
     R8TestRunResult result =
@@ -50,7 +51,7 @@
             .enableInliningAnnotations()
             .addProgramClasses(getClasses())
             .addKeepMainRule(getMainClass())
-            .addKeepRules(keepRule)
+            .addKeepRules(keepRules)
             .run(getMainClass())
             .assertFailure();
 
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 9876c95..0ecd3e4 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
@@ -113,10 +113,12 @@
   }
 
   private final List<StackTraceLine> stackTraceLines;
+  private final String originalStderr;
 
-  private StackTrace(List<StackTraceLine> stackTraceLines) {
+  private StackTrace(List<StackTraceLine> stackTraceLines, String originalStderr) {
     assert stackTraceLines.size() > 0;
     this.stackTraceLines = stackTraceLines;
+    this.originalStderr = originalStderr;
   }
 
   public int size() {
@@ -127,6 +129,10 @@
     return stackTraceLines.get(index);
   }
 
+  public String getOriginalStderr() {
+    return originalStderr;
+  }
+
   public static StackTrace extractFromArt(String stderr) {
     List<StackTraceLine> stackTraceLines = new ArrayList<>();
     List<String> stderrLines = StringUtils.splitLines(stderr);
@@ -165,15 +171,18 @@
     for (int i = first; i < last; i++) {
       stackTraceLines.add(StackTraceLine.parse(stderrLines.get(i)));
     }
-    return new StackTrace(stackTraceLines);
+    return new StackTrace(stackTraceLines, stderr);
+  }
+
+  private static List<StackTraceLine> internalExtractFromJvm(String stderr) {
+    return StringUtils.splitLines(stderr).stream()
+        .filter(s -> s.startsWith(TAB_AT_PREFIX))
+        .map(StackTraceLine::parse)
+        .collect(Collectors.toList());
   }
 
   public static StackTrace extractFromJvm(String stderr) {
-    return new StackTrace(
-        StringUtils.splitLines(stderr).stream()
-            .filter(s -> s.startsWith(TAB_AT_PREFIX))
-            .map(StackTraceLine::parse)
-            .collect(Collectors.toList()));
+    return new StackTrace(internalExtractFromJvm(stderr), stderr);
   }
 
   public static StackTrace extractFromJvm(TestRunResult result) {
@@ -188,11 +197,15 @@
     FileUtils.writeTextFile(
         stackTraceFile,
         stackTraceLines.stream().map(line -> line.originalLine).collect(Collectors.toList()));
-    return StackTrace.extractFromJvm(ToolHelper.runRetrace(mapFile, stackTraceFile));
+    // Keep the original stderr in the retraced stacktrace.
+    return new StackTrace(
+        internalExtractFromJvm(ToolHelper.runRetrace(mapFile, stackTraceFile)), originalStderr);
   }
 
   public StackTrace filter(Predicate<StackTraceLine> filter) {
-    return new StackTrace(stackTraceLines.stream().filter(filter).collect(Collectors.toList()));
+    return new StackTrace(
+        stackTraceLines.stream().filter(filter).collect(Collectors.toList()),
+        originalStderr);
   }
 
   @Override
@@ -395,7 +408,9 @@
 
     @Override
     public void describeMismatchSafely(final StackTrace stackTrace, Description description) {
-      description.appendText("stacktrace was " + stackTrace);
+      description.appendText("stacktrace was");
+      description.appendText(System.lineSeparator());
+      description.appendText(stackTrace.toString());
       description.appendText(System.lineSeparator());
       if (expected.size() != stackTrace.size()) {
         description.appendText("They have different sizes.");
diff --git a/src/test/java/com/android/tools/r8/shaking/PrintConfigurationTest.java b/src/test/java/com/android/tools/r8/shaking/PrintConfigurationTest.java
index 7621522..757e06f 100644
--- a/src/test/java/com/android/tools/r8/shaking/PrintConfigurationTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/PrintConfigurationTest.java
@@ -4,10 +4,14 @@
 
 package com.android.tools.r8.shaking;
 
+import static org.hamcrest.core.StringContains.containsString;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
 
+import com.android.tools.r8.ExternalR8TestCompileResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.StringUtils;
 import com.google.common.base.Charsets;
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
@@ -15,23 +19,77 @@
 
 class PrintConfigurationTestClass {
 
-  public static void main(String[] args) {
-
-  }
+  public static void main(String[] args) {}
 }
 
 public class PrintConfigurationTest extends TestBase {
+
   @Test
-  public void testSingleCOnfiguration() throws Exception {
-    Class mainClass = PrintConfigurationTestClass.class;
-    String proguardConfig = keepMainProguardConfiguration(mainClass);
+  public void testSingleConfigurationWithAbsolutePath() throws Exception {
     Path printConfigurationFile = temp.newFile().toPath();
-    proguardConfig += "\n-printconfiguration " + printConfigurationFile.toString();
-    compileWithR8(ImmutableList.of(mainClass), proguardConfig);
+    String proguardConfig =
+        StringUtils.lines(
+            keepMainProguardConfiguration(PrintConfigurationTestClass.class),
+            "-printconfiguration " + printConfigurationFile);
+    testForR8(Backend.DEX)
+        .addProgramClasses(PrintConfigurationTestClass.class)
+        .addKeepRules(proguardConfig)
+        .compile();
     assertEquals(proguardConfig, FileUtils.readTextFile(printConfigurationFile, Charsets.UTF_8));
   }
 
   @Test
+  public void testSingleConfigurationWithRelativePath() throws Exception {
+    Path subDirectory = temp.newFolder().toPath();
+    Path proguardConfigFile = subDirectory.resolve("proguard-config.txt");
+    Path proguardConfigOutFile = subDirectory.resolve("proguard-config-out.txt");
+
+    String proguardConfig =
+        StringUtils.lines(
+            keepMainProguardConfiguration(PrintConfigurationTestClass.class),
+            "-printconfiguration proguard-config-out.txt");
+    FileUtils.writeTextFile(proguardConfigFile, proguardConfig.trim());
+
+    ExternalR8TestCompileResult result =
+        testForExternalR8(Backend.DEX)
+            .addProgramClasses(PrintConfigurationTestClass.class)
+            .addKeepRuleFiles(proguardConfigFile)
+            .compile();
+
+    assertEquals(proguardConfig, FileUtils.readTextFile(proguardConfigOutFile, Charsets.UTF_8));
+  }
+
+  @Test
+  public void testSingleConfigurationWithRelativePathCompatibility() throws Exception {
+    Path subDirectory = temp.newFolder().toPath();
+    Path proguardConfigFile = subDirectory.resolve("proguard-config.txt");
+    Path proguardConfigOutFile = subDirectory.resolve("proguard-config-out.txt");
+
+    String proguardConfig =
+        StringUtils.lines(
+            keepMainProguardConfiguration(PrintConfigurationTestClass.class),
+            "-printconfiguration proguard-config-out.txt");
+    FileUtils.writeTextFile(proguardConfigFile, proguardConfig);
+
+    testForProguard()
+        .addProgramClasses(PrintConfigurationTestClass.class)
+        .addKeepRuleFiles(proguardConfigFile)
+        .compile();
+
+    String proguardConfigOut = FileUtils.readTextFile(proguardConfigOutFile, Charsets.UTF_8);
+    assertThat(
+        proguardConfigOut,
+        containsString(
+            StringUtils.lines(
+                "-keep class com.android.tools.r8.shaking.PrintConfigurationTestClass {",
+                "    public static void main(java.lang.String[]);",
+                "}")));
+    assertThat(
+        proguardConfigOut,
+        containsString("-printconfiguration " + proguardConfigOutFile.toString()));
+  }
+
+  @Test
   public void testIncludeFile() throws Exception {
     Class mainClass = PrintConfigurationTestClass.class;
     String includeProguardConfig = keepMainProguardConfiguration(mainClass);
diff --git a/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptByAnnotatedMethodTestRunner.java b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptByAnnotatedMethodTestRunner.java
index 19cdf04..a7e3e46 100644
--- a/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptByAnnotatedMethodTestRunner.java
+++ b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptByAnnotatedMethodTestRunner.java
@@ -86,7 +86,7 @@
         .method(fooMethod)
         .assertNotRenamed()
         .assertNotInvokedFrom(mainMethod)
-        // TODO(b/122297131): keepAnnotatedMethods should also be keeping foo alive!
+        .assertKeptBy(keepAnnotatedMethods)
         .assertKeptBy(keepClassesOfAnnotatedMethods);
 
     // Check baz is removed.
diff --git a/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptByTwoRulesTest.java b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptByTwoRulesTest.java
new file mode 100644
index 0000000..7cb464d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptByTwoRulesTest.java
@@ -0,0 +1,18 @@
+// 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.keptgraph;
+
+import com.android.tools.r8.Keep;
+
+@Keep
+public class KeptByTwoRulesTest {
+
+  public static void foo() {
+    System.out.println("called foo");
+  }
+
+  public static void main(String[] args) {
+    foo();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptByTwoRulesTestRunner.java b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptByTwoRulesTestRunner.java
new file mode 100644
index 0000000..38c3f00
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/keptgraph/KeptByTwoRulesTestRunner.java
@@ -0,0 +1,87 @@
+// 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.keptgraph;
+
+import static com.android.tools.r8.references.Reference.classFromClass;
+import static com.android.tools.r8.references.Reference.methodFromMethod;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.graphinspector.GraphInspector;
+import com.android.tools.r8.utils.graphinspector.GraphInspector.QueryNode;
+import java.util.Arrays;
+import java.util.Collection;
+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 KeptByTwoRulesTestRunner extends TestBase {
+
+  private static final Class<?> CLASS = KeptByTwoRulesTest.class;
+  private static final Collection<Class<?>> CLASSES = Arrays.asList(CLASS);
+
+  private final String EXPECTED = StringUtils.lines("called foo");
+
+  private final Backend backend;
+
+  @Parameters(name = "{0}")
+  public static Backend[] data() {
+    return Backend.values();
+  }
+
+  public KeptByTwoRulesTestRunner(Backend backend) {
+    this.backend = backend;
+  }
+
+  @Test
+  public void test() throws Exception {
+    MethodReference mainMethod = methodFromMethod(CLASS.getDeclaredMethod("main", String[].class));
+    MethodReference fooMethod = methodFromMethod(CLASS.getDeclaredMethod("foo"));
+
+    if (backend == Backend.CF) {
+      testForJvm().addProgramClasses(CLASSES).run(CLASS).assertSuccessWithOutput(EXPECTED);
+    }
+
+    String keepPublicRule = "-keep @com.android.tools.r8.Keep class * {  public *; }";
+    String keepFooRule = "-keep class " + CLASS.getTypeName() + " { public void foo(); }";
+    GraphInspector inspector =
+        testForR8(backend)
+            .enableGraphInspector()
+            .enableInliningAnnotations()
+            .addProgramClasses(CLASSES)
+            .addKeepRules(keepPublicRule, keepFooRule)
+            .run(CLASS)
+            .assertSuccessWithOutput(EXPECTED)
+            .graphInspector();
+
+    assertEquals(2, inspector.getRoots().size());
+    QueryNode keepPublic = inspector.rule(keepPublicRule).assertRoot();
+    QueryNode keepFoo = inspector.rule(keepFooRule).assertRoot();
+
+    inspector
+        .method(mainMethod)
+        .assertNotRenamed()
+        .assertKeptBy(keepPublic)
+        .assertNotKeptBy(keepFoo);
+
+    // Check foo is called from main and kept by two rules.
+    inspector
+        .method(fooMethod)
+        .assertNotRenamed()
+        .assertInvokedFrom(mainMethod)
+        .assertKeptBy(keepPublic)
+        .assertKeptBy(keepFoo);
+
+    // Check the class is also kept by both rules.
+    inspector
+        .clazz(classFromClass(CLASS))
+        .assertNotRenamed()
+        .assertKeptBy(keepPublic)
+        .assertKeptBy(keepFoo);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/graphinspector/GraphInspector.java b/src/test/java/com/android/tools/r8/utils/graphinspector/GraphInspector.java
index 107978b..940482b 100644
--- a/src/test/java/com/android/tools/r8/utils/graphinspector/GraphInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/graphinspector/GraphInspector.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils.graphinspector;
 
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
@@ -113,16 +114,26 @@
     }
 
     public QueryNode assertNotInvokedFrom(MethodReference method) {
-      assertTrue(
-          errorMessage("no invocation from " + method.toString(), "invoke"),
-          !isInvokedFrom(method));
+      assertFalse(
+          errorMessage("no invocation from " + method.toString(), "invoke"), isInvokedFrom(method));
       return this;
     }
 
     public QueryNode assertKeptBy(QueryNode node) {
-      assertTrue("Invalid call to assertKeptBy with: " + node.getNodeDescription(),
-          node.isPresent());
-      assertTrue(errorMessage("kept by " + node.getNodeDescription(), "none"), isKeptBy(node));
+      assertTrue(
+          "Invalid call to assertKeptBy with: " + node.getNodeDescription(), node.isPresent());
+      assertTrue(
+          errorMessage("kept by " + node.getNodeDescription(), "was not kept by it"),
+          isKeptBy(node));
+      return this;
+    }
+
+    public QueryNode assertNotKeptBy(QueryNode node) {
+      assertTrue(
+          "Invalid call to assertNotKeptBy with: " + node.getNodeDescription(), node.isPresent());
+      assertFalse(
+          errorMessage("not kept by " + node.getNodeDescription(), "was kept by it"),
+          isKeptBy(node));
       return this;
     }
   }
diff --git a/src/test/kotlinR8TestResources/class_inliner_lambda_j_style/main.kt b/src/test/kotlinR8TestResources/class_inliner_lambda_j_style/main.kt
index ebf26cd..f95125c 100644
--- a/src/test/kotlinR8TestResources/class_inliner_lambda_j_style/main.kt
+++ b/src/test/kotlinR8TestResources/class_inliner_lambda_j_style/main.kt
@@ -16,7 +16,6 @@
     testStateful3()
 }
 
-@Synchronized
 fun testStateless() {
     SamIface.Consumer.consume { "123" }
     SamIface.Consumer.consume { "ABC" }
@@ -31,7 +30,6 @@
     }
 }
 
-@Synchronized
 fun testStateful() {
     var someVariable = 0
 
@@ -62,7 +60,6 @@
     }
 }
 
-@Synchronized
 fun testStateful2() {
     var someVariable = 0
     SamIface.Consumer.consumeBig {
@@ -80,7 +77,6 @@
     }
 }
 
-@Synchronized
 fun testStateful3() {
     var someVariable = 0
     SamIface.Consumer.consumeBig {
diff --git a/src/test/kotlinR8TestResources/class_inliner_lambda_k_style/main.kt b/src/test/kotlinR8TestResources/class_inliner_lambda_k_style/main.kt
index 06b937e..ea315ba 100644
--- a/src/test/kotlinR8TestResources/class_inliner_lambda_k_style/main.kt
+++ b/src/test/kotlinR8TestResources/class_inliner_lambda_k_style/main.kt
@@ -17,14 +17,12 @@
 
 data class Record(val foo: String, val good: Boolean)
 
-@Synchronized
 fun testKotlinSequencesStateless(strings: Sequence<String>) {
     useRecord()
     // Stateless k-style lambda
     strings.map { Record(it, false) }.forEach { println(it) }
 }
 
-@Synchronized
 fun testKotlinSequencesStateful(a: Int, b: Int, strings: Sequence<String>) {
     useRecord()
     // Big stateful k-style lambda
@@ -40,7 +38,6 @@
     }
 }
 
-@Synchronized
 fun testBigExtraMethod() {
     useRecord()
     bigUserWithNotNullChecksAndTwoCalls(next()) { next() }
@@ -62,7 +59,6 @@
     return "$id: ${lambda()}"
 }
 
-@Synchronized
 fun testBigExtraMethodReturningLambda() {
     useRecord()
     bigUserReturningLambda(next()) { next() } // Not used
diff --git a/tools/run_on_app.py b/tools/run_on_app.py
index 31cecfc..cba1d6e 100755
--- a/tools/run_on_app.py
+++ b/tools/run_on_app.py
@@ -22,8 +22,9 @@
 
 TYPES = ['dex', 'deploy', 'proguarded']
 APPS = ['gmscore', 'nest', 'youtube', 'gmail', 'chrome']
-COMPILERS = ['d8', 'r8', 'r8lib-r8', 'r8lib-d8']
-R8_COMPILERS = ['r8', 'r8lib-r8']
+COMPILERS = ['d8', 'r8']
+COMPILER_BUILDS = ['full', 'lib']
+
 # We use this magic exit code to signal that the program OOM'ed
 OOM_EXIT_CODE = 42
 
@@ -32,6 +33,10 @@
   result.add_option('--compiler',
                     help='The compiler to use',
                     choices=COMPILERS)
+  result.add_option('--compiler-build',
+                    help='Compiler build to use',
+                    choices=COMPILER_BUILDS,
+                    default='lib')
   result.add_option('--app',
                     help='What app to run on',
                     choices=APPS)
@@ -152,18 +157,20 @@
   assert len(args) == 0
   for name, version, type, use_r8lib in get_permutations():
     compiler = 'r8' if type == 'deploy' else 'd8'
-    if use_r8lib:
-      compiler = 'r8lib-' + compiler
-    print('Executing %s with %s %s %s' % (compiler, name, version, type))
+    compiler_build = 'lib' if use_r8lib else 'full'
+    print('Executing %s/%s with %s %s %s' % (compiler, compiler_build, name,
+      version, type))
 
     fixed_options = copy.copy(options)
     fixed_options.app = name
     fixed_options.version = version
     fixed_options.compiler = compiler
+    fixed_options.compiler_build = compiler_build
     fixed_options.type = type
     exit_code = run_with_options(fixed_options, [])
     if exit_code != 0:
-      print('Failed %s %s %s with %s' % (name, version, type, compiler))
+      print('Failed %s %s %s with %s/%s' % (name, version, type, compiler,
+        compiler_build))
       exit(exit_code)
 
 def find_min_xmx(options, args):
@@ -208,7 +215,9 @@
     return find_min_xmx(options, args)
   return run_with_options(options, args)
 
-def run_with_options(options, args, extra_args=[]):
+def run_with_options(options, args, extra_args=None):
+  if extra_args is None:
+    extra_args = []
   app_provided_pg_conf = False;
   # todo(121018500): remove when memory is under control
   extra_args.append('-Xmx8G')
@@ -239,6 +248,10 @@
     raise Exception("You need to specify '--compiler={}'"
         .format('|'.join(COMPILERS)))
 
+  if options.compiler_build not in COMPILER_BUILDS:
+    raise Exception("You need to specify '--compiler-build={}'"
+        .format('|'.join(COMPILER_BUILDS)))
+
   if not options.version in data.VERSIONS.keys():
     print('No version {} for application {}'
         .format(options.version, options.app))
@@ -248,7 +261,7 @@
   version = data.VERSIONS[options.version]
 
   if not options.type:
-    options.type = 'deploy' if options.compiler in R8_COMPILERS \
+    options.type = 'deploy' if options.compiler == 'r8' \
         else 'proguarded'
 
   if options.type not in version:
@@ -260,7 +273,7 @@
   # For R8 'deploy' the JAR is located using the Proguard configuration
   # -injars option. For chrome and nest we don't have the injars in the
   # proguard files.
-  if 'inputs' in values and (options.compiler not in R8_COMPILERS
+  if 'inputs' in values and (options.compiler != 'r8'
                              or options.type != 'deploy'
                              or options.app == 'chrome'
                              or options.app == 'nest'):
@@ -273,7 +286,7 @@
   if 'main-dex-list' in values:
     args.extend(['--main-dex-list', values['main-dex-list']])
 
-  if options.compiler in R8_COMPILERS:
+  if options.compiler == 'r8':
     if 'pgconf' in values and not options.k:
       for pgconf in values['pgconf']:
         args.extend(['--pg-conf', pgconf])
@@ -297,7 +310,7 @@
   # Additional flags for the compiler from the configuration file.
   if 'flags' in values:
     args.extend(values['flags'].split(' '))
-  if options.compiler in R8_COMPILERS:
+  if options.compiler == 'r8':
     if 'r8-flags' in values:
       args.extend(values['r8-flags'].split(' '))
 
@@ -319,7 +332,7 @@
       if options.print_memoryuse and not options.track_memory_to_file:
         options.track_memory_to_file = os.path.join(temp,
             utils.MEMORY_USE_TMP_FILE)
-      if options.compiler in R8_COMPILERS and app_provided_pg_conf:
+      if options.compiler == 'r8' and app_provided_pg_conf:
         # Ensure that output of -printmapping and -printseeds go to the output
         # location and not where the app Proguard configuration places them.
         if outdir.endswith('.zip') or outdir.endswith('.jar'):
@@ -332,7 +345,12 @@
       build = not options.no_build and not options.golem
       stderr_path = os.path.join(temp, 'stderr')
       with open(stderr_path, 'w') as stderr:
-        exit_code = toolhelper.run(options.compiler, args,
+        if options.compiler_build == 'full':
+          tool = options.compiler
+        else:
+          assert(options.compiler_build == 'lib')
+          tool = 'r8lib-' + options.compiler
+        exit_code = toolhelper.run(tool, args,
             build=build,
             debug=not options.no_debug,
             profile=options.profile,