Merge commit '38eb13a53d863b5d18cee58605bb2295b6aeb547' into dev-release
diff --git a/src/main/java/com/android/tools/r8/BackportedMethodListCommand.java b/src/main/java/com/android/tools/r8/BackportedMethodListCommand.java
index a4af022..c856d87 100644
--- a/src/main/java/com/android/tools/r8/BackportedMethodListCommand.java
+++ b/src/main/java/com/android/tools/r8/BackportedMethodListCommand.java
@@ -104,7 +104,7 @@
 
   InternalOptions getInternalOptions() {
     InternalOptions options = new InternalOptions(factory, getReporter());
-    options.minApiLevel = minApiLevel;
+    options.minApiLevel = AndroidApiLevel.getAndroidApiLevel(minApiLevel);
     options.desugaredLibraryConfiguration = desugaredLibraryConfiguration;
     return options;
   }
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 8863e35..1ef4a6e 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -458,7 +458,7 @@
     internal.mainDexListConsumer = getMainDexListConsumer();
     internal.minimalMainDex = internal.debug || minimalMainDex;
     internal.enableMainDexListCheck = enableMainDexListCheck;
-    internal.minApiLevel = getMinApiLevel();
+    internal.minApiLevel = AndroidApiLevel.getAndroidApiLevel(getMinApiLevel());
     internal.intermediate = intermediate;
     internal.readCompileTimeAnnotations = intermediate;
     internal.desugarGraphConsumer = desugarGraphConsumer;
diff --git a/src/main/java/com/android/tools/r8/ExtractMarker.java b/src/main/java/com/android/tools/r8/ExtractMarker.java
index 9492e12..b21bb03 100644
--- a/src/main/java/com/android/tools/r8/ExtractMarker.java
+++ b/src/main/java/com/android/tools/r8/ExtractMarker.java
@@ -98,11 +98,10 @@
     }
   }
 
-  private static Collection<Marker> extractMarker(AndroidApp app)
-      throws IOException, ExecutionException {
+  private static Collection<Marker> extractMarker(AndroidApp app) throws IOException {
     InternalOptions options = new InternalOptions();
     options.skipReadingDexCode = true;
-    options.minApiLevel = AndroidApiLevel.P.getLevel();
+    options.minApiLevel = AndroidApiLevel.P;
     DexApplication dexApp = new ApplicationReader(app, options, new Timing("ExtractMarker")).read();
     return dexApp.dexItemFactory.extractMarkers();
   }
diff --git a/src/main/java/com/android/tools/r8/L8Command.java b/src/main/java/com/android/tools/r8/L8Command.java
index fc9a567..9ec502c 100644
--- a/src/main/java/com/android/tools/r8/L8Command.java
+++ b/src/main/java/com/android/tools/r8/L8Command.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.inspector.Inspector;
 import com.android.tools.r8.ir.desugar.DesugaredLibraryConfiguration;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.AssertionConfigurationWithDefault;
 import com.android.tools.r8.utils.DumpInputFlags;
@@ -160,7 +161,7 @@
     internal.debug = getMode() == CompilationMode.DEBUG;
     assert internal.mainDexListConsumer == null;
     assert !internal.minimalMainDex;
-    internal.minApiLevel = getMinApiLevel();
+    internal.minApiLevel = AndroidApiLevel.getAndroidApiLevel(getMinApiLevel());
     assert !internal.intermediate;
     assert internal.readCompileTimeAnnotations;
     internal.programConsumer = getProgramConsumer();
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 7aed2af..1a78e63 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -33,7 +33,8 @@
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
-import com.android.tools.r8.graph.GenericSignatureTypeVariableRemover;
+import com.android.tools.r8.graph.GenericSignatureContextBuilder;
+import com.android.tools.r8.graph.GenericSignatureCorrectnessHelper;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.InitClassLens;
 import com.android.tools.r8.graph.PrunedItems;
@@ -105,7 +106,6 @@
 import com.android.tools.r8.shaking.WhyAreYouKeepingConsumer;
 import com.android.tools.r8.synthesis.SyntheticFinalization;
 import com.android.tools.r8.synthesis.SyntheticItems;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.CollectionUtils;
@@ -332,7 +332,7 @@
               options.itemFactory, options.getProguardConfiguration().getRules())) {
             synthesizedProguardRules.add(
                 ProguardConfigurationUtils.buildAssumeNoSideEffectsRuleForApiLevel(
-                    options.itemFactory, AndroidApiLevel.getAndroidApiLevel(options.minApiLevel)));
+                    options.itemFactory, options.minApiLevel));
           }
         }
         SubtypingInfo subtypingInfo = new SubtypingInfo(appView);
@@ -377,9 +377,14 @@
           appView.withGeneratedExtensionRegistryShrinker(
               shrinker -> shrinker.run(Mode.INITIAL_TREE_SHAKING));
 
-          // Build enclosing information and type-paramter information before pruning.
-          GenericSignatureTypeVariableRemover typeVariableRemover =
-              GenericSignatureTypeVariableRemover.create(appView, appView.appInfo().classes());
+          // Build enclosing information and type-parameter information before pruning.
+          // TODO(b/187922482): Only consider referenced classes.
+          GenericSignatureContextBuilder genericContextBuilder =
+              GenericSignatureContextBuilder.create(appView.appInfo().classes());
+
+          // Compute if all signatures are valid before modifying them.
+          GenericSignatureCorrectnessHelper.createForInitialCheck(appView, genericContextBuilder)
+              .run(appView.appInfo().classes());
 
           TreePruner pruner = new TreePruner(appViewWithLiveness);
           DirectMappedDexApplication prunedApp = pruner.run(executorService);
@@ -404,8 +409,7 @@
               annotationRemoverBuilder
                   .build(appViewWithLiveness, removedClasses);
           annotationRemover.ensureValid().run();
-          typeVariableRemover.removeDeadGenericSignatureTypeVariables(appView);
-          new GenericSignatureRewriter(appView, NamingLens.getIdentityLens())
+          new GenericSignatureRewriter(appView, NamingLens.getIdentityLens(), genericContextBuilder)
               .run(appView.appInfo().classes(), executorService);
         }
       } finally {
@@ -505,7 +509,7 @@
         }
 
         HorizontalClassMerger.createForInitialClassMerging(appViewWithLiveness)
-            .runIfNecessary(runtimeTypeCheckInfo, timing);
+            .runIfNecessary(runtimeTypeCheckInfo, executorService, timing);
       }
 
       // Clear traced methods roots to not hold on to the main dex live method set.
@@ -597,8 +601,8 @@
                     shrinker -> shrinker.run(enqueuer.getMode()),
                     DefaultTreePrunerConfiguration.getInstance());
 
-            GenericSignatureTypeVariableRemover typeVariableRemover =
-                GenericSignatureTypeVariableRemover.create(appView, appView.appInfo().classes());
+            GenericSignatureContextBuilder genericContextBuilder =
+                GenericSignatureContextBuilder.create(appView.appInfo().classes());
 
             TreePruner pruner = new TreePruner(appViewWithLiveness, treePrunerConfiguration);
             DirectMappedDexApplication application = pruner.run(executorService);
@@ -639,7 +643,10 @@
             AnnotationRemover.builder()
                 .build(appView.withLiveness(), removedClasses)
                 .run();
-            typeVariableRemover.removeDeadGenericSignatureTypeVariables(appView);
+            new GenericSignatureRewriter(
+                    appView, NamingLens.getIdentityLens(), genericContextBuilder)
+                .run(appView.appInfo().classes(), executorService);
+
             // Synthesize fields for triggering class initializers.
             new ClassInitFieldSynthesizer(appViewWithLiveness).run(executorService);
           }
@@ -714,6 +721,10 @@
         SyntheticFinalization.finalizeWithClassHierarchy(appView);
       }
 
+      // Clear the reference type lattice element cache. This is required since class merging may
+      // need to build IR.
+      appView.dexItemFactory().clearTypeElementsCache();
+
       // Run horizontal class merging. This runs even if shrinking is disabled to ensure synthetics
       // are always merged.
       HorizontalClassMerger.createForFinalClassMerging(appView)
@@ -721,6 +732,7 @@
               classMergingEnqueuerExtensionBuilder != null
                   ? classMergingEnqueuerExtensionBuilder.build(appView.graphLens())
                   : null,
+              executorService,
               timing);
 
       // Perform minification.
@@ -787,6 +799,16 @@
         options.syntheticProguardRulesConsumer.accept(synthesizedProguardRules);
       }
 
+      assert appView.checkForTesting(
+              () ->
+                  !options.isShrinking()
+                      || GenericSignatureCorrectnessHelper.createForVerification(
+                              appView,
+                              GenericSignatureContextBuilder.create(appView.appInfo().classes()))
+                          .run(appView.appInfo().classes())
+                          .isValid())
+          : "Could not validate generic signatures";
+
       NamingLens prefixRewritingNamingLens =
           PrefixRewritingNamingLens.createPrefixRewritingNamingLens(appView, namingLens);
 
@@ -892,31 +914,27 @@
   private static boolean verifyMovedMethodsHaveOriginalMethodPosition(
       AppView<?> appView, DirectMappedDexApplication application) {
     application
-        .classes()
+        .classesWithDeterministicOrder()
         .forEach(
-            clazz -> {
-              clazz.forEachProgramMethod(
-                  method -> {
-                    DexMethod originalMethod =
-                        appView.graphLens().getOriginalMethodSignature(method.getReference());
-                    if (originalMethod != method.getReference()) {
-                      DexMethod originalMethod2 =
+            clazz ->
+                clazz.forEachProgramMethod(
+                    method -> {
+                      DexMethod originalMethod =
                           appView.graphLens().getOriginalMethodSignature(method.getReference());
-                      appView.graphLens().getOriginalMethodSignature(method.getReference());
-                      DexEncodedMethod definition = method.getDefinition();
-                      Code code = definition.getCode();
-                      if (code == null) {
-                        return;
+                      if (originalMethod != method.getReference()) {
+                        DexEncodedMethod definition = method.getDefinition();
+                        Code code = definition.getCode();
+                        if (code == null) {
+                          return;
+                        }
+                        if (code.isCfCode()) {
+                          assert verifyOriginalMethodInPosition(code.asCfCode(), originalMethod);
+                        } else {
+                          assert code.isDexCode();
+                          assert verifyOriginalMethodInDebugInfo(code.asDexCode(), originalMethod);
+                        }
                       }
-                      if (code.isCfCode()) {
-                        assert verifyOriginalMethodInPosition(code.asCfCode(), originalMethod);
-                      } else {
-                        assert code.isDexCode();
-                        assert verifyOriginalMethodInDebugInfo(code.asDexCode(), originalMethod);
-                      }
-                    }
-                  });
-            });
+                    }));
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 4fdb44d..7bb927c 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -833,7 +833,7 @@
     assert !internal.debug;
     internal.debug = getMode() == CompilationMode.DEBUG;
     internal.programConsumer = getProgramConsumer();
-    internal.minApiLevel = getMinApiLevel();
+    internal.minApiLevel = AndroidApiLevel.getAndroidApiLevel(getMinApiLevel());
     internal.desugarState = getDesugarState();
     assert internal.isShrinking() == getEnableTreeShaking();
     assert internal.isMinifying() == getEnableMinification();
diff --git a/src/main/java/com/android/tools/r8/androidapi/AvailableApiExceptions.java b/src/main/java/com/android/tools/r8/androidapi/AvailableApiExceptions.java
index c44f1f4..606d653 100644
--- a/src/main/java/com/android/tools/r8/androidapi/AvailableApiExceptions.java
+++ b/src/main/java/com/android/tools/r8/androidapi/AvailableApiExceptions.java
@@ -26,7 +26,7 @@
   private final Set<DexType> exceptions;
 
   public AvailableApiExceptions(InternalOptions options) {
-    assert options.minApiLevel < AndroidApiLevel.L.getLevel();
+    assert options.minApiLevel.isLessThan(AndroidApiLevel.L);
     exceptions = build(options.itemFactory, options.minApiLevel);
   }
 
@@ -35,9 +35,9 @@
   }
 
   /** The content of this method can be regenerated with GenerateAvailableExceptions.main. */
-  public static Set<DexType> build(DexItemFactory factory, int minApiLevel) {
+  public static Set<DexType> build(DexItemFactory factory, AndroidApiLevel minApiLevel) {
     Set<DexType> types = SetUtils.newIdentityHashSet(333);
-    if (minApiLevel >= 1) {
+    if (minApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.B)) {
       types.add(factory.createType("Landroid/app/PendingIntent$CanceledException;"));
       types.add(factory.createType("Landroid/content/ActivityNotFoundException;"));
       types.add(factory.createType("Landroid/content/IntentFilter$MalformedMimeTypeException;"));
@@ -305,17 +305,17 @@
       types.add(factory.createType("Lorg/xml/sax/SAXParseException;"));
       types.add(factory.createType("Lorg/xmlpull/v1/XmlPullParserException;"));
     }
-    if (minApiLevel >= 4) {
+    if (minApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.D)) {
       types.add(factory.createType("Landroid/content/IntentSender$SendIntentException;"));
     }
-    if (minApiLevel >= 5) {
+    if (minApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.E)) {
       types.add(factory.createType("Landroid/accounts/AccountsException;"));
       types.add(factory.createType("Landroid/accounts/AuthenticatorException;"));
       types.add(factory.createType("Landroid/accounts/NetworkErrorException;"));
       types.add(factory.createType("Landroid/accounts/OperationCanceledException;"));
       types.add(factory.createType("Landroid/content/OperationApplicationException;"));
     }
-    if (minApiLevel >= 8) {
+    if (minApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.F)) {
       types.add(factory.createType("Ljavax/xml/datatype/DatatypeConfigurationException;"));
       types.add(factory.createType("Ljavax/xml/transform/TransformerConfigurationException;"));
       types.add(factory.createType("Ljavax/xml/transform/TransformerException;"));
@@ -326,7 +326,7 @@
       types.add(factory.createType("Ljavax/xml/xpath/XPathFunctionException;"));
       types.add(factory.createType("Lorg/w3c/dom/ls/LSException;"));
     }
-    if (minApiLevel >= 9) {
+    if (minApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.G)) {
       types.add(factory.createType("Landroid/net/sip/SipException;"));
       types.add(factory.createType("Landroid/nfc/FormatException;"));
       types.add(factory.createType("Ljava/io/IOError;"));
@@ -346,10 +346,10 @@
       types.add(factory.createType("Ljava/util/ServiceConfigurationError;"));
       types.add(factory.createType("Ljava/util/zip/ZipError;"));
     }
-    if (minApiLevel >= 10) {
+    if (minApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.G_MR1)) {
       types.add(factory.createType("Landroid/nfc/TagLostException;"));
     }
-    if (minApiLevel >= 11) {
+    if (minApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.H)) {
       types.add(factory.createType("Landroid/app/Fragment$InstantiationException;"));
       types.add(factory.createType("Landroid/database/sqlite/SQLiteAccessPermException;"));
       types.add(
@@ -372,28 +372,28 @@
       types.add(factory.createType("Landroid/util/MalformedJsonException;"));
       types.add(factory.createType("Landroid/view/KeyCharacterMap$UnavailableException;"));
     }
-    if (minApiLevel >= 14) {
+    if (minApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.I)) {
       types.add(factory.createType("Landroid/security/KeyChainException;"));
       types.add(factory.createType("Landroid/util/NoSuchPropertyException;"));
     }
-    if (minApiLevel >= 15) {
+    if (minApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.I_MR1)) {
       types.add(factory.createType("Landroid/os/TransactionTooLargeException;"));
     }
-    if (minApiLevel >= 16) {
+    if (minApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.J)) {
       types.add(factory.createType("Landroid/media/MediaCodec$CryptoException;"));
       types.add(factory.createType("Landroid/media/MediaCryptoException;"));
       types.add(factory.createType("Landroid/os/OperationCanceledException;"));
     }
-    if (minApiLevel >= 17) {
+    if (minApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.J_MR1)) {
       types.add(factory.createType("Landroid/view/WindowManager$InvalidDisplayException;"));
     }
-    if (minApiLevel >= 18) {
+    if (minApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.J_MR2)) {
       types.add(factory.createType("Landroid/media/DeniedByServerException;"));
       types.add(factory.createType("Landroid/media/MediaDrmException;"));
       types.add(factory.createType("Landroid/media/NotProvisionedException;"));
       types.add(factory.createType("Landroid/media/UnsupportedSchemeException;"));
     }
-    if (minApiLevel >= 19) {
+    if (minApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.K)) {
       types.add(factory.createType("Landroid/media/ResourceBusyException;"));
       types.add(
           factory.createType("Landroid/os/ParcelFileDescriptor$FileDescriptorDetachedException;"));
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java b/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
index 7b6c2e7..625430b 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
@@ -144,7 +144,9 @@
     }
   }
 
-  private Invoke.Type getInvokeType(DexClassAndMethod context) {
+  // We should avoid interpreting a CF invoke using DEX semantics.
+  @Deprecated
+  public Invoke.Type getInvokeType(DexClassAndMethod context) {
     switch (opcode) {
       case Opcodes.INVOKEINTERFACE:
         return Type.INTERFACE;
@@ -171,10 +173,12 @@
     return getMethod().isInstanceInitializer(dexItemFactory);
   }
 
+  // We should avoid interpreting a CF invoke using DEX semantics.
+  @Deprecated
   public boolean isInvokeSuper(DexType clazz) {
-    return opcode == Opcodes.INVOKESPECIAL &&
-        method.holder != clazz &&
-        !method.name.toString().equals(Constants.INSTANCE_INITIALIZER_NAME);
+    return opcode == Opcodes.INVOKESPECIAL
+        && method.holder != clazz
+        && !method.name.toString().equals(Constants.INSTANCE_INITIALIZER_NAME);
   }
 
   @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 e0c1cc2..31d40bb 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -261,7 +261,7 @@
       return true;
     }
     AndroidApiLevel nativeMultiDex = AndroidApiLevel.L;
-    if (options.minApiLevel < nativeMultiDex.getLevel()) {
+    if (options.minApiLevel.isLessThan(nativeMultiDex)) {
       return true;
     }
     assert options.mainDexKeepRules.isEmpty();
@@ -270,13 +270,12 @@
     return true;
   }
 
-  private int validateOrComputeMinApiLevel(int computedMinApiLevel, DexReader dexReader) {
+  private AndroidApiLevel validateOrComputeMinApiLevel(
+      AndroidApiLevel computedMinApiLevel, DexReader dexReader) {
     DexVersion version = dexReader.getDexVersion();
-    if (options.minApiLevel == AndroidApiLevel.getDefault().getLevel()) {
-      computedMinApiLevel = Math
-          .max(computedMinApiLevel, AndroidApiLevel.getMinAndroidApiLevel(version).getLevel());
-    } else if (!version
-        .matchesApiLevel(AndroidApiLevel.getAndroidApiLevel(options.minApiLevel))) {
+    if (options.minApiLevel == AndroidApiLevel.getDefault()) {
+      computedMinApiLevel = computedMinApiLevel.max(AndroidApiLevel.getMinAndroidApiLevel(version));
+    } else if (!version.matchesApiLevel(options.minApiLevel)) {
       throw new CompilationError("Dex file with version '" + version.getIntValue() +
           "' cannot be used with min sdk level '" + options.minApiLevel + "'.");
     }
@@ -340,7 +339,7 @@
       }
       hasReadProgramResourceFromDex = true;
       List<DexParser<DexProgramClass>> dexParsers = new ArrayList<>(dexSources.size());
-      int computedMinApiLevel = options.minApiLevel;
+      AndroidApiLevel computedMinApiLevel = options.minApiLevel;
       for (ProgramResource input : dexSources) {
         DexReader dexReader = new DexReader(input);
         if (options.passthroughDexCode) {
diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java
index 42bd66b..dc93c37 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -318,7 +318,7 @@
       return true;
     }
 
-    int apiLevel = options.minApiLevel;
+    AndroidApiLevel apiLevel = options.minApiLevel;
     for (DexField field : mapping.getFields()) {
       assert field.name.isValidSimpleName(apiLevel);
     }
@@ -815,8 +815,7 @@
     dest.putBytes(
         options.testing.forceDexVersionBytes != null
             ? options.testing.forceDexVersionBytes
-            : DexVersion.getDexVersion(AndroidApiLevel.getAndroidApiLevel(options.minApiLevel))
-                .getBytes());
+            : DexVersion.getDexVersion(options.minApiLevel).getBytes());
     dest.putByte(Constants.DEX_FILE_MAGIC_SUFFIX);
     // Leave out checksum and signature for now.
     dest.moveTo(Constants.FILE_SIZE_OFFSET);
@@ -1099,7 +1098,7 @@
     private final Map<DexProgramClass, DexAnnotationDirectory> clazzToAnnotationDirectory
         = new HashMap<>();
 
-    private final int minApiLevel;
+    private final AndroidApiLevel minApiLevel;
 
     private static <T> Object2IntMap<T> createObject2IntMap() {
       Object2IntMap<T> result = new Object2IntLinkedOpenHashMap<>();
@@ -1148,7 +1147,7 @@
     public boolean add(DexAnnotationSet annotationSet) {
       // Until we fully drop support for API levels < 17, we have to emit an empty annotation set to
       // work around a DALVIK bug. See b/36951668.
-      if ((minApiLevel >= AndroidApiLevel.J_MR1.getLevel()) && annotationSet.isEmpty()) {
+      if ((minApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.J_MR1)) && annotationSet.isEmpty()) {
         return false;
       }
       return add(annotationSets, annotationSet);
@@ -1300,7 +1299,7 @@
     public int getOffsetFor(DexAnnotationSet annotationSet) {
       // Until we fully drop support for API levels < 17, we have to emit an empty annotation set to
       // work around a DALVIK bug. See b/36951668.
-      if ((minApiLevel >= AndroidApiLevel.J_MR1.getLevel()) && annotationSet.isEmpty()) {
+      if ((minApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.J_MR1)) && annotationSet.isEmpty()) {
         return 0;
       }
       return lookup(annotationSet, annotationSets);
@@ -1351,7 +1350,7 @@
     void setOffsetFor(DexAnnotationSet annotationSet, int offset) {
       // Until we fully drop support for API levels < 17, we have to emit an empty annotation set to
       // work around a DALVIK bug. See b/36951668.
-      assert (minApiLevel < AndroidApiLevel.J_MR1.getLevel()) || !annotationSet.isEmpty();
+      assert (minApiLevel.isLessThan(AndroidApiLevel.J_MR1)) || !annotationSet.isEmpty();
       setOffsetFor(annotationSet, offset, annotationSets);
     }
 
diff --git a/src/main/java/com/android/tools/r8/graph/AbstractAccessContexts.java b/src/main/java/com/android/tools/r8/graph/AbstractAccessContexts.java
index f8c4f3b..616fcf5 100644
--- a/src/main/java/com/android/tools/r8/graph/AbstractAccessContexts.java
+++ b/src/main/java/com/android/tools/r8/graph/AbstractAccessContexts.java
@@ -308,7 +308,9 @@
                 newAccessesWithContexts.computeIfAbsent(
                     lens.lookupField(access), ignore -> ProgramMethodSet.create());
             for (ProgramMethod context : contexts) {
-              newContexts.add(lens.mapProgramMethod(context, definitions));
+              ProgramMethod newContext = lens.mapProgramMethod(context, definitions);
+              assert newContext != null : "Unable to map context: " + context.toSourceString();
+              newContexts.add(newContext);
             }
           });
       return new ConcreteAccessContexts(newAccessesWithContexts);
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index bf21997..4ef90be 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -28,7 +28,6 @@
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfoFactory;
 import com.android.tools.r8.ir.optimize.library.LibraryMemberOptimizer;
 import com.android.tools.r8.ir.optimize.library.LibraryMethodSideEffectModelCollection;
-import com.android.tools.r8.optimize.MemberRebindingLens;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.KeepInfoCollection;
 import com.android.tools.r8.shaking.LibraryModeledPredicate;
@@ -688,13 +687,26 @@
     }
 
     // Insert a member rebinding lens above the first unapplied lens.
-    MemberRebindingLens appliedMemberRebindingLens =
-        firstUnappliedLens.findPrevious(GraphLens::isMemberRebindingLens);
-    GraphLens newMemberRebindingLens =
-        appliedMemberRebindingLens != null
-            ? appliedMemberRebindingLens.toRewrittenFieldRebindingLens(
-                appView.dexItemFactory(), appliedLens)
-            : GraphLens.getIdentityLens();
+    // TODO(b/182129249): Once the member rebinding phase has been removed, the MemberRebindingLens
+    //  should be removed and all uses of FieldRebindingIdentityLens should be replaced by
+    //  MemberRebindingIdentityLens.
+    NonIdentityGraphLens appliedMemberRebindingLens =
+        firstUnappliedLens.findPrevious(
+            previous ->
+                previous.isMemberRebindingLens() || previous.isMemberRebindingIdentityLens());
+    GraphLens newMemberRebindingLens;
+    if (appliedMemberRebindingLens != null) {
+      newMemberRebindingLens =
+          appliedMemberRebindingLens.isMemberRebindingLens()
+              ? appliedMemberRebindingLens
+                  .asMemberRebindingLens()
+                  .toRewrittenFieldRebindingLens(appView, appliedLens)
+              : appliedMemberRebindingLens
+                  .asMemberRebindingIdentityLens()
+                  .toRewrittenMemberRebindingIdentityLens(appView, appliedLens);
+    } else {
+      newMemberRebindingLens = GraphLens.getIdentityLens();
+    }
 
     firstUnappliedLens.withAlternativeParentLens(
         newMemberRebindingLens,
diff --git a/src/main/java/com/android/tools/r8/graph/BottomUpClassHierarchyTraversal.java b/src/main/java/com/android/tools/r8/graph/BottomUpClassHierarchyTraversal.java
index 1c7bf88..685188e 100644
--- a/src/main/java/com/android/tools/r8/graph/BottomUpClassHierarchyTraversal.java
+++ b/src/main/java/com/android/tools/r8/graph/BottomUpClassHierarchyTraversal.java
@@ -4,17 +4,19 @@
 
 package com.android.tools.r8.graph;
 
+import java.util.function.Function;
+
 public class BottomUpClassHierarchyTraversal<T extends DexClass>
     extends ClassHierarchyTraversal<T, BottomUpClassHierarchyTraversal<T>> {
 
-  private final SubtypingInfo subtypingInfo;
+  private final Function<DexType, Iterable<DexType>> immediateSubtypesProvider;
 
   private BottomUpClassHierarchyTraversal(
       AppView<? extends AppInfoWithClassHierarchy> appView,
-      SubtypingInfo subtypingInfo,
+      Function<DexType, Iterable<DexType>> immediateSubtypesProvider,
       Scope scope) {
     super(appView, scope);
-    this.subtypingInfo = subtypingInfo;
+    this.immediateSubtypesProvider = immediateSubtypesProvider;
   }
 
   /**
@@ -23,7 +25,8 @@
    */
   public static BottomUpClassHierarchyTraversal<DexClass> forAllClasses(
       AppView<? extends AppInfoWithClassHierarchy> appView, SubtypingInfo subtypingInfo) {
-    return new BottomUpClassHierarchyTraversal<>(appView, subtypingInfo, Scope.ALL_CLASSES);
+    return new BottomUpClassHierarchyTraversal<>(
+        appView, subtypingInfo::allImmediateSubtypes, Scope.ALL_CLASSES);
   }
 
   /**
@@ -32,8 +35,18 @@
    */
   public static BottomUpClassHierarchyTraversal<DexProgramClass> forProgramClasses(
       AppView<? extends AppInfoWithClassHierarchy> appView, SubtypingInfo subtypingInfo) {
+    return forProgramClasses(appView, subtypingInfo::allImmediateSubtypes);
+  }
+
+  /**
+   * Returns a visitor that can be used to visit all the program classes that are reachable from a
+   * given set of sources.
+   */
+  public static BottomUpClassHierarchyTraversal<DexProgramClass> forProgramClasses(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      Function<DexType, Iterable<DexType>> immediateSubtypesProvider) {
     return new BottomUpClassHierarchyTraversal<>(
-        appView, subtypingInfo, Scope.ONLY_PROGRAM_CLASSES);
+        appView, immediateSubtypesProvider, Scope.ONLY_PROGRAM_CLASSES);
   }
 
   @Override
@@ -57,7 +70,7 @@
     worklist.addFirst(clazzWithTypeT);
 
     // Add subtypes to worklist.
-    for (DexType subtype : subtypingInfo.allImmediateSubtypes(clazz.type)) {
+    for (DexType subtype : immediateSubtypesProvider.apply(clazz.getType())) {
       DexClass definition = appView.definitionFor(subtype);
       if (definition != null) {
         if (scope != Scope.ONLY_PROGRAM_CLASSES || definition.isProgramClass()) {
diff --git a/src/main/java/com/android/tools/r8/graph/CfCode.java b/src/main/java/com/android/tools/r8/graph/CfCode.java
index 31774f0..14f0015 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -32,7 +32,6 @@
 import com.android.tools.r8.ir.conversion.CfSourceCode;
 import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
-import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.ClassNameMapper;
@@ -314,7 +313,7 @@
       LensCodeRewriterUtils rewriter,
       MethodVisitor visitor) {
     GraphLens graphLens = appView.graphLens();
-    assert verifyFrames(method.getDefinition(), appView, null, false).isValid()
+    assert verifyFrames(method.getDefinition(), appView, null).isValid()
         : "Could not validate stack map frames";
     DexItemFactory dexItemFactory = appView.dexItemFactory();
     InitClassLens initClassLens = appView.initClassLens();
@@ -416,7 +415,8 @@
 
   @Override
   public IRCode buildIR(ProgramMethod method, AppView<?> appView, Origin origin) {
-    verifyFramesOrRemove(method.getDefinition(), appView, origin, true);
+    verifyFramesOrRemove(
+        method.getDefinition(), appView, origin, IRBuilder.lookupPrototypeChanges(appView, method));
     return internalBuildPossiblyWithLocals(method, method, appView, null, null, origin, null);
   }
 
@@ -428,21 +428,21 @@
       NumberGenerator valueNumberGenerator,
       Position callerPosition,
       Origin origin,
-      MethodProcessor methodProcessor) {
+      RewrittenPrototypeDescription protoChanges) {
     assert valueNumberGenerator != null;
     assert callerPosition != null;
-    verifyFramesOrRemove(
-        method.getDefinition(), appView, origin, methodProcessor.shouldApplyCodeRewritings(method));
+    assert protoChanges != null;
+    verifyFramesOrRemove(method.getDefinition(), appView, origin, protoChanges);
     return internalBuildPossiblyWithLocals(
-        context, method, appView, valueNumberGenerator, callerPosition, origin, methodProcessor);
+        context, method, appView, valueNumberGenerator, callerPosition, origin, protoChanges);
   }
 
   private void verifyFramesOrRemove(
       DexEncodedMethod method,
       AppView<?> appView,
       Origin origin,
-      boolean shouldApplyCodeRewritings) {
-    stackMapStatus = verifyFrames(method, appView, origin, shouldApplyCodeRewritings);
+      RewrittenPrototypeDescription protoChanges) {
+    stackMapStatus = verifyFrames(method, appView, origin, protoChanges);
     if (!stackMapStatus.isValid()) {
       ArrayList<CfInstruction> copy = new ArrayList<>(instructions);
       copy.removeIf(CfInstruction::isFrame);
@@ -458,7 +458,7 @@
       NumberGenerator valueNumberGenerator,
       Position callerPosition,
       Origin origin,
-      MethodProcessor methodProcessor) {
+      RewrittenPrototypeDescription protoChanges) {
     if (!method.getDefinition().keepLocals(appView.options())) {
       return internalBuild(
           Collections.emptyList(),
@@ -468,10 +468,10 @@
           valueNumberGenerator,
           callerPosition,
           origin,
-          methodProcessor);
+          protoChanges);
     } else {
       return internalBuildWithLocals(
-          context, method, appView, valueNumberGenerator, callerPosition, origin, methodProcessor);
+          context, method, appView, valueNumberGenerator, callerPosition, origin, protoChanges);
     }
   }
 
@@ -483,7 +483,7 @@
       NumberGenerator valueNumberGenerator,
       Position callerPosition,
       Origin origin,
-      MethodProcessor methodProcessor) {
+      RewrittenPrototypeDescription protoChanges) {
     try {
       return internalBuild(
           Collections.unmodifiableList(localVariables),
@@ -493,7 +493,7 @@
           valueNumberGenerator,
           callerPosition,
           origin,
-          methodProcessor);
+          protoChanges);
     } catch (InvalidDebugInfoException e) {
       appView.options().warningInvalidDebugInfo(method, origin, e);
       return internalBuild(
@@ -504,7 +504,7 @@
           valueNumberGenerator,
           callerPosition,
           origin,
-          methodProcessor);
+          protoChanges);
     }
   }
 
@@ -517,7 +517,7 @@
       NumberGenerator valueNumberGenerator,
       Position callerPosition,
       Origin origin,
-      MethodProcessor methodProcessor) {
+      RewrittenPrototypeDescription protoChanges) {
     CfSourceCode source =
         new CfSourceCode(
             this,
@@ -527,11 +527,15 @@
             callerPosition,
             origin,
             appView);
-    IRBuilder builder =
-        methodProcessor == null
-            ? IRBuilder.create(method, appView, source, origin)
-            : IRBuilder.createForInlining(
-                method, appView, source, origin, methodProcessor, valueNumberGenerator);
+    IRBuilder builder;
+    if (valueNumberGenerator == null) {
+      assert protoChanges == null;
+      builder = IRBuilder.create(method, appView, source, origin);
+    } else {
+      builder =
+          IRBuilder.createForInlining(
+              method, appView, source, origin, valueNumberGenerator, protoChanges);
+    }
     return builder.build(context);
   }
 
@@ -716,8 +720,15 @@
             thisLocalInfo.index, debugLocalInfo, thisLocalInfo.start, thisLocalInfo.end));
   }
 
+  public StackMapStatus verifyFrames(DexEncodedMethod method, AppView<?> appView, Origin origin) {
+    return verifyFrames(method, appView, origin, RewrittenPrototypeDescription.none());
+  }
+
   public StackMapStatus verifyFrames(
-      DexEncodedMethod method, AppView<?> appView, Origin origin, boolean applyProtoTypeChanges) {
+      DexEncodedMethod method,
+      AppView<?> appView,
+      Origin origin,
+      RewrittenPrototypeDescription protoChanges) {
     if (!appView.options().canUseInputStackMaps()
         || appView.options().testing.disableStackMapVerification) {
       return StackMapStatus.NOT_PRESENT;
@@ -786,14 +797,8 @@
     }
     DexType context = appView.graphLens().lookupType(method.getHolderType());
     DexType returnType = appView.graphLens().lookupType(method.getReference().getReturnType());
-    RewrittenPrototypeDescription rewrittenDescription = RewrittenPrototypeDescription.none();
-    if (applyProtoTypeChanges) {
-      rewrittenDescription =
-          appView.graphLens().lookupPrototypeChangesForMethodDefinition(method.getReference());
-      if (!rewrittenDescription.isEmpty()
-          && rewrittenDescription.getRewrittenReturnInfo() != null) {
-        returnType = rewrittenDescription.getRewrittenReturnInfo().getOldType();
-      }
+    if (!protoChanges.isEmpty() && protoChanges.getRewrittenReturnInfo() != null) {
+      returnType = protoChanges.getRewrittenReturnInfo().getOldType();
     }
     CfFrameVerificationHelper builder =
         new CfFrameVerificationHelper(
@@ -809,8 +814,7 @@
       builder.checkFrameAndSet(stateMap.get(null));
     } else if (shouldComputeInitialFrame()) {
       builder.checkFrameAndSet(
-          new CfFrame(
-              computeInitialLocals(context, method, rewrittenDescription), new ArrayDeque<>()));
+          new CfFrame(computeInitialLocals(context, method, protoChanges), new ArrayDeque<>()));
     }
     for (int i = 0; i < instructions.size(); i++) {
       CfInstruction instruction = instructions.get(i);
diff --git a/src/main/java/com/android/tools/r8/graph/Code.java b/src/main/java/com/android/tools/r8/graph/Code.java
index b0d84f9..0f6438f 100644
--- a/src/main/java/com/android/tools/r8/graph/Code.java
+++ b/src/main/java/com/android/tools/r8/graph/Code.java
@@ -8,7 +8,6 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.NumberGenerator;
 import com.android.tools.r8.ir.code.Position;
-import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.optimize.Outliner.OutlineCode;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.origin.Origin;
@@ -25,7 +24,7 @@
       NumberGenerator valueNumberGenerator,
       Position callerPosition,
       Origin origin,
-      MethodProcessor methodProcessor) {
+      RewrittenPrototypeDescription protoChanges) {
     throw new Unreachable("Unexpected attempt to build IR graph for inlining from: "
         + getClass().getCanonicalName());
   }
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 58862d7..ccc7dbc 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClassAndMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClassAndMethod.java
@@ -48,6 +48,10 @@
     return getReference().asMethodReference();
   }
 
+  public DexMethodSignature getMethodSignature() {
+    return getReference().getSignature();
+  }
+
   public DexType getParameter(int index) {
     return getReference().getParameter(index);
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index 2e42ccf..e4f7973 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -17,7 +17,6 @@
 import com.android.tools.r8.ir.conversion.DexSourceCode;
 import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
-import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.StringUtils;
@@ -232,7 +231,7 @@
       NumberGenerator valueNumberGenerator,
       Position callerPosition,
       Origin origin,
-      MethodProcessor methodProcessor) {
+      RewrittenPrototypeDescription protoChanges) {
     DexSourceCode source =
         new DexSourceCode(
             this,
@@ -240,7 +239,7 @@
             appView.graphLens().getOriginalMethodSignature(method.getReference()),
             callerPosition);
     return IRBuilder.createForInlining(
-            method, appView, source, origin, methodProcessor, valueNumberGenerator)
+            method, appView, source, origin, valueNumberGenerator, protoChanges)
         .build(context);
   }
 
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 04cf5cc..9051374 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -56,6 +56,7 @@
 import com.android.tools.r8.ir.optimize.NestUtils;
 import com.android.tools.r8.ir.optimize.info.CallSiteOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.DefaultMethodOptimizationInfo;
+import com.android.tools.r8.ir.optimize.info.DefaultMethodOptimizationWithMinApiInfo;
 import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackSimple;
 import com.android.tools.r8.ir.optimize.info.UpdatableMethodOptimizationInfo;
@@ -98,6 +99,8 @@
 public class DexEncodedMethod extends DexEncodedMember<DexEncodedMethod, DexMethod>
     implements StructuralItem<DexEncodedMethod> {
 
+  public static final boolean D8_R8_SYNTHESIZED = true;
+
   public static final String CONFIGURATION_DEBUGGING_PREFIX = "Shaking error: Missing method in ";
 
   /**
@@ -1318,6 +1321,7 @@
             !isAbstract(),
             builder ->
                 builder
+                    .setGenericSignature(MethodTypeSignature.noSignature())
                     .setCode(
                         ForwardMethodBuilder.builder(definitions.dexItemFactory())
                             .setStaticSource(newMethod)
@@ -1379,7 +1383,8 @@
         builder(this)
             .promoteToStatic()
             .withoutThisParameter()
-            .adjustOptimizationInfoAfterRemovingThisParameter();
+            .adjustOptimizationInfoAfterRemovingThisParameter()
+            .setGenericSignature(MethodTypeSignature.noSignature());
     DexEncodedMethod method = builder.build();
     method.copyMetadata(this);
     setObsolete();
@@ -1451,10 +1456,10 @@
 
   public synchronized UpdatableMethodOptimizationInfo getMutableOptimizationInfo() {
     checkIfObsolete();
-    if (optimizationInfo == DefaultMethodOptimizationInfo.DEFAULT_INSTANCE) {
-      optimizationInfo = optimizationInfo.mutableCopy();
-    }
-    return (UpdatableMethodOptimizationInfo) optimizationInfo;
+    UpdatableMethodOptimizationInfo updatableMethodOptimizationInfo =
+        optimizationInfo.asUpdatableMethodOptimizationInfo();
+    this.optimizationInfo = updatableMethodOptimizationInfo;
+    return updatableMethodOptimizationInfo;
   }
 
   public void setOptimizationInfo(UpdatableMethodOptimizationInfo info) {
@@ -1462,6 +1467,11 @@
     optimizationInfo = info;
   }
 
+  public void setMinApiOptimizationInfo(DefaultMethodOptimizationWithMinApiInfo info) {
+    checkIfObsolete();
+    optimizationInfo = info;
+  }
+
   public synchronized void abandonCallSiteOptimizationInfo() {
     checkIfObsolete();
     callSiteOptimizationInfo = CallSiteOptimizationInfo.abandoned();
@@ -1510,7 +1520,7 @@
 
     private DexMethod method;
     private MethodAccessFlags accessFlags;
-    private final MethodTypeSignature genericSignature;
+    private MethodTypeSignature genericSignature;
     private final DexAnnotationSet annotations;
     private OptionalBool isLibraryMethodOverride = OptionalBool.UNKNOWN;
     private ParameterAnnotationsList parameterAnnotations;
@@ -1710,5 +1720,10 @@
       buildConsumer.accept(result);
       return result;
     }
+
+    public Builder setGenericSignature(MethodTypeSignature methodSignature) {
+      this.genericSignature = methodSignature;
+      return this;
+    }
   }
 }
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 7d95e87..646c9a0 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -29,10 +29,12 @@
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.desugar.LambdaClass;
 import com.android.tools.r8.kotlin.Kotlin;
+import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.utils.ArrayUtils;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.LRUCacheTable;
+import com.android.tools.r8.utils.ListUtils;
 import com.google.common.base.Strings;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
@@ -2498,6 +2500,22 @@
     return createMethod(holder, proto, createString(name));
   }
 
+  public DexMethod createMethod(MethodReference methodReference) {
+    DexString[] formals = new DexString[methodReference.getFormalTypes().size()];
+    ListUtils.forEachWithIndex(
+        methodReference.getFormalTypes(),
+        (formal, index) -> {
+          formals[index] = createString(formal.getDescriptor());
+        });
+    return createMethod(
+        createString(methodReference.getHolderClass().getDescriptor()),
+        createString(methodReference.getMethodName()),
+        methodReference.getReturnType() == null
+            ? voidDescriptor
+            : createString(methodReference.getReturnType().getDescriptor()),
+        formals);
+  }
+
   public DexMethodHandle createMethodHandle(
       MethodHandleType type,
       DexMember<? extends DexItem, ? extends DexMember<?, ?>> fieldOrMethod,
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 c96dc4a..15ac70a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethod.java
@@ -292,6 +292,10 @@
     return dexItemFactory.createMethod(reference.getContextType(), proto, name);
   }
 
+  public DexMethod withName(String name, DexItemFactory dexItemFactory) {
+    return withName(dexItemFactory.createString(name), dexItemFactory);
+  }
+
   public DexMethod withName(DexString name, DexItemFactory dexItemFactory) {
     return dexItemFactory.createMethod(holder, proto, name);
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexString.java b/src/main/java/com/android/tools/r8/graph/DexString.java
index ba4d653..87630f2 100644
--- a/src/main/java/com/android/tools/r8/graph/DexString.java
+++ b/src/main/java/com/android/tools/r8/graph/DexString.java
@@ -399,9 +399,9 @@
     }
   }
 
-  public static boolean isValidSimpleName(int apiLevel, String string) {
+  public static boolean isValidSimpleName(AndroidApiLevel apiLevel, String string) {
     // space characters are not allowed prior to Android R
-    if (apiLevel < AndroidApiLevel.R.getLevel()) {
+    if (apiLevel.isLessThan(AndroidApiLevel.R)) {
       int cp;
       for (int i = 0; i < string.length(); ) {
         cp = string.codePointAt(i);
@@ -414,9 +414,9 @@
     return true;
   }
 
-  public boolean isValidSimpleName(int apiLevel) {
+  public boolean isValidSimpleName(AndroidApiLevel apiLevel) {
     // space characters are not allowed prior to Android R
-    if (apiLevel < AndroidApiLevel.R.getLevel()) {
+    if (apiLevel.isLessThan(AndroidApiLevel.R)) {
       try {
         return isValidSimpleName(apiLevel, decode());
       } catch (UTFDataFormatException e) {
diff --git a/src/main/java/com/android/tools/r8/graph/EnclosingMethodAttribute.java b/src/main/java/com/android/tools/r8/graph/EnclosingMethodAttribute.java
index fde5f6c..d2ea75e 100644
--- a/src/main/java/com/android/tools/r8/graph/EnclosingMethodAttribute.java
+++ b/src/main/java/com/android/tools/r8/graph/EnclosingMethodAttribute.java
@@ -52,6 +52,10 @@
     return enclosingMethod != null;
   }
 
+  public boolean hasEnclosingClass() {
+    return enclosingClass != null;
+  }
+
   public DexMethod getEnclosingMethod() {
     return enclosingMethod;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignature.java b/src/main/java/com/android/tools/r8/graph/GenericSignature.java
index 9a34638..419125d 100644
--- a/src/main/java/com/android/tools/r8/graph/GenericSignature.java
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignature.java
@@ -173,6 +173,7 @@
       this.name = name;
       this.classBound = classBound;
       this.interfaceBounds = interfaceBounds;
+      assert classBound != null;
       assert interfaceBounds != null;
     }
 
@@ -189,14 +190,16 @@
     }
 
     public FormalTypeParameter visit(GenericSignatureVisitor visitor) {
-      FieldTypeSignature rewrittenClassBound =
-          classBound == null ? null : visitor.visitClassBound(classBound);
+      FieldTypeSignature rewrittenClassBound = visitor.visitClassBound(classBound);
       List<FieldTypeSignature> rewrittenInterfaceBounds =
           visitor.visitInterfaceBounds(interfaceBounds);
       if (classBound == rewrittenClassBound && interfaceBounds == rewrittenInterfaceBounds) {
         return this;
       }
-      return new FormalTypeParameter(name, rewrittenClassBound, rewrittenInterfaceBounds);
+      return new FormalTypeParameter(
+          name,
+          rewrittenClassBound == null ? FieldTypeSignature.noSignature() : rewrittenClassBound,
+          rewrittenInterfaceBounds);
     }
   }
 
@@ -286,6 +289,10 @@
     public static ClassSignature noSignature() {
       return NO_CLASS_SIGNATURE;
     }
+
+    public ClassSignature toObjectBoundWithSameFormals(ClassTypeSignature objectBound) {
+      return new ClassSignature(formalTypeParameters, objectBound, getEmptySuperInterfaces());
+    }
   }
 
   private static class InvalidClassSignature extends ClassSignature {
@@ -649,7 +656,7 @@
       if (elementSignature == rewrittenElementSignature) {
         return this;
       }
-      return new ArrayTypeSignature(elementSignature, getWildcardIndicator());
+      return new ArrayTypeSignature(rewrittenElementSignature, getWildcardIndicator());
     }
   }
 
@@ -891,7 +898,7 @@
       return parser.parseClassSignature(signature);
     } catch (GenericSignatureFormatError e) {
       diagnosticsHandler.warning(
-          GenericSignatureDiagnostic.invalidClassSignature(signature, className, origin, e));
+          GenericSignatureFormatDiagnostic.invalidClassSignature(signature, className, origin, e));
       return ClassSignature.NO_CLASS_SIGNATURE;
     }
   }
@@ -910,7 +917,7 @@
       return parser.parseFieldTypeSignature(signature);
     } catch (GenericSignatureFormatError e) {
       diagnosticsHandler.warning(
-          GenericSignatureDiagnostic.invalidFieldSignature(signature, fieldName, origin, e));
+          GenericSignatureFormatDiagnostic.invalidFieldSignature(signature, fieldName, origin, e));
       return GenericSignature.NO_FIELD_TYPE_SIGNATURE;
     }
   }
@@ -929,7 +936,8 @@
       return parser.parseMethodTypeSignature(signature);
     } catch (GenericSignatureFormatError e) {
       diagnosticsHandler.warning(
-          GenericSignatureDiagnostic.invalidMethodSignature(signature, methodName, origin, e));
+          GenericSignatureFormatDiagnostic.invalidMethodSignature(
+              signature, methodName, origin, e));
       return MethodTypeSignature.NO_METHOD_TYPE_SIGNATURE;
     }
   }
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeVariableRemover.java b/src/main/java/com/android/tools/r8/graph/GenericSignatureContextBuilder.java
similarity index 65%
rename from src/main/java/com/android/tools/r8/graph/GenericSignatureTypeVariableRemover.java
rename to src/main/java/com/android/tools/r8/graph/GenericSignatureContextBuilder.java
index 7c0ee29..1a05232 100644
--- a/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeVariableRemover.java
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignatureContextBuilder.java
@@ -4,11 +4,11 @@
 
 package com.android.tools.r8.graph;
 
-import static com.android.tools.r8.graph.GenericSignatureTypeVariableRemover.TypeParameterContext.empty;
-import static com.google.common.base.Predicates.alwaysFalse;
+import static com.android.tools.r8.graph.GenericSignatureContextBuilder.TypeParameterContext.empty;
 
 import com.android.tools.r8.graph.GenericSignature.FormalTypeParameter;
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -18,9 +18,8 @@
 import java.util.Set;
 import java.util.function.Predicate;
 
-public class GenericSignatureTypeVariableRemover {
+public class GenericSignatureContextBuilder {
 
-  private final DexType objectType;
   private final Map<DexReference, TypeParameterSubstitutions> formalsInfo;
   private final Map<DexReference, DexReference> enclosingInfo;
 
@@ -51,7 +50,7 @@
     }
   }
 
-  static class TypeParameterContext {
+  public static class TypeParameterContext {
 
     private static final TypeParameterContext EMPTY =
         new TypeParameterContext(Collections.emptyMap(), Collections.emptySet());
@@ -69,37 +68,99 @@
       if (information == null) {
         return this;
       }
-      HashMap<String, DexType> newPruned = new HashMap<>(prunedParametersWithBounds);
-      HashSet<String> newLiveParameters = new HashSet<>(liveParameters);
-      information.parametersWithBounds.forEach(
-          (param, type) -> {
-            if (dead) {
-              newPruned.put(param, type);
-              newLiveParameters.remove(param);
-            } else {
-              newLiveParameters.add(param);
-              newPruned.remove(param);
-            }
-          });
-      return new TypeParameterContext(newPruned, newLiveParameters);
+      return dead
+          ? addPrunedSubstitutions(information.parametersWithBounds)
+          : addLiveParameters(information.parametersWithBounds.keySet());
     }
 
     public static TypeParameterContext empty() {
       return EMPTY;
     }
+
+    public boolean isLiveParameter(String parameterName) {
+      return liveParameters.contains(parameterName);
+    }
+
+    public DexType getPrunedSubstitution(String parameterName) {
+      assert !isLiveParameter(parameterName);
+      return prunedParametersWithBounds.get(parameterName);
+    }
+
+    public TypeParameterContext addLiveParameters(Collection<String> typeParameters) {
+      if (typeParameters.isEmpty()) {
+        return this;
+      }
+      HashSet<String> newLiveParameters = new HashSet<>();
+      newLiveParameters.addAll(liveParameters);
+      newLiveParameters.addAll(typeParameters);
+      HashMap<String, DexType> newPruned = new HashMap<>();
+      prunedParametersWithBounds.forEach(
+          (name, type) -> {
+            if (!typeParameters.contains(name)) {
+              newPruned.put(name, type);
+            }
+          });
+      return new TypeParameterContext(newPruned, newLiveParameters);
+    }
+
+    public TypeParameterContext addPrunedSubstitutions(Map<String, DexType> substitutions) {
+      if (substitutions.isEmpty()) {
+        return this;
+      }
+      HashMap<String, DexType> newPruned = new HashMap<>();
+      newPruned.putAll(prunedParametersWithBounds);
+      newPruned.putAll(substitutions);
+      HashSet<String> newLiveParameters = new HashSet<>();
+      liveParameters.forEach(
+          name -> {
+            if (!substitutions.containsKey(name)) {
+              newLiveParameters.add(name);
+            }
+          });
+      return new TypeParameterContext(newPruned, newLiveParameters);
+    }
   }
 
-  private GenericSignatureTypeVariableRemover(
+  public static class AlwaysLiveTypeParameterContext extends TypeParameterContext {
+
+    private AlwaysLiveTypeParameterContext() {
+      super(Collections.emptyMap(), Collections.emptySet());
+    }
+
+    public static AlwaysLiveTypeParameterContext create() {
+      return new AlwaysLiveTypeParameterContext();
+    }
+
+    @Override
+    public boolean isLiveParameter(String parameterName) {
+      return true;
+    }
+
+    @Override
+    public DexType getPrunedSubstitution(String parameterName) {
+      assert false;
+      return null;
+    }
+
+    @Override
+    public TypeParameterContext addLiveParameters(Collection<String> typeParameters) {
+      return this;
+    }
+
+    @Override
+    public TypeParameterContext addPrunedSubstitutions(Map<String, DexType> substitutions) {
+      return this;
+    }
+  }
+
+  private GenericSignatureContextBuilder(
       Map<DexReference, TypeParameterSubstitutions> formalsInfo,
-      Map<DexReference, DexReference> enclosingInfo,
-      DexType objectType) {
+      Map<DexReference, DexReference> enclosingInfo) {
     this.formalsInfo = formalsInfo;
     this.enclosingInfo = enclosingInfo;
-    this.objectType = objectType;
   }
 
-  public static GenericSignatureTypeVariableRemover create(
-      AppView<?> appView, List<DexProgramClass> programClasses) {
+  public static GenericSignatureContextBuilder create(List<DexProgramClass> programClasses) {
     Map<DexReference, TypeParameterSubstitutions> formalsInfo = new IdentityHashMap<>();
     Map<DexReference, DexReference> enclosingInfo = new IdentityHashMap<>();
     programClasses.forEach(
@@ -137,8 +198,13 @@
                     : enclosingMethodAttribute.getEnclosingClass());
           }
         });
-    return new GenericSignatureTypeVariableRemover(
-        formalsInfo, enclosingInfo, appView.dexItemFactory().objectType);
+    return new GenericSignatureContextBuilder(formalsInfo, enclosingInfo);
+  }
+
+  public TypeParameterContext computeTypeParameterContext(
+      AppView<?> appView, DexReference reference, Predicate<DexType> wasPruned) {
+    assert !wasPruned.test(reference.getContextType());
+    return computeTypeParameterContext(appView, reference, wasPruned, false);
   }
 
   private TypeParameterContext computeTypeParameterContext(
@@ -175,7 +241,7 @@
     return typeParameterContext.combine(formalsInfo.get(reference), prunedHere);
   }
 
-  private static boolean hasPrunedRelationship(
+  public boolean hasPrunedRelationship(
       AppView<?> appView,
       DexReference enclosingReference,
       DexType enclosedClassType,
@@ -189,8 +255,17 @@
     if (wasPruned.test(enclosingReference.getContextType()) || wasPruned.test(enclosedClassType)) {
       return true;
     }
-    DexClass enclosingClass = appView.definitionFor(enclosingReference.getContextType());
-    DexClass enclosedClass = appView.definitionFor(enclosedClassType);
+    // TODO(b/187035453): We should visit generic signatures in the enqueuer.
+    DexClass enclosingClass =
+        appView
+            .appInfo()
+            .definitionForWithoutExistenceAssert(
+                appView.graphLens().lookupClassType(enclosingReference.getContextType()));
+    DexClass enclosedClass =
+        appView
+            .appInfo()
+            .definitionForWithoutExistenceAssert(
+                appView.graphLens().lookupClassType(enclosedClassType));
     if (enclosingClass == null || enclosedClass == null) {
       return true;
     }
@@ -207,68 +282,15 @@
     }
   }
 
-  private static boolean hasGenericTypeVariables(
+  public boolean hasGenericTypeVariables(
       AppView<?> appView, DexType type, Predicate<DexType> wasPruned) {
     if (wasPruned.test(type)) {
       return false;
     }
-    DexClass clazz = appView.definitionFor(type);
+    DexClass clazz = appView.definitionFor(appView.graphLens().lookupClassType(type));
     if (clazz == null || clazz.isNotProgramClass() || clazz.getClassSignature().isInvalid()) {
       return true;
     }
     return !clazz.getClassSignature().getFormalTypeParameters().isEmpty();
   }
-
-  public void removeDeadGenericSignatureTypeVariables(AppView<?> appView) {
-    Predicate<DexType> wasPruned =
-        appView.hasLiveness() ? appView.withLiveness().appInfo()::wasPruned : alwaysFalse();
-    GenericSignaturePartialTypeArgumentApplier baseArgumentApplier =
-        GenericSignaturePartialTypeArgumentApplier.build(
-            objectType,
-            (enclosing, enclosed) -> hasPrunedRelationship(appView, enclosing, enclosed, wasPruned),
-            type -> hasGenericTypeVariables(appView, type, wasPruned));
-    appView
-        .appInfo()
-        .classes()
-        .forEach(
-            clazz -> {
-              if (clazz.getClassSignature().isInvalid()) {
-                return;
-              }
-              TypeParameterContext computedClassFormals =
-                  computeTypeParameterContext(appView, clazz.getType(), wasPruned, false);
-              GenericSignaturePartialTypeArgumentApplier classArgumentApplier =
-                  baseArgumentApplier.addSubstitutionsAndVariables(
-                      computedClassFormals.prunedParametersWithBounds,
-                      computedClassFormals.liveParameters);
-              clazz.setClassSignature(
-                  classArgumentApplier.visitClassSignature(clazz.getClassSignature()));
-              clazz
-                  .methods()
-                  .forEach(
-                      method -> {
-                        MethodTypeSignature methodSignature = method.getGenericSignature();
-                        if (methodSignature.hasSignature()
-                            && method.getGenericSignature().isValid()) {
-                          // The reflection api do not distinguish static methods context from
-                          // virtual methods.
-                          method.setGenericSignature(
-                              classArgumentApplier
-                                  .buildForMethod(methodSignature.getFormalTypeParameters())
-                                  .visitMethodSignature(methodSignature));
-                        }
-                      });
-              clazz
-                  .instanceFields()
-                  .forEach(
-                      field -> {
-                        if (field.getGenericSignature().hasSignature()
-                            && field.getGenericSignature().isValid()) {
-                          field.setGenericSignature(
-                              classArgumentApplier.visitFieldTypeSignature(
-                                  field.getGenericSignature()));
-                        }
-                      });
-            });
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignatureCorrectnessHelper.java b/src/main/java/com/android/tools/r8/graph/GenericSignatureCorrectnessHelper.java
index 3401560..7dcfb37 100644
--- a/src/main/java/com/android/tools/r8/graph/GenericSignatureCorrectnessHelper.java
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignatureCorrectnessHelper.java
@@ -9,7 +9,9 @@
 import static com.android.tools.r8.graph.GenericSignatureCorrectnessHelper.SignatureEvaluationResult.INVALID_SUPER_TYPE;
 import static com.android.tools.r8.graph.GenericSignatureCorrectnessHelper.SignatureEvaluationResult.INVALID_TYPE_VARIABLE_UNDEFINED;
 import static com.android.tools.r8.graph.GenericSignatureCorrectnessHelper.SignatureEvaluationResult.VALID;
+import static com.google.common.base.Predicates.alwaysFalse;
 
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.GenericSignature.ClassSignature;
 import com.android.tools.r8.graph.GenericSignature.ClassTypeSignature;
 import com.android.tools.r8.graph.GenericSignature.DexDefinitionSignature;
@@ -18,9 +20,9 @@
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
 import com.android.tools.r8.graph.GenericSignature.ReturnType;
 import com.android.tools.r8.graph.GenericSignature.TypeSignature;
-import java.util.HashSet;
+import com.android.tools.r8.graph.GenericSignatureContextBuilder.TypeParameterContext;
+import com.android.tools.r8.utils.ListUtils;
 import java.util.List;
-import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Supplier;
@@ -29,90 +31,163 @@
 
   private enum Mode {
     VERIFY,
-    MARK_AS_INVALID;
+    CLEAR_IF_INVALID;
 
     public boolean doNotVerify() {
-      return markAsInvalid();
+      return clearIfInvalid();
     }
 
-    public boolean markAsInvalid() {
-      return this == MARK_AS_INVALID;
+    public boolean clearIfInvalid() {
+      return this == CLEAR_IF_INVALID;
     }
   }
 
   public enum SignatureEvaluationResult {
     INVALID_SUPER_TYPE,
+    INVALID_INTERFACE_TYPE,
     INVALID_INTERFACE_COUNT,
     INVALID_APPLICATION_COUNT,
     INVALID_TYPE_VARIABLE_UNDEFINED,
     VALID;
 
-    boolean isValid() {
+    public boolean isValid() {
       return this == VALID;
     }
 
-    boolean isInvalid() {
+    public boolean isInvalid() {
       return this != VALID;
     }
+
+    public SignatureEvaluationResult combine(SignatureEvaluationResult other) {
+      return isInvalid() ? this : other;
+    }
+
+    public String getDescription() {
+      switch (this) {
+        case INVALID_APPLICATION_COUNT:
+          return "The applied generic arguments have different count than the expected formals";
+        case INVALID_INTERFACE_COUNT:
+          return "The generic signature has a different number of interfaces than the class";
+        case INVALID_SUPER_TYPE:
+          return "The generic super type is not the same as the class super type";
+        case INVALID_TYPE_VARIABLE_UNDEFINED:
+          return "A type variable is not in scope";
+        default:
+          assert this.isValid();
+          throw new Unreachable("Should not throw an error for a valid signature");
+      }
+    }
   }
 
   private final AppView<?> appView;
   private final Mode mode;
+  private final GenericSignatureContextBuilder contextBuilder;
 
-  private GenericSignatureCorrectnessHelper(AppView<?> appView, Mode mode) {
+  private GenericSignatureCorrectnessHelper(
+      AppView<?> appView, GenericSignatureContextBuilder contextBuilder, Mode mode) {
     this.appView = appView;
+    this.contextBuilder = contextBuilder;
     this.mode = mode;
   }
 
-  public static GenericSignatureCorrectnessHelper createForInitialCheck(AppView<?> appView) {
-    return new GenericSignatureCorrectnessHelper(appView, Mode.MARK_AS_INVALID);
+  public static GenericSignatureCorrectnessHelper createForInitialCheck(
+      AppView<?> appView, GenericSignatureContextBuilder contextBuilder) {
+    return new GenericSignatureCorrectnessHelper(appView, contextBuilder, Mode.CLEAR_IF_INVALID);
   }
 
-  public static GenericSignatureCorrectnessHelper createForVerification(AppView<?> appView) {
-    return new GenericSignatureCorrectnessHelper(appView, Mode.VERIFY);
+  public static GenericSignatureCorrectnessHelper createForVerification(
+      AppView<?> appView, GenericSignatureContextBuilder contextBuilder) {
+    return new GenericSignatureCorrectnessHelper(appView, contextBuilder, Mode.VERIFY);
   }
 
-  public void run() {
-    appView.appInfo().classes().forEach(this::evaluateSignaturesForClass);
+  public SignatureEvaluationResult run(List<DexProgramClass> programClasses) {
+    if (appView.options().disableGenericSignatureValidation) {
+      return VALID;
+    }
+    for (DexProgramClass clazz : programClasses) {
+      SignatureEvaluationResult evaluationResult = evaluateSignaturesForClass(clazz);
+      if (evaluationResult.isInvalid()) {
+        return evaluationResult;
+      }
+    }
+    return VALID;
   }
 
   public SignatureEvaluationResult evaluateSignaturesForClass(DexProgramClass clazz) {
+    if (appView.options().disableGenericSignatureValidation) {
+      return VALID;
+    }
+
+    TypeParameterContext typeParameterContext =
+        contextBuilder.computeTypeParameterContext(appView, clazz.type, alwaysFalse());
+
     GenericSignatureContextEvaluator genericSignatureContextEvaluator =
-        new GenericSignatureContextEvaluator(appView, clazz, mode);
-    ClassSignature classSignature = clazz.getClassSignature();
-    SignatureEvaluationResult result = VALID;
-    if (classSignature.hasNoSignature() || !classSignature.isInvalid()) {
-      result = genericSignatureContextEvaluator.evaluateClassSignature(classSignature);
-      if (result.isInvalid() && mode.markAsInvalid()) {
-        clazz.setClassSignature(classSignature.toInvalid());
-      }
+        new GenericSignatureContextEvaluator(appView, mode, clazz);
+
+    SignatureEvaluationResult result =
+        genericSignatureContextEvaluator.evaluateClassSignatureForContext(typeParameterContext);
+    if (result.isInvalid() && mode.clearIfInvalid()) {
+      appView
+          .options()
+          .reporter
+          .info(
+              GenericSignatureValidationDiagnostic.invalidClassSignature(
+                  clazz.getClassSignature().toString(),
+                  clazz.getTypeName(),
+                  clazz.getOrigin(),
+                  result));
+      clazz.clearClassSignature();
     }
     for (DexEncodedMethod method : clazz.methods()) {
-      SignatureEvaluationResult methodResult =
-          evaluate(
-              method::getGenericSignature,
-              genericSignatureContextEvaluator::visitMethodSignature,
-              method::setGenericSignature);
-      if (result.isValid() && methodResult.isInvalid()) {
-        result = methodResult;
-      }
+      result =
+          result.combine(
+              evaluate(
+                  method::getGenericSignature,
+                  methodSignature ->
+                      genericSignatureContextEvaluator.visitMethodSignature(
+                          methodSignature, typeParameterContext),
+                  invalidResult -> {
+                    appView
+                        .options()
+                        .reporter
+                        .info(
+                            GenericSignatureValidationDiagnostic.invalidMethodSignature(
+                                method.getGenericSignature().toString(),
+                                method.toSourceString(),
+                                clazz.getOrigin(),
+                                invalidResult));
+                    method.clearGenericSignature();
+                  }));
     }
     for (DexEncodedField field : clazz.fields()) {
-      SignatureEvaluationResult fieldResult =
-          evaluate(
-              field::getGenericSignature,
-              genericSignatureContextEvaluator::visitFieldTypeSignature,
-              field::setGenericSignature);
-      if (result.isValid() && fieldResult.isInvalid()) {
-        result = fieldResult;
-      }
+      result =
+          result.combine(
+              evaluate(
+                  field::getGenericSignature,
+                  fieldSignature ->
+                      genericSignatureContextEvaluator.visitFieldTypeSignature(
+                          fieldSignature, typeParameterContext),
+                  invalidResult -> {
+                    appView
+                        .options()
+                        .reporter
+                        .info(
+                            GenericSignatureValidationDiagnostic.invalidFieldSignature(
+                                field.getGenericSignature().toString(),
+                                field.toSourceString(),
+                                clazz.getOrigin(),
+                                invalidResult));
+                    field.clearGenericSignature();
+                  }));
     }
     return result;
   }
 
   @SuppressWarnings("unchecked")
   private <T extends DexDefinitionSignature<?>> SignatureEvaluationResult evaluate(
-      Supplier<T> getter, Function<T, SignatureEvaluationResult> evaluate, Consumer<T> setter) {
+      Supplier<T> getter,
+      Function<T, SignatureEvaluationResult> evaluate,
+      Consumer<SignatureEvaluationResult> invalidAction) {
     T signature = getter.get();
     if (signature.hasNoSignature() || signature.isInvalid()) {
       // Already marked as invalid, do nothing
@@ -120,8 +195,8 @@
     }
     SignatureEvaluationResult signatureResult = evaluate.apply(signature);
     assert signatureResult.isValid() || mode.doNotVerify();
-    if (signatureResult.isInvalid() && mode.doNotVerify()) {
-      setter.accept((T) signature.toInvalid());
+    if (signatureResult.isInvalid() && mode.clearIfInvalid()) {
+      invalidAction.accept(signatureResult);
     }
     return signatureResult;
   }
@@ -130,39 +205,39 @@
 
     private final AppView<?> appView;
     private final DexProgramClass context;
-    private final Set<String> classFormalTypeParameters = new HashSet<>();
-    private final Set<String> methodTypeArguments = new HashSet<>();
     private final Mode mode;
 
-    public GenericSignatureContextEvaluator(
-        AppView<?> appView, DexProgramClass context, Mode mode) {
+    private GenericSignatureContextEvaluator(
+        AppView<?> appView, Mode mode, DexProgramClass context) {
       this.appView = appView;
-      this.context = context;
       this.mode = mode;
+      this.context = context;
     }
 
-    private SignatureEvaluationResult evaluateClassSignature(ClassSignature classSignature) {
-      classSignature
-          .getFormalTypeParameters()
-          .forEach(param -> classFormalTypeParameters.add(param.name));
-      if (classSignature.hasNoSignature()) {
+    private SignatureEvaluationResult evaluateClassSignatureForContext(
+        TypeParameterContext typeParameterContext) {
+      ClassSignature classSignature = context.classSignature;
+      if (classSignature.hasNoSignature() || classSignature.isInvalid()) {
         return VALID;
       }
       SignatureEvaluationResult signatureEvaluationResult =
-          evaluateFormalTypeParameters(classSignature.formalTypeParameters);
+          evaluateFormalTypeParameters(classSignature.formalTypeParameters, typeParameterContext);
       if (signatureEvaluationResult.isInvalid()) {
         return signatureEvaluationResult;
       }
-      if ((context.superType != appView.dexItemFactory().objectType
-              && context.superType != classSignature.superClassSignature().type())
-          || (context.superType == appView.dexItemFactory().objectType
-              && classSignature.superClassSignature().hasNoSignature())) {
+      if (context.superType == appView.dexItemFactory().objectType
+          && classSignature.superClassSignature().hasNoSignature()) {
+        // We represent no signature as object.
+      } else if (context.superType
+          != appView.graphLens().lookupClassType(classSignature.superClassSignature().type())) {
         assert mode.doNotVerify();
         return INVALID_SUPER_TYPE;
       }
       signatureEvaluationResult =
           evaluateTypeArgumentsAppliedToType(
-              classSignature.superClassSignature().typeArguments(), context.superType);
+              classSignature.superClassSignature().typeArguments(),
+              context.superType,
+              typeParameterContext);
       if (signatureEvaluationResult.isInvalid()) {
         return signatureEvaluationResult;
       }
@@ -175,7 +250,7 @@
       for (int i = 0; i < actualInterfaces.length; i++) {
         signatureEvaluationResult =
             evaluateTypeArgumentsAppliedToType(
-                superInterfaces.get(i).typeArguments(), actualInterfaces[i]);
+                superInterfaces.get(i).typeArguments(), actualInterfaces[i], typeParameterContext);
         if (signatureEvaluationResult.isInvalid()) {
           return signatureEvaluationResult;
         }
@@ -183,37 +258,46 @@
       return VALID;
     }
 
-    private SignatureEvaluationResult visitMethodSignature(MethodTypeSignature methodSignature) {
-      methodSignature
-          .getFormalTypeParameters()
-          .forEach(param -> methodTypeArguments.add(param.name));
+    private SignatureEvaluationResult visitMethodSignature(
+        MethodTypeSignature methodSignature, TypeParameterContext typeParameterContext) {
+      // If the class context is invalid, we cannot reason about the method signatures.
+      if (context.classSignature.isInvalid()) {
+        return VALID;
+      }
+      TypeParameterContext methodContext =
+          methodSignature.formalTypeParameters.isEmpty()
+              ? typeParameterContext
+              : typeParameterContext.addLiveParameters(
+                  ListUtils.map(
+                      methodSignature.getFormalTypeParameters(), FormalTypeParameter::getName));
       SignatureEvaluationResult evaluateResult =
-          evaluateFormalTypeParameters(methodSignature.getFormalTypeParameters());
+          evaluateFormalTypeParameters(methodSignature.getFormalTypeParameters(), methodContext);
       if (evaluateResult.isInvalid()) {
         return evaluateResult;
       }
-      evaluateResult = evaluateTypeArguments(methodSignature.typeSignatures);
+      evaluateResult = evaluateTypeArguments(methodSignature.typeSignatures, methodContext);
       if (evaluateResult.isInvalid()) {
         return evaluateResult;
       }
-      evaluateResult = evaluateTypeArguments(methodSignature.throwsSignatures);
+      evaluateResult = evaluateTypeArguments(methodSignature.throwsSignatures, methodContext);
       if (evaluateResult.isInvalid()) {
         return evaluateResult;
       }
       ReturnType returnType = methodSignature.returnType();
       if (!returnType.isVoidDescriptor()) {
-        evaluateResult = evaluateTypeArgument(returnType.typeSignature());
+        evaluateResult = evaluateTypeArgument(returnType.typeSignature(), methodContext);
         if (evaluateResult.isInvalid()) {
           return evaluateResult;
         }
       }
-      methodTypeArguments.clear();
       return evaluateResult;
     }
 
-    private SignatureEvaluationResult evaluateTypeArguments(List<TypeSignature> typeSignatures) {
+    private SignatureEvaluationResult evaluateTypeArguments(
+        List<TypeSignature> typeSignatures, TypeParameterContext typeParameterContext) {
       for (TypeSignature typeSignature : typeSignatures) {
-        SignatureEvaluationResult signatureEvaluationResult = evaluateTypeArgument(typeSignature);
+        SignatureEvaluationResult signatureEvaluationResult =
+            evaluateTypeArgument(typeSignature, typeParameterContext);
         if (signatureEvaluationResult.isInvalid()) {
           return signatureEvaluationResult;
         }
@@ -221,14 +305,20 @@
       return VALID;
     }
 
-    private SignatureEvaluationResult visitFieldTypeSignature(FieldTypeSignature fieldSignature) {
-      return evaluateTypeArgument(fieldSignature);
+    private SignatureEvaluationResult visitFieldTypeSignature(
+        FieldTypeSignature fieldSignature, TypeParameterContext typeParameterContext) {
+      // If the class context is invalid, we cannot reason about the method signatures.
+      if (context.classSignature.isInvalid()) {
+        return VALID;
+      }
+      return evaluateTypeArgument(fieldSignature, typeParameterContext);
     }
 
     private SignatureEvaluationResult evaluateFormalTypeParameters(
-        List<FormalTypeParameter> typeParameters) {
+        List<FormalTypeParameter> typeParameters, TypeParameterContext typeParameterContext) {
       for (FormalTypeParameter typeParameter : typeParameters) {
-        SignatureEvaluationResult evaluationResult = evaluateTypeParameter(typeParameter);
+        SignatureEvaluationResult evaluationResult =
+            evaluateTypeParameter(typeParameter, typeParameterContext);
         if (evaluationResult.isInvalid()) {
           return evaluationResult;
         }
@@ -236,66 +326,79 @@
       return VALID;
     }
 
-    private SignatureEvaluationResult evaluateTypeParameter(FormalTypeParameter typeParameter) {
-      SignatureEvaluationResult evaluationResult = evaluateTypeArgument(typeParameter.classBound);
+    private SignatureEvaluationResult evaluateTypeParameter(
+        FormalTypeParameter typeParameter, TypeParameterContext typeParameterContext) {
+      SignatureEvaluationResult evaluationResult =
+          evaluateTypeArgument(typeParameter.classBound, typeParameterContext);
       if (evaluationResult.isInvalid()) {
         return evaluationResult;
       }
-      if (typeParameter.interfaceBounds != null) {
-        for (FieldTypeSignature interfaceBound : typeParameter.interfaceBounds) {
-          evaluationResult = evaluateTypeArgument(interfaceBound);
-          if (evaluationResult != VALID) {
-            return evaluationResult;
-          }
+      for (FieldTypeSignature interfaceBound : typeParameter.interfaceBounds) {
+        evaluationResult = evaluateTypeArgument(interfaceBound, typeParameterContext);
+        if (evaluationResult != VALID) {
+          return evaluationResult;
         }
       }
       return VALID;
     }
 
-    private SignatureEvaluationResult evaluateTypeArgument(TypeSignature typeSignature) {
+    private SignatureEvaluationResult evaluateTypeArgument(
+        TypeSignature typeSignature, TypeParameterContext typeParameterContext) {
       if (typeSignature.isBaseTypeSignature()) {
         return VALID;
       }
       FieldTypeSignature fieldTypeSignature = typeSignature.asFieldTypeSignature();
-      if (fieldTypeSignature.hasNoSignature()) {
+      if (fieldTypeSignature.hasNoSignature() || fieldTypeSignature.isStar()) {
         return VALID;
       }
       if (fieldTypeSignature.isTypeVariableSignature()) {
         // This is in an applied position, just check that the variable is registered.
         String typeVariable = fieldTypeSignature.asTypeVariableSignature().typeVariable();
-        if (classFormalTypeParameters.contains(typeVariable)
-            || methodTypeArguments.contains(typeVariable)) {
+        if (typeParameterContext.isLiveParameter(typeVariable)) {
           return VALID;
         }
         assert mode.doNotVerify();
         return INVALID_TYPE_VARIABLE_UNDEFINED;
       }
       if (fieldTypeSignature.isArrayTypeSignature()) {
-        return evaluateTypeArgument(fieldTypeSignature.asArrayTypeSignature().elementSignature());
+        return evaluateTypeArgument(
+            fieldTypeSignature.asArrayTypeSignature().elementSignature(), typeParameterContext);
       }
       assert fieldTypeSignature.isClassTypeSignature();
-      return evaluateTypeArguments(fieldTypeSignature.asClassTypeSignature());
+      return evaluateTypeArguments(fieldTypeSignature.asClassTypeSignature(), typeParameterContext);
     }
 
-    private SignatureEvaluationResult evaluateTypeArguments(ClassTypeSignature classTypeSignature) {
+    private SignatureEvaluationResult evaluateTypeArguments(
+        ClassTypeSignature classTypeSignature, TypeParameterContext typeParameterContext) {
       return evaluateTypeArgumentsAppliedToType(
-          classTypeSignature.typeArguments, classTypeSignature.type());
+          classTypeSignature.typeArguments, classTypeSignature.type(), typeParameterContext);
     }
 
     private SignatureEvaluationResult evaluateTypeArgumentsAppliedToType(
-        List<FieldTypeSignature> typeArguments, DexType type) {
+        List<FieldTypeSignature> typeArguments,
+        DexType type,
+        TypeParameterContext typeParameterContext) {
       for (FieldTypeSignature typeArgument : typeArguments) {
-        SignatureEvaluationResult evaluationResult = evaluateTypeArgument(typeArgument);
+        SignatureEvaluationResult evaluationResult =
+            evaluateTypeArgument(typeArgument, typeParameterContext);
         if (evaluationResult.isInvalid()) {
           assert mode.doNotVerify();
           return evaluationResult;
         }
       }
-      DexClass clazz = appView.definitionFor(type);
+      // TODO(b/187035453): We should visit generic signatures in the enqueuer.
+      DexClass clazz =
+          appView
+              .appInfo()
+              .definitionForWithoutExistenceAssert(appView.graphLens().lookupClassType(type));
       if (clazz == null) {
         // We do not know if the application of arguments works or not.
         return VALID;
       }
+      if (typeArguments.isEmpty()) {
+        // When type arguments are empty we are using the raw type.
+        return VALID;
+      }
       if (typeArguments.size() != clazz.classSignature.getFormalTypeParameters().size()) {
         assert mode.doNotVerify();
         return INVALID_APPLICATION_COUNT;
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignatureDiagnostic.java b/src/main/java/com/android/tools/r8/graph/GenericSignatureFormatDiagnostic.java
similarity index 78%
rename from src/main/java/com/android/tools/r8/graph/GenericSignatureDiagnostic.java
rename to src/main/java/com/android/tools/r8/graph/GenericSignatureFormatDiagnostic.java
index fa95bce..6729890 100644
--- a/src/main/java/com/android/tools/r8/graph/GenericSignatureDiagnostic.java
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignatureFormatDiagnostic.java
@@ -9,13 +9,13 @@
 import com.android.tools.r8.position.Position;
 import java.lang.reflect.GenericSignatureFormatError;
 
-public class GenericSignatureDiagnostic implements Diagnostic {
+public class GenericSignatureFormatDiagnostic implements Diagnostic {
 
   private final Origin origin;
   private final Position position;
   private final String message;
 
-  GenericSignatureDiagnostic(Origin origin, Position position, String message) {
+  GenericSignatureFormatDiagnostic(Origin origin, Position position, String message) {
     this.origin = origin;
     this.position = position;
     this.message = message;
@@ -36,22 +36,22 @@
     return message;
   }
 
-  static GenericSignatureDiagnostic invalidClassSignature(
+  static GenericSignatureFormatDiagnostic invalidClassSignature(
       String signature, String name, Origin origin, GenericSignatureFormatError error) {
     return invalidSignature(signature, "class", name, origin, error);
   }
 
-  static GenericSignatureDiagnostic invalidMethodSignature(
+  static GenericSignatureFormatDiagnostic invalidMethodSignature(
       String signature, String name, Origin origin, GenericSignatureFormatError error) {
     return invalidSignature(signature, "method", name, origin, error);
   }
 
-  static GenericSignatureDiagnostic invalidFieldSignature(
+  static GenericSignatureFormatDiagnostic invalidFieldSignature(
       String signature, String name, Origin origin, GenericSignatureFormatError error) {
     return invalidSignature(signature, "field", name, origin, error);
   }
 
-  private static GenericSignatureDiagnostic invalidSignature(
+  private static GenericSignatureFormatDiagnostic invalidSignature(
       String signature,
       String kind,
       String name,
@@ -70,6 +70,6 @@
             + System.lineSeparator()
             + "Parser error: "
             + error.getMessage();
-    return new GenericSignatureDiagnostic(origin, Position.UNKNOWN, message);
+    return new GenericSignatureFormatDiagnostic(origin, Position.UNKNOWN, message);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignaturePartialTypeArgumentApplier.java b/src/main/java/com/android/tools/r8/graph/GenericSignaturePartialTypeArgumentApplier.java
index c381074..56a6496 100644
--- a/src/main/java/com/android/tools/r8/graph/GenericSignaturePartialTypeArgumentApplier.java
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignaturePartialTypeArgumentApplier.java
@@ -14,52 +14,37 @@
 import com.android.tools.r8.graph.GenericSignature.ReturnType;
 import com.android.tools.r8.graph.GenericSignature.TypeSignature;
 import com.android.tools.r8.graph.GenericSignature.WildcardIndicator;
+import com.android.tools.r8.graph.GenericSignatureContextBuilder.TypeParameterContext;
 import com.android.tools.r8.utils.ListUtils;
-import com.google.common.collect.ImmutableSet;
-import java.util.Collections;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
 import java.util.function.BiPredicate;
 import java.util.function.Predicate;
 
 public class GenericSignaturePartialTypeArgumentApplier implements GenericSignatureVisitor {
 
-  private final Map<String, DexType> substitutions;
-  private final Set<String> liveTypeVariables;
-  private final DexType objectType;
+  private final TypeParameterContext typeParameterContext;
   private final BiPredicate<DexType, DexType> enclosingPruned;
   private final Predicate<DexType> hasGenericTypeParameters;
+  private final AppView<?> appView;
 
   private GenericSignaturePartialTypeArgumentApplier(
-      Map<String, DexType> substitutions,
-      Set<String> liveTypeVariables,
-      DexType objectType,
+      AppView<?> appView,
+      TypeParameterContext typeParameterContext,
       BiPredicate<DexType, DexType> enclosingPruned,
       Predicate<DexType> hasGenericTypeParameters) {
-    this.substitutions = substitutions;
-    this.liveTypeVariables = liveTypeVariables;
-    this.objectType = objectType;
+    this.appView = appView;
+    this.typeParameterContext = typeParameterContext;
     this.enclosingPruned = enclosingPruned;
     this.hasGenericTypeParameters = hasGenericTypeParameters;
   }
 
   public static GenericSignaturePartialTypeArgumentApplier build(
-      DexType objectType,
+      AppView<?> appView,
+      TypeParameterContext typeParameterContext,
       BiPredicate<DexType, DexType> enclosingPruned,
       Predicate<DexType> hasGenericTypeParameters) {
     return new GenericSignaturePartialTypeArgumentApplier(
-        Collections.emptyMap(),
-        Collections.emptySet(),
-        objectType,
-        enclosingPruned,
-        hasGenericTypeParameters);
-  }
-
-  public GenericSignaturePartialTypeArgumentApplier addSubstitutionsAndVariables(
-      Map<String, DexType> substitutions, Set<String> liveTypeVariables) {
-    return new GenericSignaturePartialTypeArgumentApplier(
-        substitutions, liveTypeVariables, objectType, enclosingPruned, hasGenericTypeParameters);
+        appView, typeParameterContext, enclosingPruned, hasGenericTypeParameters);
   }
 
   public GenericSignaturePartialTypeArgumentApplier buildForMethod(
@@ -67,23 +52,27 @@
     if (formals.isEmpty()) {
       return this;
     }
-    ImmutableSet.Builder<String> liveVariablesBuilder = ImmutableSet.builder();
-    liveVariablesBuilder.addAll(liveTypeVariables);
-    formals.forEach(
-        formal -> {
-          liveVariablesBuilder.add(formal.name);
-        });
     return new GenericSignaturePartialTypeArgumentApplier(
-        substitutions, liveTypeVariables, objectType, enclosingPruned, hasGenericTypeParameters);
+        appView,
+        typeParameterContext.addLiveParameters(
+            ListUtils.map(formals, FormalTypeParameter::getName)),
+        enclosingPruned,
+        hasGenericTypeParameters);
   }
 
   @Override
   public ClassSignature visitClassSignature(ClassSignature classSignature) {
+    if (classSignature.hasNoSignature() || classSignature.isInvalid()) {
+      return classSignature;
+    }
     return classSignature.visit(this);
   }
 
   @Override
   public MethodTypeSignature visitMethodSignature(MethodTypeSignature methodSignature) {
+    if (methodSignature.hasNoSignature() || methodSignature.isInvalid()) {
+      return methodSignature;
+    }
     return methodSignature.visit(this);
   }
 
@@ -111,7 +100,7 @@
 
   @Override
   public List<FieldTypeSignature> visitInterfaceBounds(List<FieldTypeSignature> fieldSignatures) {
-    if (fieldSignatures == null || fieldSignatures.isEmpty()) {
+    if (fieldSignatures.isEmpty()) {
       return fieldSignatures;
     }
     return ListUtils.mapOrElse(fieldSignatures, this::visitFieldTypeSignature);
@@ -142,6 +131,9 @@
 
   @Override
   public FieldTypeSignature visitClassBound(FieldTypeSignature fieldSignature) {
+    if (fieldSignature.hasNoSignature()) {
+      return fieldSignature;
+    }
     return visitFieldTypeSignature(fieldSignature);
   }
 
@@ -205,6 +197,9 @@
 
   @Override
   public FieldTypeSignature visitFieldTypeSignature(FieldTypeSignature fieldSignature) {
+    if (fieldSignature.hasNoSignature() || fieldSignature.isInvalid()) {
+      return fieldSignature;
+    }
     if (fieldSignature.isStar()) {
       return fieldSignature;
     } else if (fieldSignature.isClassTypeSignature()) {
@@ -214,11 +209,10 @@
     } else {
       assert fieldSignature.isTypeVariableSignature();
       String typeVariableName = fieldSignature.asTypeVariableSignature().typeVariable();
-      if (substitutions.containsKey(typeVariableName)
-          && !liveTypeVariables.contains(typeVariableName)) {
-        DexType substitution = substitutions.get(typeVariableName);
+      if (!typeParameterContext.isLiveParameter(typeVariableName)) {
+        DexType substitution = typeParameterContext.getPrunedSubstitution(typeVariableName);
         if (substitution == null) {
-          substitution = objectType;
+          substitution = appView.dexItemFactory().objectType;
         }
         return new ClassTypeSignature(substitution).asArgument(WildcardIndicator.NONE);
       }
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignaturePrinter.java b/src/main/java/com/android/tools/r8/graph/GenericSignaturePrinter.java
index d04e3af..b111c57 100644
--- a/src/main/java/com/android/tools/r8/graph/GenericSignaturePrinter.java
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignaturePrinter.java
@@ -87,15 +87,15 @@
   @Override
   public FieldTypeSignature visitClassBound(FieldTypeSignature fieldSignature) {
     sb.append(":");
+    if (fieldSignature.hasNoSignature()) {
+      return fieldSignature;
+    }
     printFieldTypeSignature(fieldSignature, false);
     return fieldSignature;
   }
 
   @Override
   public List<FieldTypeSignature> visitInterfaceBounds(List<FieldTypeSignature> fieldSignatures) {
-    if (fieldSignatures == null) {
-      return null;
-    }
     fieldSignatures.forEach(this::visitInterfaceBound);
     return fieldSignatures;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeRewriter.java b/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeRewriter.java
index da35877..332aa8d 100644
--- a/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeRewriter.java
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeRewriter.java
@@ -212,12 +212,15 @@
 
     @Override
     public FieldTypeSignature visitClassBound(FieldTypeSignature fieldSignature) {
+      if (fieldSignature.hasNoSignature()) {
+        return fieldSignature;
+      }
       return visitFieldTypeSignature(fieldSignature);
     }
 
     @Override
     public List<FieldTypeSignature> visitInterfaceBounds(List<FieldTypeSignature> fieldSignatures) {
-      if (fieldSignatures == null || fieldSignatures.isEmpty()) {
+      if (fieldSignatures.isEmpty()) {
         return fieldSignatures;
       }
       return ListUtils.mapOrElse(fieldSignatures, this::visitFieldTypeSignature);
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeVisitor.java b/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeVisitor.java
index c6d0e6a..59cd879 100644
--- a/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeVisitor.java
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeVisitor.java
@@ -69,14 +69,14 @@
 
   @Override
   public FieldTypeSignature visitClassBound(FieldTypeSignature fieldSignature) {
+    if (fieldSignature.hasNoSignature()) {
+      return fieldSignature;
+    }
     return visitFieldTypeSignature(fieldSignature);
   }
 
   @Override
   public List<FieldTypeSignature> visitInterfaceBounds(List<FieldTypeSignature> fieldSignatures) {
-    if (fieldSignatures == null) {
-      return null;
-    }
     fieldSignatures.forEach(this::visitInterfaceBound);
     return fieldSignatures;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignatureValidationDiagnostic.java b/src/main/java/com/android/tools/r8/graph/GenericSignatureValidationDiagnostic.java
new file mode 100644
index 0000000..4a85abd
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignatureValidationDiagnostic.java
@@ -0,0 +1,72 @@
+// 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.graph;
+
+import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.graph.GenericSignatureCorrectnessHelper.SignatureEvaluationResult;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+public class GenericSignatureValidationDiagnostic implements Diagnostic {
+
+  private final Origin origin;
+  private final Position position;
+  private final String message;
+
+  GenericSignatureValidationDiagnostic(Origin origin, Position position, String message) {
+    this.origin = origin;
+    this.position = position;
+    this.message = message;
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return origin;
+  }
+
+  @Override
+  public Position getPosition() {
+    return position;
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return message;
+  }
+
+  static GenericSignatureValidationDiagnostic invalidClassSignature(
+      String signature, String name, Origin origin, SignatureEvaluationResult error) {
+    return invalidSignature(signature, "class", name, origin, error);
+  }
+
+  static GenericSignatureValidationDiagnostic invalidMethodSignature(
+      String signature, String name, Origin origin, SignatureEvaluationResult error) {
+    return invalidSignature(signature, "method", name, origin, error);
+  }
+
+  static GenericSignatureValidationDiagnostic invalidFieldSignature(
+      String signature, String name, Origin origin, SignatureEvaluationResult error) {
+    return invalidSignature(signature, "field", name, origin, error);
+  }
+
+  private static GenericSignatureValidationDiagnostic invalidSignature(
+      String signature, String kind, String name, Origin origin, SignatureEvaluationResult error) {
+    String message =
+        "Invalid signature '"
+            + signature
+            + "' for "
+            + kind
+            + " "
+            + name
+            + "."
+            + System.lineSeparator()
+            + "Validation error: "
+            + error.getDescription()
+            + "."
+            + System.lineSeparator()
+            + "Signature is ignored and will not be present in the output.";
+    return new GenericSignatureValidationDiagnostic(origin, Position.UNKNOWN, message);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/GraphLens.java b/src/main/java/com/android/tools/r8/graph/GraphLens.java
index ff2fd57..a07a380 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLens.java
@@ -9,9 +9,12 @@
 import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.ir.desugar.itf.InterfaceProcessor.InterfaceProcessorNestedGraphLens;
+import com.android.tools.r8.optimize.MemberRebindingIdentityLens;
+import com.android.tools.r8.optimize.MemberRebindingLens;
 import com.android.tools.r8.shaking.KeepInfoCollection;
 import com.android.tools.r8.utils.Action;
 import com.android.tools.r8.utils.IterableUtils;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
@@ -457,6 +460,18 @@
     return false;
   }
 
+  public MemberRebindingLens asMemberRebindingLens() {
+    return null;
+  }
+
+  public boolean isMemberRebindingIdentityLens() {
+    return false;
+  }
+
+  public MemberRebindingIdentityLens asMemberRebindingIdentityLens() {
+    return null;
+  }
+
   public abstract boolean isNonIdentityLens();
 
   public NonIdentityGraphLens asNonIdentityLens() {
@@ -545,10 +560,34 @@
     return result;
   }
 
-  public <R extends DexReference, T> ImmutableMap<R, T> rewriteReferenceKeys(Map<R, T> map) {
-    ImmutableMap.Builder<R, T> builder = ImmutableMap.builder();
-    map.forEach((reference, value) -> builder.put(rewriteReference(reference), value));
-    return builder.build();
+  public <R extends DexReference, T> Map<R, T> rewriteReferenceKeys(
+      Map<R, T> map, Function<List<T>, T> merge) {
+    Map<R, T> result = new IdentityHashMap<>();
+    Map<R, List<T>> needsMerge = new IdentityHashMap<>();
+    map.forEach(
+        (reference, value) -> {
+          R rewrittenReference = rewriteReference(reference);
+          List<T> unmergedValues = needsMerge.get(rewrittenReference);
+          if (unmergedValues != null) {
+            unmergedValues.add(value);
+          } else {
+            T existingValue = result.put(rewrittenReference, value);
+            if (existingValue != null) {
+              // Remove this for now and let the merge function decide when all colliding values are
+              // known.
+              needsMerge.put(rewrittenReference, ListUtils.newArrayList(existingValue, value));
+              result.remove(rewrittenReference);
+            }
+          }
+        });
+    needsMerge.forEach(
+        (rewrittenReference, unmergedValues) -> {
+          T mergedValue = merge.apply(unmergedValues);
+          if (mergedValue != null) {
+            result.put(rewrittenReference, mergedValue);
+          }
+        });
+    return result;
   }
 
   public Object2BooleanMap<DexReference> rewriteReferenceKeys(Object2BooleanMap<DexReference> map) {
@@ -671,7 +710,8 @@
     }
 
     @SuppressWarnings("unchecked")
-    public final <T extends GraphLens> T findPrevious(Predicate<NonIdentityGraphLens> predicate) {
+    public final <T extends NonIdentityGraphLens> T findPrevious(
+        Predicate<NonIdentityGraphLens> predicate) {
       GraphLens current = getPrevious();
       while (current.isNonIdentityLens()) {
         NonIdentityGraphLens nonIdentityGraphLens = current.asNonIdentityLens();
@@ -707,7 +747,7 @@
 
     @Override
     public String lookupPackageName(String pkg) {
-      return pkg;
+      return getPrevious().lookupPackageName(pkg);
     }
 
     @Override
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 08bc5f0..fbefa45 100644
--- a/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/LazyCfCode.java
@@ -57,7 +57,6 @@
 import com.android.tools.r8.ir.code.NumberGenerator;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.ValueType;
-import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.MethodPosition;
@@ -228,16 +227,10 @@
       NumberGenerator valueNumberGenerator,
       Position callerPosition,
       Origin origin,
-      MethodProcessor methodProcessor) {
+      RewrittenPrototypeDescription protoChanges) {
     return asCfCode()
         .buildInliningIR(
-            context,
-            method,
-            appView,
-            valueNumberGenerator,
-            callerPosition,
-            origin,
-            methodProcessor);
+            context, method, appView, valueNumberGenerator, callerPosition, origin, protoChanges);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/graph/MethodAccessFlags.java b/src/main/java/com/android/tools/r8/graph/MethodAccessFlags.java
index 4a5c689..23bd9f7 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodAccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodAccessFlags.java
@@ -75,6 +75,10 @@
     return this;
   }
 
+  public static MethodAccessFlags createForClassInitializer() {
+    return fromSharedAccessFlags(Constants.ACC_STATIC | Constants.ACC_SYNTHETIC, true);
+  }
+
   public static MethodAccessFlags createPublicStaticSynthetic() {
     return fromSharedAccessFlags(
         Constants.ACC_PUBLIC | Constants.ACC_STATIC | Constants.ACC_SYNTHETIC, false);
diff --git a/src/main/java/com/android/tools/r8/graph/NestedGraphLens.java b/src/main/java/com/android/tools/r8/graph/NestedGraphLens.java
index 8eaa46f..81724fb 100644
--- a/src/main/java/com/android/tools/r8/graph/NestedGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/NestedGraphLens.java
@@ -62,6 +62,14 @@
     return false;
   }
 
+  public NestedGraphLens(AppView<?> appView) {
+    this(
+        appView,
+        NestedGraphLens.EMPTY_FIELD_MAP,
+        NestedGraphLens.EMPTY_METHOD_MAP,
+        NestedGraphLens.EMPTY_TYPE_MAP);
+  }
+
   public NestedGraphLens(
       AppView<?> appView,
       BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap,
diff --git a/src/main/java/com/android/tools/r8/graph/ProgramMethod.java b/src/main/java/com/android/tools/r8/graph/ProgramMethod.java
index 617f620..f4aa43b 100644
--- a/src/main/java/com/android/tools/r8/graph/ProgramMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/ProgramMethod.java
@@ -8,7 +8,6 @@
 import com.android.tools.r8.ir.code.NumberGenerator;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
-import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.kotlin.KotlinMemberLevelInfo;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.origin.Origin;
@@ -33,10 +32,10 @@
       NumberGenerator valueNumberGenerator,
       Position callerPosition,
       Origin origin,
-      MethodProcessor methodProcessor) {
+      RewrittenPrototypeDescription protoChanges) {
     Code code = getDefinition().getCode();
     return code.buildInliningIR(
-        context, this, appView, valueNumberGenerator, callerPosition, origin, methodProcessor);
+        context, this, appView, valueNumberGenerator, callerPosition, origin, protoChanges);
   }
 
   public void collectIndexedItems(
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
index 2b65950..dfd4f77 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
@@ -6,11 +6,8 @@
 
 import static com.google.common.base.Predicates.not;
 
-import com.android.tools.r8.cf.CfVersion;
-import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.Code;
 import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexDefinition;
@@ -30,7 +27,8 @@
 import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
-import com.android.tools.r8.horizontalclassmerging.code.ClassInitializerSynthesizedCode;
+import com.android.tools.r8.horizontalclassmerging.code.ClassInitializerMerger;
+import com.android.tools.r8.horizontalclassmerging.code.SyntheticClassInitializerConverter;
 import com.android.tools.r8.ir.analysis.value.NumberFromIntervalValue;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackSimple;
@@ -66,7 +64,7 @@
   private final Mode mode;
   private final MergeGroup group;
   private final DexItemFactory dexItemFactory;
-  private final ClassInitializerSynthesizedCode classInitializerSynthesizedCode;
+  private final ClassInitializerMerger classInitializerMerger;
   private final HorizontalClassMergerGraphLens.Builder lensBuilder;
 
   private final ClassMethodsBuilder classMethodsBuilder = new ClassMethodsBuilder();
@@ -74,7 +72,7 @@
   private final ClassStaticFieldsMerger classStaticFieldsMerger;
   private final ClassInstanceFieldsMerger classInstanceFieldsMerger;
   private final Collection<VirtualMethodMerger> virtualMethodMergers;
-  private final Collection<ConstructorMerger> constructorMergers;
+  private final Collection<InstanceInitializerMerger> instanceInitializerMergers;
 
   private ClassMerger(
       AppView<? extends AppInfoWithClassHierarchy> appView,
@@ -82,17 +80,17 @@
       HorizontalClassMergerGraphLens.Builder lensBuilder,
       MergeGroup group,
       Collection<VirtualMethodMerger> virtualMethodMergers,
-      Collection<ConstructorMerger> constructorMergers,
-      ClassInitializerSynthesizedCode classInitializerSynthesizedCode) {
+      Collection<InstanceInitializerMerger> instanceInitializerMergers,
+      ClassInitializerMerger classInitializerMerger) {
     this.appView = appView;
     this.mode = mode;
     this.lensBuilder = lensBuilder;
     this.group = group;
     this.virtualMethodMergers = virtualMethodMergers;
-    this.constructorMergers = constructorMergers;
+    this.instanceInitializerMergers = instanceInitializerMergers;
 
     this.dexItemFactory = appView.dexItemFactory();
-    this.classInitializerSynthesizedCode = classInitializerSynthesizedCode;
+    this.classInitializerMerger = classInitializerMerger;
     this.classStaticFieldsMerger = new ClassStaticFieldsMerger(appView, lensBuilder, group);
     this.classInstanceFieldsMerger = new ClassInstanceFieldsMerger(appView, lensBuilder, group);
 
@@ -104,42 +102,46 @@
     group.forEachSource(clazz -> classIdentifiers.put(clazz.getType(), classIdentifiers.size()));
   }
 
-  void mergeDirectMethods(SyntheticArgumentClass syntheticArgumentClass) {
-    mergeStaticClassInitializers();
+  void mergeDirectMethods(
+      SyntheticArgumentClass syntheticArgumentClass,
+      SyntheticClassInitializerConverter.Builder syntheticClassInitializerConverterBuilder) {
+    mergeStaticClassInitializers(syntheticClassInitializerConverterBuilder);
     mergeDirectMethods(group.getTarget());
     group.forEachSource(this::mergeDirectMethods);
     mergeConstructors(syntheticArgumentClass);
   }
 
-  void mergeStaticClassInitializers() {
-    if (classInitializerSynthesizedCode.isEmpty()) {
+  void mergeStaticClassInitializers(
+      SyntheticClassInitializerConverter.Builder syntheticClassInitializerConverterBuilder) {
+    if (classInitializerMerger.isEmpty()) {
       return;
     }
 
-    DexMethod newClinit = dexItemFactory.createClassInitializer(group.getTarget().getType());
-    Code code = classInitializerSynthesizedCode.getOrCreateCode(group.getTarget().getType());
-    if (!group.getTarget().hasClassInitializer()) {
-      classMethodsBuilder.addDirectMethod(
-          new DexEncodedMethod(
-              newClinit,
-              MethodAccessFlags.fromSharedAccessFlags(
-                  Constants.ACC_SYNTHETIC | Constants.ACC_STATIC, true),
-              MethodTypeSignature.noSignature(),
-              DexAnnotationSet.empty(),
-              ParameterAnnotationsList.empty(),
-              code,
-              true,
-              classInitializerSynthesizedCode.getCfVersion()));
-    } else {
-      DexEncodedMethod clinit = group.getTarget().getClassInitializer();
-      clinit.setCode(code, appView);
-      if (code.isCfCode()) {
-        CfVersion cfVersion = classInitializerSynthesizedCode.getCfVersion();
-        if (cfVersion != null) {
-          clinit.upgradeClassFileVersion(cfVersion);
-        }
-      }
-      classMethodsBuilder.addDirectMethod(clinit);
+    // Synthesize a new class initializer with a fresh synthetic original name.
+    DexMethod newMethodReference =
+        dexItemFactory.createClassInitializer(group.getTarget().getType());
+    DexMethod syntheticMethodReference =
+        newMethodReference.withName("$r8$clinit$synthetic", dexItemFactory);
+    lensBuilder.recordNewMethodSignature(syntheticMethodReference, newMethodReference, true);
+
+    DexEncodedMethod definition =
+        new DexEncodedMethod(
+            newMethodReference,
+            MethodAccessFlags.createForClassInitializer(),
+            MethodTypeSignature.noSignature(),
+            DexAnnotationSet.empty(),
+            ParameterAnnotationsList.empty(),
+            classInitializerMerger.getCode(syntheticMethodReference),
+            DexEncodedMethod.D8_R8_SYNTHESIZED,
+            classInitializerMerger.getCfVersion());
+    classMethodsBuilder.addDirectMethod(definition);
+
+    // In case we didn't synthesize CF code, we register the class initializer for conversion to dex
+    // after merging.
+    if (!definition.getCode().isCfCode()) {
+      assert appView.options().isGeneratingDex();
+      assert mode.isFinal();
+      syntheticClassInitializerConverterBuilder.add(group);
     }
   }
 
@@ -182,13 +184,10 @@
   }
 
   void mergeConstructors(SyntheticArgumentClass syntheticArgumentClass) {
-    constructorMergers.forEach(
+    instanceInitializerMergers.forEach(
         merger ->
             merger.merge(
-                classMethodsBuilder,
-                lensBuilder,
-                classIdentifiers,
-                syntheticArgumentClass));
+                classMethodsBuilder, lensBuilder, classIdentifiers, syntheticArgumentClass));
   }
 
   void mergeVirtualMethods() {
@@ -270,15 +269,14 @@
   }
 
   private void mergeInterfaces() {
-    DexTypeList previousInterfaces = group.getTarget().getInterfaces();
-    Set<DexType> interfaces = Sets.newLinkedHashSet(previousInterfaces);
+    Set<DexType> interfaces = Sets.newLinkedHashSet();
     if (group.isInterfaceGroup()) {
       // Add all implemented interfaces from the merge group to the target class, ignoring
       // implemented interfaces that are part of the merge group.
       Set<DexType> groupTypes =
           SetUtils.newImmutableSet(
               builder -> group.forEach(clazz -> builder.accept(clazz.getType())));
-      group.forEachSource(
+      group.forEach(
           clazz -> {
             for (DexType itf : clazz.getInterfaces()) {
               if (!groupTypes.contains(itf)) {
@@ -288,7 +286,7 @@
           });
     } else {
       // Add all implemented interfaces from the merge group to the target class.
-      group.forEachSource(clazz -> Iterables.addAll(interfaces, clazz.getInterfaces()));
+      group.forEach(clazz -> Iterables.addAll(interfaces, clazz.getInterfaces()));
     }
     group.getTarget().setInterfaces(DexTypeList.create(interfaces));
   }
@@ -302,7 +300,9 @@
     group.getTarget().setInstanceFields(classInstanceFieldsMerger.merge());
   }
 
-  public void mergeGroup(SyntheticArgumentClass syntheticArgumentClass) {
+  public void mergeGroup(
+      SyntheticArgumentClass syntheticArgumentClass,
+      SyntheticClassInitializerConverter.Builder syntheticClassInitializerConverterBuilder) {
     fixAccessFlags();
     fixNestMemberAttributes();
 
@@ -314,7 +314,7 @@
     mergeInterfaces();
 
     mergeVirtualMethods();
-    mergeDirectMethods(syntheticArgumentClass);
+    mergeDirectMethods(syntheticArgumentClass, syntheticClassInitializerConverterBuilder);
     classMethodsBuilder.setClassMethods(group.getTarget());
 
     mergeStaticFields();
@@ -355,25 +355,25 @@
           target = current;
         }
       }
-      group.setTarget(appView.testing().horizontalClassMergingTarget.apply(candidates, target));
+      group.setTarget(
+          appView.testing().horizontalClassMergingTarget.apply(appView, candidates, target));
     }
 
-    private ClassInitializerSynthesizedCode createClassInitializerMerger() {
-      ClassInitializerSynthesizedCode.Builder builder =
-          new ClassInitializerSynthesizedCode.Builder();
+    private ClassInitializerMerger createClassInitializerMerger() {
+      ClassInitializerMerger.Builder builder = new ClassInitializerMerger.Builder();
       group.forEach(
           clazz -> {
             if (clazz.hasClassInitializer()) {
-              builder.add(clazz.getClassInitializer());
+              builder.add(clazz.getProgramClassInitializer());
             }
           });
       return builder.build();
     }
 
-    private List<ConstructorMerger> createInstanceInitializerMergers() {
-      List<ConstructorMerger> constructorMergers = new ArrayList<>();
+    private List<InstanceInitializerMerger> createInstanceInitializerMergers() {
+      List<InstanceInitializerMerger> instanceInitializerMergers = new ArrayList<>();
       if (appView.options().horizontalClassMergerOptions().isConstructorMergingEnabled()) {
-        Map<DexProto, ConstructorMerger.Builder> buildersByProto = new LinkedHashMap<>();
+        Map<DexProto, InstanceInitializerMerger.Builder> buildersByProto = new LinkedHashMap<>();
         group.forEach(
             clazz ->
                 clazz.forEachProgramDirectMethodMatching(
@@ -382,10 +382,10 @@
                         buildersByProto
                             .computeIfAbsent(
                                 method.getDefinition().getProto(),
-                                ignore -> new ConstructorMerger.Builder(appView))
-                            .add(method.getDefinition())));
-        for (ConstructorMerger.Builder builder : buildersByProto.values()) {
-          constructorMergers.addAll(builder.build(group));
+                                ignore -> new InstanceInitializerMerger.Builder(appView, mode))
+                            .add(method)));
+        for (InstanceInitializerMerger.Builder builder : buildersByProto.values()) {
+          instanceInitializerMergers.addAll(builder.build(group));
         }
       } else {
         group.forEach(
@@ -393,16 +393,17 @@
                 clazz.forEachProgramDirectMethodMatching(
                     DexEncodedMethod::isInstanceInitializer,
                     method ->
-                        constructorMergers.addAll(
-                            new ConstructorMerger.Builder(appView)
-                                .add(method.getDefinition())
+                        instanceInitializerMergers.addAll(
+                            new InstanceInitializerMerger.Builder(appView, mode)
+                                .add(method)
                                 .build(group))));
       }
 
       // Try and merge the constructors with the most arguments first, to avoid using synthetic
       // arguments if possible.
-      constructorMergers.sort(Comparator.comparing(ConstructorMerger::getArity).reversed());
-      return constructorMergers;
+      instanceInitializerMergers.sort(
+          Comparator.comparing(InstanceInitializerMerger::getArity).reversed());
+      return instanceInitializerMergers;
     }
 
     private List<VirtualMethodMerger> createVirtualMethodMergers() {
@@ -443,6 +444,7 @@
           virtualMethodMergers.stream()
               .anyMatch(virtualMethodMerger -> !virtualMethodMerger.isNopOrTrivial());
       if (requiresClassIdField) {
+        assert mode.isInitial();
         createClassIdField();
       }
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java
deleted file mode 100644
index fb98a86..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java
+++ /dev/null
@@ -1,217 +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.horizontalclassmerging;
-
-import static com.android.tools.r8.dex.Constants.TEMPORARY_INSTANCE_INITIALIZER_PREFIX;
-
-import com.android.tools.r8.cf.CfVersion;
-import com.android.tools.r8.dex.Constants;
-import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexAnnotationSet;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexType;
-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.horizontalclassmerging.code.ConstructorEntryPointSynthesizedCode;
-import com.android.tools.r8.ir.conversion.ExtraConstantIntParameter;
-import com.android.tools.r8.ir.conversion.ExtraParameter;
-import com.android.tools.r8.ir.conversion.ExtraUnusedNullParameter;
-import com.android.tools.r8.utils.ListUtils;
-import com.android.tools.r8.utils.structural.Ordered;
-import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
-import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
-import it.unimi.dsi.fastutil.objects.Reference2IntMap;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
-public class ConstructorMerger {
-  private final AppView<?> appView;
-  private final MergeGroup group;
-  private final Collection<DexEncodedMethod> constructors;
-  private final DexItemFactory dexItemFactory;
-
-  ConstructorMerger(
-      AppView<?> appView, MergeGroup group, Collection<DexEncodedMethod> constructors) {
-    this.appView = appView;
-    this.group = group;
-    this.constructors = constructors;
-
-    // Constructors should not be empty and all constructors should have the same prototype.
-    assert !constructors.isEmpty();
-    assert constructors.stream().map(DexEncodedMethod::getProto).distinct().count() == 1;
-
-    this.dexItemFactory = appView.dexItemFactory();
-  }
-
-  /**
-   * The method reference template describes which arguments the constructor must have, and is used
-   * to generate the final reference by appending null arguments until it is fresh.
-   */
-  private DexMethod generateReferenceMethodTemplate() {
-    DexMethod methodTemplate = constructors.iterator().next().getReference();
-    if (!isTrivialMerge()) {
-      methodTemplate = dexItemFactory.appendTypeToMethod(methodTemplate, dexItemFactory.intType);
-    }
-    return methodTemplate;
-  }
-
-  public int getArity() {
-    return constructors.iterator().next().getReference().getArity();
-  }
-
-  public static class Builder {
-    private int estimatedDexCodeSize;
-    private final List<List<DexEncodedMethod>> constructorGroups = new ArrayList<>();
-    private final AppView<? extends AppInfoWithClassHierarchy> appView;
-
-    public Builder(AppView<? extends AppInfoWithClassHierarchy> appView) {
-      this.appView = appView;
-      createNewGroup();
-    }
-
-    private void createNewGroup() {
-      estimatedDexCodeSize = 0;
-      constructorGroups.add(new ArrayList<>());
-    }
-
-    public Builder add(DexEncodedMethod constructor) {
-      int estimatedMaxSizeInBytes = constructor.getCode().estimatedDexCodeSizeUpperBoundInBytes();
-      // If the constructor gets too large, then the constructor should be merged into a new group.
-      if (estimatedDexCodeSize + estimatedMaxSizeInBytes
-              > appView.options().minimumVerificationSizeLimitInBytes() / 2
-          && estimatedDexCodeSize > 0) {
-        createNewGroup();
-      }
-
-      ListUtils.last(constructorGroups).add(constructor);
-      estimatedDexCodeSize += estimatedMaxSizeInBytes;
-      return this;
-    }
-
-    public List<ConstructorMerger> build(MergeGroup group) {
-      assert constructorGroups.stream().noneMatch(List::isEmpty);
-      return ListUtils.map(
-          constructorGroups, constructors -> new ConstructorMerger(appView, group, constructors));
-    }
-  }
-
-  private boolean isTrivialMerge() {
-    return constructors.size() == 1;
-  }
-
-  private DexMethod moveConstructor(
-      ClassMethodsBuilder classMethodsBuilder, DexEncodedMethod constructor) {
-    DexMethod method =
-        dexItemFactory.createFreshMethodNameWithHolder(
-            TEMPORARY_INSTANCE_INITIALIZER_PREFIX,
-            constructor.getHolderType(),
-            constructor.getProto(),
-            group.getTarget().getType(),
-            classMethodsBuilder::isFresh);
-
-    DexEncodedMethod encodedMethod = constructor.toTypeSubstitutedMethod(method);
-    encodedMethod.getMutableOptimizationInfo().markForceInline();
-    encodedMethod.getAccessFlags().unsetConstructor();
-    encodedMethod.getAccessFlags().unsetPublic();
-    encodedMethod.getAccessFlags().unsetProtected();
-    encodedMethod.getAccessFlags().setPrivate();
-    classMethodsBuilder.addDirectMethod(encodedMethod);
-
-    return method;
-  }
-
-  private MethodAccessFlags getAccessFlags() {
-    // TODO(b/164998929): ensure this behaviour is correct, should probably calculate upper bound
-    return MethodAccessFlags.fromSharedAccessFlags(
-        Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC, true);
-  }
-
-  /** Synthesize a new method which selects the constructor based on a parameter type. */
-  void merge(
-      ClassMethodsBuilder classMethodsBuilder,
-      HorizontalClassMergerGraphLens.Builder lensBuilder,
-      Reference2IntMap<DexType> classIdentifiers,
-      SyntheticArgumentClass syntheticArgumentClass) {
-    // Tree map as must be sorted.
-    Int2ReferenceSortedMap<DexMethod> typeConstructorClassMap = new Int2ReferenceAVLTreeMap<>();
-
-    CfVersion classFileVersion = null;
-    for (DexEncodedMethod constructor : constructors) {
-      if (constructor.hasClassFileVersion()) {
-        classFileVersion =
-            Ordered.maxIgnoreNull(classFileVersion, constructor.getClassFileVersion());
-      }
-      DexMethod movedConstructor = moveConstructor(classMethodsBuilder, constructor);
-      lensBuilder.mapMethod(movedConstructor, movedConstructor);
-      lensBuilder.recordNewMethodSignature(constructor.getReference(), movedConstructor);
-      typeConstructorClassMap.put(
-          classIdentifiers.getInt(constructor.getHolderType()), movedConstructor);
-    }
-
-    DexMethod methodReferenceTemplate = generateReferenceMethodTemplate();
-    DexMethod newConstructorReference =
-        dexItemFactory.createInstanceInitializerWithFreshProto(
-            methodReferenceTemplate.withHolder(group.getTarget().getType(), dexItemFactory),
-            syntheticArgumentClass.getArgumentClasses(),
-            classMethodsBuilder::isFresh);
-    int extraNulls = newConstructorReference.getArity() - methodReferenceTemplate.getArity();
-
-    DexEncodedMethod representative = constructors.iterator().next();
-    DexMethod originalConstructorReference =
-        appView.graphLens().getOriginalMethodSignature(representative.getReference());
-
-    // Create a special original method signature for the synthesized constructor that did not exist
-    // prior to horizontal class merging. Otherwise we might accidentally think that the synthesized
-    // constructor corresponds to the previous <init>() method on the target class, which could have
-    // unintended side-effects such as leading to unused argument removal being applied to the
-    // synthesized constructor all-though it by construction doesn't have any unused arguments.
-    DexMethod bridgeConstructorReference =
-        dexItemFactory.createFreshMethodNameWithoutHolder(
-            "$r8$init$bridge",
-            originalConstructorReference.getProto(),
-            originalConstructorReference.getHolderType(),
-            classMethodsBuilder::isFresh);
-
-    ConstructorEntryPointSynthesizedCode synthesizedCode =
-        new ConstructorEntryPointSynthesizedCode(
-            typeConstructorClassMap,
-            newConstructorReference,
-            group.hasClassIdField() ? group.getClassIdField() : null,
-            bridgeConstructorReference);
-    DexEncodedMethod newConstructor =
-        new DexEncodedMethod(
-            newConstructorReference,
-            getAccessFlags(),
-            MethodTypeSignature.noSignature(),
-            DexAnnotationSet.empty(),
-            ParameterAnnotationsList.empty(),
-            synthesizedCode,
-            true,
-            classFileVersion);
-
-    // Map each old constructor to the newly synthesized constructor in the graph lens.
-    for (DexEncodedMethod oldConstructor : constructors) {
-      List<ExtraParameter> extraParameters = new ArrayList<>();
-      if (constructors.size() > 1) {
-        int classIdentifier = classIdentifiers.getInt(oldConstructor.getHolderType());
-        extraParameters.add(new ExtraConstantIntParameter(classIdentifier));
-      }
-      extraParameters.addAll(Collections.nCopies(extraNulls, new ExtraUnusedNullParameter()));
-      lensBuilder.mapMergedConstructor(
-          oldConstructor.getReference(), newConstructorReference, extraParameters);
-    }
-
-    // Add a mapping from a synthetic name to the synthetic constructor.
-    lensBuilder.recordNewMethodSignature(bridgeConstructorReference, newConstructorReference);
-
-    classMethodsBuilder.addDirectMethod(newConstructor);
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
index 1a88983..c05af00 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -9,16 +9,20 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.graph.PrunedItems;
+import com.android.tools.r8.horizontalclassmerging.code.SyntheticClassInitializerConverter;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.FieldAccessInfoCollectionModifier;
 import com.android.tools.r8.shaking.KeepInfoCollection;
 import com.android.tools.r8.shaking.RuntimeTypeCheckInfo;
 import com.android.tools.r8.utils.InternalOptions.HorizontalClassMergerOptions;
 import com.android.tools.r8.utils.Timing;
+import com.android.tools.r8.utils.TraversalContinuation;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
 
 public class HorizontalClassMerger {
 
@@ -55,17 +59,21 @@
     return new HorizontalClassMerger(appView, Mode.FINAL);
   }
 
-  public void runIfNecessary(RuntimeTypeCheckInfo runtimeTypeCheckInfo, Timing timing) {
+  public void runIfNecessary(
+      RuntimeTypeCheckInfo runtimeTypeCheckInfo, ExecutorService executorService, Timing timing)
+      throws ExecutionException {
     if (options.isEnabled(mode)) {
       timing.begin("HorizontalClassMerger (" + mode.toString() + ")");
-      run(runtimeTypeCheckInfo, timing);
+      run(runtimeTypeCheckInfo, executorService, timing);
       timing.end();
     } else {
       appView.setHorizontallyMergedClasses(HorizontallyMergedClasses.empty(), mode);
     }
   }
 
-  private void run(RuntimeTypeCheckInfo runtimeTypeCheckInfo, Timing timing) {
+  private void run(
+      RuntimeTypeCheckInfo runtimeTypeCheckInfo, ExecutorService executorService, Timing timing)
+      throws ExecutionException {
     // Run the policies on all program classes to produce a final grouping.
     List<Policy> policies = PolicyScheduler.getPolicies(appView, mode, runtimeTypeCheckInfo);
     Collection<MergeGroup> groups = new PolicyExecutor().run(getInitialGroups(), policies, timing);
@@ -85,7 +93,17 @@
         mode.isInitial()
             ? new SyntheticArgumentClass.Builder(appView.withLiveness()).build(groups)
             : null;
-    applyClassMergers(classMergers, syntheticArgumentClass);
+    SyntheticClassInitializerConverter.Builder syntheticClassInitializerConverterBuilder =
+        SyntheticClassInitializerConverter.builder(appView);
+    applyClassMergers(
+        classMergers, syntheticArgumentClass, syntheticClassInitializerConverterBuilder);
+
+    SyntheticClassInitializerConverter syntheticClassInitializerConverter =
+        syntheticClassInitializerConverterBuilder.build();
+    if (!syntheticClassInitializerConverter.isEmpty()) {
+      assert mode.isFinal();
+      syntheticClassInitializerConverterBuilder.build().convert(executorService);
+    }
 
     // Generate the graph lens.
     HorizontallyMergedClasses mergedClasses =
@@ -95,6 +113,8 @@
     HorizontalClassMergerGraphLens horizontalClassMergerGraphLens =
         createLens(mergedClasses, lensBuilder, syntheticArgumentClass);
 
+    assert verifyNoCyclesInInterfaceHierarchies(groups);
+
     // Prune keep info.
     KeepInfoCollection keepInfo = appView.getKeepInfo();
     keepInfo.mutate(mutator -> mutator.removeKeepInfoForPrunedItems(mergedClasses.getSources()));
@@ -186,9 +206,11 @@
 
   /** Merges all class groups using {@link ClassMerger}. */
   private void applyClassMergers(
-      Collection<ClassMerger> classMergers, SyntheticArgumentClass syntheticArgumentClass) {
+      Collection<ClassMerger> classMergers,
+      SyntheticArgumentClass syntheticArgumentClass,
+      SyntheticClassInitializerConverter.Builder syntheticClassInitializerConverterBuilder) {
     for (ClassMerger merger : classMergers) {
-      merger.mergeGroup(syntheticArgumentClass);
+      merger.mergeGroup(syntheticArgumentClass, syntheticClassInitializerConverterBuilder);
     }
   }
 
@@ -203,4 +225,23 @@
     return new TreeFixer(appView, mergedClasses, lensBuilder, syntheticArgumentClass)
         .fixupTypeReferences();
   }
+
+  private boolean verifyNoCyclesInInterfaceHierarchies(Collection<MergeGroup> groups) {
+    for (MergeGroup group : groups) {
+      if (group.isClassGroup()) {
+        continue;
+      }
+      DexProgramClass interfaceClass = group.getTarget();
+      appView
+          .appInfo()
+          .traverseSuperTypes(
+              interfaceClass,
+              (superType, subclass, isInterface) -> {
+                assert superType != interfaceClass.getType()
+                    : "Interface " + interfaceClass.getTypeName() + " inherits from itself";
+                return TraversalContinuation.CONTINUE;
+              });
+    }
+    return true;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
index 4358dc8..12a4190 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
@@ -149,12 +149,24 @@
     }
 
     void moveMethod(DexMethod from, DexMethod to) {
+      moveMethod(from, to, false);
+    }
+
+    void moveMethod(DexMethod from, DexMethod to, boolean isRepresentative) {
       mapMethod(from, to);
-      recordNewMethodSignature(from, to);
+      recordNewMethodSignature(from, to, isRepresentative);
     }
 
     void recordNewMethodSignature(DexMethod oldMethodSignature, DexMethod newMethodSignature) {
+      recordNewMethodSignature(oldMethodSignature, newMethodSignature, false);
+    }
+
+    void recordNewMethodSignature(
+        DexMethod oldMethodSignature, DexMethod newMethodSignature, boolean isRepresentative) {
       newMethodSignatures.put(oldMethodSignature, newMethodSignature);
+      if (isRepresentative) {
+        newMethodSignatures.setRepresentative(newMethodSignature, oldMethodSignature);
+      }
     }
 
     void fixupMethod(DexMethod oldMethodSignature, DexMethod newMethodSignature) {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java
new file mode 100644
index 0000000..b1f02a9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/InstanceInitializerMerger.java
@@ -0,0 +1,315 @@
+// 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.horizontalclassmerging;
+
+import static com.android.tools.r8.dex.Constants.TEMPORARY_INSTANCE_INITIALIZER_PREFIX;
+
+import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexAnnotationSet;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+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.graph.ProgramMethod;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
+import com.android.tools.r8.horizontalclassmerging.code.ConstructorEntryPointSynthesizedCode;
+import com.android.tools.r8.ir.conversion.ExtraConstantIntParameter;
+import com.android.tools.r8.ir.conversion.ExtraParameter;
+import com.android.tools.r8.ir.conversion.ExtraUnusedNullParameter;
+import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
+import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.structural.Ordered;
+import com.google.common.collect.Iterables;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
+import it.unimi.dsi.fastutil.objects.Reference2IntMap;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+public class InstanceInitializerMerger {
+
+  private final AppView<?> appView;
+  private final DexItemFactory dexItemFactory;
+  private final MergeGroup group;
+  private final List<ProgramMethod> instanceInitializers;
+  private final Mode mode;
+
+  InstanceInitializerMerger(
+      AppView<?> appView, MergeGroup group, List<ProgramMethod> instanceInitializers, Mode mode) {
+    this.appView = appView;
+    this.dexItemFactory = appView.dexItemFactory();
+    this.group = group;
+    this.instanceInitializers = instanceInitializers;
+    this.mode = mode;
+
+    // Constructors should not be empty and all constructors should have the same prototype.
+    assert !instanceInitializers.isEmpty();
+    assert instanceInitializers.stream().map(ProgramMethod::getProto).distinct().count() == 1;
+  }
+
+  /**
+   * The method reference template describes which arguments the constructor must have, and is used
+   * to generate the final reference by appending null arguments until it is fresh.
+   */
+  private DexMethod generateReferenceMethodTemplate() {
+    DexMethod methodTemplate = instanceInitializers.iterator().next().getReference();
+    if (instanceInitializers.size() > 1) {
+      methodTemplate = dexItemFactory.appendTypeToMethod(methodTemplate, dexItemFactory.intType);
+    }
+    return methodTemplate.withHolder(group.getTarget(), dexItemFactory);
+  }
+
+  public int getArity() {
+    return instanceInitializers.iterator().next().getReference().getArity();
+  }
+
+  public static class Builder {
+
+    private final AppView<? extends AppInfoWithClassHierarchy> appView;
+    private int estimatedDexCodeSize;
+    private final List<List<ProgramMethod>> instanceInitializerGroups = new ArrayList<>();
+    private final Mode mode;
+
+    public Builder(AppView<? extends AppInfoWithClassHierarchy> appView, Mode mode) {
+      this.appView = appView;
+      this.mode = mode;
+      createNewGroup();
+    }
+
+    private void createNewGroup() {
+      estimatedDexCodeSize = 0;
+      instanceInitializerGroups.add(new ArrayList<>());
+    }
+
+    public Builder add(ProgramMethod instanceInitializer) {
+      int estimatedMaxSizeInBytes =
+          instanceInitializer.getDefinition().getCode().estimatedDexCodeSizeUpperBoundInBytes();
+      // If the constructor gets too large, then the constructor should be merged into a new group.
+      if (estimatedDexCodeSize + estimatedMaxSizeInBytes
+              > appView.options().minimumVerificationSizeLimitInBytes() / 2
+          && estimatedDexCodeSize > 0) {
+        createNewGroup();
+      }
+
+      ListUtils.last(instanceInitializerGroups).add(instanceInitializer);
+      estimatedDexCodeSize += estimatedMaxSizeInBytes;
+      return this;
+    }
+
+    public List<InstanceInitializerMerger> build(MergeGroup group) {
+      assert instanceInitializerGroups.stream().noneMatch(List::isEmpty);
+      return ListUtils.map(
+          instanceInitializerGroups,
+          instanceInitializers ->
+              new InstanceInitializerMerger(appView, group, instanceInitializers, mode));
+    }
+  }
+
+  // Returns true if we can simply use an existing constructor as the new constructor.
+  private boolean isTrivialMerge(ClassMethodsBuilder classMethodsBuilder) {
+    if (group.hasClassIdField()) {
+      // We need to set the class id field.
+      return false;
+    }
+    DexMethod trivialInstanceInitializerReference =
+        ListUtils.first(instanceInitializers)
+            .getReference()
+            .withHolder(group.getTarget(), dexItemFactory);
+    if (!classMethodsBuilder.isFresh(trivialInstanceInitializerReference)) {
+      // We need to append null arguments for disambiguation.
+      return false;
+    }
+    return isMergeOfEquivalentInstanceInitializers();
+  }
+
+  private boolean isMergeOfEquivalentInstanceInitializers() {
+    Iterator<ProgramMethod> instanceInitializerIterator = instanceInitializers.iterator();
+    ProgramMethod firstInstanceInitializer = instanceInitializerIterator.next();
+    if (!instanceInitializerIterator.hasNext()) {
+      return true;
+    }
+    // We need all the constructors to be equivalent.
+    InstanceInitializerInfo instanceInitializerInfo =
+        firstInstanceInitializer
+            .getDefinition()
+            .getOptimizationInfo()
+            .getContextInsensitiveInstanceInitializerInfo();
+    if (!instanceInitializerInfo.hasParent()) {
+      // We don't know the parent constructor of the first constructor.
+      return false;
+    }
+    DexMethod parent = instanceInitializerInfo.getParent();
+    return Iterables.all(
+        instanceInitializers,
+        instanceInitializer ->
+            isSideEffectFreeInstanceInitializerWithParent(instanceInitializer, parent));
+  }
+
+  private boolean isSideEffectFreeInstanceInitializerWithParent(
+      ProgramMethod instanceInitializer, DexMethod parent) {
+    MethodOptimizationInfo optimizationInfo =
+        instanceInitializer.getDefinition().getOptimizationInfo();
+    return !optimizationInfo.mayHaveSideEffects()
+        && optimizationInfo.getContextInsensitiveInstanceInitializerInfo().getParent() == parent;
+  }
+
+  private DexMethod moveInstanceInitializer(
+      ClassMethodsBuilder classMethodsBuilder, ProgramMethod instanceInitializer) {
+    DexMethod method =
+        dexItemFactory.createFreshMethodNameWithHolder(
+            TEMPORARY_INSTANCE_INITIALIZER_PREFIX,
+            instanceInitializer.getHolderType(),
+            instanceInitializer.getProto(),
+            group.getTarget().getType(),
+            classMethodsBuilder::isFresh);
+
+    DexEncodedMethod encodedMethod =
+        instanceInitializer.getDefinition().toTypeSubstitutedMethod(method);
+    encodedMethod.getMutableOptimizationInfo().markForceInline();
+    encodedMethod.getAccessFlags().unsetConstructor();
+    encodedMethod.getAccessFlags().unsetPublic();
+    encodedMethod.getAccessFlags().unsetProtected();
+    encodedMethod.getAccessFlags().setPrivate();
+    classMethodsBuilder.addDirectMethod(encodedMethod);
+
+    return method;
+  }
+
+  private MethodAccessFlags getAccessFlags() {
+    // TODO(b/164998929): ensure this behaviour is correct, should probably calculate upper bound
+    return MethodAccessFlags.fromSharedAccessFlags(
+        Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC, true);
+  }
+
+  /** Synthesize a new method which selects the constructor based on a parameter type. */
+  void merge(
+      ClassMethodsBuilder classMethodsBuilder,
+      HorizontalClassMergerGraphLens.Builder lensBuilder,
+      Reference2IntMap<DexType> classIdentifiers,
+      SyntheticArgumentClass syntheticArgumentClass) {
+    if (isTrivialMerge(classMethodsBuilder)) {
+      mergeTrivial(classMethodsBuilder, lensBuilder);
+      return;
+    }
+
+    assert mode.isInitial();
+
+    // Tree map as must be sorted.
+    Int2ReferenceSortedMap<DexMethod> typeConstructorClassMap = new Int2ReferenceAVLTreeMap<>();
+
+    // Move constructors to target class.
+    CfVersion classFileVersion = null;
+    for (ProgramMethod instanceInitializer : instanceInitializers) {
+      if (instanceInitializer.getDefinition().hasClassFileVersion()) {
+        classFileVersion =
+            Ordered.maxIgnoreNull(
+                classFileVersion, instanceInitializer.getDefinition().getClassFileVersion());
+      }
+      DexMethod movedInstanceInitializer =
+          moveInstanceInitializer(classMethodsBuilder, instanceInitializer);
+      lensBuilder.mapMethod(movedInstanceInitializer, movedInstanceInitializer);
+      lensBuilder.recordNewMethodSignature(
+          instanceInitializer.getReference(), movedInstanceInitializer);
+      typeConstructorClassMap.put(
+          classIdentifiers.getInt(instanceInitializer.getHolderType()), movedInstanceInitializer);
+    }
+
+    // Create merged constructor reference.
+    DexMethod methodReferenceTemplate = generateReferenceMethodTemplate();
+    DexMethod newConstructorReference =
+        dexItemFactory.createInstanceInitializerWithFreshProto(
+            methodReferenceTemplate,
+            syntheticArgumentClass.getArgumentClasses(),
+            classMethodsBuilder::isFresh);
+    int extraNulls = newConstructorReference.getArity() - methodReferenceTemplate.getArity();
+
+    ProgramMethod representative = ListUtils.first(instanceInitializers);
+    DexMethod originalConstructorReference =
+        appView.graphLens().getOriginalMethodSignature(representative.getReference());
+
+    // Create a special original method signature for the synthesized constructor that did not exist
+    // prior to horizontal class merging. Otherwise we might accidentally think that the synthesized
+    // constructor corresponds to the previous <init>() method on the target class, which could have
+    // unintended side-effects such as leading to unused argument removal being applied to the
+    // synthesized constructor all-though it by construction doesn't have any unused arguments.
+    DexMethod bridgeConstructorReference =
+        dexItemFactory.createFreshMethodNameWithoutHolder(
+            "$r8$init$bridge",
+            originalConstructorReference.getProto(),
+            originalConstructorReference.getHolderType(),
+            classMethodsBuilder::isFresh);
+
+    ConstructorEntryPointSynthesizedCode synthesizedCode =
+        new ConstructorEntryPointSynthesizedCode(
+            typeConstructorClassMap,
+            newConstructorReference,
+            group.hasClassIdField() ? group.getClassIdField() : null,
+            bridgeConstructorReference);
+    DexEncodedMethod newConstructor =
+        new DexEncodedMethod(
+            newConstructorReference,
+            getAccessFlags(),
+            MethodTypeSignature.noSignature(),
+            DexAnnotationSet.empty(),
+            ParameterAnnotationsList.empty(),
+            synthesizedCode,
+            true,
+            classFileVersion);
+
+    // Map each old constructor to the newly synthesized constructor in the graph lens.
+    for (ProgramMethod oldInstanceInitializer : instanceInitializers) {
+      List<ExtraParameter> extraParameters = new ArrayList<>();
+      if (instanceInitializers.size() > 1) {
+        int classIdentifier = classIdentifiers.getInt(oldInstanceInitializer.getHolderType());
+        extraParameters.add(new ExtraConstantIntParameter(classIdentifier));
+      }
+      extraParameters.addAll(Collections.nCopies(extraNulls, new ExtraUnusedNullParameter()));
+      lensBuilder.mapMergedConstructor(
+          oldInstanceInitializer.getReference(), newConstructorReference, extraParameters);
+    }
+
+    // Add a mapping from a synthetic name to the synthetic constructor.
+    lensBuilder.recordNewMethodSignature(bridgeConstructorReference, newConstructorReference);
+
+    classMethodsBuilder.addDirectMethod(newConstructor);
+  }
+
+  private void mergeTrivial(
+      ClassMethodsBuilder classMethodsBuilder, HorizontalClassMergerGraphLens.Builder lensBuilder) {
+    ProgramMethod representative = ListUtils.first(instanceInitializers);
+    DexMethod newMethodReference =
+        representative.getReference().withHolder(group.getTarget(), dexItemFactory);
+
+    for (ProgramMethod constructor : instanceInitializers) {
+      boolean isRepresentative = constructor == representative;
+      lensBuilder.moveMethod(constructor.getReference(), newMethodReference, isRepresentative);
+    }
+
+    DexEncodedMethod newMethod =
+        representative.getHolder() == group.getTarget()
+            ? representative.getDefinition()
+            : representative.getDefinition().toTypeSubstitutedMethod(newMethodReference);
+    fixupAccessFlagsForTrivialMerge(newMethod.getAccessFlags());
+
+    classMethodsBuilder.addDirectMethod(newMethod);
+  }
+
+  private void fixupAccessFlagsForTrivialMerge(MethodAccessFlags accessFlags) {
+    if (!accessFlags.isPublic()) {
+      accessFlags.unsetPrivate();
+      accessFlags.unsetProtected();
+      accessFlags.setPublic();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
index ebedb0b..ba5d775 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.IteratorUtils;
 import com.google.common.collect.Iterables;
@@ -108,6 +109,11 @@
     return Iterables.filter(classes, clazz -> clazz != target);
   }
 
+  public DexType getSuperType() {
+    assert IterableUtils.allIdentical(classes, DexClass::getSuperType);
+    return getClasses().getFirst().getSuperType();
+  }
+
   public boolean hasTarget() {
     return target != null;
   }
@@ -134,6 +140,10 @@
     return classes.isEmpty();
   }
 
+  public boolean isClassGroup() {
+    return !isInterfaceGroup();
+  }
+
   public boolean isInterfaceGroup() {
     assert !isEmpty();
     assert IterableUtils.allIdentical(getClasses(), DexClass::isInterface);
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
index e5854fe..5bff09e 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
@@ -22,6 +22,10 @@
 
   public abstract String getName();
 
+  public boolean isIdentityForInterfaceGroups() {
+    return false;
+  }
+
   public boolean isSingleClassPolicy() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
index 01a85ee..6969f94 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
@@ -8,10 +8,9 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.policies.AllInstantiatedOrUninstantiated;
-import com.android.tools.r8.horizontalclassmerging.policies.AtMostOneClassInitializer;
 import com.android.tools.r8.horizontalclassmerging.policies.CheckAbstractClasses;
 import com.android.tools.r8.horizontalclassmerging.policies.CheckSyntheticClasses;
-import com.android.tools.r8.horizontalclassmerging.policies.LimitGroups;
+import com.android.tools.r8.horizontalclassmerging.policies.LimitClassGroups;
 import com.android.tools.r8.horizontalclassmerging.policies.MinimizeInstanceFieldCasts;
 import com.android.tools.r8.horizontalclassmerging.policies.NoAnnotationClasses;
 import com.android.tools.r8.horizontalclassmerging.policies.NoClassAnnotationCollisions;
@@ -27,14 +26,14 @@
 import com.android.tools.r8.horizontalclassmerging.policies.NoIndirectRuntimeTypeChecks;
 import com.android.tools.r8.horizontalclassmerging.policies.NoInnerClasses;
 import com.android.tools.r8.horizontalclassmerging.policies.NoInstanceFieldAnnotations;
-import com.android.tools.r8.horizontalclassmerging.policies.NoInstanceInitializers;
+import com.android.tools.r8.horizontalclassmerging.policies.NoInstanceInitializerMerging;
 import com.android.tools.r8.horizontalclassmerging.policies.NoInterfaces;
 import com.android.tools.r8.horizontalclassmerging.policies.NoKeepRules;
 import com.android.tools.r8.horizontalclassmerging.policies.NoKotlinMetadata;
 import com.android.tools.r8.horizontalclassmerging.policies.NoNativeMethods;
-import com.android.tools.r8.horizontalclassmerging.policies.NoNonPrivateVirtualMethods;
 import com.android.tools.r8.horizontalclassmerging.policies.NoServiceLoaders;
 import com.android.tools.r8.horizontalclassmerging.policies.NoVerticallyMergedClasses;
+import com.android.tools.r8.horizontalclassmerging.policies.NoVirtualMethodMerging;
 import com.android.tools.r8.horizontalclassmerging.policies.NotMatchedByNoHorizontalClassMerging;
 import com.android.tools.r8.horizontalclassmerging.policies.OnlyDirectlyConnectedOrUnrelatedInterfaces;
 import com.android.tools.r8.horizontalclassmerging.policies.PreserveMethodCharacteristics;
@@ -49,6 +48,7 @@
 import com.android.tools.r8.horizontalclassmerging.policies.VerifyPolicyAlwaysSatisfied;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.RuntimeTypeCheckInfo;
+import com.android.tools.r8.utils.ListUtils;
 import com.google.common.collect.ImmutableList;
 import java.util.List;
 
@@ -58,10 +58,13 @@
       AppView<? extends AppInfoWithClassHierarchy> appView,
       Mode mode,
       RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
-    return ImmutableList.<Policy>builder()
-        .addAll(getSingleClassPolicies(appView, mode, runtimeTypeCheckInfo))
-        .addAll(getMultiClassPolicies(appView, mode, runtimeTypeCheckInfo))
-        .build();
+    List<Policy> policies =
+        ImmutableList.<Policy>builder()
+            .addAll(getSingleClassPolicies(appView, mode, runtimeTypeCheckInfo))
+            .addAll(getMultiClassPolicies(appView, mode, runtimeTypeCheckInfo))
+            .build();
+    assert verifyPolicyOrderingConstraints(policies);
+    return policies;
   }
 
   private static List<SingleClassPolicy> getSingleClassPolicies(
@@ -78,12 +81,6 @@
           new NoDeadEnumLiteMaps(appViewWithLiveness, mode),
           new NoIllegalInlining(appViewWithLiveness, mode),
           new NoVerticallyMergedClasses(appViewWithLiveness, mode));
-    } else {
-      assert mode.isFinal();
-      // TODO(b/181846319): Allow constructors, as long as the constructor protos remain unchanged
-      //  (in particular, we can't add nulls at constructor call sites).
-      // TODO(b/181846319): Allow virtual methods, as long as they do not require any merging.
-      builder.add(new NoInstanceInitializers(mode), new NoNonPrivateVirtualMethods(mode));
     }
 
     if (appView.options().horizontalClassMergerOptions().isRestrictedToSynthetics()) {
@@ -166,12 +163,15 @@
     } else {
       assert mode.isFinal();
       // TODO(b/185472598): Add support for merging class initializers with dex code.
-      builder.add(new AtMostOneClassInitializer(mode), new NoConstructorCollisions(appView, mode));
+      builder.add(
+          new NoInstanceInitializerMerging(mode),
+          new NoVirtualMethodMerging(appView, mode),
+          new NoConstructorCollisions(appView, mode));
     }
 
     addMultiClassPoliciesForInterfaceMerging(appView, mode, builder);
 
-    return builder.add(new LimitGroups(appView)).build();
+    return builder.add(new LimitClassGroups(appView)).build();
   }
 
   private static void addRequiredMultiClassPolicies(
@@ -204,8 +204,25 @@
       Mode mode,
       ImmutableList.Builder<Policy> builder) {
     builder.add(
-        new OnlyDirectlyConnectedOrUnrelatedInterfaces(appView, mode),
         new NoDefaultInterfaceMethodMerging(appView, mode),
-        new NoDefaultInterfaceMethodCollisions(appView, mode));
+        new NoDefaultInterfaceMethodCollisions(appView, mode),
+        new OnlyDirectlyConnectedOrUnrelatedInterfaces(appView, mode));
+  }
+
+  private static boolean verifyPolicyOrderingConstraints(List<Policy> policies) {
+    // No policies that may split interface groups are allowed to run after the
+    // OnlyDirectlyConnectedOrUnrelatedInterfaces policy. This policy ensures that interface merging
+    // does not lead to any cycles in the interface hierarchy, which may be invalidated if merge
+    // groups are split after the policy has run.
+    int onlyDirectlyConnectedOrUnrelatedInterfacesIndex =
+        ListUtils.lastIndexMatching(
+            policies, policy -> policy instanceof OnlyDirectlyConnectedOrUnrelatedInterfaces);
+    if (onlyDirectlyConnectedOrUnrelatedInterfacesIndex >= 0) {
+      for (Policy successorPolicy :
+          policies.subList(onlyDirectlyConnectedOrUnrelatedInterfacesIndex + 1, policies.size())) {
+        assert successorPolicy.isIdentityForInterfaceGroups();
+      }
+    }
+    return true;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java
new file mode 100644
index 0000000..a720407
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerMerger.java
@@ -0,0 +1,295 @@
+// 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.horizontalclassmerging.code;
+
+import static java.lang.Integer.max;
+
+import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.cf.code.CfGoto;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfLabel;
+import com.android.tools.r8.cf.code.CfPosition;
+import com.android.tools.r8.cf.code.CfReturnVoid;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.ClasspathMethod;
+import com.android.tools.r8.graph.Code;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.RewrittenPrototypeDescription;
+import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.IRMetadata;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.code.NumberGenerator;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.Return;
+import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.CfVersionUtils;
+import com.android.tools.r8.utils.IterableUtils;
+import com.android.tools.r8.utils.ListUtils;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Set;
+
+/**
+ * Responsible for merging the class initializers in each merge group into a single class
+ * initializer.
+ */
+public class ClassInitializerMerger {
+
+  private final ImmutableList<ProgramMethod> classInitializers;
+
+  private ClassInitializerMerger(ImmutableList<ProgramMethod> classInitializers) {
+    this.classInitializers = classInitializers;
+  }
+
+  public boolean isEmpty() {
+    return classInitializers.isEmpty();
+  }
+
+  public Code getCode(DexMethod syntheticMethodReference) {
+    assert !classInitializers.isEmpty();
+    ProgramMethod firstClassInitializer = ListUtils.first(classInitializers);
+    if (firstClassInitializer.getDefinition().getCode().isCfCode()) {
+      assert IterableUtils.allIdentical(
+          classInitializers,
+          classInitializer -> classInitializer.getDefinition().getCode().isCfCode());
+      return new CfCodeBuilder().build(syntheticMethodReference);
+    }
+    return new IRProvider(classInitializers, syntheticMethodReference);
+  }
+
+  public CfVersion getCfVersion() {
+    ProgramMethod classInitializer = ListUtils.first(classInitializers);
+    if (classInitializers.size() == 1) {
+      DexEncodedMethod method = classInitializer.getDefinition();
+      return method.hasClassFileVersion() ? method.getClassFileVersion() : null;
+    }
+    if (classInitializer.getDefinition().getCode().isCfCode()) {
+      assert IterableUtils.allIdentical(
+          classInitializers, method -> method.getDefinition().getCode().isCfCode());
+      return CfVersionUtils.max(classInitializers);
+    }
+    return null;
+  }
+
+  public static class Builder {
+
+    private final ImmutableList.Builder<ProgramMethod> classInitializers = ImmutableList.builder();
+
+    public void add(ProgramMethod classInitializer) {
+      assert classInitializer.getDefinition().isClassInitializer();
+      assert classInitializer.getDefinition().hasCode();
+      classInitializers.add(classInitializer);
+    }
+
+    public ClassInitializerMerger build() {
+      return new ClassInitializerMerger(classInitializers.build());
+    }
+  }
+
+  /** Concatenates a collection of class initializers with CF code into a new piece of CF code. */
+  private class CfCodeBuilder {
+
+    private int maxStack = 0;
+    private int maxLocals = 0;
+
+    public CfCode build(DexMethod syntheticMethodReference) {
+      // Building the instructions will adjust maxStack and maxLocals. Build it here before invoking
+      // the CfCode constructor to ensure that the value passed in is the updated values.
+      Position callerPosition = Position.synthetic(0, syntheticMethodReference, null);
+      List<CfInstruction> instructions = buildInstructions(callerPosition);
+      return new CfCode(
+          syntheticMethodReference.getHolderType(),
+          maxStack,
+          maxLocals,
+          instructions,
+          Collections.emptyList(),
+          Collections.emptyList());
+    }
+
+    private List<CfInstruction> buildInstructions(Position callerPosition) {
+      List<CfInstruction> newInstructions = new ArrayList<>();
+      classInitializers.forEach(
+          classInitializer -> addCfCode(newInstructions, classInitializer, callerPosition));
+      newInstructions.add(new CfReturnVoid());
+      return newInstructions;
+    }
+
+    private void addCfCode(
+        List<CfInstruction> newInstructions, ProgramMethod method, Position callerPosition) {
+      CfCode code = method.getDefinition().getCode().asCfCode();
+      maxStack = max(maxStack, code.getMaxStack());
+      maxLocals = max(maxLocals, code.getMaxLocals());
+      CfLabel endLabel = new CfLabel();
+      boolean requiresLabel = false;
+      int index = 1;
+      for (CfInstruction instruction : code.getInstructions()) {
+        if (instruction.isPosition()) {
+          CfPosition cfPosition = instruction.asPosition();
+          Position position = cfPosition.getPosition();
+          newInstructions.add(
+              new CfPosition(
+                  cfPosition.getLabel(),
+                  position.hasCallerPosition()
+                      ? position
+                      : position.withCallerPosition(callerPosition)));
+        } else if (instruction.isReturn()) {
+          if (code.getInstructions().size() != index) {
+            newInstructions.add(new CfGoto(endLabel));
+            requiresLabel = true;
+          }
+        } else {
+          newInstructions.add(instruction);
+        }
+        index++;
+      }
+      if (requiresLabel) {
+        newInstructions.add(endLabel);
+      }
+    }
+  }
+
+  /**
+   * Provides a piece of {@link IRCode} that is the concatenation of a collection of class
+   * initializers.
+   */
+  private static class IRProvider extends Code {
+
+    private final ImmutableList<ProgramMethod> classInitializers;
+    private final DexMethod syntheticMethodReference;
+
+    private IRProvider(
+        ImmutableList<ProgramMethod> classInitializers, DexMethod syntheticMethodReference) {
+      this.classInitializers = classInitializers;
+      this.syntheticMethodReference = syntheticMethodReference;
+    }
+
+    @Override
+    public IRCode buildIR(ProgramMethod method, AppView<?> appView, Origin origin) {
+      assert !classInitializers.isEmpty();
+
+      Position callerPosition = Position.synthetic(0, syntheticMethodReference, null);
+      IRMetadata metadata = new IRMetadata();
+      NumberGenerator blockNumberGenerator = new NumberGenerator();
+      NumberGenerator valueNumberGenerator = new NumberGenerator();
+
+      BasicBlock block = new BasicBlock();
+      block.setNumber(blockNumberGenerator.next());
+
+      // Add "invoke-static <clinit>" for each of the class initializers to the exit block.
+      for (ProgramMethod classInitializer : classInitializers) {
+        block.add(
+            InvokeStatic.builder()
+                .setMethod(classInitializer.getReference())
+                .setPosition(callerPosition)
+                .build(),
+            metadata);
+      }
+
+      // Add "return-void" to exit block.
+      block.add(Return.builder().setPosition(Position.none()).build(), metadata);
+      block.setFilled();
+
+      IRCode code =
+          new IRCode(
+              appView.options(),
+              method,
+              ListUtils.newLinkedList(block),
+              valueNumberGenerator,
+              blockNumberGenerator,
+              metadata,
+              origin);
+
+      ListIterator<BasicBlock> blockIterator = code.listIterator();
+      InstructionListIterator instructionIterator = blockIterator.next().listIterator(code);
+
+      Set<BasicBlock> blocksToRemove = Sets.newIdentityHashSet();
+      for (ProgramMethod classInitializer : classInitializers) {
+        if (!instructionIterator.hasNext()) {
+          instructionIterator = blockIterator.next().listIterator(code);
+        }
+
+        InvokeStatic invoke = instructionIterator.next().asInvokeStatic();
+        assert invoke != null;
+
+        IRCode inliningIR =
+            classInitializer
+                .getDefinition()
+                .getCode()
+                .buildInliningIR(
+                    method,
+                    classInitializer,
+                    appView,
+                    valueNumberGenerator,
+                    callerPosition,
+                    classInitializer.getOrigin(),
+                    RewrittenPrototypeDescription.none());
+
+        DexType downcast = null;
+        instructionIterator.previous();
+        instructionIterator.inlineInvoke(
+            appView, code, inliningIR, blockIterator, blocksToRemove, downcast);
+      }
+
+      // Cleanup.
+      code.removeBlocks(blocksToRemove);
+      code.removeAllDeadAndTrivialPhis();
+
+      return code;
+    }
+
+    @Override
+    protected int computeHashCode() {
+      throw new Unreachable();
+    }
+
+    @Override
+    protected boolean computeEquals(Object other) {
+      throw new Unreachable();
+    }
+
+    @Override
+    public int estimatedDexCodeSizeUpperBoundInBytes() {
+      throw new Unreachable();
+    }
+
+    @Override
+    public boolean isEmptyVoidMethod() {
+      throw new Unreachable();
+    }
+
+    @Override
+    public void registerCodeReferences(ProgramMethod method, UseRegistry registry) {
+      throw new Unreachable();
+    }
+
+    @Override
+    public void registerCodeReferencesForDesugaring(ClasspathMethod method, UseRegistry registry) {
+      throw new Unreachable();
+    }
+
+    @Override
+    public String toString() {
+      throw new Unreachable();
+    }
+
+    @Override
+    public String toString(DexEncodedMethod method, ClassNameMapper naming) {
+      throw new Unreachable();
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerSynthesizedCode.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerSynthesizedCode.java
deleted file mode 100644
index 1b8a318..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerSynthesizedCode.java
+++ /dev/null
@@ -1,114 +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.horizontalclassmerging.code;
-
-import static com.android.tools.r8.utils.ConsumerUtils.apply;
-import static java.lang.Integer.max;
-
-import com.android.tools.r8.cf.CfVersion;
-import com.android.tools.r8.cf.code.CfGoto;
-import com.android.tools.r8.cf.code.CfInstruction;
-import com.android.tools.r8.cf.code.CfLabel;
-import com.android.tools.r8.cf.code.CfReturnVoid;
-import com.android.tools.r8.graph.CfCode;
-import com.android.tools.r8.graph.Code;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.utils.CfVersionUtils;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-public class ClassInitializerSynthesizedCode {
-  private final List<DexEncodedMethod> staticClassInitializers;
-  private int maxStack = 0;
-  private int maxLocals = 0;
-
-  private ClassInitializerSynthesizedCode(List<DexEncodedMethod> staticClassInitializers) {
-    this.staticClassInitializers = staticClassInitializers;
-  }
-
-  public boolean isEmpty() {
-    return staticClassInitializers.isEmpty();
-  }
-
-  private void addCfCode(List<CfInstruction> newInstructions, DexEncodedMethod method) {
-    CfCode code = method.getCode().asCfCode();
-    maxStack = max(maxStack, code.getMaxStack());
-    maxLocals = max(maxLocals, code.getMaxLocals());
-
-    CfLabel endLabel = new CfLabel();
-    boolean requiresLabel = false;
-    int index = 1;
-    for (CfInstruction instruction : code.getInstructions()) {
-      if (instruction.isReturn()) {
-        if (code.getInstructions().size() != index) {
-          newInstructions.add(new CfGoto(endLabel));
-          requiresLabel = true;
-        }
-      } else {
-        newInstructions.add(instruction);
-      }
-
-      index++;
-    }
-    if (requiresLabel) {
-      newInstructions.add(endLabel);
-    }
-  }
-
-  public Code getOrCreateCode(DexType originalHolder) {
-    assert !staticClassInitializers.isEmpty();
-
-    if (staticClassInitializers.size() == 1) {
-      return staticClassInitializers.get(0).getCode();
-    }
-
-    // Building the instructions will adjust maxStack and maxLocals. Build it here before invoking
-    // the CfCode constructor to ensure that the value passed in is the updated values.
-    List<CfInstruction> instructions = buildInstructions();
-    return new CfCode(
-        originalHolder,
-        maxStack,
-        maxLocals,
-        instructions,
-        Collections.emptyList(),
-        Collections.emptyList());
-  }
-
-  private List<CfInstruction> buildInstructions() {
-    List<CfInstruction> newInstructions = new ArrayList<>();
-    staticClassInitializers.forEach(apply(this::addCfCode, newInstructions));
-    newInstructions.add(new CfReturnVoid());
-    return newInstructions;
-  }
-
-  public DexEncodedMethod getFirst() {
-    return staticClassInitializers.iterator().next();
-  }
-
-  public CfVersion getCfVersion() {
-    if (staticClassInitializers.size() == 1) {
-      DexEncodedMethod method = staticClassInitializers.get(0);
-      return method.hasClassFileVersion() ? method.getClassFileVersion() : null;
-    }
-    assert staticClassInitializers.stream().allMatch(method -> method.getCode().isCfCode());
-    return CfVersionUtils.max(staticClassInitializers);
-  }
-
-  public static class Builder {
-    private final List<DexEncodedMethod> staticClassInitializers = new ArrayList<>();
-
-    public void add(DexEncodedMethod method) {
-      assert method.isClassInitializer();
-      assert method.hasCode();
-      staticClassInitializers.add(method);
-    }
-
-    public ClassInitializerSynthesizedCode build() {
-      return new ClassInitializerSynthesizedCode(staticClassInitializers);
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/SyntheticClassInitializerConverter.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/SyntheticClassInitializerConverter.java
new file mode 100644
index 0000000..a31333c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/SyntheticClassInitializerConverter.java
@@ -0,0 +1,90 @@
+// 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.horizontalclassmerging.code;
+
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackIgnore;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.Timing;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Converts synthetic class initializers that have been created as a result of merging class
+ * initializers into a single class initializer to DEX.
+ */
+public class SyntheticClassInitializerConverter {
+
+  private final AppView<? extends AppInfoWithClassHierarchy> appView;
+  private final List<MergeGroup> groups;
+
+  private SyntheticClassInitializerConverter(
+      AppView<? extends AppInfoWithClassHierarchy> appView, List<MergeGroup> groups) {
+    this.appView = appView;
+    this.groups = groups;
+  }
+
+  public static Builder builder(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    return new Builder(appView);
+  }
+
+  public void convert(ExecutorService executorService) throws ExecutionException {
+    // At this point the code rewritings described by repackaging and synthetic finalization have
+    // not been applied to the code objects. These code rewritings will be applied in the
+    // application writer. We therefore simulate that we are in D8, to allow building IR for each of
+    // the class initializers without applying the unapplied code rewritings, to avoid that we apply
+    // the lens more than once to the same piece of code.
+    AppView<AppInfo> appViewForConversion =
+        AppView.createForD8(AppInfo.createInitialAppInfo(appView.appInfo().app()));
+    appViewForConversion.setGraphLens(appView.graphLens());
+
+    // Build IR for each of the class initializers and finalize.
+    IRConverter converter = new IRConverter(appViewForConversion, Timing.empty());
+    ThreadUtils.processItems(
+        groups,
+        group -> {
+          ProgramMethod classInitializer = group.getTarget().getProgramClassInitializer();
+          IRCode code =
+              classInitializer
+                  .getDefinition()
+                  .getCode()
+                  .buildIR(classInitializer, appViewForConversion, classInitializer.getOrigin());
+          converter.removeDeadCodeAndFinalizeIR(
+              code, OptimizationFeedbackIgnore.getInstance(), Timing.empty());
+        },
+        executorService);
+  }
+
+  public boolean isEmpty() {
+    return groups.isEmpty();
+  }
+
+  public static class Builder {
+
+    private final AppView<? extends AppInfoWithClassHierarchy> appView;
+    private final List<MergeGroup> groups = new ArrayList<>();
+
+    private Builder(AppView<? extends AppInfoWithClassHierarchy> appView) {
+      this.appView = appView;
+    }
+
+    public Builder add(MergeGroup group) {
+      this.groups.add(group);
+      return this;
+    }
+
+    public SyntheticClassInitializerConverter build() {
+      return new SyntheticClassInitializerConverter(appView, groups);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AtMostOneClassInitializer.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AtMostOneClassInitializer.java
deleted file mode 100644
index a2b5887..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AtMostOneClassInitializer.java
+++ /dev/null
@@ -1,27 +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.horizontalclassmerging.policies;
-
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
-
-public class AtMostOneClassInitializer extends AtMostOneClassThatMatchesPolicy {
-
-  public AtMostOneClassInitializer(Mode mode) {
-    // TODO(b/182124475): Allow merging groups with multiple <clinit> methods in the final round of
-    //  merging.
-    assert mode.isFinal();
-  }
-
-  @Override
-  boolean atMostOneOf(DexProgramClass clazz) {
-    return clazz.hasClassInitializer();
-  }
-
-  @Override
-  public String getName() {
-    return "AtMostOneClassInitializer";
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitGroups.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitClassGroups.java
similarity index 85%
rename from src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitGroups.java
rename to src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitClassGroups.java
index c798f04..210f21d 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitGroups.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitClassGroups.java
@@ -13,18 +13,18 @@
 import java.util.Collections;
 import java.util.LinkedList;
 
-public class LimitGroups extends MultiClassPolicy {
+public class LimitClassGroups extends MultiClassPolicy {
 
   private final int maxGroupSize;
 
-  public LimitGroups(AppView<? extends AppInfoWithClassHierarchy> appView) {
+  public LimitClassGroups(AppView<? extends AppInfoWithClassHierarchy> appView) {
     maxGroupSize = appView.options().horizontalClassMergerOptions().getMaxGroupSize();
     assert maxGroupSize >= 2;
   }
 
   @Override
   public Collection<MergeGroup> apply(MergeGroup group) {
-    if (group.size() <= maxGroupSize) {
+    if (group.size() <= maxGroupSize || group.isInterfaceGroup()) {
       return Collections.singletonList(group);
     }
 
@@ -57,4 +57,9 @@
   public String getName() {
     return "LimitGroups";
   }
+
+  @Override
+  public boolean isIdentityForInterfaceGroups() {
+    return true;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java
index b091242..a2362bd 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java
@@ -22,11 +22,13 @@
 import com.android.tools.r8.horizontalclassmerging.MergeGroup;
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicyWithPreprocessing;
 import com.android.tools.r8.horizontalclassmerging.policies.NoDefaultInterfaceMethodCollisions.InterfaceInfo;
+import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.MapUtils;
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.collections.DexMethodSignatureSet;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import java.util.Collection;
 import java.util.Collections;
@@ -34,6 +36,7 @@
 import java.util.IdentityHashMap;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.Function;
 
 /**
  * This policy prevents that interface merging changes semantics of invoke-interface/invoke-virtual
@@ -153,7 +156,7 @@
     Map<DexType, Map<DexMethodSignature, Set<DexMethod>>>
         defaultMethodsInheritedBySubclassesPerClass =
             computeDefaultMethodsInheritedBySubclassesPerProgramClass(
-                classesOfInterest, inheritedDefaultMethodsPerClass, subtypingInfo);
+                classesOfInterest, inheritedDefaultMethodsPerClass, groups, subtypingInfo);
 
     // Store the computed information for each interface that is subject to merging.
     Map<DexType, InterfaceInfo> infos = new IdentityHashMap<>();
@@ -270,7 +273,16 @@
       computeDefaultMethodsInheritedBySubclassesPerProgramClass(
           Collection<DexProgramClass> classesOfInterest,
           Map<DexType, Map<DexMethodSignature, Set<DexMethod>>> inheritedDefaultMethodsPerClass,
+          Collection<MergeGroup> groups,
           SubtypingInfo subtypingInfo) {
+    // Build a mapping from class types to their merge group.
+    Map<DexType, Iterable<DexProgramClass>> classGroupsByType =
+        MapUtils.newIdentityHashMap(
+            builder ->
+                Iterables.filter(groups, MergeGroup::isClassGroup)
+                    .forEach(
+                        group -> group.forEach(clazz -> builder.accept(clazz.getType(), group))));
+
     // Copy the map from classes to their inherited default methods.
     Map<DexType, Map<DexMethodSignature, Set<DexMethod>>>
         defaultMethodsInheritedBySubclassesPerClass =
@@ -279,7 +291,28 @@
                 new HashMap<>(),
                 outerValue ->
                     MapUtils.clone(outerValue, new HashMap<>(), SetUtils::newIdentityHashSet));
-    BottomUpClassHierarchyTraversal.forProgramClasses(appView, subtypingInfo)
+
+    // Propagate data upwards. If classes A and B are in a merge group, we need to push the state
+    // for A to all of B's supertypes, and the state for B to all of A's supertypes.
+    //
+    // Therefore, it is important that we don't process any of A's supertypes until B has been
+    // processed, since that would lead to inadequate upwards propagation. To achieve this, we
+    // simulate that both A and B are subtypes of A's and B's supertypes.
+    Function<DexType, Iterable<DexType>> immediateSubtypesProvider =
+        type -> {
+          Set<DexType> immediateSubtypesAfterClassMerging = Sets.newIdentityHashSet();
+          for (DexType immediateSubtype : subtypingInfo.allImmediateSubtypes(type)) {
+            Iterable<DexProgramClass> group = classGroupsByType.get(immediateSubtype);
+            if (group != null) {
+              group.forEach(member -> immediateSubtypesAfterClassMerging.add(member.getType()));
+            } else {
+              immediateSubtypesAfterClassMerging.add(immediateSubtype);
+            }
+          }
+          return immediateSubtypesAfterClassMerging;
+        };
+
+    BottomUpClassHierarchyTraversal.forProgramClasses(appView, immediateSubtypesProvider)
         .visit(
             classesOfInterest,
             clazz -> {
@@ -287,16 +320,20 @@
               Map<DexMethodSignature, Set<DexMethod>> defaultMethodsToPropagate =
                   defaultMethodsInheritedBySubclassesPerClass.getOrDefault(
                       clazz.getType(), emptyMap());
-              for (DexType supertype : clazz.allImmediateSupertypes()) {
-                Map<DexMethodSignature, Set<DexMethod>>
-                    defaultMethodsInheritedBySubclassesForSupertype =
-                        defaultMethodsInheritedBySubclassesPerClass.computeIfAbsent(
-                            supertype, ignore -> new HashMap<>());
-                defaultMethodsToPropagate.forEach(
-                    (signature, methods) ->
-                        defaultMethodsInheritedBySubclassesForSupertype
-                            .computeIfAbsent(signature, ignore -> Sets.newIdentityHashSet())
-                            .addAll(methods));
+              Iterable<DexProgramClass> group =
+                  classGroupsByType.getOrDefault(clazz.getType(), IterableUtils.singleton(clazz));
+              for (DexProgramClass member : group) {
+                for (DexType supertype : member.allImmediateSupertypes()) {
+                  Map<DexMethodSignature, Set<DexMethod>>
+                      defaultMethodsInheritedBySubclassesForSupertype =
+                          defaultMethodsInheritedBySubclassesPerClass.computeIfAbsent(
+                              supertype, ignore -> new HashMap<>());
+                  defaultMethodsToPropagate.forEach(
+                      (signature, methods) ->
+                          defaultMethodsInheritedBySubclassesForSupertype
+                              .computeIfAbsent(signature, ignore -> Sets.newIdentityHashSet())
+                              .addAll(methods));
+                }
               }
             });
     defaultMethodsInheritedBySubclassesPerClass
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java
new file mode 100644
index 0000000..bbc5e39
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializerMerging.java
@@ -0,0 +1,101 @@
+// 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.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.DexMethodSignature;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
+import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
+import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
+import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Identifies when instance initializer merging is required and bails out. This is needed to ensure
+ * that we don't need to append extra null arguments at constructor call sites, such that the result
+ * of the final round of class merging can be described as a renaming only.
+ */
+public class NoInstanceInitializerMerging extends MultiClassPolicy {
+
+  public NoInstanceInitializerMerging(Mode mode) {
+    assert mode.isFinal();
+  }
+
+  @Override
+  public Collection<MergeGroup> apply(MergeGroup group) {
+    Map<MergeGroup, Map<DexMethodSignature, ProgramMethod>> newGroups = new LinkedHashMap<>();
+
+    for (DexProgramClass clazz : group) {
+      Map<DexMethodSignature, ProgramMethod> classSignatures = new HashMap<>();
+      clazz.forEachProgramInstanceInitializer(
+          method -> classSignatures.put(method.getMethodSignature(), method));
+
+      MergeGroup newGroup = null;
+      for (Entry<MergeGroup, Map<DexMethodSignature, ProgramMethod>> entry : newGroups.entrySet()) {
+        Map<DexMethodSignature, ProgramMethod> groupSignatures = entry.getValue();
+        if (canAddClassToGroup(classSignatures.values(), groupSignatures)) {
+          newGroup = entry.getKey();
+          groupSignatures.putAll(classSignatures);
+          break;
+        }
+      }
+
+      if (newGroup != null) {
+        newGroup.add(clazz);
+      } else {
+        newGroups.put(new MergeGroup(clazz), classSignatures);
+      }
+    }
+
+    return removeTrivialGroups(newGroups.keySet());
+  }
+
+  private boolean canAddClassToGroup(
+      Iterable<ProgramMethod> classMethods,
+      Map<DexMethodSignature, ProgramMethod> groupSignatures) {
+    for (ProgramMethod classMethod : classMethods) {
+      ProgramMethod groupMethod = groupSignatures.get(classMethod.getMethodSignature());
+      if (groupMethod != null && !equivalent(classMethod, groupMethod)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  // For now, only recognize constructors with 0 parameters that call the same parent constructor.
+  private boolean equivalent(ProgramMethod method, ProgramMethod other) {
+    if (!method.getProto().getParameters().isEmpty()) {
+      return false;
+    }
+
+    MethodOptimizationInfo optimizationInfo = method.getDefinition().getOptimizationInfo();
+    InstanceInitializerInfo instanceInitializerInfo =
+        optimizationInfo.getContextInsensitiveInstanceInitializerInfo();
+    if (instanceInitializerInfo.isDefaultInstanceInitializerInfo()) {
+      return false;
+    }
+
+    InstanceInitializerInfo otherInstanceInitializerInfo =
+        other.getDefinition().getOptimizationInfo().getContextInsensitiveInstanceInitializerInfo();
+    assert otherInstanceInitializerInfo.isNonTrivialInstanceInitializerInfo();
+    if (instanceInitializerInfo.getParent() != otherInstanceInitializerInfo.getParent()) {
+      return false;
+    }
+
+    return !method.getDefinition().getOptimizationInfo().mayHaveSideEffects()
+        && !other.getDefinition().getOptimizationInfo().mayHaveSideEffects();
+  }
+
+  @Override
+  public String getName() {
+    return "NoInstanceInitializerMerging";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializers.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializers.java
deleted file mode 100644
index 0acfe13..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializers.java
+++ /dev/null
@@ -1,29 +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.horizontalclassmerging.policies;
-
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
-import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
-
-public class NoInstanceInitializers extends SingleClassPolicy {
-
-  public NoInstanceInitializers(Mode mode) {
-    // TODO(b/181846319): Allow constructors, as long as the constructor protos remain unchanged
-    //  (in particular, we can't add nulls at constructor call sites).
-    assert mode.isFinal();
-  }
-
-  @Override
-  public boolean canMerge(DexProgramClass clazz) {
-    return !clazz.getMethodCollection().hasDirectMethods(DexEncodedMethod::isInstanceInitializer);
-  }
-
-  @Override
-  public String getName() {
-    return "NoInstanceInitializers";
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoNonPrivateVirtualMethods.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoNonPrivateVirtualMethods.java
deleted file mode 100644
index 81358f1..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoNonPrivateVirtualMethods.java
+++ /dev/null
@@ -1,27 +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.horizontalclassmerging.policies;
-
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
-import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
-
-public class NoNonPrivateVirtualMethods extends SingleClassPolicy {
-
-  public NoNonPrivateVirtualMethods(Mode mode) {
-    // TODO(b/181846319): Allow virtual methods as long as they do not require any merging.
-    assert mode.isFinal();
-  }
-
-  @Override
-  public boolean canMerge(DexProgramClass clazz) {
-    return clazz.isInterface() || !clazz.getMethodCollection().hasVirtualMethods();
-  }
-
-  @Override
-  public String getName() {
-    return "NoNonPrivateVirtualMethods";
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVirtualMethodMerging.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVirtualMethodMerging.java
new file mode 100644
index 0000000..79b2bb2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVirtualMethodMerging.java
@@ -0,0 +1,134 @@
+// 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.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexEncodedMethod;
+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.ProgramMethod;
+import com.android.tools.r8.graph.ResolutionResult.SingleResolutionResult;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
+import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
+import com.android.tools.r8.utils.IterableUtils;
+import com.android.tools.r8.utils.SetUtils;
+import com.google.common.collect.Iterables;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * Identifies when virtual method merging is required and bails out. This is needed to ensure that
+ * we don't need to synthesize any $r8$classId fields, such that the result of the final round of
+ * class merging can be described as a renaming only.
+ */
+public class NoVirtualMethodMerging extends MultiClassPolicy {
+
+  private final AppView<? extends AppInfoWithClassHierarchy> appView;
+
+  public NoVirtualMethodMerging(AppView<? extends AppInfoWithClassHierarchy> appView, Mode mode) {
+    assert mode.isFinal();
+    this.appView = appView;
+  }
+
+  @Override
+  public Collection<MergeGroup> apply(MergeGroup group) {
+    Map<MergeGroup, Map<DexMethodSignature, ProgramMethod>> newGroups = new LinkedHashMap<>();
+    for (DexProgramClass clazz : group) {
+      Map<DexMethodSignature, ProgramMethod> classMethods = new HashMap<>();
+      clazz.forEachProgramVirtualMethodMatching(
+          DexEncodedMethod::isNonAbstractVirtualMethod,
+          method -> classMethods.put(method.getMethodSignature(), method));
+
+      MergeGroup newGroup = null;
+      for (Entry<MergeGroup, Map<DexMethodSignature, ProgramMethod>> entry : newGroups.entrySet()) {
+        MergeGroup candidateGroup = entry.getKey();
+        Map<DexMethodSignature, ProgramMethod> groupMethods = entry.getValue();
+        if (canAddNonAbstractVirtualMethodsToGroup(
+            clazz, classMethods.values(), candidateGroup, groupMethods)) {
+          newGroup = candidateGroup;
+          groupMethods.putAll(classMethods);
+          break;
+        }
+      }
+
+      if (newGroup != null) {
+        newGroup.add(clazz);
+      } else {
+        newGroups.put(new MergeGroup(clazz), classMethods);
+      }
+    }
+    return removeTrivialGroups(newGroups.keySet());
+  }
+
+  private boolean canAddNonAbstractVirtualMethodsToGroup(
+      DexProgramClass clazz,
+      Collection<ProgramMethod> methods,
+      MergeGroup group,
+      Map<DexMethodSignature, ProgramMethod> groupMethods) {
+    // For each of clazz' virtual methods, check that adding these methods to the group does not
+    // require method merging.
+    for (ProgramMethod method : methods) {
+      ProgramMethod groupMethod = groupMethods.get(method.getMethodSignature());
+      if (groupMethod != null || hasNonAbstractDefinitionInHierarchy(group, method)) {
+        return false;
+      }
+    }
+    // For each of the group's virtual methods, check that adding these methods clazz does not
+    // require method merging.
+    for (ProgramMethod method : groupMethods.values()) {
+      if (hasNonAbstractDefinitionInHierarchy(clazz, method)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private boolean hasNonAbstractDefinitionInHierarchy(MergeGroup group, ProgramMethod method) {
+    return hasNonAbstractDefinitionInSuperClass(group.getSuperType(), method)
+        || hasNonAbstractDefinitionInSuperInterface(
+            SetUtils.newIdentityHashSet(IterableUtils.flatMap(group, DexClass::getInterfaces)),
+            method);
+  }
+
+  private boolean hasNonAbstractDefinitionInHierarchy(DexProgramClass clazz, ProgramMethod method) {
+    return hasNonAbstractDefinitionInSuperClass(clazz.getSuperType(), method)
+        || hasNonAbstractDefinitionInSuperInterface(clazz.getInterfaces(), method);
+  }
+
+  private boolean hasNonAbstractDefinitionInSuperClass(DexType superType, ProgramMethod method) {
+    SingleResolutionResult resolutionResult =
+        appView
+            .appInfo()
+            .resolveMethodOnClass(method.getReference(), superType)
+            .asSingleResolution();
+    return resolutionResult != null && !resolutionResult.getResolvedMethod().isAbstract();
+  }
+
+  private boolean hasNonAbstractDefinitionInSuperInterface(
+      Iterable<DexType> interfaceTypes, ProgramMethod method) {
+    return Iterables.any(
+        interfaceTypes,
+        interfaceType -> {
+          SingleResolutionResult resolutionResult =
+              appView
+                  .appInfo()
+                  .resolveMethodOnInterface(interfaceType, method.getReference())
+                  .asSingleResolution();
+          return resolutionResult != null && !resolutionResult.getResolvedMethod().isAbstract();
+        });
+  }
+
+  @Override
+  public String getName() {
+    return "NoVirtualMethodMerging";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/OnlyDirectlyConnectedOrUnrelatedInterfaces.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/OnlyDirectlyConnectedOrUnrelatedInterfaces.java
index d9e6cd1..fc457f7 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/OnlyDirectlyConnectedOrUnrelatedInterfaces.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/OnlyDirectlyConnectedOrUnrelatedInterfaces.java
@@ -8,25 +8,26 @@
 
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.SubtypingInfo;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.MergeGroup;
-import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
+import com.android.tools.r8.horizontalclassmerging.MultiClassPolicyWithPreprocessing;
+import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.WorkList;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterators;
-import com.google.common.collect.Sets;
+import com.google.common.collect.Iterables;
+import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.IdentityHashMap;
-import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.function.Consumer;
+import java.util.function.Function;
 
 /**
  * This policy ensures that we do not create cycles in the class hierarchy as a result of interface
@@ -53,11 +54,15 @@
  *   interface J extends IK, ... {}
  * </pre>
  */
-public class OnlyDirectlyConnectedOrUnrelatedInterfaces extends MultiClassPolicy {
+public class OnlyDirectlyConnectedOrUnrelatedInterfaces
+    extends MultiClassPolicyWithPreprocessing<SubtypingInfo> {
 
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
   private final Mode mode;
 
+  // The interface merge groups that this policy has committed to so far.
+  private final Map<DexProgramClass, MergeGroup> committed = new IdentityHashMap<>();
+
   public OnlyDirectlyConnectedOrUnrelatedInterfaces(
       AppView<? extends AppInfoWithClassHierarchy> appView, Mode mode) {
     this.appView = appView;
@@ -65,116 +70,84 @@
   }
 
   @Override
-  public Collection<MergeGroup> apply(MergeGroup group) {
+  public Collection<MergeGroup> apply(MergeGroup group, SubtypingInfo subtypingInfo) {
     if (!group.isInterfaceGroup()) {
       return ImmutableList.of(group);
     }
 
-    Set<DexProgramClass> classes = new LinkedHashSet<>(group.getClasses());
-    Map<DexProgramClass, Set<DexProgramClass>> ineligibleForMerging =
-        computeIneligibleForMergingGraph(classes);
-    if (ineligibleForMerging.isEmpty()) {
-      return ImmutableList.of(group);
+    List<MergeGroupWithInfo> newGroupsWithInfo = new ArrayList<>();
+    for (DexProgramClass clazz : group) {
+      Set<DexProgramClass> superInterfaces = computeSuperInterfaces(clazz);
+      Set<DexProgramClass> subInterfaces = computeSubInterfaces(clazz, subtypingInfo);
+
+      MergeGroupWithInfo newGroup = null;
+      for (MergeGroupWithInfo candidateGroup : newGroupsWithInfo) {
+        // Check if adding `clazz` to `candidateGroup` would introduce a super interface that is
+        // also a sub interface. In that case we must abort since merging would lead to a cycle in
+        // the class hierarchy.
+        if (candidateGroup.isSafeToAddSubAndSuperInterfaces(
+            clazz, subInterfaces, superInterfaces)) {
+          newGroup = candidateGroup;
+          break;
+        }
+      }
+
+      if (newGroup != null) {
+        newGroup.add(clazz, superInterfaces, subInterfaces);
+      } else {
+        newGroupsWithInfo.add(new MergeGroupWithInfo(clazz, superInterfaces, subInterfaces));
+      }
     }
 
-    // Extract sub-merge groups from the graph in such a way that all pairs of interfaces in each
-    // merge group are not connected by an edge in the graph.
     List<MergeGroup> newGroups = new LinkedList<>();
-    while (!classes.isEmpty()) {
-      Iterator<DexProgramClass> iterator = classes.iterator();
-      MergeGroup newGroup = new MergeGroup(iterator.next());
-      Iterators.addAll(
-          newGroup,
-          Iterators.filter(
-              iterator,
-              candidate -> !isConnectedToGroup(candidate, newGroup, ineligibleForMerging)));
+    for (MergeGroupWithInfo newGroupWithInfo : newGroupsWithInfo) {
+      MergeGroup newGroup = newGroupWithInfo.getGroup();
       if (!newGroup.isTrivial()) {
         newGroups.add(newGroup);
+        newGroup.forEach(clazz -> committed.put(clazz, newGroup));
       }
-      classes.removeAll(newGroup.getClasses());
     }
     return newGroups;
   }
 
-  /**
-   * Computes an undirected graph, where the nodes are the interfaces from the merge group, and an
-   * edge I <-> J represents that I and J are not eligible for merging.
-   *
-   * <p>We will insert an edge I <-> J, if interface I inherits from interface J, and the path from
-   * I to J in the class hierarchy includes an interface K that is outside the merge group. Note
-   * that if I extends J directly we will not insert an edge I <-> J (unless there are multiple
-   * paths in the class hierarchy from I to J, and one of the paths goes through an interface
-   * outside the merge group).
-   */
-  private Map<DexProgramClass, Set<DexProgramClass>> computeIneligibleForMergingGraph(
-      Set<DexProgramClass> classes) {
-    Map<DexProgramClass, Set<DexProgramClass>> ineligibleForMerging = new IdentityHashMap<>();
-    for (DexProgramClass clazz : classes) {
-      forEachIndirectlyReachableInterfaceInMergeGroup(
-          clazz,
-          classes,
-          other ->
-              ineligibleForMerging
-                  .computeIfAbsent(clazz, ignore -> Sets.newIdentityHashSet())
-                  .add(other));
-    }
-    return ineligibleForMerging;
+  private Set<DexProgramClass> computeSuperInterfaces(DexProgramClass clazz) {
+    return computeTransitiveSubOrSuperInterfaces(clazz, DexClass::getInterfaces);
   }
 
-  private void forEachIndirectlyReachableInterfaceInMergeGroup(
-      DexProgramClass clazz, Set<DexProgramClass> classes, Consumer<DexProgramClass> consumer) {
-    // First find the set of interfaces that can be reached via paths in the class hierarchy from
-    // the given interface, without visiting any interfaces outside the merge group.
-    WorkList<DexType> workList = WorkList.newIdentityWorkList(clazz.getInterfaces());
-    while (workList.hasNext()) {
-      DexProgramClass directlyReachableInterface =
-          asProgramClassOrNull(appView.definitionFor(workList.next()));
-      if (directlyReachableInterface == null) {
-        continue;
-      }
-      // If the implemented interface is a member of the merge group, then include it's interfaces.
-      if (classes.contains(directlyReachableInterface)) {
-        workList.addIfNotSeen(directlyReachableInterface.getInterfaces());
-      }
-    }
-
-    // Initialize a new worklist with the first layer of indirectly reachable interface types.
-    Set<DexType> directlyReachableInterfaceTypes = workList.getSeenSet();
-    workList = WorkList.newIdentityWorkList();
-    for (DexType directlyReachableInterfaceType : directlyReachableInterfaceTypes) {
-      DexProgramClass directlyReachableInterface =
-          asProgramClassOrNull(appView.definitionFor(directlyReachableInterfaceType));
-      if (directlyReachableInterface != null) {
-        workList.addIfNotSeen(directlyReachableInterface.getInterfaces());
-      }
-    }
-
-    // Report all interfaces from the merge group that are reachable in the class hierarchy from the
-    // worklist.
-    while (workList.hasNext()) {
-      DexProgramClass indirectlyReachableInterface =
-          asProgramClassOrNull(appView.definitionFor(workList.next()));
-      if (indirectlyReachableInterface == null) {
-        continue;
-      }
-      if (classes.contains(indirectlyReachableInterface)) {
-        consumer.accept(indirectlyReachableInterface);
-      }
-      workList.addIfNotSeen(indirectlyReachableInterface.getInterfaces());
-    }
+  private Set<DexProgramClass> computeSubInterfaces(
+      DexProgramClass clazz, SubtypingInfo subtypingInfo) {
+    return computeTransitiveSubOrSuperInterfaces(
+        clazz, definition -> subtypingInfo.allImmediateExtendsSubtypes(definition.getType()));
   }
 
-  private boolean isConnectedToGroup(
+  private Set<DexProgramClass> computeTransitiveSubOrSuperInterfaces(
       DexProgramClass clazz,
-      MergeGroup group,
-      Map<DexProgramClass, Set<DexProgramClass>> ineligibleForMerging) {
-    for (DexProgramClass member : group) {
-      if (ineligibleForMerging.getOrDefault(clazz, Collections.emptySet()).contains(member)
-          || ineligibleForMerging.getOrDefault(member, Collections.emptySet()).contains(clazz)) {
-        return true;
+      Function<DexProgramClass, Iterable<DexType>> immediateSubOrSuperInterfacesProvider) {
+    WorkList<DexProgramClass> workList = WorkList.newWorkList(new LinkedHashSet<>());
+    // Intentionally not marking `clazz` as seen, since we only want the strict sub/super types.
+    workList.addIgnoringSeenSet(clazz);
+    while (workList.hasNext()) {
+      DexProgramClass interfaceDefinition = workList.next();
+      MergeGroup group = committed.get(interfaceDefinition);
+      if (group != null) {
+        workList.addIfNotSeen(group);
+      }
+      for (DexType immediateSubOrSuperInterfaceType :
+          immediateSubOrSuperInterfacesProvider.apply(interfaceDefinition)) {
+        DexProgramClass immediateSubOrSuperInterface =
+            asProgramClassOrNull(appView.definitionFor(immediateSubOrSuperInterfaceType));
+        if (immediateSubOrSuperInterface != null) {
+          workList.addIfNotSeen(immediateSubOrSuperInterface);
+        }
       }
     }
-    return false;
+    assert !workList.isSeen(clazz);
+    return workList.getMutableSeenSet();
+  }
+
+  @Override
+  public void clear() {
+    committed.clear();
   }
 
   @Override
@@ -183,7 +156,80 @@
   }
 
   @Override
+  public SubtypingInfo preprocess(Collection<MergeGroup> groups) {
+    return new SubtypingInfo(appView);
+  }
+
+  @Override
   public boolean shouldSkipPolicy() {
     return !appView.options().horizontalClassMergerOptions().isInterfaceMergingEnabled(mode);
   }
+
+  static class MergeGroupWithInfo {
+
+    private final MergeGroup group;
+    private final Set<DexProgramClass> members;
+    private final Set<DexProgramClass> superInterfaces;
+    private final Set<DexProgramClass> subInterfaces;
+
+    MergeGroupWithInfo(
+        DexProgramClass clazz,
+        Set<DexProgramClass> superInterfaces,
+        Set<DexProgramClass> subInterfaces) {
+      this.group = new MergeGroup(clazz);
+      this.members = SetUtils.newIdentityHashSet(clazz);
+      this.superInterfaces = superInterfaces;
+      this.subInterfaces = subInterfaces;
+    }
+
+    void add(
+        DexProgramClass clazz,
+        Set<DexProgramClass> newSuperInterfaces,
+        Set<DexProgramClass> newSubInterfaces) {
+      group.add(clazz);
+      members.add(clazz);
+      Iterables.addAll(
+          superInterfaces,
+          Iterables.filter(
+              newSuperInterfaces, superInterface -> !members.contains(superInterface)));
+      superInterfaces.remove(clazz);
+      Iterables.addAll(
+          subInterfaces,
+          Iterables.filter(newSubInterfaces, subInterface -> !members.contains(subInterface)));
+      subInterfaces.remove(clazz);
+    }
+
+    MergeGroup getGroup() {
+      return group;
+    }
+
+    boolean isSafeToAddSubAndSuperInterfaces(
+        DexProgramClass clazz,
+        Set<DexProgramClass> newSubInterfaces,
+        Set<DexProgramClass> newSuperInterfaces) {
+      // Check that adding the new sub and super interfaces to the group is safe.
+      for (DexProgramClass newSubInterface : newSubInterfaces) {
+        if (!group.contains(newSubInterface) && superInterfaces.contains(newSubInterface)) {
+          return false;
+        }
+      }
+      for (DexProgramClass newSuperInterface : newSuperInterfaces) {
+        if (!group.contains(newSuperInterface) && subInterfaces.contains(newSuperInterface)) {
+          return false;
+        }
+      }
+      // Check that adding the sub and super interfaces of the group to the current class is safe.
+      for (DexProgramClass subInterface : subInterfaces) {
+        if (subInterface != clazz && newSuperInterfaces.contains(subInterface)) {
+          return false;
+        }
+      }
+      for (DexProgramClass superInterface : superInterfaces) {
+        if (superInterface != clazz && newSubInterfaces.contains(superInterface)) {
+          return false;
+        }
+      }
+      return true;
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteBuilderShrinker.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteBuilderShrinker.java
index da0ed41..06e1e90 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteBuilderShrinker.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/GeneratedMessageLiteBuilderShrinker.java
@@ -257,7 +257,7 @@
             ImmutableList.of(builderValue, defaultInstanceValue)));
 
     converter.removeDeadCodeAndFinalizeIR(
-        dynamicMethod, code, OptimizationFeedbackSimple.getInstance(), Timing.empty());
+        code, OptimizationFeedbackSimple.getInstance(), Timing.empty());
   }
 
   public static void addInliningHeuristicsForBuilderInlining(
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoEnqueuerUseRegistry.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoEnqueuerUseRegistry.java
index 17758fa..f8aae86 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoEnqueuerUseRegistry.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoEnqueuerUseRegistry.java
@@ -11,13 +11,16 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.analysis.proto.schema.ProtoEnqueuerExtension;
 import com.android.tools.r8.shaking.DefaultEnqueuerUseRegistry;
 import com.android.tools.r8.shaking.Enqueuer;
 import com.android.tools.r8.shaking.EnqueuerUseRegistryFactory;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import java.util.ListIterator;
+import java.util.Map;
 
 public class ProtoEnqueuerUseRegistry extends DefaultEnqueuerUseRegistry {
 
@@ -28,8 +31,9 @@
   public ProtoEnqueuerUseRegistry(
       AppView<? extends AppInfoWithClassHierarchy> appView,
       ProgramMethod currentMethod,
-      Enqueuer enqueuer) {
-    super(appView, currentMethod, enqueuer);
+      Enqueuer enqueuer,
+      Map<DexReference, AndroidApiLevel> apiLevelReferenceMap) {
+    super(appView, currentMethod, enqueuer, apiLevelReferenceMap);
     this.references = appView.protoShrinker().references;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/sideeffect/ClassInitializerSideEffectAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/sideeffect/ClassInitializerSideEffectAnalysis.java
index c6fdbab..ad34d29 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/sideeffect/ClassInitializerSideEffectAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/sideeffect/ClassInitializerSideEffectAnalysis.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.ir.code.ArrayPut;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.Instruction.SideEffectAssumption;
 import com.android.tools.r8.ir.code.StaticPut;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -82,6 +83,14 @@
         continue;
       }
 
+      if (instruction.isInvokeConstructor(appView.dexItemFactory())) {
+        if (instruction.instructionMayHaveSideEffects(
+            appView, context, SideEffectAssumption.IGNORE_RECEIVER_FIELD_ASSIGNMENTS)) {
+          return ClassInitializerSideEffect.SIDE_EFFECTS_THAT_CANNOT_BE_POSTPONED;
+        }
+        continue;
+      }
+
       // For other instructions, bail out if they may have side effects.
       if (instruction.instructionMayHaveSideEffects(appView, context)) {
         return ClassInitializerSideEffect.SIDE_EFFECTS_THAT_CANNOT_BE_POSTPONED;
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
index 7dbc0ad..df8f00e 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlock.java
@@ -595,6 +595,10 @@
     return filled;
   }
 
+  public void setFilled() {
+    filled = true;
+  }
+
   public void setFilledForTesting() {
     filled = true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
index 7bd9cf9..6917a40 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
@@ -325,6 +325,10 @@
       return false;
     }
     ProgramMethod context = code.context();
+    if (!toBeReplaced.instructionMayHaveSideEffects(appView, context)) {
+      removeOrReplaceByDebugLocalRead();
+      return true;
+    }
     if (toBeReplaced.instructionMayHaveSideEffects(
         appView, context, Instruction.SideEffectAssumption.CLASS_ALREADY_INITIALIZED)) {
       return false;
diff --git a/src/main/java/com/android/tools/r8/ir/code/Goto.java b/src/main/java/com/android/tools/r8/ir/code/Goto.java
index 953bdcf..dde7f0d 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Goto.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Goto.java
@@ -22,6 +22,10 @@
     setBlock(block);
   }
 
+  public static Builder builder() {
+    return new Builder();
+  }
+
   @Override
   public int opcode() {
     return Opcodes.GOTO;
@@ -121,4 +125,24 @@
   public boolean isAllowedAfterThrowingInstruction() {
     return true;
   }
+
+  public static class Builder extends BuilderBase<Builder, Goto> {
+
+    private BasicBlock target;
+
+    public Builder setTarget(BasicBlock target) {
+      this.target = target;
+      return self();
+    }
+
+    @Override
+    public Goto build() {
+      return amend(new Goto(target));
+    }
+
+    @Override
+    public Builder self() {
+      return this;
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index eb5a275..a1c7d45 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -1502,6 +1502,15 @@
           }
         };
 
+    public static final SideEffectAssumption IGNORE_RECEIVER_FIELD_ASSIGNMENTS =
+        new SideEffectAssumption() {
+
+          @Override
+          public boolean canIgnoreInstanceFieldAssignmentsToReceiver() {
+            return true;
+          }
+        };
+
     public static final SideEffectAssumption INVOKED_METHOD_DOES_NOT_HAVE_SIDE_EFFECTS =
         new SideEffectAssumption() {
 
@@ -1528,6 +1537,10 @@
       return false;
     }
 
+    public boolean canIgnoreInstanceFieldAssignmentsToReceiver() {
+      return false;
+    }
+
     public boolean canAssumeReceiverIsNotNull() {
       return false;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
index b4ff341..83dfa77 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeDirect.java
@@ -174,7 +174,8 @@
   @Override
   public DeadInstructionResult canBeDeadCode(AppView<?> appView, IRCode code) {
     ProgramMethod context = code.context();
-    if (instructionMayHaveSideEffects(appView, context)) {
+    if (instructionMayHaveSideEffects(
+        appView, context, SideEffectAssumption.IGNORE_RECEIVER_FIELD_ASSIGNMENTS)) {
       return DeadInstructionResult.notDead();
     }
     if (!getInvokedMethod().isInstanceInitializer(appView.dexItemFactory())) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java b/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
index 94ab740..39f52cb 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeMethodWithReceiver.java
@@ -259,7 +259,8 @@
 
     DexEncodedMethod singleTargetDefinition = singleTarget.getDefinition();
     MethodOptimizationInfo optimizationInfo = singleTargetDefinition.getOptimizationInfo();
-    if (singleTargetDefinition.isInstanceInitializer()) {
+    if (assumption.canIgnoreInstanceFieldAssignmentsToReceiver()
+        && singleTargetDefinition.isInstanceInitializer()) {
       assert isInvokeDirect();
       InstanceInitializerInfo initializerInfo =
           optimizationInfo.getInstanceInitializerInfo(asInvokeDirect());
diff --git a/src/main/java/com/android/tools/r8/ir/code/Position.java b/src/main/java/com/android/tools/r8/ir/code/Position.java
index 0d054ef..6cba1ee 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Position.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Position.java
@@ -101,6 +101,10 @@
     return position;
   }
 
+  public boolean hasCallerPosition() {
+    return callerPosition != null;
+  }
+
   @Override
   public Position self() {
     return this;
@@ -133,6 +137,10 @@
     return lastPosition;
   }
 
+  public Position withCallerPosition(Position callerPosition) {
+    return new Position(line, file, method, callerPosition, synthetic);
+  }
+
   @Override
   public boolean equals(Object other) {
     return other instanceof Position && compareTo((Position) other) == 0;
diff --git a/src/main/java/com/android/tools/r8/ir/code/Return.java b/src/main/java/com/android/tools/r8/ir/code/Return.java
index 19c9887..e2f81a9 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Return.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Return.java
@@ -28,6 +28,10 @@
     super(value);
   }
 
+  public static Builder builder() {
+    return new Builder();
+  }
+
   @Override
   public int opcode() {
     return Opcodes.RETURN;
@@ -126,4 +130,17 @@
     builder.add(
         isReturnVoid() ? new CfReturnVoid() : new CfReturn(ValueType.fromType(getReturnType())));
   }
+
+  public static class Builder extends BuilderBase<Builder, Return> {
+
+    @Override
+    public Return build() {
+      return amend(new Return());
+    }
+
+    @Override
+    public Builder self() {
+      return this;
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
index 7d332f0..e01cd47 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CfBuilder.java
@@ -180,7 +180,7 @@
     DexBuilder.removeRedundantDebugPositions(code);
     CfCode code = buildCfCode();
     assert verifyInvokeInterface(code, appView);
-    assert code.verifyFrames(method, appView, this.code.origin, false).isValid();
+    assert code.verifyFrames(method, appView, this.code.origin).isValid();
     return code;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index 0437f8d..4ae55f6 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -114,7 +114,6 @@
 import com.android.tools.r8.ir.code.ValueTypeConstraint;
 import com.android.tools.r8.ir.code.Xor;
 import com.android.tools.r8.ir.optimize.info.CallSiteOptimizationInfo;
-import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.naming.dexitembasedstring.NameComputationInfo;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApiLevel;
@@ -444,28 +443,22 @@
       AppView<?> appView,
       SourceCode source,
       Origin origin,
-      MethodProcessor processor,
-      NumberGenerator valueNumberGenerator) {
-    RewrittenPrototypeDescription protoChanges =
-        processor.shouldApplyCodeRewritings(method)
-            ? lookupPrototypeChanges(appView, method)
-            : RewrittenPrototypeDescription.none();
+      NumberGenerator valueNumberGenerator,
+      RewrittenPrototypeDescription protoChanges) {
     return new IRBuilder(method, appView, source, origin, protoChanges, valueNumberGenerator);
   }
 
-  private static RewrittenPrototypeDescription lookupPrototypeChanges(
+  public static RewrittenPrototypeDescription lookupPrototypeChanges(
       AppView<?> appView, ProgramMethod method) {
-    RewrittenPrototypeDescription prototypeChanges =
-        appView.graphLens().lookupPrototypeChangesForMethodDefinition(method.getReference());
-    if (Log.ENABLED && prototypeChanges.getArgumentInfoCollection().hasRemovedArguments()) {
-      Log.info(
-          IRBuilder.class,
-          "Removed "
-              + prototypeChanges.getArgumentInfoCollection().numberOfRemovedArguments()
-              + " arguments from "
-              + method.toSourceString());
+    return appView.graphLens().lookupPrototypeChangesForMethodDefinition(method.getReference());
+  }
+
+  public static RewrittenPrototypeDescription lookupPrototypeChangesForInlinee(
+      AppView<?> appView, ProgramMethod method, MethodProcessor methodProcessor) {
+    if (methodProcessor.shouldApplyCodeRewritings(method)) {
+      return appView.graphLens().lookupPrototypeChangesForMethodDefinition(method.getReference());
     }
-    return prototypeChanges;
+    return RewrittenPrototypeDescription.none();
   }
 
   private IRBuilder(
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 785fb64..fea728d 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
@@ -182,7 +182,6 @@
    * (i.e., whether we are running R8). See {@link AppView#enableWholeProgramOptimizations()}.
    */
   public IRConverter(AppView<?> appView, Timing timing, CfgPrinter printer) {
-    assert appView.appInfo().hasLiveness() || appView.graphLens().isIdentityLens();
     assert appView.options() != null;
     assert appView.options().programConsumer != null;
     assert timing != null;
@@ -816,7 +815,7 @@
               outliner.applyOutliningCandidate(code);
               printMethod(code, "IR after outlining (SSA)", null);
               removeDeadCodeAndFinalizeIR(
-                  code.context(), code, OptimizationFeedbackIgnore.getInstance(), Timing.empty());
+                  code, OptimizationFeedbackIgnore.getInstance(), Timing.empty());
             },
             executorService);
         feedback.updateVisibleOptimizationInfo();
@@ -941,8 +940,7 @@
     IRCode code = method.buildIR(appView);
     assert code != null;
     codeRewriter.rewriteMoveResult(code);
-    removeDeadCodeAndFinalizeIR(
-        method, code, OptimizationFeedbackIgnore.getInstance(), Timing.empty());
+    removeDeadCodeAndFinalizeIR(code, OptimizationFeedbackIgnore.getInstance(), Timing.empty());
   }
 
   private void generateDesugaredLibraryAPIWrappers(
@@ -1475,7 +1473,8 @@
 
     previous = printMethod(code, "IR after class inlining (SSA)", previous);
 
-    if (interfaceMethodRewriter != null) {
+    // TODO(b/183998768): Enable interface method rewriter cf to cf also in R8.
+    if (interfaceMethodRewriter != null && appView.enableWholeProgramOptimizations()) {
       timing.begin("Rewrite interface methods");
       interfaceMethodRewriter.rewriteMethodReferences(
           code, methodProcessor, methodProcessingContext);
@@ -1662,7 +1661,7 @@
   }
 
   public void removeDeadCodeAndFinalizeIR(
-      ProgramMethod method, IRCode code, OptimizationFeedback feedback, Timing timing) {
+      IRCode code, OptimizationFeedback feedback, Timing timing) {
     if (stringSwitchRemover != null) {
       stringSwitchRemover.run(code);
     }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/StringSwitchConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/StringSwitchConverter.java
index 50bec23..a377238 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/StringSwitchConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/StringSwitchConverter.java
@@ -311,7 +311,7 @@
 
       private final BasicBlock continuationBlock;
       private final DexItemFactory dexItemFactory;
-      private final Phi idValue;
+      private final Phi intermediateIdValue;
       private final Value stringValue;
 
       Builder(
@@ -321,10 +321,38 @@
           Value stringValue) {
         this.continuationBlock = continuationBlock;
         this.dexItemFactory = dexItemFactory;
-        this.idValue = idValue;
+        this.intermediateIdValue = getIntermediateIdValueOrElse(idValue, idValue);
         this.stringValue = stringValue;
       }
 
+      // Finds the intermediate id value from the given (non-intermediate) id value. If the non-
+      // intermediate id value v_id has the following structure, then v_id_intermediate is returned.
+      //
+      //   v_id_intermediate = phi(v1(0), v2(1), v_n(n-1))
+      //   v_id = phi(v0(-1), v_id_intermediate)
+      //
+      // Normally, this intermediate value is not present, and the code will have the following
+      // structure:
+      //
+      //   v_id = phi(v0(-1), v1(0), v2(1), v_n(n-1))
+      private Phi getIntermediateIdValueOrElse(Phi idValue, Phi defaultValue) {
+        if (idValue.getOperands().size() != 2) {
+          return defaultValue;
+        }
+        Phi intermediateIdValue = null;
+        for (Value operand : idValue.getOperands()) {
+          if (operand.isPhi()) {
+            if (intermediateIdValue != null) {
+              return defaultValue;
+            }
+            intermediateIdValue = operand.asPhi();
+          }
+        }
+        assert intermediateIdValue == null
+            || intermediateIdValue.getOperands().stream().noneMatch(Value::isPhi);
+        return intermediateIdValue != null ? intermediateIdValue : defaultValue;
+      }
+
       // Attempts to build a mapping from strings to their ids starting from the given block. The
       // mapping is built by traversing the control flow graph upwards, so the given block is
       // expected to be the last block in the sequence of blocks that compare the hash code of the
@@ -569,17 +597,17 @@
         InstructionIterator instructionIterator = block.iterator();
         ConstNumber constNumberInstruction;
         if (block.isTrivialGoto()) {
-          if (block.getUniqueNormalSuccessor() != idValue.getBlock()) {
+          if (block.getUniqueNormalSuccessor() != intermediateIdValue.getBlock()) {
             return false;
           }
-          int predecessorIndex = idValue.getBlock().getPredecessors().indexOf(block);
+          int predecessorIndex = intermediateIdValue.getBlock().getPredecessors().indexOf(block);
           constNumberInstruction =
-              asConstNumberOrNull(idValue.getOperand(predecessorIndex).definition);
+              asConstNumberOrNull(intermediateIdValue.getOperand(predecessorIndex).definition);
         } else {
           constNumberInstruction = instructionIterator.next().asConstNumber();
         }
         if (constNumberInstruction == null
-            || !idValue.getOperands().contains(constNumberInstruction.outValue())) {
+            || !intermediateIdValue.getOperands().contains(constNumberInstruction.outValue())) {
           return false;
         }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
index cc28692..3eada1f 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
@@ -87,8 +87,11 @@
 
   @Override
   public boolean needsDesugaring(CfInstruction instruction, ProgramMethod context) {
-    return instruction.isInvoke()
-        && getMethodProviderOrNull(instruction.asInvoke().getMethod()) != null;
+    return instruction.isInvoke() && methodIsBackport(instruction.asInvoke().getMethod());
+  }
+
+  public boolean methodIsBackport(DexMethod method) {
+    return getMethodProviderOrNull(method) != null;
   }
 
   public static List<DexMethod> generateListOfBackportedMethods(
@@ -165,19 +168,19 @@
 
       DexItemFactory factory = options.itemFactory;
 
-      if (options.minApiLevel < AndroidApiLevel.K.getLevel()) {
+      if (options.minApiLevel.isLessThan(AndroidApiLevel.K)) {
         initializeAndroidKMethodProviders(factory);
       }
-      if (options.minApiLevel < AndroidApiLevel.N.getLevel()) {
+      if (options.minApiLevel.isLessThan(AndroidApiLevel.N)) {
         initializeAndroidNMethodProviders(factory);
       }
-      if (options.minApiLevel < AndroidApiLevel.O.getLevel()) {
+      if (options.minApiLevel.isLessThan(AndroidApiLevel.O)) {
         initializeAndroidOMethodProviders(factory);
       }
-      if (options.minApiLevel < AndroidApiLevel.R.getLevel()) {
+      if (options.minApiLevel.isLessThan(AndroidApiLevel.R)) {
         initializeAndroidRMethodProviders(factory);
       }
-      if (options.minApiLevel < AndroidApiLevel.S.getLevel()) {
+      if (options.minApiLevel.isLessThan(AndroidApiLevel.S)) {
         initializeAndroidSMethodProviders(factory);
       }
 
@@ -186,13 +189,13 @@
       // libraries or natively. If Optional/Stream class is not present, we do not desugar to
       // avoid confusion in error messages.
       if (appView.rewritePrefix.hasRewrittenType(factory.optionalType, appView)
-          || options.minApiLevel >= AndroidApiLevel.N.getLevel()) {
+          || options.minApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.N)) {
         initializeJava9OptionalMethodProviders(factory);
         initializeJava10OptionalMethodProviders(factory);
         initializeJava11OptionalMethodProviders(factory);
       }
       if (appView.rewritePrefix.hasRewrittenType(factory.streamType, appView)
-          || options.minApiLevel >= AndroidApiLevel.N.getLevel()) {
+          || options.minApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.N)) {
         initializeStreamMethodProviders(factory);
       }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaring.java
index 11c18d0..9d7b629 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/CfInstructionDesugaring.java
@@ -36,4 +36,14 @@
    * <p>This should return true if-and-only-if {@link #desugarInstruction} returns non-null.
    */
   boolean needsDesugaring(CfInstruction instruction, ProgramMethod context);
+
+  /**
+   * Returns true if and only if needsDesugaring() answering true implies a desugaring is needed.
+   * Some optimizations may have some heuristics, so that needsDesugaring() answers true in rare
+   * case even if no desugaring is needed.
+   */
+  // TODO(b/187913003): Fixing interface desugaring should eliminate the need for this.
+  default boolean hasPreciseNeedsDesugaring() {
+    return true;
+  }
 }
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 a0c4c64..6951b39 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
@@ -15,6 +15,7 @@
 import com.android.tools.r8.ir.desugar.backports.BackportedMethodDesugaringEventConsumer;
 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.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;
@@ -40,7 +41,8 @@
         LambdaDesugaringEventConsumer,
         NestBasedAccessDesugaringEventConsumer,
         RecordDesugaringEventConsumer,
-        TwrCloseResourceDesugaringEventConsumer {
+        TwrCloseResourceDesugaringEventConsumer,
+        InterfaceMethodDesugaringEventConsumer {
 
   public static D8CfInstructionDesugaringEventConsumer createForD8(
       D8MethodProcessor methodProcessor) {
@@ -59,6 +61,17 @@
     return new CfInstructionDesugaringEventConsumer() {
 
       @Override
+      public void acceptThrowMethod(ProgramMethod method, ProgramMethod context) {
+        assert false;
+      }
+
+      @Override
+      public void acceptInvokeStaticInterfaceOutliningMethod(
+          ProgramMethod method, ProgramMethod context) {
+        assert false;
+      }
+
+      @Override
       public void acceptRecordClass(DexProgramClass recordClass) {
         assert false;
       }
@@ -168,6 +181,17 @@
       methodProcessor.scheduleDesugaredMethodForProcessing(closeMethod);
     }
 
+    @Override
+    public void acceptThrowMethod(ProgramMethod method, ProgramMethod context) {
+      methodProcessor.scheduleDesugaredMethodForProcessing(method);
+    }
+
+    @Override
+    public void acceptInvokeStaticInterfaceOutliningMethod(
+        ProgramMethod method, ProgramMethod context) {
+      methodProcessor.scheduleDesugaredMethodForProcessing(method);
+    }
+
     public List<ProgramMethod> finalizeDesugaring(
         AppView<?> appView, ClassConverterResult.Builder classConverterResultBuilder) {
       List<ProgramMethod> needsProcessing = new ArrayList<>();
@@ -257,6 +281,17 @@
     }
 
     @Override
+    public void acceptThrowMethod(ProgramMethod method, ProgramMethod context) {
+      assert false : "TODO(b/183998768): To be implemented";
+    }
+
+    @Override
+    public void acceptInvokeStaticInterfaceOutliningMethod(
+        ProgramMethod method, ProgramMethod context) {
+      assert false : "TODO(b/183998768): To be implemented";
+    }
+
+    @Override
     public void acceptBackportedMethod(ProgramMethod backportedMethod, ProgramMethod context) {
       // Intentionally empty. The backported 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.
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java
index 4f05b2e..6394d39 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryAPIConverter.java
@@ -190,10 +190,10 @@
         && appView.options().isDesugaredLibraryCompilation()) {
       return false;
     }
-    return overridesLibraryMethod(method);
+    return overridesNonFinalLibraryMethod(method);
   }
 
-  private boolean overridesLibraryMethod(ProgramMethod method) {
+  private boolean overridesNonFinalLibraryMethod(ProgramMethod method) {
     // We look up everywhere to see if there is a supertype/interface implementing the method...
     DexProgramClass holder = method.getHolder();
     WorkList<DexType> workList = WorkList.newIdentityWorkList();
@@ -225,6 +225,11 @@
         if (appView.rewritePrefix.hasRewrittenType(dexClass.type, appView)) {
           return false;
         }
+        if (dexEncodedMethod.isFinal()) {
+          // We do not introduce overrides of final methods, in this case, the runtime always
+          // execute the default behavior in the final method.
+          return false;
+        }
         foundOverrideToRewrite = true;
       }
     }
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 26a2c5c..0d4a470 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
@@ -15,13 +15,13 @@
 import com.android.tools.r8.ir.desugar.CfClassDesugaringCollection.EmptyCfClassDesugaringCollection;
 import com.android.tools.r8.ir.desugar.CfClassDesugaringCollection.NonEmptyCfClassDesugaringCollection;
 import com.android.tools.r8.ir.desugar.invokespecial.InvokeSpecialToSelfDesugaring;
+import com.android.tools.r8.ir.desugar.itf.InterfaceMethodRewriter;
 import com.android.tools.r8.ir.desugar.lambda.LambdaInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.nest.D8NestBasedAccessDesugaring;
 import com.android.tools.r8.ir.desugar.nest.NestBasedAccessDesugaring;
 import com.android.tools.r8.ir.desugar.stringconcat.StringConcatInstructionDesugaring;
 import com.android.tools.r8.ir.desugar.twr.TwrCloseResourceInstructionDesugaring;
 import com.android.tools.r8.utils.IntBox;
-import com.android.tools.r8.utils.IteratorUtils;
 import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.ThrowingConsumer;
@@ -43,19 +43,26 @@
   NonEmptyCfInstructionDesugaringCollection(AppView<?> appView) {
     this.appView = appView;
     this.nestBasedAccessDesugaring = NestBasedAccessDesugaring.create(appView);
+    BackportedMethodRewriter backportedMethodRewriter = null;
+    if (appView.options().enableBackportedMethodRewriting()) {
+      backportedMethodRewriter = new BackportedMethodRewriter(appView);
+    }
+    // Place TWR before Interface desugaring to eliminate potential $closeResource interface calls.
+    if (appView.options().enableTryWithResourcesDesugaring()) {
+      desugarings.add(new TwrCloseResourceInstructionDesugaring(appView));
+    }
+    // TODO(b/183998768): Enable interface method rewriter cf to cf also in R8.
+    if (appView.options().isInterfaceMethodDesugaringEnabled()
+        && !appView.enableWholeProgramOptimizations()) {
+      desugarings.add(new InterfaceMethodRewriter(appView, backportedMethodRewriter));
+    }
     desugarings.add(new LambdaInstructionDesugaring(appView));
     desugarings.add(new InvokeSpecialToSelfDesugaring(appView));
     desugarings.add(new InvokeToPrivateRewriter());
     desugarings.add(new StringConcatInstructionDesugaring(appView));
     desugarings.add(new BufferCovariantReturnTypeRewriter(appView));
-    if (appView.options().enableBackportedMethodRewriting()) {
-      BackportedMethodRewriter backportedMethodRewriter = new BackportedMethodRewriter(appView);
-      if (backportedMethodRewriter.hasBackports()) {
-        desugarings.add(backportedMethodRewriter);
-      }
-    }
-    if (appView.options().enableTryWithResourcesDesugaring()) {
-      desugarings.add(new TwrCloseResourceInstructionDesugaring(appView));
+    if (backportedMethodRewriter != null && backportedMethodRewriter.hasBackports()) {
+      desugarings.add(backportedMethodRewriter);
     }
     if (nestBasedAccessDesugaring != null) {
       desugarings.add(nestBasedAccessDesugaring);
@@ -152,10 +159,32 @@
       cfCode.setMaxLocals(maxLocalsForCode.get());
       cfCode.setMaxStack(maxStackForCode.get());
     } else {
-      assert false : "Expected code to be desugared";
+      assert noDesugaringBecauseOfImpreciseDesugaring(method);
     }
   }
 
+  private boolean noDesugaringBecauseOfImpreciseDesugaring(ProgramMethod method) {
+    assert desugarings.stream().anyMatch(desugaring -> !desugaring.hasPreciseNeedsDesugaring())
+        : "Expected code to be desugared";
+    assert needsDesugaring(method);
+    boolean foundFalsePositive = false;
+    for (CfInstruction instruction :
+        method.getDefinition().getCode().asCfCode().getInstructions()) {
+      for (CfInstructionDesugaring impreciseDesugaring :
+          Iterables.filter(desugarings, desugaring -> !desugaring.hasPreciseNeedsDesugaring())) {
+        if (impreciseDesugaring.needsDesugaring(instruction, method)) {
+          foundFalsePositive = true;
+        }
+      }
+      for (CfInstructionDesugaring preciseDesugaring :
+          Iterables.filter(desugarings, desugaring -> desugaring.hasPreciseNeedsDesugaring())) {
+        assert !preciseDesugaring.needsDesugaring(instruction, method);
+      }
+    }
+    assert foundFalsePositive;
+    return true;
+  }
+
   @Override
   public CfClassDesugaringCollection createClassDesugaringCollection() {
     if (recordRewriter == null) {
@@ -185,8 +214,8 @@
               methodProcessingContext,
               appView.dexItemFactory());
       if (replacement != null) {
-        assert verifyNoOtherDesugaringNeeded(
-            instruction, context, methodProcessingContext, iterator);
+        assert desugaring.needsDesugaring(instruction, context);
+        assert verifyNoOtherDesugaringNeeded(instruction, context, iterator, desugaring);
         return replacement;
       }
     }
@@ -220,26 +249,26 @@
   private boolean verifyNoOtherDesugaringNeeded(
       CfInstruction instruction,
       ProgramMethod context,
-      MethodProcessingContext methodProcessingContext,
-      Iterator<CfInstructionDesugaring> iterator) {
-    assert IteratorUtils.nextUntil(
-            iterator,
-            desugaring ->
-                desugaring.desugarInstruction(
-                        instruction,
-                        requiredRegisters -> {
-                          assert false;
-                          return 0;
-                        },
-                        localStackHeight -> {
-                          assert false;
-                        },
-                        CfInstructionDesugaringEventConsumer.createForDesugaredCode(),
-                        context,
-                        methodProcessingContext,
-                        appView.dexItemFactory())
-                    != null)
-        == null;
+      Iterator<CfInstructionDesugaring> iterator,
+      CfInstructionDesugaring appliedDesugaring) {
+    iterator.forEachRemaining(
+        desugaring -> {
+          boolean alsoApplicable = desugaring.needsDesugaring(instruction, context);
+          // TODO(b/187913003): As part of precise interface desugaring, make sure the
+          //  identification is explicitly non-overlapping and remove the exceptions below.
+          assert !alsoApplicable
+                  || (appliedDesugaring instanceof InterfaceMethodRewriter
+                      && (desugaring instanceof InvokeToPrivateRewriter
+                          || desugaring instanceof D8NestBasedAccessDesugaring))
+                  || (appliedDesugaring instanceof TwrCloseResourceInstructionDesugaring
+                      && desugaring instanceof InterfaceMethodRewriter)
+              : "Desugaring of "
+                  + instruction
+                  + " has multiple matches: "
+                  + appliedDesugaring.getClass().getName()
+                  + " and "
+                  + desugaring.getClass().getName();
+        });
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/RecordRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/RecordRewriter.java
index 1b5e6aa..2e71826 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/RecordRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/RecordRewriter.java
@@ -305,15 +305,8 @@
   @Override
   public boolean needsDesugaring(CfInstruction instruction, ProgramMethod context) {
     assert !instruction.isInitClass();
-    // TODO(b/179146128): This is a temporary work-around to test desugaring of records
-    // without rewriting the record invoke-custom. This should be removed when the record support
-    // is complete.
-    if (instruction.isInvokeDynamic()
-        && context.getHolder().superType == factory.recordType
-        && (context.getName() == factory.toStringMethodName
-            || context.getName() == factory.hashCodeMethodName
-            || context.getName() == factory.equalsMethodName)) {
-      return true;
+    if (instruction.isInvokeDynamic()) {
+      return needsDesugaring(instruction.asInvokeDynamic(), context);
     }
     if (instruction.isInvoke()) {
       CfInvoke cfInvoke = instruction.asInvoke();
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodDesugaringEventConsumer.java b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodDesugaringEventConsumer.java
new file mode 100644
index 0000000..031e142
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/desugar/itf/InterfaceMethodDesugaringEventConsumer.java
@@ -0,0 +1,16 @@
+// 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.itf;
+
+import com.android.tools.r8.graph.ProgramMethod;
+
+public interface InterfaceMethodDesugaringEventConsumer {
+
+  void acceptThrowMethod(ProgramMethod method, ProgramMethod context);
+
+  void acceptInvokeStaticInterfaceOutliningMethod(ProgramMethod method, ProgramMethod context);
+
+  // TODO(b/183998768): Add acceptCompanionClass and acceptEmulatedInterface.
+}
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 b0a4ffa..782ff63 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
@@ -9,23 +9,24 @@
 import static com.android.tools.r8.ir.code.Invoke.Type.STATIC;
 import static com.android.tools.r8.ir.code.Invoke.Type.SUPER;
 import static com.android.tools.r8.ir.code.Invoke.Type.VIRTUAL;
-import static com.android.tools.r8.ir.code.Opcodes.INVOKE_CUSTOM;
-import static com.android.tools.r8.ir.code.Opcodes.INVOKE_DIRECT;
-import static com.android.tools.r8.ir.code.Opcodes.INVOKE_INTERFACE;
-import static com.android.tools.r8.ir.code.Opcodes.INVOKE_STATIC;
-import static com.android.tools.r8.ir.code.Opcodes.INVOKE_SUPER;
-import static com.android.tools.r8.ir.code.Opcodes.INVOKE_VIRTUAL;
 import static com.android.tools.r8.ir.desugar.DesugaredLibraryRetargeter.getRetargetPackageAndClassPrefixDescriptor;
 import static com.android.tools.r8.ir.desugar.DesugaredLibraryWrapperSynthesizer.TYPE_WRAPPER_SUFFIX;
 import static com.android.tools.r8.ir.desugar.DesugaredLibraryWrapperSynthesizer.VIVIFIED_TYPE_WRAPPER_SUFFIX;
 
 import com.android.tools.r8.DesugarGraphConsumer;
 import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.cf.code.CfConstNull;
+import com.android.tools.r8.cf.code.CfConstNumber;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfInvoke;
+import com.android.tools.r8.cf.code.CfStackInstruction;
 import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.ClasspathOrLibraryClass;
 import com.android.tools.r8.graph.DexApplication.Builder;
 import com.android.tools.r8.graph.DexCallSite;
@@ -38,6 +39,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.DexValue;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ProgramMethod;
@@ -48,17 +50,20 @@
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.Invoke.Type;
-import com.android.tools.r8.ir.code.InvokeCustom;
-import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.InvokeMethod;
-import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.InvokeStatic;
-import com.android.tools.r8.ir.code.InvokeSuper;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.desugar.BackportedMethodRewriter;
+import com.android.tools.r8.ir.desugar.CfInstructionDesugaring;
+import com.android.tools.r8.ir.desugar.CfInstructionDesugaringEventConsumer;
 import com.android.tools.r8.ir.desugar.DesugaredLibraryConfiguration;
-import com.android.tools.r8.ir.desugar.itf.DefaultMethodsHelper.Collection;
+import com.android.tools.r8.ir.desugar.FreshLocalProvider;
+import com.android.tools.r8.ir.desugar.LocalStackAllocator;
+import com.android.tools.r8.ir.desugar.lambda.LambdaInstructionDesugaring;
+import com.android.tools.r8.ir.desugar.stringconcat.StringConcatInstructionDesugaring;
 import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations;
 import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations.MethodSynthesizerConsumer;
 import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations.UtilityMethodForCodeOptimizations;
@@ -78,6 +83,9 @@
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 import java.util.ListIterator;
 import java.util.Map;
@@ -86,6 +94,8 @@
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.function.Predicate;
 
 //
@@ -114,7 +124,7 @@
 //       set of default interface methods missing and add them, the created methods
 //       forward the call to an appropriate method in interface companion class.
 //
-public final class InterfaceMethodRewriter {
+public final class InterfaceMethodRewriter implements CfInstructionDesugaring {
 
   // Public for testing.
   public static final String EMULATE_LIBRARY_CLASS_NAME_SUFFIX = "$-EL";
@@ -137,6 +147,10 @@
   private final Map<DexType, DefaultMethodsHelper.Collection> cache = new ConcurrentHashMap<>();
 
   private final Predicate<DexType> shouldIgnoreFromReportsPredicate;
+
+  // This is used to filter out double desugaring on backported methods.
+  private final BackportedMethodRewriter backportedMethodRewriter;
+
   /** Defines a minor variation in desugaring. */
   public enum Flavor {
     /** Process all application resources. */
@@ -145,10 +159,24 @@
     ExcludeDexResources
   }
 
+  // Constructor for cf to cf desugaring.
+  public InterfaceMethodRewriter(AppView<?> appView, BackportedMethodRewriter rewriter) {
+    this.appView = appView;
+    this.converter = null;
+    this.backportedMethodRewriter = rewriter;
+    this.options = appView.options();
+    this.factory = appView.dexItemFactory();
+    this.emulatedInterfaces = options.desugaredLibraryConfiguration.getEmulateLibraryInterface();
+    this.shouldIgnoreFromReportsPredicate = getShouldIgnoreFromReportsPredicate(appView);
+    initializeEmulatedInterfaceVariables();
+  }
+
+  // Constructor for IR desugaring.
   public InterfaceMethodRewriter(AppView<?> appView, IRConverter converter) {
     assert converter != null;
     this.appView = appView;
     this.converter = converter;
+    this.backportedMethodRewriter = null;
     this.options = appView.options();
     this.factory = appView.dexItemFactory();
     this.emulatedInterfaces = options.desugaredLibraryConfiguration.getEmulateLibraryInterface();
@@ -232,6 +260,8 @@
   }
 
   private boolean invokeNeedsRewriting(DexMethod method, Type invokeType) {
+    // TODO(b/187913003): Refactor the implementation of needsDesugaring and desugarInstruction so
+    //  that the identification is shared and thus guaranteed to be equivalent.
     if (invokeType == SUPER || invokeType == STATIC || invokeType == DIRECT) {
       DexClass clazz = appView.appInfo().definitionFor(method.getHolderType());
       if (clazz != null && clazz.isInterface()) {
@@ -240,11 +270,257 @@
       return emulatedMethods.contains(method.getName());
     }
     if (invokeType == VIRTUAL || invokeType == INTERFACE) {
-      return defaultMethodForEmulatedDispatchOrNull(method, invokeType) != null;
+      // A virtual dispatch can target a private method, on self or on a nest mate.
+      AppInfoWithClassHierarchy appInfoForDesugaring = appView.appInfoForDesugaring();
+      SingleResolutionResult resolution =
+          appInfoForDesugaring.resolveMethod(method, invokeType == INTERFACE).asSingleResolution();
+      if (resolution != null && resolution.getResolvedMethod().isPrivateMethod()) {
+        return true;
+      }
+      return defaultMethodForEmulatedDispatchOrNull(method, invokeType == INTERFACE) != null;
     }
     return true;
   }
 
+  @Override
+  public boolean hasPreciseNeedsDesugaring() {
+    return false;
+  }
+
+  /**
+   * If the method is not required to be desugared, scanning is used to upgrade when required the
+   * class file version, as well as reporting missing type.
+   */
+  @Override
+  public void scan(ProgramMethod context, CfInstructionDesugaringEventConsumer eventConsumer) {
+    if (isSyntheticMethodThatShouldNotBeDoubleProcessed(context)) {
+      leavingStaticInvokeToInterface(context);
+      return;
+    }
+    CfCode code = context.getDefinition().getCode().asCfCode();
+    for (CfInstruction instruction : code.getInstructions()) {
+      if (instruction.isInvokeDynamic()
+          && !LambdaInstructionDesugaring.isLambdaInvoke(instruction, context, appView)
+          && !StringConcatInstructionDesugaring.isStringConcatInvoke(
+              instruction, appView.dexItemFactory())) {
+        reportInterfaceMethodHandleCallSite(instruction.asInvokeDynamic().getCallSite(), context);
+        continue;
+      }
+      if (instruction.isInvoke()) {
+        CfInvoke cfInvoke = instruction.asInvoke();
+        if (backportedMethodRewriter.methodIsBackport(cfInvoke.getMethod())) {
+          continue;
+        }
+        if (cfInvoke.isInvokeStatic()) {
+          scanInvokeStatic(cfInvoke, context);
+        } else if (cfInvoke.isInvokeSpecial()) {
+          scanInvokeDirectOrSuper(cfInvoke, context);
+        }
+      }
+    }
+  }
+
+  private void scanInvokeDirectOrSuper(CfInvoke cfInvoke, ProgramMethod context) {
+    if (cfInvoke.isInvokeConstructor(factory)) {
+      return;
+    }
+    DexMethod invokedMethod = cfInvoke.getMethod();
+    DexClass clazz = appView.definitionFor(invokedMethod.holder, context);
+    if (clazz == null) {
+      // NOTE: For invoke-super, this leaves unchanged those calls to undefined targets.
+      // This may lead to runtime exception but we can not report it as error since it can also be
+      // the intended behavior.
+      // For invoke-direct, this reports the missing class since we don't know if it is an
+      // interface.
+      warnMissingType(context, invokedMethod.holder);
+    }
+  }
+
+  private void scanInvokeStatic(CfInvoke cfInvoke, ProgramMethod context) {
+    DexMethod invokedMethod = cfInvoke.getMethod();
+    DexClass clazz = appView.definitionFor(invokedMethod.holder, context);
+    if (clazz == null) {
+      // NOTE: leave unchanged those calls to undefined targets. This may lead to runtime
+      // exception but we can not report it as error since it can also be the intended
+      // behavior.
+      if (cfInvoke.isInterface()) {
+        leavingStaticInvokeToInterface(context);
+      }
+      warnMissingType(context, invokedMethod.holder);
+      return;
+    }
+
+    if (!clazz.isInterface()) {
+      if (cfInvoke.isInterface()) {
+        leavingStaticInvokeToInterface(context);
+      }
+      return;
+    }
+
+    if (isNonDesugaredLibraryClass(clazz)) {
+      // NOTE: we intentionally don't desugar static calls into static interface
+      // methods coming from android.jar since it is only possible in case v24+
+      // version of android.jar is provided.
+      //
+      // We assume such calls are properly guarded by if-checks like
+      //    'if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.XYZ) { ... }'
+      //
+      // WARNING: This may result in incorrect code on older platforms!
+      // Retarget call to an appropriate method of companion class.
+
+      if (options.canLeaveStaticInterfaceMethodInvokes()) {
+        // When leaving static interface method invokes upgrade the class file version.
+        leavingStaticInvokeToInterface(context);
+      }
+    }
+  }
+
+  @Override
+  public boolean needsDesugaring(CfInstruction instruction, ProgramMethod context) {
+    if (instruction.isInvoke()) {
+      CfInvoke cfInvoke = instruction.asInvoke();
+      return needsRewriting(cfInvoke.getMethod(), cfInvoke.getInvokeType(context), context);
+    }
+    return false;
+  }
+
+  @Override
+  public Collection<CfInstruction> desugarInstruction(
+      CfInstruction instruction,
+      FreshLocalProvider freshLocalProvider,
+      LocalStackAllocator localStackAllocator,
+      CfInstructionDesugaringEventConsumer eventConsumer,
+      ProgramMethod context,
+      MethodProcessingContext methodProcessingContext,
+      DexItemFactory dexItemFactory) {
+    if (!instruction.isInvoke() || isSyntheticMethodThatShouldNotBeDoubleProcessed(context)) {
+      return null;
+    }
+    CfInvoke invoke = instruction.asInvoke();
+    if (backportedMethodRewriter.methodIsBackport(invoke.getMethod())) {
+      return null;
+    }
+
+    Function<DexMethod, Collection<CfInstruction>> rewriteInvoke =
+        (newTarget) ->
+            Collections.singletonList(
+                new CfInvoke(org.objectweb.asm.Opcodes.INVOKESTATIC, newTarget, false));
+
+    Function<SingleResolutionResult, Collection<CfInstruction>> rewriteToThrow =
+        (resolutionResult) ->
+            rewriteInvokeToThrowCf(
+                invoke, resolutionResult, eventConsumer, context, methodProcessingContext);
+
+    if (invoke.isInvokeVirtual() || invoke.isInvokeInterface()) {
+      AppInfoWithClassHierarchy appInfoForDesugaring = appView.appInfoForDesugaring();
+      SingleResolutionResult resolution =
+          appInfoForDesugaring
+              .resolveMethod(invoke.getMethod(), invoke.isInterface())
+              .asSingleResolution();
+      if (resolution != null
+          && resolution.getResolvedMethod().isPrivateMethod()
+          && resolution.isAccessibleFrom(context, appInfoForDesugaring).isTrue()) {
+        return rewriteInvokeDirect(invoke.getMethod(), context, rewriteInvoke);
+      }
+      return rewriteInvokeInterfaceOrInvokeVirtual(
+          invoke.getMethod(), invoke.isInterface(), rewriteInvoke);
+    }
+    if (invoke.isInvokeStatic()) {
+      Consumer<ProgramMethod> staticOutliningMethodConsumer =
+          staticOutliningMethod -> {
+            synthesizedMethods.add(staticOutliningMethod);
+            eventConsumer.acceptInvokeStaticInterfaceOutliningMethod(
+                staticOutliningMethod, context);
+          };
+      return rewriteInvokeStatic(
+          invoke.getMethod(),
+          invoke.isInterface(),
+          methodProcessingContext,
+          context,
+          staticOutliningMethodConsumer,
+          rewriteInvoke,
+          rewriteToThrow);
+    }
+    assert invoke.isInvokeSpecial();
+    if (invoke.isInvokeSuper(context.getHolderType())) {
+      return rewriteInvokeSuper(invoke.getMethod(), context, rewriteInvoke, rewriteToThrow);
+    }
+    return rewriteInvokeDirect(invoke.getMethod(), context, rewriteInvoke);
+  }
+
+  private Collection<CfInstruction> rewriteInvokeToThrowCf(
+      CfInvoke invoke,
+      SingleResolutionResult resolutionResult,
+      CfInstructionDesugaringEventConsumer eventConsumer,
+      ProgramMethod context,
+      MethodProcessingContext methodProcessingContext) {
+    if (backportedMethodRewriter != null
+        && backportedMethodRewriter.methodIsBackport(invoke.getMethod())) {
+      // In Cf to Cf it is not allowed to desugar twice the same instruction, if the backported
+      // method rewriter already desugars the instruction, it takes precedence and nothing has
+      // to be done here.
+      return null;
+    }
+
+    MethodSynthesizerConsumer methodSynthesizerConsumer;
+    if (resolutionResult == null) {
+      methodSynthesizerConsumer =
+          UtilityMethodsForCodeOptimizations::synthesizeThrowNoSuchMethodErrorMethod;
+    } else if (resolutionResult.getResolvedMethod().isStatic() != invoke.isInvokeStatic()) {
+      methodSynthesizerConsumer =
+          UtilityMethodsForCodeOptimizations::synthesizeThrowIncompatibleClassChangeErrorMethod;
+    } else {
+      assert false;
+      return null;
+    }
+
+    assert needsDesugaring(invoke, context);
+
+    // Replace the entire effect of the invoke by by call to the throwing helper:
+    //   ...
+    //   invoke <method> [receiver] args*
+    // =>
+    //   ...
+    //   (pop arg)*
+    //   [pop receiver]
+    //   invoke <throwing-method>
+    //   pop exception result
+    //   [push fake result for <method>]
+    UtilityMethodForCodeOptimizations throwMethod =
+        methodSynthesizerConsumer.synthesizeMethod(appView, methodProcessingContext);
+    ProgramMethod throwProgramMethod = throwMethod.uncheckedGetMethod();
+    eventConsumer.acceptThrowMethod(throwProgramMethod, context);
+
+    ArrayList<CfInstruction> replacement = new ArrayList<>();
+    DexTypeList parameters = invoke.getMethod().getParameters();
+    for (int i = parameters.values.length - 1; i >= 0; i--) {
+      replacement.add(
+          new CfStackInstruction(
+              parameters.get(i).isWideType()
+                  ? CfStackInstruction.Opcode.Pop2
+                  : CfStackInstruction.Opcode.Pop));
+    }
+    if (!invoke.isInvokeStatic()) {
+      replacement.add(new CfStackInstruction(CfStackInstruction.Opcode.Pop));
+    }
+
+    CfInvoke throwInvoke =
+        new CfInvoke(
+            org.objectweb.asm.Opcodes.INVOKESTATIC, throwProgramMethod.getReference(), false);
+    assert throwInvoke.getMethod().getReturnType().isClassType();
+    replacement.add(throwInvoke);
+    replacement.add(new CfStackInstruction(CfStackInstruction.Opcode.Pop));
+
+    DexType returnType = invoke.getMethod().getReturnType();
+    if (returnType != factory.voidType) {
+      replacement.add(
+          returnType.isPrimitiveType()
+              ? new CfConstNumber(0, ValueType.fromDexType(returnType))
+              : new CfConstNull());
+    }
+    return replacement;
+  }
+
   DexType getEmulatedInterface(DexType itf) {
     return emulatedInterfaces.get(itf);
   }
@@ -290,44 +566,16 @@
       InstructionListIterator instructions = block.listIterator(code);
       while (instructions.hasNext()) {
         Instruction instruction = instructions.next();
-        switch (instruction.opcode()) {
-          case INVOKE_CUSTOM:
-            rewriteInvokeCustom(instruction.asInvokeCustom(), context);
-            break;
-          case INVOKE_DIRECT:
-            rewriteInvokeDirect(instruction.asInvokeDirect(), instructions, context);
-            break;
-          case INVOKE_STATIC:
-            rewriteInvokeStatic(
-                instruction.asInvokeStatic(),
-                code,
-                blocks,
-                instructions,
-                affectedValues,
-                blocksToRemove,
-                methodProcessor,
-                methodProcessingContext);
-            break;
-          case INVOKE_SUPER:
-            rewriteInvokeSuper(
-                instruction.asInvokeSuper(),
-                code,
-                blocks,
-                instructions,
-                affectedValues,
-                blocksToRemove,
-                methodProcessor,
-                methodProcessingContext);
-            break;
-          case INVOKE_INTERFACE:
-          case INVOKE_VIRTUAL:
-            rewriteInvokeInterfaceOrInvokeVirtual(
-                instruction.asInvokeMethodWithReceiver(), instructions);
-            break;
-          default:
-            // Intentionally empty.
-            break;
-        }
+        rewriteMethodReferences(
+            code,
+            methodProcessor,
+            methodProcessingContext,
+            context,
+            affectedValues,
+            blocksToRemove,
+            blocks,
+            instructions,
+            instruction);
       }
     }
 
@@ -340,14 +588,71 @@
     assert code.isConsistentSSA();
   }
 
+  private void rewriteMethodReferences(
+      IRCode code,
+      MethodProcessor methodProcessor,
+      MethodProcessingContext methodProcessingContext,
+      ProgramMethod context,
+      Set<Value> affectedValues,
+      Set<BasicBlock> blocksToRemove,
+      ListIterator<BasicBlock> blocks,
+      InstructionListIterator instructions,
+      Instruction instruction) {
+    if (instruction.isInvokeCustom()) {
+      reportInterfaceMethodHandleCallSite(instruction.asInvokeCustom().getCallSite(), context);
+      return;
+    }
+    if (!instruction.isInvokeMethod()) {
+      return;
+    }
+    InvokeMethod invoke = instruction.asInvokeMethod();
+    Function<DexMethod, Collection<CfInstruction>> rewriteInvoke =
+        (newTarget) -> {
+          instructions.replaceCurrentInstruction(
+              new InvokeStatic(newTarget, invoke.outValue(), invoke.arguments()));
+          return null;
+        };
+    if (instruction.isInvokeDirect()) {
+      rewriteInvokeDirect(invoke.getInvokedMethod(), context, rewriteInvoke);
+    } else if (instruction.isInvokeVirtual() || instruction.isInvokeInterface()) {
+      rewriteInvokeInterfaceOrInvokeVirtual(
+          invoke.getInvokedMethod(), invoke.getInterfaceBit(), rewriteInvoke);
+    } else {
+      Function<SingleResolutionResult, Collection<CfInstruction>> rewriteToThrow =
+          (resolutionResult) ->
+              rewriteInvokeToThrowIR(
+                  invoke,
+                  resolutionResult,
+                  code,
+                  blocks,
+                  instructions,
+                  affectedValues,
+                  blocksToRemove,
+                  methodProcessor,
+                  methodProcessingContext);
+      if (instruction.isInvokeStatic()) {
+        rewriteInvokeStatic(
+            invoke.getInvokedMethod(),
+            invoke.getInterfaceBit(),
+            methodProcessingContext,
+            context,
+            synthesizedMethods::add,
+            rewriteInvoke,
+            rewriteToThrow);
+      } else {
+        assert instruction.isInvokeSuper();
+        rewriteInvokeSuper(invoke.getInvokedMethod(), context, rewriteInvoke, rewriteToThrow);
+      }
+    }
+  }
+
   private boolean isSyntheticMethodThatShouldNotBeDoubleProcessed(ProgramMethod method) {
     return appView.getSyntheticItems().isSyntheticMethodThatShouldNotBeDoubleProcessed(method);
   }
 
-  private void rewriteInvokeCustom(InvokeCustom invoke, ProgramMethod context) {
+  private void reportInterfaceMethodHandleCallSite(DexCallSite callSite, ProgramMethod context) {
     // Check that static interface methods are not referenced from invoke-custom instructions via
     // method handles.
-    DexCallSite callSite = invoke.getCallSite();
     reportStaticInterfaceMethodHandle(context, callSite.bootstrapMethod);
     for (DexValue arg : callSite.bootstrapArgs) {
       if (arg.isDexValueMethodHandle()) {
@@ -356,22 +661,23 @@
     }
   }
 
-  private void rewriteInvokeDirect(
-      InvokeDirect invoke, InstructionListIterator instructions, ProgramMethod context) {
-    DexMethod method = invoke.getInvokedMethod();
-    if (factory.isConstructor(method)) {
-      return;
+  private Collection<CfInstruction> rewriteInvokeDirect(
+      DexMethod invokedMethod,
+      ProgramMethod context,
+      Function<DexMethod, Collection<CfInstruction>> rewriteInvoke) {
+    if (factory.isConstructor(invokedMethod)) {
+      return null;
     }
 
-    DexClass clazz = appView.definitionForHolder(method, context);
+    DexClass clazz = appView.definitionForHolder(invokedMethod, context);
     if (clazz == null) {
       // Report missing class since we don't know if it is an interface.
-      warnMissingType(context, method.holder);
-      return;
+      warnMissingType(context, invokedMethod.holder);
+      return null;
     }
 
     if (!clazz.isInterface()) {
-      return;
+      return null;
     }
 
     if (clazz.isLibraryClass()) {
@@ -382,72 +688,63 @@
           getMethodOrigin(context.getReference()));
     }
 
-    DexClassAndMethod directTarget = clazz.lookupClassMethod(method);
+    DexClassAndMethod directTarget = clazz.lookupClassMethod(invokedMethod);
     if (directTarget != null) {
       // This can be a private instance method call. Note that the referenced
       // method is expected to be in the current class since it is private, but desugaring
       // may move some methods or their code into other classes.
-      assert invokeNeedsRewriting(method, DIRECT);
-      instructions.replaceCurrentInstruction(
-          new InvokeStatic(
-              directTarget.getDefinition().isPrivateMethod()
-                  ? privateAsMethodOfCompanionClass(directTarget)
-                  : defaultAsMethodOfCompanionClass(directTarget),
-              invoke.outValue(),
-              invoke.arguments()));
+      assert invokeNeedsRewriting(invokedMethod, DIRECT);
+      return rewriteInvoke.apply(
+          directTarget.getDefinition().isPrivateMethod()
+              ? privateAsMethodOfCompanionClass(directTarget)
+              : defaultAsMethodOfCompanionClass(directTarget));
     } else {
       // The method can be a default method in the interface hierarchy.
       DexClassAndMethod virtualTarget =
-          appView.appInfoForDesugaring().lookupMaximallySpecificMethod(clazz, method);
+          appView.appInfoForDesugaring().lookupMaximallySpecificMethod(clazz, invokedMethod);
       if (virtualTarget != null) {
         // This is a invoke-direct call to a virtual method.
-        assert invokeNeedsRewriting(method, DIRECT);
-        instructions.replaceCurrentInstruction(
-            new InvokeStatic(
-                defaultAsMethodOfCompanionClass(virtualTarget),
-                invoke.outValue(),
-                invoke.arguments()));
+        assert invokeNeedsRewriting(invokedMethod, DIRECT);
+        return rewriteInvoke.apply(defaultAsMethodOfCompanionClass(virtualTarget));
       } else {
         // The below assert is here because a well-type program should have a target, but we
         // cannot throw a compilation error, since we have no knowledge about the input.
         assert false;
       }
     }
+    return null;
   }
 
-  private void rewriteInvokeStatic(
-      InvokeStatic invoke,
-      IRCode code,
-      ListIterator<BasicBlock> blockIterator,
-      InstructionListIterator instructions,
-      Set<Value> affectedValues,
-      Set<BasicBlock> blocksToRemove,
-      MethodProcessor methodProcessor,
-      MethodProcessingContext methodProcessingContext) {
-    DexMethod invokedMethod = invoke.getInvokedMethod();
+  private Collection<CfInstruction> rewriteInvokeStatic(
+      DexMethod invokedMethod,
+      boolean interfaceBit,
+      MethodProcessingContext methodProcessingContext,
+      ProgramMethod context,
+      Consumer<ProgramMethod> staticOutliningMethodConsumer,
+      Function<DexMethod, Collection<CfInstruction>> rewriteInvoke,
+      Function<SingleResolutionResult, Collection<CfInstruction>> rewriteToThrow) {
     if (appView.getSyntheticItems().isPendingSynthetic(invokedMethod.holder)) {
       // We did not create this code yet, but it will not require rewriting.
-      return;
+      return null;
     }
 
-    ProgramMethod context = code.context();
     DexClass clazz = appView.definitionFor(invokedMethod.holder, context);
     if (clazz == null) {
       // NOTE: leave unchanged those calls to undefined targets. This may lead to runtime
       // exception but we can not report it as error since it can also be the intended
       // behavior.
-      if (invoke.getInterfaceBit()) {
+      if (interfaceBit) {
         leavingStaticInvokeToInterface(context);
       }
       warnMissingType(context, invokedMethod.holder);
-      return;
+      return null;
     }
 
     if (!clazz.isInterface()) {
-      if (invoke.getInterfaceBit()) {
+      if (interfaceBit) {
         leavingStaticInvokeToInterface(context);
       }
-      return;
+      return null;
     }
 
     if (isNonDesugaredLibraryClass(clazz)) {
@@ -467,6 +764,19 @@
         // so the user class is not rejected because it make this call directly.
         // TODO(b/166247515): If this an incorrect invoke-static without the interface bit
         //  we end up "fixing" the code and remove and ICCE error.
+        if (synthesizedMethods.contains(context)) {
+          // When reprocessing the method generated below, the desugaring asserts this method
+          // does not need any new desugaring, while the interface method rewriter tries
+          // to outline again the invoke-static. Just do nothing instead.
+          return null;
+        }
+        if (backportedMethodRewriter != null
+            && backportedMethodRewriter.methodIsBackport(invokedMethod)) {
+          // In Cf to Cf it is not allowed to desugar twice the same instruction, if the backported
+          // method rewriter already desugars the instruction, it takes precedence and nothing has
+          // to be done here.
+          return null;
+        }
         ProgramMethod newProgramMethod =
             appView
                 .getSyntheticItems()
@@ -484,19 +794,17 @@
                                         .setStaticTarget(invokedMethod, true)
                                         .setStaticSource(m)
                                         .build()));
+        staticOutliningMethodConsumer.accept(newProgramMethod);
         assert invokeNeedsRewriting(invokedMethod, STATIC);
-        instructions.replaceCurrentInstruction(
-            new InvokeStatic(
-                newProgramMethod.getReference(), invoke.outValue(), invoke.arguments()));
         // The synthetic dispatch class has static interface method invokes, so set
         // the class file version accordingly.
-        newProgramMethod.getDefinition().upgradeClassFileVersion(CfVersion.V1_8);
-        synthesizedMethods.add(newProgramMethod);
+        leavingStaticInvokeToInterface(newProgramMethod);
+        return rewriteInvoke.apply(newProgramMethod.getReference());
       } else {
         // When leaving static interface method invokes upgrade the class file version.
-        context.getDefinition().upgradeClassFileVersion(CfVersion.V1_8);
+        leavingStaticInvokeToInterface(context);
       }
-      return;
+      return null;
     }
 
     SingleResolutionResult resolutionResult =
@@ -504,67 +812,38 @@
             .appInfoForDesugaring()
             .resolveMethodOnInterface(clazz, invokedMethod)
             .asSingleResolution();
-    if (clazz.isInterface()
-        && rewriteInvokeToThrow(
-            invoke,
-            resolutionResult,
-            code,
-            blockIterator,
-            instructions,
-            affectedValues,
-            blocksToRemove,
-            methodProcessor,
-            methodProcessingContext)) {
-      assert invokeNeedsRewriting(invoke.getInvokedMethod(), STATIC);
-      return;
+    if (clazz.isInterface() && shouldRewriteToInvokeToThrow(resolutionResult, true)) {
+      assert invokeNeedsRewriting(invokedMethod, STATIC);
+      return rewriteToThrow.apply(resolutionResult);
     }
 
     assert resolutionResult != null;
     assert resolutionResult.getResolvedMethod().isStatic();
     assert invokeNeedsRewriting(invokedMethod, STATIC);
 
-    instructions.replaceCurrentInstruction(
-        new InvokeStatic(
-            staticAsMethodOfCompanionClass(resolutionResult.getResolutionPair()),
-            invoke.outValue(),
-            invoke.arguments()));
+    return rewriteInvoke.apply(
+        staticAsMethodOfCompanionClass(resolutionResult.getResolutionPair()));
   }
 
-  private void rewriteInvokeSuper(
-      InvokeSuper invoke,
-      IRCode code,
-      ListIterator<BasicBlock> blockIterator,
-      InstructionListIterator instructions,
-      Set<Value> affectedValues,
-      Set<BasicBlock> blocksToRemove,
-      MethodProcessor methodProcessor,
-      MethodProcessingContext methodProcessingContext) {
-    ProgramMethod context = code.context();
-    DexMethod invokedMethod = invoke.getInvokedMethod();
+  private Collection<CfInstruction> rewriteInvokeSuper(
+      DexMethod invokedMethod,
+      ProgramMethod context,
+      Function<DexMethod, Collection<CfInstruction>> rewriteInvoke,
+      Function<SingleResolutionResult, Collection<CfInstruction>> rewriteToThrow) {
     DexClass clazz = appView.definitionFor(invokedMethod.holder, context);
     if (clazz == null) {
       // NOTE: leave unchanged those calls to undefined targets. This may lead to runtime
       // exception but we can not report it as error since it can also be the intended
       // behavior.
       warnMissingType(context, invokedMethod.holder);
-      return;
+      return null;
     }
 
     SingleResolutionResult resolutionResult =
         appView.appInfoForDesugaring().resolveMethodOn(clazz, invokedMethod).asSingleResolution();
-    if (clazz.isInterface()
-        && rewriteInvokeToThrow(
-            invoke,
-            resolutionResult,
-            code,
-            blockIterator,
-            instructions,
-            affectedValues,
-            blocksToRemove,
-            methodProcessor,
-            methodProcessingContext)) {
-      assert invokeNeedsRewriting(invoke.getInvokedMethod(), SUPER);
-      return;
+    if (clazz.isInterface() && shouldRewriteToInvokeToThrow(resolutionResult, false)) {
+      assert invokeNeedsRewriting(invokedMethod, SUPER);
+      return rewriteToThrow.apply(resolutionResult);
     }
 
     if (clazz.isInterface() && !clazz.isLibraryClass()) {
@@ -578,81 +857,83 @@
       // WARNING: This may result in incorrect code on older platforms!
       // Retarget call to an appropriate method of companion class.
       assert invokeNeedsRewriting(invokedMethod, SUPER);
-      DexMethod amendedMethod = amendDefaultMethod(context.getHolder(), invokedMethod);
-      instructions.replaceCurrentInstruction(
-          new InvokeStatic(
-              defaultAsMethodOfCompanionClass(amendedMethod, appView.dexItemFactory()),
-              invoke.outValue(),
-              invoke.arguments()));
-    } else {
-      DexType emulatedItf = maximallySpecificEmulatedInterfaceOrNull(invokedMethod);
-      if (emulatedItf == null) {
-        if (clazz.isInterface() && appView.rewritePrefix.hasRewrittenType(clazz.type, appView)) {
-          DexClassAndMethod target =
-              appView.appInfoForDesugaring().lookupSuperTarget(invokedMethod, context);
-          if (target != null && target.getDefinition().isDefaultMethod()) {
-            DexClass holder = target.getHolder();
-            if (holder.isLibraryClass() && holder.isInterface()) {
-              assert invokeNeedsRewriting(invokedMethod, SUPER);
-              instructions.replaceCurrentInstruction(
-                  new InvokeStatic(
-                      defaultAsMethodOfCompanionClass(target),
-                      invoke.outValue(),
-                      invoke.arguments()));
-            }
-          }
+      if (resolutionResult.getResolvedMethod().isPrivateMethod()) {
+        if (resolutionResult.isAccessibleFrom(context, appView.appInfoForDesugaring()).isFalse()) {
+          // TODO(b/145775365): This should throw IAE.
+          return rewriteToThrow.apply(null);
         }
+        return rewriteInvoke.apply(
+            privateAsMethodOfCompanionClass(resolutionResult.getResolutionPair()));
       } else {
-        // That invoke super may not resolve since the super method may not be present
-        // since it's in the emulated interface. We need to force resolution. If it resolves
-        // to a library method, then it needs to be rewritten.
-        // If it resolves to a program overrides, the invoke-super can remain.
-        DexClassAndMethod superTarget =
-            appView.appInfoForDesugaring().lookupSuperTarget(invoke.getInvokedMethod(), context);
-        if (superTarget != null && superTarget.isLibraryMethod()) {
-          // Rewriting is required because the super invoke resolves into a missing
-          // method (method is on desugared library). Find out if it needs to be
-          // retarget or if it just calls a companion class method and rewrite.
-          DexMethod retargetMethod =
-              options.desugaredLibraryConfiguration.retargetMethod(superTarget, appView);
-          if (retargetMethod == null) {
-            DexMethod originalCompanionMethod = defaultAsMethodOfCompanionClass(superTarget);
-            DexMethod companionMethod =
-                factory.createMethod(
-                    getCompanionClassType(emulatedItf),
-                    factory.protoWithDifferentFirstParameter(
-                        originalCompanionMethod.proto, emulatedItf),
-                    originalCompanionMethod.name);
+        DexMethod amendedMethod = amendDefaultMethod(context.getHolder(), invokedMethod);
+        return rewriteInvoke.apply(
+            defaultAsMethodOfCompanionClass(amendedMethod, appView.dexItemFactory()));
+      }
+    }
+
+    DexType emulatedItf = maximallySpecificEmulatedInterfaceOrNull(invokedMethod);
+    if (emulatedItf == null) {
+      if (clazz.isInterface() && appView.rewritePrefix.hasRewrittenType(clazz.type, appView)) {
+        DexClassAndMethod target =
+            appView.appInfoForDesugaring().lookupSuperTarget(invokedMethod, context);
+        if (target != null && target.getDefinition().isDefaultMethod()) {
+          DexClass holder = target.getHolder();
+          if (holder.isLibraryClass() && holder.isInterface()) {
             assert invokeNeedsRewriting(invokedMethod, SUPER);
-            instructions.replaceCurrentInstruction(
-                new InvokeStatic(companionMethod, invoke.outValue(), invoke.arguments()));
-          } else {
-            assert invokeNeedsRewriting(invokedMethod, SUPER);
-            instructions.replaceCurrentInstruction(
-                new InvokeStatic(retargetMethod, invoke.outValue(), invoke.arguments()));
+            return rewriteInvoke.apply(defaultAsMethodOfCompanionClass(target));
           }
         }
       }
+      return null;
     }
+    // That invoke super may not resolve since the super method may not be present
+    // since it's in the emulated interface. We need to force resolution. If it resolves
+    // to a library method, then it needs to be rewritten.
+    // If it resolves to a program overrides, the invoke-super can remain.
+    DexClassAndMethod superTarget =
+        appView.appInfoForDesugaring().lookupSuperTarget(invokedMethod, context);
+    if (superTarget != null && superTarget.isLibraryMethod()) {
+      // Rewriting is required because the super invoke resolves into a missing
+      // method (method is on desugared library). Find out if it needs to be
+      // retarget or if it just calls a companion class method and rewrite.
+      DexMethod retargetMethod =
+          options.desugaredLibraryConfiguration.retargetMethod(superTarget, appView);
+      if (retargetMethod == null) {
+        DexMethod originalCompanionMethod = defaultAsMethodOfCompanionClass(superTarget);
+        DexMethod companionMethod =
+            factory.createMethod(
+                getCompanionClassType(emulatedItf),
+                factory.protoWithDifferentFirstParameter(
+                    originalCompanionMethod.proto, emulatedItf),
+                originalCompanionMethod.name);
+        assert invokeNeedsRewriting(invokedMethod, SUPER);
+        return rewriteInvoke.apply(companionMethod);
+      } else {
+        assert invokeNeedsRewriting(invokedMethod, SUPER);
+        return rewriteInvoke.apply(retargetMethod);
+      }
+    }
+    return null;
   }
 
   private DexClassAndMethod defaultMethodForEmulatedDispatchOrNull(
-      DexMethod method, Type invokeType) {
-    assert invokeType == VIRTUAL || invokeType == INTERFACE;
-    boolean interfaceBit = invokeType.isInterface();
-    DexType emulatedItf = maximallySpecificEmulatedInterfaceOrNull(method);
+      DexMethod invokedMethod, boolean interfaceBit) {
+    DexType emulatedItf = maximallySpecificEmulatedInterfaceOrNull(invokedMethod);
     if (emulatedItf == null) {
       return null;
     }
     // The call potentially ends up in a library class, in which case we need to rewrite, since the
     // code may be in the desugared library.
     SingleResolutionResult resolution =
-        appView.appInfoForDesugaring().resolveMethod(method, interfaceBit).asSingleResolution();
+        appView
+            .appInfoForDesugaring()
+            .resolveMethod(invokedMethod, interfaceBit)
+            .asSingleResolution();
     if (resolution != null
         && (resolution.getResolvedHolder().isLibraryClass()
             || appView.options().isDesugaredLibraryCompilation())) {
       DexClassAndMethod defaultMethod =
-          appView.definitionFor(emulatedItf).lookupClassMethod(method);
+          appView.definitionFor(emulatedItf).lookupClassMethod(invokedMethod);
       if (defaultMethod != null && !dontRewrite(defaultMethod)) {
         assert !defaultMethod.getAccessFlags().isAbstract();
         return defaultMethod;
@@ -661,20 +942,25 @@
     return null;
   }
 
-  private void rewriteInvokeInterfaceOrInvokeVirtual(
-      InvokeMethodWithReceiver invoke, InstructionListIterator instructions) {
+  private Collection<CfInstruction> rewriteInvokeInterfaceOrInvokeVirtual(
+      DexMethod invokedMethod,
+      boolean interfaceBit,
+      Function<DexMethod, Collection<CfInstruction>> rewriteInvoke) {
     DexClassAndMethod defaultMethod =
-        defaultMethodForEmulatedDispatchOrNull(invoke.getInvokedMethod(), invoke.getType());
+        defaultMethodForEmulatedDispatchOrNull(invokedMethod, interfaceBit);
     if (defaultMethod != null) {
-      instructions.replaceCurrentInstruction(
-          new InvokeStatic(
-              emulateInterfaceLibraryMethod(defaultMethod, factory),
-              invoke.outValue(),
-              invoke.arguments()));
+      return rewriteInvoke.apply(emulateInterfaceLibraryMethod(defaultMethod, factory));
     }
+    return null;
   }
 
-  private boolean rewriteInvokeToThrow(
+  private boolean shouldRewriteToInvokeToThrow(
+      SingleResolutionResult resolutionResult, boolean isInvokeStatic) {
+    return resolutionResult == null
+        || resolutionResult.getResolvedMethod().isStatic() != isInvokeStatic;
+  }
+
+  private Collection<CfInstruction> rewriteInvokeToThrowIR(
       InvokeMethod invoke,
       SingleResolutionResult resolutionResult,
       IRCode code,
@@ -692,7 +978,8 @@
       methodSynthesizerConsumer =
           UtilityMethodsForCodeOptimizations::synthesizeThrowIncompatibleClassChangeErrorMethod;
     } else {
-      return false;
+      assert false;
+      return null;
     }
 
     // Replace by throw new SomeException.
@@ -725,7 +1012,7 @@
     throwBlockIterator.next();
     throwBlockIterator.replaceCurrentInstructionWithThrow(
         appView, code, blockIterator, throwInvoke.outValue(), blocksToRemove, affectedValues);
-    return true;
+    return null;
   }
 
   private DexType maximallySpecificEmulatedInterfaceOrNull(DexMethod invokedMethod) {
@@ -1100,7 +1387,7 @@
       return collection;
     }
     collection = createInterfaceInfo(classToDesugar, implementing, iface);
-    Collection existing = cache.putIfAbsent(iface, collection);
+    DefaultMethodsHelper.Collection existing = cache.putIfAbsent(iface, collection);
     return existing != null ? existing : collection;
   }
 
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 b0df7c7..bd95f96 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
@@ -30,7 +30,9 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexValue.DexValueInt;
 import com.android.tools.r8.graph.FieldAccessFlags;
+import com.android.tools.r8.graph.GenericSignature.ClassTypeSignature;
 import com.android.tools.r8.graph.GenericSignature.FieldTypeSignature;
+import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.MethodCollection;
@@ -70,10 +72,12 @@
   private final InterfaceMethodRewriter rewriter;
   private final Map<DexProgramClass, PostProcessingInterfaceInfo> postProcessingInterfaceInfos =
       new ConcurrentHashMap<>();
+  private final ClassTypeSignature objectTypeSignature;
 
   InterfaceProcessor(AppView<?> appView, InterfaceMethodRewriter rewriter) {
     this.appView = appView;
     this.rewriter = rewriter;
+    this.objectTypeSignature = new ClassTypeSignature(appView.dexItemFactory().objectType);
   }
 
   @Override
@@ -129,6 +133,8 @@
                 appView,
                 builder -> {
                   builder.setSourceFile(iface.sourceFile);
+                  builder.setGenericSignature(
+                      iface.getClassSignature().toObjectBoundWithSameFormals(objectTypeSignature));
                   ensureCompanionClassInitializesInterface(iface, builder);
                   processVirtualInterfaceMethods(iface, builder);
                   processDirectInterfaceMethods(iface, builder);
@@ -260,7 +266,7 @@
                     .setName(companionMethod.getName())
                     .setProto(companionMethod.getProto())
                     .setAccessFlags(newFlags)
-                    .setGenericSignature(virtual.getGenericSignature())
+                    .setGenericSignature(MethodTypeSignature.noSignature())
                     .setAnnotations(virtual.annotations())
                     .setParameterAnnotationsList(virtual.getParameterAnnotations())
                     .setCode(ignored -> virtual.getCode())
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/lambda/LambdaInstructionDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/lambda/LambdaInstructionDesugaring.java
index ad1cacb..71010c3 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/lambda/LambdaInstructionDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/lambda/LambdaInstructionDesugaring.java
@@ -158,6 +158,11 @@
 
   @Override
   public boolean needsDesugaring(CfInstruction instruction, ProgramMethod context) {
+    return isLambdaInvoke(instruction, context, appView);
+  }
+
+  public static boolean isLambdaInvoke(
+      CfInstruction instruction, ProgramMethod context, AppView<?> appView) {
     return instruction.isInvokeDynamic()
         && LambdaDescriptor.tryInfer(
                 instruction.asInvokeDynamic().getCallSite(),
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/stringconcat/StringConcatInstructionDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/stringconcat/StringConcatInstructionDesugaring.java
index 4f4d963..607b088 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/stringconcat/StringConcatInstructionDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/stringconcat/StringConcatInstructionDesugaring.java
@@ -216,13 +216,17 @@
 
   @Override
   public boolean needsDesugaring(CfInstruction instruction, ProgramMethod context) {
-    return instruction.isInvokeDynamic()
-        && needsDesugaring(instruction.asInvokeDynamic().getCallSite());
+    return isStringConcatInvoke(instruction, factory);
   }
 
-  private boolean needsDesugaring(DexCallSite callSite) {
+  public static boolean isStringConcatInvoke(CfInstruction instruction, DexItemFactory factory) {
+    CfInvokeDynamic invoke = instruction.asInvokeDynamic();
+    if (invoke == null) {
+      return false;
+    }
     // We are interested in bootstrap methods StringConcatFactory::makeConcat
     // and StringConcatFactory::makeConcatWithConstants, both are static.
+    DexCallSite callSite = invoke.getCallSite();
     if (callSite.bootstrapMethod.type.isInvokeStatic()) {
       DexMethod bootstrapMethod = callSite.bootstrapMethod.asMethod();
       return bootstrapMethod == factory.stringConcatFactoryMembers.makeConcat
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
index 8e66601..ececbe3 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.optimize;
 
+import static com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo.isApiSafeForInlining;
 import static com.android.tools.r8.ir.optimize.inliner.InlinerUtils.addMonitorEnterValue;
 import static com.android.tools.r8.ir.optimize.inliner.InlinerUtils.collectAllMonitorEnterValues;
 
@@ -33,6 +34,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
 import com.android.tools.r8.ir.optimize.Inliner.InlineeWithReason;
 import com.android.tools.r8.ir.optimize.Inliner.Reason;
+import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
 import com.android.tools.r8.ir.optimize.inliner.InliningReasonStrategy;
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
@@ -121,11 +123,22 @@
       Reason reason,
       WhyAreYouNotInliningReporter whyAreYouNotInliningReporter) {
     DexEncodedMethod singleTargetMethod = singleTarget.getDefinition();
-    if (singleTargetMethod.getOptimizationInfo().neverInline()) {
+    MethodOptimizationInfo targetOptimizationInfo = singleTargetMethod.getOptimizationInfo();
+    if (targetOptimizationInfo.neverInline()) {
       whyAreYouNotInliningReporter.reportMarkedAsNeverInline();
       return false;
     }
 
+    // Do not inline if the inlinee is greater than the api caller level.
+    MethodOptimizationInfo callerOptimizationInfo = method.getDefinition().getOptimizationInfo();
+    // TODO(b/188498051): We should not force inline lower api method calls.
+    if (reason != Reason.FORCE
+        && isApiSafeForInlining(callerOptimizationInfo, targetOptimizationInfo, appView.options())
+            .isPossiblyFalse()) {
+      whyAreYouNotInliningReporter.reportInlineeHigherApiCall();
+      return false;
+    }
+
     // We don't inline into constructors when producing class files since this can mess up
     // the stackmap, see b/136250031
     if (method.getDefinition().isInstanceInitializer()
@@ -138,7 +151,7 @@
     if (method.getDefinition() == singleTargetMethod) {
       // Cannot handle recursive inlining at this point.
       // Force inlined method should never be recursive.
-      assert !singleTargetMethod.getOptimizationInfo().forceInline();
+      assert !targetOptimizationInfo.forceInline();
       whyAreYouNotInliningReporter.reportRecursiveMethod();
       return false;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UtilityMethodsForCodeOptimizations.java b/src/main/java/com/android/tools/r8/ir/optimize/UtilityMethodsForCodeOptimizations.java
index 953a78c..2b15739 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/UtilityMethodsForCodeOptimizations.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/UtilityMethodsForCodeOptimizations.java
@@ -177,6 +177,10 @@
       return method;
     }
 
+    public ProgramMethod uncheckedGetMethod() {
+      return method;
+    }
+
     public void optimize(MethodProcessor methodProcessor) {
       methodProcessor.scheduleDesugaredMethodForProcessing(method);
       optimized = true;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
index aacfec7..5f6ad1a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
@@ -505,7 +505,15 @@
           method, code, methodCallsOnInstance, inliningIRProvider, Timing.empty());
     } else {
       assert indirectMethodCallsOnInstance.stream()
-          .noneMatch(method -> method.getDefinition().getOptimizationInfo().mayHaveSideEffects());
+          .filter(method -> method.getDefinition().getOptimizationInfo().mayHaveSideEffects())
+          .allMatch(
+              method ->
+                  method.getDefinition().isInstanceInitializer()
+                      && !method
+                          .getDefinition()
+                          .getOptimizationInfo()
+                          .getContextInsensitiveInstanceInitializerInfo()
+                          .mayHaveOtherSideEffectsThanInstanceFieldAssignments());
     }
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
index d49189f..2b7ef1a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationInfo.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.ir.optimize.info.bridge.BridgeInfo;
 import com.android.tools.r8.ir.optimize.info.initializer.DefaultInstanceInitializerInfo;
 import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.google.common.collect.ImmutableSet;
 import java.util.BitSet;
 import java.util.Set;
@@ -38,8 +39,9 @@
   static boolean UNKNOWN_RETURN_VALUE_ONLY_DEPENDS_ON_ARGUMENTS = false;
   static BitSet NO_NULL_PARAMETER_OR_THROW_FACTS = null;
   static BitSet NO_NULL_PARAMETER_ON_NORMAL_EXITS_FACTS = null;
+  static AndroidApiLevel UNKNOWN_API_REFERENCE_LEVEL = null;
 
-  private DefaultMethodOptimizationInfo() {}
+  protected DefaultMethodOptimizationInfo() {}
 
   public static DefaultMethodOptimizationInfo getInstance() {
     return DEFAULT_INSTANCE;
@@ -57,7 +59,7 @@
 
   @Override
   public UpdatableMethodOptimizationInfo asUpdatableMethodOptimizationInfo() {
-    return null;
+    return mutableCopy();
   }
 
   @Override
@@ -192,6 +194,16 @@
   }
 
   @Override
+  public AndroidApiLevel getApiReferenceLevel(AndroidApiLevel minApi) {
+    return UNKNOWN_API_REFERENCE_LEVEL;
+  }
+
+  @Override
+  public boolean hasApiReferenceLevel() {
+    return false;
+  }
+
+  @Override
   public UpdatableMethodOptimizationInfo mutableCopy() {
     return new UpdatableMethodOptimizationInfo();
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationWithMinApiInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationWithMinApiInfo.java
new file mode 100644
index 0000000..fc61ba6
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/DefaultMethodOptimizationWithMinApiInfo.java
@@ -0,0 +1,35 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.info;
+
+import com.android.tools.r8.utils.AndroidApiLevel;
+
+public class DefaultMethodOptimizationWithMinApiInfo extends DefaultMethodOptimizationInfo {
+
+  private static final DefaultMethodOptimizationWithMinApiInfo DEFAULT_MIN_API_INSTANCE =
+      new DefaultMethodOptimizationWithMinApiInfo();
+
+  public static DefaultMethodOptimizationWithMinApiInfo getInstance() {
+    return DEFAULT_MIN_API_INSTANCE;
+  }
+
+  @Override
+  public boolean hasApiReferenceLevel() {
+    return true;
+  }
+
+  @Override
+  public AndroidApiLevel getApiReferenceLevel(AndroidApiLevel minApi) {
+    return minApi;
+  }
+
+  @Override
+  public UpdatableMethodOptimizationInfo mutableCopy() {
+    UpdatableMethodOptimizationInfo updatableMethodOptimizationInfo = super.mutableCopy();
+    // Use null to specify that the min api is set to minApi.
+    updatableMethodOptimizationInfo.setApiReferenceLevel(null);
+    return updatableMethodOptimizationInfo;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
index bdc35f3..0190cd0 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfo.java
@@ -4,6 +4,8 @@
 
 package com.android.tools.r8.ir.optimize.info;
 
+import static com.android.tools.r8.utils.OptionalBool.UNKNOWN;
+
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.analysis.inlining.SimpleInliningConstraint;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
@@ -13,6 +15,9 @@
 import com.android.tools.r8.ir.optimize.classinliner.constraint.ClassInlinerMethodConstraint;
 import com.android.tools.r8.ir.optimize.info.bridge.BridgeInfo;
 import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.OptionalBool;
 import java.util.BitSet;
 import java.util.Set;
 
@@ -95,5 +100,23 @@
 
   public abstract boolean returnValueHasBeenPropagated();
 
+  public abstract AndroidApiLevel getApiReferenceLevel(AndroidApiLevel minApi);
+
+  public abstract boolean hasApiReferenceLevel();
+
   public abstract UpdatableMethodOptimizationInfo mutableCopy();
+
+  public static OptionalBool isApiSafeForInlining(
+      MethodOptimizationInfo caller, MethodOptimizationInfo inlinee, InternalOptions options) {
+    if (!options.apiModelingOptions().enableApiCallerIdentification) {
+      return OptionalBool.TRUE;
+    }
+    if (!caller.hasApiReferenceLevel() || !inlinee.hasApiReferenceLevel()) {
+      return UNKNOWN;
+    }
+    return OptionalBool.of(
+        caller
+            .getApiReferenceLevel(options.minApiLevel)
+            .isGreaterThanOrEqualTo(inlinee.getApiReferenceLevel(options.minApiLevel)));
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
index 436f996..15afa8c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
@@ -73,6 +73,7 @@
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.InstancePut;
 import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.Instruction.SideEffectAssumption;
 import com.android.tools.r8.ir.code.InstructionIterator;
 import com.android.tools.r8.ir.code.InvokeDirect;
 import com.android.tools.r8.ir.code.InvokeMethod;
@@ -370,7 +371,8 @@
               if (singleTarget == null) {
                 return null;
               }
-              if (singleTarget.isInstanceInitializer() && invoke.getReceiver() == receiver) {
+              if (singleTarget.isInstanceInitializer()
+                  && invoke.getReceiver().getAliasedValue() == receiver) {
                 if (builder.hasParent() && builder.getParent() != singleTarget.getReference()) {
                   return null;
                 }
@@ -927,7 +929,18 @@
       mayHaveSideEffects = false;
       // Otherwise, check if there is an instruction that has side effects.
       for (Instruction instruction : code.instructions()) {
-        if (instruction.instructionMayHaveSideEffects(appView, context)) {
+        if (instruction.isInvokeConstructor(appView.dexItemFactory())
+            && instruction
+                .asInvokeDirect()
+                .getReceiver()
+                .getAliasedValue()
+                .isDefinedByInstructionSatisfying(Instruction::isNewInstance)) {
+          if (instruction.instructionMayHaveSideEffects(
+              appView, context, SideEffectAssumption.IGNORE_RECEIVER_FIELD_ASSIGNMENTS)) {
+            mayHaveSideEffects = true;
+            break;
+          }
+        } else if (instruction.instructionMayHaveSideEffects(appView, context)) {
           mayHaveSideEffects = true;
           break;
         }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/UpdatableMethodOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/UpdatableMethodOptimizationInfo.java
index 0d2e43b..85e2576 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/UpdatableMethodOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/UpdatableMethodOptimizationInfo.java
@@ -22,8 +22,10 @@
 import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfo;
 import com.android.tools.r8.ir.optimize.info.initializer.InstanceInitializerInfoCollection;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
 import java.util.BitSet;
+import java.util.Optional;
 import java.util.Set;
 
 public class UpdatableMethodOptimizationInfo extends MethodOptimizationInfo {
@@ -66,6 +68,8 @@
   private SimpleInliningConstraint simpleInliningConstraint =
       NeverSimpleInliningConstraint.getInstance();
 
+  private Optional<AndroidApiLevel> apiReferenceLevel = null;
+
   // To reduce the memory footprint of UpdatableMethodOptimizationInfo, all the boolean fields are
   // merged into a flag int field. The various static final FLAG fields indicate which bit is
   // used by each boolean. DEFAULT_FLAGS encodes the default value for efficient instantiation and
@@ -146,6 +150,7 @@
     nonNullParamOrThrow = template.nonNullParamOrThrow;
     nonNullParamOnNormalExits = template.nonNullParamOnNormalExits;
     classInlinerConstraint = template.classInlinerConstraint;
+    apiReferenceLevel = template.apiReferenceLevel;
   }
 
   public UpdatableMethodOptimizationInfo fixupClassTypeReferences(
@@ -495,6 +500,23 @@
   }
 
   @Override
+  public AndroidApiLevel getApiReferenceLevel(AndroidApiLevel minApi) {
+    assert hasApiReferenceLevel();
+    return apiReferenceLevel.orElse(minApi);
+  }
+
+  @SuppressWarnings("OptionalAssignedToNull")
+  @Override
+  public boolean hasApiReferenceLevel() {
+    return apiReferenceLevel != null;
+  }
+
+  public UpdatableMethodOptimizationInfo setApiReferenceLevel(AndroidApiLevel apiReferenceLevel) {
+    this.apiReferenceLevel = Optional.ofNullable(apiReferenceLevel);
+    return this;
+  }
+
+  @Override
   public UpdatableMethodOptimizationInfo mutableCopy() {
     return new UpdatableMethodOptimizationInfo(this);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/DefaultInstanceInitializerInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/DefaultInstanceInitializerInfo.java
index af59b7c..e976680 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/DefaultInstanceInitializerInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/DefaultInstanceInitializerInfo.java
@@ -30,6 +30,11 @@
   }
 
   @Override
+  public boolean hasParent() {
+    return false;
+  }
+
+  @Override
   public DexMethod getParent() {
     return null;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/InstanceInitializerInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/InstanceInitializerInfo.java
index 3865bc1..27576e9 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/InstanceInitializerInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/InstanceInitializerInfo.java
@@ -25,6 +25,8 @@
     return null;
   }
 
+  public abstract boolean hasParent();
+
   public abstract DexMethod getParent();
 
   public abstract InstanceFieldInitializationInfoCollection fieldInitializationInfos();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/NonTrivialInstanceInitializerInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/NonTrivialInstanceInitializerInfo.java
index 40b3edc..9dd2750 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/NonTrivialInstanceInitializerInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/initializer/NonTrivialInstanceInitializerInfo.java
@@ -63,6 +63,11 @@
   }
 
   @Override
+  public boolean hasParent() {
+    return parent != null;
+  }
+
+  @Override
   public DexMethod getParent() {
     return parent;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningIRProvider.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningIRProvider.java
index baf75b5..dba0cde 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningIRProvider.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/InliningIRProvider.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.NumberGenerator;
 import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.origin.Origin;
 import java.util.IdentityHashMap;
@@ -40,7 +41,12 @@
     Position position = Position.getPositionForInlining(appView, invoke, context);
     Origin origin = method.getOrigin();
     return method.buildInliningIR(
-        context, appView, valueNumberGenerator, position, origin, methodProcessor);
+        context,
+        appView,
+        valueNumberGenerator,
+        position,
+        origin,
+        IRBuilder.lookupPrototypeChangesForInlinee(appView, method, methodProcessor));
   }
 
   public IRCode getAndCacheInliningIR(InvokeMethod invoke, ProgramMethod method) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/NopWhyAreYouNotInliningReporter.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/NopWhyAreYouNotInliningReporter.java
index e398dce..218a8fc 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/NopWhyAreYouNotInliningReporter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/NopWhyAreYouNotInliningReporter.java
@@ -58,6 +58,9 @@
   public void reportInlineeNotSimple() {}
 
   @Override
+  public void reportInlineeHigherApiCall() {}
+
+  @Override
   public void reportInlineeRefersToClassesNotInMainDex() {}
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java
index 81c6a35..8063674 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java
@@ -69,6 +69,8 @@
 
   public abstract void reportInlineeNotSimple();
 
+  public abstract void reportInlineeHigherApiCall();
+
   public abstract void reportInlineeRefersToClassesNotInMainDex();
 
   public abstract void reportInliningAcrossFeatureSplit();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporterImpl.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporterImpl.java
index 257cc15..f9418b4 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporterImpl.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporterImpl.java
@@ -120,6 +120,11 @@
   }
 
   @Override
+  public void reportInlineeHigherApiCall() {
+    print("inlinee having a higher api call than caller context.");
+  }
+
+  @Override
   public void reportInlineeRefersToClassesNotInMainDex() {
     print(
         "inlining could increase the main dex size "
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
index 69d65c8..b809e23 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
@@ -391,7 +391,7 @@
     IRCode code = method.buildIR(appView);
     codeOptimizations.forEach(codeOptimization -> codeOptimization.accept(code, methodProcessor));
     CodeRewriter.removeAssumeInstructions(appView, code);
-    converter.removeDeadCodeAndFinalizeIR(method, code, feedback, Timing.empty());
+    converter.removeDeadCodeAndFinalizeIR(code, feedback, Timing.empty());
   }
 
   private void insertAssumeInstructions(IRCode code, MethodProcessor methodProcessor) {
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/AbstractSynthesizedCode.java b/src/main/java/com/android/tools/r8/ir/synthetic/AbstractSynthesizedCode.java
index 4dc56de..2c1b9a3 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/AbstractSynthesizedCode.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/AbstractSynthesizedCode.java
@@ -11,12 +11,12 @@
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.RewrittenPrototypeDescription;
 import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.NumberGenerator;
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.conversion.IRBuilder;
-import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.ir.conversion.SourceCode;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.origin.Origin;
@@ -51,14 +51,14 @@
       NumberGenerator valueNumberGenerator,
       Position callerPosition,
       Origin origin,
-      MethodProcessor methodProcessor) {
+      RewrittenPrototypeDescription protoChanges) {
     return IRBuilder.createForInlining(
             method,
             appView,
             getSourceCodeProvider().get(context, callerPosition),
             origin,
-            methodProcessor,
-            valueNumberGenerator)
+            valueNumberGenerator,
+            protoChanges)
         .build(context);
   }
 
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataEnqueuerExtension.java b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataEnqueuerExtension.java
index 6422c69..fc56e4e 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataEnqueuerExtension.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataEnqueuerExtension.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexDefinitionSupplier;
+import com.android.tools.r8.graph.DexEncodedMember;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
@@ -100,7 +101,20 @@
       }
       appView.setCfByteCodePassThrough(keepByteCodeFunctions);
     } else {
-      assert verifyKotlinMetadataModeledForAllClasses(enqueuer, keepMetadata);
+      assert enqueuer.getMode().isFinalTreeShaking();
+      enqueuer.forAllLiveClasses(
+          clazz -> {
+            if (!enqueuer.isPinned(clazz.getType())) {
+              clazz.setKotlinInfo(getNoKotlinInfo());
+              clazz.members().forEach(DexEncodedMember::clearKotlinInfo);
+              clazz.removeAnnotations(
+                  annotation -> annotation.getAnnotationType() == kotlinMetadataType);
+            } else {
+              assert !hasKotlinClassMetadataAnnotation(clazz, definitionsForContext(clazz))
+                  || !keepMetadata
+                  || clazz.getKotlinInfo() != getNoKotlinInfo();
+            }
+          });
     }
     // Trace through the modeled kotlin metadata.
     enqueuer.forAllLiveClasses(
@@ -112,19 +126,6 @@
         });
   }
 
-  private boolean verifyKotlinMetadataModeledForAllClasses(
-      Enqueuer enqueuer, boolean keepMetadata) {
-    enqueuer.forAllLiveClasses(
-        clazz -> {
-          // Trace through class and member definitions
-          assert !hasKotlinClassMetadataAnnotation(clazz, definitionsForContext(clazz))
-              || !keepMetadata
-              || !enqueuer.isPinned(clazz.type)
-              || clazz.getKotlinInfo() != getNoKotlinInfo();
-        });
-    return true;
-  }
-
   public class KotlinMetadataDefinitionSupplier implements DexDefinitionSupplier {
 
     private final ProgramDefinition context;
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapSupplier.java b/src/main/java/com/android/tools/r8/naming/ProguardMapSupplier.java
index ccc2ef7..8ac5e71 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapSupplier.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapSupplier.java
@@ -93,7 +93,7 @@
             + Version.LABEL
             + "\n");
     if (options.isGeneratingDex()) {
-      builder.append("# " + MARKER_KEY_MIN_API + ": " + options.minApiLevel + "\n");
+      builder.append("# " + MARKER_KEY_MIN_API + ": " + options.minApiLevel.getLevel() + "\n");
     }
     if (Version.isDevelopmentVersion()) {
       builder.append(
diff --git a/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureRewriter.java b/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureRewriter.java
index c44e4e7..16b6508 100644
--- a/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureRewriter.java
+++ b/src/main/java/com/android/tools/r8/naming/signature/GenericSignatureRewriter.java
@@ -4,23 +4,37 @@
 
 package com.android.tools.r8.naming.signature;
 
+import static com.google.common.base.Predicates.alwaysFalse;
+
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GenericSignatureContextBuilder;
+import com.android.tools.r8.graph.GenericSignaturePartialTypeArgumentApplier;
 import com.android.tools.r8.graph.GenericSignatureTypeRewriter;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.utils.ThreadUtils;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import java.util.function.BiPredicate;
+import java.util.function.Predicate;
 
 // TODO(b/169516860): We should generalize this to handle rewriting of attributes in general.
 public class GenericSignatureRewriter {
 
   private final AppView<?> appView;
   private final NamingLens namingLens;
+  private final GenericSignatureContextBuilder contextBuilder;
 
   public GenericSignatureRewriter(AppView<?> appView, NamingLens namingLens) {
+    this(appView, namingLens, null);
+  }
+
+  public GenericSignatureRewriter(
+      AppView<?> appView, NamingLens namingLens, GenericSignatureContextBuilder contextBuilder) {
     this.appView = appView;
     this.namingLens = namingLens;
+    this.contextBuilder = contextBuilder;
   }
 
   public void run(Iterable<? extends DexProgramClass> classes, ExecutorService executorService)
@@ -34,20 +48,54 @@
     // arguments. If that is the case, the ProguardMapMinifier will pass in all classes that is
     // either ProgramClass or has a mapping. This is then transitively called inside the
     // ClassNameMinifier.
+    Predicate<DexType> wasPruned =
+        appView.hasLiveness() ? appView.withLiveness().appInfo()::wasPruned : alwaysFalse();
+    BiPredicate<DexType, DexType> hasPrunedRelationship =
+        (enclosing, enclosed) ->
+            contextBuilder.hasPrunedRelationship(appView, enclosing, enclosed, wasPruned);
+    Predicate<DexType> hasGenericTypeVariables =
+        type -> contextBuilder.hasGenericTypeVariables(appView, type, wasPruned);
     ThreadUtils.processItems(
         classes,
         clazz -> {
+          GenericSignaturePartialTypeArgumentApplier classArgumentApplier =
+              contextBuilder != null
+                  ? GenericSignaturePartialTypeArgumentApplier.build(
+                      appView,
+                      contextBuilder.computeTypeParameterContext(
+                          appView, clazz.getType(), wasPruned),
+                      hasPrunedRelationship,
+                      hasGenericTypeVariables)
+                  : null;
           GenericSignatureTypeRewriter genericSignatureTypeRewriter =
               new GenericSignatureTypeRewriter(appView, clazz);
-          clazz.setClassSignature(genericSignatureTypeRewriter.rewrite(clazz.getClassSignature()));
+          clazz.setClassSignature(
+              genericSignatureTypeRewriter.rewrite(
+                  classArgumentApplier != null
+                      ? classArgumentApplier.visitClassSignature(clazz.getClassSignature())
+                      : clazz.getClassSignature()));
           clazz.forEachField(
               field ->
                   field.setGenericSignature(
-                      genericSignatureTypeRewriter.rewrite(field.getGenericSignature())));
+                      genericSignatureTypeRewriter.rewrite(
+                          classArgumentApplier != null
+                              ? classArgumentApplier.visitFieldTypeSignature(
+                                  field.getGenericSignature())
+                              : field.getGenericSignature())));
           clazz.forEachMethod(
-              method ->
-                  method.setGenericSignature(
-                      genericSignatureTypeRewriter.rewrite(method.getGenericSignature())));
+              method -> {
+                // The reflection api do not distinguish static methods context and
+                // from virtual methods we therefore always base the context for a method on
+                // the class context.
+                method.setGenericSignature(
+                    genericSignatureTypeRewriter.rewrite(
+                        classArgumentApplier != null
+                            ? classArgumentApplier
+                                .buildForMethod(
+                                    method.getGenericSignature().getFormalTypeParameters())
+                                .visitMethodSignature(method.getGenericSignature())
+                            : method.getGenericSignature()));
+              });
         },
         executorService);
   }
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
index 3f77dbb..0fdc499 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
@@ -41,7 +41,12 @@
   }
 
   public static Builder builder(AppView<? extends AppInfoWithClassHierarchy> appView) {
-    return new Builder(appView);
+    return builder(appView, appView.graphLens());
+  }
+
+  public static Builder builder(
+      AppView<? extends AppInfoWithClassHierarchy> appView, GraphLens previousLens) {
+    return new Builder(appView, previousLens);
   }
 
   @Override
@@ -129,16 +134,55 @@
     return getPrevious().isContextFreeForMethods();
   }
 
+  @Override
+  public boolean isMemberRebindingIdentityLens() {
+    return true;
+  }
+
+  @Override
+  public MemberRebindingIdentityLens asMemberRebindingIdentityLens() {
+    return this;
+  }
+
+  public MemberRebindingIdentityLens toRewrittenMemberRebindingIdentityLens(
+      AppView<? extends AppInfoWithClassHierarchy> appView, GraphLens lens) {
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    Builder builder = builder(appView, getIdentityLens());
+    nonReboundFieldReferenceToDefinitionMap.forEach(
+        (nonReboundFieldReference, reboundFieldReference) -> {
+          DexField rewrittenReboundFieldReference = lens.lookupField(reboundFieldReference);
+          DexField rewrittenNonReboundFieldReference =
+              rewrittenReboundFieldReference.withHolder(
+                  lens.lookupType(nonReboundFieldReference.getHolderType()), dexItemFactory);
+          builder.recordNonReboundFieldAccess(
+              rewrittenNonReboundFieldReference, rewrittenReboundFieldReference);
+        });
+    nonReboundMethodReferenceToDefinitionMap.forEach(
+        (nonReboundMethodReference, reboundMethodReference) -> {
+          DexMethod rewrittenReboundMethodReference =
+              lens.getRenamedMethodSignature(reboundMethodReference);
+          DexMethod rewrittenNonReboundMethodReference =
+              rewrittenReboundMethodReference.withHolder(
+                  lens.lookupType(nonReboundMethodReference.getHolderType()), dexItemFactory);
+          builder.recordNonReboundMethodAccess(
+              rewrittenNonReboundMethodReference, rewrittenReboundMethodReference);
+        });
+    return builder.build();
+  }
+
   public static class Builder {
 
     private final AppView<? extends AppInfoWithClassHierarchy> appView;
+    private final GraphLens previousLens;
+
     private final Map<DexField, DexField> nonReboundFieldReferenceToDefinitionMap =
         new IdentityHashMap<>();
     private final Map<DexMethod, DexMethod> nonReboundMethodReferenceToDefinitionMap =
         new IdentityHashMap<>();
 
-    private Builder(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    private Builder(AppView<? extends AppInfoWithClassHierarchy> appView, GraphLens previousLens) {
       this.appView = appView;
+      this.previousLens = previousLens;
     }
 
     void recordNonReboundFieldAccesses(FieldAccessInfo fieldAccessInfo) {
@@ -152,6 +196,12 @@
       nonReboundFieldReferenceToDefinitionMap.put(nonReboundFieldReference, reboundFieldReference);
     }
 
+    private void recordNonReboundMethodAccess(
+        DexMethod nonReboundMethodReference, DexMethod reboundMethodReference) {
+      nonReboundMethodReferenceToDefinitionMap.put(
+          nonReboundMethodReference, reboundMethodReference);
+    }
+
     void recordMethodAccess(DexMethod reference) {
       if (reference.getHolderType().isArrayType()) {
         return;
@@ -161,7 +211,7 @@
         SingleResolutionResult resolutionResult =
             appView.appInfo().resolveMethodOn(holder, reference).asSingleResolution();
         if (resolutionResult != null && resolutionResult.getResolvedHolder() != holder) {
-          nonReboundMethodReferenceToDefinitionMap.put(
+          recordNonReboundMethodAccess(
               reference, resolutionResult.getResolvedMethod().getReference());
         }
       }
@@ -175,7 +225,7 @@
           nonReboundFieldReferenceToDefinitionMap,
           nonReboundMethodReferenceToDefinitionMap,
           appView.dexItemFactory(),
-          appView.graphLens());
+          previousLens);
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
index 4310236..d202af8 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.graph.NestedGraphLens.mapVirtualInterfaceInvocationTypes;
 
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -47,6 +48,11 @@
   }
 
   @Override
+  public MemberRebindingLens asMemberRebindingLens() {
+    return this;
+  }
+
+  @Override
   public DexType getOriginalType(DexType type) {
     return getPrevious().getOriginalType(type);
   }
@@ -131,7 +137,8 @@
   }
 
   public FieldRebindingIdentityLens toRewrittenFieldRebindingLens(
-      DexItemFactory dexItemFactory, GraphLens lens) {
+      AppView<? extends AppInfoWithClassHierarchy> appView, GraphLens lens) {
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
     FieldRebindingIdentityLens.Builder builder = FieldRebindingIdentityLens.builder();
     nonReboundFieldReferenceToDefinitionMap.forEach(
         (nonReboundFieldReference, reboundFieldReference) -> {
diff --git a/src/main/java/com/android/tools/r8/repackaging/Repackaging.java b/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
index 188e749..e193f4c 100644
--- a/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
+++ b/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.graph.InnerClassAttribute;
+import com.android.tools.r8.graph.NestedGraphLens;
 import com.android.tools.r8.graph.ProgramPackage;
 import com.android.tools.r8.graph.ProgramPackageCollection;
 import com.android.tools.r8.graph.SortedProgramPackageCollection;
@@ -26,7 +27,6 @@
 import com.android.tools.r8.shaking.AnnotationFixer;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.ProguardConfiguration;
-import com.android.tools.r8.synthesis.CommittedItems;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions.PackageObfuscationMode;
 import com.android.tools.r8.utils.Timing;
@@ -104,25 +104,22 @@
             assert false;
           }
         }.fixupClasses(appView.appInfo().classesWithDeterministicOrder());
-    CommittedItems committedItems =
+    NestedGraphLens emptyRepackagingLens =
+        new NestedGraphLens(appView) {
+          @Override
+          protected boolean isLegitimateToHaveEmptyMappings() {
+            return true;
+          }
+        };
+    DirectMappedDexApplication newApplication =
         appView
-            .getSyntheticItems()
-            .commit(
-                appView
-                    .appInfo()
-                    .app()
-                    .builder()
-                    .replaceProgramClasses(new ArrayList<>(newProgramClasses))
-                    .build());
-    if (appView.appInfo().hasLiveness()) {
-      appView
-          .withLiveness()
-          .setAppInfo(appView.withLiveness().appInfo().rebuildWithLiveness(committedItems));
-    } else {
-      appView
-          .withClassHierarchy()
-          .setAppInfo(appView.appInfo().rebuildWithClassHierarchy(committedItems));
-    }
+            .appInfo()
+            .app()
+            .asDirect()
+            .builder()
+            .replaceProgramClasses(new ArrayList<>(newProgramClasses))
+            .build();
+    appView.rewriteWithLensAndApplication(emptyRepackagingLens, newApplication);
     return true;
   }
 
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceElement.java b/src/main/java/com/android/tools/r8/retrace/RetraceElement.java
index cb2ac8c..55de558 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceElement.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceElement.java
@@ -3,11 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.retrace;
 
+import com.android.tools.r8.KeepForSubclassing;
+
 /**
  * Base interface for any element in a retrace result.
  *
  * <p>The element represents an unambiguous retracing.
  */
+@KeepForSubclassing
 public interface RetraceElement<R extends RetraceResult<?>> {
 
   R getRetraceResultContext();
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
index c4ebfe7..239c0da 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -1072,9 +1072,11 @@
         objectAllocationInfoCollection.rewrittenWithLens(definitionSupplier, lens),
         lens.rewriteCallSites(callSites, definitionSupplier),
         keepInfo.rewrite(lens, application.options),
-        lens.rewriteReferenceKeys(mayHaveSideEffects),
-        lens.rewriteReferenceKeys(noSideEffects),
-        lens.rewriteReferenceKeys(assumedValues),
+        // Take any rule in case of collisions.
+        lens.rewriteReferenceKeys(mayHaveSideEffects, ListUtils::first),
+        // Drop assume rules in case of collisions.
+        lens.rewriteReferenceKeys(noSideEffects, rules -> null),
+        lens.rewriteReferenceKeys(assumedValues, rules -> null),
         lens.rewriteMethods(alwaysInline),
         lens.rewriteMethods(forceInline),
         lens.rewriteMethods(neverInline),
diff --git a/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java b/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
index f8302d7..cb49580 100644
--- a/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
+++ b/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
@@ -13,25 +13,33 @@
 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.DexReference;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import java.util.ListIterator;
+import java.util.Map;
 
 public class DefaultEnqueuerUseRegistry extends UseRegistry {
 
   protected final AppView<? extends AppInfoWithClassHierarchy> appView;
   private final ProgramMethod context;
   protected final Enqueuer enqueuer;
+  private final Map<DexReference, AndroidApiLevel> apiReferenceMapping;
+  private AndroidApiLevel maxApiReferenceLevel;
 
   public DefaultEnqueuerUseRegistry(
       AppView<? extends AppInfoWithClassHierarchy> appView,
       ProgramMethod context,
-      Enqueuer enqueuer) {
+      Enqueuer enqueuer,
+      Map<DexReference, AndroidApiLevel> apiReferenceMapping) {
     super(appView.dexItemFactory());
     this.appView = appView;
     this.context = context;
     this.enqueuer = enqueuer;
+    this.apiReferenceMapping = apiReferenceMapping;
+    this.maxApiReferenceLevel = appView.options().minApiLevel;
   }
 
   public ProgramMethod getContext() {
@@ -53,26 +61,31 @@
 
   @Override
   public void registerInvokeVirtual(DexMethod invokedMethod) {
+    setMaxApiReferenceLevel(invokedMethod);
     enqueuer.traceInvokeVirtual(invokedMethod, context);
   }
 
   @Override
   public void registerInvokeDirect(DexMethod invokedMethod) {
+    setMaxApiReferenceLevel(invokedMethod);
     enqueuer.traceInvokeDirect(invokedMethod, context);
   }
 
   @Override
   public void registerInvokeStatic(DexMethod invokedMethod) {
+    setMaxApiReferenceLevel(invokedMethod);
     enqueuer.traceInvokeStatic(invokedMethod, context);
   }
 
   @Override
   public void registerInvokeInterface(DexMethod invokedMethod) {
+    setMaxApiReferenceLevel(invokedMethod);
     enqueuer.traceInvokeInterface(invokedMethod, context);
   }
 
   @Override
   public void registerInvokeSuper(DexMethod invokedMethod) {
+    setMaxApiReferenceLevel(invokedMethod);
     enqueuer.traceInvokeSuper(invokedMethod, context);
   }
 
@@ -158,4 +171,14 @@
     super.registerCallSite(callSite);
     enqueuer.traceCallSite(callSite, context);
   }
+
+  private void setMaxApiReferenceLevel(DexMethod invokedMethod) {
+    this.maxApiReferenceLevel =
+        maxApiReferenceLevel.max(
+            apiReferenceMapping.getOrDefault(invokedMethod, maxApiReferenceLevel));
+  }
+
+  public AndroidApiLevel getMaxApiReferenceLevel() {
+    return maxApiReferenceLevel;
+  }
 }
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 f6d9e6e..1023e70 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -102,6 +102,8 @@
 import com.android.tools.r8.ir.desugar.DesugaredLibraryAPIConverter;
 import com.android.tools.r8.ir.desugar.LambdaClass;
 import com.android.tools.r8.ir.desugar.LambdaDescriptor;
+import com.android.tools.r8.ir.optimize.info.DefaultMethodOptimizationInfo;
+import com.android.tools.r8.ir.optimize.info.DefaultMethodOptimizationWithMinApiInfo;
 import com.android.tools.r8.kotlin.KotlinMetadataEnqueuerExtension;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.naming.identifiernamestring.IdentifierNameStringLookupResult;
@@ -120,6 +122,7 @@
 import com.android.tools.r8.shaking.ScopedDexMethodSet.AddMethodIfMoreVisibleResult;
 import com.android.tools.r8.synthesis.SyntheticItems.SynthesizingContextOracle;
 import com.android.tools.r8.utils.Action;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.IteratorUtils;
 import com.android.tools.r8.utils.ListUtils;
@@ -254,6 +257,8 @@
 
   private final Set<DexReference> identifierNameStrings = Sets.newIdentityHashSet();
 
+  private final Map<DexReference, AndroidApiLevel> referenceToApiLevelMap;
+
   /**
    * Tracks the dependency between a method and the super-method it calls, if any. Used to make
    * super methods become live when they become reachable from a live sub-method.
@@ -463,6 +468,17 @@
     } else {
       desugaredLibraryWrapperAnalysis = null;
     }
+    referenceToApiLevelMap = new IdentityHashMap<>();
+    if (options.apiModelingOptions().enableApiCallerIdentification) {
+      options
+          .apiModelingOptions()
+          .methodApiMapping
+          .forEach(
+              (methodReference, apiLevel) -> {
+                referenceToApiLevelMap.put(
+                    options.dexItemFactory().createMethod(methodReference), apiLevel);
+              });
+    }
   }
 
   private AppInfoWithClassHierarchy appInfo() {
@@ -2961,7 +2977,8 @@
       RootSet rootSet, ExecutorService executorService, Timing timing) throws ExecutionException {
     this.rootSet = rootSet;
     // Translate the result of root-set computation into enqueuer actions.
-    if (appView.options().getProguardConfiguration() != null
+    if (mode.isTreeShaking()
+        && appView.options().hasProguardConfiguration()
         && !options.kotlinOptimizationOptions().disableKotlinSpecificOptimizations) {
       registerAnalysis(
           new KotlinMetadataEnqueuerExtension(
@@ -3895,7 +3912,22 @@
   }
 
   void traceCode(ProgramMethod method) {
-    method.registerCodeReferences(useRegistryFactory.create(appView, method, this));
+    DefaultEnqueuerUseRegistry registry =
+        useRegistryFactory.create(appView, method, this, referenceToApiLevelMap);
+    method.registerCodeReferences(registry);
+    DexEncodedMethod methodDefinition = method.getDefinition();
+    AndroidApiLevel maxApiReferenceLevel = registry.getMaxApiReferenceLevel();
+    assert maxApiReferenceLevel.isGreaterThanOrEqualTo(options.minApiLevel);
+    // To not have mutable update information for all methods that all has min api level we
+    // swap the default optimization info for one with that marks the api level to be min api.
+    if (methodDefinition.getOptimizationInfo() == DefaultMethodOptimizationInfo.getInstance()
+        && maxApiReferenceLevel == options.minApiLevel) {
+      methodDefinition.setMinApiOptimizationInfo(
+          DefaultMethodOptimizationWithMinApiInfo.getInstance());
+      return;
+    }
+    methodDefinition.setOptimizationInfo(
+        methodDefinition.getMutableOptimizationInfo().setApiReferenceLevel(maxApiReferenceLevel));
   }
 
   private void checkMemberForSoftPinning(ProgramMember<?, ?> member) {
diff --git a/src/main/java/com/android/tools/r8/shaking/EnqueuerUseRegistryFactory.java b/src/main/java/com/android/tools/r8/shaking/EnqueuerUseRegistryFactory.java
index c15f8d7..6bd8473 100644
--- a/src/main/java/com/android/tools/r8/shaking/EnqueuerUseRegistryFactory.java
+++ b/src/main/java/com/android/tools/r8/shaking/EnqueuerUseRegistryFactory.java
@@ -6,13 +6,16 @@
 
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.ProgramMethod;
-import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import java.util.Map;
 
 public interface EnqueuerUseRegistryFactory {
 
-  UseRegistry create(
+  DefaultEnqueuerUseRegistry create(
       AppView<? extends AppInfoWithClassHierarchy> appView,
       ProgramMethod currentMethod,
-      Enqueuer enqueuer);
+      Enqueuer enqueuer,
+      Map<DexReference, AndroidApiLevel> apiLevelReferenceMap);
 }
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java
index ef3b4d2..8e78b09 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java
@@ -44,6 +44,7 @@
   private List<DexEncodedMethod> directMethods = new ArrayList<>();
   private List<DexEncodedMethod> virtualMethods = new ArrayList<>();
   private List<SyntheticMethodBuilder> methods = new ArrayList<>();
+  private ClassSignature signature = ClassSignature.noSignature();
 
   SyntheticClassBuilder(DexType type, SynthesizingContext context, DexItemFactory factory) {
     this.factory = factory;
@@ -87,6 +88,11 @@
     return self();
   }
 
+  public B setGenericSignature(ClassSignature signature) {
+    this.signature = signature;
+    return self();
+  }
+
   public B setStaticFields(List<DexEncodedField> fields) {
     staticFields.clear();
     staticFields.addAll(fields);
@@ -153,7 +159,7 @@
             nestMembers,
             enclosingMembers,
             innerClasses,
-            ClassSignature.noSignature(),
+            signature,
             DexAnnotationSet.empty(),
             staticFields.toArray(DexEncodedField.EMPTY_ARRAY),
             instanceFields.toArray(DexEncodedField.EMPTY_ARRAY),
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
index ba68e81..7c1decc 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
@@ -71,6 +71,7 @@
       ClassToFeatureSplitMap classToFeatureSplitMap,
       SyntheticItems syntheticItems) {
     Hasher hasher = Hashing.murmur3_128().newHasher();
+    hasher.putInt(kind.id);
     if (getKind().isFixedSuffixSynthetic) {
       // Fixed synthetics are non-shareable. Its unique type is used as the hash key.
       getHolder().getType().hash(hasher);
@@ -101,6 +102,12 @@
       boolean includeContext,
       GraphLens graphLens,
       ClassToFeatureSplitMap classToFeatureSplitMap) {
+    {
+      int order = kind.compareTo(other.getKind());
+      if (order != 0) {
+        return order;
+      }
+    }
     DexType thisType = getHolder().getType();
     DexType otherType = other.getHolder().getType();
     if (getKind().isFixedSuffixSynthetic) {
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
index 55b5e00..a07b61b 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
@@ -48,6 +48,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.IdentityHashMap;
 import java.util.List;
@@ -549,9 +550,9 @@
         (externalSyntheticTypePrefix, groups) -> {
           // Sort the equivalence groups that go into 'context' including the context type of the
           // representative which is equal to 'context' here (see assert below).
-          groups.sort(
-              (a, b) ->
-                  a.compareToIncludingContext(b, appView.graphLens(), classToFeatureSplitMap));
+          Comparator<EquivalenceGroup<T>> comparator =
+              (a, b) -> a.compareToIncludingContext(b, appView.graphLens(), classToFeatureSplitMap);
+          ListUtils.destructiveSort(groups, comparator);
           for (int i = 0; i < groups.size(); i++) {
             EquivalenceGroup<T> group = groups.get(i);
             assert group
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
index b5fc30d..ffaad01 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
@@ -22,7 +22,8 @@
 
 public class SyntheticMarker {
 
-  private static final String SYNTHETIC_MARKER_ATTRIBUTE_TYPE_NAME = "R8SynthesizedClass";
+  private static final String SYNTHETIC_MARKER_ATTRIBUTE_TYPE_NAME =
+      "com.android.tools.r8.SynthesizedClass";
 
   public static Attribute getMarkerAttributePrototype() {
     return MarkerAttribute.PROTOTYPE;
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
index ce1647b..20f0ab4 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -207,6 +207,11 @@
         createDescriptor(EXTERNAL_SYNTHETIC_CLASS_SEPARATOR, kind, context.getBinaryName(), id));
   }
 
+  public static boolean isInternalStaticInterfaceCall(ClassReference reference) {
+    return SyntheticNaming.isSynthetic(
+        reference, Phase.INTERNAL, SyntheticKind.STATIC_INTERFACE_CALL);
+  }
+
   static boolean isSynthetic(ClassReference clazz, Phase phase, SyntheticKind kind) {
     String typeName = clazz.getTypeName();
     if (kind.isFixedSuffixSynthetic) {
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java b/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java
index 523552a..6ea2f40 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java
@@ -40,7 +40,8 @@
   P(28),
   Q(29),
   R(30),
-  S(31);
+  S(31),
+  UNKNOWN(10000);
 
   public static final AndroidApiLevel LATEST = S;
 
@@ -64,6 +65,10 @@
     return AndroidApiLevel.B;
   }
 
+  public AndroidApiLevel max(AndroidApiLevel other) {
+    return Ordered.max(this, other);
+  }
+
   public DexVersion getDexVersion() {
     return DexVersion.getDexVersion(this);
   }
@@ -88,10 +93,9 @@
   }
 
   public static AndroidApiLevel getAndroidApiLevel(int apiLevel) {
+    assert apiLevel > 0;
+    assert UNKNOWN.isGreaterThan(LATEST);
     switch (apiLevel) {
-      case 0:
-        // 0 is not supported, it should not happen
-        throw new Unreachable();
       case 1:
         return B;
       case 2:
@@ -152,8 +156,12 @@
         return Q;
       case 30:
         return R;
+      case 31:
+        return S;
       default:
-        return LATEST;
+        // This has to be updated when we add new api levels.
+        assert S == LATEST;
+        return UNKNOWN;
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/CfVersionUtils.java b/src/main/java/com/android/tools/r8/utils/CfVersionUtils.java
index dadf460..01ec7c1 100644
--- a/src/main/java/com/android/tools/r8/utils/CfVersionUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/CfVersionUtils.java
@@ -5,17 +5,17 @@
 package com.android.tools.r8.utils;
 
 import com.android.tools.r8.cf.CfVersion;
-import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.utils.structural.Ordered;
 import java.util.List;
 
 public class CfVersionUtils {
 
-  public static CfVersion max(List<DexEncodedMethod> methods) {
+  public static CfVersion max(List<ProgramMethod> methods) {
     CfVersion result = null;
-    for (DexEncodedMethod method : methods) {
-      if (method.hasClassFileVersion()) {
-        result = Ordered.maxIgnoreNull(result, method.getClassFileVersion());
+    for (ProgramMethod method : methods) {
+      if (method.getDefinition().hasClassFileVersion()) {
+        result = Ordered.maxIgnoreNull(result, method.getDefinition().getClassFileVersion());
       }
     }
     return result;
diff --git a/src/main/java/com/android/tools/r8/utils/DexVersion.java b/src/main/java/com/android/tools/r8/utils/DexVersion.java
index f06f3d7..65ac80a 100644
--- a/src/main/java/com/android/tools/r8/utils/DexVersion.java
+++ b/src/main/java/com/android/tools/r8/utils/DexVersion.java
@@ -38,6 +38,9 @@
 
   public static DexVersion getDexVersion(AndroidApiLevel androidApiLevel) {
     switch (androidApiLevel) {
+        // UNKNOWN is an unknown higher api version we therefore choose the highest known
+        // version.
+      case UNKNOWN:
       case S:
       case R:
       case Q:
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 12126a4..9003eca 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -85,7 +85,6 @@
 import java.util.TreeSet;
 import java.util.concurrent.atomic.AtomicBoolean;
 import java.util.function.BiConsumer;
-import java.util.function.BiFunction;
 import java.util.function.BiPredicate;
 import java.util.function.Consumer;
 import java.util.function.Function;
@@ -349,9 +348,6 @@
   // Contain the contents of the build properties file from the compiler command.
   public DumpOptions dumpOptions;
 
-  // A mapping from methods to the api-level introducing them.
-  public Map<MethodReference, AndroidApiLevel> methodApiMapping = new HashMap<>();
-
   // Hidden marker for classes.dex
   private boolean hasMarker = false;
   private Marker marker;
@@ -383,7 +379,7 @@
     // since the output depends on the min API in this case. There is basically no min API entry
     // in R8 cf to cf.
     if (isGeneratingDex() || desugarState == DesugarState.ON) {
-      marker.setMinApi(minApiLevel);
+      marker.setMinApi(minApiLevel.getLevel());
     }
     if (desugaredLibraryConfiguration.getIdentifier() != null) {
       marker.setDesugaredLibraryIdentifiers(desugaredLibraryConfiguration.getIdentifier());
@@ -526,7 +522,7 @@
       getExtensiveInterfaceMethodMinifierLoggingFilter();
 
   public List<String> methodsFilter = ImmutableList.of();
-  public int minApiLevel = AndroidApiLevel.getDefault().getLevel();
+  public AndroidApiLevel minApiLevel = AndroidApiLevel.getDefault();
   // Skipping min_api check and compiling an intermediate result intended for later merging.
   // Intermediate builds also emits or update synthesized classes mapping.
   public boolean intermediate = false;
@@ -657,6 +653,7 @@
   public boolean ignoreMissingClasses = false;
   public boolean reportMissingClassesInEnclosingMethodAttribute = false;
   public boolean reportMissingClassesInInnerClassAttributes = false;
+  public boolean disableGenericSignatureValidation = false;
 
   // EXPERIMENTAL flag to get behaviour as close to Proguard as possible.
   public boolean forceProguardCompatibility = false;
@@ -675,6 +672,7 @@
   private final ProtoShrinkingOptions protoShrinking = new ProtoShrinkingOptions();
   private final KotlinOptimizationOptions kotlinOptimizationOptions =
       new KotlinOptimizationOptions();
+  private final ApiModelTestingOptions apiModelTestingOptions = new ApiModelTestingOptions();
   private final DesugarSpecificOptions desugarSpecificOptions = new DesugarSpecificOptions();
   public final TestingOptions testing = new TestingOptions();
 
@@ -707,6 +705,10 @@
     return kotlinOptimizationOptions;
   }
 
+  public ApiModelTestingOptions apiModelingOptions() {
+    return apiModelTestingOptions;
+  }
+
   public DesugarSpecificOptions desugarSpecificOptions() {
     return desugarSpecificOptions;
   }
@@ -1215,7 +1217,8 @@
     private boolean enable =
         !Version.isDevelopmentVersion()
             || System.getProperty("com.android.tools.r8.disableHorizontalClassMerging") == null;
-    private boolean enableInterfaceMerging = false;
+    private boolean enableInterfaceMergingInInitial = false;
+    private boolean enableInterfaceMergingInFinal = false;
     private boolean enableSyntheticMerging = true;
     private boolean ignoreRuntimeTypeChecksForTesting = false;
     private boolean restrictToSynthetics = false;
@@ -1238,10 +1241,6 @@
       this.enable = enable;
     }
 
-    public void enableInterfaceMerging() {
-      enableInterfaceMerging = true;
-    }
-
     public int getMaxGroupSize() {
       return maxGroupSize;
     }
@@ -1265,23 +1264,30 @@
       return ignoreRuntimeTypeChecksForTesting;
     }
 
-    public boolean isInterfaceMergingEnabled() {
-      assert !isInterfaceMergingEnabled(HorizontalClassMerger.Mode.INITIAL);
-      return isInterfaceMergingEnabled(HorizontalClassMerger.Mode.FINAL);
-    }
-
     public boolean isSyntheticMergingEnabled() {
       return enableSyntheticMerging;
     }
 
     public boolean isInterfaceMergingEnabled(HorizontalClassMerger.Mode mode) {
-      return enableInterfaceMerging && mode.isFinal();
+      if (mode.isInitial()) {
+        return enableInterfaceMergingInInitial;
+      }
+      assert mode.isFinal();
+      return enableInterfaceMergingInFinal;
     }
 
     public boolean isRestrictedToSynthetics() {
       return restrictToSynthetics || !isOptimizing() || !isShrinking();
     }
 
+    public void setEnableInterfaceMergingInInitial() {
+      enableInterfaceMergingInInitial = true;
+    }
+
+    public void setEnableInterfaceMergingInFinal() {
+      enableInterfaceMergingInFinal = true;
+    }
+
     public void setIgnoreRuntimeTypeChecksForTesting() {
       ignoreRuntimeTypeChecksForTesting = true;
     }
@@ -1291,6 +1297,14 @@
     }
   }
 
+  public static class ApiModelTestingOptions {
+
+    // A mapping from methods to the api-level introducing them.
+    public Map<MethodReference, AndroidApiLevel> methodApiMapping = new HashMap<>();
+
+    public boolean enableApiCallerIdentification = false;
+  }
+
   public static class ProtoShrinkingOptions {
 
     public boolean enableGeneratedExtensionRegistryShrinking = false;
@@ -1351,8 +1365,8 @@
 
     public BiConsumer<DexItemFactory, HorizontallyMergedClasses> horizontallyMergedClassesConsumer =
         ConsumerUtils.emptyBiConsumer();
-    public BiFunction<Iterable<DexProgramClass>, DexProgramClass, DexProgramClass>
-        horizontalClassMergingTarget = (candidates, target) -> target;
+    public TriFunction<AppView<?>, Iterable<DexProgramClass>, DexProgramClass, DexProgramClass>
+        horizontalClassMergingTarget = (appView, candidates, target) -> target;
 
     public BiConsumer<DexItemFactory, EnumDataMap> unboxedEnumsConsumer =
         ConsumerUtils.emptyBiConsumer();
@@ -1505,7 +1519,7 @@
   }
 
   private boolean hasMinApi(AndroidApiLevel level) {
-    return minApiLevel >= level.getLevel();
+    return minApiLevel.isGreaterThanOrEqualTo(level);
   }
 
   /**
@@ -1588,7 +1602,7 @@
     // the highest known API level when the compiler is built. This ensures that when this is used
     // by the Android Platform build (which normally use an API level of 10000) there will be
     // no rewriting of backported methods. See b/147480264.
-    return desugarState.isOn() && minApiLevel <= AndroidApiLevel.LATEST.getLevel();
+    return desugarState.isOn() && minApiLevel.isLessThanOrEqualTo(AndroidApiLevel.LATEST);
   }
 
   public boolean enableTryWithResourcesDesugaring() {
@@ -1690,7 +1704,7 @@
   // being thrown on out of bounds.
   public boolean canUseSameArrayAndResultRegisterInArrayGetWide() {
     assert isGeneratingDex();
-    return minApiLevel > AndroidApiLevel.O_MR1.getLevel();
+    return minApiLevel.isGreaterThan(AndroidApiLevel.O_MR1);
   }
 
   // Some Lollipop versions of Art found in the wild perform invalid bounds
@@ -1707,7 +1721,7 @@
   //
   // See b/69364976 and b/77996377.
   public boolean canHaveBoundsCheckEliminationBug() {
-    return isGeneratingDex() && minApiLevel < AndroidApiLevel.M.getLevel();
+    return isGeneratingDex() && minApiLevel.isLessThan(AndroidApiLevel.M);
   }
 
   // MediaTek JIT compilers for KitKat phones did not implement the not
@@ -1723,7 +1737,7 @@
   // assumed to not change. If the receiver register is reused for something else the verifier
   // will fail and the code will not run.
   public boolean canHaveThisTypeVerifierBug() {
-    return isGeneratingDex() && minApiLevel < AndroidApiLevel.M.getLevel();
+    return isGeneratingDex() && minApiLevel.isLessThan(AndroidApiLevel.M);
   }
 
   // Art crashes if we do dead reference elimination of the receiver in release mode and Art
@@ -1732,13 +1746,13 @@
   //
   // See b/116683601 and b/116837585.
   public boolean canHaveThisJitCodeDebuggingBug() {
-    return minApiLevel < AndroidApiLevel.Q.getLevel();
+    return minApiLevel.isLessThan(AndroidApiLevel.Q);
   }
 
   // The dalvik jit had a bug where the long operations add, sub, or, xor and and would write
   // the first part of the result long before reading the second part of the input longs.
   public boolean canHaveOverlappingLongRegisterBug() {
-    return isGeneratingDex() && minApiLevel < AndroidApiLevel.L.getLevel();
+    return isGeneratingDex() && minApiLevel.isLessThan(AndroidApiLevel.L);
   }
 
   // Some dalvik versions found in the wild perform invalid JIT compilation of cmp-long
@@ -1771,7 +1785,7 @@
   //
   // See b/75408029.
   public boolean canHaveCmpLongBug() {
-    return isGeneratingDex() && minApiLevel < AndroidApiLevel.L.getLevel();
+    return isGeneratingDex() && minApiLevel.isLessThan(AndroidApiLevel.L);
   }
 
   // Some Lollipop VMs crash if there is a const instruction between a cmp and an if instruction.
@@ -1799,7 +1813,7 @@
   //
   // See b/115552239.
   public boolean canHaveCmpIfFloatBug() {
-    return minApiLevel < AndroidApiLevel.M.getLevel();
+    return minApiLevel.isLessThan(AndroidApiLevel.M);
   }
 
   // Some Lollipop VMs incorrectly optimize code with mul2addr instructions. In particular,
@@ -1821,7 +1835,7 @@
   //
   // This issue has only been observed on a Verizon Ellipsis 8 tablet. See b/76115465.
   public boolean canHaveMul2AddrBug() {
-    return isGeneratingDex() && minApiLevel < AndroidApiLevel.M.getLevel();
+    return isGeneratingDex() && minApiLevel.isLessThan(AndroidApiLevel.M);
   }
 
   // Some Marshmallow VMs create an incorrect doubly-linked list of instructions. When the VM
@@ -1830,7 +1844,7 @@
   //
   // See b/77842465.
   public boolean canHaveDex2OatLinkedListBug() {
-    return isGeneratingDex() && minApiLevel < AndroidApiLevel.N.getLevel();
+    return isGeneratingDex() && minApiLevel.isLessThan(AndroidApiLevel.N);
   }
 
   // dex2oat on Marshmallow VMs does aggressive inlining which can eat up all the memory on
@@ -1838,7 +1852,7 @@
   //
   // See b/111960171
   public boolean canHaveDex2OatInliningIssue() {
-    return isGeneratingDex() && minApiLevel < AndroidApiLevel.N.getLevel();
+    return isGeneratingDex() && minApiLevel.isLessThan(AndroidApiLevel.N);
   }
 
   // Art 7.0.0 and later Art JIT may perform an invalid optimization if a string new-instance does
@@ -1846,7 +1860,7 @@
   //
   // See b/78493232 and b/80118070.
   public boolean canHaveArtStringNewInitBug() {
-    return isGeneratingDex() && minApiLevel < AndroidApiLevel.Q.getLevel();
+    return isGeneratingDex() && minApiLevel.isLessThan(AndroidApiLevel.Q);
   }
 
   // Dalvik tracing JIT may perform invalid optimizations when int/float values are converted to
@@ -1854,7 +1868,7 @@
   //
   // See b/77496850.
   public boolean canHaveNumberConversionRegisterAllocationBug() {
-    return isGeneratingDex() && minApiLevel < AndroidApiLevel.L.getLevel();
+    return isGeneratingDex() && minApiLevel.isLessThan(AndroidApiLevel.L);
   }
 
   // Some Lollipop mediatek VMs have a peculiar bug where the inliner crashes if there is a
@@ -1867,7 +1881,7 @@
   //
   // See b/68378480.
   public boolean canHaveForwardingInitInliningBug() {
-    return isGeneratingDex() && minApiLevel < AndroidApiLevel.M.getLevel();
+    return isGeneratingDex() && minApiLevel.isLessThan(AndroidApiLevel.M);
   }
 
   // Some Lollipop x86_64 VMs have a bug causing a segfault if an exception handler directly targets
@@ -1879,7 +1893,7 @@
   //
   // See b/111337896.
   public boolean canHaveExceptionTargetingLoopHeaderBug() {
-    return isGeneratingDex() && !debug && minApiLevel < AndroidApiLevel.M.getLevel();
+    return isGeneratingDex() && !debug && minApiLevel.isLessThan(AndroidApiLevel.M);
   }
 
   // The Dalvik tracing JIT can trace past the end of the instruction stream and end up
@@ -1894,7 +1908,7 @@
   // We also could not insert any dead code (e.g. a return) because that would make mediatek
   // dominator calculations on 7.0.0 crash. See b/128926846.
   public boolean canHaveTracingPastInstructionsStreamBug() {
-    return minApiLevel < AndroidApiLevel.L.getLevel();
+    return minApiLevel.isLessThan(AndroidApiLevel.L);
   }
 
   // The art verifier incorrectly propagates type information for the following pattern:
@@ -1922,7 +1936,7 @@
   // Fixed in Android Q, see b/120985556.
   public boolean canHaveArtInstanceOfVerifierBug() {
     assert isGeneratingDex();
-    return minApiLevel < AndroidApiLevel.Q.getLevel();
+    return minApiLevel.isLessThan(AndroidApiLevel.Q);
   }
 
   // Some Art Lollipop version do not deal correctly with long-to-int conversions.
@@ -1945,7 +1959,7 @@
   public boolean canHaveLongToIntBug() {
     // We have only seen this happening on Lollipop arm64 backends. We have tested on
     // Marshmallow and Nougat arm64 devices and they do not have the bug.
-    return minApiLevel < AndroidApiLevel.M.getLevel();
+    return minApiLevel.isLessThan(AndroidApiLevel.M);
   }
 
   // The Art VM for Android N through P has a bug in the JIT that means that if the same
@@ -1958,7 +1972,7 @@
   //
   // See b/120164595.
   public boolean canHaveExceptionTypeBug() {
-    return minApiLevel < AndroidApiLevel.Q.getLevel();
+    return minApiLevel.isLessThan(AndroidApiLevel.Q);
   }
 
   // Art 4.0.4 fails with a verification error when a null-literal is being passed directly to an
@@ -1966,7 +1980,7 @@
   // elimination of check-cast instructions where the value being cast is the constant null.
   // See b/123269162.
   public boolean canHaveArtCheckCastVerifierBug() {
-    return minApiLevel < AndroidApiLevel.J.getLevel();
+    return minApiLevel.isLessThan(AndroidApiLevel.J);
   }
 
   // The verifier will merge A[] and B[] to Object[], even when both A and B implement an interface
@@ -1990,7 +2004,7 @@
   //
   // See b/131349148
   public boolean canHaveDalvikCatchHandlerVerificationBug() {
-    return isGeneratingClassFiles() || minApiLevel < AndroidApiLevel.L.getLevel();
+    return isGeneratingClassFiles() || minApiLevel.isLessThan(AndroidApiLevel.L);
   }
 
   // Having an invoke instruction that targets an abstract method on a non-abstract class will fail
@@ -1998,7 +2012,7 @@
   //
   // See b/132953944.
   public boolean canHaveDalvikAbstractMethodOnNonAbstractClassVerificationBug() {
-    return isGeneratingDex() && minApiLevel < AndroidApiLevel.L.getLevel();
+    return isGeneratingDex() && minApiLevel.isLessThan(AndroidApiLevel.L);
   }
 
   // On dalvik we see issues when using an int value in places where a boolean, byte, char, or short
@@ -2012,14 +2026,14 @@
   //
   // See also b/134304597 and b/124152497.
   public boolean canHaveDalvikIntUsedAsNonIntPrimitiveTypeBug() {
-    return isGeneratingClassFiles() || minApiLevel < AndroidApiLevel.L.getLevel();
+    return isGeneratingClassFiles() || minApiLevel.isLessThan(AndroidApiLevel.L);
   }
 
   // The standard library prior to API 19 did not contain a ZipFile that implemented Closable.
   //
   // See b/177532008.
   public boolean canHaveZipFileWithMissingCloseableBug() {
-    return isGeneratingClassFiles() || minApiLevel < AndroidApiLevel.K.getLevel();
+    return isGeneratingClassFiles() || minApiLevel.isLessThan(AndroidApiLevel.K);
   }
 
   // Some versions of Dalvik had a bug where a switch with a MAX_INT key would still go to
@@ -2027,7 +2041,7 @@
   //
   // See b/177790310.
   public boolean canHaveSwitchMaxIntBug() {
-    return isGeneratingDex() && minApiLevel < AndroidApiLevel.K.getLevel();
+    return isGeneratingDex() && minApiLevel.isLessThan(AndroidApiLevel.K);
   }
 
   // On Dalvik the methods Integer.parseInt and Long.parseLong does not support strings with a '+'
@@ -2035,6 +2049,6 @@
   //
   // See b/182137865.
   public boolean canParseNumbersWithPlusPrefix() {
-    return minApiLevel > AndroidApiLevel.K.getLevel();
+    return minApiLevel.isGreaterThan(AndroidApiLevel.K);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/ListUtils.java b/src/main/java/com/android/tools/r8/utils/ListUtils.java
index 1c260e3..5781f91 100644
--- a/src/main/java/com/android/tools/r8/utils/ListUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ListUtils.java
@@ -7,6 +7,7 @@
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Comparator;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Optional;
@@ -147,6 +148,13 @@
     return list;
   }
 
+  public static <T> ArrayList<T> newArrayList(T element, T other) {
+    ArrayList<T> list = new ArrayList<>();
+    list.add(element);
+    list.add(other);
+    return list;
+  }
+
   public static <T> ArrayList<T> newArrayList(ForEachable<T> forEachable) {
     ArrayList<T> list = new ArrayList<>();
     forEachable.forEach(list::add);
@@ -209,4 +217,36 @@
   public interface ReferenceAndIntConsumer<T> {
     void accept(T item, int index);
   }
+
+  public static <T> void destructiveSort(List<T> items, Comparator<T> comparator) {
+    items.sort(comparator);
+  }
+
+  // Utility to add a slow verification of a comparator as part of sorting. Note that this
+  // should not generally be used in asserts unless the quadratic behavior can be tolerated.
+  public static <T> void destructiveSortAndVerify(List<T> items, Comparator<T> comparator) {
+    destructiveSort(items, comparator);
+    assert verifyComparatorOnSortedList(items, comparator);
+  }
+
+  private static <T> boolean verifyComparatorOnSortedList(List<T> items, Comparator<T> comparator) {
+    for (int i = 0; i < items.size(); i++) {
+      boolean allowEqual = true;
+      for (int j = i; j < items.size(); j++) {
+        T a = items.get(i);
+        T b = items.get(j);
+        int result1 = comparator.compare(a, b);
+        int result2 = comparator.compare(b, a);
+        boolean isEqual = result1 == 0 && result2 == 0;
+        if (i == j) {
+          assert isEqual;
+        } else if (!allowEqual || !isEqual) {
+          allowEqual = false;
+          assert result1 < 0;
+          assert result2 > 0;
+        }
+      }
+    }
+    return true;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/MapUtils.java b/src/main/java/com/android/tools/r8/utils/MapUtils.java
index 63e881e..fc39146 100644
--- a/src/main/java/com/android/tools/r8/utils/MapUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/MapUtils.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.utils;
 
 import com.android.tools.r8.utils.StringUtils.BraceType;
+import java.util.IdentityHashMap;
 import java.util.Map;
 import java.util.function.BiFunction;
 import java.util.function.Function;
@@ -50,6 +51,12 @@
     return result;
   }
 
+  public static <K, V> IdentityHashMap<K, V> newIdentityHashMap(BiForEachable<K, V> forEachable) {
+    IdentityHashMap<K, V> map = new IdentityHashMap<>();
+    forEachable.forEach(map::put);
+    return map;
+  }
+
   public static <T> void removeIdentityMappings(Map<T, T> map) {
     map.entrySet().removeIf(entry -> entry.getKey() == entry.getValue());
   }
diff --git a/src/main/java/com/android/tools/r8/utils/WorkList.java b/src/main/java/com/android/tools/r8/utils/WorkList.java
index 4279cb7..67e4148 100644
--- a/src/main/java/com/android/tools/r8/utils/WorkList.java
+++ b/src/main/java/com/android/tools/r8/utils/WorkList.java
@@ -48,6 +48,10 @@
     return workList;
   }
 
+  public static <T> WorkList<T> newWorkList(Set<T> seen) {
+    return new WorkList<>(seen);
+  }
+
   private WorkList(EqualityTest equalityTest) {
     this(equalityTest == EqualityTest.HASH ? new HashSet<>() : Sets.newIdentityHashSet());
   }
@@ -56,6 +60,10 @@
     this.seen = seen;
   }
 
+  public void addIgnoringSeenSet(T item) {
+    workingList.addLast(item);
+  }
+
   public void addAllIgnoringSeenSet(Iterable<T> items) {
     items.forEach(workingList::addLast);
   }
@@ -86,6 +94,10 @@
     return !hasNext();
   }
 
+  public boolean isSeen(T item) {
+    return seen.contains(item);
+  }
+
   public void markAsSeen(T item) {
     seen.add(item);
   }
@@ -103,6 +115,10 @@
     return Collections.unmodifiableSet(seen);
   }
 
+  public Set<T> getMutableSeenSet() {
+    return seen;
+  }
+
   public enum EqualityTest {
     HASH,
     IDENTITY
diff --git a/src/test/examples/classmerging/NoHorizontalClassMerging.java b/src/test/examples/classmerging/NoHorizontalClassMerging.java
new file mode 100644
index 0000000..3b7a831
--- /dev/null
+++ b/src/test/examples/classmerging/NoHorizontalClassMerging.java
@@ -0,0 +1,10 @@
+// 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 classmerging;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Target;
+
+@Target({ElementType.TYPE})
+public @interface NoHorizontalClassMerging {}
diff --git a/src/test/examples/classmerging/SimpleInterfaceAccessTest.java b/src/test/examples/classmerging/SimpleInterfaceAccessTest.java
index 6fde90e..804b4ed 100644
--- a/src/test/examples/classmerging/SimpleInterfaceAccessTest.java
+++ b/src/test/examples/classmerging/SimpleInterfaceAccessTest.java
@@ -32,12 +32,14 @@
   }
 
   // Should only be merged into OtherSimpleInterfaceImpl if access modifications are allowed.
+  @NoHorizontalClassMerging
   public interface SimpleInterface {
 
     void foo();
   }
 
   // Should only be merged into OtherSimpleInterfaceImpl if access modifications are allowed.
+  @NoHorizontalClassMerging
   public interface OtherSimpleInterface {
 
     void bar();
diff --git a/src/test/examples/classmerging/keep-rules.txt b/src/test/examples/classmerging/keep-rules.txt
index 64ce874..46fcb91 100644
--- a/src/test/examples/classmerging/keep-rules.txt
+++ b/src/test/examples/classmerging/keep-rules.txt
@@ -72,3 +72,4 @@
 -neverinline class * {
   @classmerging.NeverInline <methods>;
 }
+-nohorizontalclassmerging @classmerging.NoHorizontalClassMerging class *
diff --git a/src/test/java/com/android/tools/r8/AsmTestBase.java b/src/test/java/com/android/tools/r8/AsmTestBase.java
index a8b8a49..a3d5cf6 100644
--- a/src/test/java/com/android/tools/r8/AsmTestBase.java
+++ b/src/test/java/com/android/tools/r8/AsmTestBase.java
@@ -29,7 +29,7 @@
   protected void ensureSameOutput(String main, AndroidApiLevel apiLevel,
       List<String> args, byte[]... classes) throws Exception {
     AndroidApp app = buildAndroidApp(classes);
-    Consumer<InternalOptions> setMinApiLevel = o -> o.minApiLevel = apiLevel.getLevel();
+    Consumer<InternalOptions> setMinApiLevel = o -> o.minApiLevel = apiLevel;
     ProcessResult javaResult = runOnJavaRaw(main, Arrays.asList(classes), args);
     Consumer<ArtCommandBuilder> cmdBuilder = builder -> {
       for (String arg : args) {
diff --git a/src/test/java/com/android/tools/r8/DiagnosticsMatcher.java b/src/test/java/com/android/tools/r8/DiagnosticsMatcher.java
index a800801..6517f6e 100644
--- a/src/test/java/com/android/tools/r8/DiagnosticsMatcher.java
+++ b/src/test/java/com/android/tools/r8/DiagnosticsMatcher.java
@@ -100,6 +100,16 @@
     explain(description.appendText("a diagnostic "));
   }
 
+  @Override
+  protected void describeMismatchSafely(Diagnostic item, Description mismatchDescription) {
+    mismatchDescription
+        .appendText("was ")
+        .appendText(item.getClass().getName())
+        .appendText(" with message(")
+        .appendValue(item.getDiagnosticMessage())
+        .appendText(")");
+  }
+
   protected abstract boolean eval(Diagnostic diagnostic);
 
   protected abstract void explain(Description description);
diff --git a/src/test/java/com/android/tools/r8/R8CompatTestBuilder.java b/src/test/java/com/android/tools/r8/R8CompatTestBuilder.java
index 568aa74..8074250 100644
--- a/src/test/java/com/android/tools/r8/R8CompatTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8CompatTestBuilder.java
@@ -5,7 +5,6 @@
 
 import com.android.tools.r8.R8Command.Builder;
 import com.android.tools.r8.TestBase.Backend;
-import java.nio.file.Path;
 
 public class R8CompatTestBuilder extends R8TestBuilder<R8CompatTestBuilder> {
 
@@ -21,6 +20,11 @@
   }
 
   @Override
+  public boolean isR8CompatTestBuilder() {
+    return true;
+  }
+
+  @Override
   R8CompatTestBuilder self() {
     return this;
   }
diff --git a/src/test/java/com/android/tools/r8/R8FullTestBuilder.java b/src/test/java/com/android/tools/r8/R8FullTestBuilder.java
index 3343329..a8f6d60 100644
--- a/src/test/java/com/android/tools/r8/R8FullTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8FullTestBuilder.java
@@ -24,6 +24,11 @@
   }
 
   @Override
+  public boolean isR8TestBuilder() {
+    return true;
+  }
+
+  @Override
   R8FullTestBuilder self() {
     return this;
   }
diff --git a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
index 95198ec..bcd4fea 100644
--- a/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunArtTestsTest.java
@@ -191,6 +191,9 @@
   // TODO(zerny): Amend flaky tests with an expected flaky result to track issues.
   private static Multimap<String, TestCondition> flakyRunWithArt =
       new ImmutableListMultimap.Builder<String, TestCondition>()
+          // Sometime output also appends "Actually slept about X msec..."
+          // So far only seen on the 5.1.1 runtime.
+          .put("002-sleep", TestCondition.match(TestCondition.runtimes(DexVm.Version.V5_1_1)))
           // Can crash but mostly passes
           // Crashes:
           // check_reference_map_visitor.h:44] At Main.f
diff --git a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
index 488480c..608f635 100644
--- a/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
+++ b/src/test/java/com/android/tools/r8/R8RunExamplesAndroidOTest.java
@@ -108,7 +108,7 @@
         .withOptionConsumer(opts -> opts.enableClassInlining = false)
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS, Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 18, "lambdadesugaring"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 16, "lambdadesugaring"))
         .run();
 
     test("lambdadesugaring", "lambdadesugaring", "LambdaDesugaring")
@@ -147,7 +147,7 @@
         .withOptionConsumer(opts -> opts.enableClassInlining = false)
         .withBuilderTransformation(
             b -> b.addProguardConfiguration(PROGUARD_OPTIONS, Origin.unknown()))
-        .withDexCheck(inspector -> checkLambdaCount(inspector, 18, "lambdadesugaring"))
+        .withDexCheck(inspector -> checkLambdaCount(inspector, 16, "lambdadesugaring"))
         .run();
 
     test("lambdadesugaring", "lambdadesugaring", "LambdaDesugaring")
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index d78af9d..0e11c5b 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -5,7 +5,7 @@
 
 import static com.android.tools.r8.dexsplitter.SplitterTestBase.simpleSplitProvider;
 import static com.android.tools.r8.dexsplitter.SplitterTestBase.splitWithNonJavaFile;
-import static org.hamcrest.CoreMatchers.containsString;
+import static com.android.tools.r8.utils.codeinspector.Matchers.proguardConfigurationRuleDoesNotMatch;
 
 import com.android.tools.r8.R8Command.Builder;
 import com.android.tools.r8.TestBase.Backend;
@@ -139,8 +139,7 @@
       case NONE:
         if (allowUnusedProguardConfigurationRules) {
           compileResult
-              .assertAllInfoMessagesMatch(
-                  containsString("Proguard configuration rule does not match anything"))
+              .assertAllInfosMatch(proguardConfigurationRuleDoesNotMatch())
               .assertNoErrorMessages()
               .assertNoWarningMessages();
         } else {
@@ -154,11 +153,9 @@
         throw new Unreachable();
     }
     if (allowUnusedProguardConfigurationRules) {
-      compileResult.assertInfoMessageThatMatches(
-          containsString("Proguard configuration rule does not match anything"));
+      compileResult.assertInfoThatMatches(proguardConfigurationRuleDoesNotMatch());
     } else {
-      compileResult.assertNoInfoMessageThatMatches(
-          containsString("Proguard configuration rule does not match anything"));
+      compileResult.assertNoInfoThatMatches(proguardConfigurationRuleDoesNotMatch());
     }
     return compileResult;
   }
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index b24d16a..19c77fb 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -7,6 +7,7 @@
 import static com.android.tools.r8.TestBuilder.getTestingAnnotations;
 import static com.android.tools.r8.utils.InternalOptions.ASM_VERSION;
 import static com.google.common.collect.Lists.cartesianProduct;
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
@@ -1827,4 +1828,17 @@
         extractor, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
     return extractor.getClassFileVersion();
   }
+
+  public static void verifyAllInfoFromGenericSignatureTypeParameterValidation(
+      TestCompileResult<?, ?> compileResult) {
+    compileResult.assertAtLeastOneInfoMessage();
+    compileResult.assertAllInfoMessagesMatch(containsString("A type variable is not in scope"));
+  }
+
+  public static void verifyExpectedInfoFromGenericSignatureSuperTypeValidation(
+      TestCompileResult<?, ?> compileResult) {
+    compileResult.assertAtLeastOneInfoMessage();
+    compileResult.assertAllInfoMessagesMatch(
+        containsString("The generic super type is not the same as the class super type"));
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/TestCompileResult.java b/src/test/java/com/android/tools/r8/TestCompileResult.java
index 5788681..827684d 100644
--- a/src/test/java/com/android/tools/r8/TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/TestCompileResult.java
@@ -358,15 +358,30 @@
     return self();
   }
 
+  public CR assertInfoThatMatches(Matcher<Diagnostic> matcher) {
+    getDiagnosticMessages().assertInfoThatMatches(matcher);
+    return self();
+  }
+
   public CR assertInfoMessageThatMatches(Matcher<String> matcher) {
     getDiagnosticMessages().assertInfoThatMatches(diagnosticMessage(matcher));
     return self();
   }
 
+  public CR assertAllInfosMatch(Matcher<Diagnostic> matcher) {
+    getDiagnosticMessages().assertNoInfosMatch(not(matcher));
+    return self();
+  }
+
   public CR assertAllInfoMessagesMatch(Matcher<String> matcher) {
     return assertNoInfoMessageThatMatches(not(matcher));
   }
 
+  public CR assertNoInfoThatMatches(Matcher<Diagnostic> matcher) {
+    getDiagnosticMessages().assertNoInfosMatch(matcher);
+    return self();
+  }
+
   public CR assertNoInfoMessageThatMatches(Matcher<String> matcher) {
     getDiagnosticMessages().assertNoInfosMatch(diagnosticMessage(matcher));
     return self();
diff --git a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
index aad62cb..c421c83 100644
--- a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
@@ -44,6 +44,14 @@
     return false;
   }
 
+  public boolean isR8TestBuilder() {
+    return false;
+  }
+
+  public boolean isR8CompatTestBuilder() {
+    return false;
+  }
+
   @Override
   public boolean isTestShrinkerBuilder() {
     return true;
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 5a9dba5..4c765be 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -826,6 +826,7 @@
       case J_MR1:
       case J_MR2:
       case K_WATCH:
+      case UNKNOWN:
         return false;
       default:
         return true;
diff --git a/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelInterfaceTest.java b/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelInterfaceTest.java
new file mode 100644
index 0000000..4e5c39d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelInterfaceTest.java
@@ -0,0 +1,90 @@
+// 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.apimodeling;
+
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.setMockApiLevelForMethod;
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.verifyThat;
+
+import com.android.tools.r8.NeverInline;
+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.AndroidApiLevel;
+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 ApiModelNoInliningOfHigherApiLevelInterfaceTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevels().build();
+  }
+
+  public ApiModelNoInliningOfHigherApiLevelInterfaceTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    Method apiMethod = Api.class.getDeclaredMethod("apiLevel22");
+    Method apiCaller = ApiCaller.class.getDeclaredMethod("callInterfaceMethod", Api.class);
+    Method apiCallerCaller = A.class.getDeclaredMethod("noApiCall");
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class, A.class, ApiCaller.class)
+        .addLibraryClasses(Api.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .apply(setMockApiLevelForMethod(apiMethod, AndroidApiLevel.L_MR1))
+        .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A::noApiCall", "ApiCaller::callInterfaceMethod")
+        .inspect(
+            verifyThat(parameters, apiCaller)
+                .inlinedIntoFromApiLevel(apiCallerCaller, AndroidApiLevel.L_MR1));
+  }
+
+  public interface Api {
+
+    void apiLevel22();
+  }
+
+  @NoHorizontalClassMerging
+  public static class ApiCaller {
+
+    public static void callInterfaceMethod(Api api) {
+      System.out.println("ApiCaller::callInterfaceMethod");
+      if (api != null) {
+        api.apiLevel22();
+      }
+    }
+  }
+
+  @NoHorizontalClassMerging
+  public static class A {
+
+    @NeverInline
+    public static void noApiCall() {
+      System.out.println("A::noApiCall");
+      ApiCaller.callInterfaceMethod(null);
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      A.noApiCall();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelIntoLowerDirectTest.java b/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelIntoLowerDirectTest.java
new file mode 100644
index 0000000..048874d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelIntoLowerDirectTest.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.apimodeling;
+
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.setMockApiLevelForMethod;
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.verifyThat;
+
+import com.android.tools.r8.NeverInline;
+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.AndroidApiLevel;
+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 ApiModelNoInliningOfHigherApiLevelIntoLowerDirectTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ApiModelNoInliningOfHigherApiLevelIntoLowerDirectTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test()
+  public void testR8() throws Exception {
+    Method apiLevel21 = A.class.getDeclaredMethod("apiLevel21");
+    Method apiLevel22 = B.class.getDeclaredMethod("apiLevel22");
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .apply(setMockApiLevelForMethod(apiLevel21, AndroidApiLevel.L))
+        .apply(setMockApiLevelForMethod(apiLevel22, AndroidApiLevel.L_MR1))
+        .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A::apiLevel21", "B::apiLevel22")
+        .inspect(verifyThat(parameters, apiLevel22).inlinedInto(apiLevel21));
+  }
+
+  // This tests that program classes where we directly mock the methods to have an api level will
+  // be inlined.
+  @NoHorizontalClassMerging
+  public static class B {
+    public static void apiLevel22() {
+      System.out.println("B::apiLevel22");
+    }
+  }
+
+  @NoHorizontalClassMerging
+  public static class A {
+
+    @NeverInline
+    public static void apiLevel21() {
+      System.out.println("A::apiLevel21");
+      B.apiLevel22();
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      A.apiLevel21();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelStaticTest.java b/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelStaticTest.java
new file mode 100644
index 0000000..dfb8628
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelStaticTest.java
@@ -0,0 +1,92 @@
+// 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.apimodeling;
+
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.setMockApiLevelForMethod;
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.verifyThat;
+
+import com.android.tools.r8.NeverInline;
+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.AndroidApiLevel;
+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 ApiModelNoInliningOfHigherApiLevelStaticTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ApiModelNoInliningOfHigherApiLevelStaticTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    Method apiMethod = Api.class.getDeclaredMethod("apiLevel22");
+    Method apiCaller = ApiCaller.class.getDeclaredMethod("callStaticMethod");
+    Method apiCallerCaller = A.class.getDeclaredMethod("noApiCall");
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class, A.class, ApiCaller.class)
+        .addLibraryClasses(Api.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .apply(setMockApiLevelForMethod(apiMethod, AndroidApiLevel.L_MR1))
+        .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+        .compile()
+        .inspect(
+            verifyThat(parameters, apiCaller)
+                .inlinedIntoFromApiLevel(apiCallerCaller, AndroidApiLevel.L_MR1))
+        .addRunClasspathClasses(Api.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(
+            "A::noApiCall", "ApiCaller::callStaticMethod", "Api::apiLevel22");
+  }
+
+  public static class Api {
+
+    public static void apiLevel22() {
+      System.out.println("Api::apiLevel22");
+    }
+  }
+
+  @NoHorizontalClassMerging
+  public static class ApiCaller {
+    public static void callStaticMethod() {
+      System.out.println("ApiCaller::callStaticMethod");
+      Api.apiLevel22();
+    }
+  }
+
+  @NoHorizontalClassMerging
+  public static class A {
+
+    @NeverInline
+    public static void noApiCall() {
+      System.out.println("A::noApiCall");
+      ApiCaller.callStaticMethod();
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      A.noApiCall();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelSuperTest.java b/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelSuperTest.java
new file mode 100644
index 0000000..415a163
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelSuperTest.java
@@ -0,0 +1,97 @@
+// 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.apimodeling;
+
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.setMockApiLevelForMethod;
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.verifyThat;
+
+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.utils.AndroidApiLevel;
+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 ApiModelNoInliningOfHigherApiLevelSuperTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  }
+
+  public ApiModelNoInliningOfHigherApiLevelSuperTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    Method apiMethod = Api.class.getDeclaredMethod("apiLevel22");
+    Method apiCaller = ApiCaller.class.getDeclaredMethod("apiLevel22");
+    Method apiCallerCaller = A.class.getDeclaredMethod("noApiCall");
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .apply(setMockApiLevelForMethod(apiMethod, AndroidApiLevel.L_MR1))
+        .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+        .compile()
+        .inspect(
+            verifyThat(parameters, apiCaller)
+                .inlinedIntoFromApiLevel(apiCallerCaller, AndroidApiLevel.L_MR1))
+        .addRunClasspathClasses(Api.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A::noApiCall", "ApiCaller::apiLevel22", "Api::apiLevel22");
+  }
+
+  @NoVerticalClassMerging
+  public static class Api {
+
+    void apiLevel22() {
+      System.out.println("Api::apiLevel22");
+    }
+  }
+
+  @NeverClassInline
+  public static class ApiCaller extends Api {
+
+    @Override
+    void apiLevel22() {
+      System.out.println("ApiCaller::apiLevel22");
+      super.apiLevel22();
+    }
+  }
+
+  @NoHorizontalClassMerging
+  public static class A {
+
+    @NeverInline
+    public static void noApiCall() {
+      System.out.println("A::noApiCall");
+      new ApiCaller().apiLevel22();
+    }
+  }
+
+  @NoHorizontalClassMerging
+  public static class Main {
+
+    public static void main(String[] args) {
+      A.noApiCall();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelVirtualTest.java b/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelVirtualTest.java
new file mode 100644
index 0000000..31af9e3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apimodeling/ApiModelNoInliningOfHigherApiLevelVirtualTest.java
@@ -0,0 +1,94 @@
+// 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.apimodeling;
+
+import static com.android.tools.r8.apimodeling.ApiModelNoInliningOfHigherApiLevelVirtualTest.ApiCaller.callVirtualMethod;
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.setMockApiLevelForMethod;
+import static com.android.tools.r8.apimodeling.ApiModelingTestHelper.verifyThat;
+
+import com.android.tools.r8.NeverInline;
+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.AndroidApiLevel;
+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 ApiModelNoInliningOfHigherApiLevelVirtualTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  }
+
+  public ApiModelNoInliningOfHigherApiLevelVirtualTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    Method apiMethod = Api.class.getDeclaredMethod("apiLevel22");
+    Method apiCaller = ApiCaller.class.getDeclaredMethod("callVirtualMethod");
+    Method apiCallerCaller = A.class.getDeclaredMethod("noApiCall");
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class, A.class, ApiCaller.class)
+        .addLibraryClasses(Api.class)
+        .addDefaultRuntimeLibrary(parameters)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .apply(setMockApiLevelForMethod(apiMethod, AndroidApiLevel.L_MR1))
+        .apply(ApiModelingTestHelper::enableApiCallerIdentification)
+        .compile()
+        .inspect(
+            verifyThat(parameters, apiCaller)
+                .inlinedIntoFromApiLevel(apiCallerCaller, AndroidApiLevel.L_MR1))
+        .addRunClasspathClasses(Api.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(
+            "A::noApiCall", "ApiCaller::callVirtualMethod", "Api::apiLevel22");
+  }
+
+  public static class Api {
+
+    public void apiLevel22() {
+      System.out.println("Api::apiLevel22");
+    }
+  }
+
+  @NoHorizontalClassMerging
+  public static class ApiCaller {
+
+    public static void callVirtualMethod() {
+      System.out.println("ApiCaller::callVirtualMethod");
+      new Api().apiLevel22();
+    }
+  }
+
+  @NoHorizontalClassMerging
+  public static class A {
+
+    @NeverInline
+    public static void noApiCall() {
+      System.out.println("A::noApiCall");
+      callVirtualMethod();
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      A.noApiCall();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apioutlining/ApiOutliningTestHelper.java b/src/test/java/com/android/tools/r8/apimodeling/ApiModelingTestHelper.java
similarity index 69%
rename from src/test/java/com/android/tools/r8/apioutlining/ApiOutliningTestHelper.java
rename to src/test/java/com/android/tools/r8/apimodeling/ApiModelingTestHelper.java
index f10a6e9..5b1a2f7 100644
--- a/src/test/java/com/android/tools/r8/apioutlining/ApiOutliningTestHelper.java
+++ b/src/test/java/com/android/tools/r8/apimodeling/ApiModelingTestHelper.java
@@ -2,7 +2,7 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-package com.android.tools.r8.apioutlining;
+package com.android.tools.r8.apimodeling;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.not;
@@ -19,36 +19,45 @@
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.lang.reflect.Method;
 
-public abstract class ApiOutliningTestHelper {
+public abstract class ApiModelingTestHelper {
 
   static <T extends TestCompilerBuilder<?, ?, ?, ?, ?>>
       ThrowableConsumer<T> setMockApiLevelForMethod(Method method, AndroidApiLevel apiLevel) {
     return compilerBuilder -> {
       compilerBuilder.addOptionsModification(
           options -> {
-            options.methodApiMapping.put(Reference.methodFromMethod(method), apiLevel);
+            options
+                .apiModelingOptions()
+                .methodApiMapping
+                .put(Reference.methodFromMethod(method), apiLevel);
           });
     };
   }
 
-  static ApiOutliningMethodVerificationHelper verifyThat(TestParameters parameters, Method method) {
-    return new ApiOutliningMethodVerificationHelper(parameters, method);
+  static void enableApiCallerIdentification(TestCompilerBuilder<?, ?, ?, ?, ?> compilerBuilder) {
+    compilerBuilder.addOptionsModification(
+        options -> {
+          options.apiModelingOptions().enableApiCallerIdentification = true;
+        });
   }
 
-  public static class ApiOutliningMethodVerificationHelper {
+  static ApiModelingMethodVerificationHelper verifyThat(TestParameters parameters, Method method) {
+    return new ApiModelingMethodVerificationHelper(parameters, method);
+  }
+
+  public static class ApiModelingMethodVerificationHelper {
 
     private final Method methodOfInterest;
     private final TestParameters parameters;
 
-    public ApiOutliningMethodVerificationHelper(
-        TestParameters parameters, Method methodOfInterest) {
+    public ApiModelingMethodVerificationHelper(TestParameters parameters, Method methodOfInterest) {
       this.methodOfInterest = methodOfInterest;
       this.parameters = parameters;
     }
 
     protected ThrowingConsumer<CodeInspector, Exception> inlinedIntoFromApiLevel(
         Method method, AndroidApiLevel apiLevel) {
-      return parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevel)
+      return parameters.isDexRuntime() && parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevel)
           ? inlinedInto(method)
           : notInlinedInto(method);
     }
@@ -63,7 +72,7 @@
       };
     }
 
-    private ThrowingConsumer<CodeInspector, Exception> inlinedInto(Method method) {
+    public ThrowingConsumer<CodeInspector, Exception> inlinedInto(Method method) {
       return inspector -> {
         MethodSubject candidate = inspector.method(methodOfInterest);
         if (!candidate.isPresent()) {
diff --git a/src/test/java/com/android/tools/r8/apioutlining/ApiOutliningNoInliningOfHigherApiLevelIntoLowerTest.java b/src/test/java/com/android/tools/r8/apioutlining/ApiOutliningNoInliningOfHigherApiLevelIntoLowerTest.java
deleted file mode 100644
index 7620ae4..0000000
--- a/src/test/java/com/android/tools/r8/apioutlining/ApiOutliningNoInliningOfHigherApiLevelIntoLowerTest.java
+++ /dev/null
@@ -1,87 +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.apioutlining;
-
-import static com.android.tools.r8.apioutlining.ApiOutliningTestHelper.setMockApiLevelForMethod;
-import static com.android.tools.r8.apioutlining.ApiOutliningTestHelper.verifyThat;
-import static org.junit.Assert.assertThrows;
-
-import com.android.tools.r8.NeverInline;
-import com.android.tools.r8.R8TestRunResult;
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.utils.AndroidApiLevel;
-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 ApiOutliningNoInliningOfHigherApiLevelIntoLowerTest extends TestBase {
-
-  private final TestParameters parameters;
-
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
-  }
-
-  public ApiOutliningNoInliningOfHigherApiLevelIntoLowerTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
-  @Test()
-  public void testR8() throws Exception {
-    Method apiLevel21 = A.class.getDeclaredMethod("apiLevel21");
-    Method apiLevel22 = B.class.getDeclaredMethod("apiLevel22");
-    R8TestRunResult runResult =
-        testForR8(parameters.getBackend())
-            .addInnerClasses(getClass())
-            .setMinApi(parameters.getApiLevel())
-            .addKeepMainRule(Main.class)
-            .enableInliningAnnotations()
-            .apply(setMockApiLevelForMethod(apiLevel21, AndroidApiLevel.L))
-            .apply(setMockApiLevelForMethod(apiLevel22, AndroidApiLevel.L_MR1))
-            .run(parameters.getRuntime(), Main.class)
-            .assertSuccessWithOutputLines("A::apiLevel21", "B::apiLevel22");
-    if (parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.L_MR1)) {
-      runResult.inspect(
-          verifyThat(parameters, apiLevel22)
-              .inlinedIntoFromApiLevel(apiLevel21, AndroidApiLevel.L_MR1));
-    } else {
-      // TODO(b/188388130): Should only inline on minApi >= 22.
-      assertThrows(
-          AssertionError.class,
-          () ->
-              runResult.inspect(
-                  verifyThat(parameters, apiLevel22)
-                      .inlinedIntoFromApiLevel(apiLevel21, AndroidApiLevel.L_MR1)));
-    }
-  }
-
-  public static class B {
-    public static void apiLevel22() {
-      System.out.println("B::apiLevel22");
-    }
-  }
-
-  public static class A {
-
-    @NeverInline
-    public static void apiLevel21() {
-      System.out.println("A::apiLevel21");
-      B.apiLevel22();
-    }
-  }
-
-  public static class Main {
-
-    public static void main(String[] args) {
-      A.apiLevel21();
-    }
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/apioutlining/ApiOutliningNoInliningOfHigherApiLevelTest.java b/src/test/java/com/android/tools/r8/apioutlining/ApiOutliningNoInliningOfHigherApiLevelTest.java
deleted file mode 100644
index 44263ba..0000000
--- a/src/test/java/com/android/tools/r8/apioutlining/ApiOutliningNoInliningOfHigherApiLevelTest.java
+++ /dev/null
@@ -1,86 +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.apioutlining;
-
-import static com.android.tools.r8.apioutlining.ApiOutliningTestHelper.setMockApiLevelForMethod;
-import static com.android.tools.r8.apioutlining.ApiOutliningTestHelper.verifyThat;
-import static org.junit.Assert.assertThrows;
-
-import com.android.tools.r8.NeverInline;
-import com.android.tools.r8.R8TestRunResult;
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.utils.AndroidApiLevel;
-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 ApiOutliningNoInliningOfHigherApiLevelTest extends TestBase {
-
-  private final TestParameters parameters;
-
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
-  }
-
-  public ApiOutliningNoInliningOfHigherApiLevelTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
-  @Test
-  public void testR8() throws Exception {
-    Method minApi = A.class.getDeclaredMethod("minApi");
-    Method apiLevel22 = B.class.getDeclaredMethod("apiLevel22");
-    R8TestRunResult runResult =
-        testForR8(parameters.getBackend())
-            .addInnerClasses(getClass())
-            .setMinApi(parameters.getApiLevel())
-            .addKeepMainRule(Main.class)
-            .enableInliningAnnotations()
-            .apply(setMockApiLevelForMethod(apiLevel22, AndroidApiLevel.L_MR1))
-            .run(parameters.getRuntime(), Main.class)
-            .assertSuccessWithOutputLines("A::minApi", "B::apiLevel22");
-    if (parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.L_MR1)) {
-      runResult.inspect(
-          verifyThat(parameters, apiLevel22)
-              .inlinedIntoFromApiLevel(minApi, AndroidApiLevel.L_MR1));
-    } else {
-      // TODO(b/188388130): Should only inline on minApi >= 22.
-      assertThrows(
-          AssertionError.class,
-          () ->
-              runResult.inspect(
-                  verifyThat(parameters, apiLevel22)
-                      .inlinedIntoFromApiLevel(minApi, AndroidApiLevel.L_MR1)));
-    }
-  }
-
-  public static class B {
-    public static void apiLevel22() {
-      System.out.println("B::apiLevel22");
-    }
-  }
-
-  public static class A {
-
-    @NeverInline
-    public static void minApi() {
-      System.out.println("A::minApi");
-      B.apiLevel22();
-    }
-  }
-
-  public static class Main {
-
-    public static void main(String[] args) {
-      A.minApi();
-    }
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/B77836766.java b/src/test/java/com/android/tools/r8/bridgeremoval/B77836766.java
index 76ebd58..07ad8f0 100644
--- a/src/test/java/com/android/tools/r8/bridgeremoval/B77836766.java
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/B77836766.java
@@ -9,24 +9,35 @@
 import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.code.InvokeVirtual;
 import com.android.tools.r8.code.ReturnVoid;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.jasmin.JasminBuilder;
 import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder;
-import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.google.common.collect.ImmutableList;
-import java.nio.file.Path;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
 
+@RunWith(Parameterized.class)
 public class B77836766 extends TestBase {
 
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public B77836766(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
   /**
    * The below Jasmin code mimics the following Kotlin code:
    *
@@ -93,15 +104,18 @@
     ClassBuilder itf2 = jasminBuilder.addInterface("Itf2");
     itf2.addAbstractMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V");
 
-    ClassBuilder cls2 = jasminBuilder.addClass("Cls2", absCls.name, itf2.name);
+    ClassBuilder cls2Class = jasminBuilder.addClass("Cls2", absCls.name, itf2.name);
     // Mimic Kotlin's "internal" class
-    cls2.setAccess("");
-    cls2.addBridgeMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V",
+    cls2Class.setAccess("");
+    cls2Class.addBridgeMethod(
+        "foo",
+        ImmutableList.of("Ljava/lang/Integer;"),
+        "V",
         ".limit stack 2",
         ".limit locals 2",
         "aload_0",
         "aload_1",
-        "invokevirtual " + cls2.name + "/foo(Ljava/lang/Object;)V",
+        "invokevirtual " + cls2Class.name + "/foo(Ljava/lang/Object;)V",
         "return");
 
     ClassBuilder mainClass = jasminBuilder.addClass("Main");
@@ -115,54 +129,66 @@
         "aload_0",
         "ldc \"Hello\"",
         "invokevirtual " + cls1.name + "/foo(Ljava/lang/String;)V",
-        "new " + cls2.name,
+        "new " + cls2Class.name,
         "dup",
-        "invokespecial " + cls2.name + "/<init>()V",
+        "invokespecial " + cls2Class.name + "/<init>()V",
         "astore_0",
         "aload_0",
         "iconst_0",
         "invokestatic java/lang/Integer/valueOf(I)Ljava/lang/Integer;",
-        "invokevirtual " + cls2.name + "/foo(Ljava/lang/Integer;)V",
-        "return"
-    );
+        "invokevirtual " + cls2Class.name + "/foo(Ljava/lang/Integer;)V",
+        "return");
 
-    final String mainClassName = mainClass.name;
-    String proguardConfig = keepMainProguardConfiguration(mainClass.name, false, false);
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(jasminBuilder.buildClasses())
+        .addKeepMainRule(mainClass.name)
+        .addOptionsModification(this::configure)
+        .noHorizontalClassMerging(cls2Class.name)
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject absSubject = inspector.clazz(absCls.name);
+              assertThat(absSubject, isPresent());
+              ClassSubject cls1Subject = inspector.clazz(cls1.name);
+              assertThat(cls1Subject, isPresent());
+              ClassSubject cls2Subject = inspector.clazz(cls2Class.name);
+              assertThat(cls2Subject, isPresent());
 
-    AndroidApp processedApp = runAndVerifyOnJvmAndArt(jasminBuilder, mainClassName, proguardConfig);
+              // Cls1#foo and Cls2#foo should not refer to each other.
+              // They can invoke their own bridge method or AbsCls#foo (via member rebinding).
 
-    CodeInspector inspector = new CodeInspector(processedApp);
-    ClassSubject absSubject = inspector.clazz(absCls.name);
-    assertThat(absSubject, isPresent());
-    ClassSubject cls1Subject = inspector.clazz(cls1.name);
-    assertThat(cls1Subject, isPresent());
-    ClassSubject cls2Subject = inspector.clazz(cls2.name);
-    assertThat(cls2Subject, isPresent());
+              // Cls2#foo has been moved to AbsCls#foo as a result of bridge hoisting.
+              MethodSubject fooInCls2 = cls2Subject.method("void", "foo", "java.lang.Integer");
+              assertThat(fooInCls2, not(isPresent()));
 
-    // Cls1#foo and Cls2#foo should not refer to each other.
-    // They can invoke their own bridge method or AbsCls#foo (via member rebinding).
+              MethodSubject fooFromCls2InAbsCls =
+                  absSubject.method("void", "foo", "java.lang.Integer");
+              assertThat(fooFromCls2InAbsCls, isPresent());
 
-    // Cls2#foo has been moved to AbsCls#foo as a result of bridge hoisting.
-    MethodSubject fooInCls2 = cls2Subject.method("void", "foo", "java.lang.Integer");
-    assertThat(fooInCls2, not(isPresent()));
+              // Cls1#foo has been moved to AbsCls#foo as a result of bridge hoisting.
+              MethodSubject fooInCls1 = cls1Subject.method("void", "foo", "java.lang.String");
+              assertThat(fooInCls1, not(isPresent()));
 
-    MethodSubject fooFromCls2InAbsCls = absSubject.method("void", "foo", "java.lang.Integer");
-    assertThat(fooFromCls2InAbsCls, isPresent());
-    DexCode code = fooFromCls2InAbsCls.getMethod().getCode().asDexCode();
-    checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
-    InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
-    assertEquals(absSubject.getDexProgramClass().type, invoke.getMethod().holder);
+              MethodSubject fooFromCls1InAbsCls =
+                  absSubject.method("void", "foo", "java.lang.String");
+              assertThat(fooFromCls1InAbsCls, isPresent());
 
-    // Cls1#foo has been moved to AbsCls#foo as a result of bridge hoisting.
-    MethodSubject fooInCls1 = cls1Subject.method("void", "foo", "java.lang.String");
-    assertThat(fooInCls1, not(isPresent()));
+              if (parameters.isDexRuntime()) {
+                DexCode code = fooFromCls2InAbsCls.getMethod().getCode().asDexCode();
+                checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
+                InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
+                assertEquals(absSubject.getDexProgramClass().type, invoke.getMethod().holder);
 
-    MethodSubject fooFromCls1InAbsCls = absSubject.method("void", "foo", "java.lang.String");
-    assertThat(fooFromCls1InAbsCls, isPresent());
-    code = fooFromCls1InAbsCls.getMethod().getCode().asDexCode();
-    checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
-    invoke = (InvokeVirtual) code.instructions[0];
-    assertEquals(absSubject.getDexProgramClass().type, invoke.getMethod().holder);
+                code = fooFromCls1InAbsCls.getMethod().getCode().asDexCode();
+                checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
+                invoke = (InvokeVirtual) code.instructions[0];
+                assertEquals(absSubject.getDexProgramClass().type, invoke.getMethod().holder);
+              }
+            })
+        .run(parameters.getRuntime(), mainClass.name)
+        .assertSuccessWithOutput("Hello0");
   }
 
   /**
@@ -199,13 +225,17 @@
     ClassBuilder itf = jasminBuilder.addInterface("ItfInteger");
     itf.addAbstractMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V");
 
-    ClassBuilder cls1 = jasminBuilder.addClass("DerivedInteger", baseCls.name, itf.name);
-    cls1.addBridgeMethod("foo", ImmutableList.of("Ljava/lang/Integer;"), "V",
+    ClassBuilder derivedIntegerClass =
+        jasminBuilder.addClass("DerivedInteger", baseCls.name, itf.name);
+    derivedIntegerClass.addBridgeMethod(
+        "foo",
+        ImmutableList.of("Ljava/lang/Integer;"),
+        "V",
         ".limit stack 2",
         ".limit locals 2",
         "aload_0",
         "aload_1",
-        "invokevirtual " + cls1.name + "/foo(Ljava/lang/Object;)V",
+        "invokevirtual " + derivedIntegerClass.name + "/foo(Ljava/lang/Object;)V",
         "return");
 
     ClassBuilder cls2 = jasminBuilder.addClass("DerivedString", baseCls.name);
@@ -221,14 +251,14 @@
     mainClass.addMainMethod(
         ".limit stack 5",
         ".limit locals 2",
-        "new " + cls1.name,
+        "new " + derivedIntegerClass.name,
         "dup",
-        "invokespecial " + cls1.name + "/<init>()V",
+        "invokespecial " + derivedIntegerClass.name + "/<init>()V",
         "astore_0",
         "aload_0",
         "iconst_0",
         "invokestatic java/lang/Integer/valueOf(I)Ljava/lang/Integer;",
-        "invokevirtual " + cls1.name + "/foo(Ljava/lang/Integer;)V",
+        "invokevirtual " + derivedIntegerClass.name + "/foo(Ljava/lang/Integer;)V",
         "new " + cls2.name,
         "dup",
         "invokespecial " + cls2.name + "/<init>()V",
@@ -236,41 +266,51 @@
         "aload_0",
         "ldc \"Bar\"",
         "invokevirtual " + cls2.name + "/bar(Ljava/lang/String;)V",
-        "return"
-    );
+        "return");
 
-    final String mainClassName = mainClass.name;
-    String proguardConfig = keepMainProguardConfiguration(mainClass.name, false, false);
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(jasminBuilder.buildClasses())
+        .addKeepMainRule(mainClass.name)
+        .addOptionsModification(this::configure)
+        .noHorizontalClassMerging(derivedIntegerClass.name)
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject baseSubject = inspector.clazz(baseCls.name);
+              assertThat(baseSubject, isPresent());
+              ClassSubject cls1Subject = inspector.clazz(derivedIntegerClass.name);
+              assertThat(cls1Subject, isPresent());
+              ClassSubject cls2Subject = inspector.clazz(cls2.name);
+              assertThat(cls2Subject, isPresent());
 
-    AndroidApp processedApp = runAndVerifyOnJvmAndArt(jasminBuilder, mainClassName, proguardConfig);
+              // Cls1#foo and Cls2#bar should refer to Base#foo.
 
-    CodeInspector inspector = new CodeInspector(processedApp);
-    ClassSubject baseSubject = inspector.clazz(baseCls.name);
-    assertThat(baseSubject, isPresent());
-    ClassSubject cls1Subject = inspector.clazz(cls1.name);
-    assertThat(cls1Subject, isPresent());
-    ClassSubject cls2Subject = inspector.clazz(cls2.name);
-    assertThat(cls2Subject, isPresent());
+              MethodSubject barInCls2 = cls2Subject.method("void", "bar", "java.lang.String");
+              assertThat(barInCls2, isPresent());
 
-    // Cls1#foo and Cls2#bar should refer to Base#foo.
+              // Cls1#foo has been moved to Base#foo as a result of bridge hoisting.
+              MethodSubject fooInCls1 = cls1Subject.method("void", "foo", "java.lang.Integer");
+              assertThat(fooInCls1, not(isPresent()));
 
-    MethodSubject barInCls2 = cls2Subject.method("void", "bar", "java.lang.String");
-    assertThat(barInCls2, isPresent());
-    DexCode code = barInCls2.getMethod().getCode().asDexCode();
-    checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
-    InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
-    assertEquals(baseSubject.getDexProgramClass().type, invoke.getMethod().holder);
+              MethodSubject fooInBase = baseSubject.method("void", "foo", "java.lang.Integer");
+              assertThat(fooInBase, isPresent());
 
-    // Cls1#foo has been moved to Base#foo as a result of bridge hoisting.
-    MethodSubject fooInCls1 = cls1Subject.method("void", "foo", "java.lang.Integer");
-    assertThat(fooInCls1, not(isPresent()));
+              if (parameters.isDexRuntime()) {
+                DexCode code = barInCls2.getMethod().getCode().asDexCode();
+                checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
+                InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
+                assertEquals(baseSubject.getDexProgramClass().type, invoke.getMethod().holder);
 
-    MethodSubject fooInBase = baseSubject.method("void", "foo", "java.lang.Integer");
-    assertThat(fooInBase, isPresent());
-    code = fooInBase.getMethod().getCode().asDexCode();
-    checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
-    invoke = (InvokeVirtual) code.instructions[0];
-    assertEquals(baseSubject.getDexProgramClass().type, invoke.getMethod().holder);
+                code = fooInBase.getMethod().getCode().asDexCode();
+                checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
+                invoke = (InvokeVirtual) code.instructions[0];
+                assertEquals(baseSubject.getDexProgramClass().type, invoke.getMethod().holder);
+              }
+            })
+        .run(parameters.getRuntime(), mainClass.name)
+        .assertSuccessWithOutput("0Bar");
   }
 
   /**
@@ -337,25 +377,34 @@
         "return"
     );
 
-    final String mainClassName = mainClass.name;
-    String proguardConfig = keepMainProguardConfiguration(mainClass.name, false, false);
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(jasminBuilder.buildClasses())
+        .addKeepMainRule(mainClass.name)
+        .addOptionsModification(this::configure)
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject baseSubject = inspector.clazz(baseCls.name);
+              assertThat(baseSubject, isPresent());
+              ClassSubject subSubject = inspector.clazz(subCls.name);
+              assertThat(subSubject, isPresent());
 
-    AndroidApp processedApp = runAndVerifyOnJvmAndArt(jasminBuilder, mainClassName, proguardConfig);
+              // DerivedString2#bar should refer to Base#foo.
 
-    CodeInspector inspector = new CodeInspector(processedApp);
-    ClassSubject baseSubject = inspector.clazz(baseCls.name);
-    assertThat(baseSubject, isPresent());
-    ClassSubject subSubject = inspector.clazz(subCls.name);
-    assertThat(subSubject, isPresent());
+              MethodSubject barInSub = subSubject.method("void", "bar", "java.lang.String");
+              assertThat(barInSub, isPresent());
 
-    // DerivedString2#bar should refer to Base#foo.
-
-    MethodSubject barInSub = subSubject.method("void", "bar", "java.lang.String");
-    assertThat(barInSub, isPresent());
-    DexCode code = barInSub.getMethod().getCode().asDexCode();
-    checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
-    InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
-    assertEquals(baseSubject.getDexProgramClass().type, invoke.getMethod().holder);
+              if (parameters.isDexRuntime()) {
+                DexCode code = barInSub.getMethod().getCode().asDexCode();
+                checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
+                InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
+                assertEquals(baseSubject.getDexProgramClass().type, invoke.getMethod().holder);
+              }
+            })
+        .run(parameters.getRuntime(), mainClass.name)
+        .assertSuccessWithOutput("0Bar");
   }
 
   /*
@@ -413,40 +462,33 @@
         "invokevirtual " + cls.name + "/bar(Ljava/lang/String;)V",
         "return"
     );
-    final String mainClassName = mainClass.name;
-    String proguardConfig = keepMainProguardConfiguration(mainClass.name, false, false);
-    AndroidApp processedApp = runAndVerifyOnJvmAndArt(jasminBuilder, mainClassName, proguardConfig);
 
-    CodeInspector inspector = new CodeInspector(processedApp);
-    ClassSubject baseSubject = inspector.clazz(cls.name);
-    assertThat(baseSubject, isPresent());
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(jasminBuilder.buildClasses())
+        .addKeepMainRule(mainClass.name)
+        .addOptionsModification(this::configure)
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject baseSubject = inspector.clazz(cls.name);
+              assertThat(baseSubject, isPresent());
 
-    // Base#bar should remain as-is, i.e., refer to Base#foo(Object).
+              // Base#bar should remain as-is, i.e., refer to Base#foo(Object).
 
-    MethodSubject barInSub = baseSubject.method("void", "bar", "java.lang.String");
-    assertThat(barInSub, isPresent());
-    DexCode code = barInSub.getMethod().getCode().asDexCode();
-    checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
-    InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
-    assertEquals(baseSubject.getDexProgramClass().type, invoke.getMethod().holder);
-  }
+              MethodSubject barInSub = baseSubject.method("void", "bar", "java.lang.String");
+              assertThat(barInSub, isPresent());
 
-  private AndroidApp runAndVerifyOnJvmAndArt(
-      JasminBuilder jasminBuilder, String mainClassName, String proguardConfig) throws Exception {
-    // Run input program on java.
-    Path outputDirectory = temp.newFolder().toPath();
-    jasminBuilder.writeClassFiles(outputDirectory);
-    ProcessResult javaResult = ToolHelper.runJava(outputDirectory, mainClassName);
-    assertEquals(0, javaResult.exitCode);
-
-    AndroidApp processedApp = compileWithR8(jasminBuilder.build(), proguardConfig, this::configure);
-
-    // Run processed (output) program on ART
-    ProcessResult artResult = runOnArtRaw(processedApp, mainClassName);
-    assertEquals(javaResult.stdout, artResult.stdout);
-    assertEquals(-1, artResult.stderr.indexOf("VerifyError"));
-
-    return processedApp;
+              if (parameters.isDexRuntime()) {
+                DexCode code = barInSub.getMethod().getCode().asDexCode();
+                checkInstructions(code, ImmutableList.of(InvokeVirtual.class, ReturnVoid.class));
+                InvokeVirtual invoke = (InvokeVirtual) code.instructions[0];
+                assertEquals(baseSubject.getDexProgramClass().type, invoke.getMethod().holder);
+              }
+            })
+        .run(parameters.getRuntime(), mainClass.name)
+        .assertSuccessWithOutput("0Bar");
   }
 
   private void configure(InternalOptions options) {
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..4e9353b 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
@@ -105,7 +105,9 @@
           .setMode(mode)
           .addProgramFiles(ToolHelper.R8_WITH_RELOCATED_DEPS_JAR)
           .addKeepRuleFiles(MAIN_KEEP)
+          .allowDiagnosticInfoMessages()
           .compile()
+          .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation)
           .apply(c -> FileUtils.writeTextFile(map, c.getProguardMap()))
           .writeToZip(jar);
     }
diff --git a/src/test/java/com/android/tools/r8/classmerging/StatelessSingletonClassesMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/StatelessSingletonClassesMergingTest.java
new file mode 100644
index 0000000..e6752f8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/StatelessSingletonClassesMergingTest.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.classmerging;
+
+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 org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class StatelessSingletonClassesMergingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public StatelessSingletonClassesMergingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector -> inspector.assertIsCompleteMergeGroup(A.class, B.class))
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .noClassStaticizing()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A", "B");
+  }
+
+  static class Main {
+    public static void main(String[] args) {
+      A.INSTANCE.f();
+      B.INSTANCE.g();
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+
+    static final A INSTANCE = new A();
+
+    @NeverInline
+    void f() {
+      System.out.println("A");
+    }
+  }
+
+  @NeverClassInline
+  static class B {
+
+    static final B INSTANCE = new B();
+
+    @NeverInline
+    void g() {
+      System.out.println("B");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorCantInlineTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorCantInlineTest.java
index 07cea40..1cd81d7 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorCantInlineTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorCantInlineTest.java
@@ -4,9 +4,9 @@
 
 package com.android.tools.r8.classmerging.horizontal;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.core.IsNot.not;
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
@@ -30,10 +30,10 @@
         .assertSuccessWithOutputLines("c", "foo: foo")
         .inspect(
             codeInspector -> {
-              assertThat(codeInspector.clazz(A.class), not(isPresent()));
-              assertThat(codeInspector.clazz(B.class), isPresent());
+              assertThat(codeInspector.clazz(A.class), isAbsent());
+              assertThat(codeInspector.clazz(B.class), isAbsent());
               assertThat(codeInspector.clazz(C.class), isPresent());
-              assertThat(codeInspector.clazz(D.class), not(isPresent()));
+              assertThat(codeInspector.clazz(D.class), isAbsent());
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/InnerOuterClassesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/InnerOuterClassesTest.java
index 8a80d6e..6fad862 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/InnerOuterClassesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/InnerOuterClassesTest.java
@@ -41,7 +41,7 @@
         .addOptionsModification(
             options ->
                 options.testing.horizontalClassMergingTarget =
-                    (candidates, target) -> candidates.iterator().next())
+                    (appView, candidates, target) -> candidates.iterator().next())
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("a", "b", "c", "d")
         .inspect(
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/InstantiatedAndUninstantiatedClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/InstantiatedAndUninstantiatedClassMergingTest.java
index d6b6643..7bc50fa 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/InstantiatedAndUninstantiatedClassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/InstantiatedAndUninstantiatedClassMergingTest.java
@@ -5,13 +5,13 @@
 package com.android.tools.r8.classmerging.horizontal;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
 import org.junit.Test;
 
 public class InstantiatedAndUninstantiatedClassMergingTest extends HorizontalClassMergingTestBase {
@@ -36,14 +36,14 @@
         .addKeepMainRule(TestClass.class)
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
-        .addHorizontallyMergedClassesInspector(
-            HorizontallyMergedClassesInspector::assertNoClassesMerged)
         .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(
             inspector -> {
               assertThat(inspector.clazz(Instantiated.class), isPresent());
-              assertThat(inspector.clazz(Uninstantiated.class), isPresent());
+              assertThat(
+                  inspector.clazz(Uninstantiated.class),
+                  notIf(isPresent(), testBuilder.isR8CompatTestBuilder()));
             })
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutputLines("Instantiated", "Uninstantiated");
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/NestClassMergingTestRunner.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/NestClassMergingTestRunner.java
index bf50ab1..0844722 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/NestClassMergingTestRunner.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/NestClassMergingTestRunner.java
@@ -111,7 +111,7 @@
                 .addOptionsModification(
                     options -> {
                       options.testing.horizontalClassMergingTarget =
-                          (canditates, target) -> {
+                          (appView, canditates, target) -> {
                             Set<ClassReference> candidateClassReferences =
                                 Streams.stream(canditates)
                                     .map(DexClass::getClassReference)
@@ -164,7 +164,7 @@
                 .addOptionsModification(
                     options -> {
                       options.testing.horizontalClassMergingTarget =
-                          (canditates, target) -> {
+                          (appView, canditates, target) -> {
                             Set<ClassReference> candidateClassReferences =
                                 Streams.stream(canditates)
                                     .map(DexClass::getClassReference)
@@ -217,7 +217,7 @@
                 .addOptionsModification(
                     options -> {
                       options.testing.horizontalClassMergingTarget =
-                          (canditates, target) -> {
+                          (appView, canditates, target) -> {
                             Set<ClassReference> candidateClassReferences =
                                 Streams.stream(canditates)
                                     .map(DexClass::getClassReference)
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/PackagePrivateMemberAccessTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/PackagePrivateMemberAccessTest.java
index 820af37..dd9ab2a 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/PackagePrivateMemberAccessTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/PackagePrivateMemberAccessTest.java
@@ -4,9 +4,9 @@
 
 package com.android.tools.r8.classmerging.horizontal;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.core.IsNot.not;
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.TestParameters;
@@ -15,6 +15,7 @@
 import org.junit.Test;
 
 public class PackagePrivateMemberAccessTest extends HorizontalClassMergingTestBase {
+
   public PackagePrivateMemberAccessTest(TestParameters parameters) {
     super(parameters);
   }
@@ -23,10 +24,8 @@
   public void testR8() throws Exception {
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
-        .addProgramClasses(A.class)
-        .addProgramClasses(B.class)
+        .addProgramClasses(A.class, B.class)
         .addKeepMainRule(Main.class)
-        .allowAccessModification(false)
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
@@ -34,9 +33,9 @@
         .assertSuccessWithOutputLines("foo", "B", "bar", "5", "foobar")
         .inspect(
             codeInspector -> {
-                assertThat(codeInspector.clazz(A.class), isPresent());
-                assertThat(codeInspector.clazz(B.class), not(isPresent()));
-                assertThat(codeInspector.clazz(C.class), isPresent());
+              assertThat(codeInspector.clazz(A.class), isAbsent());
+              assertThat(codeInspector.clazz(B.class), isAbsent());
+              assertThat(codeInspector.clazz(C.class), isPresent());
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideAbstractMethodWithDefaultTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideAbstractMethodWithDefaultTest.java
index 3787201..874c095 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideAbstractMethodWithDefaultTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideAbstractMethodWithDefaultTest.java
@@ -4,7 +4,9 @@
 
 package com.android.tools.r8.classmerging.horizontal.dispatch;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isAbsent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.onlyIf;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NeverClassInline;
@@ -12,7 +14,6 @@
 import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.classmerging.horizontal.HorizontalClassMergingTestBase;
-import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
 import org.junit.Test;
 
 public class OverrideAbstractMethodWithDefaultTest extends HorizontalClassMergingTestBase {
@@ -26,21 +27,31 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector
+                    .assertIsCompleteMergeGroup(I.class, J.class)
+                    .applyIf(
+                        !parameters.canUseDefaultAndStaticInterfaceMethods(),
+                        i -> i.assertIsCompleteMergeGroup(B1.class, B2.class))
+                    .assertNoOtherClassesMerged())
+        .addOptionsModification(
+            options -> options.horizontalClassMergerOptions().setEnableInterfaceMergingInFinal())
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
-        .addHorizontallyMergedClassesInspector(
-            HorizontallyMergedClassesInspector::assertNoClassesMerged)
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("J", "B2")
         .inspect(
             codeInspector -> {
               assertThat(codeInspector.clazz(I.class), isPresent());
-              assertThat(codeInspector.clazz(J.class), isPresent());
+              assertThat(codeInspector.clazz(J.class), isAbsent());
               assertThat(codeInspector.clazz(A.class), isPresent());
               assertThat(codeInspector.clazz(B1.class), isPresent());
-              assertThat(codeInspector.clazz(B2.class), isPresent());
+              assertThat(
+                  codeInspector.clazz(B2.class),
+                  onlyIf(parameters.canUseDefaultAndStaticInterfaceMethods(), isPresent()));
               assertThat(codeInspector.clazz(C1.class), isPresent());
               assertThat(codeInspector.clazz(C2.class), isPresent());
             });
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultMethodTest.java
index 580598b..de8d7b2 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultMethodTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultMethodTest.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.classmerging.horizontal.dispatch;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.onlyIf;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NeverClassInline;
@@ -25,6 +26,8 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options -> options.horizontalClassMergerOptions().setEnableInterfaceMergingInFinal())
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
@@ -36,9 +39,11 @@
               } else {
                 inspector
                     .assertClassesNotMerged(A.class, B.class)
+                    .assertIsCompleteMergeGroup(I.class, J.class)
                     .assertIsCompleteMergeGroup(
                         SyntheticItemsTestUtils.syntheticCompanionClass(I.class),
-                        SyntheticItemsTestUtils.syntheticCompanionClass(J.class));
+                        SyntheticItemsTestUtils.syntheticCompanionClass(J.class))
+                    .assertNoOtherClassesMerged();
               }
             })
         .run(parameters.getRuntime(), Main.class)
@@ -46,7 +51,9 @@
         .inspect(
             codeInspector -> {
               assertThat(codeInspector.clazz(I.class), isPresent());
-              assertThat(codeInspector.clazz(J.class), isPresent());
+              assertThat(
+                  codeInspector.clazz(J.class),
+                  onlyIf(parameters.canUseDefaultAndStaticInterfaceMethods(), isPresent()));
               assertThat(codeInspector.clazz(A.class), isPresent());
               assertThat(codeInspector.clazz(B.class), isPresent());
               assertThat(codeInspector.clazz(C.class), isPresent());
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultOnSuperMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultOnSuperMethodTest.java
index 65ca772..56be50b 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultOnSuperMethodTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultOnSuperMethodTest.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.classmerging.horizontal.dispatch;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.onlyIf;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NeverClassInline;
@@ -30,6 +31,8 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options -> options.horizontalClassMergerOptions().setEnableInterfaceMergingInFinal())
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoUnusedInterfaceRemovalAnnotations()
@@ -42,9 +45,11 @@
               } else {
                 inspector
                     .assertClassesNotMerged(A.class, B.class)
+                    .assertIsCompleteMergeGroup(I.class, J.class)
                     .assertIsCompleteMergeGroup(
                         SyntheticItemsTestUtils.syntheticCompanionClass(I.class),
-                        SyntheticItemsTestUtils.syntheticCompanionClass(J.class));
+                        SyntheticItemsTestUtils.syntheticCompanionClass(J.class))
+                    .assertNoOtherClassesMerged();
               }
             })
         .run(parameters.getRuntime(), Main.class)
@@ -52,7 +57,9 @@
         .inspect(
             codeInspector -> {
               assertThat(codeInspector.clazz(I.class), isPresent());
-              assertThat(codeInspector.clazz(J.class), isPresent());
+              assertThat(
+                  codeInspector.clazz(J.class),
+                  onlyIf(parameters.canUseDefaultAndStaticInterfaceMethods(), isPresent()));
               assertThat(codeInspector.clazz(Parent.class), isPresent());
               assertThat(codeInspector.clazz(A.class), isPresent());
               assertThat(codeInspector.clazz(B.class), isPresent());
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/ClassHierarchyCycleAfterMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/ClassHierarchyCycleAfterMergingTest.java
index 650d804..4d81f1a 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/ClassHierarchyCycleAfterMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/ClassHierarchyCycleAfterMergingTest.java
@@ -44,11 +44,6 @@
         // hierarchy.
         .addHorizontallyMergedClassesInspector(
             HorizontallyMergedClassesInspector::assertNoClassesMerged)
-        .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
         .enableNoHorizontalClassMergingAnnotations()
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupAfterSubclassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupAfterSubclassMergingTest.java
index 1c5d915..c1912b5 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupAfterSubclassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupAfterSubclassMergingTest.java
@@ -7,7 +7,6 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isImplementing;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
@@ -16,8 +15,9 @@
 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.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -25,19 +25,21 @@
 @RunWith(Parameterized.class)
 public class CollisionWithDefaultMethodOutsideMergeGroupAfterSubclassMergingTest extends TestBase {
 
+  private final boolean enableInterfaceMergingInInitial;
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  @Parameterized.Parameters(name = "{1}, enableInterfaceMergingInInitial: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
   }
 
   public CollisionWithDefaultMethodOutsideMergeGroupAfterSubclassMergingTest(
-      TestParameters parameters) {
+      boolean enableInterfaceMergingInInitial, TestParameters parameters) {
+    this.enableInterfaceMergingInInitial = enableInterfaceMergingInInitial;
     this.parameters = parameters;
   }
 
-  // TODO(b/173990042): Disallow merging of A and B in the first round of class merging.
   @Test
   public void test() throws Exception {
     testForR8(parameters.getBackend())
@@ -48,23 +50,23 @@
         // the default method J.m() to A.
         .addHorizontallyMergedClassesInspector(
             inspector -> {
+              inspector
+                  .assertIsCompleteMergeGroup(A.class, B.class)
+                  .assertMergedInto(B.class, A.class);
               if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
-                inspector
-                    .assertIsCompleteMergeGroup(A.class, B.class)
-                    .assertMergedInto(B.class, A.class)
-                    .assertClassesNotMerged(I.class, J.class, K.class);
+                inspector.assertClassesNotMerged(I.class, J.class, K.class);
               } else {
                 inspector
-                    .assertIsCompleteMergeGroup(A.class, B.class)
-                    .assertMergedInto(B.class, A.class)
                     .assertIsCompleteMergeGroup(I.class, J.class)
                     .assertClassesNotMerged(K.class);
               }
             })
         .addOptionsModification(
             options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
+              if (enableInterfaceMergingInInitial) {
+                options.horizontalClassMergerOptions().setEnableInterfaceMergingInInitial();
+              }
+              options.horizontalClassMergerOptions().setEnableInterfaceMergingInFinal();
             })
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
@@ -80,10 +82,10 @@
               assertThat(aClassSubject, isImplementing(inspector.clazz(I.class)));
               assertThat(aClassSubject, isImplementing(inspector.clazz(K.class)));
 
-              ClassSubject bClassSubject = inspector.clazz(C.class);
-              assertThat(bClassSubject, isPresent());
+              ClassSubject cClassSubject = inspector.clazz(C.class);
+              assertThat(cClassSubject, isPresent());
               assertThat(
-                  bClassSubject,
+                  cClassSubject,
                   isImplementing(
                       inspector.clazz(
                           parameters.canUseDefaultAndStaticInterfaceMethods()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupClassTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupClassTest.java
index d4f14bd..1c64d9d 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupClassTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupClassTest.java
@@ -7,7 +7,6 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isImplementing;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
@@ -52,10 +51,7 @@
               }
             })
         .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
+            options -> options.horizontalClassMergerOptions().setEnableInterfaceMergingInFinal())
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupLambdaTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupLambdaTest.java
index f9e85ba..3a33b0f 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupLambdaTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/CollisionWithDefaultMethodOutsideMergeGroupLambdaTest.java
@@ -7,7 +7,6 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isImplementing;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
@@ -53,11 +52,7 @@
               }
             })
         .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-              options.horizontalClassMergerOptions().setIgnoreRuntimeTypeChecksForTesting();
-            })
+            options -> options.horizontalClassMergerOptions().setEnableInterfaceMergingInFinal())
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesMergingTest.java
index 7ca9cc5..c28bf26 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesMergingTest.java
@@ -4,8 +4,6 @@
 
 package com.android.tools.r8.classmerging.horizontal.interfaces;
 
-import static org.junit.Assert.assertFalse;
-
 import com.android.tools.r8.NoUnusedInterfaceRemoval;
 import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.TestBase;
@@ -37,10 +35,7 @@
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
         .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
+            options -> options.horizontalClassMergerOptions().setEnableInterfaceMergingInFinal())
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .noClassInliningOfSynthetics()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithIntersectionMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithIntersectionMergingTest.java
index dad75a4..8cfb7af 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithIntersectionMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithIntersectionMergingTest.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.classmerging.horizontal.interfaces;
 
-import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.NoUnusedInterfaceRemoval;
 import com.android.tools.r8.NoVerticalClassMerging;
@@ -37,10 +36,7 @@
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
         .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
+            options -> options.horizontalClassMergerOptions().setEnableInterfaceMergingInFinal())
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .noClassInliningOfSynthetics()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithSameNameAndDifferentParametersMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithSameNameAndDifferentParametersMergingTest.java
index e58ca16..13940fe 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithSameNameAndDifferentParametersMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithSameNameAndDifferentParametersMergingTest.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.classmerging.horizontal.interfaces;
 
-import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.NoUnusedInterfaceRemoval;
 import com.android.tools.r8.NoVerticalClassMerging;
@@ -39,10 +38,7 @@
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
         .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
+            options -> options.horizontalClassMergerOptions().setEnableInterfaceMergingInFinal())
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .noClassInliningOfSynthetics()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithSameNameAndDifferentReturnTypeMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithSameNameAndDifferentReturnTypeMergingTest.java
index a877121..3236862 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithSameNameAndDifferentReturnTypeMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointFunctionalInterfacesWithSameNameAndDifferentReturnTypeMergingTest.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.classmerging.horizontal.interfaces;
 
-import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.NoUnusedInterfaceRemoval;
 import com.android.tools.r8.NoVerticalClassMerging;
@@ -39,10 +38,7 @@
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
         .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
+            options -> options.horizontalClassMergerOptions().setEnableInterfaceMergingInFinal())
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .noClassInliningOfSynthetics()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithDefaultMethodsMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithDefaultMethodsMergingTest.java
index 0807547..2d1ac21 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithDefaultMethodsMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithDefaultMethodsMergingTest.java
@@ -7,7 +7,6 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isImplementing;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
@@ -43,10 +42,7 @@
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
         .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
+            options -> options.horizontalClassMergerOptions().setEnableInterfaceMergingInFinal())
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoUnusedInterfaceRemovalAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithoutDefaultMethodsMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithoutDefaultMethodsMergingTest.java
index 5f30eff..7ca2089 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithoutDefaultMethodsMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/DisjointInterfacesWithoutDefaultMethodsMergingTest.java
@@ -7,7 +7,6 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isImplementing;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
@@ -43,10 +42,7 @@
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
         .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
+            options -> options.horizontalClassMergerOptions().setEnableInterfaceMergingInFinal())
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoUnusedInterfaceRemovalAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/EmptyInterfaceChainMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/EmptyInterfaceChainMergingTest.java
new file mode 100644
index 0000000..d059bbf
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/EmptyInterfaceChainMergingTest.java
@@ -0,0 +1,81 @@
+// 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.classmerging.horizontal.interfaces;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isImplementing;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NoUnusedInterfaceRemoval;
+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 org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class EmptyInterfaceChainMergingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public EmptyInterfaceChainMergingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector ->
+                inspector
+                    .assertIsCompleteMergeGroup(I.class, J.class, K.class)
+                    .assertNoOtherClassesMerged())
+        .addOptionsModification(
+            options -> options.horizontalClassMergerOptions().setEnableInterfaceMergingInFinal())
+        .enableNoUnusedInterfaceRemovalAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject aClassSubject = inspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              assertThat(aClassSubject, isImplementing(inspector.clazz(I.class)));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccess();
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(A.class);
+    }
+  }
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface I {}
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface J extends I {}
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface K extends J {}
+
+  static class A implements K {}
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/EmptyInterfacesMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/EmptyInterfacesMergingTest.java
index 24e5de8..e2b196c 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/EmptyInterfacesMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/EmptyInterfacesMergingTest.java
@@ -7,7 +7,6 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isImplementing;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.NoUnusedInterfaceRemoval;
 import com.android.tools.r8.NoVerticalClassMerging;
@@ -41,10 +40,7 @@
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
         .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
+            options -> options.horizontalClassMergerOptions().setEnableInterfaceMergingInFinal())
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/IdenticalFunctionalInterfacesMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/IdenticalFunctionalInterfacesMergingTest.java
index 2d264db..100c54b 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/IdenticalFunctionalInterfacesMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/IdenticalFunctionalInterfacesMergingTest.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.classmerging.horizontal.interfaces;
 
-import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.NoUnusedInterfaceRemoval;
 import com.android.tools.r8.NoVerticalClassMerging;
@@ -37,10 +36,7 @@
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
         .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
+            options -> options.horizontalClassMergerOptions().setEnableInterfaceMergingInFinal())
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .noClassInliningOfSynthetics()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/IdenticalFunctionalInterfacesWithIntersectionMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/IdenticalFunctionalInterfacesWithIntersectionMergingTest.java
index 23ed1ea..645a0c8 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/IdenticalFunctionalInterfacesWithIntersectionMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/IdenticalFunctionalInterfacesWithIntersectionMergingTest.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.classmerging.horizontal.interfaces;
 
-import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.NoUnusedInterfaceRemoval;
 import com.android.tools.r8.NoVerticalClassMerging;
@@ -37,10 +36,7 @@
         .addHorizontallyMergedClassesInspector(
             inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
         .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
+            options -> options.horizontalClassMergerOptions().setEnableInterfaceMergingInFinal())
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .noClassInliningOfSynthetics()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/NoDefaultMethodMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/NoDefaultMethodMergingTest.java
index 6ad6ef1..2284113 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/NoDefaultMethodMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/NoDefaultMethodMergingTest.java
@@ -7,7 +7,6 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isImplementing;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertFalse;
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
@@ -51,10 +50,7 @@
               }
             })
         .addOptionsModification(
-            options -> {
-              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
-              options.horizontalClassMergerOptions().enableInterfaceMerging();
-            })
+            options -> options.horizontalClassMergerOptions().setEnableInterfaceMergingInFinal())
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontalstatic/StaticClassMergerInterfaceTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontalstatic/StaticClassMergerInterfaceTest.java
index 1c33206..392c587 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontalstatic/StaticClassMergerInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontalstatic/StaticClassMergerInterfaceTest.java
@@ -40,19 +40,21 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(TestClass.class)
-        // TODO(b/173990042): Extend horizontal class merging to interfaces.
         .addHorizontallyMergedClassesInspector(
             inspector -> {
               if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
-                inspector.assertNoClassesMerged();
+                inspector.assertIsCompleteMergeGroup(I.class, J.class).assertNoOtherClassesMerged();
               } else {
                 inspector
+                    .assertClassesNotMerged(I.class, J.class)
                     .assertClassReferencesMerged(
                         SyntheticItemsTestUtils.syntheticCompanionClass(I.class),
                         SyntheticItemsTestUtils.syntheticCompanionClass(J.class))
-                    .assertClassesNotMerged(I.class, J.class);
+                    .assertNoOtherClassesMerged();
               }
             })
+        .addOptionsModification(
+            options -> options.horizontalClassMergerOptions().setEnableInterfaceMergingInFinal())
         .enableInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), TestClass.class)
@@ -62,11 +64,9 @@
               // We do not allow horizontal class merging of interfaces and classes. Therefore, A
               // should remain in the output.
               assertThat(inspector.clazz(A.class), isPresent());
-
-              // TODO(b/173990042): I and J should be merged.
               if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
                 assertThat(inspector.clazz(I.class), isPresent());
-                assertThat(inspector.clazz(J.class), isPresent());
+                assertThat(inspector.clazz(J.class), isAbsent());
               } else {
                 assertThat(inspector.clazz(syntheticCompanionClass(I.class)), isPresent());
                 assertThat(inspector.clazz(syntheticCompanionClass(J.class)), isAbsent());
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
index 5468cae..2f157d0 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
@@ -1052,7 +1052,8 @@
           CF_DIR.resolve("pkg/SimpleInterfaceImplRetriever.class"),
           CF_DIR.resolve("pkg/SimpleInterfaceImplRetriever$SimpleInterfaceImpl.class"),
           CF_DIR.resolve("pkg/SimpleInterfaceImplRetriever$1.class"),
-          CF_DIR.resolve("NeverInline.class")
+          CF_DIR.resolve("NeverInline.class"),
+          CF_DIR.resolve("NoHorizontalClassMerging.class")
         };
     // SimpleInterface cannot be merged into SimpleInterfaceImpl because SimpleInterfaceImpl
     // is in a different package and is not public.
diff --git a/src/test/java/com/android/tools/r8/debug/ContinuousKotlinSteppingTest.java b/src/test/java/com/android/tools/r8/debug/ContinuousKotlinSteppingTest.java
index 539af51..c3e45b6 100644
--- a/src/test/java/com/android/tools/r8/debug/ContinuousKotlinSteppingTest.java
+++ b/src/test/java/com/android/tools/r8/debug/ContinuousKotlinSteppingTest.java
@@ -17,7 +17,7 @@
 
   private static final String MAIN_METHOD_NAME = "main";
 
-  @Parameters(name = "{0}")
+  @Parameters(name = "{0}, {1}")
   public static List<Object[]> data() {
     return buildParameters(
         getTestParameters().withDexRuntimes().withAllApiLevels().build(),
diff --git a/src/test/java/com/android/tools/r8/debug/DoNotCrashOnAccessToThisRunner.java b/src/test/java/com/android/tools/r8/debug/DoNotCrashOnAccessToThisRunner.java
index 3e562cf..2363533 100644
--- a/src/test/java/com/android/tools/r8/debug/DoNotCrashOnAccessToThisRunner.java
+++ b/src/test/java/com/android/tools/r8/debug/DoNotCrashOnAccessToThisRunner.java
@@ -27,17 +27,19 @@
     DelayedDebugTestConfig cf =
         temp -> new CfDebugTestConfig().addPaths(ToolHelper.getClassPathForTests());
     DelayedDebugTestConfig d8 =
-        temp -> new D8DebugTestConfig().compileAndAdd(
-            temp,
-            ImmutableList.of(ToolHelper.getClassFileForTestClass(CLASS)),
-            options -> {
-              // Release mode so receiver can be clobbered.
-              options.debug = false;
-              // Api level M so that the workarounds for Lollipop verifier doesn't
-              // block the receiver register. We want to check b/116683601 which
-              // happens on at least 7.0.0.
-              options.minApiLevel = AndroidApiLevel.M.getLevel();
-            });
+        temp ->
+            new D8DebugTestConfig()
+                .compileAndAdd(
+                    temp,
+                    ImmutableList.of(ToolHelper.getClassFileForTestClass(CLASS)),
+                    options -> {
+                      // Release mode so receiver can be clobbered.
+                      options.debug = false;
+                      // Api level M so that the workarounds for Lollipop verifier doesn't
+                      // block the receiver register. We want to check b/116683601 which
+                      // happens on at least 7.0.0.
+                      options.minApiLevel = AndroidApiLevel.M;
+                    });
     return ImmutableList.of(new Object[]{"CF", cf}, new Object[]{"D8", d8});
   }
 
diff --git a/src/test/java/com/android/tools/r8/desugar/DesugarLambdaWithAnonymousClass.java b/src/test/java/com/android/tools/r8/desugar/DesugarLambdaWithAnonymousClass.java
index 72549c5..9691b07 100644
--- a/src/test/java/com/android/tools/r8/desugar/DesugarLambdaWithAnonymousClass.java
+++ b/src/test/java/com/android/tools/r8/desugar/DesugarLambdaWithAnonymousClass.java
@@ -29,10 +29,10 @@
 @RunWith(Parameterized.class)
 public class DesugarLambdaWithAnonymousClass extends TestBase {
 
-  private List<String> EXPECTED_JAVAC_RESULT =
+  private final List<String> EXPECTED_JAVAC_RESULT =
       ImmutableList.of("Hello from inside lambda$test$0", "Hello from inside lambda$testStatic$1");
 
-  private List<String> EXPECTED_D8_DESUGARED_RESULT =
+  private final List<String> EXPECTED_D8_DESUGARED_RESULT =
       ImmutableList.of(
           "Hello from inside lambda$test$0$DesugarLambdaWithAnonymousClass$TestClass",
           "Hello from inside lambda$testStatic$1");
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/AbstractBackportTest.java b/src/test/java/com/android/tools/r8/desugar/backports/AbstractBackportTest.java
index e4dd6d9..7f77265 100644
--- a/src/test/java/com/android/tools/r8/desugar/backports/AbstractBackportTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/backports/AbstractBackportTest.java
@@ -4,14 +4,18 @@
 
 package com.android.tools.r8.desugar.backports;
 
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticType;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static java.util.stream.Collectors.toList;
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestBuilder;
 import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.errors.InterfaceDesugarMissingTypeDiagnostic;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -148,7 +152,23 @@
           .setMinApi(parameters.getApiLevel())
           .apply(this::configureProgram)
           .setIncludeClassesChecksum(true)
-          .compile()
+          .compileWithExpectedDiagnostics(
+              diagnostics -> {
+                if (diagnostics.getWarnings().isEmpty()) {
+                  diagnostics.assertNoMessages();
+                  return;
+                }
+                // When compiling with an old android.jar some tests refer to non-present types.
+                // Check only java.util types are missing and that none of them are about the target
+                // type that is being backported.
+                diagnostics
+                    .assertOnlyWarnings()
+                    .assertAllWarningsMatch(
+                        diagnosticType(InterfaceDesugarMissingTypeDiagnostic.class))
+                    .assertAllWarningsMatch(diagnosticMessage(containsString("java.util")))
+                    .assertNoWarningsMatch(
+                        diagnosticMessage(containsString(targetClass.getName())));
+              })
           .run(parameters.getRuntime(), testClassName)
           .assertSuccess()
           .inspect(this::assertDesugaring);
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/TestBackportedNotPresentInAndroidJar.java b/src/test/java/com/android/tools/r8/desugar/backports/TestBackportedNotPresentInAndroidJar.java
index 8096c47..5cfc6cd 100644
--- a/src/test/java/com/android/tools/r8/desugar/backports/TestBackportedNotPresentInAndroidJar.java
+++ b/src/test/java/com/android/tools/r8/desugar/backports/TestBackportedNotPresentInAndroidJar.java
@@ -39,7 +39,7 @@
       // android.jar for that level.
       CodeInspector inspector = new CodeInspector(ToolHelper.getAndroidJar(apiLevel));
       InternalOptions options = new InternalOptions();
-      options.minApiLevel = apiLevel.getLevel();
+      options.minApiLevel = apiLevel;
       List<DexMethod> backportedMethods =
           BackportedMethodRewriter.generateListOfBackportedMethods(
               AndroidApp.builder().build(), options, ThreadUtils.getExecutorService(options));
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/PriorityQueueSubclassTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/PriorityQueueSubclassTest.java
new file mode 100644
index 0000000..2c29f58
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/PriorityQueueSubclassTest.java
@@ -0,0 +1,86 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.desugaredlibrary;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.util.List;
+import java.util.PriorityQueue;
+import java.util.stream.Stream;
+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 PriorityQueueSubclassTest extends DesugaredLibraryTestBase {
+
+  private final TestParameters parameters;
+  private final boolean shrinkDesugaredLibrary;
+
+  @Parameters(name = "{1}, shrinkDesugaredLibrary: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withDexRuntimes().withAllApiLevels().build());
+  }
+
+  public PriorityQueueSubclassTest(boolean shrinkDesugaredLibrary, TestParameters parameters) {
+    this.shrinkDesugaredLibrary = shrinkDesugaredLibrary;
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testPriorityQueueD8() throws Exception {
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    testForD8()
+        .addInnerClasses(PriorityQueueSubclassTest.class)
+        .setMinApi(parameters.getApiLevel())
+        .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .compile()
+        .addDesugaredCoreLibraryRunClassPath(
+            this::buildDesugaredLibrary,
+            parameters.getApiLevel(),
+            keepRuleConsumer.get(),
+            shrinkDesugaredLibrary)
+        .run(parameters.getRuntime(), Executor.class)
+        .assertSuccessWithOutputLines("1");
+  }
+
+  @Test
+  public void testPriorityQueueR8() throws Exception {
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    testForR8(Backend.DEX)
+        .addInnerClasses(PriorityQueueSubclassTest.class)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepClassAndMembersRules(Executor.class)
+        .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .compile()
+        .addDesugaredCoreLibraryRunClassPath(
+            this::buildDesugaredLibrary,
+            parameters.getApiLevel(),
+            keepRuleConsumer.get(),
+            shrinkDesugaredLibrary)
+        .run(parameters.getRuntime(), Executor.class)
+        .assertSuccessWithOutputLines("1");
+  }
+
+  static class Executor {
+
+    public static void main(String[] args) {
+      MyPriorityQueue strings = new MyPriorityQueue();
+      strings.add("1");
+      strings.add("2");
+      System.out.println(strings.iterator().next());
+    }
+  }
+
+  static class MyPriorityQueue extends PriorityQueue<String> {
+
+    @Override
+    public Stream<String> stream() {
+      return Stream.empty();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/kotlin/KotlinMetadataTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/kotlin/KotlinMetadataTest.java
index ef8f61a..55e2905 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/kotlin/KotlinMetadataTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/kotlin/KotlinMetadataTest.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
@@ -130,7 +131,7 @@
             .addKeepAllClassesRule()
             .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
             .setMinApi(parameters.getApiLevel())
-            .allowDiagnosticWarningMessages()
+            .allowDiagnosticMessages()
             .allowUnusedDontWarnKotlinReflectJvmInternal(
                 kotlinParameters.getCompiler().isNot(KOTLINC_1_3_72));
     KeepRuleConsumer keepRuleConsumer = null;
@@ -141,6 +142,8 @@
     R8TestCompileResult compileResult =
         testBuilder
             .compile()
+            .assertNoErrorMessages()
+            .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation)
             .apply(KotlinMetadataTestBase::verifyExpectedWarningsFromKotlinReflectAndStdLib);
     if (desugarLibrary) {
       assertNotNull(keepRuleConsumer);
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..de1c949 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
@@ -77,7 +77,9 @@
                       options.desugarState = DesugarState.ON;
                       options.cfToCfDesugar = true;
                     }))
+        .allowDiagnosticInfoMessages()
         .compile()
+        .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation)
         .inspect(inspector -> assertNests(inspector, desugar))
         .writeToZip();
   }
diff --git a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/Java11R8CompilationTest.java b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/Java11R8CompilationTest.java
index cb549f5..31f7aba 100644
--- a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/Java11R8CompilationTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/Java11R8CompilationTest.java
@@ -50,7 +50,9 @@
         .setMinApi(parameters.getApiLevel())
         .addProgramFiles(ToolHelper.R8_WITH_RELOCATED_DEPS_11_JAR)
         .addKeepRuleFiles(MAIN_KEEP)
+        .allowDiagnosticInfoMessages()
         .compile()
+        .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation)
         .inspect(this::assertNotEmpty)
         .inspect(Java11R8CompilationTest::assertNoNests);
   }
diff --git a/src/test/java/com/android/tools/r8/desugar/staticinterfacemethod/InvokeStaticDesugarTest.java b/src/test/java/com/android/tools/r8/desugar/staticinterfacemethod/InvokeStaticDesugarTest.java
index b54a7d5..35e52b4 100644
--- a/src/test/java/com/android/tools/r8/desugar/staticinterfacemethod/InvokeStaticDesugarTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/staticinterfacemethod/InvokeStaticDesugarTest.java
@@ -15,12 +15,12 @@
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.ToolHelper.DexVm;
-import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.IntBox;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
 import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
 import java.util.Collection;
@@ -149,8 +149,8 @@
     return box.get();
   }
 
-  private Set<MethodReference> getSyntheticMethods(CodeInspector inspector) {
-    Set<MethodReference> methods = new HashSet<>();
+  private Set<FoundMethodSubject> getSyntheticMethods(CodeInspector inspector) {
+    Set<FoundMethodSubject> methods = new HashSet<>();
     assert inspector.allClasses().stream()
         .allMatch(
             c ->
@@ -159,10 +159,7 @@
                         c.getFinalReference()));
     inspector.allClasses().stream()
         .filter(c -> SyntheticItemsTestUtils.isExternalStaticInterfaceCall(c.getFinalReference()))
-        .forEach(
-            c ->
-                c.allMethods(m -> !m.isInstanceInitializer())
-                    .forEach(m -> methods.add(m.asMethodReference())));
+        .forEach(c -> methods.addAll(c.allMethods(m -> !m.isInstanceInitializer())));
     return methods;
   }
 
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterMergeRegression.java b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterMergeRegression.java
index 1bfbaac..71ddab8 100644
--- a/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterMergeRegression.java
+++ b/src/test/java/com/android/tools/r8/dexsplitter/DexSplitterMergeRegression.java
@@ -63,7 +63,7 @@
                 .addOptionsModification(
                     options ->
                         options.testing.horizontalClassMergingTarget =
-                            (candidates, target) -> candidates.iterator().next())
+                            (appView, candidates, target) -> candidates.iterator().next())
                 .addHorizontallyMergedClassesInspector(
                     inspector ->
                         inspector.assertMergedInto(BaseWithStatic.class, AFeatureWithStatic.class))
diff --git a/src/test/java/com/android/tools/r8/graph/GenericSignatureIdentityTest.java b/src/test/java/com/android/tools/r8/graph/GenericSignatureIdentityTest.java
index f4adbfb..3cb6899 100644
--- a/src/test/java/com/android/tools/r8/graph/GenericSignatureIdentityTest.java
+++ b/src/test/java/com/android/tools/r8/graph/GenericSignatureIdentityTest.java
@@ -78,6 +78,10 @@
       if (signature == null) {
         return;
       }
+      if (signature.equals(
+          "<R::Lcom/android/tools/r8/synthesis/Rewritable<TR;>;>Ljava/lang/Object;")) {
+        int xyz = 0;
+      }
       TestDiagnosticMessagesImpl testDiagnosticMessages = new TestDiagnosticMessagesImpl();
       ClassSignature classSignature =
           GenericSignature.parseClassSignature(
diff --git a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureCorrectnessHelperTests.java b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureCorrectnessHelperTests.java
index 65b434b..969a59f 100644
--- a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureCorrectnessHelperTests.java
+++ b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureCorrectnessHelperTests.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.GenericSignatureContextBuilder;
 import com.android.tools.r8.graph.GenericSignatureCorrectnessHelper;
 import com.android.tools.r8.graph.GenericSignatureCorrectnessHelper.SignatureEvaluationResult;
 import com.android.tools.r8.transformers.ClassFileTransformer.MethodPredicate;
@@ -44,9 +45,10 @@
             buildInnerClasses(GenericSignatureCorrectnessHelperTests.class)
                 .addLibraryFile(ToolHelper.getJava8RuntimeJar())
                 .build());
-    GenericSignatureCorrectnessHelper check =
-        GenericSignatureCorrectnessHelper.createForVerification(appView);
-    check.run();
+    GenericSignatureContextBuilder contextBuilder =
+        GenericSignatureContextBuilder.create(appView.appInfo().classes());
+    GenericSignatureCorrectnessHelper.createForVerification(appView, contextBuilder)
+        .run(appView.appInfo().classes());
   }
 
   @Test
@@ -139,7 +141,7 @@
                     })
                 .transform()),
         ClassWithInvalidArgumentCount.class,
-        SignatureEvaluationResult.INVALID_APPLICATION_COUNT);
+        SignatureEvaluationResult.VALID);
   }
 
   @Test
@@ -182,8 +184,10 @@
                 .addClassProgramData(transformations)
                 .addLibraryFile(ToolHelper.getJava8RuntimeJar())
                 .build());
+    GenericSignatureContextBuilder contextBuilder =
+        GenericSignatureContextBuilder.create(appView.appInfo().classes());
     GenericSignatureCorrectnessHelper check =
-        GenericSignatureCorrectnessHelper.createForInitialCheck(appView);
+        GenericSignatureCorrectnessHelper.createForInitialCheck(appView, contextBuilder);
     DexProgramClass clazz =
         appView
             .definitionFor(
diff --git a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePartialTypeArgumentApplierTest.java b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePartialTypeArgumentApplierTest.java
index 64f2ac3..1fb82ab 100644
--- a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePartialTypeArgumentApplierTest.java
+++ b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePartialTypeArgumentApplierTest.java
@@ -13,12 +13,16 @@
 import com.android.tools.r8.TestDiagnosticMessagesImpl;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GenericSignature;
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
+import com.android.tools.r8.graph.GenericSignatureContextBuilder.TypeParameterContext;
 import com.android.tools.r8.graph.GenericSignaturePartialTypeArgumentApplier;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.BiPredicateUtils;
 import com.google.common.collect.ImmutableMap;
 import java.util.Collections;
@@ -48,7 +52,7 @@
   }
 
   @Test
-  public void testVariablesInOuterPosition() {
+  public void testVariablesInOuterPosition() throws Exception {
     runTest(
             ImmutableMap.of("T", objectType, "R", objectType),
             Collections.emptySet(),
@@ -60,7 +64,7 @@
   }
 
   @Test
-  public void testVariablesInInnerPosition() {
+  public void testVariablesInInnerPosition() throws Exception {
     runTest(
             ImmutableMap.of("T", objectType, "R", objectType),
             Collections.emptySet(),
@@ -72,7 +76,7 @@
   }
 
   @Test
-  public void testRemovingPrunedLink() {
+  public void testRemovingPrunedLink() throws Exception {
     runTest(
             Collections.emptyMap(),
             Collections.emptySet(),
@@ -85,7 +89,7 @@
   }
 
   @Test
-  public void testRemovedGenericArguments() {
+  public void testRemovedGenericArguments() throws Exception {
     runTest(
             Collections.emptyMap(),
             Collections.emptySet(),
@@ -102,11 +106,17 @@
       BiPredicate<DexType, DexType> removedLink,
       Predicate<DexType> hasFormalTypeParameters,
       String initialSignature,
-      String expectedRewrittenSignature) {
+      String expectedRewrittenSignature)
+      throws Exception {
+    AppView<AppInfo> appView = computeAppView(AndroidApp.builder().build());
     GenericSignaturePartialTypeArgumentApplier argumentApplier =
         GenericSignaturePartialTypeArgumentApplier.build(
-                objectType, removedLink, hasFormalTypeParameters)
-            .addSubstitutionsAndVariables(substitutions, liveVariables);
+            appView,
+            TypeParameterContext.empty()
+                .addPrunedSubstitutions(substitutions)
+                .addLiveParameters(liveVariables),
+            removedLink,
+            hasFormalTypeParameters);
     TestDiagnosticMessagesImpl diagnosticsHandler = new TestDiagnosticMessagesImpl();
     MethodTypeSignature methodTypeSignature =
         argumentApplier.visitMethodSignature(
diff --git a/src/test/java/com/android/tools/r8/graph/genericsignature/UnboundedFormalTypeGenericSignatureTest.java b/src/test/java/com/android/tools/r8/graph/genericsignature/UnboundedFormalTypeGenericSignatureTest.java
index 58812ee..80939de 100644
--- a/src/test/java/com/android/tools/r8/graph/genericsignature/UnboundedFormalTypeGenericSignatureTest.java
+++ b/src/test/java/com/android/tools/r8/graph/genericsignature/UnboundedFormalTypeGenericSignatureTest.java
@@ -6,7 +6,6 @@
 
 import static org.hamcrest.CoreMatchers.containsString;
 
-import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -87,53 +86,51 @@
 
   @Test
   public void testUnboundParametersInClassR8() throws Exception {
-    R8TestRunResult runResult =
-        testForR8(parameters.getBackend())
-            .addProgramClassFileData(
-                transformer(Main.class)
-                    .removeInnerClasses()
-                    .setGenericSignature("L" + SUPER_BINARY_NAME + "<TR;>;")
-                    .transform(),
-                transformer(Super.class).removeInnerClasses().transform())
-            .addKeepAllClassesRule()
-            .addKeepAttributes(
-                ProguardKeepAttributes.SIGNATURE,
-                ProguardKeepAttributes.INNER_CLASSES,
-                ProguardKeepAttributes.ENCLOSING_METHOD)
-            .setMinApi(parameters.getApiLevel())
-            .run(parameters.getRuntime(), Main.class);
-    if (parameters.isCfRuntime()) {
-      runResult.assertFailureWithErrorThatMatches(containsString("java.lang.NullPointerException"));
-    } else {
-      runResult.assertSuccessWithOutputLines(Super.class.getTypeName() + "<R>", "R", "T");
-    }
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(
+            transformer(Main.class)
+                .removeInnerClasses()
+                .setGenericSignature("L" + SUPER_BINARY_NAME + "<TR;>;")
+                .transform(),
+            transformer(Super.class).removeInnerClasses().transform())
+        .addKeepAllClassesRule()
+        .addKeepAttributes(
+            ProguardKeepAttributes.SIGNATURE,
+            ProguardKeepAttributes.INNER_CLASSES,
+            ProguardKeepAttributes.ENCLOSING_METHOD)
+        .setMinApi(parameters.getApiLevel())
+        .allowDiagnosticInfoMessages()
+        .compile()
+        .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(
+            "class " + Super.class.getTypeName(), "R", "class java.lang.Object");
   }
 
   @Test
   public void testUnboundParametersInMethodR8() throws Exception {
-    R8TestRunResult runResult =
-        testForR8(parameters.getBackend())
-            .addProgramClassFileData(
-                transformer(Main.class)
-                    .removeInnerClasses()
-                    .setGenericSignature(
-                        MethodPredicate.onName("testStatic"), "<R:Ljava/lang/Object;>()TS;")
-                    .setGenericSignature(
-                        MethodPredicate.onName("testVirtual"), "<R:Ljava/lang/Object;>()TQ;")
-                    .transform(),
-                transformer(Super.class).removeInnerClasses().transform())
-            .addKeepAllClassesRule()
-            .addKeepAttributes(
-                ProguardKeepAttributes.SIGNATURE,
-                ProguardKeepAttributes.INNER_CLASSES,
-                ProguardKeepAttributes.ENCLOSING_METHOD)
-            .setMinApi(parameters.getApiLevel())
-            .run(parameters.getRuntime(), Main.class);
-    if (parameters.isCfRuntime()) {
-      runResult.assertSuccessWithOutputLines(Super.class.getTypeName() + "<T>", "null", "null");
-    } else {
-      runResult.assertSuccessWithOutputLines(Super.class.getTypeName() + "<T>", "S", "Q");
-    }
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(
+            transformer(Main.class)
+                .removeInnerClasses()
+                .setGenericSignature(
+                    MethodPredicate.onName("testStatic"), "<R:Ljava/lang/Object;>()TS;")
+                .setGenericSignature(
+                    MethodPredicate.onName("testVirtual"), "<R:Ljava/lang/Object;>()TQ;")
+                .transform(),
+            transformer(Super.class).removeInnerClasses().transform())
+        .addKeepAllClassesRule()
+        .addKeepAttributes(
+            ProguardKeepAttributes.SIGNATURE,
+            ProguardKeepAttributes.INNER_CLASSES,
+            ProguardKeepAttributes.ENCLOSING_METHOD)
+        .setMinApi(parameters.getApiLevel())
+        .allowDiagnosticInfoMessages()
+        .compile()
+        .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(
+            Super.class.getTypeName() + "<T>", "class java.lang.Object", "class java.lang.Object");
   }
 
   public static class Super<T> {}
diff --git a/src/test/java/com/android/tools/r8/graph/genericsignature/UnknownClassInSignatureTest.java b/src/test/java/com/android/tools/r8/graph/genericsignature/UnknownClassInSignatureTest.java
index 4c2ac5e..cf67f18 100644
--- a/src/test/java/com/android/tools/r8/graph/genericsignature/UnknownClassInSignatureTest.java
+++ b/src/test/java/com/android/tools/r8/graph/genericsignature/UnknownClassInSignatureTest.java
@@ -7,6 +7,7 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -27,7 +28,6 @@
 public class UnknownClassInSignatureTest extends TestBase {
 
   private final TestParameters parameters;
-  private final String NEW_CLASS_SIGNATURE = "<T:LUnknownClass1;>LUnknownClass2<LUnknownClass3;>;";
   private final String NEW_FIELD_SIGNATURE = "LUnknownClass4<LUnknownClass4;>;";
   private final String NEW_METHOD_SIGNATURE = "()LUnkownClass5<LunknownPackage/UnknownClass6;>;";
 
@@ -46,7 +46,7 @@
         .addProgramClassFileData(
             transformer(Main.class)
                 .removeInnerClasses()
-                .setGenericSignature(NEW_CLASS_SIGNATURE)
+                .setGenericSignature("<T:LUnknownClass1;>LUnknownClass2<LUnknownClass3;>;")
                 .setGenericSignature(FieldPredicate.onName("field"), NEW_FIELD_SIGNATURE)
                 .setGenericSignature(MethodPredicate.onName("main"), NEW_METHOD_SIGNATURE)
                 .transform())
@@ -56,13 +56,16 @@
             ProguardKeepAttributes.ENCLOSING_METHOD,
             ProguardKeepAttributes.INNER_CLASSES)
         .setMinApi(parameters.getApiLevel())
+        .allowDiagnosticInfoMessages()
+        .compile()
+        .apply(TestBase::verifyExpectedInfoFromGenericSignatureSuperTypeValidation)
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("Hello World!")
         .inspect(
             inspector -> {
               ClassSubject clazz = inspector.clazz(Main.class);
               assertThat(clazz, isPresent());
-              assertEquals(NEW_CLASS_SIGNATURE, clazz.getFinalSignatureAttribute());
+              assertNull(clazz.getFinalSignatureAttribute());
               FieldSubject field = clazz.uniqueFieldWithFinalName("field");
               assertThat(field, isPresent());
               assertEquals(NEW_FIELD_SIGNATURE, field.getFinalSignatureAttribute());
diff --git a/src/test/java/com/android/tools/r8/internal/ClankDepsTest.java b/src/test/java/com/android/tools/r8/internal/ClankDepsTest.java
index 82570e9..183031e 100644
--- a/src/test/java/com/android/tools/r8/internal/ClankDepsTest.java
+++ b/src/test/java/com/android/tools/r8/internal/ClankDepsTest.java
@@ -3,8 +3,11 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.internal;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.proguardConfigurationRuleDoesNotMatch;
+import static com.android.tools.r8.utils.codeinspector.Matchers.typeVariableNotInScope;
+import static org.hamcrest.CoreMatchers.anyOf;
+
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestDiagnosticMessages;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.utils.AndroidApiLevel;
@@ -45,6 +48,10 @@
         .allowUnusedProguardConfigurationRules()
         .allowUnnecessaryDontWarnWildcards()
         .setMinApi(AndroidApiLevel.N)
-        .compileWithExpectedDiagnostics(TestDiagnosticMessages::assertOnlyInfos);
+        .allowDiagnosticInfoMessages()
+        .compileWithExpectedDiagnostics(
+            diagnostics ->
+                diagnostics.assertAllInfosMatch(
+                    anyOf(typeVariableNotInScope(), proguardConfigurationRuleDoesNotMatch())));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/internal/Gmail17060416TreeShakeJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/Gmail17060416TreeShakeJarVerificationTest.java
index 99237cf..7319852 100644
--- a/src/test/java/com/android/tools/r8/internal/Gmail17060416TreeShakeJarVerificationTest.java
+++ b/src/test/java/com/android/tools/r8/internal/Gmail17060416TreeShakeJarVerificationTest.java
@@ -4,6 +4,8 @@
 package com.android.tools.r8.internal;
 
 import static com.android.tools.r8.ToolHelper.isLocalDevelopment;
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
@@ -20,16 +22,14 @@
 public class Gmail17060416TreeShakeJarVerificationTest extends GmailCompilationBase {
   private static final int MAX_SIZE = 20000000;
 
-  private final TestParameters parameters;
-
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withDexRuntimes().build();
+    return getTestParameters().withNoneRuntime().build();
   }
 
   public Gmail17060416TreeShakeJarVerificationTest(TestParameters parameters) {
     super(170604, 16);
-    this.parameters = parameters;
+    assert parameters.isNoneRuntime();
   }
 
   @Test
@@ -37,13 +37,24 @@
     assumeTrue(isLocalDevelopment());
 
     R8TestCompileResult compileResult =
-        testForR8(parameters.getBackend())
+        testForR8(Backend.DEX)
             .addKeepRuleFiles(Paths.get(base).resolve(BASE_PG_CONF))
+            .allowDiagnosticMessages()
+            .allowUnusedDontWarnPatterns()
             .allowUnusedProguardConfigurationRules()
-            .compile();
+            .compile()
+            .assertAllInfoMessagesMatch(
+                anyOf(
+                    containsString("Ignoring option: -optimizations"),
+                    containsString("Proguard configuration rule does not match anything"),
+                    containsString("Invalid signature"),
+                    containsString("Validation error: A type variable is not in scope")))
+            .assertAllWarningMessagesMatch(
+                anyOf(
+                    containsString("Ignoring option: -outjars"),
+                    containsString("Cannot determine what identifier string flows to")));
 
     int appSize = compileResult.app.applicationSize();
     assertTrue("Expected max size of " + MAX_SIZE+ ", got " + appSize, appSize < MAX_SIZE);
   }
-
 }
diff --git a/src/test/java/com/android/tools/r8/internal/proto/ChromeProtoRewritingTest.java b/src/test/java/com/android/tools/r8/internal/proto/ChromeProtoRewritingTest.java
index 12236fc..4e784a3 100644
--- a/src/test/java/com/android/tools/r8/internal/proto/ChromeProtoRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/internal/proto/ChromeProtoRewritingTest.java
@@ -8,6 +8,9 @@
 import static com.android.tools.r8.internal.proto.ProtoShrinkingTestBase.keepAllProtosRule;
 import static com.android.tools.r8.internal.proto.ProtoShrinkingTestBase.keepDynamicMethodSignatureRule;
 import static com.android.tools.r8.internal.proto.ProtoShrinkingTestBase.keepNewMessageInfoSignatureRule;
+import static com.android.tools.r8.utils.codeinspector.Matchers.proguardConfigurationRuleDoesNotMatch;
+import static com.android.tools.r8.utils.codeinspector.Matchers.typeVariableNotInScope;
+import static org.hamcrest.CoreMatchers.anyOf;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.TestParameters;
@@ -49,7 +52,12 @@
         .allowUnusedProguardConfigurationRules()
         .enableProtoShrinking(false)
         .setMinApi(AndroidApiLevel.N)
+        .allowDiagnosticInfoMessages()
         .compile()
+        .inspectDiagnosticMessages(
+            diagnostics ->
+                diagnostics.assertAllInfosMatch(
+                    anyOf(typeVariableNotInScope(), proguardConfigurationRuleDoesNotMatch())))
         .inspect(this::inspect);
   }
 
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerWithInstancePutCanBePostponedTest.java b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerWithInstancePutCanBePostponedTest.java
new file mode 100644
index 0000000..8d3cae4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerWithInstancePutCanBePostponedTest.java
@@ -0,0 +1,100 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.analysis.sideeffect;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverPropagateValue;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ir.analysis.sideeffect.SingletonClassInitializerPatternCanBePostponedTest.A;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class SingletonClassInitializerWithInstancePutCanBePostponedTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public SingletonClassInitializerWithInstancePutCanBePostponedTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableMemberValuePropagationAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello world!");
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject classSubject = inspector.clazz(A.class);
+    assertThat(classSubject, isPresent());
+    assertThat(classSubject.uniqueFieldWithName("INSTANCE"), isPresent());
+
+    // A.inlineable() should be inlined, but we should not synthesize an $r8$clinit field.
+    assertThat(classSubject.uniqueMethodWithName("inlineable"), not(isPresent()));
+    assertEquals(2, classSubject.allStaticFields().size());
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      A.inlineable();
+      System.out.println(A.getInstance().getMessage());
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+
+    private static A INSTANCE;
+
+    static {
+      A a = new A();
+      a.message = " world!";
+      INSTANCE = a;
+    }
+
+    @NeverPropagateValue private String message;
+
+    static void inlineable() {
+      System.out.print("Hello");
+    }
+
+    @NeverInline
+    static A getInstance() {
+      return INSTANCE;
+    }
+
+    @NeverInline
+    String getMessage() {
+      return message;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerWithInstancePutCannotBePostponedTest.java b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerWithInstancePutCannotBePostponedTest.java
new file mode 100644
index 0000000..5914a59
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/SingletonClassInitializerWithInstancePutCannotBePostponedTest.java
@@ -0,0 +1,109 @@
+// 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.analysis.sideeffect;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverPropagateValue;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class SingletonClassInitializerWithInstancePutCannotBePostponedTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public SingletonClassInitializerWithInstancePutCannotBePostponedTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableMemberValuePropagationAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello world!");
+  }
+
+  private void inspect(CodeInspector inspector) {
+    ClassSubject classSubject = inspector.clazz(A.class);
+    assertThat(classSubject, isPresent());
+    assertThat(classSubject.uniqueFieldWithName("INSTANCE"), isPresent());
+
+    // A.inlineable() should be inlined, but we should synthesize an $r8$clinit field.
+    assertThat(classSubject.uniqueMethodWithName("inlineable"), not(isPresent()));
+    assertEquals(2, classSubject.allStaticFields().size());
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      Environment.data = " world!";
+      // Triggers A.<clinit>(), which sets A.INSTANCE to a new instance with A.message=" world".
+      A.inlineable();
+      // Unset Environment.data, such that the following call to println() prints null if we failed
+      // to trigger A.<clinit>() above.
+      Environment.data = null;
+      System.out.println(A.getInstance().getMessage());
+    }
+  }
+
+  @NeverClassInline
+  static class A {
+
+    private static A INSTANCE;
+
+    static {
+      A a = new A();
+      a.message = Environment.data;
+      INSTANCE = a;
+    }
+
+    @NeverPropagateValue private String message;
+
+    static void inlineable() {
+      System.out.print("Hello");
+    }
+
+    @NeverInline
+    static A getInstance() {
+      return INSTANCE;
+    }
+
+    @NeverInline
+    String getMessage() {
+      return message;
+    }
+  }
+
+  static class Environment {
+
+    static String data;
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/checkcast/CheckCastInterfaceArrayTest.java b/src/test/java/com/android/tools/r8/ir/optimize/checkcast/CheckCastInterfaceArrayTest.java
index a8ef7a1..4431c5a 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/checkcast/CheckCastInterfaceArrayTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/checkcast/CheckCastInterfaceArrayTest.java
@@ -15,7 +15,7 @@
 @RunWith(Parameterized.class)
 public class CheckCastInterfaceArrayTest extends TestBase {
 
-  private static String EXPECTED = StringUtils.lines("A", "B");
+  private static final String EXPECTED = StringUtils.lines("A", "B");
 
   private final TestParameters parameters;
 
@@ -30,18 +30,10 @@
 
   @Test
   public void testJvmAndD8() throws Exception {
-    if (parameters.isCfRuntime()) {
-      testForJvm()
-          .addInnerClasses(CheckCastInterfaceArrayTest.class)
-          .run(parameters.getRuntime(), TestClass.class)
-          .assertSuccessWithOutput(EXPECTED);
-    } else {
-      testForD8()
-          .addInnerClasses(CheckCastInterfaceArrayTest.class)
-          .setMinApi(parameters.getApiLevel())
-          .run(parameters.getRuntime(), TestClass.class)
-          .assertSuccessWithOutput(EXPECTED);
-    }
+    testForRuntime(parameters)
+        .addProgramClasses(I.class, A.class, B.class, TestClass.class)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(EXPECTED);
   }
 
   @Test
@@ -58,6 +50,17 @@
         .assertSuccessWithOutput(EXPECTED);
   }
 
+  @Test
+  public void testJvmAndD8Throwing() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(I.class, A.class, B.class, TestClassError.class)
+        .run(parameters.getRuntime(), TestClassError.class)
+        .applyIf(
+            parameters.isDexRuntime(),
+            result -> result.assertFailureWithErrorThatThrows(VerifyError.class),
+            result -> result.assertSuccessWithOutput(EXPECTED));
+  }
+
   interface I {
     int id();
   }
@@ -98,4 +101,21 @@
       }
     }
   }
+
+  static class TestClassError {
+
+    public static I[] get(I kind) {
+      // Work around the ART bug by inserting an explicit check cast (fortunately javac keeps it).
+      return (kind.id() == A.id ? A.values() : B.values());
+    }
+
+    public static void main(String[] args) {
+      for (I i : get(A.A)) {
+        System.out.println(i);
+      }
+      for (I i : get(B.B)) {
+        System.out.println(i);
+      }
+    }
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/checkcast/TrivialArrayCheckCastTest.java b/src/test/java/com/android/tools/r8/ir/optimize/checkcast/TrivialArrayCheckCastTest.java
index 8cf8b26..b4d7969 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/checkcast/TrivialArrayCheckCastTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/checkcast/TrivialArrayCheckCastTest.java
@@ -27,7 +27,7 @@
     testForJvm().addTestClasspath().run(TestClass.class).assertSuccessWithOutput(expectedOutput);
 
     InternalOptions options = new InternalOptions();
-    options.minApiLevel = AndroidApiLevel.I_MR1.getLevel();
+    options.minApiLevel = AndroidApiLevel.I_MR1;
     assert options.canHaveArtCheckCastVerifierBug();
 
     testForR8(Backend.DEX)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/ZipFileInstanceOfAutoCloseableTest.java b/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/ZipFileInstanceOfAutoCloseableTest.java
index b15cca9..14d84e7 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/ZipFileInstanceOfAutoCloseableTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/instanceofremoval/ZipFileInstanceOfAutoCloseableTest.java
@@ -103,7 +103,7 @@
     assumeTrue(parameters.isDexRuntime());
     // Set the min API and create the raw app.
     InternalOptions options = new InternalOptions();
-    options.minApiLevel = parameters.getApiLevel().getLevel();
+    options.minApiLevel = parameters.getApiLevel();
     DirectMappedDexApplication application =
         new ApplicationReader(
                 AndroidApp.builder()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/switches/StringSwitchWitNonIntermediateIdValueTest.java b/src/test/java/com/android/tools/r8/ir/optimize/switches/StringSwitchWitNonIntermediateIdValueTest.java
new file mode 100644
index 0000000..213f02b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/switches/StringSwitchWitNonIntermediateIdValueTest.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.ir.optimize.switches;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class StringSwitchWitNonIntermediateIdValueTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public StringSwitchWitNonIntermediateIdValueTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    testForD8()
+        .addProgramClasses(Main.class)
+        .addOptionsModification(options -> options.minimumStringSwitchSize = 4)
+        .release()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::verifyRewrittenToIfs)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Foo", "Bar", "Baz", "Qux");
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class)
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(options -> options.minimumStringSwitchSize = 4)
+        .enableInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::verifyRewrittenToIfs)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Foo", "Bar", "Baz", "Qux");
+  }
+
+  private void verifyRewrittenToIfs(CodeInspector inspector) {
+    MethodSubject testMethodSubject = inspector.clazz(Main.class).uniqueMethodWithName("test");
+    assertThat(testMethodSubject, isPresent());
+    assertTrue(testMethodSubject.streamInstructions().noneMatch(InstructionSubject::isSwitch));
+    assertEquals(
+        3, testMethodSubject.streamInstructions().filter(InstructionSubject::isIf).count());
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      test("Foo");
+      test("Bar");
+      test("Baz");
+      test("Qux");
+    }
+
+    @NeverInline
+    static void test(String str) {
+      int hashCode = str.hashCode();
+      int id = 0;
+      outer:
+      {
+        int nonZeroId;
+        switch (hashCode) {
+          case 70822: // "Foo".hashCode()
+            if (str.equals("Foo")) {
+              nonZeroId = 1;
+              break;
+            }
+            break outer;
+          case 66547: // "Bar".hashCode()
+            if (str.equals("Bar")) {
+              nonZeroId = 2;
+              break;
+            }
+            break outer;
+          case 66555: // "Baz".hashCode()
+            if (str.equals("Baz")) {
+              nonZeroId = 3;
+              break;
+            }
+            break outer;
+          default:
+            break outer;
+        }
+        id = nonZeroId;
+      }
+      switch (id) {
+        case 1:
+          System.out.println("Foo");
+          break;
+        case 2:
+          System.out.println("Bar");
+          break;
+        case 3:
+          System.out.println("Baz");
+          break;
+        default:
+          System.out.println("Qux");
+          break;
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/NestedInterfaceMethodTest.java b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/NestedInterfaceMethodTest.java
index 3cbaed0..f07d6b6 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/NestedInterfaceMethodTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/uninstantiatedtypes/NestedInterfaceMethodTest.java
@@ -9,6 +9,7 @@
 
 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;
@@ -51,6 +52,7 @@
             .addKeepMainRule(TestClass.class)
             .enableInliningAnnotations()
             .enableNeverClassInliningAnnotations()
+            .enableNoHorizontalClassMergingAnnotations()
             .enableNoVerticalClassMergingAnnotations()
             .addOptionsModification(
                 options -> {
@@ -112,5 +114,6 @@
   @NeverClassInline
   static class C extends A {}
 
+  @NoHorizontalClassMerging
   static class Uninstantiated {}
 }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceRemovalTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceRemovalTest.java
index 9d11655..12bc414 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceRemovalTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceRemovalTest.java
@@ -85,6 +85,7 @@
     void foo();
   }
 
+  @NoHorizontalClassMerging
   @NoVerticalClassMerging
   interface J {
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceWithDefaultMethodTest.java b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceWithDefaultMethodTest.java
index 768fbd6..6bc0862 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceWithDefaultMethodTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/unusedinterfaces/UnusedInterfaceWithDefaultMethodTest.java
@@ -10,6 +10,7 @@
 
 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;
@@ -42,6 +43,7 @@
         .addKeepMainRule(TestClass.class)
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
         .compile()
@@ -86,6 +88,7 @@
     void m();
   }
 
+  @NoHorizontalClassMerging
   @NoVerticalClassMerging
   interface J extends I {
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java
index 548ce4d..049974c 100644
--- a/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java
@@ -100,12 +100,20 @@
 
   @Test
   public void testDontOptimize() throws Exception {
-    test(TestShrinkerBuilder::noOptimization);
+    test(
+        builder -> {
+          builder.allowDiagnosticInfoMessages();
+          builder.noOptimization();
+        });
   }
 
   @Test
   public void testDontObfuscate() throws Exception {
-    test(TestShrinkerBuilder::noMinification);
+    test(
+        builder -> {
+          builder.allowDiagnosticInfoMessages();
+          builder.noMinification();
+        });
   }
 
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataStripTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataStripTest.java
index 6406593..ca441cd 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataStripTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataStripTest.java
@@ -14,6 +14,7 @@
 
 import com.android.tools.r8.KotlinTestParameters;
 import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
 import com.android.tools.r8.utils.codeinspector.AnnotationSubject;
@@ -58,10 +59,12 @@
             .addKeepMainRule(mainClassName)
             .addKeepKotlinMetadata()
             .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
-            .allowDiagnosticWarningMessages()
+            .allowDiagnosticMessages()
             .setMinApi(parameters.getApiLevel())
             .allowUnusedDontWarnKotlinReflectJvmInternal(kotlinc.isNot(KOTLINC_1_3_72))
             .compile()
+            .assertNoErrorMessages()
+            .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation)
             .apply(KotlinMetadataTestBase::verifyExpectedWarningsFromKotlinReflectAndStdLib)
             .run(parameters.getRuntime(), mainClassName);
     CodeInspector inspector = result.inspector();
diff --git a/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java b/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java
index 9c01d63..8a27b3f 100644
--- a/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/optimize/switches/KotlinEnumSwitchTest.java
@@ -11,8 +11,12 @@
 import static org.junit.Assert.assertNotEquals;
 
 import com.android.tools.r8.KotlinCompilerTool;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompilerVersion;
 import com.android.tools.r8.KotlinTestBase;
 import com.android.tools.r8.KotlinTestParameters;
+import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestCompileResult;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -60,10 +64,18 @@
               options.enableEnumValueOptimization = enableSwitchMapRemoval;
               options.enableEnumSwitchMapRemoval = enableSwitchMapRemoval;
             })
-        .allowDiagnosticWarningMessages()
+        .applyIf(
+            kotlinParameters.getCompiler().is(KotlinCompilerVersion.KOTLINC_1_5_0_M2),
+            R8TestBuilder::allowDiagnosticMessages,
+            R8TestBuilder::allowDiagnosticWarningMessages)
         .setMinApi(parameters.getApiLevel())
         .noMinification()
         .compile()
+        .assertNoErrorMessages()
+        .applyIf(
+            kotlinParameters.getCompiler().is(KotlinCompilerVersion.KOTLINC_1_5_0_M2),
+            TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation,
+            TestCompileResult::assertNoInfoMessages)
         .assertAllWarningMessagesMatch(equalTo("Resource 'META-INF/MANIFEST.MF' already exists."))
         .inspect(
             inspector -> {
diff --git a/src/test/java/com/android/tools/r8/kotlin/reflection/KotlinReflectTest.java b/src/test/java/com/android/tools/r8/kotlin/reflection/KotlinReflectTest.java
index 4e376ed..6ee2517 100644
--- a/src/test/java/com/android/tools/r8/kotlin/reflection/KotlinReflectTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/reflection/KotlinReflectTest.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.DexIndexedConsumer.ArchiveConsumer;
 import com.android.tools.r8.KotlinTestBase;
 import com.android.tools.r8.KotlinTestParameters;
+import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.kotlin.metadata.KotlinMetadataTestBase;
@@ -91,11 +92,13 @@
         .setMinApi(parameters.getApiLevel())
         .addKeepAllClassesRule()
         .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
-        .allowDiagnosticWarningMessages()
+        .allowDiagnosticMessages()
         .allowUnusedDontWarnKotlinReflectJvmInternal(kotlinc.isNot(KOTLINC_1_3_72))
         .compile()
-        .writeToZip(foo.toPath())
+        .assertNoErrorMessages()
+        .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation)
         .apply(KotlinMetadataTestBase::verifyExpectedWarningsFromKotlinReflectAndStdLib)
+        .writeToZip(foo.toPath())
         .run(parameters.getRuntime(), PKG + ".SimpleReflectKt")
         .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
   }
diff --git a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
index a01f7dc..0005549 100644
--- a/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
+++ b/src/test/java/com/android/tools/r8/maindexlist/MainDexListTests.java
@@ -812,7 +812,7 @@
     Timing timing = Timing.empty();
     InternalOptions options =
         new InternalOptions(new DexItemFactory(), new Reporter(diagnosticsHandler));
-    options.minApiLevel = minApi;
+    options.minApiLevel = AndroidApiLevel.getAndroidApiLevel(minApi);
     options.intermediate = intermediate;
     DexItemFactory factory = options.itemFactory;
     AppView<?> appView = AppView.createForR8(DexApplication.builder(options, timing).build());
diff --git a/src/test/java/com/android/tools/r8/naming/AdaptResourceFileNamesTest.java b/src/test/java/com/android/tools/r8/naming/AdaptResourceFileNamesTest.java
index 85c8459..3897351 100644
--- a/src/test/java/com/android/tools/r8/naming/AdaptResourceFileNamesTest.java
+++ b/src/test/java/com/android/tools/r8/naming/AdaptResourceFileNamesTest.java
@@ -118,10 +118,6 @@
           for (DataEntryResource dataResource : getOriginalDataResources()) {
             ImmutableList<String> object =
                 dataResourceConsumer.get(getExpectedRenamingFor(dataResource.getName(), mapper));
-            if (object == null) {
-              object =
-                  dataResourceConsumer.get(getExpectedRenamingFor(dataResource.getName(), mapper));
-            }
             assertNotNull("Resource not renamed as expected: " + dataResource.getName(), object);
           }
         });
diff --git a/src/test/java/com/android/tools/r8/naming/MinifierClassSignatureTest.java b/src/test/java/com/android/tools/r8/naming/MinifierClassSignatureTest.java
index dc3acee..0aa7a87 100644
--- a/src/test/java/com/android/tools/r8/naming/MinifierClassSignatureTest.java
+++ b/src/test/java/com/android/tools/r8/naming/MinifierClassSignatureTest.java
@@ -330,7 +330,6 @@
                     internalOptions.testing.disableMappingToOriginalProgramVerification = true)
             .compile();
 
-    compileResult.assertNoInfoMessages();
     compileResult.assertNoErrorMessages();
 
     CodeInspector inspector = compileResult.inspector();
@@ -415,28 +414,6 @@
   }
 
   @Test
-  public void classSignatureOuter_valid() throws Exception {
-    // class Outer<T extends Simple> extends Base<T>
-    String signature = "<T:LSimple;>LBase<TT;>;";
-    testSingleClass(
-        "Outer",
-        signature,
-        TestDiagnosticMessages::assertNoWarnings,
-        inspector -> {
-          ClassSubject outer = inspector.clazz("Outer");
-          ClassSubject simple = inspector.clazz("Simple");
-          ClassSubject base = inspector.clazz("Base");
-          String baseDescriptorWithoutSemicolon =
-              base.getFinalDescriptor().substring(0, base.getFinalDescriptor().length() - 1);
-          String minifiedSignature =
-              "<T:" + simple.getFinalDescriptor() + ">" + baseDescriptorWithoutSemicolon + "<TT;>;";
-          assertEquals(minifiedSignature, outer.getFinalSignatureAttribute());
-          assertEquals(signature, outer.getOriginalSignatureAttribute());
-        },
-        false);
-  }
-
-  @Test
   public void classSignatureExtendsInner_valid() throws Exception {
     String signature = "LOuter<TT;>.Inner;";
     testSingleClass(
@@ -469,9 +446,9 @@
         inspector -> {
           assertThat(inspector.clazz("NotFound"), not(isPresent()));
           ClassSubject outer = inspector.clazz("Outer");
-          assertEquals(signature, outer.getOriginalSignatureAttribute());
+          assertNull(outer.getOriginalSignatureAttribute());
         },
-        false);
+        true);
   }
 
   @Test
@@ -484,8 +461,8 @@
         inspector -> {
           assertThat(inspector.clazz("NotFound"), not(isPresent()));
           ClassSubject outer = inspector.clazz("Outer$ExtendsInner");
-          // TODO(b/1867459990): What to do here.
-          assertEquals("LOuter$NotFound;", outer.getOriginalSignatureAttribute());
+          // TODO(b/186745999): What to do here.
+          assertNull(outer.getOriginalSignatureAttribute());
         },
         false);
   }
@@ -500,7 +477,7 @@
         inspector -> {
           assertThat(inspector.clazz("NotFound"), not(isPresent()));
           ClassSubject outer = inspector.clazz("Outer$ExtendsInner");
-          assertEquals(signature, outer.getOriginalSignatureAttribute());
+          assertNull(outer.getOriginalSignatureAttribute());
         },
         false);
   }
@@ -516,13 +493,13 @@
           assertThat(inspector.clazz("NotFound"), not(isPresent()));
           ClassSubject outer = inspector.clazz("Outer$ExtendsInner");
           // TODO(b/1867459990): What to do here.
-          assertEquals("LOuter$Inner$NotFound;", outer.getOriginalSignatureAttribute());
+          assertNull(outer.getOriginalSignatureAttribute());
         },
         false);
   }
 
   @Test
-  public void classSignatureExtendsInner_multipleMestedInnerClassesNotFound() throws Exception {
+  public void classSignatureExtendsInner_multipleNestedInnerClassesNotFound() throws Exception {
     String signature = "LOuter<TT;>.NotFound.AlsoNotFound;";
     testSingleClass(
         "Outer$ExtendsInner",
@@ -531,8 +508,7 @@
         inspector -> {
           assertThat(inspector.clazz("NotFound"), not(isPresent()));
           ClassSubject outer = inspector.clazz("Outer$ExtendsInner");
-          // TODO(b/1867459990): What to do here.
-          assertEquals("LOuter$NotFound$AlsoNotFound;", outer.getOriginalSignatureAttribute());
+          assertNull(outer.getOriginalSignatureAttribute());
         },
         false);
   }
diff --git a/src/test/java/com/android/tools/r8/regress/b69906048/Regress69906048Test.java b/src/test/java/com/android/tools/r8/regress/b69906048/Regress69906048Test.java
index 06c0ca4..bb1fed9 100644
--- a/src/test/java/com/android/tools/r8/regress/b69906048/Regress69906048Test.java
+++ b/src/test/java/com/android/tools/r8/regress/b69906048/Regress69906048Test.java
@@ -15,15 +15,16 @@
 
   @Test
   public void buildWithD8AndRunWithDalvikOrArt() throws Exception {
-    AndroidApp androidApp = ToolHelper.runR8(
-        ToolHelper.prepareR8CommandBuilder(
-            readClasses(ClassWithAnnotations.class, AnAnnotation.class))
-            .setDisableTreeShaking(true)
-            .setDisableMinification(true)
-            .addProguardConfiguration(
-                ImmutableList.of("-keepattributes *Annotation*"), Origin.unknown())
-            .build(),
-        options -> options.minApiLevel = ToolHelper.getMinApiLevelForDexVm().getLevel());
+    AndroidApp androidApp =
+        ToolHelper.runR8(
+            ToolHelper.prepareR8CommandBuilder(
+                    readClasses(ClassWithAnnotations.class, AnAnnotation.class))
+                .setDisableTreeShaking(true)
+                .setDisableMinification(true)
+                .addProguardConfiguration(
+                    ImmutableList.of("-keepattributes *Annotation*"), Origin.unknown())
+                .build(),
+            options -> options.minApiLevel = ToolHelper.getMinApiLevelForDexVm());
     String result = runOnArt(androidApp, ClassWithAnnotations.class);
     Assert.assertEquals("@" + AnAnnotation.class.getCanonicalName() + "()", result);
   }
diff --git a/src/test/java/com/android/tools/r8/regress/b77496850/B77496850.java b/src/test/java/com/android/tools/r8/regress/b77496850/B77496850.java
index f3ab264..03efc25 100644
--- a/src/test/java/com/android/tools/r8/regress/b77496850/B77496850.java
+++ b/src/test/java/com/android/tools/r8/regress/b77496850/B77496850.java
@@ -456,10 +456,10 @@
       throws Exception {
     AndroidApp app = readClasses(Path.class, Log.class, testClass);
     if (compiler == Tool.D8) {
-      app = compileWithD8(app, o -> o.minApiLevel = apiLevel.getLevel());
+      app = compileWithD8(app, o -> o.minApiLevel = apiLevel);
     } else {
       assert compiler == Tool.R8;
-      app = compileWithR8(app, "-keep class * { *; }", o -> o.minApiLevel = apiLevel.getLevel());
+      app = compileWithR8(app, "-keep class * { *; }", o -> o.minApiLevel = apiLevel);
     }
     checkPathParserMethods(app, testClass, a, b);
   }
@@ -479,10 +479,10 @@
       throws Exception {
     AndroidApp app = readClasses(testClass);
     if (compiler == Tool.D8) {
-      app = compileWithD8(app, o -> o.minApiLevel = apiLevel.getLevel());
+      app = compileWithD8(app, o -> o.minApiLevel = apiLevel);
     } else {
       assert compiler == Tool.R8;
-      app = compileWithR8(app, "-keep class * { *; }", o -> o.minApiLevel = apiLevel.getLevel());
+      app = compileWithR8(app, "-keep class * { *; }", o -> o.minApiLevel = apiLevel);
     }
     CodeInspector inspector = new CodeInspector(app);
     DexItemFactory factory = inspector.getFactory();
diff --git a/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java b/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
index 550356e..9947888 100644
--- a/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
@@ -5,6 +5,9 @@
 
 import static com.android.tools.r8.DiagnosticsMatcher.diagnosticException;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.proguardConfigurationRuleDoesNotMatch;
+import static com.android.tools.r8.utils.codeinspector.Matchers.typeVariableNotInScope;
+import static org.hamcrest.CoreMatchers.anyOf;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
@@ -12,21 +15,26 @@
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.R8;
+import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.utils.ListUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
 import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.android.tools.r8.utils.graphinspector.GraphInspector;
+import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Sets.SetView;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Set;
 import java.util.TreeSet;
@@ -77,9 +85,10 @@
         .addDontWarnGoogle()
         .addDontWarnJavax()
         .addDontWarn("org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement")
+        .allowDiagnosticInfoMessages()
         .compileWithExpectedDiagnostics(
-            diagnostics ->
-                diagnostics.assertErrorsMatch(diagnosticException(AssertionError.class)));
+            diagnostics -> diagnostics.assertErrorsMatch(diagnosticException(AssertionError.class)))
+        .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation);
   }
 
   @Test
@@ -92,7 +101,9 @@
         .addDontWarnGoogle()
         .addDontWarnJavax()
         .addDontWarn("org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement")
-        .compile();
+        .allowDiagnosticInfoMessages()
+        .compile()
+        .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation);
   }
 
   @Test
@@ -101,7 +112,11 @@
         .addProgramFiles(R8_JAR)
         .allowUnusedProguardConfigurationRules()
         .addKeepRules("-keepclasseswithmembers class * { @" + ABSENT_ANNOTATION + " *; }")
-        .compile()
+        .allowDiagnosticInfoMessages()
+        .compileWithExpectedDiagnostics(
+            diagnostics ->
+                diagnostics.assertAllInfosMatch(
+                    anyOf(typeVariableNotInScope(), proguardConfigurationRuleDoesNotMatch())))
         .inspect(inspector -> assertEquals(0, inspector.allClasses().size()));
   }
 
@@ -113,7 +128,9 @@
         .addDontWarnGoogle()
         .addDontWarnJavax()
         .addDontWarn("org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement")
+        .allowDiagnosticInfoMessages()
         .compile()
+        .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation)
         .inspect(
             inspector -> {
               ClassSubject clazz = inspector.clazz(CLASS_WITH_ANNOTATED_METHOD);
@@ -129,7 +146,9 @@
         // TODO(b/159971974): Technically this rule does not hit anything and should fail due to
         //  missing allowUnusedProguardConfigurationRules()
         .addKeepRules("-keepclassmembers class * { @" + PRESENT_ANNOTATION + " *** *(...); }")
+        .allowDiagnosticInfoMessages()
         .compile()
+        .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation)
         .inspect(inspector -> assertEquals(0, inspector.allClasses().size()));
   }
 
@@ -139,7 +158,9 @@
         .addProgramFiles(R8_JAR)
         .addKeepClassRules(CLASS_WITH_ANNOTATED_METHOD)
         .addKeepRules("-keepclassmembers class * { @" + PRESENT_ANNOTATION + " *** *(...); }")
+        .allowDiagnosticInfoMessages()
         .compile()
+        .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation)
         .inspect(
             inspector -> {
               assertEquals(1, inspector.allClasses().size());
@@ -162,7 +183,11 @@
         .addProgramFiles(R8_JAR)
         .allowUnusedProguardConfigurationRules()
         .addKeepRules("-if class * -keep class <1> { @" + PRESENT_ANNOTATION + " *** *(...); }")
-        .compile()
+        .allowDiagnosticInfoMessages()
+        .compileWithExpectedDiagnostics(
+            diagnostics ->
+                diagnostics.assertAllInfosMatch(
+                    anyOf(typeVariableNotInScope(), proguardConfigurationRuleDoesNotMatch())))
         .inspect(inspector -> assertEquals(0, inspector.allClasses().size()));
   }
 
@@ -172,7 +197,9 @@
         .addProgramFiles(R8_JAR)
         .addKeepClassRules(CLASS_WITH_ANNOTATED_METHOD)
         .addKeepRules("-if class * -keep class <1> { @" + PRESENT_ANNOTATION + " *** *(...); }")
+        .allowDiagnosticInfoMessages()
         .compile()
+        .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation)
         .inspect(
             inspector -> {
               assertEquals(1, inspector.allClasses().size());
@@ -204,7 +231,10 @@
             .addKeepRules("-keepclassmembers class * { @" + PRESENT_ANNOTATION + " *** *(...); }")
             .addDontWarnGoogle()
             .addDontWarnJavaxNullableAnnotation()
+            .allowDiagnosticInfoMessages()
+            .apply(this::configureHorizontalClassMerging)
             .compile()
+            .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation)
             .graphInspector();
 
     GraphInspector ifThenKeepClassMembersInspector =
@@ -222,7 +252,10 @@
                     + " *** *(...); }")
             .addDontWarnGoogle()
             .addDontWarnJavaxNullableAnnotation()
+            .apply(this::configureHorizontalClassMerging)
+            .allowDiagnosticInfoMessages()
             .compile()
+            .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation)
             .graphInspector();
     assertRetainedClassesEqual(referenceInspector, ifThenKeepClassMembersInspector);
 
@@ -241,7 +274,10 @@
                     + " *** *(...); }")
             .addDontWarnGoogle()
             .addDontWarnJavaxNullableAnnotation()
+            .apply(this::configureHorizontalClassMerging)
+            .allowDiagnosticInfoMessages()
             .compile()
+            .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation)
             .graphInspector();
     assertRetainedClassesEqual(referenceInspector, ifThenKeepClassesWithMembersInspector);
 
@@ -262,11 +298,29 @@
                     + " *** <2>(...); }")
             .addDontWarnGoogle()
             .addDontWarnJavaxNullableAnnotation()
+            .apply(this::configureHorizontalClassMerging)
+            .allowDiagnosticInfoMessages()
             .compile()
+            .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation)
             .graphInspector();
     assertRetainedClassesEqual(referenceInspector, ifHasMemberThenKeepClassInspector);
   }
 
+  private void configureHorizontalClassMerging(R8FullTestBuilder testBuilder) {
+    // Attempt to ensure similar class merging across different builds by choosing the merge target
+    // as the class with the lexicographically smallest original name.
+    testBuilder.addOptionsModification(
+        options ->
+            options.testing.horizontalClassMergingTarget =
+                (appView, candidates, target) -> {
+                  List<DexProgramClass> classes = Lists.newArrayList(candidates);
+                  classes.sort(
+                      Comparator.comparing(
+                          clazz -> appView.graphLens().getOriginalType(clazz.getType())));
+                  return ListUtils.first(classes);
+                });
+  }
+
   private void assertRetainedClassesEqual(
       GraphInspector referenceResult, GraphInspector conditionalResult) {
     assertRetainedClassesEqual(referenceResult, conditionalResult, false, false);
diff --git a/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java b/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java
index 59c649b..7f6323a 100644
--- a/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/ParameterTypeTest.java
@@ -11,10 +11,11 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotEquals;
 
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.jasmin.JasminBuilder;
@@ -33,16 +34,18 @@
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
 
+@NoHorizontalClassMerging
 interface B112452064SuperInterface1 {
   void foo();
 }
 
+@NoHorizontalClassMerging
 interface B112452064SuperInterface2 {
   void bar();
 }
 
-interface B112452064SubInterface extends B112452064SuperInterface1, B112452064SuperInterface2 {
-}
+@NoHorizontalClassMerging
+interface B112452064SubInterface extends B112452064SuperInterface1, B112452064SuperInterface2 {}
 
 class B112452064TestMain {
 
@@ -82,7 +85,8 @@
 
   @Parameters(name = "{1}, argument removal: {0}")
   public static List<Object[]> data() {
-    return buildParameters(BooleanUtils.values(), getTestParameters().withAllRuntimes().build());
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withAllRuntimesAndApiLevels().build());
   }
 
   public ParameterTypeTest(boolean enableArgumentRemoval, TestParameters parameters) {
@@ -128,7 +132,8 @@
                   options.enableUnusedInterfaceRemoval = enableUnusedInterfaceRemoval;
                   options.enableVerticalClassMerging = enableVerticalClassMerging;
                 })
-            .setMinApi(parameters.getRuntime())
+            .enableNoHorizontalClassMergingAnnotations()
+            .setMinApi(parameters.getApiLevel())
             .compile()
             .run(parameters.getRuntime(), B112452064TestMain.class)
             .assertSuccessWithOutput(javaResult.stdout)
@@ -286,7 +291,6 @@
         "return");
 
     final String mainClassName = mainClass.name;
-    String proguardConfig = keepMainProguardConfiguration(mainClassName, false, false);
 
     // Run input program on java.
     Path outputDirectory = temp.newFolder().toPath();
@@ -296,36 +300,34 @@
     assertThat(javaResult.stdout, containsString(bar.name));
     assertEquals(-1, javaResult.stderr.indexOf("ClassNotFoundException"));
 
-    AndroidApp processedApp =
-        compileWithR8(
-            jasminBuilder.build(),
-            proguardConfig,
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(jasminBuilder.buildClasses())
+        .addKeepMainRule(mainClassName)
+        .addOptionsModification(
             options -> {
               // Disable inlining to avoid the (short) tested method from being inlined and removed.
               options.enableInlining = false;
               options.enableArgumentRemoval = enableArgumentRemoval;
-            });
-
-    // Run processed (output) program on ART
-    ProcessResult artResult = runOnArtRaw(processedApp, mainClassName);
-    if (enableArgumentRemoval) {
-      assertEquals(0, artResult.exitCode);
-    } else {
-      assertNotEquals(0, artResult.exitCode);
-
-      DexVm.Version currentVersion = ToolHelper.getDexVm().getVersion();
-      String errorMessage =
-          currentVersion.isNewerThan(Version.V4_4_4)
-              ? "type Precise Reference: Foo[] but expected Reference: SubInterface[]"
-              : "[LFoo; is not instance of [LSubInterface;";
-      assertThat(artResult.stderr, containsString(errorMessage));
-    }
-
-    assertEquals(-1, artResult.stderr.indexOf("ClassNotFoundException"));
-
-    CodeInspector inspector = new CodeInspector(processedApp);
-    ClassSubject subSubject = inspector.clazz(sub.name);
-    assertNotEquals(enableArgumentRemoval, subSubject.isPresent());
+            })
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject subSubject = inspector.clazz(sub.name);
+              assertNotEquals(enableArgumentRemoval, subSubject.isPresent());
+            })
+        .run(parameters.getRuntime(), mainClassName)
+        .applyIf(
+            enableArgumentRemoval || parameters.isCfRuntime(),
+            SingleTestRunResult::assertSuccess,
+            result ->
+                result.assertFailureWithErrorThatMatches(
+                    containsString(
+                        parameters.getDexRuntimeVersion().isNewerThan(Version.V4_4_4)
+                            ? "type Precise Reference: Foo[] but expected Reference: SubInterface[]"
+                            : "[LFoo; is not instance of [LSubInterface;")))
+        .assertStderrMatches(not(containsString("ClassNotFoundException")));
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializationTriggersIndirectInterfaceInitializationTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializationTriggersIndirectInterfaceInitializationTest.java
index c6e5eff..b427736 100644
--- a/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializationTriggersIndirectInterfaceInitializationTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/ClassInitializationTriggersIndirectInterfaceInitializationTest.java
@@ -12,6 +12,7 @@
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoUnusedInterfaceRemoval;
 import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.TestBase;
@@ -43,6 +44,7 @@
         .addKeepMainRule(Main.class)
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
@@ -79,6 +81,7 @@
     }
   }
 
+  @NoHorizontalClassMerging
   @NoUnusedInterfaceRemoval
   @NoVerticalClassMerging
   interface I {
@@ -94,6 +97,7 @@
     }
   }
 
+  @NoHorizontalClassMerging
   @NoUnusedInterfaceRemoval
   @NoVerticalClassMerging
   interface J extends I {}
diff --git a/src/test/java/com/android/tools/r8/shaking/keptgraph/WhyAreYouKeepingAllTest.java b/src/test/java/com/android/tools/r8/shaking/keptgraph/WhyAreYouKeepingAllTest.java
index 94bdbf1..ac22ab7 100644
--- a/src/test/java/com/android/tools/r8/shaking/keptgraph/WhyAreYouKeepingAllTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/keptgraph/WhyAreYouKeepingAllTest.java
@@ -48,7 +48,9 @@
         .addKeepRuleFiles(MAIN_KEEP)
         .addKeepRules(WHY_ARE_YOU_KEEPING_ALL)
         .collectStdout()
+        .allowDiagnosticInfoMessages()
         .compile()
+        .apply(TestBase::verifyAllInfoFromGenericSignatureTypeParameterValidation)
         .assertStdoutThatMatches(containsString("referenced in keep rule"))
         // TODO(b/124655065): We should always know the reason for keeping.
         // It is OK if this starts failing while the kept-graph API is incomplete, in which case
diff --git a/src/test/java/com/android/tools/r8/testing/ToolHelperTest.java b/src/test/java/com/android/tools/r8/testing/ToolHelperTest.java
index b0e1ab2..2353e6c 100644
--- a/src/test/java/com/android/tools/r8/testing/ToolHelperTest.java
+++ b/src/test/java/com/android/tools/r8/testing/ToolHelperTest.java
@@ -30,7 +30,8 @@
         ToolHelper.getFirstSupportedAndroidJar(AndroidApiLevel.K_WATCH), AndroidApiLevel.L);
     // All android.jar's for API level L are present.
     for (AndroidApiLevel androidApiLevel : AndroidApiLevel.values()) {
-      if (androidApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.L)) {
+      if (androidApiLevel.isGreaterThanOrEqualTo(AndroidApiLevel.L)
+          && androidApiLevel.isLessThanOrEqualTo(AndroidApiLevel.LATEST)) {
         checkExpectedAndroidJar(
             ToolHelper.getFirstSupportedAndroidJar(androidApiLevel), androidApiLevel);
       }
diff --git a/src/test/java/com/android/tools/r8/utils/Smali.java b/src/test/java/com/android/tools/r8/utils/Smali.java
index eb269c1..d698e0b 100644
--- a/src/test/java/com/android/tools/r8/utils/Smali.java
+++ b/src/test/java/com/android/tools/r8/utils/Smali.java
@@ -108,7 +108,7 @@
     SingleFileConsumer consumer = new SingleFileConsumer();
     AndroidApp app = AndroidApp.builder().addDexProgramData(data, Origin.unknown()).build();
     InternalOptions options = new InternalOptions();
-    options.minApiLevel = apiLevel;
+    options.minApiLevel = AndroidApiLevel.getAndroidApiLevel(apiLevel);
     options.programConsumer = consumer;
     ExecutorService executor = ThreadUtils.getExecutorService(1);
     try {
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java
index 0410bc0..8179e48 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java
@@ -10,6 +10,7 @@
 import static org.junit.Assert.fail;
 
 import com.android.tools.r8.TestBase;
+import com.android.tools.r8.ThrowableConsumer;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
@@ -21,6 +22,7 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 import java.util.function.BiConsumer;
@@ -32,6 +34,8 @@
   private final DexItemFactory dexItemFactory;
   private final HorizontallyMergedClasses horizontallyMergedClasses;
 
+  private final Set<ClassReference> seen = new HashSet<>();
+
   public HorizontallyMergedClassesInspector(
       DexItemFactory dexItemFactory, HorizontallyMergedClasses horizontallyMergedClasses) {
     this.dexItemFactory = dexItemFactory;
@@ -65,6 +69,14 @@
     return horizontallyMergedClasses.getTargets();
   }
 
+  public HorizontallyMergedClassesInspector applyIf(
+      boolean condition, ThrowableConsumer<HorizontallyMergedClassesInspector> consumer) {
+    if (condition) {
+      consumer.acceptWithRuntimeException(this);
+    }
+    return this;
+  }
+
   public HorizontallyMergedClassesInspector assertMergedInto(Class<?> from, Class<?> target) {
     assertEquals(
         horizontallyMergedClasses.getMergeTargetOrDefault(toDexType(from)), toDexType(target));
@@ -102,6 +114,7 @@
             + StringUtils.join(", ", unmerged, DexType::getTypeName),
         0,
         unmerged.size());
+    seen.addAll(types.stream().map(DexType::asClassReference).collect(Collectors.toList()));
     return this;
   }
 
@@ -117,6 +130,17 @@
     return this;
   }
 
+  public HorizontallyMergedClassesInspector assertNoOtherClassesMerged() {
+    horizontallyMergedClasses.forEachMergeGroup(
+        (sources, target) -> {
+          for (DexType source : sources) {
+            assertTrue(source.getTypeName(), seen.contains(source.asClassReference()));
+          }
+          assertTrue(target.getTypeName(), seen.contains(target.asClassReference()));
+        });
+    return this;
+  }
+
   public HorizontallyMergedClassesInspector assertClassesNotMerged(Class<?>... classes) {
     return assertClassesNotMerged(Arrays.asList(classes));
   }
@@ -145,6 +169,7 @@
       assertTrue(type.isClassType());
       assertFalse(horizontallyMergedClasses.hasBeenMergedOrIsMergeTarget(type));
     }
+    seen.addAll(types.stream().map(DexType::asClassReference).collect(Collectors.toList()));
     return this;
   }
 
@@ -204,6 +229,7 @@
         classReferences.size() - 1,
         sources.size());
     assertTrue(types.containsAll(sources));
+    seen.addAll(classReferences);
     return this;
   }
 
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java b/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
index 3e70473..4f4b656 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/Matchers.java
@@ -4,10 +4,13 @@
 
 package com.android.tools.r8.utils.codeinspector;
 
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.Collectors;
+import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.DiagnosticsMatcher;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AccessFlags;
 import com.android.tools.r8.graph.DexClass;
@@ -220,6 +223,15 @@
     };
   }
 
+  public static Matcher<Diagnostic> typeVariableNotInScope() {
+    return DiagnosticsMatcher.diagnosticMessage(containsString("A type variable is not in scope"));
+  }
+
+  public static Matcher<Diagnostic> proguardConfigurationRuleDoesNotMatch() {
+    return DiagnosticsMatcher.diagnosticMessage(
+        containsString("Proguard configuration rule does not match anything"));
+  }
+
   public static Matcher<Subject> isSynthetic() {
     return new TypeSafeMatcher<Subject>() {
       @Override
diff --git a/src/test/java/com/android/tools/r8/workaround/ExtendingInterfaceArrayCheckCastTest.java b/src/test/java/com/android/tools/r8/workaround/ExtendingInterfaceArrayCheckCastTest.java
new file mode 100644
index 0000000..eee79f0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/workaround/ExtendingInterfaceArrayCheckCastTest.java
@@ -0,0 +1,78 @@
+// 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.workaround;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+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 ExtendingInterfaceArrayCheckCastTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final String[] EXPECTED = new String[] {"Hello World!"};
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ExtendingInterfaceArrayCheckCastTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addInnerClasses(getClass())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  public static class Utility {
+
+    public static <T extends Base<T> & I> void get(T enumType) {
+      // If using I[] as the local variable type, the compiler will automatically insert a checkcast
+      // and therefore circumvent b/188112948. Another option is to explicitly insert a checkcast.
+      I[] params = enumType.getParams();
+      tryGet(params);
+    }
+
+    // this will become void tryGet(I[] value) when compiled due to the extends.
+    public static <T extends I> void tryGet(T[] value) {
+      System.out.println("Hello World!");
+    }
+  }
+
+  public interface I {
+    void call();
+  }
+
+  public static class Base<T extends Base<T>> {
+
+    T[] getParams() {
+      return null;
+    }
+  }
+
+  public static class Foo extends Base<Foo> implements I {
+
+    @Override
+    public void call() {
+      System.out.println("I::Foo");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      Utility.get(new Foo());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/workaround/ExtendingInterfaceArrayMissingCheckCastTest.java b/src/test/java/com/android/tools/r8/workaround/ExtendingInterfaceArrayMissingCheckCastTest.java
new file mode 100644
index 0000000..971fac1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/workaround/ExtendingInterfaceArrayMissingCheckCastTest.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.workaround;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+// This is a reproduction of b/188112948.
+public class ExtendingInterfaceArrayMissingCheckCastTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final String[] EXPECTED = new String[] {"Hello World!"};
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ExtendingInterfaceArrayMissingCheckCastTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addInnerClasses(getClass())
+        .run(parameters.getRuntime(), Main.class)
+        .applyIf(
+            parameters.isDexRuntime(),
+            // TODO(b/188112948): This should not throw a verification error.
+            result -> result.assertFailureWithErrorThatThrows(VerifyError.class),
+            result -> result.assertSuccessWithOutputLines(EXPECTED));
+  }
+
+  public static class Utility {
+
+    public static <T extends Base<T> & I> void get(T enumType) {
+      // JDK 8 will add a checkcast [I such that the [Base array is cast to the correct type.
+      // JDK 9 and later will not add the checkcast.
+      tryGet(enumType.getParams());
+    }
+
+    // this will become void tryGet(I[] value) when compiled due to the extends.
+    public static <T extends I> void tryGet(T[] value) {
+      System.out.println("Hello World!");
+    }
+  }
+
+  public interface I {
+    void call();
+  }
+
+  public static class Base<T extends Base<T>> {
+
+    T[] getParams() {
+      return null;
+    }
+  }
+
+  public static class Foo extends Base<Foo> implements I {
+
+    @Override
+    public void call() {
+      System.out.println("I::Foo");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      Utility.get(new Foo());
+    }
+  }
+}