Merge commit '9593a2af0256a0002b206d229420fe55429569dd' into dev-release
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index ac5cd11..46cc30f 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -240,21 +240,9 @@
 
       // Preserve markers from input dex code and add a marker with the current version
       // if there were class file inputs.
-      boolean hasClassResources = false;
-      boolean hasDexResources = false;
-      for (DexProgramClass dexProgramClass : appView.appInfo().classes()) {
-        if (dexProgramClass.originatesFromClassResource()) {
-          hasClassResources = true;
-          if (hasDexResources) {
-            break;
-          }
-        } else if (dexProgramClass.originatesFromDexResource()) {
-          hasDexResources = true;
-          if (hasClassResources) {
-            break;
-          }
-        }
-      }
+      boolean hasClassResources = appView.appInfo().app().getFlags().hasReadProgramClassFromCf();
+      boolean hasDexResources = appView.appInfo().app().getFlags().hasReadProgramClassFromDex();
+
       Marker marker = options.getMarker(Tool.D8);
       Set<Marker> markers = new HashSet<>(appView.dexItemFactory().extractMarkers());
       // TODO(b/166617364): Don't add an additional marker when desugaring is turned off.
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 22308ef..91f6172 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -46,6 +46,8 @@
 import com.android.tools.r8.inspector.internal.InspectorImpl;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.desugar.BackportedMethodRewriter;
+import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaringCollection;
+import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryRetargeterLibraryTypeSynthesizor;
 import com.android.tools.r8.ir.desugar.itf.InterfaceMethodRewriter;
 import com.android.tools.r8.ir.desugar.records.RecordRewriter;
@@ -322,6 +324,19 @@
       CfUtilityMethodsForCodeOptimizations.registerSynthesizedCodeReferences(
           appView.dexItemFactory());
 
+      // Upfront desugaring generation: Generates new program classes to be added in the app.
+      CfClassSynthesizerDesugaringEventConsumer classSynthesizerEventConsumer =
+          new CfClassSynthesizerDesugaringEventConsumer();
+      CfClassSynthesizerDesugaringCollection.create(appView, null)
+          .synthesizeClasses(executorService, classSynthesizerEventConsumer);
+      if (appView.getSyntheticItems().hasPendingSyntheticClasses()) {
+        appView.setAppInfo(
+            appView
+                .appInfo()
+                .rebuildWithClassHierarchy(
+                    appView.getSyntheticItems().commit(appView.appInfo().app())));
+      }
+
       List<ProguardConfigurationRule> synthesizedProguardRules = new ArrayList<>();
       timing.begin("Strip unused code");
       RuntimeTypeCheckInfo.Builder classMergingEnqueuerExtensionBuilder =
diff --git a/src/main/java/com/android/tools/r8/cf/CfCodePrinter.java b/src/main/java/com/android/tools/r8/cf/CfCodePrinter.java
index 02aebd3..fdec52c 100644
--- a/src/main/java/com/android/tools/r8/cf/CfCodePrinter.java
+++ b/src/main/java/com/android/tools/r8/cf/CfCodePrinter.java
@@ -49,6 +49,7 @@
 import com.android.tools.r8.cf.code.CfTryCatch;
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.graph.CfCode;
+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.DexProto;
@@ -56,6 +57,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.code.If.Type;
 import com.android.tools.r8.ir.code.MemberType;
+import com.android.tools.r8.ir.code.Monitor;
 import com.android.tools.r8.ir.code.NumericType;
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.utils.StringUtils;
@@ -72,6 +74,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.stream.Collectors;
+import org.objectweb.asm.Opcodes;
 
 /** Rudimentary printer to print the source representation for creating CfCode object. */
 public class CfCodePrinter extends CfPrinter {
@@ -198,6 +201,14 @@
     return r8Type("FrameType", ImmutableList.of("cf", "code", "CfFrame"));
   }
 
+  private String monitorType() {
+    return r8Type("Monitor", ImmutableList.of("ir", "code"));
+  }
+
+  private String asmOpcodesType() {
+    return type("Opcodes", ImmutableList.of("org", "objectweb", "asm"));
+  }
+
   private String dexItemFactoryType() {
     return r8Type("DexItemFactory", "graph");
   }
@@ -312,6 +323,16 @@
         + ")";
   }
 
+  private String dexField(DexField field) {
+    return "options.itemFactory.createField("
+        + dexType(field.holder)
+        + ", "
+        + dexType(field.type)
+        + ", "
+        + dexString(field.name)
+        + ")";
+  }
+
   private void ensureComma() {
     if (pendingComma) {
       builder.append(",");
@@ -363,7 +384,7 @@
 
   @Override
   public void print(CfConstClass constClass) {
-    throw new Unimplemented(constClass.getClass().getSimpleName());
+    printNewInstruction("CfConstClass", dexType(constClass.getType()));
   }
 
   @Override
@@ -378,7 +399,11 @@
 
   @Override
   public void print(CfMonitor monitor) {
-    throw new Unimplemented(monitor.getClass().getSimpleName());
+    printNewInstruction(
+        "CfMonitor",
+        monitor.getType() == Monitor.Type.ENTER
+            ? monitorType() + ".Type.ENTER"
+            : monitorType() + ".Type.EXIT");
   }
 
   @Override
@@ -500,7 +525,23 @@
 
   @Override
   public void print(CfFieldInstruction insn) {
-    throw new Unimplemented(insn.getClass().getSimpleName());
+    printNewInstruction(
+        "CfFieldInstruction", fieldInstructionOpcode(insn), dexField(insn.getField()));
+  }
+
+  private String fieldInstructionOpcode(CfFieldInstruction insn) {
+    switch (insn.getOpcode()) {
+      case Opcodes.GETSTATIC:
+        return asmOpcodesType() + ".GETSTATIC";
+      case Opcodes.PUTSTATIC:
+        return asmOpcodesType() + ".PUTSTATIC";
+      case Opcodes.GETFIELD:
+        return asmOpcodesType() + ".GETFIELD";
+      case Opcodes.PUTFIELD:
+        return asmOpcodesType() + ".PUTFIELD";
+      default:
+        throw new Unimplemented();
+    }
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index b231b6e..4050961 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.errors.UnsupportedMainDexListUsageDiagnostic;
 import com.android.tools.r8.graph.ClassKind;
 import com.android.tools.r8.graph.DexApplication;
+import com.android.tools.r8.graph.DexApplicationReadFlags;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexClasspathClass;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -67,8 +68,7 @@
   private final Timing timing;
   private final AndroidApp inputApp;
 
-  private boolean hasReadProgramResourcesFromCf = false;
-  private boolean hasReadProgramResourcesFromDex = false;
+  private DexApplicationReadFlags flags;
 
   public interface ProgramClassConflictResolver {
     DexProgramClass resolveClassConflict(DexProgramClass a, DexProgramClass b);
@@ -162,9 +162,9 @@
       readProguardMap(proguardMap, builder, executorService, futures);
       ClassReader classReader = new ClassReader(executorService, futures);
       classReader.readSources();
-      hasReadProgramResourcesFromCf = classReader.hasReadProgramResourceFromCf;
-      hasReadProgramResourcesFromDex = classReader.hasReadProgramResourceFromDex;
       ThreadUtils.awaitFutures(futures);
+      flags = classReader.getDexApplicationReadFlags();
+      builder.setFlags(flags);
       classReader.initializeLazyClassCollection(builder);
       for (ProgramResourceProvider provider : inputApp.getProgramResourceProviders()) {
         DataResourceProvider dataResourceProvider = provider.getDataResourceProvider();
@@ -211,7 +211,7 @@
   }
 
   public MainDexInfo readMainDexClasses(DexApplication app) {
-    return readMainDexClasses(app, hasReadProgramResourcesFromCf);
+    return readMainDexClasses(app, flags.hasReadProgramClassFromCf());
   }
 
   public MainDexInfo readMainDexClassesForR8(DexApplication app) {
@@ -325,18 +325,24 @@
     // Jar application reader to share across all class readers.
     private final JarApplicationReader application = new JarApplicationReader(options);
 
-    // Flag of which input resource types have flowen into the program classes.
+    // Flag of which input resource types have flown into the program classes.
     // Note that this is just at the level of the resources having been given.
     // It is possible to have, e.g., an empty dex file, so no classes, but this will still be true
     // as there was a dex resource.
     private boolean hasReadProgramResourceFromCf = false;
     private boolean hasReadProgramResourceFromDex = false;
+    private boolean hasReadProgramRecord = false;
 
     ClassReader(ExecutorService executorService, List<Future<?>> futures) {
       this.executorService = executorService;
       this.futures = futures;
     }
 
+    public DexApplicationReadFlags getDexApplicationReadFlags() {
+      return new DexApplicationReadFlags(
+          hasReadProgramResourceFromDex, hasReadProgramResourceFromCf, hasReadProgramRecord);
+    }
+
     private void readDexSources(List<ProgramResource> dexSources, Queue<DexProgramClass> classes)
         throws IOException, ResourceException {
       if (dexSources.isEmpty()) {
@@ -376,7 +382,15 @@
       }
       hasReadProgramResourceFromCf = true;
       JarClassFileReader<DexProgramClass> reader =
-          new JarClassFileReader<>(application, classes::add, PROGRAM);
+          new JarClassFileReader<>(
+              application,
+              clazz -> {
+                classes.add(clazz);
+                if (clazz.isRecord()) {
+                  hasReadProgramRecord = true;
+                }
+              },
+              PROGRAM);
       // Read classes in parallel.
       for (ProgramResource input : classSources) {
         futures.add(
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
index 30a1c04..c8b99a4 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
@@ -599,7 +599,7 @@
     assert checkIfObsolete();
     DexType holder = method.holder;
     if (holder.isArrayType()) {
-      return resolveMethodOnArray(holder, method);
+      return resolveMethodOnArray(holder, method.getProto(), method.getName());
     }
     DexClass definition = definitionFor(holder);
     if (definition == null) {
@@ -615,9 +615,18 @@
   }
 
   public MethodResolutionResult resolveMethodOn(DexClass holder, DexMethod method) {
+    return resolveMethodOn(holder, method.getProto(), method.getName());
+  }
+
+  public MethodResolutionResult resolveMethodOn(DexClass holder, DexMethodSignature method) {
+    return resolveMethodOn(holder, method.getProto(), method.getName());
+  }
+
+  public MethodResolutionResult resolveMethodOn(
+      DexClass holder, DexProto methodProto, DexString methodName) {
     return holder.isInterface()
-        ? resolveMethodOnInterface(holder, method)
-        : resolveMethodOnClass(method, holder);
+        ? resolveMethodOnInterface(holder, methodProto, methodName)
+        : resolveMethodOnClass(methodProto, methodName, holder);
   }
 
   /**
@@ -646,18 +655,23 @@
    * 10.7 of the Java Language Specification</a>. All invokations will have target java.lang.Object
    * except clone which has no target.
    */
-  private MethodResolutionResult resolveMethodOnArray(DexType holder, DexMethod method) {
+  private MethodResolutionResult resolveMethodOnArray(
+      DexType holder, DexProto methodProto, DexString methodName) {
     assert checkIfObsolete();
     assert holder.isArrayType();
-    if (method.name == dexItemFactory().cloneMethodName) {
+    if (methodName == dexItemFactory().cloneMethodName) {
       return ArrayCloneMethodResult.INSTANCE;
     } else {
-      return resolveMethodOnClass(method, dexItemFactory().objectType);
+      return resolveMethodOnClass(methodProto, methodName, dexItemFactory().objectType);
     }
   }
 
   public MethodResolutionResult resolveMethodOnClass(DexMethod method) {
-    return resolveMethodOnClass(method, method.holder);
+    return resolveMethodOnClass(method.getProto(), method.getName(), method.getHolderType());
+  }
+
+  public MethodResolutionResult resolveMethodOnClass(DexMethod method, DexType holder) {
+    return resolveMethodOnClass(method.getProto(), method.getName(), holder);
   }
 
   /**
@@ -671,10 +685,27 @@
    * invoke on the given descriptor to a corresponding invoke on the resolved descriptor, as the
    * resolved method is used as basis for dispatch.
    */
-  public MethodResolutionResult resolveMethodOnClass(DexMethod method, DexType holder) {
+  public MethodResolutionResult resolveMethodOnClass(
+      DexProto methodProto, DexString methodName, DexType holder) {
     assert checkIfObsolete();
     if (holder.isArrayType()) {
-      return resolveMethodOnArray(holder, method);
+      return resolveMethodOnArray(holder, methodProto, methodName);
+    }
+    DexClass clazz = definitionFor(holder);
+    if (clazz == null) {
+      return ClassNotFoundResult.INSTANCE;
+    }
+    // Step 1: If holder is an interface, resolution fails with an ICCE. We return null.
+    if (clazz.isInterface()) {
+      return IncompatibleClassResult.INSTANCE;
+    }
+    return resolveMethodOnClass(methodProto, methodName, clazz);
+  }
+
+  public MethodResolutionResult resolveMethodOnClass(DexMethodSignature method, DexType holder) {
+    assert checkIfObsolete();
+    if (holder.isArrayType()) {
+      return resolveMethodOnArray(holder, method.getProto(), method.getName());
     }
     DexClass clazz = definitionFor(holder);
     if (clazz == null) {
@@ -688,11 +719,15 @@
   }
 
   public MethodResolutionResult resolveMethodOnClass(DexMethod method, DexClass clazz) {
-    return resolveMethodOnClass(clazz, method.getProto(), method.getName());
+    return resolveMethodOnClass(method.getProto(), method.getName(), clazz);
+  }
+
+  public MethodResolutionResult resolveMethodOnClass(DexMethodSignature method, DexClass clazz) {
+    return resolveMethodOnClass(method.getProto(), method.getName(), clazz);
   }
 
   public MethodResolutionResult resolveMethodOnClass(
-      DexClass clazz, DexProto methodProto, DexString methodName) {
+      DexProto methodProto, DexString methodName, DexClass clazz) {
     assert checkIfObsolete();
     assert !clazz.isInterface();
     // Step 2:
@@ -862,10 +897,15 @@
   }
 
   public MethodResolutionResult resolveMethodOnInterface(DexClass definition, DexMethod desc) {
+    return resolveMethodOnInterface(definition, desc.getProto(), desc.getName());
+  }
+
+  public MethodResolutionResult resolveMethodOnInterface(
+      DexClass definition, DexProto methodProto, DexString methodName) {
     assert checkIfObsolete();
     assert definition.isInterface();
     // Step 2: Look for exact method on interface.
-    DexEncodedMethod result = definition.lookupMethod(desc);
+    DexEncodedMethod result = definition.lookupMethod(methodProto, methodName);
     if (result != null) {
       return new SingleResolutionResult(definition, definition, result);
     }
@@ -874,13 +914,13 @@
     if (objectClass == null) {
       return ClassNotFoundResult.INSTANCE;
     }
-    result = objectClass.lookupMethod(desc);
+    result = objectClass.lookupMethod(methodProto, methodName);
     if (result != null && result.accessFlags.isPublic() && !result.accessFlags.isAbstract()) {
       return new SingleResolutionResult(definition, objectClass, result);
     }
     // Step 3: Look for maximally-specific superinterface methods or any interface definition.
     //         This is the same for classes and interfaces.
-    return resolveMethodStep3(definition, desc.getProto(), desc.getName());
+    return resolveMethodStep3(definition, methodProto, methodName);
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplication.java b/src/main/java/com/android/tools/r8/graph/DexApplication.java
index 31b83b2..309a768 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplication.java
@@ -32,6 +32,7 @@
 
   public final InternalOptions options;
   public final DexItemFactory dexItemFactory;
+  private final DexApplicationReadFlags flags;
 
   // Information on the lexicographically largest string referenced from code.
   public final DexString highestSortingString;
@@ -39,11 +40,13 @@
   /** Constructor should only be invoked by the DexApplication.Builder. */
   DexApplication(
       ClassNameMapper proguardMap,
+      DexApplicationReadFlags flags,
       ImmutableList<DataResourceProvider> dataResourceProviders,
       InternalOptions options,
       DexString highestSortingString,
       Timing timing) {
     this.proguardMap = proguardMap;
+    this.flags = flags;
     this.dataResourceProviders = dataResourceProviders;
     this.options = options;
     this.dexItemFactory = options.itemFactory;
@@ -119,6 +122,10 @@
     return classes;
   }
 
+  public DexApplicationReadFlags getFlags() {
+    return flags;
+  }
+
   public abstract DexClass definitionFor(DexType type);
 
   public abstract DexProgramClass programDefinitionFor(DexType type);
@@ -140,6 +147,7 @@
     public final DexItemFactory dexItemFactory;
     ClassNameMapper proguardMap;
     final Timing timing;
+    DexApplicationReadFlags flags;
 
     DexString highestSortingString;
     private final Collection<DexProgramClass> synthesizedClasses;
@@ -154,6 +162,7 @@
     abstract T self();
 
     public Builder(DexApplication application) {
+      flags = application.flags;
       programClasses.addAll(application.programClasses());
       dataResourceProviders.addAll(application.dataResourceProviders);
       proguardMap = application.getProguardMap();
@@ -172,6 +181,10 @@
       return null;
     }
 
+    public void setFlags(DexApplicationReadFlags flags) {
+      this.flags = flags;
+    }
+
     public synchronized T setProguardMap(ClassNameMapper proguardMap) {
       assert this.proguardMap == null;
       this.proguardMap = proguardMap;
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplicationReadFlags.java b/src/main/java/com/android/tools/r8/graph/DexApplicationReadFlags.java
new file mode 100644
index 0000000..d5089a7
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/DexApplicationReadFlags.java
@@ -0,0 +1,34 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.graph;
+
+// Flags set based on the application when it was read.
+// Note that in r8, once classes are pruned, the flags may not reflect the application anymore.
+public class DexApplicationReadFlags {
+
+  private final boolean hasReadProgramClassFromDex;
+  private final boolean hasReadProgramClassFromCf;
+  private final boolean hasReadProgramRecord;
+
+  public DexApplicationReadFlags(
+      boolean hasReadProgramClassFromDex,
+      boolean hasReadProgramClassFromCf,
+      boolean hasReadProgramRecord) {
+    this.hasReadProgramClassFromDex = hasReadProgramClassFromDex;
+    this.hasReadProgramClassFromCf = hasReadProgramClassFromCf;
+    this.hasReadProgramRecord = hasReadProgramRecord;
+  }
+
+  public boolean hasReadProgramClassFromCf() {
+    return hasReadProgramClassFromCf;
+  }
+
+  public boolean hasReadProgramClassFromDex() {
+    return hasReadProgramClassFromDex;
+  }
+
+  public boolean hasReadProgramRecord() {
+    return hasReadProgramRecord;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/DexClassAndMethod.java b/src/main/java/com/android/tools/r8/graph/DexClassAndMethod.java
index b39a1a7..765fef3 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClassAndMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClassAndMethod.java
@@ -59,6 +59,10 @@
     return getDefinition().getOptimizationInfo();
   }
 
+  public DexType getArgumentType(int index) {
+    return getDefinition().getArgumentType(index);
+  }
+
   public DexType getParameter(int index) {
     return getReference().getParameter(index);
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index c2b204e..0243703 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -403,7 +403,7 @@
   }
 
   public DexMethodSignature getSignature() {
-    return new DexMethodSignature(getReference());
+    return DexMethodSignature.create(getReference());
   }
 
   public DexType returnType() {
@@ -1469,6 +1469,7 @@
     if (from.hasClassFileVersion()) {
       upgradeClassFileVersion(from.getClassFileVersion());
     }
+    apiLevelForCode = getApiLevelForCode().max(from.getApiLevelForCode());
   }
 
   public MethodTypeSignature getGenericSignature() {
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 0100fd5..82fa005 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -1122,6 +1122,7 @@
   public class JavaUtilArraysMethods {
 
     public final DexMethod asList;
+    public final DexMethod equalsObjectArray;
 
     private JavaUtilArraysMethods() {
       asList =
@@ -1130,6 +1131,12 @@
               createString("asList"),
               listDescriptor,
               new DexString[] {objectArrayDescriptor});
+      equalsObjectArray =
+          createMethod(
+              arraysDescriptor,
+              equalsMethodName,
+              booleanDescriptor,
+              new DexString[] {objectArrayDescriptor, objectArrayDescriptor});
     }
   }
 
@@ -2254,7 +2261,7 @@
       String baseName, DexType holder, DexProto proto, Predicate<DexMethodSignature> isFresh) {
     return createFreshMember(
         name -> {
-          DexMethodSignature trySignature = new DexMethodSignature(proto, name);
+          DexMethodSignature trySignature = DexMethodSignature.create(name, proto);
           if (isFresh.test(trySignature)) {
             return Optional.of(trySignature);
           } else {
diff --git a/src/main/java/com/android/tools/r8/graph/DexMethod.java b/src/main/java/com/android/tools/r8/graph/DexMethod.java
index c5cb0ff..5eaf805 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethod.java
@@ -157,7 +157,7 @@
   }
 
   public DexMethodSignature getSignature() {
-    return new DexMethodSignature(this);
+    return DexMethodSignature.create(this);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/graph/DexMethodSignature.java b/src/main/java/com/android/tools/r8/graph/DexMethodSignature.java
index 74d464f..0ac4558 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethodSignature.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethodSignature.java
@@ -9,36 +9,32 @@
 import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.Objects;
 
-public class DexMethodSignature implements StructuralItem<DexMethodSignature> {
+public abstract class DexMethodSignature implements StructuralItem<DexMethodSignature> {
 
-  private final DexProto proto;
-  private final DexString name;
+  DexMethodSignature() {}
 
-  public DexMethodSignature(DexMethod method) {
-    this(method.proto, method.name);
+  public static DexMethodSignature create(DexMethod method) {
+    return new MethodBased(method);
   }
 
-  public DexMethodSignature(DexProto proto, DexString name) {
-    assert proto != null;
-    assert name != null;
-    this.proto = proto;
-    this.name = name;
+  public static DexMethodSignature create(DexString name, DexProto proto) {
+    return new NameAndProtoBased(name, proto);
   }
 
+  public abstract DexString getName();
+
+  public abstract DexProto getProto();
+
   public int getArity() {
-    return proto.getArity();
+    return getProto().getArity();
   }
 
-  public DexString getName() {
-    return name;
-  }
-
-  public DexProto getProto() {
-    return proto;
+  public DexType getParameter(int index) {
+    return getProto().getParameter(index);
   }
 
   public DexType getReturnType() {
-    return proto.returnType;
+    return getProto().getReturnType();
   }
 
   @Override
@@ -51,11 +47,11 @@
   }
 
   public DexMethodSignature withName(DexString name) {
-    return new DexMethodSignature(proto, name);
+    return create(name, getProto());
   }
 
   public DexMethodSignature withProto(DexProto proto) {
-    return new DexMethodSignature(proto, name);
+    return create(getName(), proto);
   }
 
   public DexMethod withHolder(ProgramDefinition definition, DexItemFactory dexItemFactory) {
@@ -63,20 +59,20 @@
   }
 
   public DexMethod withHolder(DexReference reference, DexItemFactory dexItemFactory) {
-    return dexItemFactory.createMethod(reference.getContextType(), proto, name);
+    return dexItemFactory.createMethod(reference.getContextType(), getProto(), getName());
   }
 
   @Override
   public boolean equals(Object o) {
     if (this == o) return true;
-    if (o == null || getClass() != o.getClass()) return false;
+    if (!(o instanceof DexMethodSignature)) return false;
     DexMethodSignature that = (DexMethodSignature) o;
-    return proto == that.proto && name == that.name;
+    return getProto() == that.getProto() && getName() == that.getName();
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(proto, name);
+    return Objects.hash(getProto(), getName());
   }
 
   @Override
@@ -86,7 +82,7 @@
 
   @Override
   public String toString() {
-    return "Method Signature " + name + " " + proto.toString();
+    return "Method Signature " + getName() + " " + getProto();
   }
 
   private String toSourceString() {
@@ -98,13 +94,53 @@
     if (includeReturnType) {
       builder.append(getReturnType().toSourceString()).append(" ");
     }
-    builder.append(name).append("(");
+    builder.append(getName()).append("(");
     for (int i = 0; i < getArity(); i++) {
       if (i != 0) {
         builder.append(", ");
       }
-      builder.append(proto.parameters.values[i].toSourceString());
+      builder.append(getProto().parameters.values[i].toSourceString());
     }
     return builder.append(")").toString();
   }
+
+  static class MethodBased extends DexMethodSignature {
+
+    private final DexMethod method;
+
+    MethodBased(DexMethod method) {
+      this.method = method;
+    }
+
+    @Override
+    public DexString getName() {
+      return method.getName();
+    }
+
+    @Override
+    public DexProto getProto() {
+      return method.getProto();
+    }
+  }
+
+  static class NameAndProtoBased extends DexMethodSignature {
+
+    private final DexString name;
+    private final DexProto proto;
+
+    NameAndProtoBased(DexString name, DexProto proto) {
+      this.name = name;
+      this.proto = proto;
+    }
+
+    @Override
+    public DexString getName() {
+      return name;
+    }
+
+    @Override
+    public DexProto getProto() {
+      return proto;
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java b/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
index ceac26a..f222dbf 100644
--- a/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
@@ -35,6 +35,7 @@
 
   private DirectMappedDexApplication(
       ClassNameMapper proguardMap,
+      DexApplicationReadFlags flags,
       Map<DexType, DexClass> allClasses,
       ImmutableList<DexProgramClass> programClasses,
       ImmutableList<DexClasspathClass> classpathClasses,
@@ -43,12 +44,7 @@
       InternalOptions options,
       DexString highestSortingString,
       Timing timing) {
-    super(
-        proguardMap,
-        dataResourceProviders,
-        options,
-        highestSortingString,
-        timing);
+    super(proguardMap, flags, dataResourceProviders, options, highestSortingString, timing);
     this.allClasses = Collections.unmodifiableMap(allClasses);
     this.programClasses = programClasses;
     this.classpathClasses = classpathClasses;
@@ -307,6 +303,7 @@
       addAll(allClasses, getProgramClasses());
       return new DirectMappedDexApplication(
           proguardMap,
+          flags,
           allClasses,
           ImmutableList.copyOf(getProgramClasses()),
           ImmutableList.copyOf(getClasspathClasses()),
diff --git a/src/main/java/com/android/tools/r8/graph/FieldResolutionResult.java b/src/main/java/com/android/tools/r8/graph/FieldResolutionResult.java
index 03627d7..702d1fd 100644
--- a/src/main/java/com/android/tools/r8/graph/FieldResolutionResult.java
+++ b/src/main/java/com/android/tools/r8/graph/FieldResolutionResult.java
@@ -66,10 +66,6 @@
     return false;
   }
 
-  public boolean isFailedResolution() {
-    return false;
-  }
-
   public DexClass getInitialResolutionHolder() {
     return null;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
index a520ae3..174dca5 100644
--- a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
@@ -62,6 +62,7 @@
 import com.android.tools.r8.position.MethodPosition;
 import com.android.tools.r8.shaking.ProguardConfiguration;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
+import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
@@ -125,31 +126,35 @@
   @Override
   public CfCode asCfCode() {
     if (code == null) {
-      ReparseContext context = this.context;
-      JarApplicationReader application = this.application;
-      assert application != null;
-      assert context != null;
-      // The ClassCodeVisitor is in charge of setting this.context to null.
-      try {
-        parseCode(context, false);
-      } catch (JsrEncountered e) {
-        for (Code code : context.codeList) {
-          code.asLazyCfCode().code = null;
-          code.asLazyCfCode().context = context;
-          code.asLazyCfCode().application = application;
-        }
-        try {
-          parseCode(context, true);
-        } catch (JsrEncountered e1) {
-          throw new Unreachable(e1);
-        }
-      }
-      assert verifyNoReparseContext(context.owner);
+      ExceptionUtils.withOriginAttachmentHandler(origin, this::internalParseCode);
     }
     assert code != null;
     return code;
   }
 
+  private void internalParseCode() {
+    ReparseContext context = this.context;
+    JarApplicationReader application = this.application;
+    assert application != null;
+    assert context != null;
+    // The ClassCodeVisitor is in charge of setting this.context to null.
+    try {
+      parseCode(context, false);
+    } catch (JsrEncountered e) {
+      for (Code code : context.codeList) {
+        code.asLazyCfCode().code = null;
+        code.asLazyCfCode().context = context;
+        code.asLazyCfCode().application = application;
+      }
+      try {
+        parseCode(context, true);
+      } catch (JsrEncountered e1) {
+        throw new Unreachable(e1);
+      }
+    }
+    assert verifyNoReparseContext(context.owner);
+  }
+
   @Override
   public Code getCodeAsInlining(DexMethod caller, DexMethod callee) {
     return asCfCode().getCodeAsInlining(caller, callee);
@@ -773,6 +778,9 @@
     @Override
     public void visitMethodInsn(int opcode, String owner, String name, String desc, boolean itf) {
       DexMethod method = application.getMethod(owner, name, desc);
+      if (application.getFactory().isClassConstructor(method)) {
+        throw new CompilationError("Invalid input code with a call to <clinit>");
+      }
       instructions.add(new CfInvoke(opcode, method, itf));
     }
 
diff --git a/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java b/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java
index 509a10d..cf0d761 100644
--- a/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/LazyLoadedDexApplication.java
@@ -29,6 +29,7 @@
   /** Constructor should only be invoked by the DexApplication.Builder. */
   private LazyLoadedDexApplication(
       ClassNameMapper proguardMap,
+      DexApplicationReadFlags flags,
       ProgramClassCollection programClasses,
       ImmutableList<DataResourceProvider> dataResourceProviders,
       ClasspathClassCollection classpathClasses,
@@ -36,12 +37,7 @@
       InternalOptions options,
       DexString highestSortingString,
       Timing timing) {
-    super(
-        proguardMap,
-        dataResourceProviders,
-        options,
-        highestSortingString,
-        timing);
+    super(proguardMap, flags, dataResourceProviders, options, highestSortingString, timing);
     this.programClasses = programClasses;
     this.classpathClasses = classpathClasses;
     this.libraryClasses = libraryClasses;
@@ -236,6 +232,7 @@
     public LazyLoadedDexApplication build() {
       return new LazyLoadedDexApplication(
           proguardMap,
+          flags,
           ProgramClassCollection.create(getProgramClasses(), resolver),
           ImmutableList.copyOf(dataResourceProviders),
           classpathClasses,
diff --git a/src/main/java/com/android/tools/r8/graph/MemberResolutionResult.java b/src/main/java/com/android/tools/r8/graph/MemberResolutionResult.java
index c3f3c42..e5467ff 100644
--- a/src/main/java/com/android/tools/r8/graph/MemberResolutionResult.java
+++ b/src/main/java/com/android/tools/r8/graph/MemberResolutionResult.java
@@ -25,6 +25,15 @@
     return isAccessibleFrom(context, appView.appInfo());
   }
 
+  /**
+   * Returns true if resolution failed.
+   *
+   * <p>Note the disclaimer in the doc of {@code MethodResolutionResult.isSingleResolution()}.
+   */
+  public boolean isFailedResolution() {
+    return false;
+  }
+
   public boolean isFieldResolutionResult() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/MethodMapBacking.java b/src/main/java/com/android/tools/r8/graph/MethodMapBacking.java
index 8f705d4..5c41207 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodMapBacking.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodMapBacking.java
@@ -7,41 +7,41 @@
 import com.android.tools.r8.utils.IteratorUtils;
 import com.android.tools.r8.utils.TraversalContinuation;
 import com.google.common.collect.Lists;
+import it.unimi.dsi.fastutil.objects.Object2ReferenceLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2ReferenceRBTreeMap;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
-import java.util.TreeMap;
+import java.util.SortedMap;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
 public class MethodMapBacking extends MethodCollectionBacking {
 
-  private Map<DexMethodSignature, DexEncodedMethod> methodMap;
+  private SortedMap<DexMethodSignature, DexEncodedMethod> methodMap;
 
   public MethodMapBacking() {
-    this(createMap());
+    this(createdLinkedMap());
   }
 
-  private MethodMapBacking(Map<DexMethodSignature, DexEncodedMethod> methodMap) {
+  private MethodMapBacking(SortedMap<DexMethodSignature, DexEncodedMethod> methodMap) {
     this.methodMap = methodMap;
   }
 
   public static MethodMapBacking createSorted() {
-    return new MethodMapBacking(new TreeMap<>());
+    return new MethodMapBacking(new Object2ReferenceRBTreeMap<>());
   }
 
-  private static Map<DexMethodSignature, DexEncodedMethod> createMap() {
+  private static SortedMap<DexMethodSignature, DexEncodedMethod> createdLinkedMap() {
     // Maintain a linked map so the output order remains a deterministic function of the input.
-    return new HashMap<>();
+    return new Object2ReferenceLinkedOpenHashMap<>();
   }
 
-  private static Map<DexMethodSignature, DexEncodedMethod> createMap(int capacity) {
+  private static SortedMap<DexMethodSignature, DexEncodedMethod> createdLinkedMap(int capacity) {
     // Maintain a linked map so the output order remains a deterministic function of the input.
-    return new HashMap<>(capacity);
+    return new Object2ReferenceLinkedOpenHashMap<>(capacity);
   }
 
   private void replace(DexMethodSignature existingKey, DexEncodedMethod method) {
@@ -115,7 +115,7 @@
 
   @Override
   DexEncodedMethod getMethod(DexProto methodProto, DexString methodName) {
-    return methodMap.get(new DexMethodSignature(methodProto, methodName));
+    return methodMap.get(DexMethodSignature.create(methodName, methodProto));
   }
 
   private DexEncodedMethod getMethod(Predicate<DexEncodedMethod> predicate) {
@@ -216,7 +216,8 @@
     if (methods == null) {
       methods = DexEncodedMethod.EMPTY_ARRAY;
     }
-    Map<DexMethodSignature, DexEncodedMethod> newMap = createMap(size() + methods.length);
+    SortedMap<DexMethodSignature, DexEncodedMethod> newMap =
+        createdLinkedMap(size() + methods.length);
     forEachMethod(
         method -> {
           if (belongsToVirtualPool(method)) {
@@ -238,7 +239,8 @@
     if (methods == null) {
       methods = DexEncodedMethod.EMPTY_ARRAY;
     }
-    Map<DexMethodSignature, DexEncodedMethod> newMap = createMap(size() + methods.length);
+    SortedMap<DexMethodSignature, DexEncodedMethod> newMap =
+        createdLinkedMap(size() + methods.length);
     forEachMethod(
         method -> {
           if (belongsToDirectPool(method)) {
diff --git a/src/main/java/com/android/tools/r8/graph/MethodResolutionResult.java b/src/main/java/com/android/tools/r8/graph/MethodResolutionResult.java
index f1e8ae1..c2cb1d4 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodResolutionResult.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodResolutionResult.java
@@ -59,15 +59,6 @@
     return null;
   }
 
-  /**
-   * Returns true if resolution failed.
-   *
-   * <p>Note the disclaimer in the doc of {@code isSingleResolution()}.
-   */
-  public boolean isFailedResolution() {
-    return false;
-  }
-
   public boolean isIncompatibleClassChangeErrorResult() {
     return false;
   }
@@ -84,6 +75,10 @@
     return false;
   }
 
+  public boolean isArrayCloneMethodResult() {
+    return false;
+  }
+
   /** Returns non-null if isFailedResolution() is true, otherwise null. */
   public FailedResolutionResult asFailedResolution() {
     return null;
@@ -808,6 +803,11 @@
     public boolean isVirtualTarget() {
       return true;
     }
+
+    @Override
+    public boolean isArrayCloneMethodResult() {
+      return true;
+    }
   }
 
   /** Base class for all types of failed resolutions. */
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeElement.java
index 9e83183..65e9670 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeElement.java
@@ -135,6 +135,11 @@
   }
 
   @Override
+  public ClassTypeElement asDefinitelyNotNull() {
+    return getOrCreateVariant(Nullability.definitelyNotNull());
+  }
+
+  @Override
   public ClassTypeElement asMeetWithNotNull() {
     return getOrCreateVariant(nullability.meet(Nullability.definitelyNotNull()));
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/DynamicType.java b/src/main/java/com/android/tools/r8/ir/analysis/type/DynamicType.java
index 5db6f54..1a32390 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/DynamicType.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/DynamicType.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.ir.analysis.type;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.Objects;
@@ -19,6 +20,7 @@
 public class DynamicType {
 
   private static final DynamicType BOTTOM = new DynamicType(TypeElement.getBottom());
+  private static final DynamicType NULL_TYPE = new DynamicType(TypeElement.getNull());
   private static final DynamicType UNKNOWN = new DynamicType(TypeElement.getTop());
 
   private final TypeElement dynamicUpperBoundType;
@@ -29,12 +31,29 @@
   }
 
   public static DynamicType create(
+      AppView<AppInfoWithLiveness> appView, TypeElement dynamicUpperBoundType) {
+    ClassTypeElement dynamicLowerBoundType = null;
+    if (dynamicUpperBoundType.isClassType()) {
+      ClassTypeElement dynamicUpperBoundClassType = dynamicUpperBoundType.asClassType();
+      DexClass dynamicUpperBoundClass =
+          appView.definitionFor(dynamicUpperBoundClassType.getClassType());
+      if (dynamicUpperBoundClass != null && dynamicUpperBoundClass.isEffectivelyFinal(appView)) {
+        dynamicLowerBoundType = dynamicUpperBoundClassType;
+      }
+    }
+    return create(appView, dynamicUpperBoundType, dynamicLowerBoundType);
+  }
+
+  public static DynamicType create(
       AppView<AppInfoWithLiveness> appView,
       TypeElement dynamicUpperBoundType,
       ClassTypeElement dynamicLowerBoundType) {
     if (dynamicUpperBoundType.isBottom()) {
       return bottom();
     }
+    if (dynamicUpperBoundType.isNullType()) {
+      return definitelyNull();
+    }
     if (dynamicUpperBoundType.isTop()) {
       return unknown();
     }
@@ -47,6 +66,7 @@
       return DynamicTypeWithLowerBound.create(
           appView, dynamicUpperBoundType.asClassType(), dynamicLowerBoundType);
     }
+    assert verifyNotEffectivelyFinalClassType(appView, dynamicUpperBoundType);
     return new DynamicType(dynamicUpperBoundType);
   }
 
@@ -54,11 +74,12 @@
     return new ExactDynamicType(exactDynamicType);
   }
 
-  public static DynamicType create(Value value, AppView<AppInfoWithLiveness> appView) {
+  public static DynamicType create(AppView<AppInfoWithLiveness> appView, Value value) {
     assert value.getType().isReferenceType();
     TypeElement dynamicUpperBoundType = value.getDynamicUpperBoundType(appView);
     ClassTypeElement dynamicLowerBoundType =
-        value.getDynamicLowerBoundType(appView, dynamicUpperBoundType.nullability());
+        value.getDynamicLowerBoundType(
+            appView, dynamicUpperBoundType, dynamicUpperBoundType.nullability());
     return create(appView, dynamicUpperBoundType, dynamicLowerBoundType);
   }
 
@@ -66,6 +87,10 @@
     return BOTTOM;
   }
 
+  public static DynamicType definitelyNull() {
+    return NULL_TYPE;
+  }
+
   public static DynamicType unknown() {
     return UNKNOWN;
   }
@@ -82,12 +107,16 @@
     return null;
   }
 
+  public Nullability getNullability() {
+    return getDynamicUpperBoundType().nullability();
+  }
+
   public boolean isBottom() {
     return getDynamicUpperBoundType().isBottom();
   }
 
-  public boolean isTrivial(TypeElement staticType) {
-    return staticType.equals(getDynamicUpperBoundType()) || isUnknown();
+  public boolean isNullType() {
+    return getDynamicUpperBoundType().isNullType();
   }
 
   public boolean isUnknown() {
@@ -116,6 +145,18 @@
 
   private ClassTypeElement meetDynamicLowerBound(
       AppView<AppInfoWithLiveness> appView, DynamicType dynamicType) {
+    if (isNullType()) {
+      if (dynamicType.hasDynamicLowerBoundType()) {
+        return dynamicType.getDynamicLowerBoundType().joinNullability(Nullability.definitelyNull());
+      }
+      return null;
+    }
+    if (dynamicType.isNullType()) {
+      if (hasDynamicLowerBoundType()) {
+        return getDynamicLowerBoundType().joinNullability(Nullability.definitelyNull());
+      }
+      return null;
+    }
     if (!hasDynamicLowerBoundType() || !dynamicType.hasDynamicLowerBoundType()) {
       return null;
     }
@@ -143,4 +184,27 @@
   public int hashCode() {
     return dynamicUpperBoundType.hashCode();
   }
+
+  private static boolean verifyNotEffectivelyFinalClassType(
+      AppView<AppInfoWithLiveness> appView, TypeElement type) {
+    if (type.isClassType()) {
+      ClassTypeElement classType = type.asClassType();
+      DexClass clazz = appView.definitionFor(classType.getClassType());
+      assert clazz == null || !clazz.isEffectivelyFinal(appView);
+    }
+    return true;
+  }
+
+  public DynamicType withNullability(Nullability nullability) {
+    assert !hasDynamicLowerBoundType();
+    if (!getDynamicUpperBoundType().isReferenceType()) {
+      return this;
+    }
+    ReferenceTypeElement dynamicUpperBoundReferenceType =
+        getDynamicUpperBoundType().asReferenceType();
+    if (dynamicUpperBoundReferenceType.nullability() == nullability) {
+      return this;
+    }
+    return new DynamicType(dynamicUpperBoundReferenceType.getOrCreateVariant(nullability));
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/DynamicTypeWithLowerBound.java b/src/main/java/com/android/tools/r8/ir/analysis/type/DynamicTypeWithLowerBound.java
index 400f99e..e4190bc 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/DynamicTypeWithLowerBound.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/DynamicTypeWithLowerBound.java
@@ -12,7 +12,7 @@
 
   private final ClassTypeElement dynamicLowerBoundType;
 
-  DynamicTypeWithLowerBound(
+  private DynamicTypeWithLowerBound(
       ClassTypeElement dynamicUpperBoundType, ClassTypeElement dynamicLowerBoundType) {
     super(dynamicUpperBoundType);
     assert !dynamicUpperBoundType.equals(dynamicLowerBoundType);
@@ -34,6 +34,11 @@
   }
 
   @Override
+  public ClassTypeElement getDynamicUpperBoundType() {
+    return super.getDynamicUpperBoundType().asClassType();
+  }
+
+  @Override
   public boolean hasDynamicLowerBoundType() {
     return true;
   }
@@ -44,11 +49,6 @@
   }
 
   @Override
-  public boolean isTrivial(TypeElement staticType) {
-    return false;
-  }
-
-  @Override
   public boolean equals(Object other) {
     if (other == null || getClass() != other.getClass()) {
       return false;
@@ -62,4 +62,14 @@
   public int hashCode() {
     return Objects.hash(getDynamicUpperBoundType(), getDynamicLowerBoundType());
   }
+
+  @Override
+  public DynamicType withNullability(Nullability nullability) {
+    if (getDynamicUpperBoundType().nullability() == nullability) {
+      return this;
+    }
+    return new DynamicTypeWithLowerBound(
+        getDynamicUpperBoundType().getOrCreateVariant(nullability),
+        getDynamicLowerBoundType().getOrCreateVariant(nullability));
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/ExactDynamicType.java b/src/main/java/com/android/tools/r8/ir/analysis/type/ExactDynamicType.java
index a6061d9..d66cfcc 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/ExactDynamicType.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/ExactDynamicType.java
@@ -16,13 +16,13 @@
   }
 
   @Override
-  public ClassTypeElement getDynamicLowerBoundType() {
-    return getDynamicUpperBoundType().asClassType();
+  public ClassTypeElement getDynamicUpperBoundType() {
+    return super.getDynamicUpperBoundType().asClassType();
   }
 
   @Override
-  public boolean isTrivial(TypeElement staticType) {
-    return false;
+  public ClassTypeElement getDynamicLowerBoundType() {
+    return getDynamicUpperBoundType();
   }
 
   @Override
@@ -38,4 +38,12 @@
   public int hashCode() {
     return getDynamicLowerBoundType().hashCode();
   }
+
+  @Override
+  public DynamicType withNullability(Nullability nullability) {
+    if (getDynamicUpperBoundType().nullability() == nullability) {
+      return this;
+    }
+    return new ExactDynamicType(getDynamicUpperBoundType().getOrCreateVariant(nullability));
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/Nullability.java b/src/main/java/com/android/tools/r8/ir/analysis/type/Nullability.java
index e45ac9a..c3a5298 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/Nullability.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/Nullability.java
@@ -49,6 +49,10 @@
     return isMaybeNull() || isDefinitelyNull();
   }
 
+  public boolean isUnknown() {
+    return isMaybeNull();
+  }
+
   public Nullability join(Nullability other) {
     if (this == BOTTOM) {
       return other;
@@ -79,6 +83,10 @@
     return join(other) == other;
   }
 
+  public boolean strictlyLessThan(Nullability other) {
+    return !equals(other) && other == join(other);
+  }
+
   public static Nullability definitelyNull() {
     return DEFINITELY_NULL;
   }
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 0447c1a..e5c792f 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
@@ -27,6 +27,7 @@
 import com.android.tools.r8.utils.DequeUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.IteratorUtils;
+import com.android.tools.r8.utils.LinkedHashSetUtils;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.StringUtils;
@@ -61,7 +62,7 @@
 
   public static class LiveAtEntrySets {
     // Set of live SSA values (regardless of whether they denote a local variable).
-    public final Set<Value> liveValues;
+    public final LinkedHashSet<Value> liveValues;
 
     // Subset of live local-variable values.
     public final Set<Value> liveLocalValues;
@@ -69,7 +70,7 @@
     public final Deque<Value> liveStackValues;
 
     public LiveAtEntrySets(
-        Set<Value> liveValues, Set<Value> liveLocalValues, Deque<Value> liveStackValues) {
+        LinkedHashSet<Value> liveValues, Set<Value> liveLocalValues, Deque<Value> liveStackValues) {
       assert liveValues.containsAll(liveLocalValues);
       this.liveValues = liveValues;
       this.liveLocalValues = liveLocalValues;
@@ -176,14 +177,14 @@
     while (!worklist.isEmpty()) {
       BasicBlock block = worklist.poll();
       // Note that the iteration order of live values matters when inserting spill/restore moves.
-      Set<Value> live = new LinkedHashSet<>();
+      LinkedHashSet<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()) {
         LiveAtEntrySets liveAtSucc = liveAtEntrySets.get(succ);
         if (liveAtSucc != null) {
-          live.addAll(liveAtSucc.liveValues);
+          LinkedHashSetUtils.addAll(live, liveAtSucc.liveValues);
           liveLocals.addAll(liveAtSucc.liveLocalValues);
           // The stack is only allowed to be non-empty in the case of linear-flow (so-far).
           // If succ is an exceptional successor the successor stack should be empty
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 4df7f91..0c58032 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
@@ -1086,7 +1086,7 @@
   }
 
   public DynamicType getDynamicType(AppView<AppInfoWithLiveness> appView) {
-    return DynamicType.create(this, appView);
+    return DynamicType.create(appView, this);
   }
 
   public TypeElement getDynamicUpperBoundType(
@@ -1137,18 +1137,23 @@
   }
 
   public ClassTypeElement getDynamicLowerBoundType(AppView<AppInfoWithLiveness> appView) {
-    return getDynamicLowerBoundType(appView, Nullability.maybeNull());
+    return getDynamicLowerBoundType(appView, null, Nullability.maybeNull());
   }
 
   public ClassTypeElement getDynamicLowerBoundType(
-      AppView<AppInfoWithLiveness> appView, Nullability maxNullability) {
-    // If it is a final or effectively-final class type, then we know the lower bound.
-    if (getType().isClassType()) {
-      ClassTypeElement classType = getType().asClassType();
-      DexType type = classType.getClassType();
-      DexClass clazz = appView.definitionFor(type);
-      if (clazz != null && clazz.isEffectivelyFinal(appView)) {
-        return classType.meetNullability(maxNullability);
+      AppView<AppInfoWithLiveness> appView,
+      TypeElement dynamicUpperBoundType,
+      Nullability maxNullability) {
+    // If the dynamic upper bound type is a final or effectively-final class type, then we know the
+    // lower bound. Since the dynamic upper bound type is below the static type in the class
+    // hierarchy, we only need to check if the dynamic upper bound type is effectively final if it
+    // is present.
+    TypeElement upperBoundType = dynamicUpperBoundType != null ? dynamicUpperBoundType : getType();
+    if (upperBoundType.isClassType()) {
+      ClassTypeElement upperBoundClassType = upperBoundType.asClassType();
+      DexClass upperBoundClass = appView.definitionFor(upperBoundClassType.getClassType());
+      if (upperBoundClass != null && upperBoundClass.isEffectivelyFinal(appView)) {
+        return upperBoundClassType.meetNullability(maxNullability);
       }
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/ClassConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/ClassConverter.java
index 2a30a23..5d49406 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/ClassConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/ClassConverter.java
@@ -9,11 +9,9 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.desugar.CfClassDesugaringEventConsumer;
-import com.android.tools.r8.ir.desugar.CfClassDesugaringEventConsumer.D8CfClassDesugaringEventConsumer;
+import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer.D8CfInstructionDesugaringEventConsumer;
-import com.android.tools.r8.ir.desugar.CfL8ClassSynthesizerEventConsumer;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
@@ -55,20 +53,17 @@
       throws ExecutionException {
     List<DexProgramClass> classes = appView.appInfo().classes();
 
-    if (appView.options().isDesugaredLibraryCompilation()) {
-      CfL8ClassSynthesizerEventConsumer l8ClassSynthesizerEventConsumer =
-          new CfL8ClassSynthesizerEventConsumer();
-      converter.l8ClassSynthesis(executorService, l8ClassSynthesizerEventConsumer);
+      CfClassSynthesizerDesugaringEventConsumer classSynthesizerEventConsumer =
+          new CfClassSynthesizerDesugaringEventConsumer();
+      converter.classSynthesisDesugaring(executorService, classSynthesizerEventConsumer);
+    if (!classSynthesizerEventConsumer.getSynthesizedClasses().isEmpty()) {
       classes =
           ImmutableList.<DexProgramClass>builder()
               .addAll(classes)
-              .addAll(l8ClassSynthesizerEventConsumer.getSynthesizedClasses())
+              .addAll(classSynthesizerEventConsumer.getSynthesizedClasses())
               .build();
     }
 
-    D8CfClassDesugaringEventConsumer classDesugaringEventConsumer =
-        CfClassDesugaringEventConsumer.createForD8(methodProcessor);
-    converter.desugarClassesForD8(classes, classDesugaringEventConsumer, executorService);
     converter.prepareDesugaringForD8(executorService);
 
     while (!classes.isEmpty()) {
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 8595ecd..c00f43c 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
@@ -44,13 +44,11 @@
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.DefaultMethodConversionOptions;
 import com.android.tools.r8.ir.conversion.MethodConversionOptions.MutableMethodConversionOptions;
-import com.android.tools.r8.ir.desugar.CfClassDesugaringCollection;
-import com.android.tools.r8.ir.desugar.CfClassDesugaringEventConsumer.D8CfClassDesugaringEventConsumer;
+import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaringCollection;
+import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringCollection;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer.D8CfInstructionDesugaringEventConsumer;
-import com.android.tools.r8.ir.desugar.CfL8ClassSynthesizerCollection;
-import com.android.tools.r8.ir.desugar.CfL8ClassSynthesizerEventConsumer;
 import com.android.tools.r8.ir.desugar.CfPostProcessingDesugaringCollection;
 import com.android.tools.r8.ir.desugar.CfPostProcessingDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.CfPostProcessingDesugaringEventConsumer.D8CfPostProcessingDesugaringEventConsumer;
@@ -99,7 +97,6 @@
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.naming.IdentifierNameStringMarker;
-import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagator;
 import com.android.tools.r8.position.MethodPosition;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.LibraryMethodOverrideAnalysis;
@@ -135,7 +132,6 @@
   private final Timing timing;
   private final Outliner outliner;
   private final ClassInitializerDefaultsOptimization classInitializerDefaultsOptimization;
-  private final CfClassDesugaringCollection classDesugaring;
   private final CfInstructionDesugaringCollection instructionDesugaring;
   private final FieldAccessAnalysis fieldAccessAnalysis;
   private final LibraryMethodOverrideAnalysis libraryMethodOverrideAnalysis;
@@ -226,7 +222,6 @@
       // - invoke-special desugaring.
       assert options.desugarState.isOn();
       this.instructionDesugaring = CfInstructionDesugaringCollection.create(appView);
-      this.classDesugaring = CfClassDesugaringCollection.create(appView);
       this.interfaceMethodRewriter = null;
       this.covariantReturnTypeAnnotationTransformer = null;
       this.dynamicTypeOptimization = null;
@@ -253,10 +248,6 @@
         appView.enableWholeProgramOptimizations()
             ? CfInstructionDesugaringCollection.empty()
             : CfInstructionDesugaringCollection.create(appView);
-    this.classDesugaring =
-        appView.enableWholeProgramOptimizations()
-            ? CfClassDesugaringCollection.empty()
-            : CfClassDesugaringCollection.create(appView);
     this.interfaceMethodRewriter =
         options.isInterfaceMethodDesugaringEnabled() && appView.enableWholeProgramOptimizations()
             ? new InterfaceMethodRewriter(appView, this)
@@ -453,19 +444,20 @@
     }
   }
 
-  public void l8ClassSynthesis(
+  public void classSynthesisDesugaring(
       ExecutorService executorService,
-      CfL8ClassSynthesizerEventConsumer l8ClassSynthesizerEventConsumer)
+      CfClassSynthesizerDesugaringEventConsumer classSynthesizerEventConsumer)
       throws ExecutionException {
-    new CfL8ClassSynthesizerCollection(appView, instructionDesugaring.getRetargetingInfo())
-        .synthesizeClasses(executorService, l8ClassSynthesizerEventConsumer);
+    CfClassSynthesizerDesugaringCollection.create(
+            appView, instructionDesugaring.getRetargetingInfo())
+        .synthesizeClasses(executorService, classSynthesizerEventConsumer);
   }
 
   private void postProcessingDesugaringForD8(
       D8MethodProcessor methodProcessor, ExecutorService executorService)
       throws ExecutionException {
     D8CfPostProcessingDesugaringEventConsumer eventConsumer =
-        CfPostProcessingDesugaringEventConsumer.createForD8(methodProcessor);
+        CfPostProcessingDesugaringEventConsumer.createForD8(methodProcessor, instructionDesugaring);
     methodProcessor.newWave();
     InterfaceMethodProcessorFacade interfaceDesugaring =
         instructionDesugaring.getInterfaceMethodPostProcessingDesugaring(ExcludeDexResources);
@@ -495,26 +487,6 @@
         DesugaredLibraryAPIConverter::generateTrackingWarnings);
   }
 
-  public void desugarClassesForD8(
-      List<DexProgramClass> classes,
-      D8CfClassDesugaringEventConsumer desugaringEventConsumer,
-      ExecutorService executorService)
-      throws ExecutionException {
-    if (classDesugaring.isEmpty()) {
-      return;
-    }
-    // Currently the classes can be processed in any order and do not require to be sorted.
-    ThreadUtils.processItems(
-        classes, clazz -> desugarClassForD8(clazz, desugaringEventConsumer), executorService);
-  }
-
-  private void desugarClassForD8(
-      DexProgramClass clazz, D8CfClassDesugaringEventConsumer desugaringEventConsumer) {
-    if (classDesugaring.needsDesugaring(clazz)) {
-      classDesugaring.desugar(clazz, desugaringEventConsumer);
-    }
-  }
-
   public void prepareDesugaringForD8(ExecutorService executorService) throws ExecutionException {
     // Prepare desugaring by collecting all the synthetic methods required on program classes.
     ProgramAdditions programAdditions = new ProgramAdditions();
@@ -686,7 +658,8 @@
     printPhase("Primary optimization pass");
 
     // Setup the argument propagator for the primary optimization pass.
-    appView.withArgumentPropagator(ArgumentPropagator::initializeCodeScanner);
+    appView.withArgumentPropagator(
+        argumentPropagator -> argumentPropagator.initializeCodeScanner(executorService, timing));
     appView.withCallSiteOptimizationInfoPropagator(
         optimization -> {
           optimization.abandonCallSitePropagationForLambdaImplementationMethods(
@@ -744,11 +717,9 @@
     // Analyze the data collected by the argument propagator, use the analysis result to update
     // the parameter optimization infos, and rewrite the application.
     appView.withArgumentPropagator(
-        argumentPropagator -> {
-          argumentPropagator.populateParameterOptimizationInfo(executorService);
-          argumentPropagator.optimizeMethodParameters();
-          argumentPropagator.enqueueMethodsForProcessing(postMethodProcessorBuilder);
-        });
+        argumentPropagator ->
+            argumentPropagator.tearDownCodeScanner(
+                postMethodProcessorBuilder, executorService, timing));
 
     if (libraryMethodOverrideAnalysis != null) {
       libraryMethodOverrideAnalysis.finish();
@@ -1618,7 +1589,7 @@
       MutableMethodConversionOptions conversionOptions,
       Timing timing) {
     appView.withArgumentPropagator(
-        argumentPropagator -> argumentPropagator.scan(method, code, methodProcessor));
+        argumentPropagator -> argumentPropagator.scan(method, code, methodProcessor, timing));
 
     if (enumUnboxer != null && methodProcessor.isPrimaryMethodProcessor()) {
       enumUnboxer.analyzeEnums(code, conversionOptions);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfClassDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/CfClassDesugaring.java
deleted file mode 100644
index a885344..0000000
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfClassDesugaring.java
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.ir.desugar;
-
-import com.android.tools.r8.graph.DexProgramClass;
-
-/** Interface for desugaring a class. */
-public interface CfClassDesugaring {
-
-  void desugar(DexProgramClass clazz, CfClassDesugaringEventConsumer eventConsumer);
-
-  /** Returns true if the given class needs desugaring. */
-  boolean needsDesugaring(DexProgramClass clazz);
-}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfClassDesugaringCollection.java b/src/main/java/com/android/tools/r8/ir/desugar/CfClassDesugaringCollection.java
deleted file mode 100644
index 1ddeb09..0000000
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfClassDesugaringCollection.java
+++ /dev/null
@@ -1,89 +0,0 @@
-// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.ir.desugar;
-
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.ir.desugar.records.RecordRewriter;
-import com.google.common.collect.Iterables;
-import java.util.ArrayList;
-import java.util.List;
-
-/** Interface for desugaring a class. */
-public abstract class CfClassDesugaringCollection {
-
-  public abstract void desugar(DexProgramClass clazz, CfClassDesugaringEventConsumer eventConsumer);
-
-  /** Returns true if the given class needs desugaring. */
-  public abstract boolean needsDesugaring(DexProgramClass clazz);
-
-  public abstract boolean isEmpty();
-
-  public static CfClassDesugaringCollection empty() {
-    return EmptyCfClassDesugaringCollection.getInstance();
-  }
-
-  public static CfClassDesugaringCollection create(AppView<?> appView) {
-    List<CfClassDesugaring> desugarings = new ArrayList<>();
-    RecordRewriter recordRewriter = RecordRewriter.create(appView);
-    if (recordRewriter != null) {
-      desugarings.add(recordRewriter);
-    }
-    if (desugarings.isEmpty()) {
-      return empty();
-    }
-    return new NonEmptyCfClassDesugaringCollection(desugarings);
-  }
-
-  public static class NonEmptyCfClassDesugaringCollection extends CfClassDesugaringCollection {
-    private final List<CfClassDesugaring> desugarings;
-
-    NonEmptyCfClassDesugaringCollection(List<CfClassDesugaring> desugarings) {
-      this.desugarings = desugarings;
-    }
-
-    @Override
-    public void desugar(DexProgramClass clazz, CfClassDesugaringEventConsumer eventConsumer) {
-      for (CfClassDesugaring desugaring : desugarings) {
-        desugaring.desugar(clazz, eventConsumer);
-      }
-    }
-
-    @Override
-    public boolean needsDesugaring(DexProgramClass clazz) {
-      return Iterables.any(desugarings, desugaring -> desugaring.needsDesugaring(clazz));
-    }
-
-    @Override
-    public boolean isEmpty() {
-      return false;
-    }
-  }
-
-  public static class EmptyCfClassDesugaringCollection extends CfClassDesugaringCollection {
-
-    private static final EmptyCfClassDesugaringCollection INSTANCE =
-        new EmptyCfClassDesugaringCollection();
-
-    public static EmptyCfClassDesugaringCollection getInstance() {
-      return INSTANCE;
-    }
-
-    @Override
-    public void desugar(DexProgramClass clazz, CfClassDesugaringEventConsumer eventConsumer) {
-      // Intentionally empty.
-    }
-
-    @Override
-    public boolean needsDesugaring(DexProgramClass clazz) {
-      return false;
-    }
-
-    @Override
-    public boolean isEmpty() {
-      return true;
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfClassDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/CfClassDesugaringEventConsumer.java
deleted file mode 100644
index ef09aba..0000000
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfClassDesugaringEventConsumer.java
+++ /dev/null
@@ -1,38 +0,0 @@
-// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.ir.desugar;
-
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.conversion.D8MethodProcessor;
-import com.android.tools.r8.ir.desugar.records.RecordDesugaringEventConsumer;
-
-public abstract class CfClassDesugaringEventConsumer implements RecordDesugaringEventConsumer {
-
-  public static D8CfClassDesugaringEventConsumer createForD8(D8MethodProcessor methodProcessor) {
-    return new D8CfClassDesugaringEventConsumer(methodProcessor);
-  }
-
-  public static class D8CfClassDesugaringEventConsumer extends CfClassDesugaringEventConsumer {
-
-    private final D8MethodProcessor methodProcessor;
-
-    public D8CfClassDesugaringEventConsumer(D8MethodProcessor methodProcessor) {
-      this.methodProcessor = methodProcessor;
-    }
-
-    @Override
-    public void acceptRecordClass(DexProgramClass recordClass) {
-      methodProcessor.scheduleDesugaredMethodsForProcessing(recordClass.programMethods());
-    }
-
-    @Override
-    public void acceptRecordMethod(ProgramMethod method) {
-      assert false;
-    }
-  }
-
-  // TODO(b/): Implement R8CfClassDesugaringEventConsumer
-}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfL8ClassSynthesizer.java b/src/main/java/com/android/tools/r8/ir/desugar/CfClassSynthesizerDesugaring.java
similarity index 66%
rename from src/main/java/com/android/tools/r8/ir/desugar/CfL8ClassSynthesizer.java
rename to src/main/java/com/android/tools/r8/ir/desugar/CfClassSynthesizerDesugaring.java
index 2eefa63..38fa09a 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfL8ClassSynthesizer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfClassSynthesizerDesugaring.java
@@ -4,7 +4,7 @@
 
 package com.android.tools.r8.ir.desugar;
 
-public interface CfL8ClassSynthesizer {
+public interface CfClassSynthesizerDesugaring {
 
-  void synthesizeClasses(CfL8ClassSynthesizerEventConsumer eventConsumer);
+  void synthesizeClasses(CfClassSynthesizerDesugaringEventConsumer eventConsumer);
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfClassSynthesizerDesugaringCollection.java b/src/main/java/com/android/tools/r8/ir/desugar/CfClassSynthesizerDesugaringCollection.java
new file mode 100644
index 0000000..b0ea9ef
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfClassSynthesizerDesugaringCollection.java
@@ -0,0 +1,80 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.ir.desugar;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryRetargeterL8Synthesizer;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryWrapperSynthesizer;
+import com.android.tools.r8.ir.desugar.desugaredlibrary.RetargetingInfo;
+import com.android.tools.r8.ir.desugar.itf.ProgramEmulatedInterfaceSynthesizer;
+import com.android.tools.r8.ir.desugar.records.RecordRewriter;
+import com.android.tools.r8.utils.ThreadUtils;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+public abstract class CfClassSynthesizerDesugaringCollection {
+
+  public static CfClassSynthesizerDesugaringCollection create(
+      AppView<?> appView, RetargetingInfo retargetingInfo) {
+    Collection<CfClassSynthesizerDesugaring> synthesizers = new ArrayList<>();
+    if (appView.options().isDesugaredLibraryCompilation()) {
+      ProgramEmulatedInterfaceSynthesizer emulatedInterfaceSynthesizer =
+          ProgramEmulatedInterfaceSynthesizer.create(appView);
+      if (emulatedInterfaceSynthesizer != null) {
+        synthesizers.add(emulatedInterfaceSynthesizer);
+      }
+      DesugaredLibraryRetargeterL8Synthesizer retargeterL8Synthesizer =
+          DesugaredLibraryRetargeterL8Synthesizer.create(appView, retargetingInfo);
+      if (retargeterL8Synthesizer != null) {
+        synthesizers.add(retargeterL8Synthesizer);
+      }
+      synthesizers.add(new DesugaredLibraryWrapperSynthesizer(appView));
+    }
+    RecordRewriter recordRewriter = RecordRewriter.create(appView);
+    if (recordRewriter != null) {
+      synthesizers.add(recordRewriter);
+    }
+    if (synthesizers.isEmpty()) {
+      return new EmptyCfClassSynthesizerCollection();
+    }
+    return new NonEmptyCfClassSynthesizerCollection(synthesizers);
+  }
+
+  public abstract void synthesizeClasses(
+      ExecutorService executorService, CfClassSynthesizerDesugaringEventConsumer eventConsumer)
+      throws ExecutionException;
+
+  static class NonEmptyCfClassSynthesizerCollection extends CfClassSynthesizerDesugaringCollection {
+
+    private final Collection<CfClassSynthesizerDesugaring> synthesizers;
+
+    public NonEmptyCfClassSynthesizerCollection(
+        Collection<CfClassSynthesizerDesugaring> synthesizers) {
+      assert !synthesizers.isEmpty();
+      this.synthesizers = synthesizers;
+    }
+
+    @Override
+    public void synthesizeClasses(
+        ExecutorService executorService, CfClassSynthesizerDesugaringEventConsumer eventConsumer)
+        throws ExecutionException {
+      ThreadUtils.processItems(
+          synthesizers,
+          synthesizer -> synthesizer.synthesizeClasses(eventConsumer),
+          executorService);
+    }
+  }
+
+  static class EmptyCfClassSynthesizerCollection extends CfClassSynthesizerDesugaringCollection {
+
+    @Override
+    public void synthesizeClasses(
+        ExecutorService executorService, CfClassSynthesizerDesugaringEventConsumer eventConsumer)
+        throws ExecutionException {
+      // Intentionally empty.
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfL8ClassSynthesizerEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/CfClassSynthesizerDesugaringEventConsumer.java
similarity index 70%
rename from src/main/java/com/android/tools/r8/ir/desugar/CfL8ClassSynthesizerEventConsumer.java
rename to src/main/java/com/android/tools/r8/ir/desugar/CfClassSynthesizerDesugaringEventConsumer.java
index 5e41810..3f60ffe 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfL8ClassSynthesizerEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfClassSynthesizerDesugaringEventConsumer.java
@@ -7,19 +7,21 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryRetargeterSynthesizerEventConsumer.DesugaredLibraryRetargeterL8SynthesizerEventConsumer;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryWrapperSynthesizerEventConsumer.DesugaredLibraryL8ProgramWrapperSynthesizerEventConsumer;
-import com.android.tools.r8.ir.desugar.itf.EmulatedInterfaceSynthesizerEventConsumer;
+import com.android.tools.r8.ir.desugar.itf.EmulatedInterfaceSynthesizerEventConsumer.L8ProgramEmulatedInterfaceSynthesizerEventConsumer;
+import com.android.tools.r8.ir.desugar.records.RecordDesugaringEventConsumer;
 import com.google.common.collect.Sets;
 import java.util.Set;
 
-public class CfL8ClassSynthesizerEventConsumer
-    implements EmulatedInterfaceSynthesizerEventConsumer,
+public class CfClassSynthesizerDesugaringEventConsumer
+    implements L8ProgramEmulatedInterfaceSynthesizerEventConsumer,
         DesugaredLibraryL8ProgramWrapperSynthesizerEventConsumer,
-        DesugaredLibraryRetargeterL8SynthesizerEventConsumer {
+        DesugaredLibraryRetargeterL8SynthesizerEventConsumer,
+        RecordDesugaringEventConsumer {
 
   private Set<DexProgramClass> synthesizedClasses = Sets.newConcurrentHashSet();
 
   @Override
-  public void acceptEmulatedInterface(DexProgramClass clazz) {
+  public void acceptProgramEmulatedInterface(DexProgramClass clazz) {
     synthesizedClasses.add(clazz);
   }
 
@@ -33,8 +35,12 @@
     synthesizedClasses.add(clazz);
   }
 
+  @Override
+  public void acceptRecordClass(DexProgramClass clazz) {
+    synthesizedClasses.add(clazz);
+  }
+
   public Set<DexProgramClass> getSynthesizedClasses() {
     return synthesizedClasses;
   }
-
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
index 6eda99c..4f6dd6c 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaringEventConsumer.java
@@ -18,11 +18,12 @@
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryWrapperSynthesizerEventConsumer.DesugaredLibraryAPIConverterEventConsumer;
 import com.android.tools.r8.ir.desugar.invokespecial.InvokeSpecialBridgeInfo;
 import com.android.tools.r8.ir.desugar.invokespecial.InvokeSpecialToSelfDesugaringEventConsumer;
+import com.android.tools.r8.ir.desugar.itf.EmulatedInterfaceSynthesizerEventConsumer.ClasspathEmulatedInterfaceSynthesizerEventConsumer;
 import com.android.tools.r8.ir.desugar.itf.InterfaceMethodDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.lambda.LambdaDeserializationMethodRemover;
 import com.android.tools.r8.ir.desugar.lambda.LambdaDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.nest.NestBasedAccessDesugaringEventConsumer;
-import com.android.tools.r8.ir.desugar.records.RecordDesugaringEventConsumer;
+import com.android.tools.r8.ir.desugar.records.RecordDesugaringEventConsumer.RecordInstructionDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.twr.TwrCloseResourceDesugaringEventConsumer;
 import com.android.tools.r8.shaking.Enqueuer.SyntheticAdditions;
 import com.google.common.collect.Sets;
@@ -45,11 +46,12 @@
         InvokeSpecialToSelfDesugaringEventConsumer,
         LambdaDesugaringEventConsumer,
         NestBasedAccessDesugaringEventConsumer,
-        RecordDesugaringEventConsumer,
+        RecordInstructionDesugaringEventConsumer,
         TwrCloseResourceDesugaringEventConsumer,
         InterfaceMethodDesugaringEventConsumer,
         DesugaredLibraryRetargeterInstructionEventConsumer,
-        DesugaredLibraryAPIConverterEventConsumer {
+        DesugaredLibraryAPIConverterEventConsumer,
+        ClasspathEmulatedInterfaceSynthesizerEventConsumer {
 
   public static D8CfInstructionDesugaringEventConsumer createForD8(
       D8MethodProcessor methodProcessor) {
@@ -69,6 +71,11 @@
     return new CfInstructionDesugaringEventConsumer() {
 
       @Override
+      public void acceptClasspathEmulatedInterface(DexClasspathClass clazz) {
+        assert false;
+      }
+
+      @Override
       public void acceptWrapperClasspathClass(DexClasspathClass clazz) {
         assert false;
       }
@@ -165,6 +172,11 @@
     }
 
     @Override
+    public void acceptClasspathEmulatedInterface(DexClasspathClass clazz) {
+      // Intentionally empty.
+    }
+
+    @Override
     public void acceptBackportedMethod(ProgramMethod backportedMethod, ProgramMethod context) {
       methodProcessor.scheduleMethodForProcessing(backportedMethod, this);
     }
@@ -326,19 +338,24 @@
     }
 
     @Override
+    public void acceptClasspathEmulatedInterface(DexClasspathClass clazz) {
+      additions.addLiveClasspathClass(clazz);
+    }
+
+    @Override
     public void acceptCompanionClassClinit(ProgramMethod method) {
       // TODO(b/183998768): Update this once desugaring is moved to the enqueuer.
     }
 
     @Override
     public void acceptRecordClass(DexProgramClass recordClass) {
-      // This is called each time an instruction or a class is found to require the record class.
-      assert false : "TODO(b/179146128): To be implemented";
+      // Intentionally empty. The class will be hit by tracing if required.
     }
 
     @Override
     public void acceptRecordMethod(ProgramMethod method) {
-      assert false : "TODO(b/179146128): To be implemented";
+      // Intentionally empty. The method will be hit by the tracing in R8 as if it was
+      // present in the input code, and thus nothing needs to be done.
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfL8ClassSynthesizerCollection.java b/src/main/java/com/android/tools/r8/ir/desugar/CfL8ClassSynthesizerCollection.java
deleted file mode 100644
index 9accf26..0000000
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfL8ClassSynthesizerCollection.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.ir.desugar;
-
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryRetargeterL8Synthesizer;
-import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryWrapperSynthesizer;
-import com.android.tools.r8.ir.desugar.desugaredlibrary.RetargetingInfo;
-import com.android.tools.r8.ir.desugar.itf.EmulatedInterfaceSynthesizer;
-import com.android.tools.r8.utils.ThreadUtils;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-
-public class CfL8ClassSynthesizerCollection {
-
-  private Collection<CfL8ClassSynthesizer> synthesizers = new ArrayList<>();
-
-  public CfL8ClassSynthesizerCollection(AppView<?> appView, RetargetingInfo retargetingInfo) {
-    assert appView.options().isDesugaredLibraryCompilation();
-    EmulatedInterfaceSynthesizer emulatedInterfaceSynthesizer =
-        EmulatedInterfaceSynthesizer.create(appView);
-    if (emulatedInterfaceSynthesizer != null) {
-      synthesizers.add(emulatedInterfaceSynthesizer);
-    }
-    DesugaredLibraryRetargeterL8Synthesizer retargeterL8Synthesizer =
-        DesugaredLibraryRetargeterL8Synthesizer.create(appView, retargetingInfo);
-    if (retargeterL8Synthesizer != null) {
-      synthesizers.add(retargeterL8Synthesizer);
-    }
-    synthesizers.add(new DesugaredLibraryWrapperSynthesizer(appView));
-  }
-
-  public void synthesizeClasses(
-      ExecutorService executorService, CfL8ClassSynthesizerEventConsumer eventConsumer)
-      throws ExecutionException {
-    ThreadUtils.processItems(
-        synthesizers, synthesizer -> synthesizer.synthesizeClasses(eventConsumer), executorService);
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfPostProcessingDesugaringCollection.java b/src/main/java/com/android/tools/r8/ir/desugar/CfPostProcessingDesugaringCollection.java
index 2d99720..dbba296 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfPostProcessingDesugaringCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfPostProcessingDesugaringCollection.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryRetargeterPostProcessor;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.RetargetingInfo;
 import com.android.tools.r8.ir.desugar.itf.InterfaceMethodProcessorFacade;
+import com.android.tools.r8.ir.desugar.records.RecordRewriter;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -69,6 +70,10 @@
       if (apiCallbackSynthesizor != null) {
         desugarings.add(apiCallbackSynthesizor);
       }
+      RecordRewriter recordRewriter = RecordRewriter.create(appView);
+      if (recordRewriter != null) {
+        desugarings.add(recordRewriter);
+      }
       if (desugarings.isEmpty()) {
         return empty();
       }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfPostProcessingDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/CfPostProcessingDesugaringEventConsumer.java
index 95407f8..66042a0 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfPostProcessingDesugaringEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfPostProcessingDesugaringEventConsumer.java
@@ -26,8 +26,8 @@
         DesugaredLibraryAPICallbackSynthesizorEventConsumer {
 
   public static D8CfPostProcessingDesugaringEventConsumer createForD8(
-      D8MethodProcessor methodProcessor) {
-    return new D8CfPostProcessingDesugaringEventConsumer(methodProcessor);
+      D8MethodProcessor methodProcessor, CfInstructionDesugaringCollection instructionDesugaring) {
+    return new D8CfPostProcessingDesugaringEventConsumer(methodProcessor, instructionDesugaring);
   }
 
   public static R8PostProcessingDesugaringEventConsumer createForR8(SyntheticAdditions additions) {
@@ -43,9 +43,21 @@
     // Methods cannot be processed directly because we cannot add method to classes while
     // concurrently processing other methods.
     private final ProgramMethodSet methodsToReprocess = ProgramMethodSet.createConcurrent();
+    private final CfInstructionDesugaringCollection instructionDesugaring;
 
-    private D8CfPostProcessingDesugaringEventConsumer(D8MethodProcessor methodProcessor) {
+    private D8CfPostProcessingDesugaringEventConsumer(
+        D8MethodProcessor methodProcessor,
+        CfInstructionDesugaringCollection instructionDesugaring) {
       this.methodProcessor = methodProcessor;
+      this.instructionDesugaring = instructionDesugaring;
+    }
+
+    private void addMethodToReprocess(ProgramMethod method) {
+      if (instructionDesugaring.needsDesugaring(method)) {
+        instructionDesugaring.needsDesugaring(method);
+      }
+      assert !instructionDesugaring.needsDesugaring(method);
+      methodsToReprocess.add(method);
     }
 
     @Override
@@ -60,7 +72,7 @@
 
     @Override
     public void acceptForwardingMethod(ProgramMethod method) {
-      methodsToReprocess.add(method);
+      addMethodToReprocess(method);
     }
 
     @Override
@@ -78,7 +90,7 @@
 
     @Override
     public void acceptAPIConversionCallback(ProgramMethod method) {
-      methodsToReprocess.add(method);
+      addMethodToReprocess(method);
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java b/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
index c4ce73e..b117828 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/NonEmptyCfInstructionDesugaringCollection.java
@@ -114,7 +114,6 @@
     }
     this.recordRewriter = RecordRewriter.create(appView);
     if (recordRewriter != null) {
-      assert !appView.enableWholeProgramOptimizations() : "To be implemented";
       desugarings.add(recordRewriter);
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryRetargeterL8Synthesizer.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryRetargeterL8Synthesizer.java
index dad4cd6..73cbd3d 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryRetargeterL8Synthesizer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryRetargeterL8Synthesizer.java
@@ -5,11 +5,11 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClassAndMethod;
-import com.android.tools.r8.ir.desugar.CfL8ClassSynthesizer;
-import com.android.tools.r8.ir.desugar.CfL8ClassSynthesizerEventConsumer;
+import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaring;
+import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaringEventConsumer;
 import com.android.tools.r8.utils.collections.DexClassAndMethodSet;
 
-public class DesugaredLibraryRetargeterL8Synthesizer implements CfL8ClassSynthesizer {
+public class DesugaredLibraryRetargeterL8Synthesizer implements CfClassSynthesizerDesugaring {
 
   private final AppView<?> appView;
   private final DesugaredLibraryRetargeterSyntheticHelper syntheticHelper;
@@ -33,7 +33,7 @@
   }
 
   @Override
-  public void synthesizeClasses(CfL8ClassSynthesizerEventConsumer eventConsumer) {
+  public void synthesizeClasses(CfClassSynthesizerDesugaringEventConsumer eventConsumer) {
     assert !emulatedDispatchMethods.isEmpty();
     for (DexClassAndMethod emulatedDispatchMethod : emulatedDispatchMethods) {
       syntheticHelper.ensureProgramEmulatedHolderDispatchMethod(
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryWrapperSynthesizer.java b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryWrapperSynthesizer.java
index 3e21feb..aa69845 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryWrapperSynthesizer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/desugaredlibrary/DesugaredLibraryWrapperSynthesizer.java
@@ -25,8 +25,8 @@
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
-import com.android.tools.r8.ir.desugar.CfL8ClassSynthesizer;
-import com.android.tools.r8.ir.desugar.CfL8ClassSynthesizerEventConsumer;
+import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaring;
+import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryWrapperSynthesizerEventConsumer.DesugaredLibraryClasspathWrapperSynthesizeEventConsumer;
 import com.android.tools.r8.ir.desugar.desugaredlibrary.DesugaredLibraryWrapperSynthesizerEventConsumer.DesugaredLibraryL8ProgramWrapperSynthesizerEventConsumer;
 import com.android.tools.r8.ir.synthetic.DesugaredLibraryAPIConversionCfCodeProvider.APIConverterConstructorCfCodeProvider;
@@ -93,7 +93,7 @@
 //     return new j$....BiFunction$-WRP(wrappedValue);
 //     }
 //   }
-public class DesugaredLibraryWrapperSynthesizer implements CfL8ClassSynthesizer {
+public class DesugaredLibraryWrapperSynthesizer implements CfClassSynthesizerDesugaring {
 
   private final AppView<?> appView;
   private final DexItemFactory factory;
@@ -633,7 +633,7 @@
   // the wrappers with the conversion methods only, then the virtual methods assuming the
   // conversion methods are present.
   @Override
-  public void synthesizeClasses(CfL8ClassSynthesizerEventConsumer eventConsumer) {
+  public void synthesizeClasses(CfClassSynthesizerDesugaringEventConsumer eventConsumer) {
     DesugaredLibraryConfiguration conf = appView.options().desugaredLibraryConfiguration;
     List<DexProgramClass> validClassesToWrap = new ArrayList<>();
     for (DexType type : conf.getWrapperConversions()) {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/EmulatedInterfaceSynthesizerEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/EmulatedInterfaceSynthesizerEventConsumer.java
index cb73851..ee8f0d8 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/EmulatedInterfaceSynthesizerEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/EmulatedInterfaceSynthesizerEventConsumer.java
@@ -4,9 +4,16 @@
 
 package com.android.tools.r8.ir.desugar.itf;
 
+import com.android.tools.r8.graph.DexClasspathClass;
 import com.android.tools.r8.graph.DexProgramClass;
 
 public interface EmulatedInterfaceSynthesizerEventConsumer {
 
-  void acceptEmulatedInterface(DexProgramClass clazz);
+  interface L8ProgramEmulatedInterfaceSynthesizerEventConsumer {
+    void acceptProgramEmulatedInterface(DexProgramClass clazz);
+  }
+
+  interface ClasspathEmulatedInterfaceSynthesizerEventConsumer {
+    void acceptClasspathEmulatedInterface(DexClasspathClass clazz);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceDesugaringSyntheticHelper.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceDesugaringSyntheticHelper.java
index 65816d4..92a64fe 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceDesugaringSyntheticHelper.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceDesugaringSyntheticHelper.java
@@ -32,6 +32,7 @@
 import com.android.tools.r8.graph.InvalidCode;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.desugar.itf.EmulatedInterfaceSynthesizerEventConsumer.ClasspathEmulatedInterfaceSynthesizerEventConsumer;
 import com.android.tools.r8.synthesis.SyntheticMethodBuilder;
 import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 import com.android.tools.r8.utils.InternalOptions;
@@ -127,8 +128,8 @@
     return true;
   }
 
-  public static DexMethod emulateInterfaceLibraryMethod(
-      DexClassAndMethod method, DexItemFactory factory) {
+  public DexMethod emulateInterfaceLibraryMethod(DexClassAndMethod method) {
+    DexItemFactory factory = appView.dexItemFactory();
     return factory.createMethod(
         getEmulateLibraryInterfaceClassType(method.getHolderType(), factory),
         factory.prependTypeToProto(method.getHolderType(), method.getProto()),
@@ -184,6 +185,42 @@
     return factory.createType(interfaceTypeDescriptor);
   }
 
+  DexClassAndMethod ensureEmulatedInterfaceMethod(
+      DexClassAndMethod method, ClasspathEmulatedInterfaceSynthesizerEventConsumer eventConsumer) {
+    DexMethod emulatedInterfaceMethod = emulateInterfaceLibraryMethod(method);
+    if (method.isProgramMethod()) {
+      assert appView.options().isDesugaredLibraryCompilation();
+      DexProgramClass emulatedInterface =
+          appView
+              .getSyntheticItems()
+              .getExistingFixedClass(
+                  SyntheticKind.EMULATED_INTERFACE_CLASS,
+                  method.asProgramMethod().getHolder(),
+                  appView);
+      return emulatedInterface.lookupProgramMethod(emulatedInterfaceMethod);
+    }
+    return appView
+        .getSyntheticItems()
+        .ensureFixedClasspathClassMethod(
+            emulatedInterfaceMethod.getName(),
+            emulatedInterfaceMethod.getProto(),
+            SyntheticKind.EMULATED_INTERFACE_CLASS,
+            method.getHolder().asClasspathOrLibraryClass(),
+            appView,
+            classBuilder -> {},
+            clazz -> {
+              // TODO(b/183998768): When interface method desugaring is cf to cf in R8, the
+              //  eventConsumer should always be non null.
+              if (eventConsumer != null) {
+                eventConsumer.acceptClasspathEmulatedInterface(clazz);
+              }
+            },
+            methodBuilder ->
+                methodBuilder
+                    .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
+                    .setCode(DexEncodedMethod::buildEmptyThrowingCfCode));
+  }
+
   DexClassAndMethod ensureDefaultAsMethodOfCompanionClassStub(DexClassAndMethod method) {
     if (method.isProgramMethod()) {
       return ensureDefaultAsMethodOfProgramCompanionClassStub(method.asProgramMethod());
@@ -323,6 +360,7 @@
             context,
             appView,
             classBuilder -> {},
+            ignored -> {},
             methodBuilder ->
                 methodBuilder
                     .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
index dfdfb45..8b3f70e 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodRewriter.java
@@ -447,7 +447,7 @@
         return rewriteInvokeDirect(invoke.getMethod(), context, rewriteInvoke);
       }
       return rewriteInvokeInterfaceOrInvokeVirtual(
-          invoke.getMethod(), invoke.isInterface(), rewriteInvoke);
+          invoke.getMethod(), invoke.isInterface(), rewriteInvoke, eventConsumer);
     }
     if (invoke.isInvokeStatic()) {
       Consumer<ProgramMethod> staticOutliningMethodConsumer =
@@ -643,7 +643,7 @@
       rewriteInvokeDirect(invoke.getInvokedMethod(), context, rewriteInvoke);
     } else if (instruction.isInvokeVirtual() || instruction.isInvokeInterface()) {
       rewriteInvokeInterfaceOrInvokeVirtual(
-          invoke.getInvokedMethod(), invoke.getInterfaceBit(), rewriteInvoke);
+          invoke.getInvokedMethod(), invoke.getInterfaceBit(), rewriteInvoke, null);
     } else {
       Function<SingleResolutionResult, Collection<CfInstruction>> rewriteToThrow =
           (resolutionResult) ->
@@ -971,7 +971,7 @@
             .asSingleResolution();
     if (resolution != null
         && (resolution.getResolvedHolder().isLibraryClass()
-            || appView.options().isDesugaredLibraryCompilation())) {
+            || helper.isEmulatedInterface(resolution.getResolvedHolder().type))) {
       DexClassAndMethod defaultMethod =
           appView.definitionFor(emulatedItf).lookupClassMethod(invokedMethod);
       if (defaultMethod != null && !helper.dontRewrite(defaultMethod)) {
@@ -985,12 +985,13 @@
   private Collection<CfInstruction> rewriteInvokeInterfaceOrInvokeVirtual(
       DexMethod invokedMethod,
       boolean interfaceBit,
-      Function<DexMethod, Collection<CfInstruction>> rewriteInvoke) {
+      Function<DexMethod, Collection<CfInstruction>> rewriteInvoke,
+      CfInstructionDesugaringEventConsumer eventConsumer) {
     DexClassAndMethod defaultMethod =
         defaultMethodForEmulatedDispatchOrNull(invokedMethod, interfaceBit);
     if (defaultMethod != null) {
       return rewriteInvoke.apply(
-          InterfaceDesugaringSyntheticHelper.emulateInterfaceLibraryMethod(defaultMethod, factory));
+          helper.ensureEmulatedInterfaceMethod(defaultMethod, eventConsumer).getReference());
     }
     return null;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceProcessor.java
index 1437d48..ac12741 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceProcessor.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.ir.desugar.itf;
 
-
 import com.android.tools.r8.cf.code.CfInstruction;
 import com.android.tools.r8.cf.code.CfInvoke;
 import com.android.tools.r8.code.Instruction;
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/EmulatedInterfaceSynthesizer.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/ProgramEmulatedInterfaceSynthesizer.java
similarity index 78%
rename from src/main/java/com/android/tools/r8/ir/desugar/itf/EmulatedInterfaceSynthesizer.java
rename to src/main/java/com/android/tools/r8/ir/desugar/itf/ProgramEmulatedInterfaceSynthesizer.java
index 5af4f7e..d68dec8 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/itf/EmulatedInterfaceSynthesizer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/ProgramEmulatedInterfaceSynthesizer.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.ir.desugar.itf;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethod;
@@ -12,11 +13,13 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.desugar.CfL8ClassSynthesizer;
-import com.android.tools.r8.ir.desugar.CfL8ClassSynthesizerEventConsumer;
+import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaring;
+import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaringEventConsumer;
+import com.android.tools.r8.ir.desugar.itf.EmulatedInterfaceSynthesizerEventConsumer.L8ProgramEmulatedInterfaceSynthesizerEventConsumer;
 import com.android.tools.r8.ir.synthetic.EmulateInterfaceSyntheticCfCodeProvider;
 import com.android.tools.r8.synthesis.SyntheticMethodBuilder;
 import com.android.tools.r8.synthesis.SyntheticNaming;
+import com.android.tools.r8.synthesis.SyntheticProgramClassBuilder;
 import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.google.common.collect.Iterables;
@@ -30,21 +33,21 @@
 import java.util.Map;
 import java.util.Set;
 
-public final class EmulatedInterfaceSynthesizer implements CfL8ClassSynthesizer {
+public final class ProgramEmulatedInterfaceSynthesizer implements CfClassSynthesizerDesugaring {
 
   private final AppView<?> appView;
   private final InterfaceDesugaringSyntheticHelper helper;
   private final Map<DexType, List<DexType>> emulatedInterfacesHierarchy;
 
-  public static EmulatedInterfaceSynthesizer create(AppView<?> appView) {
+  public static ProgramEmulatedInterfaceSynthesizer create(AppView<?> appView) {
     if (!appView.options().isDesugaredLibraryCompilation()
         || appView.options().desugaredLibraryConfiguration.getEmulateLibraryInterface().isEmpty()) {
       return null;
     }
-    return new EmulatedInterfaceSynthesizer(appView);
+    return new ProgramEmulatedInterfaceSynthesizer(appView);
   }
 
-  EmulatedInterfaceSynthesizer(AppView<?> appView) {
+  public ProgramEmulatedInterfaceSynthesizer(AppView<?> appView) {
     this.appView = appView;
     helper = new InterfaceDesugaringSyntheticHelper(appView);
     // Avoid the computation outside L8 since it is not needed.
@@ -92,35 +95,48 @@
     }
   }
 
-  DexProgramClass ensureEmulateInterfaceLibrary(
-      DexProgramClass emulatedInterface, EmulatedInterfaceSynthesizerEventConsumer eventConsumer) {
+  DexProgramClass synthesizeProgramEmulatedInterface(
+      DexProgramClass emulatedInterface,
+      L8ProgramEmulatedInterfaceSynthesizerEventConsumer eventConsumer) {
+    return appView
+        .getSyntheticItems()
+        .ensureFixedClass(
+            SyntheticNaming.SyntheticKind.EMULATED_INTERFACE_CLASS,
+            emulatedInterface,
+            appView,
+            builder -> synthesizeEmulateInterfaceMethods(emulatedInterface, builder),
+            eventConsumer::acceptProgramEmulatedInterface);
+  }
+
+  private void synthesizeEmulateInterfaceMethods(
+      DexProgramClass emulatedInterface, SyntheticProgramClassBuilder builder) {
     assert helper.isEmulatedInterface(emulatedInterface.type);
-    DexProgramClass emulateInterfaceClass =
-        appView
-            .getSyntheticItems()
-            .ensureFixedClass(
-                SyntheticNaming.SyntheticKind.EMULATED_INTERFACE_CLASS,
-                emulatedInterface,
-                appView,
-                builder ->
-                    emulatedInterface.forEachProgramMethodMatching(
-                        DexEncodedMethod::isDefaultMethod,
-                        method ->
-                            builder.addMethod(
-                                methodBuilder ->
-                                    synthesizeEmulatedInterfaceMethod(
-                                        method, emulatedInterface, methodBuilder))),
-                ignored -> {});
-    eventConsumer.acceptEmulatedInterface(emulateInterfaceClass);
-    assert emulateInterfaceClass.getType()
+    emulatedInterface.forEachProgramVirtualMethodMatching(
+        DexEncodedMethod::isDefaultMethod,
+        method ->
+            builder.addMethod(
+                methodBuilder ->
+                    synthesizeEmulatedInterfaceMethod(method, emulatedInterface, methodBuilder)));
+    assert builder.getType()
         == InterfaceDesugaringSyntheticHelper.getEmulateLibraryInterfaceClassType(
             emulatedInterface.type, appView.dexItemFactory());
-    return emulateInterfaceClass;
   }
 
   private void synthesizeEmulatedInterfaceMethod(
       ProgramMethod method, DexProgramClass theInterface, SyntheticMethodBuilder methodBuilder) {
     assert !method.getDefinition().isStatic();
+    DexMethod emulatedMethod = helper.emulateInterfaceLibraryMethod(method);
+    methodBuilder
+        .setName(emulatedMethod.getName())
+        .setProto(emulatedMethod.getProto())
+        .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
+        .setCode(
+            emulatedInterfaceMethod ->
+                synthesizeCfCode(method.asProgramMethod(), theInterface, emulatedInterfaceMethod));
+  }
+
+  private CfCode synthesizeCfCode(
+      ProgramMethod method, DexProgramClass theInterface, DexMethod emulatedInterfaceMethod) {
     DexMethod libraryMethod =
         method
             .getReference()
@@ -129,23 +145,14 @@
         helper.ensureDefaultAsMethodOfProgramCompanionClassStub(method).getReference();
     List<Pair<DexType, DexMethod>> extraDispatchCases =
         getDispatchCases(method, theInterface, companionMethod);
-    DexMethod emulatedMethod =
-        InterfaceDesugaringSyntheticHelper.emulateInterfaceLibraryMethod(
-            method, appView.dexItemFactory());
-    methodBuilder
-        .setName(emulatedMethod.getName())
-        .setProto(emulatedMethod.getProto())
-        .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
-        .setCode(
-            emulatedInterfaceMethod ->
-                new EmulateInterfaceSyntheticCfCodeProvider(
-                        emulatedInterfaceMethod.getHolderType(),
-                        method.getHolderType(),
-                        companionMethod,
-                        libraryMethod,
-                        extraDispatchCases,
-                        appView)
-                    .generateCfCode());
+    return new EmulateInterfaceSyntheticCfCodeProvider(
+            emulatedInterfaceMethod.getHolderType(),
+            method.getHolderType(),
+            companionMethod,
+            libraryMethod,
+            extraDispatchCases,
+            appView)
+        .generateCfCode();
   }
 
   private List<Pair<DexType, DexMethod>> getDispatchCases(
@@ -219,7 +226,7 @@
   }
 
   @Override
-  public void synthesizeClasses(CfL8ClassSynthesizerEventConsumer eventConsumer) {
+  public void synthesizeClasses(CfClassSynthesizerDesugaringEventConsumer eventConsumer) {
     assert appView.options().isDesugaredLibraryCompilation();
     for (DexType emulatedInterfaceType : helper.getEmulatedInterfaces()) {
       DexClass emulatedInterfaceClazz = appView.definitionFor(emulatedInterfaceType);
@@ -231,12 +238,12 @@
       assert emulatedInterface != null;
       if (!appView.isAlreadyLibraryDesugared(emulatedInterface)
           && needsEmulateInterfaceLibrary(emulatedInterface)) {
-        ensureEmulateInterfaceLibrary(emulatedInterface, eventConsumer);
+        synthesizeProgramEmulatedInterface(emulatedInterface, eventConsumer);
       }
     }
   }
 
-  private boolean needsEmulateInterfaceLibrary(DexProgramClass emulatedInterface) {
+  private boolean needsEmulateInterfaceLibrary(DexClass emulatedInterface) {
     return Iterables.any(emulatedInterface.methods(), DexEncodedMethod::isDefaultMethod);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordCfMethods.java b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordCfMethods.java
index 0de85b5..adab36a 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordCfMethods.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordCfMethods.java
@@ -11,7 +11,6 @@
 import com.android.tools.r8.cf.code.CfArithmeticBinop;
 import com.android.tools.r8.cf.code.CfArrayLength;
 import com.android.tools.r8.cf.code.CfArrayLoad;
-import com.android.tools.r8.cf.code.CfCheckCast;
 import com.android.tools.r8.cf.code.CfConstNumber;
 import com.android.tools.r8.cf.code.CfConstString;
 import com.android.tools.r8.cf.code.CfFrame;
@@ -44,123 +43,22 @@
 public final class RecordCfMethods {
 
   public static void registerSynthesizedCodeReferences(DexItemFactory factory) {
-    factory.createSynthesizedType("Ljava/lang/Record;");
     factory.createSynthesizedType("Ljava/util/Arrays;");
     factory.createSynthesizedType("[Ljava/lang/Object;");
     factory.createSynthesizedType("[Ljava/lang/String;");
   }
 
-  public static CfCode RecordMethods_equals(InternalOptions options, DexMethod method) {
-    CfLabel label0 = new CfLabel();
-    CfLabel label1 = new CfLabel();
-    CfLabel label2 = new CfLabel();
-    CfLabel label3 = new CfLabel();
-    CfLabel label4 = new CfLabel();
-    CfLabel label5 = new CfLabel();
-    return new CfCode(
-        method.holder,
-        2,
-        2,
-        ImmutableList.of(
-            label0,
-            new CfLoad(ValueType.OBJECT, 0),
-            new CfInvoke(
-                182,
-                options.itemFactory.createMethod(
-                    options.itemFactory.objectType,
-                    options.itemFactory.createProto(options.itemFactory.classType),
-                    options.itemFactory.createString("getClass")),
-                false),
-            new CfLoad(ValueType.OBJECT, 1),
-            new CfInvoke(
-                182,
-                options.itemFactory.createMethod(
-                    options.itemFactory.objectType,
-                    options.itemFactory.createProto(options.itemFactory.classType),
-                    options.itemFactory.createString("getClass")),
-                false),
-            new CfIfCmp(If.Type.NE, ValueType.OBJECT, label3),
-            new CfLoad(ValueType.OBJECT, 1),
-            new CfCheckCast(options.itemFactory.createType("Ljava/lang/Record;")),
-            label1,
-            new CfInvoke(
-                182,
-                options.itemFactory.createMethod(
-                    options.itemFactory.createType("Ljava/lang/Record;"),
-                    options.itemFactory.createProto(
-                        options.itemFactory.createType("[Ljava/lang/Object;")),
-                    options.itemFactory.createString("$record$getFieldsAsObjects")),
-                false),
-            new CfLoad(ValueType.OBJECT, 0),
-            new CfInvoke(
-                182,
-                options.itemFactory.createMethod(
-                    options.itemFactory.createType("Ljava/lang/Record;"),
-                    options.itemFactory.createProto(
-                        options.itemFactory.createType("[Ljava/lang/Object;")),
-                    options.itemFactory.createString("$record$getFieldsAsObjects")),
-                false),
-            label2,
-            new CfInvoke(
-                184,
-                options.itemFactory.createMethod(
-                    options.itemFactory.createType("Ljava/util/Arrays;"),
-                    options.itemFactory.createProto(
-                        options.itemFactory.booleanType,
-                        options.itemFactory.createType("[Ljava/lang/Object;"),
-                        options.itemFactory.createType("[Ljava/lang/Object;")),
-                    options.itemFactory.createString("equals")),
-                false),
-            new CfIf(If.Type.EQ, ValueType.INT, label3),
-            new CfConstNumber(1, ValueType.INT),
-            new CfGoto(label4),
-            label3,
-            new CfFrame(
-                new Int2ReferenceAVLTreeMap<>(
-                    new int[] {0, 1},
-                    new FrameType[] {
-                      FrameType.initialized(options.itemFactory.createType("Ljava/lang/Record;")),
-                      FrameType.initialized(options.itemFactory.objectType)
-                    }),
-                new ArrayDeque<>(Arrays.asList())),
-            new CfConstNumber(0, ValueType.INT),
-            label4,
-            new CfFrame(
-                new Int2ReferenceAVLTreeMap<>(
-                    new int[] {0, 1},
-                    new FrameType[] {
-                      FrameType.initialized(options.itemFactory.createType("Ljava/lang/Record;")),
-                      FrameType.initialized(options.itemFactory.objectType)
-                    }),
-                new ArrayDeque<>(
-                    Arrays.asList(FrameType.initialized(options.itemFactory.intType)))),
-            new CfReturn(ValueType.INT),
-            label5),
-        ImmutableList.of(),
-        ImmutableList.of());
-  }
-
   public static CfCode RecordMethods_hashCode(InternalOptions options, DexMethod method) {
     CfLabel label0 = new CfLabel();
     CfLabel label1 = new CfLabel();
-    CfLabel label2 = new CfLabel();
-    CfLabel label3 = new CfLabel();
     return new CfCode(
         method.holder,
         2,
-        1,
+        2,
         ImmutableList.of(
             label0,
             new CfConstNumber(31, ValueType.INT),
-            new CfLoad(ValueType.OBJECT, 0),
-            new CfInvoke(
-                182,
-                options.itemFactory.createMethod(
-                    options.itemFactory.createType("Ljava/lang/Record;"),
-                    options.itemFactory.createProto(
-                        options.itemFactory.createType("[Ljava/lang/Object;")),
-                    options.itemFactory.createString("$record$getFieldsAsObjects")),
-                false),
+            new CfLoad(ValueType.OBJECT, 1),
             new CfInvoke(
                 184,
                 options.itemFactory.createMethod(
@@ -172,14 +70,6 @@
                 false),
             new CfArithmeticBinop(CfArithmeticBinop.Opcode.Mul, NumericType.INT),
             new CfLoad(ValueType.OBJECT, 0),
-            label1,
-            new CfInvoke(
-                182,
-                options.itemFactory.createMethod(
-                    options.itemFactory.objectType,
-                    options.itemFactory.createProto(options.itemFactory.classType),
-                    options.itemFactory.createString("getClass")),
-                false),
             new CfInvoke(
                 182,
                 options.itemFactory.createMethod(
@@ -188,9 +78,8 @@
                     options.itemFactory.createString("hashCode")),
                 false),
             new CfArithmeticBinop(CfArithmeticBinop.Opcode.Add, NumericType.INT),
-            label2,
             new CfReturn(ValueType.INT),
-            label3),
+            label1),
         ImmutableList.of(),
         ImmutableList.of());
   }
@@ -210,11 +99,10 @@
     CfLabel label11 = new CfLabel();
     CfLabel label12 = new CfLabel();
     CfLabel label13 = new CfLabel();
-    CfLabel label14 = new CfLabel();
     return new CfCode(
         method.holder,
         3,
-        7,
+        6,
         ImmutableList.of(
             label0,
             new CfLoad(ValueType.OBJECT, 2),
@@ -234,7 +122,7 @@
                 new Int2ReferenceAVLTreeMap<>(
                     new int[] {0, 1, 2},
                     new FrameType[] {
-                      FrameType.initialized(options.itemFactory.createType("Ljava/lang/Record;")),
+                      FrameType.initialized(options.itemFactory.createType("[Ljava/lang/Object;")),
                       FrameType.initialized(options.itemFactory.stringType),
                       FrameType.initialized(options.itemFactory.stringType)
                     }),
@@ -255,7 +143,7 @@
                 new Int2ReferenceAVLTreeMap<>(
                     new int[] {0, 1, 2},
                     new FrameType[] {
-                      FrameType.initialized(options.itemFactory.createType("Ljava/lang/Record;")),
+                      FrameType.initialized(options.itemFactory.createType("[Ljava/lang/Object;")),
                       FrameType.initialized(options.itemFactory.stringType),
                       FrameType.initialized(options.itemFactory.stringType)
                     }),
@@ -265,17 +153,6 @@
                             options.itemFactory.createType("[Ljava/lang/String;"))))),
             new CfStore(ValueType.OBJECT, 3),
             label3,
-            new CfLoad(ValueType.OBJECT, 0),
-            new CfInvoke(
-                182,
-                options.itemFactory.createMethod(
-                    options.itemFactory.createType("Ljava/lang/Record;"),
-                    options.itemFactory.createProto(
-                        options.itemFactory.createType("[Ljava/lang/Object;")),
-                    options.itemFactory.createString("$record$getFieldsAsObjects")),
-                false),
-            new CfStore(ValueType.OBJECT, 4),
-            label4,
             new CfNew(options.itemFactory.stringBuilderType),
             new CfStackInstruction(CfStackInstruction.Opcode.Dup),
             new CfInvoke(
@@ -285,9 +162,9 @@
                     options.itemFactory.createProto(options.itemFactory.voidType),
                     options.itemFactory.createString("<init>")),
                 false),
-            new CfStore(ValueType.OBJECT, 5),
-            label5,
-            new CfLoad(ValueType.OBJECT, 5),
+            new CfStore(ValueType.OBJECT, 4),
+            label4,
+            new CfLoad(ValueType.OBJECT, 4),
             new CfLoad(ValueType.OBJECT, 1),
             new CfInvoke(
                 182,
@@ -307,31 +184,30 @@
                     options.itemFactory.createString("append")),
                 false),
             new CfStackInstruction(CfStackInstruction.Opcode.Pop),
-            label6,
+            label5,
             new CfConstNumber(0, ValueType.INT),
-            new CfStore(ValueType.INT, 6),
-            label7,
+            new CfStore(ValueType.INT, 5),
+            label6,
             new CfFrame(
                 new Int2ReferenceAVLTreeMap<>(
-                    new int[] {0, 1, 2, 3, 4, 5, 6},
+                    new int[] {0, 1, 2, 3, 4, 5},
                     new FrameType[] {
-                      FrameType.initialized(options.itemFactory.createType("Ljava/lang/Record;")),
+                      FrameType.initialized(options.itemFactory.createType("[Ljava/lang/Object;")),
                       FrameType.initialized(options.itemFactory.stringType),
                       FrameType.initialized(options.itemFactory.stringType),
                       FrameType.initialized(options.itemFactory.createType("[Ljava/lang/String;")),
-                      FrameType.initialized(options.itemFactory.createType("[Ljava/lang/Object;")),
                       FrameType.initialized(options.itemFactory.stringBuilderType),
                       FrameType.initialized(options.itemFactory.intType)
                     }),
                 new ArrayDeque<>(Arrays.asList())),
-            new CfLoad(ValueType.INT, 6),
+            new CfLoad(ValueType.INT, 5),
             new CfLoad(ValueType.OBJECT, 3),
             new CfArrayLength(),
-            new CfIfCmp(If.Type.GE, ValueType.INT, label12),
-            label8,
-            new CfLoad(ValueType.OBJECT, 5),
+            new CfIfCmp(If.Type.GE, ValueType.INT, label11),
+            label7,
+            new CfLoad(ValueType.OBJECT, 4),
             new CfLoad(ValueType.OBJECT, 3),
-            new CfLoad(ValueType.INT, 6),
+            new CfLoad(ValueType.INT, 5),
             new CfArrayLoad(MemberType.OBJECT),
             new CfInvoke(
                 182,
@@ -350,8 +226,8 @@
                         options.itemFactory.stringBuilderType, options.itemFactory.stringType),
                     options.itemFactory.createString("append")),
                 false),
-            new CfLoad(ValueType.OBJECT, 4),
-            new CfLoad(ValueType.INT, 6),
+            new CfLoad(ValueType.OBJECT, 0),
+            new CfLoad(ValueType.INT, 5),
             new CfArrayLoad(MemberType.OBJECT),
             new CfInvoke(
                 182,
@@ -362,15 +238,15 @@
                     options.itemFactory.createString("append")),
                 false),
             new CfStackInstruction(CfStackInstruction.Opcode.Pop),
-            label9,
-            new CfLoad(ValueType.INT, 6),
+            label8,
+            new CfLoad(ValueType.INT, 5),
             new CfLoad(ValueType.OBJECT, 3),
             new CfArrayLength(),
             new CfConstNumber(1, ValueType.INT),
             new CfArithmeticBinop(CfArithmeticBinop.Opcode.Sub, NumericType.INT),
-            new CfIfCmp(If.Type.EQ, ValueType.INT, label11),
-            label10,
-            new CfLoad(ValueType.OBJECT, 5),
+            new CfIfCmp(If.Type.EQ, ValueType.INT, label10),
+            label9,
+            new CfLoad(ValueType.OBJECT, 4),
             new CfConstString(options.itemFactory.createString(", ")),
             new CfInvoke(
                 182,
@@ -381,36 +257,34 @@
                     options.itemFactory.createString("append")),
                 false),
             new CfStackInstruction(CfStackInstruction.Opcode.Pop),
-            label11,
-            new CfFrame(
-                new Int2ReferenceAVLTreeMap<>(
-                    new int[] {0, 1, 2, 3, 4, 5, 6},
-                    new FrameType[] {
-                      FrameType.initialized(options.itemFactory.createType("Ljava/lang/Record;")),
-                      FrameType.initialized(options.itemFactory.stringType),
-                      FrameType.initialized(options.itemFactory.stringType),
-                      FrameType.initialized(options.itemFactory.createType("[Ljava/lang/String;")),
-                      FrameType.initialized(options.itemFactory.createType("[Ljava/lang/Object;")),
-                      FrameType.initialized(options.itemFactory.stringBuilderType),
-                      FrameType.initialized(options.itemFactory.intType)
-                    }),
-                new ArrayDeque<>(Arrays.asList())),
-            new CfIinc(6, 1),
-            new CfGoto(label7),
-            label12,
+            label10,
             new CfFrame(
                 new Int2ReferenceAVLTreeMap<>(
                     new int[] {0, 1, 2, 3, 4, 5},
                     new FrameType[] {
-                      FrameType.initialized(options.itemFactory.createType("Ljava/lang/Record;")),
+                      FrameType.initialized(options.itemFactory.createType("[Ljava/lang/Object;")),
                       FrameType.initialized(options.itemFactory.stringType),
                       FrameType.initialized(options.itemFactory.stringType),
                       FrameType.initialized(options.itemFactory.createType("[Ljava/lang/String;")),
+                      FrameType.initialized(options.itemFactory.stringBuilderType),
+                      FrameType.initialized(options.itemFactory.intType)
+                    }),
+                new ArrayDeque<>(Arrays.asList())),
+            new CfIinc(5, 1),
+            new CfGoto(label6),
+            label11,
+            new CfFrame(
+                new Int2ReferenceAVLTreeMap<>(
+                    new int[] {0, 1, 2, 3, 4},
+                    new FrameType[] {
                       FrameType.initialized(options.itemFactory.createType("[Ljava/lang/Object;")),
+                      FrameType.initialized(options.itemFactory.stringType),
+                      FrameType.initialized(options.itemFactory.stringType),
+                      FrameType.initialized(options.itemFactory.createType("[Ljava/lang/String;")),
                       FrameType.initialized(options.itemFactory.stringBuilderType)
                     }),
                 new ArrayDeque<>(Arrays.asList())),
-            new CfLoad(ValueType.OBJECT, 5),
+            new CfLoad(ValueType.OBJECT, 4),
             new CfConstString(options.itemFactory.createString("]")),
             new CfInvoke(
                 182,
@@ -421,8 +295,8 @@
                     options.itemFactory.createString("append")),
                 false),
             new CfStackInstruction(CfStackInstruction.Opcode.Pop),
-            label13,
-            new CfLoad(ValueType.OBJECT, 5),
+            label12,
+            new CfLoad(ValueType.OBJECT, 4),
             new CfInvoke(
                 182,
                 options.itemFactory.createMethod(
@@ -431,7 +305,7 @@
                     options.itemFactory.createString("toString")),
                 false),
             new CfReturn(ValueType.OBJECT),
-            label14),
+            label13),
         ImmutableList.of(),
         ImmutableList.of());
   }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordDesugaringEventConsumer.java
index 5affc2b..f81b5d6 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordDesugaringEventConsumer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordDesugaringEventConsumer.java
@@ -1,7 +1,6 @@
 // Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
-
 package com.android.tools.r8.ir.desugar.records;
 
 import com.android.tools.r8.graph.DexProgramClass;
@@ -11,5 +10,8 @@
 
   void acceptRecordClass(DexProgramClass recordClass);
 
-  void acceptRecordMethod(ProgramMethod method);
+  interface RecordInstructionDesugaringEventConsumer extends RecordDesugaringEventConsumer {
+
+    void acceptRecordMethod(ProgramMethod method);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordRewriter.java
index 5333665..39a6ae1 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/records/RecordRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/records/RecordRewriter.java
@@ -4,11 +4,15 @@
 
 package com.android.tools.r8.ir.desugar.records;
 
+import static com.android.tools.r8.cf.code.CfStackInstruction.Opcode.Dup;
+import static com.android.tools.r8.cf.code.CfStackInstruction.Opcode.Swap;
+
 import com.android.tools.r8.cf.code.CfConstString;
 import com.android.tools.r8.cf.code.CfFieldInstruction;
 import com.android.tools.r8.cf.code.CfInstruction;
 import com.android.tools.r8.cf.code.CfInvoke;
 import com.android.tools.r8.cf.code.CfInvokeDynamic;
+import com.android.tools.r8.cf.code.CfStackInstruction;
 import com.android.tools.r8.cf.code.CfTypeInstruction;
 import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
 import com.android.tools.r8.dex.Constants;
@@ -35,14 +39,20 @@
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.desugar.CfClassDesugaring;
-import com.android.tools.r8.ir.desugar.CfClassDesugaringEventConsumer;
+import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaring;
+import com.android.tools.r8.ir.desugar.CfClassSynthesizerDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer;
+import com.android.tools.r8.ir.desugar.CfPostProcessingDesugaring;
+import com.android.tools.r8.ir.desugar.CfPostProcessingDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.FreshLocalProvider;
 import com.android.tools.r8.ir.desugar.LocalStackAllocator;
+import com.android.tools.r8.ir.desugar.ProgramAdditions;
+import com.android.tools.r8.ir.desugar.records.RecordDesugaringEventConsumer.RecordInstructionDesugaringEventConsumer;
 import com.android.tools.r8.ir.synthetic.CallObjectInitCfCodeProvider;
-import com.android.tools.r8.ir.synthetic.RecordGetFieldsAsObjectsCfCodeProvider;
+import com.android.tools.r8.ir.synthetic.RecordCfCodeProvider.RecordEqualsCfCodeProvider;
+import com.android.tools.r8.ir.synthetic.RecordCfCodeProvider.RecordGetFieldsAsObjectsCfCodeProvider;
+import com.android.tools.r8.ir.synthetic.SyntheticCfCodeProvider;
 import com.android.tools.r8.naming.dexitembasedstring.ClassNameComputationInfo;
 import com.android.tools.r8.synthesis.SyntheticNaming;
 import com.android.tools.r8.utils.InternalOptions;
@@ -51,18 +61,21 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
 import java.util.function.BiFunction;
 import org.objectweb.asm.Opcodes;
 
-public class RecordRewriter implements CfInstructionDesugaring, CfClassDesugaring {
+public class RecordRewriter
+    implements CfInstructionDesugaring, CfClassSynthesizerDesugaring, CfPostProcessingDesugaring {
 
   private final AppView<?> appView;
   private final DexItemFactory factory;
   private final DexProto recordToStringHelperProto;
-  private final DexProto recordEqualsHelperProto;
   private final DexProto recordHashCodeHelperProto;
 
   public static final String GET_FIELDS_AS_OBJECTS_METHOD_NAME = "$record$getFieldsAsObjects";
+  public static final String EQUALS_RECORD_METHOD_NAME = "$record$equals";
 
   public static RecordRewriter create(AppView<?> appView) {
     return appView.options().shouldDesugarRecords() ? new RecordRewriter(appView) : null;
@@ -71,6 +84,7 @@
   public static void registerSynthesizedCodeReferences(DexItemFactory factory) {
     RecordCfMethods.registerSynthesizedCodeReferences(factory);
     RecordGetFieldsAsObjectsCfCodeProvider.registerSynthesizedCodeReferences(factory);
+    RecordEqualsCfCodeProvider.registerSynthesizedCodeReferences(factory);
   }
 
   private RecordRewriter(AppView<?> appView) {
@@ -78,10 +92,34 @@
     factory = appView.dexItemFactory();
     recordToStringHelperProto =
         factory.createProto(
-            factory.stringType, factory.recordType, factory.stringType, factory.stringType);
-    recordEqualsHelperProto =
-        factory.createProto(factory.booleanType, factory.recordType, factory.objectType);
-    recordHashCodeHelperProto = factory.createProto(factory.intType, factory.recordType);
+            factory.stringType, factory.objectArrayType, factory.stringType, factory.stringType);
+    recordHashCodeHelperProto =
+        factory.createProto(factory.intType, factory.classType, factory.objectArrayType);
+  }
+
+  @Override
+  public void prepare(ProgramMethod method, ProgramAdditions programAdditions) {
+    CfCode cfCode = method.getDefinition().getCode().asCfCode();
+    for (CfInstruction instruction : cfCode.getInstructions()) {
+      if (instruction.isInvokeDynamic() && needsDesugaring(instruction, method)) {
+        prepareInvokeDynamicOnRecord(instruction.asInvokeDynamic(), method, programAdditions);
+      }
+    }
+  }
+
+  private void prepareInvokeDynamicOnRecord(
+      CfInvokeDynamic invokeDynamic, ProgramMethod context, ProgramAdditions programAdditions) {
+    RecordInvokeDynamic recordInvokeDynamic = parseInvokeDynamicOnRecord(invokeDynamic, context);
+    if (recordInvokeDynamic.getMethodName() == factory.toStringMethodName
+        || recordInvokeDynamic.getMethodName() == factory.hashCodeMethodName) {
+      ensureGetFieldsAsObjects(recordInvokeDynamic, programAdditions);
+      return;
+    }
+    if (recordInvokeDynamic.getMethodName() == factory.equalsMethodName) {
+      ensureEqualsRecord(recordInvokeDynamic, programAdditions);
+      return;
+    }
+    throw new Unreachable("Invoke dynamic needs record desugaring but could not be desugared.");
   }
 
   @Override
@@ -103,21 +141,21 @@
     if (instruction.isInvoke()) {
       CfInvoke cfInvoke = instruction.asInvoke();
       if (refersToRecord(cfInvoke.getMethod())) {
-        requiresRecordClass(eventConsumer);
+        ensureRecordClass(eventConsumer);
       }
       return;
     }
     if (instruction.isFieldInstruction()) {
       CfFieldInstruction fieldInstruction = instruction.asFieldInstruction();
       if (refersToRecord(fieldInstruction.getField())) {
-        requiresRecordClass(eventConsumer);
+        ensureRecordClass(eventConsumer);
       }
       return;
     }
     if (instruction.isTypeInstruction()) {
       CfTypeInstruction typeInstruction = instruction.asTypeInstruction();
       if (refersToRecord(typeInstruction.getType())) {
-        requiresRecordClass(eventConsumer);
+        ensureRecordClass(eventConsumer);
       }
       return;
     }
@@ -134,7 +172,10 @@
       MethodProcessingContext methodProcessingContext,
       DexItemFactory dexItemFactory) {
     assert !instruction.isInitClass();
-    if (instruction.isInvokeDynamic() && needsDesugaring(instruction.asInvokeDynamic(), context)) {
+    if (!needsDesugaring(instruction, context)) {
+      return null;
+    }
+    if (instruction.isInvokeDynamic()) {
       return desugarInvokeDynamicOnRecord(
           instruction.asInvokeDynamic(),
           localStackAllocator,
@@ -142,24 +183,52 @@
           eventConsumer,
           methodProcessingContext);
     }
-    if (instruction.isInvoke()) {
-      CfInvoke cfInvoke = instruction.asInvoke();
-      DexMethod newMethod =
-          rewriteMethod(cfInvoke.getMethod(), cfInvoke.isInvokeSuper(context.getHolderType()));
-      if (newMethod != cfInvoke.getMethod()) {
-        return Collections.singletonList(
-            new CfInvoke(cfInvoke.getOpcode(), newMethod, cfInvoke.isInterface()));
-      }
-    }
-    return null;
+    assert instruction.isInvoke();
+    CfInvoke cfInvoke = instruction.asInvoke();
+    DexMethod newMethod =
+        rewriteMethod(cfInvoke.getMethod(), cfInvoke.isInvokeSuper(context.getHolderType()));
+    assert newMethod != cfInvoke.getMethod();
+    return Collections.singletonList(
+        new CfInvoke(cfInvoke.getOpcode(), newMethod, cfInvoke.isInterface()));
   }
 
-  public List<CfInstruction> desugarInvokeDynamicOnRecord(
-      CfInvokeDynamic invokeDynamic,
-      LocalStackAllocator localStackAllocator,
-      ProgramMethod context,
-      CfInstructionDesugaringEventConsumer eventConsumer,
-      MethodProcessingContext methodProcessingContext) {
+  static class RecordInvokeDynamic {
+
+    private final DexString methodName;
+    private final DexString fieldNames;
+    private final DexField[] fields;
+    private final DexProgramClass recordClass;
+
+    private RecordInvokeDynamic(
+        DexString methodName,
+        DexString fieldNames,
+        DexField[] fields,
+        DexProgramClass recordClass) {
+      this.methodName = methodName;
+      this.fieldNames = fieldNames;
+      this.fields = fields;
+      this.recordClass = recordClass;
+    }
+
+    DexField[] getFields() {
+      return fields;
+    }
+
+    DexProgramClass getRecordClass() {
+      return recordClass;
+    }
+
+    DexString getFieldNames() {
+      return fieldNames;
+    }
+
+    DexString getMethodName() {
+      return methodName;
+    }
+  }
+
+  private RecordInvokeDynamic parseInvokeDynamicOnRecord(
+      CfInvokeDynamic invokeDynamic, ProgramMethod context) {
     assert needsDesugaring(invokeDynamic, context);
     DexCallSite callSite = invokeDynamic.getCallSite();
     DexValueType recordValueType = callSite.bootstrapArgs.get(0).asDexValueType();
@@ -172,34 +241,53 @@
     }
     DexProgramClass recordClass =
         appView.definitionFor(recordValueType.getValue()).asProgramClass();
-    if (callSite.methodName == factory.toStringMethodName) {
-      DexString simpleName =
-          ClassNameComputationInfo.ClassNameMapping.SIMPLE_NAME.map(
-              recordValueType.getValue().toDescriptorString(), context.getHolder(), factory);
+    return new RecordInvokeDynamic(callSite.methodName, fieldNames, fields, recordClass);
+  }
+
+  private List<CfInstruction> desugarInvokeDynamicOnRecord(
+      CfInvokeDynamic invokeDynamic,
+      LocalStackAllocator localStackAllocator,
+      ProgramMethod context,
+      CfInstructionDesugaringEventConsumer eventConsumer,
+      MethodProcessingContext methodProcessingContext) {
+    RecordInvokeDynamic recordInvokeDynamic = parseInvokeDynamicOnRecord(invokeDynamic, context);
+    if (recordInvokeDynamic.getMethodName() == factory.toStringMethodName) {
       return desugarInvokeRecordToString(
-          recordClass,
-          fieldNames,
-          fields,
-          simpleName,
+          recordInvokeDynamic,
           localStackAllocator,
+          context,
           eventConsumer,
           methodProcessingContext);
     }
-    if (callSite.methodName == factory.hashCodeMethodName) {
+    if (recordInvokeDynamic.getMethodName() == factory.hashCodeMethodName) {
       return desugarInvokeRecordHashCode(
-          recordClass, fields, eventConsumer, methodProcessingContext);
+          recordInvokeDynamic, localStackAllocator, eventConsumer, methodProcessingContext);
     }
-    if (callSite.methodName == factory.equalsMethodName) {
-      return desugarInvokeRecordEquals(recordClass, fields, eventConsumer, methodProcessingContext);
+    if (recordInvokeDynamic.getMethodName() == factory.equalsMethodName) {
+      return desugarInvokeRecordEquals(recordInvokeDynamic);
     }
     throw new Unreachable("Invoke dynamic needs record desugaring but could not be desugared.");
   }
 
+  private ProgramMethod synthesizeEqualsRecordMethod(
+      DexProgramClass clazz, DexMethod getFieldsAsObjects, DexMethod method) {
+    return synthesizeMethod(
+        clazz, new RecordEqualsCfCodeProvider(appView, clazz.type, getFieldsAsObjects), method);
+  }
+
   private ProgramMethod synthesizeGetFieldsAsObjectsMethod(
       DexProgramClass clazz, DexField[] fields, DexMethod method) {
+    return synthesizeMethod(
+        clazz,
+        new RecordGetFieldsAsObjectsCfCodeProvider(appView, factory.recordTagType, fields),
+        method);
+  }
+
+  private ProgramMethod synthesizeMethod(
+      DexProgramClass clazz, SyntheticCfCodeProvider provider, DexMethod method) {
     MethodAccessFlags methodAccessFlags =
         MethodAccessFlags.fromSharedAccessFlags(
-            Constants.ACC_SYNTHETIC | Constants.ACC_PUBLIC, false);
+            Constants.ACC_SYNTHETIC | Constants.ACC_PRIVATE, false);
     DexEncodedMethod encodedMethod =
         new DexEncodedMethod(
             method,
@@ -209,26 +297,30 @@
             ParameterAnnotationsList.empty(),
             null,
             true);
-    encodedMethod.setCode(
-        new RecordGetFieldsAsObjectsCfCodeProvider(appView, factory.recordTagType, fields)
-            .generateCfCode(),
-        appView);
+    encodedMethod.setCode(provider.generateCfCode(), appView);
     return new ProgramMethod(clazz, encodedMethod);
   }
 
-  private void ensureGetFieldsAsObjects(
-      DexProgramClass clazz, DexField[] fields, RecordDesugaringEventConsumer eventConsumer) {
+  private DexMethod ensureEqualsRecord(
+      RecordInvokeDynamic recordInvokeDynamic, ProgramAdditions programAdditions) {
+    DexMethod getFieldsAsObjects = ensureGetFieldsAsObjects(recordInvokeDynamic, programAdditions);
+    DexProgramClass clazz = recordInvokeDynamic.getRecordClass();
+    DexMethod method = equalsRecordMethod(clazz.type);
+    assert clazz.lookupProgramMethod(method) == null;
+    programAdditions.accept(
+        method, () -> synthesizeEqualsRecordMethod(clazz, getFieldsAsObjects, method));
+    return method;
+  }
+
+  private DexMethod ensureGetFieldsAsObjects(
+      RecordInvokeDynamic recordInvokeDynamic, ProgramAdditions programAdditions) {
+    DexProgramClass clazz = recordInvokeDynamic.getRecordClass();
     DexMethod method = getFieldsAsObjectsMethod(clazz.type);
-    synchronized (clazz.getMethodCollection()) {
-      ProgramMethod getFieldsAsObjects = clazz.lookupProgramMethod(method);
-      if (getFieldsAsObjects == null) {
-        getFieldsAsObjects = synthesizeGetFieldsAsObjectsMethod(clazz, fields, method);
-        clazz.addVirtualMethod(getFieldsAsObjects.getDefinition());
-        if (eventConsumer != null) {
-          eventConsumer.acceptRecordMethod(getFieldsAsObjects);
-        }
-      }
-    }
+    assert clazz.lookupProgramMethod(method) == null;
+    programAdditions.accept(
+        method,
+        () -> synthesizeGetFieldsAsObjectsMethod(clazz, recordInvokeDynamic.getFields(), method));
+    return method;
   }
 
   private DexMethod getFieldsAsObjectsMethod(DexType holder) {
@@ -236,6 +328,13 @@
         holder, factory.createProto(factory.objectArrayType), GET_FIELDS_AS_OBJECTS_METHOD_NAME);
   }
 
+  private DexMethod equalsRecordMethod(DexType holder) {
+    return factory.createMethod(
+        holder,
+        factory.createProto(factory.booleanType, factory.objectType),
+        EQUALS_RECORD_METHOD_NAME);
+  }
+
   private ProgramMethod synthesizeRecordHelper(
       DexProto helperProto,
       BiFunction<InternalOptions, DexMethod, CfCode> codeGenerator,
@@ -254,50 +353,54 @@
   }
 
   private List<CfInstruction> desugarInvokeRecordHashCode(
-      DexProgramClass recordClass,
-      DexField[] fields,
-      CfInstructionDesugaringEventConsumer eventConsumer,
+      RecordInvokeDynamic recordInvokeDynamic,
+      LocalStackAllocator localStackAllocator,
+      RecordInstructionDesugaringEventConsumer eventConsumer,
       MethodProcessingContext methodProcessingContext) {
-    ensureGetFieldsAsObjects(recordClass, fields, eventConsumer);
+    localStackAllocator.allocateLocalStack(1);
+    DexMethod getFieldsAsObjects =
+        getFieldsAsObjectsMethod(recordInvokeDynamic.getRecordClass().type);
+    assert recordInvokeDynamic.getRecordClass().lookupProgramMethod(getFieldsAsObjects) != null;
+    ArrayList<CfInstruction> instructions = new ArrayList<>();
+    instructions.add(new CfStackInstruction(Dup));
+    instructions.add(new CfInvoke(Opcodes.INVOKEVIRTUAL, factory.objectMembers.getClass, false));
+    instructions.add(new CfStackInstruction(Swap));
+    instructions.add(new CfInvoke(Opcodes.INVOKESPECIAL, getFieldsAsObjects, false));
     ProgramMethod programMethod =
         synthesizeRecordHelper(
             recordHashCodeHelperProto,
             RecordCfMethods::RecordMethods_hashCode,
             methodProcessingContext);
     eventConsumer.acceptRecordMethod(programMethod);
-    return ImmutableList.of(
-        new CfInvoke(Opcodes.INVOKESTATIC, programMethod.getReference(), false));
+    instructions.add(new CfInvoke(Opcodes.INVOKESTATIC, programMethod.getReference(), false));
+    return instructions;
   }
 
-  private List<CfInstruction> desugarInvokeRecordEquals(
-      DexProgramClass recordClass,
-      DexField[] fields,
-      CfInstructionDesugaringEventConsumer eventConsumer,
-      MethodProcessingContext methodProcessingContext) {
-    ensureGetFieldsAsObjects(recordClass, fields, eventConsumer);
-    ProgramMethod programMethod =
-        synthesizeRecordHelper(
-            recordEqualsHelperProto,
-            RecordCfMethods::RecordMethods_equals,
-            methodProcessingContext);
-    eventConsumer.acceptRecordMethod(programMethod);
-    return ImmutableList.of(
-        new CfInvoke(Opcodes.INVOKESTATIC, programMethod.getReference(), false));
+  private List<CfInstruction> desugarInvokeRecordEquals(RecordInvokeDynamic recordInvokeDynamic) {
+    DexMethod equalsRecord = equalsRecordMethod(recordInvokeDynamic.getRecordClass().type);
+    assert recordInvokeDynamic.getRecordClass().lookupProgramMethod(equalsRecord) != null;
+    return Collections.singletonList(new CfInvoke(Opcodes.INVOKESPECIAL, equalsRecord, false));
   }
 
   private List<CfInstruction> desugarInvokeRecordToString(
-      DexProgramClass recordClass,
-      DexString fieldNames,
-      DexField[] fields,
-      DexString simpleName,
+      RecordInvokeDynamic recordInvokeDynamic,
       LocalStackAllocator localStackAllocator,
-      CfInstructionDesugaringEventConsumer eventConsumer,
+      ProgramMethod context,
+      RecordInstructionDesugaringEventConsumer eventConsumer,
       MethodProcessingContext methodProcessingContext) {
-    ensureGetFieldsAsObjects(recordClass, fields, eventConsumer);
-    ArrayList<CfInstruction> instructions = new ArrayList<>();
-    instructions.add(new CfConstString(simpleName));
-    instructions.add(new CfConstString(fieldNames));
+    DexString simpleName =
+        ClassNameComputationInfo.ClassNameMapping.SIMPLE_NAME.map(
+            recordInvokeDynamic.getRecordClass().type.toDescriptorString(),
+            context.getHolder(),
+            factory);
     localStackAllocator.allocateLocalStack(2);
+    DexMethod getFieldsAsObjects =
+        getFieldsAsObjectsMethod(recordInvokeDynamic.getRecordClass().type);
+    assert recordInvokeDynamic.getRecordClass().lookupProgramMethod(getFieldsAsObjects) != null;
+    ArrayList<CfInstruction> instructions = new ArrayList<>();
+    instructions.add(new CfInvoke(Opcodes.INVOKESPECIAL, getFieldsAsObjects, false));
+    instructions.add(new CfConstString(simpleName));
+    instructions.add(new CfConstString(recordInvokeDynamic.getFieldNames()));
     ProgramMethod programMethod =
         synthesizeRecordHelper(
             recordToStringHelperProto,
@@ -321,24 +424,35 @@
     return false;
   }
 
-  private void requiresRecordClass(RecordDesugaringEventConsumer eventConsumer) {
-    DexProgramClass recordClass = synthesizeR8Record();
-    if (recordClass != null) {
-      eventConsumer.acceptRecordClass(recordClass);
-    }
+  private void ensureRecordClass(RecordDesugaringEventConsumer eventConsumer) {
+    DexItemFactory factory = appView.dexItemFactory();
+    checkRecordTagNotPresent(factory);
+    appView
+        .getSyntheticItems()
+        .ensureFixedClassFromType(
+            SyntheticNaming.SyntheticKind.RECORD_TAG,
+            factory.recordType,
+            appView,
+            builder -> {
+              DexEncodedMethod init = synthesizeRecordInitMethod();
+              builder
+                  .setAbstract()
+                  .setDirectMethods(ImmutableList.of(init));
+            },
+            eventConsumer::acceptRecordClass);
   }
 
-  @Override
-  public boolean needsDesugaring(DexProgramClass clazz) {
-    return clazz.isRecord();
-  }
-
-  @Override
-  public void desugar(DexProgramClass clazz, CfClassDesugaringEventConsumer eventConsumer) {
-    if (clazz.isRecord()) {
-      assert clazz.superType == factory.recordType;
-      requiresRecordClass(eventConsumer);
-      clazz.accessFlags.unsetRecord();
+  private void checkRecordTagNotPresent(DexItemFactory factory) {
+    DexClass r8RecordClass =
+        appView.appInfo().definitionForWithoutExistenceAssert(factory.recordTagType);
+    if (r8RecordClass != null && r8RecordClass.isProgramClass()) {
+      appView
+          .options()
+          .reporter
+          .error(
+              "D8/R8 is compiling a mix of desugared and non desugared input using"
+                  + " java.lang.Record, but the application reader did not import correctly "
+                  + factory.recordTagType);
     }
   }
 
@@ -471,65 +585,6 @@
     return factory.objectMembers.toString;
   }
 
-  private DexProgramClass synthesizeR8Record() {
-    DexItemFactory factory = appView.dexItemFactory();
-    DexClass r8RecordClass =
-        appView.appInfo().definitionForWithoutExistenceAssert(factory.recordTagType);
-    if (r8RecordClass != null && r8RecordClass.isProgramClass()) {
-      appView
-          .options()
-          .reporter
-          .error(
-              "D8/R8 is compiling a mix of desugared and non desugared input using"
-                  + " java.lang.Record, but the application reader did not import correctly "
-                  + factory.recordTagType.toString());
-    }
-    DexClass recordClass =
-        appView.appInfo().definitionForWithoutExistenceAssert(factory.recordType);
-    if (recordClass != null && recordClass.isProgramClass()) {
-      return null;
-    }
-    return synchronizedSynthesizeR8Record();
-  }
-
-  private synchronized DexProgramClass synchronizedSynthesizeR8Record() {
-    DexItemFactory factory = appView.dexItemFactory();
-    DexClass recordClass =
-        appView.appInfo().definitionForWithoutExistenceAssert(factory.recordType);
-    if (recordClass != null && recordClass.isProgramClass()) {
-      return null;
-    }
-    DexEncodedMethod init = synthesizeRecordInitMethod();
-    DexEncodedMethod abstractGetFieldsAsObjectsMethod =
-        synthesizeAbstractGetFieldsAsObjectsMethod();
-    return appView
-        .getSyntheticItems()
-        .createFixedClassFromType(
-            SyntheticNaming.SyntheticKind.RECORD_TAG,
-            factory.recordType,
-            factory,
-            builder ->
-                builder
-                    .setAbstract()
-                    .setVirtualMethods(ImmutableList.of(abstractGetFieldsAsObjectsMethod))
-                    .setDirectMethods(ImmutableList.of(init)));
-  }
-
-  private DexEncodedMethod synthesizeAbstractGetFieldsAsObjectsMethod() {
-    MethodAccessFlags methodAccessFlags =
-        MethodAccessFlags.fromSharedAccessFlags(
-            Constants.ACC_SYNTHETIC | Constants.ACC_PUBLIC | Constants.ACC_ABSTRACT, false);
-    DexMethod fieldsAsObjectsMethod = getFieldsAsObjectsMethod(factory.recordType);
-    return new DexEncodedMethod(
-        fieldsAsObjectsMethod,
-        methodAccessFlags,
-        MethodTypeSignature.noSignature(),
-        DexAnnotationSet.empty(),
-        ParameterAnnotationsList.empty(),
-        null,
-        true);
-  }
-
   private DexEncodedMethod synthesizeRecordInitMethod() {
     MethodAccessFlags methodAccessFlags =
         MethodAccessFlags.fromSharedAccessFlags(
@@ -547,4 +602,25 @@
         new CallObjectInitCfCodeProvider(appView, factory.recordTagType).generateCfCode(), appView);
     return init;
   }
+
+  @Override
+  public void synthesizeClasses(CfClassSynthesizerDesugaringEventConsumer eventConsumer) {
+    if (appView.appInfo().app().getFlags().hasReadProgramRecord()) {
+      ensureRecordClass(eventConsumer);
+    }
+  }
+
+  @Override
+  public void postProcessingDesugaring(
+      Collection<DexProgramClass> programClasses,
+      CfPostProcessingDesugaringEventConsumer eventConsumer,
+      ExecutorService executorService)
+      throws ExecutionException {
+    for (DexProgramClass clazz : programClasses) {
+      if (clazz.isRecord()) {
+        assert clazz.superType == factory.recordType;
+        clazz.accessFlags.unsetRecord();
+      }
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/ConcreteCallSiteOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/ConcreteCallSiteOptimizationInfo.java
index ed27028..ae702f9 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/ConcreteCallSiteOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/ConcreteCallSiteOptimizationInfo.java
@@ -217,7 +217,7 @@
 
       // Constant propagation.
       if (allowConstantPropagation) {
-        AbstractValue abstractValue = concreteParameterState.getAbstractValue();
+        AbstractValue abstractValue = concreteParameterState.getAbstractValue(appView);
         if (abstractValue.isNonTrivial()) {
           newCallSiteInfo.constants.put(argumentIndex, abstractValue);
           isTop = false;
@@ -234,20 +234,24 @@
             if (allowConstantPropagation) {
               newCallSiteInfo.constants.put(
                   argumentIndex, appView.abstractValueFactory().createNullValue());
+            } else {
+              newCallSiteInfo.dynamicUpperBoundTypes.put(
+                  argumentIndex, staticTypeElement.asArrayType().asDefinitelyNull());
             }
+            isTop = false;
           } else if (nullability.isDefinitelyNotNull()) {
             newCallSiteInfo.dynamicUpperBoundTypes.put(
                 argumentIndex, staticTypeElement.asArrayType().asDefinitelyNotNull());
+            isTop = false;
           } else {
-            // Should never happen, since the parameter state is unknown in this case.
+            // The nullability should never be unknown, since we should use the unknown method state
+            // in this case. It should also not be bottom, since we should change the method's body
+            // to throw null in this case.
             assert false;
           }
         } else if (staticType.isClassType()) {
-          DynamicType dynamicType =
-              method.getDefinition().isInstance() && argumentIndex == 0
-                  ? concreteParameterState.asReceiverParameter().getDynamicType()
-                  : concreteParameterState.asClassParameter().getDynamicType();
-          if (!dynamicType.isTrivial(staticTypeElement)) {
+          DynamicType dynamicType = concreteParameterState.asReferenceParameter().getDynamicType();
+          if (!dynamicType.isUnknown()) {
             newCallSiteInfo.dynamicUpperBoundTypes.put(
                 argumentIndex, dynamicType.getDynamicUpperBoundType());
             isTop = false;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/typechecks/CheckCastAndInstanceOfMethodSpecialization.java b/src/main/java/com/android/tools/r8/ir/optimize/typechecks/CheckCastAndInstanceOfMethodSpecialization.java
index c3f2bc3..906ac7f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/typechecks/CheckCastAndInstanceOfMethodSpecialization.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/typechecks/CheckCastAndInstanceOfMethodSpecialization.java
@@ -147,6 +147,9 @@
     }
 
     method.getHolder().removeMethod(method.getReference());
+
+    appView.withArgumentPropagator(
+        argumentPropagator -> argumentPropagator.transferArgumentInformation(method, parentMethod));
   }
 
   private ProgramMethod resolveOnSuperClass(ProgramMethod method) {
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 e98adec..537d9af 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,6 +38,7 @@
 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.LinkedHashSetUtils;
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.google.common.collect.HashMultiset;
@@ -60,6 +61,7 @@
 import java.util.Comparator;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.ListIterator;
@@ -2524,9 +2526,10 @@
       Map<BasicBlock, LiveAtEntrySets> liveAtEntrySets,
       List<LiveIntervals> liveIntervals) {
     for (BasicBlock block : code.topologicallySortedBlocks()) {
-      Set<Value> live = Sets.newIdentityHashSet();
-      Set<Value> phiOperands = Sets.newIdentityHashSet();
-      Set<Value> liveAtThrowingInstruction = Sets.newIdentityHashSet();
+      // Linked collections to ensure determinism of liveIntervals (see also b/197643889).
+      LinkedHashSet<Value> live = Sets.newLinkedHashSet();
+      LinkedHashSet<Value> phiOperands = Sets.newLinkedHashSet();
+      LinkedHashSet<Value> liveAtThrowingInstruction = Sets.newLinkedHashSet();
       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
@@ -2534,9 +2537,10 @@
         // the block only if they are used in normal flow as well.
         boolean isExceptionalSuccessor = exceptionalSuccessors.contains(successor);
         if (isExceptionalSuccessor) {
-          liveAtThrowingInstruction.addAll(liveAtEntrySets.get(successor).liveValues);
+          LinkedHashSetUtils.addAll(
+              liveAtThrowingInstruction, liveAtEntrySets.get(successor).liveValues);
         } else {
-          live.addAll(liveAtEntrySets.get(successor).liveValues);
+          LinkedHashSetUtils.addAll(live, liveAtEntrySets.get(successor).liveValues);
         }
         // Exception blocks should not have any phis (if an exception block has more than one
         // predecessor, then we insert a split block in-between).
@@ -2546,7 +2550,7 @@
           phiOperands.add(phi.getOperand(successor.getPredecessors().indexOf(block)));
         }
       }
-      live.addAll(phiOperands);
+      LinkedHashSetUtils.addAll(live, phiOperands);
       List<Instruction> instructions = block.getInstructions();
       for (Value value : live) {
         int end = block.entry().getNumber() + instructions.size() * INSTRUCTION_NUMBER_DELTA;
@@ -2635,7 +2639,9 @@
           // In debug mode, or if the method is reachability sensitive, extend the live range
           // to cover the full scope of a local variable (encoded as debug values).
           int number = instruction.getNumber();
-          for (Value use : instruction.getDebugValues()) {
+          List<Value> sortedDebugValues = new ArrayList<>(instruction.getDebugValues());
+          sortedDebugValues.sort(Value::compareTo);
+          for (Value use : sortedDebugValues) {
             assert use.needsRegister();
             if (!live.contains(use)) {
               live.add(use);
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/RecordCfCodeProvider.java b/src/main/java/com/android/tools/r8/ir/synthetic/RecordCfCodeProvider.java
new file mode 100644
index 0000000..ce66391
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/RecordCfCodeProvider.java
@@ -0,0 +1,170 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.synthetic;
+
+import com.android.tools.r8.cf.code.CfArrayStore;
+import com.android.tools.r8.cf.code.CfCheckCast;
+import com.android.tools.r8.cf.code.CfConstNumber;
+import com.android.tools.r8.cf.code.CfFieldInstruction;
+import com.android.tools.r8.cf.code.CfFrame;
+import com.android.tools.r8.cf.code.CfFrame.FrameType;
+import com.android.tools.r8.cf.code.CfIfCmp;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.cf.code.CfLabel;
+import com.android.tools.r8.cf.code.CfLoad;
+import com.android.tools.r8.cf.code.CfNewArray;
+import com.android.tools.r8.cf.code.CfReturn;
+import com.android.tools.r8.cf.code.CfStore;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
+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.If;
+import com.android.tools.r8.ir.code.MemberType;
+import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.utils.collections.ImmutableDeque;
+import com.android.tools.r8.utils.collections.ImmutableInt2ReferenceSortedMap;
+import java.util.ArrayList;
+import java.util.List;
+import org.objectweb.asm.Opcodes;
+
+public abstract class RecordCfCodeProvider {
+
+  /**
+   * Generates a method which answers all field values as an array of objects. If the field value is
+   * a primitive type, it uses the primitive wrapper to wrap it.
+   *
+   * <p>The fields in parameters are in the order where they should be in the array generated by the
+   * method, which is not necessarily the class instanceFields order.
+   *
+   * <p>Example: <code>record Person{ int age; String name;}</code>
+   *
+   * <p><code>Object[] getFieldsAsObjects() {
+   * Object[] fields = new Object[2];
+   * fields[0] = name;
+   * fields[1] = Integer.valueOf(age);
+   * return fields;</code>
+   */
+  public static class RecordGetFieldsAsObjectsCfCodeProvider extends SyntheticCfCodeProvider {
+
+    public static void registerSynthesizedCodeReferences(DexItemFactory factory) {
+      factory.createSynthesizedType("[Ljava/lang/Object;");
+      factory.primitiveToBoxed.forEach(
+          (primitiveType, boxedType) -> {
+            factory.createSynthesizedType(primitiveType.toDescriptorString());
+            factory.createSynthesizedType(boxedType.toDescriptorString());
+          });
+    }
+
+    private final DexField[] fields;
+
+    public RecordGetFieldsAsObjectsCfCodeProvider(
+        AppView<?> appView, DexType holder, DexField[] fields) {
+      super(appView, holder);
+      this.fields = fields;
+    }
+
+    @Override
+    public CfCode generateCfCode() {
+      // Stack layout:
+      // 0 : receiver (the record instance)
+      // 1 : the array to return
+      // 2+: spills
+      DexItemFactory factory = appView.dexItemFactory();
+      List<CfInstruction> instructions = new ArrayList<>();
+      // Object[] fields = new Object[*length*];
+      instructions.add(new CfConstNumber(fields.length, ValueType.INT));
+      instructions.add(new CfNewArray(factory.objectArrayType));
+      instructions.add(new CfStore(ValueType.OBJECT, 1));
+      // fields[*i*] = this.*field* || *PrimitiveWrapper*.valueOf(this.*field*);
+      for (int i = 0; i < fields.length; i++) {
+        DexField field = fields[i];
+        instructions.add(new CfLoad(ValueType.OBJECT, 1));
+        instructions.add(new CfConstNumber(i, ValueType.INT));
+        instructions.add(new CfLoad(ValueType.OBJECT, 0));
+        instructions.add(new CfFieldInstruction(Opcodes.GETFIELD, field, field));
+        if (field.type.isPrimitiveType()) {
+          factory.primitiveToBoxed.forEach(
+              (primitiveType, boxedType) -> {
+                if (primitiveType == field.type) {
+                  instructions.add(
+                      new CfInvoke(
+                          Opcodes.INVOKESTATIC,
+                          factory.createMethod(
+                              boxedType,
+                              factory.createProto(boxedType, primitiveType),
+                              factory.valueOfMethodName),
+                          false));
+                }
+              });
+        }
+        instructions.add(new CfArrayStore(MemberType.OBJECT));
+      }
+      // return fields;
+      instructions.add(new CfLoad(ValueType.OBJECT, 1));
+      instructions.add(new CfReturn(ValueType.OBJECT));
+      return standardCfCodeFromInstructions(instructions);
+    }
+  }
+
+  public static class RecordEqualsCfCodeProvider extends SyntheticCfCodeProvider {
+
+    private final DexMethod getFieldsAsObjects;
+
+    public RecordEqualsCfCodeProvider(
+        AppView<?> appView, DexType holder, DexMethod getFieldsAsObjects) {
+      super(appView, holder);
+      this.getFieldsAsObjects = getFieldsAsObjects;
+    }
+
+    public static void registerSynthesizedCodeReferences(DexItemFactory factory) {
+      factory.createSynthesizedType("[Ljava/lang/Object;");
+      factory.createSynthesizedType("[Ljava/util/Arrays;");
+    }
+
+    @Override
+    public CfCode generateCfCode() {
+      // This generates something along the lines of:
+      // if (this.getClass() != other.getClass()) {
+      //     return false;
+      // }
+      // return Arrays.equals(
+      //     recordInstance.getFieldsAsObjects(),
+      //     ((RecordClass) other).getFieldsAsObjects());
+      ImmutableInt2ReferenceSortedMap<FrameType> locals =
+          ImmutableInt2ReferenceSortedMap.<FrameType>builder()
+              .put(0, FrameType.initialized(getHolder()))
+              .put(1, FrameType.initialized(appView.dexItemFactory().objectType))
+              .build();
+      DexItemFactory factory = appView.dexItemFactory();
+      List<CfInstruction> instructions = new ArrayList<>();
+      CfLabel fieldCmp = new CfLabel();
+      ValueType recordType = ValueType.fromDexType(getHolder());
+      ValueType objectType = ValueType.fromDexType(factory.objectType);
+      instructions.add(new CfLoad(recordType, 0));
+      instructions.add(new CfInvoke(Opcodes.INVOKEVIRTUAL, factory.objectMembers.getClass, false));
+      instructions.add(new CfLoad(objectType, 1));
+      instructions.add(new CfInvoke(Opcodes.INVOKEVIRTUAL, factory.objectMembers.getClass, false));
+      instructions.add(new CfIfCmp(If.Type.EQ, ValueType.OBJECT, fieldCmp));
+      instructions.add(new CfConstNumber(0, ValueType.INT));
+      instructions.add(new CfReturn(ValueType.INT));
+      instructions.add(fieldCmp);
+      instructions.add(new CfFrame(locals, ImmutableDeque.of()));
+      instructions.add(new CfLoad(recordType, 0));
+      instructions.add(new CfInvoke(Opcodes.INVOKESPECIAL, getFieldsAsObjects, false));
+      instructions.add(new CfLoad(objectType, 1));
+      instructions.add(new CfCheckCast(getHolder()));
+      instructions.add(new CfInvoke(Opcodes.INVOKESPECIAL, getFieldsAsObjects, false));
+      instructions.add(
+          new CfInvoke(
+              Opcodes.INVOKESTATIC, factory.javaUtilArraysMethods.equalsObjectArray, false));
+      instructions.add(new CfReturn(ValueType.INT));
+      return standardCfCodeFromInstructions(instructions);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/RecordGetFieldsAsObjectsCfCodeProvider.java b/src/main/java/com/android/tools/r8/ir/synthetic/RecordGetFieldsAsObjectsCfCodeProvider.java
deleted file mode 100644
index 4f534da..0000000
--- a/src/main/java/com/android/tools/r8/ir/synthetic/RecordGetFieldsAsObjectsCfCodeProvider.java
+++ /dev/null
@@ -1,102 +0,0 @@
-// 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.synthetic;
-
-import com.android.tools.r8.cf.code.CfArrayStore;
-import com.android.tools.r8.cf.code.CfConstNumber;
-import com.android.tools.r8.cf.code.CfFieldInstruction;
-import com.android.tools.r8.cf.code.CfInstruction;
-import com.android.tools.r8.cf.code.CfInvoke;
-import com.android.tools.r8.cf.code.CfLoad;
-import com.android.tools.r8.cf.code.CfNewArray;
-import com.android.tools.r8.cf.code.CfReturn;
-import com.android.tools.r8.cf.code.CfStore;
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.CfCode;
-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.code.MemberType;
-import com.android.tools.r8.ir.code.ValueType;
-import java.util.ArrayList;
-import java.util.List;
-import org.objectweb.asm.Opcodes;
-
-/**
- * Generates a method which answers all field values as an array of objects. If the field value is a
- * primitive type, it uses the primitive wrapper to wrap it.
- *
- * <p>The fields in parameters are in the order where they should be in the array generated by the
- * method, which is not necessarily the class instanceFields order.
- *
- * <p>Example: <code>record Person{ int age; String name;}</code>
- *
- * <p><code>Object[] getFieldsAsObjects() {
- * Object[] fields = new Object[2];
- * fields[0] = name;
- * fields[1] = Integer.valueOf(age);
- * return fields;</code>
- */
-public class RecordGetFieldsAsObjectsCfCodeProvider extends SyntheticCfCodeProvider {
-
-  public static void registerSynthesizedCodeReferences(DexItemFactory factory) {
-    factory.createSynthesizedType("[Ljava/lang/Object;");
-    factory.primitiveToBoxed.forEach(
-        (primitiveType, boxedType) -> {
-          factory.createSynthesizedType(primitiveType.toDescriptorString());
-          factory.createSynthesizedType(boxedType.toDescriptorString());
-        });
-  }
-
-  private final DexField[] fields;
-
-  public RecordGetFieldsAsObjectsCfCodeProvider(
-      AppView<?> appView, DexType holder, DexField[] fields) {
-    super(appView, holder);
-    this.fields = fields;
-  }
-
-  @Override
-  public CfCode generateCfCode() {
-    // Stack layout:
-    // 0 : receiver (the record instance)
-    // 1 : the array to return
-    // 2+: spills
-    DexItemFactory factory = appView.dexItemFactory();
-    List<CfInstruction> instructions = new ArrayList<>();
-    // Object[] fields = new Object[*length*];
-    instructions.add(new CfConstNumber(fields.length, ValueType.INT));
-    instructions.add(new CfNewArray(factory.objectArrayType));
-    instructions.add(new CfStore(ValueType.OBJECT, 1));
-    // fields[*i*] = this.*field* || *PrimitiveWrapper*.valueOf(this.*field*);
-    for (int i = 0; i < fields.length; i++) {
-      DexField field = fields[i];
-      instructions.add(new CfLoad(ValueType.OBJECT, 1));
-      instructions.add(new CfConstNumber(i, ValueType.INT));
-      instructions.add(new CfLoad(ValueType.OBJECT, 0));
-      instructions.add(new CfFieldInstruction(Opcodes.GETFIELD, field, field));
-      if (field.type.isPrimitiveType()) {
-        factory.primitiveToBoxed.forEach(
-            (primitiveType, boxedType) -> {
-              if (primitiveType == field.type) {
-                instructions.add(
-                    new CfInvoke(
-                        Opcodes.INVOKESTATIC,
-                        factory.createMethod(
-                            boxedType,
-                            factory.createProto(boxedType, primitiveType),
-                            factory.valueOfMethodName),
-                        false));
-              }
-            });
-      }
-      instructions.add(new CfArrayStore(MemberType.OBJECT));
-    }
-    // return fields;
-    instructions.add(new CfLoad(ValueType.OBJECT, 1));
-    instructions.add(new CfReturn(ValueType.OBJECT));
-    return standardCfCodeFromInstructions(instructions);
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/SyntheticCfCodeProvider.java b/src/main/java/com/android/tools/r8/ir/synthetic/SyntheticCfCodeProvider.java
index c01b0b8..05f88a1 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/SyntheticCfCodeProvider.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/SyntheticCfCodeProvider.java
@@ -22,6 +22,10 @@
     this.holder = holder;
   }
 
+  public DexType getHolder() {
+    return holder;
+  }
+
   public abstract CfCode generateCfCode();
 
   protected CfCode standardCfCodeFromInstructions(List<CfInstruction> instructions) {
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinDeclarationContainerInfo.java b/src/main/java/com/android/tools/r8/kotlin/KotlinDeclarationContainerInfo.java
index d166380..9e79ee0 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinDeclarationContainerInfo.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinDeclarationContainerInfo.java
@@ -18,7 +18,7 @@
 import com.android.tools.r8.shaking.EnqueuerMetadataTraceable;
 import com.android.tools.r8.utils.Reporter;
 import com.google.common.collect.ImmutableList;
-import java.util.IdentityHashMap;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Consumer;
@@ -174,7 +174,7 @@
       rewritten |= typeAlias.rewrite(typeAliasProvider, appView, namingLens);
     }
     // For properties, we need to combine potentially a field, setter and getter.
-    Map<KotlinPropertyInfo, KotlinPropertyGroup> properties = new IdentityHashMap<>();
+    Map<KotlinPropertyInfo, KotlinPropertyGroup> properties = new LinkedHashMap<>();
     for (DexEncodedField field : clazz.fields()) {
       if (field.getKotlinInfo().isProperty()) {
         properties
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
index 00ea1ac..2173883 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagator.java
@@ -4,6 +4,8 @@
 
 package com.android.tools.r8.optimize.argumentpropagation;
 
+import static com.android.tools.r8.optimize.argumentpropagation.utils.StronglyConnectedProgramClasses.computeStronglyConnectedProgramClasses;
+
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -14,13 +16,18 @@
 import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.PostMethodProcessor;
 import com.android.tools.r8.ir.optimize.info.CallSiteOptimizationInfo;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.VirtualRootMethodsAnalysis;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.Timing;
+import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 
 /** Optimization that propagates information about arguments from call sites to method entries. */
-// TODO(b/190154391): Add timing information for performance tracking.
 public class ArgumentPropagator {
 
   private final AppView<AppInfoWithLiveness> appView;
@@ -48,67 +55,118 @@
    * Called by {@link IRConverter} *before* the primary optimization pass to setup the scanner for
    * collecting argument information from the code objects.
    */
-  public void initializeCodeScanner() {
+  public void initializeCodeScanner(ExecutorService executorService, Timing timing)
+      throws ExecutionException {
+    assert !appView.getSyntheticItems().hasPendingSyntheticClasses();
+
+    timing.begin("Argument propagator");
+    timing.begin("Initialize code scanner");
+
     codeScanner = new ArgumentPropagatorCodeScanner(appView);
 
-    // Disable argument propagation for methods that should not be optimized.
     ImmediateProgramSubtypingInfo immediateSubtypingInfo =
         ImmediateProgramSubtypingInfo.create(appView);
-    // TODO(b/190154391): Consider computing the strongly connected components and running this in
-    //  parallel for each scc.
-    new ArgumentPropagatorUnoptimizableMethods(
-            appView, immediateSubtypingInfo, codeScanner.getMethodStates())
-        .disableArgumentPropagationForUnoptimizableMethods(appView.appInfo().classes());
+    List<Set<DexProgramClass>> stronglyConnectedProgramClasses =
+        computeStronglyConnectedProgramClasses(appView, immediateSubtypingInfo);
+    ThreadUtils.processItems(
+        stronglyConnectedProgramClasses,
+        classes -> {
+          // Disable argument propagation for methods that should not be optimized by setting their
+          // method state to unknown.
+          new ArgumentPropagatorUnoptimizableMethods(
+                  appView, immediateSubtypingInfo, codeScanner.getMethodStates())
+              .disableArgumentPropagationForUnoptimizableMethods(classes);
+
+          // Compute the mapping from virtual methods to their root virtual method and the set of
+          // monomorphic virtual methods.
+          new VirtualRootMethodsAnalysis(appView, immediateSubtypingInfo)
+              .extendVirtualRootMethods(appView.appInfo().classes(), codeScanner);
+        },
+        executorService);
+
+    timing.end();
+    timing.end();
   }
 
   /** Called by {@link IRConverter} prior to finalizing methods. */
-  public void scan(ProgramMethod method, IRCode code, MethodProcessor methodProcessor) {
+  public void scan(
+      ProgramMethod method, IRCode code, MethodProcessor methodProcessor, Timing timing) {
     if (codeScanner != null) {
       // TODO(b/190154391): Do we process synthetic methods using a OneTimeMethodProcessor
       //  during the primary optimization pass?
       assert methodProcessor.isPrimaryMethodProcessor();
-      codeScanner.scan(method, code);
+      codeScanner.scan(method, code, timing);
     } else {
       assert !methodProcessor.isPrimaryMethodProcessor();
     }
   }
 
+  public void transferArgumentInformation(ProgramMethod from, ProgramMethod to) {
+    assert codeScanner != null;
+    MethodStateCollectionByReference methodStates = codeScanner.getMethodStates();
+    MethodState methodState = methodStates.remove(from);
+    if (!methodState.isBottom()) {
+      methodStates.addMethodState(appView, to, methodState);
+    }
+  }
+
+  public void tearDownCodeScanner(
+      PostMethodProcessor.Builder postMethodProcessorBuilder,
+      ExecutorService executorService,
+      Timing timing)
+      throws ExecutionException {
+    assert !appView.getSyntheticItems().hasPendingSyntheticClasses();
+    timing.begin("Argument propagator");
+    populateParameterOptimizationInfo(executorService, timing);
+    optimizeMethodParameters();
+    enqueueMethodsForProcessing(postMethodProcessorBuilder);
+    timing.end();
+  }
+
   /**
    * Called by {@link IRConverter} *after* the primary optimization pass to populate the parameter
    * optimization info.
    */
-  public void populateParameterOptimizationInfo(ExecutorService executorService)
+  private void populateParameterOptimizationInfo(ExecutorService executorService, Timing timing)
       throws ExecutionException {
     // Unset the scanner since all code objects have been scanned at this point.
     assert appView.isAllCodeProcessed();
     MethodStateCollectionByReference codeScannerResult = codeScanner.getMethodStates();
+    appView.testing().argumentPropagatorEventConsumer.acceptCodeScannerResult(codeScannerResult);
     codeScanner = null;
 
+    timing.begin("Compute optimization info");
     new ArgumentPropagatorOptimizationInfoPopulator(appView, codeScannerResult)
-        .populateOptimizationInfo(executorService);
+        .populateOptimizationInfo(executorService, timing);
+    timing.end();
   }
 
   /** Called by {@link IRConverter} to optimize method definitions. */
-  public void optimizeMethodParameters() {
+  private void optimizeMethodParameters() {
     // TODO(b/190154391): Remove parameters with constant values.
     // TODO(b/190154391): Remove unused parameters by simulating they are constant.
     // TODO(b/190154391): Strengthen the static type of parameters.
     // TODO(b/190154391): If we learn that a method returns a constant, then consider changing its
     //  return type to void.
+    // TODO(b/69963623): If we optimize a method to be unconditionally throwing (because it has a
+    //  bottom parameter), then for each caller that becomes unconditionally throwing, we could
+    //  also enqueue the caller's callers for reprocessing. This would propagate the throwing
+    //  information to all call sites.
   }
 
   /**
    * Called by {@link IRConverter} to add all methods that require reprocessing to {@param
    * postMethodProcessorBuilder}.
    */
-  public void enqueueMethodsForProcessing(PostMethodProcessor.Builder postMethodProcessorBuilder) {
+  private void enqueueMethodsForProcessing(PostMethodProcessor.Builder postMethodProcessorBuilder) {
     for (DexProgramClass clazz : appView.appInfo().classes()) {
       clazz.forEachProgramMethodMatching(
           DexEncodedMethod::hasCode,
           method -> {
             CallSiteOptimizationInfo callSiteOptimizationInfo =
                 method.getDefinition().getCallSiteOptimizationInfo();
-            if (callSiteOptimizationInfo.isConcreteCallSiteOptimizationInfo()) {
+            if (callSiteOptimizationInfo.isConcreteCallSiteOptimizationInfo()
+                && !appView.appInfo().isNeverReprocessMethod(method.getReference())) {
               postMethodProcessorBuilder.add(method);
             }
           });
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
index 509079e..77beb56 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorCodeScanner.java
@@ -4,13 +4,14 @@
 
 package com.android.tools.r8.optimize.argumentpropagation;
 
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexMethodHandle;
-import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
@@ -27,8 +28,10 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteArrayTypeParameterState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteClassTypeParameterState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMonomorphicMethodState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMonomorphicMethodStateOrBottom;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMonomorphicMethodStateOrUnknown;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePolymorphicMethodState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePolymorphicMethodStateOrBottom;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePrimitiveTypeParameterState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteReceiverParameterState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameter;
@@ -36,12 +39,16 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ParameterState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.UnknownMethodState;
+import com.android.tools.r8.optimize.argumentpropagation.utils.WideningUtils;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.Timing;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
 import java.util.ArrayList;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /**
  * Analyzes each {@link IRCode} during the primary optimization to collect information about the
@@ -49,64 +56,68 @@
  *
  * <p>State pruning is applied on-the-fly to avoid storing redundant information.
  */
-class ArgumentPropagatorCodeScanner {
+public class ArgumentPropagatorCodeScanner {
 
   private static AliasedValueConfiguration aliasedValueConfiguration =
       AssumeAndCheckCastAliasedValueConfiguration.getInstance();
 
   private final AppView<AppInfoWithLiveness> appView;
 
+  private final Set<DexMethod> monomorphicVirtualMethods = Sets.newIdentityHashSet();
+
   /**
-   * Maps each non-interface method to the upper most method in the super class chain with the same
-   * method signature. This only contains an entry for non-private virtual methods that override
-   * another method in the program.
+   * Maps each non-private virtual method to the upper most method in the class hierarchy with the
+   * same method signature. Virtual methods that do not override other virtual methods are mapped to
+   * themselves.
    */
-  private final Map<DexMethod, DexMethod> classMethodRoots;
+  private final Map<DexMethod, DexMethod> virtualRootMethods = new IdentityHashMap<>();
 
   /**
    * The abstract program state for this optimization. Intuitively maps each parameter to its
    * abstract value and dynamic type.
    */
-  private final MethodStateCollectionByReference methodStates;
+  private final MethodStateCollectionByReference methodStates =
+      MethodStateCollectionByReference.createConcurrent();
 
   ArgumentPropagatorCodeScanner(AppView<AppInfoWithLiveness> appView) {
     this.appView = appView;
-    this.classMethodRoots = computeClassMethodRoots();
-    this.methodStates = computeInitialMethodStates();
   }
 
-  private Map<DexMethod, DexMethod> computeClassMethodRoots() {
-    // TODO(b/190154391): Group methods related by overriding to enable more effective pruning.
-    Map<DexMethod, DexMethod> roots = new IdentityHashMap<>();
-    for (DexProgramClass clazz : appView.appInfo().classes()) {
-      clazz.forEachProgramVirtualMethod(
-          method -> roots.put(method.getReference(), method.getReference()));
-    }
-    return roots;
+  public synchronized void addMonomorphicVirtualMethods(Set<DexMethod> extension) {
+    monomorphicVirtualMethods.addAll(extension);
   }
 
-  private MethodStateCollectionByReference computeInitialMethodStates() {
-    // TODO(b/190154391): There is no need to track an abstract value for receivers; we only care
-    //  about the dynamic type for such parameters. Consider initializing the initial state to have
-    //  unknown abstract values for all receivers.
-    return MethodStateCollectionByReference.createConcurrent();
+  public synchronized void addVirtualRootMethods(Map<DexMethod, DexMethod> extension) {
+    virtualRootMethods.putAll(extension);
   }
 
   MethodStateCollectionByReference getMethodStates() {
     return methodStates;
   }
 
-  void scan(ProgramMethod method, IRCode code) {
+  DexMethod getVirtualRootMethod(ProgramMethod method) {
+    return virtualRootMethods.get(method.getReference());
+  }
+
+  boolean isMonomorphicVirtualMethod(ProgramMethod method) {
+    boolean isMonomorphicVirtualMethod = monomorphicVirtualMethods.contains(method.getReference());
+    assert method.getDefinition().belongsToVirtualPool() || !isMonomorphicVirtualMethod;
+    return isMonomorphicVirtualMethod;
+  }
+
+  void scan(ProgramMethod method, IRCode code, Timing timing) {
+    timing.begin("Argument propagation scanner");
     for (Invoke invoke : code.<Invoke>instructions(Instruction::isInvoke)) {
       if (invoke.isInvokeMethod()) {
-        scan(invoke.asInvokeMethod(), method);
+        scan(invoke.asInvokeMethod(), method, timing);
       } else if (invoke.isInvokeCustom()) {
         scan(invoke.asInvokeCustom(), method);
       }
     }
+    timing.end();
   }
 
-  private void scan(InvokeMethod invoke, ProgramMethod context) {
+  private void scan(InvokeMethod invoke, ProgramMethod context, Timing timing) {
     List<Value> arguments = invoke.arguments();
     if (arguments.isEmpty()) {
       // Nothing to propagate.
@@ -115,7 +126,13 @@
 
     DexMethod invokedMethod = invoke.getInvokedMethod();
     if (invokedMethod.getHolderType().isArrayType()) {
-      // Nothing to propagate.
+      // Nothing to propagate; the targeted method is not a program method.
+      return;
+    }
+
+    if (invoke.isInvokeMethodWithReceiver()
+        && invoke.asInvokeMethodWithReceiver().getReceiver().isAlwaysNull(appView)) {
+      // Nothing to propagate; the invoke instruction always fails.
       return;
     }
 
@@ -126,7 +143,6 @@
       return;
     }
 
-    // TODO(b/190154391): Also bail out if the method is an unoptimizable program method.
     if (!resolutionResult.getResolvedHolder().isProgramClass()) {
       // Nothing to propagate; this could dispatch to a program method, but we cannot optimize
       // methods that override non-program methods.
@@ -147,6 +163,28 @@
       return;
     }
 
+    if (invoke.isInvokeSuper()) {
+      // Use the super target instead of the resolved method to ensure that we propagate the
+      // argument information to the targeted method.
+      DexClassAndMethod target =
+          resolutionResult.lookupInvokeSuperTarget(context.getHolder(), appView.appInfo());
+      if (target == null) {
+        // Nothing to propagate; the invoke instruction fails.
+        return;
+      }
+      if (!target.isProgramMethod()) {
+        throw new Unreachable(
+            "Expected super target of a non-library override to be a program method ("
+                + "resolved program method: "
+                + resolvedMethod
+                + ", "
+                + "super non-program method: "
+                + target
+                + ")");
+      }
+      resolvedMethod = target.asProgramMethod();
+    }
+
     // Find the method where to store the information about the arguments from this invoke.
     // If the invoke may dispatch to more than one method, we intentionally do not compute all
     // possible dispatch targets and propagate the information to these methods (this is expensive).
@@ -155,53 +193,147 @@
     DexMethod representativeMethodReference =
         getRepresentativeForPolymorphicInvokeOrElse(
             invoke, resolvedMethod, resolvedMethod.getReference());
-    methodStates.addMethodState(
+    ProgramMethod finalResolvedMethod = resolvedMethod;
+    timing.begin("Add method state");
+    methodStates.addTemporaryMethodState(
         appView,
         representativeMethodReference,
-        () -> computeMethodState(invoke, resolvedMethod, context));
+        existingMethodState ->
+            computeMethodState(invoke, finalResolvedMethod, context, existingMethodState, timing),
+        timing);
+    timing.end();
   }
 
   private MethodState computeMethodState(
-      InvokeMethod invoke, ProgramMethod resolvedMethod, ProgramMethod context) {
+      InvokeMethod invoke,
+      ProgramMethod resolvedMethod,
+      ProgramMethod context,
+      MethodState existingMethodState,
+      Timing timing) {
+    assert !existingMethodState.isUnknown();
+
     // If this invoke may target at most one method, then we compute a state that maps each
     // parameter to the abstract value and dynamic type provided by this call site. Otherwise, we
     // compute a polymorphic method state, which includes information about the receiver's dynamic
     // type bounds.
+    timing.begin("Compute method state for invoke");
     boolean isPolymorphicInvoke =
         getRepresentativeForPolymorphicInvokeOrElse(invoke, resolvedMethod, null) != null;
-    return isPolymorphicInvoke
-        ? computePolymorphicMethodState(invoke.asInvokeMethodWithReceiver(), context)
-        : computeMonomorphicMethodState(invoke, context);
+    MethodState result;
+    if (isPolymorphicInvoke) {
+      assert existingMethodState.isBottom() || existingMethodState.isPolymorphic();
+      result =
+          computePolymorphicMethodState(
+              invoke.asInvokeMethodWithReceiver(),
+              resolvedMethod,
+              context,
+              existingMethodState.asPolymorphicOrBottom());
+    } else {
+      assert existingMethodState.isBottom() || existingMethodState.isMonomorphic();
+      result =
+          computeMonomorphicMethodState(
+              invoke, resolvedMethod, context, existingMethodState.asMonomorphicOrBottom());
+    }
+    timing.end();
+    return result;
   }
 
   // TODO(b/190154391): Add a strategy that widens the dynamic receiver type to allow easily
   //  experimenting with the performance/size trade-off between precise/imprecise handling of
   //  dynamic dispatch.
   private MethodState computePolymorphicMethodState(
-      InvokeMethodWithReceiver invoke, ProgramMethod context) {
+      InvokeMethodWithReceiver invoke,
+      ProgramMethod resolvedMethod,
+      ProgramMethod context,
+      ConcretePolymorphicMethodStateOrBottom existingMethodState) {
     DynamicType dynamicReceiverType = invoke.getReceiver().getDynamicType(appView);
-    ConcretePolymorphicMethodState methodState =
-        new ConcretePolymorphicMethodState(
-            dynamicReceiverType,
-            computeMonomorphicMethodState(invoke, context, dynamicReceiverType));
-    // TODO(b/190154391): If the receiver type is effectively unknown, and the computed monomorphic
-    //  method state is also unknown (i.e., we have "unknown receiver type" -> "unknown method
-    //  state"), then return the canonicalized UnknownMethodState instance instead.
-    return methodState;
+    assert !dynamicReceiverType.getDynamicUpperBoundType().nullability().isDefinitelyNull();
+
+    DynamicType bounds =
+        computeBoundsForPolymorphicMethodState(
+            invoke, resolvedMethod, context, dynamicReceiverType);
+    MethodState existingMethodStateForBounds =
+        existingMethodState.isPolymorphic()
+            ? existingMethodState.asPolymorphic().getMethodStateForBounds(bounds)
+            : MethodState.bottom();
+
+    if (existingMethodStateForBounds.isPolymorphic()) {
+      assert false;
+      return MethodState.unknown();
+    }
+
+    // If we already don't know anything about the parameters for the given type bounds, then don't
+    // compute a method state.
+    if (existingMethodStateForBounds.isUnknown()) {
+      return MethodState.unknown();
+    }
+
+    ConcreteMonomorphicMethodStateOrUnknown methodStateForBounds =
+        computeMonomorphicMethodState(
+            invoke,
+            resolvedMethod,
+            context,
+            existingMethodStateForBounds.asMonomorphicOrBottom(),
+            dynamicReceiverType);
+    return ConcretePolymorphicMethodState.create(bounds, methodStateForBounds);
+  }
+
+  private DynamicType computeBoundsForPolymorphicMethodState(
+      InvokeMethodWithReceiver invoke,
+      ProgramMethod resolvedMethod,
+      ProgramMethod context,
+      DynamicType dynamicReceiverType) {
+    ProgramMethod singleTarget = invoke.lookupSingleProgramTarget(appView, context);
+    DynamicType bounds =
+        singleTarget != null
+            ? DynamicType.createExact(
+                singleTarget.getHolderType().toTypeElement(appView).asClassType())
+            : dynamicReceiverType.withNullability(Nullability.maybeNull());
+
+    // We intentionally drop the nullability for the type bounds. This increases the number of
+    // collisions in the polymorphic method states, which does not change the precision (since the
+    // nullability does not have any impact on the possible dispatch targets) and is good for state
+    // pruning.
+    assert bounds.getDynamicUpperBoundType().nullability().isMaybeNull();
+
+    // If the bounds are trivial (i.e., the upper bound is equal to the holder of the virtual root
+    // method), then widen the type bounds to 'unknown'.
+    DexMethod virtualRootMethod = getVirtualRootMethod(resolvedMethod);
+    if (virtualRootMethod == null) {
+      assert false : "Unexpected virtual method without root: " + resolvedMethod;
+      return bounds;
+    }
+
+    DynamicType trivialBounds =
+        DynamicType.create(
+            appView, virtualRootMethod.getHolderType().toTypeElement(appView).asClassType());
+    if (bounds.equals(trivialBounds)) {
+      return DynamicType.unknown();
+    }
+    return bounds;
   }
 
   private ConcreteMonomorphicMethodStateOrUnknown computeMonomorphicMethodState(
-      InvokeMethod invoke, ProgramMethod context) {
+      InvokeMethod invoke,
+      ProgramMethod resolvedMethod,
+      ProgramMethod context,
+      ConcreteMonomorphicMethodStateOrBottom existingMethodState) {
     return computeMonomorphicMethodState(
         invoke,
+        resolvedMethod,
         context,
+        existingMethodState,
         invoke.isInvokeMethodWithReceiver()
             ? invoke.getFirstArgument().getDynamicType(appView)
             : null);
   }
 
   private ConcreteMonomorphicMethodStateOrUnknown computeMonomorphicMethodState(
-      InvokeMethod invoke, ProgramMethod context, DynamicType dynamicReceiverType) {
+      InvokeMethod invoke,
+      ProgramMethod resolvedMethod,
+      ProgramMethod context,
+      ConcreteMonomorphicMethodStateOrBottom existingMethodState,
+      DynamicType dynamicReceiverType) {
     List<ParameterState> parameterStates = new ArrayList<>(invoke.arguments().size());
 
     int argumentIndex = 0;
@@ -209,14 +341,21 @@
       assert dynamicReceiverType != null;
       parameterStates.add(
           computeParameterStateForReceiver(
-              invoke.asInvokeMethodWithReceiver(), dynamicReceiverType));
+              invoke.asInvokeMethodWithReceiver(),
+              resolvedMethod,
+              dynamicReceiverType,
+              existingMethodState));
       argumentIndex++;
     }
 
     for (; argumentIndex < invoke.arguments().size(); argumentIndex++) {
       parameterStates.add(
           computeParameterStateForNonReceiver(
-              invoke, argumentIndex, invoke.getArgument(argumentIndex), context));
+              invoke,
+              argumentIndex,
+              invoke.getArgument(argumentIndex),
+              context,
+              existingMethodState));
     }
 
     // If all parameter states are unknown, then return a canonicalized unknown method state that
@@ -233,27 +372,39 @@
   // TODO(b/190154391): Consider validating the above hypothesis by using
   //  computeParameterStateForNonReceiver() for receivers.
   private ParameterState computeParameterStateForReceiver(
-      InvokeMethodWithReceiver invoke, DynamicType dynamicReceiverType) {
-    ClassTypeElement staticReceiverType =
-        invoke
-            .getInvokedMethod()
-            .getHolderType()
-            .toTypeElement(appView)
-            .asClassType()
-            .asMeetWithNotNull();
-    return dynamicReceiverType.isTrivial(staticReceiverType)
+      InvokeMethodWithReceiver invoke,
+      ProgramMethod resolvedMethod,
+      DynamicType dynamicReceiverType,
+      ConcreteMonomorphicMethodStateOrBottom existingMethodState) {
+    // Don't compute a state for this parameter if the stored state is already unknown.
+    if (existingMethodState.isMonomorphic()
+        && existingMethodState.asMonomorphic().getParameterState(0).isUnknown()) {
+      return ParameterState.unknown();
+    }
+
+    DynamicType widenedDynamicReceiverType =
+        WideningUtils.widenDynamicReceiverType(appView, resolvedMethod, dynamicReceiverType);
+    return widenedDynamicReceiverType.isUnknown()
         ? ParameterState.unknown()
         : new ConcreteReceiverParameterState(dynamicReceiverType);
   }
 
   private ParameterState computeParameterStateForNonReceiver(
-      InvokeMethod invoke, int argumentIndex, Value argument, ProgramMethod context) {
+      InvokeMethod invoke,
+      int argumentIndex,
+      Value argument,
+      ProgramMethod context,
+      ConcreteMonomorphicMethodStateOrBottom existingMethodState) {
+    // Don't compute a state for this parameter if the stored state is already unknown.
+    if (existingMethodState.isMonomorphic()
+        && existingMethodState.asMonomorphic().getParameterState(argumentIndex).isUnknown()) {
+      return ParameterState.unknown();
+    }
+
     Value argumentRoot = argument.getAliasedValue(aliasedValueConfiguration);
-    TypeElement parameterType =
-        invoke
-            .getInvokedMethod()
-            .getArgumentType(argumentIndex, invoke.isInvokeStatic())
-            .toTypeElement(appView);
+    DexType parameterType =
+        invoke.getInvokedMethod().getArgumentType(argumentIndex, invoke.isInvokeStatic());
+    TypeElement parameterTypeElement = parameterType.toTypeElement(appView);
 
     // If the value is an argument of the enclosing method, then clearly we have no information
     // about its abstract value. Instead of treating this as having an unknown runtime value, we
@@ -264,18 +415,18 @@
       MethodParameter forwardedParameter =
           new MethodParameter(
               context.getReference(), argumentRoot.getDefinition().asArgument().getIndex());
-      if (parameterType.isClassType()) {
+      if (parameterTypeElement.isClassType()) {
         return new ConcreteClassTypeParameterState(forwardedParameter);
-      } else if (parameterType.isArrayType()) {
+      } else if (parameterTypeElement.isArrayType()) {
         return new ConcreteArrayTypeParameterState(forwardedParameter);
       } else {
-        assert parameterType.isPrimitiveType();
+        assert parameterTypeElement.isPrimitiveType();
         return new ConcretePrimitiveTypeParameterState(forwardedParameter);
       }
     }
 
     // Only track the nullability for array types.
-    if (parameterType.isArrayType()) {
+    if (parameterTypeElement.isArrayType()) {
       Nullability nullability = argument.getType().nullability();
       return nullability.isMaybeNull()
           ? ParameterState.unknown()
@@ -286,16 +437,18 @@
 
     // For class types, we track both the abstract value and the dynamic type. If both are unknown,
     // then use UnknownParameterState.
-    if (parameterType.isClassType()) {
+    if (parameterTypeElement.isClassType()) {
       DynamicType dynamicType = argument.getDynamicType(appView);
-      return abstractValue.isUnknown() && dynamicType.isTrivial(parameterType)
+      DynamicType widenedDynamicType =
+          WideningUtils.widenDynamicNonReceiverType(appView, dynamicType, parameterType);
+      return abstractValue.isUnknown() && widenedDynamicType.isUnknown()
           ? ParameterState.unknown()
-          : new ConcreteClassTypeParameterState(abstractValue, dynamicType);
+          : new ConcreteClassTypeParameterState(abstractValue, widenedDynamicType);
     }
 
     // For primitive types, we only track the abstract value, thus if the abstract value is unknown,
     // we use UnknownParameterState.
-    assert parameterType.isPrimitiveType();
+    assert parameterTypeElement.isPrimitiveType();
     return abstractValue.isUnknown()
         ? ParameterState.unknown()
         : new ConcretePrimitiveTypeParameterState(abstractValue);
@@ -303,16 +456,24 @@
 
   private DexMethod getRepresentativeForPolymorphicInvokeOrElse(
       InvokeMethod invoke, ProgramMethod resolvedMethod, DexMethod defaultValue) {
-    DexMethod resolvedMethodReference = resolvedMethod.getReference();
+    if (resolvedMethod.getDefinition().belongsToDirectPool()) {
+      return defaultValue;
+    }
+
     if (invoke.isInvokeInterface()) {
-      return resolvedMethodReference;
+      assert !isMonomorphicVirtualMethod(resolvedMethod);
+      return getVirtualRootMethod(resolvedMethod);
     }
-    DexMethod rootMethod = classMethodRoots.get(resolvedMethodReference);
-    if (rootMethod != null) {
-      assert invoke.isInvokeVirtual();
-      return rootMethod;
+
+    assert invoke.isInvokeSuper() || invoke.isInvokeVirtual();
+
+    if (isMonomorphicVirtualMethod(resolvedMethod)) {
+      return defaultValue;
     }
-    return defaultValue;
+
+    DexMethod rootMethod = getVirtualRootMethod(resolvedMethod);
+    assert rootMethod != null;
+    return rootMethod;
   }
 
   private void scan(InvokeCustom invoke, ProgramMethod context) {
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorEventConsumer.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorEventConsumer.java
new file mode 100644
index 0000000..c02d1ad
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorEventConsumer.java
@@ -0,0 +1,33 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation;
+
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
+
+public interface ArgumentPropagatorEventConsumer {
+
+  static ArgumentPropagatorEventConsumer emptyConsumer() {
+    return new ArgumentPropagatorEventConsumer() {
+      @Override
+      public void acceptCodeScannerResult(MethodStateCollectionByReference methodStates) {
+        // Intentionally empty.
+      }
+    };
+  }
+
+  void acceptCodeScannerResult(MethodStateCollectionByReference methodStates);
+
+  default ArgumentPropagatorEventConsumer andThen(
+      ArgumentPropagatorEventConsumer nextEventConsumer) {
+    ArgumentPropagatorEventConsumer self = this;
+    return new ArgumentPropagatorEventConsumer() {
+      @Override
+      public void acceptCodeScannerResult(MethodStateCollectionByReference methodStates) {
+        self.acceptCodeScannerResult(methodStates);
+        nextEventConsumer.acceptCodeScannerResult(methodStates);
+      }
+    };
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorIROptimizer.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorIROptimizer.java
index 6f7ec2c..e7060d3 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorIROptimizer.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorIROptimizer.java
@@ -78,7 +78,11 @@
       if (argumentValue.getType().isReferenceType()) {
         TypeElement dynamicUpperBoundType =
             optimizationInfo.getDynamicUpperBoundType(argument.getIndex());
-        if (dynamicUpperBoundType == null) {
+        if (dynamicUpperBoundType == null || dynamicUpperBoundType.isTop()) {
+          continue;
+        }
+        if (dynamicUpperBoundType.isBottom()) {
+          assert false;
           continue;
         }
         if (dynamicUpperBoundType.isDefinitelyNull()) {
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
index dbec312..6d4f3b7 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorOptimizationInfoPopulator.java
@@ -4,10 +4,15 @@
 
 package com.android.tools.r8.optimize.argumentpropagation;
 
+import static com.android.tools.r8.optimize.argumentpropagation.utils.StronglyConnectedProgramClasses.computeStronglyConnectedProgramClasses;
+
 import com.android.tools.r8.graph.AppView;
+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.ImmediateProgramSubtypingInfo;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.optimize.info.ConcreteCallSiteOptimizationInfo;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMonomorphicMethodState;
@@ -18,15 +23,15 @@
 import com.android.tools.r8.optimize.argumentpropagation.propagation.InParameterFlowPropagator;
 import com.android.tools.r8.optimize.argumentpropagation.propagation.InterfaceMethodArgumentPropagator;
 import com.android.tools.r8.optimize.argumentpropagation.propagation.VirtualDispatchMethodArgumentPropagator;
+import com.android.tools.r8.optimize.argumentpropagation.utils.WideningUtils;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.ThreadUtils;
-import com.android.tools.r8.utils.WorkList;
-import com.google.common.collect.Sets;
-import java.util.ArrayList;
+import com.android.tools.r8.utils.Timing;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import java.util.stream.IntStream;
 
 /**
  * Propagates the argument flow information collected by the {@link ArgumentPropagatorCodeScanner}.
@@ -50,47 +55,15 @@
         ImmediateProgramSubtypingInfo.create(appView);
     this.immediateSubtypingInfo = immediateSubtypingInfo;
     this.stronglyConnectedComponents =
-        computeStronglyConnectedComponents(appView, immediateSubtypingInfo);
-  }
-
-  /**
-   * Computes the strongly connected components in the program class hierarchy (where extends and
-   * implements edges are treated as bidirectional).
-   *
-   * <p>All strongly connected components can be processed in parallel.
-   */
-  private static List<Set<DexProgramClass>> computeStronglyConnectedComponents(
-      AppView<AppInfoWithLiveness> appView, ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
-    Set<DexProgramClass> seen = Sets.newIdentityHashSet();
-    List<Set<DexProgramClass>> stronglyConnectedComponents = new ArrayList<>();
-    for (DexProgramClass clazz : appView.appInfo().classes()) {
-      if (seen.contains(clazz)) {
-        continue;
-      }
-      Set<DexProgramClass> stronglyConnectedComponent =
-          computeStronglyConnectedComponent(clazz, immediateSubtypingInfo);
-      stronglyConnectedComponents.add(stronglyConnectedComponent);
-      seen.addAll(stronglyConnectedComponent);
-    }
-    return stronglyConnectedComponents;
-  }
-
-  private static Set<DexProgramClass> computeStronglyConnectedComponent(
-      DexProgramClass clazz, ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
-    WorkList<DexProgramClass> worklist = WorkList.newIdentityWorkList(clazz);
-    while (worklist.hasNext()) {
-      DexProgramClass current = worklist.next();
-      immediateSubtypingInfo.forEachImmediateProgramSuperClass(current, worklist::addIfNotSeen);
-      worklist.addIfNotSeen(immediateSubtypingInfo.getSubclasses(current));
-    }
-    return worklist.getSeenSet();
+        computeStronglyConnectedProgramClasses(appView, immediateSubtypingInfo);
   }
 
   /**
    * Computes an over-approximation of each parameter's value and type and stores the result in
    * {@link com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo}.
    */
-  void populateOptimizationInfo(ExecutorService executorService) throws ExecutionException {
+  void populateOptimizationInfo(ExecutorService executorService, Timing timing)
+      throws ExecutionException {
     // TODO(b/190154391): Propagate argument information to handle virtual dispatch.
     // TODO(b/190154391): To deal with arguments that are themselves passed as arguments to invoke
     //  instructions, build a flow graph where nodes are parameters and there is an edge from a
@@ -99,14 +72,20 @@
     // TODO(b/190154391): If we learn that parameter p1 is constant, and that the enclosing method
     //  returns p1 according to the optimization info, then update the optimization info to describe
     //  that the method returns the constant.
+    timing.begin("Propagate argument information for virtual methods");
     ThreadUtils.processItems(
         stronglyConnectedComponents, this::processStronglyConnectedComponent, executorService);
+    timing.end();
 
     // Solve the parameter flow constraints.
+    timing.begin("Solve flow constraints");
     new InParameterFlowPropagator(appView, methodStates).run(executorService);
+    timing.end();
 
     // The information stored on each method is now sound, and can be used as optimization info.
+    timing.begin("Set optimization info");
     setOptimizationInfo(executorService);
+    timing.end();
 
     assert methodStates.isEmpty();
   }
@@ -130,6 +109,13 @@
     // of the method's holder. All that remains is to propagate the information downwards in the
     // class hierarchy to propagate the argument information for a non-private virtual method to its
     // overrides.
+    // TODO(b/190154391): Before running the top-down traversal, consider lowering the argument
+    //  information for non-private virtual methods. If we have some argument information with upper
+    //  bound=B, which is stored on a method on class A, we could move this argument information
+    //  from class A to B. This way we could potentially get rid of the "inactive argument
+    //  information" during the depth-first class hierarchy traversal, since the argument
+    //  information would be active by construction when it is first seen during the top-down class
+    //  hierarchy traversal.
     new VirtualDispatchMethodArgumentPropagator(appView, immediateSubtypingInfo, methodStates)
         .run(stronglyConnectedComponent);
   }
@@ -144,10 +130,20 @@
   }
 
   private void setOptimizationInfo(ProgramMethod method) {
-    MethodState methodState = methodStates.remove(method);
+    MethodState methodState = methodStates.removeOrElse(method, null);
+    if (methodState == null) {
+      return;
+    }
+
     if (methodState.isBottom()) {
-      // TODO(b/190154391): This should only happen if the method is never called. Consider removing
-      //  the method in this case.
+      if (!appView.options().canUseDefaultAndStaticInterfaceMethods()
+          && method.getHolder().isInterface()) {
+        // TODO(b/190154391): The method has not been moved to the companion class yet, so we can't
+        //  remove its code object.
+        return;
+      }
+      DexEncodedMethod definition = method.getDefinition();
+      definition.setCode(definition.buildEmptyThrowingCode(appView.options()), appView);
       return;
     }
 
@@ -163,11 +159,37 @@
     }
 
     ConcreteMonomorphicMethodState monomorphicMethodState = concreteMethodState.asMonomorphic();
+
+    // Verify that there is no parameter with bottom info.
+    assert monomorphicMethodState.getParameterStates().stream().noneMatch(ParameterState::isBottom);
+
+    // Verify that all in-parameter information has been pruned by the InParameterFlowPropagator.
     assert monomorphicMethodState.getParameterStates().stream()
         .filter(ParameterState::isConcrete)
         .map(ParameterState::asConcrete)
         .noneMatch(ConcreteParameterState::hasInParameters);
 
+    // Verify that the dynamic type information is correct.
+    assert IntStream.range(0, monomorphicMethodState.getParameterStates().size())
+        .filter(
+            index -> {
+              ParameterState parameterState = monomorphicMethodState.getParameterState(index);
+              return parameterState.isConcrete() && parameterState.asConcrete().isClassParameter();
+            })
+        .allMatch(
+            index -> {
+              DynamicType dynamicType =
+                  monomorphicMethodState
+                      .getParameterState(index)
+                      .asConcrete()
+                      .asClassParameter()
+                      .getDynamicType();
+              DexType staticType = method.getArgumentType(index);
+              assert dynamicType
+                  == WideningUtils.widenDynamicNonReceiverType(appView, dynamicType, staticType);
+              return true;
+            });
+
     method
         .getDefinition()
         .joinCallSiteOptimizationInfo(
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableMethods.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableMethods.java
index 36dae85..0e643f6 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableMethods.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagatorUnoptimizableMethods.java
@@ -95,7 +95,10 @@
     for (DexProgramClass clazz : stronglyConnectedComponent) {
       clazz.forEachProgramMethod(
           method -> {
-            assert !method.getDefinition().isLibraryMethodOverride().isUnknown();
+            assert !method.getDefinition().belongsToVirtualPool()
+                    || !method.getDefinition().isLibraryMethodOverride().isUnknown()
+                : "Unexpected virtual method without library method override information: "
+                    + method.toSourceString();
             if (method.getDefinition().isLibraryMethodOverride().isPossiblyTrue()
                 || appInfo.isMethodTargetedByInvokeDynamic(method)
                 || !appInfo
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomArrayTypeParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomArrayTypeParameterState.java
new file mode 100644
index 0000000..666c87b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomArrayTypeParameterState.java
@@ -0,0 +1,46 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.Nullability;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.Action;
+
+public class BottomArrayTypeParameterState extends BottomParameterState {
+
+  private static final BottomArrayTypeParameterState INSTANCE = new BottomArrayTypeParameterState();
+
+  private BottomArrayTypeParameterState() {}
+
+  public static BottomArrayTypeParameterState get() {
+    return INSTANCE;
+  }
+
+  @Override
+  public ParameterState mutableJoin(
+      AppView<AppInfoWithLiveness> appView,
+      ParameterState parameterState,
+      DexType parameterType,
+      Action onChangedAction) {
+    if (parameterState.isBottom()) {
+      return this;
+    }
+    if (parameterState.isUnknown()) {
+      return parameterState;
+    }
+    assert parameterState.isConcrete();
+    assert parameterState.asConcrete().isReferenceParameter();
+    ConcreteReferenceTypeParameterState concreteParameterState =
+        parameterState.asConcrete().asReferenceParameter();
+    Nullability nullability = concreteParameterState.getNullability();
+    if (nullability.isMaybeNull()) {
+      return unknown();
+    }
+    return new ConcreteArrayTypeParameterState(
+        nullability, concreteParameterState.copyInParameters());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomClassTypeParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomClassTypeParameterState.java
new file mode 100644
index 0000000..d1df0ac
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomClassTypeParameterState.java
@@ -0,0 +1,50 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.optimize.argumentpropagation.utils.WideningUtils;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.Action;
+
+public class BottomClassTypeParameterState extends BottomParameterState {
+
+  private static final BottomClassTypeParameterState INSTANCE = new BottomClassTypeParameterState();
+
+  private BottomClassTypeParameterState() {}
+
+  public static BottomClassTypeParameterState get() {
+    return INSTANCE;
+  }
+
+  @Override
+  public ParameterState mutableJoin(
+      AppView<AppInfoWithLiveness> appView,
+      ParameterState parameterState,
+      DexType parameterType,
+      Action onChangedAction) {
+    if (parameterState.isBottom()) {
+      return this;
+    }
+    if (parameterState.isUnknown()) {
+      return parameterState;
+    }
+    assert parameterState.isConcrete();
+    assert parameterState.asConcrete().isReferenceParameter();
+    ConcreteReferenceTypeParameterState concreteParameterState =
+        parameterState.asConcrete().asReferenceParameter();
+    AbstractValue abstractValue = concreteParameterState.getAbstractValue(appView);
+    DynamicType dynamicType = concreteParameterState.getDynamicType();
+    DynamicType widenedDynamicType =
+        WideningUtils.widenDynamicNonReceiverType(appView, dynamicType, parameterType);
+    return abstractValue.isUnknown() && widenedDynamicType.isUnknown()
+        ? unknown()
+        : new ConcreteClassTypeParameterState(
+            abstractValue, widenedDynamicType, concreteParameterState.copyInParameters());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomMethodState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomMethodState.java
index fea97a2..f5f117e 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomMethodState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomMethodState.java
@@ -5,10 +5,12 @@
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethodSignature;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import java.util.function.Supplier;
+import java.util.function.Function;
 
-public class BottomMethodState extends MethodStateBase {
+public class BottomMethodState extends MethodStateBase
+    implements ConcreteMonomorphicMethodStateOrBottom, ConcretePolymorphicMethodStateOrBottom {
 
   private static final BottomMethodState INSTANCE = new BottomMethodState();
 
@@ -24,13 +26,33 @@
   }
 
   @Override
-  public MethodState mutableJoin(AppView<AppInfoWithLiveness> appView, MethodState methodState) {
-    return methodState;
+  public ConcreteMonomorphicMethodStateOrBottom asMonomorphicOrBottom() {
+    return this;
+  }
+
+  @Override
+  public ConcretePolymorphicMethodStateOrBottom asPolymorphicOrBottom() {
+    return this;
+  }
+
+  @Override
+  public MethodState mutableCopy() {
+    return this;
   }
 
   @Override
   public MethodState mutableJoin(
-      AppView<AppInfoWithLiveness> appView, Supplier<MethodState> methodStateSupplier) {
-    return methodStateSupplier.get();
+      AppView<AppInfoWithLiveness> appView,
+      DexMethodSignature methodSignature,
+      MethodState methodState) {
+    return methodState.mutableCopy();
+  }
+
+  @Override
+  public MethodState mutableJoin(
+      AppView<AppInfoWithLiveness> appView,
+      DexMethodSignature methodSignature,
+      Function<MethodState, MethodState> methodStateSupplier) {
+    return mutableJoin(appView, methodSignature, methodStateSupplier.apply(this));
   }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomParameterState.java
new file mode 100644
index 0000000..a1a3908
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomParameterState.java
@@ -0,0 +1,29 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+public abstract class BottomParameterState extends ParameterState {
+
+  BottomParameterState() {}
+
+  @Override
+  public final AbstractValue getAbstractValue(AppView<AppInfoWithLiveness> appView) {
+    return AbstractValue.bottom();
+  }
+
+  @Override
+  public final boolean isBottom() {
+    return true;
+  }
+
+  @Override
+  public final ParameterState mutableCopy() {
+    return this;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomPrimitiveTypeParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomPrimitiveTypeParameterState.java
new file mode 100644
index 0000000..fc88145
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomPrimitiveTypeParameterState.java
@@ -0,0 +1,40 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.Action;
+
+public class BottomPrimitiveTypeParameterState extends BottomParameterState {
+
+  private static final BottomPrimitiveTypeParameterState INSTANCE =
+      new BottomPrimitiveTypeParameterState();
+
+  private BottomPrimitiveTypeParameterState() {}
+
+  public static BottomPrimitiveTypeParameterState get() {
+    return INSTANCE;
+  }
+
+  @Override
+  public ParameterState mutableJoin(
+      AppView<AppInfoWithLiveness> appView,
+      ParameterState parameterState,
+      DexType parameterType,
+      Action onChangedAction) {
+    if (parameterState.isBottom()) {
+      assert parameterState == bottomPrimitiveTypeParameter();
+      return this;
+    }
+    if (parameterState.isUnknown()) {
+      return parameterState;
+    }
+    assert parameterState.isConcrete();
+    assert parameterState.asConcrete().isPrimitiveParameter();
+    return parameterState.mutableCopy();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomReceiverParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomReceiverParameterState.java
new file mode 100644
index 0000000..3a94858
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/BottomReceiverParameterState.java
@@ -0,0 +1,46 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.Action;
+
+public class BottomReceiverParameterState extends BottomParameterState {
+
+  private static final BottomReceiverParameterState INSTANCE = new BottomReceiverParameterState();
+
+  private BottomReceiverParameterState() {}
+
+  public static BottomReceiverParameterState get() {
+    return INSTANCE;
+  }
+
+  @Override
+  public ParameterState mutableJoin(
+      AppView<AppInfoWithLiveness> appView,
+      ParameterState parameterState,
+      DexType parameterType,
+      Action onChangedAction) {
+    if (parameterState.isBottom()) {
+      return this;
+    }
+    if (parameterState.isUnknown()) {
+      return parameterState;
+    }
+    assert parameterState.isConcrete();
+    assert parameterState.asConcrete().isReferenceParameter();
+    ConcreteReferenceTypeParameterState concreteParameterState =
+        parameterState.asConcrete().asReferenceParameter();
+    DynamicType dynamicType = concreteParameterState.getDynamicType();
+    if (dynamicType.isUnknown()) {
+      return unknown();
+    }
+    return new ConcreteReceiverParameterState(
+        dynamicType, concreteParameterState.copyInParameters());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteArrayTypeParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteArrayTypeParameterState.java
index e7533e5..eb75ac9 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteArrayTypeParameterState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteArrayTypeParameterState.java
@@ -4,48 +4,68 @@
 
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Action;
+import com.android.tools.r8.utils.SetUtils;
+import java.util.Collections;
+import java.util.Set;
 
-public class ConcreteArrayTypeParameterState extends ConcreteParameterState {
+public class ConcreteArrayTypeParameterState extends ConcreteReferenceTypeParameterState {
 
   private Nullability nullability;
 
   public ConcreteArrayTypeParameterState(MethodParameter inParameter) {
-    super(inParameter);
-    this.nullability = Nullability.bottom();
+    this(Nullability.bottom(), SetUtils.newHashSet(inParameter));
   }
 
   public ConcreteArrayTypeParameterState(Nullability nullability) {
-    assert !nullability.isMaybeNull() : "Must use UnknownParameterState instead";
-    this.nullability = nullability;
+    this(nullability, Collections.emptySet());
   }
 
-  public ParameterState mutableJoin(
-      ConcreteArrayTypeParameterState parameterState, Action onChangedAction) {
-    assert !nullability.isMaybeNull();
-    assert !parameterState.nullability.isMaybeNull();
-    boolean inParametersChanged = mutableJoinInParameters(parameterState);
-    if (widenInParameters()) {
-      return unknown();
+  public ConcreteArrayTypeParameterState(
+      Nullability nullability, Set<MethodParameter> inParameters) {
+    super(inParameters);
+    this.nullability = nullability;
+    assert !isEffectivelyBottom() : "Must use BottomArrayTypeParameterState instead";
+    assert !isEffectivelyUnknown() : "Must use UnknownParameterState instead";
+  }
+
+  @Override
+  public ParameterState clearInParameters() {
+    if (hasInParameters()) {
+      if (nullability.isBottom()) {
+        return bottomArrayTypeParameter();
+      }
+      internalClearInParameters();
     }
-    if (inParametersChanged) {
-      onChangedAction.execute();
-    }
+    assert !isEffectivelyBottom();
     return this;
   }
 
   @Override
-  public AbstractValue getAbstractValue() {
+  public AbstractValue getAbstractValue(AppView<AppInfoWithLiveness> appView) {
+    if (getNullability().isDefinitelyNull()) {
+      return appView.abstractValueFactory().createNullValue();
+    }
     return AbstractValue.unknown();
   }
 
   @Override
+  public DynamicType getDynamicType() {
+    return DynamicType.unknown();
+  }
+
+  @Override
   public ConcreteParameterStateKind getKind() {
     return ConcreteParameterStateKind.ARRAY;
   }
 
+  @Override
   public Nullability getNullability() {
     return nullability;
   }
@@ -59,4 +79,39 @@
   public ConcreteArrayTypeParameterState asArrayParameter() {
     return this;
   }
+
+  public boolean isEffectivelyBottom() {
+    return nullability.isBottom() && !hasInParameters();
+  }
+
+  public boolean isEffectivelyUnknown() {
+    return nullability.isMaybeNull();
+  }
+
+  @Override
+  public ParameterState mutableCopy() {
+    return new ConcreteArrayTypeParameterState(nullability, copyInParameters());
+  }
+
+  @Override
+  public ParameterState mutableJoin(
+      AppView<AppInfoWithLiveness> appView,
+      ConcreteReferenceTypeParameterState parameterState,
+      DexType parameterType,
+      Action onChangedAction) {
+    assert parameterType.isArrayType();
+    assert !nullability.isUnknown();
+    nullability = nullability.join(parameterState.getNullability());
+    if (nullability.isUnknown()) {
+      return unknown();
+    }
+    boolean inParametersChanged = mutableJoinInParameters(parameterState);
+    if (widenInParameters()) {
+      return unknown();
+    }
+    if (inParametersChanged) {
+      onChangedAction.execute();
+    }
+    return this;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteClassTypeParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteClassTypeParameterState.java
index 3c59a07..6dc6c5c 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteClassTypeParameterState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteClassTypeParameterState.java
@@ -5,69 +5,68 @@
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.type.DynamicType;
+import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.optimize.argumentpropagation.utils.WideningUtils;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Action;
+import com.android.tools.r8.utils.SetUtils;
+import java.util.Collections;
+import java.util.Set;
 
-public class ConcreteClassTypeParameterState extends ConcreteParameterState {
+public class ConcreteClassTypeParameterState extends ConcreteReferenceTypeParameterState {
 
   private AbstractValue abstractValue;
   private DynamicType dynamicType;
 
   public ConcreteClassTypeParameterState(MethodParameter inParameter) {
-    super(inParameter);
-    this.abstractValue = AbstractValue.bottom();
-    this.dynamicType = DynamicType.bottom();
+    this(AbstractValue.bottom(), DynamicType.bottom(), SetUtils.newHashSet(inParameter));
   }
 
   public ConcreteClassTypeParameterState(AbstractValue abstractValue, DynamicType dynamicType) {
-    this.abstractValue = abstractValue;
-    this.dynamicType = dynamicType;
+    this(abstractValue, dynamicType, Collections.emptySet());
   }
 
-  public ParameterState mutableJoin(
-      AppView<AppInfoWithLiveness> appView,
-      ConcreteClassTypeParameterState parameterState,
-      Action onChangedAction) {
-    boolean allowNullOrAbstractValue = true;
-    boolean allowNonConstantNumbers = false;
-    AbstractValue oldAbstractValue = abstractValue;
-    abstractValue =
-        abstractValue.join(
-            parameterState.abstractValue,
-            appView.abstractValueFactory(),
-            allowNullOrAbstractValue,
-            allowNonConstantNumbers);
-    // TODO(b/190154391): Join the dynamic types using SubtypingInfo.
-    // TODO(b/190154391): Take in the static type as an argument, and unset the dynamic type if it
-    //  equals the static type.
-    DynamicType oldDynamicType = dynamicType;
-    dynamicType =
-        dynamicType.equals(parameterState.dynamicType) ? dynamicType : DynamicType.unknown();
-    if (abstractValue.isUnknown() && dynamicType.isUnknown()) {
-      return unknown();
+  public ConcreteClassTypeParameterState(
+      AbstractValue abstractValue, DynamicType dynamicType, Set<MethodParameter> inParameters) {
+    super(inParameters);
+    this.abstractValue = abstractValue;
+    this.dynamicType = dynamicType;
+    assert !isEffectivelyBottom() : "Must use BottomClassTypeParameterState instead";
+    assert !isEffectivelyUnknown() : "Must use UnknownParameterState instead";
+  }
+
+  @Override
+  public ParameterState clearInParameters() {
+    if (hasInParameters()) {
+      if (abstractValue.isBottom()) {
+        assert dynamicType.isBottom();
+        return bottomClassTypeParameter();
+      }
+      internalClearInParameters();
     }
-    boolean inParametersChanged = mutableJoinInParameters(parameterState);
-    if (widenInParameters()) {
-      return unknown();
-    }
-    if (abstractValue != oldAbstractValue || dynamicType != oldDynamicType || inParametersChanged) {
-      onChangedAction.execute();
-    }
+    assert !isEffectivelyBottom();
     return this;
   }
 
   @Override
-  public AbstractValue getAbstractValue() {
+  public AbstractValue getAbstractValue(AppView<AppInfoWithLiveness> appView) {
     return abstractValue;
   }
 
+  @Override
   public DynamicType getDynamicType() {
     return dynamicType;
   }
 
   @Override
+  public Nullability getNullability() {
+    return getDynamicType().getDynamicUpperBoundType().nullability();
+  }
+
+  @Override
   public ConcreteParameterStateKind getKind() {
     return ConcreteParameterStateKind.CLASS;
   }
@@ -81,4 +80,53 @@
   public ConcreteClassTypeParameterState asClassParameter() {
     return this;
   }
+
+  public boolean isEffectivelyBottom() {
+    return abstractValue.isBottom() && dynamicType.isBottom() && !hasInParameters();
+  }
+
+  public boolean isEffectivelyUnknown() {
+    return abstractValue.isUnknown() && dynamicType.isUnknown();
+  }
+
+  @Override
+  public ParameterState mutableCopy() {
+    return new ConcreteClassTypeParameterState(abstractValue, dynamicType, copyInParameters());
+  }
+
+  @Override
+  public ParameterState mutableJoin(
+      AppView<AppInfoWithLiveness> appView,
+      ConcreteReferenceTypeParameterState parameterState,
+      DexType parameterType,
+      Action onChangedAction) {
+    assert parameterType.isClassType();
+    boolean allowNullOrAbstractValue = true;
+    boolean allowNonConstantNumbers = false;
+    AbstractValue oldAbstractValue = abstractValue;
+    abstractValue =
+        abstractValue.join(
+            parameterState.getAbstractValue(appView),
+            appView.abstractValueFactory(),
+            allowNullOrAbstractValue,
+            allowNonConstantNumbers);
+    DynamicType oldDynamicType = dynamicType;
+    DynamicType joinedDynamicType = dynamicType.join(appView, parameterState.getDynamicType());
+    DynamicType widenedDynamicType =
+        WideningUtils.widenDynamicNonReceiverType(appView, joinedDynamicType, parameterType);
+    dynamicType = widenedDynamicType;
+    if (abstractValue.isUnknown() && dynamicType.isUnknown()) {
+      return unknown();
+    }
+    boolean inParametersChanged = mutableJoinInParameters(parameterState);
+    if (widenInParameters()) {
+      return unknown();
+    }
+    if (abstractValue != oldAbstractValue
+        || !dynamicType.equals(oldDynamicType)
+        || inParametersChanged) {
+      onChangedAction.execute();
+    }
+    return this;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMethodState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMethodState.java
index 6aae75f..ecd3e08 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMethodState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMethodState.java
@@ -5,8 +5,9 @@
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethodSignature;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import java.util.function.Supplier;
+import java.util.function.Function;
 
 public abstract class ConcreteMethodState extends MethodStateBase {
 
@@ -20,38 +21,37 @@
     return this;
   }
 
-  public boolean isPolymorphic() {
-    return false;
-  }
-
-  public ConcretePolymorphicMethodState asPolymorphic() {
-    return null;
-  }
-
   @Override
-  public MethodState mutableJoin(AppView<AppInfoWithLiveness> appView, MethodState methodState) {
+  public MethodState mutableJoin(
+      AppView<AppInfoWithLiveness> appView,
+      DexMethodSignature methodSignature,
+      MethodState methodState) {
     if (methodState.isBottom()) {
       return this;
     }
     if (methodState.isUnknown()) {
       return methodState;
     }
-    return mutableJoin(appView, methodState.asConcrete());
+    return mutableJoin(appView, methodSignature, methodState.asConcrete());
   }
 
   @Override
   public MethodState mutableJoin(
-      AppView<AppInfoWithLiveness> appView, Supplier<MethodState> methodStateSupplier) {
-    return mutableJoin(appView, methodStateSupplier.get());
+      AppView<AppInfoWithLiveness> appView,
+      DexMethodSignature methodSignature,
+      Function<MethodState, MethodState> methodStateSupplier) {
+    return mutableJoin(appView, methodSignature, methodStateSupplier.apply(this));
   }
 
   private MethodState mutableJoin(
-      AppView<AppInfoWithLiveness> appView, ConcreteMethodState methodState) {
+      AppView<AppInfoWithLiveness> appView,
+      DexMethodSignature methodSignature,
+      ConcreteMethodState methodState) {
     if (isMonomorphic() && methodState.isMonomorphic()) {
-      return asMonomorphic().mutableJoin(appView, methodState.asMonomorphic());
+      return asMonomorphic().mutableJoin(appView, methodSignature, methodState.asMonomorphic());
     }
     if (isPolymorphic() && methodState.isPolymorphic()) {
-      return asPolymorphic().mutableJoin(appView, methodState.asPolymorphic());
+      return asPolymorphic().mutableJoin(appView, methodSignature, methodState.asPolymorphic());
     }
     assert false;
     return unknown();
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMonomorphicMethodState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMonomorphicMethodState.java
index fc8fe25..b49e48f 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMonomorphicMethodState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMonomorphicMethodState.java
@@ -5,16 +5,22 @@
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethodSignature;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.google.common.collect.Iterables;
+import com.google.common.collect.Streams;
+import java.util.ArrayList;
 import java.util.List;
 
 public class ConcreteMonomorphicMethodState extends ConcreteMethodState
-    implements ConcreteMonomorphicMethodStateOrUnknown {
+    implements ConcreteMonomorphicMethodStateOrBottom, ConcreteMonomorphicMethodStateOrUnknown {
 
   List<ParameterState> parameterStates;
 
   public ConcreteMonomorphicMethodState(List<ParameterState> parameterStates) {
+    assert Streams.stream(Iterables.skip(parameterStates, 1))
+        .noneMatch(x -> x.isConcrete() && x.asConcrete().isReceiverParameter());
     assert Iterables.any(parameterStates, parameterState -> !parameterState.isUnknown())
         : "Must use UnknownMethodState instead";
     this.parameterStates = parameterStates;
@@ -28,17 +34,43 @@
     return parameterStates;
   }
 
+  @Override
+  public ConcreteMonomorphicMethodState mutableCopy() {
+    List<ParameterState> copiedParametersStates = new ArrayList<>(size());
+    for (ParameterState parameterState : getParameterStates()) {
+      copiedParametersStates.add(parameterState.mutableCopy());
+    }
+    return new ConcreteMonomorphicMethodState(copiedParametersStates);
+  }
+
   public ConcreteMonomorphicMethodStateOrUnknown mutableJoin(
-      AppView<AppInfoWithLiveness> appView, ConcreteMonomorphicMethodState methodState) {
+      AppView<AppInfoWithLiveness> appView,
+      DexMethodSignature methodSignature,
+      ConcreteMonomorphicMethodState methodState) {
     if (size() != methodState.size()) {
       assert false;
       return unknown();
     }
 
-    for (int i = 0; i < size(); i++) {
-      ParameterState parameterState = parameterStates.get(i);
-      ParameterState otherParameterState = methodState.parameterStates.get(i);
-      parameterStates.set(i, parameterState.mutableJoin(appView, otherParameterState));
+    int argumentIndex = 0;
+    if (size() > methodSignature.getArity()) {
+      assert size() == methodSignature.getArity() + 1;
+      ParameterState parameterState = parameterStates.get(0);
+      ParameterState otherParameterState = methodState.parameterStates.get(0);
+      DexType parameterType = null;
+      parameterStates.set(
+          0, parameterState.mutableJoin(appView, otherParameterState, parameterType));
+      argumentIndex++;
+    }
+
+    for (int parameterIndex = 0; argumentIndex < size(); argumentIndex++, parameterIndex++) {
+      ParameterState parameterState = parameterStates.get(argumentIndex);
+      ParameterState otherParameterState = methodState.parameterStates.get(argumentIndex);
+      DexType parameterType = methodSignature.getParameter(parameterIndex);
+      parameterStates.set(
+          argumentIndex, parameterState.mutableJoin(appView, otherParameterState, parameterType));
+      assert !parameterStates.get(argumentIndex).isConcrete()
+          || !parameterStates.get(argumentIndex).asConcrete().isReceiverParameter();
     }
 
     if (Iterables.all(parameterStates, ParameterState::isUnknown)) {
@@ -57,7 +89,15 @@
     return this;
   }
 
+  @Override
+  public ConcreteMonomorphicMethodStateOrBottom asMonomorphicOrBottom() {
+    return this;
+  }
+
   public void setParameterState(int index, ParameterState parameterState) {
+    assert index == 0
+        || !parameterState.isConcrete()
+        || !parameterState.asConcrete().isReceiverParameter();
     parameterStates.set(index, parameterState);
   }
 
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMonomorphicMethodStateOrBottom.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMonomorphicMethodStateOrBottom.java
new file mode 100644
index 0000000..9528fd7
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMonomorphicMethodStateOrBottom.java
@@ -0,0 +1,7 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+public interface ConcreteMonomorphicMethodStateOrBottom extends MethodState {}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMonomorphicMethodStateOrUnknown.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMonomorphicMethodStateOrUnknown.java
index 1b90e84..2239ffd 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMonomorphicMethodStateOrUnknown.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteMonomorphicMethodStateOrUnknown.java
@@ -4,4 +4,8 @@
 
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
-public interface ConcreteMonomorphicMethodStateOrUnknown extends MethodState {}
+public interface ConcreteMonomorphicMethodStateOrUnknown extends MethodState {
+
+  @Override
+  ConcreteMonomorphicMethodStateOrUnknown mutableCopy();
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteParameterState.java
index 1e5ebae..3b9da36 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteParameterState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteParameterState.java
@@ -5,16 +5,16 @@
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Action;
-import com.android.tools.r8.utils.SetUtils;
-import com.google.common.collect.Sets;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Set;
 
-public abstract class ConcreteParameterState extends ParameterState {
+public abstract class ConcreteParameterState extends NonEmptyParameterState {
 
-  enum ConcreteParameterStateKind {
+  public enum ConcreteParameterStateKind {
     ARRAY,
     CLASS,
     PRIMITIVE,
@@ -23,16 +23,22 @@
 
   private Set<MethodParameter> inParameters;
 
-  ConcreteParameterState() {
-    this.inParameters = Collections.emptySet();
+  ConcreteParameterState(Set<MethodParameter> inParameters) {
+    this.inParameters = inParameters;
   }
 
-  ConcreteParameterState(MethodParameter inParameter) {
-    this.inParameters = SetUtils.newHashSet(inParameter);
+  public abstract ParameterState clearInParameters();
+
+  void internalClearInParameters() {
+    inParameters = Collections.emptySet();
   }
 
-  public void clearInParameters() {
-    inParameters.clear();
+  public Set<MethodParameter> copyInParameters() {
+    if (inParameters.isEmpty()) {
+      assert inParameters == Collections.<MethodParameter>emptySet();
+      return inParameters;
+    }
+    return new HashSet<>(inParameters);
   }
 
   public boolean hasInParameters() {
@@ -40,6 +46,7 @@
   }
 
   public Set<MethodParameter> getInParameters() {
+    assert inParameters.isEmpty() || inParameters instanceof HashSet<?>;
     return inParameters;
   }
 
@@ -77,6 +84,14 @@
     return null;
   }
 
+  public boolean isReferenceParameter() {
+    return false;
+  }
+
+  public ConcreteReferenceTypeParameterState asReferenceParameter() {
+    return null;
+  }
+
   @Override
   public boolean isConcrete() {
     return true;
@@ -88,36 +103,30 @@
   }
 
   @Override
-  public ParameterState mutableJoin(
-      AppView<AppInfoWithLiveness> appView, ParameterState parameterState, Action onChangedAction) {
+  public final ParameterState mutableJoin(
+      AppView<AppInfoWithLiveness> appView,
+      ParameterState parameterState,
+      DexType parameterType,
+      Action onChangedAction) {
+    if (parameterState.isBottom()) {
+      return this;
+    }
     if (parameterState.isUnknown()) {
       return parameterState;
     }
-    ConcreteParameterStateKind kind = getKind();
-    ConcreteParameterStateKind otherKind = parameterState.asConcrete().getKind();
-    if (kind == otherKind) {
-      switch (getKind()) {
-        case ARRAY:
-          return asArrayParameter()
-              .mutableJoin(parameterState.asConcrete().asArrayParameter(), onChangedAction);
-        case CLASS:
-          return asClassParameter()
-              .mutableJoin(
-                  appView, parameterState.asConcrete().asClassParameter(), onChangedAction);
-        case PRIMITIVE:
-          return asPrimitiveParameter()
-              .mutableJoin(
-                  appView, parameterState.asConcrete().asPrimitiveParameter(), onChangedAction);
-        case RECEIVER:
-          return asReceiverParameter()
-              .mutableJoin(parameterState.asConcrete().asReceiverParameter(), onChangedAction);
-        default:
-          // Dead.
-      }
+    ConcreteParameterState concreteParameterState = parameterState.asConcrete();
+    if (isReferenceParameter()) {
+      assert concreteParameterState.isReferenceParameter();
+      return asReferenceParameter()
+          .mutableJoin(
+              appView,
+              concreteParameterState.asReferenceParameter(),
+              parameterType,
+              onChangedAction);
     }
-
-    assert false;
-    return unknown();
+    return asPrimitiveParameter()
+        .mutableJoin(
+            appView, concreteParameterState.asPrimitiveParameter(), parameterType, onChangedAction);
   }
 
   boolean mutableJoinInParameters(ConcreteParameterState parameterState) {
@@ -126,7 +135,7 @@
     }
     if (inParameters.isEmpty()) {
       assert inParameters == Collections.<MethodParameter>emptySet();
-      inParameters = Sets.newIdentityHashSet();
+      inParameters = new HashSet<>();
     }
     return inParameters.addAll(parameterState.inParameters);
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePolymorphicMethodState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePolymorphicMethodState.java
index 2e5a908..c4a1feb 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePolymorphicMethodState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePolymorphicMethodState.java
@@ -5,62 +5,169 @@
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethodSignature;
 import com.android.tools.r8.ir.analysis.type.DynamicType;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.function.BiConsumer;
+import java.util.function.Function;
 
-public class ConcretePolymorphicMethodState extends ConcreteMethodState {
+public class ConcretePolymorphicMethodState extends ConcreteMethodState
+    implements ConcretePolymorphicMethodStateOrBottom, ConcretePolymorphicMethodStateOrUnknown {
 
-  private final Map<DynamicType, ConcreteMonomorphicMethodStateOrUnknown> receiverBoundsToState =
-      new HashMap<>();
+  private final Map<DynamicType, ConcreteMonomorphicMethodStateOrUnknown> receiverBoundsToState;
 
-  public ConcretePolymorphicMethodState(
-      DynamicType receiverBounds, ConcreteMonomorphicMethodStateOrUnknown methodState) {
-    // TODO(b/190154391): Ensure that we use the unknown state instead of mapping unknown -> unknown
-    //  here.
-    receiverBoundsToState.put(receiverBounds, methodState);
+  private ConcretePolymorphicMethodState(
+      Map<DynamicType, ConcreteMonomorphicMethodStateOrUnknown> receiverBoundsToState) {
+    this.receiverBoundsToState = receiverBoundsToState;
+    assert !isEffectivelyBottom();
+    assert !isEffectivelyUnknown();
   }
 
-  public void forEach(BiConsumer<DynamicType, MethodState> consumer) {
+  private ConcretePolymorphicMethodState(
+      DynamicType receiverBounds, ConcreteMonomorphicMethodStateOrUnknown methodState) {
+    this.receiverBoundsToState = new HashMap<>(1);
+    receiverBoundsToState.put(receiverBounds, methodState);
+    assert !isEffectivelyUnknown();
+  }
+
+  public static ConcretePolymorphicMethodStateOrUnknown create(
+      DynamicType receiverBounds, ConcreteMonomorphicMethodStateOrUnknown methodState) {
+    return receiverBounds.isUnknown() && methodState.isUnknown()
+        ? MethodState.unknown()
+        : new ConcretePolymorphicMethodState(receiverBounds, methodState);
+  }
+
+  private ConcretePolymorphicMethodStateOrUnknown add(
+      AppView<AppInfoWithLiveness> appView,
+      DexMethodSignature methodSignature,
+      DynamicType bounds,
+      ConcreteMonomorphicMethodStateOrUnknown methodState) {
+    assert !isEffectivelyBottom();
+    assert !isEffectivelyUnknown();
+    if (methodState.isUnknown()) {
+      if (bounds.isUnknown()) {
+        return unknown();
+      } else {
+        receiverBoundsToState.put(bounds, methodState);
+        return this;
+      }
+    } else {
+      assert methodState.isMonomorphic();
+      ConcreteMonomorphicMethodStateOrUnknown newMethodStateForBounds =
+          joinInner(appView, methodSignature, receiverBoundsToState.get(bounds), methodState);
+      if (bounds.isUnknown() && newMethodStateForBounds.isUnknown()) {
+        return unknown();
+      } else {
+        receiverBoundsToState.put(bounds, newMethodStateForBounds);
+        return this;
+      }
+    }
+  }
+
+  private static ConcreteMonomorphicMethodStateOrUnknown joinInner(
+      AppView<AppInfoWithLiveness> appView,
+      DexMethodSignature methodSignature,
+      ConcreteMonomorphicMethodStateOrUnknown methodState,
+      ConcreteMonomorphicMethodStateOrUnknown other) {
+    if (methodState == null) {
+      return other.mutableCopy();
+    }
+    if (methodState.isUnknown() || other.isUnknown()) {
+      return unknown();
+    }
+    assert methodState.isMonomorphic();
+    return methodState.asMonomorphic().mutableJoin(appView, methodSignature, other.asMonomorphic());
+  }
+
+  public void forEach(
+      BiConsumer<? super DynamicType, ? super ConcreteMonomorphicMethodStateOrUnknown> consumer) {
     receiverBoundsToState.forEach(consumer);
   }
 
-  public boolean isEmpty() {
+  public MethodState getMethodStateForBounds(DynamicType dynamicType) {
+    ConcreteMonomorphicMethodStateOrUnknown methodStateForBounds =
+        receiverBoundsToState.get(dynamicType);
+    if (methodStateForBounds != null) {
+      return methodStateForBounds;
+    }
+    return MethodState.bottom();
+  }
+
+  public boolean isEffectivelyBottom() {
     return receiverBoundsToState.isEmpty();
   }
 
+  public boolean isEffectivelyUnknown() {
+    return getMethodStateForBounds(DynamicType.unknown()).isUnknown();
+  }
+
+  @Override
+  public MethodState mutableCopy() {
+    assert !isEffectivelyBottom();
+    assert !isEffectivelyUnknown();
+    Map<DynamicType, ConcreteMonomorphicMethodStateOrUnknown> receiverBoundsToState =
+        new HashMap<>();
+    forEach((bounds, methodState) -> receiverBoundsToState.put(bounds, methodState.mutableCopy()));
+    return new ConcretePolymorphicMethodState(receiverBoundsToState);
+  }
+
+  public MethodState mutableCopyWithRewrittenBounds(
+      AppView<AppInfoWithLiveness> appView,
+      Function<DynamicType, DynamicType> boundsRewriter,
+      DexMethodSignature methodSignature) {
+    assert !isEffectivelyBottom();
+    assert !isEffectivelyUnknown();
+    Map<DynamicType, ConcreteMonomorphicMethodStateOrUnknown> rewrittenReceiverBoundsToState =
+        new HashMap<>();
+    for (Entry<DynamicType, ConcreteMonomorphicMethodStateOrUnknown> entry :
+        receiverBoundsToState.entrySet()) {
+      DynamicType rewrittenBounds = boundsRewriter.apply(entry.getKey());
+      if (rewrittenBounds == null) {
+        continue;
+      }
+      ConcreteMonomorphicMethodStateOrUnknown existingMethodStateForBounds =
+          rewrittenReceiverBoundsToState.get(rewrittenBounds);
+      ConcreteMonomorphicMethodStateOrUnknown newMethodStateForBounds =
+          joinInner(appView, methodSignature, existingMethodStateForBounds, entry.getValue());
+      if (rewrittenBounds.isUnknown() && newMethodStateForBounds.isUnknown()) {
+        return unknown();
+      }
+      rewrittenReceiverBoundsToState.put(rewrittenBounds, newMethodStateForBounds);
+    }
+    return rewrittenReceiverBoundsToState.isEmpty()
+        ? bottom()
+        : new ConcretePolymorphicMethodState(rewrittenReceiverBoundsToState);
+  }
+
   public MethodState mutableJoin(
-      AppView<AppInfoWithLiveness> appView, ConcretePolymorphicMethodState methodState) {
-    assert !isEmpty();
-    assert !methodState.isEmpty();
-    methodState.receiverBoundsToState.forEach(
-        (receiverBounds, stateToAdd) -> {
-          if (stateToAdd.isUnknown()) {
-            receiverBoundsToState.put(receiverBounds, stateToAdd);
-          } else {
-            assert stateToAdd.isMonomorphic();
-            receiverBoundsToState.compute(
-                receiverBounds,
-                (ignore, existingState) -> {
-                  if (existingState == null) {
-                    return stateToAdd;
-                  }
-                  if (existingState.isUnknown()) {
-                    return existingState;
-                  }
-                  assert existingState.isMonomorphic();
-                  return existingState
-                      .asMonomorphic()
-                      .mutableJoin(appView, stateToAdd.asMonomorphic());
-                });
-          }
-        });
-    // TODO(b/190154391): Widen to unknown when the unknown dynamic type is mapped to unknown.
+      AppView<AppInfoWithLiveness> appView,
+      DexMethodSignature methodSignature,
+      ConcretePolymorphicMethodState methodState) {
+    assert !isEffectivelyBottom();
+    assert !isEffectivelyUnknown();
+    assert !methodState.isEffectivelyBottom();
+    assert !methodState.isEffectivelyUnknown();
+    for (Entry<DynamicType, ConcreteMonomorphicMethodStateOrUnknown> entry :
+        methodState.receiverBoundsToState.entrySet()) {
+      ConcretePolymorphicMethodStateOrUnknown result =
+          add(appView, methodSignature, entry.getKey(), entry.getValue());
+      if (result.isUnknown()) {
+        return result;
+      }
+      assert result == this;
+    }
+    assert !isEffectivelyUnknown();
     return this;
   }
 
+  public Collection<ConcreteMonomorphicMethodStateOrUnknown> values() {
+    return receiverBoundsToState.values();
+  }
+
   @Override
   public boolean isPolymorphic() {
     return true;
@@ -70,4 +177,9 @@
   public ConcretePolymorphicMethodState asPolymorphic() {
     return this;
   }
+
+  @Override
+  public ConcretePolymorphicMethodStateOrBottom asPolymorphicOrBottom() {
+    return this;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePolymorphicMethodStateOrBottom.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePolymorphicMethodStateOrBottom.java
new file mode 100644
index 0000000..6577c0e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePolymorphicMethodStateOrBottom.java
@@ -0,0 +1,7 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+public interface ConcretePolymorphicMethodStateOrBottom extends MethodState {}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePolymorphicMethodStateOrUnknown.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePolymorphicMethodStateOrUnknown.java
new file mode 100644
index 0000000..ae7bdfd
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePolymorphicMethodStateOrUnknown.java
@@ -0,0 +1,7 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+public interface ConcretePolymorphicMethodStateOrUnknown extends MethodState {}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePrimitiveTypeParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePrimitiveTypeParameterState.java
index 0b85fb6..3f1baff 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePrimitiveTypeParameterState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcretePrimitiveTypeParameterState.java
@@ -5,28 +5,57 @@
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Action;
+import com.android.tools.r8.utils.SetUtils;
+import java.util.Collections;
+import java.util.Set;
 
 public class ConcretePrimitiveTypeParameterState extends ConcreteParameterState {
 
   private AbstractValue abstractValue;
 
   public ConcretePrimitiveTypeParameterState(AbstractValue abstractValue) {
-    assert !abstractValue.isUnknown() : "Must use UnknownParameterState";
+    this(abstractValue, Collections.emptySet());
+  }
+
+  public ConcretePrimitiveTypeParameterState(
+      AbstractValue abstractValue, Set<MethodParameter> inParameters) {
+    super(inParameters);
     this.abstractValue = abstractValue;
+    assert !isEffectivelyBottom() : "Must use BottomPrimitiveTypeParameterState instead";
+    assert !isEffectivelyUnknown() : "Must use UnknownParameterState instead";
   }
 
   public ConcretePrimitiveTypeParameterState(MethodParameter inParameter) {
-    super(inParameter);
-    this.abstractValue = AbstractValue.bottom();
+    this(AbstractValue.bottom(), SetUtils.newHashSet(inParameter));
+  }
+
+  @Override
+  public ParameterState clearInParameters() {
+    if (hasInParameters()) {
+      if (abstractValue.isBottom()) {
+        return bottomPrimitiveTypeParameter();
+      }
+      internalClearInParameters();
+    }
+    assert !isEffectivelyBottom();
+    return this;
+  }
+
+  @Override
+  public ParameterState mutableCopy() {
+    return new ConcretePrimitiveTypeParameterState(abstractValue, copyInParameters());
   }
 
   public ParameterState mutableJoin(
       AppView<AppInfoWithLiveness> appView,
       ConcretePrimitiveTypeParameterState parameterState,
+      DexType parameterType,
       Action onChangedAction) {
+    assert parameterType.isPrimitiveType();
     boolean allowNullOrAbstractValue = false;
     boolean allowNonConstantNumbers = false;
     AbstractValue oldAbstractValue = abstractValue;
@@ -50,7 +79,7 @@
   }
 
   @Override
-  public AbstractValue getAbstractValue() {
+  public AbstractValue getAbstractValue(AppView<AppInfoWithLiveness> appView) {
     return abstractValue;
   }
 
@@ -59,6 +88,14 @@
     return ConcreteParameterStateKind.PRIMITIVE;
   }
 
+  public boolean isEffectivelyBottom() {
+    return abstractValue.isBottom() && !hasInParameters();
+  }
+
+  public boolean isEffectivelyUnknown() {
+    return abstractValue.isUnknown();
+  }
+
   @Override
   public boolean isPrimitiveParameter() {
     return true;
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReceiverParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReceiverParameterState.java
index 145d29b..d7f7bbc 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReceiverParameterState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReceiverParameterState.java
@@ -4,53 +4,72 @@
 
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.type.DynamicType;
+import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Action;
+import java.util.Collections;
+import java.util.Set;
 
-public class ConcreteReceiverParameterState extends ConcreteParameterState {
+public class ConcreteReceiverParameterState extends ConcreteReferenceTypeParameterState {
 
   private DynamicType dynamicType;
 
   public ConcreteReceiverParameterState(DynamicType dynamicType) {
-    this.dynamicType = dynamicType;
+    this(dynamicType, Collections.emptySet());
   }
 
-  public ParameterState mutableJoin(
-      ConcreteReceiverParameterState parameterState, Action onChangedAction) {
-    // TODO(b/190154391): Join the dynamic types using SubtypingInfo.
-    // TODO(b/190154391): Take in the static type as an argument, and unset the dynamic type if it
-    //  equals the static type.
-    DynamicType oldDynamicType = dynamicType;
-    dynamicType =
-        dynamicType.equals(parameterState.dynamicType) ? dynamicType : DynamicType.unknown();
-    if (dynamicType.isUnknown()) {
-      return unknown();
+  public ConcreteReceiverParameterState(
+      DynamicType dynamicType, Set<MethodParameter> inParameters) {
+    super(inParameters);
+    this.dynamicType = dynamicType;
+    assert !isEffectivelyBottom() : "Must use BottomReceiverParameterState instead";
+    assert !isEffectivelyUnknown() : "Must use UnknownParameterState instead";
+  }
+
+  @Override
+  public ParameterState clearInParameters() {
+    if (hasInParameters()) {
+      if (dynamicType.isBottom()) {
+        return bottomReceiverParameter();
+      }
+      internalClearInParameters();
     }
-    boolean inParametersChanged = mutableJoinInParameters(parameterState);
-    if (widenInParameters()) {
-      return unknown();
-    }
-    if (dynamicType != oldDynamicType || inParametersChanged) {
-      onChangedAction.execute();
-    }
+    assert !isEffectivelyBottom();
     return this;
   }
 
   @Override
-  public AbstractValue getAbstractValue() {
+  public AbstractValue getAbstractValue(AppView<AppInfoWithLiveness> appView) {
     return AbstractValue.unknown();
   }
 
+  @Override
   public DynamicType getDynamicType() {
     return dynamicType;
   }
 
   @Override
+  public Nullability getNullability() {
+    return getDynamicType().getDynamicUpperBoundType().nullability();
+  }
+
+  @Override
   public ConcreteParameterStateKind getKind() {
     return ConcreteParameterStateKind.RECEIVER;
   }
 
+  public boolean isEffectivelyBottom() {
+    return dynamicType.isBottom() && !hasInParameters();
+  }
+
+  public boolean isEffectivelyUnknown() {
+    return dynamicType.isUnknown();
+  }
+
   @Override
   public boolean isReceiverParameter() {
     return true;
@@ -60,4 +79,33 @@
   public ConcreteReceiverParameterState asReceiverParameter() {
     return this;
   }
+
+  @Override
+  public ParameterState mutableCopy() {
+    return new ConcreteReceiverParameterState(dynamicType, copyInParameters());
+  }
+
+  @Override
+  public ParameterState mutableJoin(
+      AppView<AppInfoWithLiveness> appView,
+      ConcreteReferenceTypeParameterState parameterState,
+      DexType parameterType,
+      Action onChangedAction) {
+    // TODO(b/190154391): Always take in the static type as an argument, and unset the dynamic type
+    //  if it equals the static type.
+    assert parameterType == null || parameterType.isClassType();
+    DynamicType oldDynamicType = dynamicType;
+    dynamicType = dynamicType.join(appView, parameterState.getDynamicType());
+    if (dynamicType.isUnknown()) {
+      return unknown();
+    }
+    boolean inParametersChanged = mutableJoinInParameters(parameterState);
+    if (widenInParameters()) {
+      return unknown();
+    }
+    if (!dynamicType.equals(oldDynamicType) || inParametersChanged) {
+      onChangedAction.execute();
+    }
+    return this;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReferenceTypeParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReferenceTypeParameterState.java
new file mode 100644
index 0000000..30506a0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ConcreteReferenceTypeParameterState.java
@@ -0,0 +1,40 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
+import com.android.tools.r8.ir.analysis.type.Nullability;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.Action;
+import java.util.Set;
+
+public abstract class ConcreteReferenceTypeParameterState extends ConcreteParameterState {
+
+  ConcreteReferenceTypeParameterState(Set<MethodParameter> inParameters) {
+    super(inParameters);
+  }
+
+  public abstract DynamicType getDynamicType();
+
+  public abstract Nullability getNullability();
+
+  @Override
+  public boolean isReferenceParameter() {
+    return true;
+  }
+
+  @Override
+  public ConcreteReferenceTypeParameterState asReferenceParameter() {
+    return this;
+  }
+
+  public abstract ParameterState mutableJoin(
+      AppView<AppInfoWithLiveness> appView,
+      ConcreteReferenceTypeParameterState parameterState,
+      DexType parameterType,
+      Action onChangedAction);
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodState.java
index 5bc6049..0de2d3e 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodState.java
@@ -5,8 +5,9 @@
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethodSignature;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import java.util.function.Supplier;
+import java.util.function.Function;
 
 public interface MethodState {
 
@@ -28,10 +29,25 @@
 
   ConcreteMonomorphicMethodState asMonomorphic();
 
+  ConcreteMonomorphicMethodStateOrBottom asMonomorphicOrBottom();
+
+  boolean isPolymorphic();
+
+  ConcretePolymorphicMethodState asPolymorphic();
+
+  ConcretePolymorphicMethodStateOrBottom asPolymorphicOrBottom();
+
   boolean isUnknown();
 
-  MethodState mutableJoin(AppView<AppInfoWithLiveness> appView, MethodState methodState);
+  MethodState mutableCopy();
 
   MethodState mutableJoin(
-      AppView<AppInfoWithLiveness> appView, Supplier<MethodState> methodStateSupplier);
+      AppView<AppInfoWithLiveness> appView,
+      DexMethodSignature methodSignature,
+      MethodState methodState);
+
+  MethodState mutableJoin(
+      AppView<AppInfoWithLiveness> appView,
+      DexMethodSignature methodSignature,
+      Function<MethodState, MethodState> methodStateSupplier);
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateBase.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateBase.java
index 936210f..1b1800d 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateBase.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateBase.java
@@ -40,6 +40,26 @@
   }
 
   @Override
+  public ConcreteMonomorphicMethodStateOrBottom asMonomorphicOrBottom() {
+    return null;
+  }
+
+  @Override
+  public boolean isPolymorphic() {
+    return false;
+  }
+
+  @Override
+  public ConcretePolymorphicMethodState asPolymorphic() {
+    return null;
+  }
+
+  @Override
+  public ConcretePolymorphicMethodStateOrBottom asPolymorphicOrBottom() {
+    return null;
+  }
+
+  @Override
   public boolean isUnknown() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollection.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollection.java
index c8b2456..50f02c9 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollection.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollection.java
@@ -5,10 +5,13 @@
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethodSignature;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.Timing;
 import java.util.Map;
 import java.util.function.BiConsumer;
+import java.util.function.Function;
 import java.util.function.Supplier;
 
 abstract class MethodStateCollection<K> {
@@ -16,11 +19,14 @@
   private final Map<K, MethodState> methodStates;
 
   MethodStateCollection(Map<K, MethodState> methodStates) {
+    assert methodStates.values().stream().noneMatch(MethodState::isBottom);
     this.methodStates = methodStates;
   }
 
   abstract K getKey(ProgramMethod method);
 
+  abstract DexMethodSignature getSignature(K method);
+
   public void addMethodState(
       AppView<AppInfoWithLiveness> appView, ProgramMethod method, MethodState methodState) {
     addMethodState(appView, getKey(method), methodState);
@@ -34,10 +40,15 @@
       methodStates.compute(
           method,
           (ignore, existingMethodState) -> {
+            MethodState newMethodState;
             if (existingMethodState == null) {
-              return methodState;
+              newMethodState = methodState.mutableCopy();
+            } else {
+              newMethodState =
+                  existingMethodState.mutableJoin(appView, getSignature(method), methodState);
             }
-            return existingMethodState.mutableJoin(appView, methodState);
+            assert !newMethodState.isBottom();
+            return newMethodState;
           });
     }
   }
@@ -46,22 +57,26 @@
    * This intentionally takes a {@link Supplier<MethodState>} to avoid computing the method state
    * for a given call site when nothing is known about the arguments of the method.
    */
-  public void addMethodState(
+  public void addTemporaryMethodState(
       AppView<AppInfoWithLiveness> appView,
-      ProgramMethod method,
-      Supplier<MethodState> methodStateSupplier) {
-    addMethodState(appView, getKey(method), methodStateSupplier);
-  }
-
-  public void addMethodState(
-      AppView<AppInfoWithLiveness> appView, K method, Supplier<MethodState> methodStateSupplier) {
+      K method,
+      Function<MethodState, MethodState> methodStateSupplier,
+      Timing timing) {
     methodStates.compute(
         method,
         (ignore, existingMethodState) -> {
           if (existingMethodState == null) {
-            return methodStateSupplier.get();
+            MethodState newMethodState = methodStateSupplier.apply(MethodState.bottom());
+            assert !newMethodState.isBottom();
+            return newMethodState;
           }
-          return existingMethodState.mutableJoin(appView, methodStateSupplier);
+          assert !existingMethodState.isBottom();
+          timing.begin("Join temporary method state");
+          MethodState joinResult =
+              existingMethodState.mutableJoin(appView, getSignature(method), methodStateSupplier);
+          assert !joinResult.isBottom();
+          timing.end();
+          return joinResult;
         });
   }
 
@@ -76,7 +91,11 @@
   }
 
   public MethodState get(ProgramMethod method) {
-    return methodStates.getOrDefault(getKey(method), MethodState.bottom());
+    return get(getKey(method));
+  }
+
+  public MethodState get(K method) {
+    return methodStates.getOrDefault(method, MethodState.bottom());
   }
 
   public boolean isEmpty() {
@@ -84,11 +103,23 @@
   }
 
   public MethodState remove(ProgramMethod method) {
+    return removeOrElse(method, MethodState.bottom());
+  }
+
+  public MethodState removeOrElse(ProgramMethod method, MethodState defaultValue) {
     MethodState removed = methodStates.remove(getKey(method));
-    return removed != null ? removed : MethodState.bottom();
+    return removed != null ? removed : defaultValue;
   }
 
   public void set(ProgramMethod method, MethodState methodState) {
-    methodStates.put(getKey(method), methodState);
+    set(getKey(method), methodState);
+  }
+
+  private void set(K method, MethodState methodState) {
+    if (methodState.isBottom()) {
+      methodStates.remove(method);
+    } else {
+      methodStates.put(method, methodState);
+    }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionByReference.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionByReference.java
index ea48bfb..a715205 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionByReference.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionByReference.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexMethodSignature;
 import com.android.tools.r8.graph.ProgramMethod;
 import java.util.IdentityHashMap;
 import java.util.Map;
@@ -28,4 +29,9 @@
   DexMethod getKey(ProgramMethod method) {
     return method.getReference();
   }
+
+  @Override
+  DexMethodSignature getSignature(DexMethod method) {
+    return method.getSignature();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionBySignature.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionBySignature.java
index 2113be0..a244616 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionBySignature.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/MethodStateCollectionBySignature.java
@@ -28,4 +28,9 @@
   DexMethodSignature getKey(ProgramMethod method) {
     return method.getMethodSignature();
   }
+
+  @Override
+  DexMethodSignature getSignature(DexMethodSignature method) {
+    return method;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/NonEmptyParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/NonEmptyParameterState.java
new file mode 100644
index 0000000..68e0842
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/NonEmptyParameterState.java
@@ -0,0 +1,13 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+public abstract class NonEmptyParameterState extends ParameterState {
+
+  @Override
+  public NonEmptyParameterState asNonEmpty() {
+    return this;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ParameterState.java
index 2c9a205..ad7b97c 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ParameterState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/ParameterState.java
@@ -5,17 +5,38 @@
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Action;
 
 public abstract class ParameterState {
 
+  public static BottomParameterState bottomArrayTypeParameter() {
+    return BottomArrayTypeParameterState.get();
+  }
+
+  public static BottomParameterState bottomClassTypeParameter() {
+    return BottomClassTypeParameterState.get();
+  }
+
+  public static BottomParameterState bottomPrimitiveTypeParameter() {
+    return BottomPrimitiveTypeParameterState.get();
+  }
+
+  public static BottomParameterState bottomReceiverParameter() {
+    return BottomReceiverParameterState.get();
+  }
+
   public static UnknownParameterState unknown() {
     return UnknownParameterState.get();
   }
 
-  public abstract AbstractValue getAbstractValue();
+  public abstract AbstractValue getAbstractValue(AppView<AppInfoWithLiveness> appView);
+
+  public boolean isBottom() {
+    return false;
+  }
 
   public boolean isConcrete() {
     return false;
@@ -25,15 +46,24 @@
     return null;
   }
 
+  public NonEmptyParameterState asNonEmpty() {
+    return null;
+  }
+
   public boolean isUnknown() {
     return false;
   }
 
+  public abstract ParameterState mutableCopy();
+
   public final ParameterState mutableJoin(
-      AppView<AppInfoWithLiveness> appView, ParameterState parameterState) {
-    return mutableJoin(appView, parameterState, Action.empty());
+      AppView<AppInfoWithLiveness> appView, ParameterState parameterState, DexType parameterType) {
+    return mutableJoin(appView, parameterState, parameterType, Action.empty());
   }
 
   public abstract ParameterState mutableJoin(
-      AppView<AppInfoWithLiveness> appView, ParameterState parameterState, Action onChangedAction);
+      AppView<AppInfoWithLiveness> appView,
+      ParameterState parameterState,
+      DexType parameterType,
+      Action onChangedAction);
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownMethodState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownMethodState.java
index 1cfe728..b4bb41f 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownMethodState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownMethodState.java
@@ -5,12 +5,13 @@
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethodSignature;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import java.util.function.Supplier;
+import java.util.function.Function;
 
 // Use this when the nothing is known.
 public class UnknownMethodState extends MethodStateBase
-    implements ConcreteMonomorphicMethodStateOrUnknown {
+    implements ConcreteMonomorphicMethodStateOrUnknown, ConcretePolymorphicMethodStateOrUnknown {
 
   private static final UnknownMethodState INSTANCE = new UnknownMethodState();
 
@@ -26,13 +27,23 @@
   }
 
   @Override
-  public MethodState mutableJoin(AppView<AppInfoWithLiveness> appView, MethodState methodState) {
+  public UnknownMethodState mutableCopy() {
     return this;
   }
 
   @Override
   public MethodState mutableJoin(
-      AppView<AppInfoWithLiveness> appView, Supplier<MethodState> methodStateSupplier) {
+      AppView<AppInfoWithLiveness> appView,
+      DexMethodSignature methodSignature,
+      MethodState methodState) {
+    return this;
+  }
+
+  @Override
+  public MethodState mutableJoin(
+      AppView<AppInfoWithLiveness> appView,
+      DexMethodSignature methodSignature,
+      Function<MethodState, MethodState> methodStateSupplier) {
     return this;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownParameterState.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownParameterState.java
index bfc835d..bcb5317 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownParameterState.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/UnknownParameterState.java
@@ -5,11 +5,12 @@
 package com.android.tools.r8.optimize.argumentpropagation.codescanner;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Action;
 
-public class UnknownParameterState extends ParameterState {
+public class UnknownParameterState extends NonEmptyParameterState {
 
   private static final UnknownParameterState INSTANCE = new UnknownParameterState();
 
@@ -20,7 +21,7 @@
   }
 
   @Override
-  public AbstractValue getAbstractValue() {
+  public AbstractValue getAbstractValue(AppView<AppInfoWithLiveness> appView) {
     return AbstractValue.unknown();
   }
 
@@ -30,8 +31,16 @@
   }
 
   @Override
+  public ParameterState mutableCopy() {
+    return this;
+  }
+
+  @Override
   public ParameterState mutableJoin(
-      AppView<AppInfoWithLiveness> appView, ParameterState parameterState, Action onChangedAction) {
+      AppView<AppInfoWithLiveness> appView,
+      ParameterState parameterState,
+      DexType parameterType,
+      Action onChangedAction) {
     return this;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java
new file mode 100644
index 0000000..5d5399d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/codescanner/VirtualRootMethodsAnalysis.java
@@ -0,0 +1,134 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation.codescanner;
+
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexMethodSignature;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorCodeScanner;
+import com.android.tools.r8.optimize.argumentpropagation.utils.DepthFirstTopDownClassHierarchyTraversal;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import com.google.common.collect.Sets;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+
+public class VirtualRootMethodsAnalysis extends DepthFirstTopDownClassHierarchyTraversal {
+
+  static class VirtualRootMethod {
+
+    private final ProgramMethod root;
+    private final ProgramMethodSet overrides = ProgramMethodSet.create();
+
+    VirtualRootMethod(ProgramMethod root) {
+      this.root = root;
+    }
+
+    void addOverride(ProgramMethod override) {
+      assert override != root;
+      assert override.getMethodSignature().equals(root.getMethodSignature());
+      overrides.add(override);
+    }
+
+    ProgramMethod getRoot() {
+      return root;
+    }
+
+    void forEach(Consumer<ProgramMethod> consumer) {
+      consumer.accept(root);
+      overrides.forEach(consumer);
+    }
+
+    boolean hasOverrides() {
+      return !overrides.isEmpty();
+    }
+  }
+
+  private final Map<DexProgramClass, Map<DexMethodSignature, VirtualRootMethod>>
+      virtualRootMethodsPerClass = new IdentityHashMap<>();
+
+  private final Set<DexMethod> monomorphicVirtualMethods = Sets.newIdentityHashSet();
+
+  private final Map<DexMethod, DexMethod> virtualRootMethods = new IdentityHashMap<>();
+
+  public VirtualRootMethodsAnalysis(
+      AppView<AppInfoWithLiveness> appView, ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
+    super(appView, immediateSubtypingInfo);
+  }
+
+  public void extendVirtualRootMethods(
+      Collection<DexProgramClass> stronglyConnectedComponent,
+      ArgumentPropagatorCodeScanner codeScanner) {
+    // Find all the virtual root methods in the strongly connected component.
+    run(stronglyConnectedComponent);
+
+    // Commit the result to the code scanner.
+    codeScanner.addMonomorphicVirtualMethods(monomorphicVirtualMethods);
+    codeScanner.addVirtualRootMethods(virtualRootMethods);
+  }
+
+  @Override
+  public void visit(DexProgramClass clazz) {
+    Map<DexMethodSignature, VirtualRootMethod> state = computeVirtualRootMethodsState(clazz);
+    virtualRootMethodsPerClass.put(clazz, state);
+  }
+
+  private Map<DexMethodSignature, VirtualRootMethod> computeVirtualRootMethodsState(
+      DexProgramClass clazz) {
+    Map<DexMethodSignature, VirtualRootMethod> virtualRootMethodsForClass = new HashMap<>();
+    immediateSubtypingInfo.forEachImmediateProgramSuperClass(
+        clazz,
+        superclass -> {
+          Map<DexMethodSignature, VirtualRootMethod> virtualRootMethodsForSuperclass =
+              virtualRootMethodsPerClass.get(superclass);
+          virtualRootMethodsForSuperclass.forEach(
+              (signature, info) ->
+                  virtualRootMethodsForClass.computeIfAbsent(signature, ignoreKey(() -> info)));
+        });
+    clazz.forEachProgramVirtualMethod(
+        method -> {
+          DexMethodSignature signature = method.getMethodSignature();
+          if (virtualRootMethodsForClass.containsKey(signature)) {
+            virtualRootMethodsForClass.get(signature).addOverride(method);
+          } else {
+            virtualRootMethodsForClass.put(signature, new VirtualRootMethod(method));
+          }
+        });
+    return virtualRootMethodsForClass;
+  }
+
+  @Override
+  public void prune(DexProgramClass clazz) {
+    // Record the overrides for each virtual method that is rooted at this class.
+    Map<DexMethodSignature, VirtualRootMethod> virtualRootMethodsForClass =
+        virtualRootMethodsPerClass.remove(clazz);
+    clazz.forEachProgramVirtualMethod(
+        rootCandidate -> {
+          VirtualRootMethod virtualRootMethod =
+              virtualRootMethodsForClass.remove(rootCandidate.getMethodSignature());
+          if (!rootCandidate.isStructurallyEqualTo(virtualRootMethod.getRoot())) {
+            return;
+          }
+          boolean isMonomorphicVirtualMethod =
+              !clazz.isInterface() && !virtualRootMethod.hasOverrides();
+          if (isMonomorphicVirtualMethod) {
+            monomorphicVirtualMethods.add(rootCandidate.getReference());
+          } else {
+            virtualRootMethod.forEach(
+                method ->
+                    virtualRootMethods.put(method.getReference(), rootCandidate.getReference()));
+          }
+        });
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InParameterFlowPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InParameterFlowPropagator.java
index cba390e..b551b68 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InParameterFlowPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InParameterFlowPropagator.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.graph.AppView;
 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.ProgramMethod;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcreteMonomorphicMethodState;
@@ -17,6 +18,7 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodParameter;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.NonEmptyParameterState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.ParameterState;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.Action;
@@ -91,9 +93,12 @@
   private void propagate(
       ParameterNode parameterNode, Consumer<ParameterNode> affectedNodeConsumer) {
     ParameterState parameterState = parameterNode.getState();
+    if (parameterState.isBottom()) {
+      return;
+    }
     for (ParameterNode successorNode : parameterNode.getSuccessors()) {
       successorNode.addState(
-          appView, parameterState, () -> affectedNodeConsumer.accept(successorNode));
+          appView, parameterState.asNonEmpty(), () -> affectedNodeConsumer.accept(successorNode));
     }
   }
 
@@ -112,12 +117,20 @@
       return;
     }
     assert methodState.isMonomorphic();
+    boolean allUnknown = true;
     for (ParameterState parameterState : methodState.asMonomorphic().getParameterStates()) {
-      if (!parameterState.isUnknown()) {
+      if (parameterState.isBottom()) {
+        methodStates.set(method, MethodState.bottom());
         return;
       }
+      if (!parameterState.isUnknown()) {
+        assert parameterState.isConcrete();
+        allUnknown = false;
+      }
     }
-    methodStates.set(method, MethodState.unknown());
+    if (allUnknown) {
+      methodStates.set(method, MethodState.unknown());
+    }
   }
 
   private class FlowGraph {
@@ -145,8 +158,7 @@
       }
 
       // Add nodes for the parameters for which we have non-trivial information.
-      ConcreteMonomorphicMethodState monomorphicMethodState =
-          methodState.asConcrete().asMonomorphic();
+      ConcreteMonomorphicMethodState monomorphicMethodState = methodState.asMonomorphic();
       List<ParameterState> parameterStates = monomorphicMethodState.getParameterStates();
       for (int parameterIndex = 0; parameterIndex < parameterStates.size(); parameterIndex++) {
         ParameterState parameterState = parameterStates.get(parameterIndex);
@@ -159,8 +171,9 @@
         int parameterIndex,
         ConcreteMonomorphicMethodState methodState,
         ParameterState parameterState) {
-      // No need to create nodes for parameters we don't know anything about.
-      if (parameterState.isUnknown()) {
+      // No need to create nodes for parameters with no in-parameters and parameters we don't know
+      // anything about.
+      if (parameterState.isBottom() || parameterState.isUnknown()) {
         return;
       }
 
@@ -172,10 +185,10 @@
         return;
       }
 
-      ParameterNode node =
-          getOrCreateParameterNode(method.getReference(), parameterIndex, methodState);
+      ParameterNode node = getOrCreateParameterNode(method, parameterIndex, methodState);
       for (MethodParameter inParameter : concreteParameterState.getInParameters()) {
-        MethodState enclosingMethodState = getEnclosingMethodStateForParameter(inParameter);
+        ProgramMethod enclosingMethod = getEnclosingMethod(inParameter);
+        MethodState enclosingMethodState = getMethodState(enclosingMethod);
         if (enclosingMethodState.isBottom()) {
           // The current method is called from a dead method; no need to propagate any information
           // from the dead call site.
@@ -194,32 +207,38 @@
 
         ParameterNode predecessor =
             getOrCreateParameterNode(
-                inParameter.getMethod(),
+                enclosingMethod,
                 inParameter.getIndex(),
                 enclosingMethodState.asConcrete().asMonomorphic());
         node.addPredecessor(predecessor);
       }
-      concreteParameterState.clearInParameters();
+
+      if (!node.getState().isUnknown()) {
+        assert node.getState() == concreteParameterState;
+        node.setState(concreteParameterState.clearInParameters());
+      }
     }
 
     private ParameterNode getOrCreateParameterNode(
-        DexMethod key, int parameterIndex, ConcreteMonomorphicMethodState methodState) {
+        ProgramMethod method, int parameterIndex, ConcreteMonomorphicMethodState methodState) {
       Int2ReferenceMap<ParameterNode> parameterNodesForMethod =
-          nodes.computeIfAbsent(key, ignoreKey(Int2ReferenceOpenHashMap::new));
+          nodes.computeIfAbsent(method.getReference(), ignoreKey(Int2ReferenceOpenHashMap::new));
       return parameterNodesForMethod.compute(
           parameterIndex,
           (ignore, parameterNode) ->
               parameterNode != null
                   ? parameterNode
-                  : new ParameterNode(methodState, parameterIndex));
+                  : new ParameterNode(
+                      methodState, parameterIndex, method.getArgumentType(parameterIndex)));
     }
 
-    private MethodState getEnclosingMethodStateForParameter(MethodParameter methodParameter) {
+    private ProgramMethod getEnclosingMethod(MethodParameter methodParameter) {
       DexMethod methodReference = methodParameter.getMethod();
-      ProgramMethod method =
-          methodReference.lookupOnProgramClass(
-              asProgramClassOrNull(
-                  appView.definitionFor(methodParameter.getMethod().getHolderType())));
+      return methodReference.lookupOnProgramClass(
+          asProgramClassOrNull(appView.definitionFor(methodParameter.getMethod().getHolderType())));
+    }
+
+    private MethodState getMethodState(ProgramMethod method) {
       if (method == null) {
         // Conservatively return unknown if for some reason we can't find the method.
         assert false;
@@ -233,15 +252,18 @@
 
     private final ConcreteMonomorphicMethodState methodState;
     private final int parameterIndex;
+    private final DexType parameterType;
 
     private final Set<ParameterNode> predecessors = Sets.newIdentityHashSet();
     private final Set<ParameterNode> successors = Sets.newIdentityHashSet();
 
     private boolean pending = true;
 
-    ParameterNode(ConcreteMonomorphicMethodState methodState, int parameterIndex) {
+    ParameterNode(
+        ConcreteMonomorphicMethodState methodState, int parameterIndex, DexType parameterType) {
       this.methodState = methodState;
       this.parameterIndex = parameterIndex;
+      this.parameterType = parameterType;
     }
 
     void addPredecessor(ParameterNode predecessor) {
@@ -274,11 +296,12 @@
 
     void addState(
         AppView<AppInfoWithLiveness> appView,
-        ParameterState parameterStateToAdd,
+        NonEmptyParameterState parameterStateToAdd,
         Action onChangedAction) {
       ParameterState oldParameterState = getState();
       ParameterState newParameterState =
-          oldParameterState.mutableJoin(appView, parameterStateToAdd, onChangedAction);
+          oldParameterState.mutableJoin(
+              appView, parameterStateToAdd, parameterType, onChangedAction);
       if (newParameterState != oldParameterState) {
         setState(newParameterState);
         onChangedAction.execute();
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InterfaceMethodArgumentPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InterfaceMethodArgumentPropagator.java
index 69cbb7b..72990cb 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InterfaceMethodArgumentPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/InterfaceMethodArgumentPropagator.java
@@ -6,11 +6,15 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
-import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
-import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
+import com.android.tools.r8.graph.MethodResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
+import com.android.tools.r8.ir.analysis.type.Nullability;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.ConcretePolymorphicMethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionBySignature;
@@ -121,21 +125,12 @@
         subclass ->
             interfaceState.forEach(
                 (interfaceMethod, interfaceMethodState) -> {
-                  // TODO(b/190154391): Change resolution to take a signature.
-                  DexMethod interfaceMethodToResolve =
-                      appView
-                          .dexItemFactory()
-                          .createMethod(
-                              subclass.getType(),
-                              interfaceMethod.getProto(),
-                              interfaceMethod.getName());
-                  SingleResolutionResult resolutionResult =
-                      appView
-                          .appInfo()
-                          .resolveMethodOnClass(interfaceMethodToResolve, subclass)
-                          .asSingleResolution();
-                  if (resolutionResult == null) {
-                    assert false;
+                  MethodResolutionResult resolutionResult =
+                      appView.appInfo().resolveMethodOnClass(interfaceMethod, subclass);
+                  if (resolutionResult.isFailedResolution()) {
+                    // TODO(b/190154391): Do we need to propagate argument information to the first
+                    //  virtual method above the inaccessible method in the class hierarchy?
+                    assert resolutionResult.isIllegalAccessErrorResult(subclass, appView.appInfo());
                     return;
                   }
 
@@ -146,10 +141,66 @@
                     return;
                   }
 
-                  methodStates.addMethodState(appView, resolvedMethod, interfaceMethodState);
+                  MethodState transformedInterfaceMethodState =
+                      transformInterfaceMethodStateForClassMethod(
+                          subclass, resolvedMethod, interfaceMethodState);
+                  if (!transformedInterfaceMethodState.isBottom()) {
+                    methodStates.addMethodState(
+                        appView, resolvedMethod, transformedInterfaceMethodState);
+                  }
                 }));
   }
 
+  private MethodState transformInterfaceMethodStateForClassMethod(
+      DexProgramClass clazz, ProgramMethod resolvedMethod, MethodState methodState) {
+    if (!methodState.isPolymorphic()) {
+      return methodState.mutableCopy();
+    }
+
+    // Rewrite the bounds of the polymorphic method state. If a given piece of argument information
+    // should be propagated to the resolved method, we replace the type bounds by the holder of the
+    // resolved method.
+    ConcretePolymorphicMethodState polymorphicMethodState = methodState.asPolymorphic();
+    MethodState rewrittenMethodState =
+        polymorphicMethodState.mutableCopyWithRewrittenBounds(
+            appView,
+            bounds -> {
+              boolean shouldPropagateMethodStateForBounds;
+              if (bounds.isUnknown()) {
+                shouldPropagateMethodStateForBounds = true;
+              } else {
+                ClassTypeElement upperBound = bounds.getDynamicUpperBoundType().asClassType();
+                shouldPropagateMethodStateForBounds =
+                    upperBound
+                        .getInterfaces()
+                        .anyMatch(
+                            (interfaceType, isKnown) ->
+                                appView.appInfo().isSubtype(clazz.getType(), interfaceType));
+              }
+              if (shouldPropagateMethodStateForBounds) {
+                return DynamicType.createExact(
+                    TypeElement.fromDexType(
+                            resolvedMethod.getHolderType(), Nullability.maybeNull(), appView)
+                        .asClassType());
+              }
+              return null;
+            },
+            resolvedMethod.getMethodSignature());
+
+    // If the resolved method is a virtual method that does not override any methods and are not
+    // overridden by any methods, then we use a monomorphic method state for it. Therefore, we
+    // transform this polymorphic method state into a monomorphic method state, before joining it
+    // into the method's state.
+    if (methodStates.get(resolvedMethod).isMonomorphic() && rewrittenMethodState.isPolymorphic()) {
+      ConcretePolymorphicMethodState rewrittenPolymorphicMethodState =
+          rewrittenMethodState.asPolymorphic();
+      assert rewrittenPolymorphicMethodState.values().size() == 1;
+      return rewrittenPolymorphicMethodState.values().iterator().next();
+    }
+
+    return rewrittenMethodState;
+  }
+
   private boolean verifyAllInterfacesFinished(
       Collection<DexProgramClass> stronglyConnectedComponent) {
     assert stronglyConnectedComponent.stream()
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/VirtualDispatchMethodArgumentPropagator.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/VirtualDispatchMethodArgumentPropagator.java
index 771a718..6df20f0 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/VirtualDispatchMethodArgumentPropagator.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/propagation/VirtualDispatchMethodArgumentPropagator.java
@@ -4,13 +4,16 @@
 
 package com.android.tools.r8.optimize.argumentpropagation.propagation;
 
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.android.tools.r8.ir.analysis.type.Nullability.maybeNull;
 import static com.android.tools.r8.utils.MapUtils.ignoreKey;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexMethodSignature;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
+import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.analysis.type.DynamicType;
@@ -20,6 +23,7 @@
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionBySignature;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.UnknownMethodState;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.Collection;
 import java.util.HashMap;
@@ -80,7 +84,7 @@
 
       // Add the argument information that is inactive until a given upper bound.
       parentState.inactiveUntilUpperBound.forEach(
-          (bounds, inactiveMethodState) -> {
+          (bounds, inactiveMethodStates) -> {
             ClassTypeElement upperBound = bounds.getDynamicUpperBoundType().asClassType();
             if (upperBound.equalUpToNullability(classType)) {
               // The upper bound is the current class, thus this inactive information now becomes
@@ -90,10 +94,38 @@
                     .computeIfAbsent(
                         bounds.getDynamicLowerBoundType().getClassType(),
                         ignoreKey(MethodStateCollectionBySignature::create))
-                    .addMethodStates(appView, inactiveMethodState);
+                    .addMethodStates(appView, inactiveMethodStates);
               } else {
-                active.addMethodStates(appView, inactiveMethodState);
+                active.addMethodStates(appView, inactiveMethodStates);
               }
+
+              inactiveMethodStates.forEach(
+                  (signature, methodState) -> {
+                    SingleResolutionResult resolutionResult =
+                        appView.appInfo().resolveMethodOn(clazz, signature).asSingleResolution();
+
+                    // Find the first virtual method in the super class hierarchy.
+                    while (resolutionResult != null
+                        && resolutionResult.getResolvedMethod().belongsToDirectPool()) {
+                      resolutionResult =
+                          appView
+                              .appInfo()
+                              .resolveMethodOnClass(
+                                  signature, resolutionResult.getResolvedHolder().getSuperType())
+                              .asSingleResolution();
+                    }
+
+                    // Propagate the argument information to the method on the super class.
+                    if (resolutionResult != null
+                        && resolutionResult.getResolvedHolder().isProgramClass()
+                        && resolutionResult.getResolvedHolder() != clazz) {
+                      propagationStates
+                          .get(resolutionResult.getResolvedHolder().asProgramClass())
+                          .active
+                          .addMethodState(
+                              appView, resolutionResult.getResolvedProgramMethod(), methodState);
+                    }
+                  });
             } else {
               // Still inactive.
               // TODO(b/190154391): Only carry this information downwards if the upper bound is a
@@ -101,16 +133,19 @@
               //  although clearly the information will never become active.
               inactiveUntilUpperBound
                   .computeIfAbsent(bounds, ignoreKey(MethodStateCollectionBySignature::create))
-                  .addMethodStates(appView, inactiveMethodState);
+                  .addMethodStates(appView, inactiveMethodStates);
             }
           });
     }
 
     private MethodState computeMethodStateForPolymorhicMethod(ProgramMethod method) {
       assert method.getDefinition().isNonPrivateVirtualMethod();
-      MethodState methodState = active.get(method);
-      for (MethodStateCollectionBySignature methodStates : activeUntilLowerBound.values()) {
-        methodState = methodState.mutableJoin(appView, methodStates.get(method));
+      MethodState methodState = active.get(method).mutableCopy();
+      if (!activeUntilLowerBound.isEmpty()) {
+        DexMethodSignature methodSignature = method.getMethodSignature();
+        for (MethodStateCollectionBySignature methodStates : activeUntilLowerBound.values()) {
+          methodState = methodState.mutableJoin(appView, methodSignature, methodStates.get(method));
+        }
       }
       return methodState;
     }
@@ -141,13 +176,10 @@
   @Override
   public void visit(DexProgramClass clazz) {
     assert !propagationStates.containsKey(clazz);
-    PropagationState propagationState = computePropagationState(clazz);
-    computeFinalMethodStates(clazz, propagationState);
+    computePropagationState(clazz);
   }
 
-  private PropagationState computePropagationState(DexProgramClass clazz) {
-    ClassTypeElement classType =
-        TypeElement.fromDexType(clazz.getType(), maybeNull(), appView).asClassType();
+  private void computePropagationState(DexProgramClass clazz) {
     PropagationState propagationState = new PropagationState(clazz);
 
     // Join the argument information from the methods of the current class.
@@ -162,7 +194,7 @@
           //  distinguish monomorphic unknown method states from polymorphic unknown method states.
           //  We only need to propagate polymorphic unknown method states here.
           if (methodState.isUnknown()) {
-            propagationState.active.addMethodState(appView, method, methodState);
+            propagationState.active.set(method, UnknownMethodState.get());
             return;
           }
 
@@ -183,7 +215,7 @@
                   // TODO(b/190154391): Verify that the bounds are not trivial according to the
                   //  static receiver type.
                   ClassTypeElement upperBound = bounds.getDynamicUpperBoundType().asClassType();
-                  if (upperBound.equalUpToNullability(classType)) {
+                  if (isUpperBoundSatisfied(upperBound, clazz, propagationState)) {
                     if (bounds.hasDynamicLowerBoundType()) {
                       // TODO(b/190154391): Verify that the lower bound is a subtype of the current
                       //  class.
@@ -197,7 +229,10 @@
                       propagationState.active.addMethodState(appView, method, methodStateForBounds);
                     }
                   } else {
-                    assert !classType.lessThanOrEqualUpToNullability(upperBound, appView);
+                    assert !clazz
+                        .getType()
+                        .toTypeElement(appView)
+                        .lessThanOrEqualUpToNullability(upperBound, appView);
                     propagationState
                         .inactiveUntilUpperBound
                         .computeIfAbsent(
@@ -209,11 +244,30 @@
         });
 
     propagationStates.put(clazz, propagationState);
-    return propagationState;
+  }
+
+  private boolean isUpperBoundSatisfied(
+      ClassTypeElement upperBound,
+      DexProgramClass currentClass,
+      PropagationState propagationState) {
+    DexType upperBoundType =
+        upperBound.getClassType() == appView.dexItemFactory().objectType
+                && upperBound.getInterfaces().hasSingleKnownInterface()
+            ? upperBound.getInterfaces().getSingleKnownInterface()
+            : upperBound.getClassType();
+    DexProgramClass upperBoundClass = asProgramClassOrNull(appView.definitionFor(upperBoundType));
+    if (upperBoundClass == null) {
+      // We should generally never have a dynamic receiver upper bound for a program method which is
+      // not a program class. However, since the program may not type change or there could be
+      // missing classes, we still need to cover this case. In the rare cases where this happens, we
+      // conservatively consider the upper bound to be satisfied.
+      return true;
+    }
+    return upperBoundClass == currentClass;
   }
 
   private void computeFinalMethodStates(DexProgramClass clazz, PropagationState propagationState) {
-    clazz.forEachProgramMethod(method -> computeFinalMethodState(method, propagationState));
+    clazz.forEachProgramVirtualMethod(method -> computeFinalMethodState(method, propagationState));
   }
 
   private void computeFinalMethodState(ProgramMethod method, PropagationState propagationState) {
@@ -223,19 +277,23 @@
     }
 
     MethodState methodState = methodStates.get(method);
-
-    // If this is a polymorphic method, we need to compute the method state to account for dynamic
-    // dispatch.
-    if (methodState.isConcrete() && methodState.asConcrete().isPolymorphic()) {
-      methodState = propagationState.computeMethodStateForPolymorhicMethod(method);
-      assert !methodState.isConcrete() || methodState.asConcrete().isMonomorphic();
-      methodStates.set(method, methodState);
+    if (methodState.isMonomorphic() || methodState.isUnknown()) {
+      return;
     }
+
+    assert methodState.isBottom() || methodState.isPolymorphic();
+
+    // This is a polymorphic method and we need to compute the method state to account for dynamic
+    // dispatch.
+    methodState = propagationState.computeMethodStateForPolymorhicMethod(method);
+    assert !methodState.isConcrete() || methodState.asConcrete().isMonomorphic();
+    methodStates.set(method, methodState);
   }
 
   @Override
   public void prune(DexProgramClass clazz) {
-    propagationStates.remove(clazz);
+    PropagationState propagationState = propagationStates.remove(clazz);
+    computeFinalMethodStates(clazz, propagationState);
   }
 
   private boolean verifyAllClassesFinished(Collection<DexProgramClass> stronglyConnectedComponent) {
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/DepthFirstTopDownClassHierarchyTraversal.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/DepthFirstTopDownClassHierarchyTraversal.java
index 379efaf..14d47ac 100644
--- a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/DepthFirstTopDownClassHierarchyTraversal.java
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/DepthFirstTopDownClassHierarchyTraversal.java
@@ -102,13 +102,12 @@
     assert newlySeenButNotFinishedRoots.stream()
         .allMatch(
             newlySeenButNotFinishedRoot -> {
-              assert newlySeenButNotFinishedRoot.isInterface();
               assert isRoot(newlySeenButNotFinishedRoot);
               assert isClassSeenButNotFinished(newlySeenButNotFinishedRoot);
               return true;
             });
-    // Prioritize this interface over other not yet seen interfaces. This leads to more efficient
-    // state pruning.
+    // Prioritize this class over other not yet seen classes. This leads to more efficient state
+    // pruning.
     roots.addAll(newlySeenButNotFinishedRoots);
     newlySeenButNotFinishedRoots.clear();
   }
@@ -122,7 +121,7 @@
     // Before continuing the top-down traversal, ensure that all super interfaces are processed,
     // but without visiting the entire subtree of each super interface.
     if (!isClassSeenButNotFinished(clazz)) {
-      processImplementedInterfaces(clazz);
+      processSuperClasses(clazz);
       processClass(clazz);
     }
 
@@ -130,24 +129,22 @@
     markFinished(clazz);
   }
 
-  private void processImplementedInterfaces(DexProgramClass interfaceDefinition) {
-    assert !isClassSeenButNotFinished(interfaceDefinition);
-    assert !isClassFinished(interfaceDefinition);
-    for (DexType implementedType : interfaceDefinition.getInterfaces()) {
-      DexProgramClass implementedDefinition =
-          asProgramClassOrNull(appView.definitionFor(implementedType));
-      if (implementedDefinition == null || isClassSeenButNotFinished(implementedDefinition)) {
-        continue;
-      }
-      assert isClassUnseen(implementedDefinition);
-      processImplementedInterfaces(implementedDefinition);
-      processClass(implementedDefinition);
+  private void processSuperClasses(DexProgramClass clazz) {
+    assert !isClassSeenButNotFinished(clazz);
+    assert !isClassFinished(clazz);
+    immediateSubtypingInfo.forEachImmediateProgramSuperClassMatching(
+        clazz,
+        superclass -> !isClassSeenButNotFinished(superclass),
+        superclass -> {
+          assert isClassUnseen(superclass);
+          processSuperClasses(superclass);
+          processClass(superclass);
 
-      // If this is a root, then record that this root is seen but not finished.
-      if (isRoot(implementedDefinition)) {
-        newlySeenButNotFinishedRoots.add(implementedDefinition);
-      }
-    }
+          // If this is a root, then record that this root is seen but not finished.
+          if (isRoot(superclass)) {
+            newlySeenButNotFinishedRoots.add(superclass);
+          }
+        });
   }
 
   private void processSubclasses(DexProgramClass clazz) {
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/StronglyConnectedProgramClasses.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/StronglyConnectedProgramClasses.java
new file mode 100644
index 0000000..25d28cb
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/StronglyConnectedProgramClasses.java
@@ -0,0 +1,49 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation.utils;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ImmediateProgramSubtypingInfo;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.WorkList;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public class StronglyConnectedProgramClasses {
+
+  /**
+   * Computes the strongly connected components in the program class hierarchy (where extends and
+   * implements edges are treated as bidirectional).
+   */
+  public static List<Set<DexProgramClass>> computeStronglyConnectedProgramClasses(
+      AppView<AppInfoWithLiveness> appView, ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
+    Set<DexProgramClass> seen = Sets.newIdentityHashSet();
+    List<Set<DexProgramClass>> stronglyConnectedComponents = new ArrayList<>();
+    for (DexProgramClass clazz : appView.appInfo().classes()) {
+      if (seen.contains(clazz)) {
+        continue;
+      }
+      Set<DexProgramClass> stronglyConnectedComponent =
+          internalComputeStronglyConnectedProgramClasses(clazz, immediateSubtypingInfo);
+      stronglyConnectedComponents.add(stronglyConnectedComponent);
+      seen.addAll(stronglyConnectedComponent);
+    }
+    return stronglyConnectedComponents;
+  }
+
+  private static Set<DexProgramClass> internalComputeStronglyConnectedProgramClasses(
+      DexProgramClass clazz, ImmediateProgramSubtypingInfo immediateSubtypingInfo) {
+    WorkList<DexProgramClass> worklist = WorkList.newIdentityWorkList(clazz);
+    while (worklist.hasNext()) {
+      DexProgramClass current = worklist.next();
+      immediateSubtypingInfo.forEachImmediateProgramSuperClass(current, worklist::addIfNotSeen);
+      worklist.addIfNotSeen(immediateSubtypingInfo.getSubclasses(current));
+    }
+    return worklist.getSeenSet();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/WideningUtils.java b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/WideningUtils.java
new file mode 100644
index 0000000..f30aa8e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/optimize/argumentpropagation/utils/WideningUtils.java
@@ -0,0 +1,88 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation.utils;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ObjectAllocationInfoCollection;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
+import com.android.tools.r8.ir.analysis.type.DynamicType;
+import com.android.tools.r8.ir.analysis.type.Nullability;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+public class WideningUtils {
+
+  public static DynamicType widenDynamicReceiverType(
+      AppView<AppInfoWithLiveness> appView,
+      ProgramMethod resolvedMethod,
+      DynamicType dynamicReceiverType) {
+    return shouldWidenDynamicType(
+            appView,
+            dynamicReceiverType,
+            resolvedMethod.getHolderType(),
+            Nullability.definitelyNotNull())
+        ? DynamicType.unknown()
+        : dynamicReceiverType;
+  }
+
+  public static DynamicType widenDynamicNonReceiverType(
+      AppView<AppInfoWithLiveness> appView, DynamicType dynamicType, DexType staticType) {
+    return widenDynamicNonReceiverType(appView, dynamicType, staticType, Nullability.maybeNull());
+  }
+
+  public static DynamicType widenDynamicNonReceiverType(
+      AppView<AppInfoWithLiveness> appView,
+      DynamicType dynamicType,
+      DexType staticType,
+      Nullability staticNullability) {
+    return shouldWidenDynamicType(appView, dynamicType, staticType, staticNullability)
+        ? DynamicType.unknown()
+        : dynamicType;
+  }
+
+  private static boolean shouldWidenDynamicType(
+      AppView<AppInfoWithLiveness> appView,
+      DynamicType dynamicType,
+      DexType staticType,
+      Nullability staticNullability) {
+    assert staticType.isClassType();
+    if (dynamicType.isUnknown()) {
+      return true;
+    }
+    if (dynamicType.isBottom()
+        || dynamicType.isNullType()
+        || dynamicType.getNullability().strictlyLessThan(staticNullability)) {
+      return false;
+    }
+    ClassTypeElement staticTypeElement =
+        staticType.toTypeElement(appView).asClassType().getOrCreateVariant(staticNullability);
+    if (!dynamicType.getDynamicUpperBoundType().equals(staticTypeElement)) {
+      return false;
+    }
+    if (!dynamicType.hasDynamicLowerBoundType()) {
+      return true;
+    }
+
+    DexClass staticTypeClass = appView.definitionFor(staticType);
+    if (staticTypeClass == null || !staticTypeClass.isProgramClass()) {
+      // TODO(b/190154391): If this is a library class with no program subtypes, then we might as
+      //  well widen to 'unknown'.
+      return false;
+    }
+
+    // If the static type does not have any program subtypes, then widen the dynamic type to
+    // unknown.
+    //
+    // Note that if the static type is pinned, it could have subtypes outside the set of program
+    // classes, but in this case it is still unlikely that we can use the dynamic lower bound type
+    // information for anything, so we intentionally also widen to 'unknown' in this case.
+    ObjectAllocationInfoCollection objectAllocationInfoCollection =
+        appView.appInfo().getObjectAllocationInfoCollection();
+    return !objectAllocationInfoCollection.hasInstantiatedStrictSubtype(
+        staticTypeClass.asProgramClass());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/repackaging/RepackagingUseRegistry.java b/src/main/java/com/android/tools/r8/repackaging/RepackagingUseRegistry.java
index 2e7b6b5..a0ac6e8 100644
--- a/src/main/java/com/android/tools/r8/repackaging/RepackagingUseRegistry.java
+++ b/src/main/java/com/android/tools/r8/repackaging/RepackagingUseRegistry.java
@@ -128,6 +128,7 @@
       }
       MethodResolutionResult methodResult = resolutionResult.asMethodResolutionResult();
       if (methodResult.isClassNotFoundResult()
+          || methodResult.isArrayCloneMethodResult()
           || methodResult.isNoSuchMethodErrorResult(context.getContextClass(), appInfo)) {
         return;
       }
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 ba6fb3b..41eb11b 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -6,8 +6,6 @@
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.android.tools.r8.graph.FieldAccessInfoImpl.MISSING_FIELD_ACCESS_INFO;
 import static com.android.tools.r8.ir.desugar.LambdaDescriptor.isLambdaMetafactoryMethod;
-import static com.android.tools.r8.ir.desugar.itf.InterfaceDesugaringSyntheticHelper.emulateInterfaceLibraryMethod;
-import static com.android.tools.r8.ir.desugar.itf.InterfaceDesugaringSyntheticHelper.getEmulateLibraryInterfaceClassType;
 import static com.android.tools.r8.naming.IdentifierNameStringUtils.identifyIdentifier;
 import static com.android.tools.r8.naming.IdentifierNameStringUtils.isReflectionMethod;
 import static com.android.tools.r8.utils.FunctionUtils.ignoreArgument;
@@ -25,7 +23,6 @@
 import com.android.tools.r8.experimental.graphinfo.GraphConsumer;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.ClassAccessFlags;
 import com.android.tools.r8.graph.ClassDefinition;
 import com.android.tools.r8.graph.ClasspathOrLibraryClass;
 import com.android.tools.r8.graph.ClasspathOrLibraryDefinition;
@@ -60,19 +57,16 @@
 import com.android.tools.r8.graph.FieldAccessInfoCollectionImpl;
 import com.android.tools.r8.graph.FieldAccessInfoImpl;
 import com.android.tools.r8.graph.FieldResolutionResult;
-import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
 import com.android.tools.r8.graph.GenericSignatureEnqueuerAnalysis;
 import com.android.tools.r8.graph.InnerClassAttribute;
 import com.android.tools.r8.graph.LookupLambdaTarget;
 import com.android.tools.r8.graph.LookupTarget;
-import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.MethodAccessInfoCollection;
 import com.android.tools.r8.graph.MethodResolutionResult;
 import com.android.tools.r8.graph.MethodResolutionResult.FailedResolutionResult;
 import com.android.tools.r8.graph.MethodResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.NestMemberClassAttribute;
 import com.android.tools.r8.graph.ObjectAllocationInfoCollectionImpl;
-import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.ProgramDerivedContext;
 import com.android.tools.r8.graph.ProgramField;
@@ -128,7 +122,6 @@
 import com.android.tools.r8.utils.Action;
 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.OptionalBool;
 import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.SetUtils;
@@ -3555,9 +3548,6 @@
         assert false;
       }
     }
-    if (mode.isInitialTreeShaking()) {
-      libraryClasses.addAll(synthesizeDesugaredLibraryClasses());
-    }
 
     // Add just referenced non-program types. We can't replace the program classes at this point as
     // they are needed in tree pruning.
@@ -3694,56 +3684,6 @@
     return true;
   }
 
-  private List<DexLibraryClass> synthesizeDesugaredLibraryClasses() {
-    List<DexLibraryClass> synthesizedClasses = new ArrayList<>();
-    Map<DexType, DexType> emulateLibraryInterface =
-        options.desugaredLibraryConfiguration.getEmulateLibraryInterface();
-    emulateLibraryInterface
-        .keySet()
-        .forEach(
-            interfaceType -> {
-              DexClass interfaceClass = appView.definitionFor(interfaceType);
-              if (interfaceClass == null) {
-                appView
-                    .reporter()
-                    .error(
-                        new StringDiagnostic(
-                            "The interface "
-                                + interfaceType.getTypeName()
-                                + " is missing, but is required for Java 8+ API desugaring."));
-                return;
-              }
-
-              DexType emulateInterfaceType =
-                  getEmulateLibraryInterfaceClassType(interfaceType, dexItemFactory);
-              assert appView.definitionFor(emulateInterfaceType) == null;
-
-              List<DexEncodedMethod> emulateInterfaceClassMethods =
-                  ListUtils.newArrayList(
-                      builder ->
-                          interfaceClass.forEachClassMethodMatching(
-                              DexEncodedMethod::isDefaultMethod,
-                              method ->
-                                  builder.accept(
-                                      new DexEncodedMethod(
-                                          emulateInterfaceLibraryMethod(method, dexItemFactory),
-                                          MethodAccessFlags.createPublicStaticSynthetic(),
-                                          MethodTypeSignature.noSignature(),
-                                          DexAnnotationSet.empty(),
-                                          ParameterAnnotationsList.empty(),
-                                          null,
-                                          true))));
-
-              synthesizedClasses.add(
-                  DexLibraryClass.builder(dexItemFactory)
-                      .setAccessFlags(ClassAccessFlags.createPublicFinalSynthetic())
-                      .setDirectMethods(emulateInterfaceClassMethods)
-                      .setType(emulateInterfaceType)
-                      .build());
-            });
-    return synthesizedClasses;
-  }
-
   private static <D extends DexEncodedMember<D, R>, R extends DexMember<D, R>>
       Set<R> toDescriptorSet(Set<D> set) {
     ImmutableSet.Builder<R> builder = new ImmutableSet.Builder<>();
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
index a86e143..71ae785 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -474,7 +474,44 @@
     return legacyItem;
   }
 
-  private DexProgramClass internalCreateClass(
+  private DexProgramClass internalEnsureDexProgramClass(
+      SyntheticKind kind,
+      Consumer<SyntheticProgramClassBuilder> classConsumer,
+      Consumer<DexProgramClass> onCreationConsumer,
+      SynthesizingContext outerContext,
+      DexType type,
+      AppView<?> appView) {
+    // Fast path is that the synthetic is already present. If so it must be a program class.
+    DexClass dexClass = appView.definitionFor(type);
+    if (dexClass != null) {
+      assert dexClass.isProgramClass();
+      return dexClass.asProgramClass();
+    }
+    // Slow path creates the class using the context to make it thread safe.
+    synchronized (type) {
+      // Recheck if it is present now the lock is held.
+      dexClass = appView.definitionFor(type);
+      if (dexClass != null) {
+        assert dexClass.isProgramClass();
+        return dexClass.asProgramClass();
+      }
+      assert !isSyntheticClass(type);
+      DexProgramClass dexProgramClass =
+          internalCreateProgramClass(
+              kind,
+              syntheticProgramClassBuilder -> {
+                syntheticProgramClassBuilder.setUseSortedMethodBacking(true);
+                classConsumer.accept(syntheticProgramClassBuilder);
+              },
+              outerContext,
+              type,
+              appView.dexItemFactory());
+      onCreationConsumer.accept(dexProgramClass);
+      return dexProgramClass;
+    }
+  }
+
+  private DexProgramClass internalCreateProgramClass(
       SyntheticKind kind,
       Consumer<SyntheticProgramClassBuilder> fn,
       SynthesizingContext outerContext,
@@ -499,7 +536,7 @@
     DexType type =
         SyntheticNaming.createInternalType(
             kind, outerContext, context.getSyntheticSuffix(), appView.dexItemFactory());
-    return internalCreateClass(kind, fn, outerContext, type, appView.dexItemFactory());
+    return internalCreateProgramClass(kind, fn, outerContext, type, appView.dexItemFactory());
   }
 
   // TODO(b/172194101): Make this take a unique context.
@@ -510,7 +547,7 @@
       Consumer<SyntheticProgramClassBuilder> fn) {
     SynthesizingContext outerContext = internalGetOuterContext(context, appView);
     DexType type = SyntheticNaming.createFixedType(kind, outerContext, appView.dexItemFactory());
-    return internalCreateClass(kind, fn, outerContext, type, appView.dexItemFactory());
+    return internalCreateProgramClass(kind, fn, outerContext, type, appView.dexItemFactory());
   }
 
   public DexProgramClass getExistingFixedClass(
@@ -547,36 +584,7 @@
     assert kind.isFixedSuffixSynthetic;
     SynthesizingContext outerContext = internalGetOuterContext(context, appView);
     DexType type = SyntheticNaming.createFixedType(kind, outerContext, appView.dexItemFactory());
-    // Fast path is that the synthetic is already present. If so it must be a program class.
-    DexClass clazz = appView.definitionFor(type);
-    if (clazz != null) {
-      assert isSyntheticClass(type);
-      assert clazz.isProgramClass();
-      return clazz.asProgramClass();
-    }
-    // Slow path creates the class using the context to make it thread safe.
-    synchronized (context) {
-      // Recheck if it is present now the lock is held.
-      clazz = appView.definitionFor(type);
-      if (clazz != null) {
-        assert isSyntheticClass(type);
-        assert clazz.isProgramClass();
-        return clazz.asProgramClass();
-      }
-      assert !isSyntheticClass(type);
-      DexProgramClass dexProgramClass =
-          internalCreateClass(
-              kind,
-              syntheticProgramClassBuilder -> {
-                syntheticProgramClassBuilder.setUseSortedMethodBacking(true);
-                fn.accept(syntheticProgramClassBuilder);
-              },
-              outerContext,
-              type,
-              appView.dexItemFactory());
-      onCreationConsumer.accept(dexProgramClass);
-      return dexProgramClass;
-    }
+    return internalEnsureDexProgramClass(kind, fn, onCreationConsumer, outerContext, type, appView);
   }
 
   public ProgramMethod ensureFixedClassMethod(
@@ -668,9 +676,11 @@
       ClasspathOrLibraryClass context,
       AppView<?> appView,
       Consumer<SyntheticClasspathClassBuilder> buildClassCallback,
+      Consumer<DexClasspathClass> onClassCreationCallback,
       Consumer<SyntheticMethodBuilder> buildMethodCallback) {
     DexClasspathClass clazz =
-        ensureFixedClasspathClass(kind, context, appView, buildClassCallback, ignored -> {});
+        ensureFixedClasspathClass(
+            kind, context, appView, buildClassCallback, onClassCreationCallback);
     DexMethod methodReference =
         appView.dexItemFactory().createMethod(clazz.getType(), methodProto, methodName);
     DexEncodedMethod methodDefinition =
@@ -708,16 +718,17 @@
     }
   }
 
-  public DexProgramClass createFixedClassFromType(
+  public DexProgramClass ensureFixedClassFromType(
       SyntheticKind kind,
       DexType contextType,
-      DexItemFactory factory,
-      Consumer<SyntheticProgramClassBuilder> fn) {
+      AppView<?> appView,
+      Consumer<SyntheticProgramClassBuilder> fn,
+      Consumer<DexProgramClass> onCreationConsumer) {
     // Obtain the outer synthesizing context in the case the context itself is synthetic.
     // This is to ensure a flat input-type -> synthetic-item mapping.
     SynthesizingContext outerContext = SynthesizingContext.fromType(contextType);
-    DexType type = SyntheticNaming.createFixedType(kind, outerContext, factory);
-    return internalCreateClass(kind, fn, outerContext, type, factory);
+    DexType type = SyntheticNaming.createFixedType(kind, outerContext, appView.dexItemFactory());
+    return internalEnsureDexProgramClass(kind, fn, onCreationConsumer, outerContext, type, appView);
   }
 
   /** Create a single synthetic method item. */
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 a66d383..9311d46 100644
--- a/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
+++ b/src/main/java/com/android/tools/r8/utils/ArchiveResourceProvider.java
@@ -51,6 +51,10 @@
     this.ignoreDexInArchive = ignoreDexInArchive;
   }
 
+  public Origin getOrigin() {
+    return origin;
+  }
+
   private List<ProgramResource> readArchive() throws IOException {
     List<ProgramResource> dexResources = new ArrayList<>();
     List<ProgramResource> classResources = new ArrayList<>();
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 5ee21c1..f391fb1 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -50,6 +50,7 @@
 import com.android.tools.r8.ir.optimize.Inliner;
 import com.android.tools.r8.ir.optimize.enums.EnumDataMap;
 import com.android.tools.r8.naming.MapVersion;
+import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorEventConsumer;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
 import com.android.tools.r8.references.ClassReference;
@@ -1484,6 +1485,9 @@
 
     public static int NO_LIMIT = -1;
 
+    public ArgumentPropagatorEventConsumer argumentPropagatorEventConsumer =
+        ArgumentPropagatorEventConsumer.emptyConsumer();
+
     // Force writing the specified bytes as the DEX version content.
     public byte[] forceDexVersionBytes = null;
 
diff --git a/src/main/java/com/android/tools/r8/utils/LinkedHashSetUtils.java b/src/main/java/com/android/tools/r8/utils/LinkedHashSetUtils.java
new file mode 100644
index 0000000..d7812fb
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/LinkedHashSetUtils.java
@@ -0,0 +1,14 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.utils;
+
+import java.util.LinkedHashSet;
+
+public class LinkedHashSetUtils {
+
+  public static <T> void addAll(LinkedHashSet<T> set, LinkedHashSet<T> elements) {
+    set.addAll(elements);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java b/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java
index 37deccc..9818d3e 100644
--- a/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/MethodReferenceUtils.java
@@ -7,6 +7,8 @@
 import static com.android.tools.r8.utils.ClassReferenceUtils.getClassReferenceComparator;
 import static com.android.tools.r8.utils.TypeReferenceUtils.getTypeReferenceComparator;
 
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.references.ArrayReference;
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.FieldReference;
@@ -87,6 +89,15 @@
     }
   }
 
+  public static DexMethod toDexMethod(
+      MethodReference methodReference, DexItemFactory dexItemFactory) {
+    return dexItemFactory.createMethod(
+        ClassReferenceUtils.toDexType(methodReference.getHolderClass(), dexItemFactory),
+        TypeReferenceUtils.toDexProto(
+            methodReference.getFormalTypes(), methodReference.getReturnType(), dexItemFactory),
+        methodReference.getMethodName());
+  }
+
   public static String toSourceStringWithoutHolderAndReturnType(MethodReference methodReference) {
     return toSourceString(methodReference, false, false);
   }
diff --git a/src/main/java/com/android/tools/r8/utils/TypeReferenceUtils.java b/src/main/java/com/android/tools/r8/utils/TypeReferenceUtils.java
index 9d322e6..a5145f4 100644
--- a/src/main/java/com/android/tools/r8/utils/TypeReferenceUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/TypeReferenceUtils.java
@@ -37,6 +37,15 @@
     return COMPARATOR;
   }
 
+  public static DexProto toDexProto(
+      List<TypeReference> formalTypes, TypeReference returnType, DexItemFactory dexItemFactory) {
+    return toDexProto(
+        formalTypes,
+        returnType,
+        dexItemFactory,
+        classReference -> ClassReferenceUtils.toDexType(classReference, dexItemFactory));
+  }
+
   /**
    * Converts the given {@param formalTypes} and {@param returnType} to a {@link DexProto}.
    *
@@ -55,6 +64,13 @@
             formalType -> toDexType(formalType, dexItemFactory, classReferenceConverter)));
   }
 
+  public static DexType toDexType(TypeReference typeReference, DexItemFactory dexItemFactory) {
+    return toDexType(
+        typeReference,
+        dexItemFactory,
+        classReference -> ClassReferenceUtils.toDexType(classReference, dexItemFactory));
+  }
+
   /**
    * Converts the given {@param typeReference} to a {@link DexType}.
    *
diff --git a/src/test/java/com/android/tools/r8/DiagnosticsMatcher.java b/src/test/java/com/android/tools/r8/DiagnosticsMatcher.java
index 6517f6e..a826739 100644
--- a/src/test/java/com/android/tools/r8/DiagnosticsMatcher.java
+++ b/src/test/java/com/android/tools/r8/DiagnosticsMatcher.java
@@ -58,15 +58,20 @@
   }
 
   public static Matcher<Diagnostic> diagnosticOrigin(Origin origin) {
+    return diagnosticOrigin(CoreMatchers.is(origin));
+  }
+
+  public static Matcher<Diagnostic> diagnosticOrigin(Matcher<Origin> originMatcher) {
     return new DiagnosticsMatcher() {
       @Override
       protected boolean eval(Diagnostic diagnostic) {
-        return diagnostic.getOrigin().equals(origin);
+        return originMatcher.matches(diagnostic.getOrigin());
       }
 
       @Override
       protected void explain(Description description) {
-        description.appendText("origin ").appendText(origin.toString());
+        description.appendText("origin with ");
+        originMatcher.describeTo(description);
       }
     };
   }
diff --git a/src/test/java/com/android/tools/r8/OriginMatcher.java b/src/test/java/com/android/tools/r8/OriginMatcher.java
new file mode 100644
index 0000000..8484ea3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/OriginMatcher.java
@@ -0,0 +1,33 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import com.android.tools.r8.origin.Origin;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+public abstract class OriginMatcher extends TypeSafeMatcher<Origin> {
+
+  public static Matcher<Origin> hasParent(Origin parent) {
+    return new OriginMatcher() {
+      @Override
+      protected boolean matchesSafely(Origin origin) {
+        Origin current = origin;
+        do {
+          if (current == parent) {
+            return true;
+          }
+          current = current.parent();
+        } while (current != null);
+        return false;
+      }
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("not a parent " + parent);
+      }
+    };
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index e75ab6c..f93398c 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8;
 
 import static com.android.tools.r8.TestBuilder.getTestingAnnotations;
+import static com.android.tools.r8.ToolHelper.R8_TEST_BUCKET;
 import static com.android.tools.r8.utils.InternalOptions.ASM_VERSION;
 import static com.google.common.collect.Lists.cartesianProduct;
 import static org.hamcrest.CoreMatchers.containsString;
@@ -1865,4 +1866,50 @@
     compileResult.assertAllInfoMessagesMatch(
         containsString("The generic super type is not the same as the class super type"));
   }
+
+  public static boolean uploadJarsToCloudStorageIfTestFails(
+      ThrowingBiFunction<Path, Path, Boolean, Exception> test, Path expected, Path actual)
+      throws Exception {
+    boolean filesAreEqual = false;
+    Throwable error = null;
+    try {
+      filesAreEqual = test.apply(expected, actual);
+    } catch (AssertionError | Exception assertionError) {
+      error = assertionError;
+    }
+    if (filesAreEqual) {
+      return true;
+    }
+    // Only upload files if we are running on the bots.
+    if (ToolHelper.isBot()) {
+      try {
+        System.out.println("DIFFERENCE IN JARS DETECTED");
+        System.out.println(
+            "***********************************************************************");
+        String expectedSha = ToolHelper.uploadFileToGoogleCloudStorage(R8_TEST_BUCKET, expected);
+        System.out.println("EXPECTED JAR SHA1: " + expectedSha);
+        System.out.println(
+            String.format(
+                "DOWNLOAD BY: `download_from_google_storage.py --bucket %s %s -o %s`",
+                R8_TEST_BUCKET, expectedSha, expected.getFileName()));
+        String actualSha = ToolHelper.uploadFileToGoogleCloudStorage(R8_TEST_BUCKET, actual);
+        System.out.println("ACTUAL JAR SHA1: " + actualSha);
+        System.out.println(
+            String.format(
+                "DOWNLOAD BY: `download_from_google_storage.py --bucket %s %s -o %s`",
+                R8_TEST_BUCKET, expectedSha, actual.getFileName()));
+        System.out.println(
+            "***********************************************************************");
+      } catch (Throwable e) {
+        e.printStackTrace();
+      }
+    }
+    if (error != null) {
+      if (error instanceof Exception) {
+        throw (Exception) error;
+      }
+      throw new RuntimeException(error);
+    }
+    return false;
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index 1e1b55f..3702430 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -10,6 +10,8 @@
 
 import com.android.tools.r8.TestBase.Backend;
 import com.android.tools.r8.debug.DebugTestConfig;
+import com.android.tools.r8.optimize.argumentpropagation.ArgumentPropagatorEventConsumer;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
 import com.android.tools.r8.testing.AndroidBuildVersion;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
@@ -18,6 +20,7 @@
 import com.android.tools.r8.utils.ForwardingOutputStream;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ThrowingOutputStream;
+import com.android.tools.r8.utils.codeinspector.ArgumentPropagatorCodeScannerResultInspector;
 import com.android.tools.r8.utils.codeinspector.EnumUnboxingInspector;
 import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
 import com.android.tools.r8.utils.codeinspector.VerticallyMergedClassesInspector;
@@ -103,6 +106,23 @@
       B builder, Consumer<InternalOptions> optionsConsumer, Supplier<AndroidApp> app)
       throws CompilationFailedException;
 
+  public T addArgumentPropagatorCodeScannerResultInspector(
+      ThrowableConsumer<ArgumentPropagatorCodeScannerResultInspector> inspector) {
+    return addOptionsModification(
+        options ->
+            options.testing.argumentPropagatorEventConsumer =
+                options.testing.argumentPropagatorEventConsumer.andThen(
+                    new ArgumentPropagatorEventConsumer() {
+                      @Override
+                      public void acceptCodeScannerResult(
+                          MethodStateCollectionByReference methodStates) {
+                        inspector.acceptWithRuntimeException(
+                            new ArgumentPropagatorCodeScannerResultInspector(
+                                options.dexItemFactory(), methodStates));
+                      }
+                    }));
+  }
+
   public T addOptionsModification(Consumer<InternalOptions> optionsConsumer) {
     if (optionsConsumer != null) {
       this.optionsConsumer = this.optionsConsumer.andThen(optionsConsumer);
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index a38852e..a457e50 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -124,6 +124,8 @@
   public static final String JAVA_CLASSES_DIR = BUILD_DIR + "classes/java/";
   public static final String JDK_11_TESTS_CLASSES_DIR = JAVA_CLASSES_DIR + "jdk11Tests/";
 
+  public static final String R8_TEST_BUCKET = "r8-test-results";
+
   public static final String ASM_JAR = BUILD_DIR + "deps/asm-9.2.jar";
   public static final String ASM_UTIL_JAR = BUILD_DIR + "deps/asm-util-9.2.jar";
 
@@ -217,6 +219,11 @@
     return (backend == Backend.CF && outputMode == OutputMode.ClassFile) || backend == Backend.DEX;
   }
 
+  public static boolean isBot() {
+    String swarming_bot_id = System.getenv("SWARMING_BOT_ID");
+    return swarming_bot_id != null && !swarming_bot_id.isEmpty();
+  }
+
   public static StringConsumer consumeString(Consumer<String> consumer) {
     return new StringConsumer() {
 
@@ -2262,4 +2269,31 @@
       return walker.filter(path -> path.toString().endsWith(endsWith)).collect(Collectors.toList());
     }
   }
+
+  /** This code only works if run with depot_tools on the path */
+  public static String uploadFileToGoogleCloudStorage(String bucket, Path file) throws IOException {
+    ImmutableList.Builder<String> command =
+        new ImmutableList.Builder<String>()
+            .add("upload_to_google_storage.py")
+            .add("-f")
+            .add("--bucket")
+            .add(bucket)
+            .add(file.toAbsolutePath().toString());
+    ProcessResult result = ToolHelper.runProcess(new ProcessBuilder(command.build()));
+    if (result.exitCode != 0) {
+      throw new RuntimeException(
+          "Could not upload "
+              + file
+              + " to cloud storage:\n"
+              + result.stdout
+              + "\n"
+              + result.stderr);
+    }
+    // Upload will add a sha1 file at the same location.
+    Path sha1file = file.resolveSibling(file.getFileName() + ".sha1");
+    assert Files.exists(sha1file) : sha1file.toString();
+    List<String> strings = Files.readAllLines(sha1file);
+    assert !strings.isEmpty();
+    return strings.get(0);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/apimodel/ApiModelInlineInSameClassTest.java b/src/test/java/com/android/tools/r8/apimodel/ApiModelInlineInSameClassTest.java
new file mode 100644
index 0000000..1ccb626
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodel/ApiModelInlineInSameClassTest.java
@@ -0,0 +1,108 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.apimodel;
+
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.setMockApiLevelForMethod;
+import static com.android.tools.r8.apimodel.ApiModelingTestHelper.verifyThat;
+import static com.android.tools.r8.utils.AndroidApiLevel.L_MR1;
+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.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeMatchers;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.lang.reflect.Method;
+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 ApiModelInlineInSameClassTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ApiModelInlineInSameClassTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    Method apiMethod = Api.class.getDeclaredMethod("apiLevel22");
+    Method callingApi = ApiCaller.class.getDeclaredMethod("callingApi");
+    Method notCallingApi = ApiCaller.class.getDeclaredMethod("notCallingApi");
+    Method main = Main.class.getDeclaredMethod("main", String[].class);
+    testForR8(parameters.getBackend())
+        .addProgramClasses(ApiCaller.class, ApiCallerCaller.class, Main.class)
+        .addLibraryClasses(Api.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .apply(setMockApiLevelForMethod(apiMethod, AndroidApiLevel.L_MR1))
+        .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+        .compile()
+        .inspect(verifyThat(parameters, notCallingApi).inlinedIntoFromApiLevel(main, L_MR1))
+        .inspect(
+            inspector -> {
+              // No matter the api level, we should always inline callingApi into notCallingApi.
+              assertThat(inspector.method(callingApi), not(isPresent()));
+              if (parameters.isDexRuntime()
+                  && parameters.getApiLevel().isGreaterThanOrEqualTo(L_MR1)) {
+                ClassSubject mainSubject = inspector.clazz(Main.class);
+                MethodSubject mainMethodSubject = mainSubject.uniqueMethodWithName("main");
+                assertThat(mainMethodSubject, isPresent());
+                assertThat(mainMethodSubject, CodeMatchers.invokesMethodWithName("apiLevel22"));
+              }
+            })
+        .addRunClasspathClasses(Api.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Api::apiLevel22");
+  }
+
+  public static class Api {
+
+    public static void apiLevel22() {
+      System.out.println("Api::apiLevel22");
+    }
+  }
+
+  public static class ApiCaller {
+
+    public static void callingApi() {
+      Api.apiLevel22();
+    }
+
+    public static void notCallingApi() {
+      // If api level information is not propagated correctly, inlining `callingApi` into
+      // `notCallingApi` will have an api call that is not modeled correctly and it could be inlined
+      // into its callers incorrectly.
+      callingApi();
+    }
+  }
+
+  public static class ApiCallerCaller {
+
+    public static void foo() {
+      ApiCaller.notCallingApi();
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      ApiCallerCaller.foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/InvokeClinitTest.java b/src/test/java/com/android/tools/r8/cf/InvokeClinitTest.java
new file mode 100644
index 0000000..568fff6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/InvokeClinitTest.java
@@ -0,0 +1,104 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.cf;
+
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
+
+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 com.android.tools.r8.utils.AndroidApiLevel;
+import java.io.IOException;
+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 InvokeClinitTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withAllRuntimes()
+        .withApiLevel(AndroidApiLevel.B)
+        .enableApiLevelsForCf()
+        .build();
+  }
+
+  public InvokeClinitTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramClasses(A.class)
+        .addProgramClassFileData(transformMain())
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatMatches(
+            anyOf(
+                containsString(ClassFormatError.class.getSimpleName()),
+                containsString(VerifyError.class.getSimpleName())));
+  }
+
+  @Test
+  public void testD8() {
+    assertThrows(
+        CompilationFailedException.class,
+        () ->
+            testForD8(parameters.getBackend())
+                .addProgramClasses(A.class)
+                .addProgramClassFileData(transformMain())
+                .setMinApi(parameters.getApiLevel())
+                .compile());
+  }
+
+  @Test
+  public void testR8() {
+    assertThrows(
+        CompilationFailedException.class,
+        () ->
+            testForR8(parameters.getBackend())
+                .addProgramClasses(A.class)
+                .addProgramClassFileData(transformMain())
+                .addKeepMainRule(Main.class)
+                .setMinApi(parameters.getApiLevel())
+                .compile());
+  }
+
+  private byte[] transformMain() throws IOException {
+    return transformer(Main.class)
+        .transformMethodInsnInMethod(
+            "main",
+            (opcode, owner, name, descriptor, isInterface, continuation) ->
+                continuation.visitMethodInsn(opcode, owner, "<clinit>", descriptor, isInterface))
+        .transform();
+  }
+
+  static class A {
+    static {
+      System.out.println("A.<clinit>");
+    }
+
+    static void willBeClinit() {
+      System.out.println("unused");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      A.willBeClinit();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapCurrentEqualityTest.java b/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapCurrentEqualityTest.java
index a8d57c0..e41bf8d 100644
--- a/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapCurrentEqualityTest.java
+++ b/src/test/java/com/android/tools/r8/cf/bootstrap/BootstrapCurrentEqualityTest.java
@@ -14,7 +14,6 @@
 import static org.junit.Assert.assertNotEquals;
 
 import com.android.tools.r8.ArchiveClassFileProvider;
-import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.ExternalR8TestCompileResult;
 import com.android.tools.r8.TestBase;
@@ -160,7 +159,7 @@
   }
 
   @Test
-  public void testR8LibCompatibility() throws IOException, CompilationFailedException {
+  public void testR8LibCompatibility() throws Exception {
     // Produce r81 = R8Lib(R8WithDeps) and r82 = R8LibNoDeps + Deps(R8WithDeps) and test that r81 is
     // equal to r82. This test should only run if we are testing r8lib and we expect both R8libs to
     // be built by gradle. If we are not testing with R8Lib, do not run this test.
@@ -184,7 +183,8 @@
             .setMode(CompilationMode.RELEASE)
             .compile()
             .outputJar();
-    assert filesAreEqual(runR81, runR82);
+    assert uploadJarsToCloudStorageIfTestFails(
+        BootstrapCurrentEqualityTest::filesAreEqual, runR81, runR82);
   }
 
   @Test
@@ -243,12 +243,13 @@
     assertEquals(result.getStdout(), runR8R8.getStdout());
     assertEquals(result.getStderr(), runR8R8.getStderr());
     // Check that the output jars are the same.
-    assertProgramsEqual(result.outputJar(), runR8R8.outputJar());
+    uploadJarsToCloudStorageIfTestFails(
+        BootstrapCurrentEqualityTest::assertProgramsEqual, result.outputJar(), runR8R8.outputJar());
   }
 
-  public static void assertProgramsEqual(Path expectedJar, Path actualJar) throws Exception {
+  public static boolean assertProgramsEqual(Path expectedJar, Path actualJar) throws Exception {
     if (filesAreEqual(expectedJar, actualJar)) {
-      return;
+      return true;
     }
     ArchiveClassFileProvider expected = new ArchiveClassFileProvider(expectedJar);
     ArchiveClassFileProvider actual = new ArchiveClassFileProvider(actualJar);
@@ -259,6 +260,7 @@
           getClassAsBytes(expected, descriptor),
           getClassAsBytes(actual, descriptor));
     }
+    return false;
   }
 
   public static boolean filesAreEqual(Path file1, Path file2) throws IOException {
diff --git a/src/test/java/com/android/tools/r8/desugar/constantdynamic/BasicConstantDynamicTest.java b/src/test/java/com/android/tools/r8/desugar/constantdynamic/BasicConstantDynamicTest.java
new file mode 100644
index 0000000..b0d261e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/constantdynamic/BasicConstantDynamicTest.java
@@ -0,0 +1,126 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.desugar.constantdynamic;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticOrigin;
+import static com.android.tools.r8.OriginMatcher.hasParent;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class BasicConstantDynamicTest extends TestBase {
+
+  @Parameter() public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build());
+  }
+
+  private static final String EXPECTED_OUTPUT = StringUtils.lines("true", "true");
+
+  @Test
+  public void testReference() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    assumeTrue(parameters.getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK11));
+    assumeTrue(parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
+
+    testForJvm()
+        .addProgramClassFileData(getTransformedClasses())
+        .run(parameters.getRuntime(), A.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.getRuntime().isDex());
+
+    assertThrows(
+        CompilationFailedException.class,
+        () ->
+            testForD8(parameters.getBackend())
+                .addProgramClassFileData(getTransformedClasses())
+                .setMinApi(parameters.getApiLevel())
+                .compileWithExpectedDiagnostics(
+                    diagnostics -> {
+                      diagnostics.assertOnlyErrors();
+                      diagnostics.assertErrorsMatch(
+                          allOf(
+                              diagnosticMessage(containsString("Unsupported dynamic constant")),
+                              diagnosticOrigin(hasParent(Origin.unknown()))));
+                    }));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assumeTrue(parameters.isDexRuntime() || parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
+
+    assertThrows(
+        CompilationFailedException.class,
+        () ->
+            testForR8(parameters.getBackend())
+                .addProgramClassFileData(getTransformedClasses())
+                .setMinApi(parameters.getApiLevel())
+                .addKeepMainRule(A.class)
+                .compileWithExpectedDiagnostics(
+                    diagnostics -> {
+                      diagnostics.assertOnlyErrors();
+                      diagnostics.assertErrorsMatch(
+                          allOf(
+                              diagnosticMessage(containsString("Unsupported dynamic constant")),
+                              diagnosticOrigin(hasParent(Origin.unknown()))));
+                    }));
+  }
+
+  private byte[] getTransformedClasses() throws IOException {
+    return transformer(A.class)
+        .setVersion(CfVersion.V11)
+        .transformConstStringToConstantDynamic(
+            "condy1", A.class, "myConstant", "constantName", Object.class)
+        .transformConstStringToConstantDynamic(
+            "condy2", A.class, "myConstant", "constantName", Object.class)
+        .transform();
+  }
+
+  public static class A {
+
+    public static Object f() {
+      return "condy1"; // Will be transformed to Constant_DYNAMIC.
+    }
+
+    public static Object g() {
+      return "condy2"; // Will be transformed to Constant_DYNAMIC.
+    }
+
+    public static void main(String[] args) {
+      System.out.println(f() != null);
+      System.out.println(f() == g());
+    }
+
+    private static Object myConstant(MethodHandles.Lookup lookup, String name, Class<?> type) {
+      return new Object();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/constantdynamic/JacocoConstantDynamicTest.java b/src/test/java/com/android/tools/r8/desugar/constantdynamic/JacocoConstantDynamicTest.java
index ee5142f..d1a4d35 100644
--- a/src/test/java/com/android/tools/r8/desugar/constantdynamic/JacocoConstantDynamicTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/constantdynamic/JacocoConstantDynamicTest.java
@@ -5,6 +5,7 @@
 
 import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
 import static com.android.tools.r8.DiagnosticsMatcher.diagnosticOrigin;
+import static com.android.tools.r8.OriginMatcher.hasParent;
 import static com.android.tools.r8.utils.DescriptorUtils.JAVA_PACKAGE_SEPARATOR;
 import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
 import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
@@ -23,7 +24,7 @@
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.cf.CfVersion;
-import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.ArchiveResourceProvider;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.ZipUtils;
@@ -141,28 +142,23 @@
     } else {
       assertThrows(
           CompilationFailedException.class,
-          () ->
-              testForD8(parameters.getBackend())
-                  .addProgramFiles(testClasses.getInstrumented())
-                  .addProgramFiles(ToolHelper.JACOCO_AGENT)
-                  .setMinApi(parameters.getApiLevel())
-                  .compileWithExpectedDiagnostics(
-                      diagnostics -> {
-                        // Check that the error is reported as an error to the diagnostics handler.
-                        diagnostics.assertErrorsCount(1);
-                        diagnostics.assertAllErrorsMatch(
-                            allOf(
-                                diagnosticMessage(containsString("Unsupported dynamic constant")),
-                                // The fatal error is not given an origin, so it can't provide it.
-                                // Note: This could be fixed by delaying reporting and associate the
-                                // info
-                                //  at the top-level handler. It would require mangling of the
-                                // diagnostic,
-                                //  so maybe not that elegant.
-                                diagnosticOrigin(Origin.unknown())));
-                        diagnostics.assertWarningsCount(0);
-                        diagnostics.assertInfosCount(0);
-                      }));
+          () -> {
+            ArchiveResourceProvider provider =
+                ArchiveResourceProvider.fromArchive(testClasses.getInstrumented(), true);
+            testForD8(parameters.getBackend())
+                .addProgramResourceProviders(provider)
+                .addProgramFiles(ToolHelper.JACOCO_AGENT)
+                .setMinApi(parameters.getApiLevel())
+                .compileWithExpectedDiagnostics(
+                    diagnostics -> {
+                      // Check that the error is reported as an error to the diagnostics handler.
+                      diagnostics.assertOnlyErrors();
+                      diagnostics.assertErrorsMatch(
+                          allOf(
+                              diagnosticMessage(containsString("Unsupported dynamic constant")),
+                              diagnosticOrigin(hasParent(provider.getOrigin()))));
+                    });
+          });
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/desugar/constantdynamic/MultipleNamedConstantDynamicTest.java b/src/test/java/com/android/tools/r8/desugar/constantdynamic/MultipleNamedConstantDynamicTest.java
new file mode 100644
index 0000000..69cd883
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/constantdynamic/MultipleNamedConstantDynamicTest.java
@@ -0,0 +1,144 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.desugar.constantdynamic;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticOrigin;
+import static com.android.tools.r8.OriginMatcher.hasParent;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class MultipleNamedConstantDynamicTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build());
+  }
+
+  private static final String EXPECTED_OUTPUT =
+      StringUtils.lines("true", "true", "true", "true", "true");
+
+  @Test
+  public void testReference() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    assumeTrue(parameters.getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK11));
+    assumeTrue(parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
+
+    testForJvm()
+        .addProgramClassFileData(getTransformedClasses())
+        .run(parameters.getRuntime(), A.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+
+    assertThrows(
+        CompilationFailedException.class,
+        () ->
+            testForD8(parameters.getBackend())
+                .addProgramClassFileData(getTransformedClasses())
+                .setMinApi(parameters.getApiLevel())
+                .compileWithExpectedDiagnostics(
+                    diagnostics -> {
+                      diagnostics.assertOnlyErrors();
+                      diagnostics.assertErrorsMatch(
+                          allOf(
+                              diagnosticMessage(containsString("Unsupported dynamic constant")),
+                              diagnosticOrigin(hasParent(Origin.unknown()))));
+                    }));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assumeTrue(
+        parameters.getRuntime().isDex() || parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
+
+    assertThrows(
+        CompilationFailedException.class,
+        () ->
+            testForR8(parameters.getBackend())
+                .addProgramClassFileData(getTransformedClasses())
+                .setMinApi(parameters.getApiLevel())
+                .addKeepMainRule(A.class)
+                .compileWithExpectedDiagnostics(
+                    diagnostics -> {
+                      diagnostics.assertOnlyErrors();
+                      diagnostics.assertErrorsMatch(
+                          allOf(
+                              diagnosticMessage(containsString("Unsupported dynamic constant")),
+                              diagnosticOrigin(hasParent(Origin.unknown()))));
+                    }));
+  }
+
+  private byte[] getTransformedClasses() throws IOException {
+    return transformer(A.class)
+        .setVersion(CfVersion.V11)
+        .transformConstStringToConstantDynamic(
+            "condy1", A.class, "myConstant", "constantF", Object.class)
+        .transformConstStringToConstantDynamic(
+            "condy2", A.class, "myConstant", "constantF", Object.class)
+        .transformConstStringToConstantDynamic(
+            "condy3", A.class, "myConstant", "constantG", Object.class)
+        .transformConstStringToConstantDynamic(
+            "condy4", A.class, "myConstant", "constantG", Object.class)
+        .transform();
+  }
+
+  public static class A {
+
+    public static Object f1() {
+      return "condy1"; // Will be transformed to Constant_DYNAMIC.
+    }
+
+    public static Object f2() {
+      return "condy2"; // Will be transformed to Constant_DYNAMIC.
+    }
+
+    public static Object g1() {
+      return "condy3"; // Will be transformed to Constant_DYNAMIC.
+    }
+
+    public static Object g2() {
+      return "condy4"; // Will be transformed to Constant_DYNAMIC.
+    }
+
+    public static void main(String[] args) {
+      System.out.println(f1() != null);
+      System.out.println(f1() == f2());
+      System.out.println(g1() != null);
+      System.out.println(g1() == g2());
+      System.out.println(f1() != g2());
+    }
+
+    private static Object myConstant(MethodHandles.Lookup lookup, String name, Class<?> type) {
+      return new Object();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/constantdynamic/MultipleTypesConstantDynamicTest.java b/src/test/java/com/android/tools/r8/desugar/constantdynamic/MultipleTypesConstantDynamicTest.java
new file mode 100644
index 0000000..b04792f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/constantdynamic/MultipleTypesConstantDynamicTest.java
@@ -0,0 +1,147 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.desugar.constantdynamic;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticOrigin;
+import static com.android.tools.r8.OriginMatcher.hasParent;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class MultipleTypesConstantDynamicTest extends TestBase {
+
+  @Parameter public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build());
+  }
+
+  private static final String EXPECTED_OUTPUT =
+      StringUtils.lines("true", "true", "true", "true", "true");
+
+  @Test
+  public void testReference() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    assumeTrue(parameters.getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK11));
+    assumeTrue(parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
+
+    testForJvm()
+        .addProgramClassFileData(getTransformedClasses())
+        .run(parameters.getRuntime(), A.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+
+    assertThrows(
+        CompilationFailedException.class,
+        () ->
+            testForD8(parameters.getBackend())
+                .addProgramClassFileData(getTransformedClasses())
+                .setMinApi(parameters.getApiLevel())
+                .compileWithExpectedDiagnostics(
+                    diagnostics -> {
+                      diagnostics.assertOnlyErrors();
+                      diagnostics.assertErrorsMatch(
+                          allOf(
+                              diagnosticMessage(containsString("Unsupported dynamic constant")),
+                              diagnosticOrigin(hasParent(Origin.unknown()))));
+                    }));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assumeTrue(
+        parameters.getRuntime().isDex() || parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
+
+    assertThrows(
+        CompilationFailedException.class,
+        () ->
+            testForR8(parameters.getBackend())
+                .addProgramClassFileData(getTransformedClasses())
+                .setMinApi(parameters.getApiLevel())
+                .addKeepMainRule(A.class)
+                .compileWithExpectedDiagnostics(
+                    diagnostics -> {
+                      diagnostics.assertOnlyErrors();
+                      diagnostics.assertErrorsMatch(
+                          allOf(
+                              diagnosticMessage(containsString("Unsupported dynamic constant")),
+                              diagnosticOrigin(hasParent(Origin.unknown()))));
+                    }));
+  }
+
+  private byte[] getTransformedClasses() throws IOException {
+    return transformer(A.class)
+        .setVersion(CfVersion.V11)
+        .transformConstStringToConstantDynamic(
+            "condy1", A.class, "myConstant", "constantName", Object.class)
+        .transformConstStringToConstantDynamic(
+            "condy2", A.class, "myConstant", "constantName", Object.class)
+        .transformConstStringToConstantDynamic(
+            "condy3", A.class, "myConstant", "constantName", boolean[].class)
+        .transformConstStringToConstantDynamic(
+            "condy4", A.class, "myConstant", "constantName", boolean[].class)
+        .transform();
+  }
+
+  public static class A {
+
+    public static Object f1() {
+      return "condy1"; // Will be transformed to Constant_DYNAMIC.
+    }
+
+    public static Object f2() {
+      return "condy2"; // Will be transformed to Constant_DYNAMIC.
+    }
+
+    public static Object g1() {
+      return "condy3"; // Will be transformed to Constant_DYNAMIC.
+    }
+
+    public static Object g2() {
+      return "condy4"; // Will be transformed to Constant_DYNAMIC.
+    }
+
+    public static void main(String[] args) {
+      System.out.println(f1() != null);
+      System.out.println(f1() == f2());
+      System.out.println(g1() != null);
+      System.out.println(g1() == g2());
+      System.out.println(f1() != g2());
+    }
+
+    private static Object myConstant(MethodHandles.Lookup lookup, String name, Class<?> type) {
+      if (type.isArray()) {
+        return new boolean[0];
+      } else {
+        return new Object();
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/constantdynamic/SharedBootstrapMethodConstantDynamicTest.java b/src/test/java/com/android/tools/r8/desugar/constantdynamic/SharedBootstrapMethodConstantDynamicTest.java
new file mode 100644
index 0000000..24c9ed8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/constantdynamic/SharedBootstrapMethodConstantDynamicTest.java
@@ -0,0 +1,152 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.desugar.constantdynamic;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticOrigin;
+import static com.android.tools.r8.OriginMatcher.hasParent;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.lang.invoke.MethodHandles;
+import java.util.Collection;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class SharedBootstrapMethodConstantDynamicTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build());
+  }
+
+  private static final String EXPECTED_OUTPUT =
+      StringUtils.lines("true", "true", "true", "true", "true");
+
+  @Test
+  public void testReference() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    assumeTrue(parameters.getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK11));
+    assumeTrue(parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
+
+    testForJvm()
+        .addProgramClassFileData(getTransformedClasses())
+        .run(parameters.getRuntime(), A.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+
+    assertThrows(
+        CompilationFailedException.class,
+        () ->
+            testForD8(parameters.getBackend())
+                .addProgramClassFileData(getTransformedClasses())
+                .setMinApi(parameters.getApiLevel())
+                .compileWithExpectedDiagnostics(
+                    diagnostics -> {
+                      diagnostics.assertOnlyErrors();
+                      diagnostics.assertErrorsMatch(
+                          allOf(
+                              diagnosticMessage(containsString("Unsupported dynamic constant")),
+                              diagnosticOrigin(hasParent(Origin.unknown()))));
+                    }));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assumeTrue(parameters.isDexRuntime() || parameters.getApiLevel().isEqualTo(AndroidApiLevel.B));
+
+    assertThrows(
+        CompilationFailedException.class,
+        () ->
+            testForR8(parameters.getBackend())
+                .addProgramClassFileData(getTransformedClasses())
+                .setMinApi(parameters.getApiLevel())
+                .addKeepMainRule(A.class)
+                .compileWithExpectedDiagnostics(
+                    diagnostics -> {
+                      diagnostics.assertOnlyErrors();
+                      diagnostics.assertErrorsMatch(
+                          allOf(
+                              diagnosticMessage(containsString("Unsupported dynamic constant")),
+                              diagnosticOrigin(hasParent(Origin.unknown()))));
+                    }));
+  }
+
+  private Collection<byte[]> getTransformedClasses() throws IOException {
+    return ImmutableList.of(
+        transformer(A.class)
+            .setVersion(CfVersion.V11)
+            .transformConstStringToConstantDynamic(
+                "condy1", A.class, "myConstant", "constantName", Object.class)
+            .transformConstStringToConstantDynamic(
+                "condy2", A.class, "myConstant", "constantName", Object.class)
+            .transform(),
+        transformer(B.class)
+            .setVersion(CfVersion.V11)
+            .transformConstStringToConstantDynamic(
+                "condy3", A.class, "myConstant", "constantName", Object.class)
+            .transformConstStringToConstantDynamic(
+                "condy4", A.class, "myConstant", "constantName", Object.class)
+            .transform());
+  }
+
+  public static class A {
+
+    public static Object f() {
+      return "condy1"; // Will be transformed to Constant_DYNAMIC.
+    }
+
+    public static Object g() {
+      return "condy2"; // Will be transformed to Constant_DYNAMIC.
+    }
+
+    public static void main(String[] args) {
+      System.out.println(A.f() != null);
+      System.out.println(A.f() == A.g());
+      System.out.println(B.f() != null);
+      System.out.println(B.f() == B.g());
+      System.out.println(A.f() != B.g());
+    }
+
+    public static Object myConstant(MethodHandles.Lookup lookup, String name, Class<?> type) {
+      return new Object();
+    }
+  }
+
+  public static class B {
+
+    public static Object f() {
+      return "condy3"; // Will be transformed to Constant_DYNAMIC.
+    }
+
+    public static Object g() {
+      return "condy4"; // Will be transformed to Constant_DYNAMIC.
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/Java11R8BootstrapTest.java b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/Java11R8BootstrapTest.java
index 1eac80e..66bdd07 100644
--- a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/Java11R8BootstrapTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/Java11R8BootstrapTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.desugar.nestaccesscontrol;
 
+import static com.android.tools.r8.cf.bootstrap.BootstrapCurrentEqualityTest.uploadJarsToCloudStorageIfTestFails;
 import static com.android.tools.r8.utils.FileUtils.JAR_EXTENSION;
 import static junit.framework.TestCase.assertEquals;
 import static junit.framework.TestCase.assertTrue;
@@ -128,7 +129,8 @@
       }
       prevRunResult = runResult;
       if (prevGeneratedJar != null) {
-        BootstrapCurrentEqualityTest.assertProgramsEqual(prevGeneratedJar, generatedJar);
+        uploadJarsToCloudStorageIfTestFails(
+            BootstrapCurrentEqualityTest::assertProgramsEqual, prevGeneratedJar, generatedJar);
       }
       prevGeneratedJar = generatedJar;
     }
@@ -149,7 +151,8 @@
               .compile()
               .outputJar();
       if (prevGeneratedJar != null) {
-        BootstrapCurrentEqualityTest.assertProgramsEqual(prevGeneratedJar, generatedJar);
+        uploadJarsToCloudStorageIfTestFails(
+            BootstrapCurrentEqualityTest::assertProgramsEqual, prevGeneratedJar, generatedJar);
       }
       prevGeneratedJar = generatedJar;
     }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java b/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java
index 3f0992a..389744e 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/EmptyRecordTest.java
@@ -13,7 +13,6 @@
 import com.android.tools.r8.utils.StringUtils;
 import java.nio.file.Path;
 import java.util.List;
-import org.junit.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -63,23 +62,35 @@
   }
 
   @Test
-  public void testR8Cf() throws Exception {
-    Assume.assumeTrue(parameters.isCfRuntime());
-    Path output =
-        testForR8(parameters.getBackend())
-            .addProgramClassFileData(PROGRAM_DATA)
-            .setMinApi(parameters.getApiLevel())
-            .addKeepRules(RECORD_KEEP_RULE)
-            .addKeepMainRule(MAIN_TYPE)
-            .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
-            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
-            .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
-            .compile()
-            .writeToZip();
-    RecordTestUtils.assertRecordsAreRecords(output);
-    testForJvm()
-        .addRunClasspathFiles(output)
-        .enablePreview()
+  public void testR8() throws Exception {
+    if (parameters.isCfRuntime()) {
+      Path output =
+          testForR8(parameters.getBackend())
+              .addProgramClassFileData(PROGRAM_DATA)
+              .setMinApi(parameters.getApiLevel())
+              .addKeepRules(RECORD_KEEP_RULE)
+              .addKeepMainRule(MAIN_TYPE)
+              .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
+              .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+              .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
+              .compile()
+              .writeToZip();
+      RecordTestUtils.assertRecordsAreRecords(output);
+      testForJvm()
+          .addRunClasspathFiles(output)
+          .enablePreview()
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+      return;
+    }
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(PROGRAM_DATA)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepRules(RECORD_KEEP_RULE)
+        .addKeepMainRule(MAIN_TYPE)
+        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+        .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
+        .compile()
         .run(parameters.getRuntime(), MAIN_TYPE)
         .assertSuccessWithOutput(EXPECTED_RESULT);
   }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/GenerateRecordMethods.java b/src/test/java/com/android/tools/r8/desugar/records/GenerateRecordMethods.java
index cbc7ef2..895aa97 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/GenerateRecordMethods.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/GenerateRecordMethods.java
@@ -8,31 +8,14 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.TestRuntime.CfVm;
-import com.android.tools.r8.cf.code.CfFrame;
-import com.android.tools.r8.cf.code.CfFrame.FrameType;
-import com.android.tools.r8.cf.code.CfInstruction;
-import com.android.tools.r8.cf.code.CfInvoke;
-import com.android.tools.r8.cf.code.CfTypeInstruction;
 import com.android.tools.r8.cfmethodgeneration.MethodGenerationBase;
-import com.android.tools.r8.desugar.records.RecordMethods.RecordStub;
-import com.android.tools.r8.graph.CfCode;
-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.ir.desugar.records.RecordRewriter;
-import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.FileUtils;
 import com.google.common.collect.ImmutableList;
-import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
-import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
 import java.nio.charset.StandardCharsets;
-import java.util.ArrayDeque;
 import java.util.ArrayList;
 import java.util.Comparator;
-import java.util.Deque;
 import java.util.List;
-import java.util.SortedMap;
-import java.util.stream.Collectors;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -42,8 +25,6 @@
 public class GenerateRecordMethods extends MethodGenerationBase {
   private final DexType GENERATED_TYPE =
       factory.createType("Lcom/android/tools/r8/ir/desugar/records/RecordCfMethods;");
-  private final DexType RECORD_STUB_TYPE =
-      factory.createType(DescriptorUtils.javaTypeToDescriptor(RecordStub.class.getTypeName()));
   private final List<Class<?>> METHOD_TEMPLATE_CLASSES = ImmutableList.of(RecordMethods.class);
 
   protected final TestParameters parameters;
@@ -72,77 +53,6 @@
     return 2021;
   }
 
-  @Override
-  protected CfCode getCode(String holderName, String methodName, CfCode code) {
-    code.setInstructions(
-        code.getInstructions().stream()
-            .map(instruction -> rewriteRecordStub(instruction))
-            .collect(Collectors.toList()));
-    return code;
-  }
-
-  private CfInstruction rewriteRecordStub(CfInstruction instruction) {
-    if (instruction.isTypeInstruction()) {
-      CfTypeInstruction typeInstruction = instruction.asTypeInstruction();
-      return typeInstruction.withType(rewriteType(typeInstruction.getType()));
-    }
-    if (instruction.isInvoke()) {
-      CfInvoke cfInvoke = instruction.asInvoke();
-      DexMethod method = cfInvoke.getMethod();
-      DexMethod newMethod =
-          factory.createMethod(rewriteType(method.holder), method.proto, rewriteName(method.name));
-      return new CfInvoke(cfInvoke.getOpcode(), newMethod, cfInvoke.isInterface());
-    }
-    if (instruction.isFrame()) {
-      CfFrame cfFrame = instruction.asFrame();
-      return new CfFrame(
-          rewriteLocals(cfFrame.getLocalsAsSortedMap()), rewriteStack(cfFrame.getStack()));
-    }
-    return instruction;
-  }
-
-  private String rewriteName(DexString name) {
-    return name.toString().equals("getFieldsAsObjects")
-        ? RecordRewriter.GET_FIELDS_AS_OBJECTS_METHOD_NAME
-        : name.toString();
-  }
-
-  private DexType rewriteType(DexType type) {
-    DexType baseType = type.isArrayType() ? type.toBaseType(factory) : type;
-    if (baseType != RECORD_STUB_TYPE) {
-      return type;
-    }
-    return type.isArrayType()
-        ? type.replaceBaseType(factory.recordType, factory)
-        : factory.recordType;
-  }
-
-  private FrameType rewriteFrameType(FrameType frameType) {
-    if (frameType.isInitialized() && frameType.getInitializedType().isReferenceType()) {
-      DexType newType = rewriteType(frameType.getInitializedType());
-      if (newType == frameType.getInitializedType()) {
-        return frameType;
-      }
-      return FrameType.initialized(newType);
-    } else {
-      assert !frameType.isUninitializedNew();
-      assert !frameType.isUninitializedThis();
-      return frameType;
-    }
-  }
-
-  private SortedMap<Integer, FrameType> rewriteLocals(SortedMap<Integer, FrameType> locals) {
-    Int2ReferenceSortedMap<FrameType> newLocals = new Int2ReferenceAVLTreeMap<>();
-    locals.forEach((index, local) -> newLocals.put((int) index, rewriteFrameType(local)));
-    return newLocals;
-  }
-
-  private Deque<FrameType> rewriteStack(Deque<FrameType> stack) {
-    ArrayDeque<FrameType> newStack = new ArrayDeque<>();
-    stack.forEach(frameType -> newStack.add(rewriteFrameType(frameType)));
-    return newStack;
-  }
-
   @Test
   public void testRecordMethodsGenerated() throws Exception {
     ArrayList<Class<?>> sorted = new ArrayList<>(getMethodTemplateClasses());
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordInstanceOfTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordInstanceOfTest.java
index c6b3f68..08b0e85 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordInstanceOfTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordInstanceOfTest.java
@@ -13,7 +13,6 @@
 import com.android.tools.r8.utils.StringUtils;
 import java.nio.file.Path;
 import java.util.List;
-import org.junit.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -63,23 +62,35 @@
   }
 
   @Test
-  public void testR8Cf() throws Exception {
-    Assume.assumeTrue(parameters.isCfRuntime());
-    Path output =
-        testForR8(parameters.getBackend())
-            .addProgramClassFileData(PROGRAM_DATA)
-            .setMinApi(parameters.getApiLevel())
-            .addKeepRules(RECORD_KEEP_RULE)
-            .addKeepMainRule(MAIN_TYPE)
-            .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
-            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
-            .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
-            .compile()
-            .writeToZip();
-    RecordTestUtils.assertRecordsAreRecords(output);
-    testForJvm()
-        .addRunClasspathFiles(output)
-        .enablePreview()
+  public void testR8() throws Exception {
+    if (parameters.isCfRuntime()) {
+      Path output =
+          testForR8(parameters.getBackend())
+              .addProgramClassFileData(PROGRAM_DATA)
+              .setMinApi(parameters.getApiLevel())
+              .addKeepRules(RECORD_KEEP_RULE)
+              .addKeepMainRule(MAIN_TYPE)
+              .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
+              .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+              .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
+              .compile()
+              .writeToZip();
+      RecordTestUtils.assertRecordsAreRecords(output);
+      testForJvm()
+          .addRunClasspathFiles(output)
+          .enablePreview()
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+      return;
+    }
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(PROGRAM_DATA)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepRules(RECORD_KEEP_RULE)
+        .addKeepMainRule(MAIN_TYPE)
+        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+        .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
+        .compile()
         .run(parameters.getRuntime(), MAIN_TYPE)
         .assertSuccessWithOutput(EXPECTED_RESULT);
   }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomTest.java
index 55cb175..9bf3327 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordInvokeCustomTest.java
@@ -13,7 +13,6 @@
 import com.android.tools.r8.utils.StringUtils;
 import java.nio.file.Path;
 import java.util.List;
-import org.junit.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -76,23 +75,35 @@
   }
 
   @Test
-  public void testR8Cf() throws Exception {
-    Assume.assumeTrue(parameters.isCfRuntime());
-    Path output =
-        testForR8(parameters.getBackend())
-            .addProgramClassFileData(PROGRAM_DATA)
-            .setMinApi(parameters.getApiLevel())
-            .addKeepRules(RECORD_KEEP_RULE)
-            .addKeepMainRule(MAIN_TYPE)
-            .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
-            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
-            .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
-            .compile()
-            .writeToZip();
-    RecordTestUtils.assertRecordsAreRecords(output);
-    testForJvm()
-        .addRunClasspathFiles(output)
-        .enablePreview()
+  public void testR8() throws Exception {
+    if (parameters.isCfRuntime()) {
+      Path output =
+          testForR8(parameters.getBackend())
+              .addProgramClassFileData(PROGRAM_DATA)
+              .setMinApi(parameters.getApiLevel())
+              .addKeepRules(RECORD_KEEP_RULE)
+              .addKeepMainRule(MAIN_TYPE)
+              .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
+              .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+              .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
+              .compile()
+              .writeToZip();
+      RecordTestUtils.assertRecordsAreRecords(output);
+      testForJvm()
+          .addRunClasspathFiles(output)
+          .enablePreview()
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+      return;
+    }
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(PROGRAM_DATA)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepRules(RECORD_KEEP_RULE)
+        .addKeepMainRule(MAIN_TYPE)
+        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+        .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
+        .compile()
         .run(parameters.getRuntime(), MAIN_TYPE)
         .assertSuccessWithOutput(EXPECTED_RESULT);
   }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordMethods.java b/src/test/java/com/android/tools/r8/desugar/records/RecordMethods.java
index 7b1b2e5..a1fc48e 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordMethods.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordMethods.java
@@ -10,14 +10,14 @@
 // rewrites relevant calls to one of the following methods.
 public class RecordMethods {
 
-  public static String toString(RecordStub recordInstance, String simpleName, String fieldNames) {
+  public static String toString(
+      Object[] recordFieldsAsObjects, String simpleName, String fieldNames) {
     // Example: "Person[name=Jane Doe, age=42]"
     String[] fieldNamesSplit = fieldNames.isEmpty() ? new String[0] : fieldNames.split(";");
-    Object[] fields = recordInstance.getFieldsAsObjects();
     StringBuilder builder = new StringBuilder();
     builder.append(simpleName).append("[");
     for (int i = 0; i < fieldNamesSplit.length; i++) {
-      builder.append(fieldNamesSplit[i]).append("=").append(fields[i]);
+      builder.append(fieldNamesSplit[i]).append("=").append(recordFieldsAsObjects[i]);
       if (i != fieldNamesSplit.length - 1) {
         builder.append(", ");
       }
@@ -26,18 +26,7 @@
     return builder.toString();
   }
 
-  public static int hashCode(RecordStub recordInstance) {
-    return 31 * Arrays.hashCode(recordInstance.getFieldsAsObjects())
-        + recordInstance.getClass().hashCode();
-  }
-
-  public static boolean equals(RecordStub recordInstance, Object other) {
-    return recordInstance.getClass() == other.getClass()
-        && Arrays.equals(
-            ((RecordStub) other).getFieldsAsObjects(), recordInstance.getFieldsAsObjects());
-  }
-
-  public abstract static class RecordStub {
-    abstract Object[] getFieldsAsObjects();
+  public static int hashCode(Class<?> recordClass, Object[] recordFieldsAsObjects) {
+    return 31 * Arrays.hashCode(recordFieldsAsObjects) + recordClass.hashCode();
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordWithMembersTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordWithMembersTest.java
index c36375e..36b2377 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/RecordWithMembersTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordWithMembersTest.java
@@ -13,7 +13,6 @@
 import com.android.tools.r8.utils.StringUtils;
 import java.nio.file.Path;
 import java.util.List;
-import org.junit.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -65,23 +64,35 @@
   }
 
   @Test
-  public void testR8Cf() throws Exception {
-    Assume.assumeTrue(parameters.isCfRuntime());
-    Path output =
-        testForR8(parameters.getBackend())
-            .addProgramClassFileData(PROGRAM_DATA)
-            .setMinApi(parameters.getApiLevel())
-            .addKeepRules(RECORD_KEEP_RULE)
-            .addKeepMainRule(MAIN_TYPE)
-            .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
-            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
-            .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
-            .compile()
-            .writeToZip();
-    RecordTestUtils.assertRecordsAreRecords(output);
-    testForJvm()
-        .addRunClasspathFiles(output)
-        .enablePreview()
+  public void testR8() throws Exception {
+    if (parameters.isCfRuntime()) {
+      Path output =
+          testForR8(parameters.getBackend())
+              .addProgramClassFileData(PROGRAM_DATA)
+              .setMinApi(parameters.getApiLevel())
+              .addKeepRules(RECORD_KEEP_RULE)
+              .addKeepMainRule(MAIN_TYPE)
+              .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
+              .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+              .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
+              .compile()
+              .writeToZip();
+      RecordTestUtils.assertRecordsAreRecords(output);
+      testForJvm()
+          .addRunClasspathFiles(output)
+          .enablePreview()
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+      return;
+    }
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(PROGRAM_DATA)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepRules(RECORD_KEEP_RULE)
+        .addKeepMainRule(MAIN_TYPE)
+        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+        .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
+        .compile()
         .run(parameters.getRuntime(), MAIN_TYPE)
         .assertSuccessWithOutput(EXPECTED_RESULT);
   }
diff --git a/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java b/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java
index 51596a1..08c9468 100644
--- a/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/records/SimpleRecordTest.java
@@ -13,7 +13,6 @@
 import com.android.tools.r8.utils.StringUtils;
 import java.nio.file.Path;
 import java.util.List;
-import org.junit.Assume;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -64,23 +63,35 @@
   }
 
   @Test
-  public void testR8Cf() throws Exception {
-    Assume.assumeTrue(parameters.isCfRuntime());
-    Path output =
-        testForR8(parameters.getBackend())
-            .addProgramClassFileData(PROGRAM_DATA)
-            .setMinApi(parameters.getApiLevel())
-            .addKeepRules(RECORD_KEEP_RULE)
-            .addKeepMainRule(MAIN_TYPE)
-            .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
-            .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
-            .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
-            .compile()
-            .writeToZip();
-    RecordTestUtils.assertRecordsAreRecords(output);
-    testForJvm()
-        .addRunClasspathFiles(output)
-        .enablePreview()
+  public void testR8() throws Exception {
+    if (parameters.isCfRuntime()) {
+      Path output =
+          testForR8(parameters.getBackend())
+              .addProgramClassFileData(PROGRAM_DATA)
+              .setMinApi(parameters.getApiLevel())
+              .addKeepRules(RECORD_KEEP_RULE)
+              .addKeepMainRule(MAIN_TYPE)
+              .addLibraryFiles(RecordTestUtils.getJdk15LibraryFiles(temp))
+              .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+              .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
+              .compile()
+              .writeToZip();
+      RecordTestUtils.assertRecordsAreRecords(output);
+      testForJvm()
+          .addRunClasspathFiles(output)
+          .enablePreview()
+          .run(parameters.getRuntime(), MAIN_TYPE)
+          .assertSuccessWithOutput(EXPECTED_RESULT);
+      return;
+    }
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(PROGRAM_DATA)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepRules(RECORD_KEEP_RULE)
+        .addKeepMainRule(MAIN_TYPE)
+        .addOptionsModification(TestingOptions::allowExperimentClassFileVersion)
+        .addOptionsModification(opt -> opt.testing.enableExperimentalRecordDesugaring = true)
+        .compile()
         .run(parameters.getRuntime(), MAIN_TYPE)
         .assertSuccessWithOutput(EXPECTED_RESULT);
   }
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagationToClassesOutsideLowerBoundTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagationToClassesOutsideLowerBoundTest.java
new file mode 100644
index 0000000..1e2140c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/ArgumentPropagationToClassesOutsideLowerBoundTest.java
@@ -0,0 +1,130 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static junit.framework.TestCase.assertTrue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NoHorizontalClassMerging;
+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.HorizontallyMergedClassesInspector;
+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.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ArgumentPropagationToClassesOutsideLowerBoundTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              MethodSubject aMethodSubject = inspector.clazz(A.class).uniqueMethodWithName("m");
+              assertThat(aMethodSubject, isPresent());
+              assertTrue(
+                  aMethodSubject
+                      .streamInstructions()
+                      .anyMatch(instruction -> instruction.isConstString("A: Not null")));
+              assertTrue(
+                  aMethodSubject
+                      .streamInstructions()
+                      .anyMatch(instruction -> instruction.isConstString("A: Null")));
+
+              // TODO(b/190154391): B.m() is always called with non-null.
+              MethodSubject bMethodSubject = inspector.clazz(B.class).uniqueMethodWithName("m");
+              assertThat(bMethodSubject, isPresent());
+              assertTrue(
+                  bMethodSubject
+                      .streamInstructions()
+                      .anyMatch(instruction -> instruction.isConstString("B: Not null")));
+              assertTrue(
+                  bMethodSubject
+                      .streamInstructions()
+                      .anyMatch(instruction -> instruction.isConstString("B: Null")));
+
+              // TODO(b/190154391): C.m() is always called with null.
+              MethodSubject cMethodSubject = inspector.clazz(C.class).uniqueMethodWithName("m");
+              assertThat(cMethodSubject, isPresent());
+              assertTrue(
+                  cMethodSubject
+                      .streamInstructions()
+                      .anyMatch(instruction -> instruction.isConstString("C: Not null")));
+              assertTrue(
+                  cMethodSubject
+                      .streamInstructions()
+                      .anyMatch(instruction -> instruction.isConstString("C: Null")));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A: Not null", "A: Null");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      A aOrB = System.currentTimeMillis() > 0 ? new A() : new B();
+      aOrB.m(new Object());
+
+      A aOrC = System.currentTimeMillis() > 0 ? new A() : new C();
+      aOrC.m(null);
+    }
+  }
+
+  static class A {
+
+    void m(Object o) {
+      if (o != null) {
+        System.out.println("A: Not null");
+      } else {
+        System.out.println("A: Null");
+      }
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class B extends A {
+
+    void m(Object o) {
+      if (o != null) {
+        System.out.println("B: Not null");
+      } else {
+        System.out.println("B: Null");
+      }
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class C extends A {
+
+    void m(Object o) {
+      if (o != null) {
+        System.out.println("C: Not null");
+      } else {
+        System.out.println("C: Null");
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/ImpreciseReceiverWithUnknownArgumentInformationWidenedToUnknownTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/ImpreciseReceiverWithUnknownArgumentInformationWidenedToUnknownTest.java
new file mode 100644
index 0000000..36c74a1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/ImpreciseReceiverWithUnknownArgumentInformationWidenedToUnknownTest.java
@@ -0,0 +1,85 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.BooleanBox;
+import com.android.tools.r8.utils.codeinspector.VerticallyMergedClassesInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ImpreciseReceiverWithUnknownArgumentInformationWidenedToUnknownTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    BooleanBox inspected = new BooleanBox();
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addArgumentPropagatorCodeScannerResultInspector(
+            inspector ->
+                inspector
+                    .assertHasUnknownMethodState(
+                        Reference.methodFromMethod(A.class.getDeclaredMethod("test")))
+                    .assertHasBottomMethodState(
+                        Reference.methodFromMethod(B.class.getDeclaredMethod("test")))
+                    .apply(ignore -> inspected.set()))
+        .addOptionsModification(
+            options ->
+                options
+                    .callSiteOptimizationOptions()
+                    .setEnableExperimentalArgumentPropagation(true))
+        .addVerticallyMergedClassesInspector(
+            VerticallyMergedClassesInspector::assertNoClassesMerged)
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A");
+    assertTrue(inspected.isTrue());
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      A aOrB = System.currentTimeMillis() >= 0 ? new A() : new B();
+      aOrB.test();
+    }
+  }
+
+  @NoVerticalClassMerging
+  static class A {
+
+    void test() {
+      System.out.println("A");
+    }
+  }
+
+  static class B extends A {
+
+    @Override
+    void test() {
+      System.out.println("B");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/MonomorphicVirtualMethodTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/MonomorphicVirtualMethodTest.java
new file mode 100644
index 0000000..74ba4e2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/MonomorphicVirtualMethodTest.java
@@ -0,0 +1,74 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.BooleanBox;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class MonomorphicVirtualMethodTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    BooleanBox inspected = new BooleanBox();
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addArgumentPropagatorCodeScannerResultInspector(
+            inspector ->
+                inspector
+                    .assertHasMonomorphicMethodState(
+                        Reference.methodFromMethod(A.class.getDeclaredMethod("m", int.class)))
+                    .apply(ignore -> inspected.set()))
+        .addOptionsModification(
+            options ->
+                options
+                    .callSiteOptimizationOptions()
+                    .setEnableExperimentalArgumentPropagation(true))
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("42");
+    assertTrue(inspected.isTrue());
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new A().m(42);
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+
+    @NeverInline
+    void m(int x) {
+      System.out.println(x);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/MonomorphicVirtualMethodWithInterfaceMethodSiblingTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/MonomorphicVirtualMethodWithInterfaceMethodSiblingTest.java
new file mode 100644
index 0000000..8731617
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/MonomorphicVirtualMethodWithInterfaceMethodSiblingTest.java
@@ -0,0 +1,115 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.BooleanBox;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
+import com.android.tools.r8.utils.codeinspector.VerticallyMergedClassesInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class MonomorphicVirtualMethodWithInterfaceMethodSiblingTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    BooleanBox inspected = new BooleanBox();
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addArgumentPropagatorCodeScannerResultInspector(
+            inspector ->
+                inspector
+                    .assertHasPolymorphicMethodState(
+                        Reference.methodFromMethod(I.class.getDeclaredMethod("m", int.class)))
+                    .assertHasMonomorphicMethodState(
+                        Reference.methodFromMethod(A.class.getDeclaredMethod("m", int.class)))
+                    .assertHasBottomMethodState(
+                        Reference.methodFromMethod(C.class.getDeclaredMethod("m", int.class)))
+                    .apply(ignore -> inspected.set()))
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .addOptionsModification(
+            options ->
+                options
+                    .callSiteOptimizationOptions()
+                    .setEnableExperimentalArgumentPropagation(true))
+        .addVerticallyMergedClassesInspector(
+            VerticallyMergedClassesInspector::assertNoClassesMerged)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A: 42", "A: 42");
+    assertTrue(inspected.isTrue());
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      // Since A.m() does not override any methods (ignoring siblings) and do not have any overrides
+      // this invoke leads to a monomorphic method state for A.m().
+      A a = new A();
+      a.m(42);
+
+      // This invoke leads to a polymorphic method state for I.m(). When we propagate this
+      // information to A.m() in the interface method propagation phase, we need to transform the
+      // polymorphic method state into a monomorphic method state.
+      I i = System.currentTimeMillis() >= 0 ? new B() : new C();
+      i.m(42);
+    }
+  }
+
+  interface I {
+
+    void m(int x);
+  }
+
+  @NeverClassInline
+  @NoHorizontalClassMerging
+  @NoVerticalClassMerging
+  static class A {
+
+    @NeverInline
+    public void m(int x) {
+      System.out.println("A: " + x);
+    }
+  }
+
+  static class B extends A implements I {}
+
+  @NoHorizontalClassMerging
+  static class C implements I {
+
+    @Override
+    public void m(int x) {
+      System.out.println("C: " + x);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/ParameterWithUnknownArgumentInformationWidenedToUnknownTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/ParameterWithUnknownArgumentInformationWidenedToUnknownTest.java
new file mode 100644
index 0000000..ade398c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/ParameterWithUnknownArgumentInformationWidenedToUnknownTest.java
@@ -0,0 +1,79 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation;
+
+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.references.Reference;
+import com.android.tools.r8.utils.BooleanBox;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ParameterWithUnknownArgumentInformationWidenedToUnknownTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    BooleanBox inspected = new BooleanBox();
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addArgumentPropagatorCodeScannerResultInspector(
+            inspector ->
+                inspector
+                    .assertHasUnknownMethodState(
+                        Reference.methodFromMethod(Main.class.getDeclaredMethod("test", A.class)))
+                    .apply(ignore -> inspected.set()))
+        .addOptionsModification(
+            options ->
+                options
+                    .callSiteOptimizationOptions()
+                    .setEnableExperimentalArgumentPropagation(true))
+        .enableInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(null, "A");
+    assertTrue(inspected.isTrue());
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      A alwaysNull = null;
+      A neverNull = new A();
+      test(alwaysNull);
+      test(neverNull);
+    }
+
+    @NeverInline
+    static void test(A a) {
+      System.out.println(a);
+    }
+  }
+
+  static class A {
+
+    @Override
+    public String toString() {
+      return "A";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/UpwardsInterfacePropagationToUnrelatedMethodTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/UpwardsInterfacePropagationToUnrelatedMethodTest.java
new file mode 100644
index 0000000..03d63d9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/UpwardsInterfacePropagationToUnrelatedMethodTest.java
@@ -0,0 +1,147 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static junit.framework.TestCase.assertTrue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NoHorizontalClassMerging;
+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.HorizontallyMergedClassesInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.android.tools.r8.utils.codeinspector.VerticallyMergedClassesInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class UpwardsInterfacePropagationToUnrelatedMethodTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options ->
+                options
+                    .callSiteOptimizationOptions()
+                    .setEnableExperimentalArgumentPropagation(true))
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .addVerticallyMergedClassesInspector(
+            VerticallyMergedClassesInspector::assertNoClassesMerged)
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              MethodSubject aMethodSubject = inspector.clazz(A.class).uniqueMethodWithName("m");
+              assertThat(aMethodSubject, isPresent());
+              assertTrue(
+                  aMethodSubject
+                      .streamInstructions()
+                      .anyMatch(instruction -> instruction.isConstString("A: Not null")));
+              assertTrue(
+                  aMethodSubject
+                      .streamInstructions()
+                      .anyMatch(instruction -> instruction.isConstString("A: Null")));
+
+              MethodSubject b2MethodSubject = inspector.clazz(B2.class).uniqueMethodWithName("m");
+              assertThat(b2MethodSubject, isPresent());
+              assertTrue(
+                  b2MethodSubject
+                      .streamInstructions()
+                      .anyMatch(instruction -> instruction.isConstString("B2: Not null")));
+              assertTrue(
+                  b2MethodSubject
+                      .streamInstructions()
+                      .noneMatch(instruction -> instruction.isConstString("B2: Null")));
+
+              MethodSubject b3MethodSubject = inspector.clazz(B3.class).uniqueMethodWithName("m");
+              assertThat(b3MethodSubject, isPresent());
+              assertTrue(
+                  b3MethodSubject
+                      .streamInstructions()
+                      .noneMatch(instruction -> instruction.isConstString("B3: Not null")));
+              assertTrue(
+                  b3MethodSubject
+                      .streamInstructions()
+                      .anyMatch(instruction -> instruction.isConstString("B3: Null")));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A: Not null", "A: Null");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      // Call A.m() or B2.m() with a non-null object.
+      A a = System.currentTimeMillis() >= 0 ? new B() : new B2();
+      a.m(new Object());
+
+      // Call A.m() or B3.m() with a null object.
+      I i = System.currentTimeMillis() >= 0 ? new B() : new B3();
+      i.m(null);
+    }
+  }
+
+  interface I {
+
+    void m(Object o);
+  }
+
+  static class A {
+
+    public void m(Object o) {
+      if (o != null) {
+        System.out.println("A: Not null");
+      } else {
+        System.out.println("A: Null");
+      }
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class B extends A implements I {}
+
+  @NoHorizontalClassMerging
+  static class B2 extends A {
+
+    @Override
+    public void m(Object o) {
+      if (o != null) {
+        System.out.println("B2: Not null");
+      } else {
+        System.out.println("B2: Null");
+      }
+    }
+  }
+
+  static class B3 implements I {
+
+    @Override
+    public void m(Object o) {
+      if (o != null) {
+        System.out.println("B3: Not null");
+      } else {
+        System.out.println("B3: Null");
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/optimize/argumentpropagation/VirtualMethodWithConstantArgumentThroughSiblingInterfaceMethodTest.java b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/VirtualMethodWithConstantArgumentThroughSiblingInterfaceMethodTest.java
new file mode 100644
index 0000000..7be4823
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/optimize/argumentpropagation/VirtualMethodWithConstantArgumentThroughSiblingInterfaceMethodTest.java
@@ -0,0 +1,102 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.optimize.argumentpropagation;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static junit.framework.TestCase.assertTrue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NoVerticalClassMerging;
+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.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.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class VirtualMethodWithConstantArgumentThroughSiblingInterfaceMethodTest extends TestBase {
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options ->
+                options
+                    .callSiteOptimizationOptions()
+                    .setEnableExperimentalArgumentPropagation(true))
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              // The A.m() and C.m() methods have been optimized.
+              for (Class<?> clazz : new Class[] {A.class, C.class}) {
+                ClassSubject aClassSubject = inspector.clazz(clazz);
+                assertThat(aClassSubject, isPresent());
+                MethodSubject aMethodSubject = aClassSubject.uniqueMethodWithName("m");
+                assertThat(aMethodSubject, isPresent());
+                assertTrue(aMethodSubject.streamInstructions().noneMatch(InstructionSubject::isIf));
+              }
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A: Not null");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      I i = System.currentTimeMillis() > 0 ? new B() : new C();
+      i.m(new Object());
+    }
+  }
+
+  @NoVerticalClassMerging
+  interface I {
+
+    void m(Object o);
+  }
+
+  @NoVerticalClassMerging
+  static class A {
+
+    public void m(Object o) {
+      if (o != null) {
+        System.out.println("A: Not null");
+      } else {
+        System.out.println("A: Null");
+      }
+    }
+  }
+
+  static class B extends A implements I {}
+
+  static class C implements I {
+
+    @Override
+    public void m(Object o) {
+      if (o != null) {
+        System.out.println("C: Not null");
+      } else {
+        System.out.println("C: Null");
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageCloneTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageCloneTest.java
new file mode 100644
index 0000000..722a9cc
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageCloneTest.java
@@ -0,0 +1,85 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.repackage;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class RepackageCloneTest extends RepackageTestBase {
+
+  public RepackageCloneTest(
+      String flattenPackageHierarchyOrRepackageClasses, TestParameters parameters) {
+    super(flattenPackageHierarchyOrRepackageClasses, parameters);
+  }
+
+  @Test
+  public void testClone() throws Exception {
+    testForR8Compat(parameters.getBackend())
+        .addProgramClasses(A.class, B.class, C.class, Main.class)
+        .addKeepMainRule(Main.class)
+        .addKeepClassRulesWithAllowObfuscation(A.class)
+        // Ensure we keep values() which has a call to clone.
+        .addKeepRules("-keepclassmembers class " + typeName(A.class) + " { *; }")
+        .apply(this::configureRepackaging)
+        .setMinApi(parameters.getApiLevel())
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .compile()
+        .inspect(
+            inspector -> {
+              assertThat(A.class, isRepackaged(inspector));
+              assertThat(B.class, isRepackaged(inspector));
+              assertThat(C.class, isRepackaged(inspector));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("foo", "null");
+  }
+
+  public enum A {
+    foo;
+  }
+
+  @NoHorizontalClassMerging
+  public static class B {
+
+    @NeverInline
+    public static void foo() {
+      try {
+        new B().clone();
+      } catch (CloneNotSupportedException e) {
+        e.printStackTrace();
+      }
+    }
+  }
+
+  @NoHorizontalClassMerging
+  public static class C {
+
+    @NeverInline
+    public static void foo() {
+      System.out.println(new A[10].clone()[1]);
+      ;
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(A.foo);
+      B.foo();
+      C.foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/resolution/interfacetargets/InvokeInterfaceClInitTest.java b/src/test/java/com/android/tools/r8/resolution/interfacetargets/InvokeInterfaceClInitTest.java
deleted file mode 100644
index b15d446..0000000
--- a/src/test/java/com/android/tools/r8/resolution/interfacetargets/InvokeInterfaceClInitTest.java
+++ /dev/null
@@ -1,156 +0,0 @@
-// Copyright (c) 2020, 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.resolution.interfacetargets;
-
-import static org.hamcrest.core.StringContains.containsString;
-import static org.junit.Assume.assumeTrue;
-
-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 com.android.tools.r8.TestRuntime.CfVm;
-import com.android.tools.r8.TestRuntime.DexRuntime;
-import com.android.tools.r8.ToolHelper.DexVm;
-import com.android.tools.r8.dex.Constants;
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.transformers.ClassTransformer;
-import java.io.IOException;
-import java.util.concurrent.ExecutionException;
-import org.hamcrest.CoreMatchers;
-import org.hamcrest.Matcher;
-import org.junit.Assert;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
-import org.junit.runners.Parameterized.Parameters;
-import org.objectweb.asm.MethodVisitor;
-
-@RunWith(Parameterized.class)
-public class InvokeInterfaceClInitTest extends TestBase {
-
-  private final TestParameters parameters;
-
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
-  }
-
-  public InvokeInterfaceClInitTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
-  @Test
-  public void testResolution() throws Exception {
-    assumeTrue(parameters.useRuntimeAsNoneRuntime());
-    AppView<AppInfoWithLiveness> appView =
-        computeAppViewWithLiveness(
-            buildClasses(A.class, B.class)
-                .addClassProgramData(transformI(), transformMain())
-                .addLibraryFile(parameters.getDefaultRuntimeLibrary())
-                .build(),
-            Main.class);
-    AppInfoWithLiveness appInfo = appView.appInfo();
-    DexMethod method = buildNullaryVoidMethod(I.class, "<clinit>", appInfo.dexItemFactory());
-    DexProgramClass context =
-        appView.definitionForProgramType(buildType(Main.class, appInfo.dexItemFactory()));
-    Assert.assertThrows(
-        AssertionError.class,
-        () ->
-            appInfo
-                .resolveMethodOnInterface(method)
-                .lookupVirtualDispatchTargets(context, appInfo));
-  }
-
-  private Matcher<String> getExpected() {
-    if (parameters.getRuntime().isCf()) {
-      Matcher<String> expected = containsString("java.lang.VerifyError");
-      // JDK 9 and 11 output VerifyError or ClassFormatError non-deterministically.
-      if (parameters.getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK9)) {
-        expected = CoreMatchers.anyOf(expected, containsString("java.lang.ClassFormatError"));
-      }
-      return expected;
-    }
-    assert parameters.getRuntime().isDex();
-    DexRuntime dexRuntime = parameters.getRuntime().asDex();
-    if (dexRuntime.getVm().isOlderThanOrEqual(DexVm.ART_4_4_4_TARGET)) {
-      return containsString("NoSuchMethodError");
-    }
-    return containsString("java.lang.VerifyError");
-  }
-
-  @Test
-  public void testRuntimeClInit()
-      throws IOException, CompilationFailedException, ExecutionException {
-    testForRuntime(parameters)
-        .addProgramClasses(A.class, B.class)
-        .addProgramClassFileData(transformMain(), transformI())
-        .run(parameters.getRuntime(), Main.class)
-        .assertFailureWithErrorThatMatches(getExpected());
-  }
-
-  @Test
-  public void testR8ClInit() throws IOException, CompilationFailedException, ExecutionException {
-    testForR8(parameters.getBackend())
-        .addProgramClasses(A.class, B.class)
-        .addProgramClassFileData(transformMain(), transformI())
-        .addKeepMainRule(Main.class)
-        .setMinApi(parameters.getApiLevel())
-        .run(parameters.getRuntime(), Main.class)
-        .assertFailureWithErrorThatMatches(getExpected());
-  }
-
-  private byte[] transformI() throws IOException {
-    return transformer(I.class)
-        .addClassTransformer(
-            new ClassTransformer() {
-              @Override
-              public MethodVisitor visitMethod(
-                  int access,
-                  String name,
-                  String descriptor,
-                  String signature,
-                  String[] exceptions) {
-                return super.visitMethod(
-                    access | Constants.ACC_STATIC, "<clinit>", descriptor, signature, exceptions);
-              }
-            })
-        .transform();
-  }
-
-  private byte[] transformMain() throws IOException {
-    return transformer(Main.class)
-        .transformMethodInsnInMethod(
-            "callClInit",
-            (opcode, owner, name, descriptor, isInterface, continuation) ->
-                continuation.visitMethodInsn(opcode, owner, "<clinit>", descriptor, isInterface))
-        .transform();
-  }
-
-  public interface I {
-
-    default void foo() { // <-- will be rewritten to <clinit>
-      System.out.println("I.foo");
-    }
-  }
-
-  public static class A implements I {}
-
-  public static class B implements I {}
-
-  public static class Main {
-
-    public static void main(String[] args) {
-      callClInit(args.length == 0 ? new A() : new B());
-    }
-
-    private static void callClInit(I i) {
-      i.foo(); // <-- will be i.<clinit>()
-    }
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
index ea7b6cc..2bbcf4a 100644
--- a/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
+++ b/src/test/java/com/android/tools/r8/transformers/ClassFileTransformer.java
@@ -38,10 +38,12 @@
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.ClassVisitor;
 import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.ConstantDynamic;
 import org.objectweb.asm.FieldVisitor;
 import org.objectweb.asm.Handle;
 import org.objectweb.asm.Label;
 import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
 import org.objectweb.asm.Type;
 
 public class ClassFileTransformer {
@@ -890,6 +892,37 @@
         });
   }
 
+  public ClassFileTransformer transformConstStringToConstantDynamic(
+      String constantName,
+      Class<?> bootstrapMethodHolder,
+      String bootstrapMethodName,
+      String name,
+      Class<?> type) {
+    return addMethodTransformer(
+        new MethodTransformer() {
+          @Override
+          public void visitLdcInsn(Object value) {
+            String bootstrapMethodSignature =
+                "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;";
+            if (value instanceof String && value.equals(constantName)) {
+              super.visitLdcInsn(
+                  new ConstantDynamic(
+                      name,
+                      Reference.classFromClass(type).getDescriptor(),
+                      new Handle(
+                          Opcodes.H_INVOKESTATIC,
+                          DescriptorUtils.getClassBinaryName(bootstrapMethodHolder),
+                          bootstrapMethodName,
+                          bootstrapMethodSignature,
+                          false),
+                      new Object[] {}));
+            } else {
+              super.visitLdcInsn(value);
+            }
+          }
+        });
+  }
+
   @FunctionalInterface
   private interface VisitMethodInsnCallback {
     void visitMethodInsn(
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/ArgumentPropagatorCodeScannerResultInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/ArgumentPropagatorCodeScannerResultInspector.java
new file mode 100644
index 0000000..7fe9502
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/ArgumentPropagatorCodeScannerResultInspector.java
@@ -0,0 +1,82 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.utils.codeinspector;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.ThrowableConsumer;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodState;
+import com.android.tools.r8.optimize.argumentpropagation.codescanner.MethodStateCollectionByReference;
+import com.android.tools.r8.references.MethodReference;
+import com.android.tools.r8.utils.MethodReferenceUtils;
+import java.util.function.Predicate;
+
+public class ArgumentPropagatorCodeScannerResultInspector {
+
+  private final DexItemFactory dexItemFactory;
+  private final MethodStateCollectionByReference methodStates;
+
+  public ArgumentPropagatorCodeScannerResultInspector(
+      DexItemFactory dexItemFactory, MethodStateCollectionByReference methodStates) {
+    this.dexItemFactory = dexItemFactory;
+    this.methodStates = methodStates;
+  }
+
+  public ArgumentPropagatorCodeScannerResultInspector apply(
+      ThrowableConsumer<ArgumentPropagatorCodeScannerResultInspector> consumer) {
+    consumer.acceptWithRuntimeException(this);
+    return this;
+  }
+
+  public ArgumentPropagatorCodeScannerResultInspector assertHasBottomMethodState(
+      MethodReference methodReference) {
+    return assertHasMethodStateThatMatches(
+        "Expected method state for "
+            + MethodReferenceUtils.toSourceString(methodReference)
+            + " to be bottom",
+        methodReference,
+        MethodState::isBottom);
+  }
+
+  public ArgumentPropagatorCodeScannerResultInspector assertHasMonomorphicMethodState(
+      MethodReference methodReference) {
+    return assertHasMethodStateThatMatches(
+        "Expected method state for "
+            + MethodReferenceUtils.toSourceString(methodReference)
+            + " to be monomorphic",
+        methodReference,
+        MethodState::isMonomorphic);
+  }
+
+  public ArgumentPropagatorCodeScannerResultInspector assertHasPolymorphicMethodState(
+      MethodReference methodReference) {
+    return assertHasMethodStateThatMatches(
+        "Expected method state for "
+            + MethodReferenceUtils.toSourceString(methodReference)
+            + " to be polymorphic",
+        methodReference,
+        MethodState::isPolymorphic);
+  }
+
+  public ArgumentPropagatorCodeScannerResultInspector assertHasUnknownMethodState(
+      MethodReference methodReference) {
+    return assertHasMethodStateThatMatches(
+        "Expected method state for "
+            + MethodReferenceUtils.toSourceString(methodReference)
+            + " to be unknown",
+        methodReference,
+        MethodState::isUnknown);
+  }
+
+  public ArgumentPropagatorCodeScannerResultInspector assertHasMethodStateThatMatches(
+      String message, MethodReference methodReference, Predicate<MethodState> predicate) {
+    DexMethod method = MethodReferenceUtils.toDexMethod(methodReference, dexItemFactory);
+    MethodState methodState = methodStates.get(method);
+    assertTrue(message + ", was: " + methodState, predicate.test(methodState));
+    return this;
+  }
+}
diff --git a/tools/youtube_data.py b/tools/youtube_data.py
index 64bc995..34426c0 100644
--- a/tools/youtube_data.py
+++ b/tools/youtube_data.py
@@ -125,10 +125,10 @@
 def GetMemoryData(version):
   assert version == '16.20'
   return {
-      'find-xmx-min': 2800,
-      'find-xmx-max': 3200,
+      'find-xmx-min': 3150,
+      'find-xmx-max': 3300,
       'find-xmx-range': 64,
-      'oom-threshold': 3000,
+      'oom-threshold': 3100,
       # TODO(b/143431825): Youtube can OOM randomly in memory configurations
       #  that should work.
       'skip-find-xmx-max': True,