Merge commit '190440afd5c304438851011526ebe11bcde62cce' into 1.7.x-dev
Change-Id: I74db49506e6b2beb6eb94ec95a5a704d0a29f728
diff --git a/.gitignore b/.gitignore
index b74632a..bc99d6a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
 buildSrc/out/
 .idea/
 .gradle/
+.gradle_user_home
 android-data*/
 tests/2016-12-19/art.tar.gz
 tests/2016-12-19/art
diff --git a/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java b/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java
index a6389da..31e0dbd 100644
--- a/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java
+++ b/src/main/java/com/android/tools/r8/ArchiveClassFileProvider.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
 import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.io.ByteStreams;
 import java.io.Closeable;
@@ -60,7 +61,7 @@
     assert isArchive(archive);
     origin = new PathOrigin(archive);
     try {
-      zipFile = new ZipFile(archive.toFile(), StandardCharsets.UTF_8);
+      zipFile = FileUtils.createZipFile(archive.toFile(), StandardCharsets.UTF_8);
     } catch (IOException e) {
       if (!Files.exists(archive)) {
         throw new NoSuchFileException(archive.toString());
diff --git a/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java b/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java
index c5065f2..d5dd240 100644
--- a/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/ArchiveProgramResourceProvider.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
 import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.io.ByteStreams;
 import java.io.IOException;
@@ -62,7 +63,7 @@
       Path archive, Predicate<String> include) {
     return fromSupplier(
         new PathOrigin(archive),
-        () -> new ZipFile(archive.toFile(), StandardCharsets.UTF_8),
+        () -> FileUtils.createZipFile(archive.toFile(), StandardCharsets.UTF_8),
         include);
   }
 
diff --git a/src/main/java/com/android/tools/r8/BaseCommand.java b/src/main/java/com/android/tools/r8/BaseCommand.java
index 318305d..50e5be1 100644
--- a/src/main/java/com/android/tools/r8/BaseCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCommand.java
@@ -166,15 +166,14 @@
     public B addProgramFiles(Collection<Path> files) {
       guard(
           () -> {
-            files.forEach(
-                path -> {
-                  try {
-                    app.addProgramFile(path);
-                    programFiles.add(path);
-                  } catch (CompilationError e) {
-                    error(new ProgramInputOrigin(path), e);
-                  }
-                });
+            for (Path path : files) {
+              try {
+                app.addProgramFile(path);
+                programFiles.add(path);
+              } catch (CompilationError e) {
+                error(new ProgramInputOrigin(path), e);
+              }
+            }
           });
       return self();
     }
@@ -201,14 +200,13 @@
     public B addLibraryFiles(Collection<Path> files) {
       guard(
           () -> {
-            files.forEach(
-                path -> {
-                  try {
-                    app.addLibraryFile(path);
-                  } catch (CompilationError e) {
-                    error(new LibraryInputOrigin(path), e);
-                  }
-                });
+            for (Path path : files) {
+              try {
+                app.addLibraryFile(path);
+              } catch (CompilationError e) {
+                error(new LibraryInputOrigin(path), e);
+              }
+            }
           });
       return self();
     }
diff --git a/src/main/java/com/android/tools/r8/L8.java b/src/main/java/com/android/tools/r8/L8.java
index f17bab9..7834adb 100644
--- a/src/main/java/com/android/tools/r8/L8.java
+++ b/src/main/java/com/android/tools/r8/L8.java
@@ -113,10 +113,6 @@
           options.desugaredLibraryConfiguration.createPrefixRewritingMapper(options.itemFactory);
 
       app = new L8TreePruner(options).prune(app, rewritePrefix);
-      if (app.classes().size() == 0) {
-        // TODO(b/134732760): report error instead of fatalError.
-        throw options.reporter.fatalError("Empty desugared library.");
-      }
       AppInfo appInfo = new AppInfo(app);
 
       AppView<?> appView = AppView.createForL8(appInfo, options, rewritePrefix);
diff --git a/src/main/java/com/android/tools/r8/cf/TypeVerificationHelper.java b/src/main/java/com/android/tools/r8/cf/TypeVerificationHelper.java
index a48bbd1..e77cba3 100644
--- a/src/main/java/com/android/tools/r8/cf/TypeVerificationHelper.java
+++ b/src/main/java/com/android/tools/r8/cf/TypeVerificationHelper.java
@@ -20,9 +20,9 @@
 import com.android.tools.r8.ir.code.StackValue;
 import com.android.tools.r8.ir.code.Value;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.IdentityHashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -223,7 +223,7 @@
     computingVerificationTypes = true;
     types = new HashMap<>();
     List<ConstNumber> nullsUsedInPhis = new ArrayList<>();
-    Set<Value> worklist = new HashSet<>();
+    Set<Value> worklist = Sets.newIdentityHashSet();
     {
       InstructionIterator it = code.instructionIterator();
       Instruction instruction = null;
diff --git a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
index 7321b53..68e9e69 100644
--- a/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
+++ b/src/main/java/com/android/tools/r8/compatdx/CompatDx.java
@@ -569,7 +569,7 @@
       // For each input archive file, add all class files within.
       for (Path input : inputs) {
         if (FileUtils.isArchive(input)) {
-          try (ZipFile zipFile = new ZipFile(input.toFile(), StandardCharsets.UTF_8)) {
+          try (ZipFile zipFile = FileUtils.createZipFile(input.toFile(), StandardCharsets.UTF_8)) {
             final Enumeration<? extends ZipEntry> entries = zipFile.entries();
             while (entries.hasMoreElements()) {
               ZipEntry entry = entries.nextElement();
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java b/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
index 460885c..3425946 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithSubtyping.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.graph;
 
 import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.ir.analysis.type.ClassTypeLatticeElement;
 import com.android.tools.r8.ir.desugar.LambdaDescriptor;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.SetUtils;
@@ -131,7 +132,7 @@
 
   public AppInfoWithSubtyping(DexApplication application) {
     super(application);
-    typeInfo = Collections.synchronizedMap(new IdentityHashMap<>());
+    typeInfo = new ConcurrentHashMap<>();
     // Recompute subtype map if we have modified the graph.
     populateSubtypeMap(application.asDirect(), application.dexItemFactory);
   }
@@ -140,7 +141,7 @@
     super(previous);
     missingClasses.addAll(previous.missingClasses);
     subtypeMap.putAll(previous.subtypeMap);
-    typeInfo = Collections.synchronizedMap(new IdentityHashMap<>(previous.typeInfo));
+    typeInfo = new ConcurrentHashMap<>(previous.typeInfo);
     assert app() instanceof DirectMappedDexApplication;
   }
 
@@ -211,6 +212,7 @@
   }
 
   private TypeInfo getTypeInfo(DexType type) {
+    assert type != null;
     return typeInfo.computeIfAbsent(type, TypeInfo::new);
   }
 
@@ -274,11 +276,11 @@
       }
       assert !seenTypes.contains(next);
       seenTypes.add(next);
-      TypeInfo superInfo = getTypeInfo(superType);
       TypeInfo nextInfo = getTypeInfo(next);
       if (superType == null) {
         assert nextInfo.hierarchyLevel == ROOT_LEVEL;
       } else {
+        TypeInfo superInfo = getTypeInfo(superType);
         assert superInfo.hierarchyLevel == nextInfo.hierarchyLevel - 1
             || (superInfo.hierarchyLevel == ROOT_LEVEL
                 && nextInfo.hierarchyLevel == INTERFACE_LEVEL);
@@ -714,8 +716,17 @@
     return !isSubtype(type1, type2) && !isSubtype(type2, type1);
   }
 
-  public boolean mayHaveFinalizeMethodDirectlyOrIndirectly(DexType type) {
-    return computeMayHaveFinalizeMethodDirectlyOrIndirectlyIfAbsent(type, true);
+  public boolean mayHaveFinalizeMethodDirectlyOrIndirectly(ClassTypeLatticeElement type) {
+    Set<DexType> interfaces = type.getInterfaces();
+    if (!interfaces.isEmpty()) {
+      for (DexType interfaceType : interfaces) {
+        if (computeMayHaveFinalizeMethodDirectlyOrIndirectlyIfAbsent(interfaceType, false)) {
+          return true;
+        }
+      }
+      return false;
+    }
+    return computeMayHaveFinalizeMethodDirectlyOrIndirectlyIfAbsent(type.getClassType(), true);
   }
 
   private boolean computeMayHaveFinalizeMethodDirectlyOrIndirectlyIfAbsent(
@@ -727,8 +738,10 @@
     }
     DexClass clazz = definitionFor(type);
     if (clazz == null) {
-      mayHaveFinalizeMethodDirectlyOrIndirectlyCache.put(type, true);
-      return true;
+      // This is strictly not conservative but is needed to avoid that we treat Object as having
+      // a subtype that has a non-default finalize() implementation.
+      mayHaveFinalizeMethodDirectlyOrIndirectlyCache.put(type, false);
+      return false;
     }
     if (clazz.isProgramClass()) {
       if (lookUpwards) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index 6f5df28..7ce2685 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -924,4 +924,13 @@
     assert verifyNoDuplicateMethods();
     return true;
   }
+
+  public boolean hasStaticSynchronizedMethods() {
+    for (DexEncodedMethod encodedMethod : directMethods()) {
+      if (encodedMethod.accessFlags.isStatic() && encodedMethod.accessFlags.isSynchronized()) {
+        return true;
+      }
+    }
+    return false;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 0cc264a..f60e877 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -62,12 +62,12 @@
 
   public static final String throwableDescriptorString = "Ljava/lang/Throwable;";
 
-  private final ConcurrentHashMap<DexString, DexString> strings = new ConcurrentHashMap<>();
-  private final ConcurrentHashMap<DexString, DexType> types = new ConcurrentHashMap<>();
-  private final ConcurrentHashMap<DexField, DexField> fields = new ConcurrentHashMap<>();
-  private final ConcurrentHashMap<DexProto, DexProto> protos = new ConcurrentHashMap<>();
-  private final ConcurrentHashMap<DexMethod, DexMethod> methods = new ConcurrentHashMap<>();
-  private final ConcurrentHashMap<DexMethodHandle, DexMethodHandle> methodHandles =
+  private final Map<DexString, DexString> strings = new ConcurrentHashMap<>();
+  private final Map<DexString, DexType> types = new ConcurrentHashMap<>();
+  private final Map<DexField, DexField> fields = new ConcurrentHashMap<>();
+  private final Map<DexProto, DexProto> protos = new ConcurrentHashMap<>();
+  private final Map<DexMethod, DexMethod> methods = new ConcurrentHashMap<>();
+  private final Map<DexMethodHandle, DexMethodHandle> methodHandles =
       new ConcurrentHashMap<>();
 
   // DexDebugEvent Canonicalization.
@@ -730,10 +730,13 @@
 
   public class EnumMethods {
 
-    public DexMethod valueOf;
-    public DexMethod ordinal;
-    public DexMethod name;
-    public DexMethod toString;
+    public final DexMethod valueOf;
+    public final DexMethod ordinal;
+    public final DexMethod name;
+    public final DexMethod toString;
+
+    public final DexMethod finalize =
+        createMethod(enumType, createProto(voidType), finalizeMethodName);
 
     private EnumMethods() {
       valueOf =
@@ -1152,7 +1155,7 @@
     }
   }
 
-  private static <T extends DexItem> T canonicalize(ConcurrentHashMap<T, T> map, T item) {
+  private static <T extends DexItem> T canonicalize(Map<T, T> map, T item) {
     assert item != null;
     assert !DexItemFactory.isInternalSentinel(item);
     T previous = map.putIfAbsent(item, item);
diff --git a/src/main/java/com/android/tools/r8/graph/DexValue.java b/src/main/java/com/android/tools/r8/graph/DexValue.java
index aa6b651..af70929 100644
--- a/src/main/java/com/android/tools/r8/graph/DexValue.java
+++ b/src/main/java/com/android/tools/r8/graph/DexValue.java
@@ -56,6 +56,22 @@
     return null;
   }
 
+  public DexValueType asDexValueType() {
+    return null;
+  }
+
+  public DexValueArray asDexValueArray() {
+    return null;
+  }
+
+  public boolean isDexValueType() {
+    return false;
+  }
+
+  public boolean isDexValueArray() {
+    return false;
+  }
+
   public static DexValue fromAsmBootstrapArgument(
       Object value, JarApplicationReader application, DexType clazz) {
     if (value instanceof Integer) {
@@ -835,6 +851,16 @@
         DexMethod method, int instructionOffset) {
       value.collectIndexedItems(indexedItems, method, instructionOffset);
     }
+
+    @Override
+    public DexValueType asDexValueType() {
+      return this;
+    }
+
+    @Override
+    public boolean isDexValueType() {
+      return true;
+    }
   }
 
   static public class DexValueField extends NestedDexValue<DexField> {
@@ -979,6 +1005,16 @@
     public String toString() {
       return "Array " + Arrays.toString(values);
     }
+
+    @Override
+    public boolean isDexValueArray() {
+      return true;
+    }
+
+    @Override
+    public DexValueArray asDexValueArray() {
+      return this;
+    }
   }
 
   static public class DexValueAnnotation extends DexValue {
diff --git a/src/main/java/com/android/tools/r8/graph/ParameterAnnotationsList.java b/src/main/java/com/android/tools/r8/graph/ParameterAnnotationsList.java
index 68c7da1..6ff5c75 100644
--- a/src/main/java/com/android/tools/r8/graph/ParameterAnnotationsList.java
+++ b/src/main/java/com/android/tools/r8/graph/ParameterAnnotationsList.java
@@ -5,8 +5,10 @@
 
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.MixedSectionCollection;
+import com.android.tools.r8.utils.ArrayUtils;
 import java.util.Arrays;
 import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.function.Predicate;
 
 /**
@@ -193,4 +195,12 @@
     }
     return new ParameterAnnotationsList(filtered, missingParameterAnnotations);
   }
+
+  public ParameterAnnotationsList rewrite(Function<DexAnnotationSet, DexAnnotationSet> mapper) {
+    DexAnnotationSet[] rewritten = ArrayUtils.map(DexAnnotationSet[].class, values, mapper);
+    if (rewritten != values) {
+      return new ParameterAnnotationsList(rewritten, missingParameterAnnotations);
+    }
+    return this;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/ValueMayDependOnEnvironmentAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/ValueMayDependOnEnvironmentAnalysis.java
new file mode 100644
index 0000000..d654c87
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/ValueMayDependOnEnvironmentAnalysis.java
@@ -0,0 +1,261 @@
+// 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.analysis;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.ArrayPut;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeNewArray;
+import com.android.tools.r8.ir.code.NewArrayEmpty;
+import com.android.tools.r8.ir.code.NewArrayFilledData;
+import com.android.tools.r8.ir.code.StaticPut;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.utils.LongInterval;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import java.util.Set;
+
+public class ValueMayDependOnEnvironmentAnalysis {
+
+  private final AppView<?> appView;
+  private final IRCode code;
+  private final DexType context;
+
+  public ValueMayDependOnEnvironmentAnalysis(AppView<?> appView, IRCode code) {
+    this.appView = appView;
+    this.code = code;
+    this.context = code.method.method.holder;
+  }
+
+  public boolean valueMayDependOnEnvironment(Value value) {
+    Value root = value.getAliasedValue();
+    if (root.isConstant()) {
+      return false;
+    }
+    if (isConstantArrayThroughoutMethod(root)) {
+      return false;
+    }
+    if (isNewInstanceWithoutEnvironmentDependentFields(root)) {
+      return false;
+    }
+    return true;
+  }
+
+  public boolean isConstantArrayThroughoutMethod(Value value) {
+    Value root = value.getAliasedValue();
+    if (root.isPhi()) {
+      // Would need to track the aliases, just give up.
+      return false;
+    }
+
+    Instruction definition = root.definition;
+
+    // Check that it is a constant array with a known size at this point in the IR.
+    long size;
+    if (definition.isInvokeNewArray()) {
+      InvokeNewArray invokeNewArray = definition.asInvokeNewArray();
+      for (Value argument : invokeNewArray.arguments()) {
+        if (!argument.isConstant()) {
+          return false;
+        }
+      }
+      size = invokeNewArray.arguments().size();
+    } else if (definition.isNewArrayEmpty()) {
+      NewArrayEmpty newArrayEmpty = definition.asNewArrayEmpty();
+      Value sizeValue = newArrayEmpty.size().getAliasedValue();
+      if (!sizeValue.hasValueRange()) {
+        return false;
+      }
+      LongInterval sizeRange = sizeValue.getValueRange();
+      if (!sizeRange.isSingleValue()) {
+        return false;
+      }
+      size = sizeRange.getSingleValue();
+    } else {
+      // Some other array creation.
+      return false;
+    }
+
+    if (size < 0) {
+      // Check for NegativeArraySizeException.
+      return false;
+    }
+
+    if (size == 0) {
+      // Empty arrays are always constant.
+      return true;
+    }
+
+    // Allow array-put and new-array-filled-data instructions that immediately follow the array
+    // creation.
+    Set<Instruction> consumedInstructions = Sets.newIdentityHashSet();
+
+    for (Instruction instruction : definition.getBlock().instructionsAfter(definition)) {
+      if (instruction.isArrayPut()) {
+        ArrayPut arrayPut = instruction.asArrayPut();
+        Value array = arrayPut.array().getAliasedValue();
+        if (array != root) {
+          // This ends the chain of array-put instructions that are allowed immediately after the
+          // array creation.
+          break;
+        }
+
+        LongInterval indexRange = arrayPut.index().getValueRange();
+        if (!indexRange.isSingleValue()) {
+          return false;
+        }
+
+        long index = indexRange.getSingleValue();
+        if (index < 0 || index >= size) {
+          return false;
+        }
+
+        if (!arrayPut.value().isConstant()) {
+          return false;
+        }
+
+        consumedInstructions.add(arrayPut);
+        continue;
+      }
+
+      if (instruction.isNewArrayFilledData()) {
+        NewArrayFilledData newArrayFilledData = instruction.asNewArrayFilledData();
+        Value array = newArrayFilledData.src();
+        if (array != root) {
+          break;
+        }
+
+        consumedInstructions.add(newArrayFilledData);
+        continue;
+      }
+
+      if (instruction.instructionMayHaveSideEffects(appView, context)) {
+        // This ends the chain of array-put instructions that are allowed immediately after the
+        // array creation.
+        break;
+      }
+    }
+
+    // Check that the array is not mutated before the end of this method.
+    //
+    // Currently, we only allow the array to flow into static-put instructions that are not
+    // followed by an instruction that may have side effects. Instructions that do not have any
+    // side effects are ignored because they cannot mutate the array.
+    return !valueMayBeMutatedBeforeMethodExit(root, consumedInstructions);
+  }
+
+  private boolean isNewInstanceWithoutEnvironmentDependentFields(Value value) {
+    assert !value.hasAliasedValue();
+
+    if (value.isPhi() || !value.definition.isNewInstance()) {
+      return false;
+    }
+
+    // Find the single constructor invocation.
+    InvokeMethod constructorInvoke = null;
+    for (Instruction instruction : value.uniqueUsers()) {
+      if (!instruction.isInvokeDirect()) {
+        continue;
+      }
+
+      InvokeDirect invoke = instruction.asInvokeDirect();
+      if (!appView.dexItemFactory().isConstructor(invoke.getInvokedMethod())) {
+        continue;
+      }
+
+      if (invoke.getReceiver().getAliasedValue() != value) {
+        continue;
+      }
+
+      if (constructorInvoke == null) {
+        constructorInvoke = invoke;
+      } else {
+        // Not a single constructor invocation, give up.
+        return false;
+      }
+    }
+
+    if (constructorInvoke == null) {
+      // Didn't find a constructor invocation, give up.
+      return false;
+    }
+
+    // Check that it is a trivial initializer (otherwise, the constructor could do anything).
+    DexEncodedMethod constructor = appView.definitionFor(constructorInvoke.getInvokedMethod());
+    if (constructor == null) {
+      return false;
+    }
+
+    TrivialInitializer initializerInfo =
+        constructor.getOptimizationInfo().getTrivialInitializerInfo();
+    if (initializerInfo == null || !initializerInfo.isTrivialInstanceInitializer()) {
+      return false;
+    }
+
+    // Check that none of the arguments to the constructor depend on the environment.
+    for (int i = 1; i < constructorInvoke.arguments().size(); i++) {
+      Value argument = constructorInvoke.arguments().get(i);
+      if (valueMayDependOnEnvironment(argument)) {
+        return false;
+      }
+    }
+
+    // Finally, check that the object does not escape.
+    return !valueMayBeMutatedBeforeMethodExit(value, ImmutableSet.of(constructorInvoke));
+  }
+
+  private boolean valueMayBeMutatedBeforeMethodExit(Value value, Set<Instruction> whitelist) {
+    assert !value.hasAliasedValue();
+
+    if (value.numberOfPhiUsers() > 0) {
+      // Could be mutated indirectly.
+      return true;
+    }
+
+    Set<Instruction> visited = Sets.newIdentityHashSet();
+    for (Instruction user : value.uniqueUsers()) {
+      if (whitelist.contains(user)) {
+        continue;
+      }
+
+      if (user.isStaticPut()) {
+        StaticPut staticPut = user.asStaticPut();
+        if (visited.contains(staticPut)) {
+          // Already visited previously.
+          continue;
+        }
+        for (Instruction instruction : code.getInstructionsReachableFrom(staticPut)) {
+          if (!visited.add(instruction)) {
+            // Already visited previously.
+            continue;
+          }
+          if (instruction.isStaticPut()) {
+            StaticPut otherStaticPut = instruction.asStaticPut();
+            if (otherStaticPut.getField().holder == staticPut.getField().holder
+                && instruction.instructionInstanceCanThrow(appView, context).cannotThrow()) {
+              continue;
+            }
+            return true;
+          }
+          if (instruction.instructionMayTriggerMethodInvocation(appView, context)) {
+            return true;
+          }
+        }
+        continue;
+      }
+
+      // Other user than static-put, just give up.
+      return false;
+    }
+
+    return false;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/FieldValueAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/FieldValueAnalysis.java
index 2d67908..1ef58fb 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/FieldValueAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/FieldValueAnalysis.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.ClassTypeLatticeElement;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.code.BasicBlock;
@@ -245,9 +246,14 @@
   private void updateFieldOptimizationInfo(DexEncodedField field, Value value) {
     TypeLatticeElement fieldType =
         TypeLatticeElement.fromDexType(field.field.type, Nullability.maybeNull(), appView);
-    TypeLatticeElement valueType = value.getTypeLattice();
-    if (valueType.strictlyLessThan(fieldType, appView)) {
-      feedback.markFieldHasDynamicType(field, valueType);
+    TypeLatticeElement dynamicUpperBoundType = value.getDynamicUpperBoundType(appView);
+    if (dynamicUpperBoundType.strictlyLessThan(fieldType, appView)) {
+      feedback.markFieldHasDynamicUpperBoundType(field, dynamicUpperBoundType);
+    }
+    ClassTypeLatticeElement dynamicLowerBoundType = value.getDynamicLowerBoundType(appView);
+    if (dynamicLowerBoundType != null) {
+      assert dynamicLowerBoundType.lessThanOrEqual(dynamicUpperBoundType, appView);
+      feedback.markFieldHasDynamicLowerBoundType(field, dynamicLowerBoundType);
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/sideeffect/ClassInitializerSideEffectAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/sideeffect/ClassInitializerSideEffectAnalysis.java
index 4071c99..35c483b 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/sideeffect/ClassInitializerSideEffectAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/sideeffect/ClassInitializerSideEffectAnalysis.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.ValueMayDependOnEnvironmentAnalysis;
 import com.android.tools.r8.ir.code.ArrayPut;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
@@ -42,6 +43,8 @@
     OptionalBool controlFlowMayDependOnEnvironment = OptionalBool.unknown();
     boolean mayHaveSideEffects = false;
 
+    ValueMayDependOnEnvironmentAnalysis environmentAnalysis =
+        new ValueMayDependOnEnvironmentAnalysis(appView, code);
     for (Instruction instruction : code.instructions()) {
       // Array stores to a newly created array are only observable if they may throw, or if the
       // array content may depend on the environment.
@@ -50,14 +53,14 @@
         Value array = arrayPut.array().getAliasedValue();
         if (array.isPhi()
             || !array.definition.isCreatingArray()
-            || arrayPut.index().mayDependOnEnvironment(appView, code)
-            || arrayPut.value().mayDependOnEnvironment(appView, code)
+            || environmentAnalysis.valueMayDependOnEnvironment(arrayPut.index())
+            || environmentAnalysis.valueMayDependOnEnvironment(arrayPut.value())
             || arrayPut.instructionInstanceCanThrow(appView, context).isThrowing()) {
           return ClassInitializerSideEffect.SIDE_EFFECTS_THAT_CANNOT_BE_POSTPONED;
         }
         if (controlFlowMayDependOnEnvironment.isUnknown()) {
           controlFlowMayDependOnEnvironment =
-              OptionalBool.of(code.controlFlowMayDependOnEnvironment(appView));
+              OptionalBool.of(code.controlFlowMayDependOnEnvironment(environmentAnalysis));
         }
         if (controlFlowMayDependOnEnvironment.isTrue()) {
           return ClassInitializerSideEffect.SIDE_EFFECTS_THAT_CANNOT_BE_POSTPONED;
@@ -76,7 +79,7 @@
         }
         if (controlFlowMayDependOnEnvironment.isUnknown()) {
           controlFlowMayDependOnEnvironment =
-              OptionalBool.of(code.controlFlowMayDependOnEnvironment(appView));
+              OptionalBool.of(code.controlFlowMayDependOnEnvironment(environmentAnalysis));
         }
         if (controlFlowMayDependOnEnvironment.isTrue()) {
           return ClassInitializerSideEffect.SIDE_EFFECTS_THAT_CANNOT_BE_POSTPONED;
@@ -92,7 +95,7 @@
           return ClassInitializerSideEffect.SIDE_EFFECTS_THAT_CANNOT_BE_POSTPONED;
         }
         for (Value argument : invokeNewArray.arguments()) {
-          if (argument.mayDependOnEnvironment(appView, code)) {
+          if (environmentAnalysis.valueMayDependOnEnvironment(argument)) {
             return ClassInitializerSideEffect.SIDE_EFFECTS_THAT_CANNOT_BE_POSTPONED;
           }
         }
@@ -111,7 +114,7 @@
         DexEncodedField field = appView.appInfo().resolveField(staticPut.getField());
         if (field == null
             || field.field.holder != context
-            || staticPut.value().mayDependOnEnvironment(appView, code)
+            || environmentAnalysis.valueMayDependOnEnvironment(staticPut.value())
             || instruction.instructionInstanceCanThrow(appView, context).isThrowing()) {
           return ClassInitializerSideEffect.SIDE_EFFECTS_THAT_CANNOT_BE_POSTPONED;
         }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Assume.java b/src/main/java/com/android/tools/r8/ir/code/Assume.java
index 9ba29fd..37de422 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Assume.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Assume.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import java.util.Objects;
 import java.util.Set;
 
 public class Assume<An extends Assumption> extends Instruction {
@@ -46,14 +47,18 @@
   }
 
   public static Assume<DynamicTypeAssumption> createAssumeDynamicTypeInstruction(
-      TypeLatticeElement type,
-      ClassTypeLatticeElement lowerBoundType,
+      TypeLatticeElement dynamicUpperBoundType,
+      ClassTypeLatticeElement dynamicLowerBoundType,
       Value dest,
       Value src,
       Instruction origin,
       AppView<?> appView) {
     return new Assume<>(
-        new DynamicTypeAssumption(type, lowerBoundType), dest, src, origin, appView);
+        new DynamicTypeAssumption(dynamicUpperBoundType, dynamicLowerBoundType),
+        dest,
+        src,
+        origin,
+        appView);
   }
 
   @Override
@@ -170,7 +175,7 @@
       return true;
     }
     if (assumption.isAssumeDynamicType()) {
-      outType = asAssumeDynamicType().assumption.getType();
+      outType = asAssumeDynamicType().assumption.getDynamicUpperBoundType();
     }
     if (appView.appInfo().hasSubtyping()) {
       if (outType.isClassType()
@@ -288,19 +293,24 @@
 
   @Override
   public String toString() {
-    // During branch simplification, the origin `if` could be simplified.
-    // It means the assumption became "truth."
-    assert origin.hasBlock() || isAssumeNonNull();
+    // `origin` could become obsolete:
+    //   1) during branch simplification, the origin `if` could be simplified, which means the
+    //     assumption became "truth."
+    //   2) invoke-interface could be devirtualized, while its dynamic type and/or non-null receiver
+    //     are still valid.
     String originString =
-        origin.hasBlock() ? " (origin: `" + origin.toString() + "`)" : " (origin simplified)";
+        origin.hasBlock() ? " (origin: `" + origin.toString() + "`)" : " (obsolete origin)";
     if (isAssumeNone() || isAssumeNonNull()) {
       return super.toString() + originString;
     }
     if (isAssumeDynamicType()) {
       DynamicTypeAssumption assumption = asAssumeDynamicType().getAssumption();
       return super.toString()
-          + "; upper bound: " + assumption.type
-          + (assumption.lowerBoundType != null ? "; lower bound: " + assumption.lowerBoundType : "")
+          + "; upper bound: "
+          + assumption.dynamicUpperBoundType
+          + (assumption.dynamicLowerBoundType != null
+              ? "; lower bound: " + assumption.dynamicLowerBoundType
+              : "")
           + originString;
     }
     return super.toString();
@@ -348,20 +358,21 @@
 
   public static class DynamicTypeAssumption extends Assumption {
 
-    private final TypeLatticeElement type;
-    private final ClassTypeLatticeElement lowerBoundType;
+    private final TypeLatticeElement dynamicUpperBoundType;
+    private final ClassTypeLatticeElement dynamicLowerBoundType;
 
-    private DynamicTypeAssumption(TypeLatticeElement type, ClassTypeLatticeElement lowerBoundType) {
-      this.type = type;
-      this.lowerBoundType = lowerBoundType;
+    private DynamicTypeAssumption(
+        TypeLatticeElement dynamicUpperBoundType, ClassTypeLatticeElement dynamicLowerBoundType) {
+      this.dynamicUpperBoundType = dynamicUpperBoundType;
+      this.dynamicLowerBoundType = dynamicLowerBoundType;
     }
 
-    public TypeLatticeElement getType() {
-      return type;
+    public TypeLatticeElement getDynamicUpperBoundType() {
+      return dynamicUpperBoundType;
     }
 
-    public ClassTypeLatticeElement getLowerBoundType() {
-      return lowerBoundType;
+    public ClassTypeLatticeElement getDynamicLowerBoundType() {
+      return dynamicLowerBoundType;
     }
 
     @Override
@@ -371,7 +382,7 @@
 
     @Override
     public boolean verifyCorrectnessOfValues(Value dest, Value src, AppView<?> appView) {
-      assert type.lessThanOrEqualUpToNullability(src.getTypeLattice(), appView);
+      assert dynamicUpperBoundType.lessThanOrEqualUpToNullability(src.getTypeLattice(), appView);
       return true;
     }
 
@@ -384,12 +395,13 @@
         return false;
       }
       DynamicTypeAssumption assumption = (DynamicTypeAssumption) other;
-      return type == assumption.type;
+      return dynamicUpperBoundType == assumption.dynamicUpperBoundType
+          && dynamicLowerBoundType == assumption.dynamicLowerBoundType;
     }
 
     @Override
     public int hashCode() {
-      return type.hashCode();
+      return Objects.hash(dynamicUpperBoundType, dynamicLowerBoundType);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index 78018f9..a4c8d4b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -26,6 +26,7 @@
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.ints.IntArrayList;
 import it.unimi.dsi.fastutil.ints.IntList;
@@ -35,7 +36,6 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
@@ -854,7 +854,7 @@
   }
 
   public Set<Value> cleanForRemoval() {
-    Set<Value> affectedValues = new HashSet<>();
+    Set<Value> affectedValues = Sets.newIdentityHashSet();
     for (BasicBlock block : successors) {
       affectedValues.addAll(block.getPhis());
       block.removePredecessor(this, affectedValues);
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
index 63f6636..708e947 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
@@ -20,7 +20,6 @@
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
-import java.util.HashSet;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Set;
@@ -617,7 +616,7 @@
     if (normalExits.isEmpty()) {
       assert inlineeCanThrow;
       DominatorTree dominatorTree = new DominatorTree(code, MAY_HAVE_UNREACHABLE_BLOCKS);
-      Set<Value> affectedValues = new HashSet<>();
+      Set<Value> affectedValues = Sets.newIdentityHashSet();
       blocksToRemove.addAll(invokePredecessor.unlink(invokeBlock, dominatorTree, affectedValues));
       new TypeAnalysis(appView).narrowing(affectedValues);
     }
diff --git a/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java b/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
index b95376e..72e4c9d 100644
--- a/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
@@ -8,13 +8,16 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.AbstractError;
 import com.android.tools.r8.ir.analysis.fieldvalueanalysis.AbstractFieldSet;
 import com.android.tools.r8.ir.analysis.fieldvalueanalysis.ConcreteMutableFieldSet;
 import com.android.tools.r8.ir.analysis.fieldvalueanalysis.EmptyFieldSet;
 import com.android.tools.r8.ir.analysis.fieldvalueanalysis.UnknownFieldSet;
+import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.Collections;
 import java.util.List;
@@ -163,4 +166,39 @@
     assert isFieldPut();
     return EmptyFieldSet.getInstance();
   }
+
+  /**
+   * Returns {@code true} if this instruction may store an instance of a class that has a non-
+   * default finalize() method in a field. In that case, it is not safe to remove this instruction,
+   * since that could change the lifetime of the value.
+   */
+  boolean isStoringObjectWithFinalizer(AppInfoWithLiveness appInfo) {
+    assert isFieldPut();
+    TypeLatticeElement type = value().getTypeLattice();
+    TypeLatticeElement baseType =
+        type.isArrayType() ? type.asArrayTypeLatticeElement().getArrayBaseTypeLattice() : type;
+    if (baseType.isClassType()) {
+      Value root = value().getAliasedValue();
+      if (!root.isPhi() && root.definition.isNewInstance()) {
+        DexClass clazz = appInfo.definitionFor(root.definition.asNewInstance().clazz);
+        if (clazz == null) {
+          return true;
+        }
+        if (clazz.superType == null) {
+          return false;
+        }
+        DexItemFactory dexItemFactory = appInfo.dexItemFactory();
+        DexEncodedMethod resolutionResult =
+            appInfo
+                .resolveMethod(clazz.type, dexItemFactory.objectMethods.finalize)
+                .asSingleTarget();
+        return resolutionResult != null && resolutionResult.isProgramMethod(appInfo);
+      }
+
+      return appInfo.mayHaveFinalizeMethodDirectlyOrIndirectly(
+          baseType.asClassTypeLatticeElement());
+    }
+
+    return false;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCode.java b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
index 99a4956..955182b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCode.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCode.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.TypeChecker;
+import com.android.tools.r8.ir.analysis.ValueMayDependOnEnvironmentAnalysis;
 import com.android.tools.r8.ir.analysis.type.ClassTypeLatticeElement;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
@@ -35,6 +36,7 @@
 import java.util.Deque;
 import java.util.HashSet;
 import java.util.IdentityHashMap;
+import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.ListIterator;
@@ -152,8 +154,9 @@
     worklist.addAll(sorted.reverse());
     while (!worklist.isEmpty()) {
       BasicBlock block = worklist.poll();
-      Set<Value> live = new HashSet<>();
-      Set<Value> liveLocals = new HashSet<>();
+      // Note that the iteration order of live values matters when inserting spill/restore moves.
+      Set<Value> live = new LinkedHashSet<>();
+      Set<Value> liveLocals = Sets.newIdentityHashSet();
       Deque<Value> liveStack = new ArrayDeque<>();
       Set<BasicBlock> exceptionalSuccessors = block.getCatchHandlers().getUniqueTargets();
       for (BasicBlock succ : block.getSuccessors()) {
@@ -259,7 +262,8 @@
     return liveAtEntrySets;
   }
 
-  public boolean controlFlowMayDependOnEnvironment(AppView<?> appView) {
+  public boolean controlFlowMayDependOnEnvironment(
+      ValueMayDependOnEnvironmentAnalysis environmentAnalysis) {
     for (BasicBlock block : blocks) {
       if (block.hasCatchHandlers()) {
         // Whether an instruction throws may generally depend on the environment.
@@ -267,16 +271,16 @@
       }
       if (block.exit().isIf()) {
         If ifInstruction = block.exit().asIf();
-        if (ifInstruction.lhs().mayDependOnEnvironment(appView, this)) {
+        if (environmentAnalysis.valueMayDependOnEnvironment(ifInstruction.lhs())) {
           return true;
         }
         if (!ifInstruction.isZeroTest()
-            && ifInstruction.rhs().mayDependOnEnvironment(appView, this)) {
+            && environmentAnalysis.valueMayDependOnEnvironment(ifInstruction.rhs())) {
           return true;
         }
       } else if (block.exit().isSwitch()) {
         Switch switchInstruction = block.exit().asSwitch();
-        if (switchInstruction.value().mayDependOnEnvironment(appView, this)) {
+        if (environmentAnalysis.valueMayDependOnEnvironment(switchInstruction.value())) {
           return true;
         }
       }
@@ -631,7 +635,7 @@
   }
 
   private boolean consistentDefUseChains() {
-    Set<Value> values = new HashSet<>();
+    Set<Value> values = Sets.newIdentityHashSet();
 
     for (BasicBlock block : blocks) {
       int predecessorCount = block.getPredecessors().size();
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstancePut.java b/src/main/java/com/android/tools/r8/ir/code/InstancePut.java
index 4dcb749..32484c3 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstancePut.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstancePut.java
@@ -110,7 +110,8 @@
 
       DexEncodedField encodedField = appInfoWithLiveness.resolveField(getField());
       assert encodedField != null : "NoSuchFieldError (resolution failure) should be caught.";
-      return appInfoWithLiveness.isFieldRead(encodedField);
+      return appInfoWithLiveness.isFieldRead(encodedField)
+          || isStoringObjectWithFinalizer(appInfoWithLiveness);
     }
 
     // In D8, we always have to assume that the field can be read, and thus have side effects.
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 5bddf13..1129616 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -34,8 +34,8 @@
 import com.android.tools.r8.utils.StringUtils.BraceType;
 import com.google.common.collect.ImmutableSet;
 import java.util.ArrayList;
-import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.function.Function;
@@ -46,7 +46,7 @@
   protected final List<Value> inValues = new ArrayList<>();
   private BasicBlock block = null;
   private int number = -1;
-  private Set<Value> debugValues = null;
+  private LinkedHashSet<Value> debugValues = null;
   private Position position = null;
 
   protected Instruction(Value outValue) {
@@ -141,7 +141,7 @@
   public void addDebugValue(Value value) {
     assert value.hasLocalInfo();
     if (debugValues == null) {
-      debugValues = new HashSet<>();
+      debugValues = new LinkedHashSet<>();
     }
     if (debugValues.add(value)) {
       value.addDebugUser(this);
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
index 48baef4..ab51239 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
@@ -25,6 +25,7 @@
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
+import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -218,14 +219,21 @@
         assert false : "Expected to be able to find the enclosing class of a method definition";
         return true;
       }
-      boolean targetMayHaveSideEffects;
+
       if (appViewWithLiveness.appInfo().noSideEffects.containsKey(target.method)) {
-        targetMayHaveSideEffects = false;
-      } else {
-        targetMayHaveSideEffects = target.getOptimizationInfo().mayHaveSideEffects();
+        return false;
       }
 
-      return targetMayHaveSideEffects;
+      MethodOptimizationInfo optimizationInfo = target.getOptimizationInfo();
+      if (target.isInstanceInitializer()) {
+        TrivialInitializer trivialInitializerInfo = optimizationInfo.getTrivialInitializerInfo();
+        if (trivialInitializerInfo != null
+            && trivialInitializerInfo.isTrivialInstanceInitializer()) {
+          return false;
+        }
+      }
+
+      return target.getOptimizationInfo().mayHaveSideEffects();
     }
 
     return true;
diff --git a/src/main/java/com/android/tools/r8/ir/code/Move.java b/src/main/java/com/android/tools/r8/ir/code/Move.java
index 84da0d1..9054d07 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Move.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Move.java
@@ -122,4 +122,11 @@
   public boolean instructionMayTriggerMethodInvocation(AppView<?> appView, DexType context) {
     return false;
   }
+
+  @Override
+  public boolean verifyTypes(AppView<?> appView) {
+    super.verifyTypes(appView);
+    assert src().getTypeLattice().equals(outValue().getTypeLattice());
+    return true;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewInstance.java b/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
index 7e6fffe..7a38882 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
@@ -11,7 +11,10 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ResolutionResult;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis.AnalysisAssumption;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis.Query;
@@ -168,6 +171,18 @@
       return true;
     }
 
+    // Verify that the object does not have a finalizer.
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    ResolutionResult finalizeResolutionResult =
+        appView.appInfo().resolveMethod(clazz, dexItemFactory.objectMethods.finalize);
+    if (finalizeResolutionResult.hasSingleTarget()) {
+      DexMethod finalizeMethod = finalizeResolutionResult.asSingleTarget().method;
+      if (finalizeMethod != dexItemFactory.enumMethods.finalize
+          && finalizeMethod != dexItemFactory.objectMethods.finalize) {
+        return true;
+      }
+    }
+
     return false;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/Phi.java b/src/main/java/com/android/tools/r8/ir/code/Phi.java
index 95dac01..ae145b3 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Phi.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Phi.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -342,7 +343,7 @@
 
   @Override
   public boolean isValueOnStack() {
-    assert verifyIsStackPhi(new HashSet<>());
+    assert verifyIsStackPhi(Sets.newIdentityHashSet());
     return isStackPhi;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/code/StaticPut.java b/src/main/java/com/android/tools/r8/ir/code/StaticPut.java
index 5d8fb67..63e5c10 100644
--- a/src/main/java/com/android/tools/r8/ir/code/StaticPut.java
+++ b/src/main/java/com/android/tools/r8/ir/code/StaticPut.java
@@ -15,16 +15,12 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
-import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis.AnalysisAssumption;
 import com.android.tools.r8.ir.analysis.ClassInitializationAnalysis.Query;
-import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.conversion.CfBuilder;
 import com.android.tools.r8.ir.conversion.DexBuilder;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
@@ -132,40 +128,6 @@
     return true;
   }
 
-  /**
-   * Returns {@code true} if this instruction may store a value that has a non-default finalize()
-   * method in a static field. In that case, it is not safe to remove this instruction, since that
-   * could change the lifetime of the value.
-   */
-  private boolean isStoringObjectWithFinalizer(AppInfoWithLiveness appInfo) {
-    TypeLatticeElement type = value().getTypeLattice();
-    TypeLatticeElement baseType =
-        type.isArrayType() ? type.asArrayTypeLatticeElement().getArrayBaseTypeLattice() : type;
-    if (baseType.isClassType()) {
-      Value root = value().getAliasedValue();
-      if (!root.isPhi() && root.definition.isNewInstance()) {
-        DexClass clazz = appInfo.definitionFor(root.definition.asNewInstance().clazz);
-        if (clazz == null) {
-          return true;
-        }
-        if (clazz.superType == null) {
-          return false;
-        }
-        DexItemFactory dexItemFactory = appInfo.dexItemFactory();
-        DexEncodedMethod resolutionResult =
-            appInfo
-                .resolveMethod(clazz.type, dexItemFactory.objectMethods.finalize)
-                .asSingleTarget();
-        return resolutionResult != null && resolutionResult.isProgramMethod(appInfo);
-      }
-
-      return appInfo.mayHaveFinalizeMethodDirectlyOrIndirectly(
-          baseType.asClassTypeLatticeElement().getClassType());
-    }
-
-    return false;
-  }
-
   @Override
   public boolean canBeDeadCode(AppView<?> appView, IRCode code) {
     // static-put can be dead as long as it cannot have any of the following:
diff --git a/src/main/java/com/android/tools/r8/ir/code/Value.java b/src/main/java/com/android/tools/r8/ir/code/Value.java
index 33b06c5..74add42 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Value.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Value.java
@@ -9,6 +9,8 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
+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.ir.analysis.type.ClassTypeLatticeElement;
@@ -28,7 +30,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedList;
 import java.util.List;
@@ -37,7 +38,7 @@
 import java.util.Set;
 import java.util.function.Predicate;
 
-public class Value {
+public class Value implements Comparable<Value> {
 
   public void constrainType(
       ValueTypeConstraint constraint, DexMethod method, Origin origin, Reporter reporter) {
@@ -431,16 +432,20 @@
 
   public Set<Instruction> aliasedUsers() {
     Set<Instruction> users = SetUtils.newIdentityHashSet(uniqueUsers());
-    collectAliasedUsersViaAssume(uniqueUsers(), users);
+    Set<Instruction> visited = Sets.newIdentityHashSet();
+    collectAliasedUsersViaAssume(visited, uniqueUsers(), users);
     return users;
   }
 
   private static void collectAliasedUsersViaAssume(
-      Set<Instruction> usersToTest, Set<Instruction> collectedUsers) {
+      Set<Instruction> visited, Set<Instruction> usersToTest, Set<Instruction> collectedUsers) {
     for (Instruction user : usersToTest) {
+      if (!visited.add(user)) {
+        continue;
+      }
       if (user.isAssume()) {
         collectedUsers.addAll(user.outValue().uniqueUsers());
-        collectAliasedUsersViaAssume(user.outValue().uniqueUsers(), collectedUsers);
+        collectAliasedUsersViaAssume(visited, user.outValue().uniqueUsers(), collectedUsers);
       }
     }
   }
@@ -510,17 +515,6 @@
     return false;
   }
 
-  public boolean mayDependOnEnvironment(AppView<?> appView, IRCode code) {
-    Value root = getAliasedValue();
-    if (root.isConstant()) {
-      return false;
-    }
-    if (root.isConstantArrayThroughoutMethod(appView, code)) {
-      return false;
-    }
-    return true;
-  }
-
   public boolean usedInMonitorOperation() {
     for (Instruction instruction : uniqueUsers()) {
       if (instruction.isMonitor()) {
@@ -760,6 +754,11 @@
   }
 
   @Override
+  public int compareTo(Value value) {
+    return Integer.compare(this.number, value.number);
+  }
+
+  @Override
   public int hashCode() {
     return number;
   }
@@ -831,149 +830,33 @@
     return definition.isOutConstant() && !hasLocalInfo();
   }
 
-  public boolean isConstantArrayThroughoutMethod(AppView<?> appView, IRCode code) {
+  public DexEncodedField getEnumField(AppView<?> appView) {
+    if (!appView.enableWholeProgramOptimizations()) {
+      return null;
+    }
+
     Value root = getAliasedValue();
-    if (root.isPhi()) {
-      // Would need to track the aliases, just give up.
-      return false;
+    if (root.isPhi() || !root.definition.isStaticGet()) {
+      return null;
     }
 
-    DexType context = code.method.method.holder;
-    Instruction definition = root.definition;
-
-    // Check that it is a constant array with a known size at this point in the IR.
-    long size;
-    if (definition.isInvokeNewArray()) {
-      InvokeNewArray invokeNewArray = definition.asInvokeNewArray();
-      for (Value argument : invokeNewArray.arguments()) {
-        if (!argument.isConstant()) {
-          return false;
-        }
-      }
-      size = invokeNewArray.arguments().size();
-    } else if (definition.isNewArrayEmpty()) {
-      NewArrayEmpty newArrayEmpty = definition.asNewArrayEmpty();
-      Value sizeValue = newArrayEmpty.size().getAliasedValue();
-      if (!sizeValue.hasValueRange()) {
-        return false;
-      }
-      LongInterval sizeRange = sizeValue.getValueRange();
-      if (!sizeRange.isSingleValue()) {
-        return false;
-      }
-      size = sizeRange.getSingleValue();
-    } else {
-      // Some other array creation.
-      return false;
+    StaticGet staticGet = root.definition.asStaticGet();
+    DexField field = staticGet.getField();
+    if (field.type != field.holder) {
+      return null;
     }
 
-    if (size < 0) {
-      // Check for NegativeArraySizeException.
-      return false;
+    DexEncodedField encodedField = appView.definitionFor(field);
+    if (encodedField == null) {
+      return null;
     }
 
-    if (size == 0) {
-      // Empty arrays are always constant.
-      return true;
+    DexClass clazz = appView.definitionFor(encodedField.field.holder);
+    if (clazz == null || !clazz.isEnum()) {
+      return null;
     }
 
-    // Allow array-put and new-array-filled-data instructions that immediately follow the array
-    // creation.
-    Set<Instruction> consumedInstructions = Sets.newIdentityHashSet();
-
-    for (Instruction instruction : definition.getBlock().instructionsAfter(definition)) {
-      if (instruction.isArrayPut()) {
-        ArrayPut arrayPut = instruction.asArrayPut();
-        Value array = arrayPut.array().getAliasedValue();
-        if (array != root) {
-          // This ends the chain of array-put instructions that are allowed immediately after the
-          // array creation.
-          break;
-        }
-
-        LongInterval indexRange = arrayPut.index().getValueRange();
-        if (!indexRange.isSingleValue()) {
-          return false;
-        }
-
-        long index = indexRange.getSingleValue();
-        if (index < 0 || index >= size) {
-          return false;
-        }
-
-        if (!arrayPut.value().isConstant()) {
-          return false;
-        }
-
-        consumedInstructions.add(arrayPut);
-        continue;
-      }
-
-      if (instruction.isNewArrayFilledData()) {
-        NewArrayFilledData newArrayFilledData = instruction.asNewArrayFilledData();
-        Value array = newArrayFilledData.src();
-        if (array != root) {
-          break;
-        }
-
-        consumedInstructions.add(newArrayFilledData);
-        continue;
-      }
-
-      if (instruction.instructionMayHaveSideEffects(appView, context)) {
-        // This ends the chain of array-put instructions that are allowed immediately after the
-        // array creation.
-        break;
-      }
-    }
-
-    // Check that the array is not mutated before the end of this method.
-    //
-    // Currently, we only allow the array to flow into static-put instructions that are not
-    // followed by an instruction that may have side effects. Instructions that do not have any
-    // side effects are ignored because they cannot mutate the array.
-    Set<Instruction> visitedFromStaticPut = Sets.newIdentityHashSet();
-    for (Instruction user : root.uniqueUsers()) {
-      if (consumedInstructions.contains(user)) {
-        continue;
-      }
-
-      if (user.isStaticPut()) {
-        StaticPut staticPut = user.asStaticPut();
-        if (visitedFromStaticPut.contains(staticPut)) {
-          // Already visited previously.
-          continue;
-        }
-        for (Instruction instruction : code.getInstructionsReachableFrom(staticPut)) {
-          if (!visitedFromStaticPut.add(instruction)) {
-            // Already visited previously.
-            continue;
-          }
-          if (instruction.isStaticPut()) {
-            StaticPut otherStaticPut = instruction.asStaticPut();
-            if (otherStaticPut.getField().holder == staticPut.getField().holder
-                && instruction.instructionInstanceCanThrow(appView, context).cannotThrow()) {
-              continue;
-            }
-            return false;
-          }
-          if (instruction.instructionMayTriggerMethodInvocation(appView, context)) {
-            return false;
-          }
-        }
-        continue;
-      }
-
-      // Other user than static-put, just give up.
-      return false;
-    }
-
-    if (root.numberOfPhiUsers() > 0) {
-      // Could be mutated indirectly.
-      return false;
-    }
-
-    return true;
+    return encodedField;
   }
 
   public boolean isPhi() {
@@ -1030,7 +913,7 @@
     if (isPhi()) {
       Phi self = this.asPhi();
       if (seen == null) {
-        seen = new HashSet<>();
+        seen = Sets.newIdentityHashSet();
       }
       if (seen.contains(self)) {
         return true;
@@ -1101,7 +984,7 @@
 
   public boolean isDead(AppView<?> appView, IRCode code, Predicate<Instruction> ignoreUser) {
     // Totally unused values are trivially dead.
-    return !isUsed() || isDead(appView, code, ignoreUser, new HashSet<>());
+    return !isUsed() || isDead(appView, code, ignoreUser, Sets.newIdentityHashSet());
   }
 
   /**
@@ -1209,7 +1092,8 @@
     if (aliasedValue != null) {
       // If there is an alias of the receiver, which is defined by an Assume<DynamicTypeAssumption>
       // instruction, then use the dynamic type as the refined receiver type.
-      lattice = aliasedValue.definition.asAssumeDynamicType().getAssumption().getType();
+      lattice =
+          aliasedValue.definition.asAssumeDynamicType().getAssumption().getDynamicUpperBoundType();
 
       // For precision, verify that the dynamic type is at least as precise as the static type.
       assert lattice.lessThanOrEqualUpToNullability(typeLattice, appView)
@@ -1250,7 +1134,7 @@
     Value aliasedValue = getSpecificAliasedValue(value -> value.definition.isAssumeDynamicType());
     if (aliasedValue != null) {
       ClassTypeLatticeElement lattice =
-          aliasedValue.definition.asAssumeDynamicType().getAssumption().getLowerBoundType();
+          aliasedValue.definition.asAssumeDynamicType().getAssumption().getDynamicLowerBoundType();
       return lattice != null && typeLattice.isDefinitelyNotNull() && lattice.isNullable()
           ? lattice.asMeetWithNotNull().asClassTypeLatticeElement()
           : lattice;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
index 307215a..b9f72f7 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
@@ -6,8 +6,7 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.ir.conversion.CallGraphBuilder.CycleEliminator;
-import com.android.tools.r8.ir.conversion.CallGraphBuilder.CycleEliminator.CycleEliminationResult;
+import com.android.tools.r8.ir.conversion.CallGraphBuilderBase.CycleEliminator.CycleEliminationResult;
 import com.android.tools.r8.ir.conversion.CallSiteInformation.CallGraphBasedCallSiteInformation;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Timing;
@@ -76,7 +75,14 @@
       caller.callees.remove(this);
     }
 
-    public void cleanForRemoval() {
+    public void cleanCalleesForRemoval() {
+      assert callers.isEmpty();
+      for (Node callee : callees) {
+        callee.callers.remove(this);
+      }
+    }
+
+    public void cleanCallersForRemoval() {
       assert callees.isEmpty();
       for (Node caller : callers) {
         caller.callees.remove(this);
@@ -103,6 +109,10 @@
       return callers.contains(method);
     }
 
+    public boolean isRoot() {
+      return callers.isEmpty();
+    }
+
     public boolean isLeaf() {
       return callees.isEmpty();
     }
@@ -123,21 +133,24 @@
       builder.append(callers.size());
       builder.append(" callers");
       builder.append(", invoke count ").append(numberOfCallSites);
-      builder.append(").\n");
+      builder.append(").");
+      builder.append(System.lineSeparator());
       if (callees.size() > 0) {
-        builder.append("Callees:\n");
+        builder.append("Callees:");
+        builder.append(System.lineSeparator());
         for (Node call : callees) {
           builder.append("  ");
           builder.append(call.method.toSourceString());
-          builder.append("\n");
+          builder.append(System.lineSeparator());
         }
       }
       if (callers.size() > 0) {
-        builder.append("Callers:\n");
+        builder.append("Callers:");
+        builder.append(System.lineSeparator());
         for (Node caller : callers) {
           builder.append("  ");
           builder.append(caller.method.toSourceString());
-          builder.append("\n");
+          builder.append(System.lineSeparator());
         }
       }
       return builder.toString();
@@ -156,14 +169,6 @@
     return new CallGraphBuilder(appView);
   }
 
-  /**
-   * Extract the next set of leaves (nodes with an call (outgoing) degree of 0) if any.
-   *
-   * <p>All nodes in the graph are extracted if called repeatedly until null is returned. Please
-   * note that there are no cycles in this graph (see {@link CycleEliminator#breakCycles}).
-   *
-   * <p>
-   */
   static MethodProcessor createMethodProcessor(
       AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing)
       throws ExecutionException {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilder.java
index 8078fe0..546c021 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilder.java
@@ -4,59 +4,26 @@
 
 package com.android.tools.r8.ir.conversion;
 
-import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexCallSite;
-import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexEncodedField;
 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.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.GraphLense.GraphLenseLookupResult;
-import com.android.tools.r8.graph.ResolutionResult;
-import com.android.tools.r8.graph.UseRegistry;
-import com.android.tools.r8.ir.code.Invoke.Type;
-import com.android.tools.r8.ir.conversion.CallGraph.Node;
-import com.android.tools.r8.ir.conversion.CallGraphBuilder.CycleEliminator.CycleEliminationResult;
-import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.InternalOptions;
-import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.ThreadUtils;
-import com.android.tools.r8.utils.Timing;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Sets;
-import java.util.ArrayDeque;
+import com.google.common.base.Predicates;
 import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Deque;
-import java.util.IdentityHashMap;
-import java.util.Iterator;
-import java.util.LinkedList;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
-import java.util.function.Consumer;
 
-public class CallGraphBuilder {
-
-  private final AppView<AppInfoWithLiveness> appView;
-  private final Map<DexMethod, Node> nodes = new IdentityHashMap<>();
-  private final Map<DexMethod, Set<DexEncodedMethod>> possibleTargetsCache =
-      new ConcurrentHashMap<>();
+public class CallGraphBuilder extends CallGraphBuilderBase {
 
   CallGraphBuilder(AppView<AppInfoWithLiveness> appView) {
-    this.appView = appView;
+    super(appView);
   }
 
-  public CallGraph build(ExecutorService executorService, Timing timing) throws ExecutionException {
+  @Override
+  void process(ExecutorService executorService) throws ExecutionException {
     List<Future<?>> futures = new ArrayList<>();
     for (DexProgramClass clazz : appView.appInfo().classes()) {
       if (clazz.hasMethods()) {
@@ -69,19 +36,6 @@
       }
     }
     ThreadUtils.awaitFutures(futures);
-
-    assert verifyAllMethodsWithCodeExists();
-
-    timing.begin("Cycle elimination");
-    // Sort the nodes for deterministic cycle elimination.
-    Set<Node> nodesWithDeterministicOrder = Sets.newTreeSet(nodes.values());
-    CycleEliminator cycleEliminator =
-        new CycleEliminator(nodesWithDeterministicOrder, appView.options());
-    CycleEliminationResult cycleEliminationResult = cycleEliminator.breakCycles();
-    timing.end();
-    assert cycleEliminator.breakCycles().numberOfRemovedEdges() == 0; // The cycles should be gone.
-
-    return new CallGraph(nodesWithDeterministicOrder, cycleEliminationResult);
   }
 
   private void processClass(DexProgramClass clazz) {
@@ -90,17 +44,13 @@
 
   private void processMethod(DexEncodedMethod method) {
     if (method.hasCode()) {
-      method.registerCodeReferences(new InvokeExtractor(getOrCreateNode(method)));
+      method.registerCodeReferences(
+          new InvokeExtractor(getOrCreateNode(method), Predicates.alwaysTrue()));
     }
   }
 
-  private Node getOrCreateNode(DexEncodedMethod method) {
-    synchronized (nodes) {
-      return nodes.computeIfAbsent(method.method, ignore -> new Node(method));
-    }
-  }
-
-  private boolean verifyAllMethodsWithCodeExists() {
+  @Override
+  boolean verifyAllMethodsWithCodeExists() {
     for (DexProgramClass clazz : appView.appInfo().classes()) {
       for (DexEncodedMethod method : clazz.methods()) {
         assert !method.hasCode() || nodes.get(method.method) != null;
@@ -108,454 +58,4 @@
     }
     return true;
   }
-
-  private class InvokeExtractor extends UseRegistry {
-
-    private final Node caller;
-
-    InvokeExtractor(Node caller) {
-      super(appView.dexItemFactory());
-      this.caller = caller;
-    }
-
-    private void addClassInitializerTarget(DexClass clazz) {
-      assert clazz != null;
-      if (clazz.isProgramClass() && clazz.hasClassInitializer()) {
-        addTarget(clazz.getClassInitializer(), false);
-      }
-    }
-
-    private void addClassInitializerTarget(DexType type) {
-      assert type.isClassType();
-      DexClass clazz = appView.definitionFor(type);
-      if (clazz != null) {
-        addClassInitializerTarget(clazz);
-      }
-    }
-
-    private void addTarget(DexEncodedMethod callee, boolean likelySpuriousCallEdge) {
-      if (callee.accessFlags.isAbstract()) {
-        // Not a valid target.
-        return;
-      }
-      if (appView.appInfo().isPinned(callee.method)) {
-        // Since the callee is kept, we cannot inline it into the caller, and we also cannot collect
-        // any optimization info for the method. Therefore, we drop the call edge to reduce the
-        // total number of call graph edges, which should lead to fewer call graph cycles.
-        return;
-      }
-      assert callee.isProgramMethod(appView);
-      getOrCreateNode(callee).addCallerConcurrently(caller, likelySpuriousCallEdge);
-    }
-
-    private void processInvoke(Type originalType, DexMethod originalMethod) {
-      DexEncodedMethod source = caller.method;
-      DexMethod context = source.method;
-      GraphLenseLookupResult result =
-          appView.graphLense().lookupMethod(originalMethod, context, originalType);
-      DexMethod method = result.getMethod();
-      Type type = result.getType();
-      if (type == Type.INTERFACE || type == Type.VIRTUAL) {
-        // For virtual and interface calls add all potential targets that could be called.
-        ResolutionResult resolutionResult = appView.appInfo().resolveMethod(method.holder, method);
-        resolutionResult.forEachTarget(target -> processInvokeWithDynamicDispatch(type, target));
-      } else {
-        DexEncodedMethod singleTarget =
-            appView.appInfo().lookupSingleTarget(type, method, context.holder);
-        if (singleTarget != null) {
-          assert !source.accessFlags.isBridge() || singleTarget != caller.method;
-          DexClass clazz = appView.definitionFor(singleTarget.method.holder);
-          assert clazz != null;
-          if (clazz.isProgramClass()) {
-            // For static invokes, the class could be initialized.
-            if (type == Type.STATIC) {
-              addClassInitializerTarget(clazz);
-            }
-            addTarget(singleTarget, false);
-          }
-        }
-      }
-    }
-
-    private void processInvokeWithDynamicDispatch(Type type, DexEncodedMethod encodedTarget) {
-      DexMethod target = encodedTarget.method;
-      DexClass clazz = appView.definitionFor(target.holder);
-      if (clazz == null) {
-        assert false : "Unable to lookup holder of `" + target.toSourceString() + "`";
-        return;
-      }
-
-      if (!appView.options().testing.addCallEdgesForLibraryInvokes) {
-        if (clazz.isLibraryClass()) {
-          // Likely to have many possible targets.
-          return;
-        }
-      }
-
-      boolean isInterface = type == Type.INTERFACE;
-      Set<DexEncodedMethod> possibleTargets =
-          possibleTargetsCache.computeIfAbsent(
-              target,
-              method -> {
-                ResolutionResult resolution =
-                    appView.appInfo().resolveMethod(method.holder, method, isInterface);
-                if (resolution.isValidVirtualTarget(appView.options())) {
-                  return resolution.lookupVirtualDispatchTargets(isInterface, appView.appInfo());
-                }
-                return null;
-              });
-      if (possibleTargets != null) {
-        boolean likelySpuriousCallEdge =
-            possibleTargets.size() >= appView.options().callGraphLikelySpuriousCallEdgeThreshold;
-        for (DexEncodedMethod possibleTarget : possibleTargets) {
-          if (possibleTarget.isProgramMethod(appView)) {
-            addTarget(possibleTarget, likelySpuriousCallEdge);
-          }
-        }
-      }
-    }
-
-    private void processFieldAccess(DexField field) {
-      // Any field access implicitly calls the class initializer.
-      if (field.holder.isClassType()) {
-        DexEncodedField encodedField = appView.appInfo().resolveField(field);
-        if (encodedField != null && encodedField.isStatic()) {
-          addClassInitializerTarget(field.holder);
-        }
-      }
-    }
-
-    @Override
-    public boolean registerInvokeVirtual(DexMethod method) {
-      processInvoke(Type.VIRTUAL, method);
-      return false;
-    }
-
-    @Override
-    public boolean registerInvokeDirect(DexMethod method) {
-      processInvoke(Type.DIRECT, method);
-      return false;
-    }
-
-    @Override
-    public boolean registerInvokeStatic(DexMethod method) {
-      processInvoke(Type.STATIC, method);
-      return false;
-    }
-
-    @Override
-    public boolean registerInvokeInterface(DexMethod method) {
-      processInvoke(Type.INTERFACE, method);
-      return false;
-    }
-
-    @Override
-    public boolean registerInvokeSuper(DexMethod method) {
-      processInvoke(Type.SUPER, method);
-      return false;
-    }
-
-    @Override
-    public boolean registerInstanceFieldWrite(DexField field) {
-      processFieldAccess(field);
-      return false;
-    }
-
-    @Override
-    public boolean registerInstanceFieldRead(DexField field) {
-      processFieldAccess(field);
-      return false;
-    }
-
-    @Override
-    public boolean registerNewInstance(DexType type) {
-      if (type.isClassType()) {
-        addClassInitializerTarget(type);
-      }
-      return false;
-    }
-
-    @Override
-    public boolean registerStaticFieldRead(DexField field) {
-      processFieldAccess(field);
-      return false;
-    }
-
-    @Override
-    public boolean registerStaticFieldWrite(DexField field) {
-      processFieldAccess(field);
-      return false;
-    }
-
-    @Override
-    public boolean registerTypeReference(DexType type) {
-      return false;
-    }
-
-    @Override
-    public void registerCallSite(DexCallSite callSite) {
-      registerMethodHandle(
-          callSite.bootstrapMethod, MethodHandleUse.NOT_ARGUMENT_TO_LAMBDA_METAFACTORY);
-    }
-  }
-
-  public static class CycleEliminator {
-
-    public static final String CYCLIC_FORCE_INLINING_MESSAGE =
-        "Unable to satisfy force inlining constraints due to cyclic force inlining";
-
-    private static class CallEdge {
-
-      private final Node caller;
-      private final Node callee;
-
-      public CallEdge(Node caller, Node callee) {
-        this.caller = caller;
-        this.callee = callee;
-      }
-
-      public void remove() {
-        callee.removeCaller(caller);
-      }
-    }
-
-    public static class CycleEliminationResult {
-
-      private Map<Node, Set<Node>> removedEdges;
-
-      CycleEliminationResult(Map<Node, Set<Node>> removedEdges) {
-        this.removedEdges = removedEdges;
-      }
-
-      void forEachRemovedCaller(Node callee, Consumer<Node> fn) {
-        removedEdges.getOrDefault(callee, ImmutableSet.of()).forEach(fn);
-      }
-
-      public int numberOfRemovedEdges() {
-        int numberOfRemovedEdges = 0;
-        for (Set<Node> nodes : removedEdges.values()) {
-          numberOfRemovedEdges += nodes.size();
-        }
-        return numberOfRemovedEdges;
-      }
-    }
-
-    private final Collection<Node> nodes;
-    private final InternalOptions options;
-
-    // DFS stack.
-    private Deque<Node> stack = new ArrayDeque<>();
-
-    // Set of nodes on the DFS stack.
-    private Set<Node> stackSet = Sets.newIdentityHashSet();
-
-    // Set of nodes that have been visited entirely.
-    private Set<Node> marked = Sets.newIdentityHashSet();
-
-    // Mapping from callee to the set of callers that were removed from the callee.
-    private Map<Node, Set<Node>> removedEdges = new IdentityHashMap<>();
-
-    private int currentDepth = 0;
-    private int maxDepth = 0;
-
-    public CycleEliminator(Collection<Node> nodes, InternalOptions options) {
-      this.options = options;
-
-      // Call to reorderNodes must happen after assigning options.
-      this.nodes =
-          options.testing.nondeterministicCycleElimination
-              ? reorderNodes(new ArrayList<>(nodes))
-              : nodes;
-    }
-
-    public CycleEliminationResult breakCycles() {
-      // Break cycles in this call graph by removing edges causing cycles.
-      for (Node node : nodes) {
-        assert currentDepth == 0;
-        traverse(node);
-      }
-      CycleEliminationResult result = new CycleEliminationResult(removedEdges);
-      if (Log.ENABLED) {
-        Log.info(getClass(), "# call graph cycles broken: %s", result.numberOfRemovedEdges());
-        Log.info(getClass(), "# max call graph depth: %s", maxDepth);
-      }
-      reset();
-      return result;
-    }
-
-    private void reset() {
-      assert currentDepth == 0;
-      assert stack.isEmpty();
-      assert stackSet.isEmpty();
-      marked.clear();
-      maxDepth = 0;
-      removedEdges = new IdentityHashMap<>();
-    }
-
-    private void traverse(Node node) {
-      if (Log.ENABLED) {
-        if (currentDepth > maxDepth) {
-          maxDepth = currentDepth;
-        }
-      }
-
-      if (marked.contains(node)) {
-        // Already visited all nodes that can be reached from this node.
-        return;
-      }
-
-      push(node);
-
-      // The callees must be sorted before calling traverse recursively. This ensures that cycles
-      // are broken the same way across multiple compilations.
-      Collection<Node> callees = node.getCalleesWithDeterministicOrder();
-
-      if (options.testing.nondeterministicCycleElimination) {
-        callees = reorderNodes(new ArrayList<>(callees));
-      }
-
-      Iterator<Node> calleeIterator = callees.iterator();
-      while (calleeIterator.hasNext()) {
-        Node callee = calleeIterator.next();
-
-        // If we've exceeded the depth threshold, then treat it as if we have found a cycle. This
-        // ensures that we won't run into stack overflows when the call graph contains large call
-        // chains. This should have a negligible impact on code size as long as the threshold is
-        // large enough.
-        boolean foundCycle = stackSet.contains(callee);
-        boolean thresholdExceeded =
-            currentDepth >= options.callGraphCycleEliminatorMaxDepthThreshold
-                && edgeRemovalIsSafe(node, callee);
-        if (foundCycle || thresholdExceeded) {
-          // Found a cycle that needs to be eliminated.
-          if (edgeRemovalIsSafe(node, callee)) {
-            // Break the cycle by removing the edge node->callee.
-            if (options.testing.nondeterministicCycleElimination) {
-              callee.removeCaller(node);
-            } else {
-              // Need to remove `callee` from `node.callees` using the iterator to prevent a
-              // ConcurrentModificationException. This is not needed when nondeterministic cycle
-              // elimination is enabled, because we iterate a copy of `node.callees` in that case.
-              calleeIterator.remove();
-              callee.getCallersWithDeterministicOrder().remove(node);
-            }
-            recordEdgeRemoval(node, callee);
-
-            if (Log.ENABLED) {
-              Log.info(
-                  CallGraph.class,
-                  "Removed call edge from method '%s' to '%s'",
-                  node.method.toSourceString(),
-                  callee.method.toSourceString());
-            }
-          } else {
-            assert foundCycle;
-
-            // The cycle has a method that is marked as force inline.
-            LinkedList<Node> cycle = extractCycle(callee);
-
-            if (Log.ENABLED) {
-              Log.info(
-                  CallGraph.class, "Extracted cycle to find an edge that can safely be removed");
-            }
-
-            // Break the cycle by finding an edge that can be removed without breaking force
-            // inlining. If that is not possible, this call fails with a compilation error.
-            CallEdge edge = findCallEdgeForRemoval(cycle);
-
-            // The edge will be null if this cycle has already been eliminated as a result of
-            // another cycle elimination.
-            if (edge != null) {
-              assert edgeRemovalIsSafe(edge.caller, edge.callee);
-
-              // Break the cycle by removing the edge caller->callee.
-              edge.remove();
-              recordEdgeRemoval(edge.caller, edge.callee);
-
-              if (Log.ENABLED) {
-                Log.info(
-                    CallGraph.class,
-                    "Removed call edge from force inlined method '%s' to '%s' to ensure that "
-                        + "force inlining will succeed",
-                    node.method.toSourceString(),
-                    callee.method.toSourceString());
-              }
-            }
-
-            // Recover the stack.
-            recoverStack(cycle);
-          }
-        } else {
-          currentDepth++;
-          traverse(callee);
-          currentDepth--;
-        }
-      }
-      pop(node);
-      marked.add(node);
-    }
-
-    private void push(Node node) {
-      stack.push(node);
-      boolean changed = stackSet.add(node);
-      assert changed;
-    }
-
-    private void pop(Node node) {
-      Node popped = stack.pop();
-      assert popped == node;
-      boolean changed = stackSet.remove(node);
-      assert changed;
-    }
-
-    private LinkedList<Node> extractCycle(Node entry) {
-      LinkedList<Node> cycle = new LinkedList<>();
-      do {
-        assert !stack.isEmpty();
-        cycle.add(stack.pop());
-      } while (cycle.getLast() != entry);
-      return cycle;
-    }
-
-    private CallEdge findCallEdgeForRemoval(LinkedList<Node> extractedCycle) {
-      Node callee = extractedCycle.getLast();
-      for (Node caller : extractedCycle) {
-        if (!caller.hasCallee(callee)) {
-          // No need to break any edges since this cycle has already been broken previously.
-          assert !callee.hasCaller(caller);
-          return null;
-        }
-        if (edgeRemovalIsSafe(caller, callee)) {
-          return new CallEdge(caller, callee);
-        }
-        callee = caller;
-      }
-      throw new CompilationError(CYCLIC_FORCE_INLINING_MESSAGE);
-    }
-
-    private static boolean edgeRemovalIsSafe(Node caller, Node callee) {
-      // All call edges where the callee is a method that should be force inlined must be kept,
-      // to guarantee that the IR converter will process the callee before the caller.
-      return !callee.method.getOptimizationInfo().forceInline();
-    }
-
-    private void recordEdgeRemoval(Node caller, Node callee) {
-      removedEdges.computeIfAbsent(callee, ignore -> SetUtils.newIdentityHashSet(2)).add(caller);
-    }
-
-    private void recoverStack(LinkedList<Node> extractedCycle) {
-      Iterator<Node> descendingIt = extractedCycle.descendingIterator();
-      while (descendingIt.hasNext()) {
-        stack.push(descendingIt.next());
-      }
-    }
-
-    private Collection<Node> reorderNodes(List<Node> nodes) {
-      assert options.testing.nondeterministicCycleElimination;
-      if (!InternalOptions.DETERMINISTIC_DEBUGGING) {
-        Collections.shuffle(nodes);
-      }
-      return nodes;
-    }
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilderBase.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilderBase.java
new file mode 100644
index 0000000..1e0a60d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilderBase.java
@@ -0,0 +1,536 @@
+// 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.conversion;
+
+import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexCallSite;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
+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.GraphLenseLookupResult;
+import com.android.tools.r8.graph.ResolutionResult;
+import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.code.Invoke;
+import com.android.tools.r8.ir.conversion.CallGraph.Node;
+import com.android.tools.r8.ir.conversion.CallGraphBuilderBase.CycleEliminator.CycleEliminationResult;
+import com.android.tools.r8.logging.Log;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.SetUtils;
+import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+abstract class CallGraphBuilderBase {
+  final AppView<AppInfoWithLiveness> appView;
+  final Map<DexMethod, Node> nodes = new IdentityHashMap<>();
+  private final Map<DexMethod, Set<DexEncodedMethod>> possibleTargetsCache =
+      new ConcurrentHashMap<>();
+
+  CallGraphBuilderBase(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  public CallGraph build(ExecutorService executorService, Timing timing) throws ExecutionException {
+    process(executorService);
+    assert verifyAllMethodsWithCodeExists();
+
+    timing.begin("Cycle elimination");
+    // Sort the nodes for deterministic cycle elimination.
+    Set<Node> nodesWithDeterministicOrder = Sets.newTreeSet(nodes.values());
+    CycleEliminator cycleEliminator =
+        new CycleEliminator(nodesWithDeterministicOrder, appView.options());
+    CycleEliminationResult cycleEliminationResult = cycleEliminator.breakCycles();
+    timing.end();
+    assert cycleEliminator.breakCycles().numberOfRemovedEdges() == 0; // The cycles should be gone.
+
+    return new CallGraph(nodesWithDeterministicOrder, cycleEliminationResult);
+  }
+
+  abstract void process(ExecutorService executorService) throws ExecutionException;
+
+  Node getOrCreateNode(DexEncodedMethod method) {
+    synchronized (nodes) {
+      return nodes.computeIfAbsent(method.method, ignore -> new Node(method));
+    }
+  }
+
+  abstract boolean verifyAllMethodsWithCodeExists();
+
+  class InvokeExtractor extends UseRegistry {
+
+    private final Node caller;
+    private final Predicate<DexEncodedMethod> targetTester;
+
+    InvokeExtractor(Node caller, Predicate<DexEncodedMethod> targetTester) {
+      super(appView.dexItemFactory());
+      this.caller = caller;
+      this.targetTester = targetTester;
+    }
+
+    private void addClassInitializerTarget(DexClass clazz) {
+      assert clazz != null;
+      if (clazz.isProgramClass() && clazz.hasClassInitializer()) {
+        addTarget(clazz.getClassInitializer(), false);
+      }
+    }
+
+    private void addClassInitializerTarget(DexType type) {
+      assert type.isClassType();
+      DexClass clazz = appView.definitionFor(type);
+      if (clazz != null) {
+        addClassInitializerTarget(clazz);
+      }
+    }
+
+    private void addTarget(DexEncodedMethod callee, boolean likelySpuriousCallEdge) {
+      if (!targetTester.test(callee)) {
+        return;
+      }
+      if (callee.accessFlags.isAbstract()) {
+        // Not a valid target.
+        return;
+      }
+      if (appView.appInfo().isPinned(callee.method)) {
+        // Since the callee is kept, we cannot inline it into the caller, and we also cannot collect
+        // any optimization info for the method. Therefore, we drop the call edge to reduce the
+        // total number of call graph edges, which should lead to fewer call graph cycles.
+        return;
+      }
+      assert callee.isProgramMethod(appView);
+      getOrCreateNode(callee).addCallerConcurrently(caller, likelySpuriousCallEdge);
+    }
+
+    private void processInvoke(Invoke.Type originalType, DexMethod originalMethod) {
+      DexEncodedMethod source = caller.method;
+      DexMethod context = source.method;
+      GraphLenseLookupResult result =
+          appView.graphLense().lookupMethod(originalMethod, context, originalType);
+      DexMethod method = result.getMethod();
+      Invoke.Type type = result.getType();
+      if (type == Invoke.Type.INTERFACE || type == Invoke.Type.VIRTUAL) {
+        // For virtual and interface calls add all potential targets that could be called.
+        ResolutionResult resolutionResult = appView.appInfo().resolveMethod(method.holder, method);
+        resolutionResult.forEachTarget(target -> processInvokeWithDynamicDispatch(type, target));
+      } else {
+        DexEncodedMethod singleTarget =
+            appView.appInfo().lookupSingleTarget(type, method, context.holder);
+        if (singleTarget != null) {
+          assert !source.accessFlags.isBridge() || singleTarget != caller.method;
+          DexClass clazz = appView.definitionFor(singleTarget.method.holder);
+          assert clazz != null;
+          if (clazz.isProgramClass()) {
+            // For static invokes, the class could be initialized.
+            if (type == Invoke.Type.STATIC) {
+              addClassInitializerTarget(clazz);
+            }
+            addTarget(singleTarget, false);
+          }
+        }
+      }
+    }
+
+    private void processInvokeWithDynamicDispatch(
+        Invoke.Type type, DexEncodedMethod encodedTarget) {
+      DexMethod target = encodedTarget.method;
+      DexClass clazz = appView.definitionFor(target.holder);
+      if (clazz == null) {
+        assert false : "Unable to lookup holder of `" + target.toSourceString() + "`";
+        return;
+      }
+
+      if (!appView.options().testing.addCallEdgesForLibraryInvokes) {
+        if (clazz.isLibraryClass()) {
+          // Likely to have many possible targets.
+          return;
+        }
+      }
+
+      boolean isInterface = type == Invoke.Type.INTERFACE;
+      Set<DexEncodedMethod> possibleTargets =
+          possibleTargetsCache.computeIfAbsent(
+              target,
+              method -> {
+                ResolutionResult resolution =
+                    appView.appInfo().resolveMethod(method.holder, method, isInterface);
+                if (resolution.isValidVirtualTarget(appView.options())) {
+                  return resolution.lookupVirtualDispatchTargets(isInterface, appView.appInfo());
+                }
+                return null;
+              });
+      if (possibleTargets != null) {
+        boolean likelySpuriousCallEdge =
+            possibleTargets.size() >= appView.options().callGraphLikelySpuriousCallEdgeThreshold;
+        for (DexEncodedMethod possibleTarget : possibleTargets) {
+          if (possibleTarget.isProgramMethod(appView)) {
+            addTarget(possibleTarget, likelySpuriousCallEdge);
+          }
+        }
+      }
+    }
+
+    private void processFieldAccess(DexField field) {
+      // Any field access implicitly calls the class initializer.
+      if (field.holder.isClassType()) {
+        DexEncodedField encodedField = appView.appInfo().resolveField(field);
+        if (encodedField != null && encodedField.isStatic()) {
+          addClassInitializerTarget(field.holder);
+        }
+      }
+    }
+
+    @Override
+    public boolean registerInvokeVirtual(DexMethod method) {
+      processInvoke(Invoke.Type.VIRTUAL, method);
+      return false;
+    }
+
+    @Override
+    public boolean registerInvokeDirect(DexMethod method) {
+      processInvoke(Invoke.Type.DIRECT, method);
+      return false;
+    }
+
+    @Override
+    public boolean registerInvokeStatic(DexMethod method) {
+      processInvoke(Invoke.Type.STATIC, method);
+      return false;
+    }
+
+    @Override
+    public boolean registerInvokeInterface(DexMethod method) {
+      processInvoke(Invoke.Type.INTERFACE, method);
+      return false;
+    }
+
+    @Override
+    public boolean registerInvokeSuper(DexMethod method) {
+      processInvoke(Invoke.Type.SUPER, method);
+      return false;
+    }
+
+    @Override
+    public boolean registerInstanceFieldWrite(DexField field) {
+      processFieldAccess(field);
+      return false;
+    }
+
+    @Override
+    public boolean registerInstanceFieldRead(DexField field) {
+      processFieldAccess(field);
+      return false;
+    }
+
+    @Override
+    public boolean registerNewInstance(DexType type) {
+      if (type.isClassType()) {
+        addClassInitializerTarget(type);
+      }
+      return false;
+    }
+
+    @Override
+    public boolean registerStaticFieldRead(DexField field) {
+      processFieldAccess(field);
+      return false;
+    }
+
+    @Override
+    public boolean registerStaticFieldWrite(DexField field) {
+      processFieldAccess(field);
+      return false;
+    }
+
+    @Override
+    public boolean registerTypeReference(DexType type) {
+      return false;
+    }
+
+    @Override
+    public void registerCallSite(DexCallSite callSite) {
+      registerMethodHandle(
+          callSite.bootstrapMethod, MethodHandleUse.NOT_ARGUMENT_TO_LAMBDA_METAFACTORY);
+    }
+  }
+
+  static class CycleEliminator {
+
+    static final String CYCLIC_FORCE_INLINING_MESSAGE =
+        "Unable to satisfy force inlining constraints due to cyclic force inlining";
+
+    private static class CallEdge {
+
+      private final Node caller;
+      private final Node callee;
+
+      CallEdge(Node caller, Node callee) {
+        this.caller = caller;
+        this.callee = callee;
+      }
+
+      public void remove() {
+        callee.removeCaller(caller);
+      }
+    }
+
+    static class CycleEliminationResult {
+
+      private Map<Node, Set<Node>> removedEdges;
+
+      CycleEliminationResult(Map<Node, Set<Node>> removedEdges) {
+        this.removedEdges = removedEdges;
+      }
+
+      void forEachRemovedCaller(Node callee, Consumer<Node> fn) {
+        removedEdges.getOrDefault(callee, ImmutableSet.of()).forEach(fn);
+      }
+
+      int numberOfRemovedEdges() {
+        int numberOfRemovedEdges = 0;
+        for (Set<Node> nodes : removedEdges.values()) {
+          numberOfRemovedEdges += nodes.size();
+        }
+        return numberOfRemovedEdges;
+      }
+    }
+
+    private final Collection<Node> nodes;
+    private final InternalOptions options;
+
+    // DFS stack.
+    private Deque<Node> stack = new ArrayDeque<>();
+
+    // Set of nodes on the DFS stack.
+    private Set<Node> stackSet = Sets.newIdentityHashSet();
+
+    // Set of nodes that have been visited entirely.
+    private Set<Node> marked = Sets.newIdentityHashSet();
+
+    // Mapping from callee to the set of callers that were removed from the callee.
+    private Map<Node, Set<Node>> removedEdges = new IdentityHashMap<>();
+
+    private int currentDepth = 0;
+    private int maxDepth = 0;
+
+    CycleEliminator(Collection<Node> nodes, InternalOptions options) {
+      this.options = options;
+
+      // Call to reorderNodes must happen after assigning options.
+      this.nodes =
+          options.testing.nondeterministicCycleElimination
+              ? reorderNodes(new ArrayList<>(nodes))
+              : nodes;
+    }
+
+    CycleEliminationResult breakCycles() {
+      // Break cycles in this call graph by removing edges causing cycles.
+      for (Node node : nodes) {
+        assert currentDepth == 0;
+        traverse(node);
+      }
+      CycleEliminationResult result = new CycleEliminationResult(removedEdges);
+      if (Log.ENABLED) {
+        Log.info(getClass(), "# call graph cycles broken: %s", result.numberOfRemovedEdges());
+        Log.info(getClass(), "# max call graph depth: %s", maxDepth);
+      }
+      reset();
+      return result;
+    }
+
+    private void reset() {
+      assert currentDepth == 0;
+      assert stack.isEmpty();
+      assert stackSet.isEmpty();
+      marked.clear();
+      maxDepth = 0;
+      removedEdges = new IdentityHashMap<>();
+    }
+
+    private void traverse(Node node) {
+      if (Log.ENABLED) {
+        if (currentDepth > maxDepth) {
+          maxDepth = currentDepth;
+        }
+      }
+
+      if (marked.contains(node)) {
+        // Already visited all nodes that can be reached from this node.
+        return;
+      }
+
+      push(node);
+
+      // The callees must be sorted before calling traverse recursively. This ensures that cycles
+      // are broken the same way across multiple compilations.
+      Collection<Node> callees = node.getCalleesWithDeterministicOrder();
+
+      if (options.testing.nondeterministicCycleElimination) {
+        callees = reorderNodes(new ArrayList<>(callees));
+      }
+
+      Iterator<Node> calleeIterator = callees.iterator();
+      while (calleeIterator.hasNext()) {
+        Node callee = calleeIterator.next();
+
+        // If we've exceeded the depth threshold, then treat it as if we have found a cycle. This
+        // ensures that we won't run into stack overflows when the call graph contains large call
+        // chains. This should have a negligible impact on code size as long as the threshold is
+        // large enough.
+        boolean foundCycle = stackSet.contains(callee);
+        boolean thresholdExceeded =
+            currentDepth >= options.callGraphCycleEliminatorMaxDepthThreshold
+                && edgeRemovalIsSafe(node, callee);
+        if (foundCycle || thresholdExceeded) {
+          // Found a cycle that needs to be eliminated.
+          if (edgeRemovalIsSafe(node, callee)) {
+            // Break the cycle by removing the edge node->callee.
+            if (options.testing.nondeterministicCycleElimination) {
+              callee.removeCaller(node);
+            } else {
+              // Need to remove `callee` from `node.callees` using the iterator to prevent a
+              // ConcurrentModificationException. This is not needed when nondeterministic cycle
+              // elimination is enabled, because we iterate a copy of `node.callees` in that case.
+              calleeIterator.remove();
+              callee.getCallersWithDeterministicOrder().remove(node);
+            }
+            recordEdgeRemoval(node, callee);
+
+            if (Log.ENABLED) {
+              Log.info(
+                  CallGraph.class,
+                  "Removed call edge from method '%s' to '%s'",
+                  node.method.toSourceString(),
+                  callee.method.toSourceString());
+            }
+          } else {
+            assert foundCycle;
+
+            // The cycle has a method that is marked as force inline.
+            LinkedList<Node> cycle = extractCycle(callee);
+
+            if (Log.ENABLED) {
+              Log.info(
+                  CallGraph.class, "Extracted cycle to find an edge that can safely be removed");
+            }
+
+            // Break the cycle by finding an edge that can be removed without breaking force
+            // inlining. If that is not possible, this call fails with a compilation error.
+            CallEdge edge = findCallEdgeForRemoval(cycle);
+
+            // The edge will be null if this cycle has already been eliminated as a result of
+            // another cycle elimination.
+            if (edge != null) {
+              assert edgeRemovalIsSafe(edge.caller, edge.callee);
+
+              // Break the cycle by removing the edge caller->callee.
+              edge.remove();
+              recordEdgeRemoval(edge.caller, edge.callee);
+
+              if (Log.ENABLED) {
+                Log.info(
+                    CallGraph.class,
+                    "Removed call edge from force inlined method '%s' to '%s' to ensure that "
+                        + "force inlining will succeed",
+                    node.method.toSourceString(),
+                    callee.method.toSourceString());
+              }
+            }
+
+            // Recover the stack.
+            recoverStack(cycle);
+          }
+        } else {
+          currentDepth++;
+          traverse(callee);
+          currentDepth--;
+        }
+      }
+      pop(node);
+      marked.add(node);
+    }
+
+    private void push(Node node) {
+      stack.push(node);
+      boolean changed = stackSet.add(node);
+      assert changed;
+    }
+
+    private void pop(Node node) {
+      Node popped = stack.pop();
+      assert popped == node;
+      boolean changed = stackSet.remove(node);
+      assert changed;
+    }
+
+    private LinkedList<Node> extractCycle(Node entry) {
+      LinkedList<Node> cycle = new LinkedList<>();
+      do {
+        assert !stack.isEmpty();
+        cycle.add(stack.pop());
+      } while (cycle.getLast() != entry);
+      return cycle;
+    }
+
+    private CallEdge findCallEdgeForRemoval(LinkedList<Node> extractedCycle) {
+      Node callee = extractedCycle.getLast();
+      for (Node caller : extractedCycle) {
+        if (!caller.hasCallee(callee)) {
+          // No need to break any edges since this cycle has already been broken previously.
+          assert !callee.hasCaller(caller);
+          return null;
+        }
+        if (edgeRemovalIsSafe(caller, callee)) {
+          return new CallEdge(caller, callee);
+        }
+        callee = caller;
+      }
+      throw new CompilationError(CYCLIC_FORCE_INLINING_MESSAGE);
+    }
+
+    private static boolean edgeRemovalIsSafe(Node caller, Node callee) {
+      // All call edges where the callee is a method that should be force inlined must be kept,
+      // to guarantee that the IR converter will process the callee before the caller.
+      return !callee.method.getOptimizationInfo().forceInline();
+    }
+
+    private void recordEdgeRemoval(Node caller, Node callee) {
+      removedEdges.computeIfAbsent(callee, ignore -> SetUtils.newIdentityHashSet(2)).add(caller);
+    }
+
+    private void recoverStack(LinkedList<Node> extractedCycle) {
+      Iterator<Node> descendingIt = extractedCycle.descendingIterator();
+      while (descendingIt.hasNext()) {
+        stack.push(descendingIt.next());
+      }
+    }
+
+    private Collection<Node> reorderNodes(List<Node> nodes) {
+      assert options.testing.nondeterministicCycleElimination;
+      if (!InternalOptions.DETERMINISTIC_DEBUGGING) {
+        Collections.shuffle(nodes);
+      }
+      return nodes;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/FieldOptimizationFeedback.java b/src/main/java/com/android/tools/r8/ir/conversion/FieldOptimizationFeedback.java
index 50fde0e..46a0cc4 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/FieldOptimizationFeedback.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/FieldOptimizationFeedback.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.ir.conversion;
 
 import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.ir.analysis.type.ClassTypeLatticeElement;
 import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 
 public interface FieldOptimizationFeedback {
@@ -13,7 +14,9 @@
 
   void markFieldAsPropagated(DexEncodedField field);
 
-  void markFieldHasDynamicType(DexEncodedField field, TypeLatticeElement type);
+  void markFieldHasDynamicLowerBoundType(DexEncodedField field, ClassTypeLatticeElement type);
+
+  void markFieldHasDynamicUpperBoundType(DexEncodedField field, TypeLatticeElement type);
 
   void markFieldBitsRead(DexEncodedField field, int bitsRead);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index 995a583..7ef6cd5 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -620,8 +620,12 @@
     }
 
     // Update the IR code if collected call site optimization info has something useful.
+    // While aggregation of parameter information at call sites would be more precise than static
+    // types, those could be still less precise at one single call site, where specific arguments
+    // will be passed during (double) inlining. Instead of adding assumptions and removing invalid
+    // ones, it's better not to insert assumptions for inlinee in the beginning.
     CallSiteOptimizationInfo callSiteOptimizationInfo = method.getCallSiteOptimizationInfo();
-    if (appView.callSiteOptimizationInfoPropagator() != null) {
+    if (method == context && appView.callSiteOptimizationInfoPropagator() != null) {
       appView.callSiteOptimizationInfoPropagator()
           .applyCallSiteOptimizationInfo(ir, callSiteOptimizationInfo);
     }
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 dd3416f..7e15e72 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
@@ -205,7 +205,6 @@
             .map(prefix -> "L" + DescriptorUtils.getPackageBinaryNameFromJavaType(prefix))
             .map(options.itemFactory::createString)
             .collect(Collectors.toList());
-    this.methodOptimizationInfoCollector = new MethodOptimizationInfoCollector(appView);
     if (options.isDesugaredLibraryCompilation()) {
       // Specific L8 Settings.
       // BackportedMethodRewriter is needed for retarget core library members and backports.
@@ -241,6 +240,7 @@
       this.stringSwitchRemover = null;
       this.desugaredLibraryAPIConverter = null;
       this.serviceLoaderRewriter = null;
+      this.methodOptimizationInfoCollector = null;
       return;
     }
     this.lambdaRewriter = options.enableDesugaring ? new LambdaRewriter(appView, this) : null;
@@ -287,13 +287,16 @@
           options.enableTreeShakingOfLibraryMethodOverrides
               ? new LibraryMethodOverrideAnalysis(appViewWithLiveness)
               : null;
-      this.lensCodeRewriter = new LensCodeRewriter(appViewWithLiveness, lambdaRewriter);
-      this.inliner = new Inliner(appViewWithLiveness, mainDexClasses, lensCodeRewriter);
       this.lambdaMerger =
           options.enableLambdaMerging ? new LambdaMerger(appViewWithLiveness) : null;
+      this.lensCodeRewriter = new LensCodeRewriter(appViewWithLiveness, lambdaRewriter);
+      this.inliner =
+          new Inliner(appViewWithLiveness, mainDexClasses, lambdaMerger, lensCodeRewriter);
       this.outliner = new Outliner(appViewWithLiveness, this);
       this.memberValuePropagation =
           options.enableValuePropagation ? new MemberValuePropagation(appViewWithLiveness) : null;
+      this.methodOptimizationInfoCollector =
+          new MethodOptimizationInfoCollector(appViewWithLiveness);
       if (options.isMinifying()) {
         this.identifierNameStringMarker = new IdentifierNameStringMarker(appViewWithLiveness);
       } else {
@@ -329,6 +332,7 @@
       this.d8NestBasedAccessDesugaring =
           options.shouldDesugarNests() ? new D8NestBasedAccessDesugaring(appView) : null;
       this.serviceLoaderRewriter = null;
+      this.methodOptimizationInfoCollector = null;
     }
     this.stringSwitchRemover =
         options.isStringSwitchConversionEnabled()
@@ -429,7 +433,7 @@
   private void synthesizeJava8UtilityClass(
       Builder<?> builder, ExecutorService executorService) throws ExecutionException {
     if (backportedMethodRewriter != null) {
-      backportedMethodRewriter.synthesizeUtilityClass(builder, executorService, options);
+      backportedMethodRewriter.synthesizeUtilityClasses(builder, executorService);
     }
   }
 
@@ -685,6 +689,7 @@
                   CallSiteInformation.empty(),
                   Outliner::noProcessing),
           executorService);
+      feedback.updateVisibleOptimizationInfo();
       timing.end();
     }
 
@@ -852,6 +857,7 @@
                 // unused out-values.
                 codeRewriter.rewriteMoveResult(code);
                 deadCodeRemover.run(code);
+                codeRewriter.removeAssumeInstructions(code);
                 consumer.accept(code, method);
                 return null;
               }));
@@ -879,7 +885,7 @@
 
   private void collectLambdaMergingCandidates(DexApplication application) {
     if (lambdaMerger != null) {
-      lambdaMerger.collectGroupCandidates(application, appView.withLiveness());
+      lambdaMerger.collectGroupCandidates(application);
     }
   }
 
@@ -1116,6 +1122,11 @@
       }
     }
 
+    if (lambdaMerger != null) {
+      lambdaMerger.rewriteCode(method, code);
+      assert code.isConsistentSSA();
+    }
+
     if (typeChecker != null && !typeChecker.check(code)) {
       assert appView.enableWholeProgramOptimizations();
       assert options.testing.allowTypeErrors;
@@ -1242,9 +1253,10 @@
     if (stringSwitchRemover != null) {
       stringSwitchRemover.run(method, code);
     }
-    codeRewriter.rewriteSwitch(code);
     codeRewriter.processMethodsNeverReturningNormally(code);
-    codeRewriter.simplifyIf(code);
+    if (codeRewriter.simplifyControlFlow(code)) {
+      codeRewriter.removeTrivialCheckCastAndInstanceOfInstructions(code);
+    }
     if (options.enableRedundantConstNumberOptimization) {
       codeRewriter.redundantConstNumberRemoval(code);
     }
@@ -1351,7 +1363,7 @@
     previous = printMethod(code, "IR after twr close resource rewriter (SSA)", previous);
 
     if (lambdaMerger != null) {
-      lambdaMerger.processMethodCode(method, code);
+      lambdaMerger.analyzeCode(method, code);
       assert code.isConsistentSSA();
     }
 
@@ -1389,24 +1401,6 @@
       classStaticizer.examineMethodCode(method, code);
     }
 
-    if (nonNullTracker != null) {
-      // TODO(b/139246447): Once we extend this optimization to, e.g., constants of primitive args,
-      //   this may not be the right place to collect call site optimization info.
-      // Collecting call-site optimization info depends on the existence of non-null IRs.
-      // Arguments can be changed during the debug mode.
-      if (!isDebugMode && appView.callSiteOptimizationInfoPropagator() != null) {
-        appView.callSiteOptimizationInfoPropagator().collectCallSiteOptimizationInfo(code);
-      }
-      // Computation of non-null parameters on normal exits rely on the existence of non-null IRs.
-      methodOptimizationInfoCollector.computeNonNullParamOnNormalExits(feedback, code);
-    }
-    if (aliasIntroducer != null || nonNullTracker != null || dynamicTypeOptimization != null) {
-      codeRewriter.removeAssumeInstructions(code);
-      assert code.isConsistentSSA();
-    }
-    // Assert that we do not have unremoved non-sense code in the output, e.g., v <- non-null NULL.
-    assert code.verifyNoNullabilityBottomTypes();
-
     assert code.verifyTypes(appView);
 
     if (appView.enableWholeProgramOptimizations()) {
@@ -1418,31 +1412,30 @@
         fieldBitAccessAnalysis.recordFieldAccesses(code, feedback);
       }
 
+      // Arguments can be changed during the debug mode.
+      if (!isDebugMode && appView.callSiteOptimizationInfoPropagator() != null) {
+        appView.callSiteOptimizationInfoPropagator().collectCallSiteOptimizationInfo(code);
+      }
+
       // Compute optimization info summary for the current method unless it is pinned
       // (in that case we should not be making any assumptions about the behavior of the method).
       if (!appView.appInfo().withLiveness().isPinned(method.method)) {
-        methodOptimizationInfoCollector.identifyClassInlinerEligibility(method, code, feedback);
-        methodOptimizationInfoCollector.identifyParameterUsages(method, code, feedback);
-        methodOptimizationInfoCollector.identifyReturnsArgument(method, code, feedback);
-        methodOptimizationInfoCollector.identifyTrivialInitializer(method, code, feedback);
-
-        if (options.enableInlining && inliner != null) {
-          methodOptimizationInfoCollector
-              .identifyInvokeSemanticsForInlining(method, code, appView, feedback);
-        }
-
         methodOptimizationInfoCollector
-            .computeDynamicReturnType(dynamicTypeOptimization, feedback, method, code);
+            .collectMethodOptimizationInfo(method, code, feedback, dynamicTypeOptimization);
         FieldValueAnalysis.run(appView, code, feedback, method);
-        methodOptimizationInfoCollector
-            .computeInitializedClassesOnNormalExit(feedback, method, code);
-        methodOptimizationInfoCollector.computeMayHaveSideEffects(feedback, method, code);
-        methodOptimizationInfoCollector
-            .computeReturnValueOnlyDependsOnArguments(feedback, method, code);
-        methodOptimizationInfoCollector.computeNonNullParamOrThrow(feedback, method, code);
       }
     }
 
+    if (aliasIntroducer != null || nonNullTracker != null || dynamicTypeOptimization != null) {
+      codeRewriter.removeAssumeInstructions(code);
+      assert code.isConsistentSSA();
+    }
+
+    // Assert that we do not have unremoved non-sense code in the output, e.g., v <- non-null NULL.
+    assert code.verifyNoNullabilityBottomTypes();
+
+    assert code.verifyTypes(appView);
+
     previous =
         printMethod(code, "IR after computation of optimization info summary (SSA)", previous);
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java
index 9a55240..a7933d2 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/MethodProcessor.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.ir.conversion.CallGraph.Node;
+import com.android.tools.r8.ir.conversion.CallGraphBuilderBase.CycleEliminator;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Action;
 import com.android.tools.r8.utils.IROrdering;
@@ -70,7 +71,13 @@
     return waves;
   }
 
-  private static void extractLeaves(Set<Node> nodes, Consumer<Node> fn) {
+  /**
+   * Extract the next set of leaves (nodes with an outgoing call degree of 0) if any.
+   *
+   * <p>All nodes in the graph are extracted if called repeatedly until null is returned. Please
+   * note that there are no cycles in this graph (see {@link CycleEliminator#breakCycles}).
+   */
+  static void extractLeaves(Set<Node> nodes, Consumer<Node> fn) {
     Set<Node> removed = Sets.newIdentityHashSet();
     Iterator<Node> nodeIterator = nodes.iterator();
     while (nodeIterator.hasNext()) {
@@ -81,7 +88,7 @@
         removed.add(node);
       }
     }
-    removed.forEach(Node::cleanForRemoval);
+    removed.forEach(Node::cleanCallersForRemoval);
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PartialCallGraphBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/PartialCallGraphBuilder.java
new file mode 100644
index 0000000..ae2c5c5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PartialCallGraphBuilder.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.ir.conversion;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.ThreadUtils;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
+
+public class PartialCallGraphBuilder extends CallGraphBuilderBase {
+  private final Set<DexEncodedMethod> seeds;
+
+  PartialCallGraphBuilder(AppView<AppInfoWithLiveness> appView, Set<DexEncodedMethod> seeds) {
+    super(appView);
+    assert seeds != null && !seeds.isEmpty();
+    this.seeds = seeds;
+  }
+
+  @Override
+  void process(ExecutorService executorService) throws ExecutionException {
+    List<Future<?>> futures = new ArrayList<>();
+    for (DexEncodedMethod method : seeds) {
+      futures.add(
+          executorService.submit(
+              () -> {
+                processMethod(method);
+                return null; // we want a Callable not a Runnable to be able to throw
+              }));
+    }
+    ThreadUtils.awaitFutures(futures);
+  }
+
+  private void processMethod(DexEncodedMethod method) {
+    if (method.hasCode()) {
+      method.registerCodeReferences(
+          new InvokeExtractor(getOrCreateNode(method), seeds::contains));
+    }
+  }
+
+  @Override
+  boolean verifyAllMethodsWithCodeExists() {
+    for (DexEncodedMethod method : seeds) {
+      assert !method.hasCode() || nodes.get(method.method) != null;
+    }
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
new file mode 100644
index 0000000..13566359
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
@@ -0,0 +1,33 @@
+// 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.conversion;
+
+import com.android.tools.r8.ir.conversion.CallGraph.Node;
+import com.google.common.collect.Sets;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.function.Consumer;
+
+public class PostMethodProcessor {
+
+  /**
+   * Extract the next set of roots (nodes with an incoming call degree of 0) if any.
+   *
+   * <p>All nodes in the graph are extracted if called repeatedly until null is returned.
+   */
+  static void extractRoots(Set<Node> nodes, Consumer<Node> fn) {
+    Set<Node> removed = Sets.newIdentityHashSet();
+    Iterator<Node> nodeIterator = nodes.iterator();
+    while (nodeIterator.hasNext()) {
+      Node node = nodeIterator.next();
+      if (node.isRoot()) {
+        fn.accept(node);
+        nodeIterator.remove();
+        removed.add(node);
+      }
+    }
+    removed.forEach(Node::cleanCalleesForRemoval);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
index be6ffb2..bf82f85 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
@@ -100,7 +100,34 @@
       InvokeMethod invoke = instruction.asInvokeMethod();
       MethodProvider provider = getMethodProviderOrNull(invoke.getInvokedMethod());
       if (provider == null) {
-        continue;
+        if (!rewritableMethods.matchesVirtualRewrite(invoke.getInvokedMethod())) {
+          continue;
+        }
+        // We need to force resolution, even on d8, to know if the invoke has to be rewritten.
+        DexEncodedMethod dexEncodedMethod =
+            quickLookUp(invoke.getInvokedMethod());
+        if (dexEncodedMethod == null) {
+          continue;
+        }
+        provider = getMethodProviderOrNull(dexEncodedMethod.method);
+        if (provider == null) {
+          continue;
+        }
+
+        // Since we are rewriting a virtual method into a static invoke in this case, the look-up
+        // logic gets confused. Final methods rewritten in such a way are always or invokes from a
+        // library class are rewritten into the static invoke, which is correct. However,
+        // overrides of the programmer are currently disabled. We still rewrite everything to make
+        // basic cases work.
+        // TODO(b/142846107): Support overrides of retarget virtual methods by uncommenting the
+        // following and implementing doSomethingSmart().
+
+        // DexClass receiverType = appView.definitionFor(invoke.getInvokedMethod().holder);
+        // if (!(dexEncodedMethod.isFinal()
+        //     || (receiverType != null && receiverType.isLibraryClass()))) {
+        //   doSomethingSmart();
+        //   continue;
+        // }
       }
 
       provider.rewriteInvoke(invoke, iterator, code, appView);
@@ -113,6 +140,31 @@
     }
   }
 
+  private DexEncodedMethod quickLookUp(DexMethod method) {
+    // Since retargeting cannot be on interface, we do a quick look-up excluding interfaces.
+    // On R8 resolution is immediate, on d8 it may look-up.
+    DexClass current = appView.definitionFor(method.holder);
+    if (current == null) {
+     return null;
+    }
+    DexEncodedMethod dexEncodedMethod = current.lookupVirtualMethod(method);
+    if (dexEncodedMethod != null) {
+      return dexEncodedMethod;
+    }
+    while (current.superType != factory.objectType) {
+      DexType superType = current.superType;
+      current = appView.definitionFor(superType);
+      if (current == null) {
+        return null;
+      }
+      dexEncodedMethod = current.lookupVirtualMethod(method);
+      if (dexEncodedMethod != null) {
+        return dexEncodedMethod;
+      }
+    }
+    return null;
+  }
+
   private Collection<DexProgramClass> findSynthesizedFrom(Builder<?> builder, DexType holder) {
     for (DexProgramClass synthesizedClass : builder.getSynthesizedClasses()) {
       if (holder == synthesizedClass.getType()) {
@@ -126,8 +178,7 @@
     return clazz.descriptor.toString().startsWith(UTILITY_CLASS_DESCRIPTOR_PREFIX);
   }
 
-  public void synthesizeUtilityClass(
-      Builder<?> builder, ExecutorService executorService, InternalOptions options)
+  public void synthesizeUtilityClasses(Builder<?> builder, ExecutorService executorService)
       throws ExecutionException {
     if (holders.isEmpty()) {
       return;
@@ -161,7 +212,7 @@
       if (appView.definitionFor(method.holder) != null) {
         continue;
       }
-      Code code = provider.generateTemplateMethod(options, method);
+      Code code = provider.generateTemplateMethod(appView.options(), method);
       DexEncodedMethod dexEncodedMethod =
           new DexEncodedMethod(
               method, flags, DexAnnotationSet.empty(), ParameterAnnotationsList.empty(), code);
@@ -181,7 +232,7 @@
               DexAnnotationSet.empty(),
               DexEncodedField.EMPTY_ARRAY,
               DexEncodedField.EMPTY_ARRAY,
-              new DexEncodedMethod[] {dexEncodedMethod},
+              new DexEncodedMethod[]{dexEncodedMethod},
               DexEncodedMethod.EMPTY_ARRAY,
               factory.getSkipNameValidationForTesting(),
               referencingClasses);
@@ -221,6 +272,10 @@
 
     // Map backported method to a provider for creating the actual target method (with code).
     private final Map<DexMethod, MethodProvider> rewritable = new IdentityHashMap<>();
+    // Map virtualRewrites hold a methodName->method mapping for virtual methods which are
+    // rewritten while the holder is non final but no superclass implement the method. In this case
+    // d8 needs to force resolution of given methods to see if the invoke needs to be rewritten.
+    private final Map<DexString, List<DexMethod>> virtualRewrites = new IdentityHashMap<>();
 
     RewritableMethods(InternalOptions options, AppView<?> appView) {
       DexItemFactory factory = options.itemFactory;
@@ -253,6 +308,19 @@
       }
     }
 
+    boolean matchesVirtualRewrite(DexMethod method) {
+      List<DexMethod> dexMethods = virtualRewrites.get(method.name);
+      if (dexMethods == null) {
+        return false;
+      }
+      for (DexMethod dexMethod : dexMethods) {
+        if (method.match(dexMethod)) {
+          return true;
+        }
+      }
+      return false;
+    }
+
     boolean isEmpty() {
       return rewritable.isEmpty();
     }
@@ -1007,7 +1075,8 @@
                 : new MethodGenerator(
                     method,
                     (options, methodArg) ->
-                        CollectionMethodGenerators.generateListOf(options, methodArg, formalCount)));
+                        CollectionMethodGenerators.generateListOf(
+                            options, methodArg, formalCount)));
       }
       proto = factory.createProto(type, factory.objectArrayType);
       method = factory.createMethod(type, proto, name);
@@ -1099,25 +1168,25 @@
 
       // Optional{void,Int,Long,Double}.ifPresentOrElse(consumer,runnable)
       DexType[] optionalTypes =
-          new DexType[] {
-            optionalType,
-            factory.createType(factory.createString("Ljava/util/OptionalDouble;")),
-            factory.createType(factory.createString("Ljava/util/OptionalLong;")),
-            factory.createType(factory.createString("Ljava/util/OptionalInt;"))
+          new DexType[]{
+              optionalType,
+              factory.createType(factory.createString("Ljava/util/OptionalDouble;")),
+              factory.createType(factory.createString("Ljava/util/OptionalLong;")),
+              factory.createType(factory.createString("Ljava/util/OptionalInt;"))
           };
       DexType[] consumerTypes =
-          new DexType[] {
-            factory.consumerType,
-            factory.createType("Ljava/util/function/DoubleConsumer;"),
-            factory.createType("Ljava/util/function/LongConsumer;"),
-            factory.createType("Ljava/util/function/IntConsumer;")
+          new DexType[]{
+              factory.consumerType,
+              factory.createType("Ljava/util/function/DoubleConsumer;"),
+              factory.createType("Ljava/util/function/LongConsumer;"),
+              factory.createType("Ljava/util/function/IntConsumer;")
           };
       TemplateMethodFactory[] methodFactories =
-          new TemplateMethodFactory[] {
-            BackportedMethods::OptionalMethods_ifPresentOrElse,
-            BackportedMethods::OptionalMethods_ifPresentOrElseDouble,
-            BackportedMethods::OptionalMethods_ifPresentOrElseLong,
-            BackportedMethods::OptionalMethods_ifPresentOrElseInt
+          new TemplateMethodFactory[]{
+              BackportedMethods::OptionalMethods_ifPresentOrElse,
+              BackportedMethods::OptionalMethods_ifPresentOrElseDouble,
+              BackportedMethods::OptionalMethods_ifPresentOrElseLong,
+              BackportedMethods::OptionalMethods_ifPresentOrElseInt
           };
       for (int i = 0; i < optionalTypes.length; i++) {
         DexType optional = optionalTypes[i];
@@ -1151,6 +1220,10 @@
             DexType newHolder = retargetCoreLibMember.get(methodName).get(inType);
             List<DexEncodedMethod> found = findDexEncodedMethodsWithName(methodName, typeClass);
             for (DexEncodedMethod encodedMethod : found) {
+              if (!encodedMethod.isStatic()) {
+                virtualRewrites.putIfAbsent(encodedMethod.method.name, new ArrayList<>());
+                virtualRewrites.get(encodedMethod.method.name).add(encodedMethod.method);
+              }
               DexProto proto = encodedMethod.method.proto;
               DexMethod method = appView.dexItemFactory().createMethod(inType, proto, methodName);
               addProvider(
@@ -1185,6 +1258,7 @@
   }
 
   public abstract static class MethodProvider {
+
     final DexMethod method;
 
     public MethodProvider(DexMethod method) {
@@ -1244,6 +1318,7 @@
   }
 
   private static final class InvokeRewriter extends MethodProvider {
+
     private final MethodInvokeRewriter rewriter;
 
     InvokeRewriter(DexMethod method, MethodInvokeRewriter rewriter) {
@@ -1251,8 +1326,9 @@
       this.rewriter = rewriter;
     }
 
-    @Override public void rewriteInvoke(InvokeMethod invoke, InstructionListIterator iterator,
-        IRCode code, AppView<?> appView) {
+    @Override
+    public void rewriteInvoke(
+        InvokeMethod invoke, InstructionListIterator iterator, IRCode code, AppView<?> appView) {
       rewriter.rewrite(invoke, iterator, appView.dexItemFactory());
       assert code.isConsistentSSA();
     }
@@ -1365,6 +1441,7 @@
   }
 
   private interface MethodInvokeRewriter {
+
     void rewrite(InvokeMethod invoke, InstructionListIterator iterator, DexItemFactory factory);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
index d81f3fe..688e751 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.ir.code.Invoke;
@@ -114,7 +115,12 @@
     DexClass target = appView.definitionFor(method.holder);
     // NOTE: Never add a forwarding method to methods of classes unknown or coming from android.jar
     // even if this results in invalid code, these classes are never desugared.
-    assert target != null && !rewriter.isNonDesugaredLibraryClass(target);
+    assert target != null;
+    // In desugared library, emulated interface methods can be overridden by retarget lib members.
+    DexMethod forwardMethod =
+        target.isInterface()
+            ? rewriter.defaultAsMethodOfCompanionClass(method)
+            : retargetMethod(method);
     // New method will have the same name, proto, and also all the flags of the
     // default method, including bridge flag.
     DexMethod newMethod = dexItemFactory.createMethod(clazz.type, method.proto, method.name);
@@ -125,7 +131,7 @@
         ForwardMethodSourceCode.builder(newMethod);
     forwardSourceCodeBuilder
         .setReceiver(clazz.type)
-        .setTarget(rewriter.defaultAsMethodOfCompanionClass(method))
+        .setTarget(forwardMethod)
         .setInvokeType(Invoke.Type.STATIC)
         .setIsInterface(false); // Holder is companion class, not an interface.
     return new DexEncodedMethod(
@@ -136,6 +142,18 @@
         new SynthesizedCode(forwardSourceCodeBuilder::build));
   }
 
+  private DexMethod retargetMethod(DexMethod method) {
+    Map<DexString, Map<DexType, DexType>> retargetCoreLibMember =
+        appView.options().desugaredLibraryConfiguration.getRetargetCoreLibMember();
+    Map<DexType, DexType> typeMap = retargetCoreLibMember.get(method.name);
+    assert typeMap != null;
+    assert typeMap.get(method.holder) != null;
+    return dexItemFactory.createMethod(
+        typeMap.get(method.holder),
+        dexItemFactory.prependTypeToProto(method.holder, method.proto),
+        method.name);
+  }
+
   // For a given class `clazz` inspects all interfaces it implements directly or
   // indirectly and collect a set of all default methods to be implemented
   // in this class.
@@ -215,17 +233,29 @@
     // Remove from candidates methods defined in class or any of its superclasses.
     List<DexEncodedMethod> toBeImplemented = new ArrayList<>(candidates.size());
     current = clazz;
+    Map<DexString, Map<DexType, DexType>> retargetCoreLibMember =
+        appView.options().desugaredLibraryConfiguration.getRetargetCoreLibMember();
     while (true) {
       // In desugared library look-up, methods from library classes cannot hide methods from
       // emulated interfaces (the method being desugared implied the implementation is not
-      // present in the library class).
+      // present in the library class), except through retarget core lib member.
       if (desugaredLibraryLookup && current.isLibraryClass()) {
         Iterator<DexEncodedMethod> iterator = candidates.iterator();
         while (iterator.hasNext()) {
           DexEncodedMethod candidate = iterator.next();
-          if (rewriter.isEmulatedInterface(candidate.method.holder)) {
-            toBeImplemented.add(candidate);
-            iterator.remove();
+          if (rewriter.isEmulatedInterface(candidate.method.holder)
+              && current.lookupVirtualMethod(candidate.method) != null) {
+            // A library class overrides an emulated interface method. This override is valid
+            // only if it goes through retarget core lib member, else it needs to be implemented.
+            Map<DexType, DexType> typeMap = retargetCoreLibMember.get(candidate.method.name);
+            if (typeMap != null && typeMap.containsKey(current.type)) {
+              // A rewrite needs to be performed, but instead of rewriting to the companion class,
+              // D8/R8 needs to rewrite to the retarget member.
+              toBeImplemented.add(current.lookupVirtualMethod(candidate.method));
+            } else {
+              toBeImplemented.add(candidate);
+              iterator.remove();
+            }
           }
         }
       }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java
index d60ac1f..cbdef95 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java
@@ -28,6 +28,7 @@
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.StringDiagnostic;
+import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -64,11 +65,20 @@
   private final DexItemFactory factory;
   private final DesugaredLibraryWrapperSynthesizer wrapperSynthesizor;
   private final Map<DexClass, Set<DexEncodedMethod>> callBackMethods = new HashMap<>();
+  private final Set<DexMethod> trackedCallBackAPIs;
+  private final Set<DexMethod> trackedAPIs;
 
   public DesugaredLibraryAPIConverter(AppView<?> appView) {
     this.appView = appView;
     this.factory = appView.dexItemFactory();
     this.wrapperSynthesizor = new DesugaredLibraryWrapperSynthesizer(appView, this);
+    if (appView.options().testing.trackDesugaredAPIConversions) {
+      trackedCallBackAPIs = Sets.newConcurrentHashSet();
+      trackedAPIs = Sets.newConcurrentHashSet();
+    } else {
+      trackedCallBackAPIs = null;
+      trackedAPIs = null;
+    }
   }
 
   public void desugar(IRCode code) {
@@ -170,6 +180,9 @@
   }
 
   private synchronized void generateCallBack(DexClass dexClass, DexEncodedMethod originalMethod) {
+    if (trackedCallBackAPIs != null) {
+      trackedCallBackAPIs.add(originalMethod.method);
+    }
     DexMethod methodToInstall =
         methodWithVivifiedTypeInSignature(originalMethod.method, dexClass.type, appView);
     if (dexClass.isInterface()
@@ -219,6 +232,10 @@
   public void generateWrappers(
       DexApplication.Builder<?> builder, IRConverter irConverter, ExecutorService executorService)
       throws ExecutionException {
+    if (appView.options().testing.trackDesugaredAPIConversions) {
+      generateTrackDesugaredAPIWarnings(trackedAPIs, "");
+      generateTrackDesugaredAPIWarnings(trackedCallBackAPIs, "callback ");
+    }
     wrapperSynthesizor.finalizeWrappers(builder, irConverter, executorService);
     for (DexClass dexClass : callBackMethods.keySet()) {
       Set<DexEncodedMethod> dexEncodedMethods = callBackMethods.get(dexClass);
@@ -227,6 +244,16 @@
     }
   }
 
+  private void generateTrackDesugaredAPIWarnings(Set<DexMethod> tracked, String inner) {
+    StringBuilder sb = new StringBuilder();
+    sb.append("Tracked ").append(inner).append("desugared API conversions: ");
+    for (DexMethod method : tracked) {
+      sb.append("\n");
+      sb.append(method);
+    }
+    appView.options().reporter.warning(new StringDiagnostic(sb.toString()));
+  }
+
   private void warnInvalidInvoke(DexType type, DexMethod invokedMethod, String debugString) {
     DexType desugaredType = appView.rewritePrefix.rewrittenType(type);
     appView
@@ -260,6 +287,9 @@
       InstructionListIterator iterator,
       ListIterator<BasicBlock> blockIterator) {
     DexMethod invokedMethod = invokeMethod.getInvokedMethod();
+    if (trackedAPIs != null) {
+      trackedAPIs.add(invokedMethod);
+    }
 
     // Create return conversion if required.
     Instruction returnConversion = null;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/AssumeDynamicTypeRemover.java b/src/main/java/com/android/tools/r8/ir/optimize/AssumeDynamicTypeRemover.java
index 257d002..73214ba 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/AssumeDynamicTypeRemover.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/AssumeDynamicTypeRemover.java
@@ -53,7 +53,7 @@
   }
 
   public void markUsersForRemoval(Value value) {
-    for (Instruction user : value.uniqueUsers()) {
+    for (Instruction user : value.aliasedUsers()) {
       if (user.isAssumeDynamicType()) {
         assert value.numberOfAllUsers() == 1
             : "Expected value flowing into Assume<DynamicTypeAssumption> instruction to have a "
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CallSiteOptimizationInfoPropagator.java b/src/main/java/com/android/tools/r8/ir/optimize/CallSiteOptimizationInfoPropagator.java
index 31add54..f93d6c5 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CallSiteOptimizationInfoPropagator.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CallSiteOptimizationInfoPropagator.java
@@ -47,11 +47,8 @@
   private enum Mode {
     COLLECT, // Set until the end of the 1st round of IR processing. CallSiteOptimizationInfo will
              // be updated in this mode only.
-    REVISIT, // Set once the all methods are processed. IRBuilder will add other instructions that
+    REVISIT  // Set once the all methods are processed. IRBuilder will add other instructions that
              // reflect collected CallSiteOptimizationInfo.
-    FINISH;  // Set once the 2nd round of IR processing is done. Other optimizations that need post
-             // IR processing, e.g., outliner, are still using IRBuilder, and this will isolate the
-             // impact of IR manipulation due to this optimization.
   }
 
   private final AppView<AppInfoWithLiveness> appView;
@@ -256,14 +253,13 @@
         }
       }
     }
+    mode = Mode.REVISIT;
     if (targetsToRevisit.isEmpty()) {
-      mode = Mode.FINISH;
       return;
     }
     if (revisitedMethods != null) {
       revisitedMethods.addAll(targetsToRevisit);
     }
-    mode = Mode.REVISIT;
     List<Future<?>> futures = new ArrayList<>();
     for (DexEncodedMethod method : targetsToRevisit) {
       futures.add(
@@ -274,6 +270,5 @@
               }));
     }
     ThreadUtils.awaitFutures(futures);
-    mode = Mode.FINISH;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java b/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java
index 6c2110b..fb75832 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java
@@ -29,6 +29,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.ir.analysis.ValueMayDependOnEnvironmentAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import com.android.tools.r8.ir.code.ArrayPut;
 import com.android.tools.r8.ir.code.BasicBlock;
@@ -327,6 +328,8 @@
 
   private Collection<StaticPut> findFinalFieldPutsWhileCollectingUnnecessaryStaticPuts(
       IRCode code, DexClass clazz, Set<StaticPut> unnecessaryStaticPuts) {
+    ValueMayDependOnEnvironmentAnalysis environmentAnalysis =
+        new ValueMayDependOnEnvironmentAnalysis(appView, code);
     Map<DexField, StaticPut> finalFieldPuts = Maps.newIdentityHashMap();
     Map<DexField, Set<StaticPut>> isWrittenBefore = Maps.newIdentityHashMap();
     Set<DexField> isReadBefore = Sets.newIdentityHashSet();
@@ -436,7 +439,7 @@
               Value outValue = instruction.outValue();
               if (outValue.numberOfAllUsers() > 0) {
                 if (instruction.isInvokeNewArray()
-                    && outValue.isConstantArrayThroughoutMethod(appView, code)) {
+                    && environmentAnalysis.isConstantArrayThroughoutMethod(outValue)) {
                   // OK, this value is technically a constant.
                   continue;
                 }
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 932c817..a47ba2c 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
@@ -14,6 +14,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -82,12 +83,14 @@
 import com.android.tools.r8.utils.InternalOptions.AssertionProcessing;
 import com.android.tools.r8.utils.InternalOutputMode;
 import com.android.tools.r8.utils.LongInterval;
+import com.android.tools.r8.utils.SetUtils;
 import com.google.common.base.Equivalence;
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Sets;
@@ -839,9 +842,9 @@
     return outliersAsIfSize;
   }
 
-  public void rewriteSwitch(IRCode code) {
+  private boolean rewriteSwitch(IRCode code) {
     if (!code.metadata().mayHaveIntSwitch()) {
-      return;
+      return false;
     }
 
     boolean needToRemoveUnreachableBlocks = false;
@@ -985,15 +988,18 @@
       }
     }
 
-    if (needToRemoveUnreachableBlocks) {
-      code.removeUnreachableBlocks();
-    }
-
     // Rewriting of switches introduces new branching structure. It relies on critical edges
     // being split on the way in but does not maintain this property. We therefore split
     // critical edges at exit.
     code.splitCriticalEdges();
+
+    Set<Value> affectedValues =
+        needToRemoveUnreachableBlocks ? code.removeUnreachableBlocks() : ImmutableSet.of();
+    if (!affectedValues.isEmpty()) {
+      new TypeAnalysis(appView).narrowing(affectedValues);
+    }
     assert code.isConsistentSSA();
+    return !affectedValues.isEmpty();
   }
 
   private SwitchCaseEliminator removeUnnecessarySwitchCases(
@@ -1247,10 +1253,10 @@
     assumeDynamicTypeRemover.finish();
     if (!blocksToBeRemoved.isEmpty()) {
       code.removeBlocks(blocksToBeRemoved);
-      code.removeAllTrivialPhis();
+      code.removeAllTrivialPhis(affectedValues);
       assert code.getUnreachableBlocks().isEmpty();
     } else if (mayHaveRemovedTrivialPhi || assumeDynamicTypeRemover.mayHaveIntroducedTrivialPhi()) {
-      code.removeAllTrivialPhis();
+      code.removeAllTrivialPhis(affectedValues);
     }
     if (!affectedValues.isEmpty()) {
       new TypeAnalysis(appView).narrowing(affectedValues);
@@ -1407,7 +1413,10 @@
     // Removing check-cast may result in a trivial phi:
     // v3 <- phi(v1, v1)
     if (needToRemoveTrivialPhis) {
-      code.removeAllTrivialPhis();
+      code.removeAllTrivialPhis(affectedValues);
+      if (!affectedValues.isEmpty()) {
+        typeAnalysis.narrowing(affectedValues);
+      }
     }
     assert code.isConsistentSSA();
   }
@@ -1524,7 +1533,11 @@
                 value -> !value.isPhi() && value.definition.isAssumeDynamicType());
         if (aliasedValue != null) {
           TypeLatticeElement dynamicType =
-              aliasedValue.definition.asAssumeDynamicType().getAssumption().getType();
+              aliasedValue
+                  .definition
+                  .asAssumeDynamicType()
+                  .getAssumption()
+                  .getDynamicUpperBoundType();
           if (dynamicType.isDefinitelyNull()) {
             result = InstanceOfResult.FALSE;
           } else if (dynamicType.lessThanOrEqual(instanceOfType, appView)
@@ -2378,7 +2391,13 @@
     assert code.isConsistentSSA();
   }
 
-  public void simplifyIf(IRCode code) {
+  public boolean simplifyControlFlow(IRCode code) {
+    boolean anyAffectedValues = rewriteSwitch(code);
+    anyAffectedValues |= simplifyIf(code);
+    return anyAffectedValues;
+  }
+
+  private boolean simplifyIf(IRCode code) {
     for (BasicBlock block : code.blocks) {
       // Skip removed (= unreachable) blocks.
       if (block.getNumber() != 0 && block.getPredecessors().isEmpty()) {
@@ -2394,27 +2413,26 @@
 
         // Simplify if conditions when possible.
         If theIf = block.exit().asIf();
-        List<Value> inValues = theIf.inValues();
+        Value lhs = theIf.lhs();
+        Value rhs = theIf.isZeroTest() ? null : theIf.rhs();
 
-        if (inValues.get(0).isConstNumber()
-            && (theIf.isZeroTest() || inValues.get(1).isConstNumber())) {
+        if (lhs.isConstNumber() && (theIf.isZeroTest() || rhs.isConstNumber())) {
           // Zero test with a constant of comparison between between two constants.
           if (theIf.isZeroTest()) {
-            ConstNumber cond = inValues.get(0).getConstInstruction().asConstNumber();
+            ConstNumber cond = lhs.getConstInstruction().asConstNumber();
             BasicBlock target = theIf.targetFromCondition(cond);
             simplifyIfWithKnownCondition(code, block, theIf, target);
           } else {
-            ConstNumber left = inValues.get(0).getConstInstruction().asConstNumber();
-            ConstNumber right = inValues.get(1).getConstInstruction().asConstNumber();
+            ConstNumber left = lhs.getConstInstruction().asConstNumber();
+            ConstNumber right = rhs.getConstInstruction().asConstNumber();
             BasicBlock target = theIf.targetFromCondition(left, right);
             simplifyIfWithKnownCondition(code, block, theIf, target);
           }
-        } else if (inValues.get(0).hasValueRange()
-            && (theIf.isZeroTest() || inValues.get(1).hasValueRange())) {
+        } else if (lhs.hasValueRange() && (theIf.isZeroTest() || rhs.hasValueRange())) {
           // Zero test with a value range, or comparison between between two values,
           // each with a value ranges.
           if (theIf.isZeroTest()) {
-            LongInterval interval = inValues.get(0).getValueRange();
+            LongInterval interval = lhs.getValueRange();
             if (!interval.containsValue(0)) {
               // Interval doesn't contain zero at all.
               int sign = Long.signum(interval.getMin());
@@ -2448,8 +2466,8 @@
               }
             }
           } else {
-            LongInterval leftRange = inValues.get(0).getValueRange();
-            LongInterval rightRange = inValues.get(1).getValueRange();
+            LongInterval leftRange = lhs.getValueRange();
+            LongInterval rightRange = rhs.getValueRange();
             // Two overlapping ranges. Check for single point overlap.
             if (!leftRange.overlapsWith(rightRange)) {
               // No overlap.
@@ -2483,14 +2501,27 @@
               }
             }
           }
-        } else if (theIf.isZeroTest() && !inValues.get(0).isConstNumber()
-            && (theIf.getType() == Type.EQ || theIf.getType() == Type.NE)) {
-          TypeLatticeElement l = inValues.get(0).getTypeLattice();
-          if (l.isReference() && inValues.get(0).isNeverNull()) {
-            simplifyIfWithKnownCondition(code, block, theIf, 1);
+        } else if (theIf.getType() == Type.EQ || theIf.getType() == Type.NE) {
+          if (theIf.isZeroTest()) {
+            if (!lhs.isConstNumber()) {
+              TypeLatticeElement l = lhs.getTypeLattice();
+              if (l.isReference() && lhs.isNeverNull()) {
+                simplifyIfWithKnownCondition(code, block, theIf, 1);
+              } else {
+                if (!l.isPrimitive() && !l.isNullable()) {
+                  simplifyIfWithKnownCondition(code, block, theIf, 1);
+                }
+              }
+            }
           } else {
-            if (!l.isPrimitive() && !l.isNullable()) {
-              simplifyIfWithKnownCondition(code, block, theIf, 1);
+            DexEncodedField enumField = lhs.getEnumField(appView);
+            if (enumField != null) {
+              DexEncodedField otherEnumField = rhs.getEnumField(appView);
+              if (enumField == otherEnumField) {
+                simplifyIfWithKnownCondition(code, block, theIf, 0);
+              } else if (otherEnumField != null) {
+                simplifyIfWithKnownCondition(code, block, theIf, 1);
+              }
             }
           }
         }
@@ -2501,6 +2532,7 @@
       new TypeAnalysis(appView).narrowing(affectedValues);
     }
     assert code.isConsistentSSA();
+    return !affectedValues.isEmpty();
   }
 
   private void simplifyIfWithKnownCondition(
@@ -3500,8 +3532,8 @@
   }
 
   private static NewInstance findNewInstance(Phi phi) {
-    Set<Phi> seen = new HashSet<>();
-    Set<Value> values = new HashSet<>();
+    Set<Phi> seen = Sets.newIdentityHashSet();
+    Set<Value> values = Sets.newIdentityHashSet();
     recursiveAddOperands(phi, seen, values);
     if (values.size() != 1) {
       throw new CompilationError("Failed to identify unique new-instance for <init>");
@@ -3539,7 +3571,7 @@
       if (component.size() == 1 && component.iterator().next() == newInstanceValue) {
         continue;
       }
-      Set<Phi> trivialPhis = new HashSet<>();
+      Set<Phi> trivialPhis = Sets.newIdentityHashSet();
       for (Value value : component) {
         boolean isTrivial = true;
         Phi p = value.asPhi();
@@ -3569,7 +3601,7 @@
 
     private int currentTime = 0;
     private final Reference2IntMap<Value> discoverTime = new Reference2IntOpenHashMap<>();
-    private final Set<Value> unassignedSet = new HashSet<>();
+    private final Set<Value> unassignedSet = Sets.newIdentityHashSet();
     private final Deque<Value> unassignedStack = new ArrayDeque<>();
     private final Deque<Value> preorderStack = new ArrayDeque<>();
     private final List<Set<Value>> components = new ArrayList<>();
@@ -3603,7 +3635,7 @@
         // If the current element is the top of the preorder stack, then we are at entry to a
         // strongly-connected component consisting of this element and every element above this
         // element on the stack.
-        Set<Value> component = new HashSet<>(unassignedStack.size());
+        Set<Value> component = SetUtils.newIdentityHashSet(unassignedStack.size());
         while (true) {
           Value member = unassignedStack.pop();
           unassignedSet.remove(member);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
index e0d77fb..2649e62 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -428,9 +428,28 @@
   }
 
   @Override
-  public boolean canInlineInstanceInitializer(
-      IRCode inlinee,
+  public boolean allowInliningOfInvokeInInlinee(
+      InlineAction action,
+      int inliningDepth,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
+    assert inliningDepth > 0;
+
+    if (action.reason.mustBeInlined()) {
+      return true;
+    }
+
+    int threshold = appView.options().applyInliningToInlineeMaxDepth;
+    if (inliningDepth <= threshold) {
+      return true;
+    }
+
+    whyAreYouNotInliningReporter.reportWillExceedMaxInliningDepth(inliningDepth, threshold);
+    return false;
+  }
+
+  @Override
+  public boolean canInlineInstanceInitializer(
+      IRCode inlinee, WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
     // In the Java VM Specification section "4.10.2.4. Instance Initialization Methods and
     // Newly Created Objects" it says:
     //
@@ -685,13 +704,12 @@
   @Override
   public void updateTypeInformationIfNeeded(
       IRCode inlinee, ListIterator<BasicBlock> blockIterator, BasicBlock block) {
-    if (appView.options().enableNonNullTracking) {
+    boolean assumersEnabled =
+        appView.options().enableNonNullTracking
+            || appView.options().enableDynamicTypeOptimization
+            || appView.options().testing.forceAssumeNoneInsertion;
+    if (assumersEnabled) {
       BasicBlock state = IteratorUtils.peekNext(blockIterator);
-      // Move the cursor back to where the first inlinee block was added.
-      while (blockIterator.hasPrevious() && blockIterator.previous() != block) {
-        // Do nothing.
-      }
-      assert IteratorUtils.peekNext(blockIterator) == block;
 
       Set<BasicBlock> inlineeBlocks = Sets.newIdentityHashSet();
       inlineeBlocks.addAll(inlinee.blocks);
@@ -699,18 +717,28 @@
       // Introduce aliases only to the inlinee blocks.
       if (appView.options().testing.forceAssumeNoneInsertion) {
         insertAssumeInstructionsToInlinee(
-            new AliasIntroducer(appView), code, state, blockIterator, inlineeBlocks);
+            new AliasIntroducer(appView), code, block, blockIterator, inlineeBlocks);
       }
 
       // Add non-null IRs only to the inlinee blocks.
-      Consumer<BasicBlock> splitBlockConsumer = inlineeBlocks::add;
-      Assumer nonNullTracker = new NonNullTracker(appView, splitBlockConsumer);
-      insertAssumeInstructionsToInlinee(
-          nonNullTracker, code, state, blockIterator, inlineeBlocks);
+      if (appView.options().enableNonNullTracking) {
+        Consumer<BasicBlock> splitBlockConsumer = inlineeBlocks::add;
+        Assumer nonNullTracker = new NonNullTracker(appView, splitBlockConsumer);
+        insertAssumeInstructionsToInlinee(
+            nonNullTracker, code, block, blockIterator, inlineeBlocks);
+      }
 
       // Add dynamic type assumptions only to the inlinee blocks.
-      insertAssumeInstructionsToInlinee(
-          new DynamicTypeOptimization(appView), code, state, blockIterator, inlineeBlocks);
+      if (appView.options().enableDynamicTypeOptimization) {
+        insertAssumeInstructionsToInlinee(
+            new DynamicTypeOptimization(appView), code, block, blockIterator, inlineeBlocks);
+      }
+
+      // Restore the old state of the iterator.
+      while (blockIterator.hasPrevious() && blockIterator.previous() != state) {
+        // Do nothing.
+      }
+      assert IteratorUtils.peekNext(blockIterator) == state;
     }
     // TODO(b/72693244): need a test where refined env in inlinee affects the caller.
   }
@@ -718,17 +746,17 @@
   private void insertAssumeInstructionsToInlinee(
       Assumer assumer,
       IRCode code,
-      BasicBlock state,
+      BasicBlock block,
       ListIterator<BasicBlock> blockIterator,
       Set<BasicBlock> inlineeBlocks) {
-    assumer.insertAssumeInstructionsInBlocks(code, blockIterator, inlineeBlocks::contains);
-    assert !blockIterator.hasNext();
-
-    // Restore the old state of the iterator.
-    while (blockIterator.hasPrevious() && blockIterator.previous() != state) {
+    // Move the cursor back to where the first inlinee block was added.
+    while (blockIterator.hasPrevious() && blockIterator.previous() != block) {
       // Do nothing.
     }
-    assert IteratorUtils.peekNext(blockIterator) == state;
+    assert IteratorUtils.peekNext(blockIterator) == block;
+
+    assumer.insertAssumeInstructionsInBlocks(code, blockIterator, inlineeBlocks::contains);
+    assert !blockIterator.hasNext();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DynamicTypeOptimization.java b/src/main/java/com/android/tools/r8/ir/optimize/DynamicTypeOptimization.java
index 26f383b..e5c53b3 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DynamicTypeOptimization.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DynamicTypeOptimization.java
@@ -58,7 +58,7 @@
         continue;
       }
 
-      TypeLatticeElement dynamicType;
+      TypeLatticeElement dynamicUpperBoundType;
       ClassTypeLatticeElement dynamicLowerBoundType;
       if (current.isInvokeMethod()) {
         InvokeMethod invoke = current.asInvokeMethod();
@@ -80,7 +80,7 @@
           continue;
         }
 
-        dynamicType = optimizationInfo.getDynamicReturnType();
+        dynamicUpperBoundType = optimizationInfo.getDynamicReturnType();
         dynamicLowerBoundType = optimizationInfo.getDynamicLowerBoundType();
       } else if (current.isStaticGet()) {
         StaticGet staticGet = current.asStaticGet();
@@ -89,23 +89,23 @@
           continue;
         }
 
-        dynamicType = encodedField.getOptimizationInfo().getDynamicType();
-        // TODO(b/140234782): Extend to field values.
-        dynamicLowerBoundType = null;
+        dynamicUpperBoundType = encodedField.getOptimizationInfo().getDynamicUpperBoundType();
+        dynamicLowerBoundType = encodedField.getOptimizationInfo().getDynamicLowerBoundType();
       } else {
         continue;
       }
 
       Value outValue = current.outValue();
       boolean isTrivial =
-          (dynamicType == null || !dynamicType.strictlyLessThan(outValue.getTypeLattice(), appView))
+          (dynamicUpperBoundType == null
+                  || !dynamicUpperBoundType.strictlyLessThan(outValue.getTypeLattice(), appView))
               && dynamicLowerBoundType == null;
       if (isTrivial) {
         continue;
       }
 
-      if (dynamicType == null) {
-        dynamicType = outValue.getTypeLattice();
+      if (dynamicUpperBoundType == null) {
+        dynamicUpperBoundType = outValue.getTypeLattice();
       }
 
       // Split block if needed (only debug instructions are allowed after the throwing
@@ -121,7 +121,12 @@
       // Insert AssumeDynamicType instruction.
       Assume<DynamicTypeAssumption> assumeInstruction =
           Assume.createAssumeDynamicTypeInstruction(
-              dynamicType, dynamicLowerBoundType, specializedOutValue, outValue, current, appView);
+              dynamicUpperBoundType,
+              dynamicLowerBoundType,
+              specializedOutValue,
+              outValue,
+              current,
+              appView);
       assumeInstruction.setPosition(
           appView.options().debug ? current.getPosition() : Position.none());
       if (insertionBlock == block) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
index fb1ef58..f52658f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ForcedInliningOracle.java
@@ -84,6 +84,15 @@
       IRCode inlinee, ListIterator<BasicBlock> blockIterator, BasicBlock block) {}
 
   @Override
+  public boolean allowInliningOfInvokeInInlinee(
+      InlineAction action,
+      int inliningDepth,
+      WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
+    // The purpose of force inlining is generally to inline a given invoke-instruction in the IR.
+    return false;
+  }
+
+  @Override
   public boolean canInlineInstanceInitializer(
       IRCode code, WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
     return true;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index fef9538..40d01c0 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -46,16 +46,20 @@
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackIgnore;
 import com.android.tools.r8.ir.optimize.inliner.NopWhyAreYouNotInliningReporter;
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
+import com.android.tools.r8.ir.optimize.lambda.LambdaMerger;
 import com.android.tools.r8.kotlin.Kotlin;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.MainDexClasses;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.IteratorUtils;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Deque;
 import java.util.HashMap;
 import java.util.List;
 import java.util.ListIterator;
@@ -70,6 +74,7 @@
 
   protected final AppView<AppInfoWithLiveness> appView;
   private final Set<DexMethod> blacklist;
+  private final LambdaMerger lambdaMerger;
   private final LensCodeRewriter lensCodeRewriter;
   final MainDexClasses mainDexClasses;
 
@@ -82,10 +87,12 @@
   public Inliner(
       AppView<AppInfoWithLiveness> appView,
       MainDexClasses mainDexClasses,
+      LambdaMerger lambdaMerger,
       LensCodeRewriter lensCodeRewriter) {
     Kotlin.Intrinsics intrinsics = appView.dexItemFactory().kotlin.intrinsics;
     this.appView = appView;
     this.blacklist = ImmutableSet.of(intrinsics.throwNpe, intrinsics.throwParameterIsNullException);
+    this.lambdaMerger = lambdaMerger;
     this.lensCodeRewriter = lensCodeRewriter;
     this.mainDexClasses = mainDexClasses;
   }
@@ -585,6 +592,7 @@
         ValueNumberGenerator generator,
         AppView<? extends AppInfoWithSubtyping> appView,
         Position callerPosition,
+        LambdaMerger lambdaMerger,
         LensCodeRewriter lensCodeRewriter) {
       DexItemFactory dexItemFactory = appView.dexItemFactory();
       InternalOptions options = appView.options();
@@ -752,6 +760,9 @@
       if (!target.isProcessed()) {
         lensCodeRewriter.rewrite(code, target);
       }
+      if (lambdaMerger != null) {
+        lambdaMerger.rewriteCodeForInlining(target, code, context);
+      }
       assert code.isConsistentSSA();
       return new InlineeWithReason(code, reason);
     }
@@ -870,8 +881,13 @@
     ListIterator<BasicBlock> blockIterator = code.listIterator();
     ClassInitializationAnalysis classInitializationAnalysis =
         new ClassInitializationAnalysis(appView, code);
+    Deque<BasicBlock> inlineeStack = new ArrayDeque<>();
+    InternalOptions options = appView.options();
     while (blockIterator.hasNext()) {
       BasicBlock block = blockIterator.next();
+      if (!inlineeStack.isEmpty() && inlineeStack.peekFirst() == block) {
+        inlineeStack.pop();
+      }
       if (blocksToRemove.contains(block)) {
         continue;
       }
@@ -895,12 +911,19 @@
               oracle.computeInlining(
                   invoke, singleTarget, classInitializationAnalysis, whyAreYouNotInliningReporter);
           if (action == null) {
-            assert whyAreYouNotInliningReporter.verifyReasonHasBeenReported();
+            assert whyAreYouNotInliningReporter.unsetReasonHasBeenReportedFlag();
+            continue;
+          }
+
+          if (!inlineeStack.isEmpty()
+              && !strategy.allowInliningOfInvokeInInlinee(
+                  action, inlineeStack.size(), whyAreYouNotInliningReporter)) {
+            assert whyAreYouNotInliningReporter.unsetReasonHasBeenReportedFlag();
             continue;
           }
 
           if (!strategy.stillHasBudget(action, whyAreYouNotInliningReporter)) {
-            assert whyAreYouNotInliningReporter.verifyReasonHasBeenReported();
+            assert whyAreYouNotInliningReporter.unsetReasonHasBeenReportedFlag();
             continue;
           }
 
@@ -910,10 +933,11 @@
                   code.valueNumberGenerator,
                   appView,
                   getPositionForInlining(invoke, context),
+                  lambdaMerger,
                   lensCodeRewriter);
           if (strategy.willExceedBudget(
               code, invoke, inlinee, block, whyAreYouNotInliningReporter)) {
-            assert whyAreYouNotInliningReporter.verifyReasonHasBeenReported();
+            assert whyAreYouNotInliningReporter.unsetReasonHasBeenReportedFlag();
             continue;
           }
 
@@ -926,7 +950,7 @@
           if (singleTarget.isInstanceInitializer()
               && !strategy.canInlineInstanceInitializer(
                   inlinee.code, whyAreYouNotInliningReporter)) {
-            assert whyAreYouNotInliningReporter.verifyReasonHasBeenReported();
+            assert whyAreYouNotInliningReporter.unsetReasonHasBeenReportedFlag();
             continue;
           }
 
@@ -936,6 +960,8 @@
             assumeDynamicTypeRemover.markUsersForRemoval(outValue);
           }
 
+          boolean inlineeMayHaveInvokeMethod = inlinee.code.metadata().mayHaveInvokeMethod();
+
           // Inline the inlinee code in place of the invoke instruction
           // Back up before the invoke instruction.
           iterator.previous();
@@ -962,11 +988,26 @@
           }
 
           context.copyMetadata(singleTarget);
+
+          if (inlineeMayHaveInvokeMethod && options.applyInliningToInlinee) {
+            if (inlineeStack.size() + 1 > options.applyInliningToInlineeMaxDepth
+                && appView.appInfo().alwaysInline.isEmpty()
+                && appView.appInfo().forceInline.isEmpty()) {
+              continue;
+            }
+            // Record that we will be inside the inlinee until the next block.
+            BasicBlock inlineeEnd = IteratorUtils.peekNext(blockIterator);
+            inlineeStack.push(inlineeEnd);
+            // Move the cursor back to where the first inlinee block was added.
+            IteratorUtils.previousUntil(blockIterator, previous -> previous == block);
+            blockIterator.next();
+          }
         } else if (current.isAssumeDynamicType()) {
           assumeDynamicTypeRemover.removeIfMarked(current.asAssumeDynamicType(), iterator);
         }
       }
     }
+    assert inlineeStack.isEmpty();
     assumeDynamicTypeRemover.removeMarkedInstructions(blocksToRemove);
     assumeDynamicTypeRemover.finish();
     classInitializationAnalysis.finish();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
index 5f9bb2a..e0b0a2c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningStrategy.java
@@ -17,6 +17,11 @@
 
 interface InliningStrategy {
 
+  boolean allowInliningOfInvokeInInlinee(
+      InlineAction action,
+      int inliningDepth,
+      WhyAreYouNotInliningReporter whyAreYouNotInliningReporter);
+
   boolean canInlineInstanceInitializer(
       IRCode code, WhyAreYouNotInliningReporter whyAreYouNotInliningReporter);
 
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 62d5bd8..cea6258 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
@@ -3,6 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.optimize;
 
+import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
+import static com.android.tools.r8.ir.analysis.type.TypeLatticeElement.stringClassType;
+
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DebugLocalInfo;
 import com.android.tools.r8.graph.DexDefinition;
@@ -193,7 +196,8 @@
             appView.dexItemFactory().stringType,
             typeLattice.asClassTypeLatticeElement().getClassType())
         .isTrue();
-    Value returnedValue = code.createValue(typeLattice, debugLocalInfo);
+    Value returnedValue =
+        code.createValue(stringClassType(appView, definitelyNotNull()), debugLocalInfo);
     ConstString instruction =
         new ConstString(
             returnedValue, constant, ThrowingInfo.defaultForConstString(appView.options()));
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 cea57a8..9752859 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
@@ -134,8 +134,8 @@
             DexEncodedField encodedField = appView.appInfo().resolveField(field);
             if (encodedField != null) {
               FieldOptimizationInfo optimizationInfo = encodedField.getOptimizationInfo();
-              if (optimizationInfo.getDynamicType() != null
-                  && optimizationInfo.getDynamicType().isDefinitelyNotNull()) {
+              if (optimizationInfo.getDynamicUpperBoundType() != null
+                  && optimizationInfo.getDynamicUpperBoundType().isDefinitelyNotNull()) {
                 knownToBeNonNullValues.add(outValue);
               }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java b/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java
index fdfa3ee..5088d99 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java
@@ -373,7 +373,7 @@
   public void rewrite(IRCode code) {
     Set<BasicBlock> blocksToBeRemoved = Sets.newIdentityHashSet();
     ListIterator<BasicBlock> blockIterator = code.listIterator();
-    Set<Value> valuesToNarrow = new HashSet<>();
+    Set<Value> valuesToNarrow = Sets.newIdentityHashSet();
     while (blockIterator.hasNext()) {
       BasicBlock block = blockIterator.next();
       if (blocksToBeRemoved.contains(block)) {
@@ -422,12 +422,12 @@
         }
       }
     }
+    code.removeBlocks(blocksToBeRemoved);
+    code.removeAllTrivialPhis(valuesToNarrow);
+    code.removeUnreachableBlocks();
     if (!valuesToNarrow.isEmpty()) {
       new TypeAnalysis(appView).narrowing(valuesToNarrow);
     }
-    code.removeBlocks(blocksToBeRemoved);
-    code.removeAllTrivialPhis();
-    code.removeUnreachableBlocks();
     assert code.isConsistentSSA();
   }
 
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 95f29b9..a938ff2 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
@@ -226,6 +226,13 @@
     List<DexEncodedMethod> directMethods = clazz.directMethods();
     for (int i = 0; i < directMethods.size(); i++) {
       DexEncodedMethod method = directMethods.get(i);
+
+      // If this is a private or static method that is targeted by an invoke-super instruction, then
+      // don't remove any unused arguments.
+      if (appView.appInfo().brokenSuperInvokes.contains(method.method)) {
+        continue;
+      }
+
       RemovedArgumentsInfo unused = collectUnusedArguments(method);
       if (unused != null && unused.hasRemovedArguments()) {
         DexProto newProto = createProtoWithRemovedArguments(method, unused);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
index f550309..d576a22 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
@@ -246,7 +246,7 @@
       // particularly important for bridge methods.
       codeRewriter.removeTrivialCheckCastAndInstanceOfInstructions(code);
       // If a method was inlined we may be able to prune additional branches.
-      codeRewriter.simplifyIf(code);
+      codeRewriter.simplifyControlFlow(code);
       // If a method was inlined we may see more trivial computation/conversion of String.
       boolean isDebugMode =
           appView.options().debug || method.getOptimizationInfo().isReachabilitySensitive();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultFieldOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultFieldOptimizationInfo.java
index 30721f5..e82afb7 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultFieldOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultFieldOptimizationInfo.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.ir.optimize.info;
 
+import com.android.tools.r8.ir.analysis.type.ClassTypeLatticeElement;
 import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 
 public class DefaultFieldOptimizationInfo extends FieldOptimizationInfo {
@@ -32,7 +33,12 @@
   }
 
   @Override
-  public TypeLatticeElement getDynamicType() {
+  public ClassTypeLatticeElement getDynamicLowerBoundType() {
+    return null;
+  }
+
+  @Override
+  public TypeLatticeElement getDynamicUpperBoundType() {
     return null;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/FieldOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/FieldOptimizationInfo.java
index 58432ba..cc89f84 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/FieldOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/FieldOptimizationInfo.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.ir.optimize.info;
 
+import com.android.tools.r8.ir.analysis.type.ClassTypeLatticeElement;
 import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 
 public abstract class FieldOptimizationInfo {
@@ -18,7 +19,9 @@
    */
   public abstract int getReadBits();
 
-  public abstract TypeLatticeElement getDynamicType();
+  public abstract ClassTypeLatticeElement getDynamicLowerBoundType();
+
+  public abstract TypeLatticeElement getDynamicUpperBoundType();
 
   public abstract boolean valueHasBeenPropagated();
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
index 78369e3..8cbc644 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
@@ -46,6 +46,7 @@
 import com.android.tools.r8.kotlin.Kotlin;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.MethodSignatureEquivalence;
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.Sets;
@@ -60,17 +61,16 @@
 import java.util.function.Function;
 
 public class MethodOptimizationInfoCollector {
-  private final AppView<?> appView;
+  private final AppView<AppInfoWithLiveness> appView;
   private final InternalOptions options;
   private final DexItemFactory dexItemFactory;
 
-  public MethodOptimizationInfoCollector(AppView<?> appView) {
+  public MethodOptimizationInfoCollector(AppView<AppInfoWithLiveness> appView) {
     this.appView = appView;
     this.options = appView.options();
     this.dexItemFactory = appView.dexItemFactory();
   }
 
-  // TODO(b/141656615): Use this and then make all utils in this collector `private`.
   public void collectMethodOptimizationInfo(
       DexEncodedMethod method,
       IRCode code,
@@ -91,7 +91,7 @@
     computeNonNullParamOnNormalExits(feedback, code);
   }
 
-  public void identifyClassInlinerEligibility(
+  private void identifyClassInlinerEligibility(
       DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
     // Method eligibility is calculated in similar way for regular method
     // and for the constructor. To be eligible method should only be using its
@@ -126,7 +126,11 @@
 
     boolean receiverUsedAsReturnValue = false;
     boolean seenSuperInitCall = false;
-    for (Instruction insn : receiver.uniqueUsers()) {
+    for (Instruction insn : receiver.aliasedUsers()) {
+      if (insn.isAssume()) {
+        continue;
+      }
+
       if (insn.isMonitor()) {
         continue;
       }
@@ -140,11 +144,11 @@
         if (insn.isInstancePut()) {
           InstancePut instancePutInstruction = insn.asInstancePut();
           // Only allow field writes to the receiver.
-          if (instancePutInstruction.object() != receiver) {
+          if (instancePutInstruction.object().getAliasedValue() != receiver) {
             return;
           }
           // Do not allow the receiver to escape via a field write.
-          if (instancePutInstruction.value() == receiver) {
+          if (instancePutInstruction.value().getAliasedValue() == receiver) {
             return;
           }
         }
@@ -162,7 +166,8 @@
         DexMethod invokedMethod = invokedDirect.getInvokedMethod();
         if (dexItemFactory.isConstructor(invokedMethod)
             && invokedMethod.holder == clazz.superType
-            && invokedDirect.inValues().lastIndexOf(receiver) == 0
+            && ListUtils.lastIndexMatching(
+                invokedDirect.inValues(), v -> v.getAliasedValue() == receiver) == 0
             && !seenSuperInitCall
             && instanceInitializer) {
           seenSuperInitCall = true;
@@ -184,7 +189,7 @@
         method, new ClassInlinerEligibility(receiverUsedAsReturnValue));
   }
 
-  public void identifyParameterUsages(
+  private void identifyParameterUsages(
       DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
     List<ParameterUsage> usages = new ArrayList<>();
     List<Value> values = code.collectArguments();
@@ -203,7 +208,7 @@
 
   private ParameterUsage collectParameterUsages(int i, Value value) {
     ParameterUsageBuilder builder = new ParameterUsageBuilder(value, i);
-    for (Instruction user : value.uniqueUsers()) {
+    for (Instruction user : value.aliasedUsers()) {
       if (!builder.note(user)) {
         return null;
       }
@@ -211,7 +216,7 @@
     return builder.build();
   }
 
-  public void identifyReturnsArgument(
+  private void identifyReturnsArgument(
       DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
     List<BasicBlock> normalExits = code.computeNormalExitBlocks();
     if (normalExits.isEmpty()) {
@@ -233,18 +238,19 @@
       isNeverNull &= value.getTypeLattice().isReference() && value.isNeverNull();
     }
     if (returnValue != null) {
-      if (returnValue.isArgument()) {
+      Value aliasedValue = returnValue.getAliasedValue();
+      if (aliasedValue.isArgument()) {
         // Find the argument number.
-        int index = code.collectArguments().indexOf(returnValue);
-        assert index != -1;
+        int index = aliasedValue.computeArgumentPosition(code);
+        assert index >= 0;
         feedback.methodReturnsArgument(method, index);
       }
-      if (returnValue.isConstant()) {
-        if (returnValue.definition.isConstNumber()) {
-          long value = returnValue.definition.asConstNumber().getRawValue();
+      if (aliasedValue.isConstant()) {
+        if (aliasedValue.definition.isConstNumber()) {
+          long value = aliasedValue.definition.asConstNumber().getRawValue();
           feedback.methodReturnsConstantNumber(method, value);
-        } else if (returnValue.definition.isConstString()) {
-          ConstString constStringInstruction = returnValue.definition.asConstString();
+        } else if (aliasedValue.definition.isConstString()) {
+          ConstString constStringInstruction = aliasedValue.definition.asConstString();
           if (!constStringInstruction.instructionInstanceCanThrow()) {
             feedback.methodReturnsConstantString(method, constStringInstruction.getValue());
           }
@@ -256,9 +262,11 @@
     }
   }
 
-  public void identifyTrivialInitializer(
+  private void identifyTrivialInitializer(
       DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
-    if (!method.isInstanceInitializer() && !method.isClassInitializer()) {
+    assert !appView.appInfo().isPinned(method.method);
+
+    if (!method.isInitializer()) {
       return;
     }
 
@@ -266,8 +274,13 @@
       return;
     }
 
+    if (appView.appInfo().mayHaveSideEffects.containsKey(method.method)) {
+      return;
+    }
+
     DexClass clazz = appView.appInfo().definitionFor(method.method.holder);
     if (clazz == null) {
+      assert false;
       return;
     }
 
@@ -303,6 +316,10 @@
         continue;
       }
 
+      if (insn.isAssume()) {
+        continue;
+      }
+
       if (insn.isNewInstance()) {
         NewInstance newInstance = insn.asNewInstance();
         if (createdSingletonInstance != null
@@ -374,6 +391,10 @@
         continue;
       }
 
+      if (insn.isAssume()) {
+        continue;
+      }
+
       if (insn.isArgument()) {
         continue;
       }
@@ -440,7 +461,7 @@
     return TrivialInstanceInitializer.INSTANCE;
   }
 
-  public void identifyInvokeSemanticsForInlining(
+  private void identifyInvokeSemanticsForInlining(
       DexEncodedMethod method, IRCode code, AppView<?> appView, OptimizationFeedback feedback) {
     if (method.isStatic()) {
       // Identifies if the method preserves class initialization after inlining.
@@ -692,7 +713,7 @@
     return true;
   }
 
-  public void computeDynamicReturnType(
+  private void computeDynamicReturnType(
       DynamicTypeOptimization dynamicTypeOptimization,
       OptimizationFeedback feedback,
       DexEncodedMethod method,
@@ -721,7 +742,7 @@
     }
   }
 
-  public void computeInitializedClassesOnNormalExit(
+  private void computeInitializedClassesOnNormalExit(
       OptimizationFeedback feedback, DexEncodedMethod method, IRCode code) {
     if (options.enableInitializedClassesAnalysis && appView.appInfo().hasLiveness()) {
       AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
@@ -734,7 +755,7 @@
     }
   }
 
-  public void computeMayHaveSideEffects(
+  private void computeMayHaveSideEffects(
       OptimizationFeedback feedback, DexEncodedMethod method, IRCode code) {
     // If the method is native, we don't know what could happen.
     assert !method.accessFlags.isNative();
@@ -802,7 +823,7 @@
     return false;
   }
 
-  public void computeReturnValueOnlyDependsOnArguments(
+  private void computeReturnValueOnlyDependsOnArguments(
       OptimizationFeedback feedback, DexEncodedMethod method, IRCode code) {
     if (!options.enableDeterminismAnalysis) {
       return;
@@ -815,7 +836,7 @@
   }
 
   // Track usage of parameters and compute their nullability and possibility of NPE.
-  public void computeNonNullParamOrThrow(
+  private void computeNonNullParamOrThrow(
       OptimizationFeedback feedback, DexEncodedMethod method, IRCode code) {
     if (method.getOptimizationInfo().getNonNullParamOrThrow() != null) {
       return;
@@ -843,7 +864,7 @@
     }
   }
 
-  public void computeNonNullParamOnNormalExits(OptimizationFeedback feedback, IRCode code) {
+  private void computeNonNullParamOnNormalExits(OptimizationFeedback feedback, IRCode code) {
     Set<BasicBlock> normalExits = Sets.newIdentityHashSet();
     normalExits.addAll(code.computeNormalExitBlocks());
     DominatorTree dominatorTree = new DominatorTree(code, MAY_HAVE_UNREACHABLE_BLOCKS);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java
index 55ce1a4..9d3bb4a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MutableFieldOptimizationInfo.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.AppInfoWithSubtyping;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.ClassTypeLatticeElement;
 import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
 import java.util.function.Function;
 
@@ -22,12 +23,16 @@
   private int readBits = 0;
   private boolean cannotBeKept = false;
   private boolean valueHasBeenPropagated = false;
-  private TypeLatticeElement dynamicType = null;
+  private ClassTypeLatticeElement dynamicLowerBoundType = null;
+  private TypeLatticeElement dynamicUpperBoundType = null;
 
   public void fixupClassTypeReferences(
       Function<DexType, DexType> mapping, AppView<? extends AppInfoWithSubtyping> appView) {
-    if (dynamicType != null) {
-      dynamicType = dynamicType.fixupClassTypeReferences(mapping, appView);
+    if (dynamicLowerBoundType != null) {
+      dynamicLowerBoundType = dynamicLowerBoundType.fixupClassTypeReferences(mapping, appView);
+    }
+    if (dynamicUpperBoundType != null) {
+      dynamicUpperBoundType = dynamicUpperBoundType.fixupClassTypeReferences(mapping, appView);
     }
   }
 
@@ -58,12 +63,21 @@
   }
 
   @Override
-  public TypeLatticeElement getDynamicType() {
-    return dynamicType;
+  public ClassTypeLatticeElement getDynamicLowerBoundType() {
+    return dynamicLowerBoundType;
   }
 
-  void setDynamicType(TypeLatticeElement type) {
-    dynamicType = type;
+  void setDynamicLowerBoundType(ClassTypeLatticeElement type) {
+    dynamicLowerBoundType = type;
+  }
+
+  @Override
+  public TypeLatticeElement getDynamicUpperBoundType() {
+    return dynamicUpperBoundType;
+  }
+
+  void setDynamicUpperBoundType(TypeLatticeElement type) {
+    dynamicUpperBoundType = type;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
index d185a37..a994f5d 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackDelayed.java
@@ -95,8 +95,14 @@
   }
 
   @Override
-  public void markFieldHasDynamicType(DexEncodedField field, TypeLatticeElement type) {
-    getFieldOptimizationInfoForUpdating(field).setDynamicType(type);
+  public void markFieldHasDynamicLowerBoundType(
+      DexEncodedField field, ClassTypeLatticeElement type) {
+    getFieldOptimizationInfoForUpdating(field).setDynamicLowerBoundType(type);
+  }
+
+  @Override
+  public void markFieldHasDynamicUpperBoundType(DexEncodedField field, TypeLatticeElement type) {
+    getFieldOptimizationInfoForUpdating(field).setDynamicUpperBoundType(type);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
index 79b2e7f..9b5c988 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackIgnore.java
@@ -36,7 +36,11 @@
   public void markFieldAsPropagated(DexEncodedField field) {}
 
   @Override
-  public void markFieldHasDynamicType(DexEncodedField field, TypeLatticeElement type) {}
+  public void markFieldHasDynamicLowerBoundType(
+      DexEncodedField field, ClassTypeLatticeElement type) {}
+
+  @Override
+  public void markFieldHasDynamicUpperBoundType(DexEncodedField field, TypeLatticeElement type) {}
 
   @Override
   public void markFieldBitsRead(DexEncodedField field, int bitsRead) {}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
index 9119b4b..474a963 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
@@ -40,7 +40,13 @@
   }
 
   @Override
-  public void markFieldHasDynamicType(DexEncodedField field, TypeLatticeElement type) {
+  public void markFieldHasDynamicLowerBoundType(
+      DexEncodedField field, ClassTypeLatticeElement type) {
+    // Ignored.
+  }
+
+  @Override
+  public void markFieldHasDynamicUpperBoundType(DexEncodedField field, TypeLatticeElement type) {
     // Ignored.
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/ParameterUsagesInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/ParameterUsagesInfo.java
index 0495bba..7e61600 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/ParameterUsagesInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/ParameterUsagesInfo.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.Return;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.Pair;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -110,6 +111,10 @@
 
     // Returns false if the instruction is not supported.
     public boolean note(Instruction instruction) {
+      if (instruction.isAssume()) {
+        // Keep examining other users, but the param usage builder should consider aliased users.
+        return true;
+      }
       if (instruction.isIf()) {
         return note(instruction.asIf());
       }
@@ -141,7 +146,8 @@
 
     private boolean note(If ifInstruction) {
       if (ifInstruction.asIf().isZeroTest()) {
-        assert ifInstruction.inValues().size() == 1 && ifInstruction.inValues().get(0) == arg;
+        assert ifInstruction.inValues().size() == 1
+            && ifInstruction.inValues().get(0).getAliasedValue() == arg;
         ifZeroTestTypes.add(ifInstruction.asIf().getType());
         return true;
       }
@@ -150,7 +156,7 @@
 
     private boolean note(InstanceGet instanceGetInstruction) {
       assert arg != instanceGetInstruction.outValue();
-      if (instanceGetInstruction.object() == arg) {
+      if (instanceGetInstruction.object().getAliasedValue() == arg) {
         hasFieldRead = true;
         return true;
       }
@@ -159,12 +165,12 @@
 
     private boolean note(InstancePut instancePutInstruction) {
       assert arg != instancePutInstruction.outValue();
-      if (instancePutInstruction.object() == arg) {
+      if (instancePutInstruction.object().getAliasedValue() == arg) {
         hasFieldAssignment = true;
-        isAssignedToField |= instancePutInstruction.value() == arg;
+        isAssignedToField |= instancePutInstruction.value().getAliasedValue() == arg;
         return true;
       }
-      if (instancePutInstruction.value() == arg) {
+      if (instancePutInstruction.value().getAliasedValue() == arg) {
         isAssignedToField = true;
         return true;
       }
@@ -172,7 +178,8 @@
     }
 
     private boolean note(InvokeMethodWithReceiver invokeInstruction) {
-      if (invokeInstruction.inValues().lastIndexOf(arg) == 0) {
+      if (ListUtils.lastIndexMatching(
+          invokeInstruction.inValues(), v -> v.getAliasedValue() == arg) == 0) {
         callsOnReceiver.add(
             new Pair<>(
                 invokeInstruction.asInvokeMethodWithReceiver().getType(),
@@ -183,7 +190,8 @@
     }
 
     private boolean note(Return returnInstruction) {
-      assert returnInstruction.inValues().size() == 1 && returnInstruction.inValues().get(0) == arg;
+      assert returnInstruction.inValues().size() == 1
+          && returnInstruction.inValues().get(0).getAliasedValue() == arg;
       isReturned = true;
       return true;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/UpdatableMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/UpdatableMethodOptimizationInfo.java
index 06e7332..09a8d13 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/UpdatableMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/UpdatableMethodOptimizationInfo.java
@@ -371,7 +371,12 @@
     // We may get more precise type information if the method is reprocessed (e.g., due to
     // optimization info collected from all call sites), and hence the `returnsObjectOfType` is
     // allowed to become more precise.
-    assert returnsObjectOfType == UNKNOWN_TYPE || type.lessThanOrEqual(returnsObjectOfType, appView)
+    // TODO(b/142559221): non-materializable assume instructions?
+    // Nullability could be less precise, though. For example, suppose a value is known to be
+    // non-null after a safe invocation, hence recorded with the non-null variant. If that call is
+    // inlined and the method is reprocessed, such non-null assumption cannot be made again.
+    assert returnsObjectOfType == UNKNOWN_TYPE
+            || type.lessThanOrEqualUpToNullability(returnsObjectOfType, appView)
         : "return type changed from " + returnsObjectOfType + " to " + type;
     returnsObjectOfType = type;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/NopWhyAreYouNotInliningReporter.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/NopWhyAreYouNotInliningReporter.java
index 467a5b6..8cbb1f4 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/NopWhyAreYouNotInliningReporter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/NopWhyAreYouNotInliningReporter.java
@@ -119,11 +119,14 @@
   public void reportWillExceedInstructionBudget(int numberOfInstructions, int threshold) {}
 
   @Override
+  public void reportWillExceedMaxInliningDepth(int actualInliningDepth, int threshold) {}
+
+  @Override
   public void reportWillExceedMonitorEnterValuesBudget(
       int numberOfMonitorEnterValuesAfterInlining, int threshold) {}
 
   @Override
-  public boolean verifyReasonHasBeenReported() {
+  public boolean unsetReasonHasBeenReportedFlag() {
     return true;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java
index ab80544..1ff91c0 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java
@@ -113,8 +113,10 @@
 
   public abstract void reportWillExceedInstructionBudget(int numberOfInstructions, int threshold);
 
+  public abstract void reportWillExceedMaxInliningDepth(int actualInliningDepth, int threshold);
+
   public abstract void reportWillExceedMonitorEnterValuesBudget(
       int numberOfMonitorEnterValuesAfterInlining, int threshold);
 
-  public abstract boolean verifyReasonHasBeenReported();
+  public abstract boolean unsetReasonHasBeenReportedFlag();
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporterImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporterImpl.java
index 7d25661..64026ca 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporterImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporterImpl.java
@@ -247,6 +247,15 @@
   }
 
   @Override
+  public void reportWillExceedMaxInliningDepth(int actualInliningDepth, int threshold) {
+    printWithExceededThreshold(
+        "would exceed the maximum inlining depth",
+        "current inlining depth",
+        actualInliningDepth,
+        threshold);
+  }
+
+  @Override
   public void reportWillExceedMonitorEnterValuesBudget(
       int numberOfMonitorEnterValuesAfterInlining, int threshold) {
     printWithExceededThreshold(
@@ -257,8 +266,9 @@
   }
 
   @Override
-  public boolean verifyReasonHasBeenReported() {
+  public boolean unsetReasonHasBeenReportedFlag() {
     assert reasonHasBeenReported;
+    reasonHasBeenReported = false;
     return true;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/CodeProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/CodeProcessor.java
index 5c6cb8f..f10c3fe 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/lambda/CodeProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/CodeProcessor.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.CheckCast;
@@ -147,12 +148,25 @@
   public final ListIterator<BasicBlock> blocks;
   private InstructionListIterator instructions;
 
+  // The inlining context (caller), if any.
+  private final DexEncodedMethod context;
+
   CodeProcessor(
       AppView<AppInfoWithLiveness> appView,
       Function<DexType, Strategy> strategyProvider,
       LambdaTypeVisitor lambdaChecker,
       DexEncodedMethod method,
       IRCode code) {
+    this(appView, strategyProvider, lambdaChecker, method, code, null);
+  }
+
+  CodeProcessor(
+      AppView<AppInfoWithLiveness> appView,
+      Function<DexType, Strategy> strategyProvider,
+      LambdaTypeVisitor lambdaChecker,
+      DexEncodedMethod method,
+      IRCode code,
+      DexEncodedMethod context) {
     this.appView = appView;
     this.strategyProvider = strategyProvider;
     this.factory = appView.dexItemFactory();
@@ -161,6 +175,7 @@
     this.method = method;
     this.code = code;
     this.blocks = code.listIterator();
+    this.context = context;
   }
 
   public final InstructionListIterator instructions() {
@@ -178,6 +193,19 @@
     }
   }
 
+  private boolean shouldRewrite(DexField field) {
+    return shouldRewrite(field.holder);
+  }
+
+  private boolean shouldRewrite(DexMethod method) {
+    return shouldRewrite(method.holder);
+  }
+
+  private boolean shouldRewrite(DexType type) {
+    // Rewrite references to lambda classes if we are outside the class.
+    return type != (context != null ? context : method).method.holder;
+  }
+
   @Override
   public Void handleInvoke(Invoke invoke) {
     if (invoke.isInvokeNewArray()) {
@@ -199,7 +227,7 @@
       // Invalidate signature, there still should not be lambda references.
       lambdaChecker.accept(invokeMethod.getInvokedMethod().proto);
       // Only rewrite references to lambda classes if we are outside the class.
-      if (invokeMethod.getInvokedMethod().holder != this.method.method.holder) {
+      if (shouldRewrite(invokeMethod.getInvokedMethod())) {
         process(strategy, invokeMethod);
       }
       return null;
@@ -218,7 +246,7 @@
     Strategy strategy = strategyProvider.apply(newInstance.clazz);
     if (strategy.isValidNewInstance(this, newInstance)) {
       // Only rewrite references to lambda classes if we are outside the class.
-      if (newInstance.clazz != this.method.method.holder) {
+      if (shouldRewrite(newInstance.clazz)) {
         process(strategy, newInstance);
       }
     }
@@ -260,7 +288,7 @@
     DexField field = instanceGet.getField();
     Strategy strategy = strategyProvider.apply(field.holder);
     if (strategy.isValidInstanceFieldRead(this, field)) {
-      if (field.holder != this.method.method.holder) {
+      if (shouldRewrite(field)) {
         // Only rewrite references to lambda classes if we are outside the class.
         process(strategy, instanceGet);
       }
@@ -279,7 +307,7 @@
     DexField field = instancePut.getField();
     Strategy strategy = strategyProvider.apply(field.holder);
     if (strategy.isValidInstanceFieldWrite(this, field)) {
-      if (field.holder != this.method.method.holder) {
+      if (shouldRewrite(field)) {
         // Only rewrite references to lambda classes if we are outside the class.
         process(strategy, instancePut);
       }
@@ -298,7 +326,7 @@
     DexField field = staticGet.getField();
     Strategy strategy = strategyProvider.apply(field.holder);
     if (strategy.isValidStaticFieldRead(this, field)) {
-      if (field.holder != this.method.method.holder) {
+      if (shouldRewrite(field)) {
         // Only rewrite references to lambda classes if we are outside the class.
         process(strategy, staticGet);
       }
@@ -314,7 +342,7 @@
     DexField field = staticPut.getField();
     Strategy strategy = strategyProvider.apply(field.holder);
     if (strategy.isValidStaticFieldWrite(this, field)) {
-      if (field.holder != this.method.method.holder) {
+      if (shouldRewrite(field)) {
         // Only rewrite references to lambda classes if we are outside the class.
         process(strategy, staticPut);
       }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java
index a90704a..b226521 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java
@@ -56,7 +56,6 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Future;
-import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 
@@ -84,6 +83,43 @@
 //   5. synthesize group lambda classes.
 //
 public final class LambdaMerger {
+
+  private abstract static class Mode {
+
+    void rewriteCode(DexEncodedMethod method, IRCode code, DexEncodedMethod context) {}
+
+    void analyzeCode(DexEncodedMethod method, IRCode code) {}
+  }
+
+  private class AnalyzeMode extends Mode {
+
+    @Override
+    void analyzeCode(DexEncodedMethod method, IRCode code) {
+      new AnalysisStrategy(method, code).processCode();
+    }
+  }
+
+  private class ApplyMode extends Mode {
+
+    private final Set<DexType> lambdaGroupsClasses;
+    private final LambdaMergerOptimizationInfoFixer optimizationInfoFixer;
+
+    ApplyMode(
+        Set<DexType> lambdaGroupTypes, LambdaMergerOptimizationInfoFixer optimizationInfoFixer) {
+      this.lambdaGroupsClasses = lambdaGroupTypes;
+      this.optimizationInfoFixer = optimizationInfoFixer;
+    }
+
+    @Override
+    void rewriteCode(DexEncodedMethod method, IRCode code, DexEncodedMethod context) {
+      if (lambdaGroupsClasses.contains(method.method.holder)) {
+        // Don't rewrite the methods that we have synthesized for the lambda group classes.
+        return;
+      }
+      new ApplyStrategy(method, code, context, optimizationInfoFixer).processCode();
+    }
+  }
+
   // Maps lambda into a group, only contains lambdas we decided to merge.
   // NOTE: needs synchronization.
   private final Map<DexType, LambdaGroup> lambdas = new IdentityHashMap<>();
@@ -113,7 +149,7 @@
   private final Kotlin kotlin;
   private final DiagnosticsHandler reporter;
 
-  private BiFunction<DexEncodedMethod, IRCode, CodeProcessor> strategyFactory = null;
+  private Mode mode;
 
   // Lambda visitor invalidating lambdas it sees.
   private final LambdaTypeVisitor lambdaInvalidator;
@@ -153,8 +189,7 @@
   // Collect all group candidates and assign unique lambda ids inside each group.
   // We do this before methods are being processed to guarantee stable order of
   // lambdas inside each group.
-  public final void collectGroupCandidates(
-      DexApplication app, AppView<AppInfoWithLiveness> appView) {
+  public final void collectGroupCandidates(DexApplication app) {
     // Collect lambda groups.
     app.classes().stream()
         .filter(cls -> !appView.appInfo().isPinned(cls.type))
@@ -188,20 +223,50 @@
     // Remove trivial groups.
     removeTrivialLambdaGroups();
 
-    assert strategyFactory == null;
-    strategyFactory = AnalysisStrategy::new;
+    assert mode == null;
+    mode = new AnalyzeMode();
   }
 
-  // Is called by IRConverter::rewriteCode, performs different actions
-  // depending on phase:
-  //   - in ANALYZE phase just analyzes invalid usages of lambda classes
-  //     inside the method code, invalidated such lambda classes,
-  //     collects methods that need to be patched.
-  //   - in APPLY phase patches the code to use lambda group classes, also
-  //     asserts that there are no more invalid lambda class references.
-  public final void processMethodCode(DexEncodedMethod method, IRCode code) {
-    if (strategyFactory != null) {
-      strategyFactory.apply(method, code).processCode();
+  /**
+   * Is called by IRConverter::rewriteCode. Performs different actions depending on the current
+   * mode.
+   *
+   * <ol>
+   *   <li>in ANALYZE mode analyzes invalid usages of lambda classes inside the method code,
+   *       invalidated such lambda classes, collects methods that need to be patched.
+   *   <li>in APPLY mode does nothing.
+   * </ol>
+   */
+  public final void analyzeCode(DexEncodedMethod method, IRCode code) {
+    if (mode != null) {
+      mode.analyzeCode(method, code);
+    }
+  }
+
+  /**
+   * Is called by IRConverter::rewriteCode. Performs different actions depending on the current
+   * mode.
+   *
+   * <ol>
+   *   <li>in ANALYZE mode does nothing.
+   *   <li>in APPLY mode patches the code to use lambda group classes, also asserts that there are
+   *       no more invalid lambda class references.
+   * </ol>
+   */
+  public final void rewriteCode(DexEncodedMethod method, IRCode code) {
+    if (mode != null) {
+      mode.rewriteCode(method, code, null);
+    }
+  }
+
+  /**
+   * Similar to {@link #rewriteCode(DexEncodedMethod, IRCode)}, but for rewriting code for inlining.
+   * The {@param context} is the caller that {@param method} is being inlined into.
+   */
+  public final void rewriteCodeForInlining(
+      DexEncodedMethod method, IRCode code, DexEncodedMethod context) {
+    if (mode != null) {
+      mode.rewriteCode(method, code, context);
     }
   }
 
@@ -239,7 +304,9 @@
     feedback.fixupOptimizationInfos(appView, executorService, optimizationInfoFixer);
 
     // Switch to APPLY strategy.
-    this.strategyFactory = (method, code) -> new ApplyStrategy(method, code, optimizationInfoFixer);
+    Set<DexType> lambdaGroupTypes =
+        lambdaGroupsClasses.values().stream().map(clazz -> clazz.type).collect(Collectors.toSet());
+    this.mode = new ApplyMode(lambdaGroupTypes, optimizationInfoFixer);
 
     // Add synthesized lambda group classes to the builder.
     for (Entry<LambdaGroup, DexProgramClass> entry : lambdaGroupsClasses.entrySet()) {
@@ -274,7 +341,7 @@
     // Rewrite lambda class references into lambda group class
     // references inside methods from the processing queue.
     rewriteLambdaReferences(converter, executorService, feedback);
-    this.strategyFactory = null;
+    this.mode = null;
   }
 
   private void analyzeReferencesInProgramClasses(
@@ -459,13 +526,15 @@
     private ApplyStrategy(
         DexEncodedMethod method,
         IRCode code,
+        DexEncodedMethod context,
         LambdaMergerOptimizationInfoFixer optimizationInfoFixer) {
       super(
           LambdaMerger.this.appView,
           LambdaMerger.this::strategyProvider,
           lambdaChecker,
           method,
-          code);
+          code,
+          context);
       this.optimizationInfoFixer = optimizationInfoFixer;
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
index 8456912..26fbcee 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
@@ -38,11 +38,13 @@
 import com.android.tools.r8.ir.regalloc.RegisterPositions.Type;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.HashMultiset;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Multiset;
 import com.google.common.collect.Multisets;
+import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap.Entry;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap;
@@ -280,7 +282,8 @@
     boolean isEntryBlock = true;
     for (BasicBlock block : blocks) {
       InstructionListIterator instructionIterator = block.listIterator(code);
-      Set<Value> liveLocalValues = new HashSet<>(liveAtEntrySets.get(block).liveLocalValues);
+      Set<Value> liveLocalValues =
+          SetUtils.newIdentityHashSet(liveAtEntrySets.get(block).liveLocalValues);
       // Skip past arguments and open argument and phi locals.
       if (isEntryBlock) {
         isEntryBlock = false;
@@ -2516,9 +2519,9 @@
       Map<BasicBlock, LiveAtEntrySets> liveAtEntrySets,
       List<LiveIntervals> liveIntervals) {
     for (BasicBlock block : code.topologicallySortedBlocks()) {
-      Set<Value> live = new HashSet<>();
-      Set<Value> phiOperands = new HashSet<>();
-      Set<Value> liveAtThrowingInstruction = new HashSet<>();
+      Set<Value> live = Sets.newIdentityHashSet();
+      Set<Value> phiOperands = Sets.newIdentityHashSet();
+      Set<Value> liveAtThrowingInstruction = Sets.newIdentityHashSet();
       Set<BasicBlock> exceptionalSuccessors = block.getCatchHandlers().getUniqueTargets();
       for (BasicBlock successor : block.getSuccessors()) {
         // Values live at entry to a block that is an exceptional successor are only live
diff --git a/src/main/java/com/android/tools/r8/shaking/AnnotationFixer.java b/src/main/java/com/android/tools/r8/shaking/AnnotationFixer.java
new file mode 100644
index 0000000..356f609
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/AnnotationFixer.java
@@ -0,0 +1,83 @@
+// 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;
+
+import com.android.tools.r8.graph.DexAnnotation;
+import com.android.tools.r8.graph.DexAnnotationElement;
+import com.android.tools.r8.graph.DexEncodedAnnotation;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexValue;
+import com.android.tools.r8.graph.DexValue.DexValueArray;
+import com.android.tools.r8.graph.DexValue.DexValueType;
+import com.android.tools.r8.graph.GraphLense;
+import com.android.tools.r8.utils.ArrayUtils;
+
+public class AnnotationFixer {
+
+  private final GraphLense lense;
+
+  public AnnotationFixer(GraphLense lense) {
+    this.lense = lense;
+  }
+
+  public void run(Iterable<DexProgramClass> classes) {
+    for (DexProgramClass clazz : classes) {
+      clazz.annotations = clazz.annotations.rewrite(this::rewriteAnnotation);
+      clazz.forEachMethod(this::processMethod);
+      clazz.forEachField(this::processField);
+    }
+  }
+
+  private void processMethod(DexEncodedMethod method) {
+    method.annotations = method.annotations.rewrite(this::rewriteAnnotation);
+    method.parameterAnnotationsList =
+        method.parameterAnnotationsList.rewrite(
+            dexAnnotationSet -> dexAnnotationSet.rewrite(this::rewriteAnnotation));
+  }
+
+  private void processField(DexEncodedField field) {
+    field.annotations = field.annotations.rewrite(this::rewriteAnnotation);
+  }
+
+  private DexAnnotation rewriteAnnotation(DexAnnotation original) {
+    return original.rewrite(this::rewriteEncodedAnnotation);
+  }
+
+  private DexEncodedAnnotation rewriteEncodedAnnotation(DexEncodedAnnotation original) {
+    DexEncodedAnnotation rewritten =
+        original.rewrite(lense::lookupType, this::rewriteAnnotationElement);
+    assert rewritten != null;
+    return rewritten;
+  }
+
+  private DexAnnotationElement rewriteAnnotationElement(DexAnnotationElement original) {
+    DexValue rewrittenValue = rewriteValue(original.value);
+    if (rewrittenValue != original.value) {
+      return new DexAnnotationElement(original.name, rewrittenValue);
+    }
+    return original;
+  }
+
+  private DexValue rewriteValue(DexValue value) {
+    if (value.isDexValueType()) {
+      DexType originalType = value.asDexValueType().value;
+      DexType rewrittenType = lense.lookupType(originalType);
+      if (rewrittenType != originalType) {
+        return new DexValueType(rewrittenType);
+      }
+    } else if (value.isDexValueArray()) {
+      DexValue[] originalValues = value.asDexValueArray().getValues();
+      DexValue[] rewrittenValues =
+          ArrayUtils.map(DexValue[].class, originalValues, this::rewriteValue);
+      if (rewrittenValues != originalValues) {
+        return new DexValueArray(rewrittenValues);
+      }
+    }
+    return value;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
index 693058b..701904c 100644
--- a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
+++ b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
@@ -108,11 +108,6 @@
 
   private boolean isAnnotationTypeLive(DexAnnotation annotation) {
     DexType annotationType = annotation.annotation.type.toBaseType(appView.dexItemFactory());
-    DexClass definition = appView.definitionFor(annotationType);
-    // TODO(b/73102187): How to handle annotations without definition.
-    if (appView.options().isShrinking() && definition == null) {
-      return false;
-    }
     return appView.appInfo().isNonProgramTypeOrLiveProgramType(annotationType);
   }
 
@@ -276,7 +271,8 @@
   private DexAnnotationElement rewriteAnnotationElement(
       DexType annotationType, DexAnnotationElement original) {
     DexClass definition = appView.definitionFor(annotationType);
-    // TODO(b/73102187): How to handle annotations without definition.
+    // We cannot strip annotations where we cannot look up the definition, because this will break
+    // apps that rely on the annotation to exist. See b/134766810 for more information.
     if (definition == null) {
       return original;
     }
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 0689533..5eebf8b 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -182,6 +182,10 @@
   /** Set of live types defined in the library and classpath. Used to avoid duplicate tracing. */
   private final Set<DexClass> liveNonProgramTypes = Sets.newIdentityHashSet();
 
+  /** Mapping from each unused interface to the set of live types that implements the interface. */
+  private final Map<DexProgramClass, Set<DexProgramClass>> unusedInterfaceTypes =
+      new IdentityHashMap<>();
+
   /**
    * Set of proto extension types that are technically live, but which we have not traced because
    * they are dead according to the generated extension registry shrinker.
@@ -226,7 +230,7 @@
    * Set of methods that belong to live classes and can be reached by invokes. These need to be
    * kept.
    */
-  private final SetWithReason<DexEncodedMethod> liveMethods;
+  private final LiveMethodsSet liveMethods;
 
   /**
    * Set of fields that belong to live classes and can be reached by invokes. These need to be kept.
@@ -309,7 +313,7 @@
     liveAnnotations = new SetWithReason<>(graphReporter::registerAnnotation);
     instantiatedTypes = new SetWithReason<>(graphReporter::registerClass);
     targetedMethods = new SetWithReason<>(graphReporter::registerMethod);
-    liveMethods = new SetWithReason<>(graphReporter::registerMethod);
+    liveMethods = new LiveMethodsSet(graphReporter::registerMethod);
     liveFields = new SetWithReason<>(graphReporter::registerField);
     instantiatedInterfaceTypes = new SetWithReason<>(graphReporter::registerInterface);
   }
@@ -483,7 +487,7 @@
   private boolean enqueueMarkMethodLiveAction(
       DexProgramClass clazz, DexEncodedMethod method, KeepReason reason) {
     assert method.method.holder == clazz.type;
-    if (liveMethods.add(method, reason)) {
+    if (liveMethods.add(clazz, method, reason)) {
       workList.enqueueMarkMethodLiveAction(clazz, method, reason);
       return true;
     }
@@ -1128,7 +1132,7 @@
     KeepReason reason = KeepReason.reachableFromLiveType(holder.type);
 
     for (DexType iface : holder.interfaces.values) {
-      markInterfaceTypeAsLiveViaInheritanceClause(iface, reason);
+      markInterfaceTypeAsLiveViaInheritanceClause(iface, holder);
     }
 
     if (holder.superType != null) {
@@ -1139,6 +1143,10 @@
       markTypeAsLive(holder.superType, reason);
     }
 
+    // If this is an interface that has just become live, then report previously seen but unreported
+    // implemented-by edges.
+    transitionUnusedInterfaceToLive(holder);
+
     // We cannot remove virtual methods defined earlier in the type hierarchy if it is widening
     // access and is defined in an interface:
     //
@@ -1204,32 +1212,29 @@
     }
   }
 
-  private void markInterfaceTypeAsLiveViaInheritanceClause(DexType type, KeepReason reason) {
-    if (appView.options().enableUnusedInterfaceRemoval && !mode.isTracingMainDex()) {
-      DexProgramClass clazz = getProgramClassOrNull(type);
-      if (clazz == null) {
-        return;
-      }
+  private void markInterfaceTypeAsLiveViaInheritanceClause(
+      DexType type, DexProgramClass implementer) {
+    DexProgramClass clazz = getProgramClassOrNull(type);
+    if (clazz == null) {
+      return;
+    }
 
-      assert clazz.isInterface();
-
-      if (!clazz.interfaces.isEmpty()) {
-        markTypeAsLive(type, reason);
-        return;
-      }
-
-      for (DexEncodedMethod method : clazz.virtualMethods()) {
-        if (!method.accessFlags.isAbstract()) {
-          markTypeAsLive(type, reason);
-          return;
-        }
-      }
-
-      // No need to mark the type as live. If an interface type is only reachable via the
-      // inheritance clause of another type, and the interface only has abstract methods, it can
-      // simply be removed from the inheritance clause.
+    if (!appView.options().enableUnusedInterfaceRemoval || mode.isTracingMainDex()) {
+      markTypeAsLive(clazz, graphReporter.reportClassReferencedFrom(clazz, implementer));
     } else {
-      markTypeAsLive(type, reason);
+      if (liveTypes.contains(clazz)) {
+        // The interface is already live, so make sure to report this implements-edge.
+        graphReporter.reportClassReferencedFrom(clazz, implementer);
+      } else {
+        // No need to mark the type as live. If an interface type is only reachable via the
+        // inheritance clause of another type it can simply be removed from the inheritance clause.
+        // The interface is needed if it has a live default interface method or field, though.
+        // Therefore, we record that this implemented-by edge has not been reported, such that we
+        // can report it in the future if one its members becomes live.
+        unusedInterfaceTypes
+            .computeIfAbsent(clazz, ignore -> Sets.newIdentityHashSet())
+            .add(implementer);
+      }
     }
   }
 
@@ -1394,9 +1399,7 @@
       // Already targeted.
       return;
     }
-    markTypeAsLive(method.method.holder,
-        holder -> graphReporter.reportClassReferencedFrom(holder, method));
-    markParameterAndReturnTypesAsLive(method);
+    markReferencedTypesAsLive(method);
     processAnnotations(method, method.annotations.annotations);
     method.parameterAnnotationsList.forEachAnnotation(
         annotation -> processAnnotation(method, annotation));
@@ -1652,6 +1655,19 @@
         && !instantiatedTypes.contains(current.asProgramClass()));
   }
 
+  private void transitionUnusedInterfaceToLive(DexProgramClass clazz) {
+    if (clazz.isInterface()) {
+      Set<DexProgramClass> implementedBy = unusedInterfaceTypes.remove(clazz);
+      if (implementedBy != null) {
+        for (DexProgramClass implementer : implementedBy) {
+          markTypeAsLive(clazz, graphReporter.reportClassReferencedFrom(clazz, implementer));
+        }
+      }
+    } else {
+      assert !unusedInterfaceTypes.containsKey(clazz);
+    }
+  }
+
   private void markFieldAsTargeted(DexField field, DexEncodedMethod context) {
     markTypeAsLive(field.type, clazz -> graphReporter.reportClassReferencedFrom(clazz, context));
     markTypeAsLive(field.holder, clazz -> graphReporter.reportClassReferencedFrom(clazz, context));
@@ -2083,7 +2099,7 @@
     }
 
     if (resolutionTarget.accessFlags.isPrivate() || resolutionTarget.accessFlags.isStatic()) {
-      brokenSuperInvokes.add(method);
+      brokenSuperInvokes.add(resolutionTarget.method);
     }
     DexProgramClass resolutionTargetClass = getProgramClassOrNull(resolutionTarget.method.holder);
     if (resolutionTargetClass != null) {
@@ -2103,7 +2119,7 @@
       return;
     }
     if (target.accessFlags.isPrivate()) {
-      brokenSuperInvokes.add(method);
+      brokenSuperInvokes.add(resolutionTarget.method);
     }
     if (Log.ENABLED) {
       Log.verbose(getClass(), "Adding super constraint from `%s` to `%s`", from.method,
@@ -2474,11 +2490,9 @@
       }
     }
     markParameterAndReturnTypesAsLive(method);
-    if (appView.definitionFor(method.method.holder).isProgramClass()) {
-      processAnnotations(method, method.annotations.annotations);
-      method.parameterAnnotationsList.forEachAnnotation(
-          annotation -> processAnnotation(method, annotation));
-    }
+    processAnnotations(method, method.annotations.annotations);
+    method.parameterAnnotationsList.forEachAnnotation(
+        annotation -> processAnnotation(method, annotation));
     method.registerCodeReferences(new UseRegistry(options.itemFactory, clazz, method));
 
     // Add all dependent members to the workqueue.
@@ -2488,6 +2502,12 @@
     analyses.forEach(analysis -> analysis.processNewlyLiveMethod(method));
   }
 
+  private void markReferencedTypesAsLive(DexEncodedMethod method) {
+    markTypeAsLive(
+        method.method.holder, clazz -> graphReporter.reportClassReferencedFrom(clazz, method));
+    markParameterAndReturnTypesAsLive(method);
+  }
+
   private void markParameterAndReturnTypesAsLive(DexEncodedMethod method) {
     for (DexType parameterType : method.method.proto.parameters.values) {
       markTypeAsLive(
@@ -2880,6 +2900,31 @@
     }
   }
 
+  private class LiveMethodsSet {
+
+    private final Set<DexEncodedMethod> items = Sets.newIdentityHashSet();
+
+    private final BiConsumer<DexEncodedMethod, KeepReason> register;
+
+    LiveMethodsSet(BiConsumer<DexEncodedMethod, KeepReason> register) {
+      this.register = register;
+    }
+
+    boolean add(DexProgramClass clazz, DexEncodedMethod method, KeepReason reason) {
+      register.accept(method, reason);
+      transitionUnusedInterfaceToLive(clazz);
+      return items.add(method);
+    }
+
+    boolean contains(DexEncodedMethod method) {
+      return items.contains(method);
+    }
+
+    Set<DexEncodedMethod> getItems() {
+      return Collections.unmodifiableSet(items);
+    }
+  }
+
   private static class SetWithReason<T> {
 
     private final Set<T> items = Sets.newIdentityHashSet();
diff --git a/src/main/java/com/android/tools/r8/shaking/GraphReporter.java b/src/main/java/com/android/tools/r8/shaking/GraphReporter.java
index 5dc405e..521b90c 100644
--- a/src/main/java/com/android/tools/r8/shaking/GraphReporter.java
+++ b/src/main/java/com/android/tools/r8/shaking/GraphReporter.java
@@ -199,6 +199,16 @@
   }
 
   public KeepReasonWitness reportClassReferencedFrom(
+      DexProgramClass clazz, DexProgramClass implementer) {
+    if (keptGraphConsumer != null) {
+      ClassGraphNode source = getClassGraphNode(implementer.type);
+      ClassGraphNode target = getClassGraphNode(clazz.type);
+      return reportEdge(source, target, EdgeKind.ReferencedFrom);
+    }
+    return KeepReasonWitness.INSTANCE;
+  }
+
+  public KeepReasonWitness reportClassReferencedFrom(
       DexProgramClass clazz, DexEncodedMethod method) {
     if (keptGraphConsumer != null) {
       MethodGraphNode source = getMethodGraphNode(method.method);
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java
index 5ab6474..cc9fafa 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationSourceStrings.java
@@ -32,7 +32,7 @@
   }
 
   @VisibleForTesting
-  static ProguardConfigurationSourceStrings createConfigurationForTesting(
+  public static ProguardConfigurationSourceStrings createConfigurationForTesting(
       List<String> config) {
     return new ProguardConfigurationSourceStrings(config);
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
index 487afa2..9326e31 100644
--- a/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
@@ -157,6 +157,8 @@
     private final HashMultiset<Wrapper<DexField>> fieldBuckets = HashMultiset.create();
     private final HashMultiset<Wrapper<DexMethod>> methodBuckets = HashMultiset.create();
 
+    private boolean hasSynchronizedMethods = false;
+
     public Representative(DexProgramClass clazz) {
       this.clazz = clazz;
       include(clazz);
@@ -168,10 +170,14 @@
         Wrapper<DexField> wrapper = fieldEquivalence.wrap(field.field);
         fieldBuckets.add(wrapper);
       }
+      boolean classHasSynchronizedMethods = false;
       for (DexEncodedMethod method : clazz.methods()) {
+        assert !hasSynchronizedMethods || !method.accessFlags.isSynchronized();
+        classHasSynchronizedMethods |= method.accessFlags.isSynchronized();
         Wrapper<DexMethod> wrapper = methodEquivalence.wrap(method.method);
         methodBuckets.add(wrapper);
       }
+      hasSynchronizedMethods |= classHasSynchronizedMethods;
     }
 
     // Returns true if this representative should no longer be used. The current heuristic is to
@@ -322,83 +328,42 @@
     assert satisfiesMergeCriteria(clazz) == group;
     assert group != MergeGroup.DONT_MERGE;
 
-    String pkg = clazz.type.getPackageDescriptor();
-    return mayMergeAcrossPackageBoundaries(clazz)
-        ? mergeGlobally(clazz, pkg, group)
-        : mergeInsidePackage(clazz, pkg, group);
+    return merge(
+        clazz,
+        mayMergeAcrossPackageBoundaries(clazz)
+            ? group.globalKey()
+            : group.key(clazz.type.getPackageDescriptor()));
   }
 
-  private boolean mergeGlobally(DexProgramClass clazz, String pkg, MergeGroup group) {
-    Representative globalRepresentative = representatives.get(group.globalKey());
-    if (globalRepresentative == null) {
-      if (isValidRepresentative(clazz)) {
-        // Make the current class the global representative.
-        setRepresentative(group.globalKey(), getOrCreateRepresentative(group.key(pkg), clazz));
-      } else {
-        clearRepresentative(group.globalKey());
-      }
-
-      // Do not attempt to merge this class inside its own package, because that could lead to
-      // an increase in the global representative, which is not desirable.
-      return false;
-    } else {
-      // Check if we can merge the current class into the current global representative.
-      globalRepresentative.include(clazz);
-
-      if (globalRepresentative.isFull()) {
-        if (isValidRepresentative(clazz)) {
-          // Make the current class the global representative instead.
-          setRepresentative(group.globalKey(), getOrCreateRepresentative(group.key(pkg), clazz));
-        } else {
-          clearRepresentative(group.globalKey());
-        }
-
-        // Do not attempt to merge this class inside its own package, because that could lead to
-        // an increase in the global representative, which is not desirable.
+  private boolean merge(DexProgramClass clazz, MergeGroup.Key key) {
+    Representative representative = representatives.get(key);
+    if (representative != null) {
+      if (representative.hasSynchronizedMethods && clazz.hasStaticSynchronizedMethods()) {
+        // We are not allowed to merge synchronized classes with synchronized methods.
         return false;
-      } else {
-        // Merge this class into the global representative.
-        moveMembersFromSourceToTarget(clazz, globalRepresentative.clazz);
-        return true;
       }
-    }
-  }
-
-  private boolean mergeInsidePackage(DexProgramClass clazz, String pkg, MergeGroup group) {
-    MergeGroup.Key key = group.key(pkg);
-    Representative packageRepresentative = representatives.get(key);
-    if (packageRepresentative != null) {
+      // Check if current candidate is a better choice depending on visibility. For package private
+      // or protected, the key is parameterized by the package name already, so we just have to
+      // check accessibility-flags. For global this is no-op.
       if (isValidRepresentative(clazz)
-          && clazz.accessFlags.isMoreVisibleThan(
-              packageRepresentative.clazz.accessFlags,
-              clazz.type.getPackageName(),
-              packageRepresentative.clazz.type.getPackageName())) {
-        // Use `clazz` as a representative for this package instead.
+          && !representative.clazz.accessFlags.isAtLeastAsVisibleAs(clazz.accessFlags)) {
+        assert clazz.type.getPackageDescriptor().equals(key.packageOrGlobal);
+        assert representative.clazz.type.getPackageDescriptor().equals(key.packageOrGlobal);
         Representative newRepresentative = getOrCreateRepresentative(key, clazz);
-        newRepresentative.include(packageRepresentative.clazz);
-
+        newRepresentative.include(representative.clazz);
         if (!newRepresentative.isFull()) {
-          setRepresentative(group.key(pkg), newRepresentative);
-          moveMembersFromSourceToTarget(packageRepresentative.clazz, clazz);
+          setRepresentative(key, newRepresentative);
+          moveMembersFromSourceToTarget(representative.clazz, clazz);
           return true;
         }
-
-        // We are not allowed to merge members into a class that is less visible.
-        return false;
-      }
-
-      // Merge current class into the representative of this package if it has room.
-      packageRepresentative.include(clazz);
-
-      // If there is room, then merge, otherwise fall-through to update the representative of this
-      // package.
-      if (!packageRepresentative.isFull()) {
-        moveMembersFromSourceToTarget(clazz, packageRepresentative.clazz);
-        return true;
+      } else {
+        representative.include(clazz);
+        if (!representative.isFull()) {
+          moveMembersFromSourceToTarget(clazz, representative.clazz);
+          return true;
+        }
       }
     }
-
-    // We were unable to use the current representative for this package (if any).
     if (isValidRepresentative(clazz)) {
       setRepresentative(key, getOrCreateRepresentative(key, clazz));
     }
diff --git a/src/main/java/com/android/tools/r8/shaking/TreePruner.java b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
index cd23ff7..cccf0f4 100644
--- a/src/main/java/com/android/tools/r8/shaking/TreePruner.java
+++ b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
@@ -25,6 +25,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.function.Predicate;
@@ -108,32 +109,41 @@
   }
 
   private void pruneUnusedInterfaces(DexProgramClass clazz) {
-    int numberOfReachableInterfaces = 0;
+    boolean implementsUnusedInterfaces = false;
     for (DexType type : clazz.interfaces.values) {
       // TODO(christofferqa): Extend unused interface removal to library classes.
-      if (isTypeLive(type)) {
-        numberOfReachableInterfaces++;
+      if (!isTypeLive(type)) {
+        implementsUnusedInterfaces = true;
+        break;
       }
     }
 
-    if (numberOfReachableInterfaces == clazz.interfaces.size()) {
+    if (!implementsUnusedInterfaces) {
       return;
     }
 
-    if (numberOfReachableInterfaces == 0) {
-      clazz.interfaces = DexTypeList.empty();
-      return;
-    }
-
-    DexType[] reachableInterfaces = new DexType[numberOfReachableInterfaces];
-    int i = 0;
+    Set<DexType> reachableInterfaces = new LinkedHashSet<>();
     for (DexType type : clazz.interfaces.values) {
-      if (isTypeLive(type)) {
-        reachableInterfaces[i] = type;
-        i++;
+      retainReachableInterfacesFrom(type, reachableInterfaces);
+    }
+    if (reachableInterfaces.isEmpty()) {
+      clazz.interfaces = DexTypeList.empty();
+    } else {
+      clazz.interfaces = new DexTypeList(reachableInterfaces.toArray(DexType.EMPTY_ARRAY));
+    }
+  }
+
+  private void retainReachableInterfacesFrom(DexType type, Set<DexType> reachableInterfaces) {
+    if (isTypeLive(type)) {
+      reachableInterfaces.add(type);
+    } else {
+      DexProgramClass unusedInterface = appView.definitionForProgramType(type);
+      assert unusedInterface != null;
+      assert unusedInterface.isInterface();
+      for (DexType interfaceType : unusedInterface.interfaces.values) {
+        retainReachableInterfacesFrom(interfaceType, reachableInterfaces);
       }
     }
-    clazz.interfaces = new DexTypeList(reachableInterfaces);
   }
 
   private void pruneMembersAndAttributes(DexProgramClass clazz) {
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
index b8b8482..4dc15ab 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -138,6 +138,7 @@
     RESOLUTION_FOR_METHODS_MAY_CHANGE,
     SERVICE_LOADER,
     STATIC_INITIALIZERS,
+    STATIC_SYNCHRONIZED_METHODS,
     UNHANDLED_INVOKE_DIRECT,
     UNHANDLED_INVOKE_SUPER,
     UNSAFE_INLINING,
@@ -198,6 +199,9 @@
         case UNSUPPORTED_ATTRIBUTES:
           message = "since inner-class attributes are not supported";
           break;
+        case STATIC_SYNCHRONIZED_METHODS:
+          message = "since it has static synchronized methods which can lead to unwanted behavior";
+          break;
         default:
           assert false;
       }
@@ -806,6 +810,14 @@
       assert isStillMergeCandidate(clazz);
     }
 
+    // Check for static synchronized methods on source
+    if (clazz.hasStaticSynchronizedMethods()) {
+      if (Log.ENABLED) {
+        AbortReason.STATIC_SYNCHRONIZED_METHODS.printLogMessageForClass(clazz);
+      }
+      return;
+    }
+
     // Guard against the case where we have two methods that may get the same signature
     // if we replace types. This is rare, so we approximate and err on the safe side here.
     if (new CollisionDetector(clazz.type, targetClass.type).mayCollide()) {
@@ -1460,7 +1472,9 @@
       for (SynthesizedBridgeCode synthesizedBridge : synthesizedBridges) {
         synthesizedBridge.updateMethodSignatures(this::fixupMethod);
       }
-      return lensBuilder.build(appView, mergedClasses);
+      GraphLense graphLense = lensBuilder.build(appView, mergedClasses);
+      new AnnotationFixer(graphLense).run(appView.appInfo().classes());
+      return graphLense;
     }
 
     private void fixupMethods(List<DexEncodedMethod> methods, MethodSetter setter) {
diff --git a/src/main/java/com/android/tools/r8/utils/AarArchiveResourceProvider.java b/src/main/java/com/android/tools/r8/utils/AarArchiveResourceProvider.java
index 7de79dc..6b36180 100644
--- a/src/main/java/com/android/tools/r8/utils/AarArchiveResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/AarArchiveResourceProvider.java
@@ -66,7 +66,7 @@
 
   private List<ProgramResource> readArchive() throws IOException {
     List<ProgramResource> classResources = null;
-    try (ZipFile zipFile = new ZipFile(archive.toFile(), StandardCharsets.UTF_8)) {
+    try (ZipFile zipFile = FileUtils.createZipFile(archive.toFile(), StandardCharsets.UTF_8)) {
       final Enumeration<? extends ZipEntry> entries = zipFile.entries();
       while (entries.hasMoreElements()) {
         ZipEntry entry = entries.nextElement();
diff --git a/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java b/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
index e612e60..5a50f1b 100644
--- a/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
@@ -53,7 +53,8 @@
   private List<ProgramResource> readArchive() throws IOException {
     List<ProgramResource> dexResources = new ArrayList<>();
     List<ProgramResource> classResources = new ArrayList<>();
-    try (ZipFile zipFile = new ZipFile(archive.getPath().toFile(), StandardCharsets.UTF_8)) {
+    try (ZipFile zipFile =
+        FileUtils.createZipFile(archive.getPath().toFile(), StandardCharsets.UTF_8)) {
       final Enumeration<? extends ZipEntry> entries = zipFile.entries();
       while (entries.hasMoreElements()) {
         ZipEntry entry = entries.nextElement();
@@ -109,7 +110,8 @@
 
   @Override
   public void accept(Visitor resourceBrowser) throws ResourceException {
-    try (ZipFile zipFile = new ZipFile(archive.getPath().toFile(), StandardCharsets.UTF_8)) {
+    try (ZipFile zipFile =
+        FileUtils.createZipFile(archive.getPath().toFile(), StandardCharsets.UTF_8)) {
       final Enumeration<? extends ZipEntry> entries = zipFile.entries();
       while (entries.hasMoreElements()) {
         ZipEntry entry = entries.nextElement();
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 416b389..474f5d0 100644
--- a/src/main/java/com/android/tools/r8/utils/ClassMap.java
+++ b/src/main/java/com/android/tools/r8/utils/ClassMap.java
@@ -42,7 +42,7 @@
    * We also allow the transition from Supplier of a null value to the actual value null and vice
    * versa.
    */
-  private final ConcurrentHashMap<DexType, Supplier<T>> classes;
+  private final Map<DexType, Supplier<T>> classes;
 
   /**
    * Class provider if available.
@@ -55,7 +55,7 @@
    */
   private final AtomicReference<ClassProvider<T>> classProvider = new AtomicReference<>();
 
-  ClassMap(ConcurrentHashMap<DexType, Supplier<T>> classes, ClassProvider<T> classProvider) {
+  ClassMap(Map<DexType, Supplier<T>> classes, ClassProvider<T> classProvider) {
     assert classProvider == null || classProvider.getClassKind() == getClassKind();
     this.classes = classes == null ? new ConcurrentHashMap<>() : classes;
     this.classProvider.set(classProvider);
diff --git a/src/main/java/com/android/tools/r8/utils/FileUtils.java b/src/main/java/com/android/tools/r8/utils/FileUtils.java
index 87b8506..7185445 100644
--- a/src/main/java/com/android/tools/r8/utils/FileUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/FileUtils.java
@@ -9,12 +9,14 @@
 import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.OpenOption;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import java.util.Arrays;
 import java.util.List;
+import java.util.zip.ZipFile;
 
 public class FileUtils {
 
@@ -28,6 +30,9 @@
   public static final String JAVA_EXTENSION = ".java";
   public static final String MODULE_INFO_CLASS = "module-info.class";
 
+  public static final boolean isAndroid =
+      System.getProperty("java.vm.name").equalsIgnoreCase("Dalvik");
+
   public static boolean isDexFile(Path path) {
     String name = path.getFileName().toString().toLowerCase();
     return name.endsWith(DEX_EXTENSION);
@@ -94,7 +99,7 @@
   public static Path validateOutputFile(Path path, Reporter reporter) {
     if (path != null) {
       boolean isJarOrZip = isZipFile(path) || isJarFile(path);
-      if (!isJarOrZip  && !(Files.exists(path) && Files.isDirectory(path))) {
+      if (!isJarOrZip && !(Files.exists(path) && Files.isDirectory(path))) {
         reporter.error(new StringDiagnostic(
             "Invalid output: "
                 + path
@@ -184,4 +189,21 @@
       return path.replace('/', '\\');
     }
   }
+
+  public static ZipFile createZipFile(File file, Charset charset) throws IOException {
+    if (!isAndroid) {
+      return new ZipFile(file, charset);
+    }
+    // On Android pre-26 we cannot use the constructor ZipFile(file, charset).
+    // By default Android use UTF_8 as the charset, so we can use the default constructor.
+    if (Charset.defaultCharset() == StandardCharsets.UTF_8) {
+      return new ZipFile(file);
+    }
+    // If the Android runtime is started with a different default charset, the default constructor
+    // won't work. It is possible to support this case if we read/write the ZipFile not with it's
+    // own Input/OutputStream, but an external one which one can define on a different Charset than
+    // default. We do not support this at the moment since R8 on dex is used only in tests, and
+    // UTF_8 is the default charset used in tests.
+    throw new RuntimeException("R8 can run on dex only with UTF_8 as the default charset.");
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/InternalArchiveClassFileProvider.java b/src/main/java/com/android/tools/r8/utils/InternalArchiveClassFileProvider.java
index cf30d7a..e5557d9 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalArchiveClassFileProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalArchiveClassFileProvider.java
@@ -100,7 +100,7 @@
   private ZipFile getOpenZipFile() throws IOException {
     if (openedZipFile == null) {
       try {
-        openedZipFile = new ZipFile(path.toFile(), StandardCharsets.UTF_8);
+        openedZipFile = FileUtils.createZipFile(path.toFile(), StandardCharsets.UTF_8);
       } catch (IOException e) {
         if (!Files.exists(path)) {
           throw new NoSuchFileException(path.toString());
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index 89aeb2d..687e7da 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -198,6 +198,9 @@
   public boolean enableNonNullTracking = true;
   public boolean enableInlining =
       !Version.isDev() || System.getProperty("com.android.tools.r8.disableinlining") == null;
+  // TODO(b/141451716): Evaluate the effect of allowing inlining in the inlinee.
+  public boolean applyInliningToInlinee = false;
+  public int applyInliningToInlineeMaxDepth = 0;
   public boolean enableInliningOfInvokesWithNullableReceivers = true;
   public boolean disableInliningOfLibraryMethodOverrides = true;
   public boolean enableClassInlining = true;
@@ -970,6 +973,8 @@
     public boolean dontReportFailingCheckDiscarded = false;
     public boolean deterministicSortingBasedOnDexType = true;
     public PrintStream whyAreYouNotInliningConsumer = System.out;
+    public boolean trackDesugaredAPIConversions =
+        System.getProperty("com.android.tools.r8.trackDesugaredAPIConversions") != null;
 
     // Flag to turn on/off JDK11+ nest-access control even when not required (Cf backend)
     public boolean enableForceNestBasedAccessDesugaringForTest = false;
diff --git a/src/main/java/com/android/tools/r8/utils/IteratorUtils.java b/src/main/java/com/android/tools/r8/utils/IteratorUtils.java
index 8e3fe1a..7ffce35 100644
--- a/src/main/java/com/android/tools/r8/utils/IteratorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/IteratorUtils.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.utils;
 
 import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.InstructionListIterator;
@@ -65,6 +66,16 @@
     return null;
   }
 
+  public static <T> T previousUntil(ListIterator<T> iterator, Predicate<T> predicate) {
+    while (iterator.hasPrevious()) {
+      T previous = iterator.previous();
+      if (predicate.test(previous)) {
+        return previous;
+      }
+    }
+    throw new Unreachable();
+  }
+
   public static <T> void removeIf(Iterator<T> iterator, Predicate<T> predicate) {
     while (iterator.hasNext()) {
       T item = iterator.next();
diff --git a/src/test/examples/classmerging/ConflictingInterfaceSignaturesTest.java b/src/test/examples/classmerging/ConflictingInterfaceSignaturesTest.java
index 223ff37..7de7a1f 100644
--- a/src/test/examples/classmerging/ConflictingInterfaceSignaturesTest.java
+++ b/src/test/examples/classmerging/ConflictingInterfaceSignaturesTest.java
@@ -12,6 +12,17 @@
 
     B b = new InterfaceImpl();
     b.foo();
+
+    // Ensure that the instantiations are not dead code eliminated.
+    escape(a);
+    escape(b);
+  }
+
+  @NeverInline
+  static void escape(Object o) {
+    if (System.currentTimeMillis() < 0) {
+      System.out.println(o);
+    }
   }
 
   public interface A {
diff --git a/src/test/examples/classmerging/MethodCollisionTest.java b/src/test/examples/classmerging/MethodCollisionTest.java
index a9010a4..4456498 100644
--- a/src/test/examples/classmerging/MethodCollisionTest.java
+++ b/src/test/examples/classmerging/MethodCollisionTest.java
@@ -7,8 +7,22 @@
 public class MethodCollisionTest {
 
   public static void main(String[] args) {
-    new B().m();
-    new D().m();
+    B b = new B();
+    b.m();
+
+    D d = new D();
+    d.m();
+
+    // Ensure that the instantiations are not dead code eliminated.
+    escape(b);
+    escape(d);
+  }
+
+  @NeverInline
+  static void escape(Object o) {
+    if (System.currentTimeMillis() < 0) {
+      System.out.println(o);
+    }
   }
 
   public static class A {
diff --git a/src/test/examples/classmerging/NeverInline.java b/src/test/examples/classmerging/NeverInline.java
new file mode 100644
index 0000000..9438902
--- /dev/null
+++ b/src/test/examples/classmerging/NeverInline.java
@@ -0,0 +1,10 @@
+// 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 classmerging;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+@Target({ElementType.METHOD})
+public @interface NeverInline {}
diff --git a/src/test/examples/classmerging/SimpleInterfaceAccessTest.java b/src/test/examples/classmerging/SimpleInterfaceAccessTest.java
index ce7bf14..6fde90e 100644
--- a/src/test/examples/classmerging/SimpleInterfaceAccessTest.java
+++ b/src/test/examples/classmerging/SimpleInterfaceAccessTest.java
@@ -18,6 +18,17 @@
     // package references OtherSimpleInterface.
     OtherSimpleInterface y = new OtherSimpleInterfaceImpl();
     y.bar();
+
+    // Ensure that the instantiations are not dead code eliminated.
+    escape(x);
+    escape(y);
+  }
+
+  @NeverInline
+  static void escape(Object o) {
+    if (System.currentTimeMillis() < 0) {
+      System.out.println(o);
+    }
   }
 
   // Should only be merged into OtherSimpleInterfaceImpl if access modifications are allowed.
diff --git a/src/test/examples/classmerging/SuperCallRewritingTest.java b/src/test/examples/classmerging/SuperCallRewritingTest.java
index 7a3d45c..6f59419 100644
--- a/src/test/examples/classmerging/SuperCallRewritingTest.java
+++ b/src/test/examples/classmerging/SuperCallRewritingTest.java
@@ -7,6 +7,17 @@
 public class SuperCallRewritingTest {
   public static void main(String[] args) {
     System.out.println("Calling referencedMethod on SubClassThatReferencesSuperMethod");
-    System.out.println(new SubClassThatReferencesSuperMethod().referencedMethod());
+    SubClassThatReferencesSuperMethod obj = new SubClassThatReferencesSuperMethod();
+    System.out.println(obj.referencedMethod());
+
+    // Ensure that the instantiations are not dead code eliminated.
+    escape(obj);
+  }
+
+  @NeverInline
+  static void escape(Object o) {
+    if (System.currentTimeMillis() < 0) {
+      System.out.println(o);
+    }
   }
 }
diff --git a/src/test/examples/classmerging/SyntheticBridgeSignaturesTest.java b/src/test/examples/classmerging/SyntheticBridgeSignaturesTest.java
index a17d30a..c263e98 100644
--- a/src/test/examples/classmerging/SyntheticBridgeSignaturesTest.java
+++ b/src/test/examples/classmerging/SyntheticBridgeSignaturesTest.java
@@ -15,6 +15,17 @@
     BSub b = new BSub();
     a.m(b);
     b.m(a);
+
+    // Ensure that the instantiations are not dead code eliminated.
+    escape(a);
+    escape(b);
+  }
+
+  @NeverInline
+  static void escape(Object o) {
+    if (System.currentTimeMillis() < 0) {
+      System.out.println(o);
+    }
   }
 
   private static class A {
diff --git a/src/test/examples/classmerging/TemplateMethodTest.java b/src/test/examples/classmerging/TemplateMethodTest.java
index ac9de15..fe7de76 100644
--- a/src/test/examples/classmerging/TemplateMethodTest.java
+++ b/src/test/examples/classmerging/TemplateMethodTest.java
@@ -9,6 +9,16 @@
   public static void main(String[] args) {
     AbstractClass obj = new AbstractClassImpl();
     obj.foo();
+
+    // Ensure that the instantiations are not dead code eliminated.
+    escape(obj);
+  }
+
+  @NeverInline
+  static void escape(Object o) {
+    if (System.currentTimeMillis() < 0) {
+      System.out.println(o);
+    }
   }
 
   private abstract static class AbstractClass {
diff --git a/src/test/examples/classmerging/Test.java b/src/test/examples/classmerging/Test.java
index 6d5c51f..d3b2762 100644
--- a/src/test/examples/classmerging/Test.java
+++ b/src/test/examples/classmerging/Test.java
@@ -13,8 +13,17 @@
     ConflictingInterfaceImpl impl = new ConflictingInterfaceImpl();
     callMethodOnIface(impl);
     System.out.println(new SubClassThatReferencesSuperMethod().referencedMethod());
-    System.out.println(new Outer().getInstance().method());
+    Outer outer = new Outer();
+    Outer.SubClass inner = outer.getInstance();
+    System.out.println(outer.getInstance().method());
     System.out.println(new SubClass(42));
+
+    // Ensure that the instantiations are not dead code eliminated.
+    escape(clazz);
+    escape(iface);
+    escape(impl);
+    escape(inner);
+    escape(outer);
   }
 
   private static void callMethodOnIface(GenericInterface iface) {
@@ -31,4 +40,11 @@
     System.out.println(ClassWithConflictingMethod.conflict(null));
     System.out.println(OtherClassWithConflictingMethod.conflict(null));
   }
+
+  @NeverInline
+  static void escape(Object o) {
+    if (System.currentTimeMillis() < 0) {
+      System.out.println(o);
+    }
+  }
 }
diff --git a/src/test/examples/classmerging/keep-rules.txt b/src/test/examples/classmerging/keep-rules.txt
index c75058d..11a66c6 100644
--- a/src/test/examples/classmerging/keep-rules.txt
+++ b/src/test/examples/classmerging/keep-rules.txt
@@ -68,4 +68,8 @@
   public static void main(...);
 }
 
+-neverinline class * {
+  @classmerging.NeverInline <methods>;
+}
+
 -printmapping
diff --git a/src/test/examples/hello/keep-rules.txt b/src/test/examples/hello/keep-rules.txt
new file mode 100644
index 0000000..5a49e41
--- /dev/null
+++ b/src/test/examples/hello/keep-rules.txt
@@ -0,0 +1 @@
+-keep class hello.Hello { public static void main(...);}
diff --git a/src/test/examplesAndroidO/classmerging/MergeDefaultMethodIntoClassTest.java b/src/test/examplesAndroidO/classmerging/MergeDefaultMethodIntoClassTest.java
index 10f995d..5158228 100644
--- a/src/test/examplesAndroidO/classmerging/MergeDefaultMethodIntoClassTest.java
+++ b/src/test/examplesAndroidO/classmerging/MergeDefaultMethodIntoClassTest.java
@@ -11,6 +11,16 @@
     // invoke-interface instruction and not invoke-virtual instruction.
     A obj = new B();
     obj.f();
+
+    // Ensure that the instantiations are not dead code eliminated.
+    escape(obj);
+  }
+
+  @NeverInline
+  static void escape(Object o) {
+    if (System.currentTimeMillis() < 0) {
+      System.out.println(o);
+    }
   }
 
   public interface A {
diff --git a/src/test/examplesAndroidO/classmerging/NestedDefaultInterfaceMethodsTest.java b/src/test/examplesAndroidO/classmerging/NestedDefaultInterfaceMethodsTest.java
index 7a0e325..f237c2e 100644
--- a/src/test/examplesAndroidO/classmerging/NestedDefaultInterfaceMethodsTest.java
+++ b/src/test/examplesAndroidO/classmerging/NestedDefaultInterfaceMethodsTest.java
@@ -7,7 +7,18 @@
 public class NestedDefaultInterfaceMethodsTest {
 
   public static void main(String[] args) {
-    new C().m();
+    C obj = new C();
+    obj.m();
+
+    // Ensure that the instantiations are not dead code eliminated.
+    escape(obj);
+  }
+
+  @NeverInline
+  static void escape(Object o) {
+    if (System.currentTimeMillis() < 0) {
+      System.out.println(o);
+    }
   }
 
   public interface A {
diff --git a/src/test/examplesAndroidO/classmerging/NeverInline.java b/src/test/examplesAndroidO/classmerging/NeverInline.java
new file mode 100644
index 0000000..9438902
--- /dev/null
+++ b/src/test/examplesAndroidO/classmerging/NeverInline.java
@@ -0,0 +1,10 @@
+// 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 classmerging;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+@Target({ElementType.METHOD})
+public @interface NeverInline {}
diff --git a/src/test/examplesAndroidO/classmerging/keep-rules.txt b/src/test/examplesAndroidO/classmerging/keep-rules.txt
index 4df182e..18a133d 100644
--- a/src/test/examplesAndroidO/classmerging/keep-rules.txt
+++ b/src/test/examplesAndroidO/classmerging/keep-rules.txt
@@ -14,5 +14,9 @@
   public static void main(...);
 }
 
+-neverinline class * {
+  @classmerging.NeverInline <methods>;
+}
+
 # TODO(herhut): Consider supporting merging of inner-class attributes.
 # -keepattributes *
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/r8/AlwaysInline.java b/src/test/java/com/android/tools/r8/AlwaysInline.java
new file mode 100644
index 0000000..97a799a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/AlwaysInline.java
@@ -0,0 +1,10 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+@Target({ElementType.METHOD})
+public @interface AlwaysInline {}
diff --git a/src/test/java/com/android/tools/r8/ProguardTestBuilder.java b/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
index 6695f46..e53fdce 100644
--- a/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/ProguardTestBuilder.java
@@ -17,6 +17,7 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
 import java.util.function.Consumer;
@@ -193,6 +194,28 @@
   }
 
   @Override
+  public ProguardTestBuilder addLibraryClasses(Class<?>... classes) {
+    addLibraryClasses(Arrays.asList(classes));
+    return self();
+  }
+
+  @Override
+  public ProguardTestBuilder addLibraryClasses(Collection<Class<?>> classes) {
+    List<Path> pathsForClasses = new ArrayList<>(classes.size());
+    for (Class<?> clazz : classes) {
+      pathsForClasses.add(ToolHelper.getClassFileForTestClass(clazz));
+    }
+    try {
+      Path out = getState().getNewTempFolder().resolve("out.jar");
+      TestBase.writeClassFilesToJar(out, pathsForClasses);
+      libraryjars.add(out);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+    return self();
+  }
+
+  @Override
   public ProguardTestBuilder addClasspathClasses(Collection<Class<?>> classes) {
     throw new Unimplemented("No support for adding classpath data directly");
   }
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index b834aac..8a7d989 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -196,6 +196,19 @@
         options -> options.testing.allowUnusedProguardConfigurationRules = true);
   }
 
+  public T enableAlwaysInliningAnnotations() {
+    return enableAlwaysInliningAnnotations(AlwaysInline.class.getPackage().getName());
+  }
+
+  public T enableAlwaysInliningAnnotations(String annotationPackageName) {
+    if (!enableInliningAnnotations) {
+      enableInliningAnnotations = true;
+      addInternalKeepRules(
+          "-alwaysinline class * { @" + annotationPackageName + ".AlwaysInline *; }");
+    }
+    return self();
+  }
+
   public T enableInliningAnnotations() {
     return enableInliningAnnotations(NeverInline.class.getPackage().getName());
   }
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 9b8abe6..797d185 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -147,6 +147,17 @@
     return testForJvm(temp);
   }
 
+  public TestBuilder<?, ?> testForRuntime(TestRuntime runtime, AndroidApiLevel apiLevel) {
+    if (runtime.isCf()) {
+      return testForJvm();
+    } else {
+      assert runtime.isDex();
+      D8TestBuilder d8TestBuilder = testForD8();
+      d8TestBuilder.setMinApi(apiLevel);
+      return d8TestBuilder;
+    }
+  }
+
   public ProguardTestBuilder testForProguard() {
     return testForProguard(temp);
   }
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 0464ccb..84e8cc9 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -137,7 +137,7 @@
   private static final String PROGUARD6_0_1 = "third_party/proguard/proguard6.0.1/bin/proguard";
   private static final String PROGUARD = PROGUARD5_2_1;
   public static final String JACOCO_AGENT = "third_party/jacoco/org.jacoco.agent-0.8.2-runtime.jar";
-  public static final String PROGUARD_SETTINGS_FOR_INTERNAL_APPS = "third_party/proguardsettings";
+  public static final String PROGUARD_SETTINGS_FOR_INTERNAL_APPS = "third_party/proguardsettings/";
 
   private static final String RETRACE6_0_1 = "third_party/proguard/proguard6.0.1/bin/retrace";
   private static final String RETRACE = RETRACE6_0_1;
@@ -1147,6 +1147,10 @@
     return compatSink.build();
   }
 
+  public static void runL8(L8Command command) throws CompilationFailedException {
+    runL8(command, options -> {});
+  }
+
   public static void runL8(L8Command command, Consumer<InternalOptions> optionsModifier)
       throws CompilationFailedException {
     InternalOptions internalOptions = command.getInternalOptions();
diff --git a/src/test/java/com/android/tools/r8/classmerging/HorizontalClassMergerShouldMergeSynchronizedMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/HorizontalClassMergerShouldMergeSynchronizedMethodTest.java
new file mode 100644
index 0000000..7007821
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/HorizontalClassMergerShouldMergeSynchronizedMethodTest.java
@@ -0,0 +1,82 @@
+// 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.classmerging;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+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 HorizontalClassMergerShouldMergeSynchronizedMethodTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public HorizontalClassMergerShouldMergeSynchronizedMethodTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testNoMergingOfClassUsedInMonitor()
+      throws IOException, CompilationFailedException, ExecutionException {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(HorizontalClassMergerShouldMergeSynchronizedMethodTest.class)
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("1", "2", "3")
+        .inspect(
+            inspector -> {
+              assertFalse(
+                  inspector.clazz(LockOne.class).isPresent()
+                      && inspector.clazz(LockTwo.class).isPresent());
+              assertTrue(
+                  inspector.clazz(LockOne.class).isPresent()
+                      || inspector.clazz(LockTwo.class).isPresent());
+            });
+  }
+
+  // Will be merged with LockTwo.
+  static class LockOne {
+
+    static synchronized void print1() {
+      System.out.println("1");
+    }
+
+    static synchronized void print2() {
+      System.out.println("2");
+    }
+  }
+
+  public static class LockTwo {
+
+    static void print3() {
+      System.out.println("3");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      LockOne.print1();
+      LockOne.print2();
+      LockTwo.print3();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/HorizontalClassMergerSynchronizedMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/HorizontalClassMergerSynchronizedMethodTest.java
new file mode 100644
index 0000000..a811257
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/HorizontalClassMergerSynchronizedMethodTest.java
@@ -0,0 +1,150 @@
+// 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.classmerging;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import java.io.IOException;
+import java.lang.Thread.State;
+import java.util.concurrent.ExecutionException;
+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 HorizontalClassMergerSynchronizedMethodTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    // The 4.0.4 runtime will flakily mark threads as blocking and report DEADLOCKED.
+    return getTestParameters()
+        .withDexRuntimesStartingFromExcluding(Version.V4_0_4)
+        .withAllApiLevels()
+        .build();
+  }
+
+  public HorizontalClassMergerSynchronizedMethodTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testOnRuntime() throws IOException, CompilationFailedException, ExecutionException {
+    testForRuntime(parameters.getRuntime(), parameters.getApiLevel())
+        .addInnerClasses(HorizontalClassMergerSynchronizedMethodTest.class)
+        .run(parameters.getRuntime(), HorizontalClassMergerSynchronizedMethodTest.Main.class)
+        .assertSuccessWithOutput("Hello World!");
+  }
+
+  @Test
+  public void testNoMergingOfClassUsedInMonitor()
+      throws IOException, CompilationFailedException, ExecutionException {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(HorizontalClassMergerSynchronizedMethodTest.class)
+        .addKeepMainRule(Main.class)
+        .enableMergeAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput("Hello World!")
+        .inspect(inspector -> assertThat(inspector.clazz(LockOne.class), isPresent()));
+  }
+
+  private interface I {
+
+    void action();
+  }
+
+  // Will be merged with LockTwo.
+  static class LockOne {
+
+    static synchronized void acquire(I c) {
+      c.action();
+    }
+  }
+
+  public static class LockTwo {
+
+    static synchronized void acquire(I c) {
+      Main.inTwoCritical = true;
+      while (!Main.inThreeCritical) {}
+      c.action();
+    }
+  }
+
+  @NeverMerge
+  public static class LockThree {
+
+    static synchronized void acquire(I c) {
+      Main.inThreeCritical = true;
+      while (!Main.inTwoCritical) {}
+      c.action();
+    }
+  }
+
+  public static class AcquireOne implements I {
+
+    @Override
+    public void action() {
+      LockOne.acquire(() -> System.out.print("Hello "));
+    }
+  }
+
+  public static class AcquireThree implements I {
+
+    @Override
+    public void action() {
+      LockThree.acquire(() -> System.out.print("World!"));
+    }
+  }
+
+  public static class Main {
+
+    static volatile boolean inTwoCritical = false;
+    static volatile boolean inThreeCritical = false;
+    static volatile boolean arnoldWillNotBeBack = false;
+
+    private static volatile Thread t1 = new Thread(Main::lockThreeThenOne);
+    private static volatile Thread t2 = new Thread(Main::lockTwoThenThree);
+    private static volatile Thread terminator = new Thread(Main::arnold);
+
+    public static void main(String[] args) {
+      t1.start();
+      t2.start();
+      // This thread is started to ensure termination in case we are rewriting incorrectly.
+      terminator.start();
+
+      while (!arnoldWillNotBeBack) {}
+    }
+
+    static void lockThreeThenOne() {
+      LockThree.acquire(new AcquireOne());
+    }
+
+    static void lockTwoThenThree() {
+      LockTwo.acquire(new AcquireThree());
+    }
+
+    static void arnold() {
+      while (t1.getState() != State.TERMINATED || t2.getState() != State.TERMINATED) {
+        if (t1.getState() == State.BLOCKED && t2.getState() == State.BLOCKED) {
+          System.err.println("DEADLOCKED!");
+          System.exit(1);
+          break;
+        }
+      }
+      arnoldWillNotBeBack = true;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerTest.java
index cd0f8b6..3fa47cb 100644
--- a/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerTest.java
@@ -7,15 +7,17 @@
 import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.AssumeMayHaveSideEffects;
+import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
+import java.io.IOException;
 import java.util.List;
+import java.util.concurrent.ExecutionException;
 import java.util.stream.Collectors;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -51,6 +53,9 @@
     }
   }
 
+  private final String EXPECTED =
+      StringUtils.joinLines("StaticMergeCandidateA.m()", "StaticMergeCandidateB.m()");
+
   private final TestParameters parameters;
 
   public StaticClassMergerTest(TestParameters parameters) {
@@ -59,16 +64,19 @@
 
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().build();
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testRuntime() throws IOException, CompilationFailedException, ExecutionException {
+    testForRuntime(parameters.getRuntime(), parameters.getApiLevel())
+        .addInnerClasses(StaticClassMergerTest.class)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
   }
 
   @Test
   public void testStaticClassIsRemoved() throws Exception {
-    String expected =
-        StringUtils.joinLines("StaticMergeCandidateA.m()", "StaticMergeCandidateB.m()");
-
-    testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expected);
-
     CodeInspector inspector =
         testForR8(parameters.getBackend())
             .addInnerClasses(StaticClassMergerTest.class)
@@ -76,8 +84,9 @@
             .noMinification()
             .enableInliningAnnotations()
             .enableSideEffectAnnotations()
+            .setMinApi(parameters.getApiLevel())
             .run(parameters.getRuntime(), TestClass.class)
-            .assertSuccessWithOutput(expected)
+            .assertSuccessWithOutput(EXPECTED)
             .inspector();
 
     // Check that one of the two static merge candidates has been removed
diff --git a/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerVisibilityTest.java b/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerVisibilityTest.java
new file mode 100644
index 0000000..46ccfd2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerVisibilityTest.java
@@ -0,0 +1,112 @@
+// 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.classmerging;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 StaticClassMergerVisibilityTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  public StaticClassMergerVisibilityTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void testStaticClassIsRemoved() throws Exception {
+    CodeInspector inspector =
+        testForR8(parameters.getBackend())
+            .addInnerClasses(StaticClassMergerVisibilityTest.class)
+            .addKeepMainRule(Main.class)
+            .enableInliningAnnotations()
+            .setMinApi(parameters.getApiLevel())
+            .run(parameters.getRuntime(), Main.class)
+            .assertSuccessWithOutputLines("A.print()", "B.print()", "C.print()", "D.print()")
+            .inspector();
+
+    // The global group will have one representative, which is C.
+    ClassSubject clazzC = inspector.clazz(C.class);
+    assertThat(clazzC, isPresent());
+    assertEquals(1, clazzC.allMethods().size());
+
+    // The package group will be merged into one of A, B or C.
+    assertExactlyOneIsPresent(
+        inspector.clazz(A.class), inspector.clazz(B.class), inspector.clazz(D.class));
+  }
+
+  private void assertExactlyOneIsPresent(ClassSubject... subjects) {
+    boolean seenPresent = false;
+    for (ClassSubject subject : subjects) {
+      if (subject.isPresent()) {
+        assertFalse(seenPresent);
+        seenPresent = true;
+      }
+    }
+    assertTrue(seenPresent);
+  }
+
+  // Will be merged into the package group.
+  private static class A {
+    @NeverInline
+    private static void print() {
+      System.out.println("A.print()");
+    }
+  }
+
+  // Will be merged into the package group.
+  static class B {
+    @NeverInline
+    static void print() {
+      System.out.println("B.print()");
+    }
+  }
+
+  // Will be merged into global group
+  public static class C {
+    @NeverInline
+    public static void print() {
+      System.out.println("C.print()");
+    }
+  }
+
+  // Will be merged into the package group.
+  protected static class D {
+    @NeverInline
+    protected static void print() {
+      System.out.println("D.print()");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      A.print();
+      B.print();
+      C.print();
+      D.print();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerShouldMergeSynchronizedMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerShouldMergeSynchronizedMethodTest.java
new file mode 100644
index 0000000..5f6e255
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerShouldMergeSynchronizedMethodTest.java
@@ -0,0 +1,85 @@
+// 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.classmerging;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+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 VerticalClassMergerShouldMergeSynchronizedMethodTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public VerticalClassMergerShouldMergeSynchronizedMethodTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testNoMergingOfClassUsedInMonitor()
+      throws IOException, CompilationFailedException, ExecutionException {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(VerticalClassMergerShouldMergeSynchronizedMethodTest.class)
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("1", "2", "3", "4")
+        .inspect(
+            inspector -> {
+              assertThat(inspector.clazz(LockOne.class), not(isPresent()));
+              assertThat(inspector.clazz(LockTwo.class), isPresent());
+            });
+  }
+
+  // Will be merged with LockTwo.
+  static class LockOne {
+
+    synchronized void print1() {
+      System.out.println("1");
+      print2();
+    }
+
+    private synchronized void print2() {
+      System.out.println("2");
+    }
+  }
+
+  public static class LockTwo extends LockOne {
+
+    synchronized void print3() {
+      System.out.println("3");
+    }
+
+    static void print4() {
+      System.out.println("4");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      LockTwo lockTwo = new LockTwo();
+      lockTwo.print1();
+      lockTwo.print3();
+      LockTwo.print4();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedBlockTest.java b/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedBlockTest.java
new file mode 100644
index 0000000..9b3c384
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedBlockTest.java
@@ -0,0 +1,128 @@
+// 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.classmerging;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.StringContains.containsString;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import java.io.IOException;
+import java.lang.Thread.State;
+import java.util.concurrent.ExecutionException;
+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 VerticalClassMergerSynchronizedBlockTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    // The 4.0.4 runtime will flakily mark threads as blocking and report DEADLOCKED.
+    return getTestParameters()
+        .withDexRuntimesStartingFromExcluding(Version.V4_0_4)
+        .withAllApiLevels()
+        .build();
+  }
+
+  public VerticalClassMergerSynchronizedBlockTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testOnRuntime() throws IOException, CompilationFailedException, ExecutionException {
+    testForRuntime(parameters.getRuntime(), parameters.getApiLevel())
+        .addInnerClasses(VerticalClassMergerSynchronizedBlockTest.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput("Hello World!");
+  }
+
+  @Test
+  public void testNoMergingOfClassUsedInMonitor()
+      throws IOException, CompilationFailedException, ExecutionException {
+    // TODO(b/142438687): Fix expectation when fixed.
+    R8TestRunResult result =
+        testForR8(parameters.getBackend())
+            .addInnerClasses(VerticalClassMergerSynchronizedBlockTest.class)
+            .addKeepMainRule(Main.class)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .run(parameters.getRuntime(), Main.class)
+            .assertFailureWithErrorThatMatches(containsString("DEADLOCKED!"));
+    if (result.getExitCode() == 0) {
+      result.inspect(inspector -> assertThat(inspector.clazz(LockOne.class), isPresent()));
+    }
+  }
+
+  abstract static class LockOne {}
+
+  static class LockTwo extends LockOne {}
+
+  static class LockThree {}
+
+  public static class Main {
+
+    private static volatile boolean inLockThreeCritical = false;
+    private static volatile boolean inLockTwoCritical = false;
+    private static volatile boolean arnoldWillNotBeBack = false;
+
+    private static volatile Thread t1 = new Thread(Main::lockThreeThenOne);
+    private static volatile Thread t2 = new Thread(Main::lockTwoThenThree);
+    private static volatile Thread t3 = new Thread(Main::arnold);
+
+    static void synchronizedAccessThroughLocks(String arg) {
+      System.out.print(arg);
+    }
+
+    public static void main(String[] args) {
+      t1.start();
+      t2.start();
+      // This thread is started to ensure termination in case we are rewriting incorrectly.
+      t3.start();
+
+      while (!arnoldWillNotBeBack) {}
+    }
+
+    static void lockThreeThenOne() {
+      synchronized (LockThree.class) {
+        inLockThreeCritical = true;
+        while (!inLockTwoCritical) {}
+        synchronized (LockOne.class) {
+          synchronizedAccessThroughLocks("Hello ");
+        }
+      }
+    }
+
+    static void lockTwoThenThree() {
+      synchronized (LockTwo.class) {
+        inLockTwoCritical = true;
+        while (!inLockThreeCritical) {}
+        synchronized (LockThree.class) {
+          synchronizedAccessThroughLocks("World!");
+        }
+      }
+    }
+
+    static void arnold() {
+      while (t1.getState() != State.TERMINATED || t2.getState() != State.TERMINATED) {
+        if (t1.getState() == State.BLOCKED && t2.getState() == State.BLOCKED) {
+          System.err.println("DEADLOCKED!");
+          System.exit(1);
+          break;
+        }
+      }
+      arnoldWillNotBeBack = true;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedMethodTest.java
new file mode 100644
index 0000000..e77ee05
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedMethodTest.java
@@ -0,0 +1,150 @@
+// 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.classmerging;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import java.io.IOException;
+import java.lang.Thread.State;
+import java.util.concurrent.ExecutionException;
+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 VerticalClassMergerSynchronizedMethodTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    // The 4.0.4 runtime will flakily mark threads as blocking and report DEADLOCKED.
+    return getTestParameters()
+        .withDexRuntimesStartingFromExcluding(Version.V4_0_4)
+        .withAllApiLevels()
+        .build();
+  }
+
+  public VerticalClassMergerSynchronizedMethodTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testOnRuntime() throws IOException, CompilationFailedException, ExecutionException {
+    testForRuntime(parameters.getRuntime(), parameters.getApiLevel())
+        .addInnerClasses(VerticalClassMergerSynchronizedMethodTest.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput("Hello World!");
+  }
+
+  @Test
+  public void testNoMergingOfClassUsedInMonitor()
+      throws IOException, CompilationFailedException, ExecutionException {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(VerticalClassMergerSynchronizedMethodTest.class)
+        .addKeepMainRule(Main.class)
+        .enableMergeAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput("Hello World!")
+        .inspect(inspector -> assertThat(inspector.clazz(LockOne.class), isPresent()));
+  }
+
+  private interface I {
+
+    void action();
+  }
+
+  // Will be merged into LockTwo.
+  abstract static class LockOne {
+
+    static synchronized void acquire(I c) {
+      c.action();
+    }
+  }
+
+  @NeverMerge
+  public static class LockTwo extends LockOne {
+
+    static synchronized void acquire(I c) {
+      Main.inTwoCritical = true;
+      while (!Main.inThreeCritical) {}
+      c.action();
+    }
+  }
+
+  @NeverMerge
+  public static class LockThree {
+
+    static synchronized void acquire(I c) {
+      Main.inThreeCritical = true;
+      while (!Main.inTwoCritical) {}
+      c.action();
+    }
+  }
+
+  public static class AcquireOne implements I {
+
+    @Override
+    public void action() {
+      LockOne.acquire(() -> System.out.print("Hello "));
+    }
+  }
+
+  public static class AcquireThree implements I {
+
+    @Override
+    public void action() {
+      LockThree.acquire(() -> System.out.print("World!"));
+    }
+  }
+
+  public static class Main {
+
+    static volatile boolean inTwoCritical = false;
+    static volatile boolean inThreeCritical = false;
+    static volatile boolean arnoldWillNotBeBack = false;
+
+    private static volatile Thread t1 = new Thread(Main::lockThreeThenOne);
+    private static volatile Thread t2 = new Thread(Main::lockTwoThenThree);
+    private static volatile Thread terminator = new Thread(Main::arnold);
+
+    public static void main(String[] args) {
+      t1.start();
+      t2.start();
+      // This thread is started to ensure termination in case we are rewriting incorrectly.
+      terminator.start();
+
+      while (!arnoldWillNotBeBack) {}
+    }
+
+    static void lockThreeThenOne() {
+      LockThree.acquire(new AcquireOne());
+    }
+
+    static void lockTwoThenThree() {
+      LockTwo.acquire(new AcquireThree());
+    }
+
+    static void arnold() {
+      while (t1.getState() != State.TERMINATED || t2.getState() != State.TERMINATED) {
+        if (t1.getState() == State.BLOCKED && t2.getState() == State.BLOCKED) {
+          System.err.println("DEADLOCKED!");
+          System.exit(1);
+          break;
+        }
+      }
+      arnoldWillNotBeBack = true;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerTest.java
index 6b4f17d..82385a2 100644
--- a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerTest.java
@@ -10,8 +10,6 @@
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.OutputMode;
@@ -78,7 +76,6 @@
       .resolve("classmerging").resolve("keep-rules-dontoptimize.txt");
 
   private void configure(InternalOptions options) {
-    options.enableClassInlining = false;
     options.enableSideEffectAnalysis = false;
     options.enableUnusedInterfaceRemoval = false;
     options.testing.nondeterministicCycleElimination = true;
@@ -90,6 +87,7 @@
         testForR8(Backend.DEX)
             .addProgramFiles(EXAMPLE_JAR)
             .addKeepRuleFiles(proguardConfig)
+            .enableProguardTestOptions()
             .noMinification()
             .addOptionsModification(optionsConsumer)
             .compile()
@@ -110,18 +108,18 @@
     runR8(EXAMPLE_KEEP, this::configure);
     // GenericInterface should be merged into GenericInterfaceImpl.
     for (String candidate : CAN_BE_MERGED) {
-      assertFalse(inspector.clazz(candidate).isPresent());
+      assertThat(inspector.clazz(candidate), not(isPresent()));
     }
-    assertTrue(inspector.clazz("classmerging.GenericInterfaceImpl").isPresent());
-    assertTrue(inspector.clazz("classmerging.Outer$SubClass").isPresent());
-    assertTrue(inspector.clazz("classmerging.SubClass").isPresent());
+    assertThat(inspector.clazz("classmerging.GenericInterfaceImpl"), isPresent());
+    assertThat(inspector.clazz("classmerging.Outer$SubClass"), isPresent());
+    assertThat(inspector.clazz("classmerging.SubClass"), isPresent());
   }
 
   @Test
   public void testClassesHaveNotBeenMerged() throws Throwable {
     runR8(DONT_OPTIMIZE, null);
     for (String candidate : CAN_BE_MERGED) {
-      assertTrue(inspector.clazz(candidate).isPresent());
+      assertThat(inspector.clazz(candidate), isPresent());
     }
   }
 
@@ -304,8 +302,8 @@
   @Test
   public void testConflictWasDetected() throws Throwable {
     runR8(EXAMPLE_KEEP, this::configure);
-    assertTrue(inspector.clazz("classmerging.ConflictingInterface").isPresent());
-    assertTrue(inspector.clazz("classmerging.ConflictingInterfaceImpl").isPresent());
+    assertThat(inspector.clazz("classmerging.ConflictingInterface"), isPresent());
+    assertThat(inspector.clazz("classmerging.ConflictingInterfaceImpl"), isPresent());
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/desugar/corelib/EmptyDesugaredLibrary.java b/src/test/java/com/android/tools/r8/desugar/corelib/EmptyDesugaredLibrary.java
new file mode 100644
index 0000000..998682c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/corelib/EmptyDesugaredLibrary.java
@@ -0,0 +1,101 @@
+// 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.desugar.corelib;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.ByteDataView;
+import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.L8Command;
+import com.android.tools.r8.OutputMode;
+import com.android.tools.r8.StringResource;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Set;
+import java.util.zip.ZipFile;
+import org.junit.Test;
+
+public class EmptyDesugaredLibrary extends CoreLibDesugarTestBase {
+
+  private L8Command.Builder prepareL8Builder(AndroidApiLevel minApiLevel) {
+    return L8Command.builder()
+        .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
+        .addProgramFiles(ToolHelper.getDesugarJDKLibs())
+        .addDesugaredLibraryConfiguration(
+            StringResource.fromFile(ToolHelper.DESUGAR_LIB_JSON_FOR_TESTING))
+        .setMinApiLevel(minApiLevel.getLevel());
+  }
+
+  static class CountingProgramConsumer extends DexIndexedConsumer.ForwardingConsumer {
+
+    public int count = 0;
+
+    public CountingProgramConsumer() {
+      super(null);
+    }
+
+    @Override
+    public void accept(
+        int fileIndex, ByteDataView data, Set<String> descriptors, DiagnosticsHandler handler) {
+      count++;
+    }
+  }
+
+  @Test
+  public void testEmptyDesugaredLibrary() throws Exception {
+    for (AndroidApiLevel apiLevel : AndroidApiLevel.values()) {
+      if (apiLevel.getLevel() < AndroidApiLevel.K.getLevel()) {
+        // No need to test all API levels.
+        continue;
+      }
+      CountingProgramConsumer programConsumer = new CountingProgramConsumer();
+      ToolHelper.runL8(prepareL8Builder(apiLevel).setProgramConsumer(programConsumer).build());
+      assertEquals(
+          apiLevel.getLevel() >= AndroidApiLevel.O.getLevel() ? 0 : 1, programConsumer.count);
+    }
+  }
+
+  @Test
+  public void testEmptyDesugaredLibraryDexZip() throws Exception {
+    for (AndroidApiLevel apiLevel : AndroidApiLevel.values()) {
+      if (apiLevel.getLevel() < AndroidApiLevel.K.getLevel()) {
+        // No need to test all API levels.
+        continue;
+      }
+      Path desugaredLibraryZip = temp.newFolder().toPath().resolve("desugar_jdk_libs_dex.zip");
+      ToolHelper.runL8(
+          prepareL8Builder(apiLevel).setOutput(desugaredLibraryZip, OutputMode.DexIndexed).build());
+      assertTrue(Files.exists(desugaredLibraryZip));
+      assertEquals(
+          apiLevel.getLevel() >= AndroidApiLevel.O.getLevel() ? 0 : 1,
+          new ZipFile(desugaredLibraryZip.toFile(), StandardCharsets.UTF_8).size());
+    }
+  }
+
+  @Test
+  public void testEmptyDesugaredLibraryDexDirectory() throws Exception {
+    for (AndroidApiLevel apiLevel : AndroidApiLevel.values()) {
+      if (apiLevel.getLevel() < AndroidApiLevel.K.getLevel()) {
+        // No need to test all API levels.
+        continue;
+      }
+      Path desugaredLibraryDirectory = temp.newFolder().toPath();
+      ToolHelper.runL8(
+          prepareL8Builder(apiLevel)
+              .setOutput(desugaredLibraryDirectory, OutputMode.DexIndexed)
+              .build());
+      assertEquals(
+          apiLevel.getLevel() >= AndroidApiLevel.O.getLevel() ? 0 : 1,
+          Files.walk(desugaredLibraryDirectory)
+              .filter(path -> path.toString().endsWith(".dex"))
+              .count());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/corelib/HelloWorldCompiledOnArtTest.java b/src/test/java/com/android/tools/r8/desugar/corelib/HelloWorldCompiledOnArtTest.java
new file mode 100644
index 0000000..044f075
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/corelib/HelloWorldCompiledOnArtTest.java
@@ -0,0 +1,135 @@
+// 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.desugar.corelib;
+
+import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
+import static junit.framework.TestCase.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.D8;
+import com.android.tools.r8.D8TestBuilder;
+import com.android.tools.r8.D8TestCompileResult;
+import com.android.tools.r8.R8;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Collections;
+import org.junit.BeforeClass;
+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 HelloWorldCompiledOnArtTest extends CoreLibDesugarTestBase {
+
+  // TODO(b/142621961): Create an abstraction to easily run tests on External DexR8.
+  // Manage pathMock in the abstraction.
+  private static Path pathMock;
+
+  @BeforeClass
+  public static void compilePathBackport() throws Exception {
+    assumeTrue("JDK8 is not checked-in on Windows", !ToolHelper.isWindows());
+    pathMock = getStaticTemp().newFolder("PathMock").toPath();
+    ProcessResult processResult =
+        ToolHelper.runJavac(
+            CfVm.JDK8,
+            Collections.emptyList(),
+            pathMock,
+            getAllFilesWithSuffixInDirectory(Paths.get("src/test/r8OnArtBackport"), "java"));
+    assertEquals(0, processResult.exitCode);
+  }
+
+  public static Path[] getPathBackport() throws Exception {
+    return getAllFilesWithSuffixInDirectory(pathMock, "class");
+  }
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withDexRuntimesStartingFromIncluding(Version.V7_0_0)
+        .withApiLevelsStartingAtIncluding(AndroidApiLevel.L)
+        .build();
+  }
+
+  public HelloWorldCompiledOnArtTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  private static String commandLinePathFor(String string) {
+    return Paths.get(string).toAbsolutePath().toString();
+  }
+
+  private static final String HELLO_KEEP =
+      commandLinePathFor("src/test/examples/hello/keep-rules.txt");
+  private static final String HELLO_PATH =
+      commandLinePathFor(ToolHelper.EXAMPLES_BUILD_DIR + "hello" + JAR_EXTENSION);
+
+  @Test
+  public void testHelloCompiledWithR8Dex() throws Exception {
+    Path helloOutput = temp.newFolder("helloOutput").toPath().resolve("out.zip").toAbsolutePath();
+    compileR8ToDexWithD8()
+        .run(
+            parameters.getRuntime(),
+            R8.class,
+            "--release",
+            "--output",
+            helloOutput.toString(),
+            "--lib",
+            commandLinePathFor(ToolHelper.JAVA_8_RUNTIME),
+            "--pg-conf",
+            HELLO_KEEP,
+            HELLO_PATH)
+        .assertSuccess();
+    verifyResult(helloOutput);
+  }
+
+  @Test
+  public void testHelloCompiledWithD8Dex() throws Exception {
+    Path helloOutput = temp.newFolder("helloOutput").toPath().resolve("out.zip").toAbsolutePath();
+    compileR8ToDexWithD8()
+        .run(
+            parameters.getRuntime(),
+            D8.class,
+            "--release",
+            "--output",
+            helloOutput.toString(),
+            "--lib",
+            commandLinePathFor(ToolHelper.JAVA_8_RUNTIME),
+            HELLO_PATH)
+        .assertSuccess();
+    verifyResult(helloOutput);
+  }
+
+  private void verifyResult(Path helloOutput) throws IOException {
+    ProcessResult processResult = ToolHelper.runArtRaw(helloOutput.toString(), "hello.Hello");
+    assertEquals(StringUtils.lines("Hello, world"), processResult.stdout);
+  }
+
+  private D8TestCompileResult compileR8ToDexWithD8() throws Exception {
+    D8TestBuilder d8TestBuilder =
+        testForD8().addProgramFiles(ToolHelper.R8_WITH_RELOCATED_DEPS_JAR);
+    if (parameters.getApiLevel().getLevel() < AndroidApiLevel.O.getLevel()) {
+      d8TestBuilder.addProgramFiles(getPathBackport());
+    }
+    return d8TestBuilder
+        .setMinApi(parameters.getApiLevel())
+        .enableCoreLibraryDesugaring(parameters.getApiLevel())
+        .compile()
+        .addDesugaredCoreLibraryRunClassPath(this::buildDesugaredLibrary, parameters.getApiLevel())
+        .withArt6Plus64BitsLib()
+        .withArtFrameworks();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/corelib/LinkedHashSetTest.java b/src/test/java/com/android/tools/r8/desugar/corelib/LinkedHashSetTest.java
new file mode 100644
index 0000000..178af61
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/corelib/LinkedHashSetTest.java
@@ -0,0 +1,120 @@
+// 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.desugar.corelib;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import java.util.LinkedHashSet;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class LinkedHashSetTest extends CoreLibDesugarTestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public LinkedHashSetTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testLinkedHashSetOverrides() throws Exception {
+    String stdOut =
+        testForD8()
+            .addInnerClasses(LinkedHashSetTest.class)
+            .setMinApi(parameters.getApiLevel())
+            .enableCoreLibraryDesugaring(parameters.getApiLevel())
+            .compile()
+            .addDesugaredCoreLibraryRunClassPath(
+                this::buildDesugaredLibrary, parameters.getApiLevel())
+            .run(parameters.getRuntime(), Executor.class)
+            .assertSuccess()
+            .getStdOut();
+    assertLines2By2Correct(stdOut);
+  }
+
+  static class Executor {
+    @SuppressWarnings("RedundantOperationOnEmptyContainer")
+    public static void main(String[] args) {
+      Spliterator<String> spliterator;
+
+      // Spliterator of Set is only distinct.
+      // Spliterator of LinkedHashSet is distinct and ordered.
+      // Spliterator of CustomLinkedHashSetOverride is distinct, ordered and immutable.
+      // If an incorrect method is found, characteristics are incorrect.
+
+      spliterator = new LinkedHashSet<String>().spliterator();
+      System.out.println(spliterator.hasCharacteristics(Spliterator.DISTINCT));
+      System.out.println("true");
+      System.out.println(spliterator.hasCharacteristics(Spliterator.ORDERED));
+      System.out.println("true");
+      System.out.println(spliterator.hasCharacteristics(Spliterator.IMMUTABLE));
+      System.out.println("false");
+
+      spliterator = new CustomLinkedHashSetOverride<String>().spliterator();
+      System.out.println(spliterator.hasCharacteristics(Spliterator.DISTINCT));
+      System.out.println("true");
+      System.out.println(spliterator.hasCharacteristics(Spliterator.ORDERED));
+      System.out.println("true");
+      System.out.println(spliterator.hasCharacteristics(Spliterator.IMMUTABLE));
+      System.out.println("true");
+
+      spliterator = new CustomLinkedHashSetNoOverride<String>().spliterator();
+      System.out.println(spliterator.hasCharacteristics(Spliterator.DISTINCT));
+      System.out.println("true");
+      System.out.println(spliterator.hasCharacteristics(Spliterator.ORDERED));
+      System.out.println("true");
+      System.out.println(spliterator.hasCharacteristics(Spliterator.IMMUTABLE));
+      System.out.println("false");
+
+      spliterator = new CustomLinkedHashSetOverride<String>().superSpliterator();
+      System.out.println(spliterator.hasCharacteristics(Spliterator.DISTINCT));
+      System.out.println("true");
+      System.out.println(spliterator.hasCharacteristics(Spliterator.ORDERED));
+      System.out.println("true");
+      System.out.println(spliterator.hasCharacteristics(Spliterator.IMMUTABLE));
+      System.out.println("false");
+
+      spliterator = new SubclassOverride<String>().superSpliterator();
+      System.out.println(spliterator.hasCharacteristics(Spliterator.DISTINCT));
+      System.out.println("true");
+      System.out.println(spliterator.hasCharacteristics(Spliterator.ORDERED));
+      System.out.println("true");
+      System.out.println(spliterator.hasCharacteristics(Spliterator.IMMUTABLE));
+      System.out.println("true");
+    }
+  }
+
+  static class CustomLinkedHashSetOverride<E> extends LinkedHashSet<E> {
+
+    @Override
+    public Spliterator<E> spliterator() {
+      return Spliterators.spliterator(
+          this, Spliterator.DISTINCT | Spliterator.ORDERED | Spliterator.IMMUTABLE);
+    }
+
+    public Spliterator<E> superSpliterator() {
+      return super.spliterator();
+    }
+  }
+
+  static class SubclassOverride<E> extends CustomLinkedHashSetOverride<E> {
+    @Override
+    public Spliterator<E> superSpliterator() {
+      return super.spliterator();
+    }
+  }
+
+  @SuppressWarnings("WeakerAccess")
+  static class CustomLinkedHashSetNoOverride<E> extends LinkedHashSet<E> {}
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/corelib/RetargetOverrideTest.java b/src/test/java/com/android/tools/r8/desugar/corelib/RetargetOverrideTest.java
new file mode 100644
index 0000000..df4e4d8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/corelib/RetargetOverrideTest.java
@@ -0,0 +1,156 @@
+// 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.desugar.corelib;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import java.time.Instant;
+import java.time.ZonedDateTime;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.util.concurrent.atomic.AtomicInteger;
+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 RetargetOverrideTest extends CoreLibDesugarTestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public RetargetOverrideTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testRetargetOverrideD8() throws Exception {
+    String stdout =
+        testForD8()
+            .addInnerClasses(RetargetOverrideTest.class)
+            .enableCoreLibraryDesugaring(parameters.getApiLevel())
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .addDesugaredCoreLibraryRunClassPath(
+                this::buildDesugaredLibrary, parameters.getApiLevel())
+            .run(parameters.getRuntime(), Executor.class)
+            .assertSuccess()
+            .getStdOut();
+    assertLines2By2Correct(stdout);
+  }
+
+  @Test
+  public void testRetargetOverrideR8() throws Exception {
+    String stdout =
+        testForR8(Backend.DEX)
+            .addKeepMainRule(Executor.class)
+            .addInnerClasses(RetargetOverrideTest.class)
+            .enableCoreLibraryDesugaring(parameters.getApiLevel())
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .addDesugaredCoreLibraryRunClassPath(
+                this::buildDesugaredLibrary, parameters.getApiLevel())
+            .run(parameters.getRuntime(), Executor.class)
+            .assertSuccess()
+            .getStdOut();
+    assertLines2By2Correct(stdout);
+  }
+
+  static class Executor {
+
+    public static void main(String[] args) {
+      java.sql.Date date = new java.sql.Date(123456789);
+      System.out.println(date.toInstant());
+      System.out.println("1970-01-02T10:17:36.789Z");
+
+      GregorianCalendar gregCal = new GregorianCalendar(1990, 2, 22);
+      System.out.println(gregCal.toInstant());
+      System.out.println("1990-03-22T00:00:00Z");
+
+      // TODO(b/142846107): Enable overrides of retarget core members.
+      // MyCalendarOverride myCal = new MyCalendarOverride(1990, 2, 22);
+      // System.out.println(myCal.toZonedDateTime());
+      // System.out.println("1990-11-22T00:00Z[GMT]");
+      // System.out.println(myCal.toInstant());
+      // System.out.println("1990-03-22T00:00:00Z");
+
+      MyCalendarNoOverride myCalN = new MyCalendarNoOverride(1990, 2, 22);
+      System.out.println(myCalN.toZonedDateTime());
+      System.out.println("1990-03-22T00:00Z[GMT]");
+      System.out.println(myCalN.toInstant());
+      System.out.println("1990-03-22T00:00:00Z");
+
+      // TODO(b/142846107): Enable overrides of retarget core members.
+      // MyDateOverride myDate = new MyDateOverride(123456789);
+      // System.out.println(myDate.toInstant());
+      // System.out.println("1970-01-02T10:17:45.789Z");
+
+      MyDateNoOverride myDateN = new MyDateNoOverride(123456789);
+      System.out.println(myDateN.toInstant());
+      System.out.println("1970-01-02T10:17:36.789Z");
+
+      MyAtomicInteger myAtomicInteger = new MyAtomicInteger(42);
+      System.out.println(myAtomicInteger.getAndUpdate(x -> x + 1));
+      System.out.println("42");
+      System.out.println(myAtomicInteger.getAndUpdate(x -> x + 5));
+      System.out.println("43");
+      System.out.println(myAtomicInteger.updateAndGet(x -> x + 100));
+      System.out.println("148");
+      System.out.println(myAtomicInteger.updateAndGet(x -> x + 500));
+      System.out.println("648");
+    }
+  }
+
+  // static class MyCalendarOverride extends GregorianCalendar {
+  //
+  //   public MyCalendarOverride(int year, int month, int dayOfMonth) {
+  //     super(year, month, dayOfMonth);
+  //   }
+  //
+  //   // Cannot override toInstant (final).
+  //
+  //   @Override
+  //   public ZonedDateTime toZonedDateTime() {
+  //     return super.toZonedDateTime().withMonth(11);
+  //   }
+  // }
+
+  static class MyCalendarNoOverride extends GregorianCalendar {
+    public MyCalendarNoOverride(int year, int month, int dayOfMonth) {
+      super(year, month, dayOfMonth);
+    }
+  }
+
+  // static class MyDateOverride extends Date {
+  //
+  //   public MyDateOverride(long date) {
+  //     super(date);
+  //   }
+  //
+  //   @Override
+  //   public Instant toInstant() {
+  //     return super.toInstant().plusSeconds(9);
+  //   }
+  // }
+
+  static class MyDateNoOverride extends Date {
+
+    public MyDateNoOverride(long date) {
+      super(date);
+    }
+  }
+
+  static class MyAtomicInteger extends AtomicInteger {
+    // No overrides, all final methods.
+    public MyAtomicInteger(int initialValue) {
+      super(initialValue);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/corelib/conversionTests/AllTimeConversionTest.java b/src/test/java/com/android/tools/r8/desugar/corelib/conversionTests/AllTimeConversionTest.java
index ed5ca26..3775eba 100644
--- a/src/test/java/com/android/tools/r8/desugar/corelib/conversionTests/AllTimeConversionTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/corelib/conversionTests/AllTimeConversionTest.java
@@ -4,6 +4,11 @@
 
 package com.android.tools.r8.desugar.corelib.conversionTests;
 
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertTrue;
+
+import com.android.tools.r8.D8TestCompileResult;
+import com.android.tools.r8.TestDiagnosticMessages;
 import com.android.tools.r8.TestRuntime.DexRuntime;
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.utils.AndroidApiLevel;
@@ -22,12 +27,15 @@
   @Test
   public void testRewrittenAPICalls() throws Exception {
     Path customLib = testForD8().addProgramClasses(CustomLibClass.class).compile().writeToZip();
-    testForD8()
-        .setMinApi(AndroidApiLevel.B)
-        .addProgramClasses(Executor.class)
-        .addLibraryClasses(CustomLibClass.class)
-        .enableCoreLibraryDesugaring(AndroidApiLevel.B)
-        .compile()
+    D8TestCompileResult compileResult =
+        testForD8()
+            .setMinApi(AndroidApiLevel.B)
+            .addProgramClasses(Executor.class)
+            .addLibraryClasses(CustomLibClass.class)
+            .addOptionsModification(options -> options.testing.trackDesugaredAPIConversions = true)
+            .enableCoreLibraryDesugaring(AndroidApiLevel.B)
+            .compile();
+    compileResult
         .addDesugaredCoreLibraryRunClassPath(
             this::buildDesugaredLibraryWithConversionExtension, AndroidApiLevel.B)
         .addRunClasspathFiles(customLib)
@@ -41,8 +49,29 @@
                 "-1000000000-01-01T00:00:00.999999999Z",
                 "GMT",
                 "GMT"));
+    assertTrackedAPIS(compileResult.getDiagnosticMessages());
   }
 
+  private void assertTrackedAPIS(TestDiagnosticMessages diagnosticMessages) {
+    assertTrue(
+        diagnosticMessages
+            .getWarnings()
+            .get(0)
+            .getDiagnosticMessage()
+            .startsWith("Tracked desugared API conversions:"));
+    assertEquals(
+        9, diagnosticMessages.getWarnings().get(0).getDiagnosticMessage().split("\n").length);
+    assertTrue(
+        diagnosticMessages
+            .getWarnings()
+            .get(1)
+            .getDiagnosticMessage()
+            .startsWith("Tracked callback desugared API conversions:"));
+    assertEquals(
+        1, diagnosticMessages.getWarnings().get(1).getDiagnosticMessage().split("\n").length);
+  }
+
+
   static class Executor {
 
     private static final String ZONE_ID = "GMT";
diff --git a/src/test/java/com/android/tools/r8/internal/Gmail17060416TreeShakeJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/Gmail17060416TreeShakeJarVerificationTest.java
new file mode 100644
index 0000000..82e7e93
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/internal/Gmail17060416TreeShakeJarVerificationTest.java
@@ -0,0 +1,49 @@
+// 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.internal;
+
+import static com.android.tools.r8.ToolHelper.isLocalDevelopment;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import java.nio.file.Paths;
+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 Gmail17060416TreeShakeJarVerificationTest extends GmailCompilationBase {
+  private static final int MAX_SIZE = 20000000;
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().build();
+  }
+
+  public Gmail17060416TreeShakeJarVerificationTest(TestParameters parameters) {
+    super(170604, 16);
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void buildAndTreeShakeFromDeployJar() throws Exception {
+    assumeTrue(isLocalDevelopment());
+
+    R8TestCompileResult compileResult =
+        testForR8(parameters.getBackend())
+            .addKeepRuleFiles(Paths.get(base).resolve(BASE_PG_CONF))
+            .allowUnusedProguardConfigurationRules()
+            .compile();
+
+    int appSize = applicationSize(compileResult.app);
+    assertTrue("Expected max size of " + MAX_SIZE+ ", got " + appSize, appSize < MAX_SIZE);
+  }
+
+}
diff --git a/src/test/java/com/android/tools/r8/internal/Gmail18082615TreeShakeJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/Gmail18082615TreeShakeJarVerificationTest.java
new file mode 100644
index 0000000..0f6a563
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/internal/Gmail18082615TreeShakeJarVerificationTest.java
@@ -0,0 +1,51 @@
+// 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.internal;
+
+import static com.android.tools.r8.ToolHelper.isLocalDevelopment;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import java.nio.file.Paths;
+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 Gmail18082615TreeShakeJarVerificationTest extends GmailCompilationBase {
+  private static final int MAX_SIZE = 20000000;
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().build();
+  }
+
+  public Gmail18082615TreeShakeJarVerificationTest(TestParameters parameters) {
+    super(180826, 15);
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void buildAndTreeShakeFromDeployJar() throws Exception {
+    assumeTrue(isLocalDevelopment());
+
+    R8TestCompileResult compileResult =
+        testForR8(parameters.getBackend())
+            .addKeepRuleFiles(
+                Paths.get(base).resolve(BASE_PG_CONF),
+                Paths.get(ToolHelper.PROGUARD_SETTINGS_FOR_INTERNAL_APPS, PG_CONF))
+            .allowUnusedProguardConfigurationRules()
+            .compile();
+
+    int appSize = applicationSize(compileResult.app);
+    assertTrue("Expected max size of " + MAX_SIZE+ ", got " + appSize, appSize < MAX_SIZE);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/internal/GmailCompilationBase.java b/src/test/java/com/android/tools/r8/internal/GmailCompilationBase.java
new file mode 100644
index 0000000..7332dd8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/internal/GmailCompilationBase.java
@@ -0,0 +1,19 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.internal;
+
+abstract class GmailCompilationBase extends CompilationTestBase {
+  static final String APK = "Gmail_release_unsigned.apk";
+  static final String DEPLOY_JAR = "Gmail_release_unstripped_deploy.jar";
+  static final String PG_JAR = "Gmail_release_unstripped_proguard.jar";
+  static final String PG_MAP = "Gmail_release_unstripped_proguard.map";
+  static final String BASE_PG_CONF = "Gmail_release_unstripped_proguard.config";
+  static final String PG_CONF = "Gmail_proguard.config";
+
+  final String base;
+
+  GmailCompilationBase(int majorVersion, int minorVersion) {
+    this.base = "third_party/gmail/gmail_android_" + majorVersion + "." + minorVersion + "/";
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/internal/R8GMSCoreLatestTreeShakeJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/R8GMSCoreLatestTreeShakeJarVerificationTest.java
index 0168988..e8417f1 100644
--- a/src/test/java/com/android/tools/r8/internal/R8GMSCoreLatestTreeShakeJarVerificationTest.java
+++ b/src/test/java/com/android/tools/r8/internal/R8GMSCoreLatestTreeShakeJarVerificationTest.java
@@ -22,7 +22,7 @@
   public void buildAndTreeShakeFromDeployJar() throws Exception {
     List<String> additionalProguardConfiguration =
         ImmutableList.of(
-            ToolHelper.PROGUARD_SETTINGS_FOR_INTERNAL_APPS + "/GmsCore_proguard.config");
+            ToolHelper.PROGUARD_SETTINGS_FOR_INTERNAL_APPS + "GmsCore_proguard.config");
     AndroidApp app1 =
         buildAndTreeShakeFromDeployJar(
             CompilationMode.RELEASE,
diff --git a/src/test/java/com/android/tools/r8/internal/YouTubeCompilationBase.java b/src/test/java/com/android/tools/r8/internal/YouTubeCompilationBase.java
index 4ffbe2b..92b7c68 100644
--- a/src/test/java/com/android/tools/r8/internal/YouTubeCompilationBase.java
+++ b/src/test/java/com/android/tools/r8/internal/YouTubeCompilationBase.java
@@ -28,13 +28,13 @@
     this.base = "third_party/youtube/youtube.android_" + majorVersion + "." + minorVersion + "/";
   }
 
-  public List<Path> getKeepRuleFiles() {
+  protected List<Path> getKeepRuleFiles() {
     return ImmutableList.of(
         Paths.get(base).resolve(PG_CONF),
         Paths.get(ToolHelper.PROGUARD_SETTINGS_FOR_INTERNAL_APPS).resolve(PG_CONF));
   }
 
-  public List<Path> getProgramFiles() throws IOException {
+  protected List<Path> getProgramFiles() throws IOException {
     List<Path> result = new ArrayList<>();
     for (Path keepRuleFile : getKeepRuleFiles()) {
       for (String line : FileUtils.readAllLines(keepRuleFile)) {
@@ -47,12 +47,12 @@
     return result;
   }
 
-  public Path getReleaseApk() {
+  Path getReleaseApk() {
     return Paths.get(base).resolve("YouTubeRelease.apk");
   }
 
-  public Path getReleaseProguardMap() {
-    return Paths.get(base).resolve("YouTubeRelease_proguard.map");
+  Path getReleaseProguardMap() {
+    return Paths.get(base).resolve(PG_MAP);
   }
 
   void runR8AndCheckVerification(CompilationMode mode, String input) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerPatternCanBePostponedTest.java b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerPatternCanBePostponedTest.java
new file mode 100644
index 0000000..7813a2d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerPatternCanBePostponedTest.java
@@ -0,0 +1,86 @@
+// 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.analysis.sideeffect;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 SingletonClassInitializerPatternCanBePostponedTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public SingletonClassInitializerPatternCanBePostponedTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(SingletonClassInitializerPatternCanBePostponedTest.class)
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello world!");
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject classSubject = inspector.clazz(A.class);
+    assertThat(classSubject, isPresent());
+
+    // A.inlineable() should be inlined because we should be able to determine that A.<clinit>() can
+    // safely be postponed.
+    assertThat(classSubject.uniqueMethodWithName("inlineable"), not(isPresent()));
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      A.inlineable();
+      System.out.println(A.getInstance().getMessage());
+    }
+  }
+
+  static class A {
+
+    static A INSTANCE = new A(" world!");
+
+    final String message;
+
+    A(String message) {
+      this.message = message;
+    }
+
+    static void inlineable() {
+      System.out.print("Hello");
+    }
+
+    static A getInstance() {
+      return INSTANCE;
+    }
+
+    String getMessage() {
+      return message;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerPatternCannotBePostponedTest.java b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerPatternCannotBePostponedTest.java
new file mode 100644
index 0000000..d29753b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerPatternCannotBePostponedTest.java
@@ -0,0 +1,92 @@
+// 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.analysis.sideeffect;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 SingletonClassInitializerPatternCannotBePostponedTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public SingletonClassInitializerPatternCannotBePostponedTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(SingletonClassInitializerPatternCannotBePostponedTest.class)
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello world!");
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject classSubject = inspector.clazz(A.class);
+    assertThat(classSubject, isPresent());
+
+    // A.inlineable() cannot be inlined because it should trigger the class initialization of A,
+    // which should trigger the class initialization of B, which will print "Hello".
+    assertThat(classSubject.uniqueMethodWithName("inlineable"), isPresent());
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      A.inlineable();
+      System.out.println(A.getInstance().getMessage());
+    }
+  }
+
+  static class A {
+
+    static B INSTANCE = new B("world!");
+
+    static void inlineable() {
+      System.out.print(" ");
+    }
+
+    static B getInstance() {
+      return INSTANCE;
+    }
+  }
+
+  static class B {
+
+    static {
+      System.out.print("Hello");
+    }
+
+    final String message;
+
+    B(String message) {
+      this.message = message;
+    }
+
+    String getMessage() {
+      return message;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/conversion/CallGraphTestBase.java b/src/test/java/com/android/tools/r8/ir/conversion/CallGraphTestBase.java
new file mode 100644
index 0000000..b567014
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/conversion/CallGraphTestBase.java
@@ -0,0 +1,31 @@
+// 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.conversion;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
+import com.android.tools.r8.ir.conversion.CallGraph.Node;
+
+class CallGraphTestBase extends TestBase {
+  private DexItemFactory dexItemFactory = new DexItemFactory();
+
+  Node createNode(String methodName) {
+    DexMethod signature =
+        dexItemFactory.createMethod(
+            dexItemFactory.objectType,
+            dexItemFactory.createProto(dexItemFactory.voidType),
+            methodName);
+    return new Node(
+        new DexEncodedMethod(signature, null, null, ParameterAnnotationsList.empty(), null));
+  }
+
+  Node createForceInlinedNode(String methodName) {
+    Node node = createNode(methodName);
+    node.method.getMutableOptimizationInfo().markForceInline();
+    return node;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/callgraph/CycleEliminationTest.java b/src/test/java/com/android/tools/r8/ir/conversion/CycleEliminationTest.java
similarity index 87%
rename from src/test/java/com/android/tools/r8/ir/callgraph/CycleEliminationTest.java
rename to src/test/java/com/android/tools/r8/ir/conversion/CycleEliminationTest.java
index f767bf2..f38629f 100644
--- a/src/test/java/com/android/tools/r8/ir/callgraph/CycleEliminationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/conversion/CycleEliminationTest.java
@@ -2,7 +2,7 @@
 // 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.callgraph;
+package com.android.tools.r8.ir.conversion;
 
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -11,14 +11,9 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
-import com.android.tools.r8.TestBase;
 import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.ir.conversion.CallGraph.Node;
-import com.android.tools.r8.ir.conversion.CallGraphBuilder.CycleEliminator;
+import com.android.tools.r8.ir.conversion.CallGraphBuilderBase.CycleEliminator;
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
@@ -29,7 +24,7 @@
 import java.util.function.BooleanSupplier;
 import org.junit.Test;
 
-public class CycleEliminationTest extends TestBase {
+public class CycleEliminationTest extends CallGraphTestBase {
 
   private static class Configuration {
 
@@ -44,8 +39,6 @@
     }
   }
 
-  private DexItemFactory dexItemFactory = new DexItemFactory();
-
   @Test
   public void testSimpleCycle() {
     Node method = createNode("n1");
@@ -197,20 +190,4 @@
       }
     }
   }
-
-  private Node createNode(String methodName) {
-    DexMethod signature =
-        dexItemFactory.createMethod(
-            dexItemFactory.objectType,
-            dexItemFactory.createProto(dexItemFactory.voidType),
-            methodName);
-    return new Node(
-        new DexEncodedMethod(signature, null, null, ParameterAnnotationsList.empty(), null));
-  }
-
-  private Node createForceInlinedNode(String methodName) {
-    Node node = createNode(methodName);
-    node.method.getMutableOptimizationInfo().markForceInline();
-    return node;
-  }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/conversion/NodeExtractionTest.java b/src/test/java/com/android/tools/r8/ir/conversion/NodeExtractionTest.java
new file mode 100644
index 0000000..2cde812
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/conversion/NodeExtractionTest.java
@@ -0,0 +1,223 @@
+// 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.conversion;
+
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.ir.conversion.CallGraph.Node;
+import com.android.tools.r8.ir.conversion.CallGraphBuilderBase.CycleEliminator;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.Sets;
+import java.util.Set;
+import java.util.TreeSet;
+import org.junit.Test;
+
+public class NodeExtractionTest extends CallGraphTestBase {
+
+  private InternalOptions options = new InternalOptions();
+
+  // Note that building a test graph is intentionally repeated to avoid race conditions and/or
+  // non-deterministic test results due to cycle elimination.
+
+  @Test
+  public void testExtractLeaves_withoutCycle() {
+    Node n1, n2, n3, n4, n5, n6;
+    Set<Node> nodes;
+
+    n1 = createNode("n1");
+    n2 = createNode("n2");
+    n3 = createNode("n3");
+    n4 = createNode("n4");
+    n5 = createNode("n5");
+    n6 = createNode("n6");
+
+    n2.addCallerConcurrently(n1);
+    n3.addCallerConcurrently(n2);
+    n4.addCallerConcurrently(n2);
+    n4.addCallerConcurrently(n5);
+    n6.addCallerConcurrently(n5);
+
+    nodes = new TreeSet<>();
+    nodes.add(n1);
+    nodes.add(n2);
+    nodes.add(n3);
+    nodes.add(n4);
+    nodes.add(n5);
+    nodes.add(n6);
+
+    Set<Node> wave = Sets.newIdentityHashSet();
+
+    MethodProcessor.extractLeaves(nodes, wave::add);
+    assertEquals(3, wave.size());
+    assertThat(wave, hasItem(n3));
+    assertThat(wave, hasItem(n4));
+    assertThat(wave, hasItem(n6));
+    wave.clear();
+
+    MethodProcessor.extractLeaves(nodes, wave::add);
+    assertEquals(2, wave.size());
+    assertThat(wave, hasItem(n2));
+    assertThat(wave, hasItem(n5));
+    wave.clear();
+
+    MethodProcessor.extractLeaves(nodes, wave::add);
+    assertEquals(1, wave.size());
+    assertThat(wave, hasItem(n1));
+    assertTrue(nodes.isEmpty());
+  }
+
+  @Test
+  public void testExtractLeaves_withCycle() {
+    Node n1, n2, n3, n4, n5, n6;
+    Set<Node> nodes;
+
+    n1 = createNode("n1");
+    n2 = createNode("n2");
+    n3 = createNode("n3");
+    n4 = createNode("n4");
+    n5 = createNode("n5");
+    n6 = createNode("n6");
+
+    n2.addCallerConcurrently(n1);
+    n3.addCallerConcurrently(n2);
+    n4.addCallerConcurrently(n2);
+    n4.addCallerConcurrently(n5);
+    n6.addCallerConcurrently(n5);
+
+    nodes = new TreeSet<>();
+    nodes.add(n1);
+    nodes.add(n2);
+    nodes.add(n3);
+    nodes.add(n4);
+    nodes.add(n5);
+    nodes.add(n6);
+
+    n1.addCallerConcurrently(n3);
+    n3.method.getMutableOptimizationInfo().markForceInline();
+    CycleEliminator cycleEliminator = new CycleEliminator(nodes, options);
+    assertEquals(1, cycleEliminator.breakCycles().numberOfRemovedEdges());
+
+    Set<Node> wave = Sets.newIdentityHashSet();
+
+    MethodProcessor.extractLeaves(nodes, wave::add);
+    assertEquals(3, wave.size());
+    assertThat(wave, hasItem(n3));
+    assertThat(wave, hasItem(n4));
+    assertThat(wave, hasItem(n6));
+    wave.clear();
+
+    MethodProcessor.extractLeaves(nodes, wave::add);
+    assertEquals(2, wave.size());
+    assertThat(wave, hasItem(n2));
+    assertThat(wave, hasItem(n5));
+    wave.clear();
+
+    MethodProcessor.extractLeaves(nodes, wave::add);
+    assertEquals(1, wave.size());
+    assertThat(wave, hasItem(n1));
+    assertTrue(nodes.isEmpty());
+  }
+
+  @Test
+  public void testExtractRoots_withoutCycle() {
+    Node n1, n2, n3, n4, n5, n6;
+    Set<Node> nodes;
+
+    n1 = createNode("n1");
+    n2 = createNode("n2");
+    n3 = createNode("n3");
+    n4 = createNode("n4");
+    n5 = createNode("n5");
+    n6 = createNode("n6");
+
+    n2.addCallerConcurrently(n1);
+    n3.addCallerConcurrently(n2);
+    n4.addCallerConcurrently(n2);
+    n4.addCallerConcurrently(n5);
+    n6.addCallerConcurrently(n5);
+
+    nodes = new TreeSet<>();
+    nodes.add(n1);
+    nodes.add(n2);
+    nodes.add(n3);
+    nodes.add(n4);
+    nodes.add(n5);
+    nodes.add(n6);
+
+    Set<Node> wave = Sets.newIdentityHashSet();
+
+    PostMethodProcessor.extractRoots(nodes, wave::add);
+    assertEquals(2, wave.size());
+    assertThat(wave, hasItem(n1));
+    assertThat(wave, hasItem(n5));
+    wave.clear();
+
+    PostMethodProcessor.extractRoots(nodes, wave::add);
+    assertEquals(2, wave.size());
+    assertThat(wave, hasItem(n2));
+    assertThat(wave, hasItem(n6));
+    wave.clear();
+
+    PostMethodProcessor.extractRoots(nodes, wave::add);
+    assertEquals(2, wave.size());
+    assertThat(wave, hasItem(n3));
+    assertThat(wave, hasItem(n4));
+    assertTrue(nodes.isEmpty());
+  }
+
+  @Test
+  public void testExtractRoots_withCycle() {
+    Node n1, n2, n3, n4, n5, n6;
+    Set<Node> nodes;
+
+    n1 = createNode("n1");
+    n2 = createNode("n2");
+    n3 = createNode("n3");
+    n4 = createNode("n4");
+    n5 = createNode("n5");
+    n6 = createNode("n6");
+
+    n2.addCallerConcurrently(n1);
+    n3.addCallerConcurrently(n2);
+    n4.addCallerConcurrently(n2);
+    n4.addCallerConcurrently(n5);
+    n6.addCallerConcurrently(n5);
+
+    nodes = new TreeSet<>();
+    nodes.add(n1);
+    nodes.add(n2);
+    nodes.add(n3);
+    nodes.add(n4);
+    nodes.add(n5);
+    nodes.add(n6);
+
+    n1.addCallerConcurrently(n3);
+    n3.method.getMutableOptimizationInfo().markForceInline();
+    CycleEliminator cycleEliminator = new CycleEliminator(nodes, options);
+    assertEquals(1, cycleEliminator.breakCycles().numberOfRemovedEdges());
+
+    Set<Node> wave = Sets.newIdentityHashSet();
+
+    PostMethodProcessor.extractRoots(nodes, wave::add);
+    assertEquals(2, wave.size());
+    assertThat(wave, hasItem(n1));
+    assertThat(wave, hasItem(n5));
+    wave.clear();
+
+    PostMethodProcessor.extractRoots(nodes, wave::add);
+    assertEquals(2, wave.size());
+    assertThat(wave, hasItem(n2));
+    assertThat(wave, hasItem(n6));
+    wave.clear();
+
+    PostMethodProcessor.extractRoots(nodes, wave::add);
+    assertEquals(2, wave.size());
+    assertThat(wave, hasItem(n3));
+    assertThat(wave, hasItem(n4));
+    assertTrue(nodes.isEmpty());
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/conversion/PartialCallGraphTest.java b/src/test/java/com/android/tools/r8/ir/conversion/PartialCallGraphTest.java
new file mode 100644
index 0000000..769515c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/conversion/PartialCallGraphTest.java
@@ -0,0 +1,198 @@
+// 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.conversion;
+
+import static com.android.tools.r8.shaking.ProguardConfigurationSourceStrings.createConfigurationForTesting;
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.dex.ApplicationReader;
+import com.android.tools.r8.graph.AppInfoWithSubtyping;
+import com.android.tools.r8.graph.AppServices;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.ir.conversion.CallGraph.Node;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.Enqueuer;
+import com.android.tools.r8.shaking.EnqueuerFactory;
+import com.android.tools.r8.shaking.ProguardConfigurationParser;
+import com.android.tools.r8.shaking.RootSetBuilder;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import java.util.Set;
+import java.util.concurrent.ExecutorService;
+import org.junit.Test;
+
+public class PartialCallGraphTest extends CallGraphTestBase {
+  private final AppView<AppInfoWithLiveness> appView;
+  private final InternalOptions options = new InternalOptions();
+  private final ExecutorService executorService = ThreadUtils.getExecutorService(options);
+
+  public PartialCallGraphTest() throws Exception {
+    Timing timing = new Timing("PartialCallGraphTest.setup");
+    AndroidApp app = testForD8().addProgramClasses(TestClass.class).compile().app;
+    DexApplication application = new ApplicationReader(app, options, timing).read().toDirect();
+    AppView<AppInfoWithSubtyping> appView =
+        AppView.createForR8(new AppInfoWithSubtyping(application), options);
+    appView.setAppServices(AppServices.builder(appView).build());
+    ProguardConfigurationParser parser =
+        new ProguardConfigurationParser(appView.dexItemFactory(), options.reporter);
+    parser.parse(
+        createConfigurationForTesting(
+            ImmutableList.of("-keep class ** { void m1(); void m5(); }")));
+    appView.setRootSet(
+        new RootSetBuilder(
+            appView, application, parser.getConfig().getRules()).run(executorService));
+    Enqueuer enqueuer = EnqueuerFactory.createForInitialTreeShaking(appView);
+    this.appView =
+        appView.setAppInfo(
+            enqueuer.traceApplication(
+                appView.rootSet(),
+                parser.getConfig().getDontWarnPatterns(),
+                executorService,
+                timing));
+  }
+
+  @Test
+  public void testFullGraph() throws Exception {
+    CallGraph cg =
+        new CallGraphBuilder(appView).build(executorService, new Timing("testFullGraph"));
+    Node m1 = findNode(cg.nodes, "m1");
+    Node m2 = findNode(cg.nodes, "m2");
+    Node m3 = findNode(cg.nodes, "m3");
+    Node m4 = findNode(cg.nodes, "m4");
+    Node m5 = findNode(cg.nodes, "m5");
+    Node m6 = findNode(cg.nodes, "m6");
+    assertNotNull(m1);
+    assertNotNull(m2);
+    assertNotNull(m3);
+    assertNotNull(m4);
+    assertNotNull(m5);
+    assertNotNull(m6);
+
+    Set<Node> wave = Sets.newIdentityHashSet();
+
+    MethodProcessor.extractLeaves(cg.nodes, wave::add);
+    assertEquals(4, wave.size()); // including <init>
+    assertThat(wave, hasItem(m3));
+    assertThat(wave, hasItem(m4));
+    assertThat(wave, hasItem(m6));
+    wave.clear();
+
+    MethodProcessor.extractLeaves(cg.nodes, wave::add);
+    assertEquals(2, wave.size());
+    assertThat(wave, hasItem(m2));
+    assertThat(wave, hasItem(m5));
+    wave.clear();
+
+    MethodProcessor.extractLeaves(cg.nodes, wave::add);
+    assertEquals(1, wave.size());
+    assertThat(wave, hasItem(m1));
+    assertTrue(cg.nodes.isEmpty());
+  }
+
+  @Test
+  public void testPartialGraph() throws Exception {
+    DexEncodedMethod em1 = findMethod("m1");
+    DexEncodedMethod em2 = findMethod("m2");
+    DexEncodedMethod em4 = findMethod("m4");
+    DexEncodedMethod em5 = findMethod("m5");
+    assertNotNull(em1);
+    assertNotNull(em2);
+    assertNotNull(em4);
+    assertNotNull(em5);
+
+    CallGraph pg =
+        new PartialCallGraphBuilder(appView, ImmutableSet.of(em1, em2, em4, em5))
+            .build(executorService, new Timing("tetPartialGraph"));
+
+    Node m1 = findNode(pg.nodes, "m1");
+    Node m2 = findNode(pg.nodes, "m2");
+    Node m4 = findNode(pg.nodes, "m4");
+    Node m5 = findNode(pg.nodes, "m5");
+    assertNotNull(m1);
+    assertNotNull(m2);
+    assertNotNull(m4);
+    assertNotNull(m5);
+
+    Set<Node> wave = Sets.newIdentityHashSet();
+
+    PostMethodProcessor.extractRoots(pg.nodes, wave::add);
+    assertEquals(2, wave.size());
+    assertThat(wave, hasItem(m1));
+    assertThat(wave, hasItem(m5));
+    wave.clear();
+
+    PostMethodProcessor.extractRoots(pg.nodes, wave::add);
+    assertEquals(1, wave.size());
+    assertThat(wave, hasItem(m2));
+    wave.clear();
+
+    PostMethodProcessor.extractRoots(pg.nodes, wave::add);
+    assertEquals(1, wave.size());
+    assertThat(wave, hasItem(m4));
+    assertTrue(pg.nodes.isEmpty());
+  }
+
+  private Node findNode(Iterable<Node> nodes, String name) {
+    for (Node n : nodes) {
+      if (n.method.method.name.toString().equals(name)) {
+        return n;
+      }
+    }
+    return null;
+  }
+
+  private DexEncodedMethod findMethod(String name) {
+    for (DexClass clazz : appView.appInfo().classes()) {
+      for (DexEncodedMethod method : clazz.methods()) {
+        if (method.method.name.toString().equals(name)) {
+          return method;
+        }
+      }
+    }
+    return null;
+  }
+
+  static class TestClass {
+    void m1() {
+      System.out.println("m1");
+      m2();
+    }
+
+    void m2() {
+      System.out.println("m2");
+      m3();
+      m4();
+    }
+
+    void m3() {
+      System.out.println("m3");
+    }
+
+    void m4() {
+      System.out.println("m4");
+    }
+
+    void m5() {
+      System.out.println("m5");
+      m6();
+      m4();
+    }
+
+    void m6() {
+      System.out.println("m6");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
index 86245a3..5e0e77f 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
@@ -121,6 +121,9 @@
           o.enableInlining = inlining;
           o.enableInliningOfInvokesWithNullableReceivers = false;
           o.inliningInstructionLimit = 6;
+          // Tests depend on nullability of receiver and argument in general. Learning very accurate
+          // nullability from actual usage in tests bothers what we want to test.
+          o.enableCallSiteOptimizationInfoPropagation = false;
         });
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/checkcast/TrivialTypeTestsAfterBranchPruningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/checkcast/TrivialTypeTestsAfterBranchPruningTest.java
new file mode 100644
index 0000000..a1577a3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/checkcast/TrivialTypeTestsAfterBranchPruningTest.java
@@ -0,0 +1,103 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.checkcast;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class TrivialTypeTestsAfterBranchPruningTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public TrivialTypeTestsAfterBranchPruningTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(TrivialTypeTestsAfterBranchPruningTest.class)
+        .addKeepMainRule(TestClass.class)
+        .enableInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("Hello world!");
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject classSubject = inspector.clazz(TestClass.class);
+    assertThat(classSubject, isPresent());
+    assertThat(classSubject.uniqueMethodWithName("dead"), not(isPresent()));
+
+    MethodSubject trivialCastMethodSubject =
+        classSubject.uniqueMethodWithName("trivialCastAfterBranchPruningTest");
+    assertThat(trivialCastMethodSubject, isPresent());
+    assertTrue(
+        trivialCastMethodSubject.streamInstructions().noneMatch(InstructionSubject::isCheckCast));
+
+    MethodSubject branchPruningMethodSubject =
+        classSubject.uniqueMethodWithName("branchPruningAfterInstanceOfOptimization");
+    assertThat(branchPruningMethodSubject, isPresent());
+    assertTrue(
+        branchPruningMethodSubject
+            .streamInstructions()
+            .noneMatch(InstructionSubject::isInstanceOf));
+  }
+
+  static class TestClass {
+
+    static boolean alwaysTrue = true;
+
+    public static void main(String[] args) {
+      branchPruningAfterInstanceOfOptimization(trivialCastAfterBranchPruningTest());
+    }
+
+    @NeverInline
+    static A trivialCastAfterBranchPruningTest() {
+      return (A) (alwaysTrue ? new A() : new B());
+    }
+
+    @NeverInline
+    static void branchPruningAfterInstanceOfOptimization(A obj) {
+      if (obj instanceof A) {
+        System.out.println("Hello world!");
+      } else {
+        dead();
+      }
+    }
+
+    @NeverInline
+    static void dead() {
+      throw new RuntimeException();
+    }
+  }
+
+  static class A {}
+
+  static class B {}
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
index fbcc6a1..afdfabb 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
@@ -64,7 +64,8 @@
 
 @RunWith(Parameterized.class)
 public class ClassInlinerTest extends TestBase {
-  private Backend backend;
+
+  private final Backend backend;
 
   @Parameterized.Parameters(name = "Backend: {0}")
   public static Backend[] data() {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/EscapingBuilderTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/EscapingBuilderTest.java
index 4dd3551..bf5e92a 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/EscapingBuilderTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/EscapingBuilderTest.java
@@ -74,7 +74,11 @@
     }
 
     @NeverInline
-    public static void escape(Object o) {}
+    static void escape(Object o) {
+      if (System.currentTimeMillis() < 0) {
+        System.out.println(o);
+      }
+    }
   }
 
   // Simple builder that should be class inlined.
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/ifs/EnumComparisonTest.java b/src/test/java/com/android/tools/r8/ir/optimize/ifs/EnumComparisonTest.java
new file mode 100644
index 0000000..94d775b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/ifs/EnumComparisonTest.java
@@ -0,0 +1,78 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.ifs;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class EnumComparisonTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public EnumComparisonTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(EnumComparisonTest.class)
+        .addKeepMainRule(TestClass.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("MyEnum.A == MyEnum.A", "MyEnum.A != MyEnum.B");
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject classSubject = inspector.clazz(TestClass.class);
+    assertThat(classSubject, isPresent());
+
+    MethodSubject mainMethodSubject = classSubject.mainMethod();
+    assertThat(mainMethodSubject, isPresent());
+    assertTrue(mainMethodSubject.streamInstructions().noneMatch(InstructionSubject::isThrow));
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      if (MyEnum.A == MyEnum.A) {
+        System.out.println("MyEnum.A == MyEnum.A");
+      } else {
+        throw new RuntimeException();
+      }
+      if (MyEnum.A == MyEnum.B) {
+        throw new RuntimeException();
+      } else {
+        System.out.println("MyEnum.A != MyEnum.B");
+      }
+    }
+  }
+
+  enum MyEnum {
+    A,
+    B
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java
index e8308f7..5f1b7a5 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SingleTargetAfterInliningTest.java
@@ -4,19 +4,19 @@
 
 package com.android.tools.r8.ir.optimize.inliner;
 
-import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethod;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.AlwaysInline;
 import com.android.tools.r8.AssumeMayHaveSideEffects;
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -25,14 +25,17 @@
 @RunWith(Parameterized.class)
 public class SingleTargetAfterInliningTest extends TestBase {
 
+  private final int maxInliningDepth;
   private final TestParameters parameters;
 
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().build();
+  @Parameters(name = "{1}, max inlining depth: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        ImmutableList.of(0, 1), getTestParameters().withAllRuntimesAndApiLevels().build());
   }
 
-  public SingleTargetAfterInliningTest(TestParameters parameters) {
+  public SingleTargetAfterInliningTest(int maxInliningDepth, TestParameters parameters) {
+    this.maxInliningDepth = maxInliningDepth;
     this.parameters = parameters;
   }
 
@@ -41,31 +44,41 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(SingleTargetAfterInliningTest.class)
         .addKeepMainRule(TestClass.class)
+        .addOptionsModification(
+            options -> {
+              options.applyInliningToInlinee = true;
+              options.applyInliningToInlineeMaxDepth = maxInliningDepth;
+            })
+        .enableAlwaysInliningAnnotations()
         .enableClassInliningAnnotations()
         .enableSideEffectAnnotations()
-        .setMinApi(parameters.getRuntime())
-        .noMinification()
+        .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(this::inspect)
         .run(parameters.getRuntime(), TestClass.class)
-        .assertSuccessWithOutputLines("B");
+        .assertSuccessWithOutputLines("B.foo()", "B.bar()");
   }
 
   private void inspect(CodeInspector inspector) {
-    ClassSubject aClassSubject = inspector.clazz(A.class);
-    assertThat(aClassSubject, isPresent());
-
     ClassSubject testClassSubject = inspector.clazz(TestClass.class);
     assertThat(testClassSubject, isPresent());
 
     // The indirection() method should be inlined.
     assertThat(testClassSubject.uniqueMethodWithName("indirection"), not(isPresent()));
 
-    // The main() method invokes A.method().
-    // TODO(b/141451716): A.method() should be inlined into main().
-    MethodSubject mainMethodSubject = testClassSubject.mainMethod();
-    assertThat(mainMethodSubject, isPresent());
-    assertThat(mainMethodSubject, invokesMethod(aClassSubject.uniqueMethodWithName("method")));
+    // A.foo() should be absent if the max inlining depth is 1, because indirection() has been
+    // inlined into main(), which makes A.foo() eligible for inlining into main().
+    ClassSubject aClassSubject = inspector.clazz(A.class);
+    assertThat(aClassSubject, isPresent());
+    if (maxInliningDepth == 0) {
+      assertThat(aClassSubject.uniqueMethodWithName("foo"), isPresent());
+    } else {
+      assert maxInliningDepth == 1;
+      assertThat(aClassSubject.uniqueMethodWithName("foo"), not(isPresent()));
+    }
+
+    // A.bar() should always be inlined because it is marked as @AlwaysInline.
+    assertThat(aClassSubject.uniqueMethodWithName("bar"), not(isPresent()));
   }
 
   static class TestClass {
@@ -78,13 +91,16 @@
     }
 
     private static void indirection(A obj) {
-      obj.method();
+      obj.foo();
+      obj.bar();
     }
   }
 
   abstract static class A {
 
-    abstract void method();
+    abstract void foo();
+
+    abstract void bar();
   }
 
   @NeverClassInline
@@ -94,8 +110,14 @@
     B() {}
 
     @Override
-    void method() {
-      System.out.println("B");
+    void foo() {
+      System.out.println("B.foo()");
+    }
+
+    @AlwaysInline
+    @Override
+    void bar() {
+      System.out.println("B.bar()");
     }
   }
 
@@ -106,8 +128,13 @@
     C() {}
 
     @Override
-    void method() {
-      System.out.println("C");
+    void foo() {
+      System.out.println("C.foo()");
+    }
+
+    @Override
+    void bar() {
+      System.out.println("C.bar()");
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
index b0828fe..f0c6a78 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
@@ -167,6 +167,7 @@
         testForR8(parameters.getBackend())
             .addProgramClasses(classes)
             .enableInliningAnnotations()
+            .enableSideEffectAnnotations()
             .addKeepMainRule(main)
             .allowAccessModification()
             .noMinification()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostFieldOnlyTestClass.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostFieldOnlyTestClass.java
index f725d21..eb8e6a4 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostFieldOnlyTestClass.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/MoveToHostFieldOnlyTestClass.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.optimize.staticizer.movetohost;
 
+import com.android.tools.r8.AssumeMayHaveSideEffects;
 import com.android.tools.r8.NeverInline;
 
 public class MoveToHostFieldOnlyTestClass {
@@ -12,6 +13,7 @@
     test.testOk_fieldOnly();
   }
 
+  @AssumeMayHaveSideEffects
   @NeverInline
   private void testOk_fieldOnly() {
     // Any instance method call whose target holder is not the candidate will invalidate candidacy,
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceRemovalTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceRemovalTest.java
new file mode 100644
index 0000000..d2fa3f8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceRemovalTest.java
@@ -0,0 +1,119 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.unusedinterfaces;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 UnusedInterfaceRemovalTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public UnusedInterfaceRemovalTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(UnusedInterfaceRemovalTest.class)
+        .addKeepMainRule(TestClass.class)
+        .enableMergeAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("A.foo()", "A.bar()");
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject iClassSubject = inspector.clazz(I.class);
+    assertThat(iClassSubject, isPresent());
+
+    ClassSubject jClassSubject = inspector.clazz(J.class);
+    assertThat(jClassSubject, isPresent());
+
+    ClassSubject kClassSubject = inspector.clazz(K.class);
+    assertThat(kClassSubject, not(isPresent()));
+
+    ClassSubject aClassSubject = inspector.clazz(A.class);
+    assertThat(aClassSubject, isPresent());
+    assertEquals(2, aClassSubject.getDexClass().interfaces.size());
+    assertEquals(
+        aClassSubject.getDexClass().interfaces.values[0], iClassSubject.getDexClass().type);
+    assertEquals(
+        aClassSubject.getDexClass().interfaces.values[1], jClassSubject.getDexClass().type);
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      I obj = System.currentTimeMillis() >= 0 ? new A() : new B();
+      obj.foo();
+      ((J) obj).bar();
+    }
+  }
+
+  @NeverMerge
+  interface I {
+
+    void foo();
+  }
+
+  @NeverMerge
+  interface J {
+
+    void bar();
+  }
+
+  interface K extends I, J {}
+
+  static class A implements K {
+
+    @Override
+    public void foo() {
+      System.out.println("A.foo()");
+    }
+
+    @Override
+    public void bar() {
+      System.out.println("A.bar()");
+    }
+  }
+
+  // To prevent that we detect a single target and start inlining or rewriting the signature in the
+  // invoke.
+  static class B implements K {
+
+    @Override
+    public void foo() {
+      throw new RuntimeException();
+    }
+
+    @Override
+    public void bar() {
+      throw new RuntimeException();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceWithDefaultMethodTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceWithDefaultMethodTest.java
new file mode 100644
index 0000000..90761db
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceWithDefaultMethodTest.java
@@ -0,0 +1,97 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.unusedinterfaces;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 UnusedInterfaceWithDefaultMethodTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public UnusedInterfaceWithDefaultMethodTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(UnusedInterfaceWithDefaultMethodTest.class)
+        .addKeepMainRule(TestClass.class)
+        .enableInliningAnnotations()
+        .enableMergeAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("J.m()");
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject iClassSubject = inspector.clazz(I.class);
+    assertThat(iClassSubject, isPresent());
+
+    ClassSubject jClassSubject = inspector.clazz(J.class);
+    assertThat(jClassSubject, isPresent());
+
+    ClassSubject aClassSubject = inspector.clazz(A.class);
+    assertThat(aClassSubject, isPresent());
+
+    // Verify that J is not considered an unused interface, since it provides an implementation of
+    // m() that happens to be used.
+    assertEquals(1, aClassSubject.getDexClass().interfaces.size());
+    assertEquals(
+        jClassSubject.getDexClass().type, aClassSubject.getDexClass().interfaces.values[0]);
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      indirection(new A());
+    }
+
+    @NeverInline
+    private static void indirection(I obj) {
+      obj.m();
+    }
+  }
+
+  @NeverMerge
+  interface I {
+
+    void m();
+  }
+
+  @NeverMerge
+  interface J extends I {
+
+    @NeverInline
+    @Override
+    default void m() {
+      System.out.println("J.m()");
+    }
+  }
+
+  static class A implements J {}
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
index f0a8a84..cfeb611 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
@@ -291,30 +291,18 @@
   @Test
   public void testAccessor() throws Exception {
     TestKotlinCompanionClass testedClass = ACCESSOR_COMPANION_PROPERTY_CLASS;
-    String mainClass = addMainToClasspath("accessors.AccessorKt",
-        "accessor_accessPropertyFromCompanionClass");
+    String mainClass =
+        addMainToClasspath("accessors.AccessorKt", "accessor_accessPropertyFromCompanionClass");
     runTest(
         "accessors",
         mainClass,
         disableClassStaticizer,
-        (app) -> {
+        app -> {
+          // The classes are removed entirely as a result of member value propagation, inlining, and
+          // the fact that the classes do not have observable side effects.
           CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject outerClass =
-              checkClassIsKept(codeInspector, testedClass.getOuterClassName());
-          ClassSubject companionClass = checkClassIsKept(codeInspector, testedClass.getClassName());
-
-          // Property field has been removed due to member value propagation.
-          String propertyName = "property";
-          checkFieldIsRemoved(outerClass, JAVA_LANG_STRING, propertyName);
-
-          // The getter is always inlined since it just calls into the accessor.
-          MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
-          checkMethodIsAbsent(companionClass, getter);
-
-          // The accessor is also inlined.
-          MemberNaming.MethodSignature getterAccessor =
-              testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_COMPANION);
-          checkMethodIsRemoved(outerClass, getterAccessor);
+          checkClassIsRemoved(codeInspector, testedClass.getOuterClassName());
+          checkClassIsRemoved(codeInspector, testedClass.getClassName());
         });
   }
 
diff --git a/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingInterfaceClInitTest.java b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingInterfaceClInitTest.java
new file mode 100644
index 0000000..cb4bed0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/applymapping/ApplyMappingInterfaceClInitTest.java
@@ -0,0 +1,104 @@
+// 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.applymapping;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.io.IOException;
+import java.util.concurrent.ExecutionException;
+import org.junit.Ignore;
+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 ApplyMappingInterfaceClInitTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ApplyMappingInterfaceClInitTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  @Ignore("b/142909857")
+  public void testNotRenamingClInitIfNotInMap()
+      throws ExecutionException, CompilationFailedException, IOException {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(ApplyMappingInterfaceClInitTest.class)
+        .addKeepMainRule(Main.class)
+        .addKeepClassAndMembersRules(TestInterface.class)
+        .addApplyMapping("")
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), Main.class)
+        .inspect(this::verifyNoRenamingOfClInit);
+  }
+
+  @Test
+  @Ignore("b/142909857")
+  public void testNotRenamingClInitIfInMap()
+      throws ExecutionException, CompilationFailedException, IOException {
+    String interfaceName = TestInterface.class.getTypeName();
+    testForR8(parameters.getBackend())
+        .addInnerClasses(ApplyMappingInterfaceClInitTest.class)
+        .addKeepMainRule(Main.class)
+        .addKeepClassAndMembersRules(TestInterface.class)
+        .addApplyMapping(
+            StringUtils.lines(
+                interfaceName + " -> " + interfaceName + ":", "    void <clinit>() -> a"))
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), Main.class)
+        .inspect(this::verifyNoRenamingOfClInit);
+  }
+
+  private void verifyNoRenamingOfClInit(CodeInspector inspector) {
+    ClassSubject interfaceSubject = inspector.clazz(TestInterface.class);
+    assertThat(interfaceSubject, isPresent());
+    interfaceSubject.allMethods().stream()
+        .allMatch(
+            method -> {
+              boolean classInitNotRenamed = !method.isClassInitializer() || !method.isRenamed();
+              assertTrue(classInitNotRenamed);
+              return classInitNotRenamed;
+            });
+  }
+
+  public interface TestInterface {
+    Throwable t = new Throwable();
+
+    void foo();
+  }
+
+  @NeverClassInline
+  public static class Main implements TestInterface {
+
+    public static void main(String[] args) {
+      new Main().foo();
+    }
+
+    @Override
+    @NeverInline
+    public void foo() {
+      System.out.println("Hello World!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/annotations/AnnotationShakingBehaviorTest.java b/src/test/java/com/android/tools/r8/shaking/annotations/AnnotationShakingBehaviorTest.java
new file mode 100644
index 0000000..484fb10
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/annotations/AnnotationShakingBehaviorTest.java
@@ -0,0 +1,159 @@
+// Copyright (c) 2018, 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.annotations;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+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 AnnotationShakingBehaviorTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public AnnotationShakingBehaviorTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testKeepUnusedTypeInAnnotation()
+      throws IOException, CompilationFailedException, ExecutionException {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Factory.class, Main.class, C.class)
+        .addKeepMainRule(Main.class)
+        .addKeepClassAndMembersRules(Factory.class)
+        .addKeepAttributes("*Annotation*")
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello World!")
+        .inspect(inspector -> assertThat(inspector.clazz(C.class), isPresent()));
+  }
+
+  @Test
+  public void testWillKeepAnnotatedProgramClass()
+      throws IOException, CompilationFailedException, ExecutionException {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Factory.class, MainWithMethodAnnotation.class, C.class)
+        .addKeepMainRule(MainWithMethodAnnotation.class)
+        .addKeepClassAndMembersRules(Factory.class)
+        .addKeepAttributes("*Annotation*")
+        .enableInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), MainWithMethodAnnotation.class)
+        .assertSuccessWithOutputLines("Hello World!")
+        .inspect(inspector -> assertThat(inspector.clazz(C.class), isPresent()));
+  }
+
+  @Test
+  public void testGenericSignatureDoNotKeepType()
+      throws IOException, CompilationFailedException, ExecutionException {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(MainWithGenericC.class, C.class)
+        .addKeepMainRule(MainWithGenericC.class)
+        .addKeepAttributes("Signature", "InnerClasses", "EnclosingMethod")
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), MainWithGenericC.class)
+        .assertSuccessWithOutputLines("Hello World!")
+        .inspect(
+            inspector -> {
+              assertThat(inspector.clazz(C.class), not(isPresent()));
+            });
+  }
+
+  @Test
+  public void testDisappearsWhenMerging()
+      throws IOException, CompilationFailedException, ExecutionException {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Factory.class, MainWithNewB.class, A.class, B.class, C.class)
+        .addKeepMainRule(MainWithNewB.class)
+        .addKeepClassAndMembersRules(Factory.class)
+        .addKeepAttributes("*Annotation*")
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), MainWithNewB.class)
+        .assertSuccessWithOutputLines("Hello World!")
+        .inspect(
+            inspector -> {
+              assertThat(inspector.clazz(C.class), not(isPresent()));
+            });
+  }
+
+  @Retention(value = RetentionPolicy.RUNTIME)
+  public @interface Factory {
+    Class<?> ref() default Object.class;
+  }
+
+  public static class C {}
+
+  @Factory(ref = C.class) // <-- we are explicitly keeping Main.
+  public static class Main {
+
+    public static void main(String[] args) {
+      System.out.println("Hello World!");
+    }
+  }
+
+  public static class MainWithMethodAnnotation {
+
+    public static void main(String[] args) {
+      test();
+    }
+
+    @Factory(ref = C.class) // <-- We are not explicitly saying that test() should be kept.
+    @NeverInline
+    public static void test() {
+      System.out.println("Hello World!");
+    }
+  }
+
+  public static class MainWithGenericC {
+
+    static List<C> cs = new ArrayList<>();
+
+    public static void main(String[] args) {
+      if (cs.size() == args.length) {
+        System.out.println("Hello World!");
+      }
+    }
+  }
+
+  @Factory(ref = C.class)
+  public static class A {}
+
+  @NeverClassInline
+  public static class B extends A {
+    @NeverInline
+    public void world() {
+      System.out.println("Hello World!");
+    }
+  }
+
+  public static class MainWithNewB {
+
+    public static void main(String[] args) {
+      new B().world();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/annotations/LibraryAndMissingAnnotationsTest.java b/src/test/java/com/android/tools/r8/shaking/annotations/LibraryAndMissingAnnotationsTest.java
new file mode 100644
index 0000000..4e6adad
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/annotations/LibraryAndMissingAnnotationsTest.java
@@ -0,0 +1,181 @@
+// 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.annotations;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.BaseCompilerCommand;
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.TestShrinkerBuilder;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.function.Function;
+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 LibraryAndMissingAnnotationsTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final boolean includeOnLibraryPath;
+
+  @Parameters(name = "{0}, includeOnLibraryPath: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(), BooleanUtils.values());
+  }
+
+  private static Function<TestParameters, Path> compilationResults =
+      memoizeFunction(LibraryAndMissingAnnotationsTest::compileLibraryAnnotationToRuntime);
+
+  private static Path compileLibraryAnnotationToRuntime(TestParameters parameters)
+      throws CompilationFailedException, IOException {
+    return testForR8(getStaticTemp(), parameters.getBackend())
+        .addProgramClasses(LibraryAnnotation.class)
+        .addKeepClassAndMembersRulesWithAllowObfuscation(LibraryAnnotation.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .writeToZip();
+  }
+
+  public LibraryAndMissingAnnotationsTest(TestParameters parameters, boolean includeOnLibraryPath) {
+    this.parameters = parameters;
+    this.includeOnLibraryPath = includeOnLibraryPath;
+  }
+
+  @Test
+  public void testMainWithUseR8() throws Exception {
+    runTest(testForR8(parameters.getBackend()), MainWithUse.class);
+  }
+
+  @Test
+  public void testMainWithUseR8Compat() throws Exception {
+    runTest(testForR8Compat(parameters.getBackend()), MainWithUse.class);
+  }
+
+  @Test
+  public void testMainWithUseProguard() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    runTest(testForProguard(), MainWithUse.class);
+  }
+
+  @Test
+  public void testMainWithoutUseR8() throws Exception {
+    runTest(testForR8(parameters.getBackend()), MainWithoutUse.class);
+  }
+
+  @Test
+  public void testMainWithoutUseR8Compat() throws Exception {
+    runTest(testForR8Compat(parameters.getBackend()), MainWithoutUse.class);
+  }
+
+  @Test
+  public void testMainWithoutUseProguard() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    runTest(testForProguard(), MainWithoutUse.class);
+  }
+
+  private <
+          C extends BaseCompilerCommand,
+          B extends BaseCompilerCommand.Builder<C, B>,
+          CR extends TestCompileResult<CR, RR>,
+          RR extends TestRunResult<RR>,
+          T extends TestShrinkerBuilder<C, B, CR, RR, T>>
+      void runTest(TestShrinkerBuilder<C, B, CR, RR, T> builder, Class<?> mainClass)
+          throws Exception {
+    T t =
+        builder
+            .addProgramClasses(Foo.class, mainClass)
+            .addKeepAttributes("*Annotation*")
+            .addLibraryFiles(runtimeJar(parameters))
+            .addKeepClassAndMembersRules(Foo.class)
+            .addKeepRules("-dontwarn " + LibraryAndMissingAnnotationsTest.class.getTypeName())
+            .addKeepMainRule(mainClass)
+            .setMinApi(parameters.getApiLevel());
+    if (includeOnLibraryPath) {
+      t.addLibraryClasses(LibraryAnnotation.class);
+    } else {
+      t.addKeepRules("-dontwarn " + LibraryAnnotation.class.getTypeName());
+    }
+    t.compile()
+        .addRunClasspathFiles(compilationResults.apply(parameters))
+        .run(parameters.getRuntime(), mainClass)
+        .inspect(
+            inspector -> {
+              ClassSubject clazz = inspector.clazz(Foo.class);
+              assertThat(clazz, isPresent());
+              assertThat(clazz.annotation(LibraryAnnotation.class.getTypeName()), isPresent());
+              MethodSubject foo = clazz.uniqueMethodWithName("foo");
+              assertThat(foo, isPresent());
+              assertThat(foo.annotation(LibraryAnnotation.class.getTypeName()), isPresent());
+              assertFalse(foo.getMethod().parameterAnnotationsList.isEmpty());
+              assertEquals(
+                  LibraryAnnotation.class.getTypeName(),
+                  foo.getMethod()
+                      .parameterAnnotationsList
+                      .get(0)
+                      .annotations[0]
+                      .getAnnotationType()
+                      .toSourceString());
+              assertThat(
+                  clazz
+                      .uniqueFieldWithName("bar")
+                      .annotation(LibraryAnnotation.class.getTypeName()),
+                  isPresent());
+            })
+        .assertSuccessWithOutputLines("Hello World!");
+  }
+
+  @Test
+  public void testMainWithoutUse() {}
+
+  @Retention(RetentionPolicy.RUNTIME)
+  @interface LibraryAnnotation {}
+
+  @LibraryAnnotation
+  public static class Foo {
+
+    @LibraryAnnotation public String bar = "Hello World!";
+
+    @LibraryAnnotation
+    public void foo(@LibraryAnnotation String arg) {
+      System.out.println(arg);
+    }
+  }
+
+  public static class MainWithoutUse {
+
+    public static void main(String[] args) {
+      Foo foo = new Foo();
+      foo.foo(foo.bar);
+    }
+  }
+
+  public static class MainWithUse {
+    public static void main(String[] args) {
+      if (args.length > 0 && args[0].equals(LibraryAnnotation.class.getTypeName())) {
+        System.out.print("This will never be printed");
+      }
+      Foo foo = new Foo();
+      foo.foo(foo.bar);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/annotations/PrunedOrMergedAnnotationTest.java b/src/test/java/com/android/tools/r8/shaking/annotations/PrunedOrMergedAnnotationTest.java
new file mode 100644
index 0000000..45b29f7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/annotations/PrunedOrMergedAnnotationTest.java
@@ -0,0 +1,119 @@
+// Copyright (c) 2018, 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.annotations;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertTrue;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.graph.DexAnnotationSet;
+import com.android.tools.r8.graph.DexEncodedAnnotation;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexValue;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.concurrent.ExecutionException;
+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 PrunedOrMergedAnnotationTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public PrunedOrMergedAnnotationTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testRewritingInFactory()
+      throws IOException, CompilationFailedException, ExecutionException {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(PrunedOrMergedAnnotationTest.class)
+        .addKeepMainRule(Main.class)
+        .addKeepAttributes("*Annotation*")
+        .addKeepClassAndMembersRules(Factory.class)
+        .enableInliningAnnotations()
+        .enableClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello", "World!")
+        .inspect(
+            inspector -> {
+              assertThat(inspector.clazz(A.class), not(isPresent()));
+              DexType mergedType = inspector.clazz(B.class).getDexClass().type;
+              ClassSubject classC = inspector.clazz(C.class);
+              assertThat(classC, isPresent());
+              DexEncodedAnnotation annotation =
+                  classC.annotation(Factory.class.getTypeName()).getAnnotation();
+              assertTrue(valueIsDexType(mergedType, annotation.elements[0].value));
+              assertTrue(
+                  Arrays.stream(annotation.elements[1].value.asDexValueArray().getValues())
+                      .allMatch(value -> valueIsDexType(mergedType, value)));
+              // Check that method parameter annotations are rewritten as well.
+              DexEncodedMethod method = inspector.clazz(Main.class).mainMethod().getMethod();
+              DexAnnotationSet annotationSet = method.parameterAnnotationsList.get(0);
+              DexEncodedAnnotation parameterAnnotation = annotationSet.annotations[0].annotation;
+              assertTrue(valueIsDexType(mergedType, parameterAnnotation.elements[0].value));
+            });
+  }
+
+  private boolean valueIsDexType(DexType type, DexValue value) {
+    assertTrue(value.isDexValueType());
+    assertEquals(type, value.asDexValueType().value);
+    return true;
+  }
+
+  public @interface Factory {
+
+    Class<?> extending() default Object.class;
+
+    Class<?>[] other() default Object[].class;
+  }
+
+  public static class A {}
+
+  @NeverClassInline
+  public static class B extends A {
+    @NeverInline
+    public void world() {
+      System.out.println("World!");
+    }
+  }
+
+  @Factory(
+      extending = A.class,
+      other = {A.class, B.class})
+  public static class C {
+    @NeverInline
+    public static void hello() {
+      System.out.println("Hello");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(@Factory(extending = A.class) String[] args) {
+      C.hello();
+      new B().world();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/librarymethodoverride/LibraryMethodOverrideTest.java b/src/test/java/com/android/tools/r8/shaking/librarymethodoverride/LibraryMethodOverrideTest.java
index ca354f9..a5fbc59 100644
--- a/src/test/java/com/android/tools/r8/shaking/librarymethodoverride/LibraryMethodOverrideTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/librarymethodoverride/LibraryMethodOverrideTest.java
@@ -8,7 +8,6 @@
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
-import com.android.tools.r8.AssumeMayHaveSideEffects;
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverMerge;
 import com.android.tools.r8.TestBase;
@@ -29,7 +28,7 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().build();
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
   public LibraryMethodOverrideTest(TestParameters parameters) {
@@ -44,8 +43,7 @@
         .addOptionsModification(options -> options.enableTreeShakingOfLibraryMethodOverrides = true)
         .enableClassInliningAnnotations()
         .enableMergeAnnotations()
-        .enableSideEffectAnnotations()
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(this::verifyOutput)
         .run(parameters.getRuntime(), TestClass.class)
@@ -68,7 +66,16 @@
     for (Class<?> nonEscapingClass : nonEscapingClasses) {
       ClassSubject classSubject = inspector.clazz(nonEscapingClass);
       assertThat(classSubject, isPresent());
-      assertThat(classSubject.uniqueMethodWithName("toString"), not(isPresent()));
+
+      // TODO(b/142772856): None of the non-escaping classes should have a toString() method. It is
+      //  a requirement that the instance initializers are considered trivial for this to work,
+      //  though, even when they have a side effect (as long as the receiver does not escape via the
+      //  side effecting instruction).
+      if (nonEscapingClass == DoesNotEscapeWithSubThatDoesNotOverrideSub.class) {
+        assertThat(classSubject.uniqueMethodWithName("toString"), not(isPresent()));
+      } else {
+        assertThat(classSubject.uniqueMethodWithName("toString"), isPresent());
+      }
     }
   }
 
@@ -128,8 +135,12 @@
   @NeverClassInline
   static class DoesNotEscape {
 
-    @AssumeMayHaveSideEffects
-    DoesNotEscape() {}
+    // TODO(b/142772856): Should be classified as a trivial instance initializer although it has a
+    //  side effect.
+    DoesNotEscape() {
+      // Side effect to ensure that the constructor is not removed from main().
+      System.out.print("");
+    }
 
     @Override
     public String toString() {
@@ -140,8 +151,12 @@
   @NeverClassInline
   static class DoesNotEscapeWithSubThatDoesNotOverride {
 
-    @AssumeMayHaveSideEffects
-    DoesNotEscapeWithSubThatDoesNotOverride() {}
+    // TODO(b/142772856): Should be classified as a trivial instance initializer although it has a
+    //  side effect.
+    DoesNotEscapeWithSubThatDoesNotOverride() {
+      // Side effect to ensure that the constructor is not removed from main().
+      System.out.print("");
+    }
 
     @Override
     public String toString() {
@@ -153,15 +168,23 @@
   static class DoesNotEscapeWithSubThatDoesNotOverrideSub
       extends DoesNotEscapeWithSubThatDoesNotOverride {
 
-    @AssumeMayHaveSideEffects
-    DoesNotEscapeWithSubThatDoesNotOverrideSub() {}
+    // TODO(b/142772856): Should be classified as a trivial instance initializer although it has a
+    //  side effect.
+    DoesNotEscapeWithSubThatDoesNotOverrideSub() {
+      // Side effect to ensure that the constructor is not removed from main().
+      System.out.print("");
+    }
   }
 
   @NeverClassInline
   static class DoesNotEscapeWithSubThatOverrides {
 
-    @AssumeMayHaveSideEffects
-    DoesNotEscapeWithSubThatOverrides() {}
+    // TODO(b/142772856): Should be classified as a trivial instance initializer although it has a
+    //  side effect.
+    DoesNotEscapeWithSubThatOverrides() {
+      // Side effect to ensure that the constructor is not removed from main().
+      System.out.print("");
+    }
 
     @Override
     public String toString() {
@@ -172,8 +195,12 @@
   @NeverClassInline
   static class DoesNotEscapeWithSubThatOverridesSub extends DoesNotEscapeWithSubThatOverrides {
 
-    @AssumeMayHaveSideEffects
-    DoesNotEscapeWithSubThatOverridesSub() {}
+    // TODO(b/142772856): Should be classified as a trivial instance initializer although it has a
+    //  side effect.
+    DoesNotEscapeWithSubThatOverridesSub() {
+      // Side effect to ensure that the constructor is not removed from main().
+      System.out.print("");
+    }
 
     @Override
     public String toString() {
@@ -188,8 +215,12 @@
   @NeverClassInline
   static class DoesNotEscapeWithSubThatOverridesAndEscapes {
 
-    @AssumeMayHaveSideEffects
-    DoesNotEscapeWithSubThatOverridesAndEscapes() {}
+    // TODO(b/142772856): Should be classified as a trivial instance initializer although it has a
+    //  side effect.
+    DoesNotEscapeWithSubThatOverridesAndEscapes() {
+      // Side effect to ensure that the constructor is not removed from main().
+      System.out.print("");
+    }
 
     @Override
     public String toString() {
@@ -201,8 +232,12 @@
   static class DoesNotEscapeWithSubThatOverridesAndEscapesSub
       extends DoesNotEscapeWithSubThatOverridesAndEscapes {
 
-    @AssumeMayHaveSideEffects
-    DoesNotEscapeWithSubThatOverridesAndEscapesSub() {}
+    // TODO(b/142772856): Should be classified as a trivial instance initializer although it has a
+    //  side effect.
+    DoesNotEscapeWithSubThatOverridesAndEscapesSub() {
+      // Side effect to ensure that the constructor is not removed from main().
+      System.out.print("");
+    }
 
     @Override
     public String toString() {
diff --git a/src/test/r8OnArtBackport/FileAlreadyExistsException.java b/src/test/r8OnArtBackport/FileAlreadyExistsException.java
new file mode 100644
index 0000000..a725932
--- /dev/null
+++ b/src/test/r8OnArtBackport/FileAlreadyExistsException.java
@@ -0,0 +1,13 @@
+// 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 java.nio.file;
+
+import java.io.IOException;
+
+public class FileAlreadyExistsException extends IOException {
+  public FileAlreadyExistsException(String file) {
+    super(file);
+  }
+}
diff --git a/src/test/r8OnArtBackport/Files.java b/src/test/r8OnArtBackport/Files.java
new file mode 100644
index 0000000..99d1956
--- /dev/null
+++ b/src/test/r8OnArtBackport/Files.java
@@ -0,0 +1,50 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package java.nio.file;
+
+import com.android.tools.r8.MockedPath;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+
+public final class Files {
+  private Files() {}
+
+  public static boolean exists(Path path, LinkOption... options) {
+    if (options.length != 0) {
+      throw new RuntimeException("Unsupported in the Files mock.");
+    }
+    return new File(path.toString()).exists();
+  }
+
+  public static boolean isDirectory(Path path, LinkOption... options) {
+    if (options.length != 0) {
+      throw new RuntimeException("Unsupported in the Files mock.");
+    }
+    return new File(path.toString()).isDirectory();
+  }
+
+  public static byte[] readAllBytes(Path path) throws IOException {
+    if (!(path instanceof MockedPath)) {
+      throw new RuntimeException("Unsupported in the Files mock.");
+    }
+    MockedPath mockedPath = (MockedPath) path;
+    return mockedPath.getAllBytes();
+  }
+
+  public static OutputStream newOutputStream(Path path, OpenOption... options) throws IOException {
+    boolean append = false;
+    for (OpenOption option : options) {
+      if (option == StandardOpenOption.APPEND) {
+        append = true;
+      }
+    }
+    if (!(path instanceof MockedPath)) {
+      throw new RuntimeException("Unsupported in the Files mock.");
+    }
+    MockedPath mockedPath = (MockedPath) path;
+    return mockedPath.newOutputStream(append);
+  }
+}
diff --git a/src/test/r8OnArtBackport/LinkOption.java b/src/test/r8OnArtBackport/LinkOption.java
new file mode 100644
index 0000000..3b4b510
--- /dev/null
+++ b/src/test/r8OnArtBackport/LinkOption.java
@@ -0,0 +1,7 @@
+// 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 java.nio.file;
+
+public interface LinkOption {}
diff --git a/src/test/r8OnArtBackport/MockedPath.java b/src/test/r8OnArtBackport/MockedPath.java
new file mode 100644
index 0000000..c5147df
--- /dev/null
+++ b/src/test/r8OnArtBackport/MockedPath.java
@@ -0,0 +1,151 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URI;
+import java.nio.file.FileSystem;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+
+// Mock Path since Path is not present before Dex-8.
+// File is present and it contains most of the utilities.
+public class MockedPath implements Path {
+
+  private MockedPath(File file) {
+    this.wrappedFile = file;
+  }
+
+  public static Path of(File file, String... more) {
+    // Delegate to File separator management.
+    File current = file;
+    for (String s : more) {
+      current = new File(current.getPath(), s);
+    }
+    return new MockedPath(current);
+  }
+
+  public static Path of(String first, String... more) {
+    return of(new File(first), more);
+  }
+
+  // MockedPath wraps the path in a file.
+  private File wrappedFile;
+
+  @Override
+  public FileSystem getFileSystem() {
+    throw new RuntimeException("Mocked Path does not implement getFileSystem");
+  }
+
+  @Override
+  public boolean isAbsolute() {
+    throw new RuntimeException("Mocked Path does not implement isAbsolute");
+  }
+
+  @Override
+  public Path getRoot() {
+    throw new RuntimeException("Mocked Path does not implement getRoot");
+  }
+
+  @Override
+  public Path getFileName() {
+    return of(wrappedFile.getName());
+  }
+
+  @Override
+  public Path getParent() {
+    return of(wrappedFile.getParent());
+  }
+
+  @Override
+  public int getNameCount() {
+    throw new RuntimeException("Mocked Path does not implement getNameCount");
+  }
+
+  @Override
+  public Path getName(int index) {
+    throw new RuntimeException("Mocked Path does not implement getName");
+  }
+
+  @Override
+  public Path subpath(int beginIndex, int endIndex) {
+    throw new RuntimeException("Mocked Path does not implement subpath");
+  }
+
+  @Override
+  public boolean startsWith(Path other) {
+    throw new RuntimeException("Mocked Path does not implement startsWith");
+  }
+
+  @Override
+  public boolean endsWith(Path other) {
+    throw new RuntimeException("Mocked Path does not implement endswith");
+  }
+
+  @Override
+  public Path normalize() {
+    throw new RuntimeException("Mocked Path does not implement normalize");
+  }
+
+  @Override
+  public Path resolve(Path other) {
+    return new MockedPath(new File(wrappedFile.getPath(), other.toString()));
+  }
+
+  @Override
+  public Path relativize(Path other) {
+    throw new RuntimeException("Mocked Path does not implement relativize");
+  }
+
+  @Override
+  public URI toUri() {
+    throw new RuntimeException("Mocked Path does not implement toUri");
+  }
+
+  @Override
+  public Path toAbsolutePath() {
+    throw new RuntimeException("Mocked Path does not implement toAbsolutePath");
+  }
+
+  @Override
+  public Path toRealPath(LinkOption... options) throws IOException {
+    throw new RuntimeException("Mocked Path does not implement toRealPath");
+  }
+
+  @Override
+  public int compareTo(Path other) {
+    throw new RuntimeException("Mocked Path does not implement compareTo");
+  }
+
+  @Override
+  public String toString() {
+    return wrappedFile.toString();
+  }
+
+  @Override
+  public File toFile() {
+    return wrappedFile;
+  }
+
+  // Compatibility with Files.
+
+  public byte[] getAllBytes() throws IOException {
+    FileInputStream fileInputStream = new FileInputStream(wrappedFile);
+    // In android the result of file.length() is long
+    // byte count of the file-content
+    long byteLength = wrappedFile.length();
+    byte[] filecontent = new byte[(int) byteLength];
+    fileInputStream.read(filecontent, 0, (int) byteLength);
+    return filecontent;
+  }
+
+  public OutputStream newOutputStream(boolean append) throws IOException {
+    return new FileOutputStream(wrappedFile, append);
+  }
+}
diff --git a/src/test/r8OnArtBackport/NoSuchFileException.java b/src/test/r8OnArtBackport/NoSuchFileException.java
new file mode 100644
index 0000000..9c8e9cf
--- /dev/null
+++ b/src/test/r8OnArtBackport/NoSuchFileException.java
@@ -0,0 +1,13 @@
+// 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 java.nio.file;
+
+import java.io.IOException;
+
+public class NoSuchFileException extends IOException {
+  public NoSuchFileException(String file) {
+    super(file);
+  }
+}
diff --git a/src/test/r8OnArtBackport/OpenOption.java b/src/test/r8OnArtBackport/OpenOption.java
new file mode 100644
index 0000000..21b5fb6
--- /dev/null
+++ b/src/test/r8OnArtBackport/OpenOption.java
@@ -0,0 +1,7 @@
+// 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 java.nio.file;
+
+public interface OpenOption {}
diff --git a/src/test/r8OnArtBackport/Path.java b/src/test/r8OnArtBackport/Path.java
new file mode 100644
index 0000000..474725f
--- /dev/null
+++ b/src/test/r8OnArtBackport/Path.java
@@ -0,0 +1,91 @@
+// 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 java.nio.file;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URI;
+import java.util.Iterator;
+
+// Removed Watchable.
+public interface Path extends Comparable<Path>, Iterable<Path> {
+  public static Path of(String first, String... more) {
+    throw new RuntimeException("Path does not implement of, use MockedPath.of");
+  }
+
+  public static Path of(URI uri) {
+    throw new RuntimeException("Path does not implement of, use MockedPath.of");
+  }
+
+  FileSystem getFileSystem();
+
+  boolean isAbsolute();
+
+  Path getRoot();
+
+  Path getFileName();
+
+  Path getParent();
+
+  int getNameCount();
+
+  Path getName(int index);
+
+  Path subpath(int beginIndex, int endIndex);
+
+  boolean startsWith(Path other);
+
+  default boolean startsWith(String other) {
+    throw new RuntimeException("Path does not implement startsWith");
+  }
+
+  boolean endsWith(Path other);
+
+  default boolean endsWith(String other) {
+    throw new RuntimeException("Path does not implement endsWith");
+  }
+
+  Path normalize();
+
+  Path resolve(Path other);
+
+  default Path resolve(String other) {
+    throw new RuntimeException("Path does not implement resolve");
+  }
+
+  default Path resolveSibling(Path other) {
+    throw new RuntimeException("Path does not implement resolveSibling");
+  }
+
+  default Path resolveSibling(String other) {
+    throw new RuntimeException("Path does not implement resolveSibling");
+  }
+
+  Path relativize(Path other);
+
+  URI toUri();
+
+  Path toAbsolutePath();
+
+  Path toRealPath(LinkOption... options) throws IOException;
+
+  default File toFile() {
+    throw new RuntimeException("Path does not implement toFile");
+  }
+
+  @Override
+  default Iterator<Path> iterator() {
+    throw new RuntimeException("Path does not implement iterator");
+  }
+
+  @Override
+  int compareTo(Path other);
+
+  boolean equals(Object other);
+
+  int hashCode();
+
+  String toString();
+}
diff --git a/src/test/r8OnArtBackport/Paths.java b/src/test/r8OnArtBackport/Paths.java
new file mode 100644
index 0000000..06d044f
--- /dev/null
+++ b/src/test/r8OnArtBackport/Paths.java
@@ -0,0 +1,16 @@
+// 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 java.nio.file;
+
+import com.android.tools.r8.MockedPath;
+
+public final class Paths {
+
+  private Paths() {}
+
+  public static Path get(String first, String... more) {
+    return MockedPath.of(first, more);
+  }
+}
diff --git a/src/test/r8OnArtBackport/StandardOpenOption.java b/src/test/r8OnArtBackport/StandardOpenOption.java
new file mode 100644
index 0000000..0e4d208
--- /dev/null
+++ b/src/test/r8OnArtBackport/StandardOpenOption.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 java.nio.file;
+
+public enum StandardOpenOption implements OpenOption {
+  READ,
+  WRITE,
+  APPEND,
+  TRUNCATE_EXISTING,
+  CREATE,
+  CREATE_NEW,
+  DELETE_ON_CLOSE,
+  SPARSE,
+  SYNC,
+  DSYNC;
+}
diff --git a/tools/gradle.py b/tools/gradle.py
index 842816e..0e926e6 100755
--- a/tools/gradle.py
+++ b/tools/gradle.py
@@ -31,6 +31,11 @@
       default=False, action='store_true')
   parser.add_argument('--java-home', '--java_home',
       help='Use a custom java version to run gradle.')
+  parser.add_argument('--worktree',
+                      help='Gradle is running in a worktree and may lock up '
+                           'the gradle caches.',
+                      action='store_true',
+                      default=False)
   return parser.parse_known_args()
 
 def GetJavaEnv(env):
@@ -101,6 +106,8 @@
     args.append('-Dorg.gradle.java.home=' + options.java_home)
   if options.no_internal:
     args.append('-Pno_internal')
+  if options.worktree:
+    args.append('-g=' + os.path.join(utils.REPO_ROOT, ".gradle_user_home"))
   return RunGradle(args)
 
 if __name__ == '__main__':
diff --git a/tools/internal_test.py b/tools/internal_test.py
index 9654b4b..c85942b 100755
--- a/tools/internal_test.py
+++ b/tools/internal_test.py
@@ -77,7 +77,7 @@
         'find-xmx-min': 800,
         'find-xmx-max': 1200,
         'find-xmx-range': 32,
-        'oom-threshold': 1000,
+        'oom-threshold': 1037,
     },
     {
         'app': 'iosched',
@@ -90,6 +90,8 @@
 ]
 
 def find_min_xmx_command(record):
+  assert record['find-xmx-min'] < record['find-xmx-max']
+  assert record['find-xmx-range'] < record['find-xmx-max'] - record['find-xmx-min']
   return [
       'tools/run_on_app.py',
       '--compiler=r8',
diff --git a/tools/test.py b/tools/test.py
index 2d45d1c..5091f61 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -128,6 +128,9 @@
   result.add_option('--fail-fast', '--fail_fast',
       default=False, action='store_true',
       help='Stop on first failure. Passes --fail-fast to gradle test runner.')
+  result.add_option('--worktree',
+      default=False, action='store_true',
+      help='Tests are run in worktree and should not use gradle user home.')
   return result.parse_args()
 
 def archive_failures():
@@ -215,6 +218,8 @@
     gradle_args.append('R8LibNoDeps')
   if options.r8lib_no_deps:
     gradle_args.append('-Pr8lib_no_deps')
+  if options.worktree:
+    gradle_args.append('-g=' + os.path.join(utils.REPO_ROOT, ".gradle_user_home"))
 
   # Build an R8 with dependencies for bootstrapping tests before adding test sources.
   gradle_args.append('r8WithRelocatedDeps')