Merge commit '90e4860801ffe94a6e91587747fe7105a4cd4740' into dev-release
diff --git a/.gitignore b/.gitignore
index b929f34..b03bb7b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,6 +29,8 @@
 tests/2016-12-19/art.tar.gz
 tests/2017-10-04/art
 tests/2017-10-04/art.tar.gz
+third_party/api-outlining/simple-app-dump
+third_party/api-outlining/simple-app-dump.tar.gz
 third_party/android_cts_baseline
 third_party/android_cts_baseline.tar.gz
 third_party/clank/clank_google3_prebuilt
diff --git a/build.gradle b/build.gradle
index 8e20e6d..3101886 100644
--- a/build.gradle
+++ b/build.gradle
@@ -315,6 +315,7 @@
                 "android_jar/lib-v29",
                 "android_jar/lib-v30",
                 "android_jar/lib-v31",
+                "api-outlining/simple-app-dump",
                 "core-lambda-stubs",
                 "dart-sdk",
                 "ddmlib",
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 81a8f82..2fadb7d 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8;
 
 import static com.android.tools.r8.D8Command.USAGE_MESSAGE;
+import static com.android.tools.r8.utils.AssertionUtils.forTesting;
 import static com.android.tools.r8.utils.ExceptionUtils.unwrapExecutionException;
 
 import com.android.tools.r8.dex.ApplicationReader;
@@ -186,6 +187,9 @@
       // Disable global optimizations.
       options.disableGlobalOptimizations();
 
+      // Synthetic assertion to check that testing assertions works and can be enabled.
+      assert forTesting(options, () -> !options.testing.testEnableTestAssertions);
+
       AppView<AppInfo> appView = readApp(inputApp, options, executor, timing);
       SyntheticItems.collectSyntheticInputs(appView);
 
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index f1ed0cd..8863e35 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.errors.DexFileOverflowDiagnostic;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.inspector.Inspector;
 import com.android.tools.r8.inspector.internal.InspectorImpl;
 import com.android.tools.r8.ir.desugar.DesugaredLibraryConfiguration;
@@ -24,6 +25,7 @@
 import com.android.tools.r8.utils.DumpInputFlags;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.DesugarState;
+import com.android.tools.r8.utils.InternalOptions.HorizontalClassMergerOptions;
 import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringDiagnostic;
@@ -473,7 +475,6 @@
     // Assert some of R8 optimizations are disabled.
     assert !internal.enableInlining;
     assert !internal.enableClassInlining;
-    assert internal.horizontalClassMergerOptions().isDisabled();
     assert !internal.enableVerticalClassMerging;
     assert !internal.enableClassStaticizer;
     assert !internal.enableEnumValueOptimization;
@@ -481,6 +482,13 @@
     assert !internal.enableValuePropagation;
     assert !internal.enableTreeShakingOfLibraryMethodOverrides;
 
+    // TODO(b/187675788): Enable class merging for synthetics in D8.
+    HorizontalClassMergerOptions horizontalClassMergerOptions =
+        internal.horizontalClassMergerOptions();
+    horizontalClassMergerOptions.disable();
+    assert !horizontalClassMergerOptions.isEnabled(HorizontalClassMerger.Mode.INITIAL);
+    assert !horizontalClassMergerOptions.isEnabled(HorizontalClassMerger.Mode.FINAL);
+
     internal.desugarState = getDesugarState();
     internal.encodeChecksums = getIncludeClassesChecksum();
     internal.dexClassChecksumFilter = getDexClassChecksumFilter();
diff --git a/src/main/java/com/android/tools/r8/L8Command.java b/src/main/java/com/android/tools/r8/L8Command.java
index ec9955d..fc9a567 100644
--- a/src/main/java/com/android/tools/r8/L8Command.java
+++ b/src/main/java/com/android/tools/r8/L8Command.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.errors.DexFileOverflowDiagnostic;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.inspector.Inspector;
 import com.android.tools.r8.ir.desugar.DesugaredLibraryConfiguration;
 import com.android.tools.r8.origin.Origin;
@@ -18,6 +19,7 @@
 import com.android.tools.r8.utils.DumpInputFlags;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.DesugarState;
+import com.android.tools.r8.utils.InternalOptions.HorizontalClassMergerOptions;
 import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringDiagnostic;
@@ -172,7 +174,6 @@
     // Assert some of R8 optimizations are disabled.
     assert !internal.enableInlining;
     assert !internal.enableClassInlining;
-    assert internal.horizontalClassMergerOptions().isDisabled();
     assert !internal.enableVerticalClassMerging;
     assert !internal.enableClassStaticizer;
     assert !internal.enableEnumValueOptimization;
@@ -180,6 +181,12 @@
     assert !internal.enableValuePropagation;
     assert !internal.enableTreeShakingOfLibraryMethodOverrides;
 
+    HorizontalClassMergerOptions horizontalClassMergerOptions =
+        internal.horizontalClassMergerOptions();
+    horizontalClassMergerOptions.disable();
+    assert !horizontalClassMergerOptions.isEnabled(HorizontalClassMerger.Mode.INITIAL);
+    assert !horizontalClassMergerOptions.isEnabled(HorizontalClassMerger.Mode.FINAL);
+
     assert internal.desugarState == DesugarState.ON;
     assert internal.enableInheritanceClassInDexDistributor;
     internal.enableInheritanceClassInDexDistributor = false;
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index e58264e..7aed2af 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8;
 
 import static com.android.tools.r8.R8Command.USAGE_MESSAGE;
+import static com.android.tools.r8.utils.AssertionUtils.forTesting;
 import static com.android.tools.r8.utils.ExceptionUtils.unwrapExecutionException;
 
 import com.android.tools.r8.cf.code.CfInstruction;
@@ -41,8 +42,6 @@
 import com.android.tools.r8.graph.analysis.InitializedClassesInInstanceMethodsAnalysis;
 import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
-import com.android.tools.r8.horizontalclassmerging.HorizontalClassMergerResult;
-import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
 import com.android.tools.r8.inspector.internal.InspectorImpl;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.desugar.BackportedMethodRewriter;
@@ -282,6 +281,8 @@
           new StringDiagnostic(
               "Running R8 version " + Version.LABEL + " with assertions enabled."));
     }
+    // Synthetic assertion to check that testing assertions works and can be enabled.
+    assert forTesting(options, () -> !options.testing.testEnableTestAssertions);
     try {
       AppView<AppInfoWithClassHierarchy> appView;
       {
@@ -323,7 +324,7 @@
       List<ProguardConfigurationRule> synthesizedProguardRules = new ArrayList<>();
       timing.begin("Strip unused code");
       RuntimeTypeCheckInfo.Builder classMergingEnqueuerExtensionBuilder =
-          new RuntimeTypeCheckInfo.Builder(appView.dexItemFactory());
+          new RuntimeTypeCheckInfo.Builder(appView);
       try {
         // Add synthesized -assumenosideeffects from min api if relevant.
         if (options.isGeneratingDex()) {
@@ -453,7 +454,10 @@
       boolean isKotlinLibraryCompilationWithInlinePassThrough =
           options.enableCfByteCodePassThrough && appView.hasCfByteCodePassThroughMethods();
 
-      RuntimeTypeCheckInfo runtimeTypeCheckInfo = classMergingEnqueuerExtensionBuilder.build();
+      RuntimeTypeCheckInfo runtimeTypeCheckInfo =
+          classMergingEnqueuerExtensionBuilder.build(appView.graphLens());
+      classMergingEnqueuerExtensionBuilder = null;
+
       if (!isKotlinLibraryCompilationWithInlinePassThrough
           && options.getProguardConfiguration().isOptimizing()) {
         if (options.enableVerticalClassMerging) {
@@ -499,33 +503,9 @@
             timing.end();
           }
         }
-        if (options.horizontalClassMergerOptions().isEnabled()
-            && options.enableInlining
-            && options.isShrinking()) {
-          timing.begin("HorizontalClassMerger");
-          HorizontalClassMerger merger = new HorizontalClassMerger(appViewWithLiveness);
-          HorizontalClassMergerResult horizontalClassMergerResult =
-              merger.run(runtimeTypeCheckInfo, timing);
-          if (horizontalClassMergerResult != null) {
-            // Must rewrite AppInfoWithLiveness before pruning the merged classes, to ensure that
-            // allocations sites, fields accesses, etc. are correctly transferred to the target
-            // classes.
-            appView.rewriteWithLens(horizontalClassMergerResult.getGraphLens());
-            horizontalClassMergerResult
-                .getFieldAccessInfoCollectionModifier()
-                .modify(appViewWithLiveness);
 
-            appView.pruneItems(
-                PrunedItems.builder()
-                    .setPrunedApp(appView.appInfo().app())
-                    .addRemovedClasses(appView.horizontallyMergedClasses().getSources())
-                    .addNoLongerSyntheticItems(appView.horizontallyMergedClasses().getSources())
-                    .build());
-          }
-          timing.end();
-        } else {
-          appView.setHorizontallyMergedClasses(HorizontallyMergedClasses.empty());
-        }
+        HorizontalClassMerger.createForInitialClassMerging(appViewWithLiveness)
+            .runIfNecessary(runtimeTypeCheckInfo, timing);
       }
 
       // Clear traced methods roots to not hold on to the main dex live method set.
@@ -596,6 +576,10 @@
                   new SubtypingInfo(appView),
                   keptGraphConsumer,
                   prunedTypes);
+          if (options.isClassMergingExtensionRequired(enqueuer.getMode())) {
+            classMergingEnqueuerExtensionBuilder = new RuntimeTypeCheckInfo.Builder(appView);
+            classMergingEnqueuerExtensionBuilder.attach(enqueuer);
+          }
           EnqueuerResult enqueuerResult =
               enqueuer.traceApplication(appView.rootSet(), executorService, timing);
           appView.setAppInfo(enqueuerResult.getAppInfo());
@@ -730,6 +714,15 @@
         SyntheticFinalization.finalizeWithClassHierarchy(appView);
       }
 
+      // Run horizontal class merging. This runs even if shrinking is disabled to ensure synthetics
+      // are always merged.
+      HorizontalClassMerger.createForFinalClassMerging(appView)
+          .runIfNecessary(
+              classMergingEnqueuerExtensionBuilder != null
+                  ? classMergingEnqueuerExtensionBuilder.build(appView.graphLens())
+                  : null,
+              timing);
+
       // Perform minification.
       NamingLens namingLens;
       if (options.getProguardConfiguration().hasApplyMappingFile()) {
@@ -808,7 +801,6 @@
           .runIfNecessary(timing);
 
       // Generate the resulting application resources.
-      // TODO(b/165783399): Apply the graph lens to all instructions in the CF and DEX backends.
       writeApplication(
           executorService,
           appView,
@@ -968,7 +960,7 @@
               appView.dexItemFactory(), OptimizationFeedbackSimple.getInstance()));
     }
 
-    if (options.isClassMergingExtensionRequired()) {
+    if (options.isClassMergingExtensionRequired(enqueuer.getMode())) {
       classMergingEnqueuerExtensionBuilder.attach(enqueuer);
     }
 
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index e3de56c..4fdb44d 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -32,6 +32,7 @@
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.DesugarState;
+import com.android.tools.r8.utils.InternalOptions.HorizontalClassMergerOptions;
 import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringDiagnostic;
@@ -854,8 +855,11 @@
             ? LineNumberOptimization.ON
             : LineNumberOptimization.OFF;
 
+    HorizontalClassMergerOptions horizontalClassMergerOptions =
+        internal.horizontalClassMergerOptions();
     assert proguardConfiguration.isOptimizing()
-        || internal.horizontalClassMergerOptions().isDisabled();
+        || horizontalClassMergerOptions.isRestrictedToSynthetics();
+
     assert !internal.enableTreeShakingOfLibraryMethodOverrides;
     assert internal.enableVerticalClassMerging || !proguardConfiguration.isOptimizing();
     if (internal.debug) {
@@ -864,26 +868,19 @@
       internal.getProguardConfiguration().getKeepAttributes().localVariableTypeTable = true;
       internal.enableInlining = false;
       internal.enableClassInlining = false;
-      internal.horizontalClassMergerOptions().disable();
       internal.enableVerticalClassMerging = false;
       internal.enableClassStaticizer = false;
       internal.outline.enabled = false;
       internal.enableEnumUnboxing = false;
     }
+
     if (!internal.isShrinking()) {
       // If R8 is not shrinking, there is no point in running various optimizations since the
       // optimized classes will still remain in the program (the application size could increase).
       internal.enableEnumUnboxing = false;
-      internal.horizontalClassMergerOptions().disable();
       internal.enableVerticalClassMerging = false;
     }
 
-    if (!internal.enableInlining) {
-      // If R8 cannot perform inlining, then the synthetic constructors would not inline the called
-      // constructors, producing invalid code.
-      internal.horizontalClassMergerOptions().disable();
-    }
-
     // Amend the proguard-map consumer with options from the proguard configuration.
     internal.proguardMapConsumer =
         wrapStringConsumer(
@@ -939,7 +936,7 @@
     if (internal.isGeneratingClassFiles()) {
       internal.outline.enabled = false;
       internal.enableEnumUnboxing = false;
-      internal.horizontalClassMergerOptions().disable();
+      horizontalClassMergerOptions.disable();
     }
 
     // EXPERIMENTAL flags.
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfArithmeticBinop.java b/src/main/java/com/android/tools/r8/cf/code/CfArithmeticBinop.java
index e102cf7..bf66099 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfArithmeticBinop.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfArithmeticBinop.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.cf.code.CfFrame.FrameType;
 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.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -194,7 +195,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forBinop();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfArrayLength.java b/src/main/java/com/android/tools/r8/cf/code/CfArrayLength.java
index 4fa3241..21e74a3 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfArrayLength.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfArrayLength.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -69,7 +70,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forArrayLength();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfArrayLoad.java b/src/main/java/com/android/tools/r8/cf/code/CfArrayLoad.java
index 4f851a0..3385529 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfArrayLoad.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfArrayLoad.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.cf.code.CfFrame.FrameType;
 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.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -115,7 +116,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forArrayGet();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfArrayStore.java b/src/main/java/com/android/tools/r8/cf/code/CfArrayStore.java
index cc0cd63..e4350e0 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfArrayStore.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfArrayStore.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.cf.code.CfFrame.FrameType;
 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.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -105,7 +106,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forArrayPut();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfCheckCast.java b/src/main/java/com/android/tools/r8/cf/code/CfCheckCast.java
index 1e25ca7..5ed4009 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfCheckCast.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfCheckCast.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -105,7 +106,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forCheckCast(type, context);
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfCmp.java b/src/main/java/com/android/tools/r8/cf/code/CfCmp.java
index 9572e12..e1a5ce5 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfCmp.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfCmp.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.cf.code.CfFrame.FrameType;
 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.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -119,7 +120,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forBinop();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstClass.java b/src/main/java/com/android/tools/r8/cf/code/CfConstClass.java
index a6a0933..b7524be 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstClass.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstClass.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.cf.CfPrinter;
 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.CfCompareHelper;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -128,7 +129,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forConstClass(type, context);
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstMethodHandle.java b/src/main/java/com/android/tools/r8/cf/code/CfConstMethodHandle.java
index 16f5e9a..7391677 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstMethodHandle.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstMethodHandle.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -90,7 +91,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forConstMethodHandle();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstMethodType.java b/src/main/java/com/android/tools/r8/cf/code/CfConstMethodType.java
index 5bb9849..e85db92 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstMethodType.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstMethodType.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -88,7 +89,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forConstMethodType();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstNull.java b/src/main/java/com/android/tools/r8/cf/code/CfConstNull.java
index 185981e..f04dcfe 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstNull.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstNull.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -61,7 +62,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forConstInstruction();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstNumber.java b/src/main/java/com/android/tools/r8/cf/code/CfConstNumber.java
index 7484e80..759568d 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstNumber.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstNumber.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.cf.CfPrinter;
 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.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -164,7 +165,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forConstInstruction();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstString.java b/src/main/java/com/android/tools/r8/cf/code/CfConstString.java
index 376edfe..9a079e7 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstString.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstString.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexString;
@@ -91,7 +92,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forConstInstruction();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfDexItemBasedConstString.java b/src/main/java/com/android/tools/r8/cf/code/CfDexItemBasedConstString.java
index 75c5eda..924fa7e 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfDexItemBasedConstString.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfDexItemBasedConstString.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.cf.CfPrinter;
 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.CfCompareHelper;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -110,7 +111,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forDexItemBasedConstString(item, context);
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java b/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java
index 2e8eedf..592ee31 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.cf.CfPrinter;
 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.CfCompareHelper;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexField;
@@ -165,7 +166,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     switch (opcode) {
       case Opcodes.GETSTATIC:
         return inliningConstraints.forStaticGet(field, context);
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfFrame.java b/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
index e55bf9a..fbeda56 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfFrame.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.cf.CfPrinter;
 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.CfCodeStackMapValidatingException;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -466,7 +467,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return ConstraintWithTarget.ALWAYS;
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfGoto.java b/src/main/java/com/android/tools/r8/cf/code/CfGoto.java
index b3043c9..6c434c4 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfGoto.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfGoto.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -86,7 +87,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forJumpInstruction();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfIf.java b/src/main/java/com/android/tools/r8/cf/code/CfIf.java
index 7653d51..a3ad1cc 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfIf.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfIf.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.cf.CfPrinter;
 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.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -122,7 +123,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forJumpInstruction();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfIfCmp.java b/src/main/java/com/android/tools/r8/cf/code/CfIfCmp.java
index 3e5c38a..e42d7b2 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfIfCmp.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfIfCmp.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.cf.CfPrinter;
 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.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -123,7 +124,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forJumpInstruction();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfIinc.java b/src/main/java/com/android/tools/r8/cf/code/CfIinc.java
index 933dfcd..ea9e810 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfIinc.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfIinc.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -83,7 +84,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return ConstraintWithTarget.ALWAYS;
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInitClass.java b/src/main/java/com/android/tools/r8/cf/code/CfInitClass.java
index 0b8240a..8eb0db7 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInitClass.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInitClass.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexField;
@@ -103,7 +104,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forInitClass(clazz, context);
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInstanceOf.java b/src/main/java/com/android/tools/r8/cf/code/CfInstanceOf.java
index b4d6d43..8eab765 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInstanceOf.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInstanceOf.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -113,7 +114,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forInstanceOf(type, context);
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInstruction.java b/src/main/java/com/android/tools/r8/cf/code/CfInstruction.java
index a80dc16..6067cc3 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInstruction.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInstruction.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.code.CfOrDexInstruction;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.ClasspathMethod;
 import com.android.tools.r8.graph.DexClassAndMethod;
@@ -282,7 +283,7 @@
   }
 
   public abstract ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context);
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context);
 
   public abstract void evaluate(
       CfFrameVerificationHelper frameBuilder,
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 b19a183..7b6c2e7 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
@@ -8,6 +8,7 @@
 import com.android.tools.r8.errors.CompilationError;
 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.CfCompareHelper;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexClassAndMethod;
@@ -33,6 +34,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.structural.CompareToVisitor;
 import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.Arrays;
@@ -239,13 +241,9 @@
           // Using invoke-super should therefore observe the correct semantics since we cannot
           // target less specific targets (up in the hierarchy).
           canonicalMethod = method;
-          if (method.name.toString().equals(Constants.INSTANCE_INITIALIZER_NAME)) {
-            type = Type.DIRECT;
-          } else if (code.getOriginalHolder() == method.holder) {
-            type = invokeTypeForInvokeSpecialToNonInitMethodOnHolder(builder.appView, code);
-          } else {
-            type = Type.SUPER;
-          }
+          type =
+              computeInvokeTypeForInvokeSpecial(
+                  builder.appView, method, code.getOriginalHolder(), code.getOrigin());
           break;
         }
       case Opcodes.INVOKESTATIC:
@@ -277,7 +275,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     GraphLens graphLens = inliningConstraints.getGraphLens();
     AppView<?> appView = inliningConstraints.getAppView();
     DexMethod target = method;
@@ -292,23 +290,11 @@
         break;
 
       case Opcodes.INVOKESPECIAL:
-        if (appView.dexItemFactory().isConstructor(target)) {
-          type = Type.DIRECT;
-          assert noNeedToUseGraphLens(target, context.getReference(), type, graphLens);
-        } else if (target.holder == context.getHolderType()) {
-          // The method could have been publicized.
-          type = graphLens.lookupMethod(target, context.getReference(), Type.DIRECT).getType();
-          assert type == Type.DIRECT || type == Type.VIRTUAL;
-        } else {
-          // This is a super call. Note that the vertical class merger translates some invoke-super
-          // instructions to invoke-direct. However, when that happens, the invoke instruction and
-          // the target method end up being in the same class, and therefore, we will allow inlining
-          // it. The result of using type=SUPER below will be the same, since it leads to the
-          // inlining constraint SAMECLASS.
-          // TODO(christofferqa): Consider using graphLens.lookupMethod (to do this, we need the
-          // context for the graph lens, though).
-          type = Type.SUPER;
-          assert noNeedToUseGraphLens(target, context.getReference(), type, graphLens);
+        {
+          Type actualInvokeType =
+              computeInvokeTypeForInvokeSpecial(
+                  appView, method, code.getOriginalHolder(), context.getOrigin());
+          type = graphLens.lookupMethod(target, context.getReference(), actualInvokeType).getType();
         }
         break;
 
@@ -371,14 +357,19 @@
     }
   }
 
-  private static boolean noNeedToUseGraphLens(
-      DexMethod method, DexMethod context, Invoke.Type type, GraphLens graphLens) {
-    assert graphLens.lookupMethod(method, context, type).getType() == type;
-    return true;
+  private Type computeInvokeTypeForInvokeSpecial(
+      AppView<?> appView, DexMethod method, DexType originalHolder, Origin origin) {
+    if (appView.dexItemFactory().isConstructor(method)) {
+      return Type.DIRECT;
+    }
+    if (originalHolder == method.holder) {
+      return invokeTypeForInvokeSpecialToNonInitMethodOnHolder(appView, origin);
+    }
+    return Type.SUPER;
   }
 
   private Type invokeTypeForInvokeSpecialToNonInitMethodOnHolder(
-      AppView<?> appView, CfSourceCode code) {
+      AppView<?> appView, Origin origin) {
     boolean desugaringEnabled = appView.options().isInterfaceMethodDesugaringEnabled();
     MethodLookupResult lookupResult = appView.graphLens().lookupMethod(method, method, Type.DIRECT);
     if (lookupResult.getType() == Type.VIRTUAL) {
@@ -408,8 +399,7 @@
     }
     // We cannot emulate the semantics of invoke-special in this case and should throw a compilation
     // error.
-    throw new CompilationError(
-        "Failed to compile unsupported use of invokespecial", code.getOrigin());
+    throw new CompilationError("Failed to compile unsupported use of invokespecial", origin);
   }
 
   private DexEncodedMethod lookupMethodOnHolder(AppView<?> appView, DexMethod method) {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java b/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java
index 3a98a8e..3138b27 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInvokeDynamic.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.cf.CfPrinter;
 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.CfCompareHelper;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexClassAndMethod;
@@ -154,7 +155,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forInvokeCustom();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfJsrRet.java b/src/main/java/com/android/tools/r8/cf/code/CfJsrRet.java
index 812a17c..0bff416 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfJsrRet.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfJsrRet.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCodeStackMapValidatingException;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -72,7 +73,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     throw error();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfLabel.java b/src/main/java/com/android/tools/r8/cf/code/CfLabel.java
index 5a3e4e7..028eff7 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfLabel.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfLabel.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -84,7 +85,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return ConstraintWithTarget.ALWAYS;
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfLoad.java b/src/main/java/com/android/tools/r8/cf/code/CfLoad.java
index 96a6d11..7aaf08c 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfLoad.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfLoad.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.cf.CfPrinter;
 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.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -113,7 +114,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forLoad();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfLogicalBinop.java b/src/main/java/com/android/tools/r8/cf/code/CfLogicalBinop.java
index 19e5dd9..8a38898 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfLogicalBinop.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfLogicalBinop.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.cf.code.CfFrame.FrameType;
 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.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -166,7 +167,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forBinop();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfMonitor.java b/src/main/java/com/android/tools/r8/cf/code/CfMonitor.java
index 3286da2..809317f 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfMonitor.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfMonitor.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.cf.code.CfFrame.FrameType;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -83,7 +84,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forMonitor();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java b/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java
index 682c857..e83b18d 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -116,7 +117,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forInvokeMultiNewArray(type, context);
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfNeg.java b/src/main/java/com/android/tools/r8/cf/code/CfNeg.java
index 5de3787..e9a6518 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfNeg.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfNeg.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.cf.code.CfFrame.FrameType;
 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.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -108,7 +109,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forUnop();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfNew.java b/src/main/java/com/android/tools/r8/cf/code/CfNew.java
index 53fc40c..0fe8c96 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfNew.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfNew.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.cf.code.CfFrame.FrameType;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -102,7 +103,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forNewInstance(type, context);
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfNewArray.java b/src/main/java/com/android/tools/r8/cf/code/CfNewArray.java
index ad50c17..559f3ba 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfNewArray.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfNewArray.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.cf.CfPrinter;
 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.CfCompareHelper;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -152,7 +153,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forNewArrayEmpty(type, context);
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfNop.java b/src/main/java/com/android/tools/r8/cf/code/CfNop.java
index 974d928..c7be975 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfNop.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfNop.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -65,7 +66,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return ConstraintWithTarget.ALWAYS;
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfNumberConversion.java b/src/main/java/com/android/tools/r8/cf/code/CfNumberConversion.java
index dc1bc2c..9e656f7 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfNumberConversion.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfNumberConversion.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.cf.code.CfFrame.FrameType;
 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.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -179,7 +180,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forUnop();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfPosition.java b/src/main/java/com/android/tools/r8/cf/code/CfPosition.java
index f524bab..45077e2 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfPosition.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfPosition.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -100,7 +101,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return ConstraintWithTarget.ALWAYS;
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfReturn.java b/src/main/java/com/android/tools/r8/cf/code/CfReturn.java
index 2b4eed5..9a74eaf 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfReturn.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfReturn.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.cf.CfPrinter;
 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.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -101,7 +102,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forReturn();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfReturnVoid.java b/src/main/java/com/android/tools/r8/cf/code/CfReturnVoid.java
index 08de455..84a524d 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfReturnVoid.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfReturnVoid.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -75,7 +76,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forReturn();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfStackInstruction.java b/src/main/java/com/android/tools/r8/cf/code/CfStackInstruction.java
index 4fd9829..911141c 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfStackInstruction.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfStackInstruction.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.errors.CompilationError;
 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.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -342,7 +343,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return ConstraintWithTarget.ALWAYS;
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfStore.java b/src/main/java/com/android/tools/r8/cf/code/CfStore.java
index b87c87c..9177bda 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfStore.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfStore.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.cf.code.CfFrame.FrameType;
 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.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -115,7 +116,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forStore();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfSwitch.java b/src/main/java/com/android/tools/r8/cf/code/CfSwitch.java
index b84fc14..aef40c6 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfSwitch.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfSwitch.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -136,7 +137,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forJumpInstruction();
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfThrow.java b/src/main/java/com/android/tools/r8/cf/code/CfThrow.java
index 90d25c5..b562faf 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfThrow.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfThrow.java
@@ -5,6 +5,7 @@
 
 import com.android.tools.r8.cf.CfPrinter;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.CfCompareHelper;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
@@ -82,7 +83,7 @@
 
   @Override
   public ConstraintWithTarget inliningConstraint(
-      InliningConstraints inliningConstraints, ProgramMethod context) {
+      InliningConstraints inliningConstraints, CfCode code, ProgramMethod context) {
     return inliningConstraints.forJumpInstruction();
   }
 
diff --git a/src/main/java/com/android/tools/r8/dex/DexParser.java b/src/main/java/com/android/tools/r8/dex/DexParser.java
index 26bbc5e..e2b7f93 100644
--- a/src/main/java/com/android/tools/r8/dex/DexParser.java
+++ b/src/main/java/com/android/tools/r8/dex/DexParser.java
@@ -819,7 +819,8 @@
               directMethods,
               virtualMethods,
               dexItemFactory.getSkipNameValidationForTesting(),
-              checksumSupplier);
+              checksumSupplier,
+              null);
       classCollection.accept(clazz);  // Update the application object.
     }
   }
diff --git a/src/main/java/com/android/tools/r8/graph/AppServices.java b/src/main/java/com/android/tools/r8/graph/AppServices.java
index 2cfb4d9..7fedd7c 100644
--- a/src/main/java/com/android/tools/r8/graph/AppServices.java
+++ b/src/main/java/com/android/tools/r8/graph/AppServices.java
@@ -40,11 +40,15 @@
 
   private final AppView<?> appView;
 
+  // The graph lens that was previously used to rewrite this instance.
+  private final GraphLens applied;
+
   // Mapping from service types to service implementation types.
   private final Map<DexType, Map<FeatureSplit, List<DexType>>> services;
 
   private AppServices(AppView<?> appView, Map<DexType, Map<FeatureSplit, List<DexType>>> services) {
     this.appView = appView;
+    this.applied = appView.graphLens();
     this.services = services;
   }
 
@@ -123,14 +127,15 @@
     ImmutableMap.Builder<DexType, Map<FeatureSplit, List<DexType>>> rewrittenFeatureMappings =
         ImmutableMap.builder();
     for (Entry<DexType, Map<FeatureSplit, List<DexType>>> entry : services.entrySet()) {
-      DexType rewrittenServiceType = graphLens.lookupType(entry.getKey());
+      DexType rewrittenServiceType = graphLens.lookupClassType(entry.getKey(), applied);
       ImmutableMap.Builder<FeatureSplit, List<DexType>> rewrittenFeatureImplementations =
           ImmutableMap.builder();
       for (Entry<FeatureSplit, List<DexType>> featureSplitImpls : entry.getValue().entrySet()) {
         ImmutableList.Builder<DexType> rewrittenServiceImplementationTypes =
             ImmutableList.builder();
         for (DexType serviceImplementationType : featureSplitImpls.getValue()) {
-          rewrittenServiceImplementationTypes.add(graphLens.lookupType(serviceImplementationType));
+          rewrittenServiceImplementationTypes.add(
+              graphLens.lookupClassType(serviceImplementationType, applied));
         }
         rewrittenFeatureImplementations.put(
             featureSplitImpls.getKey(), rewrittenServiceImplementationTypes.build());
@@ -173,12 +178,12 @@
     return new AppServices(appView, rewrittenServicesBuilder.build());
   }
 
-  private boolean verifyRewrittenWithLens() {
+  public boolean verifyRewrittenWithLens() {
     for (Entry<DexType, Map<FeatureSplit, List<DexType>>> entry : services.entrySet()) {
-      assert entry.getKey() == appView.graphLens().lookupType(entry.getKey());
+      assert entry.getKey() == appView.graphLens().lookupClassType(entry.getKey(), applied);
       for (Entry<FeatureSplit, List<DexType>> featureEntry : entry.getValue().entrySet()) {
         for (DexType type : featureEntry.getValue()) {
-          assert type == appView.graphLens().lookupType(type);
+          assert type == appView.graphLens().lookupClassType(type, applied);
         }
       }
     }
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 b3c5308..bf21997 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.graph.analysis.InitializedClassesInInstanceMethodsAnalysis.InitializedClassesInInstanceMethods;
 import com.android.tools.r8.graph.classmerging.MergedClassesCollection;
 import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
 import com.android.tools.r8.ir.analysis.inlining.SimpleInliningConstraintFactory;
 import com.android.tools.r8.ir.analysis.proto.EnumLiteProtoShrinker;
@@ -48,6 +49,7 @@
 import java.util.Set;
 import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.function.Supplier;
 
 public class AppView<T extends AppInfo> implements DexDefinitionSupplier, LibraryModeledPredicate {
 
@@ -91,7 +93,7 @@
   private boolean allCodeProcessed = false;
   private Predicate<DexType> classesEscapingIntoLibrary = Predicates.alwaysTrue();
   private InitializedClassesInInstanceMethods initializedClassesInInstanceMethods;
-  private HorizontallyMergedClasses horizontallyMergedClasses;
+  private HorizontallyMergedClasses horizontallyMergedClasses = HorizontallyMergedClasses.empty();
   private VerticallyMergedClasses verticallyMergedClasses;
   private EnumDataMap unboxedEnums = EnumDataMap.empty();
   // TODO(b/169115389): Remove
@@ -496,7 +498,7 @@
 
   public MergedClassesCollection allMergedClasses() {
     MergedClassesCollection collection = new MergedClassesCollection();
-    if (horizontallyMergedClasses != null) {
+    if (hasHorizontallyMergedClasses()) {
       collection.add(horizontallyMergedClasses);
     }
     if (verticallyMergedClasses != null) {
@@ -505,6 +507,10 @@
     return collection;
   }
 
+  public boolean hasHorizontallyMergedClasses() {
+    return !horizontallyMergedClasses.isEmpty();
+  }
+
   /**
    * Get the result of horizontal class merging. Returns null if horizontal class merging has not
    * been run.
@@ -513,10 +519,15 @@
     return horizontallyMergedClasses;
   }
 
-  public void setHorizontallyMergedClasses(HorizontallyMergedClasses horizontallyMergedClasses) {
-    assert this.horizontallyMergedClasses == null;
-    this.horizontallyMergedClasses = horizontallyMergedClasses;
-    testing().horizontallyMergedClassesConsumer.accept(dexItemFactory(), horizontallyMergedClasses);
+  public void setHorizontallyMergedClasses(
+      HorizontallyMergedClasses horizontallyMergedClasses, HorizontalClassMerger.Mode mode) {
+    assert !hasHorizontallyMergedClasses() || mode.isFinal();
+    this.horizontallyMergedClasses = horizontallyMergedClasses().extend(horizontallyMergedClasses);
+    if (mode.isFinal()) {
+      testing()
+          .horizontallyMergedClassesConsumer
+          .accept(dexItemFactory(), horizontallyMergedClasses());
+    }
   }
 
   /**
@@ -637,7 +648,7 @@
 
   public void rewriteWithLens(NonIdentityGraphLens lens) {
     if (lens != null) {
-      rewriteWithLens(lens, appInfo().app().asDirect(), withLiveness(), lens.getPrevious());
+      rewriteWithLens(lens, appInfo().app().asDirect(), withClassHierarchy(), lens.getPrevious());
     }
   }
 
@@ -650,13 +661,13 @@
       NonIdentityGraphLens lens, DirectMappedDexApplication application, GraphLens appliedLens) {
     assert lens != null;
     assert application != null;
-    rewriteWithLens(lens, application, withLiveness(), appliedLens);
+    rewriteWithLens(lens, application, withClassHierarchy(), appliedLens);
   }
 
   private static void rewriteWithLens(
       NonIdentityGraphLens lens,
       DirectMappedDexApplication application,
-      AppView<AppInfoWithLiveness> appView,
+      AppView<? extends AppInfoWithClassHierarchy> appView,
       GraphLens appliedLens) {
     if (lens == null) {
       return;
@@ -688,7 +699,11 @@
     firstUnappliedLens.withAlternativeParentLens(
         newMemberRebindingLens,
         () -> {
-          appView.setAppInfo(appView.appInfo().rewrittenWithLens(application, lens));
+          if (appView.hasLiveness()) {
+            appView
+                .withLiveness()
+                .setAppInfo(appView.appInfoWithLiveness().rewrittenWithLens(application, lens));
+          }
           appView.setAppServices(appView.appServices().rewrittenWithLens(lens));
           if (appView.hasInitClassLens()) {
             appView.setInitClassLens(appView.initClassLens().rewrittenWithLens(lens));
@@ -715,4 +730,8 @@
     assert alreadyLibraryDesugared != null;
     return alreadyLibraryDesugared.contains(clazz.getType());
   }
+
+  public boolean checkForTesting(Supplier<Boolean> test) {
+    return testing().enableTestAssertions ? test.get() : true;
+  }
 }
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 315bb3e..31774f0 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -675,7 +675,7 @@
     for (CfInstruction insn : instructions) {
       constraint =
           ConstraintWithTarget.meet(
-              constraint, insn.inliningConstraint(inliningConstraints, context), appView);
+              constraint, insn.inliningConstraint(inliningConstraints, this, context), appView);
       if (constraint == ConstraintWithTarget.NEVER) {
         return constraint;
       }
diff --git a/src/main/java/com/android/tools/r8/graph/ClassKind.java b/src/main/java/com/android/tools/r8/graph/ClassKind.java
index baf5365..3e29db5 100644
--- a/src/main/java/com/android/tools/r8/graph/ClassKind.java
+++ b/src/main/java/com/android/tools/r8/graph/ClassKind.java
@@ -8,13 +8,56 @@
 import com.android.tools.r8.graph.DexProgramClass.ChecksumSupplier;
 import com.android.tools.r8.graph.GenericSignature.ClassSignature;
 import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.synthesis.SyntheticMarker;
 import java.util.List;
 import java.util.function.Predicate;
 
 /** Kind of the application class. Can be program, classpath or library. */
 public class ClassKind<C extends DexClass> {
   public static ClassKind<DexProgramClass> PROGRAM =
-      new ClassKind<>(DexProgramClass::new, DexClass::isProgramClass);
+      new ClassKind<>(
+          (type,
+              originKind,
+              origin,
+              accessFlags,
+              superType,
+              interfaces,
+              sourceFile,
+              nestHost,
+              nestMembers,
+              enclosingMember,
+              innerClasses,
+              classSignature,
+              classAnnotations,
+              staticFields,
+              instanceFields,
+              directMethods,
+              virtualMethods,
+              skipNameValidationForTesting,
+              checksumSupplier,
+              syntheticMarker) ->
+              new DexProgramClass(
+                  type,
+                  originKind,
+                  origin,
+                  accessFlags,
+                  superType,
+                  interfaces,
+                  sourceFile,
+                  nestHost,
+                  nestMembers,
+                  enclosingMember,
+                  innerClasses,
+                  classSignature,
+                  classAnnotations,
+                  staticFields,
+                  instanceFields,
+                  directMethods,
+                  virtualMethods,
+                  skipNameValidationForTesting,
+                  checksumSupplier,
+                  syntheticMarker),
+          DexClass::isProgramClass);
   public static ClassKind<DexClasspathClass> CLASSPATH =
       new ClassKind<>(
           (type,
@@ -35,7 +78,8 @@
               directMethods,
               virtualMethods,
               skipNameValidationForTesting,
-              checksumSupplier) ->
+              checksumSupplier,
+              syntheticMarker) ->
               new DexClasspathClass(
                   type,
                   kind,
@@ -76,7 +120,8 @@
               directMethods,
               virtualMethods,
               skipNameValidationForTesting,
-              checksumSupplier) ->
+              checksumSupplier,
+              syntheticMarker) ->
               new DexLibraryClass(
                   type,
                   kind,
@@ -118,7 +163,8 @@
         DexEncodedMethod[] directMethods,
         DexEncodedMethod[] virtualMethods,
         boolean skipNameValidationForTesting,
-        ChecksumSupplier checksumSupplier);
+        ChecksumSupplier checksumSupplier,
+        SyntheticMarker syntheticMarker);
   }
 
   private final Factory<C> factory;
@@ -148,7 +194,8 @@
       DexEncodedMethod[] directMethods,
       DexEncodedMethod[] virtualMethods,
       boolean skipNameValidationForTesting,
-      ChecksumSupplier checksumSupplier) {
+      ChecksumSupplier checksumSupplier,
+      SyntheticMarker syntheticMarker) {
     return factory.create(
         type,
         kind,
@@ -168,7 +215,8 @@
         directMethods,
         virtualMethods,
         skipNameValidationForTesting,
-        checksumSupplier);
+        checksumSupplier,
+        syntheticMarker);
   }
 
   public boolean isOfKind(DexClass clazz) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexAnnotation.java b/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
index d1e6951..0576326 100644
--- a/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
+++ b/src/main/java/com/android/tools/r8/graph/DexAnnotation.java
@@ -359,19 +359,12 @@
   }
 
   public static DexAnnotation createAnnotationSynthesizedClass(
-      SyntheticKind kind, DexType synthesizingContext, DexItemFactory dexItemFactory) {
+      SyntheticKind kind, DexItemFactory dexItemFactory) {
     DexAnnotationElement kindElement =
         new DexAnnotationElement(
             dexItemFactory.kindString,
             new DexValueString(dexItemFactory.createString(kind.descriptor)));
-    DexAnnotationElement typeElement =
-        new DexAnnotationElement(dexItemFactory.valueString, new DexValueType(synthesizingContext));
-    DexAnnotationElement[] elements;
-    if (synthesizingContext == null) {
-      elements = new DexAnnotationElement[] {kindElement};
-    } else {
-      elements = new DexAnnotationElement[] {kindElement, typeElement};
-    }
+    DexAnnotationElement[] elements = new DexAnnotationElement[] {kindElement};
     return new DexAnnotation(
         VISIBILITY_BUILD,
         new DexEncodedAnnotation(dexItemFactory.annotationSynthesizedClass, elements));
@@ -379,10 +372,10 @@
 
   public static boolean hasSynthesizedClassAnnotation(
       DexAnnotationSet annotations, DexItemFactory factory) {
-    return getSynthesizedClassAnnotationContextType(annotations, factory) != null;
+    return getSynthesizedClassAnnotationInfo(annotations, factory) != null;
   }
 
-  public static Pair<SyntheticKind, DexType> getSynthesizedClassAnnotationContextType(
+  public static SyntheticKind getSynthesizedClassAnnotationInfo(
       DexAnnotationSet annotations, DexItemFactory factory) {
     if (annotations.size() != 1) {
       return null;
@@ -392,7 +385,7 @@
       return null;
     }
     int length = annotation.annotation.elements.length;
-    if (length != 1 && length != 2) {
+    if (length != 1) {
       return null;
     }
     assert factory.kindString.isLessThan(factory.valueString);
@@ -406,20 +399,7 @@
     SyntheticKind kind =
         SyntheticNaming.SyntheticKind.fromDescriptor(
             kindElement.value.asDexValueString().getValue().toString());
-    if (kind == null) {
-      return null;
-    }
-    if (length != 2) {
-      return new Pair<>(kind, null);
-    }
-    DexAnnotationElement valueElement = annotation.annotation.elements[1];
-    if (valueElement.name != factory.valueString) {
-      return null;
-    }
-    if (!valueElement.value.isDexValueType()) {
-      return null;
-    }
-    return new Pair<>(kind, valueElement.value.asDexValueType().getValue());
+    return kind;
   }
 
   public DexAnnotation rewrite(Function<DexEncodedAnnotation, DexEncodedAnnotation> rewriter) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplication.java b/src/main/java/com/android/tools/r8/graph/DexApplication.java
index d82db4a..31b83b2 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplication.java
@@ -20,6 +20,7 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.function.Predicate;
 
 public abstract class DexApplication {
 
@@ -177,6 +178,11 @@
       return self();
     }
 
+    public synchronized T removeProgramClasses(Predicate<DexProgramClass> predicate) {
+      this.programClasses.removeIf(predicate);
+      return self();
+    }
+
     public synchronized T replaceProgramClasses(Collection<DexProgramClass> newProgramClasses) {
       assert newProgramClasses != null;
       this.programClasses.clear();
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index 2d0e429..182c91c 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -76,7 +76,7 @@
    */
   private NestHostClassAttribute nestHost;
 
-  private final List<NestMemberClassAttribute> nestMembers;
+  private List<NestMemberClassAttribute> nestMembers;
 
   /** Generic signature information if the attribute is present in the input */
   protected ClassSignature classSignature;
@@ -177,6 +177,10 @@
     return sourceFile;
   }
 
+  public void setSourceFile(DexString sourceFile) {
+    this.sourceFile = sourceFile;
+  }
+
   public Iterable<DexEncodedField> fields() {
     return fields(Predicates.alwaysTrue());
   }
@@ -1038,6 +1042,10 @@
     this.nestHost = new NestHostClassAttribute(type);
   }
 
+  public void setNestHostAttribute(NestHostClassAttribute nestHostAttribute) {
+    this.nestHost = nestHostAttribute;
+  }
+
   public boolean isNestHost() {
     return !nestMembers.isEmpty();
   }
@@ -1065,10 +1073,22 @@
     return nestHost;
   }
 
+  public boolean hasNestMemberAttributes() {
+    return nestMembers != null && !nestMembers.isEmpty();
+  }
+
   public List<NestMemberClassAttribute> getNestMembersClassAttributes() {
     return nestMembers;
   }
 
+  public void setNestMemberAttributes(List<NestMemberClassAttribute> nestMemberAttributes) {
+    this.nestMembers = nestMemberAttributes;
+  }
+
+  public void removeNestMemberAttributes(Predicate<NestMemberClassAttribute> predicate) {
+    nestMembers.removeIf(predicate);
+  }
+
   /** Returns kotlin class info if the class is synthesized by kotlin compiler. */
   public abstract KotlinClassLevelInfo getKotlinInfo();
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
index 1a2bc2b..315b8a1 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.synthesis.SyntheticMarker;
 import com.android.tools.r8.utils.TraversalContinuation;
 import com.android.tools.r8.utils.structural.Ordered;
 import com.android.tools.r8.utils.structural.StructuralItem;
@@ -53,6 +54,8 @@
 
   private final ChecksumSupplier checksumSupplier;
 
+  private SyntheticMarker syntheticMarker;
+
   public DexProgramClass(
       DexType type,
       Kind originKind,
@@ -72,7 +75,8 @@
       DexEncodedMethod[] directMethods,
       DexEncodedMethod[] virtualMethods,
       boolean skipNameValidationForTesting,
-      ChecksumSupplier checksumSupplier) {
+      ChecksumSupplier checksumSupplier,
+      SyntheticMarker syntheticMarker) {
     super(
         sourceFile,
         interfaces,
@@ -95,6 +99,50 @@
     assert classAnnotations != null;
     this.originKind = originKind;
     this.checksumSupplier = checksumSupplier;
+    this.syntheticMarker = syntheticMarker;
+  }
+
+  public DexProgramClass(
+      DexType type,
+      Kind originKind,
+      Origin origin,
+      ClassAccessFlags accessFlags,
+      DexType superType,
+      DexTypeList interfaces,
+      DexString sourceFile,
+      NestHostClassAttribute nestHost,
+      List<NestMemberClassAttribute> nestMembers,
+      EnclosingMethodAttribute enclosingMember,
+      List<InnerClassAttribute> innerClasses,
+      ClassSignature classSignature,
+      DexAnnotationSet classAnnotations,
+      DexEncodedField[] staticFields,
+      DexEncodedField[] instanceFields,
+      DexEncodedMethod[] directMethods,
+      DexEncodedMethod[] virtualMethods,
+      boolean skipNameValidationForTesting,
+      ChecksumSupplier checksumSupplier) {
+    this(
+        type,
+        originKind,
+        origin,
+        accessFlags,
+        superType,
+        interfaces,
+        sourceFile,
+        nestHost,
+        nestMembers,
+        enclosingMember,
+        innerClasses,
+        classSignature,
+        classAnnotations,
+        staticFields,
+        instanceFields,
+        directMethods,
+        virtualMethods,
+        skipNameValidationForTesting,
+        checksumSupplier,
+        null);
   }
 
   @Override
@@ -120,6 +168,15 @@
     return DexProgramClass::specify;
   }
 
+  public SyntheticMarker stripSyntheticInputMarker() {
+    SyntheticMarker marker = syntheticMarker;
+    // The synthetic input marker is "read once". It is stored only for identifying the input as
+    // synthetic and amending it to the SyntheticItems collection. After identification this field
+    // should not be used.
+    syntheticMarker = null;
+    return marker;
+  }
+
   private static void specify(StructuralSpecification<DexProgramClass, ?> spec) {
     spec.withItem(c -> c.type)
         .withItem(c -> c.superType)
diff --git a/src/main/java/com/android/tools/r8/graph/DexTypeList.java b/src/main/java/com/android/tools/r8/graph/DexTypeList.java
index 07410c0..684dd7d 100644
--- a/src/main/java/com/android/tools/r8/graph/DexTypeList.java
+++ b/src/main/java/com/android/tools/r8/graph/DexTypeList.java
@@ -16,6 +16,7 @@
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.function.Consumer;
+import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Stream;
 
@@ -66,6 +67,14 @@
     return this;
   }
 
+  public DexTypeList map(Function<DexType, DexType> fn) {
+    if (isEmpty()) {
+      return DexTypeList.empty();
+    }
+    DexType[] newTypes = ArrayUtils.map(values, fn, DexType.EMPTY_ARRAY);
+    return newTypes != values ? create(newTypes) : this;
+  }
+
   public DexTypeList removeIf(Predicate<DexType> predicate) {
     return keepIf(not(predicate));
   }
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 1a3984c..fde5f6c 100644
--- a/src/main/java/com/android/tools/r8/graph/EnclosingMethodAttribute.java
+++ b/src/main/java/com/android/tools/r8/graph/EnclosingMethodAttribute.java
@@ -48,6 +48,10 @@
     }
   }
 
+  public boolean hasEnclosingMethod() {
+    return enclosingMethod != null;
+  }
+
   public DexMethod getEnclosingMethod() {
     return enclosingMethod;
   }
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 93b3b47..da35877 100644
--- a/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeRewriter.java
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeRewriter.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.graph.GenericSignature.TypeSignature;
 import com.android.tools.r8.graph.GenericSignature.WildcardIndicator;
 import com.android.tools.r8.utils.ListUtils;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Function;
 import java.util.function.Predicate;
@@ -25,7 +26,7 @@
   private final DexItemFactory factory;
   private final Predicate<DexType> wasPruned;
   private final Function<DexType, DexType> lookupType;
-  private final DexType context;
+  private final DexProgramClass context;
 
   private final ClassTypeSignature objectTypeSignature;
 
@@ -36,14 +37,14 @@
             ? appView.appInfo().withLiveness()::wasPruned
             : alwaysFalse(),
         appView.graphLens()::lookupType,
-        context.getType());
+        context);
   }
 
   public GenericSignatureTypeRewriter(
       DexItemFactory factory,
       Predicate<DexType> wasPruned,
       Function<DexType, DexType> lookupType,
-      DexType context) {
+      DexProgramClass context) {
     this.factory = factory;
     this.wasPruned = wasPruned;
     this.lookupType = lookupType;
@@ -138,7 +139,9 @@
     @Override
     public ClassTypeSignature visitSuperClass(ClassTypeSignature classTypeSignature) {
       ClassTypeSignature rewritten = classTypeSignature.visit(this);
-      return rewritten == null || rewritten.type() == context ? objectTypeSignature : rewritten;
+      return rewritten == null || rewritten.type() == context.type
+          ? objectTypeSignature
+          : rewritten;
     }
 
     @Override
@@ -147,13 +150,25 @@
       if (interfaceSignatures.isEmpty()) {
         return interfaceSignatures;
       }
-      return ListUtils.mapOrElse(interfaceSignatures, this::visitSuperInterface);
+      List<ClassTypeSignature> rewrittenInterfaces =
+          ListUtils.mapOrElse(interfaceSignatures, this::visitSuperInterface);
+      // Map against the actual interfaces implemented on the class for us to still preserve
+      // type arguments.
+      List<ClassTypeSignature> finalInterfaces = new ArrayList<>(rewrittenInterfaces.size());
+      context.interfaces.forEach(
+          iface -> {
+            ClassTypeSignature rewrittenSignature =
+                ListUtils.firstMatching(rewrittenInterfaces, rewritten -> rewritten.type == iface);
+            finalInterfaces.add(
+                rewrittenSignature != null ? rewrittenSignature : new ClassTypeSignature(iface));
+          });
+      return finalInterfaces;
     }
 
     @Override
     public ClassTypeSignature visitSuperInterface(ClassTypeSignature classTypeSignature) {
       ClassTypeSignature rewritten = classTypeSignature.visit(this);
-      return rewritten == null || rewritten.type() == context ? null : rewritten;
+      return rewritten == null || rewritten.type() == context.type ? null : rewritten;
     }
 
     @Override
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeVariableRemover.java b/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeVariableRemover.java
index f2a359a..7c0ee29 100644
--- a/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeVariableRemover.java
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeVariableRemover.java
@@ -151,51 +151,28 @@
     }
     DexType contextType = reference.getContextType();
     // TODO(b/187035453): We should visit generic signatures in the enqueuer.
-    DexClass clazz =
-        appView.appInfo().definitionForWithoutExistenceAssert(reference.getContextType());
+    DexClass clazz = appView.appInfo().definitionForWithoutExistenceAssert(contextType);
     boolean prunedHere = seenPruned || clazz == null;
     if (appView.hasLiveness()
-        && appView
-            .withLiveness()
-            .appInfo()
-            .getMissingClasses()
-            .contains(reference.getContextType())) {
+        && appView.withLiveness().appInfo().getMissingClasses().contains(contextType)) {
       prunedHere = seenPruned;
     }
-    if (reference.isDexMethod()) {
-      TypeParameterSubstitutions typeParameterSubstitutions = formalsInfo.get(reference);
-      if (clazz != null) {
-        assert clazz.isProgramClass();
-        DexEncodedMethod method = clazz.lookupMethod(reference.asDexMethod());
-        if (method == null) {
-          prunedHere = true;
-        } else if (method.isStatic()) {
-          // Static methods define their own scope.
-          return empty().combine(typeParameterSubstitutions, seenPruned);
-        }
-      }
-      // Lookup the formals in the enclosing context.
-      return computeTypeParameterContext(
-              appView,
-              contextType,
-              wasPruned,
-              prunedHere
-                  || hasPrunedRelationship(
-                      appView, enclosingInfo.get(contextType), contextType, wasPruned))
-          // Add the formals for the class.
-          .combine(formalsInfo.get(contextType), prunedHere)
-          // Add the formals for the method.
-          .combine(formalsInfo.get(reference), prunedHere);
+    // Lookup the formals in the enclosing context.
+    TypeParameterContext typeParameterContext =
+        computeTypeParameterContext(
+                appView,
+                enclosingInfo.get(contextType),
+                wasPruned,
+                prunedHere
+                    || hasPrunedRelationship(
+                        appView, enclosingInfo.get(contextType), contextType, wasPruned))
+            // Add formals for the context
+            .combine(formalsInfo.get(contextType), prunedHere);
+    if (!reference.isDexMethod()) {
+      return typeParameterContext;
     }
-    assert reference.isDexType();
-    return computeTypeParameterContext(
-            appView,
-            enclosingInfo.get(reference),
-            wasPruned,
-            prunedHere
-                || hasPrunedRelationship(
-                    appView, enclosingInfo.get(reference), contextType, wasPruned))
-        .combine(formalsInfo.get(reference), prunedHere);
+    prunedHere = prunedHere || clazz == null || clazz.lookupMethod(reference.asDexMethod()) == null;
+    return typeParameterContext.combine(formalsInfo.get(reference), prunedHere);
   }
 
   private static boolean hasPrunedRelationship(
@@ -273,17 +250,12 @@
                         MethodTypeSignature methodSignature = method.getGenericSignature();
                         if (methodSignature.hasSignature()
                             && method.getGenericSignature().isValid()) {
-                          if (method.isStatic()) {
-                            method.setGenericSignature(
-                                baseArgumentApplier
-                                    .buildForMethod(methodSignature.getFormalTypeParameters())
-                                    .visitMethodSignature(methodSignature));
-                          } else {
-                            method.setGenericSignature(
-                                classArgumentApplier
-                                    .buildForMethod(methodSignature.getFormalTypeParameters())
-                                    .visitMethodSignature(methodSignature));
-                          }
+                          // The reflection api do not distinguish static methods context from
+                          // virtual methods.
+                          method.setGenericSignature(
+                              classArgumentApplier
+                                  .buildForMethod(methodSignature.getFormalTypeParameters())
+                                  .visitMethodSignature(methodSignature));
                         }
                       });
               clazz
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 88d0852..ff2fd57 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLens.java
@@ -23,11 +23,13 @@
 import it.unimi.dsi.fastutil.objects.Object2BooleanArrayMap;
 import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.function.Predicate;
 
@@ -336,9 +338,17 @@
 
   public abstract String lookupPackageName(String pkg);
 
-  public abstract DexType lookupClassType(DexType type);
+  public DexType lookupClassType(DexType type) {
+    return lookupClassType(type, getIdentityLens());
+  }
 
-  public abstract DexType lookupType(DexType type);
+  public abstract DexType lookupClassType(DexType type, GraphLens applied);
+
+  public DexType lookupType(DexType type) {
+    return lookupType(type, getIdentityLens());
+  }
+
+  public abstract DexType lookupType(DexType type, GraphLens applied);
 
   // This overload can be used when the graph lens is known to be context insensitive.
   public final DexMethod lookupMethod(DexMethod method) {
@@ -579,10 +589,16 @@
     return builder.build();
   }
 
-  public <T> ImmutableMap<DexType, T> rewriteTypeKeys(Map<DexType, T> map) {
-    ImmutableMap.Builder<DexType, T> builder = ImmutableMap.builder();
-    map.forEach((type, value) -> builder.put(lookupType(type), value));
-    return builder.build();
+  public <T> Map<DexType, T> rewriteTypeKeys(Map<DexType, T> map, BiFunction<T, T, T> merge) {
+    Map<DexType, T> newMap = new IdentityHashMap<>();
+    map.forEach(
+        (type, value) -> {
+          DexType rewrittenType = lookupType(type);
+          T previousValue = newMap.get(rewrittenType);
+          newMap.put(
+              rewrittenType, previousValue != null ? merge.apply(value, previousValue) : value);
+        });
+    return Collections.unmodifiableMap(newMap);
   }
 
   public boolean verifyMappingToOriginalProgram(
@@ -695,7 +711,10 @@
     }
 
     @Override
-    public final DexType lookupType(DexType type) {
+    public final DexType lookupType(DexType type, GraphLens applied) {
+      if (this == applied) {
+        return type;
+      }
       if (type.isPrimitiveType() || type.isVoidType() || type.isNullValueType()) {
         return type;
       }
@@ -713,8 +732,11 @@
     }
 
     @Override
-    public final DexType lookupClassType(DexType type) {
+    public final DexType lookupClassType(DexType type, GraphLens applied) {
       assert type.isClassType() : "Expected class type, but was `" + type.toSourceString() + "`";
+      if (this == applied) {
+        return type;
+      }
       return internalDescribeLookupClassType(getPrevious().lookupClassType(type));
     }
 
@@ -816,12 +838,12 @@
     }
 
     @Override
-    public DexType lookupType(DexType type) {
+    public DexType lookupType(DexType type, GraphLens applied) {
       return type;
     }
 
     @Override
-    public DexType lookupClassType(DexType type) {
+    public DexType lookupClassType(DexType type, GraphLens applied) {
       assert type.isClassType();
       return type;
     }
diff --git a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
index 4def125..3c01abb 100644
--- a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
@@ -37,6 +37,7 @@
 import com.android.tools.r8.jar.CfApplicationWriter;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
+import com.android.tools.r8.synthesis.SyntheticMarker;
 import com.android.tools.r8.utils.AsmUtils;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.ExceptionUtils;
@@ -58,6 +59,7 @@
 import java.util.function.Consumer;
 import java.util.zip.CRC32;
 import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.Attribute;
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.ClassVisitor;
 import org.objectweb.asm.FieldVisitor;
@@ -119,6 +121,7 @@
     }
     reader.accept(
         new CreateDexClassVisitor<>(origin, classKind, reader.b, application, classConsumer),
+        new Attribute[] {SyntheticMarker.getMarkerAttributePrototype()},
         parsingOptions);
 
     // Read marker.
@@ -219,6 +222,7 @@
     private final List<DexEncodedMethod> virtualMethods = new ArrayList<>();
     private final Set<Wrapper<DexMethod>> methodSignatures = new HashSet<>();
     private boolean hasReachabilitySensitiveMethod = false;
+    private SyntheticMarker syntheticMarker = null;
 
     public CreateDexClassVisitor(
         Origin origin,
@@ -235,6 +239,15 @@
     }
 
     @Override
+    public void visitAttribute(Attribute attribute) {
+      SyntheticMarker marker = SyntheticMarker.readMarkerAttribute(attribute);
+      if (marker != null) {
+        assert syntheticMarker == null;
+        syntheticMarker = marker;
+      }
+    }
+
+    @Override
     public void visitInnerClass(String name, String outerName, String innerName, int access) {
       if (outerName != null && innerName != null) {
         String separator = DescriptorUtils.computeInnerClassSeparator(outerName, name, innerName);
@@ -452,7 +465,8 @@
               directMethods.toArray(DexEncodedMethod.EMPTY_ARRAY),
               virtualMethods.toArray(DexEncodedMethod.EMPTY_ARRAY),
               application.getFactory().getSkipNameValidationForTesting(),
-              getChecksumSupplier(classKind));
+              getChecksumSupplier(classKind),
+              syntheticMarker);
       InnerClassAttribute innerClassAttribute = clazz.getInnerClassAttributeForThisClass();
       // A member class should not be a local or anonymous class.
       if (innerClassAttribute != null && innerClassAttribute.getOuter() != null) {
diff --git a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
index 686d7d5..457ef38 100644
--- a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
+++ b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.graph;
 
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+import static com.android.tools.r8.utils.MapUtils.ignoreKey;
 
 import com.android.tools.r8.ir.desugar.LambdaDescriptor;
 import com.android.tools.r8.shaking.GraphReporter;
@@ -461,9 +462,8 @@
               assert false;
               return;
             }
-            assert !instantiatedLambdas.containsKey(type);
             // TODO(b/150277553): Rewrite lambda descriptor.
-            instantiatedLambdas.put(type, lambdas);
+            instantiatedLambdas.computeIfAbsent(type, ignoreKey(ArrayList::new)).addAll(lambdas);
           });
       return this;
     }
diff --git a/src/main/java/com/android/tools/r8/graph/TreeFixerBase.java b/src/main/java/com/android/tools/r8/graph/TreeFixerBase.java
index 5bbc1ee..969ac7d 100644
--- a/src/main/java/com/android/tools/r8/graph/TreeFixerBase.java
+++ b/src/main/java/com/android/tools/r8/graph/TreeFixerBase.java
@@ -141,7 +141,7 @@
     return newClass;
   }
 
-  private EnclosingMethodAttribute fixupEnclosingMethodAttribute(
+  protected EnclosingMethodAttribute fixupEnclosingMethodAttribute(
       EnclosingMethodAttribute enclosingMethodAttribute) {
     if (enclosingMethodAttribute == null) {
       return null;
@@ -190,7 +190,7 @@
     return dexItemFactory.createField(newHolder, newType, field.name);
   }
 
-  private List<InnerClassAttribute> fixupInnerClassAttributes(
+  protected List<InnerClassAttribute> fixupInnerClassAttributes(
       List<InnerClassAttribute> innerClassAttributes) {
     if (innerClassAttributes.isEmpty()) {
       return innerClassAttributes;
@@ -240,13 +240,13 @@
         fixupType(method.holder), fixupProto(method.proto), method.name);
   }
 
-  private NestHostClassAttribute fixupNestHost(NestHostClassAttribute nestHostClassAttribute) {
+  protected NestHostClassAttribute fixupNestHost(NestHostClassAttribute nestHostClassAttribute) {
     return nestHostClassAttribute != null
         ? new NestHostClassAttribute(fixupType(nestHostClassAttribute.getNestHost()))
         : null;
   }
 
-  private List<NestMemberClassAttribute> fixupNestMemberAttributes(
+  protected List<NestMemberClassAttribute> fixupNestMemberAttributes(
       List<NestMemberClassAttribute> nestMemberAttributes) {
     if (nestMemberAttributes.isEmpty()) {
       return nestMemberAttributes;
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 880890d..2b65950 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
@@ -10,7 +10,7 @@
 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.CfCode;
+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;
@@ -29,12 +29,14 @@
 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.ClassInitializerSynthesizedCode;
 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;
 import com.android.tools.r8.shaking.KeepClassInfo;
 import com.android.tools.r8.utils.IterableUtils;
+import com.android.tools.r8.utils.SetUtils;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
@@ -61,6 +63,7 @@
   private static final OptimizationFeedback feedback = OptimizationFeedbackSimple.getInstance();
 
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
+  private final Mode mode;
   private final MergeGroup group;
   private final DexItemFactory dexItemFactory;
   private final ClassInitializerSynthesizedCode classInitializerSynthesizedCode;
@@ -75,12 +78,14 @@
 
   private ClassMerger(
       AppView<? extends AppInfoWithClassHierarchy> appView,
+      Mode mode,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
       MergeGroup group,
       Collection<VirtualMethodMerger> virtualMethodMergers,
       Collection<ConstructorMerger> constructorMergers,
       ClassInitializerSynthesizedCode classInitializerSynthesizedCode) {
     this.appView = appView;
+    this.mode = mode;
     this.lensBuilder = lensBuilder;
     this.group = group;
     this.virtualMethodMergers = virtualMethodMergers;
@@ -112,8 +117,7 @@
     }
 
     DexMethod newClinit = dexItemFactory.createClassInitializer(group.getTarget().getType());
-
-    CfCode code = classInitializerSynthesizedCode.synthesizeCode(group.getTarget().getType());
+    Code code = classInitializerSynthesizedCode.getOrCreateCode(group.getTarget().getType());
     if (!group.getTarget().hasClassInitializer()) {
       classMethodsBuilder.addDirectMethod(
           new DexEncodedMethod(
@@ -129,9 +133,11 @@
     } else {
       DexEncodedMethod clinit = group.getTarget().getClassInitializer();
       clinit.setCode(code, appView);
-      CfVersion cfVersion = classInitializerSynthesizedCode.getCfVersion();
-      if (cfVersion != null) {
-        clinit.upgradeClassFileVersion(cfVersion);
+      if (code.isCfCode()) {
+        CfVersion cfVersion = classInitializerSynthesizedCode.getCfVersion();
+        if (cfVersion != null) {
+          clinit.upgradeClassFileVersion(cfVersion);
+        }
       }
       classMethodsBuilder.addDirectMethod(clinit);
     }
@@ -193,6 +199,7 @@
 
   void appendClassIdField() {
     assert appView.hasLiveness();
+    assert mode.isInitial();
 
     boolean deprecated = false;
     boolean d8R8Synthesized = true;
@@ -234,6 +241,24 @@
     }
   }
 
+  void fixNestMemberAttributes() {
+    if (group.getTarget().isInANest() && !group.getTarget().hasNestMemberAttributes()) {
+      for (DexProgramClass clazz : group.getSources()) {
+        if (clazz.hasNestMemberAttributes()) {
+          // The nest host has been merged into a nest member.
+          group.getTarget().clearNestHost();
+          group.getTarget().setNestMemberAttributes(clazz.getNestMembersClassAttributes());
+          group
+              .getTarget()
+              .removeNestMemberAttributes(
+                  nestMemberAttribute ->
+                      nestMemberAttribute.getNestMember() == group.getTarget().getType());
+          break;
+        }
+      }
+    }
+  }
+
   private void mergeAnnotations() {
     assert group.getClasses().stream().filter(DexDefinition::hasAnnotations).count() <= 1;
     for (DexProgramClass clazz : group.getSources()) {
@@ -247,10 +272,25 @@
   private void mergeInterfaces() {
     DexTypeList previousInterfaces = group.getTarget().getInterfaces();
     Set<DexType> interfaces = Sets.newLinkedHashSet(previousInterfaces);
-    group.forEachSource(clazz -> Iterables.addAll(interfaces, clazz.getInterfaces()));
-    if (interfaces.size() > previousInterfaces.size()) {
-      group.getTarget().setInterfaces(new DexTypeList(interfaces));
+    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(
+          clazz -> {
+            for (DexType itf : clazz.getInterfaces()) {
+              if (!groupTypes.contains(itf)) {
+                interfaces.add(itf);
+              }
+            }
+          });
+    } else {
+      // Add all implemented interfaces from the merge group to the target class.
+      group.forEachSource(clazz -> Iterables.addAll(interfaces, clazz.getInterfaces()));
     }
+    group.getTarget().setInterfaces(DexTypeList.create(interfaces));
   }
 
   void mergeInstanceFields() {
@@ -264,6 +304,7 @@
 
   public void mergeGroup(SyntheticArgumentClass syntheticArgumentClass) {
     fixAccessFlags();
+    fixNestMemberAttributes();
 
     if (group.hasClassIdField()) {
       appendClassIdField();
@@ -282,6 +323,7 @@
 
   public static class Builder {
     private final AppView<? extends AppInfoWithClassHierarchy> appView;
+    private Mode mode;
     private final MergeGroup group;
 
     public Builder(AppView<? extends AppInfoWithClassHierarchy> appView, MergeGroup group) {
@@ -289,6 +331,11 @@
       this.group = group;
     }
 
+    Builder setMode(Mode mode) {
+      this.mode = mode;
+      return this;
+    }
+
     private void selectTarget() {
       Iterable<DexProgramClass> candidates = Iterables.filter(group, DexClass::isPublic);
       if (IterableUtils.isEmpty(candidates)) {
@@ -401,6 +448,7 @@
 
       return new ClassMerger(
           appView,
+          mode,
           lensBuilder,
           group,
           virtualMethodMergers,
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 eff9460..1a88983 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -7,78 +7,73 @@
 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.horizontalclassmerging.policies.AllInstantiatedOrUninstantiated;
-import com.android.tools.r8.horizontalclassmerging.policies.CheckAbstractClasses;
-import com.android.tools.r8.horizontalclassmerging.policies.DontInlinePolicy;
-import com.android.tools.r8.horizontalclassmerging.policies.DontMergeSynchronizedClasses;
-import com.android.tools.r8.horizontalclassmerging.policies.LimitGroups;
-import com.android.tools.r8.horizontalclassmerging.policies.MinimizeFieldCasts;
-import com.android.tools.r8.horizontalclassmerging.policies.NoAnnotationClasses;
-import com.android.tools.r8.horizontalclassmerging.policies.NoClassAnnotationCollisions;
-import com.android.tools.r8.horizontalclassmerging.policies.NoClassInitializerWithObservableSideEffects;
-import com.android.tools.r8.horizontalclassmerging.policies.NoDeadEnumLiteMaps;
-import com.android.tools.r8.horizontalclassmerging.policies.NoDirectRuntimeTypeChecks;
-import com.android.tools.r8.horizontalclassmerging.policies.NoEnums;
-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.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.NoServiceLoaders;
-import com.android.tools.r8.horizontalclassmerging.policies.NotMatchedByNoHorizontalClassMerging;
-import com.android.tools.r8.horizontalclassmerging.policies.NotVerticallyMergedIntoSubtype;
-import com.android.tools.r8.horizontalclassmerging.policies.PreserveMethodCharacteristics;
-import com.android.tools.r8.horizontalclassmerging.policies.PreventMergeIntoDifferentMainDexGroups;
-import com.android.tools.r8.horizontalclassmerging.policies.PreventMethodImplementation;
-import com.android.tools.r8.horizontalclassmerging.policies.RespectPackageBoundaries;
-import com.android.tools.r8.horizontalclassmerging.policies.SameFeatureSplit;
-import com.android.tools.r8.horizontalclassmerging.policies.SameInstanceFields;
-import com.android.tools.r8.horizontalclassmerging.policies.SameNestHost;
-import com.android.tools.r8.horizontalclassmerging.policies.SameParentClass;
-import com.android.tools.r8.horizontalclassmerging.policies.SyntheticItemsPolicy;
+import com.android.tools.r8.graph.DirectMappedDexApplication;
+import com.android.tools.r8.graph.PrunedItems;
 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.google.common.collect.ImmutableList;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Collections;
+import java.util.LinkedList;
 import java.util.List;
 
 public class HorizontalClassMerger {
 
-  // TODO(b/181846319): Add 'FINAL' mode that runs after synthetic finalization.
   public enum Mode {
-    INITIAL;
+    INITIAL,
+    FINAL;
 
     public boolean isInitial() {
       return this == INITIAL;
     }
+
+    public boolean isFinal() {
+      return this == FINAL;
+    }
   }
 
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
-  private final Mode mode = Mode.INITIAL;
+  private final Mode mode;
+  private final HorizontalClassMergerOptions options;
 
-  public HorizontalClassMerger(AppView<? extends AppInfoWithClassHierarchy> appView) {
+  private HorizontalClassMerger(AppView<? extends AppInfoWithClassHierarchy> appView, Mode mode) {
     this.appView = appView;
-    assert appView.options().enableInlining;
+    this.mode = mode;
+    this.options = appView.options().horizontalClassMergerOptions();
   }
 
-  public HorizontalClassMergerResult run(RuntimeTypeCheckInfo runtimeTypeCheckInfo, Timing timing) {
-    MergeGroup initialGroup = new MergeGroup(appView.appInfo().classesWithDeterministicOrder());
+  public static HorizontalClassMerger createForInitialClassMerging(
+      AppView<AppInfoWithLiveness> appView) {
+    return new HorizontalClassMerger(appView, Mode.INITIAL);
+  }
 
+  public static HorizontalClassMerger createForFinalClassMerging(
+      AppView<? extends AppInfoWithClassHierarchy> appView) {
+    return new HorizontalClassMerger(appView, Mode.FINAL);
+  }
+
+  public void runIfNecessary(RuntimeTypeCheckInfo runtimeTypeCheckInfo, Timing timing) {
+    if (options.isEnabled(mode)) {
+      timing.begin("HorizontalClassMerger (" + mode.toString() + ")");
+      run(runtimeTypeCheckInfo, timing);
+      timing.end();
+    } else {
+      appView.setHorizontallyMergedClasses(HorizontallyMergedClasses.empty(), mode);
+    }
+  }
+
+  private void run(RuntimeTypeCheckInfo runtimeTypeCheckInfo, Timing timing) {
     // Run the policies on all program classes to produce a final grouping.
-    List<Policy> policies = getPolicies(runtimeTypeCheckInfo);
-    Collection<MergeGroup> groups =
-        new PolicyExecutor().run(Collections.singletonList(initialGroup), policies, timing);
+    List<Policy> policies = PolicyScheduler.getPolicies(appView, mode, runtimeTypeCheckInfo);
+    Collection<MergeGroup> groups = new PolicyExecutor().run(getInitialGroups(), policies, timing);
 
     // If there are no groups, then end horizontal class merging.
     if (groups.isEmpty()) {
-      appView.setHorizontallyMergedClasses(HorizontallyMergedClasses.empty());
-      return null;
+      appView.setHorizontallyMergedClasses(HorizontallyMergedClasses.empty(), mode);
+      return;
     }
 
     HorizontalClassMergerGraphLens.Builder lensBuilder =
@@ -95,20 +90,38 @@
     // Generate the graph lens.
     HorizontallyMergedClasses mergedClasses =
         HorizontallyMergedClasses.builder().addMergeGroups(groups).build();
-    appView.setHorizontallyMergedClasses(mergedClasses);
-    HorizontalClassMergerGraphLens lens =
+    appView.setHorizontallyMergedClasses(mergedClasses, mode);
+
+    HorizontalClassMergerGraphLens horizontalClassMergerGraphLens =
         createLens(mergedClasses, lensBuilder, syntheticArgumentClass);
 
     // Prune keep info.
-    appView
-        .getKeepInfo()
-        .mutate(mutator -> mutator.removeKeepInfoForPrunedItems(mergedClasses.getSources()));
+    KeepInfoCollection keepInfo = appView.getKeepInfo();
+    keepInfo.mutate(mutator -> mutator.removeKeepInfoForPrunedItems(mergedClasses.getSources()));
 
-    return new HorizontalClassMergerResult(createFieldAccessInfoCollectionModifier(groups), lens);
+    // Must rewrite AppInfoWithLiveness before pruning the merged classes, to ensure that allocation
+    // sites, fields accesses, etc. are correctly transferred to the target classes.
+    appView.rewriteWithLensAndApplication(
+        horizontalClassMergerGraphLens, getNewApplication(mergedClasses));
+
+    // Record where the synthesized $r8$classId fields are read and written.
+    if (mode.isInitial()) {
+      createFieldAccessInfoCollectionModifier(groups).modify(appView.withLiveness());
+    } else {
+      assert groups.stream().noneMatch(MergeGroup::hasClassIdField);
+    }
+
+    appView.pruneItems(
+        PrunedItems.builder()
+            .setPrunedApp(appView.appInfo().app())
+            .addRemovedClasses(mergedClasses.getSources())
+            .addNoLongerSyntheticItems(mergedClasses.getSources())
+            .build());
   }
 
   private FieldAccessInfoCollectionModifier createFieldAccessInfoCollectionModifier(
       Collection<MergeGroup> groups) {
+    assert mode.isInitial();
     FieldAccessInfoCollectionModifier.Builder builder =
         new FieldAccessInfoCollectionModifier.Builder();
     for (MergeGroup group : groups) {
@@ -126,47 +139,34 @@
     return builder.build();
   }
 
-  private List<Policy> getPolicies(RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
-    AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
-    List<SingleClassPolicy> singleClassPolicies =
-        ImmutableList.of(
-            new NotMatchedByNoHorizontalClassMerging(appViewWithLiveness),
-            new NoDeadEnumLiteMaps(appViewWithLiveness),
-            new NoAnnotationClasses(),
-            new NoEnums(appView),
-            new NoInnerClasses(),
-            new NoInstanceFieldAnnotations(),
-            new NoInterfaces(),
-            new NoClassInitializerWithObservableSideEffects(),
-            new NoNativeMethods(),
-            new NoKeepRules(appView),
-            new NoKotlinMetadata(),
-            new NoServiceLoaders(appView),
-            new NotVerticallyMergedIntoSubtype(appView),
-            new NoDirectRuntimeTypeChecks(runtimeTypeCheckInfo),
-            new DontInlinePolicy(appViewWithLiveness));
-    List<MultiClassPolicy> multiClassPolicies =
-        ImmutableList.of(
-            new SameInstanceFields(appView),
-            new NoClassAnnotationCollisions(),
-            new CheckAbstractClasses(appView),
-            new SyntheticItemsPolicy(appView),
-            new NoIndirectRuntimeTypeChecks(appView, runtimeTypeCheckInfo),
-            new PreventMethodImplementation(appView),
-            new PreventMergeIntoDifferentMainDexGroups(appView),
-            new AllInstantiatedOrUninstantiated(appViewWithLiveness),
-            new SameParentClass(),
-            new SameNestHost(appView),
-            new PreserveMethodCharacteristics(appViewWithLiveness),
-            new SameFeatureSplit(appView),
-            new RespectPackageBoundaries(appView),
-            new DontMergeSynchronizedClasses(appViewWithLiveness),
-            new MinimizeFieldCasts(),
-            new LimitGroups(appView));
-    return ImmutableList.<Policy>builder()
-        .addAll(singleClassPolicies)
-        .addAll(multiClassPolicies)
-        .build();
+  private DirectMappedDexApplication getNewApplication(HorizontallyMergedClasses mergedClasses) {
+    // In the second round of class merging, we must forcefully remove the merged classes from the
+    // application, since we won't run tree shaking before writing the application.
+    DirectMappedDexApplication application = appView.appInfo().app().asDirect();
+    return mode.isInitial()
+        ? application
+        : application
+            .builder()
+            .removeProgramClasses(
+                clazz -> mergedClasses.hasBeenMergedIntoDifferentType(clazz.getType()))
+            .build();
+  }
+
+  private List<MergeGroup> getInitialGroups() {
+    MergeGroup initialClassGroup = new MergeGroup();
+    MergeGroup initialInterfaceGroup = new MergeGroup();
+    for (DexProgramClass clazz : appView.appInfo().classesWithDeterministicOrder()) {
+      if (clazz.isInterface()) {
+        initialInterfaceGroup.add(clazz);
+      } else {
+        initialClassGroup.add(clazz);
+      }
+    }
+    List<MergeGroup> initialGroups = new LinkedList<>();
+    initialGroups.add(initialClassGroup);
+    initialGroups.add(initialInterfaceGroup);
+    initialGroups.removeIf(MergeGroup::isTrivial);
+    return initialGroups;
   }
 
   /**
@@ -176,15 +176,11 @@
   private List<ClassMerger> initializeClassMergers(
       HorizontalClassMergerGraphLens.Builder lensBuilder,
       Collection<MergeGroup> groups) {
-    List<ClassMerger> classMergers = new ArrayList<>();
-
-    // TODO(b/166577694): Replace Collection<DexProgramClass> with MergeGroup
+    List<ClassMerger> classMergers = new ArrayList<>(groups.size());
     for (MergeGroup group : groups) {
-      assert !group.isEmpty();
-      ClassMerger merger = new ClassMerger.Builder(appView, group).build(lensBuilder);
-      classMergers.add(merger);
+      assert group.isNonTrivial();
+      classMergers.add(new ClassMerger.Builder(appView, group).setMode(mode).build(lensBuilder));
     }
-
     return classMergers;
   }
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerResult.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerResult.java
deleted file mode 100644
index 820c24a..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerResult.java
+++ /dev/null
@@ -1,28 +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 com.android.tools.r8.shaking.FieldAccessInfoCollectionModifier;
-
-public class HorizontalClassMergerResult {
-
-  private final FieldAccessInfoCollectionModifier fieldAccessInfoCollectionModifier;
-  private final HorizontalClassMergerGraphLens graphLens;
-
-  HorizontalClassMergerResult(
-      FieldAccessInfoCollectionModifier fieldAccessInfoCollectionModifier,
-      HorizontalClassMergerGraphLens graphLens) {
-    this.fieldAccessInfoCollectionModifier = fieldAccessInfoCollectionModifier;
-    this.graphLens = graphLens;
-  }
-
-  public FieldAccessInfoCollectionModifier getFieldAccessInfoCollectionModifier() {
-    return fieldAccessInfoCollectionModifier;
-  }
-
-  public HorizontalClassMergerGraphLens getGraphLens() {
-    return graphLens;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
index 927e629..9caf1a9 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
@@ -32,6 +32,24 @@
     return new HorizontallyMergedClasses(new EmptyBidirectionalOneToOneMap<>());
   }
 
+  public HorizontallyMergedClasses extend(HorizontallyMergedClasses newHorizontallyMergedClasses) {
+    if (isEmpty()) {
+      return newHorizontallyMergedClasses;
+    }
+    if (newHorizontallyMergedClasses.isEmpty()) {
+      return this;
+    }
+    Builder builder = builder();
+    forEachMergeGroup(
+        (sources, target) -> {
+          DexType rewrittenTarget = newHorizontallyMergedClasses.getMergeTargetOrDefault(target);
+          sources.forEach(source -> builder.add(source, rewrittenTarget));
+        });
+    newHorizontallyMergedClasses.forEachMergeGroup(
+        (sources, target) -> sources.forEach(source -> builder.add(source, target)));
+    return builder.build();
+  }
+
   @Override
   public void forEachMergeGroup(BiConsumer<Set<DexType>, DexType> consumer) {
     mergedClasses.forEachManyToOneMapping(consumer);
@@ -58,6 +76,10 @@
     return mergedClasses.containsKey(type);
   }
 
+  public boolean isEmpty() {
+    return mergedClasses.isEmpty();
+  }
+
   @Override
   public boolean isMergeTarget(DexType type) {
     return mergedClasses.containsValue(type);
@@ -87,8 +109,13 @@
     private final MutableBidirectionalManyToOneMap<DexType, DexType> mergedClasses =
         BidirectionalManyToOneHashMap.newIdentityHashMap();
 
+    void add(DexType source, DexType target) {
+      assert !mergedClasses.containsKey(source);
+      mergedClasses.put(source, target);
+    }
+
     void addMergeGroup(MergeGroup group) {
-      group.forEachSource(clazz -> mergedClasses.put(clazz.getType(), group.getTarget().getType()));
+      group.forEachSource(clazz -> add(clazz.getType(), group.getTarget().getType()));
     }
 
     Builder addMergeGroups(Iterable<MergeGroup> groups) {
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 4b9ad6a..ebedb0b 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MergeGroup.java
@@ -6,8 +6,10 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+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.utils.IterableUtils;
 import com.android.tools.r8.utils.IteratorUtils;
 import com.google.common.collect.Iterables;
 import java.util.Collection;
@@ -16,7 +18,7 @@
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
-public class MergeGroup implements Iterable<DexProgramClass> {
+public class MergeGroup implements Collection<DexProgramClass> {
 
   public static class Metadata {}
 
@@ -35,9 +37,9 @@
     add(clazz);
   }
 
-  public MergeGroup(Collection<DexProgramClass> classes) {
+  public MergeGroup(Iterable<DexProgramClass> classes) {
     this();
-    addAll(classes);
+    Iterables.addAll(this.classes, classes);
   }
 
   public void applyMetadataFrom(MergeGroup group) {
@@ -46,20 +48,33 @@
     }
   }
 
-  public void add(DexProgramClass clazz) {
-    classes.add(clazz);
+  @Override
+  public boolean add(DexProgramClass clazz) {
+    return classes.add(clazz);
   }
 
-  public void add(MergeGroup group) {
-    classes.addAll(group.getClasses());
+  public boolean add(MergeGroup group) {
+    return classes.addAll(group.getClasses());
   }
 
-  public void addAll(Collection<DexProgramClass> classes) {
-    this.classes.addAll(classes);
+  @Override
+  public boolean addAll(Collection<? extends DexProgramClass> classes) {
+    return this.classes.addAll(classes);
   }
 
-  public void addFirst(DexProgramClass clazz) {
-    classes.addFirst(clazz);
+  @Override
+  public void clear() {
+    classes.clear();
+  }
+
+  @Override
+  public boolean contains(Object o) {
+    return classes.contains(o);
+  }
+
+  @Override
+  public boolean containsAll(Collection<?> collection) {
+    return classes.containsAll(collection);
   }
 
   public void forEachSource(Consumer<DexProgramClass> consumer) {
@@ -110,28 +125,66 @@
     return size() < 2;
   }
 
+  public boolean isNonTrivial() {
+    return !isTrivial();
+  }
+
+  @Override
   public boolean isEmpty() {
     return classes.isEmpty();
   }
 
+  public boolean isInterfaceGroup() {
+    assert !isEmpty();
+    assert IterableUtils.allIdentical(getClasses(), DexClass::isInterface);
+    return getClasses().getFirst().isInterface();
+  }
+
   @Override
   public Iterator<DexProgramClass> iterator() {
     return classes.iterator();
   }
 
+  @Override
   public int size() {
     return classes.size();
   }
 
+  @Override
+  public boolean remove(Object o) {
+    return classes.remove(o);
+  }
+
+  @Override
+  public boolean removeAll(Collection<?> collection) {
+    return classes.removeAll(collection);
+  }
+
   public DexProgramClass removeFirst(Predicate<DexProgramClass> predicate) {
     return IteratorUtils.removeFirst(iterator(), predicate);
   }
 
-  public boolean removeIf(Predicate<DexProgramClass> predicate) {
+  @Override
+  public boolean removeIf(Predicate<? super DexProgramClass> predicate) {
     return classes.removeIf(predicate);
   }
 
   public DexProgramClass removeLast() {
     return classes.removeLast();
   }
+
+  @Override
+  public boolean retainAll(Collection<?> collection) {
+    return collection.retainAll(collection);
+  }
+
+  @Override
+  public Object[] toArray() {
+    return classes.toArray();
+  }
+
+  @Override
+  public <T> T[] toArray(T[] ts) {
+    return classes.toArray(ts);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicy.java
index b21f233..63420fe 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicy.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicy.java
@@ -4,21 +4,11 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
-import java.util.ArrayList;
 import java.util.Collection;
 
 public abstract class MultiClassPolicy extends Policy {
 
   /**
-   * Remove all groups containing no or only a single class, as there is no point in merging these.
-   */
-  protected Collection<MergeGroup> removeTrivialGroups(Collection<MergeGroup> groups) {
-    assert !(groups instanceof ArrayList);
-    groups.removeIf(MergeGroup::isTrivial);
-    return groups;
-  }
-
-  /**
    * Apply the multi class policy to a group of program classes.
    *
    * @param group This is a group of program classes which can currently still be merged.
@@ -27,4 +17,14 @@
    *     cannot be merged with any other classes they are returned as singleton lists.
    */
   public abstract Collection<MergeGroup> apply(MergeGroup group);
+
+  @Override
+  public boolean isMultiClassPolicy() {
+    return true;
+  }
+
+  @Override
+  public MultiClassPolicy asMultiClassPolicy() {
+    return this;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicyWithPreprocessing.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicyWithPreprocessing.java
new file mode 100644
index 0000000..067fcd5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicyWithPreprocessing.java
@@ -0,0 +1,33 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.horizontalclassmerging;
+
+import java.util.Collection;
+
+public abstract class MultiClassPolicyWithPreprocessing<T> extends Policy {
+
+  /**
+   * Apply the multi class policy to a group of program classes.
+   *
+   * @param group This is a group of program classes which can currently still be merged.
+   * @param data The result of calling {@link #preprocess(Collection)}.
+   * @return The same collection of program classes split into new groups of candidates which can be
+   *     merged. If the policy detects no issues then `group` will be returned unchanged. If classes
+   *     cannot be merged with any other classes they are returned as singleton lists.
+   */
+  public abstract Collection<MergeGroup> apply(MergeGroup group, T data);
+
+  public abstract T preprocess(Collection<MergeGroup> groups);
+
+  @Override
+  public boolean isMultiClassPolicyWithPreprocessing() {
+    return true;
+  }
+
+  @Override
+  public MultiClassPolicyWithPreprocessing<?> asMultiClassPolicyWithPreprocessing() {
+    return this;
+  }
+}
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 b984ed5..e5854fe 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
@@ -4,19 +4,78 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import java.util.ArrayList;
+import java.util.Collection;
+
 /**
  * The super class of all horizontal class merging policies. Most classes will either implement
  * {@link SingleClassPolicy} or {@link MultiClassPolicy}.
  */
 public abstract class Policy {
+
   /** Counter keeping track of how many classes this policy has removed. For debugging only. */
   public int numberOfRemovedClasses;
 
+  public int numberOfRemovedInterfaces;
+
   public void clear() {}
 
   public abstract String getName();
 
+  public boolean isSingleClassPolicy() {
+    return false;
+  }
+
+  public SingleClassPolicy asSingleClassPolicy() {
+    return null;
+  }
+
+  public boolean isMultiClassPolicy() {
+    return false;
+  }
+
+  public MultiClassPolicy asMultiClassPolicy() {
+    return null;
+  }
+
+  public boolean isMultiClassPolicyWithPreprocessing() {
+    return false;
+  }
+
+  public MultiClassPolicyWithPreprocessing<?> asMultiClassPolicyWithPreprocessing() {
+    return null;
+  }
+
   public boolean shouldSkipPolicy() {
     return false;
   }
+
+  /**
+   * Remove all groups containing no or only a single class, as there is no point in merging these.
+   */
+  protected Collection<MergeGroup> removeTrivialGroups(Collection<MergeGroup> groups) {
+    assert !(groups instanceof ArrayList);
+    groups.removeIf(MergeGroup::isTrivial);
+    return groups;
+  }
+
+  boolean recordRemovedClassesForDebugging(
+      boolean isInterfaceGroup, int previousGroupSize, Collection<MergeGroup> newGroups) {
+    assert previousGroupSize >= 2;
+    int previousNumberOfRemovedClasses = previousGroupSize - 1;
+    int newNumberOfRemovedClasses = 0;
+    for (MergeGroup newGroup : newGroups) {
+      if (newGroup.isNonTrivial()) {
+        newNumberOfRemovedClasses += newGroup.size() - 1;
+      }
+    }
+    assert previousNumberOfRemovedClasses >= newNumberOfRemovedClasses;
+    int change = previousNumberOfRemovedClasses - newNumberOfRemovedClasses;
+    if (isInterfaceGroup) {
+      numberOfRemovedInterfaces += change;
+    } else {
+      numberOfRemovedClasses += change;
+    }
+    return true;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java
index 33b6f7f..c1c4f64 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java
@@ -4,8 +4,8 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
-import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.Timing;
+import com.google.common.collect.ImmutableList;
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.LinkedList;
@@ -17,15 +17,16 @@
  */
 public class PolicyExecutor {
 
-  // TODO(b/165506334): if performing mutable operation ensure that linked lists are used
   private void applySingleClassPolicy(SingleClassPolicy policy, LinkedList<MergeGroup> groups) {
     Iterator<MergeGroup> i = groups.iterator();
     while (i.hasNext()) {
       MergeGroup group = i.next();
-      int previousNumberOfClasses = group.size();
+      boolean isInterfaceGroup = group.isInterfaceGroup();
+      int previousGroupSize = group.size();
       group.removeIf(clazz -> !policy.canMerge(clazz));
-      policy.numberOfRemovedClasses += previousNumberOfClasses - group.size();
-      if (group.size() < 2) {
+      assert policy.recordRemovedClassesForDebugging(
+          isInterfaceGroup, previousGroupSize, ImmutableList.of(group));
+      if (group.isTrivial()) {
         i.remove();
       }
     }
@@ -37,11 +38,30 @@
     LinkedList<MergeGroup> newGroups = new LinkedList<>();
     groups.forEach(
         group -> {
-          int previousNumberOfClasses = group.size();
+          boolean isInterfaceGroup = group.isInterfaceGroup();
+          int previousGroupSize = group.size();
           Collection<MergeGroup> policyGroups = policy.apply(group);
           policyGroups.forEach(newGroup -> newGroup.applyMetadataFrom(group));
-          policy.numberOfRemovedClasses +=
-              previousNumberOfClasses - IterableUtils.sumInt(policyGroups, MergeGroup::size);
+          assert policy.recordRemovedClassesForDebugging(
+              isInterfaceGroup, previousGroupSize, policyGroups);
+          newGroups.addAll(policyGroups);
+        });
+    return newGroups;
+  }
+
+  private <T> LinkedList<MergeGroup> applyMultiClassPolicyWithPreprocessing(
+      MultiClassPolicyWithPreprocessing<T> policy, LinkedList<MergeGroup> groups) {
+    // For each group apply the multi class policy and add all the new groups together.
+    T data = policy.preprocess(groups);
+    LinkedList<MergeGroup> newGroups = new LinkedList<>();
+    groups.forEach(
+        group -> {
+          boolean isInterfaceGroup = group.isInterfaceGroup();
+          int previousGroupSize = group.size();
+          Collection<MergeGroup> policyGroups = policy.apply(group, data);
+          policyGroups.forEach(newGroup -> newGroup.applyMetadataFrom(group));
+          assert policy.recordRemovedClassesForDebugging(
+              isInterfaceGroup, previousGroupSize, policyGroups);
           newGroups.addAll(policyGroups);
         });
     return newGroups;
@@ -68,11 +88,15 @@
       }
 
       timing.begin(policy.getName());
-      if (policy instanceof SingleClassPolicy) {
-        applySingleClassPolicy((SingleClassPolicy) policy, linkedGroups);
+      if (policy.isSingleClassPolicy()) {
+        applySingleClassPolicy(policy.asSingleClassPolicy(), linkedGroups);
+      } else if (policy.isMultiClassPolicy()) {
+        linkedGroups = applyMultiClassPolicy(policy.asMultiClassPolicy(), linkedGroups);
       } else {
-        assert policy instanceof MultiClassPolicy;
-        linkedGroups = applyMultiClassPolicy((MultiClassPolicy) policy, linkedGroups);
+        assert policy.isMultiClassPolicyWithPreprocessing();
+        linkedGroups =
+            applyMultiClassPolicyWithPreprocessing(
+                policy.asMultiClassPolicyWithPreprocessing(), linkedGroups);
       }
       timing.end();
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
new file mode 100644
index 0000000..01a85ee
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyScheduler.java
@@ -0,0 +1,211 @@
+// 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.horizontalclassmerging;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+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.MinimizeInstanceFieldCasts;
+import com.android.tools.r8.horizontalclassmerging.policies.NoAnnotationClasses;
+import com.android.tools.r8.horizontalclassmerging.policies.NoClassAnnotationCollisions;
+import com.android.tools.r8.horizontalclassmerging.policies.NoClassInitializerWithObservableSideEffects;
+import com.android.tools.r8.horizontalclassmerging.policies.NoConstructorCollisions;
+import com.android.tools.r8.horizontalclassmerging.policies.NoDeadEnumLiteMaps;
+import com.android.tools.r8.horizontalclassmerging.policies.NoDeadLocks;
+import com.android.tools.r8.horizontalclassmerging.policies.NoDefaultInterfaceMethodCollisions;
+import com.android.tools.r8.horizontalclassmerging.policies.NoDefaultInterfaceMethodMerging;
+import com.android.tools.r8.horizontalclassmerging.policies.NoDirectRuntimeTypeChecks;
+import com.android.tools.r8.horizontalclassmerging.policies.NoEnums;
+import com.android.tools.r8.horizontalclassmerging.policies.NoIllegalInlining;
+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.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.NotMatchedByNoHorizontalClassMerging;
+import com.android.tools.r8.horizontalclassmerging.policies.OnlyDirectlyConnectedOrUnrelatedInterfaces;
+import com.android.tools.r8.horizontalclassmerging.policies.PreserveMethodCharacteristics;
+import com.android.tools.r8.horizontalclassmerging.policies.PreventClassMethodAndDefaultMethodCollisions;
+import com.android.tools.r8.horizontalclassmerging.policies.RespectPackageBoundaries;
+import com.android.tools.r8.horizontalclassmerging.policies.SameFeatureSplit;
+import com.android.tools.r8.horizontalclassmerging.policies.SameInstanceFields;
+import com.android.tools.r8.horizontalclassmerging.policies.SameMainDexGroup;
+import com.android.tools.r8.horizontalclassmerging.policies.SameNestHost;
+import com.android.tools.r8.horizontalclassmerging.policies.SameParentClass;
+import com.android.tools.r8.horizontalclassmerging.policies.SyntheticItemsPolicy;
+import com.android.tools.r8.horizontalclassmerging.policies.VerifyPolicyAlwaysSatisfied;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.RuntimeTypeCheckInfo;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+
+public class PolicyScheduler {
+
+  public static List<Policy> getPolicies(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      Mode mode,
+      RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
+    return ImmutableList.<Policy>builder()
+        .addAll(getSingleClassPolicies(appView, mode, runtimeTypeCheckInfo))
+        .addAll(getMultiClassPolicies(appView, mode, runtimeTypeCheckInfo))
+        .build();
+  }
+
+  private static List<SingleClassPolicy> getSingleClassPolicies(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      Mode mode,
+      RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
+    ImmutableList.Builder<SingleClassPolicy> builder = ImmutableList.builder();
+
+    addRequiredSingleClassPolicies(appView, builder);
+
+    if (mode.isInitial()) {
+      AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
+      builder.add(
+          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()) {
+      assert verifySingleClassPoliciesIrrelevantForMergingSynthetics(appView, mode, builder);
+    } else {
+      AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
+      addSingleClassPoliciesForMergingNonSyntheticClasses(
+          appViewWithLiveness, mode, runtimeTypeCheckInfo, builder);
+    }
+
+    return builder.build();
+  }
+
+  private static void addRequiredSingleClassPolicies(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      ImmutableList.Builder<SingleClassPolicy> builder) {
+    builder.add(
+        new CheckSyntheticClasses(appView),
+        new NoKeepRules(appView),
+        new NoClassInitializerWithObservableSideEffects());
+  }
+
+  private static void addSingleClassPoliciesForMergingNonSyntheticClasses(
+      AppView<AppInfoWithLiveness> appView,
+      Mode mode,
+      RuntimeTypeCheckInfo runtimeTypeCheckInfo,
+      ImmutableList.Builder<SingleClassPolicy> builder) {
+    builder.add(
+        new NotMatchedByNoHorizontalClassMerging(appView),
+        new NoAnnotationClasses(),
+        new NoDirectRuntimeTypeChecks(appView, mode, runtimeTypeCheckInfo),
+        new NoEnums(appView),
+        new NoInterfaces(appView, mode),
+        new NoInnerClasses(),
+        new NoInstanceFieldAnnotations(),
+        new NoKotlinMetadata(),
+        new NoNativeMethods(),
+        new NoServiceLoaders(appView));
+  }
+
+  private static boolean verifySingleClassPoliciesIrrelevantForMergingSynthetics(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      Mode mode,
+      ImmutableList.Builder<SingleClassPolicy> builder) {
+    List<SingleClassPolicy> policies =
+        ImmutableList.of(
+            new NoAnnotationClasses(),
+            new NoDirectRuntimeTypeChecks(appView, mode),
+            new NoEnums(appView),
+            new NoInterfaces(appView, mode),
+            new NoInnerClasses(),
+            new NoInstanceFieldAnnotations(),
+            new NoKotlinMetadata(),
+            new NoNativeMethods(),
+            new NoServiceLoaders(appView));
+    policies.stream().map(VerifyPolicyAlwaysSatisfied::new).forEach(builder::add);
+    return true;
+  }
+
+  private static List<Policy> getMultiClassPolicies(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      Mode mode,
+      RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
+    ImmutableList.Builder<Policy> builder = ImmutableList.builder();
+
+    addRequiredMultiClassPolicies(appView, mode, builder);
+
+    if (!appView.options().horizontalClassMergerOptions().isRestrictedToSynthetics()) {
+      AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
+      addMultiClassPoliciesForMergingNonSyntheticClasses(
+          appViewWithLiveness, runtimeTypeCheckInfo, builder);
+    }
+
+    if (mode.isInitial()) {
+      AppView<AppInfoWithLiveness> appViewWithLiveness = appView.withLiveness();
+      builder.add(
+          new AllInstantiatedOrUninstantiated(appViewWithLiveness, mode),
+          new PreserveMethodCharacteristics(appViewWithLiveness, mode),
+          new MinimizeInstanceFieldCasts());
+    } 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));
+    }
+
+    addMultiClassPoliciesForInterfaceMerging(appView, mode, builder);
+
+    return builder.add(new LimitGroups(appView)).build();
+  }
+
+  private static void addRequiredMultiClassPolicies(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      Mode mode,
+      ImmutableList.Builder<Policy> builder) {
+    builder.add(
+        new CheckAbstractClasses(appView),
+        new NoClassAnnotationCollisions(),
+        new SameFeatureSplit(appView),
+        new SameInstanceFields(appView, mode),
+        new SameMainDexGroup(appView),
+        new SameNestHost(appView),
+        new SameParentClass(),
+        new SyntheticItemsPolicy(appView, mode),
+        new RespectPackageBoundaries(appView),
+        new PreventClassMethodAndDefaultMethodCollisions(appView));
+  }
+
+  private static void addMultiClassPoliciesForMergingNonSyntheticClasses(
+      AppView<AppInfoWithLiveness> appView,
+      RuntimeTypeCheckInfo runtimeTypeCheckInfo,
+      ImmutableList.Builder<Policy> builder) {
+    builder.add(
+        new NoDeadLocks(appView), new NoIndirectRuntimeTypeChecks(appView, runtimeTypeCheckInfo));
+  }
+
+  private static void addMultiClassPoliciesForInterfaceMerging(
+      AppView<? extends AppInfoWithClassHierarchy> appView,
+      Mode mode,
+      ImmutableList.Builder<Policy> builder) {
+    builder.add(
+        new OnlyDirectlyConnectedOrUnrelatedInterfaces(appView, mode),
+        new NoDefaultInterfaceMethodMerging(appView, mode),
+        new NoDefaultInterfaceMethodCollisions(appView, mode));
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/SingleClassPolicy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/SingleClassPolicy.java
index b0757c5..db2d8eb 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/SingleClassPolicy.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/SingleClassPolicy.java
@@ -13,4 +13,14 @@
    * @return {@code false} if the class should not be merged, otherwise {@code true}.
    */
   public abstract boolean canMerge(DexProgramClass program);
+
+  @Override
+  public boolean isSingleClassPolicy() {
+    return true;
+  }
+
+  @Override
+  public SingleClassPolicy asSingleClassPolicy() {
+    return this;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java
index 53ca8c8..442489d 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java
@@ -17,6 +17,8 @@
 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.EnclosingMethodAttribute;
 import com.android.tools.r8.graph.TreeFixerBase;
 import com.android.tools.r8.ir.conversion.ExtraUnusedNullParameter;
 import com.android.tools.r8.shaking.AnnotationFixer;
@@ -115,8 +117,8 @@
   public HorizontalClassMergerGraphLens fixupTypeReferences() {
     List<DexProgramClass> classes = appView.appInfo().classesWithDeterministicOrder();
     Iterables.filter(classes, DexProgramClass::isInterface).forEach(this::fixupInterfaceClass);
-
-    classes.forEach(this::fixupProgramClassSuperType);
+    classes.forEach(this::fixupAttributes);
+    classes.forEach(this::fixupProgramClassSuperTypes);
     SubtypingForrestForClasses subtypingForrest = new SubtypingForrestForClasses(appView);
     // TODO(b/170078037): parallelize this code segment.
     for (DexProgramClass root : subtypingForrest.getProgramRoots()) {
@@ -127,8 +129,24 @@
     return lens;
   }
 
-  private void fixupProgramClassSuperType(DexProgramClass clazz) {
+  private void fixupAttributes(DexProgramClass clazz) {
+    if (clazz.hasEnclosingMethodAttribute()) {
+      EnclosingMethodAttribute enclosingMethodAttribute = clazz.getEnclosingMethodAttribute();
+      if (mergedClasses.hasBeenMergedIntoDifferentType(
+          enclosingMethodAttribute.getEnclosingType())) {
+        clazz.clearEnclosingMethodAttribute();
+      } else {
+        clazz.setEnclosingMethodAttribute(fixupEnclosingMethodAttribute(enclosingMethodAttribute));
+      }
+    }
+    clazz.setInnerClasses(fixupInnerClassAttributes(clazz.getInnerClasses()));
+    clazz.setNestHostAttribute(fixupNestHost(clazz.getNestHostClassAttribute()));
+    clazz.setNestMemberAttributes(fixupNestMemberAttributes(clazz.getNestMembersClassAttributes()));
+  }
+
+  private void fixupProgramClassSuperTypes(DexProgramClass clazz) {
     clazz.superType = fixupType(clazz.superType);
+    clazz.setInterfaces(fixupInterfaces(clazz, clazz.getInterfaces()));
   }
 
   private BiMap<DexMethodSignature, DexMethodSignature> fixupProgramClass(
@@ -197,10 +215,6 @@
 
   private void fixupInterfaceClass(DexProgramClass iface) {
     Set<DexMethodSignature> newDirectMethods = new LinkedHashSet<>();
-
-    assert iface.superType == dexItemFactory.objectType;
-    iface.superType = mergedClasses.getMergeTargetOrDefault(iface.superType);
-
     iface
         .getMethodCollection()
         .replaceDirectMethods(method -> fixupDirectMethod(newDirectMethods, method));
@@ -210,6 +224,16 @@
     lensBuilder.commitPendingUpdates();
   }
 
+  private DexTypeList fixupInterfaces(DexProgramClass clazz, DexTypeList interfaceTypes) {
+    Set<DexType> seen = Sets.newIdentityHashSet();
+    return interfaceTypes.map(
+        interfaceType -> {
+          DexType rewrittenInterfaceType = mapClassType(interfaceType);
+          assert rewrittenInterfaceType != clazz.getType();
+          return seen.add(rewrittenInterfaceType) ? rewrittenInterfaceType : null;
+        });
+  }
+
   private DexEncodedMethod fixupProgramMethod(
       DexMethod newMethodReference, DexEncodedMethod method) {
     DexMethod originalMethodReference = method.getReference();
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
index 7a13785..394c884 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
@@ -53,8 +53,8 @@
   public static class Builder {
     private final List<ProgramMethod> methods = new ArrayList<>();
 
-    public Builder add(ProgramMethod constructor) {
-      methods.add(constructor);
+    public Builder add(ProgramMethod method) {
+      methods.add(method);
       return this;
     }
 
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
index 99d8f48..1b8a318 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerSynthesizedCode.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/code/ClassInitializerSynthesizedCode.java
@@ -13,6 +13,7 @@
 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;
@@ -58,7 +59,13 @@
     }
   }
 
-  public CfCode synthesizeCode(DexType originalHolder) {
+  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();
@@ -83,6 +90,11 @@
   }
 
   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);
   }
 
@@ -92,7 +104,6 @@
     public void add(DexEncodedMethod method) {
       assert method.isClassInitializer();
       assert method.hasCode();
-      assert method.getCode().isCfCode();
       staticClassInitializers.add(method);
     }
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AllInstantiatedOrUninstantiated.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AllInstantiatedOrUninstantiated.java
index 38150db..e0dc0fe 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AllInstantiatedOrUninstantiated.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AllInstantiatedOrUninstantiated.java
@@ -6,6 +6,7 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.MultiClassSameReferencePolicy;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 
@@ -13,7 +14,11 @@
 
   private final AppView<AppInfoWithLiveness> appView;
 
-  public AllInstantiatedOrUninstantiated(AppView<AppInfoWithLiveness> appView) {
+  public AllInstantiatedOrUninstantiated(AppView<AppInfoWithLiveness> appView, Mode mode) {
+    // This policy is only used to prevent that horizontal class merging regresses the
+    // uninstantiated type optimization. Since there won't be any IR processing after the final
+    // round of horizontal class merging, there is no need to use the policy.
+    assert mode.isInitial();
     this.appView = appView;
   }
 
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
new file mode 100644
index 0000000..a2b5887
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AtMostOneClassInitializer.java
@@ -0,0 +1,27 @@
+// 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/AtMostOneClassThatMatchesPolicy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AtMostOneClassThatMatchesPolicy.java
new file mode 100644
index 0000000..c73675f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/AtMostOneClassThatMatchesPolicy.java
@@ -0,0 +1,47 @@
+// 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 static com.android.tools.r8.utils.IteratorUtils.createCircularIterator;
+
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+public abstract class AtMostOneClassThatMatchesPolicy extends MultiClassPolicy {
+
+  @Override
+  public Collection<MergeGroup> apply(MergeGroup group) {
+    // Create a new merge group for each class that we want at most one of.
+    List<MergeGroup> newGroups = new LinkedList<>();
+    for (DexProgramClass clazz : group) {
+      if (atMostOneOf(clazz)) {
+        newGroups.add(new MergeGroup(clazz));
+      }
+    }
+
+    // If there were at most one class with that we could have at most one of, then just return the
+    // original merge group.
+    if (newGroups.size() <= 1) {
+      return ImmutableList.of(group);
+    }
+
+    // Otherwise, fill up the new merge groups with the remaining classes.
+    Iterator<MergeGroup> newGroupsIterator = createCircularIterator(newGroups);
+    for (DexProgramClass clazz : group) {
+      if (!atMostOneOf(clazz)) {
+        newGroupsIterator.next().add(clazz);
+      }
+    }
+    return removeTrivialGroups(newGroups);
+  }
+
+  abstract boolean atMostOneOf(DexProgramClass clazz);
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/CheckSyntheticClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/CheckSyntheticClasses.java
new file mode 100644
index 0000000..8c08712
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/CheckSyntheticClasses.java
@@ -0,0 +1,39 @@
+// 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.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
+import com.android.tools.r8.synthesis.SyntheticItems;
+import com.android.tools.r8.utils.InternalOptions.HorizontalClassMergerOptions;
+
+public class CheckSyntheticClasses extends SingleClassPolicy {
+
+  private final HorizontalClassMergerOptions options;
+  private final SyntheticItems syntheticItems;
+
+  public CheckSyntheticClasses(AppView<? extends AppInfoWithClassHierarchy> appView) {
+    this.options = appView.options().horizontalClassMergerOptions();
+    this.syntheticItems = appView.getSyntheticItems();
+  }
+
+  @Override
+  public boolean canMerge(DexProgramClass clazz) {
+    if (!options.isSyntheticMergingEnabled() && syntheticItems.isSyntheticClass(clazz)) {
+      return false;
+    }
+    if (options.isRestrictedToSynthetics() && !syntheticItems.isSyntheticClass(clazz)) {
+      return false;
+    }
+    return true;
+  }
+
+  @Override
+  public String getName() {
+    return "CheckSyntheticClasses";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/MinimizeFieldCasts.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/MinimizeInstanceFieldCasts.java
similarity index 97%
rename from src/main/java/com/android/tools/r8/horizontalclassmerging/policies/MinimizeFieldCasts.java
rename to src/main/java/com/android/tools/r8/horizontalclassmerging/policies/MinimizeInstanceFieldCasts.java
index 1ef2f6a..0242766 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/MinimizeFieldCasts.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/MinimizeInstanceFieldCasts.java
@@ -17,7 +17,7 @@
 import java.util.List;
 import java.util.Map;
 
-public class MinimizeFieldCasts extends MultiClassPolicy {
+public class MinimizeInstanceFieldCasts extends MultiClassPolicy {
 
   @Override
   public final Collection<MergeGroup> apply(MergeGroup group) {
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoClassAnnotationCollisions.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoClassAnnotationCollisions.java
index 1a96901..482a0e0 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoClassAnnotationCollisions.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoClassAnnotationCollisions.java
@@ -4,42 +4,13 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
-import static com.android.tools.r8.utils.IteratorUtils.createCircularIterator;
-
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.MergeGroup;
-import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
-import com.google.common.collect.ImmutableList;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
 
-public class NoClassAnnotationCollisions extends MultiClassPolicy {
+public class NoClassAnnotationCollisions extends AtMostOneClassThatMatchesPolicy {
 
   @Override
-  public Collection<MergeGroup> apply(MergeGroup group) {
-    // Create a new merge group for each class that has annotations.
-    List<MergeGroup> newGroups = new LinkedList<>();
-    for (DexProgramClass clazz : group) {
-      if (clazz.hasAnnotations()) {
-        newGroups.add(new MergeGroup(clazz));
-      }
-    }
-
-    // If there were at most one class with annotations, then just return the original merge group.
-    if (newGroups.size() <= 1) {
-      return ImmutableList.of(group);
-    }
-
-    // Otherwise, fill up the new merge groups with the classes that do not have annotations.
-    Iterator<MergeGroup> newGroupsIterator = createCircularIterator(newGroups);
-    for (DexProgramClass clazz : group) {
-      if (!clazz.hasAnnotations()) {
-        newGroupsIterator.next().add(clazz);
-      }
-    }
-    return removeTrivialGroups(newGroups);
+  boolean atMostOneOf(DexProgramClass clazz) {
+    return clazz.hasAnnotations();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoConstructorCollisions.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoConstructorCollisions.java
new file mode 100644
index 0000000..5b058c9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoConstructorCollisions.java
@@ -0,0 +1,150 @@
+// 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.AppView;
+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.DexProgramClass;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
+import com.android.tools.r8.horizontalclassmerging.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.MultiClassPolicyWithPreprocessing;
+import com.android.tools.r8.utils.ArrayUtils;
+import com.android.tools.r8.utils.IterableUtils;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.WorkList;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * In the final round, we're not allowed to resolve constructor collisions by appending null
+ * arguments to constructor calls.
+ *
+ * <p>As an example, if a class in the program declares the constructors {@code <init>(A)} and
+ * {@code <init>(B)}, the classes A and B must not be merged.
+ *
+ * <p>To avoid collisions of this kind, we run over all the classes in the program, and apply the
+ * current set of merge groups to the constructor signatures of each class. Then, in case of a
+ * collision, we extract all the mapped types from the constructor signatures, and prevent merging
+ * of these types.
+ */
+public class NoConstructorCollisions extends MultiClassPolicyWithPreprocessing<Set<DexType>> {
+
+  private final AppView<?> appView;
+  private final DexItemFactory dexItemFactory;
+
+  public NoConstructorCollisions(AppView<?> appView, Mode mode) {
+    assert mode.isFinal();
+    this.appView = appView;
+    this.dexItemFactory = appView.dexItemFactory();
+  }
+
+  /**
+   * Removes the classes in {@param collisionResolution} from {@param group}, and returns the new
+   * filtered group.
+   */
+  @Override
+  public Collection<MergeGroup> apply(MergeGroup group, Set<DexType> collisionResolution) {
+    MergeGroup newGroup =
+        new MergeGroup(
+            Iterables.filter(group, clazz -> !collisionResolution.contains(clazz.getType())));
+    return newGroup.isTrivial() ? Collections.emptyList() : ListUtils.newLinkedList(newGroup);
+  }
+
+  /**
+   * Computes the set of classes that must not be merged, because the merging of these classes could
+   * lead to constructor collisions.
+   */
+  @Override
+  public Set<DexType> preprocess(Collection<MergeGroup> groups) {
+    // Build a mapping from types to groups.
+    Map<DexType, MergeGroup> groupsByType = new IdentityHashMap<>();
+    for (MergeGroup group : groups) {
+      for (DexProgramClass clazz : group) {
+        groupsByType.put(clazz.getType(), group);
+      }
+    }
+
+    // Find the set of types that must not be merged, because they could lead to a constructor
+    // collision.
+    Set<DexType> collisionResolution = Sets.newIdentityHashSet();
+    WorkList<DexProgramClass> workList = WorkList.newIdentityWorkList(appView.appInfo().classes());
+    while (workList.hasNext()) {
+      // Iterate over all the instance initializers of the current class. If the current class is in
+      // a merge group, we must include all constructors of the entire merge group.
+      DexProgramClass current = workList.next();
+      Iterable<DexProgramClass> group =
+          groupsByType.containsKey(current.getType())
+              ? groupsByType.get(current.getType())
+              : IterableUtils.singleton(current);
+      Set<DexMethod> seen = Sets.newIdentityHashSet();
+      for (DexProgramClass clazz : group) {
+        for (DexEncodedMethod method :
+            clazz.directMethods(DexEncodedMethod::isInstanceInitializer)) {
+          // Rewrite the constructor reference using the current merge groups.
+          DexMethod newReference = rewriteReference(method.getReference(), groupsByType);
+          if (!seen.add(newReference)) {
+            // Found a collision. Block all referenced types from being merged.
+            for (DexType type : method.getProto().getBaseTypes(dexItemFactory)) {
+              if (type.isClassType() && groupsByType.containsKey(type)) {
+                collisionResolution.add(type);
+              }
+            }
+          }
+        }
+      }
+      workList.markAsSeen(group);
+    }
+    return collisionResolution;
+  }
+
+  private DexProto rewriteProto(DexProto proto, Map<DexType, MergeGroup> groups) {
+    DexType[] parameters =
+        ArrayUtils.map(
+            DexType[].class,
+            proto.getParameters().values,
+            parameter -> rewriteType(parameter, groups));
+    return dexItemFactory.createProto(rewriteType(proto.getReturnType(), groups), parameters);
+  }
+
+  private DexMethod rewriteReference(DexMethod method, Map<DexType, MergeGroup> groups) {
+    return dexItemFactory.createMethod(
+        rewriteType(method.getHolderType(), groups),
+        rewriteProto(method.getProto(), groups),
+        method.getName());
+  }
+
+  private DexType rewriteType(DexType type, Map<DexType, MergeGroup> groups) {
+    if (type.isArrayType()) {
+      DexType baseType = type.toBaseType(dexItemFactory);
+      DexType rewrittenBaseType = rewriteType(baseType, groups);
+      if (rewrittenBaseType == baseType) {
+        return type;
+      }
+      return type.replaceBaseType(rewrittenBaseType, dexItemFactory);
+    }
+    if (type.isClassType()) {
+      if (!groups.containsKey(type)) {
+        return type;
+      }
+      return groups.get(type).getClasses().getFirst().getType();
+    }
+    assert type.isPrimitiveType() || type.isVoidType();
+    return type;
+  }
+
+  @Override
+  public String getName() {
+    return "NoConstructorCollisions";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDeadEnumLiteMaps.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDeadEnumLiteMaps.java
index 3706d0a..8d1205d 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDeadEnumLiteMaps.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDeadEnumLiteMaps.java
@@ -7,6 +7,7 @@
 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.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
 import com.android.tools.r8.ir.analysis.proto.EnumLiteProtoShrinker;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -17,7 +18,10 @@
 
   private final Set<DexType> deadEnumLiteMaps;
 
-  public NoDeadEnumLiteMaps(AppView<AppInfoWithLiveness> appView) {
+  public NoDeadEnumLiteMaps(AppView<AppInfoWithLiveness> appView, Mode mode) {
+    // This policy is only relevant for the initial round of class merging, since the dead enum lite
+    // maps have been removed from the application when the final round of class merging runs.
+    assert mode.isInitial();
     this.deadEnumLiteMaps =
         appView.withProtoEnumShrinker(
             EnumLiteProtoShrinker::getDeadEnumLiteMaps, Collections.emptySet());
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/DontMergeSynchronizedClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDeadLocks.java
similarity index 91%
rename from src/main/java/com/android/tools/r8/horizontalclassmerging/policies/DontMergeSynchronizedClasses.java
rename to src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDeadLocks.java
index e1cb9a7..576e0ef 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/DontMergeSynchronizedClasses.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDeadLocks.java
@@ -14,10 +14,10 @@
 import java.util.Iterator;
 import java.util.LinkedList;
 
-public class DontMergeSynchronizedClasses extends MultiClassPolicy {
+public class NoDeadLocks extends MultiClassPolicy {
   private final AppView<AppInfoWithLiveness> appView;
 
-  public DontMergeSynchronizedClasses(AppView<AppInfoWithLiveness> appView) {
+  public NoDeadLocks(AppView<AppInfoWithLiveness> appView) {
     this.appView = appView;
   }
 
@@ -59,6 +59,6 @@
 
   @Override
   public String getName() {
-    return "DontMergeSynchronizedClasses";
+    return "NoDeadLocks";
   }
 }
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
new file mode 100644
index 0000000..b091242
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodCollisions.java
@@ -0,0 +1,343 @@
+// 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.horizontalclassmerging.policies;
+
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.emptySet;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.BottomUpClassHierarchyTraversal;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexMethodSignature;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.SubtypingInfo;
+import com.android.tools.r8.graph.TopDownClassHierarchyTraversal;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
+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.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.Sets;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This policy prevents that interface merging changes semantics of invoke-interface/invoke-virtual
+ * instructions that dispatch to default interface methods.
+ *
+ * <p>As a simple example, consider the following snippet of code. If we merge interfaces I and K,
+ * then we effectively add the default interface method K.m() to I, which would change the semantics
+ * of calls to A.m().
+ *
+ * <pre>
+ *   interface I {}
+ *   interface J {
+ *     default void m() { print("J"); }
+ *   }
+ *   interface K {
+ *     default void m() { print("K"); }
+ *   }
+ *   class A implements I, J {}
+ * </pre>
+ *
+ * Note that we also cannot merge I with K, even if K does not declare any methods directly:
+ *
+ * <pre>
+ *   interface K0 {
+ *     default void m() { print("K"); }
+ *   }
+ *   interface K extends K0 {}
+ * </pre>
+ *
+ * Also, note that this is not a problem if class A overrides void m().
+ */
+public class NoDefaultInterfaceMethodCollisions
+    extends MultiClassPolicyWithPreprocessing<Map<DexType, InterfaceInfo>> {
+
+  private final AppView<? extends AppInfoWithClassHierarchy> appView;
+  private final Mode mode;
+
+  public NoDefaultInterfaceMethodCollisions(
+      AppView<? extends AppInfoWithClassHierarchy> appView, Mode mode) {
+    this.appView = appView;
+    this.mode = mode;
+  }
+
+  @Override
+  public Collection<MergeGroup> apply(MergeGroup group, Map<DexType, InterfaceInfo> infos) {
+    if (!group.isInterfaceGroup()) {
+      return ImmutableList.of(group);
+    }
+
+    // For each interface I in the group, check that each (non-interface) subclass of I does not
+    // inherit a default method that is also declared by another interface J in the merge group.
+    //
+    // Note that the primary piece of work is carried out in the preprocess() method
+    //
+    // TODO(b/173990042): Consider forming multiple groups instead of just filtering. In practice,
+    //  this rarely leads to much filtering, though, since the use of default methods is somewhat
+    //  limited.
+    MergeGroup newGroup = new MergeGroup();
+    for (DexProgramClass clazz : group) {
+      Set<DexMethod> newDefaultMethodsAddedToClassByMerge =
+          computeNewDefaultMethodsAddedToClassByMerge(clazz, group, infos);
+      if (isSafeToAddDefaultMethodsToClass(clazz, newDefaultMethodsAddedToClassByMerge, infos)) {
+        newGroup.add(clazz);
+      }
+    }
+    return newGroup.isTrivial() ? Collections.emptyList() : ListUtils.newLinkedList(newGroup);
+  }
+
+  private Set<DexMethod> computeNewDefaultMethodsAddedToClassByMerge(
+      DexProgramClass clazz, MergeGroup group, Map<DexType, InterfaceInfo> infos) {
+    // Run through the other classes in the merge group, and add the default interface methods that
+    // they declare (or inherit from a super interface) to a set.
+    Set<DexMethod> newDefaultMethodsAddedToClassByMerge = Sets.newIdentityHashSet();
+    for (DexProgramClass other : group) {
+      if (other != clazz) {
+        Collection<Set<DexMethod>> inheritedDefaultMethodsFromOther =
+            infos.get(other.getType()).getInheritedDefaultMethods().values();
+        inheritedDefaultMethodsFromOther.forEach(newDefaultMethodsAddedToClassByMerge::addAll);
+      }
+    }
+    return newDefaultMethodsAddedToClassByMerge;
+  }
+
+  private boolean isSafeToAddDefaultMethodsToClass(
+      DexProgramClass clazz,
+      Set<DexMethod> newDefaultMethodsAddedToClassByMerge,
+      Map<DexType, InterfaceInfo> infos) {
+    // Check if there is a subclass of this interface, which inherits a default interface method
+    // that would also be added by to this interface by merging the interfaces in the group.
+    Map<DexMethodSignature, Set<DexMethod>> defaultMethodsInheritedBySubclassesOfClass =
+        infos.get(clazz.getType()).getDefaultMethodsInheritedBySubclasses();
+    for (DexMethod newDefaultMethodAddedToClassByMerge : newDefaultMethodsAddedToClassByMerge) {
+      Set<DexMethod> defaultMethodsInheritedBySubclassesOfClassWithSameSignature =
+          defaultMethodsInheritedBySubclassesOfClass.getOrDefault(
+              newDefaultMethodAddedToClassByMerge.getSignature(), emptySet());
+      // Look for a method different from the method we're adding.
+      for (DexMethod method : defaultMethodsInheritedBySubclassesOfClassWithSameSignature) {
+        if (method != newDefaultMethodAddedToClassByMerge) {
+          return false;
+        }
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public Map<DexType, InterfaceInfo> preprocess(Collection<MergeGroup> groups) {
+    SubtypingInfo subtypingInfo = new SubtypingInfo(appView);
+    Collection<DexProgramClass> classesOfInterest = computeClassesOfInterest(subtypingInfo);
+    Map<DexType, DexMethodSignatureSet> inheritedClassMethodsPerClass =
+        computeInheritedClassMethodsPerProgramClass(classesOfInterest);
+    Map<DexType, Map<DexMethodSignature, Set<DexMethod>>> inheritedDefaultMethodsPerClass =
+        computeInheritedDefaultMethodsPerProgramType(
+            classesOfInterest, inheritedClassMethodsPerClass);
+
+    // Finally, do a bottom-up traversal, pushing the inherited default methods upwards.
+    Map<DexType, Map<DexMethodSignature, Set<DexMethod>>>
+        defaultMethodsInheritedBySubclassesPerClass =
+            computeDefaultMethodsInheritedBySubclassesPerProgramClass(
+                classesOfInterest, inheritedDefaultMethodsPerClass, subtypingInfo);
+
+    // Store the computed information for each interface that is subject to merging.
+    Map<DexType, InterfaceInfo> infos = new IdentityHashMap<>();
+    for (MergeGroup group : groups) {
+      if (group.isInterfaceGroup()) {
+        for (DexProgramClass clazz : group) {
+          infos.put(
+              clazz.getType(),
+              new InterfaceInfo(
+                  inheritedDefaultMethodsPerClass.getOrDefault(clazz.getType(), emptyMap()),
+                  defaultMethodsInheritedBySubclassesPerClass.getOrDefault(
+                      clazz.getType(), emptyMap())));
+        }
+      }
+    }
+    return infos;
+  }
+
+  /** Returns the set of program classes that must be considered during preprocessing. */
+  private Collection<DexProgramClass> computeClassesOfInterest(SubtypingInfo subtypingInfo) {
+    // TODO(b/173990042): Limit result to the set of classes that are in the same as one of
+    //  the interfaces that is subject to merging.
+    return appView.appInfo().classes();
+  }
+
+  /**
+   * For each class, computes the (transitive) set of virtual methods that is declared on the class
+   * itself or one of its (non-interface) super classes.
+   */
+  private Map<DexType, DexMethodSignatureSet> computeInheritedClassMethodsPerProgramClass(
+      Collection<DexProgramClass> classesOfInterest) {
+    Map<DexType, DexMethodSignatureSet> inheritedClassMethodsPerClass = new IdentityHashMap<>();
+    TopDownClassHierarchyTraversal.forAllClasses(appView)
+        .excludeInterfaces()
+        .visit(
+            classesOfInterest,
+            clazz -> {
+              DexMethodSignatureSet classMethods =
+                  DexMethodSignatureSet.create(
+                      inheritedClassMethodsPerClass.getOrDefault(
+                          clazz.getSuperType(), DexMethodSignatureSet.empty()));
+              for (DexEncodedMethod method : clazz.virtualMethods()) {
+                classMethods.add(method.getSignature());
+              }
+              inheritedClassMethodsPerClass.put(clazz.getType(), classMethods);
+            });
+    inheritedClassMethodsPerClass
+        .keySet()
+        .removeIf(type -> asProgramClassOrNull(appView.definitionFor(type)) == null);
+    return inheritedClassMethodsPerClass;
+  }
+
+  /**
+   * For each class or interface, computes the (transitive) set of virtual methods that is declared
+   * on the class itself or one of its (non-interface) super classes.
+   */
+  private Map<DexType, Map<DexMethodSignature, Set<DexMethod>>>
+      computeInheritedDefaultMethodsPerProgramType(
+          Collection<DexProgramClass> classesOfInterest,
+          Map<DexType, DexMethodSignatureSet> inheritedClassMethodsPerClass) {
+    Map<DexType, Map<DexMethodSignature, Set<DexMethod>>> inheritedDefaultMethodsPerType =
+        new IdentityHashMap<>();
+    TopDownClassHierarchyTraversal.forAllClasses(appView)
+        .visit(
+            classesOfInterest,
+            clazz -> {
+              // Compute the set of default method signatures that this class inherits from its
+              // super class and interfaces.
+              Map<DexMethodSignature, Set<DexMethod>> inheritedDefaultMethods = new HashMap<>();
+              for (DexType supertype : clazz.allImmediateSupertypes()) {
+                Map<DexMethodSignature, Set<DexMethod>> inheritedDefaultMethodsFromSuperType =
+                    inheritedDefaultMethodsPerType.getOrDefault(supertype, emptyMap());
+                inheritedDefaultMethodsFromSuperType.forEach(
+                    (signature, methods) ->
+                        inheritedDefaultMethods
+                            .computeIfAbsent(signature, ignore -> Sets.newIdentityHashSet())
+                            .addAll(methods));
+              }
+
+              // If this is an interface, also include the default methods it declares.
+              if (clazz.isInterface()) {
+                for (DexEncodedMethod method :
+                    clazz.virtualMethods(DexEncodedMethod::isDefaultMethod)) {
+                  inheritedDefaultMethods
+                      .computeIfAbsent(method.getSignature(), ignore -> Sets.newIdentityHashSet())
+                      .add(method.getReference());
+                }
+              }
+
+              // Remove all default methods that are declared as (non-interface) class methods on
+              // the current class.
+              inheritedDefaultMethods
+                  .keySet()
+                  .removeAll(
+                      inheritedClassMethodsPerClass.getOrDefault(
+                          clazz.getType(), DexMethodSignatureSet.empty()));
+
+              if (!inheritedDefaultMethods.isEmpty()) {
+                inheritedDefaultMethodsPerType.put(clazz.getType(), inheritedDefaultMethods);
+              }
+            });
+    inheritedDefaultMethodsPerType
+        .keySet()
+        .removeIf(type -> asProgramClassOrNull(appView.definitionFor(type)) == null);
+    return inheritedDefaultMethodsPerType;
+  }
+
+  /**
+   * Performs a bottom-up traversal of the hierarchy, where the inherited default methods of each
+   * class are pushed upwards. This accumulates the set of default methods that are inherited by all
+   * subclasses of a given interface.
+   */
+  private Map<DexType, Map<DexMethodSignature, Set<DexMethod>>>
+      computeDefaultMethodsInheritedBySubclassesPerProgramClass(
+          Collection<DexProgramClass> classesOfInterest,
+          Map<DexType, Map<DexMethodSignature, Set<DexMethod>>> inheritedDefaultMethodsPerClass,
+          SubtypingInfo subtypingInfo) {
+    // Copy the map from classes to their inherited default methods.
+    Map<DexType, Map<DexMethodSignature, Set<DexMethod>>>
+        defaultMethodsInheritedBySubclassesPerClass =
+            MapUtils.clone(
+                inheritedDefaultMethodsPerClass,
+                new HashMap<>(),
+                outerValue ->
+                    MapUtils.clone(outerValue, new HashMap<>(), SetUtils::newIdentityHashSet));
+    BottomUpClassHierarchyTraversal.forProgramClasses(appView, subtypingInfo)
+        .visit(
+            classesOfInterest,
+            clazz -> {
+              // Push the current class' default methods upwards to all super classes.
+              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));
+              }
+            });
+    defaultMethodsInheritedBySubclassesPerClass
+        .keySet()
+        .removeIf(type -> asProgramClassOrNull(appView.definitionFor(type)) == null);
+    return defaultMethodsInheritedBySubclassesPerClass;
+  }
+
+  @Override
+  public String getName() {
+    return "NoDefaultInterfaceMethodCollisions";
+  }
+
+  @Override
+  public boolean shouldSkipPolicy() {
+    return !appView.options().horizontalClassMergerOptions().isInterfaceMergingEnabled(mode);
+  }
+
+  static class InterfaceInfo {
+
+    // The set of default interface methods (grouped by signature) that this interface declares or
+    // inherits from one of its (transitive) super interfaces.
+    private final Map<DexMethodSignature, Set<DexMethod>> inheritedDefaultMethods;
+
+    // The set of default interface methods (grouped by signature) that subclasses of this interface
+    // inherits from one of its (transitively) implemented super interfaces.
+    private final Map<DexMethodSignature, Set<DexMethod>> defaultMethodsInheritedBySubclasses;
+
+    InterfaceInfo(
+        Map<DexMethodSignature, Set<DexMethod>> inheritedDefaultMethods,
+        Map<DexMethodSignature, Set<DexMethod>> defaultMethodsInheritedBySubclasses) {
+      this.inheritedDefaultMethods = inheritedDefaultMethods;
+      this.defaultMethodsInheritedBySubclasses = defaultMethodsInheritedBySubclasses;
+    }
+
+    Map<DexMethodSignature, Set<DexMethod>> getInheritedDefaultMethods() {
+      return inheritedDefaultMethods;
+    }
+
+    Map<DexMethodSignature, Set<DexMethod>> getDefaultMethodsInheritedBySubclasses() {
+      return defaultMethodsInheritedBySubclasses;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodMerging.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodMerging.java
new file mode 100644
index 0000000..ac73202
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDefaultInterfaceMethodMerging.java
@@ -0,0 +1,84 @@
+// 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.AppView;
+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.MergeGroup;
+import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.collections.DexMethodSignatureSet;
+import com.google.common.collect.Lists;
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * For interfaces, we cannot introduce an instance field `int $r8$classId`. Therefore, we can't
+ * merge two interfaces that declare the same default interface method.
+ *
+ * <p>This policy attempts to split a merge group consisting of interfaces into smaller merge groups
+ * such that each pairs of interfaces in each merge group does not have conflicting default
+ * interface methods.
+ */
+public class NoDefaultInterfaceMethodMerging extends MultiClassPolicy {
+
+  private final Mode mode;
+  private final InternalOptions options;
+
+  public NoDefaultInterfaceMethodMerging(AppView<?> appView, Mode mode) {
+    this.mode = mode;
+    this.options = appView.options();
+  }
+
+  @Override
+  public Collection<MergeGroup> apply(MergeGroup group) {
+    if (!group.isInterfaceGroup()) {
+      return ListUtils.newLinkedList(group);
+    }
+
+    // Split the group into smaller groups such that no default methods collide.
+    Map<MergeGroup, DexMethodSignatureSet> newGroups = new LinkedHashMap<>();
+    for (DexProgramClass clazz : group) {
+      addClassToGroup(clazz, newGroups);
+    }
+
+    return removeTrivialGroups(Lists.newLinkedList(newGroups.keySet()));
+  }
+
+  private void addClassToGroup(
+      DexProgramClass clazz, Map<MergeGroup, DexMethodSignatureSet> newGroups) {
+    DexMethodSignatureSet classSignatures = DexMethodSignatureSet.create();
+    classSignatures.addAllMethods(clazz.virtualMethods(DexEncodedMethod::isDefaultMethod));
+
+    // Find a group that does not have any collisions with `clazz`.
+    for (Entry<MergeGroup, DexMethodSignatureSet> entry : newGroups.entrySet()) {
+      MergeGroup group = entry.getKey();
+      DexMethodSignatureSet groupSignatures = entry.getValue();
+      if (!groupSignatures.containsAnyOf(classSignatures)) {
+        groupSignatures.addAll(classSignatures);
+        group.add(clazz);
+        return;
+      }
+    }
+
+    // Else create a new group.
+    newGroups.put(new MergeGroup(clazz), classSignatures);
+  }
+
+  @Override
+  public String getName() {
+    return "NoDefaultInterfaceMethodMerging";
+  }
+
+  @Override
+  public boolean shouldSkipPolicy() {
+    return !options.horizontalClassMergerOptions().isInterfaceMergingEnabled(mode);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDirectRuntimeTypeChecks.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDirectRuntimeTypeChecks.java
index 17659f8..d330434 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDirectRuntimeTypeChecks.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoDirectRuntimeTypeChecks.java
@@ -4,19 +4,39 @@
 
 package com.android.tools.r8.horizontalclassmerging.policies;
 
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
 import com.android.tools.r8.shaking.RuntimeTypeCheckInfo;
+import com.android.tools.r8.synthesis.SyntheticItems;
+import com.android.tools.r8.utils.InternalOptions;
 
 public class NoDirectRuntimeTypeChecks extends SingleClassPolicy {
-  private final RuntimeTypeCheckInfo runtimeTypeCheckInfo;
 
-  public NoDirectRuntimeTypeChecks(RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
+  private final InternalOptions options;
+  private final RuntimeTypeCheckInfo runtimeTypeCheckInfo;
+  private final SyntheticItems syntheticItems;
+
+  public NoDirectRuntimeTypeChecks(AppView<?> appView, Mode mode) {
+    this(appView, mode, null);
+  }
+
+  public NoDirectRuntimeTypeChecks(
+      AppView<?> appView, Mode mode, RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
+    assert runtimeTypeCheckInfo != null || mode.isFinal();
+    this.options = appView.options();
     this.runtimeTypeCheckInfo = runtimeTypeCheckInfo;
+    this.syntheticItems = appView.getSyntheticItems();
   }
 
   @Override
   public boolean canMerge(DexProgramClass clazz) {
+    if (runtimeTypeCheckInfo == null) {
+      assert syntheticItems.isSyntheticClass(clazz)
+          : "Expected synthetic, got: " + clazz.getTypeName();
+      return true;
+    }
     return !runtimeTypeCheckInfo.isRuntimeCheckType(clazz);
   }
 
@@ -24,4 +44,9 @@
   public String getName() {
     return "NoDirectRuntimeTypeChecks";
   }
+
+  @Override
+  public boolean shouldSkipPolicy() {
+    return options.horizontalClassMergerOptions().isIgnoreRuntimeTypeChecksForTestingEnabled();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/DontInlinePolicy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoIllegalInlining.java
similarity index 80%
rename from src/main/java/com/android/tools/r8/horizontalclassmerging/policies/DontInlinePolicy.java
rename to src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoIllegalInlining.java
index e6e67e0..fce04fa 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/DontInlinePolicy.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoIllegalInlining.java
@@ -9,16 +9,20 @@
 import com.android.tools.r8.graph.Code;
 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.SingleClassPolicy;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.google.common.collect.Iterables;
 
-public class DontInlinePolicy extends SingleClassPolicy {
+public class NoIllegalInlining extends SingleClassPolicy {
 
   private final AppView<AppInfoWithLiveness> appView;
 
-  public DontInlinePolicy(AppView<AppInfoWithLiveness> appView) {
+  public NoIllegalInlining(AppView<AppInfoWithLiveness> appView, Mode mode) {
+    // This policy is only relevant for the first round of horizontal class merging, since the final
+    // round of horizontal class merging may not require any inlining.
+    assert mode.isInitial();
     this.appView = appView;
   }
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoIndirectRuntimeTypeChecks.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoIndirectRuntimeTypeChecks.java
index 12b6ba4..bd5b1da 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoIndirectRuntimeTypeChecks.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoIndirectRuntimeTypeChecks.java
@@ -54,7 +54,8 @@
       cache.put(type, true);
       return true;
     }
-    if (runtimeTypeCheckInfo.isRuntimeCheckType(clazz.asProgramClass())) {
+    if (runtimeTypeCheckInfo == null
+        || runtimeTypeCheckInfo.isRuntimeCheckType(clazz.asProgramClass())) {
       cache.put(type, true);
       return true;
     }
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
new file mode 100644
index 0000000..0acfe13
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceInitializers.java
@@ -0,0 +1,29 @@
+// 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/NoInterfaces.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInterfaces.java
index d6032e7..79b6601 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInterfaces.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInterfaces.java
@@ -4,14 +4,31 @@
 
 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.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
+import com.android.tools.r8.utils.InternalOptions.HorizontalClassMergerOptions;
 
 public class NoInterfaces extends SingleClassPolicy {
 
+  private final Mode mode;
+  private final HorizontalClassMergerOptions options;
+
+  public NoInterfaces(AppView<? extends AppInfoWithClassHierarchy> appView, Mode mode) {
+    this.mode = mode;
+    this.options = appView.options().horizontalClassMergerOptions();
+  }
+
   @Override
-  public boolean canMerge(DexProgramClass program) {
-    return !program.isInterface();
+  public boolean canMerge(DexProgramClass clazz) {
+    return !clazz.isInterface();
+  }
+
+  @Override
+  public boolean shouldSkipPolicy() {
+    return options.isInterfaceMergingEnabled(mode);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoKeepRules.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoKeepRules.java
index a13a445..82b76d9 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoKeepRules.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoKeepRules.java
@@ -7,7 +7,6 @@
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedMember;
-import com.android.tools.r8.graph.DexMember;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
@@ -29,20 +28,20 @@
     appView.appInfo().classes().forEach(this::processClass);
   }
 
-  private void processClass(DexProgramClass programClass) {
-    DexType type = programClass.getType();
-    boolean pinProgramClass = keepInfo.isPinned(type, appView);
-    for (DexEncodedMember<?, ?> member : programClass.members()) {
-      DexMember<?, ?> reference = member.getReference();
-      if (keepInfo.isPinned(reference, appView)) {
-        pinProgramClass = true;
+  private void processClass(DexProgramClass clazz) {
+    DexType type = clazz.getType();
+    boolean pinHolder = keepInfo.getClassInfo(clazz).isPinned();
+    for (DexEncodedMember<?, ?> member : clazz.members()) {
+      if (keepInfo.getMemberInfo(member, clazz).isPinned()) {
+        pinHolder = true;
         Iterables.addAll(
             dontMergeTypes,
             Iterables.filter(
-                reference.getReferencedBaseTypes(appView.dexItemFactory()), DexType::isClassType));
+                member.getReference().getReferencedBaseTypes(appView.dexItemFactory()),
+                DexType::isClassType));
       }
     }
-    if (pinProgramClass) {
+    if (pinHolder) {
       dontMergeTypes.add(type);
     }
   }
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
new file mode 100644
index 0000000..81358f1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoNonPrivateVirtualMethods.java
@@ -0,0 +1,27 @@
+// 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/NoServiceLoaders.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoServiceLoaders.java
index 30bee86..c33b868 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoServiceLoaders.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoServiceLoaders.java
@@ -17,8 +17,7 @@
 
   public NoServiceLoaders(AppView<? extends AppInfoWithClassHierarchy> appView) {
     this.appView = appView;
-
-    allServiceImplementations = appView.appServices().computeAllServiceImplementations();
+    this.allServiceImplementations = appView.appServices().computeAllServiceImplementations();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVerticallyMergedClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVerticallyMergedClasses.java
new file mode 100644
index 0000000..c7534b3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoVerticallyMergedClasses.java
@@ -0,0 +1,35 @@
+// 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.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
+import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+public class NoVerticallyMergedClasses extends SingleClassPolicy {
+  private final AppView<AppInfoWithLiveness> appView;
+
+  public NoVerticallyMergedClasses(AppView<AppInfoWithLiveness> appView, Mode mode) {
+    // This policy is only relevant for the initial round, since all vertically merged classes have
+    // been removed from the application in the final round of horizontal class merging.
+    assert mode.isInitial();
+    this.appView = appView;
+  }
+
+  @Override
+  public boolean canMerge(DexProgramClass program) {
+    if (appView.verticallyMergedClasses() == null) {
+      return true;
+    }
+    return !appView.verticallyMergedClasses().hasBeenMergedIntoSubtype(program.type);
+  }
+
+  @Override
+  public String getName() {
+    return "NotVerticallyMergedIntoSubtype";
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotMatchedByNoHorizontalClassMerging.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotMatchedByNoHorizontalClassMerging.java
index 86ad07c..2daa302 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotMatchedByNoHorizontalClassMerging.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotMatchedByNoHorizontalClassMerging.java
@@ -18,8 +18,8 @@
   }
 
   @Override
-  public boolean canMerge(DexProgramClass program) {
-    return !appView.appInfo().isNoHorizontalClassMergingOfType(program.getType());
+  public boolean canMerge(DexProgramClass clazz) {
+    return !appView.appInfo().isNoHorizontalClassMergingOfType(clazz.getType());
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotVerticallyMergedIntoSubtype.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotVerticallyMergedIntoSubtype.java
deleted file mode 100644
index 062e695..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotVerticallyMergedIntoSubtype.java
+++ /dev/null
@@ -1,31 +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.AppInfoWithClassHierarchy;
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
-
-public class NotVerticallyMergedIntoSubtype extends SingleClassPolicy {
-  private final AppView<? extends AppInfoWithClassHierarchy> appView;
-
-  public NotVerticallyMergedIntoSubtype(AppView<? extends AppInfoWithClassHierarchy> appView) {
-    this.appView = appView;
-  }
-
-  @Override
-  public boolean canMerge(DexProgramClass program) {
-    if (appView.verticallyMergedClasses() == null) {
-      return true;
-    }
-    return !appView.verticallyMergedClasses().hasBeenMergedIntoSubtype(program.type);
-  }
-
-  @Override
-  public String getName() {
-    return "NotVerticallyMergedIntoSubtype";
-  }
-}
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
new file mode 100644
index 0000000..d9e6cd1
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/OnlyDirectlyConnectedOrUnrelatedInterfaces.java
@@ -0,0 +1,189 @@
+// 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.horizontalclassmerging.policies;
+
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+
+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.DexType;
+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.WorkList;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Sets;
+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;
+
+/**
+ * This policy ensures that we do not create cycles in the class hierarchy as a result of interface
+ * merging.
+ *
+ * <p>Example: Consider that we have the following three interfaces:
+ *
+ * <pre>
+ *   interface I extends ... {}
+ *   interface J extends I, ... {}
+ *   interface K extends J, ... {}
+ * </pre>
+ *
+ * <p>In this case, it would be possible to merge the groups {I, J}, {J, K}, and {I, J, K}. Common
+ * to these merge groups is that each interface in the merge group can reach all other interfaces in
+ * the same merge group in the class hierarchy, without visiting any interfaces outside the merge
+ * group.
+ *
+ * <p>The group {I, K} cannot safely be merged, as this would lead to a cycle in the class
+ * hierarchy:
+ *
+ * <pre>
+ *   interface IK extends J, ... {}
+ *   interface J extends IK, ... {}
+ * </pre>
+ */
+public class OnlyDirectlyConnectedOrUnrelatedInterfaces extends MultiClassPolicy {
+
+  private final AppView<? extends AppInfoWithClassHierarchy> appView;
+  private final Mode mode;
+
+  public OnlyDirectlyConnectedOrUnrelatedInterfaces(
+      AppView<? extends AppInfoWithClassHierarchy> appView, Mode mode) {
+    this.appView = appView;
+    this.mode = mode;
+  }
+
+  @Override
+  public Collection<MergeGroup> apply(MergeGroup group) {
+    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);
+    }
+
+    // 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)));
+      if (!newGroup.isTrivial()) {
+        newGroups.add(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 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 boolean isConnectedToGroup(
+      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;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public String getName() {
+    return "OnlyDirectlyConnectedOrUnrelatedInterfaces";
+  }
+
+  @Override
+  public boolean shouldSkipPolicy() {
+    return !appView.options().horizontalClassMergerOptions().isInterfaceMergingEnabled(mode);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreserveMethodCharacteristics.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreserveMethodCharacteristics.java
index c8bb5c6..ab305a3 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreserveMethodCharacteristics.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreserveMethodCharacteristics.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.DexMethodSignature;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.MethodAccessFlags;
+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.shaking.AppInfoWithLiveness;
@@ -90,7 +91,10 @@
 
   private final AppView<AppInfoWithLiveness> appView;
 
-  public PreserveMethodCharacteristics(AppView<AppInfoWithLiveness> appView) {
+  public PreserveMethodCharacteristics(AppView<AppInfoWithLiveness> appView, Mode mode) {
+    // This policy checks that method merging does invalidate various properties. Thus there is no
+    // reason to run this policy if method merging is not allowed.
+    assert mode.isInitial();
     this.appView = appView;
   }
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventMethodImplementation.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventClassMethodAndDefaultMethodCollisions.java
similarity index 92%
rename from src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventMethodImplementation.java
rename to src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventClassMethodAndDefaultMethodCollisions.java
index 5bffb33..9ec219c 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventMethodImplementation.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventClassMethodAndDefaultMethodCollisions.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
 import com.android.tools.r8.horizontalclassmerging.SubtypingForrestForClasses;
 import com.android.tools.r8.utils.collections.DexMethodSignatureSet;
+import com.google.common.collect.ImmutableList;
 import java.util.Collection;
 import java.util.IdentityHashMap;
 import java.util.LinkedHashMap;
@@ -48,7 +49,7 @@
  *
  * <p>See: https://docs.oracle.com/javase/specs/jvms/se15/html/jvms-5.html#jvms-5.4.3.3)
  */
-public class PreventMethodImplementation extends MultiClassPolicy {
+public class PreventClassMethodAndDefaultMethodCollisions extends MultiClassPolicy {
 
   private final AppView<? extends AppInfoWithClassHierarchy> appView;
   private final SubtypingForrestForClasses subtypingForrestForClasses;
@@ -62,7 +63,7 @@
 
   @Override
   public String getName() {
-    return "PreventMethodImplementation";
+    return "PreventClassMethodAndDefaultMethodCollisions";
   }
 
   private abstract static class SignaturesCache<C extends DexClass> {
@@ -124,7 +125,8 @@
     }
   }
 
-  public PreventMethodImplementation(AppView<? extends AppInfoWithClassHierarchy> appView) {
+  public PreventClassMethodAndDefaultMethodCollisions(
+      AppView<? extends AppInfoWithClassHierarchy> appView) {
     this.appView = appView;
     this.subtypingForrestForClasses = new SubtypingForrestForClasses(appView);
   }
@@ -150,6 +152,11 @@
 
   @Override
   public Collection<MergeGroup> apply(MergeGroup group) {
+    // This policy is specific to issues that may arise from merging (non-interface) classes.
+    if (group.isInterfaceGroup()) {
+      return ImmutableList.of(group);
+    }
+
     DexMethodSignatureSet signatures = DexMethodSignatureSet.createLinked();
     for (DexProgramClass clazz : group) {
       signatures.addAllMethods(clazz.methods());
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameInstanceFields.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameInstanceFields.java
index 391fb75..8e57e87 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameInstanceFields.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameInstanceFields.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.FieldAccessFlags;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.MultiClassSameReferencePolicy;
 import com.android.tools.r8.horizontalclassmerging.policies.SameInstanceFields.InstanceFieldInfo;
 import com.google.common.collect.HashMultiset;
@@ -20,16 +21,24 @@
 public class SameInstanceFields extends MultiClassSameReferencePolicy<Multiset<InstanceFieldInfo>> {
 
   private final DexItemFactory dexItemFactory;
+  private final Mode mode;
 
-  public SameInstanceFields(AppView<? extends AppInfoWithClassHierarchy> appView) {
+  public SameInstanceFields(AppView<? extends AppInfoWithClassHierarchy> appView, Mode mode) {
     this.dexItemFactory = appView.dexItemFactory();
+    this.mode = mode;
   }
 
   @Override
   public Multiset<InstanceFieldInfo> getMergeKey(DexProgramClass clazz) {
     Multiset<InstanceFieldInfo> fields = HashMultiset.create();
     for (DexEncodedField field : clazz.instanceFields()) {
-      fields.add(InstanceFieldInfo.createRelaxed(field, dexItemFactory));
+      // We do not allow merging fields with different types in the final round of horizontal class
+      // merging, since that requires inserting check-cast instructions at reads.
+      InstanceFieldInfo instanceFieldInfo =
+          mode.isInitial()
+              ? InstanceFieldInfo.createRelaxed(field, dexItemFactory)
+              : InstanceFieldInfo.createExact(field);
+      fields.add(instanceFieldInfo);
     }
     return fields;
   }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventMergeIntoDifferentMainDexGroups.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameMainDexGroup.java
similarity index 80%
rename from src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventMergeIntoDifferentMainDexGroups.java
rename to src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameMainDexGroup.java
index 9d529d3..6df343a 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventMergeIntoDifferentMainDexGroups.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameMainDexGroup.java
@@ -12,14 +12,12 @@
 import com.android.tools.r8.shaking.MainDexInfo.MainDexGroup;
 import com.android.tools.r8.synthesis.SyntheticItems;
 
-public class PreventMergeIntoDifferentMainDexGroups
-    extends MultiClassSameReferencePolicy<MainDexGroup> {
+public class SameMainDexGroup extends MultiClassSameReferencePolicy<MainDexGroup> {
 
   private final MainDexInfo mainDexInfo;
   private final SyntheticItems synthetics;
 
-  public PreventMergeIntoDifferentMainDexGroups(
-      AppView<? extends AppInfoWithClassHierarchy> appView) {
+  public SameMainDexGroup(AppView<? extends AppInfoWithClassHierarchy> appView) {
     mainDexInfo = appView.appInfo().getMainDexInfo();
     synthetics = appView.getSyntheticItems();
   }
@@ -33,6 +31,6 @@
 
   @Override
   public String getName() {
-    return "PreventMergeIntoDifferentMainDexGroups";
+    return "SameMainDexGroup";
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SyntheticItemsPolicy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SyntheticItemsPolicy.java
index 753ac5d..f40b722 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SyntheticItemsPolicy.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SyntheticItemsPolicy.java
@@ -7,6 +7,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.horizontalclassmerging.HorizontalClassMerger.Mode;
 import com.android.tools.r8.horizontalclassmerging.MultiClassSameReferencePolicy;
 import com.android.tools.r8.horizontalclassmerging.policies.SyntheticItemsPolicy.ClassKind;
 import com.android.tools.r8.synthesis.SyntheticItems;
@@ -18,24 +19,23 @@
     NOT_SYNTHETIC
   }
 
-  private final AppView<? extends AppInfoWithClassHierarchy> appView;
+  private final Mode mode;
+  private final SyntheticItems syntheticItems;
 
-  public SyntheticItemsPolicy(AppView<? extends AppInfoWithClassHierarchy> appView) {
-    this.appView = appView;
+  public SyntheticItemsPolicy(AppView<? extends AppInfoWithClassHierarchy> appView, Mode mode) {
+    this.mode = mode;
+    this.syntheticItems = appView.getSyntheticItems();
   }
 
   @Override
   public ClassKind getMergeKey(DexProgramClass clazz) {
-    SyntheticItems syntheticItems = appView.getSyntheticItems();
-
-    // Allow merging non-synthetics with non-synthetics.
-    if (!syntheticItems.isSyntheticClass(clazz)) {
-      return ClassKind.NOT_SYNTHETIC;
+    // Allow merging non-synthetics with non-synthetics, and synthetics with synthetics.
+    if (syntheticItems.isSyntheticClass(clazz)) {
+      return syntheticItems.isEligibleForClassMerging(clazz, mode)
+          ? ClassKind.SYNTHETIC
+          : ineligibleForClassMerging();
     }
-
-    return syntheticItems.isEligibleForClassMerging(clazz)
-        ? ClassKind.SYNTHETIC
-        : ineligibleForClassMerging();
+    return ClassKind.NOT_SYNTHETIC;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/VerifyPolicyAlwaysSatisfied.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/VerifyPolicyAlwaysSatisfied.java
new file mode 100644
index 0000000..1fc559c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/VerifyPolicyAlwaysSatisfied.java
@@ -0,0 +1,33 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
+
+public class VerifyPolicyAlwaysSatisfied extends SingleClassPolicy {
+
+  private final SingleClassPolicy policy;
+
+  public VerifyPolicyAlwaysSatisfied(SingleClassPolicy policy) {
+    this.policy = policy;
+  }
+
+  @Override
+  public boolean canMerge(DexProgramClass program) {
+    assert policy.canMerge(program);
+    return true;
+  }
+
+  @Override
+  public String getName() {
+    return "VerifyAlwaysSatisfied(" + policy.getName() + ")";
+  }
+
+  @Override
+  public boolean shouldSkipPolicy() {
+    return policy.shouldSkipPolicy();
+  }
+}
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 2cbebcd..785fb64 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
@@ -367,6 +367,11 @@
         D8NestBasedAccessDesugaring::reportDesugarDependencies);
   }
 
+  private void clearNestAttributes() {
+    instructionDesugaring.withD8NestBasedAccessDesugaring(
+        D8NestBasedAccessDesugaring::clearNestAttributes);
+  }
+
   private void staticizeClasses(
       OptimizationFeedback feedback, ExecutorService executorService, GraphLens applied)
       throws ExecutionException {
@@ -424,6 +429,7 @@
     convertClasses(executor);
 
     reportNestDesugarDependencies();
+    clearNestAttributes();
 
     if (appView.getSyntheticItems().hasPendingSyntheticClasses()) {
       appView.setAppInfo(
@@ -581,6 +587,7 @@
     } else {
       NeedsIRDesugarUseRegistry useRegistry =
           new NeedsIRDesugarUseRegistry(
+              method,
               appView,
               desugaredLibraryRetargeter,
               interfaceMethodRewriter,
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/NeedsIRDesugarUseRegistry.java b/src/main/java/com/android/tools/r8/ir/conversion/NeedsIRDesugarUseRegistry.java
index 750ffb4..bbc0bcb 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/NeedsIRDesugarUseRegistry.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/NeedsIRDesugarUseRegistry.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.ir.desugar.DesugaredLibraryAPIConverter;
@@ -24,16 +25,19 @@
 class NeedsIRDesugarUseRegistry extends UseRegistry {
 
   private boolean needsDesugaring = false;
+  private final ProgramMethod context;
   private final DesugaredLibraryRetargeter desugaredLibraryRetargeter;
   private final InterfaceMethodRewriter interfaceMethodRewriter;
   private final DesugaredLibraryAPIConverter desugaredLibraryAPIConverter;
 
   public NeedsIRDesugarUseRegistry(
+      ProgramMethod method,
       AppView<?> appView,
       DesugaredLibraryRetargeter desugaredLibraryRetargeter,
       InterfaceMethodRewriter interfaceMethodRewriter,
       DesugaredLibraryAPIConverter desugaredLibraryAPIConverter) {
     super(appView.dexItemFactory());
+    this.context = method;
     this.desugaredLibraryRetargeter = desugaredLibraryRetargeter;
     this.interfaceMethodRewriter = interfaceMethodRewriter;
     this.desugaredLibraryAPIConverter = desugaredLibraryAPIConverter;
@@ -70,7 +74,7 @@
     if (!needsDesugaring) {
       needsDesugaring =
           interfaceMethodRewriter != null
-              && interfaceMethodRewriter.needsRewriting(method, invokeType);
+              && interfaceMethodRewriter.needsRewriting(method, invokeType, context);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryWrapperSynthesizer.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryWrapperSynthesizer.java
index f5845d4..e0c6d41 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryWrapperSynthesizer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryWrapperSynthesizer.java
@@ -244,7 +244,8 @@
         },
         virtualMethods,
         factory.getSkipNameValidationForTesting(),
-        DexProgramClass::checksumFromType);
+        DexProgramClass::checksumFromType,
+        null);
   }
 
   private DexEncodedMethod[] synthesizeVirtualMethodsForVivifiedTypeWrapper(
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 6a878f0..b0a4ffa 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
@@ -226,7 +226,12 @@
     return emulatedInterfaces.containsKey(itf);
   }
 
-  public boolean needsRewriting(DexMethod method, Type invokeType) {
+  public boolean needsRewriting(DexMethod method, Type invokeType, ProgramMethod context) {
+    return !isSyntheticMethodThatShouldNotBeDoubleProcessed(context)
+        && invokeNeedsRewriting(method, invokeType);
+  }
+
+  private boolean invokeNeedsRewriting(DexMethod method, Type invokeType) {
     if (invokeType == SUPER || invokeType == STATIC || invokeType == DIRECT) {
       DexClass clazz = appView.appInfo().definitionFor(method.getHolderType());
       if (clazz != null && clazz.isInterface()) {
@@ -267,7 +272,10 @@
       MethodProcessor methodProcessor,
       MethodProcessingContext methodProcessingContext) {
     ProgramMethod context = code.context();
-    if (synthesizedMethods.contains(context)) {
+    if (isSyntheticMethodThatShouldNotBeDoubleProcessed(code.context())) {
+      // As the synthetics for dispatching to static interface methods are not desugared again
+      // this can leave a static invoke to a static method on an interface.
+      leavingStaticInvokeToInterface(context.asProgramMethod());
       return;
     }
 
@@ -332,6 +340,10 @@
     assert code.isConsistentSSA();
   }
 
+  private boolean isSyntheticMethodThatShouldNotBeDoubleProcessed(ProgramMethod method) {
+    return appView.getSyntheticItems().isSyntheticMethodThatShouldNotBeDoubleProcessed(method);
+  }
+
   private void rewriteInvokeCustom(InvokeCustom invoke, ProgramMethod context) {
     // Check that static interface methods are not referenced from invoke-custom instructions via
     // method handles.
@@ -375,7 +387,7 @@
       // 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 needsRewriting(method, DIRECT);
+      assert invokeNeedsRewriting(method, DIRECT);
       instructions.replaceCurrentInstruction(
           new InvokeStatic(
               directTarget.getDefinition().isPrivateMethod()
@@ -389,7 +401,7 @@
           appView.appInfoForDesugaring().lookupMaximallySpecificMethod(clazz, method);
       if (virtualTarget != null) {
         // This is a invoke-direct call to a virtual method.
-        assert needsRewriting(method, DIRECT);
+        assert invokeNeedsRewriting(method, DIRECT);
         instructions.replaceCurrentInstruction(
             new InvokeStatic(
                 defaultAsMethodOfCompanionClass(virtualTarget),
@@ -472,16 +484,14 @@
                                         .setStaticTarget(invokedMethod, true)
                                         .setStaticSource(m)
                                         .build()));
-        assert needsRewriting(invokedMethod, STATIC);
+        assert invokeNeedsRewriting(invokedMethod, STATIC);
         instructions.replaceCurrentInstruction(
             new InvokeStatic(
                 newProgramMethod.getReference(), invoke.outValue(), invoke.arguments()));
-        synchronized (synthesizedMethods) {
-          // 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);
-        }
+        // 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);
       } else {
         // When leaving static interface method invokes upgrade the class file version.
         context.getDefinition().upgradeClassFileVersion(CfVersion.V1_8);
@@ -505,13 +515,13 @@
             blocksToRemove,
             methodProcessor,
             methodProcessingContext)) {
-      assert needsRewriting(invoke.getInvokedMethod(), STATIC);
+      assert invokeNeedsRewriting(invoke.getInvokedMethod(), STATIC);
       return;
     }
 
     assert resolutionResult != null;
     assert resolutionResult.getResolvedMethod().isStatic();
-    assert needsRewriting(invokedMethod, STATIC);
+    assert invokeNeedsRewriting(invokedMethod, STATIC);
 
     instructions.replaceCurrentInstruction(
         new InvokeStatic(
@@ -553,7 +563,7 @@
             blocksToRemove,
             methodProcessor,
             methodProcessingContext)) {
-      assert needsRewriting(invoke.getInvokedMethod(), SUPER);
+      assert invokeNeedsRewriting(invoke.getInvokedMethod(), SUPER);
       return;
     }
 
@@ -567,7 +577,7 @@
       //
       // WARNING: This may result in incorrect code on older platforms!
       // Retarget call to an appropriate method of companion class.
-      assert needsRewriting(invokedMethod, SUPER);
+      assert invokeNeedsRewriting(invokedMethod, SUPER);
       DexMethod amendedMethod = amendDefaultMethod(context.getHolder(), invokedMethod);
       instructions.replaceCurrentInstruction(
           new InvokeStatic(
@@ -583,7 +593,7 @@
           if (target != null && target.getDefinition().isDefaultMethod()) {
             DexClass holder = target.getHolder();
             if (holder.isLibraryClass() && holder.isInterface()) {
-              assert needsRewriting(invokedMethod, SUPER);
+              assert invokeNeedsRewriting(invokedMethod, SUPER);
               instructions.replaceCurrentInstruction(
                   new InvokeStatic(
                       defaultAsMethodOfCompanionClass(target),
@@ -613,11 +623,11 @@
                     factory.protoWithDifferentFirstParameter(
                         originalCompanionMethod.proto, emulatedItf),
                     originalCompanionMethod.name);
-            assert needsRewriting(invokedMethod, SUPER);
+            assert invokeNeedsRewriting(invokedMethod, SUPER);
             instructions.replaceCurrentInstruction(
                 new InvokeStatic(companionMethod, invoke.outValue(), invoke.arguments()));
           } else {
-            assert needsRewriting(invokedMethod, SUPER);
+            assert invokeNeedsRewriting(invokedMethod, SUPER);
             instructions.replaceCurrentInstruction(
                 new InvokeStatic(retargetMethod, invoke.outValue(), invoke.arguments()));
           }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/nest/D8NestBasedAccessDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/nest/D8NestBasedAccessDesugaring.java
index b622057..430d8ad 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/nest/D8NestBasedAccessDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/nest/D8NestBasedAccessDesugaring.java
@@ -55,6 +55,17 @@
         });
   }
 
+  public void clearNestAttributes() {
+    forEachNest(
+        nest -> {
+          nest.getHostClass().clearNestMembers();
+          nest.getMembers().forEach(DexClass::clearNestHost);
+        },
+        classWithoutHost -> {
+          // Do Nothing
+        });
+  }
+
   public void synthesizeBridgesForNestBasedAccessesOnClasspath(
       MethodProcessor methodProcessor, ExecutorService executorService) throws ExecutionException {
     List<DexClasspathClass> classpathClassesInNests = new ArrayList<>();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index 41a298a..a6f246f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -141,6 +141,11 @@
       return !singleTarget.getDefinition().getOptimizationInfo().forceInline();
     }
 
+    if (!appView.testing().allowInliningOfSynthetics
+        && appView.getSyntheticItems().isSyntheticClass(singleTarget.getHolder())) {
+      return true;
+    }
+
     return false;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java
index 014337c..7092012 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java
@@ -15,7 +15,6 @@
 import com.android.tools.r8.graph.FieldResolutionResult.SuccessfulFieldResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
-import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
 import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.value.ObjectState;
 import com.android.tools.r8.ir.analysis.value.SingleFieldValue;
@@ -245,8 +244,10 @@
               killNonFinalActiveFields(staticPut);
               ExistingValue value = new ExistingValue(staticPut.value());
               if (isFinal(field)) {
-                assert !field.getDefinition().isFinal()
-                    || method.getDefinition().isClassInitializer();
+                assert appView.checkForTesting(
+                    () ->
+                        !field.getDefinition().isFinal()
+                            || method.getDefinition().isClassInitializer());
                 activeState.putFinalStaticField(reference, value);
               } else {
                 activeState.putNonFinalStaticField(reference, value);
@@ -328,11 +329,9 @@
 
   private boolean verifyWasInstanceInitializer() {
     VerticallyMergedClasses verticallyMergedClasses = appView.verticallyMergedClasses();
-    HorizontallyMergedClasses horizontallyMergedClasses = appView.horizontallyMergedClasses();
     assert verticallyMergedClasses != null;
-    assert horizontallyMergedClasses != null;
     assert verticallyMergedClasses.isMergeTarget(method.getHolderType())
-        || horizontallyMergedClasses.isMergeTarget(method.getHolderType());
+        || appView.horizontallyMergedClasses().isMergeTarget(method.getHolderType());
     assert appView
         .dexItemFactory()
         .isConstructor(appView.graphLens().getOriginalMethodSignature(method.getReference()));
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
index f7b79aa..c3c2307 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
@@ -300,6 +300,12 @@
     if (clazz.classInitializationMayHaveSideEffects(appView)) {
       return EligibilityStatus.NOT_ELIGIBLE;
     }
+
+    if (!appView.testing().allowClassInliningOfSynthetics
+        && appView.getSyntheticItems().isSyntheticClass(clazz)) {
+      return EligibilityStatus.NOT_ELIGIBLE;
+    }
+
     return EligibilityStatus.ELIGIBLE;
   }
 }
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 03eee35..0d2e43b 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
@@ -145,6 +145,7 @@
     instanceInitializerInfoCollection = template.instanceInitializerInfoCollection;
     nonNullParamOrThrow = template.nonNullParamOrThrow;
     nonNullParamOnNormalExits = template.nonNullParamOnNormalExits;
+    classInlinerConstraint = template.classInlinerConstraint;
   }
 
   public UpdatableMethodOptimizationInfo fixupClassTypeReferences(
diff --git a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
index 5f23e34..0206e81 100644
--- a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
@@ -199,6 +199,7 @@
     }
     assert SyntheticNaming.verifyNotInternalSynthetic(name);
     writer.visit(version.raw(), access, name, signature, superName, interfaces);
+    appView.getSyntheticItems().writeAttributeIfIntermediateSyntheticClass(writer, clazz, appView);
     writeAnnotations(writer::visitAnnotation, clazz.annotations().annotations);
     ImmutableMap<DexString, DexValue> defaults = getAnnotationDefaults(clazz.annotations());
 
diff --git a/src/main/java/com/android/tools/r8/retrace/Retrace.java b/src/main/java/com/android/tools/r8/retrace/Retrace.java
index 63d0098..62c04bb 100644
--- a/src/main/java/com/android/tools/r8/retrace/Retrace.java
+++ b/src/main/java/com/android/tools/r8/retrace/Retrace.java
@@ -131,7 +131,7 @@
     try {
       return Files.readAllLines(Paths.get(stackTracePath), Charsets.UTF_8);
     } catch (IOException e) {
-      diagnostics.error(new StringDiagnostic("Could not find stack trace file: " + stackTracePath));
+      diagnostics.error(new ExceptionDiagnostic(e));
       throw new RetraceAbortException();
     }
   }
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 69c27c5..c4ebfe7 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -31,6 +31,7 @@
 import com.android.tools.r8.graph.FieldAccessInfoCollection;
 import com.android.tools.r8.graph.FieldAccessInfoCollectionImpl;
 import com.android.tools.r8.graph.FieldResolutionResult;
+import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.GraphLens.NonIdentityGraphLens;
 import com.android.tools.r8.graph.InstantiatedSubTypeInfo;
 import com.android.tools.r8.graph.LookupResult.LookupResultSuccess;
@@ -1055,7 +1056,7 @@
     return new AppInfoWithLiveness(
         committedItems,
         getClassToFeatureSplitMap().rewrittenWithLens(lens),
-        getMainDexInfo().rewrittenWithLens(lens),
+        getMainDexInfo().rewrittenWithLens(getSyntheticItems(), lens),
         deadProtoTypes,
         getMissingClasses().commitSyntheticItems(committedItems),
         lens.rewriteTypes(liveTypes),
@@ -1094,7 +1095,26 @@
         prunedTypes,
         lens.rewriteFieldKeys(switchMaps),
         lens.rewriteTypes(lockCandidates),
-        lens.rewriteTypeKeys(initClassReferences));
+        rewriteInitClassReferences(lens));
+  }
+
+  public Map<DexType, Visibility> rewriteInitClassReferences(GraphLens lens) {
+    return lens.rewriteTypeKeys(
+        initClassReferences,
+        (minimumRequiredVisibilityForCurrentMethod,
+            otherMinimumRequiredVisibilityForCurrentMethod) -> {
+          assert !minimumRequiredVisibilityForCurrentMethod.isPrivate();
+          assert !otherMinimumRequiredVisibilityForCurrentMethod.isPrivate();
+          if (minimumRequiredVisibilityForCurrentMethod.isPublic()
+              || otherMinimumRequiredVisibilityForCurrentMethod.isPublic()) {
+            return Visibility.PUBLIC;
+          }
+          if (minimumRequiredVisibilityForCurrentMethod.isProtected()
+              || otherMinimumRequiredVisibilityForCurrentMethod.isProtected()) {
+            return Visibility.PROTECTED;
+          }
+          return Visibility.PACKAGE_PRIVATE;
+        });
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/shaking/MainDexInfo.java b/src/main/java/com/android/tools/r8/shaking/MainDexInfo.java
index 5be14a3..f3f52fd 100644
--- a/src/main/java/com/android/tools/r8/shaking/MainDexInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/MainDexInfo.java
@@ -276,7 +276,7 @@
     }
   }
 
-  public MainDexInfo rewrittenWithLens(GraphLens lens) {
+  public MainDexInfo rewrittenWithLens(SyntheticItems syntheticItems, GraphLens lens) {
     Set<DexType> modifiedClassList = Sets.newIdentityHashSet();
     classList.forEach(
         type -> rewriteAndApplyIfNotPrimitiveType(lens, type, modifiedClassList::add));
@@ -289,6 +289,9 @@
             // Synthetic finalization is allowed to merge identical classes into the same class. The
             // rewritten type of a traced dependency can therefore be finalized with a traced root.
             rewriteAndApplyIfNotPrimitiveType(lens, type, builder::addDependencyIfNotRoot);
+          } else if (syntheticItems.isFinalized()) {
+            rewriteAndApplyIfNotPrimitiveType(
+                lens, type, builder.addDependencyAllowSyntheticRoot(syntheticItems));
           } else {
             rewriteAndApplyIfNotPrimitiveType(lens, type, builder::addDependency);
           }
@@ -342,6 +345,13 @@
       dependencies.add(type);
     }
 
+    public Consumer<DexType> addDependencyAllowSyntheticRoot(SyntheticItems syntheticItems) {
+      return type -> {
+        assert !roots.contains(type) || syntheticItems.isCommittedSynthetic(type);
+        addDependencyIfNotRoot(type);
+      };
+    }
+
     public void addDependencyIfNotRoot(DexType type) {
       if (roots.contains(type)) {
         return;
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardKeepAttributes.java b/src/main/java/com/android/tools/r8/shaking/ProguardKeepAttributes.java
index 0dcf1eb..e47ff67 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardKeepAttributes.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardKeepAttributes.java
@@ -152,9 +152,6 @@
     } else if (!innerClasses && enclosingMethod) {
       throw new CompilationError("Attribute EnclosingMethod requires InnerClasses attribute. "
           + "Check -keepattributes directive.");
-    } else if (signature && !innerClasses) {
-      throw new CompilationError("Attribute Signature requires InnerClasses attribute. Check "
-          + "-keepattributes directive.");
     }
     if (forceProguardCompatibility && localVariableTable && !lineNumberTable) {
       // If locals are kept, assume line numbers should be kept too.
diff --git a/src/main/java/com/android/tools/r8/shaking/RuntimeTypeCheckInfo.java b/src/main/java/com/android/tools/r8/shaking/RuntimeTypeCheckInfo.java
index fb3868a..fc2756a 100644
--- a/src/main/java/com/android/tools/r8/shaking/RuntimeTypeCheckInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/RuntimeTypeCheckInfo.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.shaking;
 
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
@@ -33,18 +34,25 @@
       implements EnqueuerInstanceOfAnalysis,
           EnqueuerCheckCastAnalysis,
           EnqueuerExceptionGuardAnalysis {
+
+    private final GraphLens appliedGraphLens;
     private final DexItemFactory factory;
 
     private final Set<DexType> instanceOfTypes = Sets.newIdentityHashSet();
     private final Set<DexType> checkCastTypes = Sets.newIdentityHashSet();
     private final Set<DexType> exceptionGuardTypes = Sets.newIdentityHashSet();
 
-    public Builder(DexItemFactory factory) {
-      this.factory = factory;
+    public Builder(AppView<?> appView) {
+      this.appliedGraphLens = appView.graphLens();
+      this.factory = appView.dexItemFactory();
     }
 
-    public RuntimeTypeCheckInfo build() {
-      return new RuntimeTypeCheckInfo(instanceOfTypes, checkCastTypes, exceptionGuardTypes);
+    public RuntimeTypeCheckInfo build(GraphLens graphLens) {
+      RuntimeTypeCheckInfo runtimeTypeCheckInfo =
+          new RuntimeTypeCheckInfo(instanceOfTypes, checkCastTypes, exceptionGuardTypes);
+      return graphLens.isNonIdentityLens() && graphLens != appliedGraphLens
+          ? runtimeTypeCheckInfo.rewriteWithLens(graphLens.asNonIdentityLens())
+          : runtimeTypeCheckInfo;
     }
 
     @Override
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 c81ad1c..ef3b4d2 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticClassBuilder.java
@@ -160,6 +160,7 @@
             directMethods.toArray(DexEncodedMethod.EMPTY_ARRAY),
             virtualMethods.toArray(DexEncodedMethod.EMPTY_ARRAY),
             factory.getSkipNameValidationForTesting(),
-            c -> checksum);
+            c -> checksum,
+            null);
   }
 }
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 cbdf447..55b5e00 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 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.GraphLens;
 import com.android.tools.r8.graph.GraphLens.NonIdentityGraphLens;
@@ -27,7 +28,9 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.KeepInfoCollection;
 import com.android.tools.r8.shaking.MainDexInfo;
+import com.android.tools.r8.synthesis.SyntheticNaming.Phase;
 import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
+import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.ListUtils;
@@ -150,7 +153,10 @@
           appView
               .appInfo()
               .rebuildWithMainDexInfo(
-                  appView.appInfo().getMainDexInfo().rewrittenWithLens(result.lens)));
+                  appView
+                      .appInfo()
+                      .getMainDexInfo()
+                      .rewrittenWithLens(appView.getSyntheticItems(), result.lens)));
       appView.setGraphLens(result.lens);
     }
     appView.pruneItems(result.prunedItems);
@@ -167,7 +173,10 @@
           appView
               .appInfo()
               .rebuildWithMainDexInfo(
-                  appView.appInfo().getMainDexInfo().rewrittenWithLens(result.lens)));
+                  appView
+                      .appInfo()
+                      .getMainDexInfo()
+                      .rewrittenWithLens(appView.getSyntheticItems(), result.lens)));
     }
     appView.pruneItems(result.prunedItems);
   }
@@ -185,7 +194,7 @@
   }
 
   Result computeFinalSynthetics(AppView<?> appView) {
-    assert verifyNoNestedSynthetics();
+    assert verifyNoNestedSynthetics(appView.dexItemFactory());
     assert verifyOneSyntheticPerSyntheticClass();
     DexApplication application;
     Builder lensBuilder = new Builder();
@@ -285,12 +294,19 @@
     return !committed.containsNonLegacyType(type);
   }
 
-  private boolean verifyNoNestedSynthetics() {
-    // Check that a context is never itself synthetic class.
+  private boolean verifyNoNestedSynthetics(DexItemFactory dexItemFactory) {
+    // Check that the prefix of each synthetic is never itself synthetic.
     committed.forEachNonLegacyItem(
         item -> {
-          assert isNotSyntheticType(item.getContext().getSynthesizingContextType())
-              || item.getKind().allowSyntheticContext();
+          if (item.getKind().allowSyntheticContext()) {
+            return;
+          }
+          String prefix =
+              SyntheticNaming.getPrefixForExternalSyntheticType(item.getKind(), item.getHolder());
+          assert !prefix.contains(SyntheticNaming.getPhaseSeparator(Phase.INTERNAL));
+          DexType context =
+              dexItemFactory.createType(DescriptorUtils.getDescriptorFromClassBinaryName(prefix));
+          assert isNotSyntheticType(context);
         });
     return true;
   }
@@ -321,6 +337,13 @@
     return true;
   }
 
+  private static void ensureSourceFile(
+      DexProgramClass externalSyntheticClass, DexString syntheticSourceFileName) {
+    if (externalSyntheticClass.getSourceFile() == null) {
+      externalSyntheticClass.setSourceFile(syntheticSourceFileName);
+    }
+  }
+
   private static DexApplication buildLensAndProgram(
       AppView<?> appView,
       Map<DexType, EquivalenceGroup<SyntheticMethodDefinition>> syntheticMethodGroups,
@@ -362,8 +385,7 @@
           SyntheticMethodDefinition representative = syntheticGroup.getRepresentative();
           SynthesizingContext context = representative.getContext();
           context.registerPrefixRewriting(syntheticType, appView);
-          addSyntheticMarker(
-              representative.getKind(), representative.getHolder(), context, appView);
+          addSyntheticMarker(representative.getKind(), representative.getHolder(), appView);
           if (syntheticGroup.isDerivedFromMainDexList(mainDexInfo)) {
             derivedMainDexSynthetics.add(syntheticType);
           }
@@ -379,8 +401,7 @@
           SyntheticProgramClassDefinition representative = syntheticGroup.getRepresentative();
           SynthesizingContext context = representative.getContext();
           context.registerPrefixRewriting(syntheticType, appView);
-          addSyntheticMarker(
-              representative.getKind(), representative.getHolder(), context, appView);
+          addSyntheticMarker(representative.getKind(), representative.getHolder(), appView);
           if (syntheticGroup.isDerivedFromMainDexList(mainDexInfo)) {
             derivedMainDexSynthetics.add(syntheticType);
           }
@@ -417,6 +438,11 @@
       application = builder.build();
     }
 
+    DexString syntheticSourceFileName =
+        appView.enableWholeProgramOptimizations()
+            ? appView.dexItemFactory().createString("R8$$SyntheticClass")
+            : appView.dexItemFactory().createString("D8$$SyntheticClass");
+
     // Add the synthesized from after repackaging which changed class definitions.
     final DexApplication appForLookup = application;
     syntheticClassGroups.forEach(
@@ -424,6 +450,7 @@
           DexProgramClass externalSyntheticClass = appForLookup.programDefinitionFor(syntheticType);
           assert externalSyntheticClass != null
               : "Expected definition for " + syntheticType.getTypeName();
+          ensureSourceFile(externalSyntheticClass, syntheticSourceFileName);
           SyntheticProgramClassDefinition representative = syntheticGroup.getRepresentative();
           addFinalSyntheticClass.accept(
               externalSyntheticClass,
@@ -435,6 +462,7 @@
     syntheticMethodGroups.forEach(
         (syntheticType, syntheticGroup) -> {
           DexProgramClass externalSyntheticClass = appForLookup.programDefinitionFor(syntheticType);
+          ensureSourceFile(externalSyntheticClass, syntheticSourceFileName);
           SyntheticMethodDefinition representative = syntheticGroup.getRepresentative();
           assert externalSyntheticClass.getMethodCollection().size() == 1;
           assert externalSyntheticClass.getMethodCollection().hasDirectMethods();
@@ -474,24 +502,16 @@
   private static void addSyntheticMarker(
       SyntheticKind kind,
       DexProgramClass externalSyntheticClass,
-      SynthesizingContext context,
       AppView<?> appView) {
     if (shouldAnnotateSynthetics(appView.options())) {
-      SyntheticMarker.addMarkerToClass(
-          externalSyntheticClass,
-          kind,
-          context,
-          appView.dexItemFactory(),
-          appView.options().forceAnnotateSynthetics);
+      SyntheticMarker.addMarkerToClass(externalSyntheticClass, kind, appView.options());
     }
   }
 
   private static boolean shouldAnnotateSynthetics(InternalOptions options) {
     // Only intermediate builds have annotated synthetics to allow later sharing.
-    // This is currently also disabled on non-L8 CF to CF desugaring to avoid missing class
-    // references to the annotated classes.
-    // TODO(b/147485959): Find an alternative encoding for synthetics to avoid missing-class refs.
-    return options.intermediate && (!options.cfToCfDesugar || options.forceAnnotateSynthetics);
+    // Also, CF builds are marked in the writer using an attribute.
+    return options.intermediate && options.isGeneratingDex();
   }
 
   private <T extends SyntheticDefinition<?, T, ?>>
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
index f709533..78c046f 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.PrunedItems;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.synthesis.SyntheticFinalization.Result;
 import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 import com.android.tools.r8.utils.IterableUtils;
@@ -30,6 +31,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -38,6 +40,7 @@
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.function.Supplier;
+import org.objectweb.asm.ClassWriter;
 
 public class SyntheticItems implements SyntheticDefinitionsProvider {
 
@@ -119,12 +122,8 @@
     assert synthetics.nextSyntheticId == 0;
     assert synthetics.committed.isEmpty();
     assert synthetics.pending.isEmpty();
-    if (appView.options().intermediate) {
-      // If the compilation is in intermediate mode the synthetics should just be passed through.
-      return;
-    }
     CommittedSyntheticsCollection.Builder builder = synthetics.committed.builder();
-    // TODO(b/158159959): Consider identifying synthetics in the input reader to speed this up.
+    // TODO(b/158159959): Consider populating the input synthetics when identified.
     for (DexProgramClass clazz : appView.appInfo().classes()) {
       SyntheticMarker marker = SyntheticMarker.stripMarkerFromClass(clazz, appView);
       if (marker.isSyntheticMethods()) {
@@ -184,6 +183,10 @@
     return baseDefinitionFor.apply(type);
   }
 
+  public boolean isFinalized() {
+    return nextSyntheticId == INVALID_ID_AFTER_SYNTHETIC_FINALIZATION;
+  }
+
   public boolean hasPendingSyntheticClasses() {
     return !pending.isEmpty();
   }
@@ -192,7 +195,7 @@
     return pending.getAllProgramClasses();
   }
 
-  private boolean isCommittedSynthetic(DexType type) {
+  public boolean isCommittedSynthetic(DexType type) {
     return committed.containsType(type);
   }
 
@@ -200,6 +203,10 @@
     return committed.containsLegacyType(type);
   }
 
+  private boolean isNonLegacyCommittedSynthetic(DexType type) {
+    return committed.containsNonLegacyType(type);
+  }
+
   public boolean isPendingSynthetic(DexType type) {
     return pending.containsType(type);
   }
@@ -208,6 +215,10 @@
     return pending.legacyClasses.containsKey(type);
   }
 
+  private boolean isNonLegacyPendingSynthetic(DexType type) {
+    return pending.nonLegacyDefinitions.containsKey(type);
+  }
+
   public boolean isLegacySyntheticClass(DexType type) {
     return isLegacyCommittedSynthetic(type) || isLegacyPendingSynthetic(type);
   }
@@ -221,15 +232,14 @@
   }
 
   public boolean isNonLegacySynthetic(DexType type) {
-    return isCommittedSynthetic(type) || isPendingSynthetic(type);
+    return isNonLegacyCommittedSynthetic(type) || isNonLegacyPendingSynthetic(type);
   }
 
-  public boolean isEligibleForClassMerging(DexProgramClass clazz) {
+  public boolean isEligibleForClassMerging(DexProgramClass clazz, HorizontalClassMerger.Mode mode) {
     assert isSyntheticClass(clazz);
-    return isSyntheticLambda(clazz);
+    return mode.isFinal() || isSyntheticLambda(clazz);
   }
 
-  // TODO(b/186211926): Allow merging of legacy synthetics.
   private boolean isSyntheticLambda(DexProgramClass clazz) {
     if (!isNonLegacySynthetic(clazz)) {
       return false;
@@ -335,6 +345,23 @@
     Set<DexReference> getSynthesizingContexts(DexProgramClass clazz);
   }
 
+  public boolean isSyntheticMethodThatShouldNotBeDoubleProcessed(ProgramMethod method) {
+    for (SyntheticMethodReference reference :
+        committed
+            .getNonLegacyMethods()
+            .getOrDefault(method.getHolderType(), Collections.emptyList())) {
+      if (reference.getKind() == SyntheticKind.STATIC_INTERFACE_CALL) {
+        return true;
+      }
+    }
+    SyntheticDefinition<?, ?, ?> definition =
+        pending.nonLegacyDefinitions.get(method.getHolderType());
+    if (definition != null) {
+      return definition.getKind() == SyntheticKind.STATIC_INTERFACE_CALL;
+    }
+    return false;
+  }
+
   // The compiler should not inspect the kind of a synthetic, so this provided only as a assertion
   // utility.
   public boolean verifySyntheticLambdaProperty(
@@ -661,6 +688,23 @@
     return true;
   }
 
+  public void writeAttributeIfIntermediateSyntheticClass(
+      ClassWriter writer, DexProgramClass clazz, AppView<?> appView) {
+    if (!appView.options().intermediate || !appView.options().isGeneratingClassFiles()) {
+      return;
+    }
+    Iterator<SyntheticReference<?, ?, ?>> it =
+        committed.getNonLegacyItems(clazz.getType()).iterator();
+    if (it.hasNext()) {
+      SyntheticKind kind = it.next().getKind();
+      // When compiling intermediates there should not be any mergings as they may invalidate the
+      // single kind of a synthetic which is required for marking synthetics. This check could be
+      // relaxed to ensure that all kinds are equivalent if merging is possible.
+      assert !it.hasNext();
+      SyntheticMarker.writeMarkerAttribute(writer, kind);
+    }
+  }
+
   // Finalization of synthetic items.
 
   Result computeFinalSynthetics(AppView<?> appView) {
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 1d9b5b9..b5fc30d 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMarker.java
@@ -13,27 +13,92 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.synthesis.SyntheticNaming.SyntheticKind;
 import com.android.tools.r8.utils.DescriptorUtils;
-import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.InternalOptions;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.ByteVector;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Label;
 
 public class SyntheticMarker {
 
+  private static final String SYNTHETIC_MARKER_ATTRIBUTE_TYPE_NAME = "R8SynthesizedClass";
+
+  public static Attribute getMarkerAttributePrototype() {
+    return MarkerAttribute.PROTOTYPE;
+  }
+
+  public static void writeMarkerAttribute(ClassWriter writer, SyntheticKind kind) {
+    writer.visitAttribute(new MarkerAttribute(kind));
+  }
+
+  public static SyntheticMarker readMarkerAttribute(Attribute attribute) {
+    if (attribute instanceof MarkerAttribute) {
+      MarkerAttribute marker = (MarkerAttribute) attribute;
+      return new SyntheticMarker(marker.kind, null);
+    }
+    return null;
+  }
+
+  private static class MarkerAttribute extends Attribute {
+
+    private static final MarkerAttribute PROTOTYPE = new MarkerAttribute(null);
+
+    private SyntheticKind kind;
+
+    public MarkerAttribute(SyntheticKind kind) {
+      super(SYNTHETIC_MARKER_ATTRIBUTE_TYPE_NAME);
+      this.kind = kind;
+    }
+
+    @Override
+    protected Attribute read(
+        ClassReader classReader,
+        int offset,
+        int length,
+        char[] charBuffer,
+        int codeAttributeOffset,
+        Label[] labels) {
+      short id = classReader.readShort(offset);
+      assert id >= 0;
+      SyntheticKind kind = SyntheticKind.fromId(id);
+      return new MarkerAttribute(kind);
+    }
+
+    @Override
+    protected ByteVector write(
+        ClassWriter classWriter, byte[] code, int codeLength, int maxStack, int maxLocals) {
+      ByteVector byteVector = new ByteVector();
+      assert 0 <= kind.id && kind.id <= Short.MAX_VALUE;
+      byteVector.putShort(kind.id);
+      return byteVector;
+    }
+  }
+
   public static void addMarkerToClass(
-      DexProgramClass clazz,
-      SyntheticKind kind,
-      SynthesizingContext context,
-      DexItemFactory factory,
-      boolean dontRecordSynthesizingContext) {
+      DexProgramClass clazz, SyntheticKind kind, InternalOptions options) {
+    // TODO(b/158159959): Consider moving this to the dex writer similar to the CF case.
+    assert !options.isGeneratingClassFiles();
     clazz.setAnnotations(
         clazz
             .annotations()
             .getWithAddedOrReplaced(
-                DexAnnotation.createAnnotationSynthesizedClass(
-                    kind,
-                    dontRecordSynthesizingContext ? null : context.getSynthesizingContextType(),
-                    factory)));
+                DexAnnotation.createAnnotationSynthesizedClass(kind, options.itemFactory)));
   }
 
   public static SyntheticMarker stripMarkerFromClass(DexProgramClass clazz, AppView<?> appView) {
+    if (clazz.originatesFromClassResource()) {
+      SyntheticMarker marker = clazz.stripSyntheticInputMarker();
+      if (marker == null) {
+        return NO_MARKER;
+      }
+      assert marker.getContext() == null;
+      DexType contextType =
+          getSyntheticContextType(clazz.type, marker.kind, appView.dexItemFactory());
+      SynthesizingContext context =
+          SynthesizingContext.fromSyntheticInputClass(clazz, contextType, appView);
+      return new SyntheticMarker(marker.kind, context);
+    }
     SyntheticMarker marker = internalStripMarkerFromClass(clazz, appView);
     assert marker != NO_MARKER
         || !DexAnnotation.hasSynthesizedClassAnnotation(
@@ -50,15 +115,13 @@
     if (!flags.isSynthetic() || flags.isAbstract() || flags.isEnum()) {
       return NO_MARKER;
     }
-    Pair<SyntheticKind, DexType> info =
-        DexAnnotation.getSynthesizedClassAnnotationContextType(
+    SyntheticKind kind =
+        DexAnnotation.getSynthesizedClassAnnotationInfo(
             clazz.annotations(), appView.dexItemFactory());
-    if (info == null) {
+    if (kind == null) {
       return NO_MARKER;
     }
     assert clazz.annotations().size() == 1;
-    SyntheticKind kind = info.getFirst();
-    DexType context = info.getSecond();
     if (kind.isSingleSyntheticMethod) {
       if (!clazz.interfaces.isEmpty()) {
         return NO_MARKER;
@@ -70,22 +133,17 @@
       }
     }
     clazz.setAnnotations(DexAnnotationSet.empty());
-    if (context == null) {
-      // If the class is marked as synthetic but has no synthesizing context, then we read the
-      // context type as the prefix. This happens for desugared library builds where the context of
-      // the generated
-      // synthetics becomes themselves. Using the original context could otherwise have referenced
-      // a type in the non-rewritten library and cause an non-rewritten output type.
-      String prefix = SyntheticNaming.getPrefixForExternalSyntheticType(kind, clazz.type);
-      context =
-          appView
-              .dexItemFactory()
-              .createType(DescriptorUtils.getDescriptorFromClassBinaryName(prefix));
-    }
+    DexType context = getSyntheticContextType(clazz.type, kind, appView.dexItemFactory());
     return new SyntheticMarker(
         kind, SynthesizingContext.fromSyntheticInputClass(clazz, context, appView));
   }
 
+  private static DexType getSyntheticContextType(
+      DexType type, SyntheticKind kind, DexItemFactory factory) {
+    String prefix = SyntheticNaming.getPrefixForExternalSyntheticType(kind, type);
+    return factory.createType(DescriptorUtils.getDescriptorFromClassBinaryName(prefix));
+  }
+
   private static final SyntheticMarker NO_MARKER = new SyntheticMarker(null, null);
 
   private final SyntheticKind kind;
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodReference.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodReference.java
index b6bb218..80e4e19 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodReference.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodReference.java
@@ -58,9 +58,8 @@
     // If the reference has been non-trivially rewritten the compiler has changed it and it can no
     // longer be considered a synthetic. The context may or may not have changed.
     if (method != rewritten && !lens.isSimpleRenaming(method, rewritten)) {
-      // If the referenced item is rewritten, it should be moved to another holder as the
-      // synthetic holder is no longer part of the synthetic collection.
-      assert method.holder != rewritten.holder : "The synthetic method reference should have moved";
+      // If the referenced item is rewritten, it should be moved to a non-internal synthetic holder
+      // as the synthetic holder is no longer part of the synthetic collection.
       assert SyntheticNaming.verifyNotInternalSynthetic(rewritten.holder);
       return null;
     }
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 8b19177..ce1647b 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticNaming.java
@@ -23,47 +23,53 @@
    */
   public enum SyntheticKind {
     // Class synthetics.
-    RECORD_TAG("", false, true, true),
-    COMPANION_CLASS("-CC", false, true),
-    EMULATED_INTERFACE_CLASS("-EL", false, true),
-    LAMBDA("Lambda", false),
-    INIT_TYPE_ARGUMENT("-IA", false, true),
-    HORIZONTAL_INIT_TYPE_ARGUMENT_1(SYNTHETIC_CLASS_SEPARATOR + "IA$1", false, true),
-    HORIZONTAL_INIT_TYPE_ARGUMENT_2(SYNTHETIC_CLASS_SEPARATOR + "IA$2", false, true),
-    HORIZONTAL_INIT_TYPE_ARGUMENT_3(SYNTHETIC_CLASS_SEPARATOR + "IA$3", false, true),
+    RECORD_TAG("", 1, false, true, true),
+    COMPANION_CLASS("-CC", 2, false, true),
+    EMULATED_INTERFACE_CLASS("-EL", 3, false, true),
+    LAMBDA("Lambda", 4, false),
+    INIT_TYPE_ARGUMENT("-IA", 5, false, true),
+    HORIZONTAL_INIT_TYPE_ARGUMENT_1(SYNTHETIC_CLASS_SEPARATOR + "IA$1", 6, false, true),
+    HORIZONTAL_INIT_TYPE_ARGUMENT_2(SYNTHETIC_CLASS_SEPARATOR + "IA$2", 7, false, true),
+    HORIZONTAL_INIT_TYPE_ARGUMENT_3(SYNTHETIC_CLASS_SEPARATOR + "IA$3", 8, false, true),
     // Method synthetics.
-    RECORD_HELPER("Record", true),
-    BACKPORT("Backport", true),
-    STATIC_INTERFACE_CALL("StaticInterfaceCall", true),
-    TO_STRING_IF_NOT_NULL("ToStringIfNotNull", true),
-    THROW_CCE_IF_NOT_NULL("ThrowCCEIfNotNull", true),
-    THROW_IAE("ThrowIAE", true),
-    THROW_ICCE("ThrowICCE", true),
-    THROW_NSME("ThrowNSME", true),
-    TWR_CLOSE_RESOURCE("TwrCloseResource", true),
-    SERVICE_LOADER("ServiceLoad", true),
-    OUTLINE("Outline", true);
+    RECORD_HELPER("Record", 9, true),
+    BACKPORT("Backport", 10, true),
+    STATIC_INTERFACE_CALL("StaticInterfaceCall", 11, true),
+    TO_STRING_IF_NOT_NULL("ToStringIfNotNull", 12, true),
+    THROW_CCE_IF_NOT_NULL("ThrowCCEIfNotNull", 13, true),
+    THROW_IAE("ThrowIAE", 14, true),
+    THROW_ICCE("ThrowICCE", 15, true),
+    THROW_NSME("ThrowNSME", 16, true),
+    TWR_CLOSE_RESOURCE("TwrCloseResource", 17, true),
+    SERVICE_LOADER("ServiceLoad", 18, true),
+    OUTLINE("Outline", 19, true);
 
     public final String descriptor;
+    public final int id;
     public final boolean isSingleSyntheticMethod;
     public final boolean isFixedSuffixSynthetic;
     public final boolean mayOverridesNonProgramType;
 
-    SyntheticKind(String descriptor, boolean isSingleSyntheticMethod) {
-      this(descriptor, isSingleSyntheticMethod, false);
-    }
-
-    SyntheticKind(
-        String descriptor, boolean isSingleSyntheticMethod, boolean isFixedSuffixSynthetic) {
-      this(descriptor, isSingleSyntheticMethod, isFixedSuffixSynthetic, false);
+    SyntheticKind(String descriptor, int id, boolean isSingleSyntheticMethod) {
+      this(descriptor, id, isSingleSyntheticMethod, false);
     }
 
     SyntheticKind(
         String descriptor,
+        int id,
+        boolean isSingleSyntheticMethod,
+        boolean isFixedSuffixSynthetic) {
+      this(descriptor, id, isSingleSyntheticMethod, isFixedSuffixSynthetic, false);
+    }
+
+    SyntheticKind(
+        String descriptor,
+        int id,
         boolean isSingleSyntheticMethod,
         boolean isFixedSuffixSynthetic,
         boolean mayOverridesNonProgramType) {
       this.descriptor = descriptor;
+      this.id = id;
       this.isSingleSyntheticMethod = isSingleSyntheticMethod;
       this.isFixedSuffixSynthetic = isFixedSuffixSynthetic;
       this.mayOverridesNonProgramType = mayOverridesNonProgramType;
@@ -81,6 +87,15 @@
       }
       return null;
     }
+
+    public static SyntheticKind fromId(int id) {
+      for (SyntheticKind kind : values()) {
+        if (kind.id == id) {
+          return kind;
+        }
+      }
+      return null;
+    }
   }
 
   private static final String SYNTHETIC_CLASS_SEPARATOR = "$$";
@@ -199,7 +214,7 @@
       return clazz.getBinaryName().endsWith(kind.descriptor);
     }
     String separator = getPhaseSeparator(phase);
-    int i = typeName.indexOf(separator);
+    int i = typeName.lastIndexOf(separator);
     return i >= 0 && checkMatchFrom(kind, typeName, i, separator, phase == Phase.EXTERNAL);
   }
 
diff --git a/src/main/java/com/android/tools/r8/utils/ArrayUtils.java b/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
index 5a4ee11..aae7abe 100644
--- a/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ArrayUtils.java
@@ -93,10 +93,21 @@
    * @param clazz target type's Class to cast
    * @param original an array of original elements
    * @param mapper a mapper that rewrites an original element to a new one, maybe `null`
-   * @param <T> target type
    * @return an array with written elements
    */
   public static <T> T[] map(Class<T[]> clazz, T[] original, Function<T, T> mapper) {
+    return map(original, mapper, clazz.cast(Array.newInstance(clazz.getComponentType(), 0)));
+  }
+
+  /**
+   * Rewrites the input array based on the given function.
+   *
+   * @param original an array of original elements
+   * @param mapper a mapper that rewrites an original element to a new one, maybe `null`
+   * @param emptyArray an empty array
+   * @return an array with written elements
+   */
+  public static <T> T[] map(T[] original, Function<T, T> mapper, T[] emptyArray) {
     ArrayList<T> results = null;
     for (int i = 0; i < original.length; i++) {
       T oldOne = original[i];
@@ -117,11 +128,7 @@
         }
       }
     }
-    if (results == null) {
-      return original;
-    }
-    return results.toArray(
-        clazz.cast(Array.newInstance(clazz.getComponentType(), results.size())));
+    return results != null ? results.toArray(emptyArray) : original;
   }
 
   public static int[] createIdentityArray(int size) {
diff --git a/src/main/java/com/android/tools/r8/utils/AssertionUtils.java b/src/main/java/com/android/tools/r8/utils/AssertionUtils.java
index c185096..bb1e59f 100644
--- a/src/main/java/com/android/tools/r8/utils/AssertionUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/AssertionUtils.java
@@ -4,10 +4,16 @@
 
 package com.android.tools.r8.utils;
 
+import java.util.function.Supplier;
+
 public class AssertionUtils {
 
   public static boolean assertNotNull(Object o) {
     assert o != null;
     return true;
   }
+
+  public static boolean forTesting(InternalOptions options, Supplier<Boolean> test) {
+    return options.testing.enableTestAssertions ? test.get() : true;
+  }
 }
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 94b8404..12126a4 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -40,6 +40,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
 import com.android.tools.r8.inspector.internal.InspectorImpl;
 import com.android.tools.r8.ir.code.IRCode;
@@ -50,6 +51,7 @@
 import com.android.tools.r8.naming.MapVersion;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
+import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.repackaging.Repackaging.DefaultRepackagingConfiguration;
 import com.android.tools.r8.repackaging.Repackaging.RepackagingConfiguration;
@@ -209,7 +211,6 @@
     enableClassInlining = false;
     enableClassStaticizer = false;
     enableDevirtualization = false;
-    horizontalClassMergerOptions.disable();
     enableVerticalClassMerging = false;
     enableEnumUnboxing = false;
     enableUninstantiatedTypeOptimization = false;
@@ -219,6 +220,7 @@
     enableSideEffectAnalysis = false;
     enableTreeShakingOfLibraryMethodOverrides = false;
     callSiteOptimizationOptions.disableOptimization();
+    horizontalClassMergerOptions.setRestrictToSynthetics();
   }
 
   public boolean printTimes = System.getProperty("com.android.tools.r8.printtimes") != null;
@@ -321,8 +323,6 @@
   // TODO(b/138917494): Disable until we have numbers on potential performance penalties.
   public boolean enableRedundantConstNumberOptimization = false;
 
-  public boolean enablePcDebugInfoOutput = false;
-
   public String synthesizedClassPrefix = "";
 
   // Number of threads to use while processing the dex files.
@@ -349,6 +349,9 @@
   // 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;
@@ -560,6 +563,10 @@
   private final boolean enableTreeShaking;
   private final boolean enableMinification;
 
+  public boolean isOptimizing() {
+    return hasProguardConfiguration() && getProguardConfiguration().isOptimizing();
+  }
+
   public boolean isRelease() {
     return !debug;
   }
@@ -616,8 +623,18 @@
    * If any non-static class merging is enabled, information about types referred to by instanceOf
    * and check cast instructions needs to be collected.
    */
-  public boolean isClassMergingExtensionRequired() {
-    return horizontalClassMergerOptions.isEnabled() || enableVerticalClassMerging;
+  public boolean isClassMergingExtensionRequired(Enqueuer.Mode mode) {
+    if (mode.isInitialTreeShaking()) {
+      return (horizontalClassMergerOptions.isEnabled(HorizontalClassMerger.Mode.INITIAL)
+              && !horizontalClassMergerOptions.isRestrictedToSynthetics())
+          || enableVerticalClassMerging;
+    }
+    if (mode.isFinalTreeShaking()) {
+      return horizontalClassMergerOptions.isEnabled(HorizontalClassMerger.Mode.FINAL)
+          && !horizontalClassMergerOptions.isRestrictedToSynthetics();
+    }
+    assert false;
+    return false;
   }
 
   @Override
@@ -1192,13 +1209,16 @@
     }
   }
 
-  public static class HorizontalClassMergerOptions {
+  public class HorizontalClassMergerOptions {
 
     // TODO(b/138781768): Set enable to true when this bug is resolved.
-    public boolean enable =
+    private boolean enable =
         !Version.isDevelopmentVersion()
             || System.getProperty("com.android.tools.r8.disableHorizontalClassMerging") == null;
-    public boolean enableConstructorMerging = true;
+    private boolean enableInterfaceMerging = false;
+    private boolean enableSyntheticMerging = true;
+    private boolean ignoreRuntimeTypeChecksForTesting = false;
+    private boolean restrictToSynthetics = false;
 
     public int maxGroupSize = 30;
 
@@ -1206,6 +1226,10 @@
       enable = false;
     }
 
+    public void disableSyntheticMerging() {
+      enableSyntheticMerging = false;
+    }
+
     public void enable() {
       enable = true;
     }
@@ -1214,20 +1238,56 @@
       this.enable = enable;
     }
 
+    public void enableInterfaceMerging() {
+      enableInterfaceMerging = true;
+    }
+
     public int getMaxGroupSize() {
       return maxGroupSize;
     }
 
     public boolean isConstructorMergingEnabled() {
-      return enableConstructorMerging;
+      return true;
     }
 
-    public boolean isDisabled() {
-      return !isEnabled();
+    public boolean isEnabled(HorizontalClassMerger.Mode mode) {
+      if (!enable || debug || intermediate) {
+        return false;
+      }
+      if (mode.isInitial()) {
+        return enableInlining && isShrinking();
+      }
+      assert mode.isFinal();
+      return true;
     }
 
-    public boolean isEnabled() {
-      return enable;
+    public boolean isIgnoreRuntimeTypeChecksForTestingEnabled() {
+      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();
+    }
+
+    public boolean isRestrictedToSynthetics() {
+      return restrictToSynthetics || !isOptimizing() || !isShrinking();
+    }
+
+    public void setIgnoreRuntimeTypeChecksForTesting() {
+      ignoreRuntimeTypeChecksForTesting = true;
+    }
+
+    public void setRestrictToSynthetics() {
+      restrictToSynthetics = true;
     }
   }
 
@@ -1318,7 +1378,9 @@
     public boolean addCallEdgesForLibraryInvokes = false;
 
     public boolean allowCheckDiscardedErrors = false;
+    public boolean allowClassInliningOfSynthetics = true;
     public boolean allowInjectedAnnotationMethods = false;
+    public boolean allowInliningOfSynthetics = true;
     public boolean allowTypeErrors =
         !Version.isDevelopmentVersion()
             || System.getProperty("com.android.tools.r8.allowTypeErrors") != null;
@@ -1366,6 +1428,9 @@
     public boolean checkForNotExpandingMainDexTracingResult = false;
     public Set<String> allowedUnusedDontWarnPatterns = new HashSet<>();
     public boolean repackageWithNoMinification = false;
+    public boolean enableTestAssertions =
+        System.getProperty("com.android.tools.r8.enableTestAssertions") != null;
+    public boolean testEnableTestAssertions = false;
 
     public boolean allowConflictingSyntheticTypes = false;
 
@@ -1541,7 +1606,7 @@
   }
 
   public boolean canUseDexPcAsDebugInformation() {
-    return enablePcDebugInfoOutput && !debug && hasMinApi(AndroidApiLevel.O);
+    return !debug && hasMinApi(AndroidApiLevel.O);
   }
 
   public boolean isInterfaceMethodDesugaringEnabled() {
diff --git a/src/main/java/com/android/tools/r8/utils/IterableUtils.java b/src/main/java/com/android/tools/r8/utils/IterableUtils.java
index 1a61b44..89063c4 100644
--- a/src/main/java/com/android/tools/r8/utils/IterableUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/IterableUtils.java
@@ -19,6 +19,25 @@
 
 public class IterableUtils {
 
+  public static <S, T> boolean allIdentical(Iterable<S> iterable) {
+    return allIdentical(iterable, Function.identity());
+  }
+
+  public static <S, T> boolean allIdentical(Iterable<S> iterable, Function<S, T> fn) {
+    Iterator<S> iterator = iterable.iterator();
+    if (!iterator.hasNext()) {
+      return true;
+    }
+    T first = fn.apply(iterator.next());
+    while (iterator.hasNext()) {
+      T other = fn.apply(iterator.next());
+      if (other != first) {
+        return false;
+      }
+    }
+    return true;
+  }
+
   public static <S, T> boolean any(
       Iterable<S> iterable, Function<S, T> transform, Predicate<T> predicate) {
     for (S element : iterable) {
diff --git a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
index 3663ec2..da825d1 100644
--- a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
@@ -358,7 +358,7 @@
           if (code != null) {
             if (code.isDexCode() && doesContainPositions(code.asDexCode())) {
               if (appView.options().canUseDexPcAsDebugInformation() && methods.size() == 1) {
-                optimizeDexCodePositionsForPc(method, kotlinRemapper, mappedPositions);
+                optimizeDexCodePositionsForPc(method, appView, kotlinRemapper, mappedPositions);
               } else {
                 optimizeDexCodePositions(
                     method, appView, kotlinRemapper, mappedPositions, identityMapping);
@@ -761,6 +761,7 @@
 
   private static void optimizeDexCodePositionsForPc(
       DexEncodedMethod method,
+      AppView<?> appView,
       PositionRemapper positionRemapper,
       List<MappedPosition> mappedPositions) {
     // Do the actual processing for each method.
@@ -770,7 +771,9 @@
     Pair<Integer, Position> lastPosition = new Pair<>();
 
     DexDebugEventVisitor visitor =
-        new DexDebugPositionState(debugInfo.startLine, method.getReference()) {
+        new DexDebugPositionState(
+            debugInfo.startLine,
+            appView.graphLens().getOriginalMethodSignature(method.getReference())) {
           @Override
           public void visit(Default defaultEvent) {
             super.visit(defaultEvent);
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 7b4976c..1c260e3 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.LinkedList;
 import java.util.List;
 import java.util.Optional;
 import java.util.function.BiFunction;
@@ -50,6 +51,11 @@
     return list.get(0);
   }
 
+  public static <T> T firstMatching(List<T> list, Predicate<T> tester) {
+    int i = firstIndexMatching(list, tester);
+    return i >= 0 ? list.get(i) : null;
+  }
+
   public static <T> int firstIndexMatching(List<T> list, Predicate<T> tester) {
     for (int i = 0; i < list.size(); i++) {
       if (tester.test(list.get(i))) {
@@ -153,6 +159,18 @@
     return builder.build();
   }
 
+  public static <T> LinkedList<T> newLinkedList(T element) {
+    LinkedList<T> list = new LinkedList<>();
+    list.add(element);
+    return list;
+  }
+
+  public static <T> LinkedList<T> newLinkedList(ForEachable<T> forEachable) {
+    LinkedList<T> list = new LinkedList<>();
+    forEachable.forEach(list::add);
+    return list;
+  }
+
   public static <T> Optional<T> removeFirstMatch(List<T> list, Predicate<T> element) {
     int index = firstIndexMatching(list, element);
     if (index >= 0) {
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 abae424..63e881e 100644
--- a/src/main/java/com/android/tools/r8/utils/MapUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/MapUtils.java
@@ -9,9 +9,16 @@
 import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.function.IntFunction;
+import java.util.function.Supplier;
 
 public class MapUtils {
 
+  public static <K, V> Map<K, V> clone(
+      Map<K, V> mapToClone, Map<K, V> newMap, Function<V, V> valueCloner) {
+    mapToClone.forEach((key, value) -> newMap.put(key, valueCloner.apply(value)));
+    return newMap;
+  }
+
   public static <K, V> K firstKey(Map<K, V> map) {
     return map.keySet().iterator().next();
   }
@@ -20,6 +27,10 @@
     return map.values().iterator().next();
   }
 
+  public static <T, R> Function<T, R> ignoreKey(Supplier<R> supplier) {
+    return ignore -> supplier.get();
+  }
+
   public static <K, V> Map<K, V> map(
       Map<K, V> map,
       IntFunction<Map<K, V>> factory,
diff --git a/src/main/java/com/android/tools/r8/utils/StreamUtils.java b/src/main/java/com/android/tools/r8/utils/StreamUtils.java
index 8d4ebc3..53a3010 100644
--- a/src/main/java/com/android/tools/r8/utils/StreamUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/StreamUtils.java
@@ -8,6 +8,7 @@
 import java.io.InputStream;
 
 public class StreamUtils {
+
   /**
    * Read all data from the stream into a byte[], close the stream and return the bytes.
    * @return The bytes of the stream
diff --git a/src/main/java/com/android/tools/r8/utils/collections/DexMethodSignatureSet.java b/src/main/java/com/android/tools/r8/utils/collections/DexMethodSignatureSet.java
index 5a4bc89..8ef5506 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/DexMethodSignatureSet.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/DexMethodSignatureSet.java
@@ -8,13 +8,19 @@
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexMethodSignature;
+import com.google.common.collect.Iterables;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.Set;
 import java.util.function.Function;
 
-public class DexMethodSignatureSet implements Iterable<DexMethodSignature> {
+public class DexMethodSignatureSet implements Collection<DexMethodSignature> {
+
+  private static final DexMethodSignatureSet EMPTY =
+      new DexMethodSignatureSet(Collections.emptySet());
 
   private final Set<DexMethodSignature> backing;
 
@@ -34,6 +40,11 @@
     return new DexMethodSignatureSet(new LinkedHashSet<>());
   }
 
+  public static DexMethodSignatureSet empty() {
+    return EMPTY;
+  }
+
+  @Override
   public boolean add(DexMethodSignature signature) {
     return backing.add(signature);
   }
@@ -50,8 +61,9 @@
     return add(method.getReference());
   }
 
-  public void addAll(Iterable<DexMethodSignature> signatures) {
-    signatures.forEach(this::add);
+  @Override
+  public boolean addAll(Collection<? extends DexMethodSignature> collection) {
+    return backing.addAll(collection);
   }
 
   public void addAllMethods(Iterable<DexEncodedMethod> methods) {
@@ -64,19 +76,53 @@
 
   public <T> void addAll(Iterable<T> elements, Function<T, Iterable<DexMethodSignature>> fn) {
     for (T element : elements) {
-      addAll(fn.apply(element));
+      Iterables.addAll(this, fn.apply(element));
     }
   }
 
+  @Override
+  public void clear() {
+    backing.clear();
+  }
+
+  @Override
+  public boolean contains(Object o) {
+    return backing.contains(o);
+  }
+
   public boolean contains(DexMethodSignature signature) {
     return backing.contains(signature);
   }
 
   @Override
+  public boolean containsAll(Collection<?> collection) {
+    return backing.containsAll(collection);
+  }
+
+  public boolean containsAnyOf(Iterable<DexMethodSignature> signatures) {
+    for (DexMethodSignature signature : signatures) {
+      if (contains(signature)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public boolean isEmpty() {
+    return backing.isEmpty();
+  }
+
+  @Override
   public Iterator<DexMethodSignature> iterator() {
     return backing.iterator();
   }
 
+  @Override
+  public boolean remove(Object o) {
+    return backing.remove(o);
+  }
+
   public boolean remove(DexMethodSignature signature) {
     return backing.remove(signature);
   }
@@ -85,11 +131,32 @@
     return remove(method.getSignature());
   }
 
-  public void removeAll(Iterable<DexMethodSignature> signatures) {
-    signatures.forEach(this::remove);
+  @Override
+  public boolean removeAll(Collection<?> collection) {
+    return backing.removeAll(collection);
   }
 
   public void removeAllMethods(Iterable<DexEncodedMethod> methods) {
     methods.forEach(this::remove);
   }
+
+  @Override
+  public boolean retainAll(Collection<?> collection) {
+    return backing.retainAll(collection);
+  }
+
+  @Override
+  public int size() {
+    return backing.size();
+  }
+
+  @Override
+  public Object[] toArray() {
+    return backing.toArray();
+  }
+
+  @Override
+  public <T> T[] toArray(T[] ts) {
+    return backing.toArray(ts);
+  }
 }
diff --git a/src/test/examples/shaking1/print-mapping-dex.ref b/src/test/examples/shaking1/print-mapping-dex.ref
index 602e5f8..7ba8ee8 100644
--- a/src/test/examples/shaking1/print-mapping-dex.ref
+++ b/src/test/examples/shaking1/print-mapping-dex.ref
@@ -1,5 +1,5 @@
 shaking1.Shaking -> shaking1.Shaking:
 shaking1.Used -> a.a:
     java.lang.String method() -> a
-    1:1:void main(java.lang.String[]):8:8 -> main
-    1:1:void <init>(java.lang.String):12:12 -> <init>
+    0:16:void main(java.lang.String[]):8:8 -> main
+    0:3:void <init>(java.lang.String):12:12 -> <init>
diff --git a/src/test/examplesAndroidApi/softverificationerror/ApiCallerInlined.java b/src/test/examplesAndroidApi/softverificationerror/ApiCallerInlined.java
new file mode 100644
index 0000000..0ef2b5c
--- /dev/null
+++ b/src/test/examplesAndroidApi/softverificationerror/ApiCallerInlined.java
@@ -0,0 +1,41 @@
+// 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.example.softverificationerror;
+
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.os.Build;
+
+public class ApiCallerInlined {
+
+  public static void callApi(android.content.Context context) {
+    // Create the NotificationChannel, but only on API 26+ because
+    // the NotificationChannel class is new and not in the support library
+    if (Build.VERSION.SDK_INT >= 26) {
+      constructUnknownObjectAndCallUnknownMethod(context);
+    }
+  }
+
+  public static void constructUnknownObject(android.content.Context context) {
+    NotificationChannel channel =
+        new NotificationChannel("CHANNEL_ID", "FOO", NotificationManager.IMPORTANCE_DEFAULT);
+    channel.setDescription("This is a test channel");
+  }
+
+  public static void callUnknownMethod(android.content.Context context) {
+    NotificationManager notificationManager =
+        (NotificationManager) context.getSystemService(NotificationManager.class);
+    notificationManager.createNotificationChannel(null);
+  }
+
+  public static void constructUnknownObjectAndCallUnknownMethod(android.content.Context context) {
+    NotificationChannel channel =
+        new NotificationChannel("CHANNEL_ID", "FOO", NotificationManager.IMPORTANCE_DEFAULT);
+    channel.setDescription("This is a test channel");
+    NotificationManager notificationManager =
+        (NotificationManager) context.getSystemService(NotificationManager.class);
+    notificationManager.createNotificationChannel(channel);
+  }
+}
diff --git a/src/test/examplesAndroidApi/softverificationerror/ApiCallerOutlined.java b/src/test/examplesAndroidApi/softverificationerror/ApiCallerOutlined.java
new file mode 100644
index 0000000..47c3e61
--- /dev/null
+++ b/src/test/examplesAndroidApi/softverificationerror/ApiCallerOutlined.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.example.softverificationerror;
+
+import android.os.Build;
+
+public class ApiCallerOutlined {
+
+  public static void callApi(android.content.Context context) {
+    if (Build.VERSION.SDK_INT >= 26) {
+      ApiCallerInlined.constructUnknownObjectAndCallUnknownMethod(context);
+    }
+  }
+}
diff --git a/src/test/examplesAndroidApi/softverificationerror/Main.java b/src/test/examplesAndroidApi/softverificationerror/Main.java
new file mode 100644
index 0000000..f2a4376
--- /dev/null
+++ b/src/test/examplesAndroidApi/softverificationerror/Main.java
@@ -0,0 +1,13 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.example.softverificationerror;
+
+public class Main {
+
+  public static void test(android.content.Context context) {
+    ApiCallerInlined.callApi(context);
+    ApiCallerOutlined.callApi(context);
+  }
+}
diff --git a/src/test/examplesJava11/horizontalclassmerging/BasicNestHostHorizontalClassMerging.java b/src/test/examplesJava11/horizontalclassmerging/BasicNestHostHorizontalClassMerging.java
deleted file mode 100644
index 2362165..0000000
--- a/src/test/examplesJava11/horizontalclassmerging/BasicNestHostHorizontalClassMerging.java
+++ /dev/null
@@ -1,43 +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 horizontalclassmerging;
-
-import com.android.tools.r8.NeverClassInline;
-import com.android.tools.r8.NeverInline;
-
-public class BasicNestHostHorizontalClassMerging {
-  // Prevent merging with BasicNestHostHorizontalClassMerging2.
-  private String name;
-
-  private BasicNestHostHorizontalClassMerging(String name) {
-    this.name = name;
-  }
-
-  @NeverInline
-  private void print(String v) {
-    System.out.println(name + ": " + v);
-  }
-
-  public static void main(String[] args) {
-    BasicNestHostHorizontalClassMerging host = new BasicNestHostHorizontalClassMerging("1");
-    new A(host);
-    new B(host);
-    BasicNestHostHorizontalClassMerging2.main(args);
-  }
-
-  @NeverClassInline
-  public static class A {
-    public A(BasicNestHostHorizontalClassMerging parent) {
-      parent.print("a");
-    }
-  }
-
-  @NeverClassInline
-  public static class B {
-    public B(BasicNestHostHorizontalClassMerging parent) {
-      parent.print("b");
-    }
-  }
-}
diff --git a/src/test/examplesJava11/horizontalclassmerging/BasicNestHostHorizontalClassMerging2.java b/src/test/examplesJava11/horizontalclassmerging/BasicNestHostHorizontalClassMerging2.java
deleted file mode 100644
index 78718e1..0000000
--- a/src/test/examplesJava11/horizontalclassmerging/BasicNestHostHorizontalClassMerging2.java
+++ /dev/null
@@ -1,35 +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 horizontalclassmerging;
-
-import com.android.tools.r8.NeverClassInline;
-import com.android.tools.r8.NeverInline;
-
-public class BasicNestHostHorizontalClassMerging2 {
-  @NeverInline
-  public static void main(String[] args) {
-    new A();
-    new B();
-  }
-
-  @NeverInline
-  private static void print(String v) {
-    System.out.println("2: " + v);
-  }
-
-  @NeverClassInline
-  public static class A {
-    public A() {
-      print("a");
-    }
-  }
-
-  @NeverClassInline
-  public static class B {
-    public B() {
-      print("b");
-    }
-  }
-}
diff --git a/src/test/examplesJava11/horizontalclassmerging/NestClassMergingTest.java b/src/test/examplesJava11/horizontalclassmerging/NestClassMergingTest.java
new file mode 100644
index 0000000..b8ef472
--- /dev/null
+++ b/src/test/examplesJava11/horizontalclassmerging/NestClassMergingTest.java
@@ -0,0 +1,17 @@
+// 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 horizontalclassmerging;
+
+public class NestClassMergingTest {
+
+  public static void main(String[] args) {
+    NestHostA hostA = new NestHostA();
+    new NestHostA.NestMemberA();
+    new NestHostA.NestMemberB(hostA);
+    NestHostB hostB = new NestHostB();
+    new NestHostB.NestMemberA();
+    new NestHostB.NestMemberB(hostB);
+  }
+}
diff --git a/src/test/examplesJava11/horizontalclassmerging/NestHostA.java b/src/test/examplesJava11/horizontalclassmerging/NestHostA.java
new file mode 100644
index 0000000..13973b6
--- /dev/null
+++ b/src/test/examplesJava11/horizontalclassmerging/NestHostA.java
@@ -0,0 +1,36 @@
+// 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 horizontalclassmerging;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+
+@NeverClassInline
+public class NestHostA {
+
+  @NeverInline
+  private void privatePrint(String v) {
+    System.out.println(v);
+  }
+
+  @NeverInline
+  private static void privateStaticPrint(String v) {
+    System.out.println(v);
+  }
+
+  @NeverClassInline
+  public static class NestMemberA {
+    public NestMemberA() {
+      NestHostA.privateStaticPrint("NestHostA$NestMemberA");
+    }
+  }
+
+  @NeverClassInline
+  public static class NestMemberB {
+    public NestMemberB(NestHostA host) {
+      host.privatePrint("NestHostA$NestMemberB");
+    }
+  }
+}
diff --git a/src/test/examplesJava11/horizontalclassmerging/NestHostB.java b/src/test/examplesJava11/horizontalclassmerging/NestHostB.java
new file mode 100644
index 0000000..99ac10b
--- /dev/null
+++ b/src/test/examplesJava11/horizontalclassmerging/NestHostB.java
@@ -0,0 +1,36 @@
+// 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 horizontalclassmerging;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+
+@NeverClassInline
+public class NestHostB {
+
+  @NeverInline
+  private void privatePrint(String v) {
+    System.out.println(v);
+  }
+
+  @NeverInline
+  private static void privateStaticPrint(String v) {
+    System.out.println(v);
+  }
+
+  @NeverClassInline
+  public static class NestMemberA {
+    public NestMemberA() {
+      NestHostB.privateStaticPrint("NestHostB$NestMemberA");
+    }
+  }
+
+  @NeverClassInline
+  public static class NestMemberB {
+    public NestMemberB(NestHostB host) {
+      host.privatePrint("NestHostB$NestMemberB");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index 637a88b..d78af9d 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -61,6 +61,8 @@
   private List<String> applyMappingMaps = new ArrayList<>();
   private final List<Path> features = new ArrayList<>();
 
+  private boolean createDefaultProguardMapConsumer = true;
+
   @Override
   R8TestCompileResult internalCompile(
       Builder builder, Consumer<InternalOptions> optionsConsumer, Supplier<AndroidApp> app)
@@ -69,21 +71,23 @@
       builder.addProguardConfiguration(keepRules, Origin.unknown());
     }
     builder.addMainDexRulesFiles(mainDexRulesFiles);
-    StringBuilder proguardMapBuilder = new StringBuilder();
     builder.setDisableTreeShaking(!enableTreeShaking);
     builder.setDisableMinification(!enableMinification);
-    builder.setProguardMapConsumer(
-        new StringConsumer() {
-          @Override
-          public void accept(String string, DiagnosticsHandler handler) {
-            proguardMapBuilder.append(string);
-          }
+    StringBuilder proguardMapBuilder = new StringBuilder();
+    if (createDefaultProguardMapConsumer) {
+      builder.setProguardMapConsumer(
+          new StringConsumer() {
+            @Override
+            public void accept(String string, DiagnosticsHandler handler) {
+              proguardMapBuilder.append(string);
+            }
 
-          @Override
-          public void finished(DiagnosticsHandler handler) {
-            // Nothing to do.
-          }
-        });
+            @Override
+            public void finished(DiagnosticsHandler handler) {
+              // Nothing to do.
+            }
+          });
+    }
 
     if (!applyMappingMaps.isEmpty()) {
       try {
@@ -118,7 +122,7 @@
             app.get(),
             box.proguardConfiguration,
             box.syntheticProguardRules,
-            proguardMapBuilder.toString(),
+            createDefaultProguardMapConsumer ? proguardMapBuilder.toString() : null,
             graphConsumer,
             minApiLevel,
             features);
@@ -440,6 +444,11 @@
     return self();
   }
 
+  public T noClassInliningOfSynthetics() {
+    return addOptionsModification(
+        options -> options.testing.allowClassInliningOfSynthetics = false);
+  }
+
   public T noClassStaticizing() {
     return noClassStaticizing(true);
   }
@@ -463,8 +472,21 @@
   }
 
   public T noHorizontalClassMerging(Class<?> clazz) {
-    return addKeepRules(
-        "-" + NoHorizontalClassMergingRule.RULE_NAME + " class " + clazz.getTypeName());
+    return noHorizontalClassMerging(clazz.getTypeName());
+  }
+
+  public T noHorizontalClassMerging(String typeName) {
+    return addKeepRules("-" + NoHorizontalClassMergingRule.RULE_NAME + " class " + typeName)
+        .enableProguardTestOptions();
+  }
+
+  public T noHorizontalClassMergingOfSynthetics() {
+    return addOptionsModification(
+        options -> options.horizontalClassMergerOptions().disableSyntheticMerging());
+  }
+
+  public T noInliningOfSynthetics() {
+    return addOptionsModification(options -> options.testing.allowInliningOfSynthetics = false);
   }
 
   public T enableNoUnusedInterfaceRemovalAnnotations() {
@@ -489,6 +511,13 @@
     return addInternalKeepRules("-nohorizontalclassmerging class " + clazz);
   }
 
+  public T addNoHorizontalClassMergingRule(String... classes) {
+    for (String clazz : classes) {
+      addNoHorizontalClassMergingRule(clazz);
+    }
+    return self();
+  }
+
   public T enableMemberValuePropagationAnnotations() {
     return enableMemberValuePropagationAnnotations(true);
   }
@@ -661,4 +690,9 @@
     features.add(path);
     return self();
   }
+
+  public T noDefaultProguardMapConsumer() {
+    createDefaultProguardMapConsumer = false;
+    return self();
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/SoftVerificationErrorJarGenerator.java b/src/test/java/com/android/tools/r8/SoftVerificationErrorJarGenerator.java
new file mode 100644
index 0000000..50f6e05
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/SoftVerificationErrorJarGenerator.java
@@ -0,0 +1,347 @@
+// 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;
+
+import com.android.tools.r8.utils.IntBox;
+import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.ZipUtils;
+import java.io.BufferedOutputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.function.Supplier;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+public class SoftVerificationErrorJarGenerator {
+
+  public enum ApiCallerName {
+    CONSTRUCT_UNKNOWN("constructUnknownObject"),
+    CALL_UNKNOWN("callUnknownMethod"),
+    CONSTRUCT_AND_CALL_UNKNOWN("constructUnknownObjectAndCallUnknownMethod");
+
+    private String apiCallerMethodName;
+
+    ApiCallerName(String apiCallerMethodName) {
+      this.apiCallerMethodName = apiCallerMethodName;
+    }
+
+    public String getApiCallerMethodName() {
+      return apiCallerMethodName;
+    }
+  }
+
+  public static String NEW_API_CLASS_NAME = "android/app/NotificationChannel";
+  public static String NEW_API_CLASS_METHOD_NAME = "setDescription";
+  public static String EXISTING_API_METHOD_NAME = "createNotificationChannel";
+
+  public static void createJar(
+      Path archive,
+      int numberOfClasses,
+      boolean isOutlined,
+      ApiCallerName callerName,
+      String newApiClassName,
+      String newApiClassMethodName,
+      String existingApiNewMethodName)
+      throws IOException {
+    OpenOption[] options =
+        new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING};
+    try (ZipOutputStream out =
+        new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(archive, options)))) {
+      IntBox intBox = new IntBox(0);
+      Pair<byte[], String> callerPair =
+          isOutlined
+              ? Dumps.dumpApiCallerInlined(
+                  -1,
+                  callerName.getApiCallerMethodName(),
+                  newApiClassName,
+                  newApiClassMethodName,
+                  existingApiNewMethodName)
+              : null;
+      if (callerPair != null) {
+        // The outlined code will call the apiCallerName on the callerPair code.
+        ZipUtils.writeToZipStream(
+            out, callerPair.getSecond() + ".class", callerPair.getFirst(), ZipEntry.STORED);
+      }
+      byte[] mainBytes =
+          Dumps.dumpMain(
+              () -> {
+                if (intBox.get() > numberOfClasses) {
+                  return null;
+                }
+                Pair<byte[], String> classData =
+                    isOutlined
+                        ? Dumps.dumpApiCallerOutlined(
+                            intBox.getAndIncrement(),
+                            callerPair.getSecond(),
+                            callerName.getApiCallerMethodName())
+                        : Dumps.dumpApiCallerInlined(
+                            intBox.getAndIncrement(),
+                            callerName.getApiCallerMethodName(),
+                            newApiClassName,
+                            newApiClassMethodName,
+                            existingApiNewMethodName);
+                try {
+                  ZipUtils.writeToZipStream(
+                      out, classData.getSecond() + ".class", classData.getFirst(), ZipEntry.STORED);
+                } catch (IOException exception) {
+                  throw new RuntimeException(exception);
+                }
+                return classData.getSecond();
+              });
+      ZipUtils.writeToZipStream(
+          out, "com/example/softverificationsample/TestRunner.class", mainBytes, ZipEntry.STORED);
+    }
+  }
+
+  public static class Dumps implements Opcodes {
+
+    public static byte[] dumpMain(Supplier<String> targetSupplier) {
+
+      ClassWriter classWriter = new ClassWriter(0);
+      MethodVisitor methodVisitor;
+
+      classWriter.visit(
+          V1_8,
+          ACC_PUBLIC | ACC_SUPER,
+          "com/example/softverificationsample/TestRunner",
+          null,
+          "java/lang/Object",
+          null);
+
+      {
+        methodVisitor =
+            classWriter.visitMethod(
+                ACC_PUBLIC | ACC_STATIC, "run", "(Landroid/content/Context;)V", null, null);
+        methodVisitor.visitCode();
+        String target = targetSupplier.get();
+        while (target != null) {
+          methodVisitor.visitVarInsn(ALOAD, 0);
+          methodVisitor.visitMethodInsn(
+              INVOKESTATIC, target, "callApi", "(Landroid/content/Context;)V", false);
+          target = targetSupplier.get();
+        }
+        methodVisitor.visitInsn(RETURN);
+        methodVisitor.visitMaxs(1, 1);
+        methodVisitor.visitEnd();
+      }
+      classWriter.visitEnd();
+
+      return classWriter.toByteArray();
+    }
+
+    public static Pair<byte[], String> dumpApiCallerOutlined(
+        int index, String apiCallerName, String apiMethodCaller) {
+
+      ClassWriter classWriter = new ClassWriter(0);
+      MethodVisitor methodVisitor;
+
+      String binaryName =
+          "com/example/softverificationsample/ApiCallerOutlined" + (index > -1 ? index : "");
+
+      classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, binaryName, null, "java/lang/Object", null);
+
+      classWriter.visitInnerClass(
+          "android/os/Build$VERSION", "android/os/Build", "VERSION", ACC_PUBLIC | ACC_STATIC);
+
+      {
+        methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
+        methodVisitor.visitCode();
+        methodVisitor.visitVarInsn(ALOAD, 0);
+        methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+        methodVisitor.visitInsn(RETURN);
+        methodVisitor.visitMaxs(1, 1);
+        methodVisitor.visitEnd();
+      }
+      {
+        methodVisitor =
+            classWriter.visitMethod(
+                ACC_PUBLIC | ACC_STATIC, "callApi", "(Landroid/content/Context;)V", null, null);
+        methodVisitor.visitCode();
+        methodVisitor.visitFieldInsn(GETSTATIC, "android/os/Build$VERSION", "SDK_INT", "I");
+        methodVisitor.visitIntInsn(BIPUSH, 26);
+        Label label0 = new Label();
+        methodVisitor.visitJumpInsn(IF_ICMPLT, label0);
+        methodVisitor.visitVarInsn(ALOAD, 0);
+        methodVisitor.visitMethodInsn(
+            INVOKESTATIC, apiCallerName, apiMethodCaller, "(Landroid/content/Context;)V", false);
+        methodVisitor.visitLabel(label0);
+        methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+        methodVisitor.visitInsn(RETURN);
+        methodVisitor.visitMaxs(2, 1);
+        methodVisitor.visitEnd();
+      }
+      classWriter.visitEnd();
+
+      return Pair.create(classWriter.toByteArray(), binaryName);
+    }
+
+    public static Pair<byte[], String> dumpApiCallerInlined(
+        int index,
+        String apiMethodCaller,
+        String newApiClassName,
+        String newApiClassMethodName,
+        String existingApiNewMethodName) {
+
+      ClassWriter classWriter = new ClassWriter(0);
+      MethodVisitor methodVisitor;
+
+      String binaryName =
+          "com/example/softverificationsample/ApiCallerInlined" + (index > -1 ? index : "");
+
+      classWriter.visit(V1_8, ACC_PUBLIC | ACC_SUPER, binaryName, null, "java/lang/Object", null);
+
+      classWriter.visitInnerClass(
+          "android/os/Build$VERSION", "android/os/Build", "VERSION", ACC_PUBLIC | ACC_STATIC);
+
+      {
+        methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
+        methodVisitor.visitCode();
+        methodVisitor.visitVarInsn(ALOAD, 0);
+        methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+        methodVisitor.visitInsn(RETURN);
+        methodVisitor.visitMaxs(1, 1);
+        methodVisitor.visitEnd();
+      }
+      {
+        methodVisitor =
+            classWriter.visitMethod(
+                ACC_PUBLIC | ACC_STATIC, "callApi", "(Landroid/content/Context;)V", null, null);
+        methodVisitor.visitCode();
+        methodVisitor.visitFieldInsn(GETSTATIC, "android/os/Build$VERSION", "SDK_INT", "I");
+        methodVisitor.visitIntInsn(BIPUSH, 26);
+        Label label0 = new Label();
+        methodVisitor.visitJumpInsn(IF_ICMPLT, label0);
+        methodVisitor.visitVarInsn(ALOAD, 0);
+        methodVisitor.visitMethodInsn(
+            INVOKESTATIC, binaryName, apiMethodCaller, "(Landroid/content/Context;)V", false);
+        methodVisitor.visitLabel(label0);
+        methodVisitor.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
+        methodVisitor.visitInsn(RETURN);
+        methodVisitor.visitMaxs(2, 1);
+        methodVisitor.visitEnd();
+      }
+      {
+        methodVisitor =
+            classWriter.visitMethod(
+                ACC_PUBLIC | ACC_STATIC,
+                "constructUnknownObject",
+                "(Landroid/content/Context;)V",
+                null,
+                null);
+        methodVisitor.visitCode();
+        methodVisitor.visitTypeInsn(NEW, newApiClassName);
+        methodVisitor.visitInsn(DUP);
+        methodVisitor.visitLdcInsn("CHANNEL_ID");
+        methodVisitor.visitLdcInsn("FOO");
+        methodVisitor.visitFieldInsn(
+            GETSTATIC, "android/app/NotificationManager", "IMPORTANCE_DEFAULT", "I");
+        methodVisitor.visitMethodInsn(
+            INVOKESPECIAL,
+            newApiClassName,
+            "<init>",
+            "(Ljava/lang/String;Ljava/lang/String;I)V",
+            false);
+        methodVisitor.visitVarInsn(ASTORE, 1);
+        methodVisitor.visitVarInsn(ALOAD, 1);
+        methodVisitor.visitLdcInsn("This is a test channel");
+        methodVisitor.visitMethodInsn(
+            INVOKEVIRTUAL, newApiClassName, newApiClassMethodName, "(Ljava/lang/String;)V", false);
+        methodVisitor.visitInsn(RETURN);
+        methodVisitor.visitMaxs(5, 2);
+        methodVisitor.visitEnd();
+      }
+      {
+        methodVisitor =
+            classWriter.visitMethod(
+                ACC_PUBLIC | ACC_STATIC,
+                "callUnknownMethod",
+                "(Landroid/content/Context;)V",
+                null,
+                null);
+        methodVisitor.visitCode();
+        methodVisitor.visitVarInsn(ALOAD, 0);
+        methodVisitor.visitLdcInsn(Type.getType("Landroid/app/NotificationManager;"));
+        methodVisitor.visitMethodInsn(
+            INVOKEVIRTUAL,
+            "android/content/Context",
+            "getSystemService",
+            "(Ljava/lang/Class;)Ljava/lang/Object;",
+            false);
+        methodVisitor.visitTypeInsn(CHECKCAST, "android/app/NotificationManager");
+        methodVisitor.visitVarInsn(ASTORE, 1);
+        methodVisitor.visitVarInsn(ALOAD, 1);
+        methodVisitor.visitInsn(ACONST_NULL);
+        methodVisitor.visitMethodInsn(
+            INVOKEVIRTUAL,
+            "android/app/NotificationManager",
+            existingApiNewMethodName,
+            "(L" + newApiClassName + ";)V",
+            false);
+        methodVisitor.visitInsn(RETURN);
+        methodVisitor.visitMaxs(2, 2);
+        methodVisitor.visitEnd();
+      }
+      {
+        methodVisitor =
+            classWriter.visitMethod(
+                ACC_PUBLIC | ACC_STATIC,
+                "constructUnknownObjectAndCallUnknownMethod",
+                "(Landroid/content/Context;)V",
+                null,
+                null);
+        methodVisitor.visitCode();
+        methodVisitor.visitTypeInsn(NEW, newApiClassName);
+        methodVisitor.visitInsn(DUP);
+        methodVisitor.visitLdcInsn("CHANNEL_ID");
+        methodVisitor.visitLdcInsn("FOO");
+        methodVisitor.visitFieldInsn(
+            GETSTATIC, "android/app/NotificationManager", "IMPORTANCE_DEFAULT", "I");
+        methodVisitor.visitMethodInsn(
+            INVOKESPECIAL,
+            newApiClassName,
+            "<init>",
+            "(Ljava/lang/String;Ljava/lang/String;I)V",
+            false);
+        methodVisitor.visitVarInsn(ASTORE, 1);
+        methodVisitor.visitVarInsn(ALOAD, 1);
+        methodVisitor.visitLdcInsn("This is a test channel");
+        methodVisitor.visitMethodInsn(
+            INVOKEVIRTUAL, newApiClassName, newApiClassMethodName, "(Ljava/lang/String;)V", false);
+        methodVisitor.visitVarInsn(ALOAD, 0);
+        methodVisitor.visitLdcInsn(Type.getType("Landroid/app/NotificationManager;"));
+        methodVisitor.visitMethodInsn(
+            INVOKEVIRTUAL,
+            "android/content/Context",
+            "getSystemService",
+            "(Ljava/lang/Class;)Ljava/lang/Object;",
+            false);
+        methodVisitor.visitTypeInsn(CHECKCAST, "android/app/NotificationManager");
+        methodVisitor.visitVarInsn(ASTORE, 2);
+        methodVisitor.visitVarInsn(ALOAD, 2);
+        methodVisitor.visitVarInsn(ALOAD, 1);
+        methodVisitor.visitMethodInsn(
+            INVOKEVIRTUAL,
+            "android/app/NotificationManager",
+            existingApiNewMethodName,
+            "(L" + newApiClassName + ";)V",
+            false);
+        methodVisitor.visitInsn(RETURN);
+        methodVisitor.visitMaxs(5, 3);
+        methodVisitor.visitEnd();
+      }
+      classWriter.visitEnd();
+
+      return Pair.create(classWriter.toByteArray(), binaryName);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/SoftVerificationErrorJarRunner.java b/src/test/java/com/android/tools/r8/SoftVerificationErrorJarRunner.java
new file mode 100644
index 0000000..80a2879
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/SoftVerificationErrorJarRunner.java
@@ -0,0 +1,118 @@
+// 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;
+
+import static com.android.tools.r8.SoftVerificationErrorJarGenerator.EXISTING_API_METHOD_NAME;
+import static com.android.tools.r8.SoftVerificationErrorJarGenerator.NEW_API_CLASS_METHOD_NAME;
+import static com.android.tools.r8.SoftVerificationErrorJarGenerator.NEW_API_CLASS_NAME;
+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 com.android.tools.r8.SoftVerificationErrorJarGenerator.ApiCallerName;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.ApkUtils;
+import com.android.tools.r8.utils.BooleanBox;
+import com.android.tools.r8.utils.ZipUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class SoftVerificationErrorJarRunner extends TestBase {
+
+  public static Path DUMP_PATH =
+      Paths.get("third_party", "api-outlining", "simple-app-dump", "simple-app-dump.zip");
+  public static Path APK_PATH =
+      Paths.get("third_party", "api-outlining", "simple-app-dump", "app-release-unsigned.apk");
+
+  private final int numberOfClasses;
+  private final boolean isOutlined;
+
+  public SoftVerificationErrorJarRunner(int numberOfClasses, boolean isOutlined) {
+    this.numberOfClasses = numberOfClasses;
+    this.isOutlined = isOutlined;
+  }
+
+  public static void main(String[] args) throws Exception {
+    new SoftVerificationErrorJarRunner(1000, false).runTest();
+  }
+
+  public void runTest() throws Exception {
+
+    temp.create();
+
+    Path tempFolder = temp.newFolder().toPath();
+    Path outlineJar = tempFolder.resolve("outlined.jar");
+
+    SoftVerificationErrorJarGenerator.createJar(
+        outlineJar,
+        numberOfClasses,
+        isOutlined,
+        ApiCallerName.CONSTRUCT_AND_CALL_UNKNOWN,
+        NEW_API_CLASS_NAME,
+        NEW_API_CLASS_METHOD_NAME,
+        EXISTING_API_METHOD_NAME);
+
+    ZipUtils.unzip(DUMP_PATH.toString(), tempFolder.toFile());
+
+    File filteredProgramFolder = temp.newFolder();
+    BooleanBox seenTestRunner = new BooleanBox();
+    ZipUtils.unzip(
+        tempFolder.resolve("program.jar").toFile().toString(),
+        filteredProgramFolder,
+        zipEntry -> {
+          if (zipEntry.getName().equals("com/example/softverificationsample/TestRunner.class")) {
+            seenTestRunner.set();
+            return false;
+          }
+          return true;
+        });
+
+    assertTrue(seenTestRunner.get());
+
+    Path filteredProgramJar = tempFolder.resolve("filtered_program.jar");
+    ZipUtils.zip(filteredProgramJar, filteredProgramFolder.toPath());
+
+    // Build the app with R8.
+    Path output =
+        testForR8(Backend.DEX)
+            .addProgramFiles(outlineJar, filteredProgramJar)
+            .addClasspathFiles(tempFolder.resolve("classpath.jar"))
+            .addLibraryFiles(tempFolder.resolve("library.jar"))
+            // TODO(b/187496508): Modify keep rules to allow inlining and keep test code.
+            .addKeepRuleFiles(tempFolder.resolve("proguard.config"))
+            .addKeepRules("-keep class com.example.softverificationsample.* { *; }")
+            .setMinApi(AndroidApiLevel.M)
+            .allowUnusedProguardConfigurationRules()
+            .allowUnusedDontWarnPatterns()
+            .allowDiagnosticInfoMessages()
+            .compile()
+            .inspect(this::inspect)
+            .writeToZip();
+
+    Path finalApk = tempFolder.resolve("app-release-final.apk");
+    ProcessResult processResult = ApkUtils.apkMasseur(APK_PATH, output, finalApk);
+    // TODO(mkroghj): Figure out to have this command succeed when installing the apk
+    assertEquals(0, processResult.exitCode);
+  }
+
+  private void inspect(CodeInspector inspector) {
+    String name =
+        "com.example.softverificationsample."
+            + (isOutlined ? "ApiCallerOutlined" : "ApiCallerInlined")
+            + (numberOfClasses - 1);
+    ClassSubject clazz = inspector.clazz(name);
+    assertThat(clazz, isPresent());
+    if (isOutlined) {
+      ClassSubject apiCallerInlined =
+          inspector.clazz("com.example.softverificationsample.ApiCallerInlined");
+      assertThat(apiCallerInlined, isPresent());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index a6895ce..b24d16a 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -1715,8 +1715,16 @@
     return clazz.getTypeName();
   }
 
-  public static String examplesTypeName(Class<? extends ExamplesClass> clazz) throws Exception {
-    return ReflectiveBuildPathUtils.resolveClassName(clazz);
+  public static ClassReference examplesClassReference(Class<? extends ExamplesClass> clazz) {
+    return Reference.classFromTypeName(examplesTypeName(clazz));
+  }
+
+  public static String examplesTypeName(Class<? extends ExamplesClass> clazz) {
+    try {
+      return ReflectiveBuildPathUtils.resolveClassName(clazz);
+    } catch (Exception e) {
+      throw new RuntimeException(e);
+    }
   }
 
   public static AndroidApiLevel apiLevelWithDefaultInterfaceMethodsSupport() {
@@ -1746,6 +1754,10 @@
     return AndroidApiLevel.K;
   }
 
+  public static AndroidApiLevel apiLevelWithPcAsLineNumberSupport() {
+    return AndroidApiLevel.O;
+  }
+
   public static boolean canUseJavaUtilObjects(TestParameters parameters) {
     return parameters.isCfRuntime()
         || parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.K);
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index 121e7c4..3c5b479 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -10,7 +10,6 @@
 
 import com.android.tools.r8.TestBase.Backend;
 import com.android.tools.r8.debug.DebugTestConfig;
-import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase.KeepRuleConsumer;
 import com.android.tools.r8.testing.AndroidBuildVersion;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
@@ -48,6 +47,7 @@
 
   public static final Consumer<InternalOptions> DEFAULT_OPTIONS =
       options -> {
+        options.testing.enableTestAssertions = true;
         options.testing.allowUnusedDontWarnRules = false;
         options.testing.allowUnnecessaryDontWarnWildcards = false;
         options.testing.reportUnusedProguardConfigurationRules = true;
@@ -438,7 +438,7 @@
   }
 
   public T enableCoreLibraryDesugaring(
-      AndroidApiLevel minApiLevel, KeepRuleConsumer keepRuleConsumer) {
+      AndroidApiLevel minApiLevel, StringConsumer keepRuleConsumer) {
     return enableCoreLibraryDesugaring(
         minApiLevel,
         keepRuleConsumer,
diff --git a/src/test/java/com/android/tools/r8/TestParameters.java b/src/test/java/com/android/tools/r8/TestParameters.java
index 5a7423a..1df89b1 100644
--- a/src/test/java/com/android/tools/r8/TestParameters.java
+++ b/src/test/java/com/android/tools/r8/TestParameters.java
@@ -43,6 +43,11 @@
         .isGreaterThanOrEqualTo(TestBase.apiLevelWithDefaultInterfaceMethodsSupport());
   }
 
+  public boolean canUseNestBasedAccesses() {
+    assert isCfRuntime() || isDexRuntime();
+    return isCfRuntime() && getRuntime().asCf().isNewerThanOrEqual(CfVm.JDK11);
+  }
+
   // Convenience predicates.
   public boolean isDexRuntime() {
     return runtime.isDex();
diff --git a/src/test/java/com/android/tools/r8/TestingAssertionsTest.java b/src/test/java/com/android/tools/r8/TestingAssertionsTest.java
new file mode 100644
index 0000000..b2b64ec
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/TestingAssertionsTest.java
@@ -0,0 +1,65 @@
+// 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;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticException;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.utils.AndroidApiLevel;
+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 TestingAssertionsTest extends TestBase {
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public TestingAssertionsTest(TestParameters parameters) {}
+
+  @Test(expected = CompilationFailedException.class)
+  public void testR8() throws Exception {
+    testForR8(Backend.DEX)
+        .addInnerClasses(getClass())
+        .setMinApi(AndroidApiLevel.B)
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options -> {
+              assertTrue(options.testing.enableTestAssertions);
+              options.testing.testEnableTestAssertions = true;
+            })
+        .compileWithExpectedDiagnostics(
+            diagnostics -> {
+              diagnostics.assertErrorThatMatches(diagnosticException(AssertionError.class));
+            });
+  }
+
+  @Test(expected = CompilationFailedException.class)
+  public void testD8() throws Exception {
+    testForD8(Backend.DEX)
+        .addInnerClasses(getClass())
+        .setMinApi(AndroidApiLevel.B)
+        .addOptionsModification(
+            options -> {
+              assertTrue(options.testing.enableTestAssertions);
+              options.testing.testEnableTestAssertions = true;
+            })
+        .compileWithExpectedDiagnostics(
+            diagnostics -> {
+              diagnostics.assertErrorThatMatches(diagnosticException(AssertionError.class));
+            });
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      System.out.println("Hello World");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/apioutlining/ApiOutliningNoInliningOfHigherApiLevelIntoLowerTest.java b/src/test/java/com/android/tools/r8/apioutlining/ApiOutliningNoInliningOfHigherApiLevelIntoLowerTest.java
new file mode 100644
index 0000000..7620ae4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apioutlining/ApiOutliningNoInliningOfHigherApiLevelIntoLowerTest.java
@@ -0,0 +1,87 @@
+// 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
new file mode 100644
index 0000000..44263ba
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apioutlining/ApiOutliningNoInliningOfHigherApiLevelTest.java
@@ -0,0 +1,86 @@
+// 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/apioutlining/ApiOutliningTestHelper.java b/src/test/java/com/android/tools/r8/apioutlining/ApiOutliningTestHelper.java
new file mode 100644
index 0000000..f10a6e9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/apioutlining/ApiOutliningTestHelper.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.apioutlining;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestCompilerBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ThrowableConsumer;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.ThrowingConsumer;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.CodeMatchers;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.lang.reflect.Method;
+
+public abstract class ApiOutliningTestHelper {
+
+  static <T extends TestCompilerBuilder<?, ?, ?, ?, ?>>
+      ThrowableConsumer<T> setMockApiLevelForMethod(Method method, AndroidApiLevel apiLevel) {
+    return compilerBuilder -> {
+      compilerBuilder.addOptionsModification(
+          options -> {
+            options.methodApiMapping.put(Reference.methodFromMethod(method), apiLevel);
+          });
+    };
+  }
+
+  static ApiOutliningMethodVerificationHelper verifyThat(TestParameters parameters, Method method) {
+    return new ApiOutliningMethodVerificationHelper(parameters, method);
+  }
+
+  public static class ApiOutliningMethodVerificationHelper {
+
+    private final Method methodOfInterest;
+    private final TestParameters parameters;
+
+    public ApiOutliningMethodVerificationHelper(
+        TestParameters parameters, Method methodOfInterest) {
+      this.methodOfInterest = methodOfInterest;
+      this.parameters = parameters;
+    }
+
+    protected ThrowingConsumer<CodeInspector, Exception> inlinedIntoFromApiLevel(
+        Method method, AndroidApiLevel apiLevel) {
+      return parameters.getApiLevel().isGreaterThanOrEqualTo(apiLevel)
+          ? inlinedInto(method)
+          : notInlinedInto(method);
+    }
+
+    private ThrowingConsumer<CodeInspector, Exception> notInlinedInto(Method method) {
+      return inspector -> {
+        MethodSubject candidate = inspector.method(methodOfInterest);
+        assertThat(candidate, isPresent());
+        MethodSubject target = inspector.method(method);
+        assertThat(target, isPresent());
+        assertThat(target, CodeMatchers.invokesMethod(candidate));
+      };
+    }
+
+    private ThrowingConsumer<CodeInspector, Exception> inlinedInto(Method method) {
+      return inspector -> {
+        MethodSubject candidate = inspector.method(methodOfInterest);
+        if (!candidate.isPresent()) {
+          return;
+        }
+        MethodSubject target = inspector.method(method);
+        assertThat(target, isPresent());
+        assertThat(target, not(CodeMatchers.invokesMethod(candidate)));
+      };
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorForwardingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorForwardingTest.java
index 5484fd8..817c786 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorForwardingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorForwardingTest.java
@@ -30,6 +30,8 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
+        .addHorizontallyMergedClassesInspector(
+            inspector -> inspector.assertIsCompleteMergeGroup(A.class, B.class))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
@@ -37,27 +39,25 @@
         .assertSuccessWithOutputLines("42", "13", "21", "39", "print a", "print b")
         .inspect(
             codeInspector -> {
-                ClassSubject aClassSubject = codeInspector.clazz(A.class);
-                assertThat(aClassSubject, isPresent());
-                FieldSubject classIdFieldSubject =
-                    aClassSubject.uniqueFieldWithName(ClassMerger.CLASS_ID_FIELD_NAME);
-                assertThat(classIdFieldSubject, isPresent());
+              ClassSubject aClassSubject = codeInspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              FieldSubject classIdFieldSubject =
+                  aClassSubject.uniqueFieldWithName(ClassMerger.CLASS_ID_FIELD_NAME);
+              assertThat(classIdFieldSubject, isPresent());
 
-                MethodSubject firstInitSubject = aClassSubject.init("int");
-                assertThat(firstInitSubject, isPresent());
-                assertThat(
-                    firstInitSubject, writesInstanceField(classIdFieldSubject.getDexField()));
+              MethodSubject firstInitSubject = aClassSubject.init("int");
+              assertThat(firstInitSubject, isPresent());
+              assertThat(firstInitSubject, writesInstanceField(classIdFieldSubject.getDexField()));
 
-                MethodSubject otherInitSubject = aClassSubject.init("long", "int");
-                assertThat(otherInitSubject, isPresent());
-                assertThat(
-                    otherInitSubject, writesInstanceField(classIdFieldSubject.getDexField()));
+              MethodSubject otherInitSubject = aClassSubject.init("long", "int");
+              assertThat(otherInitSubject, isPresent());
+              assertThat(otherInitSubject, writesInstanceField(classIdFieldSubject.getDexField()));
 
-                MethodSubject printSubject = aClassSubject.method("void", "print$bridge");
-                assertThat(printSubject, isPresent());
-                assertThat(printSubject, readsInstanceField(classIdFieldSubject.getDexField()));
+              MethodSubject printSubject = aClassSubject.method("void", "print$bridge");
+              assertThat(printSubject, isPresent());
+              assertThat(printSubject, readsInstanceField(classIdFieldSubject.getDexField()));
 
-                assertThat(codeInspector.clazz(B.class), not(isPresent()));
+              assertThat(codeInspector.clazz(B.class), not(isPresent()));
             });
   }
 
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
new file mode 100644
index 0000000..bf50ab1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/NestClassMergingTestRunner.java
@@ -0,0 +1,269 @@
+// 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.classmerging.horizontal;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
+import com.android.tools.r8.ThrowableConsumer;
+import com.android.tools.r8.classmerging.horizontal.NestClassMergingTestRunner.R.horizontalclassmerging.NestClassMergingTest;
+import com.android.tools.r8.classmerging.horizontal.NestClassMergingTestRunner.R.horizontalclassmerging.NestHostA;
+import com.android.tools.r8.classmerging.horizontal.NestClassMergingTestRunner.R.horizontalclassmerging.NestHostB;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.references.ClassReference;
+import com.android.tools.r8.utils.ReflectiveBuildPathUtils.ExamplesClass;
+import com.android.tools.r8.utils.ReflectiveBuildPathUtils.ExamplesJava11RootPackage;
+import com.android.tools.r8.utils.ReflectiveBuildPathUtils.ExamplesPackage;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Streams;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runners.Parameterized;
+
+public class NestClassMergingTestRunner extends HorizontalClassMergingTestBase {
+
+  public static class R extends ExamplesJava11RootPackage {
+    public static class horizontalclassmerging extends ExamplesPackage {
+      public static class NestClassMergingTest extends ExamplesClass {}
+
+      public static class NestHostA extends ExamplesClass {
+        public static class NestMemberA extends ExamplesClass {}
+
+        public static class NestMemberB extends ExamplesClass {}
+      }
+
+      public static class NestHostB extends ExamplesClass {
+        public static class NestMemberA extends ExamplesClass {}
+
+        public static class NestMemberB extends ExamplesClass {}
+      }
+    }
+  }
+
+  public NestClassMergingTestRunner(TestParameters parameters) {
+    super(parameters);
+  }
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withCfRuntimesStartingFromIncluding(CfVm.JDK11)
+        .withDexRuntimes()
+        .withAllApiLevels()
+        .build();
+  }
+
+  @Test
+  public void test() throws Exception {
+    runTest(
+        builder ->
+            builder.addHorizontallyMergedClassesInspector(
+                inspector -> {
+                  if (parameters.canUseNestBasedAccesses()) {
+                    inspector
+                        .assertIsCompleteMergeGroup(
+                            classRef(NestHostA.class),
+                            classRef(NestHostA.NestMemberA.class),
+                            classRef(NestHostA.NestMemberB.class))
+                        .assertIsCompleteMergeGroup(
+                            classRef(NestHostB.class),
+                            classRef(NestHostB.NestMemberA.class),
+                            classRef(NestHostB.NestMemberB.class));
+                  } else {
+                    inspector.assertIsCompleteMergeGroup(
+                        classRef(NestHostA.class),
+                        classRef(NestHostA.NestMemberA.class),
+                        classRef(NestHostA.NestMemberB.class),
+                        classRef(NestHostB.class),
+                        classRef(NestHostB.NestMemberA.class),
+                        classRef(NestHostB.NestMemberB.class));
+                  }
+                }));
+  }
+
+  @Test
+  public void testMergeHostIntoNestMemberA() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    runTest(
+        builder ->
+            builder
+                .addHorizontallyMergedClassesInspector(
+                    inspector ->
+                        inspector
+                            .assertIsCompleteMergeGroup(
+                                classRef(NestHostA.class), classRef(NestHostA.NestMemberA.class))
+                            .assertIsCompleteMergeGroup(
+                                classRef(NestHostB.class), classRef(NestHostB.NestMemberA.class))
+                            .assertClassReferencesNotMerged(
+                                classRef(NestHostA.NestMemberB.class),
+                                classRef(NestHostB.NestMemberB.class)))
+                .addNoHorizontalClassMergingRule(
+                    examplesTypeName(NestHostA.NestMemberB.class),
+                    examplesTypeName(NestHostB.NestMemberB.class))
+                .addOptionsModification(
+                    options -> {
+                      options.testing.horizontalClassMergingTarget =
+                          (canditates, target) -> {
+                            Set<ClassReference> candidateClassReferences =
+                                Streams.stream(canditates)
+                                    .map(DexClass::getClassReference)
+                                    .collect(Collectors.toSet());
+                            if (candidateClassReferences.contains(classRef(NestHostA.class))) {
+                              assertEquals(
+                                  ImmutableSet.of(
+                                      classRef(NestHostA.class),
+                                      classRef(NestHostA.NestMemberA.class)),
+                                  candidateClassReferences);
+                            } else {
+                              assertEquals(
+                                  ImmutableSet.of(
+                                      classRef(NestHostB.class),
+                                      classRef(NestHostB.NestMemberA.class)),
+                                  candidateClassReferences);
+                            }
+                            return Iterables.find(
+                                canditates,
+                                candidate -> {
+                                  ClassReference classReference = candidate.getClassReference();
+                                  return classReference.equals(
+                                          classRef(NestHostA.NestMemberA.class))
+                                      || classReference.equals(
+                                          classRef(NestHostB.NestMemberA.class));
+                                });
+                          };
+                    }));
+  }
+
+  @Test
+  public void testMergeHostIntoNestMemberB() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    runTest(
+        builder ->
+            builder
+                .addHorizontallyMergedClassesInspector(
+                    inspector ->
+                        inspector
+                            .assertIsCompleteMergeGroup(
+                                classRef(NestHostA.class), classRef(NestHostA.NestMemberB.class))
+                            .assertIsCompleteMergeGroup(
+                                classRef(NestHostB.class), classRef(NestHostB.NestMemberB.class))
+                            .assertClassReferencesNotMerged(
+                                classRef(NestHostA.NestMemberA.class),
+                                classRef(NestHostB.NestMemberA.class)))
+                .addNoHorizontalClassMergingRule(
+                    examplesTypeName(NestHostA.NestMemberA.class),
+                    examplesTypeName(NestHostB.NestMemberA.class))
+                .addOptionsModification(
+                    options -> {
+                      options.testing.horizontalClassMergingTarget =
+                          (canditates, target) -> {
+                            Set<ClassReference> candidateClassReferences =
+                                Streams.stream(canditates)
+                                    .map(DexClass::getClassReference)
+                                    .collect(Collectors.toSet());
+                            if (candidateClassReferences.contains(classRef(NestHostA.class))) {
+                              assertEquals(
+                                  ImmutableSet.of(
+                                      classRef(NestHostA.class),
+                                      classRef(NestHostA.NestMemberB.class)),
+                                  candidateClassReferences);
+                            } else {
+                              assertEquals(
+                                  ImmutableSet.of(
+                                      classRef(NestHostB.class),
+                                      classRef(NestHostB.NestMemberB.class)),
+                                  candidateClassReferences);
+                            }
+                            return Iterables.find(
+                                canditates,
+                                candidate -> {
+                                  ClassReference classReference = candidate.getClassReference();
+                                  return classReference.equals(
+                                          classRef(NestHostA.NestMemberB.class))
+                                      || classReference.equals(
+                                          classRef(NestHostB.NestMemberB.class));
+                                });
+                          };
+                    }));
+  }
+
+  @Test
+  public void testMergeMemberAIntoNestHost() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    runTest(
+        builder ->
+            builder
+                .addHorizontallyMergedClassesInspector(
+                    inspector ->
+                        inspector
+                            .assertIsCompleteMergeGroup(
+                                classRef(NestHostA.class), classRef(NestHostA.NestMemberA.class))
+                            .assertIsCompleteMergeGroup(
+                                classRef(NestHostB.class), classRef(NestHostB.NestMemberA.class))
+                            .assertClassReferencesNotMerged(
+                                classRef(NestHostA.NestMemberB.class),
+                                classRef(NestHostB.NestMemberB.class)))
+                .addNoHorizontalClassMergingRule(
+                    examplesTypeName(NestHostA.NestMemberB.class),
+                    examplesTypeName(NestHostB.NestMemberB.class))
+                .addOptionsModification(
+                    options -> {
+                      options.testing.horizontalClassMergingTarget =
+                          (canditates, target) -> {
+                            Set<ClassReference> candidateClassReferences =
+                                Streams.stream(canditates)
+                                    .map(DexClass::getClassReference)
+                                    .collect(Collectors.toSet());
+                            if (candidateClassReferences.contains(classRef(NestHostA.class))) {
+                              assertEquals(
+                                  ImmutableSet.of(
+                                      classRef(NestHostA.class),
+                                      classRef(NestHostA.NestMemberA.class)),
+                                  candidateClassReferences);
+                            } else {
+                              assertEquals(
+                                  ImmutableSet.of(
+                                      classRef(NestHostB.class),
+                                      classRef(NestHostB.NestMemberA.class)),
+                                  candidateClassReferences);
+                            }
+                            return Iterables.find(
+                                canditates,
+                                candidate -> {
+                                  ClassReference classReference = candidate.getClassReference();
+                                  return classReference.equals(classRef(NestHostA.class))
+                                      || classReference.equals(classRef(NestHostB.class));
+                                });
+                          };
+                    }));
+  }
+
+  private void runTest(ThrowableConsumer<R8FullTestBuilder> configuration) throws Exception {
+    testForR8(parameters.getBackend())
+        .addKeepMainRule(examplesTypeName(NestClassMergingTest.class))
+        .addExamplesProgramFiles(R.class)
+        .apply(configuration)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), examplesTypeName(NestClassMergingTest.class))
+        .assertSuccessWithOutputLines(
+            "NestHostA$NestMemberA",
+            "NestHostA$NestMemberB",
+            "NestHostB$NestMemberA",
+            "NestHostB$NestMemberB");
+  }
+
+  private static ClassReference classRef(Class<? extends ExamplesClass> clazz) {
+    return examplesClassReference(clazz);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/NestClassTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/NestClassTest.java
deleted file mode 100644
index 215ce7a..0000000
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/NestClassTest.java
+++ /dev/null
@@ -1,104 +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.classmerging.horizontal;
-
-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.isPrivate;
-import static com.android.tools.r8.utils.codeinspector.Matchers.isStatic;
-import static org.hamcrest.MatcherAssert.assertThat;
-
-import com.android.tools.r8.Jdk9TestUtils;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.TestRuntime.CfVm;
-import com.android.tools.r8.classmerging.horizontal.NestClassTest.R.horizontalclassmerging.BasicNestHostHorizontalClassMerging;
-import com.android.tools.r8.classmerging.horizontal.NestClassTest.R.horizontalclassmerging.BasicNestHostHorizontalClassMerging2;
-import com.android.tools.r8.utils.ReflectiveBuildPathUtils.ExamplesClass;
-import com.android.tools.r8.utils.ReflectiveBuildPathUtils.ExamplesJava11RootPackage;
-import com.android.tools.r8.utils.ReflectiveBuildPathUtils.ExamplesPackage;
-import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.MethodSubject;
-import org.junit.Test;
-import org.junit.runners.Parameterized;
-
-public class NestClassTest extends HorizontalClassMergingTestBase {
-  public static class R extends ExamplesJava11RootPackage {
-    public static class horizontalclassmerging extends ExamplesPackage {
-      public static class BasicNestHostHorizontalClassMerging extends ExamplesClass {
-        public static class A extends ExamplesClass {}
-
-        public static class B extends ExamplesClass {}
-      }
-
-      public static class BasicNestHostHorizontalClassMerging2 extends ExamplesClass {
-        public static class A extends ExamplesClass {}
-
-        public static class B extends ExamplesClass {}
-      }
-    }
-  }
-
-  public NestClassTest(TestParameters parameters) {
-    super(parameters);
-  }
-
-  @Parameterized.Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withCfRuntimesStartingFromIncluding(CfVm.JDK11).build();
-  }
-
-  @Test
-  public void testR8() throws Exception {
-    testForR8(parameters.getBackend())
-        .addKeepMainRule(examplesTypeName(BasicNestHostHorizontalClassMerging.class))
-        .addExamplesProgramFiles(R.class)
-        .applyIf(parameters.isCfRuntime(), Jdk9TestUtils.addJdk9LibraryFiles(temp))
-        .enableInliningAnnotations()
-        .enableNeverClassInliningAnnotations()
-        .compile()
-        .run(parameters.getRuntime(), examplesTypeName(BasicNestHostHorizontalClassMerging.class))
-        .assertSuccessWithOutputLines("1: a", "1: b", "2: a", "2: b")
-        .inspect(
-            codeInspector -> {
-              ClassSubject class1A =
-                  codeInspector.clazz(
-                      examplesTypeName(BasicNestHostHorizontalClassMerging.A.class));
-              ClassSubject class2A =
-                  codeInspector.clazz(
-                      examplesTypeName(BasicNestHostHorizontalClassMerging2.A.class));
-              ClassSubject class1 =
-                  codeInspector.clazz(examplesTypeName(BasicNestHostHorizontalClassMerging.class));
-              ClassSubject class2 =
-                  codeInspector.clazz(examplesTypeName(BasicNestHostHorizontalClassMerging2.class));
-              assertThat(class1, isPresent());
-              assertThat(class2, isPresent());
-              assertThat(class1A, isPresent());
-              assertThat(class2A, isPresent());
-
-              MethodSubject printClass1MethodSubject =
-                  class1.method("void", "print", String.class.getTypeName());
-              assertThat(printClass1MethodSubject, isPresent());
-              assertThat(printClass1MethodSubject, isPrivate());
-
-              MethodSubject printClass2MethodSubject =
-                  class2.method("void", "print", String.class.getTypeName());
-              assertThat(printClass2MethodSubject, isPresent());
-              assertThat(printClass2MethodSubject, isPrivate());
-              assertThat(printClass2MethodSubject, isStatic());
-
-              assertThat(
-                  codeInspector.clazz(
-                      examplesTypeName(BasicNestHostHorizontalClassMerging.B.class)),
-                  isAbsent());
-              assertThat(
-                  codeInspector.clazz(
-                      examplesTypeName(BasicNestHostHorizontalClassMerging2.B.class)),
-                  isAbsent());
-
-              // TODO(b/165517236): Explicitly check 1.B is merged into 1.A, and 2.B into 2.A.
-            });
-  }
-}
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 d74ae0a..580598b 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
@@ -12,7 +12,7 @@
 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 com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import org.junit.Test;
 
 public class OverrideDefaultMethodTest extends HorizontalClassMergingTestBase {
@@ -30,7 +30,17 @@
         .enableNoVerticalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
         .addHorizontallyMergedClassesInspector(
-            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+            inspector -> {
+              if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
+                inspector.assertNoClassesMerged();
+              } else {
+                inspector
+                    .assertClassesNotMerged(A.class, B.class)
+                    .assertIsCompleteMergeGroup(
+                        SyntheticItemsTestUtils.syntheticCompanionClass(I.class),
+                        SyntheticItemsTestUtils.syntheticCompanionClass(J.class));
+              }
+            })
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("I", "B", "J")
         .inspect(
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 95dc561..65ca772 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
@@ -13,7 +13,11 @@
 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 com.android.tools.r8.classmerging.horizontal.dispatch.OverrideDefaultMethodTest.A;
+import com.android.tools.r8.classmerging.horizontal.dispatch.OverrideDefaultMethodTest.B;
+import com.android.tools.r8.classmerging.horizontal.dispatch.OverrideDefaultMethodTest.I;
+import com.android.tools.r8.classmerging.horizontal.dispatch.OverrideDefaultMethodTest.J;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import org.junit.Test;
 
 public class OverrideDefaultOnSuperMethodTest extends HorizontalClassMergingTestBase {
@@ -32,7 +36,17 @@
         .enableNoVerticalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
         .addHorizontallyMergedClassesInspector(
-            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+            inspector -> {
+              if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
+                inspector.assertNoClassesMerged();
+              } else {
+                inspector
+                    .assertClassesNotMerged(A.class, B.class)
+                    .assertIsCompleteMergeGroup(
+                        SyntheticItemsTestUtils.syntheticCompanionClass(I.class),
+                        SyntheticItemsTestUtils.syntheticCompanionClass(J.class));
+              }
+            })
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("I", "B", "J")
         .inspect(
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
new file mode 100644
index 0000000..650d804
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/interfaces/ClassHierarchyCycleAfterMergingTest.java
@@ -0,0 +1,84 @@
+// 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 static org.junit.Assert.assertFalse;
+
+import com.android.tools.r8.NoHorizontalClassMerging;
+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 com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ClassHierarchyCycleAfterMergingTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public ClassHierarchyCycleAfterMergingTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        // I and J are not eligible for merging since that would lead to a cycle in the class
+        // hierarchy.
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+        .addOptionsModification(
+            options -> {
+              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
+              options.horizontalClassMergerOptions().enableInterfaceMerging();
+            })
+        .enableNoHorizontalClassMergingAnnotations()
+        .enableNoUnusedInterfaceRemovalAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject mainClassSubject = inspector.clazz(Main.class);
+              assertThat(mainClassSubject, isPresent());
+              assertThat(mainClassSubject, isImplementing(inspector.clazz(K.class)));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  static class Main implements K {
+
+    public static void main(String[] args) {}
+  }
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface I {}
+
+  @NoHorizontalClassMerging
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface J extends I {}
+
+  @NoUnusedInterfaceRemoval
+  @NoVerticalClassMerging
+  interface K extends J {}
+}
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 862c386..1c5d915 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,6 +7,7 @@
 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;
@@ -36,6 +37,7 @@
     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())
@@ -45,11 +47,25 @@
         // contributes the default method K.m() to A, and the merging of J into I would contribute
         // the default method J.m() to A.
         .addHorizontallyMergedClassesInspector(
-            inspector ->
+            inspector -> {
+              if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
                 inspector
                     .assertIsCompleteMergeGroup(A.class, B.class)
                     .assertMergedInto(B.class, A.class)
-                    .assertClassesNotMerged(I.class, J.class, K.class))
+                    .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();
+            })
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
@@ -66,7 +82,13 @@
 
               ClassSubject bClassSubject = inspector.clazz(C.class);
               assertThat(bClassSubject, isPresent());
-              assertThat(bClassSubject, isImplementing(inspector.clazz(J.class)));
+              assertThat(
+                  bClassSubject,
+                  isImplementing(
+                      inspector.clazz(
+                          parameters.canUseDefaultAndStaticInterfaceMethods()
+                              ? J.class
+                              : I.class)));
             })
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("A", "K", "J");
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 538f7fe..d4f14bd 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,6 +7,7 @@
 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;
@@ -17,7 +18,6 @@
 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.HorizontallyMergedClassesInspector;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -44,7 +44,18 @@
         // I and J are not eligible for merging, since class A (implements I) inherits a default m()
         // method from K, which is also on J.
         .addHorizontallyMergedClassesInspector(
-            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+            inspector -> {
+              if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
+                inspector.assertNoClassesMerged();
+              } else {
+                inspector.assertIsCompleteMergeGroup(I.class, J.class);
+              }
+            })
+        .addOptionsModification(
+            options -> {
+              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
+              options.horizontalClassMergerOptions().enableInterfaceMerging();
+            })
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
@@ -61,7 +72,13 @@
 
               ClassSubject bClassSubject = inspector.clazz(B.class);
               assertThat(bClassSubject, isPresent());
-              assertThat(bClassSubject, isImplementing(inspector.clazz(J.class)));
+              assertThat(
+                  bClassSubject,
+                  isImplementing(
+                      inspector.clazz(
+                          parameters.canUseDefaultAndStaticInterfaceMethods()
+                              ? J.class
+                              : I.class)));
             })
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("K", "J");
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 6fcd094..f9e85ba 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,6 +7,7 @@
 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 +17,8 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfVm;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -44,7 +45,19 @@
         // I and J are not eligible for merging, since the lambda that implements I & J inherits a
         // default m() method from K, which is also on J.
         .addHorizontallyMergedClassesInspector(
-            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+            inspector -> {
+              if (parameters.isCfRuntime()) {
+                inspector.assertIsCompleteMergeGroup(I.class, J.class);
+              } else {
+                inspector.assertNoClassesMerged();
+              }
+            })
+        .addOptionsModification(
+            options -> {
+              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
+              options.horizontalClassMergerOptions().enableInterfaceMerging();
+              options.horizontalClassMergerOptions().setIgnoreRuntimeTypeChecksForTesting();
+            })
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
@@ -56,10 +69,22 @@
             inspector -> {
               ClassSubject aClassSubject = inspector.clazz(A.class);
               assertThat(aClassSubject, isPresent());
-              assertThat(aClassSubject, isImplementing(inspector.clazz(J.class)));
+              if (parameters.isCfRuntime()) {
+                assertThat(aClassSubject, isImplementing(inspector.clazz(I.class)));
+              } else {
+                assertThat(aClassSubject, isImplementing(inspector.clazz(J.class)));
+              }
             })
         .run(parameters.getRuntime(), Main.class)
-        .assertSuccessWithOutputLines("K", "J");
+        // TODO(b/173990042): Should succeed with "K", "J".
+        .applyIf(
+            parameters.isCfRuntime(),
+            builder ->
+                builder.assertFailureWithErrorThatThrows(
+                    parameters.isCfRuntime(CfVm.JDK11)
+                        ? AbstractMethodError.class
+                        : IncompatibleClassChangeError.class),
+            builder -> builder.assertSuccessWithOutputLines("K", "J"));
   }
 
   static class Main {
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 7c647b4..7ca9cc5 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,6 +4,8 @@
 
 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;
@@ -33,9 +35,17 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addHorizontallyMergedClassesInspector(
-            inspector -> inspector.assertClassesNotMerged(I.class, J.class))
+            inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
+        .addOptionsModification(
+            options -> {
+              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
+              options.horizontalClassMergerOptions().enableInterfaceMerging();
+            })
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
+        .noClassInliningOfSynthetics()
+        .noHorizontalClassMergingOfSynthetics()
+        .noInliningOfSynthetics()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .run(parameters.getRuntime(), Main.class)
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 31aac13..dad75a4 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,6 +4,8 @@
 
 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;
@@ -32,11 +34,18 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
-        // TODO(b/173990042): We should be able to merge I and J.
         .addHorizontallyMergedClassesInspector(
-            inspector -> inspector.assertClassesNotMerged(I.class, J.class))
+            inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
+        .addOptionsModification(
+            options -> {
+              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
+              options.horizontalClassMergerOptions().enableInterfaceMerging();
+            })
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
+        .noClassInliningOfSynthetics()
+        .noHorizontalClassMergingOfSynthetics()
+        .noInliningOfSynthetics()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .run(parameters.getRuntime(), Main.class)
@@ -46,7 +55,7 @@
   static class Main {
 
     public static void main(String[] args) {
-      Object o = (I & J) () -> "I & J";
+      Object o = (I & J) () -> System.currentTimeMillis() > 0 ? "I & J" : null;
       System.out.println(((I) o).f());
       System.out.println(((J) o).f());
     }
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 683ce85..e58ca16 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,6 +4,8 @@
 
 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;
@@ -34,11 +36,18 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
-        // TODO(b/173990042): We should be able to merge I and J.
         .addHorizontallyMergedClassesInspector(
-            inspector -> inspector.assertClassesNotMerged(I.class, J.class))
+            inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
+        .addOptionsModification(
+            options -> {
+              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
+              options.horizontalClassMergerOptions().enableInterfaceMerging();
+            })
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
+        .noClassInliningOfSynthetics()
+        .noHorizontalClassMergingOfSynthetics()
+        .noInliningOfSynthetics()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .run(parameters.getRuntime(), Main.class)
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 8cb2212..a877121 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,6 +4,8 @@
 
 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;
@@ -34,11 +36,18 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
-        // TODO(b/173990042): We should be able to merge I and J.
         .addHorizontallyMergedClassesInspector(
-            inspector -> inspector.assertClassesNotMerged(I.class, J.class))
+            inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
+        .addOptionsModification(
+            options -> {
+              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
+              options.horizontalClassMergerOptions().enableInterfaceMerging();
+            })
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
+        .noClassInliningOfSynthetics()
+        .noHorizontalClassMergingOfSynthetics()
+        .noInliningOfSynthetics()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .run(parameters.getRuntime(), Main.class)
@@ -48,8 +57,8 @@
   static class Main {
 
     public static void main(String[] args) {
-      System.out.println(((I) () -> "I").f());
-      System.out.println(((J) () -> "J").f());
+      System.out.println(((I) () -> System.currentTimeMillis() > 0 ? "I" : null).f());
+      System.out.println(((J) () -> System.currentTimeMillis() > 0 ? "J" : null).f());
     }
   }
 
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 c9a0d19..0807547 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,6 +7,7 @@
 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,7 +17,6 @@
 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.HorizontallyMergedClassesInspector;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -40,9 +40,13 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
-        // TODO(b/173990042): I and J should be merged.
         .addHorizontallyMergedClassesInspector(
-            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+            inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
+        .addOptionsModification(
+            options -> {
+              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
+              options.horizontalClassMergerOptions().enableInterfaceMerging();
+            })
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoUnusedInterfaceRemovalAnnotations()
@@ -54,7 +58,6 @@
               ClassSubject aClassSubject = inspector.clazz(A.class);
               assertThat(aClassSubject, isPresent());
               assertThat(aClassSubject, isImplementing(inspector.clazz(I.class)));
-              assertThat(aClassSubject, isImplementing(inspector.clazz(J.class)));
             })
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("I", "J");
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 9a9ccc3..5f30eff 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,6 +7,7 @@
 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,7 +17,6 @@
 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.HorizontallyMergedClassesInspector;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -40,9 +40,13 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
-        // TODO(b/173990042): I and J should be merged.
         .addHorizontallyMergedClassesInspector(
-            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+            inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
+        .addOptionsModification(
+            options -> {
+              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
+              options.horizontalClassMergerOptions().enableInterfaceMerging();
+            })
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoUnusedInterfaceRemovalAnnotations()
@@ -54,7 +58,6 @@
               ClassSubject aClassSubject = inspector.clazz(A.class);
               assertThat(aClassSubject, isPresent());
               assertThat(aClassSubject, isImplementing(inspector.clazz(I.class)));
-              assertThat(aClassSubject, isImplementing(inspector.clazz(J.class)));
             })
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("I", "J");
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 74aa943..24e5de8 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,6 +7,7 @@
 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;
@@ -14,7 +15,6 @@
 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.HorizontallyMergedClassesInspector;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -38,9 +38,13 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
-        // TODO(b/173990042): I and J should be merged.
         .addHorizontallyMergedClassesInspector(
-            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+            inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
+        .addOptionsModification(
+            options -> {
+              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
+              options.horizontalClassMergerOptions().enableInterfaceMerging();
+            })
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
@@ -50,7 +54,6 @@
               ClassSubject aClassSubject = inspector.clazz(A.class);
               assertThat(aClassSubject, isPresent());
               assertThat(aClassSubject, isImplementing(inspector.clazz(I.class)));
-              assertThat(aClassSubject, isImplementing(inspector.clazz(J.class)));
             })
         .run(parameters.getRuntime(), Main.class)
         .assertSuccess();
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 96b8ea9..2d264db 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,6 +4,8 @@
 
 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;
@@ -32,11 +34,18 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
-        // TODO(b/173990042): I and J should be merged.
         .addHorizontallyMergedClassesInspector(
-            inspector -> inspector.assertClassesNotMerged(I.class, J.class))
+            inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
+        .addOptionsModification(
+            options -> {
+              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
+              options.horizontalClassMergerOptions().enableInterfaceMerging();
+            })
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
+        .noClassInliningOfSynthetics()
+        .noHorizontalClassMergingOfSynthetics()
+        .noInliningOfSynthetics()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .run(parameters.getRuntime(), Main.class)
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 cd691d1..23ed1ea 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,6 +4,8 @@
 
 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;
@@ -32,11 +34,17 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
-        // TODO(b/173990042): I and J should be merged.
         .addHorizontallyMergedClassesInspector(
-            inspector -> inspector.assertClassesNotMerged(I.class, J.class))
+            inspector -> inspector.assertIsCompleteMergeGroup(I.class, J.class))
+        .addOptionsModification(
+            options -> {
+              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
+              options.horizontalClassMergerOptions().enableInterfaceMerging();
+            })
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
+        .noClassInliningOfSynthetics()
+        .noInliningOfSynthetics()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .run(parameters.getRuntime(), Main.class)
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 6a1b373..6ad6ef1 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,6 +7,7 @@
 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;
@@ -17,7 +18,6 @@
 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.HorizontallyMergedClassesInspector;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -43,7 +43,18 @@
         .addKeepMainRule(Main.class)
         // I and J are not eligible for merging, since they declare the same default method.
         .addHorizontallyMergedClassesInspector(
-            HorizontallyMergedClassesInspector::assertNoClassesMerged)
+            inspector -> {
+              if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
+                inspector.assertNoClassesMerged();
+              } else {
+                inspector.assertIsCompleteMergeGroup(I.class, J.class);
+              }
+            })
+        .addOptionsModification(
+            options -> {
+              assertFalse(options.horizontalClassMergerOptions().isInterfaceMergingEnabled());
+              options.horizontalClassMergerOptions().enableInterfaceMerging();
+            })
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
@@ -59,7 +70,13 @@
 
               ClassSubject bClassSubject = inspector.clazz(B.class);
               assertThat(bClassSubject, isPresent());
-              assertThat(bClassSubject, isImplementing(inspector.clazz(J.class)));
+              assertThat(
+                  bClassSubject,
+                  isImplementing(
+                      inspector.clazz(
+                          parameters.canUseDefaultAndStaticInterfaceMethods()
+                              ? J.class
+                              : I.class)));
             })
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("I", "J");
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 53f5ae2..1c33206 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
@@ -5,6 +5,7 @@
 package com.android.tools.r8.classmerging.horizontalstatic;
 
 import static com.android.tools.r8.synthesis.SyntheticItemsTestUtils.syntheticCompanionClass;
+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;
 
@@ -12,9 +13,8 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
 import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -37,32 +37,41 @@
   @Test
   public void test() throws Exception {
     String expectedOutput = StringUtils.lines("In I.a()", "In J.b()", "In A.c()");
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        // TODO(b/173990042): Extend horizontal class merging to interfaces.
+        .addHorizontallyMergedClassesInspector(
+            inspector -> {
+              if (parameters.canUseDefaultAndStaticInterfaceMethods()) {
+                inspector.assertNoClassesMerged();
+              } else {
+                inspector
+                    .assertClassReferencesMerged(
+                        SyntheticItemsTestUtils.syntheticCompanionClass(I.class),
+                        SyntheticItemsTestUtils.syntheticCompanionClass(J.class))
+                    .assertClassesNotMerged(I.class, J.class);
+              }
+            })
+        .enableInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(expectedOutput)
+        .inspect(
+            inspector -> {
+              // We do not allow horizontal class merging of interfaces and classes. Therefore, A
+              // should remain in the output.
+              assertThat(inspector.clazz(A.class), isPresent());
 
-    CodeInspector inspector =
-        testForR8(parameters.getBackend())
-            .addInnerClasses(getClass())
-            .addKeepMainRule(TestClass.class)
-            // TODO(b/173990042): Extend horizontal class merging to interfaces.
-            .addHorizontallyMergedClassesInspector(
-                HorizontallyMergedClassesInspector::assertNoClassesMerged)
-            .enableInliningAnnotations()
-            .setMinApi(parameters.getApiLevel())
-            .run(parameters.getRuntime(), TestClass.class)
-            .assertSuccessWithOutput(expectedOutput)
-            .inspector();
-
-    // 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());
-    } else {
-      assertThat(inspector.clazz(syntheticCompanionClass(I.class)), isPresent());
-      assertThat(inspector.clazz(syntheticCompanionClass(I.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());
+              } else {
+                assertThat(inspector.clazz(syntheticCompanionClass(I.class)), isPresent());
+                assertThat(inspector.clazz(syntheticCompanionClass(J.class)), isAbsent());
+              }
+            });
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/MergeSynthesizingContextIntoSyntheticLambdaTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/MergeSynthesizingContextIntoSyntheticLambdaTest.java
new file mode 100644
index 0000000..be8211b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/MergeSynthesizingContextIntoSyntheticLambdaTest.java
@@ -0,0 +1,73 @@
+// 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.vertical;
+
+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 MergeSynthesizingContextIntoSyntheticLambdaTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public MergeSynthesizingContextIntoSyntheticLambdaTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        // Disable inlining to ensure that the synthetic lambdas remain in the residual
+        // program.
+        .addOptionsModification(options -> options.enableInlining = false)
+        .addVerticallyMergedClassesInspector(
+            inspector -> {
+              if (parameters.isCfRuntime()) {
+                inspector.assertNoClassesMerged();
+              } else {
+                inspector.assertMergedIntoSubtype(I.class);
+              }
+            })
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("I", "J");
+  }
+
+  static class Main {
+    public static void main(String[] args) {
+      I i = () -> System.out.println("I");
+      i.f();
+      i.g().h();
+    }
+  }
+
+  interface I {
+
+    void f();
+
+    default J g() {
+      // Has synthesizing context I. After vertical class merging, this synthesizing context is
+      // rewritten into the synthetic lambda defined in main().
+      return () -> System.out.println("J");
+    }
+  }
+
+  interface J {
+
+    void h();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/debug/DebugInfoWhenInliningTest.java b/src/test/java/com/android/tools/r8/debug/DebugInfoWhenInliningTest.java
index 319327d..965b610 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugInfoWhenInliningTest.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugInfoWhenInliningTest.java
@@ -3,23 +3,17 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.debug;
 
+import static org.junit.Assume.assumeTrue;
 
-import static com.android.tools.r8.naming.ClassNameMapper.MissingFileAction.MISSING_FILE_IS_EMPTY_MAP;
-
-import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.OutputMode;
-import com.android.tools.r8.R8Command;
-import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersBuilder;
 import com.android.tools.r8.debug.DebugTestBase.JUnit3Wrapper.Command;
-import com.android.tools.r8.debug.DebugTestConfig.RuntimeKind;
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.naming.ClassNameMapper.MissingFileAction;
+import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
 import com.google.common.collect.ImmutableList;
-import java.nio.file.Path;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -30,85 +24,61 @@
 @RunWith(Parameterized.class)
 public class DebugInfoWhenInliningTest extends DebugTestBase {
 
-  public enum Config {
-    CF,
-    DEX_NO_FORCE_JUMBO,
-    DEX_FORCE_JUMBO
-  };
-
   private static final String CLASS_NAME = "Inlining1";
   private static final String SOURCE_FILE = "Inlining1.java";
 
   private DebugTestConfig makeConfig(
-      LineNumberOptimization lineNumberOptimization,
-      boolean writeProguardMap,
-      RuntimeKind runtimeKind)
-      throws Exception {
-    DebugTestConfig config = null;
-    Path outdir = temp.newFolder().toPath();
-    Path outjar = outdir.resolve("r8_compiled.jar");
-    R8Command.Builder builder =
-        R8Command.builder()
+      LineNumberOptimization lineNumberOptimization, boolean writeProguardMap) throws Exception {
+    R8TestCompileResult result =
+        testForR8(parameters.getBackend())
             .addProgramFiles(DEBUGGEE_JAR)
-            .setMode(CompilationMode.RELEASE)
-            .addProguardConfiguration(
-                ImmutableList.of(
-                    "-keep class "
-                        + CLASS_NAME
-                        + " { public static void main(java.lang.String[]); }"),
-                Origin.unknown())
-            .setDisableMinification(true)
-            .addProguardConfiguration(
-                ImmutableList.of("-keepattributes SourceFile,LineNumberTable"), Origin.unknown());
-
-    if (runtimeKind == RuntimeKind.DEX) {
-      AndroidApiLevel minSdk = ToolHelper.getMinApiLevelForDexVm();
-      builder
-          .setMinApiLevel(minSdk.getLevel())
-          .addLibraryFiles(ToolHelper.getAndroidJar(minSdk))
-          .setOutput(outjar, OutputMode.DexIndexed);
-      config = new DexDebugTestConfig(outjar);
-    } else {
-      assert (runtimeKind == RuntimeKind.CF);
-      builder
-          .setOutput(outjar, OutputMode.ClassFile)
-          .addLibraryFiles(ToolHelper.getJava8RuntimeJar());
-      config = new CfDebugTestConfig(outjar);
-    }
-
+            .addKeepMainRule(CLASS_NAME)
+            .noMinification()
+            .addKeepAttributeSourceFile()
+            .addKeepAttributeLineNumberTable()
+            .setMinApi(parameters.getApiLevel())
+            .addOptionsModification(
+                options -> {
+                  options.lineNumberOptimization = lineNumberOptimization;
+                  options.testing.forceJumboStringProcessing = forceJumboStringProcessing;
+                  // TODO(b/117848700): Can we make these tests neutral to inlining threshold?
+                  // Also CF needs improvements here.
+                  options.inliningInstructionLimit = parameters.isCfRuntime() ? 5 : 4;
+                })
+            .compile();
+    DebugTestConfig config = result.debugConfig();
     if (writeProguardMap) {
-      Path proguardMapPath = outdir.resolve("proguard.map");
-      builder.setProguardMapOutputPath(proguardMapPath);
-      config.setProguardMap(proguardMapPath, MISSING_FILE_IS_EMPTY_MAP);
+      config.setProguardMap(result.writeProguardMap(), MissingFileAction.MISSING_FILE_IS_EMPTY_MAP);
     }
-
-    ToolHelper.runR8(
-        builder.build(), options -> {
-          options.lineNumberOptimization = lineNumberOptimization;
-          options.testing.forceJumboStringProcessing = forceJumboStringProcessing;
-          // TODO(b/117848700): Can we make these tests neutral to inlining threshold?
-          // Also CF needs improvements here.
-          options.inliningInstructionLimit = runtimeKind == RuntimeKind.CF ? 5 : 4;
-        });
-
     return config;
   }
 
-  private boolean forceJumboStringProcessing;
-  private RuntimeKind runtimeKind;
+  private final TestParameters parameters;
+  private final boolean forceJumboStringProcessing;
 
-  @Parameters(name = "config: {0}")
-  public static Collection<Config> data() {
-    return Arrays.asList(Config.values());
+  @Parameters(name = "{0}, force-jumbo: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        TestParametersBuilder.builder().withAllRuntimesAndApiLevels().build(),
+        BooleanUtils.values());
   }
 
-  public DebugInfoWhenInliningTest(Config config) {
-    this.forceJumboStringProcessing = config == Config.DEX_FORCE_JUMBO;
-    this.runtimeKind = config == Config.CF ? RuntimeKind.CF : RuntimeKind.DEX;
+  public DebugInfoWhenInliningTest(TestParameters parameters, boolean forceJumboString) {
+    assumeTrue(!parameters.isCfRuntime() || !forceJumboString);
+    this.parameters = parameters;
+    this.forceJumboStringProcessing = forceJumboString;
+  }
+
+  private void assumeMappingIsNotToPCs() {
+    assumeTrue(
+        "Ignoring test when the line number table is removed.",
+        parameters.isCfRuntime()
+            || parameters.getApiLevel().isLessThan(apiLevelWithPcAsLineNumberSupport()));
   }
 
   @Test
   public void testEachLineNotOptimized() throws Throwable {
+    assumeMappingIsNotToPCs();
     // The reason why the not-optimized test contains half as many line numbers as the optimized
     // one:
     //
@@ -116,11 +86,12 @@
     // (innermost callee) the line numbers are actually 7, 7, 32, 32, ... but even if the positions
     // are emitted duplicated in the dex code, the debugger stops only when there's a change.
     int[] lineNumbers = {7, 32, 11, 7};
-    testEachLine(makeConfig(LineNumberOptimization.OFF, false, runtimeKind), lineNumbers);
+    testEachLine(makeConfig(LineNumberOptimization.OFF, false), lineNumbers);
   }
 
   @Test
   public void testEachLineNotOptimizedWithMap() throws Throwable {
+    assumeMappingIsNotToPCs();
     // The reason why the not-optimized test contains half as many line numbers as the optimized
     // one:
     //
@@ -128,17 +99,19 @@
     // (innermost callee) the line numbers are actually 7, 7, 32, 32, ... but even if the positions
     // are emitted duplicated in the dex code, the debugger stops only when there's a change.
     int[] lineNumbers = {7, 32, 11, 7};
-    testEachLine(makeConfig(LineNumberOptimization.OFF, true, runtimeKind), lineNumbers);
+    testEachLine(makeConfig(LineNumberOptimization.OFF, true), lineNumbers);
   }
 
   @Test
   public void testEachLineOptimized() throws Throwable {
+    assumeMappingIsNotToPCs();
     int[] lineNumbers = {1, 2, 3, 4, 5, 6, 7, 8};
-    testEachLine(makeConfig(LineNumberOptimization.ON, false, runtimeKind), lineNumbers);
+    testEachLine(makeConfig(LineNumberOptimization.ON, false), lineNumbers);
   }
 
   @Test
   public void testEachLineOptimizedWithMap() throws Throwable {
+    assumeMappingIsNotToPCs();
     int[] lineNumbers = {7, 7, 32, 32, 11, 11, 7, 7};
     List<List<SignatureAndLine>> inlineFramesList =
         ImmutableList.of(
@@ -170,8 +143,7 @@
                 new SignatureAndLine("void Inlining3.differentFileMultilevelInliningLevel2()", 7),
                 new SignatureAndLine("void Inlining2.differentFileMultilevelInliningLevel1()", 36),
                 new SignatureAndLine("void main(java.lang.String[])", 26)));
-    testEachLine(
-        makeConfig(LineNumberOptimization.ON, true, runtimeKind), lineNumbers, inlineFramesList);
+    testEachLine(makeConfig(LineNumberOptimization.ON, true), lineNumbers, inlineFramesList);
   }
 
   private void testEachLine(DebugTestConfig config, int[] lineNumbers) throws Throwable {
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
index c34c633..d92d4ec 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestBase.java
@@ -652,10 +652,23 @@
 
       String getObfuscatedMethodName(
           String originalClassName, String originalMethodName, String methodSignature);
+
+      boolean canUsePcForMissingLineNumberTable();
     }
 
     private class IdentityTranslator implements Translator {
 
+      private final boolean usePcForMissingLineTable;
+
+      public IdentityTranslator(boolean usePcForMissingLineNumberTable) {
+        this.usePcForMissingLineTable = usePcForMissingLineNumberTable;
+      }
+
+      @Override
+      public boolean canUsePcForMissingLineNumberTable() {
+        return usePcForMissingLineTable;
+      }
+
       @Override
       public String getOriginalClassName(String obfuscatedClassName) {
         return obfuscatedClassName;
@@ -709,7 +722,9 @@
     private class ClassNameMapperTranslator extends IdentityTranslator {
       private final ClassNameMapper classNameMapper;
 
-      public ClassNameMapperTranslator(ClassNameMapper classNameMapper) {
+      public ClassNameMapperTranslator(
+          ClassNameMapper classNameMapper, boolean usePcForMissingLineTable) {
+        super(usePcForMissingLineTable);
         this.classNameMapper = classNameMapper;
       }
 
@@ -872,9 +887,11 @@
       this.debuggeeClassName = debuggeeClassName;
       this.commandsQueue = new ArrayDeque<>(commands);
       if (classNameMapper == null) {
-        this.translator = new IdentityTranslator();
+        this.translator = new IdentityTranslator(config.shouldUsePcForMissingLineNumberTable());
       } else {
-        this.translator = new ClassNameMapperTranslator(classNameMapper);
+        this.translator =
+            new ClassNameMapperTranslator(
+                classNameMapper, config.shouldUsePcForMissingLineNumberTable());
       }
     }
 
@@ -1161,6 +1178,9 @@
           long startCodeIndex = reply.getNextValueAsLong();
           long endCodeIndex = reply.getNextValueAsLong();
           int lines = reply.getNextValueAsInt();
+          if (lines == 0 && translator.canUsePcForMissingLineNumberTable()) {
+            return (int) location.index;
+          }
           int line = -1;
           long previousLineCodeIndex = -1;
           for (int i = 0; i < lines; ++i) {
diff --git a/src/test/java/com/android/tools/r8/debug/DebugTestConfig.java b/src/test/java/com/android/tools/r8/debug/DebugTestConfig.java
index 1dc24d8..4ab22aa 100644
--- a/src/test/java/com/android/tools/r8/debug/DebugTestConfig.java
+++ b/src/test/java/com/android/tools/r8/debug/DebugTestConfig.java
@@ -25,6 +25,7 @@
 
   private Path proguardMap = null;
   private ClassNameMapper.MissingFileAction missingProguardMapAction;
+  private boolean usePcForMissingLineNumberTable = false;
 
   /** The expected runtime kind for the debuggee. */
   public abstract RuntimeKind getRuntimeKind();
@@ -37,6 +38,14 @@
     return getRuntimeKind() == RuntimeKind.DEX;
   }
 
+  public void allowUsingPcForMissingLineNumberTable() {
+    usePcForMissingLineNumberTable = true;
+  }
+
+  public boolean shouldUsePcForMissingLineNumberTable() {
+    return usePcForMissingLineNumberTable;
+  }
+
   /** Classpath paths for the debuggee. */
   public List<Path> getPaths() {
     return paths;
diff --git a/src/test/java/com/android/tools/r8/debug/LineNumberOptimizationTest.java b/src/test/java/com/android/tools/r8/debug/LineNumberOptimizationTest.java
index 35f24c9..d307de3 100644
--- a/src/test/java/com/android/tools/r8/debug/LineNumberOptimizationTest.java
+++ b/src/test/java/com/android/tools/r8/debug/LineNumberOptimizationTest.java
@@ -4,18 +4,14 @@
 package com.android.tools.r8.debug;
 
 import static com.android.tools.r8.naming.ClassNameMapper.MissingFileAction.MISSING_FILE_IS_EMPTY_MAP;
+import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.OutputMode;
-import com.android.tools.r8.R8Command;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.debug.DebugTestConfig.RuntimeKind;
-import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersBuilder;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.utils.InternalOptions.LineNumberOptimization;
-import com.google.common.collect.ImmutableList;
-import java.nio.file.Path;
-import java.util.Collection;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -29,114 +25,96 @@
     20, 7, 8, 28, 29, 9, 21, 12, 13, 22, 16, 17
   };
   private static final int[] OPTIMIZED_LINE_NUMBERS = {1, 1, 2, 1, 2, 1, 2, 3, 2, 3, 4, 3};
+
   private static final String CLASS1 = "LineNumberOptimization1";
   private static final String CLASS2 = "LineNumberOptimization2";
   private static final String FILE1 = CLASS1 + ".java";
   private static final String FILE2 = CLASS2 + ".java";
   private static final String MAIN_SIGNATURE = "([Ljava/lang/String;)V";
 
-  private RuntimeKind runtimeKind;
+  private final TestParameters parameters;
 
   @Parameterized.Parameters(name = "{0}")
-  public static Collection<Object[]> setup() {
-    return ImmutableList.of(
-        new Object[] {"CF", RuntimeKind.CF}, new Object[] {"DEX", RuntimeKind.DEX});
+  public static TestParametersCollection setup() {
+    return TestParametersBuilder.builder().withAllRuntimesAndApiLevels().build();
   }
 
-  public LineNumberOptimizationTest(String name, RuntimeKind runtimeKind) {
-    this.runtimeKind = runtimeKind;
+  public LineNumberOptimizationTest(TestParameters parameters) {
+    this.parameters = parameters;
   }
 
-  private static DebugTestConfig makeConfig(
+  private DebugTestConfig makeConfig(
       LineNumberOptimization lineNumberOptimization,
       boolean writeProguardMap,
-      boolean dontOptimizeByEnablingDebug,
-      RuntimeKind runtimeKind)
+      boolean dontOptimizeByEnablingDebug)
       throws Exception {
-    Path outdir = temp.newFolder().toPath();
-    Path outjar = outdir.resolve("r8_compiled.jar");
 
-    R8Command.Builder builder =
-        R8Command.builder()
+    R8TestCompileResult result =
+        testForR8(parameters.getBackend())
             .addProgramFiles(DEBUGGEE_JAR)
+            .setMinApi(parameters.getApiLevel())
             .setMode(dontOptimizeByEnablingDebug ? CompilationMode.DEBUG : CompilationMode.RELEASE)
-            .setDisableTreeShaking(true)
-            .setDisableMinification(true)
-            .addProguardConfiguration(
-                ImmutableList.of("-keepattributes SourceFile,LineNumberTable"), Origin.unknown());
+            .noTreeShaking()
+            .noMinification()
+            .addKeepAttributeSourceFile()
+            .addKeepAttributeLineNumberTable()
+            .addOptionsModification(
+                options -> {
+                  if (!dontOptimizeByEnablingDebug) {
+                    options.lineNumberOptimization = lineNumberOptimization;
+                  }
+                  options.enableInlining = false;
+                })
+            .compile();
 
-    DebugTestConfig config = null;
-
-    if (runtimeKind == RuntimeKind.CF) {
-      builder
-          .setOutput(outjar, OutputMode.ClassFile)
-          .addLibraryFiles(ToolHelper.getJava8RuntimeJar());
-      config = new CfDebugTestConfig(outjar);
-    } else {
-      assert (runtimeKind == RuntimeKind.DEX);
-      AndroidApiLevel minSdk = ToolHelper.getMinApiLevelForDexVm();
-      builder
-          .setMinApiLevel(minSdk.getLevel())
-          .addLibraryFiles(ToolHelper.getAndroidJar(minSdk))
-          .setOutput(outjar, OutputMode.DexIndexed);
-      config = new D8DebugTestConfig();
-    }
-
-    config.addPaths(outjar);
+    DebugTestConfig config = result.debugConfig();
     if (writeProguardMap) {
-      Path proguardMapPath = outdir.resolve("proguard.map");
-      builder.setProguardMapOutputPath(proguardMapPath);
-      config.setProguardMap(proguardMapPath, MISSING_FILE_IS_EMPTY_MAP);
+      config.setProguardMap(result.writeProguardMap(), MISSING_FILE_IS_EMPTY_MAP);
     }
-
-    ToolHelper.runR8(
-        builder.build(),
-        options -> {
-          if (!dontOptimizeByEnablingDebug) {
-            options.lineNumberOptimization = lineNumberOptimization;
-          }
-          options.enableInlining = false;
-        });
-
     return config;
   }
 
+  private void assumeMappingIsNotToPCs() {
+    assumeTrue(
+        "Ignoring test when the line number table is removed.",
+        parameters.isCfRuntime()
+            || parameters.getApiLevel().isLessThan(apiLevelWithPcAsLineNumberSupport()));
+  }
+
   @Test
   public void testNotOptimized() throws Throwable {
-    testRelease(
-        makeConfig(LineNumberOptimization.OFF, false, false, runtimeKind), ORIGINAL_LINE_NUMBERS);
+    assumeMappingIsNotToPCs();
+    testRelease(makeConfig(LineNumberOptimization.OFF, false, false), ORIGINAL_LINE_NUMBERS);
   }
 
   @Test
   public void testNotOptimizedWithMap() throws Throwable {
-    testRelease(
-        makeConfig(LineNumberOptimization.OFF, true, false, runtimeKind), ORIGINAL_LINE_NUMBERS);
+    assumeMappingIsNotToPCs();
+    testRelease(makeConfig(LineNumberOptimization.OFF, true, false), ORIGINAL_LINE_NUMBERS);
   }
 
   @Test
   public void testNotOptimizedByEnablingDebug() throws Throwable {
-    testDebug(
-        makeConfig(LineNumberOptimization.OFF, false, true, runtimeKind),
-        ORIGINAL_LINE_NUMBERS_DEBUG);
+    testDebug(makeConfig(LineNumberOptimization.OFF, false, true), ORIGINAL_LINE_NUMBERS_DEBUG);
   }
 
   @Test
   public void testNotOptimizedByEnablingDebugWithMap() throws Throwable {
-    testDebug(
-        makeConfig(LineNumberOptimization.OFF, true, true, runtimeKind),
-        ORIGINAL_LINE_NUMBERS_DEBUG);
+    testDebug(makeConfig(LineNumberOptimization.OFF, true, true), ORIGINAL_LINE_NUMBERS_DEBUG);
   }
 
   @Test
   public void testOptimized() throws Throwable {
-    testRelease(
-        makeConfig(LineNumberOptimization.ON, false, false, runtimeKind), OPTIMIZED_LINE_NUMBERS);
+    assumeMappingIsNotToPCs();
+    DebugTestConfig config = makeConfig(LineNumberOptimization.ON, false, false);
+    config.allowUsingPcForMissingLineNumberTable();
+    testRelease(config, OPTIMIZED_LINE_NUMBERS);
   }
 
   @Test
   public void testOptimizedWithMap() throws Throwable {
-    testRelease(
-        makeConfig(LineNumberOptimization.ON, true, false, runtimeKind), ORIGINAL_LINE_NUMBERS);
+    assumeMappingIsNotToPCs();
+    testRelease(makeConfig(LineNumberOptimization.ON, true, false), ORIGINAL_LINE_NUMBERS);
   }
 
   private void testDebug(DebugTestConfig config, int[] lineNumbers) throws Throwable {
diff --git a/src/test/java/com/android/tools/r8/debug/R8DebugNonMinifiedProgramTestRunner.java b/src/test/java/com/android/tools/r8/debug/R8DebugNonMinifiedProgramTestRunner.java
index 3f61638..9771a28 100644
--- a/src/test/java/com/android/tools/r8/debug/R8DebugNonMinifiedProgramTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debug/R8DebugNonMinifiedProgramTestRunner.java
@@ -6,6 +6,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.R8TestCompileResult;
@@ -69,6 +70,13 @@
             });
   }
 
+  private void assumeMappingIsNotToPCs() {
+    assumeTrue(
+        "Ignoring test when the line number table is removed.",
+        parameters.isCfRuntime()
+            || parameters.getApiLevel().isLessThan(apiLevelWithPcAsLineNumberSupport()));
+  }
+
   @Test
   public void testDebugMode() throws Throwable {
     runTest(compiledDebug.apply(parameters.getBackend(), parameters.getApiLevel()));
@@ -76,6 +84,7 @@
 
   @Test
   public void testNoOptimizationAndNoMinification() throws Throwable {
+    assumeMappingIsNotToPCs();
     runTest(compiledNoOptNoMinify.apply(parameters.getBackend(), parameters.getApiLevel()));
   }
 
diff --git a/src/test/java/com/android/tools/r8/debuginfo/CanonicalizeWithInline.java b/src/test/java/com/android/tools/r8/debuginfo/CanonicalizeWithInline.java
index 395f29f..73aff7f 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/CanonicalizeWithInline.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/CanonicalizeWithInline.java
@@ -8,17 +8,35 @@
 import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.dex.DexParser;
 import com.android.tools.r8.dex.DexSection;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import org.junit.Assert;
 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 CanonicalizeWithInline extends TestBase {
 
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  private final TestParameters parameters;
+
+  public CanonicalizeWithInline(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
   private int getNumberOfDebugInfos(Path file) throws IOException {
     DexSection[] dexSections = DexParser.parseMapFrom(file);
     for (DexSection dexSection : dexSections) {
@@ -36,6 +54,7 @@
 
     R8TestCompileResult result =
         testForR8(Backend.DEX)
+            .setMinApi(AndroidApiLevel.B)
             .addProgramClasses(clazzA, clazzB)
             .addKeepRules(
                 "-keepattributes SourceFile,LineNumberTable",
diff --git a/src/test/java/com/android/tools/r8/debuginfo/DexPcWithDebugInfoForOverloadedMethodsTestRunner.java b/src/test/java/com/android/tools/r8/debuginfo/DexPcWithDebugInfoForOverloadedMethodsTestRunner.java
index 18913cc..d6e30b4 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/DexPcWithDebugInfoForOverloadedMethodsTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/DexPcWithDebugInfoForOverloadedMethodsTestRunner.java
@@ -70,11 +70,6 @@
         .addKeepAttributeLineNumberTable()
         .addKeepAttributes(ProguardKeepAttributes.SOURCE_FILE)
         .setMinApi(parameters.getApiLevel())
-        .addOptionsModification(
-            internalOptions -> {
-              // TODO(b/37830524): Remove when activated.
-              internalOptions.enablePcDebugInfoOutput = true;
-            })
         .run(parameters.getRuntime(), MAIN)
         .assertFailureWithErrorThatMatches(containsString(EXPECTED))
         .inspectOriginalStackTrace(
diff --git a/src/test/java/com/android/tools/r8/debuginfo/EnsureNoDebugInfoEmittedForPcOnlyTestRunner.java b/src/test/java/com/android/tools/r8/debuginfo/EnsureNoDebugInfoEmittedForPcOnlyTestRunner.java
index 270e7cd..c2f1811 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/EnsureNoDebugInfoEmittedForPcOnlyTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/EnsureNoDebugInfoEmittedForPcOnlyTestRunner.java
@@ -19,7 +19,6 @@
 import com.android.tools.r8.naming.retrace.StackTrace;
 import com.android.tools.r8.naming.retrace.StackTrace.StackTraceLine;
 import com.android.tools.r8.utils.AndroidApiLevel;
-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;
@@ -53,11 +52,6 @@
     return parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.O);
   }
 
-  // TODO(b/37830524): Remove when activated.
-  private void enablePcDebugInfoOutput(InternalOptions options) {
-    options.enablePcDebugInfoOutput = true;
-  }
-
   @Test
   public void testD8Debug() throws Exception {
     testForD8(parameters.getBackend())
@@ -65,7 +59,6 @@
         .addProgramClasses(MAIN)
         .setMinApi(parameters.getApiLevel())
         .internalEnableMappingOutput()
-        .addOptionsModification(this::enablePcDebugInfoOutput)
         .run(parameters.getRuntime(), MAIN)
         // For a debug build we always expect the output to have actual line information.
         .inspectFailure(this::checkHasLineNumberInfo)
@@ -79,7 +72,6 @@
         .addProgramClasses(MAIN)
         .setMinApi(parameters.getApiLevel())
         .internalEnableMappingOutput()
-        .addOptionsModification(this::enablePcDebugInfoOutput)
         .run(parameters.getRuntime(), MAIN)
         .inspectFailure(
             inspector -> {
@@ -98,7 +90,6 @@
         .release()
         .addProgramClasses(MAIN)
         .setMinApi(parameters.getApiLevel())
-        .addOptionsModification(this::enablePcDebugInfoOutput)
         .run(parameters.getRuntime(), MAIN)
         // If compiling without a map output actual debug info should also be retained. Otherwise
         // there would not be any way to obtain the actual lines.
@@ -114,7 +105,6 @@
         .addKeepMainRule(MAIN)
         .addKeepAttributeLineNumberTable()
         .setMinApi(parameters.getApiLevel())
-        .addOptionsModification(this::enablePcDebugInfoOutput)
         .run(parameters.getRuntime(), MAIN)
         .inspectOriginalStackTrace(
             (stackTrace, inspector) -> {
diff --git a/src/test/java/com/android/tools/r8/debuginfo/InliningWithoutPositionsTestRunner.java b/src/test/java/com/android/tools/r8/debuginfo/InliningWithoutPositionsTestRunner.java
index 3e60ef1..9efecb4 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/InliningWithoutPositionsTestRunner.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/InliningWithoutPositionsTestRunner.java
@@ -6,25 +6,22 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.OutputMode;
-import com.android.tools.r8.R8Command;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersBuilder;
 import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.ToolHelper.ArtCommandBuilder;
-import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.debuginfo.InliningWithoutPositionsTestSourceDump.Location;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.ClassNamingForNameMapper;
 import com.android.tools.r8.naming.ClassNamingForNameMapper.MappedRange;
 import com.android.tools.r8.naming.ClassNamingForNameMapper.MappedRangesOfName;
 import com.android.tools.r8.naming.Range;
-import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApiLevel;
-import com.google.common.collect.ImmutableList;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
@@ -39,12 +36,7 @@
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
-public class InliningWithoutPositionsTestRunner {
-
-  enum Backend {
-    CF,
-    DEX
-  }
+public class InliningWithoutPositionsTestRunner extends TestBase {
 
   private static final String TEST_CLASS = "InliningWithoutPositionsTestSource";
   private static final String TEST_PACKAGE = "com.android.tools.r8.debuginfo";
@@ -52,7 +44,7 @@
 
   @ClassRule public static TemporaryFolder temp = ToolHelper.getTemporaryFolderForTest();
 
-  private final Backend backend;
+  private final TestParameters parameters;
   private final boolean mainPos;
   private final boolean foo1Pos;
   private final boolean barPos;
@@ -62,13 +54,14 @@
   @Parameters(name = "{0}: main/foo1/bar/foo2 positions: {1}/{2}/{3}/{4}, throwLocation: {5}")
   public static Collection<Object[]> data() {
     List<Object[]> testCases = new ArrayList<>();
-    for (Backend backend : Backend.values()) {
+    for (TestParameters parameters :
+        TestParametersBuilder.builder().withAllRuntimes().withApiLevel(AndroidApiLevel.B).build()) {
     for (int i = 0; i < 16; ++i) {
       for (Location throwLocation : Location.values()) {
         if (throwLocation != Location.MAIN) {
             testCases.add(
                 new Object[] {
-                  backend, (i & 1) != 0, (i & 2) != 0, (i & 4) != 0, (i & 8) != 0, throwLocation
+                  parameters, (i & 1) != 0, (i & 2) != 0, (i & 4) != 0, (i & 8) != 0, throwLocation
                 });
         }
       }
@@ -78,13 +71,13 @@
   }
 
   public InliningWithoutPositionsTestRunner(
-      Backend backend,
+      TestParameters parameters,
       boolean mainPos,
       boolean foo1Pos,
       boolean barPos,
       boolean foo2Pos,
       Location throwLocation) {
-    this.backend = backend;
+    this.parameters = parameters;
     this.mainPos = mainPos;
     this.foo1Pos = foo1Pos;
     this.barPos = barPos;
@@ -97,54 +90,23 @@
     // See InliningWithoutPositionsTestSourceDump for the code compiled here.
     Path testClassDir = temp.newFolder().toPath();
     Path testClassPath = testClassDir.resolve(TEST_CLASS + ".class");
-    Path outputPath = temp.newFolder().toPath();
-
     Files.write(
         testClassPath,
         InliningWithoutPositionsTestSourceDump.dump(
             mainPos, foo1Pos, barPos, foo2Pos, throwLocation));
 
-    Path proguardMapPath = testClassDir.resolve("proguard.map");
-
-    R8Command.Builder builder =
-        R8Command.builder()
+    R8TestRunResult result =
+        testForR8(parameters.getBackend())
+            .setMinApi(parameters.getApiLevel())
             .addProgramFiles(testClassPath)
             .setMode(CompilationMode.RELEASE)
-            .setProguardMapOutputPath(proguardMapPath);
-    if (backend == Backend.DEX) {
-      AndroidApiLevel minSdk = ToolHelper.getMinApiLevelForDexVm();
-      builder
-          .setMinApiLevel(minSdk.getLevel())
-          .addLibraryFiles(ToolHelper.getAndroidJar(minSdk))
-          .setOutput(outputPath, OutputMode.DexIndexed);
-    } else {
-      assert (backend == Backend.CF);
-      builder
-          .addLibraryFiles(ToolHelper.getJava8RuntimeJar())
-          .setOutput(outputPath, OutputMode.ClassFile);
-    }
-    builder
-        .addProguardConfiguration(
-            ImmutableList.of(
-                "-keep class " + MAIN_CLASS + " { public static void main(java.lang.String[]); }"),
-            Origin.unknown())
-        .setDisableMinification(true)
-        .addProguardConfiguration(
-            ImmutableList.of("-keepattributes SourceFile,LineNumberTable"), Origin.unknown());
-
-    ToolHelper.runR8(builder.build(), options -> options.inliningInstructionLimit = 40);
-
-    ProcessResult result;
-    if (backend == Backend.DEX) {
-      ArtCommandBuilder artCommandBuilder = new ArtCommandBuilder();
-      artCommandBuilder.appendClasspath(outputPath.resolve("classes.dex").toString());
-      artCommandBuilder.setMainClass(MAIN_CLASS);
-
-      result = ToolHelper.runArtRaw(artCommandBuilder);
-    } else {
-      result = ToolHelper.runJava(outputPath, MAIN_CLASS);
-    }
-    assertNotEquals(result.exitCode, 0);
+            .addKeepMainRule(MAIN_CLASS)
+            .noMinification()
+            .addKeepAttributeSourceFile()
+            .addKeepAttributeLineNumberTable()
+            .addOptionsModification(options -> options.inliningInstructionLimit = 40)
+            .run(parameters.getRuntime(), MAIN_CLASS)
+            .assertFailure();
 
     // Verify stack trace.
     // result.stderr looks like this:
@@ -152,7 +114,7 @@
     //     Exception in thread "main" java.lang.RuntimeException: <FOO1-exception>
     //       at
     // com.android.tools.r8.debuginfo.InliningWithoutPositionsTestSource.main(InliningWithoutPositionsTestSource.java:1)
-    String[] lines = result.stderr.split("\n");
+    String[] lines = result.getStdErr().split("\n");
 
     // The line containing 'java.lang.RuntimeException' should contain the expected message, which
     // is "LOCATIONCODE-exception>"
@@ -189,7 +151,7 @@
     //     1:1:void bar():0:0 -> main
     //     1:1:void foo(boolean):0 -> main
     //     1:1:void main(java.lang.String[]):0 -> main
-    ClassNameMapper mapper = ClassNameMapper.mapperFromFile(proguardMapPath);
+    ClassNameMapper mapper = ClassNameMapper.mapperFromString(result.proguardMap());
     assertNotNull(mapper);
 
     ClassNamingForNameMapper classNaming = mapper.getClassNaming(TEST_PACKAGE + "." + TEST_CLASS);
diff --git a/src/test/java/com/android/tools/r8/desugar/DesugarInnerClassesInInterfaces.java b/src/test/java/com/android/tools/r8/desugar/DesugarInnerClassesInInterfaces.java
index 993b56f..2d396f7d 100644
--- a/src/test/java/com/android/tools/r8/desugar/DesugarInnerClassesInInterfaces.java
+++ b/src/test/java/com/android/tools/r8/desugar/DesugarInnerClassesInInterfaces.java
@@ -73,6 +73,7 @@
         .setMinApi(parameters.getApiLevel())
         .addKeepAllClassesRule()
         .addKeepAttributeInnerClassesAndEnclosingMethod()
+        .noHorizontalClassMergingOfSynthetics()
         .compile()
         .run(parameters.getRuntime(), TestClass.class)
         .applyIf(
@@ -125,7 +126,7 @@
     static Callable<Class<?>> staticOuter() {
       return new Callable<Class<?>>() {
         @Override
-        public Class<?> call() throws Exception {
+        public Class<?> call() {
           return getClass().getEnclosingClass();
         }
       };
@@ -134,7 +135,7 @@
     default Callable<Class<?>> defaultOuter() {
       return new Callable<Class<?>>() {
         @Override
-        public Class<?> call() throws Exception {
+        public Class<?> call() {
           return getClass().getEnclosingClass();
         }
       };
@@ -145,7 +146,7 @@
     static Callable<Class<?>> staticOuter() {
       class Local implements Callable<Class<?>> {
         @Override
-        public Class<?> call() throws Exception {
+        public Class<?> call() {
           return getClass().getEnclosingClass();
         }
       }
@@ -155,7 +156,7 @@
     default Callable<Class<?>> defaultOuter() {
       class Local implements Callable<Class<?>> {
         @Override
-        public Class<?> call() throws Exception {
+        public Class<?> call() {
           return getClass().getEnclosingClass();
         }
       }
diff --git a/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java b/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
index acfc021..00d6d45 100644
--- a/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/backports/BackportDuplicationTest.java
@@ -91,9 +91,6 @@
 
   @Test
   public void testD8Merging() throws Exception {
-    assumeTrue(
-        "b/147485959: Merging does not happen for CF due to lack of synthetic annotations",
-        parameters.isDexRuntime());
     boolean intermediate = true;
     runD8Merging(intermediate);
   }
diff --git a/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaInStacktraceTest.java b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaInStacktraceTest.java
index 3a923e0..640796d 100644
--- a/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaInStacktraceTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/lambdas/LambdaInStacktraceTest.java
@@ -3,11 +3,13 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.desugar.lambdas;
 
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.utils.StringUtils;
 import java.util.ArrayList;
 import java.util.List;
@@ -35,6 +37,17 @@
       StringUtils.lines(
           "getStacktraceWithFileNames(" + fileName + ")",
           "lambda$main$0(" + fileName + ")",
+          "call(D8$$SyntheticClass)",
+          "main(" + fileName + ")",
+          "getStacktraceWithFileNames(" + fileName + ")",
+          "lambda$main$1(" + fileName + ")",
+          "call(D8$$SyntheticClass)",
+          "main(" + fileName + ")");
+
+  static final String EXPECTED_D8_ANDROID_O =
+      StringUtils.lines(
+          "getStacktraceWithFileNames(" + fileName + ")",
+          "lambda$main$0(" + fileName + ")",
           "call(NULL)",
           "main(" + fileName + ")",
           "getStacktraceWithFileNames(" + fileName + ")",
@@ -43,6 +56,7 @@
           "main(" + fileName + ")");
 
   private final TestParameters parameters;
+  private final boolean isAndroidOOrLater;
   private final boolean isDalvik;
 
   @Parameterized.Parameters(name = "{0}")
@@ -53,6 +67,9 @@
   public LambdaInStacktraceTest(TestParameters parameters) {
     this.parameters = parameters;
     isDalvik = parameters.isDexRuntime() && parameters.getDexRuntimeVersion().isDalvik();
+    isAndroidOOrLater =
+        parameters.isDexRuntime()
+            && parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V8_1_0);
   }
 
   @Test
@@ -71,7 +88,26 @@
         .addInnerClasses(LambdaInStacktraceTest.class)
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), TestRunner.class, Boolean.toString(isDalvik))
-        .assertSuccessWithOutput(EXPECTED_D8);
+        .assertSuccessWithOutput(isAndroidOOrLater ? EXPECTED_D8_ANDROID_O : EXPECTED_D8);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assumeTrue(parameters.getRuntime().isDex());
+    String stdout =
+        testForR8(parameters.getBackend())
+            .addInnerClasses(LambdaInStacktraceTest.class)
+            .setMinApi(parameters.getApiLevel())
+            .addKeepMainRule(TestRunner.class)
+            .addKeepAttributeSourceFile()
+            .addKeepRules("-renamesourcefileattribute SourceFile")
+            .noTreeShaking()
+            .run(parameters.getRuntime(), TestRunner.class, Boolean.toString(isDalvik))
+            .assertSuccess()
+            .getStdOut();
+    assertTrue(
+        StringUtils.splitLines(stdout).stream()
+            .allMatch(s -> s.contains(isAndroidOOrLater ? "NULL" : "SourceFile")));
   }
 
   static class TestRunner {
diff --git a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/Java11D8CompilationTest.java b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/Java11D8CompilationTest.java
index 3d063c5..a4c0ca7 100644
--- a/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/Java11D8CompilationTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/nestaccesscontrol/Java11D8CompilationTest.java
@@ -4,17 +4,32 @@
 
 package com.android.tools.r8.desugar.nestaccesscontrol;
 
+import static com.android.tools.r8.utils.FileUtils.CLASS_EXTENSION;
+import static com.android.tools.r8.utils.InternalOptions.ASM_VERSION;
+import static junit.framework.TestCase.assertEquals;
 import static junit.framework.TestCase.assertTrue;
+import static junit.framework.TestCase.fail;
 
 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.cf.CfVersion;
+import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.synthesis.SyntheticItemsTestUtils;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.ZipUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
 
 @RunWith(Parameterized.class)
 public class Java11D8CompilationTest extends TestBase {
@@ -41,4 +56,83 @@
         .compile()
         .inspect(Java11D8CompilationTest::assertNoNests);
   }
+
+  @Test
+  public void testR8CompiledWithD8ToCf() throws Exception {
+    Path r8Desugared =
+        testForD8(Backend.CF)
+            .addProgramFiles(ToolHelper.R8_WITH_RELOCATED_DEPS_11_JAR)
+            .addLibraryFiles(ToolHelper.getJava8RuntimeJar())
+            .setMinApi(AndroidApiLevel.B)
+            .compile()
+            .inspect(Java11D8CompilationTest::assertNoNests)
+            .writeToZip();
+
+    // Check that the desugared classes has the expected class file versions and that no nest
+    // related attributes remains.
+    ZipUtils.iter(
+        r8Desugared,
+        (entry, input) -> {
+          if (SyntheticItemsTestUtils.isExternalStaticInterfaceCall(
+              Reference.classFromBinaryName(
+                  entry
+                      .getName()
+                      .substring(0, entry.getName().length() - CLASS_EXTENSION.length())))) {
+            assertEquals(CfVersion.V1_8, extractClassFileVersionAndAssertNoNestAttributes(input));
+          } else {
+            assertTrue(
+                extractClassFileVersionAndAssertNoNestAttributes(input)
+                    .isLessThanOrEqualTo(CfVersion.V1_7));
+          }
+        });
+  }
+
+  protected static CfVersion extractClassFileVersionAndAssertNoNestAttributes(InputStream classFile)
+      throws IOException {
+    class ClassFileVersionExtractor extends ClassVisitor {
+      private int version;
+
+      private ClassFileVersionExtractor() {
+        super(ASM_VERSION);
+      }
+
+      @Override
+      public void visit(
+          int version,
+          int access,
+          String name,
+          String signature,
+          String superName,
+          String[] interfaces) {
+        this.version = version;
+      }
+
+      @Override
+      public void visitAttribute(Attribute attribute) {}
+
+      CfVersion getClassFileVersion() {
+        return CfVersion.fromRaw(version);
+      }
+
+      @Override
+      public void visitNestHost(String nestHost) {
+        // ASM will always report the NestHost attribute if present (independently of class
+        // file version).
+        fail();
+      }
+
+      @Override
+      public void visitNestMember(String nestMember) {
+        // ASM will always report the NestHost attribute if present (independently of class
+        // file version).
+        fail();
+      }
+    }
+
+    ClassReader reader = new ClassReader(classFile);
+    ClassFileVersionExtractor extractor = new ClassFileVersionExtractor();
+    reader.accept(
+        extractor, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+    return extractor.getClassFileVersion();
+  }
 }
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 8bcb3a6..b54a7d5 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
@@ -6,14 +6,26 @@
 
 import static com.android.tools.r8.desugar.staticinterfacemethod.InvokeStaticDesugarTest.Library.foo;
 import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+import static org.junit.Assume.assumeFalse;
 
+import com.android.tools.r8.DesugarTestConfiguration;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
 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.google.common.collect.ImmutableList;
 import java.nio.file.Path;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Set;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -23,19 +35,26 @@
 public class InvokeStaticDesugarTest extends TestBase {
 
   private final TestParameters parameters;
+  private final boolean intermediate;
   private final String EXPECTED = "Hello World!";
 
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  @Parameters(name = "{0}, intermediate in first step: {1}")
+  public static Collection<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build(),
+        BooleanUtils.values());
   }
 
-  public InvokeStaticDesugarTest(TestParameters parameters) {
+  public InvokeStaticDesugarTest(TestParameters parameters, boolean intermediate) {
     this.parameters = parameters;
+    this.intermediate = intermediate;
   }
 
   @Test
   public void testDesugar() throws Exception {
+    // Intermediate not used in this test.
+    assumeFalse(intermediate);
+
     final TestRunResult<?> runResult =
         testForDesugaring(parameters)
             .addLibraryClasses(Library.class)
@@ -50,6 +69,57 @@
     }
   }
 
+  @Test
+  public void testDoubleDesugar() throws Exception {
+    // Desugar using API level that cannot leave static interface invokes.
+    Path jar =
+        testForD8(Backend.CF)
+            .addLibraryClasses(Library.class)
+            .addProgramClasses(Main.class)
+            .setMinApi(AndroidApiLevel.B)
+            .setIntermediate(intermediate)
+            .compile()
+            .inspect(i -> assertEquals(1, getSyntheticMethods(i).size()))
+            .writeToZip();
+
+    testForDesugaring(parameters)
+        .addLibraryClasses(Library.class)
+        .addProgramFiles(jar)
+        .addRunClasspathFiles(compileRunClassPath())
+        .run(parameters.getRuntime(), Main.class)
+        .applyIf(
+            // When double desugaring to API level below L two synthetics are seen.
+            c ->
+                DesugarTestConfiguration.isDesugared(c)
+                    && (parameters.isCfRuntime()
+                        || parameters
+                            .getRuntime()
+                            .asDex()
+                            .getVm()
+                            .isNewerThan(DexVm.ART_4_4_4_HOST))
+                    && parameters.getApiLevel().isLessThan(AndroidApiLevel.L),
+            r -> {
+              assertEquals(intermediate ? 1 : 2, countSynthetics(r));
+              r.assertSuccessWithOutputLines(EXPECTED);
+            },
+            // Don't inspect failing code, as inspection is only supported when run succeeds,
+            // and testForDesugaring does not have separate compile where the code can be
+            // inspected before running.
+            c ->
+                parameters.isDexRuntime()
+                    && parameters
+                        .getRuntime()
+                        .asDex()
+                        .getVm()
+                        .isOlderThanOrEqual(DexVm.ART_4_4_4_HOST),
+            r -> r.assertFailureWithErrorThatMatches(containsString("java.lang.VerifyError")),
+            // When double desugaring to API level L and above one synthetics seen.
+            r -> {
+              assertEquals(1, countSynthetics(r));
+              r.assertSuccessWithOutputLines(EXPECTED);
+            });
+  }
+
   private Path compileRunClassPath() throws Exception {
     if (parameters.isCfRuntime()) {
       return compileToZip(parameters, ImmutableList.of(), Library.class);
@@ -68,6 +138,34 @@
     }
   }
 
+  private int countSynthetics(TestRunResult<?> r) {
+    IntBox box = new IntBox();
+    try {
+      r.inspect(inspector -> box.set(getSyntheticMethods(inspector).size()));
+    } catch (Exception e) {
+      box.set(-1);
+      fail();
+    }
+    return box.get();
+  }
+
+  private Set<MethodReference> getSyntheticMethods(CodeInspector inspector) {
+    Set<MethodReference> methods = new HashSet<>();
+    assert inspector.allClasses().stream()
+        .allMatch(
+            c ->
+                !SyntheticItemsTestUtils.isExternalSynthetic(c.getFinalReference())
+                    || SyntheticItemsTestUtils.isExternalStaticInterfaceCall(
+                        c.getFinalReference()));
+    inspector.allClasses().stream()
+        .filter(c -> SyntheticItemsTestUtils.isExternalStaticInterfaceCall(c.getFinalReference()))
+        .forEach(
+            c ->
+                c.allMethods(m -> !m.isInstanceInitializer())
+                    .forEach(m -> methods.add(m.asMethodReference())));
+    return methods;
+  }
+
   public interface Library {
 
     static void foo() {
diff --git a/src/test/java/com/android/tools/r8/desugar/twr/TwrCloseResourceDuplicationTest.java b/src/test/java/com/android/tools/r8/desugar/twr/TwrCloseResourceDuplicationTest.java
index aac4a9e..1f15b61 100644
--- a/src/test/java/com/android/tools/r8/desugar/twr/TwrCloseResourceDuplicationTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/twr/TwrCloseResourceDuplicationTest.java
@@ -9,6 +9,7 @@
 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.StringUtils;
 import com.android.tools.r8.utils.ZipUtils;
 import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
@@ -102,9 +103,8 @@
               // R8 will optimize the generated methods for the two cases below where the thrown
               // exception is known or not, thus the synthetic methods will be 2.
               int expectedSynthetics =
-                  parameters.getApiLevel().isLessThan(apiLevelWithTwrCloseResourceSupport())
-                      ? 2
-                      : 0;
+                  BooleanUtils.intValue(
+                      parameters.getApiLevel().isLessThan(apiLevelWithTwrCloseResourceSupport()));
               List<FoundClassSubject> foundClassSubjects = inspector.allClasses();
               assertEquals(INPUT_CLASSES + expectedSynthetics, foundClassSubjects.size());
             });
diff --git a/src/test/java/com/android/tools/r8/graph/genericsignature/ClassSignatureTest.java b/src/test/java/com/android/tools/r8/graph/genericsignature/ClassSignatureTest.java
index 12a819d..75306f7 100644
--- a/src/test/java/com/android/tools/r8/graph/genericsignature/ClassSignatureTest.java
+++ b/src/test/java/com/android/tools/r8/graph/genericsignature/ClassSignatureTest.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GenericSignature;
 import com.android.tools.r8.graph.GenericSignature.ClassSignature;
+import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
 import com.android.tools.r8.graph.GenericSignaturePrinter;
 import com.android.tools.r8.graph.GenericSignatureTypeRewriter;
 import com.android.tools.r8.naming.NamingLens;
@@ -162,30 +163,29 @@
   public void testPruningInterfaceBound() {
     DexItemFactory factory = new DexItemFactory();
     DexType context = factory.createType("Lj$/util/stream/Node$OfPrimitive;");
-    String className = "j$.util.stream.Node$OfPrimitive";
+    String methodName = "j$.util.stream.Node$OfPrimitive.foo()";
     TestDiagnosticMessagesImpl testDiagnosticMessages = new TestDiagnosticMessagesImpl();
-    ClassSignature parsedClassSignature =
-        GenericSignature.parseClassSignature(
-            className,
-            "<T_SPLITR::Lj$/util/Spliterator$OfPrimitive;T_NODE:Ljava/lang/Object;>"
-                + "Ljava/lang/Object;",
+    MethodTypeSignature parsedMethodSignature =
+        GenericSignature.parseMethodSignature(
+            methodName,
+            "<T_SPLITR::Lj$/util/Spliterator$OfPrimitive;T_NODE:Ljava/lang/Object;>()V",
             Origin.unknown(),
             factory,
             testDiagnosticMessages);
     testDiagnosticMessages.assertNoMessages();
-    assertTrue(parsedClassSignature.hasSignature());
+    assertTrue(parsedMethodSignature.hasSignature());
     GenericSignatureTypeRewriter rewriter =
         new GenericSignatureTypeRewriter(
             factory,
             dexType -> dexType.toDescriptorString().equals("Lj$/util/Spliterator$OfPrimitive;"),
             Function.identity(),
-            context);
-    ClassSignature rewritten = rewriter.rewrite(parsedClassSignature);
+            null);
+    MethodTypeSignature rewritten = rewriter.rewrite(parsedMethodSignature);
     assertNotNull(rewritten);
     assertTrue(rewritten.hasSignature());
-    ClassSignature reparsed =
-        GenericSignature.parseClassSignature(
-            className, rewritten.toString(), Origin.unknown(), factory, testDiagnosticMessages);
+    MethodTypeSignature reparsed =
+        GenericSignature.parseMethodSignature(
+            methodName, rewritten.toString(), Origin.unknown(), factory, testDiagnosticMessages);
     assertTrue(reparsed.hasSignature());
     testDiagnosticMessages.assertNoMessages();
     assertEquals(rewritten.toString(), reparsed.toString());
diff --git a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureEnclosingTest.java b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureEnclosingTest.java
new file mode 100644
index 0000000..5eccad7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureEnclosingTest.java
@@ -0,0 +1,136 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.graph.genericsignature;
+
+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 static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.graph.genericsignature.GenericSignatureEnclosingTest.Bar.Inner;
+import com.android.tools.r8.graph.genericsignature.GenericSignatureEnclosingTest.Bar.SubInner;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.util.List;
+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 GenericSignatureEnclosingTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final boolean isCompat;
+  private final String objectDescriptor = "Ljava/lang/Object;";
+
+  @Parameters(name = "{0}, isCompat: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(), BooleanUtils.values());
+  }
+
+  public GenericSignatureEnclosingTest(TestParameters parameters, boolean isCompat) {
+    this.parameters = parameters;
+    this.isCompat = isCompat;
+  }
+
+  @Test()
+  public void testR8() throws Exception {
+    (isCompat ? testForR8Compat(parameters.getBackend()) : testForR8(parameters.getBackend()))
+        .addInnerClasses(getClass())
+        .addKeepClassAndMembersRules(Foo.class, Bar.class, Bar.Inner.class, Bar.SubInner.class)
+        .addKeepMainRule(Main.class)
+        .addKeepClassRules(Foo.class)
+        .addKeepAttributeSignature()
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(
+            options -> {
+              options.horizontalClassMergerOptions().disable();
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(
+            "Bar::enclosingMethod", "Hello World", "Bar::enclosingMethod2", "Hello World")
+        .inspect(
+            inspector -> {
+              inspectEnclosingClass(inspector, "$1", descriptor(Main.class));
+              inspectEnclosingClass(inspector, "$2", objectDescriptor);
+
+              ClassSubject inner = inspector.clazz(Inner.class);
+              assertThat(inner, isPresent());
+              assertTrue(inner.getDexProgramClass().getInnerClasses().isEmpty());
+              assertEquals(
+                  "<T:" + objectDescriptor + ">" + objectDescriptor,
+                  inner.getFinalSignatureAttribute());
+
+              ClassSubject subInner = inspector.clazz(SubInner.class);
+              assertThat(subInner, isPresent());
+              assertTrue(subInner.getDexProgramClass().getInnerClasses().isEmpty());
+              assertEquals(
+                  "L" + binaryName(Bar.Inner.class) + "<Ljava/lang/String;>;",
+                  subInner.getFinalSignatureAttribute());
+            });
+  }
+
+  private void inspectEnclosingClass(CodeInspector inspector, String suffix, String secondArg) {
+    ClassSubject enclosing = inspector.clazz(Bar.class.getTypeName() + suffix);
+    assertThat(enclosing, isPresent());
+    assertNull(enclosing.getDexProgramClass().getEnclosingMethodAttribute());
+    assertEquals(
+        isCompat ? "L" + binaryName(Foo.class) + "<Ljava/lang/Object;" + secondArg + ">;" : null,
+        enclosing.getFinalSignatureAttribute());
+  }
+
+  public abstract static class Foo<T, R> {
+
+    R foo(T r) {
+      System.out.println("Hello World");
+      return null;
+    }
+  }
+
+  public static class Bar<S> {
+
+    public class Inner<T> {}
+
+    public class SubInner extends Bar<Integer>.Inner<String> {}
+
+    public static <T, R extends Main> Foo<T, R> enclosingMethod() {
+      return new Foo<T, R>() {
+        @Override
+        R foo(T r) {
+          System.out.println("Bar::enclosingMethod");
+          return super.foo(r);
+        }
+      };
+    }
+
+    public static <T, R> Foo<T, R> enclosingMethod2() {
+      return new Foo<T, R>() {
+        @Override
+        R foo(T r) {
+          System.out.println("Bar::enclosingMethod2");
+          return super.foo(r);
+        }
+      };
+    }
+
+    public static void run() {
+      enclosingMethod().foo(null);
+      enclosingMethod2().foo(null);
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      Bar.run();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePrunedInterfacesKeepTest.java b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePrunedInterfacesKeepTest.java
new file mode 100644
index 0000000..0e09d6a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePrunedInterfacesKeepTest.java
@@ -0,0 +1,73 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.graph.genericsignature;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import java.lang.reflect.Type;
+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 GenericSignaturePrunedInterfacesKeepTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final String[] EXPECTED =
+      new String[] {
+        "interface com.android.tools.r8.graph.genericsignature"
+            + ".GenericSignaturePrunedInterfacesKeepTest$J",
+        "com.android.tools.r8.graph.genericsignature"
+            + ".GenericSignaturePrunedInterfacesKeepTest$J<java.lang.Object>"
+      };
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public GenericSignaturePrunedInterfacesKeepTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepAllClassesRule()
+        .addKeepAttributeSignature()
+        .addKeepAttributeInnerClassesAndEnclosingMethod()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  public interface I {}
+
+  public interface J<T> {}
+
+  public static class A implements I {}
+
+  public static class B extends A implements I, J<Object> {
+
+    public static void foo() {
+      for (Type genericInterface : B.class.getInterfaces()) {
+        System.out.println(genericInterface);
+      }
+      for (Type genericInterface : B.class.getGenericInterfaces()) {
+        System.out.println(genericInterface);
+      }
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      B.foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePrunedInterfacesObfuscationTest.java b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePrunedInterfacesObfuscationTest.java
new file mode 100644
index 0000000..7cf462c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePrunedInterfacesObfuscationTest.java
@@ -0,0 +1,72 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.graph.genericsignature;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import java.lang.reflect.Type;
+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 GenericSignaturePrunedInterfacesObfuscationTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final String[] EXPECTED = new String[] {"interface a.b", "a.b<java.lang.Object>"};
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public GenericSignaturePrunedInterfacesObfuscationTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8Compat(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .addKeepClassRules(I.class, A.class)
+        .addKeepClassRulesWithAllowObfuscation(J.class)
+        .addKeepAttributeSignature()
+        .addKeepAttributeInnerClassesAndEnclosingMethod()
+        .enableInliningAnnotations()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  public interface I {}
+
+  public interface J<T> {}
+
+  public static class A implements I {}
+
+  public static class B extends A implements I, J<Object> {
+
+    @NeverInline
+    public static void foo() {
+      for (Type genericInterface : B.class.getInterfaces()) {
+        System.out.println(genericInterface);
+      }
+      for (Type genericInterface : B.class.getGenericInterfaces()) {
+        System.out.println(genericInterface);
+      }
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      B.foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePrunedInterfacesTest.java b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePrunedInterfacesTest.java
new file mode 100644
index 0000000..85cee97
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignaturePrunedInterfacesTest.java
@@ -0,0 +1,77 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.graph.genericsignature;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import java.lang.reflect.Type;
+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 GenericSignaturePrunedInterfacesTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final String[] EXPECTED =
+      new String[] {
+        "interface com.android.tools.r8.graph.genericsignature"
+            + ".GenericSignaturePrunedInterfacesTest$J",
+        "com.android.tools.r8.graph.genericsignature"
+            + ".GenericSignaturePrunedInterfacesTest$J<java.lang.Object>"
+      };
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public GenericSignaturePrunedInterfacesTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8Compat(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .addKeepClassRules(I.class, J.class, A.class)
+        .addKeepAttributeSignature()
+        .addKeepAttributeInnerClassesAndEnclosingMethod()
+        .enableInliningAnnotations()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  public interface I {}
+
+  public interface J<T> {}
+
+  public static class A implements I {}
+
+  public static class B extends A implements I, J<Object> {
+
+    @NeverInline
+    public static void foo() {
+      for (Type genericInterface : B.class.getInterfaces()) {
+        System.out.println(genericInterface);
+      }
+      for (Type genericInterface : B.class.getGenericInterfaces()) {
+        System.out.println(genericInterface);
+      }
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      B.foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureStaticMethodTest.java b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureStaticMethodTest.java
new file mode 100644
index 0000000..05c8aa5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/graph/genericsignature/GenericSignatureStaticMethodTest.java
@@ -0,0 +1,88 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.graph.genericsignature;
+
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.graph.AccessFlags;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+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 GenericSignatureStaticMethodTest extends TestBase {
+
+  private final String[] EXPECTED =
+      new String[] {
+        "true",
+        "public class com.android.tools.r8.graph.genericsignature"
+            + ".GenericSignatureStaticMethodTest$Main<T>"
+      };
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public GenericSignatureStaticMethodTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramClassFileData(
+            transformer(Main.class)
+                .removeInnerClasses()
+                .setAccessFlags(Main.class.getDeclaredMethod("test"), AccessFlags::setStatic)
+                .transform())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForR8(parameters.getBackend())
+        .addProgramClassFileData(
+            transformer(Main.class)
+                .setAccessFlags(Main.class.getDeclaredMethod("test"), AccessFlags::setStatic)
+                .transform())
+        .addKeepAllClassesRule()
+        .addKeepAttributeSignature()
+        .addKeepAttributeInnerClassesAndEnclosingMethod()
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  public static class Main<T> {
+
+    public static void main(String[] args) throws Exception {
+      Method test = Main.class.getDeclaredMethod("test");
+      System.out.println(Modifier.isStatic(test.getModifiers()));
+      Type genericReturnType = test.getGenericReturnType();
+      if (genericReturnType instanceof TypeVariable) {
+        Class<?> genericDeclaration =
+            (Class<?>) ((TypeVariable<?>) genericReturnType).getGenericDeclaration();
+        System.out.println(genericDeclaration.toGenericString());
+      }
+    }
+
+    public /* static after rewriting */ T test() {
+      return null;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/internal/GMSCoreCompilationTestBase.java b/src/test/java/com/android/tools/r8/internal/GMSCoreCompilationTestBase.java
index 4ed740b..93656d1 100644
--- a/src/test/java/com/android/tools/r8/internal/GMSCoreCompilationTestBase.java
+++ b/src/test/java/com/android/tools/r8/internal/GMSCoreCompilationTestBase.java
@@ -6,5 +6,7 @@
 public abstract class GMSCoreCompilationTestBase extends CompilationTestBase {
   // Files pertaining to the full GMSCore build.
   static final String PG_CONF = "GmsCore_prod_alldpi_release_all_locales_proguard.config";
+  static final String PG_MAP = "GmsCore_prod_alldpi_release_all_locales_proguard.map";
   static final String DEPLOY_JAR = "GmsCore_prod_alldpi_release_all_locales_deploy.jar";
+  static final String RELEASE_APK_X86 = "x86_GmsCore_prod_alldpi_release.apk";
 }
diff --git a/src/test/java/com/android/tools/r8/internal/R8DisassemblerTest.java b/src/test/java/com/android/tools/r8/internal/GMSCoreV10DisassemblerTest.java
similarity index 67%
rename from src/test/java/com/android/tools/r8/internal/R8DisassemblerTest.java
rename to src/test/java/com/android/tools/r8/internal/GMSCoreV10DisassemblerTest.java
index 192b554..014f5d3 100644
--- a/src/test/java/com/android/tools/r8/internal/R8DisassemblerTest.java
+++ b/src/test/java/com/android/tools/r8/internal/GMSCoreV10DisassemblerTest.java
@@ -3,16 +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.ToolHelper.DEFAULT_PROGUARD_MAP_FILE;
-
 import com.android.tools.r8.Disassemble;
-import com.android.tools.r8.utils.FileUtils;
 import java.io.OutputStream;
 import java.io.PrintStream;
-import java.nio.file.Files;
 import java.nio.file.Paths;
 import java.util.Arrays;
-import java.util.stream.Collectors;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -21,14 +16,14 @@
 
 // Invoke R8 on the dex files extracted from GMSCore.apk to disassemble the dex code.
 @RunWith(Parameterized.class)
-public class R8DisassemblerTest {
+public class GMSCoreV10DisassemblerTest extends GMSCoreCompilationTestBase {
 
-  static final String APP_DIR = "third_party/gmscore/v5/";
+  private static final String APP_DIR = "third_party/gmscore/gmscore_v10/";
 
   @Parameters(name = "deobfuscate: {0} smali: {1}")
   public static Iterable<Object[]> data() {
-    return Arrays
-        .asList(new Object[][]{{false, false}, {false, true}, {true, false}, {true, true}});
+    return Arrays.asList(
+        new Object[][] {{false, false}, {false, true}, {true, false}, {true, true}});
   }
 
   @Parameter(0)
@@ -42,20 +37,22 @@
     // This test only ensures that we do not break disassembling of dex code. It does not
     // check the generated code. To make it fast, we get rid of the output.
     PrintStream originalOut = System.out;
-    System.setOut(new PrintStream(new OutputStream() {
-      public void write(int b) { /* ignore*/ }
-    }));
+    System.setOut(
+        new PrintStream(
+            new OutputStream() {
+              @Override
+              public void write(int b) {
+                // Intentionally empty.
+              }
+            }));
 
     try {
       Disassemble.DisassembleCommand.Builder builder = Disassemble.DisassembleCommand.builder();
       builder.setUseSmali(smali);
       if (deobfuscate) {
-        builder.setProguardMapFile(Paths.get(APP_DIR, DEFAULT_PROGUARD_MAP_FILE));
+        builder.setProguardMapFile(Paths.get(APP_DIR, PG_MAP));
       }
-      builder.addProgramFiles(
-          Files.list(Paths.get(APP_DIR))
-              .filter(FileUtils::isDexFile)
-              .collect(Collectors.toList()));
+      builder.addProgramFiles(Paths.get(APP_DIR).resolve(RELEASE_APK_X86));
       Disassemble.DisassembleCommand command = builder.build();
       Disassemble.disassemble(command);
     } finally {
diff --git a/src/test/java/com/android/tools/r8/internal/GMSCoreV10ProguardMapReaderTest.java b/src/test/java/com/android/tools/r8/internal/GMSCoreV10ProguardMapReaderTest.java
new file mode 100644
index 0000000..2c2742a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/internal/GMSCoreV10ProguardMapReaderTest.java
@@ -0,0 +1,24 @@
+// Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.internal;
+
+import com.android.tools.r8.naming.ClassNameMapper;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class GMSCoreV10ProguardMapReaderTest extends GMSCoreCompilationTestBase {
+
+  private static final String APP_DIR = "third_party/gmscore/gmscore_v10/";
+
+  @Test
+  public void roundTripTestGmsCoreV10() throws IOException {
+    Path map = Paths.get(APP_DIR).resolve(PG_MAP);
+    ClassNameMapper firstMapper = ClassNameMapper.mapperFromFile(map);
+    ClassNameMapper secondMapper = ClassNameMapper.mapperFromString(firstMapper.toString());
+    Assert.assertEquals(firstMapper, secondMapper);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/internal/ProguardMapReaderGMSCoreTest.java b/src/test/java/com/android/tools/r8/internal/ProguardMapReaderGMSCoreTest.java
deleted file mode 100644
index f3fdca4..0000000
--- a/src/test/java/com/android/tools/r8/internal/ProguardMapReaderGMSCoreTest.java
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.internal;
-
-import com.android.tools.r8.naming.ClassNameMapper;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import org.junit.Assert;
-import org.junit.Test;
-
-public class ProguardMapReaderGMSCoreTest {
-
-  public static final String GMSCORE_V4_MAP = "third_party/gmscore/v4/proguard.map";
-  public static final String GMSCORE_V5_MAP = "third_party/gmscore/v5/proguard.map";
-  public static final String GMSCORE_V6_MAP = "third_party/gmscore/v6/proguard.map";
-  public static final String GMSCORE_V7_MAP = "third_party/gmscore/v7/proguard.map";
-  public static final String GMSCORE_V8_MAP = "third_party/gmscore/v8/proguard.map";
-  public static final String GMSCORE_V9_MAP =
-      "third_party/gmscore/gmscore_v9/GmsCore_prod_alldpi_release_all_locales_proguard.map";
-  public static final String GMSCORE_V10_MAP =
-      "third_party/gmscore/gmscore_v10/GmsCore_prod_alldpi_release_all_locales_proguard.map";
-
-  public void roundTripTest(Path path) throws IOException {
-    ClassNameMapper firstMapper = ClassNameMapper.mapperFromFile(path);
-    ClassNameMapper secondMapper = ClassNameMapper.mapperFromString(firstMapper.toString());
-    Assert.assertEquals(firstMapper, secondMapper);
-  }
-
-  @Test
-  public void roundTripTestGmsCoreV4() throws IOException {
-    roundTripTest(Paths.get(GMSCORE_V4_MAP));
-  }
-
-  @Test
-  public void roundTripTestGmsCoreV5() throws IOException {
-    roundTripTest(Paths.get(GMSCORE_V5_MAP));
-  }
-
-  @Test
-  public void roundTripTestGmsCoreV6() throws IOException {
-    roundTripTest(Paths.get(GMSCORE_V6_MAP));
-  }
-
-  @Test
-  public void roundTripTestGmsCoreV7() throws IOException {
-    roundTripTest(Paths.get(GMSCORE_V7_MAP));
-  }
-
-  @Test
-  public void roundTripTestGmsCoreV8() throws IOException {
-    roundTripTest(Paths.get(GMSCORE_V8_MAP));
-  }
-
-  @Test
-  public void roundTripTestGmsCoreV9() throws IOException {
-    roundTripTest(Paths.get(GMSCORE_V9_MAP));
-  }
-
-  @Test
-  public void roundTripTestGmsCoreV10() throws IOException {
-    roundTripTest(Paths.get(GMSCORE_V10_MAP));
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/SyntheticInitClassPositionTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SyntheticInitClassPositionTest.java
index 1dc11c0..fe6ef20 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/SyntheticInitClassPositionTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SyntheticInitClassPositionTest.java
@@ -52,7 +52,6 @@
         .addKeepAttributeLineNumberTable()
         .addKeepAttributeSourceFile()
         .setMinApi(parameters.getApiLevel())
-        .compile()
         .run(parameters.getRuntime(), Main.class)
         .assertFailureWithErrorThatThrows(ExceptionInInitializerError.class)
         .inspectStackTrace(stackTrace -> assertThat(stackTrace, isSame(expectedStackTrace)));
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/OutlineFromStaticInterfaceMethodTest.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/OutlineFromStaticInterfaceMethodTest.java
index a86ee63..0531857 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/outliner/OutlineFromStaticInterfaceMethodTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/OutlineFromStaticInterfaceMethodTest.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.ir.desugar.itf.InterfaceMethodRewriter;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import org.junit.Test;
@@ -49,8 +50,11 @@
               options.outline.threshold = 2;
               options.outline.minSize = 2;
             })
+        .addHorizontallyMergedClassesInspector(
+            HorizontallyMergedClassesInspector::assertNoClassesMerged)
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
+        .noHorizontalClassMergingOfSynthetics()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(this::inspect)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/b112247415/B112247415.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/b112247415/B112247415.java
index b4dd6b7..1e5ef3f 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/outliner/b112247415/B112247415.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/b112247415/B112247415.java
@@ -62,7 +62,7 @@
 
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().build();
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
   private final TestParameters parameters;
@@ -80,23 +80,26 @@
           .assertSuccessWithOutput(EXPECTED);
     }
 
-    CodeInspector inspector = testForR8(parameters.getBackend())
-        .noMinification()
-        .setMinApi(parameters.getRuntime())
-        .addProgramClassesAndInnerClasses(TestClass.class)
-        .addKeepMainRule(TestClass.class)
-        .addOptionsModification(options -> {
-          if (parameters.isCfRuntime()) {
-            assert !options.outline.enabled;
-            options.outline.enabled = true;
-          }
-          // To trigger outliner, set # of expected outline candidate as threshold.
-          options.outline.threshold = 2;
-          options.enableInlining = false;
-        })
-        .run(parameters.getRuntime(), TestClass.class)
-        .assertSuccessWithOutput(EXPECTED)
-        .inspector();
+    CodeInspector inspector =
+        testForR8(parameters.getBackend())
+            .noMinification()
+            .setMinApi(parameters.getApiLevel())
+            .addProgramClassesAndInnerClasses(TestClass.class)
+            .addKeepMainRule(TestClass.class)
+            .addOptionsModification(
+                options -> {
+                  if (parameters.isCfRuntime()) {
+                    assert !options.outline.enabled;
+                    options.outline.enabled = true;
+                  }
+                  // To trigger outliner, set # of expected outline candidate as threshold.
+                  options.outline.threshold = 2;
+                  options.enableInlining = false;
+                })
+            .noHorizontalClassMergingOfSynthetics()
+            .run(parameters.getRuntime(), TestClass.class)
+            .assertSuccessWithOutput(EXPECTED)
+            .inspector();
 
     for (FoundClassSubject clazz : inspector.allClasses()) {
       if (!SyntheticItemsTestUtils.isExternalOutlineClass(clazz.getFinalReference())) {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/outliner/b149971007/B149971007.java b/src/test/java/com/android/tools/r8/ir/optimize/outliner/b149971007/B149971007.java
index dd6dace..0d1f637 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/outliner/b149971007/B149971007.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/outliner/b149971007/B149971007.java
@@ -6,6 +6,7 @@
 
 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.notIf;
 import static junit.framework.TestCase.assertEquals;
 import static junit.framework.TestCase.assertFalse;
 import static junit.framework.TestCase.assertTrue;
@@ -25,7 +26,6 @@
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.nio.file.Path;
-import java.util.ArrayList;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -66,18 +66,23 @@
     return false;
   }
 
-  private void checkOutlineFromFeature(CodeInspector inspector) {
-    // There are two expected outlines, each is currently in its own class.
-    List<FoundMethodSubject> allMethods = new ArrayList<>();
-    for (int i = 0; i < 2; i++) {
-      ClassSubject clazz =
-          inspector.clazz(SyntheticItemsTestUtils.syntheticOutlineClass(FeatureClass.class, i));
-      assertThat(clazz, isPresent());
-      assertEquals(1, clazz.allMethods().size());
-      allMethods.addAll(clazz.allMethods());
-    }
+  private ClassSubject checkOutlineFromFeature(CodeInspector inspector) {
+    // There are two expected outlines, in a single single class after horizontal class merging.
+    ClassSubject classSubject0 =
+        inspector.clazz(SyntheticItemsTestUtils.syntheticOutlineClass(FeatureClass.class, 0));
+    ClassSubject classSubject1 =
+        inspector.clazz(SyntheticItemsTestUtils.syntheticOutlineClass(FeatureClass.class, 1));
+    assertThat(classSubject1, notIf(isPresent(), classSubject0.isPresent()));
+
+    ClassSubject classSubject = classSubject0.isPresent() ? classSubject0 : classSubject1;
+
+    List<FoundMethodSubject> allMethods = classSubject.allMethods();
+    assertEquals(2, allMethods.size());
+
     // One of the methods is StringBuilder the other references the feature.
     assertTrue(allMethods.stream().anyMatch(this::referenceFeatureClass));
+
+    return classSubject;
   }
 
   @Test
@@ -89,33 +94,24 @@
             .addKeepClassAndMembersRules(FeatureClass.class)
             .setMinApi(parameters.getApiLevel())
             .addOptionsModification(options -> options.outline.threshold = 2)
-            .compile()
-            .inspect(this::checkOutlineFromFeature);
+            .compile();
+
+    CodeInspector inspector = compileResult.inspector();
+    ClassSubject outlineClass = checkOutlineFromFeature(inspector);
 
     // Check that parts of method1, ..., method4 in FeatureClass was outlined.
-    ClassSubject featureClass = compileResult.inspector().clazz(FeatureClass.class);
+    ClassSubject featureClass = inspector.clazz(FeatureClass.class);
     assertThat(featureClass, isPresent());
 
     // Find the final names of the two outline classes.
-    String outlineClassName0 =
-        ClassNameMapper.mapperFromString(compileResult.getProguardMap())
-            .getObfuscatedToOriginalMapping()
-            .inverse
-            .get(
-                SyntheticItemsTestUtils.syntheticOutlineClass(FeatureClass.class, 0).getTypeName());
-    String outlineClassName1 =
-        ClassNameMapper.mapperFromString(compileResult.getProguardMap())
-            .getObfuscatedToOriginalMapping()
-            .inverse
-            .get(
-                SyntheticItemsTestUtils.syntheticOutlineClass(FeatureClass.class, 1).getTypeName());
+    String outlineClassName = outlineClass.getFinalName();
 
     // Verify they are called from the feature methods.
     // Note: should the choice of synthetic grouping change these expectations will too.
-    assertTrue(invokesOutline(featureClass.uniqueMethodWithName("method1"), outlineClassName0));
-    assertTrue(invokesOutline(featureClass.uniqueMethodWithName("method2"), outlineClassName0));
-    assertTrue(invokesOutline(featureClass.uniqueMethodWithName("method3"), outlineClassName1));
-    assertTrue(invokesOutline(featureClass.uniqueMethodWithName("method4"), outlineClassName1));
+    assertTrue(invokesOutline(featureClass.uniqueMethodWithName("method1"), outlineClassName));
+    assertTrue(invokesOutline(featureClass.uniqueMethodWithName("method2"), outlineClassName));
+    assertTrue(invokesOutline(featureClass.uniqueMethodWithName("method3"), outlineClassName));
+    assertTrue(invokesOutline(featureClass.uniqueMethodWithName("method4"), outlineClassName));
 
     compileResult.run(parameters.getRuntime(), TestClass.class).assertSuccessWithOutput("123456");
   }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
index d268888..5a60806 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerTest.java
@@ -14,7 +14,6 @@
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.R8TestRunResult;
-import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -117,6 +116,7 @@
         .addKeepAttributes("InnerClasses", "EnclosingMethod")
         .addOptionsModification(this::configure)
         .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), main)
         .assertSuccessWithOutput(EXPECTED);
@@ -124,10 +124,11 @@
 
   @Test
   public void testTrivial() throws Exception {
-    SingleTestRunResult result =
+    R8TestRunResult result =
         testForR8(parameters.getBackend())
             .addProgramClasses(classes)
             .enableInliningAnnotations()
+            .enableNoHorizontalClassMergingAnnotations()
             .addKeepMainRule(main)
             .noMinification()
             .addKeepAttributes("InnerClasses", "EnclosingMethod")
@@ -241,7 +242,7 @@
         HostOkFieldOnly.class,
         CandidateOkFieldOnly.class
     };
-    SingleTestRunResult result =
+    R8TestRunResult result =
         testForR8(parameters.getBackend())
             .addProgramClasses(classes)
             .enableInliningAnnotations()
@@ -283,6 +284,7 @@
             .addProgramClasses(classes)
             .enableInliningAnnotations()
             .enableNoHorizontalClassMergingAnnotations()
+            .enableNoHorizontalClassMergingAnnotations()
             .enableMemberValuePropagationAnnotations()
             .addKeepMainRule(main)
             .allowAccessModification()
@@ -405,7 +407,7 @@
         Candidate.class
     };
     String javaOutput = runOnJava(main);
-    SingleTestRunResult result =
+    R8TestRunResult result =
         testForR8(parameters.getBackend())
             .addProgramClasses(classes)
             .enableInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/InvokeStaticWithNullOutvalueTest.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/InvokeStaticWithNullOutvalueTest.java
index 4e3f691..fe09d69 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/InvokeStaticWithNullOutvalueTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/InvokeStaticWithNullOutvalueTest.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.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -45,6 +46,7 @@
         .addKeepMainRule(MAIN)
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), MAIN)
         .assertSuccessWithOutputLines("Companion#boo", "Companion#foo")
@@ -79,6 +81,7 @@
   static class Host {
     private static final Companion companion = new Companion();
 
+    @NoHorizontalClassMerging
     static class Companion {
       @NeverInline
       private static Object boo() {
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateConflictField.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateConflictField.java
index 295657a..8c0b01f 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateConflictField.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/movetohost/CandidateConflictField.java
@@ -6,7 +6,9 @@
 
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NeverPropagateValue;
+import com.android.tools.r8.NoHorizontalClassMerging;
 
+@NoHorizontalClassMerging
 public class CandidateConflictField {
 
   @NeverPropagateValue
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithGetter.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithGetter.java
index 6bc91b4..a743f11 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithGetter.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithGetter.java
@@ -5,7 +5,9 @@
 package com.android.tools.r8.ir.optimize.staticizer.trivial;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 
+@NoHorizontalClassMerging
 public class SimpleWithGetter {
   private static SimpleWithGetter INSTANCE = new SimpleWithGetter();
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithParams.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithParams.java
index 24edea4..b02dc73 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithParams.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithParams.java
@@ -5,7 +5,9 @@
 package com.android.tools.r8.ir.optimize.staticizer.trivial;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 
+@NoHorizontalClassMerging
 public class SimpleWithParams {
   static SimpleWithParams INSTANCE = new SimpleWithParams(123);
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithPhi.java b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithPhi.java
index f3c76cb..24786f4 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithPhi.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/staticizer/trivial/SimpleWithPhi.java
@@ -4,7 +4,9 @@
 package com.android.tools.r8.ir.optimize.staticizer.trivial;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 
+@NoHorizontalClassMerging
 public class SimpleWithPhi {
   public static class Companion {
     @NeverInline
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/b159688129/LambdaGroupGCLimitTest.java b/src/test/java/com/android/tools/r8/kotlin/lambda/b159688129/LambdaGroupGCLimitTest.java
index 1f82c8b..0a80034 100644
--- a/src/test/java/com/android/tools/r8/kotlin/lambda/b159688129/LambdaGroupGCLimitTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/b159688129/LambdaGroupGCLimitTest.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.InternalOptions.HorizontalClassMergerOptions;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
@@ -66,7 +67,7 @@
             .addHorizontallyMergedClassesInspector(
                 inspector -> {
                   HorizontalClassMergerOptions defaultHorizontalClassMergerOptions =
-                      new HorizontalClassMergerOptions();
+                      new InternalOptions().horizontalClassMergerOptions();
                   assertEquals(4833, inspector.getSources().size());
                   assertEquals(167, inspector.getTargets().size());
                   assertTrue(
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java b/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java
index 3b0b052..8dde075 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java
@@ -23,6 +23,7 @@
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
 import org.hamcrest.TypeSafeMatcher;
@@ -284,11 +285,13 @@
     return new StackTrace(stackTraceLines, stderr);
   }
 
+  private static List<StackTraceLine> internalConvert(Stream<String> lines) {
+    return lines.map(StackTraceLine::parse).collect(Collectors.toList());
+  }
+
   private static List<StackTraceLine> internalExtractFromJvm(String stderr) {
-    return StringUtils.splitLines(stderr).stream()
-        .filter(s -> s.startsWith(TAB_AT_PREFIX))
-        .map(StackTraceLine::parse)
-        .collect(Collectors.toList());
+    return internalConvert(
+        StringUtils.splitLines(stderr).stream().filter(s -> s.startsWith(TAB_AT_PREFIX)));
   }
 
   public static StackTrace extractFromJvm(String stderr) {
@@ -324,7 +327,7 @@
             .build(),
         allowExperimentalMapping);
     // Keep the original stderr in the retraced stacktrace.
-    return new StackTrace(internalExtractFromJvm(StringUtils.lines(box.result)), originalStderr);
+    return new StackTrace(internalConvert(box.result.stream()), originalStderr);
   }
 
   public StackTrace filter(Predicate<StackTraceLine> filter) {
diff --git a/src/test/java/com/android/tools/r8/naming/retraceproguard/DesugarStaticInterfaceMethodsRetraceTest.java b/src/test/java/com/android/tools/r8/naming/retraceproguard/DesugarStaticInterfaceMethodsRetraceTest.java
index 47240a5..04f722d 100644
--- a/src/test/java/com/android/tools/r8/naming/retraceproguard/DesugarStaticInterfaceMethodsRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retraceproguard/DesugarStaticInterfaceMethodsRetraceTest.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.naming.retraceproguard.StackTrace.isSameExceptForFileName;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.CompilationMode;
 import com.android.tools.r8.NeverInline;
@@ -51,6 +52,9 @@
 
   @Test
   public void testSourceFileAndLineNumberTable() throws Exception {
+    // TODO(b/186015503): This test fails when mapping via PCs.
+    //  also the test should be updated to use TestParameters and api levels.
+    assumeTrue("b/186015503", !backend.isDex() || mode != CompilationMode.RELEASE);
     runTest(
         ImmutableList.of("-keepattributes SourceFile,LineNumberTable"),
         // For the desugaring to companion classes the retrace stacktrace is still the same
diff --git a/src/test/java/com/android/tools/r8/regress/b150400371/DebuginfoForInlineFrameRegressionTest.java b/src/test/java/com/android/tools/r8/regress/b150400371/DebuginfoForInlineFrameRegressionTest.java
index 68c1d36..90617f7 100644
--- a/src/test/java/com/android/tools/r8/regress/b150400371/DebuginfoForInlineFrameRegressionTest.java
+++ b/src/test/java/com/android/tools/r8/regress/b150400371/DebuginfoForInlineFrameRegressionTest.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.regress.b150400371;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -44,8 +45,12 @@
             (inspector) -> {
               MethodSubject main =
                   inspector.method(InlineInto.class.getDeclaredMethod("main", String[].class));
-              IntSet lines = new IntArraySet(main.getLineNumberTable().getLines());
-              assertEquals(2, lines.size());
+              if (parameters.getApiLevel().isLessThan(apiLevelWithPcAsLineNumberSupport())) {
+                IntSet lines = new IntArraySet(main.getLineNumberTable().getLines());
+                assertEquals(2, lines.size());
+              } else {
+                assertNull(main.getLineNumberTable());
+              }
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/retrace/KotlinInlineFunctionInSameFileRetraceTests.java b/src/test/java/com/android/tools/r8/retrace/KotlinInlineFunctionInSameFileRetraceTests.java
index dfe1002..24fad30 100644
--- a/src/test/java/com/android/tools/r8/retrace/KotlinInlineFunctionInSameFileRetraceTests.java
+++ b/src/test/java/com/android/tools/r8/retrace/KotlinInlineFunctionInSameFileRetraceTests.java
@@ -47,7 +47,11 @@
   public static List<Object[]> data() {
     // TODO(b/141817471): Extend with compilation modes.
     return buildParameters(
-        getTestParameters().withAllRuntimesAndApiLevels().build(),
+        getTestParameters()
+            .withAllRuntimes()
+            // TODO(b/186018416): Update to support tests retracing with PC mappings.
+            .withApiLevelsEndingAtExcluding(apiLevelWithPcAsLineNumberSupport())
+            .build(),
         getKotlinTestParameters().withAllCompilersAndTargetVersions().build());
   }
 
diff --git a/src/test/java/com/android/tools/r8/retrace/KotlinInlineFunctionRetraceTest.java b/src/test/java/com/android/tools/r8/retrace/KotlinInlineFunctionRetraceTest.java
index 232c1f5..ed842eb 100644
--- a/src/test/java/com/android/tools/r8/retrace/KotlinInlineFunctionRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/retrace/KotlinInlineFunctionRetraceTest.java
@@ -47,7 +47,11 @@
   public static List<Object[]> data() {
     // TODO(b/141817471): Extend with compilation modes.
     return buildParameters(
-        getTestParameters().withAllRuntimesAndApiLevels().build(),
+        getTestParameters()
+            .withAllRuntimes()
+            // TODO(b/186018416): Update to support tests retracing with PC mappings.
+            .withApiLevelsEndingAtExcluding(apiLevelWithPcAsLineNumberSupport())
+            .build(),
         getKotlinTestParameters().withAllCompilersAndTargetVersions().build());
   }
 
diff --git a/src/test/java/com/android/tools/r8/retrace/RetraceCommandLineTests.java b/src/test/java/com/android/tools/r8/retrace/RetraceCommandLineTests.java
index cd839f1..c1a704e 100644
--- a/src/test/java/com/android/tools/r8/retrace/RetraceCommandLineTests.java
+++ b/src/test/java/com/android/tools/r8/retrace/RetraceCommandLineTests.java
@@ -54,7 +54,7 @@
 
   private final boolean testExternal;
 
-  @Parameters(name = "{0}")
+  @Parameters(name = "external: {0}")
   public static Boolean[] data() {
     return BooleanUtils.values();
   }
@@ -96,6 +96,13 @@
   }
 
   @Test
+  public void testMissingStackTraceFile() throws IOException {
+    Path mappingFile = folder.newFile("mapping.txt").toPath();
+    Files.write(mappingFile, "foo.bar.baz -> foo:".getBytes());
+    runAbortTest(containsString("NoSuchFileException"), mappingFile.toString(), "stacktrace.txt");
+  }
+
+  @Test
   public void testVerbose() throws IOException {
     FoundMethodVerboseStackTrace stackTrace = new FoundMethodVerboseStackTrace();
     runTest(
diff --git a/src/test/java/com/android/tools/r8/shaking/allowshrinking/ConditionalKeepClassMethodsAllowShrinkingCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/allowshrinking/ConditionalKeepClassMethodsAllowShrinkingCompatibilityTest.java
index f7e50d9..abb4c9f 100644
--- a/src/test/java/com/android/tools/r8/shaking/allowshrinking/ConditionalKeepClassMethodsAllowShrinkingCompatibilityTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/allowshrinking/ConditionalKeepClassMethodsAllowShrinkingCompatibilityTest.java
@@ -9,6 +9,7 @@
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestShrinkerBuilder;
@@ -61,9 +62,12 @@
   @Test
   public void test() throws Exception {
     if (shrinker.isR8()) {
-      run(testForR8(parameters.getBackend()));
+      run(testForR8(parameters.getBackend()).enableNoHorizontalClassMergingAnnotations());
     } else {
-      run(testForProguard(shrinker.getProguardVersion()).addDontWarn(getClass()));
+      run(
+          testForProguard(shrinker.getProguardVersion())
+              .addDontWarn(getClass())
+              .addNoHorizontalClassMergingAnnotations());
     }
   }
 
@@ -113,6 +117,7 @@
     }
   }
 
+  @NoHorizontalClassMerging
   static class B {
     public String foo() {
       return "B::foo";
diff --git a/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepAllowShrinkingCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepAllowShrinkingCompatibilityTest.java
index c9b3bc7..f198845 100644
--- a/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepAllowShrinkingCompatibilityTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepAllowShrinkingCompatibilityTest.java
@@ -9,6 +9,7 @@
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestShrinkerBuilder;
@@ -64,9 +65,13 @@
       run(
           testForR8(parameters.getBackend())
               // Allowing all of shrinking, optimization and obfuscation will amount to a nop rule.
-              .allowUnusedProguardConfigurationRules(allowOptimization && allowObfuscation));
+              .allowUnusedProguardConfigurationRules(allowOptimization && allowObfuscation)
+              .enableNoHorizontalClassMergingAnnotations());
     } else {
-      run(testForProguard(shrinker.getProguardVersion()).addDontWarn(getClass()));
+      run(
+          testForProguard(shrinker.getProguardVersion())
+              .addDontWarn(getClass())
+              .addNoHorizontalClassMergingAnnotations());
     }
   }
 
@@ -107,6 +112,7 @@
     }
   }
 
+  @NoHorizontalClassMerging
   static class B {
     public String foo() {
       return "B::foo";
diff --git a/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassMethodsAllowShrinkingCompatibilityTest.java b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassMethodsAllowShrinkingCompatibilityTest.java
index e7d5191..5a1d0f9 100644
--- a/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassMethodsAllowShrinkingCompatibilityTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/allowshrinking/KeepClassMethodsAllowShrinkingCompatibilityTest.java
@@ -9,6 +9,7 @@
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestShrinkerBuilder;
@@ -64,9 +65,13 @@
       run(
           testForR8(parameters.getBackend())
               // Allowing all of shrinking, optimization and obfuscation will amount to a nop rule.
-              .allowUnusedProguardConfigurationRules(allowOptimization && allowObfuscation));
+              .allowUnusedProguardConfigurationRules(allowOptimization && allowObfuscation)
+              .enableNoHorizontalClassMergingAnnotations());
     } else {
-      run(testForProguard(shrinker.getProguardVersion()).addDontWarn(getClass()));
+      run(
+          testForProguard(shrinker.getProguardVersion())
+              .addDontWarn(getClass())
+              .addNoHorizontalClassMergingAnnotations());
     }
   }
 
@@ -116,6 +121,7 @@
     }
   }
 
+  @NoHorizontalClassMerging
   static class B {
     public String foo() {
       return "B::foo";
diff --git a/src/test/java/com/android/tools/r8/shaking/attributes/KeepAttributesTest.java b/src/test/java/com/android/tools/r8/shaking/attributes/KeepAttributesTest.java
index a314e96..82bcb21 100644
--- a/src/test/java/com/android/tools/r8/shaking/attributes/KeepAttributesTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/attributes/KeepAttributesTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.shaking.attributes;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
@@ -62,6 +63,11 @@
     assertFalse(mainMethod.hasLocalVariableTable());
   }
 
+  private boolean doesNotHavePcSupport() {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isLessThan(apiLevelWithPcAsLineNumberSupport());
+  }
+
   @Test
   public void keepLineNumberTable()
       throws CompilationFailedException, IOException, ExecutionException {
@@ -69,7 +75,7 @@
         "-keepattributes " + ProguardKeepAttributes.LINE_NUMBER_TABLE
     );
     MethodSubject mainMethod = compileRunAndGetMain(keepRules, CompilationMode.RELEASE);
-    assertTrue(mainMethod.hasLineNumberTable());
+    assertEquals(doesNotHavePcSupport(), mainMethod.hasLineNumberTable());
     assertFalse(mainMethod.hasLocalVariableTable());
   }
 
@@ -83,7 +89,7 @@
             + ProguardKeepAttributes.LOCAL_VARIABLE_TABLE
     );
     MethodSubject mainMethod = compileRunAndGetMain(keepRules, CompilationMode.RELEASE);
-    assertTrue(mainMethod.hasLineNumberTable());
+    assertEquals(doesNotHavePcSupport(), mainMethod.hasLineNumberTable());
     // Locals are never included in release builds.
     assertFalse(mainMethod.hasLocalVariableTable());
   }
diff --git a/src/test/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java b/src/test/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
index fe06c06..9e49bc9 100644
--- a/src/test/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
+++ b/src/test/java/com/android/tools/r8/synthesis/SyntheticItemsTestUtils.java
@@ -65,6 +65,24 @@
         originalMethod.getMethodDescriptor());
   }
 
+  public static boolean isExternalSynthetic(ClassReference reference) {
+    for (SyntheticKind kind : SyntheticKind.values()) {
+      if (kind == SyntheticKind.RECORD_TAG) {
+        continue;
+      }
+      if (kind.isFixedSuffixSynthetic) {
+        if (SyntheticNaming.isSynthetic(reference, null, kind)) {
+          return true;
+        }
+      } else {
+        if (SyntheticNaming.isSynthetic(reference, Phase.EXTERNAL, kind)) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
   public static boolean isInternalLambda(ClassReference reference) {
     return SyntheticNaming.isSynthetic(reference, Phase.INTERNAL, SyntheticKind.LAMBDA);
   }
diff --git a/src/test/java/com/android/tools/r8/utils/ApkUtils.java b/src/test/java/com/android/tools/r8/utils/ApkUtils.java
new file mode 100644
index 0000000..0fae220
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/ApkUtils.java
@@ -0,0 +1,30 @@
+// Copyright (c) 2021, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.utils;
+
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+
+public class ApkUtils {
+
+  public static ProcessResult apkMasseur(Path apk, Path dexSources, Path out) throws IOException {
+    ImmutableList.Builder<String> command =
+        new ImmutableList.Builder<String>()
+            .add("tools/apk_masseur.py")
+            .add("--dex")
+            .add(dexSources.toString())
+            .add("--out")
+            .add(out.toString())
+            .add("--install")
+            .add(apk.toString());
+    ProcessBuilder builder = new ProcessBuilder(command.build());
+    builder.directory(Paths.get(ToolHelper.THIRD_PARTY_DIR).toAbsolutePath().getParent().toFile());
+    return ToolHelper.runProcess(builder);
+  }
+}
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 e6fa548..0410bc0 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
@@ -106,7 +106,14 @@
   }
 
   public HorizontallyMergedClassesInspector assertNoClassesMerged() {
-    assertTrue(horizontallyMergedClasses.getSources().isEmpty());
+    if (!horizontallyMergedClasses.getSources().isEmpty()) {
+      DexType source = horizontallyMergedClasses.getSources().iterator().next();
+      fail(
+          "Expected no classes to be merged, got: "
+              + source.getTypeName()
+              + " -> "
+              + getTarget(source).getTypeName());
+    }
     return this;
   }
 
@@ -146,6 +153,11 @@
         Stream.of(classes).map(Reference::classFromClass).collect(Collectors.toList()));
   }
 
+  public HorizontallyMergedClassesInspector assertIsCompleteMergeGroup(
+      ClassReference... classReferences) {
+    return assertIsCompleteMergeGroup(Arrays.asList(classReferences));
+  }
+
   public HorizontallyMergedClassesInspector assertIsCompleteMergeGroup(String... typeNames) {
     return assertIsCompleteMergeGroup(
         Stream.of(typeNames).map(Reference::classFromTypeName).collect(Collectors.toList()));
diff --git a/third_party/api-outlining/simple-app-dump.tar.gz.sha1 b/third_party/api-outlining/simple-app-dump.tar.gz.sha1
new file mode 100644
index 0000000..cf9cc96
--- /dev/null
+++ b/third_party/api-outlining/simple-app-dump.tar.gz.sha1
@@ -0,0 +1 @@
+9913f083a29519e3fdc626a769ad33c7ba2498c1
\ No newline at end of file