Merge commit 'e13467c2cee237bceaa4d002472ede43d9a116e8' into dev-release
diff --git a/build.gradle b/build.gradle
index 7429345..2e2132d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -35,7 +35,7 @@
 
 ext {
     androidSupportVersion = '25.4.0'
-    asmVersion = '8.0'
+    asmVersion = '8.0'  // When updating update tools/asmifier.py as well.
     espressoVersion = '3.0.0'
     fastutilVersion = '7.2.0'
     guavaVersion = '23.0'
@@ -438,7 +438,8 @@
         "youtube/youtube.android_14.19",
         "youtube/youtube.android_14.44",
         "youtube/youtube.android_15.08",
-        "youtube/youtube.android_15.09"
+        "youtube/youtube.android_15.09",
+        "youtube/youtube.android_15.33"
     ],
 ]
 
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index e502f2f..8def7ae 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.dex.Marker;
 import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppServices;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -24,6 +25,7 @@
 import com.android.tools.r8.ir.optimize.AssertionsRewriter;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackSimple;
 import com.android.tools.r8.jar.CfApplicationWriter;
+import com.android.tools.r8.kotlin.KotlinMetadataRewriter;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.naming.PrefixRewritingNamingLens;
 import com.android.tools.r8.naming.signature.GenericSignatureRewriter;
@@ -166,7 +168,7 @@
       DexApplication app = new ApplicationReader(inputApp, options, timing).read(executor);
       PrefixRewritingMapper rewritePrefix =
           options.desugaredLibraryConfiguration.createPrefixRewritingMapper(options);
-      AppInfo appInfo = new AppInfo(app);
+      AppInfo appInfo = AppInfo.createInitialAppInfo(app);
 
       final CfgPrinter printer = options.printCfg ? new CfgPrinter() : null;
 
@@ -189,6 +191,10 @@
 
       AppView<?> appView = AppView.createForD8(appInfo, rewritePrefix);
 
+      if (options.testing.enableD8ResourcesPassThrough) {
+        appView.setAppServices(AppServices.builder(appView).build());
+      }
+
       IRConverter converter = new IRConverter(appView, timing, printer);
       app = converter.convert(app, executor);
 
@@ -259,6 +265,7 @@
                   : PrefixRewritingNamingLens.createPrefixRewritingNamingLens(appView);
           new GenericSignatureRewriter(appView, namingLens)
               .run(appView.appInfo().classes(), executor);
+          new KotlinMetadataRewriter(appView, namingLens).runForD8(executor);
         } else {
           // There are both cf and dex inputs in the program, and rewriting is required for
           // desugared library only on cf inputs. We cannot easily rewrite part of the program
@@ -322,6 +329,7 @@
         PrefixRewritingNamingLens.createPrefixRewritingNamingLens(appView);
     new GenericSignatureRewriter(appView, prefixRewritingNamingLens)
         .run(appView.appInfo().classes(), executor);
+    new KotlinMetadataRewriter(appView, prefixRewritingNamingLens).runForD8(executor);
     new ApplicationWriter(
             cfApp,
             null,
diff --git a/src/main/java/com/android/tools/r8/DexFileMergerHelper.java b/src/main/java/com/android/tools/r8/DexFileMergerHelper.java
index 1171440..8e6c389 100644
--- a/src/main/java/com/android/tools/r8/DexFileMergerHelper.java
+++ b/src/main/java/com/android/tools/r8/DexFileMergerHelper.java
@@ -90,7 +90,7 @@
                     null,
                     executor,
                     new DexFileMergerHelper(inputOrdering)::keepFirstProgramClassConflictResolver);
-        AppInfo appInfo = new AppInfo(app);
+        AppInfo appInfo = AppInfo.createInitialAppInfo(app);
         app = D8.optimize(app, appInfo, options, timing, executor);
 
         List<Marker> markers = app.dexItemFactory.extractMarkers();
diff --git a/src/main/java/com/android/tools/r8/DexIndexedConsumer.java b/src/main/java/com/android/tools/r8/DexIndexedConsumer.java
index 3ca4bf2..300588f 100644
--- a/src/main/java/com/android/tools/r8/DexIndexedConsumer.java
+++ b/src/main/java/com/android/tools/r8/DexIndexedConsumer.java
@@ -129,18 +129,19 @@
       this(archive, null, false);
     }
 
-    public ArchiveConsumer(Path archive, boolean consumeDataResouces) {
-      this(archive, null, consumeDataResouces);
+    public ArchiveConsumer(Path archive, boolean consumeDataResources) {
+      this(archive, null, consumeDataResources);
     }
 
     public ArchiveConsumer(Path archive, DexIndexedConsumer consumer) {
       this(archive, consumer, false);
     }
 
-    public ArchiveConsumer(Path archive, DexIndexedConsumer consumer, boolean consumeDataResouces) {
+    public ArchiveConsumer(
+        Path archive, DexIndexedConsumer consumer, boolean consumeDataResources) {
       super(consumer);
       this.outputBuilder = new ArchiveBuilder(archive);
-      this.consumeDataResources = consumeDataResouces;
+      this.consumeDataResources = consumeDataResources;
       this.outputBuilder.open();
       if (getDataResourceConsumer() != null) {
         this.outputBuilder.open();
diff --git a/src/main/java/com/android/tools/r8/DexSplitterHelper.java b/src/main/java/com/android/tools/r8/DexSplitterHelper.java
index 8d2d47d..b7ece81 100644
--- a/src/main/java/com/android/tools/r8/DexSplitterHelper.java
+++ b/src/main/java/com/android/tools/r8/DexSplitterHelper.java
@@ -86,7 +86,7 @@
         assert !options.hasMethodsFilter();
 
         // Run d8 optimize to ensure jumbo strings are handled.
-        AppInfo appInfo = new AppInfo(featureApp);
+        AppInfo appInfo = AppInfo.createInitialAppInfo(featureApp);
         featureApp = D8.optimize(featureApp, appInfo, options, timing, executor);
         // We create a specific consumer for each split.
         Path outputDir = Paths.get(output).resolve(entry.getKey());
diff --git a/src/main/java/com/android/tools/r8/FeatureSplit.java b/src/main/java/com/android/tools/r8/FeatureSplit.java
index 9bb637e..d0eafdb 100644
--- a/src/main/java/com/android/tools/r8/FeatureSplit.java
+++ b/src/main/java/com/android/tools/r8/FeatureSplit.java
@@ -28,9 +28,15 @@
  * </pre>
  */
 @Keep
-public final class FeatureSplit {
+public class FeatureSplit {
 
-  public static final FeatureSplit BASE = new FeatureSplit(null, null);
+  public static final FeatureSplit BASE =
+      new FeatureSplit(null, null) {
+        @Override
+        public boolean isBase() {
+          return true;
+        }
+      };
 
   private final ProgramConsumer programConsumer;
   private final List<ProgramResourceProvider> programResourceProviders;
@@ -41,6 +47,10 @@
     this.programResourceProviders = programResourceProviders;
   }
 
+  public boolean isBase() {
+    return false;
+  }
+
   public List<ProgramResourceProvider> getProgramResourceProviders() {
     return programResourceProviders;
   }
diff --git a/src/main/java/com/android/tools/r8/GenerateLintFiles.java b/src/main/java/com/android/tools/r8/GenerateLintFiles.java
index 7913783..2bfba83 100644
--- a/src/main/java/com/android/tools/r8/GenerateLintFiles.java
+++ b/src/main/java/com/android/tools/r8/GenerateLintFiles.java
@@ -311,7 +311,7 @@
         lintFile(compilationApiLevel, minApiLevel, ".txt"), desugaredApisSignatures);
 
     // Write a header jar with the desugared APIs.
-    AppInfo appInfo = new AppInfo(app);
+    AppInfo appInfo = AppInfo.createInitialAppInfo(app);
     AppView<?> appView = AppView.createForD8(appInfo);
     CfApplicationWriter writer =
         new CfApplicationWriter(
diff --git a/src/main/java/com/android/tools/r8/L8.java b/src/main/java/com/android/tools/r8/L8.java
index 8049508..99a87d3 100644
--- a/src/main/java/com/android/tools/r8/L8.java
+++ b/src/main/java/com/android/tools/r8/L8.java
@@ -126,7 +126,7 @@
           options.desugaredLibraryConfiguration.createPrefixRewritingMapper(options);
 
       DexApplication app = new L8TreePruner(options).prune(lazyApp, rewritePrefix);
-      AppInfo appInfo = new AppInfo(app);
+      AppInfo appInfo = AppInfo.createInitialAppInfo(app);
 
       AppView<?> appView = AppView.createForL8(appInfo, rewritePrefix);
       IRConverter converter = new IRConverter(appView, timing);
diff --git a/src/main/java/com/android/tools/r8/PrintUses.java b/src/main/java/com/android/tools/r8/PrintUses.java
index 45ce043..47ed5a0 100644
--- a/src/main/java/com/android/tools/r8/PrintUses.java
+++ b/src/main/java/com/android/tools/r8/PrintUses.java
@@ -93,13 +93,12 @@
     }
 
     @Override
-    public boolean registerInitClass(DexType clazz) {
+    public void registerInitClass(DexType clazz) {
       addType(clazz);
-      return false;
     }
 
     @Override
-    public boolean registerInvokeVirtual(DexMethod method) {
+    public void registerInvokeVirtual(DexMethod method) {
       ResolutionResult resolutionResult = appInfo.unsafeResolveMethodDueToDexFormat(method);
       DexEncodedMethod target =
           resolutionResult.isVirtualTarget() ? resolutionResult.getSingleTarget() : null;
@@ -109,17 +108,15 @@
       } else {
         addMethod(method);
       }
-      return false;
     }
 
     @Override
-    public boolean registerInvokeDirect(DexMethod method) {
+    public void registerInvokeDirect(DexMethod method) {
       addMethod(method);
-      return false;
     }
 
     @Override
-    public boolean registerInvokeStatic(DexMethod method) {
+    public void registerInvokeStatic(DexMethod method) {
       DexEncodedMethod target = appInfo.unsafeResolveMethodDueToDexFormat(method).getSingleTarget();
       if (target != null && target.method != method) {
         addType(method.holder);
@@ -127,65 +124,56 @@
       } else {
         addMethod(method);
       }
-      return false;
     }
 
     @Override
-    public boolean registerInvokeInterface(DexMethod method) {
-      return registerInvokeVirtual(method);
+    public void registerInvokeInterface(DexMethod method) {
+      registerInvokeVirtual(method);
     }
 
     @Override
-    public boolean registerInvokeSuper(DexMethod method) {
+    public void registerInvokeSuper(DexMethod method) {
       DexEncodedMethod superTarget = appInfo.lookupSuperTarget(method, context);
       if (superTarget != null) {
         addMethod(superTarget.method);
       } else {
         addMethod(method);
       }
-      return false;
     }
 
     @Override
-    public boolean registerInstanceFieldWrite(DexField field) {
+    public void registerInstanceFieldWrite(DexField field) {
       addField(field);
-      return false;
     }
 
     @Override
-    public boolean registerInstanceFieldRead(DexField field) {
+    public void registerInstanceFieldRead(DexField field) {
       addField(field);
-      return false;
     }
 
     @Override
-    public boolean registerNewInstance(DexType type) {
+    public void registerNewInstance(DexType type) {
       addType(type);
-      return false;
     }
 
     @Override
-    public boolean registerStaticFieldRead(DexField field) {
+    public void registerStaticFieldRead(DexField field) {
       addField(field);
-      return false;
     }
 
     @Override
-    public boolean registerStaticFieldWrite(DexField field) {
+    public void registerStaticFieldWrite(DexField field) {
       addField(field);
-      return false;
     }
 
     @Override
-    public boolean registerTypeReference(DexType type) {
+    public void registerTypeReference(DexType type) {
       addType(type);
-      return false;
     }
 
     @Override
-    public boolean registerInstanceOf(DexType type) {
+    public void registerInstanceOf(DexType type) {
       addType(type);
-      return false;
     }
 
     private void addType(DexType type) {
@@ -361,7 +349,7 @@
     InternalOptions options = new InternalOptions();
     application =
         new ApplicationReader(inputApp, options, new Timing("PrintUses")).read().toDirect();
-    appInfo = new AppInfoWithClassHierarchy(application);
+    appInfo = AppInfoWithClassHierarchy.createInitialAppInfoWithClassHierarchy(application);
   }
 
   private void analyze() {
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index c006eb5..38e1556 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -31,6 +31,7 @@
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
+import com.android.tools.r8.graph.DirectMappedDexApplication.Builder;
 import com.android.tools.r8.graph.EnumValueInfoMapCollection;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
@@ -38,6 +39,7 @@
 import com.android.tools.r8.graph.SubtypingInfo;
 import com.android.tools.r8.graph.analysis.ClassInitializerAssertionEnablingAnalysis;
 import com.android.tools.r8.graph.analysis.InitializedClassesInInstanceMethodsAnalysis;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.inspector.internal.InspectorImpl;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.desugar.BackportedMethodRewriter;
@@ -74,10 +76,12 @@
 import com.android.tools.r8.optimize.MemberRebindingAnalysis;
 import com.android.tools.r8.optimize.VisibilityBridgeRemover;
 import com.android.tools.r8.origin.CommandLineOrigin;
+import com.android.tools.r8.repackaging.Repackaging;
 import com.android.tools.r8.shaking.AbstractMethodRemover;
 import com.android.tools.r8.shaking.AnnotationRemover;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.ClassInitFieldSynthesizer;
+import com.android.tools.r8.shaking.ClassMergingEnqueuerExtension;
 import com.android.tools.r8.shaking.DefaultTreePrunerConfiguration;
 import com.android.tools.r8.shaking.DiscardedChecker;
 import com.android.tools.r8.shaking.Enqueuer;
@@ -323,6 +327,8 @@
       timing.begin("Strip unused code");
       Set<DexType> classesToRetainInnerClassAttributeFor = null;
       Set<DexType> missingClasses = null;
+      ClassMergingEnqueuerExtension classMergingEnqueuerExtension =
+          new ClassMergingEnqueuerExtension(appView.dexItemFactory());
       try {
         // TODO(b/154849103): Find a better way to determine missing classes.
         missingClasses = new SubtypingInfo(appView).getMissingClasses();
@@ -370,7 +376,12 @@
         AnnotationRemover.Builder annotationRemoverBuilder =
             options.isShrinking() ? AnnotationRemover.builder() : null;
         AppView<AppInfoWithLiveness> appViewWithLiveness =
-            runEnqueuer(annotationRemoverBuilder, executorService, appView, subtypingInfo);
+            runEnqueuer(
+                annotationRemoverBuilder,
+                executorService,
+                appView,
+                subtypingInfo,
+                classMergingEnqueuerExtension);
         assert appView.rootSet().verifyKeptFieldsAreAccessedAndLive(appViewWithLiveness.appInfo());
         assert appView.rootSet().verifyKeptMethodsAreTargetedAndLive(appViewWithLiveness.appInfo());
         assert appView.rootSet().verifyKeptTypesAreLive(appViewWithLiveness.appInfo());
@@ -487,8 +498,11 @@
       if (options.shouldDesugarNests()) {
         timing.begin("NestBasedAccessDesugaring");
         R8NestBasedAccessDesugaring analyzer = new R8NestBasedAccessDesugaring(appViewWithLiveness);
-        NestedPrivateMethodLens lens = analyzer.run(executorService);
-        appView.rewriteWithLens(lens);
+        Builder appBuilder = getDirectApp(appView).builder();
+        NestedPrivateMethodLens lens = analyzer.run(executorService, appBuilder);
+        if (lens != null) {
+          appView.rewriteWithLensAndApplication(lens, appBuilder.build());
+        }
         timing.end();
       } else {
         timing.begin("NestReduction");
@@ -529,6 +543,17 @@
           }
           timing.end();
         }
+        if (options.enableHorizontalClassMerging) {
+          timing.begin("HorizontalClassMerger");
+          HorizontalClassMerger merger =
+              new HorizontalClassMerger(appViewWithLiveness, mainDexClasses);
+          merger.run();
+          timing.end();
+        }
+
+        // Only required for class merging, clear instance to save memory.
+        classMergingEnqueuerExtension = null;
+
         if (options.enableArgumentRemoval) {
           SubtypingInfo subtypingInfo = appViewWithLiveness.appInfo().computeSubtypingInfo();
           {
@@ -772,6 +797,12 @@
         }
       }
 
+      // Perform repackaging.
+      // TODO(b/165783399): Consider making repacking available without minification.
+      if (options.isMinifying() && options.testing.enableExperimentalRepackaging) {
+        new Repackaging(appView.withLiveness()).run(executorService, timing);
+      }
+
       // Perform minification.
       NamingLens namingLens;
       if (options.getProguardConfiguration().hasApplyMappingFile()) {
@@ -791,10 +822,6 @@
         namingLens = NamingLens.getIdentityLens();
       }
 
-      timing.begin("MinifyKotlinMetadata");
-      new KotlinMetadataRewriter(appView, namingLens).run(executorService);
-      timing.end();
-
       assert verifyMovedMethodsHaveOriginalMethodPosition(appView, getDirectApp(appView));
 
       timing.begin("Line number remapping");
@@ -855,10 +882,9 @@
       assert appView
           .graphLens()
           .verifyMappingToOriginalProgram(
-              appView.appInfo().classesWithDeterministicOrder(),
+              appView,
               new ApplicationReader(inputApp.withoutMainDexList(), options, timing)
-                  .read(executorService),
-              appView.dexItemFactory());
+                  .read(executorService));
 
       // Report synthetic rules (only for testing).
       // TODO(b/120959039): Move this to being reported through the graph consumer.
@@ -869,6 +895,10 @@
       NamingLens prefixRewritingNamingLens =
           PrefixRewritingNamingLens.createPrefixRewritingNamingLens(appView, namingLens);
 
+      timing.begin("MinifyKotlinMetadata");
+      new KotlinMetadataRewriter(appView, prefixRewritingNamingLens).runForR8(executorService);
+      timing.end();
+
       new GenericSignatureRewriter(appView, prefixRewritingNamingLens)
           .run(appView.appInfo().classes(), executorService);
 
@@ -951,7 +981,8 @@
       AnnotationRemover.Builder annotationRemoverBuilder,
       ExecutorService executorService,
       AppView<AppInfoWithClassHierarchy> appView,
-      SubtypingInfo subtypingInfo)
+      SubtypingInfo subtypingInfo,
+      ClassMergingEnqueuerExtension classMergingEnqueuerExtension)
       throws ExecutionException {
     Enqueuer enqueuer = EnqueuerFactory.createForInitialTreeShaking(appView, subtypingInfo);
     enqueuer.setAnnotationRemoverBuilder(annotationRemoverBuilder);
@@ -964,6 +995,10 @@
               appView.dexItemFactory(), OptimizationFeedbackSimple.getInstance()));
     }
 
+    if (options.isClassMergingExtensionRequired()) {
+      classMergingEnqueuerExtension.attach(enqueuer);
+    }
+
     AppView<AppInfoWithLiveness> appViewWithLiveness =
         appView.setAppInfo(
             enqueuer.traceApplication(
diff --git a/src/main/java/com/android/tools/r8/dex/Marker.java b/src/main/java/com/android/tools/r8/dex/Marker.java
index e15a4eb..dd179c1 100644
--- a/src/main/java/com/android/tools/r8/dex/Marker.java
+++ b/src/main/java/com/android/tools/r8/dex/Marker.java
@@ -88,10 +88,11 @@
                 new StringDiagnostic(
                     "Merging program compiled with multiple desugared libraries."));
         }
-        if (identifier.equals(NO_LIBRARY_DESUGARING) && marker.tool == Tool.R8) {
-          continue;
+        if (marker.isDesugared()) {
+          desugaredLibraryIdentifiers.add(identifier);
+        } else {
+          assert identifier.equals(NO_LIBRARY_DESUGARING);
         }
-        desugaredLibraryIdentifiers.add(identifier);
       }
     }
 
@@ -134,6 +135,12 @@
     return this;
   }
 
+  public boolean isDesugared() {
+    // For both DEX and CF output from D8 and R8 a min-api setting implies that the code has been
+    // desugared, as even the highest min-api require desugaring of lambdas.
+    return hasMinApi();
+  }
+
   public boolean hasMinApi() {
     return jsonObject.has(MIN_API);
   }
diff --git a/src/main/java/com/android/tools/r8/dex/ResourceAdapter.java b/src/main/java/com/android/tools/r8/dex/ResourceAdapter.java
index 536515d..84459b4 100644
--- a/src/main/java/com/android/tools/r8/dex/ResourceAdapter.java
+++ b/src/main/java/com/android/tools/r8/dex/ResourceAdapter.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.shaking.ProguardConfiguration;
 import com.android.tools.r8.shaking.ProguardPathFilter;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.ExceptionDiagnostic;
@@ -26,6 +27,7 @@
 import it.unimi.dsi.fastutil.ints.IntStack;
 import java.io.InputStream;
 import java.nio.charset.Charset;
+import java.util.function.Function;
 
 public class ResourceAdapter {
 
@@ -50,22 +52,14 @@
 
   public DataEntryResource adaptIfNeeded(DataEntryResource file) {
     // Adapt name, if needed.
-    ProguardPathFilter adaptResourceFileNamesFilter =
-        options.getProguardConfiguration().getAdaptResourceFilenames();
     String name =
-        adaptResourceFileNamesFilter.isEnabled()
-                && !file.getName().toLowerCase().endsWith(FileUtils.CLASS_EXTENSION)
-                && adaptResourceFileNamesFilter.matches(file.getName())
+        shouldAdapt(file, options, ProguardConfiguration::getAdaptResourceFilenames)
             ? adaptFileName(file)
             : file.getName();
     assert name != null;
     // Adapt contents, if needed.
-    ProguardPathFilter adaptResourceFileContentsFilter =
-        options.getProguardConfiguration().getAdaptResourceFileContents();
     byte[] contents =
-        adaptResourceFileContentsFilter.isEnabled()
-                && !file.getName().toLowerCase().endsWith(FileUtils.CLASS_EXTENSION)
-                && adaptResourceFileContentsFilter.matches(file.getName())
+        shouldAdapt(file, options, ProguardConfiguration::getAdaptResourceFileContents)
             ? adaptFileContents(file)
             : null;
     // Return a new resource if the name or contents changed. Otherwise return the original
@@ -85,14 +79,31 @@
 
   public DataDirectoryResource adaptIfNeeded(DataDirectoryResource directory) {
     // First check if this directory should even be in the output.
-    ProguardPathFilter keepDirectoriesFilter =
-        options.getProguardConfiguration().getKeepDirectories();
-    if (!keepDirectoriesFilter.matches(directory.getName())) {
+    if (options.getProguardConfiguration() == null) {
+      assert options.testing.enableD8ResourcesPassThrough;
+      return null;
+    }
+    if (!options.getProguardConfiguration().getKeepDirectories().matches(directory.getName())) {
       return null;
     }
     return DataDirectoryResource.fromName(adaptDirectoryName(directory), directory.getOrigin());
   }
 
+  private boolean shouldAdapt(
+      DataEntryResource file,
+      InternalOptions options,
+      Function<ProguardConfiguration, ProguardPathFilter> getFilter) {
+    final ProguardConfiguration proguardConfiguration = options.getProguardConfiguration();
+    if (proguardConfiguration == null) {
+      assert options.testing.enableD8ResourcesPassThrough;
+      return false;
+    }
+    ProguardPathFilter filter = getFilter.apply(proguardConfiguration);
+    return filter.isEnabled()
+        && !file.getName().toLowerCase().endsWith(FileUtils.CLASS_EXTENSION)
+        && filter.matches(file.getName());
+  }
+
   public boolean isService(DataEntryResource file) {
     return file.getName().startsWith(AppServices.SERVICE_DIRECTORY_NAME);
   }
diff --git a/src/main/java/com/android/tools/r8/features/FeatureSplitConfiguration.java b/src/main/java/com/android/tools/r8/features/FeatureSplitConfiguration.java
index 610104a..6e5b2ab 100644
--- a/src/main/java/com/android/tools/r8/features/FeatureSplitConfiguration.java
+++ b/src/main/java/com/android/tools/r8/features/FeatureSplitConfiguration.java
@@ -9,9 +9,8 @@
 import com.android.tools.r8.ProgramResource;
 import com.android.tools.r8.ProgramResourceProvider;
 import com.android.tools.r8.ResourceException;
-import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.Reporter;
@@ -26,8 +25,10 @@
 import java.util.Set;
 
 public class FeatureSplitConfiguration {
+
   private final List<FeatureSplit> featureSplits;
 
+  // TODO(141451259): Consider doing the mapping from DexType to Feature (with support in mapper).
   private final Map<String, FeatureSplit> javaTypeToFeatureSplitMapping = new HashMap<>();
 
   public FeatureSplitConfiguration(List<FeatureSplit> featureSplits, Reporter reporter) {
@@ -108,28 +109,23 @@
   }
 
   public boolean inBaseOrSameFeatureAs(DexProgramClass clazz, DexProgramClass context) {
-    FeatureSplit split = getFeatureSplit(clazz.type);
-    return split == null || split == getFeatureSplit(context.type);
+    FeatureSplit split = getFeatureSplit(clazz);
+    return split.isBase() || split == getFeatureSplit(context);
   }
 
   public boolean isInFeature(DexProgramClass clazz) {
-    return getFeatureSplit(clazz.type) != null;
+    return !isInBase(clazz);
   }
 
   public boolean isInBase(DexProgramClass clazz) {
-    return !isInFeature(clazz);
+    return getFeatureSplit(clazz).isBase();
   }
 
-  public boolean inSameFeatureOrBase(DexMethod a, DexMethod b){
-    return inSameFeatureOrBase(a.holder, b.holder);
+  public boolean inSameFeatureOrBothInBase(ProgramMethod a, ProgramMethod b) {
+    return inSameFeatureOrBothInBase(a.getHolder(), b.getHolder());
   }
 
-  public boolean inSameFeatureOrBase(DexType a, DexType b) {
-    assert a.isClassType() && b.isClassType();
-    if (javaTypeToFeatureSplitMapping.isEmpty()) {
-      return true;
-    }
-    // TODO(141451259): Consider doing the mapping from DexType to Feature (with support in mapper)
+  public boolean inSameFeatureOrBothInBase(DexProgramClass a, DexProgramClass b) {
     return getFeatureSplit(a) == getFeatureSplit(b);
   }
 
@@ -141,8 +137,8 @@
     return javaTypeToFeatureSplitMapping.get(DescriptorUtils.descriptorToJavaType(classDescriptor));
   }
 
-  private FeatureSplit getFeatureSplit(DexType type) {
-    assert type.isClassType();
-    return javaTypeToFeatureSplitMapping.get(type.toSourceString());
+  public FeatureSplit getFeatureSplit(DexProgramClass clazz) {
+    return javaTypeToFeatureSplitMapping.getOrDefault(
+        clazz.type.toSourceString(), FeatureSplit.BASE);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/AccessFlags.java b/src/main/java/com/android/tools/r8/graph/AccessFlags.java
index a0a1b99..8792d0c 100644
--- a/src/main/java/com/android/tools/r8/graph/AccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/AccessFlags.java
@@ -128,6 +128,10 @@
     return !isPublic() && !isPrivate() && !isProtected();
   }
 
+  public boolean isPackagePrivateOrProtected() {
+    return !isPublic() && !isPrivate();
+  }
+
   public boolean isPublic() {
     return isSet(Constants.ACC_PUBLIC);
   }
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfo.java b/src/main/java/com/android/tools/r8/graph/AppInfo.java
index ee67876..66b00b8 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfo.java
@@ -3,56 +3,44 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy.CreateDesugaringViewOnAppInfo;
 import com.android.tools.r8.graph.FieldResolutionResult.SuccessfulFieldResolutionResult;
 import com.android.tools.r8.ir.desugar.InterfaceMethodRewriter;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.BooleanBox;
 import com.android.tools.r8.utils.InternalOptions;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableMap.Builder;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
 
 public class AppInfo implements DexDefinitionSupplier {
 
   private final DexApplication app;
   private final DexItemFactory dexItemFactory;
-
-  // For some optimizations, e.g. optimizing synthetic classes, we may need to resolve the current
-  // class being optimized.
-  private final ConcurrentHashMap<DexType, DexProgramClass> synthesizedClasses;
+  private final SyntheticItems syntheticItems;
 
   // Set when a new AppInfo replaces a previous one. All public methods should verify that the
   // current instance is not obsolete, to ensure that we almost use the most recent AppInfo.
   private final BooleanBox obsolete;
 
-  public AppInfo(DexApplication application) {
-    this(application, new ConcurrentHashMap<>(), new BooleanBox());
+  public static AppInfo createInitialAppInfo(DexApplication application) {
+    return new AppInfo(application, SyntheticItems.createInitialSyntheticItems(), new BooleanBox());
+  }
+
+  public AppInfo(DexApplication application, SyntheticItems.CommittedItems committedItems) {
+    this(application, committedItems.toSyntheticItems(), new BooleanBox());
   }
 
   // For desugaring.
-  protected AppInfo(AppInfo appInfo) {
-    this(appInfo.app, appInfo.synthesizedClasses, appInfo.obsolete);
+  // This is a view onto the app info and is the only place the pending synthetics are shared.
+  AppInfo(CreateDesugaringViewOnAppInfo witness, AppInfo appInfo) {
+    this(appInfo.app, appInfo.syntheticItems, appInfo.obsolete);
+    assert witness != null;
   }
 
-  // For AppInfoWithLiveness.
-  protected AppInfo(AppInfoWithClassHierarchy previous) {
-    this(
-        ((AppInfo) previous).app,
-        new ConcurrentHashMap<>(((AppInfo) previous).synthesizedClasses),
-        new BooleanBox());
-  }
-
-  private AppInfo(
-      DexApplication application,
-      ConcurrentHashMap<DexType, DexProgramClass> synthesizedClasses,
-      BooleanBox obsolete) {
+  private AppInfo(DexApplication application, SyntheticItems syntheticItems, BooleanBox obsolete) {
     this.app = application;
     this.dexItemFactory = application.dexItemFactory;
-    this.synthesizedClasses = synthesizedClasses;
+    this.syntheticItems = syntheticItems;
     this.obsolete = obsolete;
   }
 
@@ -60,10 +48,6 @@
     return app.options;
   }
 
-  public void copyMetadataFromPrevious(AppInfo previous) {
-    this.synthesizedClasses.putAll(previous.synthesizedClasses);
-  }
-
   public boolean isObsolete() {
     return obsolete.get();
   }
@@ -92,25 +76,18 @@
     return dexItemFactory;
   }
 
+  public SyntheticItems getSyntheticItems() {
+    return syntheticItems;
+  }
+
   public void addSynthesizedClass(DexProgramClass clazz) {
     assert checkIfObsolete();
-    assert clazz.type.isD8R8SynthesizedClassType();
-    DexProgramClass previous = synthesizedClasses.put(clazz.type, clazz);
-    assert previous == null || previous == clazz;
+    syntheticItems.addSyntheticClass(clazz);
   }
 
   public Collection<DexProgramClass> synthesizedClasses() {
     assert checkIfObsolete();
-    return Collections.unmodifiableCollection(synthesizedClasses.values());
-  }
-
-  private Map<DexField, DexEncodedField> computeFieldDefinitions(DexType type) {
-    Builder<DexField, DexEncodedField> builder = ImmutableMap.builder();
-    DexClass clazz = definitionFor(type);
-    if (clazz != null) {
-      clazz.forEachField(field -> builder.put(field.field, field));
-    }
-    return builder.build();
+    return syntheticItems.getPendingSyntheticClasses();
   }
 
   public Collection<DexProgramClass> classes() {
@@ -130,12 +107,7 @@
 
   public final DexClass definitionForWithoutExistenceAssert(DexType type) {
     assert checkIfObsolete();
-    DexProgramClass cached = synthesizedClasses.get(type);
-    if (cached != null) {
-      assert app.definitionFor(type) == null;
-      return cached;
-    }
-    return app.definitionFor(type);
+    return syntheticItems.definitionFor(type, app::definitionFor);
   }
 
   public DexClass definitionForDesugarDependency(DexClass dependent, DexType type) {
diff --git a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
index 481af1e..f1b0dc2 100644
--- a/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
+++ b/src/main/java/com/android/tools/r8/graph/AppInfoWithClassHierarchy.java
@@ -39,27 +39,37 @@
  */
 public class AppInfoWithClassHierarchy extends AppInfo {
 
-  public AppInfoWithClassHierarchy(DexApplication application) {
-    super(application);
+  private static final CreateDesugaringViewOnAppInfo WITNESS = new CreateDesugaringViewOnAppInfo();
+
+  static class CreateDesugaringViewOnAppInfo {
+    private CreateDesugaringViewOnAppInfo() {}
   }
 
-  // For desugaring.
-  private AppInfoWithClassHierarchy(AppInfo appInfo) {
-    super(appInfo);
+  public static AppInfoWithClassHierarchy createInitialAppInfoWithClassHierarchy(
+      DexApplication application) {
+    return new AppInfoWithClassHierarchy(
+        application, SyntheticItems.createInitialSyntheticItems().commit(application));
   }
 
   // For AppInfoWithLiveness.
-  protected AppInfoWithClassHierarchy(AppInfoWithClassHierarchy previous) {
-    super(previous);
+  protected AppInfoWithClassHierarchy(
+      DexApplication application, SyntheticItems.CommittedItems committedItems) {
+    super(application, committedItems);
+  }
+
+  // For desugaring.
+  private AppInfoWithClassHierarchy(CreateDesugaringViewOnAppInfo witness, AppInfo appInfo) {
+    super(witness, appInfo);
   }
 
   public static AppInfoWithClassHierarchy createForDesugaring(AppInfo appInfo) {
     assert !appInfo.hasClassHierarchy();
-    return new AppInfoWithClassHierarchy(appInfo);
+    return new AppInfoWithClassHierarchy(WITNESS, appInfo);
   }
 
   public AppInfoWithClassHierarchy rebuild(Function<DexApplication, DexApplication> fn) {
-    return new AppInfoWithClassHierarchy(fn.apply(app()));
+    DexApplication application = fn.apply(app());
+    return new AppInfoWithClassHierarchy(application, getSyntheticItems().commit(application));
   }
 
   @Override
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 3bacf89..ccdfd2f 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -126,7 +126,8 @@
   }
 
   public static AppView<AppInfoWithClassHierarchy> createForR8(DexApplication application) {
-    AppInfoWithClassHierarchy appInfo = new AppInfoWithClassHierarchy(application);
+    AppInfoWithClassHierarchy appInfo =
+        AppInfoWithClassHierarchy.createInitialAppInfoWithClassHierarchy(application);
     return new AppView<>(
         appInfo, WholeProgramOptimizations.ON, defaultPrefixRewritingMapper(appInfo));
   }
@@ -396,6 +397,15 @@
     return collection;
   }
 
+  public boolean hasBeenMerged(DexProgramClass clazz) {
+    // TODO(b/165227525): Add support for the horizontal class merger here.
+    if (horizontallyMergedLambdaClasses != null
+        && horizontallyMergedLambdaClasses.hasBeenMerged(clazz)) {
+      return true;
+    }
+    return verticallyMergedClasses != null && verticallyMergedClasses.hasBeenMerged(clazz);
+  }
+
   // Get the result of horizontal lambda class merging. Returns null if horizontal lambda class
   // merging has not been run.
   public HorizontallyMergedLambdaClasses horizontallyMergedLambdaClasses() {
@@ -470,16 +480,24 @@
   }
 
   public void rewriteWithLens(NestedGraphLens lens) {
-    rewriteWithLens(lens, withLiveness());
+    if (lens != null) {
+      rewriteWithLens(lens, appInfo().app().asDirect(), withLiveness());
+    }
   }
 
-  private static void rewriteWithLens(NestedGraphLens lens, AppView<AppInfoWithLiveness> appView) {
-    if (lens == null) {
-      return;
-    }
+  public void rewriteWithLensAndApplication(
+      NestedGraphLens lens, DirectMappedDexApplication application) {
+    assert lens != null;
+    assert application != null;
+    rewriteWithLens(lens, application, withLiveness());
+  }
+
+  private static void rewriteWithLens(
+      NestedGraphLens lens,
+      DirectMappedDexApplication application,
+      AppView<AppInfoWithLiveness> appView) {
     boolean changed = appView.setGraphLens(lens);
     assert changed;
-    DirectMappedDexApplication application = appView.appInfo().app().asDirect();
     assert application.verifyWithLens(lens);
     appView.setAppInfo(appView.appInfo().rewrittenWithLens(application, lens));
   }
diff --git a/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java b/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java
index f1ddad4..491178a 100644
--- a/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java
+++ b/src/main/java/com/android/tools/r8/graph/AssemblyWriter.java
@@ -48,7 +48,7 @@
     this.writeIR = writeIR;
     this.writeCode = writeCode;
     if (writeIR) {
-      this.appInfo = new AppInfo(application.toDirect());
+      this.appInfo = AppInfo.createInitialAppInfo(application.toDirect());
       if (options.programConsumer == null) {
         // Use class-file backend, since the CF frontend for testing does not support desugaring of
         // synchronized methods for the DEX backend (b/109789541).
diff --git a/src/main/java/com/android/tools/r8/graph/DefaultUseRegistry.java b/src/main/java/com/android/tools/r8/graph/DefaultUseRegistry.java
deleted file mode 100644
index a267d5b..0000000
--- a/src/main/java/com/android/tools/r8/graph/DefaultUseRegistry.java
+++ /dev/null
@@ -1,77 +0,0 @@
-// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.graph;
-
-public class DefaultUseRegistry extends UseRegistry {
-
-  public DefaultUseRegistry(DexItemFactory factory) {
-    super(factory);
-  }
-
-  @Override
-  public boolean registerInitClass(DexType clazz) {
-    return true;
-  }
-
-  @Override
-  public boolean registerInvokeVirtual(DexMethod method) {
-    return true;
-  }
-
-  @Override
-  public boolean registerInvokeDirect(DexMethod method) {
-    return true;
-  }
-
-  @Override
-  public boolean registerInvokeStatic(DexMethod method) {
-    return true;
-  }
-
-  @Override
-  public boolean registerInvokeInterface(DexMethod method) {
-    return true;
-  }
-
-  @Override
-  public boolean registerInvokeSuper(DexMethod method) {
-    return true;
-  }
-
-  @Override
-  public boolean registerInstanceFieldWrite(DexField field) {
-    return true;
-  }
-
-  @Override
-  public boolean registerInstanceFieldRead(DexField field) {
-    return true;
-  }
-
-  @Override
-  public boolean registerNewInstance(DexType type) {
-    return true;
-  }
-
-  @Override
-  public boolean registerStaticFieldRead(DexField field) {
-    return true;
-  }
-
-  @Override
-  public boolean registerStaticFieldWrite(DexField field) {
-    return true;
-  }
-
-  @Override
-  public boolean registerTypeReference(DexType type) {
-    return true;
-  }
-
-  @Override
-  public boolean registerInstanceOf(DexType type) {
-    return true;
-  }
-}
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 a471dc2..d6d69a6 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplication.java
@@ -208,6 +208,11 @@
       return mainDexList;
     }
 
+    public Builder<T> addToMainDexList(DexType mainDex) {
+      mainDexList.add(mainDex);
+      return this;
+    }
+
     public Builder<T> addToMainDexList(Collection<DexType> mainDexList) {
       this.mainDexList.addAll(mainDexList);
       return this;
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 ae39752..fe2eabd 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -108,6 +108,11 @@
     }
   }
 
+  @Override
+  public ClassAccessFlags getAccessFlags() {
+    return accessFlags;
+  }
+
   public Iterable<DexEncodedField> fields() {
     return fields(Predicates.alwaysTrue());
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexDefinition.java b/src/main/java/com/android/tools/r8/graph/DexDefinition.java
index 09daf79..6168bd8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDefinition.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDefinition.java
@@ -25,6 +25,8 @@
     return annotations;
   }
 
+  public abstract AccessFlags<?> getAccessFlags();
+
   public DexAnnotationSet liveAnnotations(AppView<AppInfoWithLiveness> appView) {
     return annotations.keepIf(
         annotation -> AnnotationRemover.shouldKeepAnnotation(appView, this, annotation));
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
index 7a85dc8..5d9fa45 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.android.tools.r8.ir.analysis.type.Nullability.maybeNull;
 import static com.android.tools.r8.kotlin.KotlinMetadataUtils.NO_KOTLIN_INFO;
 
@@ -76,6 +77,11 @@
     return kotlinMemberInfo;
   }
 
+  @Override
+  public FieldAccessFlags getAccessFlags() {
+    return accessFlags;
+  }
+
   public void setKotlinMemberInfo(KotlinFieldLevelInfo kotlinMemberInfo) {
     assert this.kotlinMemberInfo == NO_KOTLIN_INFO;
     this.kotlinMemberInfo = kotlinMemberInfo;
@@ -126,6 +132,20 @@
     return this;
   }
 
+  @Override
+  public ProgramField asProgramMember(DexDefinitionSupplier definitions) {
+    return asProgramField(definitions);
+  }
+
+  public ProgramField asProgramField(DexDefinitionSupplier definitions) {
+    assert holder().isClassType();
+    DexProgramClass clazz = asProgramClassOrNull(definitions.definitionForHolder(field));
+    if (clazz != null) {
+      return new ProgramField(clazz, this);
+    }
+    return null;
+  }
+
   public boolean isEnum() {
     return accessFlags.isEnum();
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java
index 638a9be..df06501 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMember.java
@@ -27,6 +27,8 @@
     return this;
   }
 
+  public abstract ProgramMember<D, R> asProgramMember(DexDefinitionSupplier definitions);
+
   @Override
   public final boolean equals(Object other) {
     if (other == this) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index cab8542..b087d77 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -180,8 +180,9 @@
     obsolete = true;
   }
 
-  public CompilationState getCompilationState() {
-    return compilationState;
+  @Override
+  public MethodAccessFlags getAccessFlags() {
+    return accessFlags;
   }
 
   public DexEncodedMethod getDefaultInterfaceMethodImplementation() {
@@ -321,6 +322,11 @@
     return false;
   }
 
+  @Override
+  public ProgramMethod asProgramMember(DexDefinitionSupplier definitions) {
+    return asProgramMethod(definitions);
+  }
+
   public ProgramMethod asProgramMethod(DexDefinitionSupplier definitions) {
     assert method.holder.isClassType();
     DexProgramClass clazz = asProgramClassOrNull(definitions.definitionForHolder(method));
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index fc7ae3e..1190570 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -280,11 +280,13 @@
     return isDoubleType() || isLongType();
   }
 
+  // TODO(b/158159959): Remove usage of name-based identification.
   public boolean isD8R8SynthesizedLambdaClassType() {
     String name = toSourceString();
     return name.contains(LAMBDA_CLASS_NAME_PREFIX);
   }
 
+  // TODO(b/158159959): Remove usage of name-based identification.
   public boolean isD8R8SynthesizedClassType() {
     String name = toSourceString();
     return name.contains(COMPANION_CLASS_NAME_SUFFIX)
diff --git a/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java b/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
index 612329c..08e5190 100644
--- a/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
@@ -19,7 +19,7 @@
 import java.util.List;
 import java.util.Map;
 
-public class DirectMappedDexApplication extends DexApplication implements DexDefinitionSupplier {
+public class DirectMappedDexApplication extends DexApplication {
 
   // Mapping from code objects to their encoded-method owner. Used for asserting unique ownership
   // and debugging purposes.
@@ -73,6 +73,21 @@
     return classpathClasses;
   }
 
+  public DexDefinitionSupplier getDefinitionsSupplier(SyntheticItems syntheticItems) {
+    DirectMappedDexApplication self = this;
+    return new DexDefinitionSupplier() {
+      @Override
+      public DexClass definitionFor(DexType type) {
+        return syntheticItems.definitionFor(type, self::definitionFor);
+      }
+
+      @Override
+      public DexItemFactory dexItemFactory() {
+        return self.dexItemFactory;
+      }
+    };
+  }
+
   @Override
   public DexClass definitionFor(DexType type) {
     assert type.isClassType() : "Cannot lookup definition for type: " + type;
@@ -80,11 +95,6 @@
   }
 
   @Override
-  public DexItemFactory dexItemFactory() {
-    return dexItemFactory;
-  }
-
-  @Override
   public DexProgramClass programDefinitionFor(DexType type) {
     // The direct mapped application has no duplicates so this coincides with definitionFor.
     return DexProgramClass.asProgramClassOrNull(definitionFor(type));
diff --git a/src/main/java/com/android/tools/r8/graph/FieldResolutionResult.java b/src/main/java/com/android/tools/r8/graph/FieldResolutionResult.java
index 0d2390b..486996e 100644
--- a/src/main/java/com/android/tools/r8/graph/FieldResolutionResult.java
+++ b/src/main/java/com/android/tools/r8/graph/FieldResolutionResult.java
@@ -6,7 +6,8 @@
 
 import com.android.tools.r8.utils.OptionalBool;
 
-public abstract class FieldResolutionResult {
+public abstract class FieldResolutionResult
+    implements MemberResolutionResult<DexEncodedField, DexField> {
 
   public static FailedFieldResolutionResult failure() {
     return FailedFieldResolutionResult.INSTANCE;
@@ -31,11 +32,26 @@
     return null;
   }
 
+  @Override
+  public boolean isSuccessfulMemberResolutionResult() {
+    return false;
+  }
+
+  @Override
+  public SuccessfulFieldResolutionResult asSuccessfulMemberResolutionResult() {
+    return null;
+  }
+
   public boolean isFailedOrUnknownResolution() {
     return false;
   }
 
-  public static class SuccessfulFieldResolutionResult extends FieldResolutionResult {
+  public DexClass getInitialResolutionHolder() {
+    return null;
+  }
+
+  public static class SuccessfulFieldResolutionResult extends FieldResolutionResult
+      implements SuccessfulMemberResolutionResult<DexEncodedField, DexField> {
 
     private final DexClass initialResolutionHolder;
     private final DexClass resolvedHolder;
@@ -49,10 +65,12 @@
       this.resolvedField = resolvedField;
     }
 
+    @Override
     public DexClass getInitialResolutionHolder() {
       return initialResolutionHolder;
     }
 
+    @Override
     public DexClass getResolvedHolder() {
       return resolvedHolder;
     }
@@ -62,6 +80,11 @@
       return resolvedField;
     }
 
+    @Override
+    public DexEncodedField getResolvedMember() {
+      return resolvedField;
+    }
+
     public DexClassAndField getResolutionPair() {
       return DexClassAndField.create(resolvedHolder, resolvedField);
     }
@@ -81,6 +104,16 @@
     public SuccessfulFieldResolutionResult asSuccessfulResolution() {
       return this;
     }
+
+    @Override
+    public boolean isSuccessfulMemberResolutionResult() {
+      return true;
+    }
+
+    @Override
+    public SuccessfulFieldResolutionResult asSuccessfulMemberResolutionResult() {
+      return this;
+    }
   }
 
   public static class FailedFieldResolutionResult extends FieldResolutionResult {
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 bf6c5c8..07b3205 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLens.java
@@ -347,9 +347,9 @@
   }
 
   public boolean verifyMappingToOriginalProgram(
-      Iterable<DexProgramClass> classes,
-      DexApplication originalApplication,
-      DexItemFactory dexItemFactory) {
+      AppView<?> appView, DexApplication originalApplication) {
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    Iterable<DexProgramClass> classes = appView.appInfo().classesWithDeterministicOrder();
     // Collect all original fields and methods for efficient querying.
     Set<DexField> originalFields = Sets.newIdentityHashSet();
     Set<DexMethod> originalMethods = Sets.newIdentityHashSet();
@@ -365,7 +365,7 @@
     // Check that all fields and methods in the generated program can be mapped back to one of the
     // original fields or methods.
     for (DexProgramClass clazz : classes) {
-      if (clazz.type.isD8R8SynthesizedClassType()) {
+      if (appView.appInfo().getSyntheticItems().isSyntheticClass(clazz)) {
         continue;
       }
       for (DexEncodedField field : clazz.fields()) {
diff --git a/src/main/java/com/android/tools/r8/graph/MemberResolutionResult.java b/src/main/java/com/android/tools/r8/graph/MemberResolutionResult.java
new file mode 100644
index 0000000..da70e09
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/MemberResolutionResult.java
@@ -0,0 +1,13 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.graph;
+
+public interface MemberResolutionResult<
+    D extends DexEncodedMember<D, R>, R extends DexMember<D, R>> {
+
+  boolean isSuccessfulMemberResolutionResult();
+
+  SuccessfulMemberResolutionResult<D, R> asSuccessfulMemberResolutionResult();
+}
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 0997dc4..fb6e5d1 100644
--- a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
+++ b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
@@ -370,21 +370,6 @@
         DexDefinitionSupplier definitions,
         GraphLens lens) {
       instantiatedHierarchy = null;
-      objectAllocationInfos.classesWithAllocationSiteTracking.forEach(
-          (clazz, allocationSitesForClass) -> {
-            DexType type = lens.lookupType(clazz.type);
-            if (type.isPrimitiveType()) {
-              assert !objectAllocationInfos.hasInstantiatedStrictSubtype(clazz);
-              return;
-            }
-            DexProgramClass rewrittenClass = asProgramClassOrNull(definitions.definitionFor(type));
-            assert rewrittenClass != null;
-            assert !classesWithAllocationSiteTracking.containsKey(rewrittenClass);
-            classesWithAllocationSiteTracking.put(
-                rewrittenClass,
-                LensUtils.rewrittenWithRenamedSignature(
-                    allocationSitesForClass, definitions, lens));
-          });
       objectAllocationInfos.classesWithoutAllocationSiteTracking.forEach(
           clazz -> {
             DexType type = lens.lookupType(clazz.type);
@@ -394,10 +379,28 @@
             }
             DexProgramClass rewrittenClass = asProgramClassOrNull(definitions.definitionFor(type));
             assert rewrittenClass != null;
-            assert !classesWithAllocationSiteTracking.containsKey(rewrittenClass);
-            assert !classesWithoutAllocationSiteTracking.contains(rewrittenClass);
             classesWithoutAllocationSiteTracking.add(rewrittenClass);
           });
+      objectAllocationInfos.classesWithAllocationSiteTracking.forEach(
+          (clazz, allocationSitesForClass) -> {
+            DexType type = lens.lookupType(clazz.type);
+            if (type.isPrimitiveType()) {
+              assert !objectAllocationInfos.hasInstantiatedStrictSubtype(clazz);
+              return;
+            }
+            DexProgramClass rewrittenClass = asProgramClassOrNull(definitions.definitionFor(type));
+            assert rewrittenClass != null;
+            if (classesWithoutAllocationSiteTracking.contains(rewrittenClass)) {
+              // Either this class was merged into another class without allocation site tracking,
+              // or a class without allocation site tracking was merged into this class.
+              return;
+            }
+            classesWithAllocationSiteTracking
+                .computeIfAbsent(rewrittenClass, ignore -> Sets.newIdentityHashSet())
+                .addAll(
+                    LensUtils.rewrittenWithRenamedSignature(
+                        allocationSitesForClass, definitions, lens));
+          });
       for (DexProgramClass abstractType :
           objectAllocationInfos.interfacesWithUnknownSubtypeHierarchy) {
         DexType type = lens.lookupType(abstractType.type);
diff --git a/src/main/java/com/android/tools/r8/graph/ProgramField.java b/src/main/java/com/android/tools/r8/graph/ProgramField.java
index 29949d8..3069f4e 100644
--- a/src/main/java/com/android/tools/r8/graph/ProgramField.java
+++ b/src/main/java/com/android/tools/r8/graph/ProgramField.java
@@ -4,7 +4,8 @@
 
 package com.android.tools.r8.graph;
 
-public class ProgramField extends DexClassAndField {
+public class ProgramField extends DexClassAndField
+    implements ProgramMember<DexEncodedField, DexField> {
 
   public ProgramField(DexProgramClass holder, DexEncodedField field) {
     super(holder, field);
diff --git a/src/main/java/com/android/tools/r8/graph/ProgramMember.java b/src/main/java/com/android/tools/r8/graph/ProgramMember.java
new file mode 100644
index 0000000..7dc209c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/ProgramMember.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.graph;
+
+public interface ProgramMember<D extends DexEncodedMember<D, R>, R extends DexMember<D, R>> {
+
+  D getDefinition();
+
+  DexType getHolderType();
+}
diff --git a/src/main/java/com/android/tools/r8/graph/ProgramMethod.java b/src/main/java/com/android/tools/r8/graph/ProgramMethod.java
index 6ff5f1d..e69ade7 100644
--- a/src/main/java/com/android/tools/r8/graph/ProgramMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/ProgramMethod.java
@@ -11,7 +11,8 @@
 import com.android.tools.r8.origin.Origin;
 
 /** Type representing a method definition in the programs compilation unit and its holder. */
-public final class ProgramMethod extends DexClassAndMethod {
+public final class ProgramMethod extends DexClassAndMethod
+    implements ProgramMember<DexEncodedMethod, DexMethod> {
 
   public ProgramMethod(DexProgramClass holder, DexEncodedMethod method) {
     super(holder, method);
diff --git a/src/main/java/com/android/tools/r8/graph/ProgramPackage.java b/src/main/java/com/android/tools/r8/graph/ProgramPackage.java
new file mode 100644
index 0000000..28fc4e2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/ProgramPackage.java
@@ -0,0 +1,42 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.graph;
+
+import com.google.common.collect.Sets;
+import java.util.Iterator;
+import java.util.Set;
+import java.util.function.Consumer;
+
+public class ProgramPackage implements Iterable<DexProgramClass> {
+
+  private final String packageDescriptor;
+  private final Set<DexProgramClass> classes = Sets.newIdentityHashSet();
+
+  public ProgramPackage(String packageDescriptor) {
+    this.packageDescriptor = packageDescriptor;
+  }
+
+  public void add(DexProgramClass clazz) {
+    assert clazz.getType().getPackageDescriptor().equals(packageDescriptor);
+    classes.add(clazz);
+  }
+
+  public void forEachClass(Consumer<DexProgramClass> consumer) {
+    forEach(consumer);
+  }
+
+  public void forEachField(Consumer<ProgramField> consumer) {
+    forEach(clazz -> clazz.forEachProgramField(consumer));
+  }
+
+  public void forEachMethod(Consumer<ProgramMethod> consumer) {
+    forEach(clazz -> clazz.forEachProgramMethod(consumer));
+  }
+
+  @Override
+  public Iterator<DexProgramClass> iterator() {
+    return classes.iterator();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/ProgramPackageCollection.java b/src/main/java/com/android/tools/r8/graph/ProgramPackageCollection.java
new file mode 100644
index 0000000..57fc75a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/ProgramPackageCollection.java
@@ -0,0 +1,34 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.graph;
+
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+public class ProgramPackageCollection implements Iterable<ProgramPackage> {
+
+  private final Map<String, ProgramPackage> packages;
+
+  private ProgramPackageCollection(Map<String, ProgramPackage> packages) {
+    this.packages = packages;
+  }
+
+  public static ProgramPackageCollection create(AppView<?> appView) {
+    Map<String, ProgramPackage> packages = new HashMap<>();
+    assert !appView.appInfo().getSyntheticItems().hasPendingSyntheticClasses();
+    for (DexProgramClass clazz : appView.appInfo().classes()) {
+      packages
+          .computeIfAbsent(clazz.getType().getPackageDescriptor(), ProgramPackage::new)
+          .add(clazz);
+    }
+    return new ProgramPackageCollection(packages);
+  }
+
+  @Override
+  public Iterator<ProgramPackage> iterator() {
+    return packages.values().iterator();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/ResolutionResult.java b/src/main/java/com/android/tools/r8/graph/ResolutionResult.java
index d8eab37..3316b64 100644
--- a/src/main/java/com/android/tools/r8/graph/ResolutionResult.java
+++ b/src/main/java/com/android/tools/r8/graph/ResolutionResult.java
@@ -18,7 +18,8 @@
 import java.util.function.BiPredicate;
 import java.util.function.Consumer;
 
-public abstract class ResolutionResult {
+public abstract class ResolutionResult
+    implements MemberResolutionResult<DexEncodedMethod, DexMethod> {
 
   /**
    * Returns true if resolution succeeded *and* the resolved method has a known definition.
@@ -36,6 +37,17 @@
     return null;
   }
 
+  @Override
+  public boolean isSuccessfulMemberResolutionResult() {
+    return false;
+  }
+
+  @Override
+  public SuccessfulMemberResolutionResult<DexEncodedMethod, DexMethod>
+      asSuccessfulMemberResolutionResult() {
+    return null;
+  }
+
   /**
    * Returns true if resolution failed.
    *
@@ -59,6 +71,10 @@
     return isSingleResolution() ? asSingleResolution().getResolvedMethod() : null;
   }
 
+  public DexClass getInitialResolutionHolder() {
+    return null;
+  }
+
   public abstract OptionalBool isAccessibleFrom(
       DexProgramClass context, AppInfoWithClassHierarchy appInfo);
 
@@ -121,7 +137,8 @@
       LambdaDescriptor lambdaInstance, AppInfoWithClassHierarchy appInfo);
 
   /** Result for a resolution that succeeds with a known declaration/definition. */
-  public static class SingleResolutionResult extends ResolutionResult {
+  public static class SingleResolutionResult extends ResolutionResult
+      implements SuccessfulMemberResolutionResult<DexEncodedMethod, DexMethod> {
     private final DexClass initialResolutionHolder;
     private final DexClass resolvedHolder;
     private final DexEncodedMethod resolvedMethod;
@@ -141,10 +158,21 @@
           || initialResolutionHolder.type == resolvedMethod.holder();
     }
 
+    @Override
+    public DexClass getInitialResolutionHolder() {
+      return initialResolutionHolder;
+    }
+
+    @Override
     public DexClass getResolvedHolder() {
       return resolvedHolder;
     }
 
+    @Override
+    public DexEncodedMethod getResolvedMember() {
+      return resolvedMethod;
+    }
+
     public DexEncodedMethod getResolvedMethod() {
       return resolvedMethod;
     }
@@ -164,6 +192,17 @@
     }
 
     @Override
+    public boolean isSuccessfulMemberResolutionResult() {
+      return true;
+    }
+
+    @Override
+    public SuccessfulMemberResolutionResult<DexEncodedMethod, DexMethod>
+        asSuccessfulMemberResolutionResult() {
+      return this;
+    }
+
+    @Override
     public OptionalBool isAccessibleFrom(
         DexProgramClass context, AppInfoWithClassHierarchy appInfo) {
       return AccessControl.isMethodAccessible(
diff --git a/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java b/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java
index dd2cde4..2c50bf8 100644
--- a/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java
+++ b/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java
@@ -325,20 +325,20 @@
 
   private static final RewrittenPrototypeDescription none = new RewrittenPrototypeDescription();
 
-  private final boolean extraNullParameter;
+  private final int extraUnusedNullParameters;
   private final ArgumentInfoCollection argumentInfoCollection;
   private final RewrittenTypeInfo rewrittenReturnInfo;
 
   private RewrittenPrototypeDescription() {
-    this(false, null, ArgumentInfoCollection.empty());
+    this(0, null, ArgumentInfoCollection.empty());
   }
 
   private RewrittenPrototypeDescription(
-      boolean extraNullParameter,
+      int extraUnusedNullParameters,
       RewrittenTypeInfo rewrittenReturnInfo,
       ArgumentInfoCollection argumentsInfo) {
     assert argumentsInfo != null;
-    this.extraNullParameter = extraNullParameter;
+    this.extraUnusedNullParameters = extraUnusedNullParameters;
     this.rewrittenReturnInfo = rewrittenReturnInfo;
     this.argumentInfoCollection = argumentsInfo;
   }
@@ -350,12 +350,12 @@
     DexType returnType = method.proto.returnType;
     RewrittenTypeInfo returnInfo =
         returnType.isAlwaysNull(appView) ? RewrittenTypeInfo.toVoid(returnType, appView) : null;
-    return new RewrittenPrototypeDescription(false, returnInfo, removedArgumentsInfo);
+    return new RewrittenPrototypeDescription(0, returnInfo, removedArgumentsInfo);
   }
 
   public static RewrittenPrototypeDescription createForRewrittenTypes(
       RewrittenTypeInfo returnInfo, ArgumentInfoCollection rewrittenArgumentsInfo) {
-    return new RewrittenPrototypeDescription(false, returnInfo, rewrittenArgumentsInfo);
+    return new RewrittenPrototypeDescription(0, returnInfo, rewrittenArgumentsInfo);
   }
 
   public static RewrittenPrototypeDescription none() {
@@ -363,11 +363,13 @@
   }
 
   public boolean isEmpty() {
-    return !extraNullParameter && rewrittenReturnInfo == null && argumentInfoCollection.isEmpty();
+    return extraUnusedNullParameters == 0
+        && rewrittenReturnInfo == null
+        && argumentInfoCollection.isEmpty();
   }
 
-  public boolean hasExtraNullParameter() {
-    return extraNullParameter;
+  public int numberOfExtraUnusedNullParameters() {
+    return extraUnusedNullParameters;
   }
 
   public boolean hasBeenChangedToReturnVoid(AppView<?> appView) {
@@ -418,7 +420,7 @@
     assert rewrittenReturnInfo == null;
     return !hasBeenChangedToReturnVoid(appView)
         ? new RewrittenPrototypeDescription(
-            extraNullParameter,
+            extraUnusedNullParameters,
             RewrittenTypeInfo.toVoid(oldReturnType, appView),
             argumentInfoCollection)
         : this;
@@ -426,12 +428,21 @@
 
   public RewrittenPrototypeDescription withRemovedArguments(ArgumentInfoCollection other) {
     return new RewrittenPrototypeDescription(
-        extraNullParameter, rewrittenReturnInfo, argumentInfoCollection.combine(other));
+        extraUnusedNullParameters, rewrittenReturnInfo, argumentInfoCollection.combine(other));
   }
 
-  public RewrittenPrototypeDescription withExtraNullParameter() {
-    return !extraNullParameter
-        ? new RewrittenPrototypeDescription(true, rewrittenReturnInfo, argumentInfoCollection)
-        : this;
+  public RewrittenPrototypeDescription withExtraUnusedNullParameter() {
+    return withExtraUnusedNullParameters(1);
+  }
+
+  public RewrittenPrototypeDescription withExtraUnusedNullParameters(
+      int numberOfExtraUnusedNullParameters) {
+    if (numberOfExtraUnusedNullParameters == 0) {
+      return this;
+    }
+    return new RewrittenPrototypeDescription(
+        extraUnusedNullParameters + numberOfExtraUnusedNullParameters,
+        rewrittenReturnInfo,
+        argumentInfoCollection);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/SuccessfulMemberResolutionResult.java b/src/main/java/com/android/tools/r8/graph/SuccessfulMemberResolutionResult.java
new file mode 100644
index 0000000..84d89d0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/SuccessfulMemberResolutionResult.java
@@ -0,0 +1,15 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.graph;
+
+public interface SuccessfulMemberResolutionResult<
+    D extends DexEncodedMember<D, R>, R extends DexMember<D, R>> {
+
+  DexClass getInitialResolutionHolder();
+
+  DexClass getResolvedHolder();
+
+  D getResolvedMember();
+}
diff --git a/src/main/java/com/android/tools/r8/graph/SyntheticItems.java b/src/main/java/com/android/tools/r8/graph/SyntheticItems.java
new file mode 100644
index 0000000..20c073a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/SyntheticItems.java
@@ -0,0 +1,106 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.graph;
+
+import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
+import com.google.common.collect.ImmutableSet;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
+
+public class SyntheticItems {
+
+  public static class CommittedItems {
+    // Set of all types that represent synthesized items.
+    private final ImmutableSet<DexType> syntheticTypes;
+
+    private CommittedItems(ImmutableSet<DexType> syntheticTypes) {
+      this.syntheticTypes = syntheticTypes;
+    }
+
+    SyntheticItems toSyntheticItems() {
+      return new SyntheticItems(syntheticTypes);
+    }
+  }
+
+  // Thread safe collection of synthesized classes that are not yet committed to the application.
+  private final Map<DexType, DexProgramClass> pendingClasses = new ConcurrentHashMap<>();
+
+  // Immutable set of types that represent synthetic definitions in the application (eg, committed).
+  private final ImmutableSet<DexType> syntheticTypes;
+
+  private SyntheticItems(ImmutableSet<DexType> syntheticTypes) {
+    this.syntheticTypes = syntheticTypes;
+  }
+
+  public static SyntheticItems createInitialSyntheticItems() {
+    return new SyntheticItems(ImmutableSet.of());
+  }
+
+  public boolean hasPendingSyntheticClasses() {
+    return !pendingClasses.isEmpty();
+  }
+
+  public Collection<DexProgramClass> getPendingSyntheticClasses() {
+    return Collections.unmodifiableCollection(pendingClasses.values());
+  }
+
+  public DexClass definitionFor(DexType type, Function<DexType, DexClass> baseDefinitionFor) {
+    DexProgramClass pending = pendingClasses.get(type);
+    if (pending != null) {
+      assert baseDefinitionFor.apply(type) == null
+          : "Pending synthetic definition also present in the active program: " + type;
+      return pending;
+    }
+    return baseDefinitionFor.apply(type);
+  }
+
+  // TODO(b/158159959): Remove the usage of this direct class addition (and name-based id).
+  public void addSyntheticClass(DexProgramClass clazz) {
+    assert clazz.type.isD8R8SynthesizedClassType();
+    assert !syntheticTypes.contains(clazz.type);
+    DexProgramClass previous = pendingClasses.put(clazz.type, clazz);
+    assert previous == null || previous == clazz;
+  }
+
+  public boolean isSyntheticClass(DexType type) {
+    return syntheticTypes.contains(type)
+        || pendingClasses.containsKey(type)
+        // TODO(b/158159959): Remove usage of name-based identification.
+        || type.isD8R8SynthesizedClassType();
+  }
+
+  public boolean isSyntheticClass(DexProgramClass clazz) {
+    return isSyntheticClass(clazz.type);
+  }
+
+  public CommittedItems commit(DexApplication application) {
+    assert verifyAllPendingSyntheticsAreInApp(application, this);
+    // All synthetics are in the app proper and no further meta-data is present so the empty
+    // collection is currently returned here.
+    ImmutableSet<DexType> merged = syntheticTypes;
+    if (!pendingClasses.isEmpty()) {
+      merged =
+          ImmutableSet.<DexType>builder()
+              .addAll(syntheticTypes)
+              .addAll(pendingClasses.keySet())
+              .build();
+    }
+    return new CommittedItems(merged);
+  }
+
+  public CommittedItems commit(DexApplication application, NestedGraphLens lens) {
+    return new CommittedItems(lens.rewriteTypes(commit(application).syntheticTypes));
+  }
+
+  private static boolean verifyAllPendingSyntheticsAreInApp(
+      DexApplication app, SyntheticItems synthetics) {
+    for (DexProgramClass clazz : synthetics.getPendingSyntheticClasses()) {
+      assert app.programDefinitionFor(clazz.type) != null;
+    }
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/UseRegistry.java b/src/main/java/com/android/tools/r8/graph/UseRegistry.java
index de4021e..75e6565 100644
--- a/src/main/java/com/android/tools/r8/graph/UseRegistry.java
+++ b/src/main/java/com/android/tools/r8/graph/UseRegistry.java
@@ -17,54 +17,54 @@
     this.factory = factory;
   }
 
-  public abstract boolean registerInitClass(DexType type);
+  public abstract void registerInitClass(DexType type);
 
-  public abstract boolean registerInvokeVirtual(DexMethod method);
+  public abstract void registerInvokeVirtual(DexMethod method);
 
-  public abstract boolean registerInvokeDirect(DexMethod method);
+  public abstract void registerInvokeDirect(DexMethod method);
 
-  public abstract boolean registerInvokeStatic(DexMethod method);
+  public abstract void registerInvokeStatic(DexMethod method);
 
-  public abstract boolean registerInvokeInterface(DexMethod method);
+  public abstract void registerInvokeInterface(DexMethod method);
 
-  public abstract boolean registerInvokeSuper(DexMethod method);
+  public abstract void registerInvokeSuper(DexMethod method);
 
-  public abstract boolean registerInstanceFieldRead(DexField field);
+  public abstract void registerInstanceFieldRead(DexField field);
 
-  public boolean registerInstanceFieldReadFromMethodHandle(DexField field) {
-    return registerInstanceFieldRead(field);
+  public void registerInstanceFieldReadFromMethodHandle(DexField field) {
+    registerInstanceFieldRead(field);
   }
 
-  public abstract boolean registerInstanceFieldWrite(DexField field);
+  public abstract void registerInstanceFieldWrite(DexField field);
 
-  public boolean registerInstanceFieldWriteFromMethodHandle(DexField field) {
-    return registerInstanceFieldWrite(field);
+  public void registerInstanceFieldWriteFromMethodHandle(DexField field) {
+    registerInstanceFieldWrite(field);
   }
 
-  public abstract boolean registerNewInstance(DexType type);
+  public abstract void registerNewInstance(DexType type);
 
-  public abstract boolean registerStaticFieldRead(DexField field);
+  public abstract void registerStaticFieldRead(DexField field);
 
-  public boolean registerStaticFieldReadFromMethodHandle(DexField field) {
-    return registerStaticFieldRead(field);
+  public void registerStaticFieldReadFromMethodHandle(DexField field) {
+    registerStaticFieldRead(field);
   }
 
-  public abstract boolean registerStaticFieldWrite(DexField field);
+  public abstract void registerStaticFieldWrite(DexField field);
 
-  public boolean registerStaticFieldWriteFromMethodHandle(DexField field) {
-    return registerStaticFieldWrite(field);
+  public void registerStaticFieldWriteFromMethodHandle(DexField field) {
+    registerStaticFieldWrite(field);
   }
 
-  public abstract boolean registerTypeReference(DexType type);
+  public abstract void registerTypeReference(DexType type);
 
-  public abstract boolean registerInstanceOf(DexType type);
+  public abstract void registerInstanceOf(DexType type);
 
-  public boolean registerConstClass(DexType type) {
-    return registerTypeReference(type);
+  public void registerConstClass(DexType type) {
+    registerTypeReference(type);
   }
 
-  public boolean registerCheckCast(DexType type) {
-    return registerTypeReference(type);
+  public void registerCheckCast(DexType type) {
+    registerTypeReference(type);
   }
 
   public void registerMethodHandle(DexMethodHandle methodHandle, MethodHandleUse use) {
diff --git a/src/main/java/com/android/tools/r8/graph/classmerging/HorizontallyMergedLambdaClasses.java b/src/main/java/com/android/tools/r8/graph/classmerging/HorizontallyMergedLambdaClasses.java
index 1facc45..fc8e49b 100644
--- a/src/main/java/com/android/tools/r8/graph/classmerging/HorizontallyMergedLambdaClasses.java
+++ b/src/main/java/com/android/tools/r8/graph/classmerging/HorizontallyMergedLambdaClasses.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.graph.classmerging;
 
 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.shaking.AppInfoWithLiveness;
 import java.util.Set;
@@ -27,4 +28,9 @@
     }
     return true;
   }
+
+  @Override
+  public boolean hasBeenMerged(DexProgramClass clazz) {
+    return sources.contains(clazz.type);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/classmerging/MergedClasses.java b/src/main/java/com/android/tools/r8/graph/classmerging/MergedClasses.java
index 7b9bba3..66f9f6a 100644
--- a/src/main/java/com/android/tools/r8/graph/classmerging/MergedClasses.java
+++ b/src/main/java/com/android/tools/r8/graph/classmerging/MergedClasses.java
@@ -5,9 +5,12 @@
 package com.android.tools.r8.graph.classmerging;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 
 public interface MergedClasses {
 
   boolean verifyAllSourcesPruned(AppView<AppInfoWithLiveness> appView);
+
+  boolean hasBeenMerged(DexProgramClass clazz);
 }
diff --git a/src/main/java/com/android/tools/r8/graph/classmerging/MergedClassesCollection.java b/src/main/java/com/android/tools/r8/graph/classmerging/MergedClassesCollection.java
index ad7c71d..1ced397 100644
--- a/src/main/java/com/android/tools/r8/graph/classmerging/MergedClassesCollection.java
+++ b/src/main/java/com/android/tools/r8/graph/classmerging/MergedClassesCollection.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.graph.classmerging;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.ArrayList;
 import java.util.List;
@@ -24,4 +25,14 @@
     }
     return true;
   }
+
+  @Override
+  public boolean hasBeenMerged(DexProgramClass clazz) {
+    for (MergedClasses mergedClasses : collection) {
+      if (mergedClasses.hasBeenMerged(clazz)) {
+        return true;
+      }
+    }
+    return false;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/classmerging/VerticallyMergedClasses.java b/src/main/java/com/android/tools/r8/graph/classmerging/VerticallyMergedClasses.java
index 944d680..d4c730a 100644
--- a/src/main/java/com/android/tools/r8/graph/classmerging/VerticallyMergedClasses.java
+++ b/src/main/java/com/android/tools/r8/graph/classmerging/VerticallyMergedClasses.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.graph.classmerging;
 
 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.shaking.AppInfoWithLiveness;
 import com.google.common.collect.ImmutableList;
@@ -53,4 +54,9 @@
     }
     return true;
   }
+
+  @Override
+  public boolean hasBeenMerged(DexProgramClass clazz) {
+    return hasBeenMergedIntoSubtype(clazz.type);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/FieldMultiset.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/FieldMultiset.java
new file mode 100644
index 0000000..37222de
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/FieldMultiset.java
@@ -0,0 +1,37 @@
+// 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.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.google.common.collect.HashMultiset;
+import com.google.common.collect.Multiset;
+
+public class FieldMultiset {
+
+  private final Multiset<DexType> fields = HashMultiset.create();
+
+  public FieldMultiset(DexProgramClass clazz) {
+    for (DexEncodedField field : clazz.instanceFields()) {
+      fields.add(field.type());
+    }
+  }
+
+  @Override
+  public int hashCode() {
+    return fields.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object object) {
+    if (object instanceof FieldMultiset) {
+      FieldMultiset other = (FieldMultiset) object;
+      return fields.equals(other.fields);
+    } else {
+      return false;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
new file mode 100644
index 0000000..56da905
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -0,0 +1,48 @@
+// 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.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.MainDexClasses;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+public class HorizontalClassMerger {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final MainDexClasses mainDexClasses;
+
+  private final PolicyExecutor policyExecutor;
+
+  public HorizontalClassMerger(
+      AppView<AppInfoWithLiveness> appView, MainDexClasses mainDexClasses) {
+    this.appView = appView;
+    this.mainDexClasses = mainDexClasses;
+
+    Policy[] policies = {
+      // TODO: add policies
+    };
+    this.policyExecutor = new SimplePolicyExecutor(Arrays.asList(policies));
+  }
+
+  public Collection<Collection<DexProgramClass>> run() {
+    Map<FieldMultiset, Collection<DexProgramClass>> classes = new HashMap<>();
+
+    // Group classes by same field signature using the hash map.
+    for (DexProgramClass clazz : appView.appInfo().app().classesWithDeterministicOrder()) {
+      classes.computeIfAbsent(new FieldMultiset(clazz), ignore -> new ArrayList<>()).add(clazz);
+    }
+
+    // Run the policies on all collected classes to produce a final grouping.
+    Collection<Collection<DexProgramClass>> groups = policyExecutor.run(classes.values());
+
+    return groups;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicy.java
new file mode 100644
index 0000000..261e8d2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/MultiClassPolicy.java
@@ -0,0 +1,21 @@
+// 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.graph.DexProgramClass;
+import java.util.Collection;
+
+public abstract class MultiClassPolicy 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.
+   * @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<Collection<DexProgramClass>> apply(Collection<DexProgramClass> group);
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
new file mode 100644
index 0000000..d8dea98
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
@@ -0,0 +1,11 @@
+// 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;
+
+/**
+ * The super class of all horizontal class merging policies. Most classes will either implement
+ * {@link SingleClassPolicy} or {@link MultiClassPolicy}.
+ */
+public abstract class Policy {}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java
new file mode 100644
index 0000000..7ffaa1b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/PolicyExecutor.java
@@ -0,0 +1,24 @@
+// 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.graph.DexProgramClass;
+import java.util.Collection;
+
+public abstract class PolicyExecutor {
+  protected final Collection<Policy> policies;
+
+  public PolicyExecutor(Collection<Policy> policies) {
+    this.policies = policies;
+  }
+
+  /**
+   * Given an initial collection of class groups which can potentially be merged, run all of the
+   * policies registered to this policy executor on the class groups yielding a new collection of
+   * class groups.
+   */
+  public abstract Collection<Collection<DexProgramClass>> run(
+      Collection<Collection<DexProgramClass>> classes);
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java
new file mode 100644
index 0000000..3e6254c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java
@@ -0,0 +1,63 @@
+// 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.graph.DexProgramClass;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.stream.Collectors;
+
+/**
+ * This is a simple policy executor that ensures regular sequential execution of policies. It should
+ * primarily be readable and correct. The SimplePolicyExecutor should be a reference implementation,
+ * against which more efficient policy executors can be compared.
+ */
+public class SimplePolicyExecutor extends PolicyExecutor {
+  public SimplePolicyExecutor(Collection<Policy> policies) {
+    super(policies);
+  }
+
+  // TODO(b/165506334): if performing mutable operation ensure that linked lists are used
+  private Collection<Collection<DexProgramClass>> applySingleClassPolicy(
+      SingleClassPolicy policy, Collection<Collection<DexProgramClass>> groups) {
+    Iterator<Collection<DexProgramClass>> i = groups.iterator();
+    while (i.hasNext()) {
+      Collection<DexProgramClass> group = i.next();
+      Iterator<DexProgramClass> j = group.iterator();
+      while (j.hasNext()) {
+        DexProgramClass clazz = j.next();
+        if (!policy.canMerge(clazz)) {
+          j.remove();
+        }
+      }
+      if (group.isEmpty()) {
+        i.remove();
+      }
+    }
+    return groups;
+  }
+
+  private Collection<Collection<DexProgramClass>> applyMultiClassPolicy(
+      MultiClassPolicy policy, Collection<Collection<DexProgramClass>> groups) {
+    // For each group apply the multi class policy and add all the new groups together.
+    return groups.stream()
+        .flatMap(group -> policy.apply(group).stream())
+        .collect(Collectors.toList());
+  }
+
+  @Override
+  public Collection<Collection<DexProgramClass>> run(
+      Collection<Collection<DexProgramClass>> groups) {
+    for (Policy policy : policies) {
+      if (policy instanceof SingleClassPolicy) {
+        groups = applySingleClassPolicy((SingleClassPolicy) policy, groups);
+      } else if (policy instanceof MultiClassPolicy) {
+        groups = applyMultiClassPolicy((MultiClassPolicy) policy, groups);
+      }
+    }
+
+    return groups;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/SingleClassPolicy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/SingleClassPolicy.java
new file mode 100644
index 0000000..b0757c5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/SingleClassPolicy.java
@@ -0,0 +1,16 @@
+// 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.graph.DexProgramClass;
+
+public abstract class SingleClassPolicy extends Policy {
+  /**
+   * Determine if {@param program} can be merged with any other classes.
+   *
+   * @return {@code false} if the class should not be merged, otherwise {@code true}.
+   */
+  public abstract boolean canMerge(DexProgramClass program);
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
index f06a1c7..8d33495 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldaccess/TrivialFieldAccessReprocessor.java
@@ -183,68 +183,50 @@
     }
 
     @Override
-    public boolean registerInstanceFieldWrite(DexField field) {
-      return registerFieldAccess(field, false);
+    public void registerInstanceFieldWrite(DexField field) {
+      registerFieldAccess(field, false);
     }
 
     @Override
-    public boolean registerInstanceFieldRead(DexField field) {
-      return registerFieldAccess(field, false);
+    public void registerInstanceFieldRead(DexField field) {
+      registerFieldAccess(field, false);
     }
 
     @Override
-    public boolean registerStaticFieldRead(DexField field) {
-      return registerFieldAccess(field, true);
+    public void registerStaticFieldRead(DexField field) {
+      registerFieldAccess(field, true);
     }
 
     @Override
-    public boolean registerStaticFieldWrite(DexField field) {
-      return registerFieldAccess(field, true);
+    public void registerStaticFieldWrite(DexField field) {
+      registerFieldAccess(field, true);
     }
 
     @Override
-    public boolean registerInitClass(DexType clazz) {
-      return false;
-    }
+    public void registerInitClass(DexType clazz) {}
 
     @Override
-    public boolean registerInvokeVirtual(DexMethod method) {
-      return false;
-    }
+    public void registerInvokeVirtual(DexMethod method) {}
 
     @Override
-    public boolean registerInvokeDirect(DexMethod method) {
-      return false;
-    }
+    public void registerInvokeDirect(DexMethod method) {}
 
     @Override
-    public boolean registerInvokeStatic(DexMethod method) {
-      return false;
-    }
+    public void registerInvokeStatic(DexMethod method) {}
 
     @Override
-    public boolean registerInvokeInterface(DexMethod method) {
-      return false;
-    }
+    public void registerInvokeInterface(DexMethod method) {}
 
     @Override
-    public boolean registerInvokeSuper(DexMethod method) {
-      return false;
-    }
+    public void registerInvokeSuper(DexMethod method) {}
 
     @Override
-    public boolean registerNewInstance(DexType type) {
-      return false;
-    }
+    public void registerNewInstance(DexType type) {}
 
     @Override
-    public boolean registerTypeReference(DexType type) {
-      return false;
-    }
+    public void registerTypeReference(DexType type) {}
 
     @Override
-    public boolean registerInstanceOf(DexType type) {
-      return false;
-    }
+    public void registerInstanceOf(DexType type) {}
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoEnqueuerUseRegistry.java b/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoEnqueuerUseRegistry.java
index d5fdc33..2ef11c9 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoEnqueuerUseRegistry.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/proto/ProtoEnqueuerUseRegistry.java
@@ -37,12 +37,12 @@
    * traced manually by {@link ProtoEnqueuerExtension#tracePendingInstructionsInDynamicMethods}.
    */
   @Override
-  public boolean registerConstClass(DexType type) {
+  public void registerConstClass(DexType type) {
     if (references.isDynamicMethod(getContextMethod())) {
       enqueuer.addDeadProtoTypeCandidate(type);
-      return false;
+      return;
     }
-    return super.registerConstClass(type);
+    super.registerConstClass(type);
   }
 
   /**
@@ -53,11 +53,11 @@
    * traced manually by {@link ProtoEnqueuerExtension#tracePendingInstructionsInDynamicMethods}.
    */
   @Override
-  public boolean registerStaticFieldRead(DexField field) {
+  public void registerStaticFieldRead(DexField field) {
     if (references.isDynamicMethod(getContextMethod())) {
       enqueuer.addDeadProtoTypeCandidate(field.holder);
-      return false;
+      return;
     }
-    return super.registerStaticFieldRead(field);
+    super.registerStaticFieldRead(field);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilderBase.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilderBase.java
index 07783f5..52ab07a 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilderBase.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CallGraphBuilderBase.java
@@ -300,82 +300,67 @@
     }
 
     @Override
-    public boolean registerInitClass(DexType clazz) {
+    public void registerInitClass(DexType clazz) {
       processInitClass(clazz);
-      return false;
     }
 
     @Override
-    public boolean registerInvokeVirtual(DexMethod method) {
+    public void registerInvokeVirtual(DexMethod method) {
       processInvoke(Invoke.Type.VIRTUAL, method);
-      return false;
     }
 
     @Override
-    public boolean registerInvokeDirect(DexMethod method) {
+    public void registerInvokeDirect(DexMethod method) {
       processInvoke(Invoke.Type.DIRECT, method);
-      return false;
     }
 
     @Override
-    public boolean registerInvokeStatic(DexMethod method) {
+    public void registerInvokeStatic(DexMethod method) {
       processInvoke(Invoke.Type.STATIC, method);
-      return false;
     }
 
     @Override
-    public boolean registerInvokeInterface(DexMethod method) {
+    public void registerInvokeInterface(DexMethod method) {
       processInvoke(Invoke.Type.INTERFACE, method);
-      return false;
     }
 
     @Override
-    public boolean registerInvokeSuper(DexMethod method) {
+    public void registerInvokeSuper(DexMethod method) {
       processInvoke(Invoke.Type.SUPER, method);
-      return false;
     }
 
     @Override
-    public boolean registerInstanceFieldRead(DexField field) {
+    public void registerInstanceFieldRead(DexField field) {
       processFieldRead(field);
-      return false;
     }
 
     @Override
-    public boolean registerInstanceFieldWrite(DexField field) {
+    public void registerInstanceFieldWrite(DexField field) {
       processFieldWrite(field);
-      return false;
     }
 
     @Override
-    public boolean registerNewInstance(DexType type) {
+    public void registerNewInstance(DexType type) {
       if (type.isClassType()) {
         addClassInitializerTarget(type);
       }
-      return false;
     }
 
     @Override
-    public boolean registerStaticFieldRead(DexField field) {
+    public void registerStaticFieldRead(DexField field) {
       processFieldRead(field);
-      return false;
     }
 
     @Override
-    public boolean registerStaticFieldWrite(DexField field) {
+    public void registerStaticFieldWrite(DexField field) {
       processFieldWrite(field);
-      return false;
     }
 
     @Override
-    public boolean registerTypeReference(DexType type) {
-      return false;
-    }
+    public void registerTypeReference(DexType type) {}
 
     @Override
-    public boolean registerInstanceOf(DexType type) {
-      return false;
-    }
+    public void registerInstanceOf(DexType type) {}
 
     @Override
     public void registerCallSite(DexCallSite callSite) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
index 6b7b402..dfce72e 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRBuilder.java
@@ -538,13 +538,14 @@
       register++;
     }
 
-    int numberOfArguments =
+    int originalNumberOfArguments =
         method.method.proto.parameters.values.length
             + argumentsInfo.numberOfRemovedArguments()
-            + (method.isStatic() ? 0 : 1);
+            + (method.isStatic() ? 0 : 1)
+            - prototypeChanges.numberOfExtraUnusedNullParameters();
 
     int usedArgumentIndex = 0;
-    while (argumentIndex < numberOfArguments) {
+    while (argumentIndex < originalNumberOfArguments) {
       TypeElement type;
       ArgumentInfo argumentInfo = argumentsInfo.getArgumentInfo(argumentIndex);
       if (argumentInfo.isRemovedArgumentInfo()) {
@@ -558,14 +559,14 @@
         DexType argType;
         if (argumentInfo.isRewrittenTypeInfo()) {
           RewrittenTypeInfo argumentRewrittenTypeInfo = argumentInfo.asRewrittenTypeInfo();
-          assert method.method.proto.parameters.values[usedArgumentIndex]
+          assert method.method.proto.getParameter(usedArgumentIndex)
               == argumentRewrittenTypeInfo.getNewType();
           // The old type is used to prevent that a changed value from reference to primitive
           // type breaks IR building. Rewriting from the old to the new type will be done in the
           // IRConverter (typically through the lensCodeRewriter).
           argType = argumentRewrittenTypeInfo.getOldType();
         } else {
-          argType = method.method.proto.parameters.values[usedArgumentIndex];
+          argType = method.method.proto.getParameter(usedArgumentIndex);
         }
         usedArgumentIndex++;
         writeCallback.accept(register, argType);
@@ -579,6 +580,16 @@
       register += type.requiredRegisters();
       argumentIndex++;
     }
+
+    for (int i = 0; i < prototypeChanges.numberOfExtraUnusedNullParameters(); i++) {
+      DexType argType = method.method.proto.getParameter(usedArgumentIndex);
+      assert argType.isClassType();
+      TypeElement type = TypeElement.fromDexType(argType, Nullability.maybeNull(), appView);
+      register += type.requiredRegisters();
+      usedArgumentIndex++;
+      addExtraUnusedNullArgument(register);
+    }
+
     flushArgumentInstructions();
   }
 
@@ -954,6 +965,14 @@
     value.markAsThis();
   }
 
+  private void addExtraUnusedNullArgument(int register) {
+    // Extra unused null arguments should bypass the register check, they may use registers
+    // beyond the limit of what the method can use. They don't have debug information and are
+    // always null.
+    Value value = new Value(valueNumberGenerator.next(), TypeElement.getNull(), null);
+    addNonThisArgument(new Argument(value, currentBlock.size(), false));
+  }
+
   public void addNonThisArgument(int register, TypeElement typeLattice) {
     DebugLocalInfo local = getOutgoingLocal(register);
     Value value = writeRegister(register, typeLattice, ThrowingInfo.NO_THROW, local);
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 6cbf350..61a2f18 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
@@ -594,7 +594,7 @@
         if (appView.options().enableNeverMergePrefixes) {
           for (DexString neverMergePrefix : neverMergePrefixes) {
             // Synthetic classes will always be merged.
-            if (method.getHolderType().isD8R8SynthesizedClassType()) {
+            if (appView.appInfo().getSyntheticItems().isSyntheticClass(method.getHolder())) {
               continue;
             }
             if (method.getHolderType().descriptor.startsWith(neverMergePrefix)) {
@@ -739,11 +739,7 @@
     }
     if (enumUnboxer != null) {
       enumUnboxer.finishAnalysis();
-      enumUnboxer.unboxEnums(
-          postMethodProcessorBuilder,
-          executorService,
-          feedback,
-          classStaticizer == null ? Collections.emptySet() : classStaticizer.getCandidates());
+      enumUnboxer.unboxEnums(postMethodProcessorBuilder, executorService, feedback);
     }
     if (!options.debug) {
       new TrivialFieldAccessReprocessor(appView.withLiveness(), postMethodProcessorBuilder)
@@ -779,7 +775,7 @@
     }
 
     // Build a new application with jumbo string info.
-    Builder<?> builder = application.builder();
+    Builder<?> builder = appView.appInfo().app().builder();
     builder.setHighestSortingString(highestSortingString);
 
     printPhase("Lambda class synthesis");
@@ -1767,11 +1763,6 @@
         || definition.getOptimizationInfo().isReachabilitySensitive()) {
       return false;
     }
-    if (appView.options().enableEnumUnboxing && method.getHolder().isEnum()) {
-      // Although the method is pinned, we compute the inlining constraint for enum unboxing,
-      // but the inliner won't be able to inline the method (marked as pinned).
-      return true;
-    }
     if (appView.appInfo().hasLiveness()
         && appView.appInfo().withLiveness().isPinned(method.getReference())) {
       return false;
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
index 6c9877c..3e59e47 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
@@ -31,6 +31,7 @@
 import static com.android.tools.r8.ir.code.Opcodes.STATIC_PUT;
 import static com.android.tools.r8.utils.ObjectUtils.getBooleanOrElse;
 
+import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
@@ -283,12 +284,21 @@
                         ? null
                         : makeOutValue(invoke, code);
 
-                if (prototypeChanges.hasExtraNullParameter()) {
+                if (prototypeChanges.numberOfExtraUnusedNullParameters() > 0) {
                   iterator.previous();
-                  Value extraNullValue =
+                  Value nullInstruction =
                       iterator.insertConstNullInstruction(code, appView.options());
                   iterator.next();
-                  newInValues.add(extraNullValue);
+                  for (int i = 0; i < prototypeChanges.numberOfExtraUnusedNullParameters(); i++) {
+                    newInValues.add(nullInstruction);
+                  }
+                  // TODO(b/164901008): Fix when the number of arguments overflows.
+                  if (newInValues.size() > 255) {
+                    throw new CompilationError(
+                        "The addition of extra unused null parameters in R8 led to the overflow of"
+                            + " the number of arguments of the method "
+                            + actualTarget);
+                  }
                 }
 
                 assert newInValues.size()
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
index 8c65c1e..347f495 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/BackportedMethodRewriter.java
@@ -105,7 +105,7 @@
     if (androidApp != null) {
       DexApplication app =
           new ApplicationReader(androidApp, options, Timing.empty()).read(executor);
-      appInfo = new AppInfo(app);
+      appInfo = AppInfo.createInitialAppInfo(app);
     }
     AppView<?> appView = AppView.createForD8(appInfo, rewritePrefix);
     BackportedMethodRewriter.RewritableMethods rewritableMethods =
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
index 3709176..c4709c7 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
@@ -85,7 +85,9 @@
       Collection<LambdaClass> lambdaClasses, IRConverter converter, ExecutorService executorService)
       throws ExecutionException {
     SortedProgramMethodSet nonDexAccessibilityBridges = SortedProgramMethodSet.create();
-    for (LambdaClass lambdaClass : lambdaClasses) {
+    List<LambdaClass> sortedLambdaClasses = new ArrayList<>(lambdaClasses);
+    sortedLambdaClasses.sort((x, y) -> x.type.slowCompareTo(y.type));
+    for (LambdaClass lambdaClass : sortedLambdaClasses) {
       // This call may cause originalMethodSignatures to be updated.
       ProgramMethod accessibilityBridge = lambdaClass.target.ensureAccessibilityIfNeeded(true);
       if (accessibilityBridge != null
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/NestBasedAccessDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/NestBasedAccessDesugaring.java
index c7f6f2c..67571b3 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/NestBasedAccessDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/NestBasedAccessDesugaring.java
@@ -194,16 +194,10 @@
         DexProgramClass::checksumFromType);
   }
 
-  void synthesizeNestConstructor() {
-    synthesizeNestConstructor(null);
-  }
-
   void synthesizeNestConstructor(DexApplication.Builder<?> builder) {
     if (nestConstructorUsed) {
       appView.appInfo().addSynthesizedClass(nestConstructor);
-      if (builder != null) {
-        builder.addSynthesizedClass(nestConstructor, true);
-      }
+      builder.addSynthesizedClass(nestConstructor, true);
     }
   }
 
@@ -367,105 +361,97 @@
       this.context = context;
     }
 
-    private boolean registerInvoke(DexMethod method, Invoke.Type invokeType) {
+    private void registerInvoke(DexMethod method, Invoke.Type invokeType) {
       // Calls to non class type are not done through nest based access control.
       // Work-around for calls to enum.clone().
       if (!method.holder.isClassType()) {
-        return false;
+        return;
       }
       DexEncodedMethod encodedMethod = lookupOnHolder(method, context, invokeType);
       if (encodedMethod != null && invokeRequiresRewriting(encodedMethod, context)) {
         ensureInvokeBridge(encodedMethod);
-        return true;
       }
-      return false;
     }
 
-    private boolean registerFieldAccess(DexField field, boolean isGet) {
+    private void registerFieldAccess(DexField field, boolean isGet) {
       // Since we only need to desugar accesses to private fields, and all accesses to private
       // fields must be accessing the private field directly on its holder, we can lookup the field
       // on the holder instead of resolving the field.
       DexEncodedField encodedField = lookupOnHolder(field);
       if (encodedField != null && fieldAccessRequiresRewriting(encodedField, context)) {
         ensureFieldAccessBridge(encodedField, isGet);
-        return true;
       }
-      return false;
     }
 
     @Override
-    public boolean registerInitClass(DexType clazz) {
+    public void registerInitClass(DexType clazz) {
       // Nothing to do since we always use a public field for initializing the class.
-      return true;
     }
 
     @Override
-    public boolean registerInvokeVirtual(DexMethod method) {
+    public void registerInvokeVirtual(DexMethod method) {
       // Calls to class nest mate private methods are targeted by invokeVirtual in jdk11.
       // The spec recommends to do so, but do not enforce it, hence invokeDirect is also registered.
-      return registerInvoke(method, Invoke.Type.VIRTUAL);
+      registerInvoke(method, Invoke.Type.VIRTUAL);
     }
 
     @Override
-    public boolean registerInvokeDirect(DexMethod method) {
-      return registerInvoke(method, Invoke.Type.DIRECT);
+    public void registerInvokeDirect(DexMethod method) {
+      registerInvoke(method, Invoke.Type.DIRECT);
     }
 
     @Override
-    public boolean registerInvokeStatic(DexMethod method) {
-      return registerInvoke(method, Invoke.Type.STATIC);
+    public void registerInvokeStatic(DexMethod method) {
+      registerInvoke(method, Invoke.Type.STATIC);
     }
 
     @Override
-    public boolean registerInvokeInterface(DexMethod method) {
+    public void registerInvokeInterface(DexMethod method) {
       // Calls to interface nest mate private methods are targeted by invokeInterface in jdk11.
       // The spec recommends to do so, but do not enforce it, hence invokeDirect is also registered.
-      return registerInvoke(method, Invoke.Type.INTERFACE);
+      registerInvoke(method, Invoke.Type.INTERFACE);
     }
 
     @Override
-    public boolean registerInvokeSuper(DexMethod method) {
-      return registerInvoke(method, Invoke.Type.SUPER);
+    public void registerInvokeSuper(DexMethod method) {
+      registerInvoke(method, Invoke.Type.SUPER);
     }
 
     @Override
-    public boolean registerInstanceFieldWrite(DexField field) {
-      return registerFieldAccess(field, false);
+    public void registerInstanceFieldWrite(DexField field) {
+      registerFieldAccess(field, false);
     }
 
     @Override
-    public boolean registerInstanceFieldRead(DexField field) {
-      return registerFieldAccess(field, true);
+    public void registerInstanceFieldRead(DexField field) {
+      registerFieldAccess(field, true);
     }
 
     @Override
-    public boolean registerNewInstance(DexType type) {
+    public void registerNewInstance(DexType type) {
       // Unrelated to access based control.
       // The <init> method has to be rewritten instead
       // and <init> is called through registerInvoke.
-      return false;
     }
 
     @Override
-    public boolean registerStaticFieldRead(DexField field) {
-      return registerFieldAccess(field, true);
+    public void registerStaticFieldRead(DexField field) {
+      registerFieldAccess(field, true);
     }
 
     @Override
-    public boolean registerStaticFieldWrite(DexField field) {
-      return registerFieldAccess(field, false);
+    public void registerStaticFieldWrite(DexField field) {
+      registerFieldAccess(field, false);
     }
 
     @Override
-    public boolean registerTypeReference(DexType type) {
+    public void registerTypeReference(DexType type) {
       // Unrelated to access based control.
-      return false;
     }
 
     @Override
-    public boolean registerInstanceOf(DexType type) {
+    public void registerInstanceOf(DexType type) {
       // Unrelated to access based control.
-      return false;
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/NestedPrivateMethodLens.java b/src/main/java/com/android/tools/r8/ir/desugar/NestedPrivateMethodLens.java
index cdca27a..2669724 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/NestedPrivateMethodLens.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/NestedPrivateMethodLens.java
@@ -100,7 +100,8 @@
     if (isConstructorBridge(method)) {
       // TODO (b/132767654): Try to write a test which breaks that assertion.
       assert previousLens.lookupPrototypeChanges(method).isEmpty();
-      return RewrittenPrototypeDescription.none().withExtraNullParameter();
+      // TODO(b/164901008): Fix when the number of arguments overflows.
+      return RewrittenPrototypeDescription.none().withExtraUnusedNullParameter();
     } else {
       return previousLens.lookupPrototypeChanges(method);
     }
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/R8NestBasedAccessDesugaring.java b/src/main/java/com/android/tools/r8/ir/desugar/R8NestBasedAccessDesugaring.java
index d275f4a..e9ac116 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/R8NestBasedAccessDesugaring.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/R8NestBasedAccessDesugaring.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.ir.desugar;
 
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -32,14 +33,16 @@
     super(appView);
   }
 
-  public NestedPrivateMethodLens run(ExecutorService executorService) throws ExecutionException {
+  public NestedPrivateMethodLens run(
+      ExecutorService executorService, DexApplication.Builder<?> appBuilder)
+      throws ExecutionException {
     assert !appView.options().canUseNestBasedAccess()
         || appView.options().testing.enableForceNestBasedAccessDesugaringForTest;
     computeAndProcessNestsConcurrently(executorService);
     NestedPrivateMethodLens.Builder lensBuilder = NestedPrivateMethodLens.builder();
     addDeferredBridgesAndMapMethods(lensBuilder);
     clearNestAttributes();
-    synthesizeNestConstructor();
+    synthesizeNestConstructor(appBuilder);
     return lensBuilder.build(appView, getNestConstructorType());
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
index f1ed11b..cbb86df 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -153,8 +153,7 @@
 
     InternalOptions options = appView.options();
     if (options.featureSplitConfiguration != null
-        && !options.featureSplitConfiguration.inSameFeatureOrBase(
-            singleTarget.getReference(), method.getReference())) {
+        && !options.featureSplitConfiguration.inSameFeatureOrBothInBase(singleTarget, method)) {
       // Still allow inlining if we inline from the base into a feature.
       if (!options.featureSplitConfiguration.isInBase(singleTarget.getHolder())) {
         whyAreYouNotInliningReporter.reportInliningAcrossFeatureSplit();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java b/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
index a078c9b..89681f0 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.ir.optimize;
 
+import com.android.tools.r8.graph.AccessControl;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -23,7 +24,6 @@
 import com.android.tools.r8.ir.code.InvokeSuper;
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.Value;
-import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
@@ -166,10 +166,9 @@
         if (holderClass == null || holderClass.isInterface()) {
           continue;
         }
+
         // Due to the potential downcast below, make sure the new target holder is visible.
-        ConstraintWithTarget visibility =
-            ConstraintWithTarget.classIsVisible(context.getHolder(), holderType, appView);
-        if (visibility == ConstraintWithTarget.NEVER) {
+        if (AccessControl.isClassAccessible(holderClass, context, appView).isPossiblyFalse()) {
           continue;
         }
 
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 67b6310..fc01ffe 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
@@ -297,6 +297,13 @@
       assert SUBCLASS.ordinal() < ALWAYS.ordinal();
     }
 
+    public Constraint meet(Constraint otherConstraint) {
+      if (this.ordinal() < otherConstraint.ordinal()) {
+        return this;
+      }
+      return otherConstraint;
+    }
+
     boolean isSet(int value) {
       return (this.value & value) != 0;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
index 3b3d15d..f1bcf3e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ServiceLoaderRewriter.java
@@ -261,7 +261,6 @@
               return newClass;
             });
     assert clazz != null;
-    appView.appInfo().addSynthesizedClass(clazz);
     return clazz;
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
index a74b4b6..3f910f5 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
@@ -7,11 +7,17 @@
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.android.tools.r8.ir.analysis.type.Nullability.definitelyNotNull;
 
+import com.android.tools.r8.dex.Constants;
+import com.android.tools.r8.graph.AccessFlags;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ClassAccessFlags;
+import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexApplication.Builder;
+import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexClass.FieldSetter;
 import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMember;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -20,15 +26,19 @@
 import com.android.tools.r8.graph.DexProto;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.DexValue.DexValueInt;
 import com.android.tools.r8.graph.DexValue.DexValueNull;
+import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.graph.FieldResolutionResult;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.ResolutionResult;
 import com.android.tools.r8.graph.RewrittenPrototypeDescription;
 import com.android.tools.r8.graph.RewrittenPrototypeDescription.ArgumentInfoCollection;
 import com.android.tools.r8.graph.RewrittenPrototypeDescription.RewrittenTypeInfo;
+import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.ir.analysis.type.ArrayTypeElement;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
@@ -56,9 +66,9 @@
 import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback.OptimizationInfoFixer;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackDelayed;
+import com.android.tools.r8.origin.SynthesizedOrigin;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.BooleanUtils;
-import com.android.tools.r8.utils.IntBox;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
@@ -71,7 +81,6 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
@@ -79,6 +88,7 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
+import java.util.function.Predicate;
 
 public class EnumUnboxer implements PostOptimization {
 
@@ -333,8 +343,7 @@
   public void unboxEnums(
       PostMethodProcessor.Builder postBuilder,
       ExecutorService executorService,
-      OptimizationFeedbackDelayed feedback,
-      Set<DexType> hostsToAvoidIfPossible)
+      OptimizationFeedbackDelayed feedback)
       throws ExecutionException {
     // At this point the enumsToUnbox are no longer candidates, they will all be unboxed.
     if (enumsUnboxingCandidates.isEmpty()) {
@@ -344,13 +353,13 @@
     // Update keep info on any of the enum methods of the removed classes.
     updatePinnedItems(enumsToUnbox);
     enumUnboxerRewriter = new EnumUnboxingRewriter(appView, enumsToUnbox);
-    Map<DexType, DexType> newMethodLocation =
-        findNewMethodLocationOfUnboxableEnums(enumsToUnbox, hostsToAvoidIfPossible);
+    DirectMappedDexApplication.Builder appBuilder = appView.appInfo().app().asDirect().builder();
+    Map<DexType, DexType> newMethodLocation = synthesizeUnboxedEnumsMethodsLocations(appBuilder);
     NestedGraphLens enumUnboxingLens =
         new TreeFixer(enumsToUnbox).fixupTypeReferences(newMethodLocation);
     appView.setUnboxedEnums(enumUnboxerRewriter.getEnumsToUnbox());
     GraphLens previousLens = appView.graphLens();
-    appView.rewriteWithLens(enumUnboxingLens);
+    appView.rewriteWithLensAndApplication(enumUnboxingLens, appBuilder.build());
     // Update optimization info.
     feedback.fixupOptimizationInfos(
         appView,
@@ -388,82 +397,68 @@
   }
 
   // Some enums may have methods which require to stay in the current package for accessibility,
-  // in this case we find another class than the enum unboxing utility class to host these methods.
-  private Map<DexType, DexType> findNewMethodLocationOfUnboxableEnums(
-      Set<DexType> enumsToUnbox, Set<DexType> hostsToAvoidIfPossible) {
+  // in this case we create another class than the enum unboxing utility class to host these
+  // methods.
+  private Map<DexType, DexType> synthesizeUnboxedEnumsMethodsLocations(
+      DirectMappedDexApplication.Builder appBuilder) {
     if (enumsToUnboxWithPackageRequirement.isEmpty()) {
       return Collections.emptyMap();
     }
     Map<DexType, DexType> newMethodLocationMap = new IdentityHashMap<>();
-    Map<String, DexProgramClass> packageToClassMap =
-        getPackageToClassMapExcluding(enumsToUnbox, hostsToAvoidIfPossible);
+    Map<String, DexProgramClass> packageToClassMap = new HashMap<>();
     for (DexType toUnbox : enumsToUnboxWithPackageRequirement) {
-      DexProgramClass packageClass = packageToClassMap.get(toUnbox.getPackageDescriptor());
-      if (packageClass != null) {
-        newMethodLocationMap.put(toUnbox, packageClass.type);
+      String packageDescriptor = toUnbox.getPackageDescriptor();
+      DexProgramClass syntheticClass = packageToClassMap.get(packageDescriptor);
+      if (syntheticClass == null) {
+        syntheticClass = synthesizeUtilityClassInPackage(packageDescriptor, appBuilder);
+        packageToClassMap.put(packageDescriptor, syntheticClass);
       }
+      if (appView.appInfo().isInMainDexList(toUnbox)) {
+        appBuilder.addToMainDexList(syntheticClass.type);
+      }
+      newMethodLocationMap.put(toUnbox, syntheticClass.type);
     }
     enumsToUnboxWithPackageRequirement.clear();
     return newMethodLocationMap;
   }
 
-  // We are looking for another class in the same package as the unboxed enum to host the unboxed
-  // enum methods. We go through all classes, and for each package where a host is needed, we
-  // select a class.
-  private Map<String, DexProgramClass> getPackageToClassMapExcluding(
-      Set<DexType> enumsToUnbox, Set<DexType> hostsToAvoidIfPossible) {
-    HashSet<String> relevantPackages = new HashSet<>();
-    for (DexType toUnbox : enumsToUnbox) {
-      relevantPackages.add(toUnbox.getPackageDescriptor());
+  private DexProgramClass synthesizeUtilityClassInPackage(
+      String packageDescriptor, DirectMappedDexApplication.Builder appBuilder) {
+    DexType type =
+        factory.createType(
+            "L"
+                + packageDescriptor
+                + "/"
+                + EnumUnboxingRewriter.ENUM_UNBOXING_UTILITY_CLASS_NAME
+                + ";");
+    if (type == factory.enumUnboxingUtilityType) {
+      return appView.definitionFor(type).asProgramClass();
     }
-
-    Map<String, DexProgramClass> packageToClassMap = new HashMap<>();
-    for (DexProgramClass clazz : appView.appInfo().classes()) {
-      String packageDescriptor = clazz.type.getPackageDescriptor();
-      if (relevantPackages.contains(packageDescriptor) && !enumsToUnbox.contains(clazz.type)) {
-        DexProgramClass previousClass = packageToClassMap.get(packageDescriptor);
-        if (previousClass == null) {
-          packageToClassMap.put(packageDescriptor, clazz);
-        } else {
-          packageToClassMap.put(
-              packageDescriptor, selectHost(clazz, previousClass, hostsToAvoidIfPossible));
-        }
-      }
-    }
-
-    return packageToClassMap;
+    DexProgramClass syntheticClass =
+        new DexProgramClass(
+            type,
+            null,
+            new SynthesizedOrigin("enum unboxing", EnumUnboxer.class),
+            ClassAccessFlags.fromSharedAccessFlags(Constants.ACC_PUBLIC | Constants.ACC_SYNTHETIC),
+            factory.objectType,
+            DexTypeList.empty(),
+            null,
+            null,
+            Collections.emptyList(),
+            null,
+            Collections.emptyList(),
+            DexAnnotationSet.empty(),
+            DexEncodedField.EMPTY_ARRAY,
+            DexEncodedField.EMPTY_ARRAY,
+            DexEncodedMethod.EMPTY_ARRAY,
+            DexEncodedMethod.EMPTY_ARRAY,
+            factory.getSkipNameValidationForTesting(),
+            DexProgramClass::checksumFromType);
+    appBuilder.addSynthesizedClass(syntheticClass, false);
+    appView.appInfo().addSynthesizedClass(syntheticClass);
+    return syntheticClass;
   }
 
-  // We are trying to select a host for the enum unboxing methods, but multiple candidates are
-  // available. We need to pick one of the two classes and the result has to be deterministic.
-  // We follow the heuristics, in order:
-  //  1. don't pick a class from hostToAvoidIfPossible if possible
-  //  2. pick the class with the least number of methods
-  //  3. pick the first class name in alphabetical order for determinism.
-  private DexProgramClass selectHost(
-      DexProgramClass class1, DexProgramClass class2, Set<DexType> hostsToAvoidIfPossible) {
-    boolean avoid1 = hostsToAvoidIfPossible.contains(class1.type);
-    boolean avoid2 = hostsToAvoidIfPossible.contains(class2.type);
-    if (avoid1 && !avoid2) {
-      return class2;
-    }
-    if (avoid2 && !avoid1) {
-      return class1;
-    }
-    int size1 = class1.getMethodCollection().size();
-    int size2 = class2.getMethodCollection().size();
-    if (size1 < size2) {
-      return class1;
-    }
-    if (size2 < size1) {
-      return class2;
-    }
-    assert class1.type != class2.type;
-    if (class1.type.slowCompareTo(class2.type) < 0) {
-      return class1;
-    }
-    return class2;
-  }
 
   private void updatePinnedItems(Set<DexType> enumsToUnbox) {
     appView
@@ -504,11 +499,15 @@
 
   private Constraint analyzeAccessibilityInClass(DexProgramClass enumClass) {
     Constraint classConstraint = Constraint.ALWAYS;
+    EnumAccessibilityUseRegistry useRegistry = null;
     for (DexEncodedMethod method : enumClass.methods()) {
       // Enum initializer are analyzed in analyzeInitializers instead.
       if (!method.isInitializer()) {
-        Constraint methodConstraint = constraintForEnumUnboxing(method);
-        classConstraint = meet(classConstraint, methodConstraint);
+        if (useRegistry == null) {
+          useRegistry = new EnumAccessibilityUseRegistry(factory);
+        }
+        Constraint methodConstraint = constraintForEnumUnboxing(method, useRegistry);
+        classConstraint = classConstraint.meet(methodConstraint);
         if (classConstraint == Constraint.NEVER) {
           return classConstraint;
         }
@@ -517,45 +516,203 @@
     return classConstraint;
   }
 
-  private Constraint meet(Constraint constraint1, Constraint constraint2) {
-    if (constraint1 == Constraint.NEVER || constraint2 == Constraint.NEVER) {
-      return Constraint.NEVER;
-    }
-    if (constraint1 == Constraint.ALWAYS) {
-      return constraint2;
-    }
-    if (constraint2 == Constraint.ALWAYS) {
-      return constraint1;
-    }
-    assert constraint1 == Constraint.PACKAGE && constraint2 == Constraint.PACKAGE;
-    return Constraint.PACKAGE;
+  public Constraint constraintForEnumUnboxing(
+      DexEncodedMethod method, EnumAccessibilityUseRegistry useRegistry) {
+    return useRegistry.computeConstraint(method.asProgramMethod(appView));
   }
 
-  public Constraint constraintForEnumUnboxing(DexEncodedMethod method) {
-    // TODO(b/160939354): Use a UseRegistry instead of inlining constraints.
-    assert appView.definitionForProgramType(method.holder()) != null;
-    assert appView.definitionForProgramType(method.holder()).isEnum();
-    assert appView.definitionForProgramType(method.holder()).isEffectivelyFinal(appView);
-    assert appView.definitionForProgramType(method.holder()).superType
-        == appView.dexItemFactory().enumType;
-    assert !appView.definitionForProgramType(method.holder()).isInANest()
-        : "Unboxable enum is in a nest, this should not happen in cf to dex compilation, R8 needs"
-            + " to take care of nest access control when relocating unboxable enums methods";
-    switch (method.getCompilationState()) {
-      case PROCESSED_INLINING_CANDIDATE_ANY:
+  private class EnumAccessibilityUseRegistry extends UseRegistry {
+
+    private ProgramMethod context;
+    private Constraint constraint;
+
+    public EnumAccessibilityUseRegistry(DexItemFactory factory) {
+      super(factory);
+    }
+
+    public Constraint computeConstraint(ProgramMethod method) {
+      constraint = Constraint.ALWAYS;
+      context = method;
+      method.registerCodeReferences(this);
+      return constraint;
+    }
+
+    public Constraint deriveConstraint(DexType targetHolder, AccessFlags<?> flags) {
+      DexProgramClass contextHolder = context.getHolder();
+      if (targetHolder == contextHolder.type) {
         return Constraint.ALWAYS;
-      case PROCESSED_INLINING_CANDIDATE_SAME_NEST:
-        assert false;
-        // fall through
-      case PROCESSED_INLINING_CANDIDATE_SUBCLASS:
-        // There is no such thing as subclass access in this context, enums analyzed here have no
-        // subclasses, inherit directly from java.lang.Enum and are not analyzed if they call
-        // the protected methods in java.lang.Enum and java.lang.Object.
-      case PROCESSED_INLINING_CANDIDATE_SAME_CLASS:
-      case PROCESSED_INLINING_CANDIDATE_SAME_PACKAGE:
-        return Constraint.PACKAGE;
-      default:
+      }
+      if (flags.isPublic()) {
+        return Constraint.ALWAYS;
+      }
+      if (flags.isPrivate()) {
+        // Enum unboxing is currently happening only cf to dex, and no class should be in a nest
+        // at this point. If that is the case, we just don't unbox the enum, or we would need to
+        // support Constraint.SAMENEST in the enum unboxer.
+        assert !contextHolder.isInANest();
+        // Only accesses within the enum are allowed since all enum methods and fields will be
+        // moved to the same class, and the enum itself becomes an integer, which is
+        // accessible everywhere.
         return Constraint.NEVER;
+      }
+      assert flags.isProtected() || flags.isPackagePrivate();
+      // Protected is in practice equivalent to package private in this analysis since we are
+      // accessing the member from an enum context where subclassing is limited.
+      // At this point we don't support unboxing enums with subclasses, so we assume either
+      // same package access, or we just don't unbox.
+      // The only protected methods in java.lang.Enum are clone, finalize and the constructor.
+      // Besides calls to the constructor in the instance initializer, Enums with calls to such
+      // methods cannot be unboxed.
+      return targetHolder.isSamePackage(contextHolder.type) ? Constraint.PACKAGE : Constraint.NEVER;
+    }
+
+    @Override
+    public void registerTypeReference(DexType type) {
+      if (type.isArrayType()) {
+        registerTypeReference(type.toBaseType(factory));
+        return;
+      }
+
+      if (type.isPrimitiveType()) {
+        return;
+      }
+
+      DexClass definition = appView.definitionFor(type);
+      if (definition == null) {
+        constraint = Constraint.NEVER;
+        return;
+      }
+      constraint = constraint.meet(deriveConstraint(type, definition.accessFlags));
+    }
+
+    @Override
+    public void registerInitClass(DexType type) {
+      registerTypeReference(type);
+    }
+
+    @Override
+    public void registerInstanceOf(DexType type) {
+      registerTypeReference(type);
+    }
+
+    @Override
+    public void registerNewInstance(DexType type) {
+      registerTypeReference(type);
+    }
+
+    @Override
+    public void registerInvokeVirtual(DexMethod method) {
+      registerVirtualInvoke(method, false);
+    }
+
+    @Override
+    public void registerInvokeInterface(DexMethod method) {
+      registerVirtualInvoke(method, true);
+    }
+
+    private void registerVirtualInvoke(DexMethod method, boolean isInterface) {
+      if (method.holder.isArrayType()) {
+        return;
+      }
+      // Perform resolution and derive unboxing constraints based on the accessibility of the
+      // resolution result.
+      ResolutionResult resolutionResult = appView.appInfo().resolveMethod(method, isInterface);
+      if (!resolutionResult.isVirtualTarget()) {
+        constraint = Constraint.NEVER;
+        return;
+      }
+      registerTarget(
+          resolutionResult.getInitialResolutionHolder(), resolutionResult.getSingleTarget());
+    }
+
+    private void registerTarget(DexClass initialResolutionHolder, DexEncodedMember<?, ?> target) {
+      if (target == null) {
+        // This will fail at runtime.
+        constraint = Constraint.NEVER;
+        return;
+      }
+      DexType resolvedHolder = target.holder();
+      if (initialResolutionHolder == null) {
+        constraint = Constraint.NEVER;
+        return;
+      }
+      Constraint memberConstraint = deriveConstraint(resolvedHolder, target.getAccessFlags());
+      // We also have to take the constraint of the initial resolution holder into account.
+      Constraint classConstraint =
+          deriveConstraint(initialResolutionHolder.type, initialResolutionHolder.accessFlags);
+      Constraint instructionConstraint = memberConstraint.meet(classConstraint);
+      constraint = instructionConstraint.meet(constraint);
+    }
+
+    @Override
+    public void registerInvokeDirect(DexMethod method) {
+      registerSingleTargetInvoke(method, DexEncodedMethod::isDirectMethod);
+    }
+
+    @Override
+    public void registerInvokeStatic(DexMethod method) {
+      registerSingleTargetInvoke(method, DexEncodedMethod::isStatic);
+    }
+
+    private void registerSingleTargetInvoke(
+        DexMethod method, Predicate<DexEncodedMethod> methodValidator) {
+      if (method.holder.isArrayType()) {
+        return;
+      }
+      ResolutionResult resolutionResult =
+          appView.appInfo().unsafeResolveMethodDueToDexFormat(method);
+      DexEncodedMethod target = resolutionResult.getSingleTarget();
+      if (target == null || !methodValidator.test(target)) {
+        constraint = Constraint.NEVER;
+        return;
+      }
+      registerTarget(resolutionResult.getInitialResolutionHolder(), target);
+    }
+
+    @Override
+    public void registerInvokeSuper(DexMethod method) {
+      // Invoke-super can only target java.lang.Enum methods since we do not unbox enums with
+      // subclasses. Calls to java.lang.Object methods would have resulted in the enum to be marked
+      // as unboxable. The methods of java.lang.Enum called are already analyzed in the enum
+      // unboxer analysis, so invoke-super is always valid.
+      assert method.holder == factory.enumType;
+    }
+
+    @Override
+    public void registerCallSite(DexCallSite callSite) {
+      // This is reached after lambda desugaring, so this should not be a lambda call site.
+      // We do not unbox enums with invoke custom since it's not clear the accessibility
+      // constraints would be correct if the method holding the invoke custom is moved to
+      // another class.
+      assert !factory.isLambdaMetafactoryMethod(callSite.bootstrapMethod.asMethod());
+      constraint = Constraint.NEVER;
+    }
+
+    private void registerFieldInstruction(DexField field) {
+      FieldResolutionResult fieldResolutionResult = appView.appInfo().resolveField(field, context);
+      registerTarget(
+          fieldResolutionResult.getInitialResolutionHolder(),
+          fieldResolutionResult.getResolvedField());
+    }
+
+    @Override
+    public void registerInstanceFieldRead(DexField field) {
+      registerFieldInstruction(field);
+    }
+
+    @Override
+    public void registerInstanceFieldWrite(DexField field) {
+      registerFieldInstruction(field);
+    }
+
+    @Override
+    public void registerStaticFieldRead(DexField field) {
+      registerFieldInstruction(field);
+    }
+
+    @Override
+    public void registerStaticFieldWrite(DexField field) {
+      registerFieldInstruction(field);
     }
   }
 
@@ -908,11 +1065,9 @@
                   });
           clazz.getMethodCollection().removeMethods(methodsToRemove);
         } else {
-          IntBox index = new IntBox(0);
           clazz
               .getMethodCollection()
-              .replaceMethods(
-                  encodedMethod -> fixupEncodedMethod(encodedMethod, index.getAndIncrement()));
+              .replaceMethods(encodedMethod -> fixupEncodedMethod(encodedMethod));
           fixupFields(clazz.staticFields(), clazz::setStaticField);
           fixupFields(clazz.instanceFields(), clazz::setInstanceField);
         }
@@ -962,22 +1117,44 @@
       return encodedMethod.toTypeSubstitutedMethod(newMethod);
     }
 
-    private DexEncodedMethod fixupEncodedMethod(DexEncodedMethod encodedMethod, int index) {
+    private DexEncodedMethod fixupEncodedMethod(DexEncodedMethod encodedMethod) {
       DexProto newProto = fixupProto(encodedMethod.proto());
-      if (newProto != encodedMethod.proto()) {
-        DexString newMethodName =
-            factory.createString(
-                EnumUnboxingRewriter.ENUM_UNBOXING_UTILITY_METHOD_PREFIX
-                    + index
-                    + "$"
-                    + encodedMethod.getName().toString());
-        DexMethod newMethod = factory.createMethod(encodedMethod.holder(), newProto, newMethodName);
-        assert appView.definitionFor(encodedMethod.holder()).lookupMethod(newMethod) == null;
-        boolean isStatic = encodedMethod.isStatic();
-        lensBuilder.move(encodedMethod.method, isStatic, newMethod, isStatic);
-        return encodedMethod.toTypeSubstitutedMethod(newMethod);
+      if (newProto == encodedMethod.proto()) {
+        return encodedMethod;
       }
-      return encodedMethod;
+      assert !encodedMethod.isClassInitializer();
+      DexMethod newMethod =
+          factory.createMethod(encodedMethod.holder(), newProto, encodedMethod.getName());
+      newMethod = ensureUniqueMethod(encodedMethod, newMethod);
+      int numberOfExtraNullParameters = newMethod.getArity() - encodedMethod.method.getArity();
+      boolean isStatic = encodedMethod.isStatic();
+      lensBuilder.move(
+          encodedMethod.method, isStatic, newMethod, isStatic, numberOfExtraNullParameters);
+      return encodedMethod.toTypeSubstitutedMethod(newMethod);
+    }
+
+    private DexMethod ensureUniqueMethod(DexEncodedMethod encodedMethod, DexMethod newMethod) {
+      DexClass holder = appView.definitionFor(encodedMethod.holder());
+      assert holder != null;
+      if (encodedMethod.isInstanceInitializer()) {
+        while (holder.lookupMethod(newMethod) != null) {
+          newMethod =
+              factory.createMethod(
+                  newMethod.holder,
+                  factory.appendTypeToProto(newMethod.proto, factory.enumUnboxingUtilityType),
+                  newMethod.name);
+        }
+      } else {
+        int index = 0;
+        while (holder.lookupMethod(newMethod) != null) {
+          newMethod =
+              factory.createMethod(
+                  newMethod.holder,
+                  newMethod.proto,
+                  encodedMethod.getName().toString() + "$enumunboxing$" + index++);
+        }
+      }
+      return newMethod;
     }
 
     private void fixupFields(List<DexEncodedField> fields, FieldSetter setter) {
@@ -1091,6 +1268,15 @@
           new IdentityHashMap<>();
 
       public void move(DexMethod from, boolean fromStatic, DexMethod to, boolean toStatic) {
+        move(from, fromStatic, to, toStatic, 0);
+      }
+
+      public void move(
+          DexMethod from,
+          boolean fromStatic,
+          DexMethod to,
+          boolean toStatic,
+          int numberOfExtraNullParameters) {
         super.move(from, to);
         int offsetDiff = 0;
         int toOffset = BooleanUtils.intValue(!toStatic);
@@ -1114,7 +1300,9 @@
                 ? null
                 : new RewrittenTypeInfo(from.proto.returnType, to.proto.returnType);
         prototypeChanges.put(
-            to, RewrittenPrototypeDescription.createForRewrittenTypes(returnInfo, builder.build()));
+            to,
+            RewrittenPrototypeDescription.createForRewrittenTypes(returnInfo, builder.build())
+                .withExtraUnusedNullParameters(numberOfExtraNullParameters));
       }
 
       public EnumUnboxingLens build(
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java
index c4466cd..0372a33 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizer.java
@@ -113,11 +113,6 @@
     this.converter = converter;
   }
 
-  // This set is an approximation and can be used only for heuristics.
-  public final Set<DexType> getCandidates() {
-    return candidates.keySet();
-  }
-
   // Before doing any usage-based analysis we collect a set of classes that can be
   // candidates for staticizing. This analysis is very simple, but minimizes the
   // set of eligible classes staticizer tracks and thus time and memory it needs.
@@ -692,85 +687,82 @@
       super(factory);
     }
 
-    private boolean registerMethod(DexMethod method) {
+    private void registerMethod(DexMethod method) {
       registerTypeReference(method.holder);
       registerProto(method.proto);
-      return true;
     }
 
-    private boolean registerField(DexField field) {
+    private void registerField(DexField field) {
       registerTypeReference(field.holder);
       registerTypeReference(field.type);
-      return true;
     }
 
     @Override
-    public boolean registerInitClass(DexType clazz) {
-      return registerTypeReference(clazz);
+    public void registerInitClass(DexType clazz) {
+      registerTypeReference(clazz);
     }
 
     @Override
-    public boolean registerInvokeVirtual(DexMethod method) {
-      return registerMethod(method);
+    public void registerInvokeVirtual(DexMethod method) {
+      registerMethod(method);
     }
 
     @Override
-    public boolean registerInvokeDirect(DexMethod method) {
-      return registerMethod(method);
+    public void registerInvokeDirect(DexMethod method) {
+      registerMethod(method);
     }
 
     @Override
-    public boolean registerInvokeStatic(DexMethod method) {
-      return registerMethod(method);
+    public void registerInvokeStatic(DexMethod method) {
+      registerMethod(method);
     }
 
     @Override
-    public boolean registerInvokeInterface(DexMethod method) {
-      return registerMethod(method);
+    public void registerInvokeInterface(DexMethod method) {
+      registerMethod(method);
     }
 
     @Override
-    public boolean registerInvokeSuper(DexMethod method) {
-      return registerMethod(method);
+    public void registerInvokeSuper(DexMethod method) {
+      registerMethod(method);
     }
 
     @Override
-    public boolean registerInstanceFieldWrite(DexField field) {
-      return registerField(field);
+    public void registerInstanceFieldWrite(DexField field) {
+      registerField(field);
     }
 
     @Override
-    public boolean registerInstanceFieldRead(DexField field) {
-      return registerField(field);
+    public void registerInstanceFieldRead(DexField field) {
+      registerField(field);
     }
 
     @Override
-    public boolean registerNewInstance(DexType type) {
-      return registerTypeReference(type);
+    public void registerNewInstance(DexType type) {
+      registerTypeReference(type);
     }
 
     @Override
-    public boolean registerStaticFieldRead(DexField field) {
-      return registerField(field);
+    public void registerStaticFieldRead(DexField field) {
+      registerField(field);
     }
 
     @Override
-    public boolean registerStaticFieldWrite(DexField field) {
-      return registerField(field);
+    public void registerStaticFieldWrite(DexField field) {
+      registerField(field);
     }
 
     @Override
-    public boolean registerTypeReference(DexType type) {
+    public void registerTypeReference(DexType type) {
       CandidateInfo candidateInfo = candidates.get(type);
       if (candidateInfo != null) {
         candidateInfo.invalidate();
       }
-      return true;
     }
 
     @Override
-    public boolean registerInstanceOf(DexType type) {
-      return registerTypeReference(type);
+    public void registerInstanceOf(DexType type) {
+      registerTypeReference(type);
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
index 7af08f9..477f516 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/LinearScanRegisterAllocator.java
@@ -473,8 +473,12 @@
             ? block.getPredecessors().get(0).exceptionalExit().getNumber()
             : block.getPredecessors().get(0).exit().getNumber();
     for (LocalRange open : openRanges) {
+      Value predecessorValue =
+          open.value.isPhi() && open.value.getBlock() == block
+              ? open.value.asPhi().getOperand(0)
+              : open.value;
       int predecessorRegister =
-          allocator.getArgumentOrAllocateRegisterForValue(open.value, predecessorExitIndex);
+          allocator.getArgumentOrAllocateRegisterForValue(predecessorValue, predecessorExitIndex);
       initialLocals.put(predecessorRegister, open.local);
     }
     block.setLocalsAtEntry(initialLocals);
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinClassMetadataReader.java b/src/main/java/com/android/tools/r8/kotlin/KotlinClassMetadataReader.java
index 45cbd27..df06499 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinClassMetadataReader.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinClassMetadataReader.java
@@ -38,33 +38,44 @@
       Consumer<DexEncodedMethod> keepByteCode) {
     DexAnnotation meta = clazz.annotations().getFirstMatching(factory.kotlinMetadataType);
     if (meta != null) {
-      try {
-        KotlinClassMetadata kMetadata = toKotlinClassMetadata(kotlin, meta.annotation);
-        if (onlyProcessLambda && kMetadata.getHeader().getKind() != KOTLIN_METADATA_KIND_LAMBDA) {
-          return NO_KOTLIN_INFO;
-        }
-        return createKotlinInfo(kotlin, clazz, kMetadata, factory, reporter, keepByteCode);
-      } catch (ClassCastException | InconsistentKotlinMetadataException | MetadataError e) {
-        reporter.info(
-            new StringDiagnostic(
-                "Class "
-                    + clazz.type.toSourceString()
-                    + " has malformed kotlin.Metadata: "
-                    + e.getMessage()));
-        return INVALID_KOTLIN_INFO;
-      } catch (Throwable e) {
-        reporter.info(
-            new StringDiagnostic(
-                "Unexpected error while reading "
-                    + clazz.type.toSourceString()
-                    + "'s kotlin.Metadata: "
-                    + e.getMessage()));
-        return INVALID_KOTLIN_INFO;
-      }
+      return getKotlinInfo(kotlin, clazz, factory, reporter, onlyProcessLambda, keepByteCode, meta);
     }
     return NO_KOTLIN_INFO;
   }
 
+  public static KotlinClassLevelInfo getKotlinInfo(
+      Kotlin kotlin,
+      DexClass clazz,
+      DexItemFactory factory,
+      Reporter reporter,
+      boolean onlyProcessLambda,
+      Consumer<DexEncodedMethod> keepByteCode,
+      DexAnnotation annotation) {
+    try {
+      KotlinClassMetadata kMetadata = toKotlinClassMetadata(kotlin, annotation.annotation);
+      if (onlyProcessLambda && kMetadata.getHeader().getKind() != KOTLIN_METADATA_KIND_LAMBDA) {
+        return NO_KOTLIN_INFO;
+      }
+      return createKotlinInfo(kotlin, clazz, kMetadata, factory, reporter, keepByteCode);
+    } catch (ClassCastException | InconsistentKotlinMetadataException | MetadataError e) {
+      reporter.info(
+          new StringDiagnostic(
+              "Class "
+                  + clazz.type.toSourceString()
+                  + " has malformed kotlin.Metadata: "
+                  + e.getMessage()));
+      return INVALID_KOTLIN_INFO;
+    } catch (Throwable e) {
+      reporter.info(
+          new StringDiagnostic(
+              "Unexpected error while reading "
+                  + clazz.type.toSourceString()
+                  + "'s kotlin.Metadata: "
+                  + e.getMessage()));
+      return INVALID_KOTLIN_INFO;
+    }
+  }
+
   public static boolean hasKotlinClassMetadataAnnotation(
       DexClass clazz, DexDefinitionSupplier definitionSupplier) {
     return clazz
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataRewriter.java b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataRewriter.java
index ace8717..1001ca9 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataRewriter.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataRewriter.java
@@ -18,6 +18,8 @@
 import com.android.tools.r8.graph.DexValue.DexValueInt;
 import com.android.tools.r8.graph.DexValue.DexValueString;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.ConsumerUtils;
+import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.ThreadUtils;
 import java.util.ArrayList;
 import java.util.List;
@@ -42,7 +44,7 @@
     final boolean writePackageName;
     final boolean writeExtraInt;
 
-    public WriteMetadataFieldInfo(
+    private WriteMetadataFieldInfo(
         boolean writeKind,
         boolean writeMetadataVersion,
         boolean writeByteCodeVersion,
@@ -60,6 +62,10 @@
       this.writePackageName = writePackageName;
       this.writeExtraInt = writeExtraInt;
     }
+
+    private static WriteMetadataFieldInfo rewriteAll() {
+      return new WriteMetadataFieldInfo(true, true, true, true, true, true, true, true);
+    }
   }
 
   private final AppView<?> appView;
@@ -78,7 +84,7 @@
     return annotation.annotation.type != appView.dexItemFactory().kotlinMetadataType;
   }
 
-  public void run(ExecutorService executorService) throws ExecutionException {
+  public void runForR8(ExecutorService executorService) throws ExecutionException {
     final DexClass kotlinMetadata =
         appView.definitionFor(appView.dexItemFactory().kotlinMetadataType);
     final WriteMetadataFieldInfo writeMetadataFieldInfo =
@@ -112,26 +118,58 @@
             }
             return;
           }
-          try {
-            KotlinClassHeader kotlinClassHeader = kotlinInfo.rewrite(clazz, appView, lens);
-            DexAnnotation newMeta =
-                createKotlinMetadataAnnotation(
-                    kotlinClassHeader,
-                    kotlinInfo.getPackageName(),
-                    getMaxVersion(METADATA_VERSION_1_4, kotlinInfo.getMetadataVersion()),
-                    writeMetadataFieldInfo);
-            clazz.setAnnotations(
-                clazz.annotations().rewrite(anno -> anno == oldMeta ? newMeta : anno));
-          } catch (Throwable t) {
-            appView
-                .options()
-                .reporter
-                .warning(KotlinMetadataDiagnostic.unexpectedErrorWhenRewriting(clazz.type, t));
-          }
+          writeKotlinInfoToAnnotation(clazz, kotlinInfo, oldMeta, writeMetadataFieldInfo);
         },
         executorService);
   }
 
+  public void runForD8(ExecutorService executorService) throws ExecutionException {
+    if (lens.isIdentityLens()) {
+      return;
+    }
+    final Kotlin kotlin = factory.kotlin;
+    final Reporter reporter = appView.options().reporter;
+    final WriteMetadataFieldInfo writeMetadataFieldInfo = WriteMetadataFieldInfo.rewriteAll();
+    ThreadUtils.processItems(
+        appView.appInfo().classes(),
+        clazz -> {
+          DexAnnotation metadata = clazz.annotations().getFirstMatching(factory.kotlinMetadataType);
+          if (metadata == null) {
+            return;
+          }
+          final KotlinClassLevelInfo kotlinInfo =
+              KotlinClassMetadataReader.getKotlinInfo(
+                  kotlin, clazz, factory, reporter, false, ConsumerUtils.emptyConsumer(), metadata);
+          if (kotlinInfo == NO_KOTLIN_INFO) {
+            return;
+          }
+          writeKotlinInfoToAnnotation(clazz, kotlinInfo, metadata, writeMetadataFieldInfo);
+        },
+        executorService);
+  }
+
+  private void writeKotlinInfoToAnnotation(
+      DexClass clazz,
+      KotlinClassLevelInfo kotlinInfo,
+      DexAnnotation oldMeta,
+      WriteMetadataFieldInfo writeMetadataFieldInfo) {
+    try {
+      KotlinClassHeader kotlinClassHeader = kotlinInfo.rewrite(clazz, appView, lens);
+      DexAnnotation newMeta =
+          createKotlinMetadataAnnotation(
+              kotlinClassHeader,
+              kotlinInfo.getPackageName(),
+              getMaxVersion(METADATA_VERSION_1_4, kotlinInfo.getMetadataVersion()),
+              writeMetadataFieldInfo);
+      clazz.setAnnotations(clazz.annotations().rewrite(anno -> anno == oldMeta ? newMeta : anno));
+    } catch (Throwable t) {
+      appView
+          .options()
+          .reporter
+          .warning(KotlinMetadataDiagnostic.unexpectedErrorWhenRewriting(clazz.type, t));
+    }
+  }
+
   private boolean kotlinMetadataFieldExists(
       DexClass kotlinMetadata, AppView<?> appView, DexString fieldName) {
     if (!appView.appInfo().hasLiveness()) {
diff --git a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
index 998fad3..2d63c1e 100644
--- a/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/ClassNameMinifier.java
@@ -64,7 +64,10 @@
     this.packageNamingStrategy = packageNamingStrategy;
     this.classes = classes;
     InternalOptions options = appView.options();
-    this.packageObfuscationMode = options.getProguardConfiguration().getPackageObfuscationMode();
+    this.packageObfuscationMode =
+        options.testing.enableExperimentalRepackaging
+            ? PackageObfuscationMode.NONE
+            : options.getProguardConfiguration().getPackageObfuscationMode();
     this.isAccessModificationAllowed =
         options.getProguardConfiguration().isAccessModificationAllowed();
     this.keepInnerClassStructure = options.keepInnerClassStructure();
diff --git a/src/main/java/com/android/tools/r8/optimize/InvokeSingleTargetExtractor.java b/src/main/java/com/android/tools/r8/optimize/InvokeSingleTargetExtractor.java
index 37df60c..fadc223 100644
--- a/src/main/java/com/android/tools/r8/optimize/InvokeSingleTargetExtractor.java
+++ b/src/main/java/com/android/tools/r8/optimize/InvokeSingleTargetExtractor.java
@@ -17,7 +17,7 @@
     super(factory);
   }
 
-  private boolean setTarget(DexMethod target, InvokeKind kind) {
+  private void setTarget(DexMethod target, InvokeKind kind) {
     if (this.kind != InvokeKind.NONE) {
       this.kind = InvokeKind.ILLEGAL;
       this.target = null;
@@ -26,12 +26,10 @@
       this.target = target;
       this.kind = kind;
     }
-    return false;
   }
 
-  private boolean invalid() {
+  private void invalid() {
     kind = InvokeKind.ILLEGAL;
-    return false;
   }
 
   public DexMethod getTarget() {
@@ -43,68 +41,68 @@
   }
 
   @Override
-  public boolean registerInitClass(DexType clazz) {
-    return invalid();
+  public void registerInitClass(DexType clazz) {
+    invalid();
   }
 
   @Override
-  public boolean registerInvokeVirtual(DexMethod method) {
-    return setTarget(method, InvokeKind.VIRTUAL);
+  public void registerInvokeVirtual(DexMethod method) {
+    setTarget(method, InvokeKind.VIRTUAL);
   }
 
   @Override
-  public boolean registerInvokeDirect(DexMethod method) {
-    return invalid();
+  public void registerInvokeDirect(DexMethod method) {
+    invalid();
   }
 
   @Override
-  public boolean registerInvokeStatic(DexMethod method) {
-    return setTarget(method, InvokeKind.STATIC);
+  public void registerInvokeStatic(DexMethod method) {
+    setTarget(method, InvokeKind.STATIC);
   }
 
   @Override
-  public boolean registerInvokeInterface(DexMethod method) {
-    return invalid();
+  public void registerInvokeInterface(DexMethod method) {
+    invalid();
   }
 
   @Override
-  public boolean registerInvokeSuper(DexMethod method) {
-    return setTarget(method, InvokeKind.SUPER);
+  public void registerInvokeSuper(DexMethod method) {
+    setTarget(method, InvokeKind.SUPER);
   }
 
   @Override
-  public boolean registerInstanceFieldWrite(DexField field) {
-    return invalid();
+  public void registerInstanceFieldWrite(DexField field) {
+    invalid();
   }
 
   @Override
-  public boolean registerInstanceFieldRead(DexField field) {
-    return invalid();
+  public void registerInstanceFieldRead(DexField field) {
+    invalid();
   }
 
   @Override
-  public boolean registerNewInstance(DexType type) {
-    return invalid();
+  public void registerNewInstance(DexType type) {
+    invalid();
   }
 
   @Override
-  public boolean registerStaticFieldRead(DexField field) {
-    return invalid();
+  public void registerStaticFieldRead(DexField field) {
+    invalid();
   }
 
   @Override
-  public boolean registerStaticFieldWrite(DexField field) {
-    return invalid();
+  public void registerStaticFieldWrite(DexField field) {
+    invalid();
   }
 
   @Override
-  public boolean registerTypeReference(DexType type) {
-    return invalid();
+  public void registerTypeReference(DexType type) {
+    invalid();
   }
 
   @Override
-  public boolean registerInstanceOf(DexType type) {
-    return invalid();
+  public void registerInstanceOf(DexType type) {
+    invalid();
   }
 
   public enum InvokeKind {
diff --git a/src/main/java/com/android/tools/r8/relocator/Relocator.java b/src/main/java/com/android/tools/r8/relocator/Relocator.java
index a33424b..e45afd6 100644
--- a/src/main/java/com/android/tools/r8/relocator/Relocator.java
+++ b/src/main/java/com/android/tools/r8/relocator/Relocator.java
@@ -79,8 +79,7 @@
     Timing timing = Timing.create("Relocator", options);
     try {
       DexApplication app = new ApplicationReader(inputApp, options, timing).read(executor);
-
-      AppInfo appInfo = new AppInfo(app);
+      AppInfo appInfo = AppInfo.createInitialAppInfo(app);
       AppView<?> appView = AppView.createForRelocator(appInfo);
       appView.setAppServices(AppServices.builder(appView).build());
 
diff --git a/src/main/java/com/android/tools/r8/repackaging/Repackaging.java b/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
new file mode 100644
index 0000000..4077f36
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/repackaging/Repackaging.java
@@ -0,0 +1,72 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.repackaging;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ProgramPackage;
+import com.android.tools.r8.graph.ProgramPackageCollection;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.ProguardConfiguration;
+import com.android.tools.r8.utils.Timing;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * Entry-point for supporting the -repackageclasses and -flattenpackagehierarchy directives.
+ *
+ * <p>This pass moves all classes in the program into a user-specified package. Some classes may not
+ * be allowed to be renamed, and thus must remain in the original package.
+ *
+ * <p>A complication is that there can be (i) references to package-private or protected items that
+ * must remain in the package, and (ii) references from methods that must remain in the package to
+ * package-private or protected items. To ensure that such references remain valid after
+ * repackaging, an analysis is run that finds the minimal set of classes that must remain in the
+ * original package due to accessibility constraints.
+ */
+public class Repackaging {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final ProguardConfiguration proguardConfiguration;
+
+  public Repackaging(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+    this.proguardConfiguration = appView.options().getProguardConfiguration();
+  }
+
+  public void run(ExecutorService executorService, Timing timing) throws ExecutionException {
+    timing.begin("Repackage classes");
+    run(executorService);
+    timing.end();
+  }
+
+  private void run(ExecutorService executorService) throws ExecutionException {
+    if (proguardConfiguration.getPackageObfuscationMode().isNone()) {
+      return;
+    }
+
+    // For each package, find the set of classes that can be repackaged, and move them to the
+    // desired namespace.
+    ProgramPackageCollection packages = ProgramPackageCollection.create(appView);
+    for (ProgramPackage pkg : packages) {
+      Iterable<DexProgramClass> classesToRepackage =
+          computeClassesToRepackage(pkg, executorService);
+      // TODO(b/165783399): Move each class in `classesToRepackage`.
+      // TODO(b/165783399): Investigate if repackaging can lead to different dynamic dispatch. See,
+      //  for example, CrossPackageInvokeSuperToPackagePrivateMethodTest.
+    }
+  }
+
+  private Iterable<DexProgramClass> computeClassesToRepackage(
+      ProgramPackage pkg, ExecutorService executorService) throws ExecutionException {
+    RepackagingConstraintGraph constraintGraph = new RepackagingConstraintGraph(appView, pkg);
+    boolean canRepackageAllClasses = constraintGraph.initializeGraph();
+    if (canRepackageAllClasses) {
+      return pkg;
+    }
+    constraintGraph.populateConstraints(executorService);
+    return constraintGraph.computeClassesToRepackage();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/repackaging/RepackagingConstraintGraph.java b/src/main/java/com/android/tools/r8/repackaging/RepackagingConstraintGraph.java
new file mode 100644
index 0000000..9619f5a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/repackaging/RepackagingConstraintGraph.java
@@ -0,0 +1,124 @@
+// 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.repackaging;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexDefinition;
+import com.android.tools.r8.graph.DexEncodedMember;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.ProgramPackage;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.ThreadUtils;
+import com.google.common.collect.Sets;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * An undirected graph that contains a node for each class, field, and method in a given package.
+ *
+ * <p>An edge X <-> Y is added if X contains a reference to Y and Y is only accessible to X if X and
+ * Y are in the same package.
+ *
+ * <p>Once the graph is populated, we compute the set of reachable nodes from the set of root nodes
+ * that cannot be repackaged due to a -keep rule. The remaining nodes in the graph can all be
+ * repackaged.
+ */
+public class RepackagingConstraintGraph {
+
+  private final AppView<AppInfoWithLiveness> appView;
+  private final ProgramPackage pkg;
+  private final Map<DexDefinition, Node> nodes = new IdentityHashMap<>();
+
+  public RepackagingConstraintGraph(AppView<AppInfoWithLiveness> appView, ProgramPackage pkg) {
+    this.appView = appView;
+    this.pkg = pkg;
+  }
+
+  /** Returns true if all classes in the package can be repackaged. */
+  public boolean initializeGraph() {
+    // Add all the items in the package into the graph. This way we know which items belong to the
+    // package without having to extract package descriptor strings and comparing them with the
+    // package descriptor.
+    boolean hasPackagePrivateOrProtectedItem = false;
+    boolean hasPinnedItem = false;
+    for (DexProgramClass clazz : pkg) {
+      boolean isPinned = !appView.appInfo().isMinificationAllowed(clazz.getType());
+      hasPinnedItem |= isPinned;
+      nodes.put(clazz, new Node(clazz));
+      hasPackagePrivateOrProtectedItem |= clazz.getAccessFlags().isPackagePrivateOrProtected();
+      for (DexEncodedMember<?, ?> member : clazz.members()) {
+        nodes.put(member, new Node(member));
+        hasPackagePrivateOrProtectedItem |= member.getAccessFlags().isPackagePrivateOrProtected();
+      }
+    }
+    return !hasPinnedItem || !hasPackagePrivateOrProtectedItem;
+  }
+
+  Node getNode(DexDefinition definition) {
+    return nodes.get(definition);
+  }
+
+  public void populateConstraints(ExecutorService executorService) throws ExecutionException {
+    // Concurrently add references from methods to the graph.
+    ThreadUtils.processItems(
+        pkg::forEachMethod, this::registerReferencesFromMethod, executorService);
+
+    // TODO(b/165783399): Evaluate if it is worth to parallelize this. The work per field and class
+    //  should be little, so it may not be.
+    pkg.forEachClass(this::registerReferencesFromClass);
+    pkg.forEachField(this::registerReferencesFromField);
+  }
+
+  private void registerReferencesFromClass(DexProgramClass clazz) {
+    // TODO(b/165783399): Trace the references to the immediate super types.
+    // TODO(b/165783399): Maybe trace the references in the nest host and/or members.
+    // TODO(b/165783399): Maybe trace the references to the inner classes.
+    // TODO(b/165783399): Maybe trace the references in @kotlin.Metadata.
+  }
+
+  private void registerReferencesFromField(ProgramField field) {
+    // TODO(b/165783399): Trace the type of the field.
+    // TODO(b/165783399): Trace the references in the field annotations.
+  }
+
+  private void registerReferencesFromMethod(ProgramMethod method) {
+    // TODO(b/165783399): Trace the type references in the method signature.
+    // TODO(b/165783399): Trace the references in the method and method parameter annotations.
+    DexEncodedMethod definition = method.getDefinition();
+    if (definition.hasCode()) {
+      RepackagingUseRegistry registry = new RepackagingUseRegistry(appView, this, method);
+      definition.getCode().registerCodeReferences(method, registry);
+    }
+  }
+
+  public Iterable<DexProgramClass> computeClassesToRepackage() {
+    // TODO(b/165783399): From each node in the graph that cannot be moved elsewhere due to a -keep
+    //  rule, mark all neighbors as pinned, and repeat.
+    return Collections.emptyList();
+  }
+
+  static class Node {
+
+    private final DexDefinition definition;
+
+    private final Set<Node> neighbors = Sets.newConcurrentHashSet();
+
+    private Node(DexDefinition definition) {
+      this.definition = definition;
+    }
+
+    public void addNeighbor(Node neighbor) {
+      neighbors.add(neighbor);
+      neighbor.neighbors.add(this);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/repackaging/RepackagingUseRegistry.java b/src/main/java/com/android/tools/r8/repackaging/RepackagingUseRegistry.java
new file mode 100644
index 0000000..531e6c8
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/repackaging/RepackagingUseRegistry.java
@@ -0,0 +1,182 @@
+// 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.repackaging;
+
+import com.android.tools.r8.graph.AccessFlags;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ClassAccessFlags;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.MemberResolutionResult;
+import com.android.tools.r8.graph.ProgramMember;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.SuccessfulMemberResolutionResult;
+import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+public class RepackagingUseRegistry extends UseRegistry {
+
+  private final AppInfoWithLiveness appInfo;
+  private final RepackagingConstraintGraph constraintGraph;
+  private final ProgramMethod context;
+  private final RepackagingConstraintGraph.Node node;
+
+  public RepackagingUseRegistry(
+      AppView<AppInfoWithLiveness> appView,
+      RepackagingConstraintGraph constraintGraph,
+      ProgramMethod context) {
+    super(appView.dexItemFactory());
+    this.appInfo = appView.appInfo();
+    this.constraintGraph = constraintGraph;
+    this.context = context;
+    this.node = constraintGraph.getNode(context.getDefinition());
+  }
+
+  private boolean isOnlyAccessibleFromSamePackage(DexProgramClass referencedClass) {
+    ClassAccessFlags accessFlags = referencedClass.getAccessFlags();
+    if (accessFlags.isPackagePrivate()) {
+      return true;
+    }
+    if (accessFlags.isProtected()
+        && !appInfo.isSubtype(context.getHolderType(), referencedClass.getType())) {
+      return true;
+    }
+    return false;
+  }
+
+  private boolean isOnlyAccessibleFromSamePackage(ProgramMember<?, ?> member) {
+    AccessFlags<?> accessFlags = member.getDefinition().getAccessFlags();
+    if (accessFlags.isPackagePrivate()) {
+      return true;
+    }
+    if (accessFlags.isProtected()
+        && !appInfo.isSubtype(context.getHolderType(), member.getHolderType())) {
+      return true;
+    }
+    return false;
+  }
+
+  private void registerMemberAccess(MemberResolutionResult<?, ?> resolutionResult) {
+    SuccessfulMemberResolutionResult<?, ?> successfulResolutionResult =
+        resolutionResult.asSuccessfulMemberResolutionResult();
+    if (successfulResolutionResult == null) {
+      // TODO(b/165783399): If we want to preserve errors in the original program, we need to look
+      //  at the failure dependencies. For example, if this method accesses in a package-private
+      //  method in another package, and we move the two methods to the same package, then the
+      //  invoke would no longer fail with an IllegalAccessError.
+      return;
+    }
+
+    // Check access to the initial resolution holder.
+    registerTypeAccess(successfulResolutionResult.getInitialResolutionHolder());
+
+    // Similarly, check access to the resolved member.
+    ProgramMember<?, ?> resolvedMember =
+        successfulResolutionResult.getResolvedMember().asProgramMember(appInfo);
+    if (resolvedMember != null) {
+      RepackagingConstraintGraph.Node resolvedMemberNode =
+          constraintGraph.getNode(resolvedMember.getDefinition());
+      if (resolvedMemberNode != null && isOnlyAccessibleFromSamePackage(resolvedMember)) {
+        node.addNeighbor(resolvedMemberNode);
+      }
+    }
+  }
+
+  private void registerTypeAccess(DexType type) {
+    if (type.isArrayType()) {
+      registerTypeAccess(type.toBaseType(appInfo.dexItemFactory()));
+      return;
+    }
+    if (type.isPrimitiveType()) {
+      return;
+    }
+    assert type.isClassType();
+    DexClass clazz = appInfo.definitionFor(type);
+    if (clazz != null) {
+      registerTypeAccess(clazz);
+    }
+  }
+
+  private void registerTypeAccess(DexClass clazz) {
+    // We only want to connect the current method node to the class node if the access requires the
+    // two nodes to be in the same package. Therefore, we ignore accesses to non-program classes
+    // and program classes outside the current package.
+    DexProgramClass programClass = clazz.asProgramClass();
+    if (programClass != null) {
+      RepackagingConstraintGraph.Node classNode = constraintGraph.getNode(programClass);
+      if (classNode != null && isOnlyAccessibleFromSamePackage(programClass)) {
+        node.addNeighbor(classNode);
+      }
+    }
+  }
+
+  @Override
+  public void registerInitClass(DexType type) {
+    registerTypeAccess(type);
+  }
+
+  @Override
+  public void registerInvokeVirtual(DexMethod invokedMethod) {
+    registerMemberAccess(appInfo.resolveMethod(invokedMethod, false));
+  }
+
+  @Override
+  public void registerInvokeDirect(DexMethod invokedMethod) {
+    registerMemberAccess(appInfo.unsafeResolveMethodDueToDexFormat(invokedMethod));
+  }
+
+  @Override
+  public void registerInvokeStatic(DexMethod invokedMethod) {
+    registerMemberAccess(appInfo.unsafeResolveMethodDueToDexFormat(invokedMethod));
+  }
+
+  @Override
+  public void registerInvokeInterface(DexMethod invokedMethod) {
+    registerMemberAccess(appInfo.resolveMethod(invokedMethod, true));
+  }
+
+  @Override
+  public void registerInvokeSuper(DexMethod invokedMethod) {
+    registerMemberAccess(appInfo.unsafeResolveMethodDueToDexFormat(invokedMethod));
+  }
+
+  @Override
+  public void registerInstanceFieldRead(DexField field) {
+    registerMemberAccess(appInfo.resolveField(field));
+  }
+
+  @Override
+  public void registerInstanceFieldWrite(DexField field) {
+    registerMemberAccess(appInfo.resolveField(field));
+  }
+
+  @Override
+  public void registerNewInstance(DexType type) {
+    registerTypeAccess(type);
+  }
+
+  @Override
+  public void registerStaticFieldRead(DexField field) {
+    registerMemberAccess(appInfo.resolveField(field));
+  }
+
+  @Override
+  public void registerStaticFieldWrite(DexField field) {
+    registerMemberAccess(appInfo.resolveField(field));
+  }
+
+  @Override
+  public void registerTypeReference(DexType type) {
+    registerTypeAccess(type);
+  }
+
+  @Override
+  public void registerInstanceOf(DexType type) {
+    registerTypeAccess(type);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/retrace/RetraceRegularExpression.java b/src/main/java/com/android/tools/r8/retrace/RetraceRegularExpression.java
index b35bb12..611dea6 100644
--- a/src/main/java/com/android/tools/r8/retrace/RetraceRegularExpression.java
+++ b/src/main/java/com/android/tools/r8/retrace/RetraceRegularExpression.java
@@ -34,22 +34,19 @@
 
   private static final int NO_MATCH = -1;
 
-  private final RegularExpressionGroup[] syntheticGroups =
-      new RegularExpressionGroup[] {new SourceFileLineNumberGroup()};
-
-  private final RegularExpressionGroup[] groups =
-      new RegularExpressionGroup[] {
-        new TypeNameGroup(),
-        new BinaryNameGroup(),
-        new MethodNameGroup(),
-        new FieldNameGroup(),
-        new SourceFileGroup(),
-        new LineNumberGroup(),
-        new FieldOrReturnTypeGroup(),
-        new MethodArgumentsGroup()
-      };
+  private final SourceFileLineNumberGroup sourceFileLineNumberGroup =
+      new SourceFileLineNumberGroup();
+  private final TypeNameGroup typeNameGroup = new TypeNameGroup();
+  private final BinaryNameGroup binaryNameGroup = new BinaryNameGroup();
+  private final MethodNameGroup methodNameGroup = new MethodNameGroup();
+  private final FieldNameGroup fieldNameGroup = new FieldNameGroup();
+  private final SourceFileGroup sourceFileGroup = new SourceFileGroup();
+  private final LineNumberGroup lineNumberGroup = new LineNumberGroup();
+  private final FieldOrReturnTypeGroup fieldOrReturnTypeGroup = new FieldOrReturnTypeGroup();
+  private final MethodArgumentsGroup methodArgumentsGroup = new MethodArgumentsGroup();
 
   private static final String CAPTURE_GROUP_PREFIX = "captureGroup";
+  private static final int FIRST_CAPTURE_GROUP_INDEX = 0;
 
   RetraceRegularExpression(
       RetraceApi retracer,
@@ -64,8 +61,14 @@
 
   public RetraceCommandLineResult retrace() {
     List<RegularExpressionGroupHandler> handlers = new ArrayList<>();
-    String regularExpression = registerGroups(this.regularExpression, handlers);
-    Pattern compiledPattern = Pattern.compile(regularExpression);
+    StringBuilder refinedRegularExpressionBuilder = new StringBuilder();
+    registerGroups(
+        this.regularExpression,
+        refinedRegularExpressionBuilder,
+        handlers,
+        FIRST_CAPTURE_GROUP_INDEX);
+    String refinedRegularExpression = refinedRegularExpressionBuilder.toString();
+    Pattern compiledPattern = Pattern.compile(refinedRegularExpression);
     List<String> result = new ArrayList<>();
     for (String string : stackTrace) {
       Matcher matcher = compiledPattern.matcher(string);
@@ -130,80 +133,68 @@
     }
   }
 
-  private String registerGroups(
-      String regularExpression, List<RegularExpressionGroupHandler> handlers) {
-    int currentIndex = 0;
-    int captureGroupIndex = 0;
-    regularExpression = registerSyntheticGroups(regularExpression);
-    while (currentIndex < regularExpression.length()) {
-      RegularExpressionGroup firstGroup = null;
-      int firstIndexFromCurrent = regularExpression.length();
-      for (RegularExpressionGroup group : groups) {
-        int firstIndex =
-            firstIndexOfGroup(
-                currentIndex, firstIndexFromCurrent, regularExpression, group.shortName());
-        if (firstIndex > NO_MATCH) {
-          firstGroup = group;
-          firstIndexFromCurrent = firstIndex;
-        }
-      }
-      if (firstGroup != null) {
-        String captureGroupName = CAPTURE_GROUP_PREFIX + (captureGroupIndex++);
-        String patternToInsert = "(?<" + captureGroupName + ">" + firstGroup.subExpression() + ")";
-        regularExpression =
-            regularExpression.substring(0, firstIndexFromCurrent)
-                + patternToInsert
-                + regularExpression.substring(
-                    firstIndexFromCurrent + firstGroup.shortName().length());
-        handlers.add(firstGroup.createHandler(captureGroupName));
-        firstIndexFromCurrent += patternToInsert.length();
-      }
-      currentIndex = firstIndexFromCurrent;
-    }
-    return regularExpression;
-  }
-
-  private int firstIndexOfGroup(int startIndex, int endIndex, String expression, String shortName) {
-    int nextIndexOf = startIndex;
-    while (nextIndexOf != NO_MATCH) {
-      nextIndexOf = expression.indexOf(shortName, nextIndexOf);
-      if (nextIndexOf > NO_MATCH) {
-        if (nextIndexOf < endIndex && !isEscaped(expression, nextIndexOf)) {
-          return nextIndexOf;
-        }
-        nextIndexOf++;
-      }
-    }
-    return NO_MATCH;
-  }
-
-  private boolean isEscaped(String expression, int index) {
+  private int registerGroups(
+      String regularExpression,
+      StringBuilder refinedRegularExpression,
+      List<RegularExpressionGroupHandler> handlers,
+      int captureGroupIndex) {
+    int lastCommittedIndex = 0;
+    boolean seenPercentage = false;
     boolean escaped = false;
-    while (index > 0 && expression.charAt(--index) == '\\') {
-      escaped = !escaped;
+    for (int i = 0; i < regularExpression.length(); i++) {
+      if (seenPercentage) {
+        assert !escaped;
+        final RegularExpressionGroup group = getGroupFromVariable(regularExpression.charAt(i));
+        refinedRegularExpression.append(regularExpression, lastCommittedIndex, i - 1);
+        lastCommittedIndex = i + 1;
+        if (group.isSynthetic()) {
+          captureGroupIndex =
+              registerGroups(
+                  group.subExpression(), refinedRegularExpression, handlers, captureGroupIndex);
+        } else {
+          String captureGroupName = CAPTURE_GROUP_PREFIX + (captureGroupIndex++);
+          refinedRegularExpression
+              .append("(?<")
+              .append(captureGroupName)
+              .append(">")
+              .append(group.subExpression())
+              .append(")");
+          handlers.add(group.createHandler(captureGroupName));
+        }
+        seenPercentage = false;
+      } else {
+        seenPercentage = !escaped && regularExpression.charAt(i) == '%';
+        escaped = !escaped && regularExpression.charAt(i) == '\\';
+      }
     }
-    return escaped;
+    refinedRegularExpression.append(
+        regularExpression, lastCommittedIndex, regularExpression.length());
+    return captureGroupIndex;
   }
 
-  private String registerSyntheticGroups(String regularExpression) {
-    boolean modifiedExpression;
-    do {
-      modifiedExpression = false;
-      for (RegularExpressionGroup syntheticGroup : syntheticGroups) {
-        int firstIndex =
-            firstIndexOfGroup(
-                0, regularExpression.length(), regularExpression, syntheticGroup.shortName());
-        if (firstIndex > NO_MATCH) {
-          regularExpression =
-              regularExpression.substring(0, firstIndex)
-                  + syntheticGroup.subExpression()
-                  + regularExpression.substring(firstIndex + syntheticGroup.shortName().length());
-          // Loop as long as we can replace.
-          modifiedExpression = true;
-        }
-      }
-    } while (modifiedExpression);
-    return regularExpression;
+  private RegularExpressionGroup getGroupFromVariable(char variable) {
+    switch (variable) {
+      case 'c':
+        return typeNameGroup;
+      case 'C':
+        return binaryNameGroup;
+      case 'm':
+        return methodNameGroup;
+      case 'f':
+        return fieldNameGroup;
+      case 's':
+        return sourceFileGroup;
+      case 'l':
+        return lineNumberGroup;
+      case 'S':
+        return sourceFileLineNumberGroup;
+      case 't':
+        return fieldOrReturnTypeGroup;
+      case 'a':
+        return methodArgumentsGroup;
+      default:
+        throw new Unreachable("Unexpected variable: " + variable);
+    }
   }
 
   static class RetraceString {
@@ -427,8 +418,6 @@
 
   private abstract static class RegularExpressionGroup {
 
-    abstract String shortName();
-
     abstract String subExpression();
 
     abstract RegularExpressionGroupHandler createHandler(String captureGroup);
@@ -480,11 +469,6 @@
   private static class TypeNameGroup extends ClassNameGroup {
 
     @Override
-    String shortName() {
-      return "%c";
-    }
-
-    @Override
     String subExpression() {
       return "(" + javaIdentifierSegment + "\\.)*" + javaIdentifierSegment;
     }
@@ -503,11 +487,6 @@
   private static class BinaryNameGroup extends ClassNameGroup {
 
     @Override
-    String shortName() {
-      return "%C";
-    }
-
-    @Override
     String subExpression() {
       return "(?:" + javaIdentifierSegment + "\\/)*" + javaIdentifierSegment;
     }
@@ -526,11 +505,6 @@
   private static class MethodNameGroup extends RegularExpressionGroup {
 
     @Override
-    String shortName() {
-      return "%m";
-    }
-
-    @Override
     String subExpression() {
       return "(?:(" + javaIdentifierSegment + "|\\<init\\>|\\<clinit\\>))";
     }
@@ -595,11 +569,6 @@
   private static class FieldNameGroup extends RegularExpressionGroup {
 
     @Override
-    String shortName() {
-      return "%f";
-    }
-
-    @Override
     String subExpression() {
       return javaIdentifierSegment;
     }
@@ -649,11 +618,6 @@
   private static class SourceFileGroup extends RegularExpressionGroup {
 
     @Override
-    String shortName() {
-      return "%s";
-    }
-
-    @Override
     String subExpression() {
       return "(?:(\\w*[\\. ])?(\\w*)?)";
     }
@@ -696,11 +660,6 @@
   private class LineNumberGroup extends RegularExpressionGroup {
 
     @Override
-    String shortName() {
-      return "%l";
-    }
-
-    @Override
     String subExpression() {
       return "\\d*";
     }
@@ -771,11 +730,6 @@
   private static class SourceFileLineNumberGroup extends RegularExpressionGroup {
 
     @Override
-    String shortName() {
-      return "%S";
-    }
-
-    @Override
     String subExpression() {
       return "%s(?::%l)?";
     }
@@ -797,11 +751,6 @@
   private static class FieldOrReturnTypeGroup extends RegularExpressionGroup {
 
     @Override
-    String shortName() {
-      return "%t";
-    }
-
-    @Override
     String subExpression() {
       return JAVA_TYPE_REGULAR_EXPRESSION;
     }
@@ -843,11 +792,6 @@
   private class MethodArgumentsGroup extends RegularExpressionGroup {
 
     @Override
-    String shortName() {
-      return "%a";
-    }
-
-    @Override
     String subExpression() {
       return "((" + JAVA_TYPE_REGULAR_EXPRESSION + "\\,)*" + JAVA_TYPE_REGULAR_EXPRESSION + ")?";
     }
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 3197121..c716da8 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexClasspathClass;
 import com.android.tools.r8.graph.DexDefinition;
+import com.android.tools.r8.graph.DexDefinitionSupplier;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
@@ -40,6 +41,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.ResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.SubtypingInfo;
+import com.android.tools.r8.graph.SyntheticItems;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.ir.desugar.DesugaredLibraryAPIConverter;
@@ -195,6 +197,7 @@
   // TODO(zerny): Clean up the constructors so we have just one.
   AppInfoWithLiveness(
       DirectMappedDexApplication application,
+      SyntheticItems.CommittedItems syntheticItems,
       Set<DexType> deadProtoTypes,
       Set<DexType> missingTypes,
       Set<DexType> liveTypes,
@@ -235,7 +238,7 @@
       EnumValueInfoMapCollection enumValueInfoMaps,
       Set<DexType> constClassReferences,
       Map<DexType, Visibility> initClassReferences) {
-    super(application);
+    super(application, syntheticItems);
     this.deadProtoTypes = deadProtoTypes;
     this.missingTypes = missingTypes;
     this.liveTypes = liveTypes;
@@ -320,7 +323,9 @@
       EnumValueInfoMapCollection enumValueInfoMaps,
       Set<DexType> constClassReferences,
       Map<DexType, Visibility> initClassReferences) {
-    super(appInfoWithClassHierarchy);
+    super(
+        appInfoWithClassHierarchy.app(),
+        appInfoWithClassHierarchy.getSyntheticItems().commit(appInfoWithClassHierarchy.app()));
     this.deadProtoTypes = deadProtoTypes;
     this.missingTypes = missingTypes;
     this.liveTypes = liveTypes;
@@ -406,7 +411,6 @@
         previous.enumValueInfoMaps,
         previous.constClassReferences,
         previous.initClassReferences);
-    copyMetadataFromPrevious(previous);
   }
 
   private AppInfoWithLiveness(
@@ -416,6 +420,7 @@
       Collection<DexReference> additionalPinnedItems) {
     this(
         application,
+        previous.getSyntheticItems().commit(application),
         previous.deadProtoTypes,
         previous.missingTypes,
         previous.liveTypes,
@@ -458,7 +463,6 @@
         previous.enumValueInfoMaps,
         previous.constClassReferences,
         previous.initClassReferences);
-    copyMetadataFromPrevious(previous);
     assert keepInfo.verifyNoneArePinned(removedClasses, previous);
   }
 
@@ -503,7 +507,7 @@
       AppInfoWithLiveness previous,
       Map<DexField, Int2ReferenceMap<DexField>> switchMaps,
       EnumValueInfoMapCollection enumValueInfoMaps) {
-    super(previous);
+    super(previous.app(), previous.getSyntheticItems().commit(previous.app()));
     this.deadProtoTypes = previous.deadProtoTypes;
     this.missingTypes = previous.missingTypes;
     this.liveTypes = previous.liveTypes;
@@ -785,7 +789,9 @@
   private boolean isInstantiatedDirectly(DexProgramClass clazz) {
     assert checkIfObsolete();
     DexType type = clazz.type;
-    return type.isD8R8SynthesizedClassType()
+    return
+    // TODO(b/165224388): Synthetic classes should be represented in the allocation info.
+    getSyntheticItems().isSyntheticClass(clazz)
         || (!clazz.isInterface() && objectAllocationInfoCollection.isInstantiatedDirectly(clazz))
         // TODO(b/145344105): Model annotations in the object allocation info.
         || (clazz.isAnnotation() && liveTypes.contains(type));
@@ -808,11 +814,16 @@
     if (info != null && info.isRead()) {
       return true;
     }
-    return keepInfo.isPinned(field, this)
-        // Fields in the class that is synthesized by D8/R8 would be used soon.
-        || field.holder.isD8R8SynthesizedClassType()
-        // For library classes we don't know whether a field is read.
-        || isLibraryOrClasspathField(encodedField);
+    if (keepInfo.isPinned(field, this)) {
+      return true;
+    }
+    // Fields in the class that is synthesized by D8/R8 would be used soon.
+    // TODO(b/165229577): Do we need this special handling of synthetics?
+    if (getSyntheticItems().isSyntheticClass(field.holder)) {
+      return true;
+    }
+    // For library classes we don't know whether a field is read.
+    return isLibraryOrClasspathField(encodedField);
   }
 
   public boolean isFieldWritten(DexEncodedField encodedField) {
@@ -828,15 +839,12 @@
       // The field is written directly by the program itself.
       return true;
     }
-    if (field.holder.isD8R8SynthesizedClassType()) {
-      // Fields in the class that is synthesized by D8/R8 would be used soon.
+    // TODO(b/165229577): Do we need this special handling of synthetics?
+    if (getSyntheticItems().isSyntheticClass(field.holder)) {
       return true;
     }
-    if (isLibraryOrClasspathField(encodedField)) {
-      // For library classes we don't know whether a field is rewritten.
-      return true;
-    }
-    return false;
+    // For library classes we don't know whether a field is rewritten.
+    return isLibraryOrClasspathField(encodedField);
   }
 
   public boolean isFieldOnlyWrittenInMethod(DexEncodedField field, DexEncodedMethod method) {
@@ -998,8 +1006,11 @@
             .filter(AssertionUtils::assertNotNull)
             .collect(Collectors.toList()));
 
+    DexDefinitionSupplier definitionSupplier =
+        application.getDefinitionsSupplier(SyntheticItems.createInitialSyntheticItems());
     return new AppInfoWithLiveness(
         application,
+        getSyntheticItems().commit(application, lens),
         deadProtoTypes,
         missingTypes,
         lens.rewriteTypes(liveTypes),
@@ -1010,8 +1021,8 @@
         lens.rewriteMethods(methodsTargetedByInvokeDynamic),
         lens.rewriteMethods(virtualMethodsTargetedByInvokeDirect),
         lens.rewriteMethods(liveMethods),
-        fieldAccessInfoCollection.rewrittenWithLens(application, lens),
-        objectAllocationInfoCollection.rewrittenWithLens(application, lens),
+        fieldAccessInfoCollection.rewrittenWithLens(definitionSupplier, lens),
+        objectAllocationInfoCollection.rewrittenWithLens(definitionSupplier, lens),
         rewriteInvokesWithContexts(virtualInvokes, lens),
         rewriteInvokesWithContexts(interfaceInvokes, lens),
         rewriteInvokesWithContexts(superInvokes, lens),
diff --git a/src/main/java/com/android/tools/r8/shaking/ClassMergingEnqueuerExtension.java b/src/main/java/com/android/tools/r8/shaking/ClassMergingEnqueuerExtension.java
new file mode 100644
index 0000000..a6d7326
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/ClassMergingEnqueuerExtension.java
@@ -0,0 +1,48 @@
+// 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.shaking;
+
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.analysis.EnqueuerCheckCastAnalysis;
+import com.android.tools.r8.graph.analysis.EnqueuerInstanceOfAnalysis;
+import com.google.common.collect.Sets;
+import java.util.Set;
+
+public class ClassMergingEnqueuerExtension
+    implements EnqueuerInstanceOfAnalysis, EnqueuerCheckCastAnalysis {
+
+  private final Set<DexType> instanceOfTypes = Sets.newIdentityHashSet();
+  private final Set<DexType> checkCastTypes = Sets.newIdentityHashSet();
+  private final DexItemFactory factory;
+
+  public ClassMergingEnqueuerExtension(DexItemFactory factory) {
+    this.factory = factory;
+  }
+
+  @Override
+  public void traceCheckCast(DexType type, ProgramMethod context) {
+    checkCastTypes.add(type.toBaseType(factory));
+  }
+
+  @Override
+  public void traceInstanceOf(DexType type, ProgramMethod context) {
+    instanceOfTypes.add(type.toBaseType(factory));
+  }
+
+  public boolean isCheckCastType(DexProgramClass clazz) {
+    return checkCastTypes.contains(clazz.type);
+  }
+
+  public boolean isInstanceOfType(DexProgramClass clazz) {
+    return instanceOfTypes.contains(clazz.type);
+  }
+
+  public void attach(Enqueuer enqueuer) {
+    enqueuer.registerInstanceOfAnalysis(this).registerCheckCastAnalysis(this);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java b/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
index 9a488d1..34fb593 100644
--- a/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
+++ b/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
@@ -39,98 +39,98 @@
   }
 
   @Override
-  public boolean registerInitClass(DexType clazz) {
-    return enqueuer.traceInitClass(clazz, context);
+  public void registerInitClass(DexType clazz) {
+    enqueuer.traceInitClass(clazz, context);
   }
 
   @Override
-  public boolean registerInvokeVirtual(DexMethod invokedMethod) {
-    return enqueuer.traceInvokeVirtual(invokedMethod, context);
+  public void registerInvokeVirtual(DexMethod invokedMethod) {
+    enqueuer.traceInvokeVirtual(invokedMethod, context);
   }
 
   @Override
-  public boolean registerInvokeDirect(DexMethod invokedMethod) {
-    return enqueuer.traceInvokeDirect(invokedMethod, context);
+  public void registerInvokeDirect(DexMethod invokedMethod) {
+    enqueuer.traceInvokeDirect(invokedMethod, context);
   }
 
   @Override
-  public boolean registerInvokeStatic(DexMethod invokedMethod) {
-    return enqueuer.traceInvokeStatic(invokedMethod, context);
+  public void registerInvokeStatic(DexMethod invokedMethod) {
+    enqueuer.traceInvokeStatic(invokedMethod, context);
   }
 
   @Override
-  public boolean registerInvokeInterface(DexMethod invokedMethod) {
-    return enqueuer.traceInvokeInterface(invokedMethod, context);
+  public void registerInvokeInterface(DexMethod invokedMethod) {
+    enqueuer.traceInvokeInterface(invokedMethod, context);
   }
 
   @Override
-  public boolean registerInvokeSuper(DexMethod invokedMethod) {
-    return enqueuer.traceInvokeSuper(invokedMethod, context);
+  public void registerInvokeSuper(DexMethod invokedMethod) {
+    enqueuer.traceInvokeSuper(invokedMethod, context);
   }
 
   @Override
-  public boolean registerInstanceFieldRead(DexField field) {
-    return enqueuer.traceInstanceFieldRead(field, context);
+  public void registerInstanceFieldRead(DexField field) {
+    enqueuer.traceInstanceFieldRead(field, context);
   }
 
   @Override
-  public boolean registerInstanceFieldReadFromMethodHandle(DexField field) {
-    return enqueuer.traceInstanceFieldReadFromMethodHandle(field, context);
+  public void registerInstanceFieldReadFromMethodHandle(DexField field) {
+    enqueuer.traceInstanceFieldReadFromMethodHandle(field, context);
   }
 
   @Override
-  public boolean registerInstanceFieldWrite(DexField field) {
-    return enqueuer.traceInstanceFieldWrite(field, context);
+  public void registerInstanceFieldWrite(DexField field) {
+    enqueuer.traceInstanceFieldWrite(field, context);
   }
 
   @Override
-  public boolean registerInstanceFieldWriteFromMethodHandle(DexField field) {
-    return enqueuer.traceInstanceFieldWriteFromMethodHandle(field, context);
+  public void registerInstanceFieldWriteFromMethodHandle(DexField field) {
+    enqueuer.traceInstanceFieldWriteFromMethodHandle(field, context);
   }
 
   @Override
-  public boolean registerNewInstance(DexType type) {
-    return enqueuer.traceNewInstance(type, context);
+  public void registerNewInstance(DexType type) {
+    enqueuer.traceNewInstance(type, context);
   }
 
   @Override
-  public boolean registerStaticFieldRead(DexField field) {
-    return enqueuer.traceStaticFieldRead(field, context);
+  public void registerStaticFieldRead(DexField field) {
+    enqueuer.traceStaticFieldRead(field, context);
   }
 
   @Override
-  public boolean registerStaticFieldReadFromMethodHandle(DexField field) {
-    return enqueuer.traceStaticFieldReadFromMethodHandle(field, context);
+  public void registerStaticFieldReadFromMethodHandle(DexField field) {
+    enqueuer.traceStaticFieldReadFromMethodHandle(field, context);
   }
 
   @Override
-  public boolean registerStaticFieldWrite(DexField field) {
-    return enqueuer.traceStaticFieldWrite(field, context);
+  public void registerStaticFieldWrite(DexField field) {
+    enqueuer.traceStaticFieldWrite(field, context);
   }
 
   @Override
-  public boolean registerStaticFieldWriteFromMethodHandle(DexField field) {
-    return enqueuer.traceStaticFieldWriteFromMethodHandle(field, context);
+  public void registerStaticFieldWriteFromMethodHandle(DexField field) {
+    enqueuer.traceStaticFieldWriteFromMethodHandle(field, context);
   }
 
   @Override
-  public boolean registerConstClass(DexType type) {
-    return enqueuer.traceConstClass(type, context);
+  public void registerConstClass(DexType type) {
+    enqueuer.traceConstClass(type, context);
   }
 
   @Override
-  public boolean registerCheckCast(DexType type) {
-    return enqueuer.traceCheckCast(type, context);
+  public void registerCheckCast(DexType type) {
+    enqueuer.traceCheckCast(type, context);
   }
 
   @Override
-  public boolean registerTypeReference(DexType type) {
-    return enqueuer.traceTypeReference(type, context);
+  public void registerTypeReference(DexType type) {
+    enqueuer.traceTypeReference(type, context);
   }
 
   @Override
-  public boolean registerInstanceOf(DexType type) {
-    return enqueuer.traceInstanceOf(type, context);
+  public void registerInstanceOf(DexType type) {
+    enqueuer.traceInstanceOf(type, context);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index 6ed64fe..ac6bad1 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -918,12 +918,12 @@
     }
   }
 
-  boolean traceCheckCast(DexType type, ProgramMethod currentMethod) {
+  void traceCheckCast(DexType type, ProgramMethod currentMethod) {
     checkCastAnalyses.forEach(analysis -> analysis.traceCheckCast(type, currentMethod));
-    return traceConstClassOrCheckCast(type, currentMethod);
+    traceConstClassOrCheckCast(type, currentMethod);
   }
 
-  boolean traceConstClass(DexType type, ProgramMethod currentMethod) {
+  void traceConstClass(DexType type, ProgramMethod currentMethod) {
     // We conservatively group T.class and T[].class to ensure that we do not merge T with S if
     // potential locks on T[].class and S[].class exists.
     DexType baseType = type.toBaseType(appView.dexItemFactory());
@@ -933,12 +933,13 @@
         constClassReferences.add(baseType);
       }
     }
-    return traceConstClassOrCheckCast(type, currentMethod);
+    traceConstClassOrCheckCast(type, currentMethod);
   }
 
-  private boolean traceConstClassOrCheckCast(DexType type, ProgramMethod currentMethod) {
+  private void traceConstClassOrCheckCast(DexType type, ProgramMethod currentMethod) {
     if (!forceProguardCompatibility) {
-      return traceTypeReference(type, currentMethod);
+      traceTypeReference(type, currentMethod);
+      return;
     }
     DexType baseType = type.toBaseType(appView.dexItemFactory());
     if (baseType.isClassType()) {
@@ -948,12 +949,10 @@
         markClassAsInstantiatedWithCompatRule(
             baseClass, graphReporter.reportCompatInstantiated(baseClass, currentMethod));
       }
-      return true;
     }
-    return false;
   }
 
-  boolean traceInitClass(DexType type, ProgramMethod currentMethod) {
+  void traceInitClass(DexType type, ProgramMethod currentMethod) {
     assert type.isClassType();
 
     Visibility oldMinimumRequiredVisibility = initClassReferences.get(type);
@@ -961,7 +960,7 @@
       DexProgramClass clazz = getProgramClassOrNull(type);
       if (clazz == null) {
         assert false;
-        return false;
+        return;
       }
 
       initClassReferences.put(
@@ -969,11 +968,11 @@
 
       markTypeAsLive(type, classReferencedFromReporter(currentMethod));
       markDirectAndIndirectClassInitializersAsLive(clazz);
-      return true;
+      return;
     }
 
     if (oldMinimumRequiredVisibility.isPublic()) {
-      return false;
+      return;
     }
 
     Visibility minimumRequiredVisibilityForCurrentMethod =
@@ -984,21 +983,20 @@
 
     if (minimumRequiredVisibilityForCurrentMethod.isPublic()) {
       initClassReferences.put(type, minimumRequiredVisibilityForCurrentMethod);
-      return true;
+      return;
     }
 
     if (oldMinimumRequiredVisibility.isProtected()) {
-      return false;
+      return;
     }
 
     if (minimumRequiredVisibilityForCurrentMethod.isProtected()) {
       initClassReferences.put(type, minimumRequiredVisibilityForCurrentMethod);
-      return true;
+      return;
     }
 
     assert oldMinimumRequiredVisibility.isPackagePrivate();
     assert minimumRequiredVisibilityForCurrentMethod.isPackagePrivate();
-    return false;
   }
 
   private Visibility computeMinimumRequiredVisibilityForInitClassField(
@@ -1037,17 +1035,16 @@
     }
   }
 
-  boolean traceTypeReference(DexType type, ProgramMethod currentMethod) {
+  void traceTypeReference(DexType type, ProgramMethod currentMethod) {
     markTypeAsLive(type, classReferencedFromReporter(currentMethod));
-    return true;
   }
 
-  boolean traceInstanceOf(DexType type, ProgramMethod currentMethod) {
+  void traceInstanceOf(DexType type, ProgramMethod currentMethod) {
     instanceOfAnalyses.forEach(analysis -> analysis.traceInstanceOf(type, currentMethod));
-    return traceTypeReference(type, currentMethod);
+    traceTypeReference(type, currentMethod);
   }
 
-  boolean traceInvokeDirect(DexMethod invokedMethod, ProgramMethod context) {
+  void traceInvokeDirect(DexMethod invokedMethod, ProgramMethod context) {
     boolean skipTracing =
         registerDeferredActionForDeadProtoBuilder(
             invokedMethod.holder,
@@ -1055,10 +1052,10 @@
             () -> workList.enqueueTraceInvokeDirectAction(invokedMethod, context));
     if (skipTracing) {
       addDeadProtoTypeCandidate(invokedMethod.holder);
-      return false;
+      return;
     }
 
-    return traceInvokeDirect(invokedMethod, context, KeepReason.invokedFrom(context));
+    traceInvokeDirect(invokedMethod, context, KeepReason.invokedFrom(context));
   }
 
   /** Returns true if a deferred action was registered. */
@@ -1075,56 +1072,51 @@
     return false;
   }
 
-  boolean traceInvokeDirectFromLambda(DexMethod invokedMethod, ProgramMethod context) {
-    return traceInvokeDirect(
-        invokedMethod, context, KeepReason.invokedFromLambdaCreatedIn(context));
+  void traceInvokeDirectFromLambda(DexMethod invokedMethod, ProgramMethod context) {
+    traceInvokeDirect(invokedMethod, context, KeepReason.invokedFromLambdaCreatedIn(context));
   }
 
-  private boolean traceInvokeDirect(
+  private void traceInvokeDirect(
       DexMethod invokedMethod, ProgramMethod context, KeepReason reason) {
     if (!registerMethodWithTargetAndContext(directInvokes, invokedMethod, context)) {
-      return false;
+      return;
     }
     if (Log.ENABLED) {
       Log.verbose(getClass(), "Register invokeDirect `%s`.", invokedMethod);
     }
     handleInvokeOfDirectTarget(invokedMethod, reason);
     invokeAnalyses.forEach(analysis -> analysis.traceInvokeDirect(invokedMethod, context));
-    return true;
   }
 
-  boolean traceInvokeInterface(DexMethod invokedMethod, ProgramMethod context) {
-    return traceInvokeInterface(invokedMethod, context, KeepReason.invokedFrom(context));
+  void traceInvokeInterface(DexMethod invokedMethod, ProgramMethod context) {
+    traceInvokeInterface(invokedMethod, context, KeepReason.invokedFrom(context));
   }
 
-  boolean traceInvokeInterfaceFromLambda(DexMethod invokedMethod, ProgramMethod context) {
-    return traceInvokeInterface(
-        invokedMethod, context, KeepReason.invokedFromLambdaCreatedIn(context));
+  void traceInvokeInterfaceFromLambda(DexMethod invokedMethod, ProgramMethod context) {
+    traceInvokeInterface(invokedMethod, context, KeepReason.invokedFromLambdaCreatedIn(context));
   }
 
-  private boolean traceInvokeInterface(
+  private void traceInvokeInterface(
       DexMethod method, ProgramMethod context, KeepReason keepReason) {
     if (!registerMethodWithTargetAndContext(interfaceInvokes, method, context)) {
-      return false;
+      return;
     }
     if (Log.ENABLED) {
       Log.verbose(getClass(), "Register invokeInterface `%s`.", method);
     }
     markVirtualMethodAsReachable(method, true, context, keepReason);
     invokeAnalyses.forEach(analysis -> analysis.traceInvokeInterface(method, context));
-    return true;
   }
 
-  boolean traceInvokeStatic(DexMethod invokedMethod, ProgramMethod context) {
-    return traceInvokeStatic(invokedMethod, context, KeepReason.invokedFrom(context));
+  void traceInvokeStatic(DexMethod invokedMethod, ProgramMethod context) {
+    traceInvokeStatic(invokedMethod, context, KeepReason.invokedFrom(context));
   }
 
-  boolean traceInvokeStaticFromLambda(DexMethod invokedMethod, ProgramMethod context) {
-    return traceInvokeStatic(
-        invokedMethod, context, KeepReason.invokedFromLambdaCreatedIn(context));
+  void traceInvokeStaticFromLambda(DexMethod invokedMethod, ProgramMethod context) {
+    traceInvokeStatic(invokedMethod, context, KeepReason.invokedFromLambdaCreatedIn(context));
   }
 
-  private boolean traceInvokeStatic(
+  private void traceInvokeStatic(
       DexMethod invokedMethod, ProgramMethod context, KeepReason reason) {
     DexItemFactory dexItemFactory = appView.dexItemFactory();
     if (dexItemFactory.classMethods.isReflectiveClassLookup(invokedMethod)
@@ -1146,41 +1138,38 @@
       pendingReflectiveUses.add(context);
     }
     if (!registerMethodWithTargetAndContext(staticInvokes, invokedMethod, context)) {
-      return false;
+      return;
     }
     if (Log.ENABLED) {
       Log.verbose(getClass(), "Register invokeStatic `%s`.", invokedMethod);
     }
     handleInvokeOfStaticTarget(invokedMethod, reason);
     invokeAnalyses.forEach(analysis -> analysis.traceInvokeStatic(invokedMethod, context));
-    return true;
   }
 
-  boolean traceInvokeSuper(DexMethod invokedMethod, ProgramMethod context) {
+  void traceInvokeSuper(DexMethod invokedMethod, ProgramMethod context) {
     // We have to revisit super invokes based on the context they are found in. The same
     // method descriptor will hit different targets, depending on the context it is used in.
     DexMethod actualTarget = getInvokeSuperTarget(invokedMethod, context);
     if (!registerMethodWithTargetAndContext(superInvokes, invokedMethod, context)) {
-      return false;
+      return;
     }
     if (Log.ENABLED) {
       Log.verbose(getClass(), "Register invokeSuper `%s`.", actualTarget);
     }
     workList.enqueueMarkReachableSuperAction(invokedMethod, context);
     invokeAnalyses.forEach(analysis -> analysis.traceInvokeSuper(invokedMethod, context));
-    return true;
   }
 
-  boolean traceInvokeVirtual(DexMethod invokedMethod, ProgramMethod context) {
-    return traceInvokeVirtual(invokedMethod, context, KeepReason.invokedFrom(context));
+  void traceInvokeVirtual(DexMethod invokedMethod, ProgramMethod context) {
+    traceInvokeVirtual(invokedMethod, context, KeepReason.invokedFrom(context));
   }
 
-  boolean traceInvokeVirtualFromLambda(DexMethod invokedMethod, ProgramMethod context) {
-    return traceInvokeVirtual(
-        invokedMethod, context, KeepReason.invokedFromLambdaCreatedIn(context));
+  void traceInvokeVirtualFromLambda(DexMethod invokedMethod, ProgramMethod context) {
+    traceInvokeVirtual(invokedMethod, context, KeepReason.invokedFromLambdaCreatedIn(context));
   }
 
-  private boolean traceInvokeVirtual(
+  private void traceInvokeVirtual(
       DexMethod invokedMethod, ProgramMethod context, KeepReason reason) {
     if (invokedMethod == appView.dexItemFactory().classMethods.newInstance
         || invokedMethod == appView.dexItemFactory().constructorMethods.newInstance) {
@@ -1192,38 +1181,37 @@
       pendingReflectiveUses.add(context);
     }
     if (!registerMethodWithTargetAndContext(virtualInvokes, invokedMethod, context)) {
-      return false;
+      return;
     }
     if (Log.ENABLED) {
       Log.verbose(getClass(), "Register invokeVirtual `%s`.", invokedMethod);
     }
     markVirtualMethodAsReachable(invokedMethod, false, context, reason);
     invokeAnalyses.forEach(analysis -> analysis.traceInvokeVirtual(invokedMethod, context));
-    return true;
   }
 
-  boolean traceNewInstance(DexType type, ProgramMethod context) {
+  void traceNewInstance(DexType type, ProgramMethod context) {
     boolean skipTracing =
         registerDeferredActionForDeadProtoBuilder(
             type, context, () -> workList.enqueueTraceNewInstanceAction(type, context));
     if (skipTracing) {
       addDeadProtoTypeCandidate(type);
-      return false;
+      return;
     }
 
-    return traceNewInstance(
+    traceNewInstance(
         type,
         context,
         InstantiationReason.NEW_INSTANCE_INSTRUCTION,
         KeepReason.instantiatedIn(context));
   }
 
-  boolean traceNewInstanceFromLambda(DexType type, ProgramMethod context) {
-    return traceNewInstance(
+  void traceNewInstanceFromLambda(DexType type, ProgramMethod context) {
+    traceNewInstance(
         type, context, InstantiationReason.LAMBDA, KeepReason.invokedFromLambdaCreatedIn(context));
   }
 
-  private boolean traceNewInstance(
+  private void traceNewInstance(
       DexType type,
       ProgramMethod context,
       InstantiationReason instantiationReason,
@@ -1236,21 +1224,20 @@
         workList.enqueueMarkInstantiatedAction(clazz, context, instantiationReason, keepReason);
       }
     }
-    return true;
   }
 
-  boolean traceInstanceFieldRead(DexField field, ProgramMethod currentMethod) {
-    return traceInstanceFieldRead(field, currentMethod, false);
+  void traceInstanceFieldRead(DexField field, ProgramMethod currentMethod) {
+    traceInstanceFieldRead(field, currentMethod, false);
   }
 
-  boolean traceInstanceFieldReadFromMethodHandle(DexField field, ProgramMethod currentMethod) {
-    return traceInstanceFieldRead(field, currentMethod, true);
+  void traceInstanceFieldReadFromMethodHandle(DexField field, ProgramMethod currentMethod) {
+    traceInstanceFieldRead(field, currentMethod, true);
   }
 
-  private boolean traceInstanceFieldRead(
+  private void traceInstanceFieldRead(
       DexField fieldReference, ProgramMethod currentMethod, boolean fromMethodHandle) {
     if (!registerFieldRead(fieldReference, currentMethod)) {
-      return false;
+      return;
     }
 
     // Must mark the field as targeted even if it does not exist.
@@ -1258,14 +1245,14 @@
 
     FieldResolutionResult resolutionResult = resolveField(fieldReference);
     if (resolutionResult.isFailedOrUnknownResolution()) {
-      return false;
+      return;
     }
 
     ProgramField field =
         resolutionResult.asSuccessfulResolution().getResolutionPair().asProgramField();
     if (field == null) {
       // No need to trace into the non-program code.
-      return false;
+      return;
     }
 
     if (fromMethodHandle) {
@@ -1287,21 +1274,20 @@
     }
 
     workList.enqueueMarkReachableFieldAction(field, KeepReason.fieldReferencedIn(currentMethod));
-    return true;
   }
 
-  boolean traceInstanceFieldWrite(DexField field, ProgramMethod currentMethod) {
-    return traceInstanceFieldWrite(field, currentMethod, false);
+  void traceInstanceFieldWrite(DexField field, ProgramMethod currentMethod) {
+    traceInstanceFieldWrite(field, currentMethod, false);
   }
 
-  boolean traceInstanceFieldWriteFromMethodHandle(DexField field, ProgramMethod currentMethod) {
-    return traceInstanceFieldWrite(field, currentMethod, true);
+  void traceInstanceFieldWriteFromMethodHandle(DexField field, ProgramMethod currentMethod) {
+    traceInstanceFieldWrite(field, currentMethod, true);
   }
 
-  private boolean traceInstanceFieldWrite(
+  private void traceInstanceFieldWrite(
       DexField fieldReference, ProgramMethod currentMethod, boolean fromMethodHandle) {
     if (!registerFieldWrite(fieldReference, currentMethod)) {
-      return false;
+      return;
     }
 
     // Must mark the field as targeted even if it does not exist.
@@ -1309,14 +1295,14 @@
 
     FieldResolutionResult resolutionResult = resolveField(fieldReference);
     if (resolutionResult.isFailedOrUnknownResolution()) {
-      return false;
+      return;
     }
 
     ProgramField field =
         resolutionResult.asSuccessfulResolution().getResolutionPair().asProgramField();
     if (field == null) {
       // No need to trace into the non-program code.
-      return false;
+      return;
     }
 
     if (fromMethodHandle) {
@@ -1339,35 +1325,34 @@
 
     KeepReason reason = KeepReason.fieldReferencedIn(currentMethod);
     workList.enqueueMarkReachableFieldAction(field, reason);
-    return true;
   }
 
-  boolean traceStaticFieldRead(DexField field, ProgramMethod currentMethod) {
-    return traceStaticFieldRead(field, currentMethod, false);
+  void traceStaticFieldRead(DexField field, ProgramMethod currentMethod) {
+    traceStaticFieldRead(field, currentMethod, false);
   }
 
-  boolean traceStaticFieldReadFromMethodHandle(DexField field, ProgramMethod currentMethod) {
-    return traceStaticFieldRead(field, currentMethod, true);
+  void traceStaticFieldReadFromMethodHandle(DexField field, ProgramMethod currentMethod) {
+    traceStaticFieldRead(field, currentMethod, true);
   }
 
-  private boolean traceStaticFieldRead(
+  private void traceStaticFieldRead(
       DexField fieldReference, ProgramMethod currentMethod, boolean fromMethodHandle) {
     if (!registerFieldRead(fieldReference, currentMethod)) {
-      return false;
+      return;
     }
 
     FieldResolutionResult resolutionResult = resolveField(fieldReference);
     if (resolutionResult.isFailedOrUnknownResolution()) {
       // Must mark the field as targeted even if it does not exist.
       markFieldAsTargeted(fieldReference, currentMethod);
-      return false;
+      return;
     }
 
     ProgramField field =
         resolutionResult.asSuccessfulResolution().getResolutionPair().asProgramField();
     if (field == null) {
       // No need to trace into the non-program code.
-      return false;
+      return;
     }
 
     if (fromMethodHandle) {
@@ -1387,7 +1372,7 @@
               false);
       if (skipTracing) {
         addDeadProtoTypeCandidate(field.getHolder());
-        return false;
+        return;
       }
     }
 
@@ -1398,35 +1383,34 @@
     }
 
     markStaticFieldAsLive(field, KeepReason.fieldReferencedIn(currentMethod));
-    return true;
   }
 
-  boolean traceStaticFieldWrite(DexField field, ProgramMethod currentMethod) {
-    return traceStaticFieldWrite(field, currentMethod, false);
+  void traceStaticFieldWrite(DexField field, ProgramMethod currentMethod) {
+    traceStaticFieldWrite(field, currentMethod, false);
   }
 
-  boolean traceStaticFieldWriteFromMethodHandle(DexField field, ProgramMethod currentMethod) {
-    return traceStaticFieldWrite(field, currentMethod, true);
+  void traceStaticFieldWriteFromMethodHandle(DexField field, ProgramMethod currentMethod) {
+    traceStaticFieldWrite(field, currentMethod, true);
   }
 
-  private boolean traceStaticFieldWrite(
+  private void traceStaticFieldWrite(
       DexField fieldReference, ProgramMethod currentMethod, boolean fromMethodHandle) {
     if (!registerFieldWrite(fieldReference, currentMethod)) {
-      return false;
+      return;
     }
 
     FieldResolutionResult resolutionResult = resolveField(fieldReference);
     if (resolutionResult.isFailedOrUnknownResolution()) {
       // Must mark the field as targeted even if it does not exist.
       markFieldAsTargeted(fieldReference, currentMethod);
-      return false;
+      return;
     }
 
     ProgramField field =
         resolutionResult.asSuccessfulResolution().getResolutionPair().asProgramField();
     if (field == null) {
       // No need to trace into the non-program code.
-      return false;
+      return;
     }
 
     if (fromMethodHandle) {
@@ -1446,7 +1430,7 @@
               false);
       if (skipTracing) {
         addDeadProtoTypeCandidate(field.getHolder());
-        return false;
+        return;
       }
     }
 
@@ -1457,7 +1441,6 @@
     }
 
     markStaticFieldAsLive(field, KeepReason.fieldReferencedIn(currentMethod));
-    return true;
   }
 
   private Function<DexProgramClass, KeepReasonWitness> classReferencedFromReporter(
@@ -3032,6 +3015,7 @@
     AppInfoWithLiveness appInfoWithLiveness =
         new AppInfoWithLiveness(
             app,
+            appInfo.getSyntheticItems().commit(app),
             deadProtoTypes,
             mode.isFinalTreeShaking()
                 ? Sets.union(initialMissingTypes, missingTypes)
diff --git a/src/main/java/com/android/tools/r8/shaking/MainDexDirectReferenceTracer.java b/src/main/java/com/android/tools/r8/shaking/MainDexDirectReferenceTracer.java
index 0382893..973691f 100644
--- a/src/main/java/com/android/tools/r8/shaking/MainDexDirectReferenceTracer.java
+++ b/src/main/java/com/android/tools/r8/shaking/MainDexDirectReferenceTracer.java
@@ -102,83 +102,78 @@
     }
 
     @Override
-    public boolean registerInitClass(DexType clazz) {
+    public void registerInitClass(DexType clazz) {
       consumer.accept(clazz);
-      return true;
     }
 
     @Override
-    public boolean registerInvokeVirtual(DexMethod method) {
-      return registerInvoke(method);
+    public void registerInvokeVirtual(DexMethod method) {
+      registerInvoke(method);
     }
 
     @Override
-    public boolean registerInvokeDirect(DexMethod method) {
-      return registerInvoke(method);
+    public void registerInvokeDirect(DexMethod method) {
+      registerInvoke(method);
     }
 
     @Override
-    public boolean registerInvokeStatic(DexMethod method) {
-      return registerInvoke(method);
+    public void registerInvokeStatic(DexMethod method) {
+      registerInvoke(method);
     }
 
     @Override
-    public boolean registerInvokeInterface(DexMethod method) {
-      return registerInvoke(method);
+    public void registerInvokeInterface(DexMethod method) {
+      registerInvoke(method);
     }
 
     @Override
-    public boolean registerInvokeSuper(DexMethod method) {
-      return registerInvoke(method);
+    public void registerInvokeSuper(DexMethod method) {
+      registerInvoke(method);
     }
 
-    protected boolean registerInvoke(DexMethod method) {
+    protected void registerInvoke(DexMethod method) {
       consumer.accept(method.holder);
       traceMethodDirectDependencies(method, consumer);
-      return true;
     }
 
     @Override
-    public boolean registerInstanceFieldWrite(DexField field) {
-      return registerFieldAccess(field);
+    public void registerInstanceFieldWrite(DexField field) {
+      registerFieldAccess(field);
     }
 
     @Override
-    public boolean registerInstanceFieldRead(DexField field) {
-      return registerFieldAccess(field);
+    public void registerInstanceFieldRead(DexField field) {
+      registerFieldAccess(field);
     }
 
     @Override
-    public boolean registerStaticFieldRead(DexField field) {
-      return registerFieldAccess(field);
+    public void registerStaticFieldRead(DexField field) {
+      registerFieldAccess(field);
     }
 
     @Override
-    public boolean registerStaticFieldWrite(DexField field) {
-      return registerFieldAccess(field);
+    public void registerStaticFieldWrite(DexField field) {
+      registerFieldAccess(field);
     }
 
-    protected boolean registerFieldAccess(DexField field) {
+    protected void registerFieldAccess(DexField field) {
       consumer.accept(field.holder);
       consumer.accept(field.type);
-      return true;
     }
 
     @Override
-    public boolean registerNewInstance(DexType type) {
+    public void registerNewInstance(DexType type) {
       consumer.accept(type);
-      return true;
     }
 
     @Override
-    public boolean registerTypeReference(DexType type) {
+    public void registerTypeReference(DexType type) {
       consumer.accept(type);
-      return true;
     }
 
     @Override
-    public boolean registerInstanceOf(DexType type) {
-      return registerTypeReference(type);
+    public void registerInstanceOf(DexType type) {
+      registerTypeReference(type);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
index 58bbdae..1b51a73 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationParser.java
@@ -74,9 +74,8 @@
       "dontshrinkduringoptimization",
       "convert_proto_enum_to_string");
 
-  private static final List<String> IGNORED_CLASS_DESCRIPTOR_OPTIONS = ImmutableList.of(
-      "isclassnamestring",
-      "whyarenotsimple");
+  private static final List<String> IGNORED_CLASS_DESCRIPTOR_OPTIONS =
+      ImmutableList.of("isclassnamestring", "whyarenotsimple", "convertchecknotnull");
 
   private static final List<String> WARNED_SINGLE_ARG_OPTIONS = ImmutableList.of(
       // TODO(b/37137994): -outjars should be reported as errors, not just as warnings!
diff --git a/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
index ed87cff..0d803ea 100644
--- a/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
@@ -4,7 +4,8 @@
 
 package com.android.tools.r8.shaking;
 
-import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.FeatureSplit;
+import com.android.tools.r8.features.FeatureSplitConfiguration;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -34,6 +35,7 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -59,75 +61,12 @@
  */
 public class StaticClassMerger {
 
-  enum MergeGroup {
+  private enum MergeGroup {
     MAIN_DEX_ROOTS,
     MAIN_DEX_DEPENDENCIES,
     NOT_MAIN_DEX,
     DONT_MERGE;
 
-    private static final String GLOBAL = "<global>";
-    private static Key mainDexRootsGlobalKey = new Key(MergeGroup.MAIN_DEX_ROOTS, GLOBAL);
-    private static Key mainDexDependenciesGlobalKey =
-        new Key(MergeGroup.MAIN_DEX_DEPENDENCIES, GLOBAL);
-    private static Key notMainDexGlobalKey = new Key(MergeGroup.NOT_MAIN_DEX, GLOBAL);
-
-    private static class Key {
-      private final MergeGroup mergeGroup;
-      private final String packageOrGlobal;
-
-      public Key(MergeGroup mergeGroup, String packageOrGlobal) {
-        this.mergeGroup = mergeGroup;
-        this.packageOrGlobal = packageOrGlobal;
-      }
-
-      public MergeGroup getMergeGroup() {
-        return mergeGroup;
-      }
-
-      public String getPackageOrGlobal() {
-        return packageOrGlobal;
-      }
-
-      public boolean isGlobal() {
-        return packageOrGlobal.equals(GLOBAL);
-      }
-
-      @Override
-      public int hashCode() {
-        return mergeGroup.ordinal() * 13 + packageOrGlobal.hashCode();
-      }
-
-      @Override
-      public boolean equals(Object other) {
-        if (other == this) {
-          return true;
-        }
-        if (other == null || this.getClass() != other.getClass()) {
-          return false;
-        }
-        Key o = (Key) other;
-        return o.mergeGroup == mergeGroup && o.packageOrGlobal.equals(packageOrGlobal);
-      }
-    }
-
-    public Key globalKey() {
-      switch (this) {
-        case NOT_MAIN_DEX:
-          return notMainDexGlobalKey;
-        case MAIN_DEX_ROOTS:
-          return mainDexRootsGlobalKey;
-        case MAIN_DEX_DEPENDENCIES:
-          return mainDexDependenciesGlobalKey;
-        default:
-          throw new Unreachable("Unexpected MergeGroup value");
-      }
-    }
-
-    public Key key(String pkg) {
-      assert this != DONT_MERGE;
-      return new Key(this, pkg);
-    }
-
     @Override
     public String toString() {
       switch (this) {
@@ -144,6 +83,56 @@
     }
   }
 
+  private static class MergeKey {
+
+    private static final String GLOBAL = "<global>";
+
+    private final FeatureSplit featureSplit;
+    private final MergeGroup mergeGroup;
+    private final String packageOrGlobal;
+
+    public MergeKey(FeatureSplit featureSplit, MergeGroup mergeGroup, String packageOrGlobal) {
+      this.featureSplit = featureSplit;
+      this.mergeGroup = mergeGroup;
+      this.packageOrGlobal = packageOrGlobal;
+    }
+
+    public MergeGroup getMergeGroup() {
+      return mergeGroup;
+    }
+
+    public MergeKey toGlobal() {
+      return new MergeKey(featureSplit, mergeGroup, GLOBAL);
+    }
+
+    public String getPackageOrGlobal() {
+      return packageOrGlobal;
+    }
+
+    public boolean isGlobal() {
+      return packageOrGlobal.equals(GLOBAL);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(featureSplit, mergeGroup, packageOrGlobal);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (other == this) {
+        return true;
+      }
+      if (other == null || this.getClass() != other.getClass()) {
+        return false;
+      }
+      MergeKey o = (MergeKey) other;
+      return o.featureSplit == featureSplit
+          && o.mergeGroup == mergeGroup
+          && o.packageOrGlobal.equals(packageOrGlobal);
+    }
+  }
+
   // There are 52 characters in [a-zA-Z], so with a capacity just below 52 the minifier should be
   // able to find single-character names for all members, but around 30 appears to work better in
   // practice.
@@ -204,7 +193,7 @@
   private final Equivalence<DexField> fieldEquivalence;
   private final Equivalence<DexMethod> methodEquivalence;
 
-  private final Map<MergeGroup.Key, Representative> representatives = new HashMap<>();
+  private final Map<MergeKey, Representative> representatives = new HashMap<>();
 
   private final BiMap<DexField, DexField> fieldMapping = HashBiMap.create();
   private final BiMap<DexMethod, DexMethod> methodMapping = HashBiMap.create();
@@ -227,12 +216,7 @@
   }
 
   public NestedGraphLens run() {
-    for (DexProgramClass clazz : appView.appInfo().app().classesWithDeterministicOrder()) {
-      MergeGroup group = satisfiesMergeCriteria(clazz);
-      if (group != MergeGroup.DONT_MERGE) {
-        merge(clazz, group);
-      }
-    }
+    appView.appInfo().classesWithDeterministicOrder().forEach(this::merge);
     if (Log.ENABLED) {
       Log.info(
           getClass(),
@@ -259,15 +243,14 @@
     return null;
   }
 
-  private MergeGroup satisfiesMergeCriteria(DexProgramClass clazz) {
+  private FeatureSplitConfiguration getFeatureSplitConfiguration() {
+    return appView.options().featureSplitConfiguration;
+  }
+
+  private MergeGroup getMergeGroup(DexProgramClass clazz) {
     if (appView.appInfo().neverMerge.contains(clazz.type)) {
       return MergeGroup.DONT_MERGE;
     }
-    if (appView.options().featureSplitConfiguration != null &&
-        appView.options().featureSplitConfiguration.isInFeature(clazz)) {
-      // TODO(b/141452765): Allow class merging between classes in features.
-      return MergeGroup.DONT_MERGE;
-    }
     if (clazz.staticFields().size() + clazz.getMethodCollection().size() == 0) {
       return MergeGroup.DONT_MERGE;
     }
@@ -291,7 +274,7 @@
     if (Streams.stream(clazz.methods())
         .anyMatch(
             method ->
-                method.accessFlags.isNative()
+                method.isNative()
                     || appView.appInfo().isPinned(method.method)
                     // TODO(christofferqa): Remove the invariant that the graph lens should not
                     // modify any methods from the sets alwaysInline and noSideEffects.
@@ -319,33 +302,44 @@
     return MergeGroup.NOT_MAIN_DEX;
   }
 
+  private MergeKey getMergeKey(DexProgramClass clazz, MergeGroup mergeGroup) {
+    FeatureSplitConfiguration featureSplitConfiguration = getFeatureSplitConfiguration();
+    FeatureSplit featureSplit =
+        featureSplitConfiguration != null
+            ? featureSplitConfiguration.getFeatureSplit(clazz)
+            : FeatureSplit.BASE;
+    return new MergeKey(
+        featureSplit,
+        mergeGroup,
+        mayMergeAcrossPackageBoundaries(clazz)
+            ? MergeKey.GLOBAL
+            : clazz.type.getPackageDescriptor());
+  }
+
   private boolean isValidRepresentative(DexProgramClass clazz) {
     // Disallow interfaces from being representatives, since interface methods require desugaring.
     return !clazz.isInterface();
   }
 
-  private boolean merge(DexProgramClass clazz, MergeGroup group) {
-    assert satisfiesMergeCriteria(clazz) == group;
-    assert group != MergeGroup.DONT_MERGE;
-
-    return merge(
-        clazz,
-        mayMergeAcrossPackageBoundaries(clazz)
-            ? group.globalKey()
-            : group.key(clazz.type.getPackageDescriptor()));
+  private void merge(DexProgramClass clazz) {
+    MergeGroup mergeGroup = getMergeGroup(clazz);
+    if (mergeGroup != MergeGroup.DONT_MERGE) {
+      merge(clazz, mergeGroup);
+    }
   }
 
-  private boolean merge(DexProgramClass clazz, MergeGroup.Key key) {
+  private void merge(DexProgramClass clazz, MergeGroup mergeGroup) {
+    MergeKey key = getMergeKey(clazz, mergeGroup);
     Representative representative = representatives.get(key);
     if (representative != null) {
       if (representative.hasSynchronizedMethods && clazz.hasStaticSynchronizedMethods()) {
         // We are not allowed to merge synchronized classes with synchronized methods.
-        return false;
+        return;
       }
       if (appView.appInfo().constClassReferences.contains(clazz.type)) {
         // Since the type is const-class referenced (and the static merger does not create a lens
         // to map the merged type) the class will likely remain and there is no gain from merging.
-        return false;
+        return;
       }
       // Check if current candidate is a better choice depending on visibility. For package private
       // or protected, the key is parameterized by the package name already, so we just have to
@@ -359,24 +353,23 @@
         if (!newRepresentative.isFull()) {
           setRepresentative(key, newRepresentative);
           moveMembersFromSourceToTarget(representative.clazz, clazz);
-          return true;
+          return;
         }
       } else {
         representative.include(clazz);
         if (!representative.isFull()) {
           moveMembersFromSourceToTarget(clazz, representative.clazz);
-          return true;
+          return;
         }
       }
     }
     if (isValidRepresentative(clazz)) {
       setRepresentative(key, getOrCreateRepresentative(key, clazz));
     }
-    return false;
   }
 
-  private Representative getOrCreateRepresentative(MergeGroup.Key key, DexProgramClass clazz) {
-    Representative globalRepresentative = representatives.get(key.getMergeGroup().globalKey());
+  private Representative getOrCreateRepresentative(MergeKey key, DexProgramClass clazz) {
+    Representative globalRepresentative = representatives.get(key.toGlobal());
     if (globalRepresentative != null && globalRepresentative.clazz == clazz) {
       return globalRepresentative;
     }
@@ -387,7 +380,7 @@
     return new Representative(clazz);
   }
 
-  private void setRepresentative(MergeGroup.Key key, Representative representative) {
+  private void setRepresentative(MergeKey key, Representative representative) {
     assert isValidRepresentative(representative.clazz);
     if (Log.ENABLED) {
       if (key.isGlobal()) {
@@ -408,22 +401,10 @@
     representatives.put(key, representative);
   }
 
-  private void clearRepresentative(MergeGroup.Key key) {
-    if (Log.ENABLED) {
-      if (key.isGlobal()) {
-        Log.info(getClass(), "Removing the global representative");
-      } else {
-        Log.info(
-            getClass(), "Removing the representative for package %s", key.getPackageOrGlobal());
-      }
-    }
-    representatives.remove(key);
-  }
-
   private boolean mayMergeAcrossPackageBoundaries(DexProgramClass clazz) {
     // Check that the class is public. Otherwise, accesses to `clazz` from within its current
     // package may become illegal.
-    if (!clazz.accessFlags.isPublic()) {
+    if (!clazz.isPublic()) {
       return false;
     }
     // Check that all of the members are private or public.
@@ -471,6 +452,8 @@
     assert targetClass.accessFlags.isAtLeastAsVisibleAs(sourceClass.accessFlags);
     assert sourceClass.instanceFields().isEmpty();
     assert targetClass.instanceFields().isEmpty();
+    assert getFeatureSplitConfiguration() == null
+        || getFeatureSplitConfiguration().inSameFeatureOrBothInBase(sourceClass, targetClass);
 
     numberOfMergedClasses++;
 
diff --git a/src/main/java/com/android/tools/r8/shaking/TreePruner.java b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
index be956c3..5231fe6 100644
--- a/src/main/java/com/android/tools/r8/shaking/TreePruner.java
+++ b/src/main/java/com/android/tools/r8/shaking/TreePruner.java
@@ -36,7 +36,7 @@
 
   private final AppView<AppInfoWithLiveness> appView;
   private final TreePrunerConfiguration configuration;
-  private final UsagePrinter usagePrinter;
+  private final UnusedItemsPrinter unusedItemsPrinter;
   private final Set<DexType> prunedTypes = Sets.newIdentityHashSet();
   private final Set<DexMethod> methodsToKeepForConfigurationDebugging = Sets.newIdentityHashSet();
 
@@ -48,13 +48,13 @@
     InternalOptions options = appView.options();
     this.appView = appView;
     this.configuration = configuration;
-    this.usagePrinter =
+    this.unusedItemsPrinter =
         options.hasUsageInformationConsumer()
-            ? new UsagePrinter(
+            ? new UnusedItemsPrinter(
                 s ->
                     ExceptionUtils.withConsumeResourceHandler(
                         options.reporter, options.usageInformationConsumer, s))
-            : UsagePrinter.DONT_PRINT;
+            : UnusedItemsPrinter.DONT_PRINT;
   }
 
   public DirectMappedDexApplication run() {
@@ -114,10 +114,11 @@
         //  clazz.type.isD8R8SynthesizedType, but such test is currently expensive since it is
         //  based on strings, so we check only against the enum unboxing utility class.
         if (clazz.type != appView.dexItemFactory().enumUnboxingUtilityType) {
-          usagePrinter.printUnusedClass(clazz);
+          unusedItemsPrinter.registerUnusedClass(clazz);
         }
       }
     }
+    unusedItemsPrinter.finished();
     return newClasses;
   }
 
@@ -160,7 +161,7 @@
   }
 
   private void pruneMembersAndAttributes(DexProgramClass clazz) {
-    usagePrinter.visiting(clazz);
+    unusedItemsPrinter.visiting(clazz);
     DexEncodedMethod[] reachableDirectMethods = reachableMethods(clazz.directMethods(), clazz);
     if (reachableDirectMethods != null) {
       clazz.setDirectMethods(reachableDirectMethods);
@@ -181,7 +182,7 @@
     clazz.removeInnerClasses(this::isAttributeReferencingPrunedType);
     clazz.removeEnclosingMethodAttribute(this::isAttributeReferencingPrunedItem);
     rewriteNestAttributes(clazz);
-    usagePrinter.visited();
+    unusedItemsPrinter.visited();
     assert verifyNoDeadFields(clazz);
   }
 
@@ -318,7 +319,7 @@
         if (Log.ENABLED) {
           Log.debug(getClass(), "Removing method %s.", method.method);
         }
-        usagePrinter.printUnusedMethod(method);
+        unusedItemsPrinter.registerUnusedMethod(method);
       }
     }
     return reachableMethods.isEmpty()
@@ -338,7 +339,7 @@
     if (Log.ENABLED) {
       Log.debug(getClass(), "Removing field %s.", fields.get(firstUnreachable));
     }
-    usagePrinter.printUnusedField(fields.get(firstUnreachable));
+    unusedItemsPrinter.registerUnusedField(fields.get(firstUnreachable));
     ArrayList<DexEncodedField> reachableOrReferencedFields = new ArrayList<>(fields.size());
     for (int i = 0; i < firstUnreachable; i++) {
       reachableOrReferencedFields.add(fields.get(i));
@@ -351,7 +352,7 @@
         if (Log.ENABLED) {
           Log.debug(getClass(), "Removing field %s.", field.field);
         }
-        usagePrinter.printUnusedField(field);
+        unusedItemsPrinter.registerUnusedField(field);
       }
     }
     return reachableOrReferencedFields.isEmpty()
diff --git a/src/main/java/com/android/tools/r8/shaking/UnusedItemsPrinter.java b/src/main/java/com/android/tools/r8/shaking/UnusedItemsPrinter.java
new file mode 100644
index 0000000..f1e2fa3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/shaking/UnusedItemsPrinter.java
@@ -0,0 +1,172 @@
+// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.shaking;
+
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+class UnusedItemsPrinter {
+
+  private static class Members {
+    final List<DexEncodedField> fields = new ArrayList<>();
+    final List<DexEncodedMethod> methods = new ArrayList<>();
+
+    boolean hasMembers() {
+      return !fields.isEmpty() || !methods.isEmpty();
+    }
+
+    void sort() {
+      fields.sort((a, b) -> a.toReference().slowCompareTo(b.toReference()));
+      methods.sort((a, b) -> a.toReference().slowCompareTo(b.toReference()));
+    }
+  }
+
+  private static final String INDENT = "    ";
+
+  static final UnusedItemsPrinter DONT_PRINT = new NopPrinter();
+
+  private final Consumer<String> consumer;
+
+  private DexType currentType = null;
+  private Members currentMembers = new Members();
+
+  private List<Pair<DexType, Members>> classes = new ArrayList<>();
+
+  UnusedItemsPrinter(Consumer<String> consumer) {
+    this.consumer = consumer;
+  }
+
+  void registerUnusedClass(DexProgramClass clazz) {
+    assert currentType == null;
+    classes.add(new Pair<>(clazz.type, null));
+  }
+
+  // Visiting methods and fields of the given clazz.
+  void visiting(DexProgramClass clazz) {
+    assert currentType == null;
+    currentType = clazz.type;
+  }
+
+  // Visited methods and fields of the top at the clazz stack.
+  void visited() {
+    if (currentMembers.hasMembers()) {
+      classes.add(new Pair<>(currentType, currentMembers));
+      currentMembers = new Members();
+    }
+    currentType = null;
+  }
+
+  void registerUnusedMethod(DexEncodedMethod method) {
+    currentMembers.methods.add(method);
+  }
+
+  void registerUnusedField(DexEncodedField field) {
+    currentMembers.fields.add(field);
+  }
+
+  public void finished() {
+    classes.sort((a, b) -> a.getFirst().slowCompareTo(b.getFirst()));
+    for (Pair<DexType, Members> entry : classes) {
+      DexType type = entry.getFirst();
+      Members members = entry.getSecond();
+      consumer.accept(type.toSourceString());
+      if (members == null) {
+        consumer.accept(StringUtils.LINE_SEPARATOR);
+      } else {
+        consumer.accept(":" + StringUtils.LINE_SEPARATOR);
+        members.sort();
+        members.fields.forEach(this::printUnusedField);
+        members.methods.forEach(this::printUnusedMethod);
+      }
+    }
+    classes = null;
+  }
+
+  private void append(String string) {
+    consumer.accept(string);
+  }
+
+  private void newline() {
+    append(StringUtils.LINE_SEPARATOR);
+  }
+
+  private void printUnusedMethod(DexEncodedMethod method) {
+    append(INDENT);
+    String accessFlags = method.accessFlags.toString();
+    if (!accessFlags.isEmpty()) {
+      append(accessFlags);
+      append(" ");
+    }
+    append(method.method.proto.returnType.toSourceString());
+    append(" ");
+    append(method.method.name.toSourceString());
+    append("(");
+    for (int i = 0; i < method.method.proto.parameters.values.length; i++) {
+      if (i != 0) {
+        append(",");
+      }
+      append(method.method.proto.parameters.values[i].toSourceString());
+    }
+    append(")");
+    newline();
+  }
+
+  private void printUnusedField(DexEncodedField field) {
+    append(INDENT);
+    String accessFlags = field.accessFlags.toString();
+    if (!accessFlags.isEmpty()) {
+      append(accessFlags);
+      append(" ");
+    }
+    append(field.field.type.toSourceString());
+    append(" ");
+    append(field.field.name.toSourceString());
+    newline();
+  }
+
+  // Empty implementation to silently ignore printing dead code.
+  private static class NopPrinter extends UnusedItemsPrinter {
+
+    public NopPrinter() {
+      super(null);
+    }
+
+    @Override
+    void registerUnusedClass(DexProgramClass clazz) {
+      // Intentionally left empty.
+    }
+
+    @Override
+    void visiting(DexProgramClass clazz) {
+      // Intentionally left empty.
+    }
+
+    @Override
+    void visited() {
+      // Intentionally left empty.
+    }
+
+    @Override
+    void registerUnusedMethod(DexEncodedMethod method) {
+      // Intentionally left empty.
+    }
+
+    @Override
+    void registerUnusedField(DexEncodedField field) {
+      // Intentionally left empty.
+    }
+
+    @Override
+    public void finished() {
+      // Intentionally left empty.
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/UsagePrinter.java b/src/main/java/com/android/tools/r8/shaking/UsagePrinter.java
deleted file mode 100644
index 4c04b52..0000000
--- a/src/main/java/com/android/tools/r8/shaking/UsagePrinter.java
+++ /dev/null
@@ -1,124 +0,0 @@
-// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.shaking;
-
-import com.android.tools.r8.graph.DexEncodedField;
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.utils.StringUtils;
-import java.util.function.Consumer;
-
-class UsagePrinter {
-  private static final String INDENT = "    ";
-
-  static final UsagePrinter DONT_PRINT = new NoOpUsagePrinter();
-
-  private final Consumer<String> consumer;
-  private DexProgramClass enclosingClazz = null;
-  private boolean clazzPrefixPrinted = false;
-
-  UsagePrinter(Consumer<String> consumer) {
-    this.consumer = consumer;
-  }
-
-  void append(String string) {
-    consumer.accept(string);
-  }
-
-  void printUnusedClass(DexProgramClass clazz) {
-    append(clazz.toSourceString());
-    append(StringUtils.LINE_SEPARATOR);
-  }
-
-  // Visiting methods and fields of the given clazz.
-  void visiting(DexProgramClass clazz) {
-    assert enclosingClazz == null;
-    enclosingClazz = clazz;
-  }
-
-  // Visited methods and fields of the top at the clazz stack.
-  void visited() {
-    enclosingClazz = null;
-    clazzPrefixPrinted = false;
-  }
-
-  private void printClazzPrefixIfNecessary() {
-    assert enclosingClazz != null;
-    if (!clazzPrefixPrinted) {
-      append(enclosingClazz.toSourceString());
-      append(":");
-      append(StringUtils.LINE_SEPARATOR);
-      clazzPrefixPrinted = true;
-    }
-  }
-
-  void printUnusedMethod(DexEncodedMethod method) {
-    printClazzPrefixIfNecessary();
-    append(INDENT);
-    String accessFlags = method.accessFlags.toString();
-    if (!accessFlags.isEmpty()) {
-      append(accessFlags);
-      append(" ");
-    }
-    append(method.method.proto.returnType.toSourceString());
-    append(" ");
-    append(method.method.name.toSourceString());
-    append("(");
-    for (int i = 0; i < method.method.proto.parameters.values.length; i++) {
-      if (i != 0) {
-        append(",");
-      }
-      append(method.method.proto.parameters.values[i].toSourceString());
-    }
-    append(")");
-    append(StringUtils.LINE_SEPARATOR);
-  }
-
-  void printUnusedField(DexEncodedField field) {
-    printClazzPrefixIfNecessary();
-    append(INDENT);
-    String accessFlags = field.accessFlags.toString();
-    if (!accessFlags.isEmpty()) {
-      append(accessFlags);
-      append(" ");
-    }
-    append(field.field.type.toSourceString());
-    append(" ");
-    append(field.field.name.toSourceString());
-    append(StringUtils.LINE_SEPARATOR);
-  }
-
-  // Empty implementation to silently ignore printing dead code.
-  private static class NoOpUsagePrinter extends UsagePrinter {
-
-    public NoOpUsagePrinter() {
-      super(null);
-    }
-
-    @Override
-    void printUnusedClass(DexProgramClass clazz) {
-      // Intentionally left empty.
-    }
-
-    @Override
-    void visiting(DexProgramClass clazz) {
-      // Intentionally left empty.
-    }
-
-    @Override
-    void visited() {
-      // Intentionally left empty.
-    }
-
-    @Override
-    void printUnusedMethod(DexEncodedMethod method) {
-      // Intentionally left empty.
-    }
-
-    @Override
-    void printUnusedField(DexEncodedField field) {
-      // Intentionally left empty.
-    }
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
index 1de617d..9e709fd 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -8,6 +8,7 @@
 import static com.android.tools.r8.ir.code.Invoke.Type.STATIC;
 
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.features.FeatureSplitConfiguration;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.Code;
@@ -357,9 +358,10 @@
         .map(DexEncodedMember::toReference)
         .noneMatch(appInfo::isPinned);
 
-    if (appView.options().featureSplitConfiguration != null
-        && appView.options().featureSplitConfiguration.isInFeature(sourceClass)) {
-      // TODO(b/141452765): Allow class merging between classes in features.
+    FeatureSplitConfiguration featureSplitConfiguration =
+        appView.options().featureSplitConfiguration;
+    if (featureSplitConfiguration != null
+        && !featureSplitConfiguration.inSameFeatureOrBothInBase(sourceClass, targetClass)) {
       return false;
     }
     if (appView.appServices().allServiceTypes().contains(sourceClass.type)
@@ -1790,7 +1792,7 @@
       this.context = context;
     }
 
-    private boolean checkFieldReference(DexField field) {
+    private void checkFieldReference(DexField field) {
       if (!foundIllegalAccess) {
         DexType baseType =
             appView.graphLens().lookupType(field.holder.toBaseType(appView.dexItemFactory()));
@@ -1804,10 +1806,9 @@
           }
         }
       }
-      return true;
     }
 
-    private boolean checkMethodReference(DexMethod method, OptionalBool isInterface) {
+    private void checkMethodReference(DexMethod method, OptionalBool isInterface) {
       if (!foundIllegalAccess) {
         DexType baseType =
             appView.graphLens().lookupType(method.holder.toBaseType(appView.dexItemFactory()));
@@ -1827,10 +1828,9 @@
           }
         }
       }
-      return true;
     }
 
-    private boolean checkTypeReference(DexType type) {
+    private void checkTypeReference(DexType type) {
       if (!foundIllegalAccess) {
         DexType baseType =
             appView.graphLens().lookupType(type.toBaseType(appView.dexItemFactory()));
@@ -1841,87 +1841,86 @@
           }
         }
       }
-      return true;
     }
 
     @Override
-    public boolean registerInitClass(DexType clazz) {
-      return checkTypeReference(clazz);
+    public void registerInitClass(DexType clazz) {
+      checkTypeReference(clazz);
     }
 
     @Override
-    public boolean registerInvokeVirtual(DexMethod method) {
+    public void registerInvokeVirtual(DexMethod method) {
       assert context != null;
       GraphLensLookupResult lookup =
           appView.graphLens().lookupMethod(method, context.getReference(), Type.VIRTUAL);
-      return checkMethodReference(lookup.getMethod(), OptionalBool.FALSE);
+      checkMethodReference(lookup.getMethod(), OptionalBool.FALSE);
     }
 
     @Override
-    public boolean registerInvokeDirect(DexMethod method) {
+    public void registerInvokeDirect(DexMethod method) {
       assert context != null;
       GraphLensLookupResult lookup =
           appView.graphLens().lookupMethod(method, context.getReference(), Type.DIRECT);
-      return checkMethodReference(lookup.getMethod(), OptionalBool.UNKNOWN);
+      checkMethodReference(lookup.getMethod(), OptionalBool.UNKNOWN);
     }
 
     @Override
-    public boolean registerInvokeStatic(DexMethod method) {
+    public void registerInvokeStatic(DexMethod method) {
       assert context != null;
       GraphLensLookupResult lookup =
           appView.graphLens().lookupMethod(method, context.getReference(), Type.STATIC);
-      return checkMethodReference(lookup.getMethod(), OptionalBool.UNKNOWN);
+      checkMethodReference(lookup.getMethod(), OptionalBool.UNKNOWN);
     }
 
     @Override
-    public boolean registerInvokeInterface(DexMethod method) {
+    public void registerInvokeInterface(DexMethod method) {
       assert context != null;
       GraphLensLookupResult lookup =
           appView.graphLens().lookupMethod(method, context.getReference(), Type.INTERFACE);
-      return checkMethodReference(lookup.getMethod(), OptionalBool.TRUE);
+      checkMethodReference(lookup.getMethod(), OptionalBool.TRUE);
     }
 
     @Override
-    public boolean registerInvokeSuper(DexMethod method) {
+    public void registerInvokeSuper(DexMethod method) {
       assert context != null;
       GraphLensLookupResult lookup =
           appView.graphLens().lookupMethod(method, context.getReference(), Type.SUPER);
-      return checkMethodReference(lookup.getMethod(), OptionalBool.UNKNOWN);
+      checkMethodReference(lookup.getMethod(), OptionalBool.UNKNOWN);
     }
 
     @Override
-    public boolean registerInstanceFieldWrite(DexField field) {
-      return checkFieldReference(appView.graphLens().lookupField(field));
+    public void registerInstanceFieldWrite(DexField field) {
+      checkFieldReference(appView.graphLens().lookupField(field));
     }
 
     @Override
-    public boolean registerInstanceFieldRead(DexField field) {
-      return checkFieldReference(appView.graphLens().lookupField(field));
+    public void registerInstanceFieldRead(DexField field) {
+      checkFieldReference(appView.graphLens().lookupField(field));
     }
 
     @Override
-    public boolean registerNewInstance(DexType type) {
-      return checkTypeReference(type);
+    public void registerNewInstance(DexType type) {
+      checkTypeReference(type);
     }
 
     @Override
-    public boolean registerStaticFieldRead(DexField field) {
-      return checkFieldReference(appView.graphLens().lookupField(field));
+    public void registerStaticFieldRead(DexField field) {
+      checkFieldReference(appView.graphLens().lookupField(field));
     }
 
     @Override
-    public boolean registerStaticFieldWrite(DexField field) {
-      return checkFieldReference(appView.graphLens().lookupField(field));
+    public void registerStaticFieldWrite(DexField field) {
+      checkFieldReference(appView.graphLens().lookupField(field));
     }
 
     @Override
-    public boolean registerTypeReference(DexType type) {
-      return checkTypeReference(type);
+    public void registerTypeReference(DexType type) {
+      checkTypeReference(type);
     }
 
     @Override
-    public boolean registerInstanceOf(DexType type) {
-      return checkTypeReference(type);
+    public void registerInstanceOf(DexType type) {
+      checkTypeReference(type);
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
index 2d78c0d..8c070a5 100644
--- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -182,8 +182,14 @@
    * @return className "org/foo/bar/Baz.Nested"
    */
   public static String descriptorToKotlinClassifier(String descriptor) {
-    return getBinaryNameFromDescriptor(descriptor)
-        .replace(INNER_CLASS_SEPARATOR, JAVA_PACKAGE_SEPARATOR);
+    final String classifier =
+        getBinaryNameFromDescriptor(descriptor)
+            .replace(INNER_CLASS_SEPARATOR, JAVA_PACKAGE_SEPARATOR);
+    if (descriptor.startsWith("Lj$/")) {
+      assert classifier.startsWith("j./");
+      return "j$/" + classifier.substring(3);
+    }
+    return classifier;
   }
 
   /**
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 dfd253d..69410dd 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -226,8 +226,7 @@
   public boolean enableInlining =
       !Version.isDevelopmentVersion()
           || System.getProperty("com.android.tools.r8.disableinlining") == null;
-  // TODO(b/160854837): re-enable enum unboxing.
-  public boolean enableEnumUnboxing = false;
+  public boolean enableEnumUnboxing = true;
   // TODO(b/141451716): Evaluate the effect of allowing inlining in the inlinee.
   public boolean applyInliningToInlinee =
       System.getProperty("com.android.tools.r8.applyInliningToInlinee") != null;
@@ -326,7 +325,12 @@
             .setVersion(Version.LABEL)
             .setCompilationMode(debug ? CompilationMode.DEBUG : CompilationMode.RELEASE)
             .setHasChecksums(encodeChecksums);
-    if (!isGeneratingClassFiles()) {
+    // Compiling with D8 and L8 is always with a min API level and desugaring to that level. If
+    // desugaring is explicitly turned off for D8 the input is expected to already have been
+    // desugared to the specified min API level. For R8 desugaring is optional.
+    if (tool == Tool.D8
+        || tool == Tool.L8
+        || (tool == Tool.R8 && desugarState != DesugarState.OFF)) {
       marker.setMinApi(minApiLevel);
     }
     if (desugaredLibraryConfiguration.getIdentifier() != null) {
@@ -396,6 +400,10 @@
     return programConsumer instanceof ClassFileConsumer;
   }
 
+  public boolean isDesugaring() {
+    return !isGeneratingClassFiles() || cfToCfDesugar;
+  }
+
   public DexIndexedConsumer getDexIndexedConsumer() {
     return (DexIndexedConsumer) programConsumer;
   }
@@ -505,6 +513,14 @@
     return isMinifying();
   }
 
+  /**
+   * 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 enableHorizontalClassMerging || enableVerticalClassMerging;
+  }
+
   @Override
   public boolean isAccessModificationEnabled() {
     return getProguardConfiguration() != null
@@ -1061,7 +1077,19 @@
     // Repackaging all classes into the single user-given (or top-level) package.
     REPACKAGE,
     // Repackaging all packages into the single user-given (or top-level) package.
-    FLATTEN
+    FLATTEN;
+
+    public boolean isNone() {
+      return this == NONE;
+    }
+
+    public boolean isFlattenPackageHierarchy() {
+      return this == FLATTEN;
+    }
+
+    public boolean isRepackageClasses() {
+      return this == REPACKAGE;
+    }
   }
 
   public static class OutlineOptions {
@@ -1185,6 +1213,7 @@
     public boolean alwaysUsePessimisticRegisterAllocation = false;
     public boolean enableCheckCastAndInstanceOfRemoval = true;
     public boolean enableDeadSwitchCaseElimination = true;
+    public boolean enableExperimentalRepackaging = false;
     public boolean enableInvokeSuperToInvokeVirtualRewriting = true;
     public boolean enableSwitchToIfRewriting = true;
     public boolean enableEnumUnboxingDebugLogs = false;
@@ -1208,6 +1237,10 @@
     public boolean enumUnboxingRewriteJavaCGeneratedMethod = false;
     public boolean assertConsistentRenamingOfSignature = false;
 
+    // Flag to allow processing of resources in D8. A data resource consumer still needs to be
+    // specified.
+    public boolean enableD8ResourcesPassThrough = false;
+
     // TODO(b/144781417): This is disabled by default as some test apps appear to have such classes.
     public boolean allowNonAbstractClassesWithAbstractMethods = true;
 
@@ -1266,7 +1299,6 @@
   }
 
   private boolean hasMinApi(AndroidApiLevel level) {
-    assert isGeneratingDex();
     return minApiLevel >= level.getLevel();
   }
 
@@ -1317,23 +1349,23 @@
   }
 
   public boolean canUseDefaultAndStaticInterfaceMethods() {
-    return isGeneratingClassFiles() || hasMinApi(AndroidApiLevel.N);
+    return !isDesugaring() || hasMinApi(AndroidApiLevel.N);
   }
 
   public boolean canUseNestBasedAccess() {
-    return isGeneratingClassFiles();
+    return !isDesugaring();
   }
 
   public boolean canLeaveStaticInterfaceMethodInvokes() {
-    return isGeneratingClassFiles() || hasMinApi(AndroidApiLevel.L);
+    return !isDesugaring() || hasMinApi(AndroidApiLevel.L);
   }
 
   public boolean canUseTwrCloseResourceMethod() {
-    return isGeneratingClassFiles() || hasMinApi(AndroidApiLevel.K);
+    return !isDesugaring() || hasMinApi(AndroidApiLevel.K);
   }
 
   public boolean canUsePrivateInterfaceMethods() {
-    return isGeneratingClassFiles() || hasMinApi(AndroidApiLevel.N);
+    return !isDesugaring() || hasMinApi(AndroidApiLevel.N);
   }
 
   public boolean canUseDexPcAsDebugInformation() {
@@ -1348,7 +1380,7 @@
     }
     return desugarState == DesugarState.ON
         && interfaceMethodDesugaring == OffOrAuto.Auto
-        && (!canUseDefaultAndStaticInterfaceMethods() || cfToCfDesugar);
+        && !canUseDefaultAndStaticInterfaceMethods();
   }
 
   public boolean isStringSwitchConversionEnabled() {
@@ -1365,11 +1397,11 @@
   }
 
   public boolean canUseSuppressedExceptions() {
-    return isGeneratingClassFiles() || hasMinApi(AndroidApiLevel.K);
+    return (isGeneratingClassFiles() && !cfToCfDesugar) || hasMinApi(AndroidApiLevel.K);
   }
 
   public boolean canUseAssertionErrorTwoArgumentConstructor() {
-    return isGeneratingClassFiles() || hasMinApi(AndroidApiLevel.K);
+    return (isGeneratingClassFiles() && !cfToCfDesugar) || hasMinApi(AndroidApiLevel.K);
   }
 
   // The Apache Harmony-based AssertionError constructor which takes an Object on API 15 and older
@@ -1379,7 +1411,7 @@
   //
   // https://android.googlesource.com/platform/libcore/+/refs/heads/ics-mr1/luni/src/main/java/java/lang/AssertionError.java#56
   public boolean canInitCauseAfterAssertionErrorObjectConstructor() {
-    return isGeneratingClassFiles() || hasMinApi(AndroidApiLevel.J);
+    return (isGeneratingClassFiles() && !cfToCfDesugar) || hasMinApi(AndroidApiLevel.J);
   }
 
   // Dalvik x86-atom backend had a bug that made it crash on filled-new-array instructions for
diff --git a/src/test/java/com/android/tools/r8/D8TestRunResult.java b/src/test/java/com/android/tools/r8/D8TestRunResult.java
index cd4b663..4e13e54 100644
--- a/src/test/java/com/android/tools/r8/D8TestRunResult.java
+++ b/src/test/java/com/android/tools/r8/D8TestRunResult.java
@@ -7,7 +7,7 @@
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.utils.AndroidApp;
 
-public class D8TestRunResult extends TestRunResult<D8TestRunResult> {
+public class D8TestRunResult extends SingleTestRunResult<D8TestRunResult> {
 
   public D8TestRunResult(AndroidApp app, TestRuntime runtime, ProcessResult result) {
     super(app, runtime, result);
diff --git a/src/test/java/com/android/tools/r8/DXTestRunResult.java b/src/test/java/com/android/tools/r8/DXTestRunResult.java
index bcec2f3..b395e90 100644
--- a/src/test/java/com/android/tools/r8/DXTestRunResult.java
+++ b/src/test/java/com/android/tools/r8/DXTestRunResult.java
@@ -7,7 +7,7 @@
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.utils.AndroidApp;
 
-public class DXTestRunResult extends TestRunResult<DXTestRunResult> {
+public class DXTestRunResult extends SingleTestRunResult<DXTestRunResult> {
 
   public DXTestRunResult(AndroidApp app, TestRuntime runtime, ProcessResult result) {
     super(app, runtime, result);
diff --git a/src/test/java/com/android/tools/r8/Dex2OatTestRunResult.java b/src/test/java/com/android/tools/r8/Dex2OatTestRunResult.java
index f364232..cb33702 100644
--- a/src/test/java/com/android/tools/r8/Dex2OatTestRunResult.java
+++ b/src/test/java/com/android/tools/r8/Dex2OatTestRunResult.java
@@ -11,7 +11,7 @@
 import org.hamcrest.CoreMatchers;
 import org.hamcrest.Matcher;
 
-public class Dex2OatTestRunResult extends TestRunResult<Dex2OatTestRunResult> {
+public class Dex2OatTestRunResult extends SingleTestRunResult<Dex2OatTestRunResult> {
 
   public Dex2OatTestRunResult(AndroidApp app, TestRuntime runtime, ProcessResult result) {
     super(app, runtime, result);
diff --git a/src/test/java/com/android/tools/r8/ExternalR8TestRunResult.java b/src/test/java/com/android/tools/r8/ExternalR8TestRunResult.java
index 28e2835..469e5b5 100644
--- a/src/test/java/com/android/tools/r8/ExternalR8TestRunResult.java
+++ b/src/test/java/com/android/tools/r8/ExternalR8TestRunResult.java
@@ -13,7 +13,7 @@
 import java.nio.file.Path;
 import java.util.concurrent.ExecutionException;
 
-public class ExternalR8TestRunResult extends TestRunResult<ExternalR8TestRunResult> {
+public class ExternalR8TestRunResult extends SingleTestRunResult<ExternalR8TestRunResult> {
 
   private final Path outputJar;
   private final String proguardMap;
diff --git a/src/test/java/com/android/tools/r8/GenerateMainDexListRunResult.java b/src/test/java/com/android/tools/r8/GenerateMainDexListRunResult.java
index 166feeb..8f2e9b1 100644
--- a/src/test/java/com/android/tools/r8/GenerateMainDexListRunResult.java
+++ b/src/test/java/com/android/tools/r8/GenerateMainDexListRunResult.java
@@ -6,7 +6,8 @@
 
 import java.util.List;
 
-public class GenerateMainDexListRunResult extends TestRunResult<GenerateMainDexListRunResult> {
+public class GenerateMainDexListRunResult
+    extends SingleTestRunResult<GenerateMainDexListRunResult> {
 
   List<String> mainDexList;
 
diff --git a/src/test/java/com/android/tools/r8/IntermediateCfD8TestBuilder.java b/src/test/java/com/android/tools/r8/IntermediateCfD8TestBuilder.java
new file mode 100644
index 0000000..fcefd23
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/IntermediateCfD8TestBuilder.java
@@ -0,0 +1,101 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import com.android.tools.r8.TestBase.Backend;
+import com.android.tools.r8.debug.DebugTestConfig;
+import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.concurrent.ExecutionException;
+
+public class IntermediateCfD8TestBuilder
+    extends TestBuilder<D8TestRunResult, IntermediateCfD8TestBuilder> {
+
+  public static IntermediateCfD8TestBuilder create(TestState state, AndroidApiLevel apiLevel) {
+    assert state != null;
+    assert apiLevel != null;
+    return new IntermediateCfD8TestBuilder(state, apiLevel);
+  }
+
+  private final D8TestBuilder cf2cf;
+  private final D8TestBuilder cf2dex;
+
+  private IntermediateCfD8TestBuilder(TestState state, AndroidApiLevel apiLevel) {
+    super(state);
+    cf2cf = D8TestBuilder.create(state, Backend.CF).setMinApi(apiLevel);
+    cf2dex =
+        D8TestBuilder.create(state, Backend.DEX).setMinApi(apiLevel).setDisableDesugaring(true);
+  }
+
+  @Override
+  IntermediateCfD8TestBuilder self() {
+    return this;
+  }
+
+  @Override
+  public D8TestRunResult run(TestRuntime runtime, String mainClass, String... args)
+      throws CompilationFailedException, ExecutionException, IOException {
+    return cf2dex.addProgramFiles(cf2cf.compile().writeToZip()).run(runtime, mainClass, args);
+  }
+
+  @Override
+  public DebugTestConfig debugConfig() {
+    throw new Unimplemented("Unsupported debug config as of now...");
+  }
+
+  @Override
+  public IntermediateCfD8TestBuilder addProgramFiles(Collection<Path> files) {
+    cf2cf.addProgramFiles(files);
+    return self();
+  }
+
+  @Override
+  public IntermediateCfD8TestBuilder addProgramClassFileData(Collection<byte[]> classes) {
+    cf2cf.addProgramClassFileData(classes);
+    return self();
+  }
+
+  @Override
+  public IntermediateCfD8TestBuilder addProgramDexFileData(Collection<byte[]> data) {
+    cf2cf.addProgramDexFileData(data);
+    return self();
+  }
+
+  @Override
+  public IntermediateCfD8TestBuilder addLibraryFiles(Collection<Path> files) {
+    cf2cf.addLibraryFiles(files);
+    cf2dex.addLibraryFiles(files);
+    return self();
+  }
+
+  @Override
+  public IntermediateCfD8TestBuilder addLibraryClasses(Collection<Class<?>> classes) {
+    cf2cf.addLibraryClasses(classes);
+    cf2dex.addLibraryClasses(classes);
+    return self();
+  }
+
+  @Override
+  public IntermediateCfD8TestBuilder addClasspathClasses(Collection<Class<?>> classes) {
+    cf2cf.addClasspathClasses(classes);
+    cf2dex.addClasspathClasses(classes);
+    return self();
+  }
+
+  @Override
+  public IntermediateCfD8TestBuilder addClasspathFiles(Collection<Path> files) {
+    cf2cf.addClasspathFiles(files);
+    cf2dex.addClasspathFiles(files);
+    return self();
+  }
+
+  @Override
+  public IntermediateCfD8TestBuilder addRunClasspathFiles(Collection<Path> files) {
+    cf2dex.addRunClasspathFiles(files);
+    return self();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/JvmTestRunResult.java b/src/test/java/com/android/tools/r8/JvmTestRunResult.java
index a94fd38..fdd2580 100644
--- a/src/test/java/com/android/tools/r8/JvmTestRunResult.java
+++ b/src/test/java/com/android/tools/r8/JvmTestRunResult.java
@@ -7,7 +7,7 @@
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.utils.AndroidApp;
 
-public class JvmTestRunResult extends TestRunResult<JvmTestRunResult> {
+public class JvmTestRunResult extends SingleTestRunResult<JvmTestRunResult> {
 
   public JvmTestRunResult(AndroidApp app, TestRuntime runtime, ProcessResult result) {
     super(app, runtime, result);
diff --git a/src/test/java/com/android/tools/r8/MarkerMatcher.java b/src/test/java/com/android/tools/r8/MarkerMatcher.java
index 13065b7..ea6ea4a 100644
--- a/src/test/java/com/android/tools/r8/MarkerMatcher.java
+++ b/src/test/java/com/android/tools/r8/MarkerMatcher.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.dex.Marker;
 import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.google.common.collect.ImmutableList;
 import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
@@ -19,6 +20,10 @@
 
 public abstract class MarkerMatcher extends TypeSafeMatcher<Marker> {
 
+  public static void assertMarkersMatch(Iterable<Marker> markers, Matcher<Marker> matcher) {
+    assertMarkersMatch(markers, ImmutableList.of(matcher));
+  }
+
   public static void assertMarkersMatch(
       Iterable<Marker> markers, Collection<Matcher<Marker>> matchers) {
     // Match is unordered, but we make no attempts to find the maximum match.
@@ -103,6 +108,20 @@
     };
   }
 
+  public static Matcher<Marker> markerIsDesugared() {
+    return new MarkerMatcher() {
+      @Override
+      protected boolean eval(Marker marker) {
+        return marker.isDesugared();
+      }
+
+      @Override
+      protected void explain(Description description) {
+        description.appendText("desugared ");
+      }
+    };
+  }
+
   public static Matcher<Marker> markerMinApi(AndroidApiLevel level) {
     return new MarkerMatcher() {
       @Override
diff --git a/src/test/java/com/android/tools/r8/MarkersTest.java b/src/test/java/com/android/tools/r8/MarkersTest.java
index f2e539b..650cf39 100644
--- a/src/test/java/com/android/tools/r8/MarkersTest.java
+++ b/src/test/java/com/android/tools/r8/MarkersTest.java
@@ -7,10 +7,15 @@
 import static com.android.tools.r8.MarkerMatcher.markerCompilationMode;
 import static com.android.tools.r8.MarkerMatcher.markerDesugaredLibraryIdentifier;
 import static com.android.tools.r8.MarkerMatcher.markerHasChecksums;
+import static com.android.tools.r8.MarkerMatcher.markerHasDesugaredLibraryIdentifier;
+import static com.android.tools.r8.MarkerMatcher.markerHasMinApi;
+import static com.android.tools.r8.MarkerMatcher.markerIsDesugared;
 import static com.android.tools.r8.MarkerMatcher.markerMinApi;
 import static com.android.tools.r8.MarkerMatcher.markerR8Mode;
 import static com.android.tools.r8.MarkerMatcher.markerTool;
 import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.not;
+import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.dex.Marker;
 import com.android.tools.r8.dex.Marker.Tool;
@@ -95,4 +100,157 @@
       assertMarkersMatch(markers, ImmutableList.of(l8Matcher, r8Matcher));
     }
   }
+
+  @Test
+  public void testD8MarkerInDex() throws Throwable {
+    AndroidApiLevel apiLevel = AndroidApiLevel.L;
+    Path output = temp.newFolder().toPath().resolve("output.zip");
+    D8Command.Builder builder =
+        D8Command.builder()
+            .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
+            .addProgramFiles(ToolHelper.getClassFileForTestClass(TestClass.class))
+            .setMode(compilationMode)
+            .setMinApiLevel(apiLevel.getLevel())
+            .setOutput(output, OutputMode.DexIndexed);
+    D8.run(builder.build());
+    Collection<Marker> markers = ExtractMarker.extractMarkerFromDexFile(output);
+    Matcher<Marker> matcher =
+        allOf(
+            markerTool(Tool.D8),
+            markerCompilationMode(compilationMode),
+            markerIsDesugared(),
+            markerMinApi(apiLevel),
+            not(markerHasDesugaredLibraryIdentifier()));
+    assertMarkersMatch(markers, matcher);
+  }
+
+  @Test
+  public void testD8MarkerInCf() throws Throwable {
+    // Shrinking of desugared library is not affecting this test.
+    assumeTrue(shrinkDesugaredLibrary);
+
+    AndroidApiLevel apiLevel = AndroidApiLevel.L;
+    Path output = temp.newFolder().toPath().resolve("output.zip");
+    D8Command.Builder builder =
+        D8Command.builder()
+            .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
+            .addProgramFiles(ToolHelper.getClassFileForTestClass(TestClass.class))
+            .setMode(compilationMode)
+            .setMinApiLevel(apiLevel.getLevel())
+            .setOutput(output, OutputMode.ClassFile);
+    D8.run(builder.build());
+    Collection<Marker> markers = ExtractMarker.extractMarkerFromDexFile(output);
+    Matcher<Marker> matcher =
+        allOf(
+            markerTool(Tool.D8),
+            markerCompilationMode(compilationMode),
+            markerIsDesugared(),
+            markerMinApi(apiLevel),
+            not(markerHasDesugaredLibraryIdentifier()));
+    assertMarkersMatch(markers, matcher);
+  }
+
+  @Test
+  public void testR8MarkerInDex() throws Throwable {
+    // Shrinking of desugared library is not affecting this test.
+    assumeTrue(shrinkDesugaredLibrary);
+
+    AndroidApiLevel apiLevel = AndroidApiLevel.L;
+    Path output = temp.newFolder().toPath().resolve("output.zip");
+    R8Command.Builder builder =
+        R8Command.builder()
+            .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
+            .addProgramFiles(ToolHelper.getClassFileForTestClass(TestClass.class))
+            .addProguardConfiguration(ImmutableList.of("-keep class * { *; }"), Origin.unknown())
+            .setMode(compilationMode)
+            .setMinApiLevel(apiLevel.getLevel())
+            .setOutput(output, OutputMode.DexIndexed);
+    R8.run(builder.build());
+    Collection<Marker> markers = ExtractMarker.extractMarkerFromDexFile(output);
+    Matcher<Marker> matcher =
+        allOf(
+            markerTool(Tool.R8),
+            markerCompilationMode(compilationMode),
+            markerIsDesugared(),
+            markerMinApi(apiLevel),
+            not(markerHasDesugaredLibraryIdentifier()));
+    assertMarkersMatch(markers, matcher);
+  }
+
+  @Test
+  public void testR8MarkerInCf() throws Throwable {
+    // Shrinking of desugared library is not affecting this test.
+    assumeTrue(shrinkDesugaredLibrary);
+
+    Path output = temp.newFolder().toPath().resolve("output.zip");
+    R8Command.Builder builder =
+        R8Command.builder()
+            .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
+            .addProgramFiles(ToolHelper.getClassFileForTestClass(TestClass.class))
+            .addProguardConfiguration(ImmutableList.of("-keep class * { *; }"), Origin.unknown())
+            .setMode(compilationMode)
+            .setOutput(output, OutputMode.ClassFile);
+    R8.run(builder.build());
+    Collection<Marker> markers = ExtractMarker.extractMarkerFromDexFile(output);
+    Matcher<Marker> matcher =
+        allOf(
+            markerTool(Tool.R8),
+            markerCompilationMode(compilationMode),
+            not(markerIsDesugared()),
+            not(markerHasMinApi()),
+            not(markerHasDesugaredLibraryIdentifier()));
+    assertMarkersMatch(markers, matcher);
+  }
+
+  @Test
+  public void testR8MarkerInCfAfterD8CfDesugar() throws Throwable {
+    // Shrinking of desugared library is not affecting this test.
+    assumeTrue(shrinkDesugaredLibrary);
+
+    AndroidApiLevel apiLevel = AndroidApiLevel.L;
+    Path d8DesugaredOutput = temp.newFolder().toPath().resolve("output.zip");
+    D8.run(
+        D8Command.builder()
+            .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
+            .addProgramFiles(ToolHelper.getClassFileForTestClass(TestClass.class))
+            .setMode(compilationMode)
+            .setMinApiLevel(apiLevel.getLevel())
+            .setOutput(d8DesugaredOutput, OutputMode.ClassFile)
+            .build());
+    assertMarkersMatch(
+        ExtractMarker.extractMarkerFromDexFile(d8DesugaredOutput),
+        allOf(
+            markerTool(Tool.D8),
+            markerCompilationMode(compilationMode),
+            markerIsDesugared(),
+            markerMinApi(apiLevel),
+            not(markerHasDesugaredLibraryIdentifier())));
+
+    // Running R8 on desugared input will clear that information and leave no markers with
+    // that information.
+    Path output = temp.newFolder().toPath().resolve("output.zip");
+    R8.run(
+        R8Command.builder()
+            .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
+            .addProgramFiles(ToolHelper.getClassFileForTestClass(TestClass.class))
+            .addProguardConfiguration(ImmutableList.of("-keep class * { *; }"), Origin.unknown())
+            .setMode(compilationMode)
+            .setOutput(output, OutputMode.ClassFile)
+            .build());
+    assertMarkersMatch(
+        ExtractMarker.extractMarkerFromDexFile(output),
+        allOf(
+            markerTool(Tool.R8),
+            markerCompilationMode(compilationMode),
+            not(markerIsDesugared()),
+            not(markerHasMinApi()),
+            not(markerHasDesugaredLibraryIdentifier())));
+  }
+
+  public static class TestClass {
+
+    public static void main(String[] args) {
+      System.out.println("Hello, world!");
+    }
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/ProguardTestRunResult.java b/src/test/java/com/android/tools/r8/ProguardTestRunResult.java
index 5cfce33..3c2bbfc 100644
--- a/src/test/java/com/android/tools/r8/ProguardTestRunResult.java
+++ b/src/test/java/com/android/tools/r8/ProguardTestRunResult.java
@@ -14,7 +14,7 @@
 import java.io.IOException;
 import java.util.concurrent.ExecutionException;
 
-public class ProguardTestRunResult extends TestRunResult<ProguardTestRunResult> {
+public class ProguardTestRunResult extends SingleTestRunResult<ProguardTestRunResult> {
 
   private final String proguardMap;
 
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index 4fc59c6..935de02 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -3,11 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import static com.android.tools.r8.dexsplitter.SplitterTestBase.simpleSplitProvider;
 import static org.hamcrest.CoreMatchers.containsString;
 
 import com.android.tools.r8.R8Command.Builder;
 import com.android.tools.r8.TestBase.Backend;
 import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase.KeepRuleConsumer;
+import com.android.tools.r8.dexsplitter.SplitterTestBase.RunInterface;
+import com.android.tools.r8.dexsplitter.SplitterTestBase.SplitRunner;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.experimental.graphinfo.GraphConsumer;
 import com.android.tools.r8.origin.Origin;
@@ -64,6 +67,7 @@
   private List<String> keepRules = new ArrayList<>();
   private List<Path> mainDexRulesFiles = new ArrayList<>();
   private List<String> applyMappingMaps = new ArrayList<>();
+  private final List<Path> features = new ArrayList<>();
 
   @Override
   R8TestCompileResult internalCompile(
@@ -137,7 +141,8 @@
             box.syntheticProguardRules,
             proguardMapBuilder.toString(),
             graphConsumer,
-            builder.getMinApiLevel());
+            builder.getMinApiLevel(),
+            features);
     switch (allowedDiagnosticMessages) {
       case ALL:
         compileResult.assertDiagnosticThatMatches(new IsAnything<>());
@@ -569,8 +574,22 @@
     return self();
   }
 
+  public T addFeatureSplitRuntime() {
+    addProgramClasses(SplitRunner.class, RunInterface.class);
+    addKeepClassAndMembersRules(SplitRunner.class, RunInterface.class);
+    return self();
+  }
+
   public T addFeatureSplit(Function<FeatureSplit.Builder, FeatureSplit> featureSplitBuilder) {
     builder.addFeatureSplit(featureSplitBuilder);
     return self();
   }
+
+  public T addFeatureSplit(Class<?>... classes) throws IOException {
+    Path path = getState().getNewTempFile("feature.zip");
+    builder.addFeatureSplit(
+        builder -> simpleSplitProvider(builder, path, getState().getTempFolder(), classes));
+    features.add(path);
+    return self();
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/R8TestCompileResult.java b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
index 5dac954..3fa1091 100644
--- a/src/test/java/com/android/tools/r8/R8TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/R8TestCompileResult.java
@@ -3,12 +3,19 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
 import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.dexsplitter.SplitterTestBase.SplitRunner;
 import com.android.tools.r8.shaking.CollectingGraphConsumer;
 import com.android.tools.r8.shaking.ProguardConfiguration;
 import com.android.tools.r8.shaking.ProguardConfigurationRule;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.FileUtils;
+import com.android.tools.r8.utils.ThrowingConsumer;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.graphinspector.GraphInspector;
 import java.io.IOException;
@@ -23,6 +30,7 @@
   private final String proguardMap;
   private final CollectingGraphConsumer graphConsumer;
   private final int minApiLevel;
+  private final List<Path> features;
 
   R8TestCompileResult(
       TestState state,
@@ -32,13 +40,15 @@
       List<ProguardConfigurationRule> syntheticProguardRules,
       String proguardMap,
       CollectingGraphConsumer graphConsumer,
-      int minApiLevel) {
+      int minApiLevel,
+      List<Path> features) {
     super(state, app, outputMode);
     this.proguardConfiguration = proguardConfiguration;
     this.syntheticProguardRules = syntheticProguardRules;
     this.proguardMap = proguardMap;
     this.graphConsumer = graphConsumer;
     this.minApiLevel = minApiLevel;
+    this.features = features;
   }
 
   @Override
@@ -51,11 +61,16 @@
     return state.getDiagnosticsMessages();
   }
 
+  @Override
   public R8TestCompileResult inspectDiagnosticMessages(Consumer<TestDiagnosticMessages> consumer) {
     consumer.accept(state.getDiagnosticsMessages());
     return self();
   }
 
+  public Path getFeature(int index) {
+    return features.get(index);
+  }
+
   @Override
   public String getStdout() {
     return state.getStdout();
@@ -71,6 +86,22 @@
     return new CodeInspector(app, proguardMap);
   }
 
+  private CodeInspector featureInspector(Path feature) throws IOException {
+    return new CodeInspector(
+        AndroidApp.builder().addProgramFile(feature).setProguardMapOutputData(proguardMap).build());
+  }
+
+  @SafeVarargs
+  public final <E extends Throwable> R8TestCompileResult inspect(
+      ThrowingConsumer<CodeInspector, E>... consumers) throws IOException, E {
+    assertEquals(1 + features.size(), consumers.length);
+    consumers[0].accept(inspector());
+    for (int i = 0; i < features.size(); i++) {
+      consumers[i + 1].accept(featureInspector(features.get(i)));
+    }
+    return self();
+  }
+
   public GraphInspector graphInspector() throws IOException {
     assert graphConsumer != null;
     return new GraphInspector(graphConsumer, inspector());
@@ -101,6 +132,37 @@
     return new R8TestRunResult(app, runtime, result, proguardMap, this::graphInspector);
   }
 
+  public R8TestRunResult runFeature(TestRuntime runtime, Class<?> mainFeatureClass)
+      throws IOException {
+    return runFeature(runtime, mainFeatureClass, features.get(0));
+  }
+
+  public R8TestRunResult runFeature(
+      TestRuntime runtime, Class<?> mainFeatureClass, Path feature, Path... featureDependencies)
+      throws IOException {
+    assert getBackend() == runtime.getBackend();
+    ClassSubject mainClassSubject = inspector().clazz(SplitRunner.class);
+    assertThat("Did you forget a keep rule for the main method?", mainClassSubject, isPresent());
+    assertThat(
+        "Did you forget a keep rule for the main method?",
+        mainClassSubject.mainMethod(),
+        isPresent());
+    ClassSubject mainFeatureClassSubject = featureInspector(feature).clazz(mainFeatureClass);
+    assertThat(
+        "Did you forget a keep rule for the run method?", mainFeatureClassSubject, isPresent());
+    assertThat(
+        "Did you forget a keep rule for the run method?",
+        mainFeatureClassSubject.uniqueMethodWithName("run"),
+        isPresent());
+    String[] args = new String[2 + featureDependencies.length];
+    args[0] = mainFeatureClassSubject.getFinalName();
+    args[1] = feature.toString();
+    for (int i = 2; i < args.length; i++) {
+      args[i] = featureDependencies[i - 2].toString();
+    }
+    return runArt(runtime, additionalRunClassPath, mainClassSubject.getFinalName(), args);
+  }
+
   public String getProguardMap() {
     return proguardMap;
   }
diff --git a/src/test/java/com/android/tools/r8/R8TestRunResult.java b/src/test/java/com/android/tools/r8/R8TestRunResult.java
index 9fe91d1..0afdccf 100644
--- a/src/test/java/com/android/tools/r8/R8TestRunResult.java
+++ b/src/test/java/com/android/tools/r8/R8TestRunResult.java
@@ -17,7 +17,7 @@
 import java.util.concurrent.ExecutionException;
 import java.util.function.Consumer;
 
-public class R8TestRunResult extends TestRunResult<R8TestRunResult> {
+public class R8TestRunResult extends SingleTestRunResult<R8TestRunResult> {
 
   public interface GraphInspectorSupplier {
     GraphInspector get() throws IOException, ExecutionException;
diff --git a/src/test/java/com/android/tools/r8/SingleTestRunResult.java b/src/test/java/com/android/tools/r8/SingleTestRunResult.java
new file mode 100644
index 0000000..4d72d22
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/SingleTestRunResult.java
@@ -0,0 +1,171 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNotNull;
+
+import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.naming.retrace.StackTrace;
+import com.android.tools.r8.utils.AndroidApp;
+import com.android.tools.r8.utils.ThrowingConsumer;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.concurrent.ExecutionException;
+import org.hamcrest.Matcher;
+
+public abstract class SingleTestRunResult<RR extends SingleTestRunResult<RR>>
+    extends TestRunResult<RR> {
+  protected final AndroidApp app;
+  private final TestRuntime runtime;
+  private final ProcessResult result;
+
+  public SingleTestRunResult(AndroidApp app, TestRuntime runtime, ProcessResult result) {
+    this.app = app;
+    this.runtime = runtime;
+    this.result = result;
+  }
+
+  public AndroidApp app() {
+    return app;
+  }
+
+  public ProcessResult getResult() {
+    return result;
+  }
+
+  public String getStdOut() {
+    return result.stdout;
+  }
+
+  public String getStdErr() {
+    return result.stderr;
+  }
+
+  public StackTrace getStackTrace() {
+    if (runtime.isDex()) {
+      return StackTrace.extractFromArt(getStdErr(), runtime.asDex().getVm());
+    } else {
+      return StackTrace.extractFromJvm(getStdErr());
+    }
+  }
+
+  public int getExitCode() {
+    return result.exitCode;
+  }
+
+  @Override
+  public RR assertSuccess() {
+    assertEquals(errorMessage("Expected run to succeed."), 0, result.exitCode);
+    return self();
+  }
+
+  @Override
+  public RR assertStdoutMatches(Matcher<String> matcher) {
+    assertThat(errorMessage("Run stdout incorrect.", matcher.toString()), result.stdout, matcher);
+    return self();
+  }
+
+  @Override
+  public RR assertFailure() {
+    assertNotEquals(errorMessage("Expected run to fail."), 0, result.exitCode);
+    return self();
+  }
+
+  @Override
+  public RR assertStderrMatches(Matcher<String> matcher) {
+    assertThat(errorMessage("Run stderr incorrect.", matcher.toString()), result.stderr, matcher);
+    return self();
+  }
+
+  public CodeInspector inspector() throws IOException, ExecutionException {
+    // Inspection post run implies success. If inspection of an invalid program is needed it should
+    // be done on the compilation result or on the input.
+    assertSuccess();
+    assertNotNull(app);
+    return new CodeInspector(app);
+  }
+
+  @Override
+  public <E extends Throwable> RR inspect(ThrowingConsumer<CodeInspector, E> consumer)
+      throws IOException, ExecutionException, E {
+    CodeInspector inspector = inspector();
+    consumer.accept(inspector);
+    return self();
+  }
+
+  public <E extends Throwable> RR inspectFailure(ThrowingConsumer<CodeInspector, E> consumer)
+      throws IOException, ExecutionException, E {
+    assertFailure();
+    assertNotNull(app);
+    CodeInspector inspector = new CodeInspector(app);
+    consumer.accept(inspector);
+    return self();
+  }
+
+  public <E extends Throwable> RR inspectStackTrace(ThrowingConsumer<StackTrace, E> consumer)
+      throws E {
+    consumer.accept(getStackTrace());
+    return self();
+  }
+
+  public RR disassemble(PrintStream ps) throws IOException, ExecutionException {
+    ToolHelper.disassemble(app, ps);
+    return self();
+  }
+
+  public RR disassemble() throws IOException, ExecutionException {
+    return disassemble(System.out);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder builder = new StringBuilder();
+    appendInfo(builder);
+    return builder.toString();
+  }
+
+  String errorMessage(String message) {
+    return errorMessage(message, null);
+  }
+
+  String errorMessage(String message, String expected) {
+    StringBuilder builder = new StringBuilder(message).append('\n');
+    if (expected != null) {
+      if (expected.contains(System.lineSeparator())) {
+        builder.append("EXPECTED:").append(System.lineSeparator()).append(expected);
+      } else {
+        builder.append("EXPECTED: ").append(expected);
+      }
+      builder.append(System.lineSeparator());
+    }
+    appendInfo(builder);
+    return builder.toString();
+  }
+
+  private void appendInfo(StringBuilder builder) {
+    builder.append("APPLICATION: ");
+    appendApplication(builder);
+    builder.append('\n');
+    appendProcessResult(builder);
+  }
+
+  private void appendApplication(StringBuilder builder) {
+    builder.append(app == null ? "<default>" : app.toString());
+  }
+
+  private void appendProcessResult(StringBuilder builder) {
+    builder.append("COMMAND: ").append(result.command).append('\n').append(result);
+  }
+
+  public RR writeProcessResult(PrintStream ps) {
+    StringBuilder sb = new StringBuilder();
+    appendProcessResult(sb);
+    ps.println(sb.toString());
+    return self();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 545ab13..35e1eec 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -65,6 +65,7 @@
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.PreloadedClassFileProvider;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringUtils;
@@ -191,7 +192,7 @@
     return testForJvm(temp);
   }
 
-  public TestBuilder<? extends TestRunResult<?>, ?> testForRuntime(
+  public TestBuilder<? extends SingleTestRunResult<?>, ?> testForRuntime(
       TestRuntime runtime, Consumer<D8TestBuilder> d8TestBuilderConsumer) {
     if (runtime.isCf()) {
       return testForJvm();
@@ -203,15 +204,40 @@
     }
   }
 
-  public TestBuilder<? extends TestRunResult<?>, ?> testForRuntime(
+  public TestBuilder<? extends SingleTestRunResult<?>, ?> testForRuntime(
       TestRuntime runtime, AndroidApiLevel apiLevel) {
     return testForRuntime(runtime, d8TestBuilder -> d8TestBuilder.setMinApi(apiLevel));
   }
 
-  public TestBuilder<? extends TestRunResult<?>, ?> testForRuntime(TestParameters parameters) {
+  public TestBuilder<? extends SingleTestRunResult<?>, ?> testForRuntime(
+      TestParameters parameters) {
     return testForRuntime(parameters.getRuntime(), parameters.getApiLevel());
   }
 
+  public TestBuilder<? extends TestRunResult<?>, ?> testForDesugaring(TestParameters parameters) {
+    return testForDesugaring(parameters.getRuntime().getBackend(), parameters.getApiLevel());
+  }
+
+  private TestBuilder<? extends TestRunResult<?>, ?> testForDesugaring(
+      Backend backend, AndroidApiLevel apiLevel) {
+    assert apiLevel != null : "No API level. Add .withAllApiLevelsAlsoForCf() to test parameters?";
+    TestState state = new TestState(temp);
+    List<Pair<String, TestBuilder<? extends TestRunResult<?>, ?>>> builders;
+    if (backend == Backend.CF) {
+      builders =
+          ImmutableList.of(
+              new Pair<>("JAVAC", JvmTestBuilder.create(state)),
+              new Pair<>("D8/CF", D8TestBuilder.create(state, Backend.CF).setMinApi(apiLevel)));
+    } else {
+      assert backend == Backend.DEX;
+      builders =
+          ImmutableList.of(
+              new Pair<>("D8/DEX", D8TestBuilder.create(state, Backend.DEX).setMinApi(apiLevel)),
+              new Pair<>("D8/DEX o D8/CF", IntermediateCfD8TestBuilder.create(state, apiLevel)));
+    }
+    return TestBuilderCollection.create(state, builders);
+  }
+
   public ProguardTestBuilder testForProguard() {
     return testForProguard(temp);
   }
@@ -588,13 +614,15 @@
   }
 
   protected static AppView<AppInfo> computeAppView(AndroidApp app) throws Exception {
-    AppInfo appInfo = new AppInfo(readApplicationForDexOutput(app, new InternalOptions()));
+    AppInfo appInfo =
+        AppInfo.createInitialAppInfo(readApplicationForDexOutput(app, new InternalOptions()));
     return AppView.createForD8(appInfo);
   }
 
   protected static AppInfoWithClassHierarchy computeAppInfoWithClassHierarchy(AndroidApp app)
       throws Exception {
-    return new AppInfoWithClassHierarchy(readApplicationForDexOutput(app, new InternalOptions()));
+    return AppInfoWithClassHierarchy.createInitialAppInfoWithClassHierarchy(
+        readApplicationForDexOutput(app, new InternalOptions()));
   }
 
   protected static AppView<AppInfoWithClassHierarchy> computeAppViewWithSubtyping(AndroidApp app)
diff --git a/src/test/java/com/android/tools/r8/TestBuilder.java b/src/test/java/com/android/tools/r8/TestBuilder.java
index d56f4d9..9a874c2 100644
--- a/src/test/java/com/android/tools/r8/TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestBuilder.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8;
 
 import com.android.tools.r8.debug.DebugTestConfig;
+import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.utils.ListUtils;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
@@ -58,8 +59,10 @@
   }
 
   @Deprecated
-  public abstract RR run(String mainClass)
-      throws CompilationFailedException, ExecutionException, IOException;
+  public RR run(String mainClass)
+      throws CompilationFailedException, ExecutionException, IOException {
+    throw new Unimplemented("Deprecated");
+  }
 
   public abstract RR run(TestRuntime runtime, String mainClass, String... args)
       throws CompilationFailedException, ExecutionException, IOException;
diff --git a/src/test/java/com/android/tools/r8/TestBuilderCollection.java b/src/test/java/com/android/tools/r8/TestBuilderCollection.java
new file mode 100644
index 0000000..a537aea
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/TestBuilderCollection.java
@@ -0,0 +1,100 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import com.android.tools.r8.debug.DebugTestConfig;
+import com.android.tools.r8.errors.Unimplemented;
+import com.android.tools.r8.utils.Pair;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+
+/** Abstraction to allow setup and execution of multiple test builders. */
+public class TestBuilderCollection
+    extends TestBuilder<TestRunResultCollection, TestBuilderCollection> {
+
+  public static TestBuilderCollection create(
+      TestState state,
+      List<Pair<String, TestBuilder<? extends TestRunResult<?>, ?>>> testBuilders) {
+    return new TestBuilderCollection(state, testBuilders);
+  }
+
+  private final List<Pair<String, TestBuilder<? extends TestRunResult<?>, ?>>> builders;
+
+  private TestBuilderCollection(
+      TestState state, List<Pair<String, TestBuilder<? extends TestRunResult<?>, ?>>> builders) {
+    super(state);
+    assert !builders.isEmpty();
+    this.builders = builders;
+  }
+
+  @Override
+  TestBuilderCollection self() {
+    return this;
+  }
+
+  @Override
+  public TestRunResultCollection run(TestRuntime runtime, String mainClass, String... args)
+      throws CompilationFailedException, ExecutionException, IOException {
+    List<Pair<String, TestRunResult<?>>> runs = new ArrayList<>(builders.size());
+    for (Pair<String, TestBuilder<? extends TestRunResult<?>, ?>> builder : builders) {
+      runs.add(new Pair<>(builder.getFirst(), builder.getSecond().run(runtime, mainClass, args)));
+    }
+    return TestRunResultCollection.create(runs);
+  }
+
+  private TestBuilderCollection forEach(Consumer<TestBuilder<? extends TestRunResult<?>, ?>> fn) {
+    builders.forEach(b -> fn.accept(b.getSecond()));
+    return self();
+  }
+
+  @Override
+  public DebugTestConfig debugConfig() {
+    throw new Unimplemented("Unsupported debug config as of now...");
+  }
+
+  @Override
+  public TestBuilderCollection addProgramFiles(Collection<Path> files) {
+    return forEach(b -> b.addProgramFiles(files));
+  }
+
+  @Override
+  public TestBuilderCollection addProgramClassFileData(Collection<byte[]> classes) {
+    return forEach(b -> b.addProgramClassFileData(classes));
+  }
+
+  @Override
+  public TestBuilderCollection addProgramDexFileData(Collection<byte[]> data) {
+    return forEach(b -> b.addProgramDexFileData(data));
+  }
+
+  @Override
+  public TestBuilderCollection addLibraryFiles(Collection<Path> files) {
+    return forEach(b -> b.addLibraryFiles(files));
+  }
+
+  @Override
+  public TestBuilderCollection addLibraryClasses(Collection<Class<?>> classes) {
+    return forEach(b -> b.addLibraryClasses(classes));
+  }
+
+  @Override
+  public TestBuilderCollection addClasspathClasses(Collection<Class<?>> classes) {
+    return forEach(b -> b.addClasspathClasses(classes));
+  }
+
+  @Override
+  public TestBuilderCollection addClasspathFiles(Collection<Path> files) {
+    return forEach(b -> b.addClasspathFiles(files));
+  }
+
+  @Override
+  public TestBuilderCollection addRunClasspathFiles(Collection<Path> files) {
+    return forEach(b -> b.addRunClasspathFiles(files));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestCompileResult.java b/src/test/java/com/android/tools/r8/TestCompileResult.java
index 0816496..3294828 100644
--- a/src/test/java/com/android/tools/r8/TestCompileResult.java
+++ b/src/test/java/com/android/tools/r8/TestCompileResult.java
@@ -419,7 +419,7 @@
     return createRunResult(runtime, result);
   }
 
-  private RR runArt(
+  RR runArt(
       TestRuntime runtime, List<Path> additionalClassPath, String mainClass, String... arguments)
       throws IOException {
     DexVm vm = runtime.asDex().getVm();
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index 9d6b1c8..da1bc91 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -229,20 +229,15 @@
   }
 
   public T setMinApi(AndroidApiLevel minApiLevel) {
-    if (backend == Backend.DEX) {
-      return setMinApi(minApiLevel.getLevel());
-    }
-    return self();
+    return setMinApi(minApiLevel.getLevel());
   }
 
   public T setMinApi(int minApiLevel) {
     assert builder.getMinApiLevel() > 0 || this.defaultMinApiLevel != null
         : "Tests must use this method to set min API level, and not"
             + " BaseCompilerCommand.Builder.setMinApiLevel()";
-    if (backend == Backend.DEX) {
-      this.defaultMinApiLevel = null;
-      builder.setMinApiLevel(minApiLevel);
-    }
+    this.defaultMinApiLevel = null;
+    builder.setMinApiLevel(minApiLevel);
     return self();
   }
 
diff --git a/src/test/java/com/android/tools/r8/TestRunResult.java b/src/test/java/com/android/tools/r8/TestRunResult.java
index 16fd725..85214af 100644
--- a/src/test/java/com/android/tools/r8/TestRunResult.java
+++ b/src/test/java/com/android/tools/r8/TestRunResult.java
@@ -1,22 +1,15 @@
-// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8;
 
 import static org.hamcrest.CoreMatchers.containsString;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotEquals;
-import static org.junit.Assert.assertNotNull;
+import static org.hamcrest.CoreMatchers.is;
 
-import com.android.tools.r8.ToolHelper.ProcessResult;
-import com.android.tools.r8.naming.retrace.StackTrace;
-import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.ThrowingConsumer;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import java.io.IOException;
-import java.io.PrintStream;
 import java.util.Arrays;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
@@ -24,93 +17,45 @@
 import java.util.function.Function;
 import org.hamcrest.Matcher;
 
+/**
+ * Abstract base for checking the result of executing (test) program(s).
+ *
+ * <p>All methods defined on this base must generalize to allow checking of multiple run results
+ * simultaneously. For methods on single runs see the SingleTestRunResult subclass.
+ */
 public abstract class TestRunResult<RR extends TestRunResult<RR>> {
-  protected final AndroidApp app;
-  private final TestRuntime runtime;
-  private final ProcessResult result;
-
-  public TestRunResult(AndroidApp app, TestRuntime runtime, ProcessResult result) {
-    this.app = app;
-    this.runtime = runtime;
-    this.result = result;
-  }
 
   abstract RR self();
 
-  public <S> S map(Function<RR, S> fn) {
-    return fn.apply(self());
-  }
+  public abstract RR assertSuccess();
+
+  public abstract RR assertStdoutMatches(Matcher<String> matcher);
+
+  public abstract RR assertFailure();
+
+  public abstract RR assertStderrMatches(Matcher<String> matcher);
+
+  public abstract <E extends Throwable> RR inspect(ThrowingConsumer<CodeInspector, E> consumer)
+      throws IOException, ExecutionException, E;
+
+  public abstract RR disassemble() throws IOException, ExecutionException;
 
   public RR apply(Consumer<RR> fn) {
     fn.accept(self());
     return self();
   }
 
-  public AndroidApp app() {
-    return app;
+  public <S> S map(Function<RR, S> fn) {
+    return fn.apply(self());
   }
 
-  public String getStdOut() {
-    return result.stdout;
-  }
-
-  public String getStdErr() {
-    return result.stderr;
-  }
-
-  public StackTrace getStackTrace() {
-    if (runtime.isDex()) {
-      return StackTrace.extractFromArt(getStdErr(), runtime.asDex().getVm());
-    } else {
-      return StackTrace.extractFromJvm(getStdErr());
-    }
-  }
-
-  public int getExitCode() {
-    return result.exitCode;
-  }
-
-  public RR assertSuccess() {
-    assertEquals(errorMessage("Expected run to succeed."), 0, result.exitCode);
-    return self();
-  }
-
-  public RR assertFailure() {
-    assertNotEquals(errorMessage("Expected run to fail."), 0, result.exitCode);
-    return self();
-  }
-
-  public RR assertFailureWithOutput(String expected) {
-    assertFailure();
-    assertEquals(errorMessage("Run stdout incorrect.", expected), expected, result.stdout);
-    return self();
-  }
-
-  public RR assertFailureWithErrorThatMatches(Matcher<String> matcher) {
-    assertFailure();
-    assertThat(
-        errorMessage("Run stderr incorrect.", matcher.toString()), result.stderr, matcher);
-    return self();
-  }
-
-  public RR assertFailureWithErrorThatThrows(Class<? extends Throwable> expectedError) {
-    assertFailure();
-    assertThat(
-        errorMessage("Run stderr incorrect.", expectedError.getName()),
-        result.stderr,
-        containsString(expectedError.getName()));
-    return self();
-  }
-
-  public RR assertStderrMatches(Matcher<String> matcher) {
-    assertThat(errorMessage("Run stderr incorrect.", matcher.toString()), result.stderr, matcher);
-    return self();
+  public RR assertSuccessWithOutputThatMatches(Matcher<String> matcher) {
+    assertStdoutMatches(matcher);
+    return assertSuccess();
   }
 
   public RR assertSuccessWithOutput(String expected) {
-    assertSuccess();
-    assertEquals(errorMessage("Run stdout incorrect.", expected), expected, result.stdout);
-    return self();
+    return assertSuccessWithOutputThatMatches(is(expected));
   }
 
   public RR assertSuccessWithEmptyOutput() {
@@ -125,95 +70,17 @@
     return assertSuccessWithOutput(StringUtils.lines(expected));
   }
 
-  public RR assertSuccessWithOutputThatMatches(Matcher<String> matcher) {
-    assertSuccess();
-    assertThat(errorMessage("Run stdout incorrect.", matcher.toString()), result.stdout, matcher);
-    return self();
+  public RR assertFailureWithErrorThatMatches(Matcher<String> matcher) {
+    assertStderrMatches(matcher);
+    return assertFailure();
   }
 
-  public CodeInspector inspector() throws IOException, ExecutionException {
-    // Inspection post run implies success. If inspection of an invalid program is needed it should
-    // be done on the compilation result or on the input.
-    assertSuccess();
-    assertNotNull(app);
-    return new CodeInspector(app);
+  public RR assertFailureWithOutput(String expected) {
+    assertStdoutMatches(is(expected));
+    return assertFailure();
   }
 
-  public <E extends Throwable> RR inspect(ThrowingConsumer<CodeInspector, E> consumer)
-      throws IOException, ExecutionException, E {
-    CodeInspector inspector = inspector();
-    consumer.accept(inspector);
-    return self();
-  }
-
-  public <E extends Throwable> RR inspectFailure(ThrowingConsumer<CodeInspector, E> consumer)
-      throws IOException, ExecutionException, E {
-    assertFailure();
-    assertNotNull(app);
-    CodeInspector inspector = new CodeInspector(app);
-    consumer.accept(inspector);
-    return self();
-  }
-
-  public <E extends Throwable> RR inspectStackTrace(ThrowingConsumer<StackTrace, E> consumer)
-      throws E {
-    consumer.accept(getStackTrace());
-    return self();
-  }
-
-  public RR disassemble(PrintStream ps) throws IOException, ExecutionException {
-    ToolHelper.disassemble(app, ps);
-    return self();
-  }
-
-  public RR disassemble() throws IOException, ExecutionException {
-    return disassemble(System.out);
-  }
-
-  @Override
-  public String toString() {
-    StringBuilder builder = new StringBuilder();
-    appendInfo(builder);
-    return builder.toString();
-  }
-
-  String errorMessage(String message) {
-    return errorMessage(message, null);
-  }
-
-  String errorMessage(String message, String expected) {
-    StringBuilder builder = new StringBuilder(message).append('\n');
-    if (expected != null) {
-      if (expected.contains(System.lineSeparator())) {
-        builder.append("EXPECTED:").append(System.lineSeparator()).append(expected);
-      } else {
-        builder.append("EXPECTED: ").append(expected);
-      }
-      builder.append(System.lineSeparator());
-    }
-    appendInfo(builder);
-    return builder.toString();
-  }
-
-  private void appendInfo(StringBuilder builder) {
-    builder.append("APPLICATION: ");
-    appendApplication(builder);
-    builder.append('\n');
-    appendProcessResult(builder);
-  }
-
-  private void appendApplication(StringBuilder builder) {
-    builder.append(app == null ? "<default>" : app.toString());
-  }
-
-  private void appendProcessResult(StringBuilder builder) {
-    builder.append("COMMAND: ").append(result.command).append('\n').append(result);
-  }
-
-  public RR writeProcessResult(PrintStream ps) {
-    StringBuilder sb = new StringBuilder();
-    appendProcessResult(sb);
-    ps.println(sb.toString());
-    return self();
+  public RR assertFailureWithErrorThatThrows(Class<? extends Throwable> expectedError) {
+    return assertFailureWithErrorThatMatches(containsString(expectedError.getName()));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/TestRunResultCollection.java b/src/test/java/com/android/tools/r8/TestRunResultCollection.java
new file mode 100644
index 0000000..af4256a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/TestRunResultCollection.java
@@ -0,0 +1,78 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8;
+
+import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.ThrowingConsumer;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.base.Strings;
+import java.io.IOException;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+import org.hamcrest.Matcher;
+
+/** Checking container for checking the same properties of multiple run results. */
+public class TestRunResultCollection extends TestRunResult<TestRunResultCollection> {
+
+  public static TestRunResultCollection create(List<Pair<String, TestRunResult<?>>> runs) {
+    assert !runs.isEmpty();
+    return new TestRunResultCollection(runs);
+  }
+
+  private final List<Pair<String, TestRunResult<?>>> runs;
+
+  public TestRunResultCollection(List<Pair<String, TestRunResult<?>>> runs) {
+    this.runs = runs;
+  }
+
+  @Override
+  TestRunResultCollection self() {
+    return this;
+  }
+
+  private TestRunResultCollection forEach(Consumer<TestRunResult<?>> fn) {
+    runs.forEach(r -> fn.accept(r.getSecond()));
+    return self();
+  }
+
+  @Override
+  public TestRunResultCollection assertSuccess() {
+    return forEach(TestRunResult::assertSuccess);
+  }
+
+  @Override
+  public TestRunResultCollection assertFailure() {
+    return forEach(TestRunResult::assertFailure);
+  }
+
+  @Override
+  public TestRunResultCollection assertStdoutMatches(Matcher<String> matcher) {
+    return forEach(r -> r.assertStdoutMatches(matcher));
+  }
+
+  @Override
+  public TestRunResultCollection assertStderrMatches(Matcher<String> matcher) {
+    return forEach(r -> r.assertStderrMatches(matcher));
+  }
+
+  @Override
+  public <E extends Throwable> TestRunResultCollection inspect(
+      ThrowingConsumer<CodeInspector, E> consumer) throws IOException, ExecutionException, E {
+    for (Pair<String, TestRunResult<?>> run : runs) {
+      run.getSecond().inspect(consumer);
+    }
+    return self();
+  }
+
+  @Override
+  public TestRunResultCollection disassemble() throws IOException, ExecutionException {
+    for (Pair<String, TestRunResult<?>> run : runs) {
+      String name = run.getFirst();
+      System.out.println(name + " " + Strings.repeat("=", 80 - name.length() - 1));
+      run.getSecond().disassemble();
+    }
+    return self();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
index f53e8b3..3069351 100644
--- a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
@@ -5,9 +5,11 @@
 package com.android.tools.r8;
 
 import com.android.tools.r8.TestBase.Backend;
+import com.android.tools.r8.dexsplitter.SplitterTestBase.RunInterface;
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.TypeReference;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
+import com.android.tools.r8.utils.AndroidApiLevel;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Arrays;
@@ -30,6 +32,22 @@
     super(state, builder, backend);
   }
 
+  @Override
+  public T setMinApi(AndroidApiLevel minApiLevel) {
+    if (backend == Backend.DEX) {
+      return super.setMinApi(minApiLevel.getLevel());
+    }
+    return self();
+  }
+
+  @Override
+  public T setMinApi(int minApiLevel) {
+    if (backend == Backend.DEX) {
+      return super.setMinApi(minApiLevel);
+    }
+    return self();
+  }
+
   public T treeShaking(boolean enable) {
     enableTreeShaking = enable;
     return self();
@@ -172,6 +190,31 @@
     return self();
   }
 
+  public T addKeepFeatureMainRule(Class<?> mainClass) {
+    return addKeepFeatureMainRule(mainClass.getTypeName());
+  }
+
+  public T addKeepFeatureMainRules(Class<?>... mainClasses) {
+    for (Class<?> mainClass : mainClasses) {
+      this.addKeepFeatureMainRule(mainClass);
+    }
+    return self();
+  }
+
+  public T addKeepFeatureMainRule(String mainClass) {
+    return addKeepRules(
+        "-keep public class " + mainClass,
+        "    implements " + RunInterface.class.getTypeName() + " {",
+        "  public void <init>();",
+        "  public void run();",
+        "}");
+  }
+
+  public T addKeepFeatureMainRules(List<String> mainClasses) {
+    mainClasses.forEach(this::addKeepFeatureMainRule);
+    return self();
+  }
+
   public T addKeepMethodRules(Class<?> clazz, String... methodSignatures) {
     StringBuilder sb = new StringBuilder();
     sb.append("-keep class " + clazz.getTypeName() + " {\n");
diff --git a/src/test/java/com/android/tools/r8/TestState.java b/src/test/java/com/android/tools/r8/TestState.java
index 78388bc..6ba75d7 100644
--- a/src/test/java/com/android/tools/r8/TestState.java
+++ b/src/test/java/com/android/tools/r8/TestState.java
@@ -19,10 +19,18 @@
     this.temp = temp;
   }
 
+  public TemporaryFolder getTempFolder() {
+    return temp;
+  }
+
   public Path getNewTempFolder() throws IOException {
     return temp.newFolder().toPath();
   }
 
+  public Path getNewTempFile(String name) throws IOException {
+    return getNewTempFolder().resolve(name);
+  }
+
   DiagnosticsHandler getDiagnosticsHandler() {
     return messages;
   }
diff --git a/src/test/java/com/android/tools/r8/classmerging/HorizontalClassMergerShouldMergeSynchronizedMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontalstatic/HorizontalClassMergerShouldMergeSynchronizedMethodTest.java
similarity index 94%
rename from src/test/java/com/android/tools/r8/classmerging/HorizontalClassMergerShouldMergeSynchronizedMethodTest.java
rename to src/test/java/com/android/tools/r8/classmerging/horizontalstatic/HorizontalClassMergerShouldMergeSynchronizedMethodTest.java
index 7007821..402d279 100644
--- a/src/test/java/com/android/tools/r8/classmerging/HorizontalClassMergerShouldMergeSynchronizedMethodTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontalstatic/HorizontalClassMergerShouldMergeSynchronizedMethodTest.java
@@ -1,8 +1,8 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// 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;
+package com.android.tools.r8.classmerging.horizontalstatic;
 
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
diff --git a/src/test/java/com/android/tools/r8/classmerging/HorizontalClassMergerSynchronizedMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontalstatic/HorizontalClassMergerSynchronizedMethodTest.java
similarity index 96%
rename from src/test/java/com/android/tools/r8/classmerging/HorizontalClassMergerSynchronizedMethodTest.java
rename to src/test/java/com/android/tools/r8/classmerging/horizontalstatic/HorizontalClassMergerSynchronizedMethodTest.java
index a811257..8ddfef4 100644
--- a/src/test/java/com/android/tools/r8/classmerging/HorizontalClassMergerSynchronizedMethodTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontalstatic/HorizontalClassMergerSynchronizedMethodTest.java
@@ -1,8 +1,8 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// 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;
+package com.android.tools.r8.classmerging.horizontalstatic;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/src/test/java/com/android/tools/r8/classmerging/InliningAfterStaticClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontalstatic/InliningAfterStaticClassMergerTest.java
similarity index 96%
rename from src/test/java/com/android/tools/r8/classmerging/InliningAfterStaticClassMergerTest.java
rename to src/test/java/com/android/tools/r8/classmerging/horizontalstatic/InliningAfterStaticClassMergerTest.java
index e2ee52f..75e26f1 100644
--- a/src/test/java/com/android/tools/r8/classmerging/InliningAfterStaticClassMergerTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontalstatic/InliningAfterStaticClassMergerTest.java
@@ -1,8 +1,8 @@
-// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// 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;
+package com.android.tools.r8.classmerging.horizontalstatic;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerInterfaceTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontalstatic/StaticClassMergerInterfaceTest.java
similarity index 95%
rename from src/test/java/com/android/tools/r8/classmerging/StaticClassMergerInterfaceTest.java
rename to src/test/java/com/android/tools/r8/classmerging/horizontalstatic/StaticClassMergerInterfaceTest.java
index b0557d3..3b0b12f 100644
--- a/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontalstatic/StaticClassMergerInterfaceTest.java
@@ -1,8 +1,8 @@
-// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// 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;
+package com.android.tools.r8.classmerging.horizontalstatic;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -58,7 +58,6 @@
       assertThat(inspector.clazz(A.class.getTypeName() + "$-CC"), isPresent());
     }
 
-
     // By the time B is processed, there is no merge representative, so it should be present.
     assertThat(inspector.clazz(B.class), isPresent());
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontalstatic/StaticClassMergerTest.java
similarity index 96%
rename from src/test/java/com/android/tools/r8/classmerging/StaticClassMergerTest.java
rename to src/test/java/com/android/tools/r8/classmerging/horizontalstatic/StaticClassMergerTest.java
index 3fa47cb..7d6aa4a 100644
--- a/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontalstatic/StaticClassMergerTest.java
@@ -1,8 +1,8 @@
-// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// 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;
+package com.android.tools.r8.classmerging.horizontalstatic;
 
 import static org.junit.Assert.assertEquals;
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerVisibilityTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontalstatic/StaticClassMergerVisibilityTest.java
similarity index 96%
rename from src/test/java/com/android/tools/r8/classmerging/StaticClassMergerVisibilityTest.java
rename to src/test/java/com/android/tools/r8/classmerging/horizontalstatic/StaticClassMergerVisibilityTest.java
index 46ccfd2..9d545e5 100644
--- a/src/test/java/com/android/tools/r8/classmerging/StaticClassMergerVisibilityTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontalstatic/StaticClassMergerVisibilityTest.java
@@ -1,8 +1,8 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// 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;
+package com.android.tools.r8.classmerging.horizontalstatic;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/src/test/java/com/android/tools/r8/classmerging/B141942381.java b/src/test/java/com/android/tools/r8/classmerging/vertical/B141942381.java
similarity index 96%
rename from src/test/java/com/android/tools/r8/classmerging/B141942381.java
rename to src/test/java/com/android/tools/r8/classmerging/vertical/B141942381.java
index 36f320b..97f84ef 100644
--- a/src/test/java/com/android/tools/r8/classmerging/B141942381.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/B141942381.java
@@ -1,7 +1,7 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// 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;
+package com.android.tools.r8.classmerging.vertical;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.not;
diff --git a/src/test/java/com/android/tools/r8/classmerging/ForceInlineConstructorWithMultiPackageAccessesTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/ForceInlineConstructorWithMultiPackageAccessesTest.java
similarity index 90%
rename from src/test/java/com/android/tools/r8/classmerging/ForceInlineConstructorWithMultiPackageAccessesTest.java
rename to src/test/java/com/android/tools/r8/classmerging/vertical/ForceInlineConstructorWithMultiPackageAccessesTest.java
index 06344db..a96f8d3 100644
--- a/src/test/java/com/android/tools/r8/classmerging/ForceInlineConstructorWithMultiPackageAccessesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/ForceInlineConstructorWithMultiPackageAccessesTest.java
@@ -1,8 +1,8 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// 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;
+package com.android.tools.r8.classmerging.vertical;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.not;
@@ -11,7 +11,7 @@
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.classmerging.testclasses.ForceInlineConstructorWithMultiPackageAccessesTestClasses;
+import com.android.tools.r8.classmerging.vertical.testclasses.ForceInlineConstructorWithMultiPackageAccessesTestClasses;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import org.junit.Test;
 import org.junit.runner.RunWith;
diff --git a/src/test/java/com/android/tools/r8/classmerging/ForceInliningWithStaticInterfaceMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/ForceInliningWithStaticInterfaceMethodTest.java
similarity index 93%
rename from src/test/java/com/android/tools/r8/classmerging/ForceInliningWithStaticInterfaceMethodTest.java
rename to src/test/java/com/android/tools/r8/classmerging/vertical/ForceInliningWithStaticInterfaceMethodTest.java
index 7c86d1a..ff5d1a8 100644
--- a/src/test/java/com/android/tools/r8/classmerging/ForceInliningWithStaticInterfaceMethodTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/ForceInliningWithStaticInterfaceMethodTest.java
@@ -1,8 +1,8 @@
-// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// 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;
+package com.android.tools.r8.classmerging.vertical;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.utils.AndroidApiLevel;
diff --git a/src/test/java/com/android/tools/r8/classmerging/IncorrectRewritingOfInvokeSuperTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/IncorrectRewritingOfInvokeSuperTest.java
similarity index 94%
rename from src/test/java/com/android/tools/r8/classmerging/IncorrectRewritingOfInvokeSuperTest.java
rename to src/test/java/com/android/tools/r8/classmerging/vertical/IncorrectRewritingOfInvokeSuperTest.java
index 25ddfcd..505aa07 100644
--- a/src/test/java/com/android/tools/r8/classmerging/IncorrectRewritingOfInvokeSuperTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/IncorrectRewritingOfInvokeSuperTest.java
@@ -1,8 +1,8 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// 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;
+package com.android.tools.r8.classmerging.vertical;
 
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
diff --git a/src/test/java/com/android/tools/r8/classmerging/InterfaceWithProxyTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/InterfaceWithProxyTest.java
similarity index 94%
rename from src/test/java/com/android/tools/r8/classmerging/InterfaceWithProxyTest.java
rename to src/test/java/com/android/tools/r8/classmerging/vertical/InterfaceWithProxyTest.java
index ba16ddc..b9e0066 100644
--- a/src/test/java/com/android/tools/r8/classmerging/InterfaceWithProxyTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/InterfaceWithProxyTest.java
@@ -1,8 +1,8 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// 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;
+package com.android.tools.r8.classmerging.vertical;
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
diff --git a/src/test/java/com/android/tools/r8/classmerging/NestedDefaultInterfaceMethodsTestDump.java b/src/test/java/com/android/tools/r8/classmerging/vertical/NestedDefaultInterfaceMethodsTestDump.java
similarity index 95%
rename from src/test/java/com/android/tools/r8/classmerging/NestedDefaultInterfaceMethodsTestDump.java
rename to src/test/java/com/android/tools/r8/classmerging/vertical/NestedDefaultInterfaceMethodsTestDump.java
index e55802d..e2bd923 100644
--- a/src/test/java/com/android/tools/r8/classmerging/NestedDefaultInterfaceMethodsTestDump.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/NestedDefaultInterfaceMethodsTestDump.java
@@ -1,8 +1,8 @@
-// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// 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;
+package com.android.tools.r8.classmerging.vertical;
 
 import org.objectweb.asm.AnnotationVisitor;
 import org.objectweb.asm.ClassWriter;
diff --git a/src/test/java/com/android/tools/r8/classmerging/StaticInitializerTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/StaticInitializerTest.java
similarity index 93%
rename from src/test/java/com/android/tools/r8/classmerging/StaticInitializerTest.java
rename to src/test/java/com/android/tools/r8/classmerging/vertical/StaticInitializerTest.java
index 72c9e57..aa6a4f7 100644
--- a/src/test/java/com/android/tools/r8/classmerging/StaticInitializerTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/StaticInitializerTest.java
@@ -1,8 +1,8 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// 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;
+package com.android.tools.r8.classmerging.vertical;
 
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.TestBase;
diff --git a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerInitTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerInitTest.java
similarity index 96%
rename from src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerInitTest.java
rename to src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerInitTest.java
index b95a89a..6e93ac6 100644
--- a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerInitTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerInitTest.java
@@ -1,8 +1,8 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// 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;
+package com.android.tools.r8.classmerging.vertical;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerShouldMergeSynchronizedMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerShouldMergeSynchronizedMethodTest.java
similarity index 95%
rename from src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerShouldMergeSynchronizedMethodTest.java
rename to src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerShouldMergeSynchronizedMethodTest.java
index 5f6e255..f9c51ce 100644
--- a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerShouldMergeSynchronizedMethodTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerShouldMergeSynchronizedMethodTest.java
@@ -1,8 +1,8 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// 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;
+package com.android.tools.r8.classmerging.vertical;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.not;
diff --git a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSuperCallInStaticTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerSuperCallInStaticTest.java
similarity index 98%
rename from src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSuperCallInStaticTest.java
rename to src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerSuperCallInStaticTest.java
index cab2a18..9de435b 100644
--- a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSuperCallInStaticTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerSuperCallInStaticTest.java
@@ -2,7 +2,7 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-package com.android.tools.r8.classmerging;
+package com.android.tools.r8.classmerging.vertical;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.not;
diff --git a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedBlockTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerSynchronizedBlockTest.java
similarity index 96%
rename from src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedBlockTest.java
rename to src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerSynchronizedBlockTest.java
index 2828ec4..327be46 100644
--- a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedBlockTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerSynchronizedBlockTest.java
@@ -1,8 +1,8 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// 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;
+package com.android.tools.r8.classmerging.vertical;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedBlockWithArraysTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerSynchronizedBlockWithArraysTest.java
similarity index 96%
rename from src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedBlockWithArraysTest.java
rename to src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerSynchronizedBlockWithArraysTest.java
index b6039db..6c3f55d 100644
--- a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedBlockWithArraysTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerSynchronizedBlockWithArraysTest.java
@@ -1,8 +1,8 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// 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;
+package com.android.tools.r8.classmerging.vertical;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerSynchronizedMethodTest.java
similarity index 97%
rename from src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedMethodTest.java
rename to src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerSynchronizedMethodTest.java
index e77ee05..deff6a8 100644
--- a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerSynchronizedMethodTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerSynchronizedMethodTest.java
@@ -1,8 +1,8 @@
-// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// 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;
+package com.android.tools.r8.classmerging.vertical;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
diff --git a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
similarity index 99%
rename from src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerTest.java
rename to src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
index 26e5a16..2411076 100644
--- a/src/test/java/com/android/tools/r8/classmerging/VerticalClassMergerTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
@@ -1,7 +1,7 @@
-// Copyright (c) 2017, the R8 project authors. Please see the AUTHORS file
+// 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;
+package com.android.tools.r8.classmerging.vertical;
 
 import static com.android.tools.r8.ir.desugar.InterfaceMethodRewriter.COMPANION_CLASS_NAME_SUFFIX;
 import static com.android.tools.r8.smali.SmaliBuilder.buildCode;
diff --git a/src/test/java/com/android/tools/r8/classmerging/testclasses/ForceInlineConstructorWithMultiPackageAccessesTestClasses.java b/src/test/java/com/android/tools/r8/classmerging/vertical/testclasses/ForceInlineConstructorWithMultiPackageAccessesTestClasses.java
similarity index 85%
rename from src/test/java/com/android/tools/r8/classmerging/testclasses/ForceInlineConstructorWithMultiPackageAccessesTestClasses.java
rename to src/test/java/com/android/tools/r8/classmerging/vertical/testclasses/ForceInlineConstructorWithMultiPackageAccessesTestClasses.java
index 80ab1d6..2b3967a 100644
--- a/src/test/java/com/android/tools/r8/classmerging/testclasses/ForceInlineConstructorWithMultiPackageAccessesTestClasses.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/testclasses/ForceInlineConstructorWithMultiPackageAccessesTestClasses.java
@@ -2,7 +2,7 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-package com.android.tools.r8.classmerging.testclasses;
+package com.android.tools.r8.classmerging.vertical.testclasses;
 
 import com.android.tools.r8.NeverMerge;
 
diff --git a/src/test/java/com/android/tools/r8/desugar/ConcurrentHashMapKeySetTest.java b/src/test/java/com/android/tools/r8/desugar/ConcurrentHashMapKeySetTest.java
new file mode 100644
index 0000000..dda9269
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/ConcurrentHashMapKeySetTest.java
@@ -0,0 +1,60 @@
+// 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.desugar;
+
+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.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.StringUtils;
+import java.util.concurrent.ConcurrentHashMap;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ConcurrentHashMapKeySetTest extends TestBase {
+
+  static final String EXPECTED = StringUtils.lines("Hello, world");
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  }
+
+  public ConcurrentHashMapKeySetTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    TestRunResult<?> result =
+        testForDesugaring(parameters)
+            .addInnerClasses(ConcurrentHashMapKeySetTest.class)
+            .run(parameters.getRuntime(), TestClass.class);
+    if (parameters.isDexRuntime()
+        && parameters.getRuntime().asDex().getMinApiLevel().isLessThan(AndroidApiLevel.Q)) {
+      // TODO(b/123160897): Support desugaring of the Java 8 change to ConcurrentHashMap::keySet.
+      result.assertFailureWithErrorThatThrows(NoSuchMethodError.class);
+    } else {
+      result.assertSuccessWithOutput(EXPECTED);
+    }
+    // TODO(b/123160897): Inspect that keySet has changed on API level < Q / JDK8.
+  }
+
+  static class TestClass {
+
+    static ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
+
+    public static void main(String[] args) {
+      map.put("Hello", "world");
+      for (String key : map.keySet()) {
+        System.out.println(key + ", " + map.get(key));
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/DesugarToClassFile.java b/src/test/java/com/android/tools/r8/desugar/DesugarToClassFile.java
index 39f0ef1..17e482f 100644
--- a/src/test/java/com/android/tools/r8/desugar/DesugarToClassFile.java
+++ b/src/test/java/com/android/tools/r8/desugar/DesugarToClassFile.java
@@ -29,10 +29,20 @@
     this.parameters = parameters;
   }
 
-  private void checkHasCompanionClass(CodeInspector inspector) {
-    assertTrue(
-        inspector.allClasses().stream()
-            .anyMatch(subject -> subject.getOriginalName().endsWith("$-CC")));
+  private void checkHasCompanionClassIfRequired(CodeInspector inspector) {
+    boolean canUseDefaultAndStaticInterfaceMethods =
+        parameters
+            .getApiLevel()
+            .isGreaterThanOrEqualTo(apiLevelWithDefaultInterfaceMethodsSupport());
+    if (canUseDefaultAndStaticInterfaceMethods) {
+      assertTrue(
+          inspector.allClasses().stream()
+              .noneMatch(subject -> subject.getOriginalName().endsWith("$-CC")));
+    } else {
+      assertTrue(
+          inspector.allClasses().stream()
+              .anyMatch(subject -> subject.getOriginalName().endsWith("$-CC")));
+    }
   }
 
   private void checkHasLambdaClass(CodeInspector inspector) {
@@ -49,7 +59,7 @@
             .addInnerClasses(DesugarToClassFile.class)
             .setMinApi(parameters.getApiLevel())
             .compile()
-            .inspect(this::checkHasCompanionClass)
+            .inspect(this::checkHasCompanionClassIfRequired)
             .inspect(this::checkHasLambdaClass)
             .writeToZip();
 
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/MergingWithDesugaredLibraryTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/MergingWithDesugaredLibraryTest.java
index f485a25..d53e0e6 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/MergingWithDesugaredLibraryTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/MergingWithDesugaredLibraryTest.java
@@ -7,7 +7,8 @@
 import static com.android.tools.r8.MarkerMatcher.assertMarkersMatch;
 import static com.android.tools.r8.MarkerMatcher.markerCompilationMode;
 import static com.android.tools.r8.MarkerMatcher.markerHasDesugaredLibraryIdentifier;
-import static com.android.tools.r8.MarkerMatcher.markerHasMinApi;
+import static com.android.tools.r8.MarkerMatcher.markerIsDesugared;
+import static com.android.tools.r8.MarkerMatcher.markerMinApi;
 import static com.android.tools.r8.MarkerMatcher.markerTool;
 import static org.hamcrest.CoreMatchers.allOf;
 import static org.hamcrest.CoreMatchers.not;
@@ -97,10 +98,9 @@
         allOf(
             markerTool(Tool.R8),
             markerCompilationMode(CompilationMode.RELEASE),
-            not(markerHasMinApi()),
+            not(markerIsDesugared()),
             not(markerHasDesugaredLibraryIdentifier()));
-    assertMarkersMatch(
-        ExtractMarker.extractMarkerFromJarFile(shrunkenLib), ImmutableList.of(libraryMatcher));
+    assertMarkersMatch(ExtractMarker.extractMarkerFromJarFile(shrunkenLib), libraryMatcher);
 
     // Build an app with the R8 compiled library.
     Path app =
@@ -116,13 +116,62 @@
     Matcher<Marker> d8Matcher =
         allOf(
             markerTool(Tool.D8),
-            markerHasMinApi(),
+            markerIsDesugared(),
             markerHasDesugaredLibraryIdentifier(
                 parameters.getApiLevel().isLessThan(AndroidApiLevel.O)));
     assertMarkersMatch(
         ExtractMarker.extractMarkerFromDexFile(app), ImmutableList.of(libraryMatcher, d8Matcher));
   }
 
+  @Test
+  public void testMergeDesugaredWithDesugaredLib() throws Exception {
+    // Compile a library with D8 to CF.
+    Path desugaredLibCf =
+        testForD8(Backend.CF)
+            .addProgramClasses(Part2.class)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .writeToZip();
+
+    // D8 class file output marker has desugaring but no library desugaring.
+    Matcher<Marker> markerMatcher =
+        allOf(
+            markerTool(Tool.D8),
+            markerCompilationMode(CompilationMode.DEBUG),
+            markerIsDesugared(),
+            markerMinApi(parameters.getApiLevel()),
+            not(markerHasDesugaredLibraryIdentifier()));
+    assertMarkersMatch(ExtractMarker.extractMarkerFromJarFile(desugaredLibCf), markerMatcher);
+
+    Path desugaredLibDex =
+        testForD8()
+            .addProgramFiles(desugaredLibCf)
+            .disableDesugaring()
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .writeToZip();
+
+    // D8 dex file output marker has the same marker as the D8 class file output.
+    assertMarkersMatch(ExtractMarker.extractMarkerFromJarFile(desugaredLibDex), markerMatcher);
+
+    // Build an app using library desugaring merging with library not using library desugaring.
+    Path app;
+    try {
+      app =
+          testForD8()
+              .addProgramFiles(buildPart1DesugaredLibrary(), desugaredLibDex)
+              .setMinApi(parameters.getApiLevel())
+              .compile()
+              .writeToZip();
+
+      assertMarkersMatch(ExtractMarker.extractMarkerFromDexFile(app), markerMatcher);
+    } catch (CompilationFailedException e) {
+      assertTrue(someLibraryDesugaringRequired());
+      return;
+    }
+    assert !someLibraryDesugaringRequired();
+  }
+
   private void assertError(TestDiagnosticMessages m) {
     List<Diagnostic> errors = m.getErrors();
     if (expectError()) {
@@ -141,6 +190,10 @@
   }
 
   private boolean expectError() {
+    return someLibraryDesugaringRequired();
+  }
+
+  private boolean someLibraryDesugaringRequired() {
     return parameters.getApiLevel().getLevel() <= AndroidApiLevel.N.getLevel();
   }
 
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/kotlin/KotlinMetadataTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/kotlin/KotlinMetadataTest.java
new file mode 100644
index 0000000..9004df6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/kotlin/KotlinMetadataTest.java
@@ -0,0 +1,182 @@
+// 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.desugar.desugaredlibrary.kotlin;
+
+import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.D8TestRunResult;
+import com.android.tools.r8.DexIndexedConsumer.ArchiveConsumer;
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
+import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
+import com.android.tools.r8.kotlin.KotlinMetadataWriter;
+import com.android.tools.r8.shaking.ProguardKeepAttributes;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.FileUtils;
+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;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import kotlinx.metadata.jvm.KotlinClassMetadata;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class KotlinMetadataTest extends DesugaredLibraryTestBase {
+
+  private static final String PKG = KotlinMetadataTest.class.getPackage().getName();
+  private final TestParameters parameters;
+  private final boolean shrinkDesugaredLibrary;
+  private final KotlinTargetVersion targetVersion;
+  private static final String EXPECTED_OUTPUT = "Wuhuu, my special day is: 1997-8-29-2-14";
+
+  @Parameters(name = "{1}, shrinkDesugaredLibrary: {0}, target: {2}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(),
+        getTestParameters().withAllRuntimesAndApiLevels().build(),
+        KotlinTargetVersion.values());
+  }
+
+  public KotlinMetadataTest(
+      boolean shrinkDesugaredLibrary,
+      TestParameters parameters,
+      KotlinTargetVersion targetVersion) {
+    this.shrinkDesugaredLibrary = shrinkDesugaredLibrary;
+    this.parameters = parameters;
+    this.targetVersion = targetVersion;
+  }
+
+  private static Map<KotlinTargetVersion, Path> compiledJars = new HashMap<>();
+
+  @BeforeClass
+  public static void createLibJar() throws Exception {
+    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
+      compiledJars.put(
+          targetVersion,
+          kotlinc(KOTLINC, targetVersion)
+              .addSourceFiles(
+                  Paths.get(
+                      ToolHelper.TESTS_DIR,
+                      "java",
+                      DescriptorUtils.getBinaryNameFromJavaType(PKG),
+                      "Main" + FileUtils.KT_EXTENSION))
+              .compile());
+    }
+  }
+
+  @Test
+  public void testCf() throws Exception {
+    assumeTrue(parameters.getRuntime().isCf());
+    testForRuntime(parameters)
+        .addProgramFiles(compiledJars.get(targetVersion))
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(ToolHelper.getKotlinReflectJar())
+        .run(parameters.getRuntime(), PKG + ".MainKt")
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testTimeD8() throws Exception {
+    assumeTrue(parameters.getRuntime().isDex());
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    final File output = temp.newFile("output.zip");
+    final D8TestRunResult d8TestRunResult =
+        testForD8()
+            .addProgramFiles(compiledJars.get(targetVersion))
+            .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+            .addProgramFiles(ToolHelper.getKotlinReflectJar())
+            .setProgramConsumer(new ArchiveConsumer(output.toPath(), true))
+            .setMinApi(parameters.getApiLevel())
+            .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+            .addOptionsModification(
+                options -> {
+                  options.testing.enableD8ResourcesPassThrough = true;
+                  options.dataResourceConsumer = options.programConsumer.getDataResourceConsumer();
+                })
+            .compile()
+            .addDesugaredCoreLibraryRunClassPath(
+                this::buildDesugaredLibrary,
+                parameters.getApiLevel(),
+                keepRuleConsumer.get(),
+                false)
+            .run(parameters.getRuntime(), PKG + ".MainKt")
+            .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+    if (requiresAnyCoreLibDesugaring(parameters)) {
+      d8TestRunResult.inspect(this::inspectRewrittenMetadata);
+    }
+  }
+
+  @Test
+  public void testTimeR8() throws Exception {
+    boolean desugarLibrary = parameters.isDexRuntime() && requiresAnyCoreLibDesugaring(parameters);
+    final R8FullTestBuilder testBuilder =
+        testForR8(parameters.getBackend())
+            .addProgramFiles(compiledJars.get(targetVersion))
+            .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+            .addProgramFiles(ToolHelper.getKotlinReflectJar())
+            .addKeepMainRule(PKG + ".MainKt")
+            .addKeepAllClassesRule()
+            .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
+            .setMinApi(parameters.getApiLevel())
+            .allowDiagnosticWarningMessages();
+    KeepRuleConsumer keepRuleConsumer = null;
+    if (desugarLibrary) {
+      keepRuleConsumer = createKeepRuleConsumer(parameters);
+      testBuilder.enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer);
+    }
+    final R8TestCompileResult compileResult =
+        testBuilder
+            .compile()
+            .assertAllWarningMessagesMatch(
+                equalTo("Resource 'META-INF/MANIFEST.MF' already exists."));
+    if (desugarLibrary) {
+      assertNotNull(keepRuleConsumer);
+      compileResult.addDesugaredCoreLibraryRunClassPath(
+          this::buildDesugaredLibrary,
+          parameters.getApiLevel(),
+          keepRuleConsumer.get(),
+          shrinkDesugaredLibrary);
+    }
+    final R8TestRunResult r8TestRunResult =
+        compileResult
+            .run(parameters.getRuntime(), PKG + ".MainKt")
+            .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+    if (desugarLibrary) {
+      r8TestRunResult.inspect(this::inspectRewrittenMetadata);
+    }
+  }
+
+  private void inspectRewrittenMetadata(CodeInspector inspector) {
+    final ClassSubject clazz =
+        inspector.clazz("com.android.tools.r8.desugar.desugaredlibrary.kotlin.Skynet");
+    assertThat(clazz, isPresent());
+    final KotlinClassMetadata kotlinClassMetadata = clazz.getKotlinClassMetadata();
+    assertNotNull(kotlinClassMetadata);
+    String metadata = KotlinMetadataWriter.kotlinMetadataToString("", kotlinClassMetadata);
+    assertThat(metadata, containsString("specialDay:Lj$/time/LocalDateTime;"));
+    assertThat(metadata, containsString("Class(name=j$/time/LocalDateTime)"));
+    assertThat(metadata, not(containsString("java.time.LocalDateTime")));
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/kotlin/Main.kt b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/kotlin/Main.kt
new file mode 100644
index 0000000..f7e56bc
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/kotlin/Main.kt
@@ -0,0 +1,18 @@
+// 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.desugar.desugaredlibrary.kotlin
+
+import kotlin.reflect.full.primaryConstructor
+
+class Skynet(val specialDay: java.time.LocalDateTime)
+
+fun main() {
+  // Create Skynet reflectively and call it in a 'self-aware' fashion.
+  val primaryConstructor = Skynet::class.primaryConstructor
+  val skynet = primaryConstructor?.call(java.time.LocalDateTime.of(1997, 8, 29, 2, 14, 0))
+  val sd = skynet?.specialDay
+  println("Wuhuu, my special day is: " +
+            "${sd?.year}-${sd?.monthValue}-${sd?.dayOfMonth}-${sd?.hour}-${sd?.minute}")
+}
diff --git a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/ReturnTest.java b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/ReturnTest.java
index 3317df1..33e6587 100644
--- a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/ReturnTest.java
+++ b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/ReturnTest.java
@@ -20,7 +20,7 @@
 
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
   }
 
   public ReturnTest(TestParameters parameters) {
@@ -29,7 +29,7 @@
 
   @Test
   public void testReturn() throws Exception {
-    testForRuntime(parameters)
+    testForDesugaring(parameters)
         .addInnerClasses(ReturnTest.class)
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutput(
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/DevirtualizationAcrossFeatureSplitTest.java b/src/test/java/com/android/tools/r8/dexsplitter/DevirtualizationAcrossFeatureSplitTest.java
new file mode 100644
index 0000000..538f764
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/dexsplitter/DevirtualizationAcrossFeatureSplitTest.java
@@ -0,0 +1,74 @@
+// 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.dexsplitter;
+
+import com.android.tools.r8.NeverInline;
+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 DevirtualizationAcrossFeatureSplitTest extends SplitterTestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public DevirtualizationAcrossFeatureSplitTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(BaseClass.class, BaseInterface.class)
+        .addFeatureSplitRuntime()
+        .addFeatureSplit(FeatureMain.class, BaseInterfaceImpl.class)
+        .addKeepFeatureMainRules(FeatureMain.class)
+        .enableInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .runFeature(parameters.getRuntime(), FeatureMain.class)
+        .assertSuccessWithOutputLines("Hello world!");
+  }
+
+  // Base.
+
+  public static class BaseClass {
+
+    @NeverInline
+    public static void run(BaseInterface instance) {
+      instance.greet();
+    }
+  }
+
+  public interface BaseInterface {
+
+    void greet();
+  }
+
+  // Feature.
+
+  public static class FeatureMain implements RunInterface {
+
+    @Override
+    public void run() {
+      BaseClass.run(new BaseInterfaceImpl());
+    }
+  }
+
+  public static class BaseInterfaceImpl implements BaseInterface {
+
+    @Override
+    public void greet() {
+      System.out.println("Hello world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java b/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java
index 536d19d..816ed12 100644
--- a/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java
+++ b/src/test/java/com/android/tools/r8/dexsplitter/SplitterTestBase.java
@@ -274,27 +274,30 @@
     void run();
   }
 
-  static class SplitRunner {
+  public static class SplitRunner {
     /* We support two different modes:
      *   - One argument to main:
      *     Pass in the class to be loaded, must implement RunInterface, run will be called
-     *   - Two arguments to main:
+     *   - Two or more arguments to main:
      *     Pass in the class to be loaded, must implement RunInterface, run will be called
-     *     Pass in the feature split that we class load
-     *
+     *     Pass in the feature split that we class load, and an optional list of other feature
+     *     splits that must be loaded before the given feature split.
      */
     public static void main(String[] args) {
-      if (args.length < 1 || args.length > 2) {
+      if (args.length < 1) {
         throw new RuntimeException("Unsupported number of arguments");
       }
       String classToRun = args[0];
       ClassLoader loader = SplitRunner.class.getClassLoader();
-      // In the case where we simulate splits, we pass in the feature as the second argument
-      if (args.length == 2) {
-        try {
-          loader = new PathClassLoader(args[1], SplitRunner.class.getClassLoader());
-        } catch (MalformedURLException e) {
-          throw new RuntimeException("Failed reading input URL");
+      // In the case where we simulate splits, the second argument is the feature to load, followed
+      // by all the other features that it depends on.
+      if (args.length >= 2) {
+        for (int i = args.length - 1; i >= 1; i--) {
+          try {
+            loader = new PathClassLoader(args[i], loader);
+          } catch (MalformedURLException e) {
+            throw new RuntimeException("Failed reading input URL");
+          }
         }
       }
 
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/StaticClassMergingInFeatureSplitTest.java b/src/test/java/com/android/tools/r8/dexsplitter/StaticClassMergingInFeatureSplitTest.java
new file mode 100644
index 0000000..a1c961e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/dexsplitter/StaticClassMergingInFeatureSplitTest.java
@@ -0,0 +1,151 @@
+// 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.dexsplitter;
+
+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.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class StaticClassMergingInFeatureSplitTest extends SplitterTestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public StaticClassMergingInFeatureSplitTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    R8TestCompileResult compileResult =
+        testForR8(parameters.getBackend())
+            .addProgramClasses(BaseClassA.class, BaseClassB.class)
+            .addFeatureSplitRuntime()
+            .addFeatureSplit(Feature1Main.class, Feature1ClassA.class, Feature1ClassB.class)
+            .addFeatureSplit(Feature2Main.class, Feature2ClassA.class, Feature2ClassB.class)
+            .addKeepFeatureMainRules(Feature1Main.class, Feature2Main.class)
+            .enableInliningAnnotations()
+            .enableNeverClassInliningAnnotations()
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspect(this::inspectBase, this::inspectFeature1, this::inspectFeature2);
+
+    compileResult
+        .runFeature(parameters.getRuntime(), Feature1Main.class, compileResult.getFeature(0))
+        .assertSuccessWithOutputLines("Hello from feature 1!");
+
+    compileResult
+        .runFeature(parameters.getRuntime(), Feature2Main.class, compileResult.getFeature(1))
+        .assertSuccessWithOutputLines("Hello from feature 2!");
+  }
+
+  private void inspectBase(CodeInspector inspector) {
+    assertThat(inspector.clazz(BaseClassA.class), isPresent());
+    assertThat(inspector.clazz(BaseClassB.class), not(isPresent()));
+  }
+
+  private void inspectFeature1(CodeInspector inspector) {
+    assertThat(inspector.clazz(Feature1ClassA.class), isPresent());
+    assertThat(inspector.clazz(Feature1ClassB.class), not(isPresent()));
+  }
+
+  private void inspectFeature2(CodeInspector inspector) {
+    assertThat(inspector.clazz(Feature2ClassA.class), isPresent());
+    assertThat(inspector.clazz(Feature2ClassB.class), not(isPresent()));
+  }
+
+  // Base.
+
+  public static class BaseClassA {
+
+    @NeverInline
+    public static void greet() {
+      System.out.print("Hello");
+    }
+  }
+
+  @NeverClassInline
+  public static class BaseClassB {
+
+    @NeverInline
+    public static void greet() {
+      System.out.print(" ");
+    }
+  }
+
+  // Feature 1.
+
+  public static class Feature1Main implements RunInterface {
+
+    public void run() {
+      BaseClassA.greet();
+      BaseClassB.greet();
+      Feature1ClassA.greet();
+      Feature1ClassB.greet();
+    }
+  }
+
+  static class Feature1ClassA {
+
+    @NeverInline
+    public static void greet() {
+      System.out.print("from feature 1");
+    }
+  }
+
+  @NeverClassInline
+  static class Feature1ClassB {
+
+    @NeverInline
+    public static void greet() {
+      System.out.println("!");
+    }
+  }
+
+  // Feature 2.
+
+  public static class Feature2Main implements RunInterface {
+
+    @NeverInline
+    public void run() {
+      BaseClassA.greet();
+      BaseClassB.greet();
+      Feature2ClassA.greet();
+      Feature2ClassB.greet();
+    }
+  }
+
+  static class Feature2ClassA {
+
+    @NeverInline
+    public static void greet() {
+      System.out.print("from feature 2");
+    }
+  }
+
+  @NeverClassInline
+  static class Feature2ClassB {
+
+    @NeverInline
+    public static void greet() {
+      System.out.println("!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/VerticalClassMergingAcrossFeatureSplitTest.java b/src/test/java/com/android/tools/r8/dexsplitter/VerticalClassMergingAcrossFeatureSplitTest.java
new file mode 100644
index 0000000..4ff8f4b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/dexsplitter/VerticalClassMergingAcrossFeatureSplitTest.java
@@ -0,0 +1,116 @@
+// 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.dexsplitter;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.dexsplitter.VerticalClassMergingInFeatureSplitTest.BaseClass;
+import com.android.tools.r8.dexsplitter.VerticalClassMergingInFeatureSplitTest.Feature1Class;
+import com.android.tools.r8.dexsplitter.VerticalClassMergingInFeatureSplitTest.Feature2Class;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class VerticalClassMergingAcrossFeatureSplitTest extends SplitterTestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public VerticalClassMergingAcrossFeatureSplitTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    R8TestCompileResult compileResult =
+        testForR8(parameters.getBackend())
+            .addProgramClasses(BaseClass.class)
+            .addFeatureSplitRuntime()
+            .addFeatureSplit(Feature1Class.class)
+            .addFeatureSplit(Feature2Main.class, Feature2Class.class)
+            .addKeepFeatureMainRule(Feature2Main.class)
+            .enableInliningAnnotations()
+            .enableNeverClassInliningAnnotations()
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspect(this::inspectBase, this::inspectFeature1, this::inspectFeature2);
+
+    // Run feature 2 on top of feature 1.
+    compileResult
+        .runFeature(
+            parameters.getRuntime(),
+            Feature2Main.class,
+            compileResult.getFeature(1),
+            compileResult.getFeature(0))
+        .assertSuccessWithOutputLines("Hello world!");
+  }
+
+  private void inspectBase(CodeInspector inspector) {
+    assertThat(inspector.clazz(BaseClass.class), isPresent());
+  }
+
+  private void inspectFeature1(CodeInspector inspector) {
+    assertThat(inspector.clazz(Feature1Class.class), isPresent());
+  }
+
+  private void inspectFeature2(CodeInspector inspector) {
+    assertThat(inspector.clazz(Feature2Class.class), isPresent());
+  }
+
+  // Base.
+
+  public static class BaseClass {
+
+    @NeverInline
+    public void greet() {
+      System.out.println("world!");
+    }
+  }
+
+  // Feature 1.
+
+  public static class Feature1Class extends BaseClass {
+
+    @NeverInline
+    @Override
+    public void greet() {
+      System.out.print(" ");
+      super.greet();
+    }
+  }
+
+  // Feature 2.
+
+  public static class Feature2Main implements RunInterface {
+
+    @Override
+    public void run() {
+      new Feature2Class().greet();
+    }
+  }
+
+  @NeverClassInline
+  static class Feature2Class extends Feature1Class {
+
+    @NeverInline
+    @Override
+    public void greet() {
+      System.out.print("Hello");
+      super.greet();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/dexsplitter/VerticalClassMergingInFeatureSplitTest.java b/src/test/java/com/android/tools/r8/dexsplitter/VerticalClassMergingInFeatureSplitTest.java
new file mode 100644
index 0000000..13234ef
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/dexsplitter/VerticalClassMergingInFeatureSplitTest.java
@@ -0,0 +1,157 @@
+// 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.dexsplitter;
+
+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.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class VerticalClassMergingInFeatureSplitTest extends SplitterTestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public VerticalClassMergingInFeatureSplitTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    R8TestCompileResult compileResult =
+        testForR8(parameters.getBackend())
+            .addProgramClasses(BaseClass.class, BaseClassWithBaseSubclass.class)
+            .addFeatureSplitRuntime()
+            .addFeatureSplit(
+                Feature1Main.class, Feature1Class.class, Feature1ClassWithSameFeatureSubclass.class)
+            .addFeatureSplit(
+                Feature2Main.class, Feature2Class.class, Feature2ClassWithSameFeatureSubclass.class)
+            .addKeepFeatureMainRules(Feature1Main.class, Feature2Main.class)
+            .enableInliningAnnotations()
+            .enableNeverClassInliningAnnotations()
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspect(this::inspectBase, this::inspectFeature1, this::inspectFeature2);
+
+    compileResult
+        .runFeature(parameters.getRuntime(), Feature1Main.class, compileResult.getFeature(0))
+        .assertSuccessWithOutputLines("Hello world!");
+
+    compileResult
+        .runFeature(parameters.getRuntime(), Feature2Main.class, compileResult.getFeature(1))
+        .assertSuccessWithOutputLines("Hello world!");
+  }
+
+  private void inspectBase(CodeInspector inspector) {
+    assertThat(inspector.clazz(BaseClass.class), isPresent());
+    assertThat(inspector.clazz(BaseClassWithBaseSubclass.class), not(isPresent()));
+  }
+
+  private void inspectFeature1(CodeInspector inspector) {
+    assertThat(inspector.clazz(Feature1Class.class), isPresent());
+    assertThat(inspector.clazz(Feature1ClassWithSameFeatureSubclass.class), not(isPresent()));
+  }
+
+  private void inspectFeature2(CodeInspector inspector) {
+    assertThat(inspector.clazz(Feature2Class.class), isPresent());
+    assertThat(inspector.clazz(Feature2ClassWithSameFeatureSubclass.class), not(isPresent()));
+  }
+
+  // Base.
+
+  static class BaseClassWithBaseSubclass {
+
+    @NeverInline
+    public void greet() {
+      System.out.print(" ");
+    }
+  }
+
+  @NeverClassInline
+  public static class BaseClass extends BaseClassWithBaseSubclass {
+
+    @NeverInline
+    @Override
+    public void greet() {
+      System.out.print("Hello");
+      super.greet();
+    }
+  }
+
+  // Feature 1.
+
+  public static class Feature1Main implements RunInterface {
+
+    @Override
+    public void run() {
+      new BaseClass().greet();
+      new Feature1Class().greet();
+    }
+  }
+
+  static class Feature1ClassWithSameFeatureSubclass {
+
+    @NeverInline
+    public void greet() {
+      System.out.println("!");
+    }
+  }
+
+  @NeverClassInline
+  static class Feature1Class extends Feature1ClassWithSameFeatureSubclass {
+
+    @NeverInline
+    @Override
+    public void greet() {
+      System.out.print("world");
+      super.greet();
+    }
+  }
+
+  // Feature 2.
+
+  public static class Feature2Main implements RunInterface {
+
+    @NeverInline
+    @Override
+    public void run() {
+      new BaseClass().greet();
+      new Feature2Class().greet();
+    }
+  }
+
+  static class Feature2ClassWithSameFeatureSubclass {
+
+    @NeverInline
+    public void greet() {
+      System.out.println("!");
+    }
+  }
+
+  @NeverClassInline
+  static class Feature2Class extends Feature2ClassWithSameFeatureSubclass {
+
+    @NeverInline
+    @Override
+    public void greet() {
+      System.out.print("world");
+      super.greet();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/ClInitSideEffectEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/ClInitSideEffectEnumUnboxingTest.java
new file mode 100644
index 0000000..d6dda57
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/ClInitSideEffectEnumUnboxingTest.java
@@ -0,0 +1,111 @@
+// 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.enumunboxing;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestParameters;
+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 ClInitSideEffectEnumUnboxingTest extends EnumUnboxingTestBase {
+
+  private final TestParameters parameters;
+  private final boolean enumValueOptimization;
+  private final EnumKeepRules enumKeepRules;
+
+  @Parameters(name = "{0} valueOpt: {1} keep: {2}")
+  public static List<Object[]> data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public ClInitSideEffectEnumUnboxingTest(
+      TestParameters parameters, boolean enumValueOptimization, EnumKeepRules enumKeepRules) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+  }
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    Class<Switch> classToTest = Switch.class;
+    R8TestRunResult run =
+        testForR8(parameters.getBackend())
+            .addInnerClasses(ClInitSideEffectEnumUnboxingTest.class)
+            .addKeepMainRule(classToTest)
+            .addKeepRules(enumKeepRules.getKeepRules())
+            .enableInliningAnnotations()
+            .enableMergeAnnotations()
+            .enableNeverClassInliningAnnotations()
+            .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+            .allowDiagnosticInfoMessages()
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspectDiagnosticMessages(
+                m -> assertEnumIsUnboxed(MyEnum.class, classToTest.getSimpleName(), m))
+            .run(parameters.getRuntime(), classToTest)
+            .assertSuccess();
+    assertLines2By2Correct(run.getStdOut());
+  }
+
+  @NeverClassInline
+  enum MyEnum {
+    A,
+    B,
+    C;
+
+    @NeverInline
+    void print() {
+      Switch.packagePrivatePrint();
+    }
+  }
+
+  @NeverClassInline
+  @NeverMerge
+  static class OtherClass {
+    static {
+      Switch.otherClassInit = true;
+    }
+  }
+
+  static class Switch {
+
+    static boolean otherClassInit = false;
+
+    public static void main(String[] args) {
+      System.out.println(MyEnum.A.ordinal());
+      System.out.println(0);
+      System.out.println(MyEnum.B.ordinal());
+      System.out.println(1);
+
+      MyEnum.A.print();
+      packagePrivatePrint();
+
+      System.out.println(otherClassInit);
+      System.out.println(false);
+
+      initializeOtherClass();
+
+      System.out.println(otherClassInit);
+      System.out.println(true);
+    }
+
+    @NeverInline
+    private static void initializeOtherClass() {
+      new OtherClass();
+    }
+
+    @NeverInline
+    static void packagePrivatePrint() {
+      System.out.println("package private dependency");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EnumToStringLibTest.java b/src/test/java/com/android/tools/r8/enumunboxing/EnumToStringLibTest.java
index 1a440cb..bdf46e3 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EnumToStringLibTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumToStringLibTest.java
@@ -133,13 +133,8 @@
             Reference.methodFromMethod(ToStringLib.class.getDeclaredMethod("directBean")),
             Reference.methodFromMethod(ToStringLib.class.getDeclaredMethod("directSugar")))
         .addKeepClassRules(ToStringLib.LibEnum.class)
-        .allowDiagnosticMessages()
         .setMinApi(parameters.getApiLevel())
-        .compile()
-        .inspectDiagnosticMessages(
-            msg ->
-                assertEnumIsBoxed(
-                    ToStringLib.LibEnum.class, ToStringLib.LibEnum.class.getSimpleName(), msg));
+        .compile();
   }
 
   // This class emulates a library with the three public methods getEnumXXX.
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingClassStaticizerTest.java b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingClassStaticizerTest.java
index 758ed36..4f724d3 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingClassStaticizerTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingClassStaticizerTest.java
@@ -12,7 +12,6 @@
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.ir.optimize.enums.EnumUnboxingRewriter;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.util.List;
@@ -71,11 +70,7 @@
       assertThat(codeInspector.clazz(Companion.class).uniqueMethodWithName("method"), isPresent());
       return;
     }
-    MethodSubject method =
-        codeInspector
-            .clazz(CompanionHost.class)
-            .uniqueMethodWithName(
-                EnumUnboxingRewriter.ENUM_UNBOXING_UTILITY_METHOD_PREFIX + "1$method");
+    MethodSubject method = codeInspector.clazz(CompanionHost.class).uniqueMethodWithName("method");
     assertThat(method, isPresent());
     assertEquals("int", method.getMethod().method.proto.parameters.toString());
   }
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
index 02b5a5f..f26a6be 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
@@ -66,23 +66,26 @@
     }
   }
 
-  void enableEnumOptions(InternalOptions options, boolean enumValueOptimization) {
-    // TODO(b/160854837): re-enable enum unboxing.
-    options.enableEnumUnboxing = true;
+  protected void enableEnumOptions(InternalOptions options, boolean enumValueOptimization) {
     options.enableEnumValueOptimization = enumValueOptimization;
     options.enableEnumSwitchMapRemoval = enumValueOptimization;
     options.testing.enableEnumUnboxingDebugLogs = true;
   }
 
-  void assertEnumIsUnboxed(Class<?> enumClass, String testName, TestDiagnosticMessages m) {
+  protected void assertEnumIsUnboxed(
+      Class<?> enumClass, String testName, TestDiagnosticMessages m) {
     assertTrue(enumClass.isEnum());
+    assertEnumIsUnboxed(enumClass.getSimpleName(), testName, m);
+  }
+
+  protected void assertEnumIsUnboxed(String enumClass, String testName, TestDiagnosticMessages m) {
     Diagnostic diagnostic = m.getInfos().get(0);
     assertTrue(diagnostic.getDiagnosticMessage().startsWith("Unboxed enums"));
     assertTrue(
         StringUtils.joinLines(
             "Expected enum to be removed (" + testName + "):",
             m.getInfos().get(1).getDiagnosticMessage()),
-        diagnostic.getDiagnosticMessage().contains(enumClass.getSimpleName()));
+        diagnostic.getDiagnosticMessage().contains(enumClass));
   }
 
   void assertEnumIsBoxed(Class<?> enumClass, String testName, TestDiagnosticMessages m) {
@@ -101,7 +104,7 @@
         getAllEnumKeepRules());
   }
 
-  static EnumKeepRules[] getAllEnumKeepRules() {
+  protected static EnumKeepRules[] getAllEnumKeepRules() {
     return EnumKeepRules.values();
   }
 }
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingVerticalClassMergeTest.java b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingVerticalClassMergeTest.java
new file mode 100644
index 0000000..d8396cb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingVerticalClassMergeTest.java
@@ -0,0 +1,113 @@
+// 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.enumunboxing;
+
+import static org.junit.Assert.assertFalse;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestParameters;
+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;
+
+@RunWith(Parameterized.class)
+public class EnumUnboxingVerticalClassMergeTest extends EnumUnboxingTestBase {
+
+  private final TestParameters parameters;
+  private final boolean enumValueOptimization;
+  private final EnumKeepRules enumKeepRules;
+
+  @Parameterized.Parameters(name = "{0} valueOpt: {1} keep: {2}")
+  public static List<Object[]> data() {
+    return enumUnboxingTestParameters();
+  }
+
+  public EnumUnboxingVerticalClassMergeTest(
+      TestParameters parameters, boolean enumValueOptimization, EnumKeepRules enumKeepRules) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+  }
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    R8TestRunResult run =
+        testForR8(parameters.getBackend())
+            .addInnerClasses(EnumUnboxingVerticalClassMergeTest.class)
+            .addKeepMainRule(Main.class)
+            .addKeepRules(enumKeepRules.getKeepRules())
+            .enableNeverClassInliningAnnotations()
+            .enableInliningAnnotations()
+            .noMinification() // For assertions.
+            .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+            .allowDiagnosticInfoMessages()
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspect(this::assertVerticalClassMerged)
+            .inspectDiagnosticMessages(
+                m ->
+                    assertEnumIsUnboxed(
+                        UnboxableEnum.class,
+                        EnumUnboxingVerticalClassMergeTest.class.getSimpleName(),
+                        m))
+            .run(parameters.getRuntime(), Main.class)
+            .assertSuccess();
+    assertLines2By2Correct(run.getStdOut());
+  }
+
+  private void assertVerticalClassMerged(CodeInspector codeInspector) {
+    assertFalse(codeInspector.clazz(Merge1.class).isPresent());
+  }
+
+  static class Main {
+    public static void main(String[] args) {
+      Merge1 merge = new Merge2();
+      merge.print();
+      System.out.println("print");
+      merge.printDefault();
+      System.out.println("print default");
+      UnboxableEnum.A.enumPrint();
+      System.out.println("0");
+      UnboxableEnum.B.enumPrint();
+      System.out.println("1");
+    }
+
+    @NeverInline
+    static void test(UnboxableEnum e) {
+      System.out.println(e.ordinal());
+    }
+  }
+
+  static class Merge2 extends Merge1 {
+    @Override
+    public void print() {
+      System.out.println("print");
+    }
+  }
+
+  abstract static class Merge1 {
+    abstract void print();
+
+    void printDefault() {
+      System.out.println("print default");
+    }
+  }
+
+  @NeverClassInline
+  enum UnboxableEnum {
+    A,
+    B,
+    C;
+
+    @NeverInline
+    void enumPrint() {
+      Main.test(this);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/OverloadingEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/OverloadingEnumUnboxingTest.java
index 8e206b8..bf038fa 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/OverloadingEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/OverloadingEnumUnboxingTest.java
@@ -72,11 +72,37 @@
     C;
   }
 
+  @NeverClassInline
+  enum MyEnum3 {
+    A,
+    B,
+    C;
+  }
+
   static class TestClass {
 
     public static void main(String[] args) {
       virtualTest();
       staticTest();
+      constructorTest();
+    }
+
+    @NeverInline
+    private static void constructorTest() {
+      new TestClass(42);
+      System.out.println("42");
+      new TestClass(MyEnum1.A);
+      System.out.println("0");
+      new TestClass(MyEnum1.B);
+      System.out.println("1");
+      new TestClass(MyEnum2.A);
+      System.out.println("0");
+      new TestClass(MyEnum2.B);
+      System.out.println("1");
+      new TestClass(MyEnum3.A);
+      System.out.println("0");
+      new TestClass(MyEnum3.B);
+      System.out.println("1");
     }
 
     @NeverInline
@@ -91,6 +117,10 @@
       System.out.println("0");
       staticMethod(MyEnum2.B);
       System.out.println("1");
+      staticMethod(MyEnum3.A);
+      System.out.println("0");
+      staticMethod(MyEnum3.B);
+      System.out.println("1");
     }
 
     @NeverInline
@@ -106,6 +136,32 @@
       System.out.println("0");
       testClass.virtualMethod(MyEnum2.B);
       System.out.println("1");
+      testClass.virtualMethod(MyEnum3.A);
+      System.out.println("0");
+      testClass.virtualMethod(MyEnum3.B);
+      System.out.println("1");
+    }
+
+    public TestClass() {}
+
+    @NeverInline
+    public TestClass(MyEnum1 e1) {
+      System.out.println(e1.ordinal());
+    }
+
+    @NeverInline
+    public TestClass(MyEnum2 e2) {
+      System.out.println(e2.ordinal());
+    }
+
+    @NeverInline
+    public TestClass(MyEnum3 e3) {
+      System.out.println(e3.ordinal());
+    }
+
+    @NeverInline
+    public TestClass(int i) {
+      System.out.println(i);
     }
 
     @NeverInline
@@ -114,8 +170,13 @@
     }
 
     @NeverInline
-    public void virtualMethod(MyEnum2 e1) {
-      System.out.println(e1.ordinal());
+    public void virtualMethod(MyEnum2 e2) {
+      System.out.println(e2.ordinal());
+    }
+
+    @NeverInline
+    public void virtualMethod(MyEnum3 e3) {
+      System.out.println(e3.ordinal());
     }
 
     @NeverInline
@@ -129,8 +190,13 @@
     }
 
     @NeverInline
-    public static void staticMethod(MyEnum2 e1) {
-      System.out.println(e1.ordinal());
+    public static void staticMethod(MyEnum2 e2) {
+      System.out.println(e2.ordinal());
+    }
+
+    @NeverInline
+    public static void staticMethod(MyEnum3 e3) {
+      System.out.println(e3.ordinal());
     }
 
     @NeverInline
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/SwitchEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/SwitchEnumUnboxingTest.java
index 76c06cb..f58a069 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/SwitchEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/SwitchEnumUnboxingTest.java
@@ -4,10 +4,15 @@
 
 package com.android.tools.r8.enumunboxing;
 
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -17,7 +22,7 @@
 @RunWith(Parameterized.class)
 public class SwitchEnumUnboxingTest extends EnumUnboxingTestBase {
 
-  private static final Class<?> ENUM_CLASS = MyEnum.class;
+  private static final Class<?> ENUM_CLASS = MyEnumFewCases.class;
 
   private final TestParameters parameters;
   private final boolean enumValueOptimization;
@@ -45,10 +50,12 @@
             .addKeepRules(enumKeepRules.getKeepRules())
             .enableInliningAnnotations()
             .enableNeverClassInliningAnnotations()
+            .noMinification() // For assertions.
             .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
             .allowDiagnosticInfoMessages()
             .setMinApi(parameters.getApiLevel())
             .compile()
+            .inspect(this::assertSwitchPresentButSwitchMapRemoved)
             .inspectDiagnosticMessages(
                 m -> assertEnumIsUnboxed(ENUM_CLASS, classToTest.getSimpleName(), m))
             .run(parameters.getRuntime(), classToTest)
@@ -56,25 +63,87 @@
     assertLines2By2Correct(run.getStdOut());
   }
 
+  private void assertSwitchPresentButSwitchMapRemoved(CodeInspector i) {
+    if (enumValueOptimization) {
+      assertFalse(
+          i.clazz("com.android.tools.r8.enumunboxing.SwitchEnumUnboxingTest$1").isPresent());
+    }
+    assertTrue(
+        i.clazz(Switch.class)
+            .uniqueMethodWithName("switchOnEnumManyCases")
+            .streamInstructions()
+            .anyMatch(InstructionSubject::isSwitch));
+  }
+
   @NeverClassInline
-  enum MyEnum {
+  enum MyEnumFewCases {
     A,
     B,
-    C
+    C;
+
+    @NeverInline
+    void print() {
+      Switch.packagePrivatePrint();
+    }
+  }
+
+  @NeverClassInline
+  enum MyEnumManyCases {
+    A,
+    B,
+    C,
+    D,
+    E,
+    F,
+    G,
+    H,
+    I;
+
+    @NeverInline
+    void print() {
+      Switch.packagePrivatePrint();
+    }
   }
 
   static class Switch {
 
     public static void main(String[] args) {
-      System.out.println(switchOnEnum(MyEnum.A));
+      System.out.println(switchOnEnumFewCases(MyEnumFewCases.A));
       System.out.println(0xC0FFEE);
-      System.out.println(switchOnEnum(MyEnum.B));
+      System.out.println(switchOnEnumFewCases(MyEnumFewCases.B));
       System.out.println(0xBABE);
+
+      System.out.println(switchOnEnumManyCases(MyEnumManyCases.A));
+      System.out.println(0xACE);
+      System.out.println(switchOnEnumManyCases(MyEnumManyCases.B));
+      System.out.println(0xBABE);
+      System.out.println(switchOnEnumManyCases(MyEnumManyCases.C));
+      System.out.println(0xC0FFEE);
+      System.out.println(switchOnEnumManyCases(MyEnumManyCases.D));
+      System.out.println(0xDEC0DE);
+      System.out.println(switchOnEnumManyCases(MyEnumManyCases.E));
+      System.out.println(0xEFFACE);
+      System.out.println(switchOnEnumManyCases(MyEnumManyCases.F));
+      System.out.println(0xF00D);
+      System.out.println(switchOnEnumManyCases(MyEnumManyCases.G));
+      System.out.println(0x0);
+      System.out.println(switchOnEnumManyCases(MyEnumManyCases.H));
+      System.out.println(0x1);
+      System.out.println(switchOnEnumManyCases(MyEnumManyCases.I));
+      System.out.println(0x2);
+
+      MyEnumFewCases.A.print();
+      MyEnumManyCases.A.print();
     }
 
-    // Avoid removing the switch entirely.
     @NeverInline
-    static int switchOnEnum(MyEnum e) {
+    static void packagePrivatePrint() {
+      System.out.println("package private dependency");
+    }
+
+    // This switch will be converted into branches.
+    @NeverInline
+    static int switchOnEnumFewCases(MyEnumFewCases e) {
       switch (e) {
         case A:
           return 0xC0FFEE;
@@ -84,5 +153,32 @@
           return 0xDEADBEEF;
       }
     }
+
+    // This switch will remain a switch.
+    @NeverInline
+    static int switchOnEnumManyCases(MyEnumManyCases e) {
+      switch (e) {
+        case A:
+          return 0xACE;
+        case B:
+          return 0xBABE;
+        case C:
+          return 0xC0FFEE;
+        case D:
+          return 0xDEC0DE;
+        case E:
+          return 0xEFFACE;
+        case F:
+          return 0xF00D;
+        case G:
+          return 0x0;
+        case H:
+          return 0x1;
+        case I:
+          return 0x2;
+        default:
+          return 0xDEADBEEF;
+      }
+    }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/kotlin/Main.kt b/src/test/java/com/android/tools/r8/enumunboxing/kotlin/Main.kt
new file mode 100644
index 0000000..3e5e636
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/kotlin/Main.kt
@@ -0,0 +1,15 @@
+// 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.enumunboxing.kotlin
+
+enum class Color {
+  RED,
+  GREEN,
+  BLUE
+}
+
+fun main() {
+  enumValues<Color>().forEach { println(it.name) }
+}
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/kotlin/SimpleKotlinEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/kotlin/SimpleKotlinEnumUnboxingTest.java
new file mode 100644
index 0000000..1bb9c1e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/kotlin/SimpleKotlinEnumUnboxingTest.java
@@ -0,0 +1,94 @@
+// 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.enumunboxing.kotlin;
+
+import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
+import com.android.tools.r8.enumunboxing.EnumUnboxingTestBase;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.DescriptorUtils;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class SimpleKotlinEnumUnboxingTest extends EnumUnboxingTestBase {
+
+  private final TestParameters parameters;
+  private final boolean enumValueOptimization;
+  private final EnumKeepRules enumKeepRules;
+  private final KotlinTargetVersion targetVersion;
+
+  private static final String PKG = SimpleKotlinEnumUnboxingTest.class.getPackage().getName();
+  private static Map<KotlinTargetVersion, Path> jars = new HashMap<>();
+
+  @Parameters(name = "{0}, valueOpt: {1}, keep: {2}, kotlin targetVersion: {3}")
+  public static List<Object[]> enumUnboxingTestParameters() {
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(),
+        BooleanUtils.values(),
+        getAllEnumKeepRules(),
+        KotlinTargetVersion.values());
+  }
+
+  public SimpleKotlinEnumUnboxingTest(
+      TestParameters parameters,
+      boolean enumValueOptimization,
+      EnumKeepRules enumKeepRules,
+      KotlinTargetVersion targetVersion) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+    this.targetVersion = targetVersion;
+  }
+
+  @BeforeClass
+  public static void createLibJar() throws Exception {
+    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
+      jars.put(
+          targetVersion,
+          kotlinc(KOTLINC, targetVersion)
+              .addSourceFiles(
+                  Paths.get(
+                      ToolHelper.TESTS_DIR,
+                      "java",
+                      DescriptorUtils.getBinaryNameFromJavaType(PKG),
+                      "Main.kt"))
+              .compile());
+    }
+  }
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    testForR8(parameters.getBackend())
+        .addProgramFiles(jars.get(targetVersion))
+        .addKeepMainRule(PKG + ".MainKt")
+        .addKeepRules(enumKeepRules.getKeepRules())
+        .addKeepRuntimeVisibleAnnotations()
+        .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+        .allowDiagnosticInfoMessages()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspectDiagnosticMessages(
+            m -> {
+              assertEnumIsUnboxed(
+                  PKG + ".Color", SimpleKotlinEnumUnboxingTest.class.getSimpleName(), m);
+            })
+        .run(parameters.getRuntime(), PKG + ".MainKt")
+        .assertSuccessWithOutputLines("RED", "GREEN", "BLUE");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/BasicBlockIteratorTest.java b/src/test/java/com/android/tools/r8/ir/BasicBlockIteratorTest.java
index 04c8788..96d5012 100644
--- a/src/test/java/com/android/tools/r8/ir/BasicBlockIteratorTest.java
+++ b/src/test/java/com/android/tools/r8/ir/BasicBlockIteratorTest.java
@@ -71,7 +71,7 @@
     InternalOptions options = new InternalOptions();
     DexApplication dexApplication =
         new ApplicationReader(application, options, Timing.empty()).read();
-    AppView<?> appView = AppView.createForD8(new AppInfo(dexApplication));
+    AppView<?> appView = AppView.createForD8(AppInfo.createInitialAppInfo(dexApplication));
 
     // Build the code, and split the code into three blocks.
     MethodSubject methodSubject = getMethodSubject(application, signature);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/AlwaysThrowNullTest.java b/src/test/java/com/android/tools/r8/ir/optimize/AlwaysThrowNullTest.java
index 3ce1ee1..0eb5fab 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/AlwaysThrowNullTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/AlwaysThrowNullTest.java
@@ -14,10 +14,10 @@
 import com.android.tools.r8.NeverMerge;
 import com.android.tools.r8.NeverPropagateValue;
 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;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -184,7 +184,7 @@
         .assertSuccessWithOutput(JAVA_OUTPUT);
   }
 
-  private void test(TestRunResult result, boolean hasLiveness) throws Exception {
+  private void test(SingleTestRunResult<?> result, boolean hasLiveness) throws Exception {
     CodeInspector codeInspector = result.inspector();
     ClassSubject mainClass = codeInspector.clazz(MAIN);
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/ConstantRemovalTest.java b/src/test/java/com/android/tools/r8/ir/optimize/ConstantRemovalTest.java
index cdb9264..2485f20 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/ConstantRemovalTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/ConstantRemovalTest.java
@@ -134,7 +134,7 @@
 
     InternalOptions options = new InternalOptions();
     options.debug = true;
-    AppInfo appInfo = new AppInfo(DexApplication.builder(options, null).build());
+    AppInfo appInfo = AppInfo.createInitialAppInfo(DexApplication.builder(options, null).build());
     AppView<?> appView = AppView.createForD8(appInfo);
     IRCode code =
         new IRCode(
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/ObjectsRequireNonNullTest.java b/src/test/java/com/android/tools/r8/ir/optimize/ObjectsRequireNonNullTest.java
index 83c5243..ca64a9f 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/ObjectsRequireNonNullTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/ObjectsRequireNonNullTest.java
@@ -12,10 +12,10 @@
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NeverPropagateValue;
 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;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.ToolHelper.DexVm.Version;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.utils.AndroidApiLevel;
@@ -81,9 +81,8 @@
   }
 
   private void test(
-      TestRunResult result,
-      int expectedCountInMain,
-      int expectedCountInConsumer) throws Exception {
+      SingleTestRunResult<?> result, int expectedCountInMain, int expectedCountInConsumer)
+      throws Exception {
     CodeInspector codeInspector = result.inspector();
     ClassSubject mainClass = codeInspector.clazz(MAIN);
     MethodSubject mainMethod = mainClass.mainMethod();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/ConstClassCanonicalizationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/ConstClassCanonicalizationTest.java
index 1867272..43ea1b7 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/ConstClassCanonicalizationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/ConstClassCanonicalizationTest.java
@@ -11,10 +11,10 @@
 import com.android.tools.r8.D8TestRunResult;
 import com.android.tools.r8.DexIndexedConsumer.ArchiveConsumer;
 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;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.ir.optimize.canonicalization.ConstClassMain.Outer;
 import com.android.tools.r8.ir.optimize.canonicalization.ConstClassMain.Outer.Inner;
 import com.android.tools.r8.utils.InternalOptions;
@@ -144,8 +144,8 @@
         });
   }
 
-  private void test(
-      TestRunResult result, int mainCount, int outerCount, int innerCount) throws Exception {
+  private void test(SingleTestRunResult result, int mainCount, int outerCount, int innerCount)
+      throws Exception {
     CodeInspector codeInspector = result.inspector();
     ClassSubject mainClass = codeInspector.clazz(MAIN);
     MethodSubject mainMethod = mainClass.mainMethod();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/DexItemBasedConstStringCanonicalizationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/DexItemBasedConstStringCanonicalizationTest.java
index 5f8e918..5ef0d45 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/DexItemBasedConstStringCanonicalizationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/DexItemBasedConstStringCanonicalizationTest.java
@@ -10,8 +10,8 @@
 
 import com.android.tools.r8.D8TestRunResult;
 import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.ir.optimize.reflection.GetNameTestBase;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -69,8 +69,8 @@
         .assertSuccessWithOutput(JAVA_OUTPUT);
   }
 
-  private void test(
-      TestRunResult result, int expectedGetNameCount, int expectedConstString) throws Exception {
+  private void test(SingleTestRunResult result, int expectedGetNameCount, int expectedConstString)
+      throws Exception {
     CodeInspector codeInspector = result.inspector();
     ClassSubject mainClass = codeInspector.clazz(MAIN);
     MethodSubject mainMethod = mainClass.mainMethod();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/IdempotentFunctionCallCanonicalizationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/IdempotentFunctionCallCanonicalizationTest.java
index d3a87fa..5c8cfd8 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/IdempotentFunctionCallCanonicalizationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/IdempotentFunctionCallCanonicalizationTest.java
@@ -11,10 +11,10 @@
 import com.android.tools.r8.D8TestRunResult;
 import com.android.tools.r8.NeverInline;
 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;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -180,11 +180,12 @@
   }
 
   private void test(
-      TestRunResult result,
+      SingleTestRunResult<?> result,
       int expectedMaxCount,
       int expectedBooleanValueOfCount,
       int expectedIntValueOfCount,
-      int expectedLongValueOfCount) throws Exception {
+      int expectedLongValueOfCount)
+      throws Exception {
     CodeInspector codeInspector = result.inspector();
     ClassSubject mainClass = codeInspector.clazz(MAIN);
     MethodSubject mainMethod = mainClass.mainMethod();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/StringInMonitorTest.java b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/StringInMonitorTest.java
index f5e1762..bc04d7e 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/StringInMonitorTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/canonicalization/StringInMonitorTest.java
@@ -13,10 +13,10 @@
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NeverPropagateValue;
 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;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -104,10 +104,11 @@
   }
 
   private void test(
-      TestRunResult result,
+      SingleTestRunResult result,
       int expectedConstStringCount1,
       int expectedConstStringCount2,
-      int expectedConstStringCount3) throws Exception {
+      int expectedConstStringCount3)
+      throws Exception {
     CodeInspector codeInspector = result.inspector();
     ClassSubject mainClass = codeInspector.clazz(MAIN);
     MethodSubject mainMethod = mainClass.mainMethod();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
index 1058615..ffacc6a 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
@@ -13,9 +13,9 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.ir.optimize.classinliner.code.C;
 import com.android.tools.r8.ir.optimize.classinliner.code.CodeTestClass;
@@ -80,7 +80,7 @@
         ClassWithFinal.class
     };
     String javaOutput = runOnJava(main);
-    TestRunResult result =
+    SingleTestRunResult<?> result =
         testForR8(parameters.getBackend())
             .addProgramClasses(classes)
             .enableInliningAnnotations()
@@ -193,7 +193,7 @@
         CodeTestClass.class
     };
     String javaOutput = runOnJava(main);
-    TestRunResult result =
+    SingleTestRunResult<?> result =
         testForR8(parameters.getBackend())
             .addProgramClasses(classes)
             .enableInliningAnnotations()
@@ -229,7 +229,7 @@
         InvalidRootsTestClass.InitNeverReturnsNormally.class
     };
     String javaOutput = runOnJava(main);
-    TestRunResult result =
+    SingleTestRunResult<?> result =
         testForR8(parameters.getBackend())
             .addProgramClasses(classes)
             .enableProguardTestOptions()
@@ -286,7 +286,7 @@
         LambdasTestClass.IfaceUtil.class
     };
     String javaOutput = runOnJava(main);
-    TestRunResult result =
+    SingleTestRunResult<?> result =
         testForR8(parameters.getBackend())
             .addProgramClasses(classes)
             .addKeepMainRule(main)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/ForNameTest.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/ForNameTest.java
index dbcb237..f99639f 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/ForNameTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/ForNameTest.java
@@ -10,9 +10,9 @@
 
 import com.android.tools.r8.D8TestRunResult;
 import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -99,7 +99,7 @@
   }
 
   private void test(
-      TestRunResult result, int expectedForNameCount, int expectedConstClassCount)
+      SingleTestRunResult<?> result, int expectedForNameCount, int expectedConstClassCount)
       throws Exception {
     CodeInspector codeInspector = result.inspector();
     ClassSubject mainClass = codeInspector.clazz(MAIN);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameTest.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameTest.java
index 848bf1d..f5d0eb9 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetNameTest.java
@@ -11,8 +11,8 @@
 import com.android.tools.r8.D8TestRunResult;
 import com.android.tools.r8.ForceInline;
 import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -227,7 +227,7 @@
         .assertSuccessWithOutput(JAVA_OUTPUT);
   }
 
-  private void test(TestRunResult result, int expectedCount) throws Exception {
+  private void test(SingleTestRunResult<?> result, int expectedCount) throws Exception {
     CodeInspector codeInspector = result.inspector();
     ClassSubject mainClass = codeInspector.clazz(MAIN);
     MethodSubject mainMethod = mainClass.mainMethod();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java
index a16e8d5..14d7837 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/reflection/GetSimpleNameTest.java
@@ -12,8 +12,8 @@
 import com.android.tools.r8.ForceInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.TestRuntime.CfVm;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.utils.StringUtils;
@@ -187,7 +187,7 @@
         .assertSuccessWithOutput(JAVA_OUTPUT);
   }
 
-  private void test(TestRunResult result) throws Exception {
+  private void test(SingleTestRunResult<?> result) throws Exception {
     CodeInspector codeInspector = result.inspector();
     ClassSubject mainClass = codeInspector.clazz(MAIN);
     MethodSubject mainMethod = mainClass.mainMethod();
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 fc87e9b..77c4810 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,10 +14,10 @@
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.SingleTestRunResult;
 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.code.Instruction;
 import com.android.tools.r8.code.InvokeDirect;
 import com.android.tools.r8.code.InvokeStatic;
@@ -124,7 +124,7 @@
 
   @Test
   public void testTrivial() throws Exception {
-    TestRunResult result =
+    SingleTestRunResult result =
         testForR8(parameters.getBackend())
             .addProgramClasses(classes)
             .enableInliningAnnotations()
@@ -243,7 +243,7 @@
         HostOkFieldOnly.class,
         CandidateOkFieldOnly.class
     };
-    TestRunResult result =
+    SingleTestRunResult result =
         testForR8(parameters.getBackend())
             .addProgramClasses(classes)
             .enableInliningAnnotations()
@@ -281,7 +281,7 @@
         CandidateConflictField.class
     };
     String javaOutput = runOnJava(main);
-    TestRunResult result =
+    SingleTestRunResult result =
         testForR8(parameters.getBackend())
             .addProgramClasses(classes)
             .enableInliningAnnotations()
@@ -408,7 +408,7 @@
         Candidate.class
     };
     String javaOutput = runOnJava(main);
-    TestRunResult result =
+    SingleTestRunResult result =
         testForR8(parameters.getBackend())
             .addProgramClasses(classes)
             .enableInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/NameThenLengthTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/NameThenLengthTest.java
index add1003..5852266 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/NameThenLengthTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/NameThenLengthTest.java
@@ -11,10 +11,10 @@
 import com.android.tools.r8.D8TestRunResult;
 import com.android.tools.r8.NeverInline;
 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;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.StringUtils;
@@ -107,7 +107,7 @@
   }
 
   private void test(
-      TestRunResult result,
+      SingleTestRunResult<?> result,
       int expectedStringLengthCountInClinit,
       int expectedConstNumberCountInClinit,
       int expectedStringLengthCountInInstanceMethod,
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatenationTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatenationTest.java
index cef72dc..ef8217a 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatenationTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringConcatenationTest.java
@@ -11,10 +11,10 @@
 
 import com.android.tools.r8.D8TestRunResult;
 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;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -87,7 +87,8 @@
         .assertSuccessWithOutput(JAVA_OUTPUT);
   }
 
-  private void test(TestRunResult result, boolean isR8, boolean isReleaseMode) throws Exception {
+  private void test(SingleTestRunResult result, boolean isR8, boolean isReleaseMode)
+      throws Exception {
     // TODO(b/154899065): The lack of subtyping made the escape analysis to regard
     //    StringBuilder#toString as an alias-introducing instruction.
     //    For now, debug v.s. release mode of D8 have the same result.
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringContentCheckTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringContentCheckTest.java
index babfa89..1f7b97d 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringContentCheckTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringContentCheckTest.java
@@ -9,10 +9,10 @@
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.SingleTestRunResult;
 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.graph.DexMethod;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -138,7 +138,8 @@
     }).count();
   }
 
-  private void test(TestRunResult result, int expectedStringContentCheckerCount) throws Exception {
+  private void test(SingleTestRunResult<?> result, int expectedStringContentCheckerCount)
+      throws Exception {
     CodeInspector codeInspector = result.inspector();
     ClassSubject mainClass = codeInspector.clazz(MAIN);
     MethodSubject mainMethod = mainClass.mainMethod();
@@ -156,7 +157,7 @@
   public void testD8() throws Exception {
     assumeTrue("Only run D8 for Dex backend", parameters.isDexRuntime());
 
-    TestRunResult result =
+    SingleTestRunResult<?> result =
         testForD8()
             .debug()
             .addProgramClasses(MAIN)
@@ -177,7 +178,7 @@
 
   @Test
   public void testR8() throws Exception {
-    TestRunResult result =
+    SingleTestRunResult<?> result =
         testForR8(parameters.getBackend())
             .addProgramClasses(MAIN)
             .enableInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringHashCodeTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringHashCodeTest.java
index 2f2ab15..caddcb6 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringHashCodeTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringHashCodeTest.java
@@ -11,10 +11,10 @@
 import com.android.tools.r8.D8TestRunResult;
 import com.android.tools.r8.NeverInline;
 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;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -62,7 +62,7 @@
   }
 
   private void test(
-      TestRunResult<?> result, int expectedStringHashCodeCount, int expectedConstNumberCount)
+      SingleTestRunResult<?> result, int expectedStringHashCodeCount, int expectedConstNumberCount)
       throws Exception {
     CodeInspector codeInspector = result.inspector();
     ClassSubject mainClass = codeInspector.clazz(MAIN);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringIsEmptyTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringIsEmptyTest.java
index 0fe6e92..5c9c9a3 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringIsEmptyTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringIsEmptyTest.java
@@ -11,10 +11,10 @@
 import com.android.tools.r8.D8TestRunResult;
 import com.android.tools.r8.NeverInline;
 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;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -61,7 +61,7 @@
     assert !options.callSiteOptimizationOptions().isConstantPropagationEnabled();
   }
 
-  private void test(TestRunResult result, int expectedStringIsEmptyCount) throws Exception {
+  private void test(SingleTestRunResult result, int expectedStringIsEmptyCount) throws Exception {
     CodeInspector codeInspector = result.inspector();
     ClassSubject mainClass = codeInspector.clazz(MAIN);
     MethodSubject mainMethod = mainClass.mainMethod();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthTest.java
index f19fa6f..59c3d5c 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringLengthTest.java
@@ -12,10 +12,10 @@
 import com.android.tools.r8.ForceInline;
 import com.android.tools.r8.NeverInline;
 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;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
@@ -59,7 +59,7 @@
   }
 
   private void test(
-      TestRunResult result, int expectedStringLengthCount, int expectedConstNumberCount)
+      SingleTestRunResult<?> result, int expectedStringLengthCount, int expectedConstNumberCount)
       throws Exception {
     CodeInspector codeInspector = result.inspector();
     ClassSubject mainClass = codeInspector.clazz(MAIN);
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringToStringTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringToStringTest.java
index 0740b43..a6f314d 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringToStringTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringToStringTest.java
@@ -11,10 +11,10 @@
 import com.android.tools.r8.D8TestRunResult;
 import com.android.tools.r8.NeverInline;
 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;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -69,7 +69,8 @@
     }).count();
   }
 
-  private void test(TestRunResult result, int expectedStringToStringCount) throws Exception {
+  private void test(SingleTestRunResult<?> result, int expectedStringToStringCount)
+      throws Exception {
     CodeInspector codeInspector = result.inspector();
     ClassSubject mainClass = codeInspector.clazz(MAIN);
     MethodSubject mainMethod = mainClass.mainMethod();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/StringValueOfTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/StringValueOfTest.java
index 0333284..7d68bda 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/string/StringValueOfTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/StringValueOfTest.java
@@ -11,10 +11,10 @@
 import com.android.tools.r8.ForceInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NeverPropagateValue;
+import com.android.tools.r8.SingleTestRunResult;
 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.ir.optimize.string.StringValueOfTest.TestClass.Foo;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.StringUtils;
@@ -80,7 +80,8 @@
         instructionSubject.isConstString("null", JumboStringMode.ALLOW)).count();
   }
 
-  private void test(TestRunResult<?> result, boolean isR8, boolean isRelease) throws Exception {
+  private void test(SingleTestRunResult<?> result, boolean isR8, boolean isRelease)
+      throws Exception {
     CodeInspector codeInspector = result.inspector();
     ClassSubject mainClass = codeInspector.clazz(MAIN);
     MethodSubject mainMethod = mainClass.mainMethod();
@@ -107,7 +108,7 @@
 
   @Test
   public void testR8() throws Exception {
-    TestRunResult result =
+    SingleTestRunResult<?> result =
         testForR8(parameters.getBackend())
             .addProgramClassesAndInnerClasses(MAIN)
             .enableInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
index 3c202dd..3f7bb60 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
@@ -367,7 +367,8 @@
   public void multipleLiveTempRegisters() {
     InternalOptions options = new InternalOptions();
     AppView<AppInfo> appInfo =
-        AppView.createForD8(new AppInfo(DexApplication.builder(options, null).build()));
+        AppView.createForD8(
+            AppInfo.createInitialAppInfo(DexApplication.builder(options, null).build()));
     TypeElement objectType =
         TypeElement.fromDexType(options.itemFactory.objectType, Nullability.maybeNull(), appInfo);
     CollectMovesIterator moves = new CollectMovesIterator();
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/Regress68656641.java b/src/test/java/com/android/tools/r8/ir/regalloc/Regress68656641.java
index e246335..ca5b2c0 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/Regress68656641.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/Regress68656641.java
@@ -56,7 +56,7 @@
   @Test
   public void splitOverlappingInactiveIntervalWithNoNextUse() {
     InternalOptions options = new InternalOptions();
-    AppInfo appInfo = new AppInfo(DexApplication.builder(options, null).build());
+    AppInfo appInfo = AppInfo.createInitialAppInfo(DexApplication.builder(options, null).build());
     AppView<?> appView = AppView.createForD8(appInfo);
     IRCode code = simpleCode();
     MyRegisterAllocator allocator = new MyRegisterAllocator(appView, code);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSealedClassNestedTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSealedClassNestedTest.java
new file mode 100644
index 0000000..3fb7b23
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSealedClassNestedTest.java
@@ -0,0 +1,106 @@
+// 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.kotlin.metadata;
+
+import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
+import com.android.tools.r8.shaking.ProguardKeepAttributes;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.StringUtils;
+import java.nio.file.Path;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class MetadataRewriteInSealedClassNestedTest extends KotlinMetadataTestBase {
+
+  private static final String EXPECTED =
+      StringUtils.lines(
+          "DataTestControllerStartEvent(source=source, name=reason, display=false, stamp=0)",
+          "[com.android.tools.r8.kotlin.metadata.sealed_lib.TestEvent.DiagnosticEvent,"
+              + " com.android.tools.r8.kotlin.metadata.sealed_lib.Log]");
+  private static final String PKG_LIB = PKG + ".sealed_lib";
+  private static final String PKG_APP = PKG + ".sealed_app";
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0} target: {1}")
+  public static Collection<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+  }
+
+  public MetadataRewriteInSealedClassNestedTest(
+      TestParameters parameters, KotlinTargetVersion targetVersion) {
+    super(targetVersion);
+    this.parameters = parameters;
+  }
+
+  private static final Map<KotlinTargetVersion, Path> sealedLibJarMap = new HashMap<>();
+
+  @BeforeClass
+  public static void createLibJar() throws Exception {
+    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
+      Path sealedLibJar =
+          kotlinc(KOTLINC, targetVersion)
+              .addSourceFiles(
+                  getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_LIB), "nested"))
+              .compile();
+      sealedLibJarMap.put(targetVersion, sealedLibJar);
+    }
+  }
+
+  @Test
+  public void smokeTest() throws Exception {
+    Path libJar = sealedLibJarMap.get(targetVersion);
+    Path output =
+        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+            .addClasspathFiles(libJar)
+            .addSourceFiles(
+                getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
+            .setOutputPath(temp.newFolder().toPath())
+            .compile();
+    testForJvm()
+        .addRunClasspathFiles(
+            ToolHelper.getKotlinStdlibJar(), ToolHelper.getKotlinReflectJar(), libJar)
+        .addClasspath(output)
+        .run(parameters.getRuntime(), PKG_APP + ".MainKt")
+        .assertSuccessWithOutput(EXPECTED);
+  }
+
+  @Test
+  public void testMetadataInSealedClass_nested() throws Exception {
+    Path libJar =
+        testForR8(parameters.getBackend())
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
+            .addProgramFiles(sealedLibJarMap.get(targetVersion))
+            .addKeepAllClassesRule()
+            .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
+            .addKeepAttributes(
+                ProguardKeepAttributes.INNER_CLASSES, ProguardKeepAttributes.ENCLOSING_METHOD)
+            .compile()
+            .writeToZip();
+    Path output =
+        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+            .addClasspathFiles(libJar)
+            .addSourceFiles(
+                getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
+            .setOutputPath(temp.newFolder().toPath())
+            .compile();
+    testForJvm()
+        .addRunClasspathFiles(
+            ToolHelper.getKotlinStdlibJar(), ToolHelper.getKotlinReflectJar(), libJar)
+        .addClasspath(output)
+        .run(parameters.getRuntime(), PKG_APP + ".MainKt")
+        .assertSuccessWithOutput(EXPECTED);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/sealed_app/main.kt b/src/test/java/com/android/tools/r8/kotlin/metadata/sealed_app/main.kt
new file mode 100644
index 0000000..2d6f4c8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/sealed_app/main.kt
@@ -0,0 +1,20 @@
+// 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.kotlin.metadata.sealed_app
+
+import com.android.tools.r8.kotlin.metadata.sealed_lib.TestEvent
+
+fun staticDeclaration() {
+  println(TestEvent.DiagnosticEvent.DataTestControllerStartEvent("source", "reason"))
+}
+
+fun reflectiveUse() {
+  println(TestEvent.DiagnosticEvent.DataTestControllerStartEvent::class.supertypes)
+}
+
+fun main() {
+  staticDeclaration()
+  reflectiveUse()
+}
\ No newline at end of file
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/sealed_lib/nested.kt b/src/test/java/com/android/tools/r8/kotlin/metadata/sealed_lib/nested.kt
new file mode 100644
index 0000000..edf60d8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/sealed_lib/nested.kt
@@ -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.kotlin.metadata.sealed_lib
+
+/** Code originating from this bug b/163359809 */
+
+interface Log {
+  val source: String
+}
+
+sealed class TestEvent(
+  open var stamp: Long = currentStamp
+) {
+  sealed class DiagnosticEvent(
+    open val name: String,
+    open val display: Boolean = false,
+    override var stamp: Long = currentStamp
+  ) : TestEvent(stamp) {
+    data class DataTestControllerStartEvent(
+      override val source: String,
+      override val name: String = DataTestControllerStartEvent::class.java.simpleName,
+      override val display: Boolean = false,
+      override var stamp: Long = currentStamp
+    ) :
+      DiagnosticEvent(name, display, stamp),
+      Log
+  }
+  companion object {
+    var currentStamp = 0L
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/reflection/KotlinReflectTest.java b/src/test/java/com/android/tools/r8/kotlin/reflection/KotlinReflectTest.java
new file mode 100644
index 0000000..a2bb5df
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/reflection/KotlinReflectTest.java
@@ -0,0 +1,114 @@
+// 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.kotlin.reflection;
+
+import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.DexIndexedConsumer.ArchiveConsumer;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
+import com.android.tools.r8.shaking.ProguardKeepAttributes;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.FileUtils;
+import java.io.File;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class KotlinReflectTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final KotlinTargetVersion targetVersion;
+  private static final String EXPECTED_OUTPUT = "Hello World!";
+  private static final String PKG = KotlinReflectTest.class.getPackage().getName();
+  private static Map<KotlinTargetVersion, Path> compiledJars = new HashMap<>();
+
+  @Parameters(name = "{0}, target: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(), KotlinTargetVersion.values());
+  }
+
+  public KotlinReflectTest(TestParameters parameters, KotlinTargetVersion targetVersion) {
+    this.parameters = parameters;
+    this.targetVersion = targetVersion;
+  }
+
+  @BeforeClass
+  public static void createLibJar() throws Exception {
+    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
+      compiledJars.put(
+          targetVersion,
+          kotlinc(KOTLINC, targetVersion)
+              .addSourceFiles(
+                  Paths.get(
+                      ToolHelper.TESTS_DIR,
+                      "java",
+                      DescriptorUtils.getBinaryNameFromJavaType(PKG),
+                      "SimpleReflect" + FileUtils.KT_EXTENSION))
+              .compile());
+    }
+  }
+
+  @Test
+  public void testCf() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addProgramFiles(compiledJars.get(targetVersion))
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(ToolHelper.getKotlinReflectJar())
+        .run(parameters.getRuntime(), PKG + ".SimpleReflectKt")
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    final File output = temp.newFile("output.zip");
+    testForD8(parameters.getBackend())
+        .addProgramFiles(compiledJars.get(targetVersion))
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(ToolHelper.getKotlinReflectJar())
+        .setProgramConsumer(new ArchiveConsumer(output.toPath(), true))
+        .setMinApi(parameters.getApiLevel())
+        .addOptionsModification(
+            options -> {
+              options.testing.enableD8ResourcesPassThrough = true;
+              options.dataResourceConsumer = options.programConsumer.getDataResourceConsumer();
+            })
+        .run(parameters.getRuntime(), PKG + ".SimpleReflectKt")
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    final File foo = temp.newFile("foo");
+    testForR8(parameters.getBackend())
+        .addProgramFiles(compiledJars.get(targetVersion))
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(ToolHelper.getKotlinReflectJar())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepAllClassesRule()
+        .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
+        .allowDiagnosticWarningMessages()
+        .compile()
+        .writeToZip(foo.toPath())
+        .assertAllWarningMessagesMatch(equalTo("Resource 'META-INF/MANIFEST.MF' already exists."))
+        .run(parameters.getRuntime(), PKG + ".SimpleReflectKt")
+        .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/kotlin/reflection/SimpleReflect.kt b/src/test/java/com/android/tools/r8/kotlin/reflection/SimpleReflect.kt
new file mode 100644
index 0000000..98eb089
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/kotlin/reflection/SimpleReflect.kt
@@ -0,0 +1,15 @@
+// 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.kotlin.reflection
+
+import kotlin.reflect.full.primaryConstructor
+
+class Container(val str: String)
+
+fun main() {
+  val primaryConstructor = Container::class.primaryConstructor
+  val container = primaryConstructor?.call("Hello World!")
+  println(container?.str)
+}
diff --git a/src/test/java/com/android/tools/r8/naming/KotlinIntrinsicsIdentifierTest.java b/src/test/java/com/android/tools/r8/naming/KotlinIntrinsicsIdentifierTest.java
index daa21a4..5b26d4f 100644
--- a/src/test/java/com/android/tools/r8/naming/KotlinIntrinsicsIdentifierTest.java
+++ b/src/test/java/com/android/tools/r8/naming/KotlinIntrinsicsIdentifierTest.java
@@ -9,8 +9,8 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestCompileResult;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
@@ -121,7 +121,7 @@
       String targetFieldName,
       String targetMethodName) throws Exception {
     String mainClassName = testMain.getClassName();
-    TestRunResult result =
+    SingleTestRunResult result =
         testForR8(Backend.DEX)
             .addProgramFiles(getKotlinJarFile(FOLDER))
             .addProgramFiles(getJavaJarFile(FOLDER))
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 d2b2aba..4dd2fb0 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
@@ -8,7 +8,7 @@
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 
-import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.retrace.Retrace;
 import com.android.tools.r8.retrace.RetraceCommand;
@@ -274,7 +274,7 @@
     return new StackTrace(internalExtractFromJvm(stderr), stderr);
   }
 
-  public static StackTrace extractFromJvm(TestRunResult result) {
+  public static StackTrace extractFromJvm(SingleTestRunResult result) {
     assertNotEquals(0, result.getExitCode());
     return extractFromJvm(result.getStdErr());
   }
diff --git a/src/test/java/com/android/tools/r8/naming/retraceproguard/StackTrace.java b/src/test/java/com/android/tools/r8/naming/retraceproguard/StackTrace.java
index 33be5b5..be74127 100644
--- a/src/test/java/com/android/tools/r8/naming/retraceproguard/StackTrace.java
+++ b/src/test/java/com/android/tools/r8/naming/retraceproguard/StackTrace.java
@@ -8,7 +8,7 @@
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 
-import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.DexVm;
 import com.android.tools.r8.utils.FileUtils;
@@ -198,7 +198,7 @@
     return new StackTrace(internalExtractFromJvm(stderr), stderr);
   }
 
-  public static StackTrace extractFromJvm(TestRunResult result) {
+  public static StackTrace extractFromJvm(SingleTestRunResult result) {
     assertNotEquals(0, result.getExitCode());
     return extractFromJvm(result.getStdErr());
   }
diff --git a/src/test/java/com/android/tools/r8/proguard/configuration/RepackagingCompatibilityTest.java b/src/test/java/com/android/tools/r8/proguard/configuration/RepackagingCompatibilityTest.java
index 39b47f6..8d62a04 100644
--- a/src/test/java/com/android/tools/r8/proguard/configuration/RepackagingCompatibilityTest.java
+++ b/src/test/java/com/android/tools/r8/proguard/configuration/RepackagingCompatibilityTest.java
@@ -13,7 +13,6 @@
 import static org.junit.Assume.assumeFalse;
 
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.TestShrinkerBuilder;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.utils.BooleanUtils;
@@ -75,26 +74,27 @@
             repackageToRoot, quote, shrinker),
         repackageToRoot && quote != Quote.NONE);
 
-    TestRunResult<?> result =
-        builder
-            .addProgramClasses(mainClass)
-            .addKeepRules(getKeepRules())
-            .run(mainClass)
-            .assertSuccessWithOutput(expectedOutput);
-
-    ClassSubject testClassSubject = result.inspector().clazz(mainClass);
-    assertThat(testClassSubject, isPresent());
-    if (repackageToRoot) {
-      if (directive.equals("-flattenpackagehierarchy")) {
-        assertThat(testClassSubject.getFinalName(), startsWith("a."));
-      } else if (directive.equals("-repackageclasses")) {
-        assertThat(testClassSubject.getFinalName(), not(containsString(".")));
-      } else {
-        fail();
-      }
-    } else {
-      assertThat(testClassSubject.getFinalName(), startsWith("greeter."));
-    }
+    builder
+        .addProgramClasses(mainClass)
+        .addKeepRules(getKeepRules())
+        .run(mainClass)
+        .assertSuccessWithOutput(expectedOutput)
+        .inspect(
+            inspector -> {
+              ClassSubject testClassSubject = inspector.clazz(mainClass);
+              assertThat(testClassSubject, isPresent());
+              if (repackageToRoot) {
+                if (directive.equals("-flattenpackagehierarchy")) {
+                  assertThat(testClassSubject.getFinalName(), startsWith("a."));
+                } else if (directive.equals("-repackageclasses")) {
+                  assertThat(testClassSubject.getFinalName(), not(containsString(".")));
+                } else {
+                  fail();
+                }
+              } else {
+                assertThat(testClassSubject.getFinalName(), startsWith("greeter."));
+              }
+            });
   }
 
   private List<String> getKeepRules() {
diff --git a/src/test/java/com/android/tools/r8/repackage/CrossPackageInvokeSuperToPackagePrivateMethodTest.java b/src/test/java/com/android/tools/r8/repackage/CrossPackageInvokeSuperToPackagePrivateMethodTest.java
new file mode 100644
index 0000000..32a8cc8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/CrossPackageInvokeSuperToPackagePrivateMethodTest.java
@@ -0,0 +1,144 @@
+// 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.repackage;
+
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestBuilder;
+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.repackage.testclasses.CrossPackageInvokeSuperToPackagePrivateMethodTestClasses;
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(Parameterized.class)
+public class CrossPackageInvokeSuperToPackagePrivateMethodTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public CrossPackageInvokeSuperToPackagePrivateMethodTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .apply(this::addProgramClasses)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines("A", "B", "A", "C", "D", "C");
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .apply(this::addProgramClasses)
+        .addKeepMainRule(TestClass.class)
+        .enableInliningAnnotations()
+        .enableMergeAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), TestClass.class)
+        .apply(this::inspectRunResult);
+  }
+
+  private void addProgramClasses(TestBuilder<?, ?> builder) throws IOException {
+    builder
+        .addProgramClasses(
+            TestClass.class,
+            A.class,
+            B.class,
+            CrossPackageInvokeSuperToPackagePrivateMethodTestClasses.C.class)
+        .addProgramClassFileData(
+            transformer(D.class)
+                .transformMethodInsnInMethod(
+                    "packagePrivate",
+                    (opcode, owner, name, descriptor, isInterface, continuation) ->
+                        continuation.visitMethodInsn(
+                            name.equals("packagePrivate") ? Opcodes.INVOKESPECIAL : opcode,
+                            name.equals("packagePrivate")
+                                ? binaryName(
+                                        CrossPackageInvokeSuperToPackagePrivateMethodTest.class)
+                                    + "$B"
+                                : owner,
+                            name,
+                            descriptor,
+                            isInterface))
+                .transform());
+  }
+
+  private void inspectRunResult(R8TestRunResult runResult) {
+    if (parameters.isCfRuntime()
+        || parameters.getRuntime().asDex().getVm().getVersion().isDalvik()) {
+      runResult.assertSuccessWithOutputLines("A", "B", "A", "C", "D", "C");
+    } else {
+      runResult.assertSuccessWithOutputLines("A", "B", "A", "C", "D", "B", "A");
+      if (parameters.getRuntime().asDex().getVm().getVersion().isOlderThanOrEqual(Version.V7_0_0)) {
+        runResult.assertStderrMatches(
+            allOf(
+                containsString("Before Android 4.1, method"),
+                containsString("would have incorrectly overridden the package-private method")));
+      }
+    }
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      new A().packagePrivate();
+      new B().packagePrivate();
+      new CrossPackageInvokeSuperToPackagePrivateMethodTestClasses.C().runPackagePrivate();
+      new D().packagePrivate();
+    }
+  }
+
+  @NeverClassInline
+  @NeverMerge
+  public static class A {
+
+    @NeverInline
+    void packagePrivate() {
+      System.out.println("A");
+    }
+  }
+
+  @NeverClassInline
+  @NeverMerge
+  public static class B extends A {
+
+    @NeverInline
+    void packagePrivate() {
+      System.out.println("B");
+      super.packagePrivate();
+    }
+  }
+
+  @NeverClassInline
+  public static class D extends CrossPackageInvokeSuperToPackagePrivateMethodTestClasses.C {
+
+    @NeverInline
+    void packagePrivate() {
+      System.out.println("D");
+      /*super.*/packagePrivate();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageTest.java
new file mode 100644
index 0000000..6d468f4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageTest.java
@@ -0,0 +1,207 @@
+// 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.repackage;
+
+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.Assume.assumeTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.repackage.testclasses.repackagetest.AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassDirect;
+import com.android.tools.r8.repackage.testclasses.repackagetest.AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassIndirect;
+import com.android.tools.r8.repackage.testclasses.repackagetest.AccessPackagePrivateKeptMethodOnReachableClassDirect;
+import com.android.tools.r8.repackage.testclasses.repackagetest.AccessPackagePrivateKeptMethodOnReachableClassIndirect;
+import com.android.tools.r8.repackage.testclasses.repackagetest.AccessPackagePrivateMethodOnKeptClassAllowRenamingDirect;
+import com.android.tools.r8.repackage.testclasses.repackagetest.AccessPackagePrivateMethodOnKeptClassAllowRenamingIndirect;
+import com.android.tools.r8.repackage.testclasses.repackagetest.AccessPackagePrivateMethodOnKeptClassDirect;
+import com.android.tools.r8.repackage.testclasses.repackagetest.AccessPackagePrivateMethodOnKeptClassIndirect;
+import com.android.tools.r8.repackage.testclasses.repackagetest.AccessPackagePrivateMethodOnReachableClassDirect;
+import com.android.tools.r8.repackage.testclasses.repackagetest.AccessPackagePrivateMethodOnReachableClassIndirect;
+import com.android.tools.r8.repackage.testclasses.repackagetest.AccessPublicKeptMethodAllowRenamingOnReachableClass;
+import com.android.tools.r8.repackage.testclasses.repackagetest.AccessPublicKeptMethodOnReachableClass;
+import com.android.tools.r8.repackage.testclasses.repackagetest.AccessPublicMethodOnKeptClass;
+import com.android.tools.r8.repackage.testclasses.repackagetest.AccessPublicMethodOnKeptClassAllowRenaming;
+import com.android.tools.r8.repackage.testclasses.repackagetest.AccessPublicMethodOnReachableClass;
+import com.android.tools.r8.repackage.testclasses.repackagetest.KeptClass;
+import com.android.tools.r8.repackage.testclasses.repackagetest.KeptClassAllowRenaming;
+import com.android.tools.r8.repackage.testclasses.repackagetest.ReachableClassWithKeptMethod;
+import com.android.tools.r8.repackage.testclasses.repackagetest.ReachableClassWithKeptMethodAllowRenaming;
+import com.android.tools.r8.repackage.testclasses.repackagetest.TestClass;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+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 RepackageTest extends TestBase {
+
+  private static final String REPACKAGE_DIR = "foo";
+
+  private static final List<String> EXPECTED =
+      ImmutableList.of(
+          "KeptClass.publicMethod()",
+          "KeptClass.packagePrivateMethod()",
+          "KeptClass.packagePrivateMethod()",
+          "KeptClassAllowRenaming.publicMethod()",
+          "KeptClassAllowRenaming.packagePrivateMethod()",
+          "KeptClassAllowRenaming.packagePrivateMethod()",
+          "ReachableClassWithKeptMethod.publicMethod()",
+          "ReachableClassWithKeptMethod.packagePrivateMethod()",
+          "ReachableClassWithKeptMethod.packagePrivateMethod()",
+          "ReachableClassWithKeptMethodAllowRenaming.publicMethod()",
+          "ReachableClassWithKeptMethodAllowRenaming.packagePrivateMethod()",
+          "ReachableClassWithKeptMethodAllowRenaming.packagePrivateMethod()",
+          "ReachableClass.publicMethod()",
+          "ReachableClass.packagePrivateMethod()",
+          "ReachableClass.packagePrivateMethod()");
+
+  private final boolean allowAccessModification;
+  private final String flattenPackageHierarchyOrRepackageClasses;
+  private final TestParameters parameters;
+
+  @Parameters(name = "{1}, allow access modification: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(),
+        ImmutableList.of("flattenpackagehierarchy", "repackageclasses"),
+        getTestParameters().withAllRuntimesAndApiLevels().build());
+  }
+
+  public RepackageTest(
+      boolean allowAccessModification,
+      String flattenPackageHierarchyOrRepackageClasses,
+      TestParameters parameters) {
+    this.allowAccessModification = allowAccessModification;
+    this.flattenPackageHierarchyOrRepackageClasses = flattenPackageHierarchyOrRepackageClasses;
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addTestClasspath()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramFiles(ToolHelper.getClassFilesForTestPackage(TestClass.class.getPackage()))
+        .addKeepMainRule(TestClass.class)
+        .addKeepRules(
+            "-" + flattenPackageHierarchyOrRepackageClasses + " \"" + REPACKAGE_DIR + "\"",
+            "-keep class " + KeptClass.class.getTypeName(),
+            "-keep,allowobfuscation class " + KeptClassAllowRenaming.class.getTypeName(),
+            "-keepclassmembers class " + ReachableClassWithKeptMethod.class.getTypeName() + " {",
+            "  <methods>;",
+            "}",
+            "-keepclassmembers,allowobfuscation class "
+                + ReachableClassWithKeptMethodAllowRenaming.class.getTypeName()
+                + " {",
+            "  <methods>;",
+            "}")
+        .allowAccessModification(allowAccessModification)
+        .enableInliningAnnotations()
+        .enableMergeAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(this::inspect)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  private void inspect(CodeInspector inspector) {
+    forEachClass(
+        (clazz, eligibleForRepackaging) -> {
+          ClassSubject subject = inspector.clazz(clazz);
+          assertThat(subject, isPresent());
+          if (eligibleForRepackaging) {
+            assertEquals(
+                clazz.getTypeName(),
+                flattenPackageHierarchyOrRepackageClasses.equals("flattenpackagehierarchy")
+                    ? REPACKAGE_DIR + ".a"
+                    : REPACKAGE_DIR + "",
+                subject.getDexProgramClass().getType().getPackageName());
+          } else {
+            assertEquals(
+                clazz.getTypeName(),
+                RepackageTest.class.getPackage().getName() + ".testclasses.repackagetest",
+                subject.getDexProgramClass().getType().getPackageName());
+          }
+        });
+  }
+
+  /**
+   * For each test class, calls {@param consumer} with a boolean that indicates if the class is
+   * eligible for repackaging (or it needs to stay in its original package).
+   */
+  private void forEachClass(BiConsumer<Class<?>, Boolean> consumer) {
+    // TODO(b/165783399): This should be renamed to markAlwaysEligible() and always pass `true` to
+    //  the consumer, since these classes should be repackaged independent of
+    //  -allowaccessmodification.
+    Consumer<Class<?>> markShouldAlwaysBeEligible =
+        clazz -> consumer.accept(clazz, allowAccessModification);
+    Consumer<Class<?>> markEligibleWithAllowAccessModification =
+        clazz -> consumer.accept(clazz, allowAccessModification);
+
+    // 1) -keep class KeptClass
+
+    // 1.A) Accessing a public method on a kept class is OK.
+    markShouldAlwaysBeEligible.accept(AccessPublicMethodOnKeptClass.class);
+
+    // 1.B) Accessing a package-private method on a kept class requires -allowaccessmodification.
+    markEligibleWithAllowAccessModification.accept(
+        AccessPackagePrivateMethodOnKeptClassDirect.class);
+
+    // 1.C) Accessing a package-private method that accesses a package-private method on a kept
+    //      class requires -allowaccessmodification.
+    markEligibleWithAllowAccessModification.accept(
+        AccessPackagePrivateMethodOnKeptClassIndirect.class);
+
+    // 2) -keep,allowobfuscation class KeptClass
+
+    // 2.A, 2.B, 2.C) Accessing a method on a kept class that is allowed to be renamed is OK.
+    markShouldAlwaysBeEligible.accept(AccessPublicMethodOnKeptClassAllowRenaming.class);
+    markShouldAlwaysBeEligible.accept(
+        AccessPackagePrivateMethodOnKeptClassAllowRenamingDirect.class);
+    markShouldAlwaysBeEligible.accept(
+        AccessPackagePrivateMethodOnKeptClassAllowRenamingIndirect.class);
+
+    // 3) -keepclassmembers class ReachableClassWithKeptMethod { <methods>; }
+
+    // 3.A, 3.B, 3.C) Accessing a kept method is OK.
+    markShouldAlwaysBeEligible.accept(AccessPublicKeptMethodOnReachableClass.class);
+    markShouldAlwaysBeEligible.accept(AccessPackagePrivateKeptMethodOnReachableClassDirect.class);
+    markShouldAlwaysBeEligible.accept(AccessPackagePrivateKeptMethodOnReachableClassIndirect.class);
+
+    // 4) -keepclassmembers,allowobfuscation class ReachableClassWithKeptMethod { <methods>; }
+
+    // 4.A, 4.B, 4.C) Accessing a kept method is OK.
+    markShouldAlwaysBeEligible.accept(AccessPublicKeptMethodAllowRenamingOnReachableClass.class);
+    markShouldAlwaysBeEligible.accept(
+        AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassDirect.class);
+    markShouldAlwaysBeEligible.accept(
+        AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassIndirect.class);
+
+    // 5) No keep rule.
+
+    // 5.A, 5.B, 5.C) Accessing a non-kept method is OK.
+    markShouldAlwaysBeEligible.accept(AccessPublicMethodOnReachableClass.class);
+    markShouldAlwaysBeEligible.accept(AccessPackagePrivateMethodOnReachableClassDirect.class);
+    markShouldAlwaysBeEligible.accept(AccessPackagePrivateMethodOnReachableClassIndirect.class);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/CrossPackageInvokeSuperToPackagePrivateMethodTestClasses.java b/src/test/java/com/android/tools/r8/repackage/testclasses/CrossPackageInvokeSuperToPackagePrivateMethodTestClasses.java
new file mode 100644
index 0000000..fc9639a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/CrossPackageInvokeSuperToPackagePrivateMethodTestClasses.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.repackage.testclasses;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+import com.android.tools.r8.repackage.CrossPackageInvokeSuperToPackagePrivateMethodTest;
+
+public class CrossPackageInvokeSuperToPackagePrivateMethodTestClasses {
+
+  @NeverClassInline
+  @NeverMerge
+  public static class C extends CrossPackageInvokeSuperToPackagePrivateMethodTest.B {
+
+    @NeverInline
+    void packagePrivate() {
+      System.out.println("C");
+    }
+
+    public void runPackagePrivate() {
+      packagePrivate();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassDirect.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassDirect.java
new file mode 100644
index 0000000..a7af296
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassDirect.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 com.android.tools.r8.repackage.testclasses.repackagetest;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+
+@NeverMerge
+public class AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassDirect {
+
+  @NeverInline
+  static void test() {
+    ReachableClassWithKeptMethodAllowRenaming.packagePrivateMethod();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassIndirect.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassIndirect.java
new file mode 100644
index 0000000..9310128
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassIndirect.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 com.android.tools.r8.repackage.testclasses.repackagetest;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+
+@NeverMerge
+public class AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassIndirect {
+
+  @NeverInline
+  static void test() {
+    AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassDirect.test();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodOnReachableClassDirect.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodOnReachableClassDirect.java
new file mode 100644
index 0000000..1c159f0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodOnReachableClassDirect.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 com.android.tools.r8.repackage.testclasses.repackagetest;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+
+@NeverMerge
+public class AccessPackagePrivateKeptMethodOnReachableClassDirect {
+
+  @NeverInline
+  static void test() {
+    ReachableClassWithKeptMethod.packagePrivateMethod();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodOnReachableClassIndirect.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodOnReachableClassIndirect.java
new file mode 100644
index 0000000..043c43a
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodOnReachableClassIndirect.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 com.android.tools.r8.repackage.testclasses.repackagetest;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+
+@NeverMerge
+public class AccessPackagePrivateKeptMethodOnReachableClassIndirect {
+
+  @NeverInline
+  static void test() {
+    AccessPackagePrivateKeptMethodOnReachableClassDirect.test();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassAllowRenamingDirect.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassAllowRenamingDirect.java
new file mode 100644
index 0000000..45049a6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassAllowRenamingDirect.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 com.android.tools.r8.repackage.testclasses.repackagetest;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+
+@NeverMerge
+public class AccessPackagePrivateMethodOnKeptClassAllowRenamingDirect {
+
+  @NeverInline
+  static void test() {
+    KeptClassAllowRenaming.packagePrivateMethod();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassAllowRenamingIndirect.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassAllowRenamingIndirect.java
new file mode 100644
index 0000000..8ff327f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassAllowRenamingIndirect.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 com.android.tools.r8.repackage.testclasses.repackagetest;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+
+@NeverMerge
+public class AccessPackagePrivateMethodOnKeptClassAllowRenamingIndirect {
+
+  @NeverInline
+  static void test() {
+    AccessPackagePrivateMethodOnKeptClassAllowRenamingDirect.test();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassDirect.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassDirect.java
new file mode 100644
index 0000000..8ae7f71
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassDirect.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 com.android.tools.r8.repackage.testclasses.repackagetest;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+
+@NeverMerge
+public class AccessPackagePrivateMethodOnKeptClassDirect {
+
+  @NeverInline
+  static void test() {
+    KeptClass.packagePrivateMethod();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassIndirect.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassIndirect.java
new file mode 100644
index 0000000..6e769dd
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassIndirect.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 com.android.tools.r8.repackage.testclasses.repackagetest;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+
+@NeverMerge
+public class AccessPackagePrivateMethodOnKeptClassIndirect {
+
+  @NeverInline
+  static void test() {
+    AccessPackagePrivateMethodOnKeptClassDirect.test();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnReachableClassDirect.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnReachableClassDirect.java
new file mode 100644
index 0000000..8ef5364
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnReachableClassDirect.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 com.android.tools.r8.repackage.testclasses.repackagetest;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+
+@NeverMerge
+public class AccessPackagePrivateMethodOnReachableClassDirect {
+
+  @NeverInline
+  static void test() {
+    ReachableClass.packagePrivateMethod();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnReachableClassIndirect.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnReachableClassIndirect.java
new file mode 100644
index 0000000..63d0072
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnReachableClassIndirect.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 com.android.tools.r8.repackage.testclasses.repackagetest;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+
+@NeverMerge
+public class AccessPackagePrivateMethodOnReachableClassIndirect {
+
+  @NeverInline
+  static void test() {
+    AccessPackagePrivateMethodOnReachableClassDirect.test();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicKeptMethodAllowRenamingOnReachableClass.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicKeptMethodAllowRenamingOnReachableClass.java
new file mode 100644
index 0000000..460e3e1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicKeptMethodAllowRenamingOnReachableClass.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 com.android.tools.r8.repackage.testclasses.repackagetest;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+
+@NeverMerge
+public class AccessPublicKeptMethodAllowRenamingOnReachableClass {
+
+  @NeverInline
+  static void test() {
+    ReachableClassWithKeptMethodAllowRenaming.publicMethod();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicKeptMethodOnReachableClass.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicKeptMethodOnReachableClass.java
new file mode 100644
index 0000000..be42852
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicKeptMethodOnReachableClass.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 com.android.tools.r8.repackage.testclasses.repackagetest;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+
+@NeverMerge
+public class AccessPublicKeptMethodOnReachableClass {
+
+  @NeverInline
+  static void test() {
+    ReachableClassWithKeptMethod.publicMethod();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnKeptClass.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnKeptClass.java
new file mode 100644
index 0000000..54f999d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnKeptClass.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 com.android.tools.r8.repackage.testclasses.repackagetest;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+
+@NeverMerge
+public class AccessPublicMethodOnKeptClass {
+
+  @NeverInline
+  static void test() {
+    KeptClass.publicMethod();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnKeptClassAllowRenaming.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnKeptClassAllowRenaming.java
new file mode 100644
index 0000000..16acfb5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnKeptClassAllowRenaming.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 com.android.tools.r8.repackage.testclasses.repackagetest;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+
+@NeverMerge
+public class AccessPublicMethodOnKeptClassAllowRenaming {
+
+  @NeverInline
+  static void test() {
+    KeptClassAllowRenaming.publicMethod();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnReachableClass.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnReachableClass.java
new file mode 100644
index 0000000..4414fd6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnReachableClass.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 com.android.tools.r8.repackage.testclasses.repackagetest;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+
+@NeverMerge
+public class AccessPublicMethodOnReachableClass {
+
+  @NeverInline
+  static void test() {
+    ReachableClass.publicMethod();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/KeptClass.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/KeptClass.java
new file mode 100644
index 0000000..5f2632f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/KeptClass.java
@@ -0,0 +1,20 @@
+// 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.repackage.testclasses.repackagetest;
+
+import com.android.tools.r8.NeverInline;
+
+public class KeptClass {
+
+  @NeverInline
+  static void packagePrivateMethod() {
+    System.out.println("KeptClass.packagePrivateMethod()");
+  }
+
+  @NeverInline
+  public static void publicMethod() {
+    System.out.println("KeptClass.publicMethod()");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/KeptClassAllowRenaming.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/KeptClassAllowRenaming.java
new file mode 100644
index 0000000..f6ca601
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/KeptClassAllowRenaming.java
@@ -0,0 +1,20 @@
+// 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.repackage.testclasses.repackagetest;
+
+import com.android.tools.r8.NeverInline;
+
+public class KeptClassAllowRenaming {
+
+  @NeverInline
+  static void packagePrivateMethod() {
+    System.out.println("KeptClassAllowRenaming.packagePrivateMethod()");
+  }
+
+  @NeverInline
+  public static void publicMethod() {
+    System.out.println("KeptClassAllowRenaming.publicMethod()");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/ReachableClass.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/ReachableClass.java
new file mode 100644
index 0000000..57b2d65
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/ReachableClass.java
@@ -0,0 +1,22 @@
+// 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.repackage.testclasses.repackagetest;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NeverMerge;
+
+@NeverMerge
+public class ReachableClass {
+
+  @NeverInline
+  static void packagePrivateMethod() {
+    System.out.println("ReachableClass.packagePrivateMethod()");
+  }
+
+  @NeverInline
+  public static void publicMethod() {
+    System.out.println("ReachableClass.publicMethod()");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/ReachableClassWithKeptMethod.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/ReachableClassWithKeptMethod.java
new file mode 100644
index 0000000..e408270
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/ReachableClassWithKeptMethod.java
@@ -0,0 +1,20 @@
+// 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.repackage.testclasses.repackagetest;
+
+import com.android.tools.r8.NeverInline;
+
+public class ReachableClassWithKeptMethod {
+
+  @NeverInline
+  static void packagePrivateMethod() {
+    System.out.println("ReachableClassWithKeptMethod.packagePrivateMethod()");
+  }
+
+  @NeverInline
+  public static void publicMethod() {
+    System.out.println("ReachableClassWithKeptMethod.publicMethod()");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/ReachableClassWithKeptMethodAllowRenaming.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/ReachableClassWithKeptMethodAllowRenaming.java
new file mode 100644
index 0000000..7555b9b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/ReachableClassWithKeptMethodAllowRenaming.java
@@ -0,0 +1,20 @@
+// 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.repackage.testclasses.repackagetest;
+
+import com.android.tools.r8.NeverInline;
+
+public class ReachableClassWithKeptMethodAllowRenaming {
+
+  @NeverInline
+  static void packagePrivateMethod() {
+    System.out.println("ReachableClassWithKeptMethodAllowRenaming.packagePrivateMethod()");
+  }
+
+  @NeverInline
+  public static void publicMethod() {
+    System.out.println("ReachableClassWithKeptMethodAllowRenaming.publicMethod()");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/TestClass.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/TestClass.java
new file mode 100644
index 0000000..51d3af0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/TestClass.java
@@ -0,0 +1,61 @@
+// 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.repackage.testclasses.repackagetest;
+
+public class TestClass {
+
+  public static void main(String[] args) {
+    testAccessesToMethodOnKeptClass();
+    testAccessesToMethodOnKeptClassAllowRenaming();
+    testAccessesToKeptMethodOnReachableClass();
+    testAccessesToKeptMethodAllowRenamingOnReachableClass();
+    testAccessesToMethodOnReachableClass();
+  }
+
+  static void testAccessesToMethodOnKeptClass() {
+    // 1) public method ingoing access
+    AccessPublicMethodOnKeptClass.test();
+
+    // 2) package-private method ingoing access
+    AccessPackagePrivateMethodOnKeptClassDirect.test();
+    AccessPackagePrivateMethodOnKeptClassIndirect.test();
+  }
+
+  static void testAccessesToMethodOnKeptClassAllowRenaming() {
+    // 1) public method ingoing access
+    AccessPublicMethodOnKeptClassAllowRenaming.test();
+
+    // 2) package-private method ingoing access
+    AccessPackagePrivateMethodOnKeptClassAllowRenamingDirect.test();
+    AccessPackagePrivateMethodOnKeptClassAllowRenamingIndirect.test();
+  }
+
+  static void testAccessesToKeptMethodOnReachableClass() {
+    // 1) public method ingoing access
+    AccessPublicKeptMethodOnReachableClass.test();
+
+    // 2) package-private method ingoing access
+    AccessPackagePrivateKeptMethodOnReachableClassDirect.test();
+    AccessPackagePrivateKeptMethodOnReachableClassIndirect.test();
+  }
+
+  static void testAccessesToKeptMethodAllowRenamingOnReachableClass() {
+    // 1) public method ingoing access
+    AccessPublicKeptMethodAllowRenamingOnReachableClass.test();
+
+    // 2) package-private method ingoing access
+    AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassDirect.test();
+    AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassIndirect.test();
+  }
+
+  static void testAccessesToMethodOnReachableClass() {
+    // 1) public method ingoing access
+    AccessPublicMethodOnReachableClass.test();
+
+    // 2) package-private method ingoing access
+    AccessPackagePrivateMethodOnReachableClassDirect.test();
+    AccessPackagePrivateMethodOnReachableClassIndirect.test();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/DeterministicPrintUsagesTest.java b/src/test/java/com/android/tools/r8/shaking/DeterministicPrintUsagesTest.java
new file mode 100644
index 0000000..b9a56b1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/DeterministicPrintUsagesTest.java
@@ -0,0 +1,159 @@
+// 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.shaking;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.transformers.ClassTransformer;
+import com.android.tools.r8.utils.DescriptorUtils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(Parameterized.class)
+public class DeterministicPrintUsagesTest extends TestBase {
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public DeterministicPrintUsagesTest(TestParameters parameters) {
+    parameters.assertNoneRuntime();
+  }
+
+  public static class UsageConsumer implements StringConsumer {
+
+    List<String> strings = new ArrayList<>();
+
+    @Override
+    public void accept(String string, DiagnosticsHandler handler) {
+      strings.add(string);
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      strings = Collections.unmodifiableList(strings);
+    }
+  }
+
+  @Test
+  public void test() throws Exception {
+    List<String> names = getClassNames();
+    List<byte[]> classes = getClasses(names);
+    UsageConsumer usage1 = getUsageInfo(classes);
+    UsageConsumer usage2 = getUsageInfo(classes);
+    assertEquals(usage1.strings, usage2.strings);
+    String content = String.join("", usage1.strings);
+    for (String name : names) {
+      assertThat(content, containsString(name));
+    }
+    for (int i = 0; i < 10; i++) {
+      assertThat(content, containsString("int f" + i));
+      assertThat(content, containsString("void m" + i + "()"));
+    }
+  }
+
+  private List<String> getClassNames() {
+    List<String> descriptors = new ArrayList<>();
+    for (int i = 0; i < 10; i++) {
+      descriptors.add("a.A" + i);
+    }
+    return descriptors;
+  }
+
+  private List<byte[]> getClasses(List<String> names) throws Exception {
+    List<byte[]> classes = new ArrayList<>();
+    for (String name : names) {
+      classes.add(
+          transformer(A.class)
+              .setClassDescriptor(DescriptorUtils.javaTypeToDescriptor(name))
+              .transform());
+    }
+    return classes;
+  }
+
+  private UsageConsumer getUsageInfo(List<byte[]> classes) throws Exception {
+    UsageConsumer consumer = new UsageConsumer();
+    testForR8(Backend.CF)
+        .addProgramClassFileData(classes)
+        .addProgramClassFileData(
+            transformer(TestClass.class)
+                .addClassTransformer(
+                    new ClassTransformer() {
+
+                      @Override
+                      public FieldVisitor visitField(
+                          int access,
+                          String name,
+                          String descriptor,
+                          String signature,
+                          Object value) {
+                        assertEquals("f", name);
+                        for (int i = 0; i < 10; i++) {
+                          FieldVisitor fv =
+                              super.visitField(access, name + i, descriptor, signature, value);
+                          fv.visitEnd();
+                        }
+                        return null;
+                      }
+
+                      @Override
+                      public MethodVisitor visitMethod(
+                          int access,
+                          String name,
+                          String descriptor,
+                          String signature,
+                          String[] exceptions) {
+                        if (name.equals("m")) {
+                          for (int i = 0; i < 10; i++) {
+                            MethodVisitor mv =
+                                super.visitMethod(
+                                    access, name + i, descriptor, signature, exceptions);
+                            mv.visitCode();
+                            mv.visitInsn(Opcodes.RETURN);
+                            mv.visitEnd();
+                          }
+                          return null;
+                        }
+                        return super.visitMethod(access, name, descriptor, signature, exceptions);
+                      }
+                    })
+                .transform())
+        .addKeepMainRule(TestClass.class)
+        .apply(b -> b.getBuilder().setProguardUsageConsumer(consumer))
+        .compile()
+        .assertNoMessages();
+    return consumer;
+  }
+
+  // Repeated with a transformer.
+  static class A {}
+
+  static class TestClass {
+
+    // Repeated with a transformer.
+    static int f;
+
+    // Repeated with a transformer.
+    static void m() {}
+
+    public static void main(String[] args) {
+      System.out.println("Hello, world");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/attributes/KeepInnerClassesEnclosingMethodAnnotationsTest.java b/src/test/java/com/android/tools/r8/shaking/attributes/KeepInnerClassesEnclosingMethodAnnotationsTest.java
index 87c3303..83c55fd 100644
--- a/src/test/java/com/android/tools/r8/shaking/attributes/KeepInnerClassesEnclosingMethodAnnotationsTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/attributes/KeepInnerClassesEnclosingMethodAnnotationsTest.java
@@ -12,8 +12,8 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestRunResult;
 import com.android.tools.r8.shaking.attributes.testclasses.Outer;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -69,7 +69,7 @@
     final ClassSubject outer;
     final ClassSubject inner;
 
-    TestResult(TestRunResult result) throws Throwable {
+    TestResult(SingleTestRunResult<?> result) throws Throwable {
       this.stdout = result.getStdOut();
       this.inspector = result.inspector();
       this.outer = inspector.clazz(Outer.class);
diff --git a/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java b/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java
index f4884c3..ad6c793 100644
--- a/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java
+++ b/src/test/java/com/android/tools/r8/smali/CatchSuccessorFallthroughTest.java
@@ -79,7 +79,7 @@
 
     ProgramMethod method = getProgramMethod(originalApplication, methodSig);
     // Get the IR pre-optimization.
-    IRCode code = method.buildIR(AppView.createForD8(new AppInfo(application)));
+    IRCode code = method.buildIR(AppView.createForD8(AppInfo.createInitialAppInfo(application)));
 
     // Find the exit block and assert that the value is a phi merging the exceptional edge
     // with the normal edge.
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java
index d4f8b5b..299a6c0 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundMethodSubject.java
@@ -62,7 +62,8 @@
   @Override
   public IRCode buildIR() {
     assert codeInspector.application.options.programConsumer != null;
-    return getProgramMethod().buildIR(AppView.createForD8(new AppInfo(codeInspector.application)));
+    return getProgramMethod()
+        .buildIR(AppView.createForD8(AppInfo.createInitialAppInfo(codeInspector.application)));
   }
 
   @Override
diff --git a/third_party/youtube/youtube.android_15.33.tar.gz.sha1 b/third_party/youtube/youtube.android_15.33.tar.gz.sha1
new file mode 100644
index 0000000..3ce1c4a
--- /dev/null
+++ b/third_party/youtube/youtube.android_15.33.tar.gz.sha1
@@ -0,0 +1 @@
+cdff350c62bf8c72d97be51590e90ad9105b5499
\ No newline at end of file
diff --git a/tools/asmifier.py b/tools/asmifier.py
index c2fbdc0..5290f0a 100755
--- a/tools/asmifier.py
+++ b/tools/asmifier.py
@@ -10,7 +10,7 @@
 import sys
 import utils
 
-ASM_VERSION = '7.2'
+ASM_VERSION = '8.0'
 ASM_JAR = 'asm-' + ASM_VERSION + '.jar'
 ASM_UTIL_JAR = 'asm-util-' + ASM_VERSION + '.jar'
 
diff --git a/tools/download_all_benchmark_dependencies.py b/tools/download_all_benchmark_dependencies.py
index 95b92ed..9d6b082 100755
--- a/tools/download_all_benchmark_dependencies.py
+++ b/tools/download_all_benchmark_dependencies.py
@@ -9,6 +9,7 @@
 import sys
 import utils
 import os
+import retrace_benchmark
 
 BUILD_TARGETS = ['downloadDeps', 'downloadAndroidCts', 'downloadDx']
 
@@ -26,8 +27,7 @@
   utils.DownloadFromGoogleCloudStorage(utils.ANDROID_SDK + '.tar.gz.sha1',
                                        bucket='r8-deps-internal',
                                        auth=True)
-  utils.DownloadFromGoogleCloudStorage(
-      os.path.join(utils.THIRD_PARTY, 'retrace_benchmark') + '.tar.gz.sha1')
+  retrace_benchmark.download_benchmarks()
 
 if __name__ == '__main__':
   sys.exit(Main())
diff --git a/tools/r8_release.py b/tools/r8_release.py
index 3641f0c..e813756 100755
--- a/tools/r8_release.py
+++ b/tools/r8_release.py
@@ -843,7 +843,7 @@
       or args.maven
       or (args.studio and not args.no_sync)
       or (args.desugar_library and not args.dry_run)):
-    utils.check_prodacces()
+    utils.check_gcert()
 
   if args.google3:
     targets_to_run.append(prepare_google3(args))
diff --git a/tools/retrace_benchmark.py b/tools/retrace_benchmark.py
index a819ea5..e29bb86 100755
--- a/tools/retrace_benchmark.py
+++ b/tools/retrace_benchmark.py
@@ -36,11 +36,20 @@
                     help='The retracer to use',
                     choices=RETRACERS,
                     required=True)
+  parser.add_argument('--download-benchmarks',
+                      help='Download retrace benchmarks',
+                      default=False,
+                      action='store_true')
   options = parser.parse_args(argv)
   return options
 
+def download_benchmarks():
+  utils.DownloadFromGoogleCloudStorage(
+      os.path.join(utils.THIRD_PARTY, 'retrace_benchmark') + '.tar.gz.sha1')
 
 def run_retrace(options, temp):
+  if options.download_benchmarks:
+    download_benchmarks()
   if options.retracer == 'r8':
     retracer_args = [
         '-cp', utils.R8LIB_JAR, 'com.android.tools.r8.retrace.Retrace']
diff --git a/tools/utils.py b/tools/utils.py
index 4a171ad..3aad6c4 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -336,8 +336,8 @@
   with tarfile.open(filename, 'r:gz') as tar:
     tar.extractall(path=dirname)
 
-def check_prodacces():
-  subprocess.check_call(['prodaccess'])
+def check_gcert():
+  subprocess.check_call(['gcert'])
 
 # Note that gcs is eventually consistent with regards to list operations.
 # This is not a problem in our case, but don't ever use this method
diff --git a/tools/youtube_data.py b/tools/youtube_data.py
index 828e04b..46b0b9d 100644
--- a/tools/youtube_data.py
+++ b/tools/youtube_data.py
@@ -34,6 +34,9 @@
 V15_09_BASE = os.path.join(BASE, 'youtube.android_15.09')
 V15_09_PREFIX = os.path.join(V15_09_BASE, 'YouTubeRelease')
 
+V15_33_BASE = os.path.join(BASE, 'youtube.android_15.33')
+V15_33_PREFIX = os.path.join(V15_33_BASE, 'YouTubeRelease')
+
 # NOTE: we always use android.jar for SDK v25, later we might want to revise it
 #       to use proper android.jar version for each of youtube version separately.
 ANDROID_JAR = utils.get_android_jar(25)
@@ -241,4 +244,34 @@
       'min-api' : ANDROID_L_API,
     }
   },
+  '15.33': {
+    'dex' : {
+      'inputs': [os.path.join(V15_33_BASE, 'YouTubeRelease_unsigned.apk')],
+      'pgmap': '%s_proguard.map' % V15_33_PREFIX,
+      'libraries' : [ANDROID_JAR],
+      'min-api' : ANDROID_L_API,
+    },
+    'deploy' : {
+      # When -injars and -libraryjars are used for specifying inputs library
+      # sanitization is on by default. For this version of YouTube -injars and
+      # -libraryjars are not used, but library sanitization is still required.
+      'sanitize_libraries': True,
+      'inputs': ['%s_deploy.jar' % V15_33_PREFIX],
+      'libraries' : [os.path.join(V15_33_BASE, 'legacy_YouTubeRelease_combined_library_jars.jar')],
+      'pgconf': [
+          '%s_proguard.config' % V15_33_PREFIX,
+          '%s_proguard_missing_classes.config' % V15_33_PREFIX,
+          '%s/proguardsettings/YouTubeRelease_proguard.config' % utils.THIRD_PARTY],
+      'maindexrules' : [
+          os.path.join(V15_33_BASE, 'mainDexClasses.rules'),
+          os.path.join(V15_33_BASE, 'main-dex-classes-release-optimized.pgcfg'),
+          os.path.join(V15_33_BASE, 'main_dex_YouTubeRelease_proguard.cfg')],
+      'min-api' : ANDROID_H_MR2_API,
+    },
+    'proguarded' : {
+      'inputs': ['%s_proguard.jar' % V15_33_PREFIX],
+      'pgmap': '%s_proguard.map' % V15_33_PREFIX,
+      'min-api' : ANDROID_L_API,
+    }
+  },
 }