Merge commit 'b97d5f0c11ca77af6b8e6b0fef52a07b7367e8dd' into dev-release
diff --git a/src/main/java/com/android/tools/r8/ClassFileConsumer.java b/src/main/java/com/android/tools/r8/ClassFileConsumer.java
index 18f68ba..8c52405 100644
--- a/src/main/java/com/android/tools/r8/ClassFileConsumer.java
+++ b/src/main/java/com/android/tools/r8/ClassFileConsumer.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.utils.OutputBuilder;
 import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.io.Closer;
+import java.io.BufferedOutputStream;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.OpenOption;
@@ -148,7 +149,9 @@
       OpenOption[] options =
           new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING};
       try (Closer closer = Closer.create()) {
-        try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(archive, options))) {
+        try (ZipOutputStream out =
+            new ZipOutputStream(
+                new BufferedOutputStream(Files.newOutputStream(archive, options)))) {
           ZipUtils.writeResourcesToZip(resources, dataResources, closer, out);
         }
       }
diff --git a/src/main/java/com/android/tools/r8/D8.java b/src/main/java/com/android/tools/r8/D8.java
index 8def7ae..2e73927 100644
--- a/src/main/java/com/android/tools/r8/D8.java
+++ b/src/main/java/com/android/tools/r8/D8.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.InitClassLens;
+import com.android.tools.r8.graph.LazyLoadedDexApplication;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.analysis.ClassInitializerAssertionEnablingAnalysis;
 import com.android.tools.r8.inspector.internal.InspectorImpl;
@@ -35,6 +36,7 @@
 import com.android.tools.r8.utils.CfgPrinter;
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.InternalOptions.DesugarState;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
@@ -158,6 +160,16 @@
         });
   }
 
+  private static AppView<AppInfo> readApp(
+      AndroidApp inputApp, InternalOptions options, ExecutorService executor, Timing timing)
+      throws IOException {
+    PrefixRewritingMapper rewritePrefix =
+        options.desugaredLibraryConfiguration.createPrefixRewritingMapper(options);
+    LazyLoadedDexApplication app = new ApplicationReader(inputApp, options, timing).read(executor);
+    AppInfo appInfo = AppInfo.createInitialAppInfo(app);
+    return AppView.createForD8(appInfo, rewritePrefix);
+  }
+
   private static void run(AndroidApp inputApp, InternalOptions options, ExecutorService executor)
       throws IOException {
     Timing timing = Timing.create("D8", options);
@@ -165,10 +177,7 @@
       // Disable global optimizations.
       options.disableGlobalOptimizations();
 
-      DexApplication app = new ApplicationReader(inputApp, options, timing).read(executor);
-      PrefixRewritingMapper rewritePrefix =
-          options.desugaredLibraryConfiguration.createPrefixRewritingMapper(options);
-      AppInfo appInfo = AppInfo.createInitialAppInfo(app);
+      AppView<AppInfo> appView = readApp(inputApp, options, executor, timing);
 
       final CfgPrinter printer = options.printCfg ? new CfgPrinter() : null;
 
@@ -177,9 +186,9 @@
         // enabling code.
         ClassInitializerAssertionEnablingAnalysis analysis =
             new ClassInitializerAssertionEnablingAnalysis(
-                appInfo.dexItemFactory(), OptimizationFeedbackSimple.getInstance());
+                appView.dexItemFactory(), OptimizationFeedbackSimple.getInstance());
         ThreadUtils.processItems(
-            appInfo.classes(),
+            appView.appInfo().classes(),
             clazz -> {
               ProgramMethod classInitializer = clazz.getProgramClassInitializer();
               if (classInitializer != null) {
@@ -189,14 +198,11 @@
             executor);
       }
 
-      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);
+      new IRConverter(appView, timing, printer).convert(appView, executor);
 
       if (options.printCfg) {
         if (options.printCfgFile == null || options.printCfgFile.isEmpty()) {
@@ -223,7 +229,7 @@
       // if there were class file inputs.
       boolean hasClassResources = false;
       boolean hasDexResources = false;
-      for (DexProgramClass dexProgramClass : app.classes()) {
+      for (DexProgramClass dexProgramClass : appView.appInfo().classes()) {
         if (dexProgramClass.originatesFromClassResource()) {
           hasClassResources = true;
           if (hasDexResources) {
@@ -237,26 +243,26 @@
         }
       }
       Marker marker = options.getMarker(Tool.D8);
-      Set<Marker> markers = new HashSet<>(app.dexItemFactory.extractMarkers());
-      if (marker != null && hasClassResources) {
+      Set<Marker> markers = new HashSet<>(appView.dexItemFactory().extractMarkers());
+      // TODO(b/166617364): Don't add an additional marker when desugaring is turned off.
+      if (hasClassResources && (options.desugarState != DesugarState.OFF || markers.isEmpty())) {
         markers.add(marker);
       }
       Marker.checkCompatibleDesugaredLibrary(markers, options.reporter);
 
-      InspectorImpl.runInspections(options.outputInspections, app);
+      InspectorImpl.runInspections(options.outputInspections, appView.appInfo().classes());
       if (options.isGeneratingClassFiles()) {
         new CfApplicationWriter(
-                app,
                 appView,
-                options,
                 marker,
                 GraphLens.getIdentityLens(),
-                NamingLens.getIdentityLens(),
+                appView.rewritePrefix.isRewriting()
+                    ? PrefixRewritingNamingLens.createPrefixRewritingNamingLens(appView)
+                    : NamingLens.getIdentityLens(),
                 null)
             .write(options.getClassFileConsumer());
       } else {
         NamingLens namingLens;
-        DexApplication finalApp = app;
         if (!hasDexResources || !hasClassResources || !appView.rewritePrefix.isRewriting()) {
           // All inputs are either dex or cf, or there is nothing to rewrite.
           namingLens =
@@ -271,11 +277,14 @@
           // desugared library only on cf inputs. We cannot easily rewrite part of the program
           // without iterating again the IR. We fall-back to writing one app with rewriting and
           // merging it with the other app in rewriteNonDexInputs.
-          finalApp = rewriteNonDexInputs(appView, inputApp, options, executor, timing, app);
+          DexApplication app =
+              rewriteNonDexInputs(
+                  appView, inputApp, options, executor, timing, appView.appInfo().app());
+          appView.setAppInfo(new AppInfo(app, appView.appInfo().getSyntheticItems().commit(app)));
           namingLens = NamingLens.getIdentityLens();
         }
         new ApplicationWriter(
-                finalApp,
+                appView.appInfo().app(),
                 appView,
                 options,
                 marker == null ? null : ImmutableList.copyOf(markers),
@@ -353,17 +362,12 @@
     return finalDexApp.build();
   }
 
-  static DexApplication optimize(
-      DexApplication application,
-      AppInfo appInfo,
-      InternalOptions options,
-      Timing timing,
-      ExecutorService executor)
+  static void optimize(
+      AppView<AppInfo> appView, InternalOptions options, Timing timing, ExecutorService executor)
       throws IOException, ExecutionException {
     final CfgPrinter printer = options.printCfg ? new CfgPrinter() : null;
 
-    IRConverter converter = new IRConverter(appInfo, timing, printer);
-    application = converter.convert(application, executor);
+    new IRConverter(appView, timing, printer).convert(appView, executor);
 
     if (options.printCfg) {
       if (options.printCfgFile == null || options.printCfgFile.isEmpty()) {
@@ -376,7 +380,6 @@
         }
       }
     }
-    return application;
   }
 
   static class ConvertedCfFiles implements DexIndexedConsumer, ProgramResourceProvider {
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index d8fc991..cb00bb7 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -200,6 +200,9 @@
 
     @Override
     void validate() {
+      if (isPrintHelp()) {
+        return;
+      }
       Reporter reporter = getReporter();
       if (getProgramConsumer() instanceof ClassFileConsumer) {
         reporter.warning("Compiling to Java class files with D8 is not officially supported");
diff --git a/src/main/java/com/android/tools/r8/DexFileMergerHelper.java b/src/main/java/com/android/tools/r8/DexFileMergerHelper.java
index 8e6c389..49ab77b 100644
--- a/src/main/java/com/android/tools/r8/DexFileMergerHelper.java
+++ b/src/main/java/com/android/tools/r8/DexFileMergerHelper.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.dex.ApplicationWriter;
 import com.android.tools.r8.dex.Marker;
 import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.GraphLens;
@@ -90,15 +91,15 @@
                     null,
                     executor,
                     new DexFileMergerHelper(inputOrdering)::keepFirstProgramClassConflictResolver);
-        AppInfo appInfo = AppInfo.createInitialAppInfo(app);
-        app = D8.optimize(app, appInfo, options, timing, executor);
+        AppView<AppInfo> appView = AppView.createForD8(AppInfo.createInitialAppInfo(app));
+        D8.optimize(appView, options, timing, executor);
 
-        List<Marker> markers = app.dexItemFactory.extractMarkers();
+        List<Marker> markers = appView.dexItemFactory().extractMarkers();
 
         assert !options.hasMethodsFilter();
         ApplicationWriter writer =
             new ApplicationWriter(
-                app,
+                appView.appInfo().app(),
                 null,
                 options,
                 markers,
diff --git a/src/main/java/com/android/tools/r8/DexFilePerClassFileConsumer.java b/src/main/java/com/android/tools/r8/DexFilePerClassFileConsumer.java
index 2409847..364f2bf 100644
--- a/src/main/java/com/android/tools/r8/DexFilePerClassFileConsumer.java
+++ b/src/main/java/com/android/tools/r8/DexFilePerClassFileConsumer.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Closer;
+import java.io.BufferedOutputStream;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.OpenOption;
@@ -215,7 +216,9 @@
       OpenOption[] options =
           new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING};
       try (Closer closer = Closer.create()) {
-        try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(archive, options))) {
+        try (ZipOutputStream out =
+            new ZipOutputStream(
+                new BufferedOutputStream(Files.newOutputStream(archive, options)))) {
           for (ProgramResource resource : resources) {
             String primaryClassDescriptor = primaryClassDescriptors.get(resource);
             String entryName = getDexFileName(primaryClassDescriptor);
diff --git a/src/main/java/com/android/tools/r8/DexIndexedConsumer.java b/src/main/java/com/android/tools/r8/DexIndexedConsumer.java
index 300588f..ec1f516 100644
--- a/src/main/java/com/android/tools/r8/DexIndexedConsumer.java
+++ b/src/main/java/com/android/tools/r8/DexIndexedConsumer.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.utils.ZipUtils;
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Closer;
+import java.io.BufferedOutputStream;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.OpenOption;
@@ -186,7 +187,9 @@
       OpenOption[] options =
           new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING};
       try (Closer closer = Closer.create()) {
-        try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(archive, options))) {
+        try (ZipOutputStream out =
+            new ZipOutputStream(
+                new BufferedOutputStream(Files.newOutputStream(archive, options)))) {
           for (int i = 0; i < resources.size(); i++) {
             ProgramResource resource = resources.get(i);
             String entryName = getDefaultDexFileName(i);
diff --git a/src/main/java/com/android/tools/r8/DexSplitterHelper.java b/src/main/java/com/android/tools/r8/DexSplitterHelper.java
index b7ece81..7203f8e 100644
--- a/src/main/java/com/android/tools/r8/DexSplitterHelper.java
+++ b/src/main/java/com/android/tools/r8/DexSplitterHelper.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.dex.ApplicationWriter;
 import com.android.tools.r8.dex.Marker;
 import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.GraphLens;
@@ -87,7 +88,8 @@
 
         // Run d8 optimize to ensure jumbo strings are handled.
         AppInfo appInfo = AppInfo.createInitialAppInfo(featureApp);
-        featureApp = D8.optimize(featureApp, appInfo, options, timing, executor);
+        AppView<AppInfo> appView = AppView.createForD8(appInfo);
+        D8.optimize(appView, options, timing, executor);
         // We create a specific consumer for each split.
         Path outputDir = Paths.get(output).resolve(entry.getKey());
         if (!Files.exists(outputDir)) {
@@ -97,7 +99,7 @@
 
         try {
           new ApplicationWriter(
-                  featureApp,
+                  appView.appInfo().app(),
                   null,
                   options,
                   markers,
diff --git a/src/main/java/com/android/tools/r8/DiagnosticsHandler.java b/src/main/java/com/android/tools/r8/DiagnosticsHandler.java
index 666e358..de3f800 100644
--- a/src/main/java/com/android/tools/r8/DiagnosticsHandler.java
+++ b/src/main/java/com/android/tools/r8/DiagnosticsHandler.java
@@ -57,4 +57,19 @@
     }
     System.out.println(info.getDiagnosticMessage());
   }
+
+  /**
+   * Modify the level of a diagnostic.
+   *
+   * <p>This modification is allowed only for non-fatal compiler diagnostics.
+   *
+   * <p>Changing a non-error into an error will cause the compiler to exit with a <code>
+   * CompilationFailedException</code> at its next error check point.
+   *
+   * <p>Changing an error into a non-error will allow the compiler to continue compilation. Note
+   * that doing so could very well lead to an internal compiler error due to a broken invariant.
+   */
+  default DiagnosticsLevel modifyDiagnosticsLevel(DiagnosticsLevel level, Diagnostic diagnostic) {
+    return level;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/DiagnosticsLevel.java b/src/main/java/com/android/tools/r8/DiagnosticsLevel.java
new file mode 100644
index 0000000..9f9d26a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/DiagnosticsLevel.java
@@ -0,0 +1,12 @@
+// 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;
+
+/** Levels of diagnostics messages reported by the compiler. */
+@Keep
+public enum DiagnosticsLevel {
+  ERROR,
+  WARNING,
+  INFO
+}
diff --git a/src/main/java/com/android/tools/r8/GenerateLintFiles.java b/src/main/java/com/android/tools/r8/GenerateLintFiles.java
index 2bfba83..8144903 100644
--- a/src/main/java/com/android/tools/r8/GenerateLintFiles.java
+++ b/src/main/java/com/android/tools/r8/GenerateLintFiles.java
@@ -303,7 +303,6 @@
 
           addMethodsToHeaderJar(builder, clazz, methods);
         });
-    DexApplication app = builder.build();
 
     // Write a plain text file with the desugared APIs.
     desugaredApisSignatures.sort(Comparator.naturalOrder());
@@ -311,13 +310,11 @@
         lintFile(compilationApiLevel, minApiLevel, ".txt"), desugaredApisSignatures);
 
     // Write a header jar with the desugared APIs.
-    AppInfo appInfo = AppInfo.createInitialAppInfo(app);
+    AppInfo appInfo = AppInfo.createInitialAppInfo(builder.build());
     AppView<?> appView = AppView.createForD8(appInfo);
     CfApplicationWriter writer =
         new CfApplicationWriter(
-            builder.build(),
             appView,
-            options,
             options.getMarker(Tool.L8),
             GraphLens.getIdentityLens(),
             NamingLens.getIdentityLens(),
diff --git a/src/main/java/com/android/tools/r8/L8.java b/src/main/java/com/android/tools/r8/L8.java
index 99a87d3..aae1486 100644
--- a/src/main/java/com/android/tools/r8/L8.java
+++ b/src/main/java/com/android/tools/r8/L8.java
@@ -119,31 +119,19 @@
       // on it.
       options.enableLoadStoreOptimization = false;
 
-      LazyLoadedDexApplication lazyApp =
-          new ApplicationReader(inputApp, options, timing).read(executor);
-
-      PrefixRewritingMapper rewritePrefix =
-          options.desugaredLibraryConfiguration.createPrefixRewritingMapper(options);
-
-      DexApplication app = new L8TreePruner(options).prune(lazyApp, rewritePrefix);
-      AppInfo appInfo = AppInfo.createInitialAppInfo(app);
-
-      AppView<?> appView = AppView.createForL8(appInfo, rewritePrefix);
-      IRConverter converter = new IRConverter(appView, timing);
+      AppView<AppInfo> appView = readApp(inputApp, options, executor, timing);
 
       if (!options.testing.disableL8AnnotationRemoval) {
         AnnotationRemover.clearAnnotations(appView);
       }
-      app = converter.convert(app, executor);
-      assert appView.appInfo() == appInfo;
+
+      new IRConverter(appView, timing).convert(appView, executor);
 
       NamingLens namingLens = PrefixRewritingNamingLens.createPrefixRewritingNamingLens(appView);
-      new GenericSignatureRewriter(appView, namingLens).run(appInfo.classes(), executor);
+      new GenericSignatureRewriter(appView, namingLens).run(appView.appInfo().classes(), executor);
 
       new CfApplicationWriter(
-              app,
               appView,
-              options,
               options.getMarker(Tool.L8),
               GraphLens.getIdentityLens(),
               namingLens,
@@ -161,6 +149,19 @@
     }
   }
 
+  private static AppView<AppInfo> readApp(
+      AndroidApp inputApp, InternalOptions options, ExecutorService executor, Timing timing)
+      throws IOException {
+    LazyLoadedDexApplication lazyApp =
+        new ApplicationReader(inputApp, options, timing).read(executor);
+
+    PrefixRewritingMapper rewritePrefix =
+        options.desugaredLibraryConfiguration.createPrefixRewritingMapper(options);
+
+    DexApplication app = new L8TreePruner(options).prune(lazyApp, rewritePrefix);
+    return AppView.createForL8(AppInfo.createInitialAppInfo(app), rewritePrefix);
+  }
+
   private static void run(String[] args) throws CompilationFailedException {
     L8Command command = L8Command.parse(args, CommandLineOrigin.INSTANCE).build();
     if (command.isPrintHelp()) {
diff --git a/src/main/java/com/android/tools/r8/L8Command.java b/src/main/java/com/android/tools/r8/L8Command.java
index cae584b..a2cf589 100644
--- a/src/main/java/com/android/tools/r8/L8Command.java
+++ b/src/main/java/com/android/tools/r8/L8Command.java
@@ -32,7 +32,7 @@
 @Keep
 public final class L8Command extends BaseCompilerCommand {
 
-  static final String USAGE_MESSAGE = R8CommandParser.USAGE_MESSAGE;
+  static final String USAGE_MESSAGE = L8CommandParser.USAGE_MESSAGE;
 
   private final D8Command d8Command;
   private final R8Command r8Command;
@@ -253,6 +253,9 @@
 
     @Override
     void validate() {
+      if (isPrintHelp()) {
+        return;
+      }
       Reporter reporter = getReporter();
       if (!hasDesugaredLibraryConfiguration()) {
         reporter.error("L8 requires a desugared library configuration");
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 38e1556..9d6b199 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -40,6 +40,7 @@
 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.horizontalclassmerging.HorizontalClassMergerGraphLens;
 import com.android.tools.r8.inspector.internal.InspectorImpl;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.desugar.BackportedMethodRewriter;
@@ -209,7 +210,6 @@
 
   static void writeApplication(
       ExecutorService executorService,
-      DexApplication application,
       AppView<?> appView,
       GraphLens graphLens,
       InitClassLens initClassLens,
@@ -217,7 +217,7 @@
       InternalOptions options,
       ProguardMapSupplier proguardMapSupplier)
       throws ExecutionException {
-    InspectorImpl.runInspections(options.outputInspections, application);
+    InspectorImpl.runInspections(options.outputInspections, appView.appInfo().classes());
     try {
       Marker marker = options.getMarker(Tool.R8);
       assert marker != null;
@@ -227,11 +227,15 @@
       markers.remove(marker);
       if (options.isGeneratingClassFiles()) {
         new CfApplicationWriter(
-                application, appView, options, marker, graphLens, namingLens, proguardMapSupplier)
+                appView,
+                marker,
+                graphLens,
+                namingLens,
+                proguardMapSupplier)
             .write(options.getClassFileConsumer());
       } else {
         new ApplicationWriter(
-                application,
+                appView.appInfo().app(),
                 appView,
                 options,
                 // Ensure that the marker for this compilation is the first in the list.
@@ -354,6 +358,7 @@
             }
           }
         }
+        options.reporter.failIfPendingErrors();
 
         // Add synthesized -assumenosideeffects from min api if relevant.
         if (options.isGeneratingDex()) {
@@ -547,7 +552,11 @@
           timing.begin("HorizontalClassMerger");
           HorizontalClassMerger merger =
               new HorizontalClassMerger(appViewWithLiveness, mainDexClasses);
-          merger.run();
+          HorizontalClassMergerGraphLens lens = merger.run();
+          if (lens != null) {
+            appView.setHorizontallyMergedClasses(lens.getHorizontallyMergedClasses());
+            appView.rewriteWithLens(lens);
+          }
           timing.end();
         }
 
@@ -905,7 +914,6 @@
       // Generate the resulting application resources.
       writeApplication(
           executorService,
-          appView.appInfo().app(),
           appView,
           appView.graphLens(),
           appView.initClassLens(),
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index f2d182d..6efe395 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -407,6 +407,9 @@
 
     @Override
     void validate() {
+      if (isPrintHelp()) {
+        return;
+      }
       Reporter reporter = getReporter();
       if (getProgramConsumer() instanceof DexFilePerClassFileConsumer) {
         reporter.error("R8 does not support compiling to a single DEX file per Java class file");
diff --git a/src/main/java/com/android/tools/r8/dex/FileWriter.java b/src/main/java/com/android/tools/r8/dex/FileWriter.java
index 8d1eac7..da90a09 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -270,7 +270,8 @@
       return; // Class constructor is always OK.
     }
     if (method.accessFlags.isStatic()) {
-      if (!options.canUseDefaultAndStaticInterfaceMethods()) {
+      if (!options.canUseDefaultAndStaticInterfaceMethods()
+          && !options.testing.allowStaticInterfaceMethodsForPreNApiLevel) {
         throw options.reporter.fatalError(
             new StaticInterfaceMethodDiagnostic(new MethodPosition(method.method)));
       }
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 dd179c1..b4852a3 100644
--- a/src/main/java/com/android/tools/r8/dex/Marker.java
+++ b/src/main/java/com/android/tools/r8/dex/Marker.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.dex;
 
 import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.errors.DesugaredLibraryMismatchDiagnostic;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringDiagnostic;
@@ -97,11 +98,7 @@
     }
 
     if (desugaredLibraryIdentifiers.size() > 1) {
-      reporter.error(
-          new StringDiagnostic(
-              "The compilation is merging inputs with different desugared library desugaring "
-                  + desugaredLibraryIdentifiers
-                  + ", which may lead to unexpected runtime errors."));
+      reporter.error(new DesugaredLibraryMismatchDiagnostic(desugaredLibraryIdentifiers));
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/errors/DesugaredLibraryMismatchDiagnostic.java b/src/main/java/com/android/tools/r8/errors/DesugaredLibraryMismatchDiagnostic.java
new file mode 100644
index 0000000..ba9af51
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/DesugaredLibraryMismatchDiagnostic.java
@@ -0,0 +1,36 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.errors;
+
+import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+import java.util.Set;
+
+public class DesugaredLibraryMismatchDiagnostic implements Diagnostic {
+
+  private final Set<String> desugaredLibraryIdentifiers;
+
+  public DesugaredLibraryMismatchDiagnostic(Set<String> desugaredLibraryIdentifiers) {
+    this.desugaredLibraryIdentifiers = desugaredLibraryIdentifiers;
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return Origin.unknown();
+  }
+
+  @Override
+  public Position getPosition() {
+    return Position.UNKNOWN;
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return "The compilation is merging inputs with different desugared library desugaring "
+        + desugaredLibraryIdentifiers
+        + ", which may lead to unexpected runtime errors.";
+  }
+}
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 ccdfd2f..c6bc6b4 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -8,8 +8,10 @@
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
 import com.android.tools.r8.graph.analysis.InitializedClassesInInstanceMethodsAnalysis.InitializedClassesInInstanceMethods;
 import com.android.tools.r8.graph.classmerging.HorizontallyMergedLambdaClasses;
+import com.android.tools.r8.graph.classmerging.MergedClasses;
 import com.android.tools.r8.graph.classmerging.MergedClassesCollection;
 import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
+import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
 import com.android.tools.r8.ir.analysis.proto.GeneratedExtensionRegistryShrinker;
 import com.android.tools.r8.ir.analysis.proto.GeneratedMessageLiteBuilderShrinker;
 import com.android.tools.r8.ir.analysis.proto.GeneratedMessageLiteShrinker;
@@ -70,6 +72,7 @@
   private Predicate<DexType> classesEscapingIntoLibrary = Predicates.alwaysTrue();
   private InitializedClassesInInstanceMethods initializedClassesInInstanceMethods;
   private HorizontallyMergedLambdaClasses horizontallyMergedLambdaClasses;
+  private HorizontallyMergedClasses horizontallyMergedClasses;
   private VerticallyMergedClasses verticallyMergedClasses;
   private EnumValueInfoMapCollection unboxedEnums = EnumValueInfoMapCollection.empty();
   private Set<DexMethod> cfByteCodePassThrough = ImmutableSet.of();
@@ -398,32 +401,48 @@
   }
 
   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);
+    return MergedClasses.hasBeenMerged(horizontallyMergedClasses, clazz)
+        || MergedClasses.hasBeenMerged(horizontallyMergedLambdaClasses, clazz)
+        || MergedClasses.hasBeenMerged(verticallyMergedClasses, clazz);
   }
 
-  // Get the result of horizontal lambda class merging. Returns null if horizontal lambda class
-  // merging has not been run.
+  /**
+   * Get the result of horizontal lambda class merging. Returns null if horizontal lambda class
+   * merging has not been run.
+   */
   public HorizontallyMergedLambdaClasses horizontallyMergedLambdaClasses() {
     return horizontallyMergedLambdaClasses;
   }
 
   public void setHorizontallyMergedLambdaClasses(
       HorizontallyMergedLambdaClasses horizontallyMergedLambdaClasses) {
+    assert this.horizontallyMergedLambdaClasses == null;
     this.horizontallyMergedLambdaClasses = horizontallyMergedLambdaClasses;
   }
 
-  // Get the result of vertical class merging. Returns null if vertical class merging has not been
-  // run.
+  /**
+   * Get the result of horizontal class merging. Returns null if horizontal lambda class merging has
+   * not been run.
+   */
+  public HorizontallyMergedClasses horizontallyMergedClasses() {
+    return horizontallyMergedClasses;
+  }
+
+  public void setHorizontallyMergedClasses(HorizontallyMergedClasses horizontallyMergedClasses) {
+    assert this.horizontallyMergedClasses == null;
+    this.horizontallyMergedClasses = horizontallyMergedClasses;
+  }
+
+  /**
+   * Get the result of vertical class merging. Returns null if vertical class merging has not been
+   * run.
+   */
   public VerticallyMergedClasses verticallyMergedClasses() {
     return verticallyMergedClasses;
   }
 
   public void setVerticallyMergedClasses(VerticallyMergedClasses verticallyMergedClasses) {
+    assert this.verticallyMergedClasses == null;
     this.verticallyMergedClasses = verticallyMergedClasses;
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/AppliedGraphLens.java b/src/main/java/com/android/tools/r8/graph/AppliedGraphLens.java
index 90b940a..318b1bc 100644
--- a/src/main/java/com/android/tools/r8/graph/AppliedGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/AppliedGraphLens.java
@@ -117,12 +117,12 @@
 
   @Override
   public GraphLensLookupResult lookupMethod(DexMethod method, DexMethod context, Invoke.Type type) {
-    return new GraphLensLookupResult(method, type);
+    return GraphLens.getIdentityLens().lookupMethod(method, context, type);
   }
 
   @Override
-  public RewrittenPrototypeDescription lookupPrototypeChanges(DexMethod method) {
-    return RewrittenPrototypeDescription.none();
+  public RewrittenPrototypeDescription lookupPrototypeChangesForMethodDefinition(DexMethod method) {
+    return GraphLens.getIdentityLens().lookupPrototypeChangesForMethodDefinition(method);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 2cf3328..91a70a9 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -170,6 +170,7 @@
   public final DexString endsWithMethodName = createString("endsWith");
   public final DexString equalsMethodName = createString("equals");
   public final DexString hashCodeMethodName = createString("hashCode");
+  public final DexString identityHashCodeName = createString("identityHashCode");
   public final DexString equalsIgnoreCaseMethodName = createString("equalsIgnoreCase");
   public final DexString contentEqualsMethodName = createString("contentEquals");
   public final DexString indexOfMethodName = createString("indexOf");
@@ -225,6 +226,7 @@
   public final DexString fieldDescriptor = createString("Ljava/lang/reflect/Field;");
   public final DexString methodDescriptor = createString("Ljava/lang/reflect/Method;");
   public final DexString enumDescriptor = createString("Ljava/lang/Enum;");
+  public final DexString javaLangSystemDescriptor = createString("Ljava/lang/System;");
   public final DexString annotationDescriptor = createString("Ljava/lang/annotation/Annotation;");
   public final DexString objectsDescriptor = createString("Ljava/util/Objects;");
   public final DexString collectionsDescriptor = createString("Ljava/util/Collections;");
@@ -353,7 +355,7 @@
   public final DexType stringBuilderType = createStaticallyKnownType(stringBuilderDescriptor);
   public final DexType stringBufferType = createStaticallyKnownType(stringBufferDescriptor);
 
-  public final DexType javaLangSystemType = createStaticallyKnownType("Ljava/lang/System;");
+  public final DexType javaLangSystemType = createStaticallyKnownType(javaLangSystemDescriptor);
   public final DexType javaIoPrintStreamType = createStaticallyKnownType("Ljava/io/PrintStream;");
 
   public final DexType varHandleType = createStaticallyKnownType(varHandleDescriptor);
@@ -464,6 +466,7 @@
   public final ClassMethods classMethods = new ClassMethods();
   public final ConstructorMethods constructorMethods = new ConstructorMethods();
   public final EnumMembers enumMembers = new EnumMembers();
+  public final JavaLangSystemMethods javaLangSystemMethods = new JavaLangSystemMethods();
   public final NullPointerExceptionMethods npeMethods = new NullPointerExceptionMethods();
   public final IllegalArgumentExceptionMethods illegalArgumentExceptionMethods =
       new IllegalArgumentExceptionMethods();
@@ -1269,6 +1272,19 @@
     }
   }
 
+  public class JavaLangSystemMethods {
+    public final DexMethod identityHashCode;
+
+    private JavaLangSystemMethods() {
+      identityHashCode =
+          createMethod(
+              javaLangSystemDescriptor,
+              identityHashCodeName,
+              intDescriptor,
+              new DexString[] {objectDescriptor});
+    }
+  }
+
   public class EnumMembers {
 
     public final DexField nameField = createField(enumType, stringType, "name");
diff --git a/src/main/java/com/android/tools/r8/graph/DexMethod.java b/src/main/java/com/android/tools/r8/graph/DexMethod.java
index 8a50931..fce08d8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethod.java
@@ -31,6 +31,10 @@
     }
   }
 
+  public DexType getReturnType() {
+    return proto.returnType;
+  }
+
   @Override
   public <T> T apply(
       Function<DexType, T> classConsumer,
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 d803a42..fe85d28 100644
--- a/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DirectMappedDexApplication.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.DataResourceProvider;
 import com.android.tools.r8.graph.LazyLoadedDexApplication.AllClasses;
+import com.android.tools.r8.graph.classmerging.MergedClasses;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Timing;
@@ -137,7 +138,8 @@
         .allMatch(
             type ->
                 lens.lookupType(type) == type
-                    || appView.verticallyMergedClasses().hasBeenMergedIntoSubtype(type));
+                    || MergedClasses.hasBeenMerged(appView.verticallyMergedClasses(), type)
+                    || MergedClasses.hasBeenMerged(appView.horizontallyMergedClasses(), type));
     assert verifyCodeObjectsOwners();
     return true;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/EnumValueInfoMapCollection.java b/src/main/java/com/android/tools/r8/graph/EnumValueInfoMapCollection.java
index 4ee6be1..362ff7c 100644
--- a/src/main/java/com/android/tools/r8/graph/EnumValueInfoMapCollection.java
+++ b/src/main/java/com/android/tools/r8/graph/EnumValueInfoMapCollection.java
@@ -84,6 +84,10 @@
       this.map = map;
     }
 
+    public Set<DexField> enumValues() {
+      return map.keySet();
+    }
+
     public int size() {
       return map.size();
     }
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 07b3205..edce8d8 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLens.java
@@ -42,16 +42,21 @@
   /**
    * Result of a method lookup in a GraphLens.
    *
-   * <p>This provide the new target and the invoke type to use.
+   * <p>This provides the new target and invoke type to use, along with a description of the
+   * prototype changes that have been made to the target method and the corresponding required
+   * changes to the invoke arguments.
    */
   public static class GraphLensLookupResult {
 
     private final DexMethod method;
     private final Type type;
+    private final RewrittenPrototypeDescription prototypeChanges;
 
-    public GraphLensLookupResult(DexMethod method, Type type) {
+    public GraphLensLookupResult(
+        DexMethod method, Type type, RewrittenPrototypeDescription prototypeChanges) {
       this.method = method;
       this.type = type;
+      this.prototypeChanges = prototypeChanges;
     }
 
     public DexMethod getMethod() {
@@ -61,6 +66,10 @@
     public Type getType() {
       return type;
     }
+
+    public RewrittenPrototypeDescription getPrototypeChanges() {
+      return prototypeChanges;
+    }
   }
 
   public static class Builder {
@@ -189,7 +198,8 @@
   public abstract GraphLensLookupResult lookupMethod(
       DexMethod method, DexMethod context, Type type);
 
-  public abstract RewrittenPrototypeDescription lookupPrototypeChanges(DexMethod method);
+  public abstract RewrittenPrototypeDescription lookupPrototypeChangesForMethodDefinition(
+      DexMethod method);
 
   public abstract DexField lookupField(DexField field);
 
@@ -434,11 +444,12 @@
 
     @Override
     public GraphLensLookupResult lookupMethod(DexMethod method, DexMethod context, Type type) {
-      return new GraphLensLookupResult(method, type);
+      return new GraphLensLookupResult(method, type, RewrittenPrototypeDescription.none());
     }
 
     @Override
-    public RewrittenPrototypeDescription lookupPrototypeChanges(DexMethod method) {
+    public RewrittenPrototypeDescription lookupPrototypeChangesForMethodDefinition(
+        DexMethod method) {
       return RewrittenPrototypeDescription.none();
     }
 
@@ -628,24 +639,38 @@
 
     @Override
     public GraphLensLookupResult lookupMethod(DexMethod method, DexMethod context, Type type) {
-      DexMethod previousContext =
-          originalMethodSignatures != null
-              ? originalMethodSignatures.getOrDefault(context, context)
-              : context;
-      GraphLensLookupResult previous = previousLens.lookupMethod(method, previousContext, type);
-      DexMethod newMethod = methodMap.get(previous.getMethod());
+      DexMethod previousContext = internalGetPreviousMethodSignature(context);
+      GraphLensLookupResult lookup = previousLens.lookupMethod(method, previousContext, type);
+      DexMethod newMethod = methodMap.get(lookup.getMethod());
       if (newMethod == null) {
-        return previous;
+        return lookup;
       }
       // TODO(sgjesse): Should we always do interface to virtual mapping? Is it a performance win
-      // that only subclasses which are known to need it actually do it?
+      //  that only subclasses which are known to need it actually do it?
       return new GraphLensLookupResult(
-          newMethod, mapInvocationType(newMethod, method, previous.getType()));
+          newMethod,
+          mapInvocationType(newMethod, method, lookup.getType()),
+          internalDescribePrototypeChanges(lookup.getPrototypeChanges(), newMethod));
     }
 
     @Override
-    public RewrittenPrototypeDescription lookupPrototypeChanges(DexMethod method) {
-      return previousLens.lookupPrototypeChanges(method);
+    public RewrittenPrototypeDescription lookupPrototypeChangesForMethodDefinition(
+        DexMethod method) {
+      DexMethod previous = internalGetPreviousMethodSignature(method);
+      RewrittenPrototypeDescription lookup =
+          previousLens.lookupPrototypeChangesForMethodDefinition(previous);
+      return internalDescribePrototypeChanges(lookup, method);
+    }
+
+    protected RewrittenPrototypeDescription internalDescribePrototypeChanges(
+        RewrittenPrototypeDescription prototypeChanges, DexMethod method) {
+      return prototypeChanges;
+    }
+
+    protected DexMethod internalGetPreviousMethodSignature(DexMethod method) {
+      return originalMethodSignatures != null
+          ? originalMethodSignatures.getOrDefault(method, method)
+          : method;
     }
 
     @Override
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 2828097..31ea06f 100644
--- a/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java
+++ b/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java
@@ -439,6 +439,9 @@
   }
 
   public RewrittenPrototypeDescription withRemovedArguments(ArgumentInfoCollection other) {
+    if (other.isEmpty()) {
+      return this;
+    }
     return new RewrittenPrototypeDescription(
         extraParameters, rewrittenReturnInfo, argumentInfoCollection.combine(other));
   }
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 fc8e49b..505dfd5 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,7 +5,6 @@
 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;
@@ -30,7 +29,7 @@
   }
 
   @Override
-  public boolean hasBeenMerged(DexProgramClass clazz) {
-    return sources.contains(clazz.type);
+  public boolean hasBeenMerged(DexType type) {
+    return sources.contains(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 66f9f6a..3f25895 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
@@ -6,11 +6,31 @@
 
 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;
 
 public interface MergedClasses {
 
   boolean verifyAllSourcesPruned(AppView<AppInfoWithLiveness> appView);
 
-  boolean hasBeenMerged(DexProgramClass clazz);
+  boolean hasBeenMerged(DexType type);
+
+  /**
+   * Determine if the class has been merged by the merged classes object. If the merged classes is
+   * null then return false.
+   */
+  static boolean hasBeenMerged(MergedClasses mergedClasses, DexProgramClass clazz) {
+    return hasBeenMerged(mergedClasses, clazz.type);
+  }
+
+  /**
+   * Determine if the class has been merged by the merged classes object. If the merged classes is
+   * null then return false.
+   */
+  static boolean hasBeenMerged(MergedClasses mergedClasses, DexType type) {
+    if (mergedClasses != null) {
+      return mergedClasses.hasBeenMerged(type);
+    }
+    return false;
+  }
 }
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 1ced397..55c34b5 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,7 +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.ArrayList;
 import java.util.List;
@@ -27,9 +27,9 @@
   }
 
   @Override
-  public boolean hasBeenMerged(DexProgramClass clazz) {
+  public boolean hasBeenMerged(DexType type) {
     for (MergedClasses mergedClasses : collection) {
-      if (mergedClasses.hasBeenMerged(clazz)) {
+      if (mergedClasses.hasBeenMerged(type)) {
         return true;
       }
     }
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 d4c730a..f76f9e7 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,7 +5,6 @@
 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;
@@ -56,7 +55,7 @@
   }
 
   @Override
-  public boolean hasBeenMerged(DexProgramClass clazz) {
-    return hasBeenMergedIntoSubtype(clazz.type);
+  public boolean hasBeenMerged(DexType type) {
+    return hasBeenMergedIntoSubtype(type);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
new file mode 100644
index 0000000..e9aa061
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
@@ -0,0 +1,129 @@
+// 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.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.utils.MethodSignatureEquivalence;
+import com.google.common.base.Equivalence.Wrapper;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.Map;
+
+/**
+ * The class merger is responsible for moving methods from {@link ClassMerger#toMergeGroup} into the
+ * class {@link ClassMerger#target}. While performing merging, this class tracks which methods have
+ * been moved, as well as which fields have been remapped in the {@link ClassMerger#lensBuilder}.
+ */
+class ClassMerger {
+  private final AppView<?> appView;
+  private final DexProgramClass target;
+  private final Collection<DexProgramClass> toMergeGroup;
+  private final DexItemFactory dexItemFactory;
+  private final HorizontalClassMergerGraphLens.Builder lensBuilder;
+
+  private final Map<DexProto, ConstructorMerger.Builder> constructorMergers;
+
+  ClassMerger(
+      AppView<?> appView,
+      HorizontalClassMergerGraphLens.Builder lensBuilder,
+      DexProgramClass target,
+      Collection<DexProgramClass> toMergeGroup) {
+    this.appView = appView;
+    this.lensBuilder = lensBuilder;
+    this.target = target;
+    this.toMergeGroup = toMergeGroup;
+    this.constructorMergers = new IdentityHashMap<>();
+
+    this.dexItemFactory = appView.dexItemFactory();
+  }
+
+  private Wrapper<DexMethod> bySignature(DexMethod method) {
+    return MethodSignatureEquivalence.get().wrap(method);
+  }
+
+  void addConstructor(DexEncodedMethod method) {
+    assert method.isInstanceInitializer();
+    constructorMergers
+        .computeIfAbsent(method.proto(), ignore -> new ConstructorMerger.Builder())
+        .add(method);
+  }
+
+  private void merge(DexProgramClass toMerge) {
+    toMerge.forEachProgramMethod(
+        programMethod -> {
+          DexEncodedMethod method = programMethod.getDefinition();
+          assert !method.isClassInitializer();
+
+          if (method.isInstanceInitializer()) {
+            addConstructor(method);
+          } else {
+            // TODO(b/166427795): Ensure that overriding relationships are not changed.
+            assert method.isVirtualMethod();
+
+            DexMethod newMethod = renameMethod(programMethod);
+            // TODO(b/165000217): Add all methods to `target` in one go using addVirtualMethods().;
+            target.addVirtualMethod(method.toTypeSubstitutedMethod(newMethod));
+            lensBuilder.moveMethod(method.method, newMethod);
+          }
+        });
+
+    // Clear the members of the class to be merged since they have now been moved to the target.
+    toMerge.setVirtualMethods(null);
+    toMerge.setDirectMethods(null);
+    toMerge.setInstanceFields(null);
+    toMerge.setStaticFields(null);
+  }
+
+  /**
+   * Find a new name for the method.
+   *
+   * @param method The class the method originally belonged to.
+   */
+  DexMethod renameMethod(ProgramMethod method) {
+    return dexItemFactory.createFreshMethodName(
+        method.getDefinition().method.name.toSourceString(),
+        method.getHolderType(),
+        method.getDefinition().proto(),
+        target.type,
+        tryMethod -> target.lookupMethod(tryMethod) == null);
+  }
+
+  void mergeConstructors() {
+    for (ConstructorMerger.Builder builder : constructorMergers.values()) {
+      ConstructorMerger constructorMerger = builder.build(appView, target);
+      constructorMerger.merge(lensBuilder);
+    }
+  }
+
+  /**
+   * To ensure constructor merging happens correctly, add all of the target constructors methods to
+   * constructor mergers.
+   */
+  void addTargetConstructors() {
+    target.forEachProgramDirectMethod(
+        programMethod -> {
+          DexEncodedMethod method = programMethod.getDefinition();
+          if (method.isInstanceInitializer()) {
+            addConstructor(method);
+          }
+        });
+  }
+
+  public void mergeGroup() {
+    addTargetConstructors();
+
+    for (DexProgramClass clazz : toMergeGroup) {
+      merge(clazz);
+    }
+
+    mergeConstructors();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java
index d7e942d..aae7b0e 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java
@@ -42,6 +42,23 @@
     this.dexItemFactory = appView.dexItemFactory();
   }
 
+  public static class Builder {
+    private final Collection<DexEncodedMethod> constructors;
+
+    public Builder() {
+      constructors = new ArrayList<>();
+    }
+
+    public Builder add(DexEncodedMethod constructor) {
+      constructors.add(constructor);
+      return this;
+    }
+
+    public ConstructorMerger build(AppView<?> appView, DexProgramClass target) {
+      return new ConstructorMerger(appView, target, constructors);
+    }
+  }
+
   private DexMethod moveConstructor(DexEncodedMethod constructor) {
     DexMethod method =
         dexItemFactory.createFreshMethodName(
@@ -75,7 +92,8 @@
     return MethodAccessFlags.fromSharedAccessFlags(Constants.ACC_PUBLIC, true);
   }
 
-  public void merge() {
+  /** Synthesize a new method which selects the constructor based on a parameter type. */
+  void mergeMany(HorizontalClassMergerGraphLens.Builder lensBuilder) {
     Map<DexType, DexMethod> typeConstructors = new IdentityHashMap<>();
 
     for (DexEncodedMethod constructor : constructors) {
@@ -99,6 +117,40 @@
             ParameterAnnotationsList.empty(),
             synthesizedCode);
 
+    // Map each old constructor to the newly synthesized constructor in the graph lens.
+    int constructorId = 0;
+    for (DexEncodedMethod constructor : constructors) {
+      lensBuilder.mapConstructor(constructor.method, newMethod.method, constructorId++);
+    }
+
     target.addDirectMethod(newMethod);
   }
+
+  /**
+   * The constructor does not conflict with any other constructors. Add the constructor (if any) to
+   * the target directly.
+   */
+  void mergeTrivial(HorizontalClassMergerGraphLens.Builder lensBuilder) {
+    assert constructors.size() <= 1;
+
+    if (!constructors.isEmpty()) {
+      DexEncodedMethod constructor = constructors.iterator().next();
+
+      // Only move the constructor if it is not already in the target type.
+      if (constructor.holder() != target.type) {
+        DexEncodedMethod newConstructor =
+            constructor.toRenamedHolderMethod(target.type, dexItemFactory);
+        target.addDirectMethod(constructor);
+        lensBuilder.moveConstructor(constructor.method, newConstructor.method);
+      }
+    }
+  }
+
+  public void merge(HorizontalClassMergerGraphLens.Builder lensBuilder) {
+    if (constructors.size() <= 1) {
+      mergeTrivial(lensBuilder);
+    } else {
+      mergeMany(lensBuilder);
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
index 56da905..60949e5 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -6,12 +6,22 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.horizontalclassmerging.policies.NoFields;
+import com.android.tools.r8.horizontalclassmerging.policies.NoInterfaces;
+import com.android.tools.r8.horizontalclassmerging.policies.NoInternalUtilityClasses;
+import com.android.tools.r8.horizontalclassmerging.policies.NoOverlappingConstructors;
+import com.android.tools.r8.horizontalclassmerging.policies.NoStaticClassInitializer;
+import com.android.tools.r8.horizontalclassmerging.policies.NotEntryPoint;
+import com.android.tools.r8.horizontalclassmerging.policies.SameParentClass;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.MainDexClasses;
+import com.google.common.collect.ImmutableList;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.HashMap;
+import java.util.IdentityHashMap;
+import java.util.List;
 import java.util.Map;
 
 public class HorizontalClassMerger {
@@ -26,13 +36,22 @@
     this.appView = appView;
     this.mainDexClasses = mainDexClasses;
 
-    Policy[] policies = {
-      // TODO: add policies
-    };
-    this.policyExecutor = new SimplePolicyExecutor(Arrays.asList(policies));
+    List<Policy> policies =
+        ImmutableList.of(
+            new NoFields(),
+            new NoInterfaces(),
+            new NoStaticClassInitializer(),
+            new NotEntryPoint(appView.dexItemFactory()),
+            new NoInternalUtilityClasses(appView.dexItemFactory()),
+            new SameParentClass(),
+            new NoOverlappingConstructors()
+            // TODO: add policies
+            );
+
+    this.policyExecutor = new SimplePolicyExecutor(policies);
   }
 
-  public Collection<Collection<DexProgramClass>> run() {
+  public HorizontalClassMergerGraphLens run() {
     Map<FieldMultiset, Collection<DexProgramClass>> classes = new HashMap<>();
 
     // Group classes by same field signature using the hash map.
@@ -43,6 +62,38 @@
     // Run the policies on all collected classes to produce a final grouping.
     Collection<Collection<DexProgramClass>> groups = policyExecutor.run(classes.values());
 
-    return groups;
+    return createLens(groups);
+  }
+
+  // TODO(b/165577835): replace Collection<DexProgramClass> with MergeGroup
+  /**
+   * Merges all class groups using {@link ClassMerger}. Then fix all references to merged classes
+   * using the {@link TreeFixer}. Constructs a graph lens containing all changes while performing
+   * merging.
+   */
+  private HorizontalClassMergerGraphLens createLens(
+      Collection<Collection<DexProgramClass>> groups) {
+    Map<DexType, DexType> mergedClasses = new IdentityHashMap<>();
+    HorizontalClassMergerGraphLens.Builder lensBuilder =
+        new HorizontalClassMergerGraphLens.Builder();
+
+    // TODO(b/166577694): Replace Collection<DexProgramClass> with MergeGroup
+    for (Collection<DexProgramClass> group : groups) {
+      assert !group.isEmpty();
+
+      DexProgramClass target = group.stream().findFirst().get();
+      group.remove(target);
+
+      for (DexProgramClass clazz : group) {
+        mergedClasses.put(clazz.type, target.type);
+      }
+
+      ClassMerger merger = new ClassMerger(appView, lensBuilder, target, group);
+      merger.mergeGroup();
+    }
+
+    HorizontalClassMergerGraphLens lens =
+        new TreeFixer(appView, lensBuilder, mergedClasses).fixupTypeReferences();
+    return lens;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
new file mode 100644
index 0000000..451e550
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
@@ -0,0 +1,129 @@
+// 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.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class HorizontalClassMergerGraphLens extends NestedGraphLens {
+  private final AppView<?> appView;
+
+  private HorizontalClassMergerGraphLens(
+      AppView<?> appView,
+      Map<DexType, DexType> typeMap,
+      Map<DexField, DexField> fieldMap,
+      Map<DexMethod, DexMethod> methodMap,
+      BiMap<DexField, DexField> originalFieldSignatures,
+      BiMap<DexMethod, DexMethod> originalMethodSignatures,
+      GraphLens previousLens) {
+    super(
+        typeMap,
+        methodMap,
+        fieldMap,
+        originalFieldSignatures,
+        originalMethodSignatures,
+        previousLens,
+        appView.dexItemFactory());
+    this.appView = appView;
+  }
+
+  public HorizontallyMergedClasses getHorizontallyMergedClasses() {
+    return new HorizontallyMergedClasses(this.typeMap);
+  }
+
+  public static class Builder {
+    protected final BiMap<DexField, DexField> fieldMap = HashBiMap.create();
+    protected final Map<DexMethod, DexMethod> methodMap = new IdentityHashMap<>();
+    protected final Map<DexMethod, Set<DexMethod>> completeInverseMethodMap =
+        new IdentityHashMap<>();
+
+    private final BiMap<DexMethod, DexMethod> originalMethodSignatures = HashBiMap.create();
+
+    private final Map<DexMethod, Integer> constructorIds = new IdentityHashMap<>();
+
+    Builder() {}
+
+    public HorizontalClassMergerGraphLens build(
+        AppView<?> appView, Map<DexType, DexType> mergedClasses) {
+      if (mergedClasses.isEmpty()) {
+        return null;
+      } else {
+        BiMap<DexField, DexField> originalFieldSignatures = fieldMap.inverse();
+        return new HorizontalClassMergerGraphLens(
+            appView,
+            mergedClasses,
+            fieldMap,
+            methodMap,
+            originalFieldSignatures,
+            originalMethodSignatures,
+            appView.graphLens());
+      }
+    }
+
+    /** Bidirectional mapping from one method to another. */
+    public Builder moveMethod(DexMethod from, DexMethod to) {
+      mapMethod(from, to);
+      originalMethodSignatures.put(to, from);
+      return this;
+    }
+
+    /**
+     * Unidirectional mapping from one method to another. This seems to only be used by synthesized
+     * constructors so is private for now. See {@link Builder#moveConstructor(DexMethod,
+     * DexMethod)}.
+     */
+    private Builder mapMethod(DexMethod from, DexMethod to) {
+      for (DexMethod existingFrom :
+          completeInverseMethodMap.getOrDefault(from, Collections.emptySet())) {
+        methodMap.put(existingFrom, to);
+
+        // We currently assume that a single method can only be remapped twice.
+        assert completeInverseMethodMap
+            .getOrDefault(existingFrom, Collections.emptySet())
+            .isEmpty();
+      }
+      methodMap.put(from, to);
+      completeInverseMethodMap.computeIfAbsent(to, ignore -> new HashSet<>()).add(from);
+      return this;
+    }
+
+    public boolean hasOriginalSignatureMappingFor(DexMethod method) {
+      return originalMethodSignatures.containsKey(method);
+    }
+
+    /**
+     * One way mapping from one constructor to another. This is used for synthesized constructors,
+     * where many constructors are merged into a single constructor. The synthesized constructor
+     * therefore does not have a unique reverse constructor.
+     *
+     * @param constructorId The id that must be appended to the constructor call to ensure the
+     *     correct constructor is called.
+     */
+    public Builder mapConstructor(DexMethod from, DexMethod to, int constructorId) {
+      mapMethod(from, to);
+      constructorIds.put(from, constructorId);
+      return this;
+    }
+
+    /**
+     * Bidirectional mapping from one constructor to another. When a single constructor is simply
+     * moved from one class to another, we can uniquely map the new constructor back to the old one.
+     */
+    public Builder moveConstructor(DexMethod from, DexMethod to) {
+      return moveMethod(from, to);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
new file mode 100644
index 0000000..00632e5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
@@ -0,0 +1,35 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.horizontalclassmerging;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.classmerging.MergedClasses;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import java.util.Map;
+
+public class HorizontallyMergedClasses implements MergedClasses {
+  private final Map<DexType, DexType> horizontallyMergedClasses;
+
+  public HorizontallyMergedClasses(Map<DexType, DexType> horizontallyMergedClasses) {
+    this.horizontallyMergedClasses = horizontallyMergedClasses;
+  }
+
+  @Override
+  public boolean verifyAllSourcesPruned(AppView<AppInfoWithLiveness> appView) {
+    for (DexType source : horizontallyMergedClasses.keySet()) {
+      assert appView.appInfo().wasPruned(source)
+          : "Expected horizontally merged lambda class `"
+              + source.toSourceString()
+              + "` to be absent";
+    }
+    return true;
+  }
+
+  @Override
+  public boolean hasBeenMerged(DexType type) {
+    return horizontallyMergedClasses.containsKey(type);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java
new file mode 100644
index 0000000..cd2cabf
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java
@@ -0,0 +1,136 @@
+// 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.DexClass.FieldSetter;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.shaking.AnnotationFixer;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.OptionalBool;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * The tree fixer traverses all program classes and finds and fixes references to old classes which
+ * have been remapped to new classes by the class merger (stored in {@link
+ * TreeFixer#mergedClasses}). While doing so, all updated changes are tracked in {@link
+ * TreeFixer#lensBuilder}.
+ */
+class TreeFixer {
+  private final Map<DexType, DexType> mergedClasses;
+  private final Map<DexProto, DexProto> protoFixupCache = new IdentityHashMap<>();
+  private final HorizontalClassMergerGraphLens.Builder lensBuilder;
+  private final AppView<AppInfoWithLiveness> appView;
+
+  public TreeFixer(
+      AppView<AppInfoWithLiveness> appView,
+      HorizontalClassMergerGraphLens.Builder lensBuilder,
+      Map<DexType, DexType> mergedClasses) {
+    this.mergedClasses = mergedClasses;
+    this.lensBuilder = lensBuilder;
+    this.appView = appView;
+  }
+
+  HorizontalClassMergerGraphLens fixupTypeReferences() {
+    // Globally substitute merged class types in protos and holders.
+    for (DexProgramClass clazz : appView.appInfo().classes()) {
+      clazz.getMethodCollection().replaceMethods(this::fixupMethod);
+      fixupFields(clazz.staticFields(), clazz::setStaticField);
+      fixupFields(clazz.instanceFields(), clazz::setInstanceField);
+    }
+    HorizontalClassMergerGraphLens lens = lensBuilder.build(appView, mergedClasses);
+
+    if (lens != null) {
+      new AnnotationFixer(lens).run(appView.appInfo().classes());
+    }
+    return lens;
+  }
+
+  private DexEncodedMethod fixupMethod(DexEncodedMethod method) {
+    DexMethod methodReference = method.method;
+    DexMethod newMethodReference = fixupMethod(methodReference);
+    if (newMethodReference == methodReference) {
+      return method;
+    }
+
+    lensBuilder.moveMethod(methodReference, newMethodReference);
+    DexEncodedMethod newMethod = method.toTypeSubstitutedMethod(newMethodReference);
+    if (newMethod.isNonPrivateVirtualMethod()) {
+      // Since we changed the return type or one of the parameters, this method cannot be a
+      // classpath or library method override, since we only class merge program classes.
+      assert !method.isLibraryMethodOverride().isTrue();
+      newMethod.setLibraryMethodOverride(OptionalBool.FALSE);
+    }
+    return newMethod;
+  }
+
+  private void fixupFields(List<DexEncodedField> fields, FieldSetter setter) {
+    if (fields == null) {
+      return;
+    }
+    for (int i = 0; i < fields.size(); i++) {
+      DexEncodedField encodedField = fields.get(i);
+      DexField field = encodedField.field;
+      DexType newType = fixupType(field.type);
+      DexType newHolder = fixupType(field.holder);
+      DexField newField = appView.dexItemFactory().createField(newHolder, newType, field.name);
+      if (newField != encodedField.field) {
+        // TODO(b/165498187): track mapped fields
+        /* lensBuilder.map(field, newField); */
+        setter.setField(i, encodedField.toTypeSubstitutedField(newField));
+      }
+    }
+  }
+
+  private DexMethod fixupMethod(DexMethod method) {
+    return appView
+        .dexItemFactory()
+        .createMethod(fixupType(method.holder), fixupProto(method.proto), method.name);
+  }
+
+  private DexProto fixupProto(DexProto proto) {
+    DexProto result = protoFixupCache.get(proto);
+    if (result == null) {
+      DexType returnType = fixupType(proto.returnType);
+      DexType[] arguments = fixupTypes(proto.parameters.values);
+      result = appView.dexItemFactory().createProto(returnType, arguments);
+      protoFixupCache.put(proto, result);
+    }
+    return result;
+  }
+
+  private DexType fixupType(DexType type) {
+    if (type.isArrayType()) {
+      DexType base = type.toBaseType(appView.dexItemFactory());
+      DexType fixed = fixupType(base);
+      if (base == fixed) {
+        return type;
+      }
+      return type.replaceBaseType(fixed, appView.dexItemFactory());
+    }
+    if (type.isClassType()) {
+      while (mergedClasses.containsKey(type)) {
+        type = mergedClasses.get(type);
+      }
+    }
+    return type;
+  }
+
+  private DexType[] fixupTypes(DexType[] types) {
+    DexType[] result = new DexType[types.length];
+    for (int i = 0; i < result.length; i++) {
+      result[i] = fixupType(types[i]);
+    }
+    return result;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoFields.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoFields.java
new file mode 100644
index 0000000..9ba49a4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoFields.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.policies;
+
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
+
+public class NoFields extends SingleClassPolicy {
+  @Override
+  public boolean canMerge(DexProgramClass program) {
+    // TODO(b/165498187): remove this policy
+    return !program.hasFields();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInterfaces.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInterfaces.java
new file mode 100644
index 0000000..7b194a2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInterfaces.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.policies;
+
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
+
+public class NoInterfaces extends SingleClassPolicy {
+
+  @Override
+  public boolean canMerge(DexProgramClass program) {
+    return program.interfaces.isEmpty();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInternalUtilityClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInternalUtilityClasses.java
new file mode 100644
index 0000000..2887849
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInternalUtilityClasses.java
@@ -0,0 +1,25 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
+import java.util.Collections;
+import java.util.Set;
+
+public class NoInternalUtilityClasses extends SingleClassPolicy {
+  private final Set<DexType> internalUtilityClasses;
+
+  public NoInternalUtilityClasses(DexItemFactory dexItemFactory) {
+    this.internalUtilityClasses = Collections.singleton(dexItemFactory.enumUnboxingUtilityType);
+  }
+
+  @Override
+  public boolean canMerge(DexProgramClass program) {
+    return !internalUtilityClasses.contains(program.type);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoOverlappingConstructors.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoOverlappingConstructors.java
new file mode 100644
index 0000000..112580c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoOverlappingConstructors.java
@@ -0,0 +1,117 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexProto;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeSet;
+
+public class NoOverlappingConstructors extends MultiClassPolicy {
+
+  public void removeNonConflicting(Map<DexProto, Set<DexProgramClass>> overlappingConstructors) {
+    Iterator<DexProto> i = overlappingConstructors.keySet().iterator();
+    while (i.hasNext()) {
+      DexProto proto = i.next();
+      if (overlappingConstructors.get(proto).size() == 1) {
+        i.remove();
+      }
+    }
+  }
+
+  private Set<DexProgramClass> sortedClassSet(Collection<DexProgramClass> classes) {
+    Set<DexProgramClass> set =
+        new TreeSet<DexProgramClass>(
+            Comparator.comparing(DexProgramClass::getType, DexType::slowCompareTo));
+    set.addAll(classes);
+    return set;
+  }
+
+  @Override
+  public Collection<Collection<DexProgramClass>> apply(Collection<DexProgramClass> group) {
+    Map<DexProto, Set<DexProgramClass>> overlappingConstructors = new IdentityHashMap<>();
+
+    for (DexProgramClass clazz : group) {
+      clazz.forEachProgramDirectMethod(
+          directMethod -> {
+            DexEncodedMethod method = directMethod.getDefinition();
+            if (method.isInstanceInitializer()) {
+              overlappingConstructors
+                  .computeIfAbsent(method.getProto(), ignore -> new HashSet<DexProgramClass>())
+                  .add(clazz);
+            }
+          });
+    }
+
+    removeNonConflicting(overlappingConstructors);
+
+    // This is probably related to the graph colouring problem so probably won't be efficient in
+    // worst cases. We assume there won't be that many overlapping constructors so this should be
+    // reasonable. Constructor merging should also make this obsolete.
+    Collection<Set<DexProgramClass>> groups = new LinkedList<>();
+    groups.add(sortedClassSet(group));
+
+    for (Set<DexProgramClass> overlappingClasses : overlappingConstructors.values()) {
+      Collection<Set<DexProgramClass>> newGroups = new LinkedList<>();
+
+      // For every set of classes that cannot be in the same group, generate a new group with the
+      // same constructor and with all remaining constructors.
+
+      for (Set<DexProgramClass> existingGroup : groups) {
+        Set<DexProgramClass> actuallyOverlapping = new HashSet<>(overlappingClasses);
+        actuallyOverlapping.retainAll(existingGroup);
+
+        if (actuallyOverlapping.size() <= 1) {
+          newGroups.add(existingGroup);
+        } else {
+          Set<DexProgramClass> notOverlapping = new HashSet<>(existingGroup);
+          notOverlapping.removeAll(overlappingClasses);
+          for (DexProgramClass overlappingClass : actuallyOverlapping) {
+            Set<DexProgramClass> newGroup = sortedClassSet(notOverlapping);
+            newGroup.add(overlappingClass);
+            newGroups.add(newGroup);
+          }
+        }
+      }
+
+      groups = newGroups;
+    }
+
+    // Ensure each class is only in a single group and remove singleton and empty groups.
+    Set<DexProgramClass> assignedClasses = new HashSet<>();
+
+    Iterator<Set<DexProgramClass>> i = groups.iterator();
+    while (i.hasNext()) {
+      Set<DexProgramClass> newGroup = i.next();
+      newGroup.removeAll(assignedClasses);
+      if (newGroup.size() <= 1) {
+        i.remove();
+      } else {
+        assignedClasses.addAll(newGroup);
+      }
+    }
+
+    // Map to collection
+    Collection<Collection<DexProgramClass>> newGroups = new ArrayList<>();
+    for (Set<DexProgramClass> newGroup : groups) {
+      List<DexProgramClass> newGroupList = new ArrayList<>(newGroup);
+      newGroups.add(new ArrayList<>(newGroup));
+    }
+
+    return newGroups;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoStaticClassInitializer.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoStaticClassInitializer.java
new file mode 100644
index 0000000..aeca6c0
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoStaticClassInitializer.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.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
+
+/**
+ * Prevent merging of classes with static initializers, as merging these causes side effects. It is
+ * okay for superclasses to have static initializers as all classes are expected to have the same
+ * super class.
+ */
+public class NoStaticClassInitializer extends SingleClassPolicy {
+  @Override
+  public boolean canMerge(DexProgramClass program) {
+    return !program.hasClassInitializer();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotEntryPoint.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotEntryPoint.java
new file mode 100644
index 0000000..4bc65e5
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotEntryPoint.java
@@ -0,0 +1,30 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
+
+public class NotEntryPoint extends SingleClassPolicy {
+  private final DexString main;
+
+  public NotEntryPoint(DexItemFactory factory) {
+    main = factory.createString("main");
+  }
+
+  @Override
+  public boolean canMerge(DexProgramClass program) {
+    // TODO(b/165000217): Account for keep rules instead.
+    for (DexEncodedMethod method : program.directMethods()) {
+      if (method.method.name.equals(main)) {
+        return false;
+      }
+    }
+    return true;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameParentClass.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameParentClass.java
new file mode 100644
index 0000000..4e1987e
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameParentClass.java
@@ -0,0 +1,27 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.LinkedList;
+import java.util.Map;
+
+public class SameParentClass extends MultiClassPolicy {
+
+  @Override
+  public Collection<Collection<DexProgramClass>> apply(Collection<DexProgramClass> group) {
+    Map<DexType, Collection<DexProgramClass>> groups = new IdentityHashMap<>();
+    for (DexProgramClass clazz : group) {
+      groups
+          .computeIfAbsent(clazz.superType, ignore -> new LinkedList<DexProgramClass>())
+          .add(clazz);
+    }
+    return groups.values();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/inspector/internal/InspectorImpl.java b/src/main/java/com/android/tools/r8/inspector/internal/InspectorImpl.java
index c3d8a64..72e322f 100644
--- a/src/main/java/com/android/tools/r8/inspector/internal/InspectorImpl.java
+++ b/src/main/java/com/android/tools/r8/inspector/internal/InspectorImpl.java
@@ -3,7 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.inspector.internal;
 
-import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.inspector.ClassInspector;
 import com.android.tools.r8.inspector.Inspector;
@@ -29,25 +28,25 @@
   }
 
   public static void runInspections(
-      List<Consumer<InspectorImpl>> inspections, DexApplication application) {
+      List<Consumer<InspectorImpl>> inspections, Collection<DexProgramClass> classes) {
     if (inspections == null || inspections.isEmpty()) {
       return;
     }
-    InspectorImpl inspector = new InspectorImpl(application);
+    InspectorImpl inspector = new InspectorImpl(classes);
     for (Consumer<InspectorImpl> inspection : inspections) {
       inspection.accept(inspector);
     }
   }
 
-  private final DexApplication application;
+  private final Collection<DexProgramClass> classes;
 
-  public InspectorImpl(DexApplication application) {
-    this.application = application;
+  public InspectorImpl(Collection<DexProgramClass> classes) {
+    this.classes = classes;
   }
 
   @Override
   public void forEachClass(Consumer<ClassInspector> inspection) {
-    for (DexProgramClass clazz : application.classes()) {
+    for (DexProgramClass clazz : classes) {
       inspection.accept(new ClassInspectorImpl(clazz));
     }
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java
index 3d8df1a..ebcdfff 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.analysis.value.AbstractValueFactory;
+import com.android.tools.r8.ir.analysis.value.EnumValuesObjectState;
 import com.android.tools.r8.ir.analysis.value.ObjectState;
 import com.android.tools.r8.ir.analysis.value.SingleFieldValue;
 import com.android.tools.r8.ir.code.ArrayPut;
@@ -25,6 +26,7 @@
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.NewArrayEmpty;
 import com.android.tools.r8.ir.code.NewInstance;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.optimize.ClassInitializerDefaultsOptimization.ClassInitializerDefaultsResult;
@@ -141,9 +143,132 @@
    * array as long as the array is identified as being the {@code $VALUES} array.
    */
   private SingleFieldValue computeSingleEnumFieldValue(Value value) {
+    if (!context.getHolder().isEnum()) {
+      return null;
+    }
     assert !value.hasAliasedValue();
-    if (!context.getHolder().isEnum()
-        || !value.isDefinedByInstructionSatisfying(Instruction::isNewInstance)) {
+    if (isEnumValuesArray(value)) {
+      return computeSingleEnumFieldValueForValuesArray(value);
+    }
+    return computeSingleEnumFieldValueForInstance(value);
+  }
+
+  private SingleFieldValue computeSingleEnumFieldValueForValuesArray(Value value) {
+    if (!value.isDefinedByInstructionSatisfying(Instruction::isNewArrayEmpty)) {
+      return null;
+    }
+
+    NewArrayEmpty newArrayEmpty = value.definition.asNewArrayEmpty();
+    if (newArrayEmpty.type.toBaseType(appView.dexItemFactory()) != context.getHolder().type) {
+      return null;
+    }
+    if (value.hasDebugUsers() || value.hasPhiUsers()) {
+      return null;
+    }
+    if (!newArrayEmpty.size().isConstNumber()) {
+      return null;
+    }
+
+    int valuesSize = newArrayEmpty.size().getConstInstruction().asConstNumber().getIntValue();
+    if (valuesSize == 0) {
+      // No need to compute the state of an empty array.
+      return null;
+    }
+
+    ObjectState[] valuesState = new ObjectState[valuesSize];
+    DexEncodedField valuesField = null;
+    for (Instruction user : value.uniqueUsers()) {
+      switch (user.opcode()) {
+        case ARRAY_PUT:
+          ArrayPut arrayPut = user.asArrayPut();
+          if (arrayPut.array() != value) {
+            return null;
+          }
+          if (!arrayPut.index().isConstNumber()) {
+            return null;
+          }
+          int index = arrayPut.index().getConstInstruction().asConstNumber().getIntValue();
+          if (index < 0 || index >= valuesSize) {
+            return null;
+          }
+          ObjectState objectState = computeEnumInstanceObjectState(arrayPut.value());
+          if (objectState == null || objectState.isEmpty()) {
+            // We need the state of all fields for the analysis to be valuable.
+            return null;
+          }
+          assert verifyValuesArrayIndexMatchesOrdinal(index, objectState);
+          if (valuesState[index] != null) {
+            return null;
+          }
+          valuesState[index] = objectState;
+          break;
+
+        case STATIC_PUT:
+          DexEncodedField field =
+              context.getHolder().lookupStaticField(user.asStaticPut().getField());
+          if (field == null) {
+            return null;
+          }
+          if (valuesField != null) {
+            return null;
+          }
+          valuesField = field;
+          break;
+
+        default:
+          return null;
+      }
+    }
+
+    if (valuesField == null) {
+      return null;
+    }
+
+    for (ObjectState objectState : valuesState) {
+      if (objectState == null) {
+        return null;
+      }
+    }
+
+    return appView
+        .abstractValueFactory()
+        .createSingleFieldValue(valuesField.field, new EnumValuesObjectState(valuesState));
+  }
+
+  private ObjectState computeEnumInstanceObjectState(Value value) {
+    Value root = value.getAliasedValue();
+    if (root.isPhi()) {
+      return ObjectState.empty();
+    }
+    Instruction definition = root.getDefinition();
+    if (definition.isNewInstance()) {
+      return computeObjectState(definition.outValue());
+    }
+    if (definition.isStaticGet()) {
+      // TODO(b/166532388) : Enums with many instance rely on staticGets to set the $VALUES data
+      // instead of directly keeping the values in registers. We could consider analysing these
+      // and answer the analysed object state here.
+      return ObjectState.empty();
+    }
+    return ObjectState.empty();
+  }
+
+  private boolean verifyValuesArrayIndexMatchesOrdinal(int ordinal, ObjectState objectState) {
+    DexEncodedField ordinalField =
+        appView
+            .appInfo()
+            .resolveField(appView.dexItemFactory().enumMembers.ordinalField, context)
+            .getResolvedField();
+    assert ordinalField != null;
+    AbstractValue ordinalState = objectState.getAbstractFieldValue(ordinalField);
+    assert ordinalState != null;
+    assert ordinalState.isSingleNumberValue();
+    assert ordinalState.asSingleNumberValue().getIntValue() == ordinal;
+    return true;
+  }
+
+  private SingleFieldValue computeSingleEnumFieldValueForInstance(Value value) {
+    if (!value.isDefinedByInstructionSatisfying(Instruction::isNewInstance)) {
       return null;
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/EnumValuesObjectState.java b/src/main/java/com/android/tools/r8/ir/analysis/value/EnumValuesObjectState.java
new file mode 100644
index 0000000..f1ec89c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/EnumValuesObjectState.java
@@ -0,0 +1,82 @@
+// 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.ir.analysis.value;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import java.util.Arrays;
+import java.util.Objects;
+
+public class EnumValuesObjectState extends ObjectState {
+
+  private final ObjectState[] state;
+
+  public EnumValuesObjectState(ObjectState[] state) {
+    assert state.length > 0;
+    assert Arrays.stream(state).noneMatch(Objects::isNull);
+    this.state = state;
+  }
+
+  @Override
+  public AbstractValue getAbstractFieldValue(DexEncodedField field) {
+    return UnknownValue.getInstance();
+  }
+
+  public ObjectState getObjectStateForOrdinal(int ordinal) {
+    if (ordinal < 0 || ordinal >= state.length) {
+      return ObjectState.empty();
+    }
+    return state[ordinal];
+  }
+
+  @Override
+  public boolean isEnumValuesObjectState() {
+    return true;
+  }
+
+  @Override
+  public EnumValuesObjectState asEnumValuesObjectState() {
+    return this;
+  }
+
+  @Override
+  public boolean isEmpty() {
+    // Non-empty by construction.
+    return false;
+  }
+
+  @Override
+  public ObjectState rewrittenWithLens(AppView<AppInfoWithLiveness> appView, GraphLens lens) {
+    ObjectState[] newState = new ObjectState[state.length];
+    for (int i = 0; i < state.length; i++) {
+      newState[i] = state[i].rewrittenWithLens(appView, lens);
+    }
+    return new EnumValuesObjectState(newState);
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (getClass() != o.getClass()) {
+      return false;
+    }
+    EnumValuesObjectState other = (EnumValuesObjectState) o;
+    if (state.length != other.state.length) {
+      return false;
+    }
+    for (int i = 0; i < state.length; i++) {
+      if (!state[i].equals(other.state[i])) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  @Override
+  public int hashCode() {
+    return Arrays.hashCode(state);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/ObjectState.java b/src/main/java/com/android/tools/r8/ir/analysis/value/ObjectState.java
index b724858..4e6d970 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/ObjectState.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/ObjectState.java
@@ -35,6 +35,14 @@
   @Override
   public abstract int hashCode();
 
+  public boolean isEnumValuesObjectState() {
+    return false;
+  }
+
+  public EnumValuesObjectState asEnumValuesObjectState() {
+    return null;
+  }
+
   public static class Builder {
 
     private final Map<DexField, AbstractValue> state = new IdentityHashMap<>();
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleNumberValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleNumberValue.java
index 0cb75c3..98a7fa4 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleNumberValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleNumberValue.java
@@ -50,6 +50,10 @@
     return value;
   }
 
+  public int getIntValue() {
+    return (int) value;
+  }
+
   @Override
   public boolean equals(Object o) {
     return this == o;
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 806917c..f2da1ac 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
@@ -457,7 +457,7 @@
   private static RewrittenPrototypeDescription lookupPrototypeChanges(
       AppView<?> appView, ProgramMethod method) {
     RewrittenPrototypeDescription prototypeChanges =
-        appView.graphLens().lookupPrototypeChanges(method.getReference());
+        appView.graphLens().lookupPrototypeChangesForMethodDefinition(method.getReference());
     if (Log.ENABLED && prototypeChanges.getArgumentInfoCollection().hasRemovedArguments()) {
       Log.info(
           IRBuilder.class,
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 61a2f18..4c10624 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
@@ -470,12 +470,12 @@
     }
   }
 
-  public DexApplication convert(DexApplication application, ExecutorService executor)
+  public void convert(AppView<AppInfo> appView, ExecutorService executor)
       throws ExecutionException {
     removeLambdaDeserializationMethods();
     workaroundAbstractMethodOnNonAbstractClassVerificationBug(
         executor, OptimizationFeedbackIgnore.getInstance());
-
+    DexApplication application = appView.appInfo().app();
     timing.begin("IR conversion");
     ThreadUtils.processItems(application.classes(), this::convertMethods, executor);
 
@@ -496,7 +496,8 @@
     handleSynthesizedClassMapping(builder);
     timing.end();
 
-    return builder.build();
+    DexApplication app = builder.build();
+    appView.setAppInfo(new AppInfo(app, appView.appInfo().getSyntheticItems().commit(app)));
   }
 
   private void handleSynthesizedClassMapping(Builder<?> builder) {
@@ -738,7 +739,6 @@
       postMethodProcessorBuilder.put(inliner);
     }
     if (enumUnboxer != null) {
-      enumUnboxer.finishAnalysis();
       enumUnboxer.unboxEnums(postMethodProcessorBuilder, executorService, feedback);
     }
     if (!options.debug) {
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 bd8ba8d..51905bf 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
@@ -227,8 +227,7 @@
               DexMethod actualTarget = lensLookup.getMethod();
               Invoke.Type actualInvokeType = lensLookup.getType();
               if (actualTarget != invokedMethod || invoke.getType() != actualInvokeType) {
-                RewrittenPrototypeDescription prototypeChanges =
-                    graphLens.lookupPrototypeChanges(actualTarget);
+                RewrittenPrototypeDescription prototypeChanges = lensLookup.getPrototypeChanges();
 
                 List<Value> newInValues;
                 ArgumentInfoCollection argumentInfoCollection =
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
index 88f6f04..2167964 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceMethodRewriter.java
@@ -265,9 +265,13 @@
                 // On pre-L devices static calls to interface methods result in verifier
                 // rejecting the whole class. We have to create special dispatch classes,
                 // so the user class is not rejected because it make this call directly.
+                // TODO(b/166247515): If this an incorrect invoke-static without the interface bit
+                //  we end up "fixing" the code and remove and ICCE error.
                 instructions.replaceCurrentInstruction(
-                    new InvokeStatic(staticAsMethodOfDispatchClass(method),
-                        invokeStatic.outValue(), invokeStatic.arguments()));
+                    new InvokeStatic(
+                        staticAsMethodOfDispatchClass(method),
+                        invokeStatic.outValue(),
+                        invokeStatic.arguments()));
                 requiredDispatchClasses
                     .computeIfAbsent(clazz.asLibraryClass(), k -> Sets.newConcurrentHashSet())
                     .add(appInfo.definitionFor(encodedMethod.holder()).asProgramClass());
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
index 5a50c02..f400369 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/InterfaceProcessor.java
@@ -259,10 +259,13 @@
       DexMethod newMethod = rewriter.staticAsMethodOfDispatchClass(origMethod);
       ForwardMethodSourceCode.Builder forwardSourceCodeBuilder =
           ForwardMethodSourceCode.builder(newMethod);
+      // Create a forwarding method to the library static interface method. The method is added
+      // to the dispatch class, however, the targeted method is still on the interface, so the
+      // interface bit should be set to true.
       forwardSourceCodeBuilder
           .setTarget(origMethod)
           .setInvokeType(Type.STATIC)
-          .setIsInterface(false); // We forward to the Companion class, not an interface.
+          .setIsInterface(true);
       DexEncodedMethod newEncodedMethod =
           new DexEncodedMethod(
               newMethod,
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
index 605397a..8deb4fc 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
@@ -10,7 +10,6 @@
 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.DexClass;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
@@ -34,7 +33,6 @@
 import com.android.tools.r8.ir.synthetic.ForwardMethodSourceCode;
 import com.android.tools.r8.ir.synthetic.SynthesizedCode;
 import com.android.tools.r8.origin.SynthesizedOrigin;
-import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OptionalBool;
 import com.google.common.base.Suppliers;
 import com.google.common.primitives.Longs;
@@ -541,18 +539,8 @@
       return accessibilityBridge;
     }
 
-    boolean holderIsInterface() {
-      InternalOptions options = appView.options();
-      DexMethod implMethod = descriptor.implHandle.asMethod();
-      DexClass implMethodHolder = appView.definitionFor(implMethod.holder);
-      if (implMethodHolder == null) {
-        assert options
-            .desugaredLibraryConfiguration
-            .getBackportCoreLibraryMember()
-            .containsKey(implMethod.holder);
-        return false;
-      }
-      return implMethodHolder.isInterface();
+    boolean isInterface() {
+      return descriptor.implHandle.isInterface;
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java
index 0d996d5..ac55e68 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaMainMethodSourceCode.java
@@ -243,7 +243,7 @@
                 methodToCall.proto,
                 argValueTypes,
                 argRegisters,
-                target.holderIsInterface()));
+                target.isInterface()));
 
     // Does the method have return value?
     if (enforcedReturnType.isVoidType()) {
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 e7c0b77..9e429a4 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
@@ -98,33 +98,36 @@
   }
 
   @Override
-  public RewrittenPrototypeDescription lookupPrototypeChanges(DexMethod method) {
+  protected RewrittenPrototypeDescription internalDescribePrototypeChanges(
+      RewrittenPrototypeDescription prototypeChanges, DexMethod method) {
     if (isConstructorBridge(method)) {
       // TODO (b/132767654): Try to write a test which breaks that assertion.
-      assert previousLens.lookupPrototypeChanges(method).isEmpty();
+      assert prototypeChanges.isEmpty();
       // TODO(b/164901008): Fix when the number of arguments overflows.
       return RewrittenPrototypeDescription.none().withExtraUnusedNullParameter();
-    } else {
-      return previousLens.lookupPrototypeChanges(method);
     }
+    return prototypeChanges;
   }
 
   @Override
   public GraphLensLookupResult lookupMethod(DexMethod method, DexMethod context, Invoke.Type type) {
     assert originalMethodSignatures == null;
-    GraphLensLookupResult previous = previousLens.lookupMethod(method, context, type);
-    DexMethod bridge = methodMap.get(previous.getMethod());
+    GraphLensLookupResult lookup = previousLens.lookupMethod(method, context, type);
+    DexMethod bridge = methodMap.get(lookup.getMethod());
     if (bridge == null) {
-      return previous;
+      return lookup;
     }
     assert context != null : "Guaranteed by isContextFreeForMethod";
     if (bridge.holder == context.holder) {
-      return previous;
+      return lookup;
     }
     if (isConstructorBridge(bridge)) {
-      return new GraphLensLookupResult(bridge, Invoke.Type.DIRECT);
+      return new GraphLensLookupResult(
+          bridge,
+          Invoke.Type.DIRECT,
+          internalDescribePrototypeChanges(lookup.getPrototypeChanges(), bridge));
     }
-    return new GraphLensLookupResult(bridge, Invoke.Type.STATIC);
+    return new GraphLensLookupResult(bridge, Invoke.Type.STATIC, lookup.getPrototypeChanges());
   }
 
   public static Builder builder() {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java b/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java
index 885a1d0..96e5e9c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java
@@ -70,21 +70,18 @@
     }
 
     @Override
-    public RewrittenPrototypeDescription lookupPrototypeChanges(DexMethod method) {
-      DexMethod originalMethod = originalMethodSignatures.getOrDefault(method, method);
-      RewrittenPrototypeDescription result = previousLens.lookupPrototypeChanges(originalMethod);
-      if (originalMethod != method) {
-        if (method.proto.returnType.isVoidType() && !originalMethod.proto.returnType.isVoidType()) {
-          result = result.withConstantReturn(originalMethod.proto.returnType, appView);
-        }
-        ArgumentInfoCollection removedArgumentsInfo = removedArgumentsInfoPerMethod.get(method);
-        if (removedArgumentsInfo != null) {
-          result = result.withRemovedArguments(removedArgumentsInfo);
-        }
-      } else {
+    protected RewrittenPrototypeDescription internalDescribePrototypeChanges(
+        RewrittenPrototypeDescription prototypeChanges, DexMethod method) {
+      DexMethod previous = internalGetPreviousMethodSignature(method);
+      if (previous == method) {
         assert !removedArgumentsInfoPerMethod.containsKey(method);
+        return prototypeChanges;
       }
-      return result;
+      if (method.getReturnType().isVoidType() && !previous.getReturnType().isVoidType()) {
+        prototypeChanges = prototypeChanges.withConstantReturn(previous.getReturnType(), appView);
+      }
+      return prototypeChanges.withRemovedArguments(
+          removedArgumentsInfoPerMethod.getOrDefault(method, ArgumentInfoCollection.empty()));
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java
index e6bd21e..0dd84e7 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/UnusedArgumentsCollector.java
@@ -76,14 +76,10 @@
     }
 
     @Override
-    public RewrittenPrototypeDescription lookupPrototypeChanges(DexMethod method) {
-      DexMethod originalMethod =
-          originalMethodSignatures != null
-              ? originalMethodSignatures.getOrDefault(method, method)
-              : method;
-      RewrittenPrototypeDescription result = previousLens.lookupPrototypeChanges(originalMethod);
-      ArgumentInfoCollection removedArguments = this.removedArguments.get(method);
-      return removedArguments != null ? result.withRemovedArguments(removedArguments) : result;
+    protected RewrittenPrototypeDescription internalDescribePrototypeChanges(
+        RewrittenPrototypeDescription prototypeChanges, DexMethod method) {
+      return prototypeChanges.withRemovedArguments(
+          removedArguments.getOrDefault(method, ArgumentInfoCollection.empty()));
     }
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumInstanceFieldData.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumInstanceFieldData.java
new file mode 100644
index 0000000..4f4dea9
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumInstanceFieldData.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.ir.optimize.enums;
+
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import java.util.Map;
+
+/*
+ * My instances represent the values of an enum field for each of the enum instance.
+ * For example:
+ * <code> enum E {
+ *  A(10),B(20);
+ *  int f;
+ *  public E(int f) {
+ *     this.f = f;
+ *   }
+ * }</code>
+ * <p> The EnumFieldData for the field E#f is A -> 10, B -> 20.
+ * The EnumFieldData may be unknown, for example, if the enum fields are not set with constants.
+ */
+public abstract class EnumInstanceFieldData {
+
+  public abstract boolean isUnknown();
+
+  public boolean isKnown() {
+    return !isUnknown();
+  }
+
+  public EnumInstanceFieldKnownData asEnumFieldKnownData() {
+    return null;
+  }
+
+  public static class EnumInstanceFieldUnknownData extends EnumInstanceFieldData {
+
+    private static final EnumInstanceFieldUnknownData INSTANCE = new EnumInstanceFieldUnknownData();
+
+    public static EnumInstanceFieldUnknownData getInstance() {
+      return INSTANCE;
+    }
+
+    private EnumInstanceFieldUnknownData() {}
+
+    @Override
+    public boolean isUnknown() {
+      return true;
+    }
+  }
+
+  public abstract static class EnumInstanceFieldKnownData extends EnumInstanceFieldData {
+
+    @Override
+    public boolean isUnknown() {
+      return false;
+    }
+
+    public abstract boolean isOrdinal();
+
+    public abstract boolean isMapping();
+
+    @Override
+    public EnumInstanceFieldKnownData asEnumFieldKnownData() {
+      return this;
+    }
+
+    public EnumInstanceFieldMappingData asEnumFieldMappingData() {
+      return null;
+    }
+  }
+
+  public static class EnumInstanceFieldOrdinalData extends EnumInstanceFieldKnownData {
+    @Override
+    public boolean isOrdinal() {
+      return true;
+    }
+
+    @Override
+    public boolean isMapping() {
+      return false;
+    }
+  }
+
+  public static class EnumInstanceFieldMappingData extends EnumInstanceFieldKnownData {
+    private final Map<DexField, AbstractValue> mapping;
+
+    public EnumInstanceFieldMappingData(Map<DexField, AbstractValue> mapping) {
+      this.mapping = mapping;
+    }
+
+    @Override
+    public boolean isOrdinal() {
+      return false;
+    }
+
+    @Override
+    public boolean isMapping() {
+      return true;
+    }
+
+    @Override
+    public EnumInstanceFieldMappingData asEnumFieldMappingData() {
+      return this;
+    }
+
+    public AbstractValue getData(DexField field) {
+      return mapping.get(field);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumInstanceFieldDataMap.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumInstanceFieldDataMap.java
new file mode 100644
index 0000000..f02e488
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumInstanceFieldDataMap.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.ir.optimize.enums;
+
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldKnownData;
+import com.google.common.collect.ImmutableMap;
+
+public class EnumInstanceFieldDataMap {
+  private final ImmutableMap<DexType, ImmutableMap<DexField, EnumInstanceFieldKnownData>>
+      instanceFieldMap;
+
+  public EnumInstanceFieldDataMap(
+      ImmutableMap<DexType, ImmutableMap<DexField, EnumInstanceFieldKnownData>> instanceFieldMap) {
+    this.instanceFieldMap = instanceFieldMap;
+  }
+
+  public EnumInstanceFieldKnownData getInstanceFieldData(
+      DexType enumType, DexField enumInstanceField) {
+    assert instanceFieldMap.containsKey(enumType);
+    assert instanceFieldMap.get(enumType).containsKey(enumInstanceField);
+    return instanceFieldMap.get(enumType).get(enumInstanceField);
+  }
+}
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 3f910f5..12b42aa 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
@@ -30,6 +30,7 @@
 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.EnumValueInfoMapCollection;
 import com.android.tools.r8.graph.FieldResolutionResult;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
@@ -42,6 +43,8 @@
 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;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.ObjectState;
 import com.android.tools.r8.ir.code.ArrayPut;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.CheckCast;
@@ -49,6 +52,7 @@
 import com.android.tools.r8.ir.code.FieldInstruction;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.If;
+import com.android.tools.r8.ir.code.InstanceGet;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.Invoke;
 import com.android.tools.r8.ir.code.InvokeMethod;
@@ -62,6 +66,10 @@
 import com.android.tools.r8.ir.conversion.PostMethodProcessor;
 import com.android.tools.r8.ir.conversion.PostOptimization;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldKnownData;
+import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldMappingData;
+import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldOrdinalData;
+import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldUnknownData;
 import com.android.tools.r8.ir.optimize.info.FieldOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfo;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedback.OptimizationInfoFixer;
@@ -69,6 +77,7 @@
 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.OptionalBool;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
@@ -97,6 +106,8 @@
   // Map the enum candidates with their dependencies, i.e., the methods to reprocess for the given
   // enum if the optimization eventually decides to unbox it.
   private final Map<DexType, ProgramMethodSet> enumsUnboxingCandidates;
+  private final Map<DexType, Set<DexField>> requiredEnumInstanceFieldData =
+      new ConcurrentHashMap<>();
   private final Set<DexType> enumsToUnboxWithPackageRequirement = Sets.newIdentityHashSet();
 
   private EnumUnboxingRewriter enumUnboxerRewriter;
@@ -118,6 +129,12 @@
     enumsUnboxingCandidates = new EnumUnboxingCandidateAnalysis(appView, this).findCandidates();
   }
 
+  private void requiredEnumInstanceFieldData(DexType enumType, DexField field) {
+    requiredEnumInstanceFieldData
+        .computeIfAbsent(enumType, ignored -> Sets.newConcurrentHashSet())
+        .add(field);
+  }
+
   private void markEnumAsUnboxable(Reason reason, DexProgramClass enumClass) {
     assert enumClass.isEnum();
     reportFailure(enumClass.type, reason);
@@ -268,27 +285,30 @@
     // We are using the ConstClass of an enum, which typically means the enum cannot be unboxed.
     // We however allow unboxing if the ConstClass is only used as an argument to Enum#valueOf, to
     // allow unboxing of: MyEnum a = Enum.valueOf(MyEnum.class, "A");.
-    if (!enumsUnboxingCandidates.containsKey(constClass.getValue())) {
+    DexType enumType = constClass.getValue();
+    if (!enumsUnboxingCandidates.containsKey(enumType)) {
       return;
     }
     if (constClass.outValue() == null) {
-      eligibleEnums.add(constClass.getValue());
+      eligibleEnums.add(enumType);
       return;
     }
+    DexProgramClass enumClass = appView.definitionFor(enumType).asProgramClass();
     if (constClass.outValue().hasPhiUsers()) {
-      markEnumAsUnboxable(
-          Reason.CONST_CLASS, appView.definitionForProgramType(constClass.getValue()));
+      markEnumAsUnboxable(Reason.CONST_CLASS, enumClass);
       return;
     }
     for (Instruction user : constClass.outValue().uniqueUsers()) {
       if (!(user.isInvokeStatic()
           && user.asInvokeStatic().getInvokedMethod() == factory.enumMembers.valueOf)) {
-        markEnumAsUnboxable(
-            Reason.CONST_CLASS, appView.definitionForProgramType(constClass.getValue()));
+        markEnumAsUnboxable(Reason.CONST_CLASS, enumClass);
         return;
       }
     }
-    eligibleEnums.add(constClass.getValue());
+    // The name data is required for the correct mapping from the enum name to the ordinal in the
+    // valueOf utility method.
+    requiredEnumInstanceFieldData(enumType, factory.enumMembers.nameField);
+    eligibleEnums.add(enumType);
   }
 
   private void addNullDependencies(IRCode code, Set<Instruction> uses, Set<DexType> eligibleEnums) {
@@ -345,6 +365,7 @@
       ExecutorService executorService,
       OptimizationFeedbackDelayed feedback)
       throws ExecutionException {
+    EnumInstanceFieldDataMap enumInstanceFieldDataMap = finishAnalysis();
     // At this point the enumsToUnbox are no longer candidates, they will all be unboxed.
     if (enumsUnboxingCandidates.isEmpty()) {
       return;
@@ -352,7 +373,7 @@
     ImmutableSet<DexType> enumsToUnbox = ImmutableSet.copyOf(this.enumsUnboxingCandidates.keySet());
     // Update keep info on any of the enum methods of the removed classes.
     updatePinnedItems(enumsToUnbox);
-    enumUnboxerRewriter = new EnumUnboxingRewriter(appView, enumsToUnbox);
+    enumUnboxerRewriter = new EnumUnboxingRewriter(appView, enumsToUnbox, enumInstanceFieldDataMap);
     DirectMappedDexApplication.Builder appBuilder = appView.appInfo().app().asDirect().builder();
     Map<DexType, DexType> newMethodLocation = synthesizeUnboxedEnumsMethodsLocations(appBuilder);
     NestedGraphLens enumUnboxingLens =
@@ -475,12 +496,38 @@
             });
   }
 
-  public void finishAnalysis() {
+  public EnumInstanceFieldDataMap finishAnalysis() {
     analyzeInitializers();
     analyzeAccessibility();
+    EnumInstanceFieldDataMap enumInstanceFieldDataMap = analyzeFields();
     if (debugLogEnabled) {
       reportEnumsAnalysis();
     }
+    return enumInstanceFieldDataMap;
+  }
+
+  private EnumInstanceFieldDataMap analyzeFields() {
+    ImmutableMap.Builder<DexType, ImmutableMap<DexField, EnumInstanceFieldKnownData>> builder =
+        ImmutableMap.builder();
+    requiredEnumInstanceFieldData.forEach(
+        (enumType, fields) -> {
+          ImmutableMap.Builder<DexField, EnumInstanceFieldKnownData> typeBuilder =
+              ImmutableMap.builder();
+          if (enumsUnboxingCandidates.containsKey(enumType)) {
+            DexProgramClass enumClass = appView.definitionFor(enumType).asProgramClass();
+            assert enumClass != null;
+            for (DexField field : fields) {
+              EnumInstanceFieldData enumInstanceFieldData = computeEnumFieldData(field, enumClass);
+              if (enumInstanceFieldData.isUnknown()) {
+                markEnumAsUnboxable(Reason.MISSING_INSTANCE_FIELD_DATA, enumClass);
+                return;
+              }
+              typeBuilder.put(field, enumInstanceFieldData.asEnumFieldKnownData());
+            }
+            builder.put(enumType, typeBuilder.build());
+          }
+        });
+    return new EnumInstanceFieldDataMap(builder.build());
   }
 
   private void analyzeAccessibility() {
@@ -721,20 +768,26 @@
       DexProgramClass enumClass = appView.definitionForProgramType(toUnbox);
       assert enumClass != null;
 
-      // Enum candidates have necessarily only one constructor matching enumMethods.constructor
-      // signature.
-      DexEncodedMethod initializer = enumClass.lookupDirectMethod(factory.enumMembers.constructor);
-      if (initializer == null) {
+      boolean hasInstanceInitializer = false;
+      for (DexEncodedMethod directMethod : enumClass.directMethods()) {
+        if (directMethod.isInstanceInitializer()) {
+          hasInstanceInitializer = true;
+          if (directMethod
+              .getOptimizationInfo()
+              .getInstanceInitializerInfo()
+              .mayHaveOtherSideEffectsThanInstanceFieldAssignments()) {
+            markEnumAsUnboxable(Reason.INVALID_INIT, enumClass);
+            break;
+          }
+        }
+      }
+      if (!hasInstanceInitializer) {
         // This case typically happens when a programmer uses EnumSet/EnumMap without using the
         // enum keep rules. The code is incorrect in this case (EnumSet/EnumMap won't work).
         // We bail out.
         markEnumAsUnboxable(Reason.NO_INIT, enumClass);
         continue;
       }
-      if (initializer.getOptimizationInfo().mayHaveSideEffects()) {
-        markEnumAsUnboxable(Reason.INVALID_INIT, enumClass);
-        continue;
-      }
 
       if (enumClass.classInitializationMayHaveSideEffects(appView)) {
         markEnumAsUnboxable(Reason.INVALID_CLINIT, enumClass);
@@ -798,6 +851,10 @@
       }
       assert dexClass.isLibraryClass();
       if (dexClass.type != factory.enumType) {
+        // System.identityHashCode(Object) is supported for proto enums.
+        if (singleTarget == factory.javaLangSystemMethods.identityHashCode) {
+          return Reason.ELIGIBLE;
+        }
         return Reason.UNSUPPORTED_LIBRARY_CALL;
       }
       // TODO(b/147860220): EnumSet and EnumMap may be interesting to model.
@@ -805,9 +862,10 @@
         return Reason.ELIGIBLE;
       } else if (singleTarget == factory.enumMembers.equals) {
         return Reason.ELIGIBLE;
-      } else if (singleTarget == factory.enumMembers.nameMethod) {
-        return Reason.ELIGIBLE;
-      } else if (singleTarget == factory.enumMembers.toString) {
+      } else if (singleTarget == factory.enumMembers.nameMethod
+          || singleTarget == factory.enumMembers.toString) {
+        assert invokeMethod.asInvokeMethodWithReceiver().getReceiver() == enumValue;
+        requiredEnumInstanceFieldData(enumClass.type, factory.enumMembers.nameField);
         return Reason.ELIGIBLE;
       } else if (singleTarget == factory.enumMembers.ordinalMethod) {
         return Reason.ELIGIBLE;
@@ -837,6 +895,10 @@
       if (dexClass == null) {
         return Reason.INVALID_FIELD_PUT;
       }
+      if (fieldInstruction.isInstancePut()
+          && fieldInstruction.asInstancePut().object() == enumValue) {
+        return Reason.ELIGIBLE;
+      }
       // The put value has to be of the field type.
       if (field.field.type.toBaseType(factory) != enumClass.type) {
         return Reason.TYPE_MISMATCH_FIELD_PUT;
@@ -844,6 +906,14 @@
       return Reason.ELIGIBLE;
     }
 
+    if (instruction.isInstanceGet()) {
+      InstanceGet instanceGet = instruction.asInstanceGet();
+      assert instanceGet.getField().holder == enumClass.type;
+      DexField field = instanceGet.getField();
+      requiredEnumInstanceFieldData(enumClass.type, field);
+      return Reason.ELIGIBLE;
+    }
+
     // An If using enum as inValue is valid if it matches e == null
     // or e == X with X of same enum type as e. Ex: if (e == MyEnum.A).
     if (instruction.isIf()) {
@@ -923,6 +993,89 @@
     return Reason.OTHER_UNSUPPORTED_INSTRUCTION;
   }
 
+  private EnumInstanceFieldData computeEnumFieldData(
+      DexField instanceField, DexProgramClass enumClass) {
+    DexEncodedField encodedInstanceField =
+        appView.appInfo().resolveFieldOn(enumClass, instanceField).getResolvedField();
+    assert encodedInstanceField != null;
+    boolean canBeOrdinal = instanceField.type.isIntType();
+    Map<DexField, AbstractValue> data = new IdentityHashMap<>();
+    EnumValueInfoMapCollection.EnumValueInfoMap enumValueInfoMap =
+        appView.appInfo().getEnumValueInfoMap(enumClass.type);
+    for (DexField staticField : enumValueInfoMap.enumValues()) {
+      ObjectState enumInstanceState =
+          computeEnumInstanceObjectState(enumClass, staticField, enumValueInfoMap);
+      if (enumInstanceState == null) {
+        // The enum instance is effectively unused. No need to generate anything for it, the path
+        // will never be taken.
+      } else {
+        AbstractValue fieldValue = enumInstanceState.getAbstractFieldValue(encodedInstanceField);
+        if (!(fieldValue.isSingleNumberValue() || fieldValue.isSingleStringValue())) {
+          return EnumInstanceFieldUnknownData.getInstance();
+        }
+        data.put(staticField, fieldValue);
+        if (canBeOrdinal) {
+          int ordinalValue = enumValueInfoMap.getEnumValueInfo(staticField).ordinal;
+          assert fieldValue.isSingleNumberValue();
+          int computedValue = fieldValue.asSingleNumberValue().getIntValue();
+          if (computedValue != ordinalValue) {
+            canBeOrdinal = false;
+          }
+        }
+      }
+    }
+    if (canBeOrdinal) {
+      return new EnumInstanceFieldOrdinalData();
+    }
+    return new EnumInstanceFieldMappingData(data);
+  }
+
+  // We need to access the enum instance object state to figure out if it contains known constant
+  // field values. The enum instance may be accessed in two ways, directly through the enum
+  // static field, or through the enum $VALUES field. If none of them are kept, the instance is
+  // effectively unused. The object state may be stored in the enum static field optimization
+  // info, if kept, or in the $VALUES optimization info, if kept.
+  // If the enum instance is unused, this method answers null.
+  private ObjectState computeEnumInstanceObjectState(
+      DexProgramClass enumClass,
+      DexField staticField,
+      EnumValueInfoMapCollection.EnumValueInfoMap enumValueInfoMap) {
+    // Attempt 1: Get object state from the instance field's optimization info.
+    DexEncodedField encodedStaticField = enumClass.lookupStaticField(staticField);
+    AbstractValue enumInstanceValue = encodedStaticField.getOptimizationInfo().getAbstractValue();
+    if (enumInstanceValue.isSingleFieldValue()) {
+      return enumInstanceValue.asSingleFieldValue().getState();
+    }
+    if (enumInstanceValue.isUnknown()) {
+      return ObjectState.empty();
+    }
+    assert enumInstanceValue.isZero();
+
+    // Attempt 2: Get object state from the values field's optimization info.
+    DexEncodedField valuesField =
+        enumClass.lookupStaticField(
+            factory.createField(
+                enumClass.type,
+                factory.createArrayType(1, enumClass.type),
+                factory.enumValuesFieldName));
+    AbstractValue valuesValue = valuesField.getOptimizationInfo().getAbstractValue();
+    if (valuesValue.isZero()) {
+      // Unused enum instance.
+      return null;
+    }
+    if (valuesValue.isUnknown()) {
+      return ObjectState.empty();
+    }
+    assert valuesValue.isSingleFieldValue();
+    ObjectState valuesState = valuesValue.asSingleFieldValue().getState();
+    if (valuesState.isEnumValuesObjectState()) {
+      return valuesState
+          .asEnumValuesObjectState()
+          .getObjectStateForOrdinal(enumValueInfoMap.getEnumValueInfo(staticField).ordinal);
+    }
+    return ObjectState.empty();
+  }
+
   private boolean isFirstInstructionAfterArguments(InvokeMethod invokeMethod, IRCode code) {
     BasicBlock basicBlock = code.entryBlock();
     for (Instruction instruction : basicBlock.getInstructions()) {
@@ -1000,7 +1153,7 @@
     DOWN_CAST,
     SUBTYPES,
     INTERFACE,
-    INSTANCE_FIELD,
+    MANY_INSTANCE_FIELDS,
     GENERIC_INVOKE,
     DEFAULT_METHOD_INVOKE,
     UNEXPECTED_STATIC_FIELD,
@@ -1018,6 +1171,8 @@
     COMPARE_TO_INVOKE,
     UNSUPPORTED_LIBRARY_CALL,
     MISSING_INFO_MAP,
+    MISSING_INSTANCE_FIELD_DATA,
+    INVALID_FIELD_READ,
     INVALID_FIELD_PUT,
     INVALID_ARRAY_PUT,
     FIELD_PUT_ON_ENUM,
@@ -1044,7 +1199,6 @@
       // Fix all methods and fields using enums to unbox.
       for (DexProgramClass clazz : appView.appInfo().classes()) {
         if (enumsToUnbox.contains(clazz.type)) {
-          assert clazz.instanceFields().size() == 0;
           // Clear the initializers and move the static methods to the new location.
           Set<DexEncodedMethod> methodsToRemove = Sets.newIdentityHashSet();
           clazz
@@ -1123,14 +1277,23 @@
         return encodedMethod;
       }
       assert !encodedMethod.isClassInitializer();
-      DexMethod newMethod =
-          factory.createMethod(encodedMethod.holder(), newProto, encodedMethod.getName());
+      // We add the $enumunboxing$ suffix to make sure we do not create a library override.
+      String newMethodName =
+          encodedMethod.getName().toString()
+              + (encodedMethod.isNonPrivateVirtualMethod() ? "$enumunboxing$" : "");
+      DexMethod newMethod = factory.createMethod(encodedMethod.holder(), newProto, newMethodName);
       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);
+      DexEncodedMethod newEncodedMethod = encodedMethod.toTypeSubstitutedMethod(newMethod);
+      assert !encodedMethod.isLibraryMethodOverride().isTrue()
+          : "Enum unboxing is changing the signature of a library override in a non unboxed class.";
+      if (newEncodedMethod.isNonPrivateVirtualMethod()) {
+        newEncodedMethod.setLibraryMethodOverride(OptionalBool.FALSE);
+      }
+      return newEncodedMethod;
     }
 
     private DexMethod ensureUniqueMethod(DexEncodedMethod encodedMethod, DexMethod newMethod) {
@@ -1213,7 +1376,7 @@
 
   private static class EnumUnboxingLens extends NestedGraphLens {
 
-    private final Map<DexMethod, RewrittenPrototypeDescription> prototypeChanges;
+    private final Map<DexMethod, RewrittenPrototypeDescription> prototypeChangesPerMethod;
     private final Set<DexType> unboxedEnums;
 
     EnumUnboxingLens(
@@ -1224,7 +1387,7 @@
         BiMap<DexMethod, DexMethod> originalMethodSignatures,
         GraphLens previousLens,
         DexItemFactory dexItemFactory,
-        Map<DexMethod, RewrittenPrototypeDescription> prototypeChanges,
+        Map<DexMethod, RewrittenPrototypeDescription> prototypeChangesPerMethod,
         Set<DexType> unboxedEnums) {
       super(
           typeMap,
@@ -1234,17 +1397,18 @@
           originalMethodSignatures,
           previousLens,
           dexItemFactory);
-      this.prototypeChanges = prototypeChanges;
+      this.prototypeChangesPerMethod = prototypeChangesPerMethod;
       this.unboxedEnums = unboxedEnums;
     }
 
     @Override
-    public RewrittenPrototypeDescription lookupPrototypeChanges(DexMethod method) {
+    protected RewrittenPrototypeDescription internalDescribePrototypeChanges(
+        RewrittenPrototypeDescription prototypeChanges, DexMethod method) {
       // During the second IR processing enum unboxing is the only optimization rewriting
       // prototype description, if this does not hold, remove the assertion and merge
       // the two prototype changes.
-      assert previousLens.lookupPrototypeChanges(method).isEmpty();
-      return prototypeChanges.getOrDefault(method, RewrittenPrototypeDescription.none());
+      assert prototypeChanges.isEmpty();
+      return prototypeChangesPerMethod.getOrDefault(method, RewrittenPrototypeDescription.none());
     }
 
     @Override
@@ -1264,7 +1428,7 @@
 
     private static class Builder extends NestedGraphLens.Builder {
 
-      private Map<DexMethod, RewrittenPrototypeDescription> prototypeChanges =
+      private Map<DexMethod, RewrittenPrototypeDescription> prototypeChangesPerMethod =
           new IdentityHashMap<>();
 
       public void move(DexMethod from, boolean fromStatic, DexMethod to, boolean toStatic) {
@@ -1299,7 +1463,7 @@
             from.proto.returnType == to.proto.returnType
                 ? null
                 : new RewrittenTypeInfo(from.proto.returnType, to.proto.returnType);
-        prototypeChanges.put(
+        prototypeChangesPerMethod.put(
             to,
             RewrittenPrototypeDescription.createForRewrittenTypes(returnInfo, builder.build())
                 .withExtraUnusedNullParameters(numberOfExtraNullParameters));
@@ -1318,7 +1482,7 @@
             originalMethodSignatures,
             previousLens,
             dexItemFactory,
-            ImmutableMap.copyOf(prototypeChanges),
+            ImmutableMap.copyOf(prototypeChangesPerMethod),
             ImmutableSet.copyOf(unboxedEnums));
       }
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
index c4a6bb4..b5a5b62 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
@@ -22,6 +22,11 @@
 
 class EnumUnboxingCandidateAnalysis {
 
+  // Each time we unbox an enum with instance fields, we need to generate a method with a
+  // switch case to dispatch from the enum to the instance field value. We introduce this heuristic
+  // to avoid unboxing enums with too many instance fields.
+  private static final int MAX_INSTANCE_FIELDS_FOR_UNBOXING = 7;
+
   private final AppView<AppInfoWithLiveness> appView;
   private final EnumUnboxer enumUnboxer;
   private final DexItemFactory factory;
@@ -55,8 +60,8 @@
       enumUnboxer.reportFailure(clazz.type, Reason.SUBTYPES);
       return false;
     }
-    if (!clazz.instanceFields().isEmpty()) {
-      enumUnboxer.reportFailure(clazz.type, Reason.INSTANCE_FIELD);
+    if (clazz.instanceFields().size() > MAX_INSTANCE_FIELDS_FOR_UNBOXING) {
+      enumUnboxer.reportFailure(clazz.type, Reason.MANY_INSTANCE_FIELDS);
       return false;
     }
     if (!enumHasBasicStaticFields(clazz)) {
@@ -69,23 +74,9 @@
       enumUnboxer.reportFailure(clazz.type, Reason.MISSING_INFO_MAP);
       return false;
     }
-
-    // TODO(b/155036467): Fail lazily when an unsupported method is not only present but also used.
-    // Only Enums with default initializers and static methods can be unboxed at the moment.
-    for (DexEncodedMethod directMethod : clazz.directMethods()) {
-      if (directMethod.isInstanceInitializer() && !isStandardEnumInitializer(directMethod)) {
-        enumUnboxer.reportFailure(clazz.type, Reason.INVALID_INIT);
-        return false;
-      }
-    }
     return true;
   }
 
-  private boolean isStandardEnumInitializer(DexEncodedMethod method) {
-    return method.isInstanceInitializer()
-        && method.method.proto == factory.enumMembers.constructor.proto;
-  }
-
   // The enum should have the $VALUES static field and only fields directly referencing the enum
   // instances.
   private boolean enumHasBasicStaticFields(DexProgramClass clazz) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
index 087e808..54d9aaf 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingRewriter.java
@@ -27,12 +27,15 @@
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.ir.analysis.type.ArrayTypeElement;
+import com.android.tools.r8.ir.analysis.type.Nullability;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.ArrayAccess;
 import com.android.tools.r8.ir.code.ConstNumber;
 import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstanceGet;
 import com.android.tools.r8.ir.code.Instruction;
 import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.InvokeStatic;
 import com.android.tools.r8.ir.code.MemberType;
@@ -40,6 +43,7 @@
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.IRConverter;
+import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldKnownData;
 import com.android.tools.r8.ir.synthetic.EnumUnboxingCfCodeProvider;
 import com.android.tools.r8.origin.SynthesizedOrigin;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -65,6 +69,7 @@
   private final AppView<AppInfoWithLiveness> appView;
   private final DexItemFactory factory;
   private final EnumValueInfoMapCollection enumsToUnbox;
+  private final EnumInstanceFieldDataMap unboxedEnumsInstanceFieldData;
   private final Map<DexMethod, DexEncodedMethod> utilityMethods = new ConcurrentHashMap<>();
   private final Map<DexField, DexEncodedField> extraUtilityFields = new ConcurrentHashMap<>();
 
@@ -73,14 +78,19 @@
   private final DexMethod compareToUtilityMethod;
   private final DexMethod valuesUtilityMethod;
 
-  EnumUnboxingRewriter(AppView<AppInfoWithLiveness> appView, Set<DexType> enumsToUnbox) {
+  EnumUnboxingRewriter(
+      AppView<AppInfoWithLiveness> appView,
+      Set<DexType> enumsToUnbox,
+      EnumInstanceFieldDataMap unboxedEnumsInstanceFieldData) {
     this.appView = appView;
     this.factory = appView.dexItemFactory();
     EnumValueInfoMapCollection.Builder builder = EnumValueInfoMapCollection.builder();
     for (DexType toUnbox : enumsToUnbox) {
+      assert appView.appInfo().withLiveness().getEnumValueInfoMap(toUnbox) != null;
       builder.put(toUnbox, appView.appInfo().withLiveness().getEnumValueInfoMap(toUnbox));
     }
     this.enumsToUnbox = builder.build();
+    this.unboxedEnumsInstanceFieldData = unboxedEnumsInstanceFieldData;
 
     // Custom methods for java.lang.Enum methods ordinal, equals and compareTo.
     this.ordinalUtilityMethod =
@@ -144,7 +154,8 @@
             continue;
           } else if (invokedMethod == factory.enumMembers.nameMethod
               || invokedMethod == factory.enumMembers.toString) {
-            DexMethod toStringMethod = computeDefaultToStringUtilityMethod(enumType);
+            DexMethod toStringMethod =
+                computeInstanceFieldUtilityMethod(enumType, factory.enumMembers.nameField);
             iterator.replaceCurrentInstruction(
                 new InvokeStatic(
                     toStringMethod, invokeMethod.outValue(), invokeMethod.arguments()));
@@ -176,16 +187,22 @@
             convertedEnums.put(invoke, enumType);
             continue;
           }
+        } else if (invokedMethod == factory.javaLangSystemMethods.identityHashCode) {
+          assert invokeStatic.inValues().size() == 1;
+          Value argument = invokeStatic.getArgument(0);
+          DexType enumType = getEnumTypeOrNull(argument, convertedEnums);
+          if (enumType != null) {
+            invokeStatic.outValue().replaceUsers(argument);
+            iterator.removeOrReplaceByDebugLocalRead();
+          }
         }
       }
-      // Rewrites direct access to enum values into the corresponding int, $VALUES is not
-      // supported.
       if (instruction.isStaticGet()) {
         StaticGet staticGet = instruction.asStaticGet();
         DexType holder = staticGet.getField().holder;
         if (enumsToUnbox.containsEnum(holder)) {
           if (staticGet.outValue() == null) {
-            iterator.removeInstructionIgnoreOutValue();
+            iterator.removeOrReplaceByDebugLocalRead();
             continue;
           }
           EnumValueInfoMap enumValueInfoMap = enumsToUnbox.getEnumValueInfoMap(holder);
@@ -218,6 +235,26 @@
           }
         }
       }
+
+      if (instruction.isInstanceGet()) {
+        InstanceGet instanceGet = instruction.asInstanceGet();
+        DexType holder = instanceGet.getField().holder;
+        if (enumsToUnbox.containsEnum(holder)) {
+          DexMethod fieldMethod = computeInstanceFieldMethod(instanceGet.getField());
+          Value rewrittenOutValue =
+              code.createValue(
+                  TypeElement.fromDexType(
+                      fieldMethod.proto.returnType, Nullability.maybeNull(), appView));
+          InvokeStatic invoke =
+              new InvokeStatic(
+                  fieldMethod, rewrittenOutValue, ImmutableList.of(instanceGet.object()));
+          iterator.replaceCurrentInstruction(invoke);
+          if (enumsToUnbox.containsEnum(instanceGet.getField().type)) {
+            convertedEnums.put(invoke, instanceGet.getField().type);
+          }
+        }
+      }
+
       // Rewrite array accesses from MyEnum[] (OBJECT) to int[] (INT).
       if (instruction.isArrayAccess()) {
         ArrayAccess arrayAccess = instruction.asArrayAccess();
@@ -234,9 +271,19 @@
     return affectedPhis;
   }
 
+  private DexMethod computeInstanceFieldMethod(DexField field) {
+    EnumInstanceFieldKnownData enumFieldKnownData =
+        unboxedEnumsInstanceFieldData.getInstanceFieldData(field.holder, field);
+    if (enumFieldKnownData.isOrdinal()) {
+      utilityMethods.computeIfAbsent(ordinalUtilityMethod, m -> synthesizeOrdinalMethod());
+      return ordinalUtilityMethod;
+    }
+    return computeInstanceFieldUtilityMethod(field.holder, field);
+  }
+
   private void replaceEnumInvoke(
       InstructionListIterator iterator,
-      InvokeMethodWithReceiver invokeMethod,
+      InvokeMethod invokeMethod,
       DexMethod method,
       Function<DexMethod, DexEncodedMethod> synthesizor) {
     utilityMethods.computeIfAbsent(method, synthesizor);
@@ -309,6 +356,25 @@
     return synthesizeUtilityMethod(cfCode, method, true);
   }
 
+  private DexMethod computeInstanceFieldUtilityMethod(DexType enumType, DexField field) {
+    assert enumsToUnbox.containsEnum(enumType);
+    assert field.holder == enumType || field.holder == factory.enumType;
+    String methodName =
+        "get"
+            + (enumType == field.holder ? "" : "Enum$")
+            + field.name
+            + "$$"
+            + compatibleName(enumType);
+    DexMethod fieldMethod =
+        factory.createMethod(
+            factory.enumUnboxingUtilityType,
+            factory.createProto(field.type, factory.intType),
+            methodName);
+    utilityMethods.computeIfAbsent(
+        fieldMethod, m -> synthesizeInstanceFieldMethod(m, enumType, field));
+    return fieldMethod;
+  }
+
   private DexMethod computeValueOfUtilityMethod(DexType type) {
     assert enumsToUnbox.containsEnum(type);
     DexMethod valueOf =
@@ -320,17 +386,6 @@
     return valueOf;
   }
 
-  private DexMethod computeDefaultToStringUtilityMethod(DexType type) {
-    assert enumsToUnbox.containsEnum(type);
-    DexMethod toString =
-        factory.createMethod(
-            factory.enumUnboxingUtilityType,
-            factory.createProto(factory.stringType, factory.intType),
-            "toString" + compatibleName(type));
-    utilityMethods.computeIfAbsent(toString, m -> synthesizeToStringUtilityMethod(m, type));
-    return toString;
-  }
-
   private DexType getEnumTypeOrNull(ArrayAccess arrayAccess) {
     ArrayTypeElement arrayType = arrayAccess.array().getType().asArrayType();
     if (arrayType == null) {
@@ -397,24 +452,37 @@
         DexProgramClass::checksumFromType);
   }
 
-  private DexEncodedMethod synthesizeToStringUtilityMethod(DexMethod method, DexType enumType) {
+  private DexEncodedMethod synthesizeInstanceFieldMethod(
+      DexMethod method, DexType enumType, DexField field) {
+    assert method.proto.returnType == field.type;
+    assert unboxedEnumsInstanceFieldData.getInstanceFieldData(enumType, field).isMapping();
     CfCode cfCode =
-        new EnumUnboxingCfCodeProvider.EnumUnboxingDefaultToStringCfCodeProvider(
+        new EnumUnboxingCfCodeProvider.EnumUnboxingInstanceFieldCfCodeProvider(
                 appView,
                 factory.enumUnboxingUtilityType,
-                enumType,
-                enumsToUnbox.getEnumValueInfoMap(enumType))
+                field.type,
+                enumsToUnbox.getEnumValueInfoMap(enumType),
+                unboxedEnumsInstanceFieldData
+                    .getInstanceFieldData(enumType, field)
+                    .asEnumFieldMappingData())
             .generateCfCode();
     return synthesizeUtilityMethod(cfCode, method, false);
   }
 
   private DexEncodedMethod synthesizeValueOfUtilityMethod(DexMethod method, DexType enumType) {
+    assert method.proto.returnType == factory.intType;
+    assert unboxedEnumsInstanceFieldData
+        .getInstanceFieldData(enumType, factory.enumMembers.nameField)
+        .isMapping();
     CfCode cfCode =
         new EnumUnboxingCfCodeProvider.EnumUnboxingValueOfCfCodeProvider(
                 appView,
                 factory.enumUnboxingUtilityType,
                 enumType,
-                enumsToUnbox.getEnumValueInfoMap(enumType))
+                enumsToUnbox.getEnumValueInfoMap(enumType),
+                unboxedEnumsInstanceFieldData
+                    .getInstanceFieldData(enumType, factory.enumMembers.nameField)
+                    .asEnumFieldMappingData())
             .generateCfCode();
     return synthesizeUtilityMethod(cfCode, method, false);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java b/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java
index 5a50ca4..8b35793 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.cf.code.CfStackInstruction;
 import com.android.tools.r8.cf.code.CfStackInstruction.Opcode;
 import com.android.tools.r8.cf.code.CfThrow;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.DexField;
@@ -26,8 +27,10 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.EnumValueInfoMapCollection.EnumValueInfoMap;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.ValueType;
+import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldMappingData;
 import java.util.ArrayList;
 import java.util.List;
 import org.objectweb.asm.Opcodes;
@@ -38,41 +41,64 @@
     super(appView, holder);
   }
 
-  public static class EnumUnboxingDefaultToStringCfCodeProvider extends EnumUnboxingCfCodeProvider {
+  void addCfInstructionsForAbstractValue(
+      List<CfInstruction> instructions, AbstractValue value, DexType returnType) {
+    // TODO(b/155368026): Support fields and const class fields.
+    // Move this to something similar than SingleValue#createMaterializingInstruction
+    if (value.isSingleStringValue()) {
+      assert returnType == appView.dexItemFactory().stringType;
+      instructions.add(new CfConstString(value.asSingleStringValue().getDexString()));
+    } else if (value.isSingleNumberValue()) {
+      instructions.add(
+          new CfConstNumber(
+              value.asSingleNumberValue().getValue(), ValueType.fromDexType(returnType)));
+    } else {
+      throw new Unreachable("Only Number and String fields in enums are supported.");
+    }
+  }
 
-    private DexType enumType;
-    private EnumValueInfoMap map;
+  public static class EnumUnboxingInstanceFieldCfCodeProvider extends EnumUnboxingCfCodeProvider {
 
-    public EnumUnboxingDefaultToStringCfCodeProvider(
-        AppView<?> appView, DexType holder, DexType enumType, EnumValueInfoMap map) {
+    private final DexType returnType;
+    private final EnumValueInfoMap enumValueInfoMap;
+    private final EnumInstanceFieldMappingData fieldDataMap;
+
+    public EnumUnboxingInstanceFieldCfCodeProvider(
+        AppView<?> appView,
+        DexType holder,
+        DexType returnType,
+        EnumValueInfoMap enumValueInfoMap,
+        EnumInstanceFieldMappingData fieldDataMap) {
       super(appView, holder);
-      this.enumType = enumType;
-      this.map = map;
+      this.returnType = returnType;
+      this.enumValueInfoMap = enumValueInfoMap;
+      this.fieldDataMap = fieldDataMap;
     }
 
     @Override
     public CfCode generateCfCode() {
-      // Generated static method, for class com.x.MyEnum {A,B} would look like:
+      // Generated static method, for class com.x.MyEnum {A(10),B(20);} would look like:
       // String UtilityClass#com.x.MyEnum_toString(int i) {
-      // if (i == 1) { return "A";}
-      // if (i == 2) { return "B";}
+      // if (i == 1) { return 10;}
+      // if (i == 2) { return 20;}
       // throw null;
       DexItemFactory factory = appView.dexItemFactory();
       List<CfInstruction> instructions = new ArrayList<>();
 
-      // if (i == 1) { return "A";}
-      // if (i == 2) { return "B";}
-      map.forEach(
+      // if (i == 1) { return 10;}
+      // if (i == 2) { return 20;}
+      enumValueInfoMap.forEach(
           (field, enumValueInfo) -> {
-            CfLabel dest = new CfLabel();
-            instructions.add(new CfLoad(ValueType.fromDexType(factory.intType), 0));
-            instructions.add(new CfConstNumber(enumValueInfo.convertToInt(), ValueType.INT));
-            instructions.add(new CfIfCmp(If.Type.NE, ValueType.INT, dest));
-            // TODO(b/160939354): Should use the value passed to the enum constructor, since this
-            //  value may be different from the enum field name.
-            instructions.add(new CfConstString(field.name));
-            instructions.add(new CfReturn(ValueType.OBJECT));
-            instructions.add(dest);
+            AbstractValue value = fieldDataMap.getData(field);
+            if (value != null) {
+              CfLabel dest = new CfLabel();
+              instructions.add(new CfLoad(ValueType.fromDexType(factory.intType), 0));
+              instructions.add(new CfConstNumber(enumValueInfo.convertToInt(), ValueType.INT));
+              instructions.add(new CfIfCmp(If.Type.NE, ValueType.INT, dest));
+              addCfInstructionsForAbstractValue(instructions, value, returnType);
+              instructions.add(new CfReturn(ValueType.fromDexType(returnType)));
+              instructions.add(dest);
+            }
           });
 
       // throw null;
@@ -87,12 +113,18 @@
 
     private DexType enumType;
     private EnumValueInfoMap map;
+    private final EnumInstanceFieldMappingData fieldDataMap;
 
     public EnumUnboxingValueOfCfCodeProvider(
-        AppView<?> appView, DexType holder, DexType enumType, EnumValueInfoMap map) {
+        AppView<?> appView,
+        DexType holder,
+        DexType enumType,
+        EnumValueInfoMap map,
+        EnumInstanceFieldMappingData fieldDataMap) {
       super(appView, holder);
       this.enumType = enumType;
       this.map = map;
+      this.fieldDataMap = fieldDataMap;
     }
 
     @Override
@@ -125,9 +157,8 @@
           (field, enumValueInfo) -> {
             CfLabel dest = new CfLabel();
             instructions.add(new CfLoad(ValueType.fromDexType(factory.stringType), 0));
-            // TODO(b/160939354): Should use the value passed to the enum constructor, since this
-            //  value may be different from the enum field name.
-            instructions.add(new CfConstString(field.name));
+            AbstractValue value = fieldDataMap.getData(field);
+            addCfInstructionsForAbstractValue(instructions, value, factory.stringType);
             instructions.add(
                 new CfInvoke(Opcodes.INVOKEVIRTUAL, factory.stringMembers.equals, false));
             instructions.add(new CfIf(If.Type.EQ, ValueType.INT, dest));
diff --git a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
index 7e98858..8c895f7 100644
--- a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
@@ -78,18 +78,16 @@
   public final ProguardMapSupplier proguardMapSupplier;
 
   public CfApplicationWriter(
-      DexApplication application,
       AppView<?> appView,
-      InternalOptions options,
       Marker marker,
       GraphLens graphLens,
       NamingLens namingLens,
       ProguardMapSupplier proguardMapSupplier) {
-    this.application = application;
+    this.application = appView.appInfo().app();
     this.appView = appView;
     this.graphLens = graphLens;
     this.namingLens = namingLens;
-    this.options = options;
+    this.options = appView.options();
     assert marker != null;
     this.marker = marker;
     this.proguardMapSupplier = proguardMapSupplier;
diff --git a/src/main/java/com/android/tools/r8/naming/SourceFileRewriter.java b/src/main/java/com/android/tools/r8/naming/SourceFileRewriter.java
index e2bf357..b8ddc06 100644
--- a/src/main/java/com/android/tools/r8/naming/SourceFileRewriter.java
+++ b/src/main/java/com/android/tools/r8/naming/SourceFileRewriter.java
@@ -83,9 +83,17 @@
 
   private DexString getSourceFileRenaming(ProguardConfiguration proguardConfiguration) {
     // If we should not be keeping the source file, null it out.
-    if (!appView.options().forceProguardCompatibility
-        && !proguardConfiguration.getKeepAttributes().sourceFile) {
-      return null;
+    if (!proguardConfiguration.getKeepAttributes().sourceFile) {
+      // For class files, we always remove the attribute
+      if (appView.options().isGeneratingClassFiles()) {
+        return null;
+      }
+      assert appView.options().isGeneratingDex();
+      // When generating DEX we only remove the attribute for full-mode to ensure that we get
+      // line-numbers printed in stack traces.
+      if (!appView.options().forceProguardCompatibility) {
+        return null;
+      }
     }
 
     String renamedSourceFileAttribute = proguardConfiguration.getRenameSourceFileAttribute();
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
index 8883f5b..2e55916 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
@@ -87,14 +87,16 @@
 
   @Override
   public GraphLensLookupResult lookupMethod(DexMethod method, DexMethod context, Type type) {
-    GraphLensLookupResult previous = previousLens.lookupMethod(method, context, type);
+    GraphLensLookupResult lookup = previousLens.lookupMethod(method, context, type);
     Map<DexMethod, DexMethod> methodMap = methodMaps.getOrDefault(type, Collections.emptyMap());
-    DexMethod newMethod = methodMap.get(previous.getMethod());
-    if (newMethod != null) {
-      return new GraphLensLookupResult(
-          newMethod, mapInvocationType(newMethod, method, previous.getType()));
+    DexMethod newMethod = methodMap.get(lookup.getMethod());
+    if (newMethod == null) {
+      return lookup;
     }
-    return previous;
+    return new GraphLensLookupResult(
+        newMethod,
+        mapInvocationType(newMethod, method, lookup.getType()),
+        lookup.getPrototypeChanges());
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/optimize/PublicizerLens.java b/src/main/java/com/android/tools/r8/optimize/PublicizerLens.java
index a2c44ce..1e8eff2 100644
--- a/src/main/java/com/android/tools/r8/optimize/PublicizerLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/PublicizerLens.java
@@ -41,14 +41,13 @@
 
   @Override
   public GraphLensLookupResult lookupMethod(DexMethod method, DexMethod context, Type type) {
-    GraphLensLookupResult previous = previousLens.lookupMethod(method, context, type);
-    method = previous.getMethod();
-    type = previous.getType();
-    if (type == Type.DIRECT && publicizedMethods.contains(method)) {
-      assert publicizedMethodIsPresentOnHolder(method, context);
-      return new GraphLensLookupResult(method, Type.VIRTUAL);
+    GraphLensLookupResult lookup = previousLens.lookupMethod(method, context, type);
+    if (lookup.getType() == Type.DIRECT && publicizedMethods.contains(lookup.getMethod())) {
+      assert publicizedMethodIsPresentOnHolder(lookup.getMethod(), context);
+      return new GraphLensLookupResult(
+          lookup.getMethod(), Type.VIRTUAL, lookup.getPrototypeChanges());
     }
-    return super.lookupMethod(method, context, type);
+    return lookup;
   }
 
   private boolean publicizedMethodIsPresentOnHolder(DexMethod method, DexMethod context) {
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 e45afd6..bf2bfe8 100644
--- a/src/main/java/com/android/tools/r8/relocator/Relocator.java
+++ b/src/main/java/com/android/tools/r8/relocator/Relocator.java
@@ -89,9 +89,7 @@
       new GenericSignatureRewriter(appView, namingLens).run(appInfo.classes(), executor);
 
       new CfApplicationWriter(
-              app,
               appView,
-              options,
               new Marker(Tool.Relocator),
               GraphLens.getIdentityLens(),
               namingLens,
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 9e709fd..17cf952 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -1140,7 +1140,9 @@
         // if I has a supertype J. This is due to the fact that invoke-super instructions that
         // resolve to a method on an interface never hit an implementation below that interface.
         deferredRenamings.mapVirtualMethodToDirectInType(
-            oldTarget, new GraphLensLookupResult(newTarget, STATIC), target.type);
+            oldTarget,
+            prototypeChanges -> new GraphLensLookupResult(newTarget, STATIC, prototypeChanges),
+            target.type);
       } else {
         // If we merge class B into class C, and class C contains an invocation super.m(), then it
         // is insufficient to rewrite "invoke-super B.m()" to "invoke-direct C.m$B()" (the method
@@ -1159,7 +1161,9 @@
                   || appInfo.lookupSuperTarget(signatureInHolder, holder) != null;
           if (resolutionSucceeds) {
             deferredRenamings.mapVirtualMethodToDirectInType(
-                signatureInHolder, new GraphLensLookupResult(newTarget, DIRECT), target.type);
+                signatureInHolder,
+                prototypeChanges -> new GraphLensLookupResult(newTarget, DIRECT, prototypeChanges),
+                target.type);
           } else {
             break;
           }
@@ -1181,7 +1185,10 @@
                       || appInfo.lookupSuperTarget(signatureInHolder, holder) != null;
               if (resolutionSucceededBeforeMerge) {
                 deferredRenamings.mapVirtualMethodToDirectInType(
-                    signatureInType, new GraphLensLookupResult(newTarget, DIRECT), target.type);
+                    signatureInType,
+                    prototypeChanges ->
+                        new GraphLensLookupResult(newTarget, DIRECT, prototypeChanges),
+                    target.type);
               }
             }
           }
@@ -1733,27 +1740,26 @@
       // First look up the method using the existing graph lens (for example, the type will have
       // changed if the method was publicized by ClassAndMemberPublicizer).
       GraphLensLookupResult lookup = appView.graphLens().lookupMethod(method, context, type);
-      DexMethod previousMethod = lookup.getMethod();
-      Type previousType = lookup.getType();
       // Then check if there is a renaming due to the vertical class merger.
-      DexMethod newMethod = renamedMembersLens.methodMap.get(previousMethod);
-      if (newMethod != null) {
-        if (previousType == Type.INTERFACE) {
-          // If an interface has been merged into a class, invoke-interface needs to be translated
-          // to invoke-virtual.
-          DexClass clazz = appInfo.definitionFor(newMethod.holder);
-          if (clazz != null && !clazz.accessFlags.isInterface()) {
-            assert appInfo.definitionFor(method.holder).accessFlags.isInterface();
-            return new GraphLensLookupResult(newMethod, Type.VIRTUAL);
-          }
-        }
-        return new GraphLensLookupResult(newMethod, previousType);
+      DexMethod newMethod = renamedMembersLens.methodMap.get(lookup.getMethod());
+      if (newMethod == null) {
+        return lookup;
       }
-      return new GraphLensLookupResult(previousMethod, previousType);
+      if (lookup.getType() == Type.INTERFACE) {
+        // If an interface has been merged into a class, invoke-interface needs to be translated
+        // to invoke-virtual.
+        DexClass clazz = appInfo.definitionFor(newMethod.holder);
+        if (clazz != null && !clazz.accessFlags.isInterface()) {
+          assert appInfo.definitionFor(method.holder).accessFlags.isInterface();
+          return new GraphLensLookupResult(newMethod, Type.VIRTUAL, lookup.getPrototypeChanges());
+        }
+      }
+      return new GraphLensLookupResult(newMethod, lookup.getType(), lookup.getPrototypeChanges());
     }
 
     @Override
-    public RewrittenPrototypeDescription lookupPrototypeChanges(DexMethod method) {
+    public RewrittenPrototypeDescription lookupPrototypeChangesForMethodDefinition(
+        DexMethod method) {
       throw new Unreachable();
     }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
index db59152..683f1c9 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
+import com.android.tools.r8.graph.RewrittenPrototypeDescription;
 import com.android.tools.r8.ir.code.Invoke.Type;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
@@ -47,9 +48,14 @@
 // For the invocation "invoke-virtual A.m()" in B.m2, this graph lens will return the method B.m.
 public class VerticalClassMergerGraphLens extends NestedGraphLens {
 
+  interface GraphLensLookupResultProvider {
+
+    abstract GraphLensLookupResult get(RewrittenPrototypeDescription prototypeChanges);
+  }
+
   private final AppView<?> appView;
 
-  private final Map<DexType, Map<DexMethod, GraphLensLookupResult>>
+  private final Map<DexType, Map<DexMethod, GraphLensLookupResultProvider>>
       contextualVirtualToDirectMethodMaps;
   private Set<DexMethod> mergedMethods;
   private final Map<DexMethod, DexMethod> originalMethodSignaturesForBridges;
@@ -60,7 +66,8 @@
       Map<DexField, DexField> fieldMap,
       Map<DexMethod, DexMethod> methodMap,
       Set<DexMethod> mergedMethods,
-      Map<DexType, Map<DexMethod, GraphLensLookupResult>> contextualVirtualToDirectMethodMaps,
+      Map<DexType, Map<DexMethod, GraphLensLookupResultProvider>>
+          contextualVirtualToDirectMethodMaps,
       BiMap<DexField, DexField> originalFieldSignatures,
       BiMap<DexMethod, DexMethod> originalMethodSignatures,
       Map<DexMethod, DexMethod> originalMethodSignaturesForBridges,
@@ -98,24 +105,31 @@
         originalMethodSignaturesForBridges.containsKey(context)
             ? originalMethodSignaturesForBridges.get(context)
             : originalMethodSignatures.getOrDefault(context, context);
-    GraphLensLookupResult previous = previousLens.lookupMethod(method, previousContext, type);
-    if (previous.getType() == Type.SUPER && !mergedMethods.contains(context)) {
-      Map<DexMethod, GraphLensLookupResult> virtualToDirectMethodMap =
+    GraphLensLookupResult lookup = previousLens.lookupMethod(method, previousContext, type);
+    if (lookup.getType() == Type.SUPER && !mergedMethods.contains(context)) {
+      Map<DexMethod, GraphLensLookupResultProvider> virtualToDirectMethodMap =
           contextualVirtualToDirectMethodMaps.get(context.holder);
       if (virtualToDirectMethodMap != null) {
-        GraphLensLookupResult lookup = virtualToDirectMethodMap.get(previous.getMethod());
-        if (lookup != null) {
+        GraphLensLookupResultProvider result = virtualToDirectMethodMap.get(lookup.getMethod());
+        if (result != null) {
           // If the super class A of the enclosing class B (i.e., context.holder())
           // has been merged into B during vertical class merging, and this invoke-super instruction
           // was resolving to a method in A, then the target method has been changed to a direct
           // method and moved into B, so that we need to use an invoke-direct instruction instead of
           // invoke-super (or invoke-static, if the method was originally a default interface
           // method).
-          return lookup;
+          return result.get(lookup.getPrototypeChanges());
         }
       }
     }
-    return super.lookupMethod(previous.getMethod(), context, previous.getType());
+    DexMethod newMethod = methodMap.get(lookup.getMethod());
+    if (newMethod == null) {
+      return lookup;
+    }
+    return new GraphLensLookupResult(
+        newMethod,
+        mapInvocationType(newMethod, lookup.getMethod(), lookup.getType()),
+        internalDescribePrototypeChanges(lookup.getPrototypeChanges(), newMethod));
   }
 
   @Override
@@ -144,7 +158,7 @@
     protected final BiMap<DexField, DexField> fieldMap = HashBiMap.create();
     protected final Map<DexMethod, DexMethod> methodMap = new IdentityHashMap<>();
     private final ImmutableSet.Builder<DexMethod> mergedMethodsBuilder = ImmutableSet.builder();
-    private final Map<DexType, Map<DexMethod, GraphLensLookupResult>>
+    private final Map<DexType, Map<DexMethod, GraphLensLookupResultProvider>>
         contextualVirtualToDirectMethodMaps = new IdentityHashMap<>();
 
     private final BiMap<DexMethod, DexMethod> originalMethodSignatures = HashBiMap.create();
@@ -173,17 +187,22 @@
         newBuilder.markMethodAsMerged(
             builder.getMethodSignatureAfterClassMerging(method, mergedClasses));
       }
-      for (Map.Entry<DexType, Map<DexMethod, GraphLensLookupResult>> entry :
+      for (Map.Entry<DexType, Map<DexMethod, GraphLensLookupResultProvider>> entry :
           builder.contextualVirtualToDirectMethodMaps.entrySet()) {
         DexType context = entry.getKey();
         assert context == builder.getTypeAfterClassMerging(context, mergedClasses);
-        for (Map.Entry<DexMethod, GraphLensLookupResult> innerEntry : entry.getValue().entrySet()) {
+        for (Map.Entry<DexMethod, GraphLensLookupResultProvider> innerEntry :
+            entry.getValue().entrySet()) {
           DexMethod from = innerEntry.getKey();
-          GraphLensLookupResult rewriting = innerEntry.getValue();
+          GraphLensLookupResult rewriting =
+              innerEntry.getValue().get(RewrittenPrototypeDescription.none());
           DexMethod to =
               builder.getMethodSignatureAfterClassMerging(rewriting.getMethod(), mergedClasses);
           newBuilder.mapVirtualMethodToDirectInType(
-              from, new GraphLensLookupResult(to, rewriting.getType()), context);
+              from,
+              prototypeChanges ->
+                  new GraphLensLookupResult(to, rewriting.getType(), prototypeChanges),
+              context);
         }
       }
       for (Map.Entry<DexMethod, DexMethod> entry : builder.originalMethodSignatures.entrySet()) {
@@ -267,7 +286,7 @@
     }
 
     public boolean hasMappingForSignatureInContext(DexProgramClass context, DexMethod signature) {
-      Map<DexMethod, GraphLensLookupResult> virtualToDirectMethodMap =
+      Map<DexMethod, GraphLensLookupResultProvider> virtualToDirectMethodMap =
           contextualVirtualToDirectMethodMaps.get(context.type);
       if (virtualToDirectMethodMap != null) {
         return virtualToDirectMethodMap.containsKey(signature);
@@ -306,8 +325,8 @@
     }
 
     public void mapVirtualMethodToDirectInType(
-        DexMethod from, GraphLensLookupResult to, DexType type) {
-      Map<DexMethod, GraphLensLookupResult> virtualToDirectMethodMap =
+        DexMethod from, GraphLensLookupResultProvider to, DexType type) {
+      Map<DexMethod, GraphLensLookupResultProvider> virtualToDirectMethodMap =
           contextualVirtualToDirectMethodMaps.computeIfAbsent(type, key -> new IdentityHashMap<>());
       virtualToDirectMethodMap.put(from, to);
     }
@@ -319,9 +338,9 @@
       originalMethodSignatures.putAll(builder.originalMethodSignatures);
       originalMethodSignaturesForBridges.putAll(builder.originalMethodSignaturesForBridges);
       for (DexType context : builder.contextualVirtualToDirectMethodMaps.keySet()) {
-        Map<DexMethod, GraphLensLookupResult> current =
+        Map<DexMethod, GraphLensLookupResultProvider> current =
             contextualVirtualToDirectMethodMaps.get(context);
-        Map<DexMethod, GraphLensLookupResult> other =
+        Map<DexMethod, GraphLensLookupResultProvider> other =
             builder.contextualVirtualToDirectMethodMaps.get(context);
         if (current != null) {
           current.putAll(other);
diff --git a/src/main/java/com/android/tools/r8/utils/ArchiveBuilder.java b/src/main/java/com/android/tools/r8/utils/ArchiveBuilder.java
index dfb625b..9a521fb 100644
--- a/src/main/java/com/android/tools/r8/utils/ArchiveBuilder.java
+++ b/src/main/java/com/android/tools/r8/utils/ArchiveBuilder.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.origin.PathOrigin;
 import com.google.common.io.ByteStreams;
+import java.io.BufferedOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.file.Files;
@@ -80,8 +81,11 @@
     if (stream != null) {
       return stream;
     }
-    stream = new ZipOutputStream(Files.newOutputStream(
-        archive, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING));
+    stream =
+        new ZipOutputStream(
+            new BufferedOutputStream(
+                Files.newOutputStream(
+                    archive, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)));
     return stream;
   }
 
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 bb51cc0..7f54137 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -1236,6 +1236,7 @@
     public boolean forceLibBackportsInL8CfToCf = false;
     public boolean enumUnboxingRewriteJavaCGeneratedMethod = false;
     public boolean assertConsistentRenamingOfSignature = false;
+    public boolean allowStaticInterfaceMethodsForPreNApiLevel = false;
 
     // Flag to allow processing of resources in D8. A data resource consumer still needs to be
     // specified.
diff --git a/src/main/java/com/android/tools/r8/utils/Reporter.java b/src/main/java/com/android/tools/r8/utils/Reporter.java
index f251b0c..43f4f57 100644
--- a/src/main/java/com/android/tools/r8/utils/Reporter.java
+++ b/src/main/java/com/android/tools/r8/utils/Reporter.java
@@ -3,8 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils;
 
+import static com.android.tools.r8.DiagnosticsLevel.ERROR;
+import static com.android.tools.r8.DiagnosticsLevel.INFO;
+import static com.android.tools.r8.DiagnosticsLevel.WARNING;
+
 import com.android.tools.r8.Diagnostic;
 import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.DiagnosticsLevel;
+import com.android.tools.r8.errors.Unreachable;
 
 public class Reporter implements DiagnosticsHandler {
 
@@ -19,14 +25,39 @@
     this.clientHandler = clientHandler;
   }
 
+  private void handleDiagnostic(DiagnosticsLevel level, Diagnostic diagnostic) {
+    // To avoid having an entry for fatal error in the public  API enum use null to signal
+    // fatal error internally.
+    if (level != null) {
+      DiagnosticsLevel modifiedLevel = clientHandler.modifyDiagnosticsLevel(level, diagnostic);
+      level = modifiedLevel != null ? modifiedLevel : level;
+    } else {
+      level = ERROR;
+    }
+    switch (level) {
+      case INFO:
+        clientHandler.info(diagnostic);
+        break;
+      case WARNING:
+        clientHandler.warning(diagnostic);
+        break;
+      case ERROR:
+        abort = new AbortException(diagnostic);
+        clientHandler.error(diagnostic);
+        break;
+      default:
+        throw new Unreachable();
+    }
+  }
+
   @Override
   public synchronized void info(Diagnostic info) {
-    clientHandler.info(info);
+    handleDiagnostic(INFO, info);
   }
 
   @Override
   public synchronized void warning(Diagnostic warning) {
-    clientHandler.warning(warning);
+    handleDiagnostic(WARNING, warning);
   }
 
   public void warning(String message) {
@@ -35,8 +66,7 @@
 
   @Override
   public synchronized void error(Diagnostic error) {
-    abort = new AbortException(error);
-    clientHandler.error(error);
+    handleDiagnostic(ERROR, error);
   }
 
   public void error(String message) {
@@ -54,7 +84,7 @@
    * @throws AbortException always.
    */
   public RuntimeException fatalError(Diagnostic error) {
-    error(error);
+    handleDiagnostic(null, error);
     throw abort;
   }
 
diff --git a/src/main/java/com/android/tools/r8/utils/ZipUtils.java b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
index acaf11f..71400bd 100644
--- a/src/main/java/com/android/tools/r8/utils/ZipUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.errors.CompilationError;
 import com.google.common.io.ByteStreams;
 import com.google.common.io.Closer;
+import java.io.BufferedOutputStream;
 import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
@@ -74,7 +75,8 @@
   }
 
   public static void zip(Path zipFile, Path inputDirectory) throws IOException {
-    try (ZipOutputStream stream = new ZipOutputStream(Files.newOutputStream(zipFile))) {
+    try (ZipOutputStream stream =
+        new ZipOutputStream(new BufferedOutputStream(Files.newOutputStream(zipFile)))) {
       List<Path> files =
           Files.walk(inputDirectory)
               .filter(path -> !Files.isDirectory(path))
diff --git a/src/test/java/com/android/tools/r8/IntermediateCfD8TestBuilder.java b/src/test/java/com/android/tools/r8/IntermediateCfD8TestBuilder.java
index fcefd23..609fad5 100644
--- a/src/test/java/com/android/tools/r8/IntermediateCfD8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/IntermediateCfD8TestBuilder.java
@@ -7,10 +7,12 @@
 import com.android.tools.r8.debug.DebugTestConfig;
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.InternalOptions;
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Collection;
 import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
 
 public class IntermediateCfD8TestBuilder
     extends TestBuilder<D8TestRunResult, IntermediateCfD8TestBuilder> {
@@ -31,6 +33,12 @@
         D8TestBuilder.create(state, Backend.DEX).setMinApi(apiLevel).setDisableDesugaring(true);
   }
 
+  public IntermediateCfD8TestBuilder addOptionsModification(Consumer<InternalOptions> fn) {
+    cf2cf.addOptionsModification(fn);
+    cf2dex.addOptionsModification(fn);
+    return self();
+  }
+
   @Override
   IntermediateCfD8TestBuilder self() {
     return this;
diff --git a/src/test/java/com/android/tools/r8/JvmTestBuilder.java b/src/test/java/com/android/tools/r8/JvmTestBuilder.java
index 4317ca8..e59ea66 100644
--- a/src/test/java/com/android/tools/r8/JvmTestBuilder.java
+++ b/src/test/java/com/android/tools/r8/JvmTestBuilder.java
@@ -73,12 +73,12 @@
 
   @Override
   public JvmTestBuilder addLibraryFiles(Collection<Path> files) {
-    throw new Unimplemented("No support for changing the Java runtime library.");
+    return addRunClasspathFiles(files);
   }
 
   @Override
   public JvmTestBuilder addLibraryClasses(Collection<Class<?>> classes) {
-    throw new Unimplemented("No support for changing the Java runtime library.");
+    return addRunClasspathFiles(writeClassesToJar(classes));
   }
 
   @Override
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index 935de02..3c8a16b 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -256,6 +256,7 @@
     return self();
   }
 
+  @Override
   public T addMainDexListClasses(Class<?>... classes) {
     builder.addMainDexClasses(
         Arrays.stream(classes).map(Class::getTypeName).collect(Collectors.toList()));
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 38637cf..1bd188a 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -216,11 +216,18 @@
   }
 
   public TestBuilder<? extends TestRunResult<?>, ?> testForDesugaring(TestParameters parameters) {
-    return testForDesugaring(parameters.getRuntime().getBackend(), parameters.getApiLevel());
+    return testForDesugaring(
+        parameters.getRuntime().getBackend(), parameters.getApiLevel(), o -> {});
+  }
+
+  public TestBuilder<? extends TestRunResult<?>, ?> testForDesugaring(
+      TestParameters parameters, Consumer<InternalOptions> optionsModification) {
+    return testForDesugaring(
+        parameters.getRuntime().getBackend(), parameters.getApiLevel(), optionsModification);
   }
 
   private TestBuilder<? extends TestRunResult<?>, ?> testForDesugaring(
-      Backend backend, AndroidApiLevel apiLevel) {
+      Backend backend, AndroidApiLevel apiLevel, Consumer<InternalOptions> optionsModification) {
     assert apiLevel != null : "No API level. Add .withAllApiLevelsAlsoForCf() to test parameters?";
     TestState state = new TestState(temp);
     List<Pair<String, TestBuilder<? extends TestRunResult<?>, ?>>> builders;
@@ -228,13 +235,24 @@
       builders =
           ImmutableList.of(
               new Pair<>("JAVAC", JvmTestBuilder.create(state)),
-              new Pair<>("D8/CF", D8TestBuilder.create(state, Backend.CF).setMinApi(apiLevel)));
+              new Pair<>(
+                  "D8/CF",
+                  D8TestBuilder.create(state, Backend.CF)
+                      .setMinApi(apiLevel)
+                      .addOptionsModification(optionsModification)));
     } 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)));
+              new Pair<>(
+                  "D8/DEX",
+                  D8TestBuilder.create(state, Backend.DEX)
+                      .setMinApi(apiLevel)
+                      .addOptionsModification(optionsModification)),
+              new Pair<>(
+                  "D8/DEX o D8/CF",
+                  IntermediateCfD8TestBuilder.create(state, apiLevel)
+                      .addOptionsModification(optionsModification)));
     }
     return TestBuilderCollection.create(state, builders);
   }
@@ -1527,7 +1545,7 @@
     }
     Path path = temp.newFolder().toPath().resolve("classes.jar");
     ArchiveConsumer consumer = new ArchiveConsumer(path);
-    for (Class clazz : classes) {
+    for (Class<?> clazz : classes) {
       consumer.accept(
           ByteDataView.of(ToolHelper.getClassAsBytes(clazz)),
           DescriptorUtils.javaTypeToDescriptor(clazz.getTypeName()),
@@ -1537,6 +1555,24 @@
     return path;
   }
 
+  public Path buildOnDexRuntime(TestParameters parameters, byte[]... classes)
+      throws IOException, CompilationFailedException {
+    if (parameters.isDexRuntime()) {
+      return testForD8()
+          .addProgramClassFileData(classes)
+          .setMinApi(parameters.getApiLevel())
+          .compile()
+          .writeToZip();
+    }
+    Path path = temp.newFolder().toPath().resolve("classes.jar");
+    ArchiveConsumer consumer = new ArchiveConsumer(path);
+    for (byte[] clazz : classes) {
+      consumer.accept(ByteDataView.of(clazz), extractClassDescriptor(clazz), null);
+    }
+    consumer.finished(null);
+    return path;
+  }
+
   public static String binaryName(Class<?> clazz) {
     return DescriptorUtils.getBinaryNameFromJavaType(typeName(clazz));
   }
@@ -1553,6 +1589,10 @@
     return AndroidApiLevel.N;
   }
 
+  public static AndroidApiLevel apiLevelWithStaticInterfaceMethodsSupport() {
+    return AndroidApiLevel.N;
+  }
+
   public static AndroidApiLevel apiLevelWithInvokeCustomSupport() {
     return AndroidApiLevel.O;
   }
diff --git a/src/test/java/com/android/tools/r8/TestBaseBuilder.java b/src/test/java/com/android/tools/r8/TestBaseBuilder.java
index 4f512b7..4388754 100644
--- a/src/test/java/com/android/tools/r8/TestBaseBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestBaseBuilder.java
@@ -19,7 +19,7 @@
         C extends BaseCommand,
         B extends BaseCommand.Builder<C, B>,
         CR extends TestBaseResult<CR, RR>,
-        RR extends TestRunResult,
+        RR extends TestRunResult<RR>,
         T extends TestBaseBuilder<C, B, CR, RR, T>>
     extends TestBuilder<RR, T> {
 
diff --git a/src/test/java/com/android/tools/r8/TestBuilder.java b/src/test/java/com/android/tools/r8/TestBuilder.java
index 9a874c2..73a1605 100644
--- a/src/test/java/com/android/tools/r8/TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestBuilder.java
@@ -3,6 +3,7 @@
 // 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.ListUtils;
@@ -13,8 +14,9 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
+import java.util.function.BiFunction;
 
-public abstract class TestBuilder<RR extends TestRunResult, T extends TestBuilder<RR, T>> {
+public abstract class TestBuilder<RR extends TestRunResult<RR>, T extends TestBuilder<RR, T>> {
 
   private final TestState state;
 
@@ -134,6 +136,16 @@
     return addLibraryFiles(Arrays.asList(files));
   }
 
+  public T addDefaultRuntimeLibrary(TestParameters parameters) {
+    if (parameters.getBackend() == Backend.DEX) {
+      addLibraryFiles(ToolHelper.getFirstSupportedAndroidJar(parameters.getApiLevel()));
+    } else {
+      assert parameters.getBackend() == Backend.CF;
+      addLibraryFiles(ToolHelper.getJava8RuntimeJar());
+    }
+    return self();
+  }
+
   public T addClasspathClasses(Class<?>... classes) {
     return addClasspathClasses(Arrays.asList(classes));
   }
@@ -179,4 +191,10 @@
   public T addRunClasspathFiles(Path... files) {
     return addRunClasspathFiles(Arrays.asList(files));
   }
+
+  public T setDiagnosticsLevelModifier(
+      BiFunction<DiagnosticsLevel, Diagnostic, DiagnosticsLevel> modifier) {
+    getState().setDiagnosticsLevelModifier(modifier);
+    return self();
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index da1bc91..6f6263a 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -34,7 +34,7 @@
         C extends BaseCompilerCommand,
         B extends BaseCompilerCommand.Builder<C, B>,
         CR extends TestCompileResult<CR, RR>,
-        RR extends TestRunResult,
+        RR extends TestRunResult<RR>,
         T extends TestCompilerBuilder<C, B, CR, RR, T>>
     extends TestBaseBuilder<C, B, CR, RR, T> {
 
diff --git a/src/test/java/com/android/tools/r8/TestDiagnosticMessagesImpl.java b/src/test/java/com/android/tools/r8/TestDiagnosticMessagesImpl.java
index 54ad442..ab5027b 100644
--- a/src/test/java/com/android/tools/r8/TestDiagnosticMessagesImpl.java
+++ b/src/test/java/com/android/tools/r8/TestDiagnosticMessagesImpl.java
@@ -16,12 +16,14 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.function.BiFunction;
 import org.hamcrest.Matcher;
 
 public class TestDiagnosticMessagesImpl implements DiagnosticsHandler, TestDiagnosticMessages {
   private final List<Diagnostic> infos = new ArrayList<>();
   private final List<Diagnostic> warnings = new ArrayList<>();
   private final List<Diagnostic> errors = new ArrayList<>();
+  BiFunction<DiagnosticsLevel, Diagnostic, DiagnosticsLevel> modifier;
 
   @Override
   public String toString() {
@@ -66,6 +68,11 @@
   }
 
   @Override
+  public DiagnosticsLevel modifyDiagnosticsLevel(DiagnosticsLevel level, Diagnostic diagnostic) {
+    return modifier == null ? level : modifier.apply(level, diagnostic);
+  }
+
+  @Override
   public List<Diagnostic> getInfos() {
     return infos;
   }
@@ -303,4 +310,9 @@
   public TestDiagnosticMessages assertAllErrorsMatch(Matcher<Diagnostic> matcher) {
     return assertAllDiagnosticsMatches(getErrors(), "error", matcher);
   }
+
+  void setDiagnosticsLevelModifier(
+      BiFunction<DiagnosticsLevel, Diagnostic, DiagnosticsLevel> modifier) {
+    this.modifier = modifier;
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/TestState.java b/src/test/java/com/android/tools/r8/TestState.java
index 6ba75d7..3e28699 100644
--- a/src/test/java/com/android/tools/r8/TestState.java
+++ b/src/test/java/com/android/tools/r8/TestState.java
@@ -5,6 +5,7 @@
 
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.function.BiFunction;
 import org.junit.rules.TemporaryFolder;
 
 public class TestState {
@@ -54,4 +55,9 @@
   void setStderr(String stderr) {
     this.stderr = stderr;
   }
+
+  void setDiagnosticsLevelModifier(
+      BiFunction<DiagnosticsLevel, Diagnostic, DiagnosticsLevel> modifier) {
+    messages.setDiagnosticsLevelModifier(modifier);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 9b83a88fa2..7de9a9a 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.dex.ApplicationReader;
 import com.android.tools.r8.errors.Unimplemented;
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.AssemblyWriter;
 import com.android.tools.r8.graph.DexApplication;
 import com.android.tools.r8.graph.DexItemFactory;
@@ -2086,12 +2087,11 @@
     return builder;
   }
 
-  public static void writeApplication(DexApplication application, InternalOptions options)
+  public static void writeApplication(AppView<?> appView, InternalOptions options)
       throws ExecutionException {
     R8.writeApplication(
         Executors.newSingleThreadExecutor(),
-        application,
-        null,
+        appView,
         GraphLens.getIdentityLens(),
         InitClassLens.getDefault(),
         NamingLens.getIdentityLens(),
diff --git a/src/test/java/com/android/tools/r8/cf/StackMapFlagThisUninitTest.java b/src/test/java/com/android/tools/r8/cf/StackMapFlagThisUninitTest.java
new file mode 100644
index 0000000..b3f7fe4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/StackMapFlagThisUninitTest.java
@@ -0,0 +1,131 @@
+// 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.cf;
+
+import static org.hamcrest.CoreMatchers.containsString;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.objectweb.asm.ClassWriter;
+import org.objectweb.asm.Label;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(Parameterized.class)
+public class StackMapFlagThisUninitTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withCfRuntimes().build();
+  }
+
+  public StackMapFlagThisUninitTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(Main.class)
+        .addProgramClassFileData(ADump.dump())
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatMatches(
+            containsString(
+                "Exception in thread \"main\" java.lang.VerifyError: Inconsistent stackmap frames"
+                    + " at branch target 22"));
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class)
+        .addProgramClassFileData(ADump.dump())
+        .addKeepAllClassesRule()
+        .run(parameters.getRuntime(), Main.class)
+        // TODO(b/166738818): We should generate the correct stack map entry.
+        .assertFailureWithErrorThatMatches(
+            containsString(
+                "Exception in thread \"main\" java.lang.VerifyError: Inconsistent stackmap frames"
+                    + " at branch target 22"));
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      new A(0);
+    }
+  }
+
+  public static class A {
+
+    public A(int i) {}
+  }
+
+  // TODO(b/166738818): This is the bytecode generated from the constructor merging in
+  //  https://r8-review.googlesource.com/c/r8/+/53180/3.
+  //  The important thing here is that the instantiation of this is not performed in one case
+  //  and we fail to create a proper stack map entry for uninitializedThis.
+  public static class ADump implements Opcodes {
+
+    public static byte[] dump() {
+
+      ClassWriter classWriter = new ClassWriter(0);
+      MethodVisitor methodVisitor;
+
+      classWriter.visit(
+          V1_8,
+          ACC_PUBLIC | ACC_SUPER,
+          "com/android/tools/r8/cf/StackMapFlagThisUninitTest$A",
+          null,
+          "java/lang/Object",
+          null);
+
+      {
+        methodVisitor = classWriter.visitMethod(ACC_PUBLIC, "<init>", "(I)V", null, null);
+        methodVisitor.visitCode();
+        methodVisitor.visitVarInsn(ILOAD, 1);
+        Label label0 = new Label();
+        methodVisitor.visitJumpInsn(IFEQ, label0);
+        methodVisitor.visitVarInsn(ILOAD, 1);
+        methodVisitor.visitInsn(ICONST_1);
+        Label label1 = new Label();
+        methodVisitor.visitJumpInsn(IF_ICMPNE, label1);
+        methodVisitor.visitVarInsn(ALOAD, 0);
+        methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+        methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+        methodVisitor.visitLdcInsn("bar");
+        methodVisitor.visitMethodInsn(
+            INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
+        methodVisitor.visitInsn(RETURN);
+        methodVisitor.visitLabel(label1);
+        methodVisitor.visitFrame(Opcodes.F_CHOP, 2, null, 0, null);
+        methodVisitor.visitInsn(ACONST_NULL);
+        methodVisitor.visitInsn(ATHROW);
+        methodVisitor.visitLabel(label0);
+        methodVisitor.visitFrame(
+            Opcodes.F_APPEND, 1, new Object[] {Opcodes.UNINITIALIZED_THIS}, 0, null);
+        methodVisitor.visitVarInsn(ALOAD, 0);
+        methodVisitor.visitMethodInsn(INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
+        methodVisitor.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
+        methodVisitor.visitLdcInsn("foo");
+        methodVisitor.visitMethodInsn(
+            INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
+        methodVisitor.visitInsn(RETURN);
+        methodVisitor.visitMaxs(2, 2);
+        methodVisitor.visitEnd();
+      }
+      classWriter.visitEnd();
+
+      return classWriter.toByteArray();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/JavaTimeTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/JavaTimeTest.java
index 6414d4b..e42f33d 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/JavaTimeTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/JavaTimeTest.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.utils.codeinspector.TryCatchSubject;
 import com.android.tools.r8.utils.codeinspector.TypeSubject;
 import com.google.common.collect.ImmutableSet;
+import java.nio.file.Path;
 import java.util.List;
 import java.util.Set;
 import java.util.stream.Collectors;
@@ -113,6 +114,41 @@
   }
 
   @Test
+  public void testTimeD8Cf() throws Exception {
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    // Use D8 to desugar with Java classfile output.
+    Path jar =
+        testForD8(Backend.CF)
+            .addInnerClasses(JavaTimeTest.class)
+            .setMinApi(parameters.getApiLevel())
+            .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+            .compile()
+            .inspect(this::checkRewrittenInvokes)
+            .writeToZip();
+
+    // Collection keep rules is only implemented in the DEX writer.
+    String desugaredLibraryKeepRules = keepRuleConsumer.get();
+    if (desugaredLibraryKeepRules != null) {
+      assertEquals(0, desugaredLibraryKeepRules.length());
+      desugaredLibraryKeepRules = "-keep class * { *; }";
+    }
+
+    // Convert to DEX without desugaring.
+    testForD8()
+        .addProgramFiles(jar)
+        .setMinApi(parameters.getApiLevel())
+        .disableDesugaring()
+        .compile()
+        .addDesugaredCoreLibraryRunClassPath(
+            this::buildDesugaredLibrary,
+            parameters.getApiLevel(),
+            desugaredLibraryKeepRules,
+            shrinkDesugaredLibrary)
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutput(expectedOutput);
+  }
+
+  @Test
   public void testTimeD8() throws Exception {
     KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
     testForD8()
diff --git a/src/test/java/com/android/tools/r8/desugar/staticinterfacemethod/InvokeStaticDesugarTest.java b/src/test/java/com/android/tools/r8/desugar/staticinterfacemethod/InvokeStaticDesugarTest.java
new file mode 100644
index 0000000..8bcb3a6
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/staticinterfacemethod/InvokeStaticDesugarTest.java
@@ -0,0 +1,84 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.staticinterfacemethod;
+
+import static com.android.tools.r8.desugar.staticinterfacemethod.InvokeStaticDesugarTest.Library.foo;
+import static org.hamcrest.CoreMatchers.containsString;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRunResult;
+import com.android.tools.r8.ToolHelper.DexVm;
+import com.google.common.collect.ImmutableList;
+import java.nio.file.Path;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class InvokeStaticDesugarTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final String EXPECTED = "Hello World!";
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build();
+  }
+
+  public InvokeStaticDesugarTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testDesugar() throws Exception {
+    final TestRunResult<?> runResult =
+        testForDesugaring(parameters)
+            .addLibraryClasses(Library.class)
+            .addProgramClasses(Main.class)
+            .addRunClasspathFiles(compileRunClassPath())
+            .run(parameters.getRuntime(), Main.class);
+    if (parameters.isDexRuntime()
+        && parameters.getRuntime().asDex().getVm().isOlderThanOrEqual(DexVm.ART_4_4_4_HOST)) {
+      runResult.assertFailureWithErrorThatMatches(containsString("java.lang.VerifyError"));
+    } else {
+      runResult.assertSuccessWithOutputLines(EXPECTED);
+    }
+  }
+
+  private Path compileRunClassPath() throws Exception {
+    if (parameters.isCfRuntime()) {
+      return compileToZip(parameters, ImmutableList.of(), Library.class);
+    } else {
+      assert parameters.isDexRuntime();
+      return testForD8(parameters.getBackend())
+          .addProgramClasses(Library.class)
+          .setMinApi(parameters.getApiLevel())
+          .disableDesugaring()
+          .addOptionsModification(
+              options -> {
+                options.testing.allowStaticInterfaceMethodsForPreNApiLevel = true;
+              })
+          .compile()
+          .writeToZip();
+    }
+  }
+
+  public interface Library {
+
+    static void foo() {
+      System.out.println("Hello World!");
+    }
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugaring/interfacemethods/StaticInterfaceMethodReferenceTest.java b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/StaticInterfaceMethodReferenceTest.java
new file mode 100644
index 0000000..e7fba9d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugaring/interfacemethods/StaticInterfaceMethodReferenceTest.java
@@ -0,0 +1,128 @@
+// 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.desugaring.interfacemethods;
+
+import static org.junit.Assert.assertFalse;
+
+import com.android.tools.r8.TestBase;
+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.DexVm;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class StaticInterfaceMethodReferenceTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final boolean isInterface;
+
+  @Parameterized.Parameters(name = "{0}, itf:{1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build(),
+        BooleanUtils.values());
+  }
+
+  public StaticInterfaceMethodReferenceTest(TestParameters parameters, boolean isInterface) {
+    this.parameters = parameters;
+    this.isInterface = isInterface;
+  }
+
+  @Test
+  public void test() throws Exception {
+    TestRunResult<?> result =
+        testForDesugaring(parameters, o -> o.testing.allowInvokeErrors = true)
+            .addProgramClasses(TestClass.class)
+            .addProgramClassFileData(getTarget(isInterface))
+            .run(parameters.getRuntime(), TestClass.class);
+    checkResult(result);
+  }
+
+  @Test
+  public void testTargetMissing() throws Exception {
+    TestRunResult<?> result =
+        testForDesugaring(parameters, o -> o.testing.allowInvokeErrors = true)
+            .addProgramClasses(TestClass.class)
+            .addRunClasspathFiles(buildOnDexRuntime(parameters, getTarget(isInterface)))
+            .run(parameters.getRuntime(), TestClass.class);
+    // Missing target will cause the call to remain as Target::foo rather than Target$-CC::foo.
+    // TODO(b/166726895): Support static interface invoke as no knowledge of Target is needed.
+    if (isInterface
+        && parameters.isDexRuntime()
+        && parameters.getApiLevel().isLessThan(apiLevelWithStaticInterfaceMethodsSupport())) {
+      result.assertFailureWithErrorThatThrows(
+          parameters
+                  .getRuntime()
+                  .asDex()
+                  .getVm()
+                  .getVersion()
+                  .isOlderThanOrEqual(DexVm.Version.V4_4_4)
+              // On <= 4.4.4 a verify error happens due to the static invoke on an interface.
+              ? VerifyError.class
+              : NoSuchMethodError.class);
+      return;
+    }
+    checkResult(result);
+  }
+
+  private void checkResult(TestRunResult<?> result) {
+    // If the reference is of correct type, or running on JDK8 which allows mismatch the
+    // output is the expected print.
+    if (isInterface
+        || (parameters.isCfRuntime() && parameters.getRuntime().asCf().getVm() == CfVm.JDK8)) {
+      result.assertSuccessWithOutputLines("Target::foo");
+      return;
+    }
+
+    // DEX runtimes do not check method reference types so the code will run.
+    // TODO(b/166732606): Compile to an ICCE? What about the "missing" target case?
+    assertFalse(isInterface);
+    if (parameters.isDexRuntime()) {
+      result.assertSuccessWithOutputLines("Target::foo");
+      return;
+    }
+
+    // Otherwise the run should result in an ICCE.
+    result.assertFailureWithErrorThatThrows(IncompatibleClassChangeError.class);
+  }
+
+  private static byte[] getTarget(boolean isInterface) throws Exception {
+    return transformer(Target.class)
+        .setAccessFlags(
+            classAccessFlags -> {
+              if (isInterface) {
+                assert !classAccessFlags.isSuper();
+                assert classAccessFlags.isAbstract();
+                assert classAccessFlags.isInterface();
+              } else {
+                classAccessFlags.unsetAbstract();
+                classAccessFlags.unsetInterface();
+              }
+            })
+        .transform();
+  }
+
+  interface Target {
+
+    static void foo() {
+      System.out.println("Target::foo");
+    }
+  }
+
+  static class TestClass {
+
+    static void call(Runnable fn) {
+      fn.run();
+    }
+
+    public static void main(String[] args) {
+      call(Target::foo);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/diagnostics/ModifyDiagnosticsLevelTest.java b/src/test/java/com/android/tools/r8/diagnostics/ModifyDiagnosticsLevelTest.java
new file mode 100644
index 0000000..8db3e12
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/diagnostics/ModifyDiagnosticsLevelTest.java
@@ -0,0 +1,96 @@
+// 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.diagnostics;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.junit.Assert.fail;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.DiagnosticsLevel;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringDiagnostic;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class ModifyDiagnosticsLevelTest extends TestBase {
+
+  private static final String MISSING_CLASS_MESSAGE_PREFIX = "Missing class: ";
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public ModifyDiagnosticsLevelTest(TestParameters parameters) {
+    parameters.assertNoneRuntime();
+  }
+
+  @Test
+  public void testWarningToInfo() throws Exception {
+    testForR8(Backend.DEX)
+        .addProgramClasses(TestClass.class)
+        .addKeepMainRule(TestClass.class)
+        .addKeepRules("-ignorewarnings")
+        .setDiagnosticsLevelModifier(
+            (level, diagnostic) -> {
+              if (level == DiagnosticsLevel.WARNING
+                  && diagnostic instanceof StringDiagnostic
+                  && diagnostic.getDiagnosticMessage().startsWith(MISSING_CLASS_MESSAGE_PREFIX)) {
+                return DiagnosticsLevel.INFO;
+              }
+              return level;
+            })
+        .allowDiagnosticInfoMessages()
+        .compileWithExpectedDiagnostics(
+            diagnostics -> {
+              diagnostics
+                  .assertOnlyInfos()
+                  .assertInfosCount(1)
+                  .assertInfosMatch(diagnosticMessage(startsWith(MISSING_CLASS_MESSAGE_PREFIX)));
+            });
+  }
+
+  @Test
+  public void testWarningToError() throws Exception {
+    try {
+      testForR8(Backend.DEX)
+          .addProgramClasses(TestClass.class)
+          .addKeepMainRule(TestClass.class)
+          .addKeepRules("-ignorewarnings")
+          .setDiagnosticsLevelModifier(
+              (level, diagnostic) -> {
+                if (level == DiagnosticsLevel.WARNING
+                    && diagnostic instanceof StringDiagnostic
+                    && diagnostic.getDiagnosticMessage().startsWith(MISSING_CLASS_MESSAGE_PREFIX)) {
+                  return DiagnosticsLevel.ERROR;
+                }
+                return level;
+              })
+          .compileWithExpectedDiagnostics(
+              diagnostics -> {
+                diagnostics
+                    .assertOnlyErrors()
+                    .assertErrorsCount(1)
+                    .assertErrorsMatch(diagnosticMessage(startsWith(MISSING_CLASS_MESSAGE_PREFIX)));
+              });
+      fail("Expected compilation to fail");
+    } catch (CompilationFailedException e) {
+      // Expected.
+    }
+  }
+
+  static class TestClass implements I {
+
+    public static void main(String[] args) {
+      System.out.println("Hello, world!");
+    }
+  }
+
+  interface I {}
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EmptyEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/EmptyEnumUnboxingTest.java
new file mode 100644
index 0000000..ddc0806
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EmptyEnumUnboxingTest.java
@@ -0,0 +1,75 @@
+// 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.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 EmptyEnumUnboxingTest 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 EmptyEnumUnboxingTest(
+      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(EmptyEnumUnboxingTest.class)
+            .addKeepMainRule(Main.class)
+            .addKeepRules(enumKeepRules.getKeepRules())
+            .enableNeverClassInliningAnnotations()
+            .enableInliningAnnotations()
+            .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+            .allowDiagnosticInfoMessages()
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspectDiagnosticMessages(
+                m ->
+                    // TODO(b/166532373): Unbox enum with no cases.
+                    assertEnumIsBoxed(MyEnum.class, Main.class.getSimpleName(), m))
+            .run(parameters.getRuntime(), Main.class)
+            .assertSuccess();
+    assertLines2By2Correct(run.getStdOut());
+  }
+
+  @NeverClassInline
+  enum MyEnum {
+    ;
+
+    @NeverInline
+    static void print() {
+      System.out.println("PRINT");
+    }
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      MyEnum.print();
+      System.out.println("PRINT");
+    }
+  }
+}
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 bdf46e3..89395ef 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EnumToStringLibTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumToStringLibTest.java
@@ -7,7 +7,6 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.core.StringContains.containsString;
 
 import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.TestParameters;
@@ -76,32 +75,15 @@
             .allowDiagnosticInfoMessages(enumUnboxing)
             .setMinApi(parameters.getApiLevel())
             .compile();
-    if (enumKeepRules.isStudio() && enumValueOptimization && enumUnboxing) {
-      // TODO(b/160939354): Enum unboxing synthesizes a toString() method based on field names.
-      compile
-          .run(parameters.getRuntime(), AlwaysCorrectProgram.class)
-          .assertFailureWithErrorThatMatches(containsString("IllegalArgumentException"));
-    } else {
       compile
           .run(parameters.getRuntime(), AlwaysCorrectProgram.class)
           .assertSuccessWithOutputLines("0", "1", "2", "0", "1", "2", "0", "1", "2");
-    }
-    if (!enumKeepRules.isSnap() && enumUnboxing) {
-      // TODO(b/160939354): Enum unboxing synthesizes a toString() method based on field names.
-      compile
-          .run(parameters.getRuntime(), AlwaysCorrectProgram2.class)
-          .assertFailureWithErrorThatMatches(containsString("IllegalArgumentException"));
-    } else {
       compile
           .run(parameters.getRuntime(), AlwaysCorrectProgram2.class)
           .assertSuccessWithOutputLines("0", "1", "2", "0", "1", "2");
-    }
   }
 
   private void assertEnumFieldsMinified(CodeInspector codeInspector) throws Exception {
-    if (enumKeepRules.isSnap()) {
-      return;
-    }
     ClassSubject clazz = codeInspector.clazz(ToStringLib.LibEnum.class);
     assertThat(clazz, isPresent());
     for (String fieldName : new String[] {"COFFEE", "BEAN", "SUGAR"}) {
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingB160535628Test.java b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingB160535628Test.java
index 5ccd903..590cc7c 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingB160535628Test.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingB160535628Test.java
@@ -21,16 +21,21 @@
 
   private final TestParameters parameters;
   private final boolean missingStaticMethods;
+  private final EnumKeepRules enumKeepRules;
 
-  @Parameterized.Parameters(name = "{0}")
+  @Parameterized.Parameters(name = "{0} missing: {1} keep: {2}")
   public static List<Object[]> data() {
     return buildParameters(
-        getTestParameters().withDexRuntimes().withAllApiLevels().build(), BooleanUtils.values());
+        getTestParameters().withDexRuntimes().withAllApiLevels().build(),
+        BooleanUtils.values(),
+        getAllEnumKeepRules());
   }
 
-  public EnumUnboxingB160535628Test(TestParameters parameters, boolean missingStaticMethods) {
+  public EnumUnboxingB160535628Test(
+      TestParameters parameters, boolean missingStaticMethods, EnumKeepRules enumKeepRules) {
     this.parameters = parameters;
     this.missingStaticMethods = missingStaticMethods;
+    this.enumKeepRules = enumKeepRules;
   }
 
   @Test
@@ -44,9 +49,10 @@
             .addProgramClasses(ProgramValueOf.class, ProgramStaticMethod.class)
             .addProgramFiles(javaLibShrunk)
             .addKeepMainRules(ProgramValueOf.class, ProgramStaticMethod.class)
+            .addKeepRules(enumKeepRules.getKeepRules())
             .addOptionsModification(
                 options -> {
-                  options.enableEnumUnboxing = true;
+                  assert options.enableEnumUnboxing;
                   options.testing.enableEnumUnboxingDebugLogs = true;
                 })
             .allowDiagnosticMessages()
@@ -58,18 +64,23 @@
                 this::assertEnumUnboxedIfStaticMethodsPresent);
     if (missingStaticMethods) {
       compile
-          .run(parameters.getRuntime(), ProgramValueOf.class)
-          .assertFailureWithErrorThatMatches(containsString("NoSuchMethodError"))
-          .assertFailureWithErrorThatMatches(containsString("valueOf"));
-      compile
           .run(parameters.getRuntime(), ProgramStaticMethod.class)
           .assertFailureWithErrorThatMatches(containsString("NoSuchMethodError"))
           .assertFailureWithErrorThatMatches(containsString("staticMethod"));
     } else {
-      compile.run(parameters.getRuntime(), ProgramValueOf.class).assertSuccessWithOutputLines("0");
       compile
           .run(parameters.getRuntime(), ProgramStaticMethod.class)
-          .assertSuccessWithOutputLines("42");
+          .assertSuccessWithOutputLines("0", "42");
+    }
+    if (missingStaticMethods && enumKeepRules == EnumKeepRules.NONE) {
+      compile
+          .run(parameters.getRuntime(), ProgramValueOf.class)
+          .assertFailureWithErrorThatMatches(containsString("NoSuchMethodError"))
+          .assertFailureWithErrorThatMatches(containsString("valueOf"));
+    } else {
+      compile
+          .run(parameters.getRuntime(), ProgramValueOf.class)
+          .assertSuccessWithOutputLines("0", "0");
     }
   }
 
@@ -77,6 +88,7 @@
     return testForR8(Backend.CF)
         .addProgramClasses(Lib.class, Lib.LibEnumStaticMethod.class, Lib.LibEnum.class)
         .addKeepRules("-keep enum * { <fields>; }")
+        .addKeepRules(enumKeepRules.getKeepRules())
         .addKeepRules(missingStaticMethods ? "" : "-keep enum * { static <methods>; }")
         .addOptionsModification(
             options -> {
@@ -130,13 +142,15 @@
   public static class ProgramValueOf {
 
     public static void main(String[] args) {
-      System.out.println(Lib.LibEnumStaticMethod.valueOf(Lib.LibEnum.A.name()).ordinal());
+      System.out.println(Lib.LibEnum.A.ordinal());
+      System.out.println(Lib.LibEnum.valueOf(Lib.LibEnum.A.name()).ordinal());
     }
   }
 
   public static class ProgramStaticMethod {
 
     public static void main(String[] args) {
+      System.out.println(Lib.LibEnumStaticMethod.A.ordinal());
       System.out.println(Lib.LibEnumStaticMethod.staticMethod());
     }
   }
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 4f724d3..1e5bd5b 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingClassStaticizerTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingClassStaticizerTest.java
@@ -65,12 +65,16 @@
   }
 
   private void assertClassStaticized(CodeInspector codeInspector) {
+    String renamedMethodName = "method$enumunboxing$";
     if (parameters.isCfRuntime()) {
       // There is no class staticizer in Cf.
-      assertThat(codeInspector.clazz(Companion.class).uniqueMethodWithName("method"), isPresent());
+      assertThat(
+          codeInspector.clazz(Companion.class).uniqueMethodWithName(renamedMethodName),
+          isPresent());
       return;
     }
-    MethodSubject method = codeInspector.clazz(CompanionHost.class).uniqueMethodWithName("method");
+    MethodSubject method =
+        codeInspector.clazz(CompanionHost.class).uniqueMethodWithName(renamedMethodName);
     assertThat(method, isPresent());
     assertEquals("int", method.getMethod().method.proto.parameters.toString());
   }
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingSideEffectClInitTest.java b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingSideEffectClInitTest.java
index 128466c..89621a2 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingSideEffectClInitTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingSideEffectClInitTest.java
@@ -44,15 +44,7 @@
         .setMinApi(parameters.getApiLevel())
         .compile()
         .inspectDiagnosticMessages(
-            m -> {
-              // The snap keep rule forces to keep the static MainEnum#e field, so the enum
-              // cannot be unboxed anymore.
-              if (enumKeepRules.isSnap()) {
-                assertEnumIsBoxed(ENUM_CLASS, classToTest.getSimpleName(), m);
-              } else {
-                assertEnumIsUnboxed(ENUM_CLASS, classToTest.getSimpleName(), m);
-              }
-            })
+            m -> assertEnumIsUnboxed(ENUM_CLASS, classToTest.getSimpleName(), m))
         .run(parameters.getRuntime(), classToTest)
         .assertSuccessWithOutputLines("0");
   }
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 f26a6be..ab837d3 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
@@ -23,18 +23,10 @@
           + " public static **[] values();\n"
           + " public static ** valueOf(java.lang.String);\n"
           + "}";
-  // Default keep rule present in Snap.
-  private static final String KEEP_ENUM_SNAP =
-      "-keepclassmembers enum * {\n"
-          + "<fields>;\n"
-          + " public static **[] values();\n"
-          + " public static ** valueOf(java.lang.String);\n"
-          + "}";
 
   public enum EnumKeepRules {
     NONE(""),
-    STUDIO(KEEP_ENUM_STUDIO),
-    SNAP(KEEP_ENUM_SNAP);
+    STUDIO(KEEP_ENUM_STUDIO);
 
     private final String keepRules;
 
@@ -46,10 +38,6 @@
       return this == STUDIO;
     }
 
-    public boolean isSnap() {
-      return this == SNAP;
-    }
-
     EnumKeepRules(String keepRules) {
       this.keepRules = keepRules;
     }
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/FailingEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/FailingEnumUnboxingTest.java
index 08ec7bd..8bf8034 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/FailingEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/FailingEnumUnboxingTest.java
@@ -5,12 +5,8 @@
 package com.android.tools.r8.enumunboxing;
 
 import com.android.tools.r8.NeverClassInline;
-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.enumunboxing.FailingEnumUnboxingTest.EnumInstanceFieldMain.EnumInstanceField;
-import com.android.tools.r8.enumunboxing.FailingEnumUnboxingTest.EnumStaticFieldMain.EnumStaticField;
 import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -20,11 +16,6 @@
 @RunWith(Parameterized.class)
 public class FailingEnumUnboxingTest extends EnumUnboxingTestBase {
 
-  private static final Class<?>[] FAILURES = {
-    EnumStaticField.class,
-    EnumInstanceField.class,
-  };
-
   private final TestParameters parameters;
   private final boolean enumValueOptimization;
   private final EnumKeepRules enumKeepRules;
@@ -43,28 +34,25 @@
 
   @Test
   public void testEnumUnboxingFailure() throws Exception {
-    R8FullTestBuilder r8FullTestBuilder =
-        testForR8(parameters.getBackend()).addInnerClasses(FailingEnumUnboxingTest.class);
-    for (Class<?> failure : FAILURES) {
-      r8FullTestBuilder.addKeepMainRule(failure.getEnclosingClass());
-    }
-    R8TestCompileResult compile =
-        r8FullTestBuilder
+    R8TestRunResult run =
+        testForR8(parameters.getBackend())
+            .addInnerClasses(FailingEnumUnboxingTest.class)
+            .addKeepMainRule(EnumStaticFieldMain.class)
             .enableNeverClassInliningAnnotations()
             .addKeepRules(enumKeepRules.getKeepRules())
             .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
             .allowDiagnosticInfoMessages()
             .setMinApi(parameters.getApiLevel())
-            .compile();
-    for (Class<?> failure : FAILURES) {
-      R8TestRunResult run =
-          compile
-              .inspectDiagnosticMessages(
-                  m -> assertEnumIsBoxed(failure, failure.getSimpleName(), m))
-              .run(parameters.getRuntime(), failure.getEnclosingClass())
-              .assertSuccess();
-      assertLines2By2Correct(run.getStdOut());
-    }
+            .compile()
+            .inspectDiagnosticMessages(
+                m ->
+                    assertEnumIsBoxed(
+                        EnumStaticFieldMain.EnumStaticField.class,
+                        EnumStaticFieldMain.class.getSimpleName(),
+                        m))
+            .run(parameters.getRuntime(), EnumStaticFieldMain.class)
+            .assertSuccess();
+    assertLines2By2Correct(run.getStdOut());
   }
 
   static class EnumStaticFieldMain {
@@ -84,26 +72,4 @@
       static EnumStaticField X = A;
     }
   }
-
-  static class EnumInstanceFieldMain {
-
-    @NeverClassInline
-    enum EnumInstanceField {
-      A(10),
-      B(20),
-      C(30);
-      private int a;
-
-      EnumInstanceField(int i) {
-        this.a = i;
-      }
-    }
-
-    public static void main(String[] args) {
-      System.out.println(EnumInstanceField.A.ordinal());
-      System.out.println(0);
-      System.out.println(EnumInstanceField.A.a);
-      System.out.println(10);
-    }
-  }
 }
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/FailingMethodEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/FailingMethodEnumUnboxingTest.java
index e42ac65..374895c 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/FailingMethodEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/FailingMethodEnumUnboxingTest.java
@@ -1,12 +1,10 @@
 // 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 junit.framework.TestCase.assertTrue;
 import static org.junit.Assert.assertEquals;
-
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.R8TestCompileResult;
@@ -22,17 +20,14 @@
 
 @RunWith(Parameterized.class)
 public class FailingMethodEnumUnboxingTest extends EnumUnboxingTestBase {
-
   private static final Class<?>[] FAILURES = {
     InstanceFieldPutObject.class,
     StaticFieldPutObject.class,
-    ToString.class,
     EnumSetTest.class,
     FailingPhi.class,
     FailingReturnType.class,
     FailingParameterType.class
   };
-
   private final TestParameters parameters;
   private final boolean enumValueOptimization;
   private final EnumKeepRules enumKeepRules;
@@ -83,18 +78,15 @@
 
   private void assertEnumsAsExpected(CodeInspector inspector) {
     // Check all as expected (else we test nothing)
-
     assertEquals(
         1,
         inspector.clazz(InstanceFieldPutObject.class).getDexProgramClass().instanceFields().size());
     assertEquals(
         1, inspector.clazz(StaticFieldPutObject.class).getDexProgramClass().staticFields().size());
-
     assertTrue(inspector.clazz(FailingPhi.class).uniqueMethodWithName("switchOn").isPresent());
   }
 
   static class InstanceFieldPutObject {
-
     @NeverClassInline
     enum MyEnum {
       A,
@@ -119,7 +111,6 @@
   }
 
   static class StaticFieldPutObject {
-
     @NeverClassInline
     enum MyEnum {
       A,
@@ -142,24 +133,7 @@
     }
   }
 
-  static class ToString {
-
-    @NeverClassInline
-    enum MyEnum {
-      A,
-      B,
-      C
-    }
-
-    public static void main(String[] args) {
-      MyEnum e1 = MyEnum.A;
-      System.out.println(e1.toString());
-      System.out.println("A");
-    }
-  }
-
   static class EnumSetTest {
-
     @NeverClassInline
     enum MyEnum {
       A,
@@ -175,7 +149,6 @@
   }
 
   static class FailingPhi {
-
     @NeverClassInline
     enum MyEnum {
       A,
@@ -189,7 +162,6 @@
       System.out.println(switchOn(2));
       System.out.println("class java.lang.Object");
     }
-
     // Avoid removing the switch entirely.
     @NeverInline
     static Object switchOn(int i) {
@@ -205,7 +177,6 @@
   }
 
   static class FailingReturnType {
-
     @NeverClassInline
     enum MyEnum {
       A,
@@ -227,7 +198,6 @@
   }
 
   static class FailingParameterType {
-
     @NeverClassInline
     enum MyEnum {
       A,
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/InstanceFieldsEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/InstanceFieldsEnumUnboxingTest.java
new file mode 100644
index 0000000..e91be2b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/InstanceFieldsEnumUnboxingTest.java
@@ -0,0 +1,549 @@
+// 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.R8TestCompileResult;
+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 InstanceFieldsEnumUnboxingTest extends EnumUnboxingTestBase {
+
+  private static final Class<?>[] FAILURES = {
+    FailureIntField.class,
+    FailurePrivateIntField.class,
+    FailureBoxedInnerEnumField.class,
+    FailureUnboxedEnumField.class,
+    FailureTooManyUsedFields.class
+  };
+
+  private static final Class<?>[] SUCCESSES = {
+    SuccessUnusedField.class,
+    SuccessIntField.class,
+    SuccessDoubleField.class,
+    SuccessIntFieldOrdinal.class,
+    SuccessIntFieldInitializerInit.class,
+    SuccessStringField.class,
+    SuccessMultiConstructorIntField.class
+  };
+
+  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 InstanceFieldsEnumUnboxingTest(
+      TestParameters parameters, boolean enumValueOptimization, EnumKeepRules enumKeepRules) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+  }
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    R8TestCompileResult compile =
+        testForR8(parameters.getBackend())
+            .addInnerClasses(InstanceFieldsEnumUnboxingTest.class)
+            .addKeepMainRules(SUCCESSES)
+            .addKeepMainRules(FAILURES)
+            .noMinification()
+            .enableInliningAnnotations()
+            .enableNeverClassInliningAnnotations()
+            .addKeepRules(enumKeepRules.getKeepRules())
+            .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+            .allowDiagnosticInfoMessages()
+            .setMinApi(parameters.getApiLevel())
+            .compile();
+    for (Class<?> failure : FAILURES) {
+      testClass(compile, failure, true);
+    }
+    for (Class<?> success : SUCCESSES) {
+      testClass(compile, success, success != SuccessUnusedField.class);
+    }
+  }
+
+  private void testClass(R8TestCompileResult compile, Class<?> testClass, boolean failure)
+      throws Exception {
+    R8TestRunResult run =
+        compile
+            .inspectDiagnosticMessages(
+                m -> {
+                  for (Class<?> declaredClass : testClass.getDeclaredClasses()) {
+                    if (declaredClass.isEnum()
+                        && !declaredClass.getSimpleName().equals("InnerEnum")) {
+                      if (failure) {
+                        assertEnumIsBoxed(declaredClass, testClass.getSimpleName(), m);
+                      } else {
+                        assertEnumIsUnboxed(declaredClass, testClass.getSimpleName(), m);
+                      }
+                    }
+                  }
+                })
+            .run(parameters.getRuntime(), testClass)
+            .assertSuccess();
+    assertLines2By2Correct(run.getStdOut());
+  }
+
+  static class SuccessUnusedField {
+
+    public static void main(String[] args) {
+      System.out.println(getEnumA().ordinal());
+      System.out.println(0);
+      System.out.println(getEnumB().ordinal());
+      System.out.println(1);
+    }
+
+    @NeverInline
+    static EnumField getEnumA() {
+      return System.currentTimeMillis() > 0 ? EnumField.A : EnumField.B;
+    }
+
+    @NeverInline
+    static EnumField getEnumB() {
+      return System.currentTimeMillis() > 0 ? EnumField.B : EnumField.A;
+    }
+
+    @NeverClassInline
+    enum EnumField {
+      A(10),
+      B(20);
+
+      int field;
+
+      EnumField(int i) {
+        this.field = i;
+      }
+    }
+  }
+
+  static class SuccessIntField {
+
+    public static void main(String[] args) {
+      System.out.println(getEnumA().field);
+      System.out.println(10);
+      System.out.println(getEnumB().field);
+      System.out.println(20);
+    }
+
+    @NeverInline
+    static EnumField getEnumA() {
+      return System.currentTimeMillis() > 0 ? EnumField.A : EnumField.B;
+    }
+
+    @NeverInline
+    static EnumField getEnumB() {
+      return System.currentTimeMillis() > 0 ? EnumField.B : EnumField.A;
+    }
+
+    @NeverClassInline
+    enum EnumField {
+      A(10),
+      B(20);
+
+      int field;
+
+      EnumField(int i) {
+        this.field = i;
+      }
+    }
+  }
+
+  static class FailurePrivateIntField {
+
+    public static void main(String[] args) {
+      System.out.println(getEnumA().field);
+      System.out.println(10);
+      System.out.println(getEnumB().field);
+      System.out.println(20);
+    }
+
+    @NeverInline
+    static EnumField getEnumA() {
+      return System.currentTimeMillis() > 0 ? EnumField.A : EnumField.B;
+    }
+
+    @NeverInline
+    static EnumField getEnumB() {
+      return System.currentTimeMillis() > 0 ? EnumField.B : EnumField.A;
+    }
+
+    @NeverClassInline
+    enum EnumField {
+      A(10),
+      B(20);
+
+      private int field;
+
+      EnumField(int i) {
+        this.field = i;
+      }
+    }
+  }
+
+  static class SuccessDoubleField {
+
+    public static void main(String[] args) {
+      System.out.println(getEnumA().field);
+      System.out.println(10.0);
+      System.out.println(getEnumB().field);
+      System.out.println(20.0);
+    }
+
+    @NeverInline
+    static EnumField getEnumA() {
+      return System.currentTimeMillis() > 0 ? EnumField.A : EnumField.B;
+    }
+
+    @NeverInline
+    static EnumField getEnumB() {
+      return System.currentTimeMillis() > 0 ? EnumField.B : EnumField.A;
+    }
+
+    @NeverClassInline
+    enum EnumField {
+      A(10.0),
+      B(20.0);
+
+      double field;
+
+      EnumField(double d) {
+        this.field = d;
+      }
+    }
+  }
+
+  static class SuccessIntFieldInitializerInit {
+
+    public static void main(String[] args) {
+      System.out.println(getEnumA().field);
+      System.out.println(10);
+      System.out.println(getEnumB().field);
+      System.out.println(10);
+    }
+
+    @NeverInline
+    static EnumField getEnumA() {
+      return System.currentTimeMillis() > 0 ? EnumField.A : EnumField.B;
+    }
+
+    @NeverInline
+    static EnumField getEnumB() {
+      return System.currentTimeMillis() > 0 ? EnumField.B : EnumField.A;
+    }
+
+    @NeverClassInline
+    enum EnumField {
+      A,
+      B,
+      C;
+
+      int field;
+
+      EnumField() {
+        this.field = 10;
+      }
+    }
+  }
+
+  // This class test an optimization where the ordinal is re-used instead of the int field.
+  static class SuccessIntFieldOrdinal {
+
+    public static void main(String[] args) {
+      System.out.println(getEnumA().field);
+      System.out.println(0);
+      System.out.println(getEnumB().field);
+      System.out.println(1);
+    }
+
+    @NeverInline
+    static EnumField getEnumA() {
+      return System.currentTimeMillis() > 0 ? EnumField.A : EnumField.B;
+    }
+
+    @NeverInline
+    static EnumField getEnumB() {
+      return System.currentTimeMillis() > 0 ? EnumField.B : EnumField.A;
+    }
+
+    @NeverClassInline
+    enum EnumField {
+      A(0),
+      B(1),
+      C(2);
+
+      int field;
+
+      EnumField(int i) {
+        this.field = i;
+      }
+    }
+  }
+
+  static class FailureIntField {
+
+    public static void main(String[] args) {
+      System.out.println(getEnumA().field);
+      System.out.println(30);
+      System.out.println(getEnumB().field);
+      System.out.println(60);
+    }
+
+    @NeverInline
+    static EnumField getEnumA() {
+      return System.currentTimeMillis() > 0 ? EnumField.A : EnumField.B;
+    }
+
+    @NeverInline
+    static EnumField getEnumB() {
+      return System.currentTimeMillis() > 0 ? EnumField.B : EnumField.A;
+    }
+
+    @NeverClassInline
+    enum EnumField {
+      A(getRandom(10)),
+      B(getRandom(20)),
+      C(getRandom(30));
+
+      @NeverInline
+      static int getRandom(int i) {
+        return i * (System.currentTimeMillis() > 0 ? 3 : -3);
+      }
+
+      int field;
+
+      EnumField(int i) {
+        this.field = i;
+      }
+    }
+  }
+
+  static class FailureTooManyUsedFields {
+
+    public static void main(String[] args) {
+      System.out.println(getEnumA().field0);
+      System.out.println(0);
+      System.out.println(getEnumA().field1);
+      System.out.println(9);
+      System.out.println(getEnumA().field2);
+      System.out.println(8);
+      System.out.println(getEnumA().field3);
+      System.out.println(7);
+      System.out.println(getEnumA().field4);
+      System.out.println(6);
+      System.out.println(getEnumA().field5);
+      System.out.println(5);
+      System.out.println(getEnumA().field6);
+      System.out.println(4);
+      System.out.println(getEnumA().field7);
+      System.out.println(3);
+      System.out.println(getEnumA().field8);
+      System.out.println(2);
+      System.out.println(getEnumA().field9);
+      System.out.println(1);
+    }
+
+    @NeverInline
+    static EnumField getEnumA() {
+      return System.currentTimeMillis() > 0 ? EnumField.A : EnumField.B;
+    }
+
+    @NeverClassInline
+    enum EnumField {
+      A(1, 2, 3, 4, 5, 6, 7, 8, 9, 0),
+      B(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
+
+      int field0;
+      int field1;
+      int field2;
+      int field3;
+      int field4;
+      int field5;
+      int field6;
+      int field7;
+      int field8;
+      int field9;
+
+      EnumField(int i9, int i8, int i7, int i6, int i5, int i4, int i3, int i2, int i1, int i0) {
+        this.field0 = i0;
+        this.field1 = i1;
+        this.field2 = i2;
+        this.field3 = i3;
+        this.field4 = i4;
+        this.field5 = i5;
+        this.field6 = i6;
+        this.field7 = i7;
+        this.field8 = i8;
+        this.field9 = i9;
+      }
+    }
+  }
+
+  static class SuccessMultiConstructorIntField {
+
+    public static void main(String[] args) {
+      System.out.println(getEnumA().field0);
+      System.out.println(10);
+      System.out.println(getEnumA().field1);
+      System.out.println(-1);
+      System.out.println(getEnumB().field0);
+      System.out.println(20);
+      System.out.println(getEnumB().field1);
+      System.out.println(30);
+    }
+
+    @NeverInline
+    static EnumField getEnumA() {
+      return System.currentTimeMillis() > 0 ? EnumField.A : EnumField.B;
+    }
+
+    @NeverInline
+    static EnumField getEnumB() {
+      return System.currentTimeMillis() > 0 ? EnumField.B : EnumField.A;
+    }
+
+    @NeverClassInline
+    enum EnumField {
+      A(10),
+      B(20, 30);
+
+      int field0;
+      int field1;
+
+      EnumField(int i0) {
+        this.field0 = i0;
+        this.field1 = -1;
+      }
+
+      EnumField(int i0, int i1) {
+        this.field0 = i0;
+        this.field1 = i1;
+      }
+    }
+  }
+
+  static class SuccessStringField {
+
+    public static void main(String[] args) {
+      System.out.println(getEnumA().field);
+      System.out.println("AA");
+      System.out.println(getEnumB().field);
+      System.out.println("BB");
+    }
+
+    @NeverInline
+    static EnumField getEnumA() {
+      return System.currentTimeMillis() > 0 ? EnumField.A : EnumField.B;
+    }
+
+    @NeverInline
+    static EnumField getEnumB() {
+      return System.currentTimeMillis() > 0 ? EnumField.B : EnumField.A;
+    }
+
+    @NeverClassInline
+    enum EnumField {
+      A("AA"),
+      B("BB"),
+      C("CC");
+
+      String field;
+
+      EnumField(String s) {
+        this.field = s;
+      }
+    }
+  }
+
+  static class FailureBoxedInnerEnumField {
+
+    public static void main(String[] args) {
+      System.out.println(getEnumA().field);
+      System.out.println("X");
+      System.out.println(getEnumB().field);
+      System.out.println("Y");
+    }
+
+    @NeverInline
+    static EnumField getEnumA() {
+      return System.currentTimeMillis() > 0 ? EnumField.A : EnumField.B;
+    }
+
+    @NeverInline
+    static EnumField getEnumB() {
+      return System.currentTimeMillis() > 0 ? EnumField.B : EnumField.A;
+    }
+
+    @NeverClassInline
+    enum InnerEnum {
+      X,
+      Y,
+      Z;
+    }
+
+    @NeverClassInline
+    enum EnumField {
+      A(InnerEnum.X),
+      B(InnerEnum.Y),
+      C(InnerEnum.Z);
+
+      InnerEnum field;
+
+      EnumField(InnerEnum s) {
+        this.field = s;
+      }
+    }
+  }
+
+  static class FailureUnboxedEnumField {
+
+    public static void main(String[] args) {
+      System.out.println(getEnumA().field.ordinal());
+      System.out.println(0);
+      System.out.println(getEnumB().field.ordinal());
+      System.out.println(1);
+    }
+
+    @NeverInline
+    static EnumField getEnumA() {
+      return System.currentTimeMillis() > 0 ? EnumField.A : EnumField.B;
+    }
+
+    @NeverInline
+    static EnumField getEnumB() {
+      return System.currentTimeMillis() > 0 ? EnumField.B : EnumField.A;
+    }
+
+    @NeverClassInline
+    enum InnerEnum {
+      X,
+      Y,
+      Z;
+    }
+
+    @NeverClassInline
+    enum EnumField {
+      A(InnerEnum.X),
+      B(InnerEnum.Y),
+      C(InnerEnum.Z);
+
+      InnerEnum field;
+
+      EnumField(InnerEnum s) {
+        this.field = s;
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/OrdinalHashCodeEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/OrdinalHashCodeEnumUnboxingTest.java
index 79f9176..1946116 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/OrdinalHashCodeEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/OrdinalHashCodeEnumUnboxingTest.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.enumunboxing;
 
 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 java.util.List;
@@ -43,6 +44,7 @@
             .addKeepMainRule(classToTest)
             .addKeepRules(enumKeepRules.getKeepRules())
             .enableNeverClassInliningAnnotations()
+            .enableInliningAnnotations()
             .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
             .allowDiagnosticInfoMessages()
             .setMinApi(parameters.getApiLevel())
@@ -66,8 +68,29 @@
     public static void main(String[] args) {
       System.out.println(MyEnum.A.ordinal());
       System.out.println(0);
-      System.out.println(MyEnum.A.hashCode());
+      System.out.println(ordinal(MyEnum.A));
       System.out.println(0);
+      System.out.println(ordinal(MyEnum.B));
+      System.out.println(1);
+      System.out.println(MyEnum.A.hashCode());
+      System.out.println(MyEnum.A.hashCode());
+      System.out.println(hash(MyEnum.A));
+      System.out.println(System.identityHashCode(MyEnum.A));
+      System.out.println(hash(null));
+      System.out.println(0);
+      Object o = new Object();
+      System.out.println(System.identityHashCode(o));
+      System.out.println(o.hashCode());
+    }
+
+    @NeverInline
+    private static int hash(MyEnum e) {
+      return System.identityHashCode(e);
+    }
+
+    @NeverInline
+    private static int ordinal(MyEnum e) {
+      return e.ordinal();
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/UnusedCaseEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/UnusedCaseEnumUnboxingTest.java
new file mode 100644
index 0000000..a6b7027
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/UnusedCaseEnumUnboxingTest.java
@@ -0,0 +1,98 @@
+// 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.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 UnusedCaseEnumUnboxingTest 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 UnusedCaseEnumUnboxingTest(
+      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(UnusedCaseEnumUnboxingTest.class)
+            .addKeepMainRule(Main.class)
+            .enableNeverClassInliningAnnotations()
+            .enableInliningAnnotations()
+            .addKeepRules(enumKeepRules.getKeepRules())
+            .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+            .allowDiagnosticInfoMessages()
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .inspect(this::assertFieldsRemoved)
+            .inspectDiagnosticMessages(
+                m -> assertEnumIsUnboxed(MyEnum.class, Main.class.getSimpleName(), m))
+            .run(parameters.getRuntime(), Main.class)
+            .assertSuccess();
+    assertLines2By2Correct(run.getStdOut());
+  }
+
+  private void assertFieldsRemoved(CodeInspector codeInspector) {
+    codeInspector.clazz(Main.class);
+  }
+
+  @NeverClassInline
+  enum MyEnum {
+    USED1("used1"),
+    UNUSED1("unused1"),
+    USED2("used2"),
+    UNUSED2("unused2");
+
+    final String myField;
+
+    MyEnum(String data) {
+      this.myField = data;
+    }
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      printEnumField(MyEnum.USED1);
+      System.out.println("used1");
+      printEnumField(MyEnum.USED2);
+      System.out.println("used2");
+
+      printOrdinal(MyEnum.USED1);
+      System.out.println("0");
+      printOrdinal(MyEnum.USED2);
+      System.out.println("2");
+    }
+
+    @NeverInline
+    private static void printEnumField(MyEnum e) {
+      System.out.println(e.myField);
+    }
+
+    @NeverInline
+    private static void printOrdinal(MyEnum e) {
+      System.out.println(e.ordinal());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/internal/YouTubeCompilationBase.java b/src/test/java/com/android/tools/r8/internal/YouTubeCompilationBase.java
index a6dd6b0..c8be865 100644
--- a/src/test/java/com/android/tools/r8/internal/YouTubeCompilationBase.java
+++ b/src/test/java/com/android/tools/r8/internal/YouTubeCompilationBase.java
@@ -22,6 +22,7 @@
   static final String PG_MAP = "YouTubeRelease_proguard.map";
   static final String PG_CONF = "YouTubeRelease_proguard.config";
   static final String PG_PROTO_CONF = "YouTubeRelease_proto_safety.pgconf";
+  static final String PG_MISSING_CLASSES_CONF = "YouTubeRelease_proguard_missing_classes.config";
 
   final String base;
 
@@ -38,9 +39,11 @@
     ImmutableList.Builder<Path> builder = ImmutableList.builder();
     builder.add(Paths.get(base).resolve(PG_CONF));
     builder.add(Paths.get(ToolHelper.PROGUARD_SETTINGS_FOR_INTERNAL_APPS).resolve(PG_CONF));
-    Path config = Paths.get(base).resolve(PG_PROTO_CONF);
-    if (config.toFile().exists()) {
-      builder.add(config);
+    for (String name : new String[] {PG_PROTO_CONF, PG_MISSING_CLASSES_CONF}) {
+      Path config = Paths.get(base).resolve(name);
+      if (config.toFile().exists()) {
+        builder.add(config);
+      }
     }
     return builder.build();
   }
diff --git a/src/test/java/com/android/tools/r8/internal/YouTubeV1533TreeShakeJarVerificationTest.java b/src/test/java/com/android/tools/r8/internal/YouTubeV1533TreeShakeJarVerificationTest.java
new file mode 100644
index 0000000..632210e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/internal/YouTubeV1533TreeShakeJarVerificationTest.java
@@ -0,0 +1,93 @@
+// 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.internal;
+
+import static com.android.tools.r8.ToolHelper.isLocalDevelopment;
+import static com.android.tools.r8.ToolHelper.shouldRunSlowTests;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.R8TestCompileResult;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.analysis.ProtoApplicationStats;
+import java.nio.file.Paths;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class YouTubeV1533TreeShakeJarVerificationTest extends YouTubeCompilationBase {
+
+  private static final boolean DUMP = false;
+  private static final int MAX_SIZE = 27500000;
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public YouTubeV1533TreeShakeJarVerificationTest(TestParameters parameters) {
+    super(15, 33);
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    // TODO(b/141603168): Enable this on the bots.
+    assumeTrue(isLocalDevelopment());
+    assumeTrue(shouldRunSlowTests());
+
+    LibrarySanitizer librarySanitizer =
+        new LibrarySanitizer(temp)
+            .addProgramFiles(getProgramFiles())
+            .addLibraryFiles(getLibraryFiles())
+            .sanitize()
+            .assertSanitizedProguardConfigurationIsEmpty();
+
+    R8TestCompileResult compileResult =
+        testForR8(Backend.DEX)
+            .addProgramFiles(getProgramFiles())
+            .addLibraryFiles(librarySanitizer.getSanitizedLibrary())
+            .addKeepRuleFiles(getKeepRuleFiles())
+            .addMainDexRuleFiles(getMainDexRuleFiles())
+            .allowDiagnosticMessages()
+            .allowUnusedProguardConfigurationRules()
+            .setMinApi(AndroidApiLevel.H_MR2)
+            .compile();
+
+    if (ToolHelper.isLocalDevelopment()) {
+      if (DUMP) {
+        long time = System.currentTimeMillis();
+        compileResult.writeToZip(Paths.get("YouTubeV1533-" + time + ".zip"));
+        compileResult.writeProguardMap(Paths.get("YouTubeV1533-" + time + ".map"));
+      }
+
+      DexItemFactory dexItemFactory = new DexItemFactory();
+      ProtoApplicationStats original =
+          new ProtoApplicationStats(dexItemFactory, new CodeInspector(getProgramFiles()));
+      ProtoApplicationStats actual =
+          new ProtoApplicationStats(dexItemFactory, compileResult.inspector(), original);
+      ProtoApplicationStats baseline =
+          new ProtoApplicationStats(
+              dexItemFactory,
+              new CodeInspector(getReleaseApk(), getReleaseProguardMap().toString()));
+      System.out.println(actual.getStats(baseline));
+    }
+
+    int applicationSize = compileResult.app.applicationSize();
+    System.out.println(applicationSize);
+
+    assertTrue(
+        "Expected max size of " + MAX_SIZE + ", got " + applicationSize,
+        applicationSize < MAX_SIZE);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/IrInjectionTestBase.java b/src/test/java/com/android/tools/r8/ir/IrInjectionTestBase.java
index a031cf8..b351ca2 100644
--- a/src/test/java/com/android/tools/r8/ir/IrInjectionTestBase.java
+++ b/src/test/java/com/android/tools/r8/ir/IrInjectionTestBase.java
@@ -100,9 +100,10 @@
       return iterator;
     }
 
-    private AndroidApp writeDex(DexApplication application, InternalOptions options) {
+    private AndroidApp writeDex() {
       try {
-        ToolHelper.writeApplication(application, options);
+        InternalOptions options = appView.options();
+        ToolHelper.writeApplication(appView, options);
         options.signalFinishedToConsumers();
         return consumers.build();
       } catch (ExecutionException e) {
@@ -114,7 +115,7 @@
       Timing timing = Timing.empty();
       IRConverter converter = new IRConverter(appView, timing, null, MainDexClasses.NONE);
       converter.replaceCodeForTesting(method, code);
-      AndroidApp app = writeDex(application, appView.options());
+      AndroidApp app = writeDex();
       return runOnArtRaw(app, DEFAULT_MAIN_CLASS_NAME).stdout;
     }
   }
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
index 1d7f782..807b061 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/R8InliningTest.java
@@ -359,7 +359,9 @@
     assertEquals(0, invokeCount);
 
     // The enum parameter may get unboxed.
-    m = clazz.uniqueMethodWithName("moreControlFlows");
+    m =
+        clazz.uniqueMethodWithName(
+            parameters.isCfRuntime() ? "moreControlFlows" : "moreControlFlows$enumunboxing$");
     assertTrue(m.isPresent());
 
     // Verify that a.b() is resolved to an inline instance-get.
diff --git a/src/test/java/com/android/tools/r8/naming/RenameSourceFileRetraceTest.java b/src/test/java/com/android/tools/r8/naming/RenameSourceFileRetraceTest.java
index 6f5aade..5dfd98f 100644
--- a/src/test/java/com/android/tools/r8/naming/RenameSourceFileRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/RenameSourceFileRetraceTest.java
@@ -109,11 +109,12 @@
   }
 
   private String getDefaultExpectedName(String name) {
-    if (!isCompat && !keepSourceFile) {
-      return null;
-    } else {
-      return name;
+    if (!keepSourceFile) {
+      if (parameters.getBackend() == Backend.CF || !isCompat) {
+        return null;
+      }
     }
+    return name;
   }
 
   private void inspectOutput(
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/DesugarLambdaRetraceTest.java b/src/test/java/com/android/tools/r8/naming/retrace/DesugarLambdaRetraceTest.java
index a55e1d5..3959d84 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/DesugarLambdaRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/DesugarLambdaRetraceTest.java
@@ -127,6 +127,7 @@
   @Test
   public void testLineNumberTableOnly() throws Exception {
     assumeTrue(compat);
+    assumeTrue(parameters.isDexRuntime());
     runTest(
         ImmutableList.of("-keepattributes LineNumberTable"), this::checkIsSameExceptForFileName);
   }
@@ -134,6 +135,7 @@
   @Test
   public void testNoLineNumberTable() throws Exception {
     assumeTrue(compat);
+    assumeTrue(parameters.isDexRuntime());
     runTest(ImmutableList.of(), this::checkIsSameExceptForFileNameAndLineNumber);
   }
 }
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/InliningRetraceTest.java b/src/test/java/com/android/tools/r8/naming/retrace/InliningRetraceTest.java
index 5562395..2ffe206 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/InliningRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/InliningRetraceTest.java
@@ -61,6 +61,7 @@
   @Test
   public void testLineNumberTableOnly() throws Exception {
     assumeTrue(compat);
+    assumeTrue(parameters.isDexRuntime());
     runTest(
         ImmutableList.of("-keepattributes LineNumberTable"),
         (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
@@ -72,6 +73,7 @@
   @Test
   public void testNoLineNumberTable() throws Exception {
     assumeTrue(compat);
+    assumeTrue(parameters.isDexRuntime());
     runTest(
         ImmutableList.of(),
         (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java b/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java
index 46da60b..0482945 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/VerticalClassMergingRetraceTest.java
@@ -93,6 +93,7 @@
   @Test
   public void testLineNumberTableOnly() throws Exception {
     assumeTrue(compat);
+    assumeTrue(parameters.isDexRuntime());
     runTest(
         ImmutableList.of("-keepattributes LineNumberTable"),
         (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
@@ -132,6 +133,7 @@
     // at com.android.tools.r8.naming.retraceproguard.MainApp.main(MainApp.java:7)
     // since the synthetic bridge belongs to ResourceWrapper.foo.
     assumeTrue(compat);
+    assumeTrue(parameters.isDexRuntime());
     haveSeenLines.clear();
     runTest(
         ImmutableList.of(),
diff --git a/src/test/java/com/android/tools/r8/naming/retraceproguard/InliningRetraceTest.java b/src/test/java/com/android/tools/r8/naming/retraceproguard/InliningRetraceTest.java
index aafa229..8d94fc8 100644
--- a/src/test/java/com/android/tools/r8/naming/retraceproguard/InliningRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retraceproguard/InliningRetraceTest.java
@@ -61,6 +61,7 @@
   @Test
   public void testLineNumberTableOnly() throws Exception {
     assumeTrue(compat);
+    assumeTrue(backend == Backend.DEX);
     runTest(
         ImmutableList.of("-keepattributes LineNumberTable"),
         (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
@@ -72,6 +73,7 @@
   @Test
   public void testNoLineNumberTable() throws Exception {
     assumeTrue(compat);
+    assumeTrue(backend == Backend.DEX);
     runTest(
         ImmutableList.of(),
         (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
diff --git a/src/test/java/com/android/tools/r8/naming/retraceproguard/VerticalClassMergingRetraceTest.java b/src/test/java/com/android/tools/r8/naming/retraceproguard/VerticalClassMergingRetraceTest.java
index 2874bf9..41c6451 100644
--- a/src/test/java/com/android/tools/r8/naming/retraceproguard/VerticalClassMergingRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/naming/retraceproguard/VerticalClassMergingRetraceTest.java
@@ -87,6 +87,7 @@
   @Test
   public void testLineNumberTableOnly() throws Exception {
     assumeTrue(compat);
+    assumeTrue(backend == Backend.DEX);
     runTest(
         ImmutableList.of("-keepattributes LineNumberTable"),
         (StackTrace actualStackTrace, StackTrace retracedStackTrace) -> {
@@ -102,6 +103,7 @@
   @Test
   public void testNoLineNumberTable() throws Exception {
     assumeTrue(compat);
+    assumeTrue(backend == Backend.DEX);
     haveSeenLines.clear();
     runTest(
         ImmutableList.of(),
diff --git a/src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/NoFieldMembersTest.java b/src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/EmptyClassTest.java
similarity index 65%
copy from src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/NoFieldMembersTest.java
copy to src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/EmptyClassTest.java
index 8dfe240..e0d9a33 100644
--- a/src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/NoFieldMembersTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/EmptyClassTest.java
@@ -6,20 +6,23 @@
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.IsNot.not;
 
-import com.android.tools.r8.*;
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.utils.BooleanUtils;
-import java.util.*;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
 @RunWith(Parameterized.class)
-public class NoFieldMembersTest extends TestBase {
+public class EmptyClassTest extends TestBase {
   private final TestParameters parameters;
   private final boolean enableHorizontalClassMerging;
 
-  public NoFieldMembersTest(TestParameters parameters, boolean enableHorizontalClassMerging) {
+  public EmptyClassTest(TestParameters parameters, boolean enableHorizontalClassMerging) {
     this.parameters = parameters;
     this.enableHorizontalClassMerging = enableHorizontalClassMerging;
   }
@@ -33,26 +36,20 @@
   @Test
   public void testR8() throws Exception {
     testForR8(parameters.getBackend())
-        .addInnerClasses(NoFieldMembersTest.class)
+        .addInnerClasses(EmptyClassTest.class)
         .addKeepMainRule(Main.class)
         .addOptionsModification(
             options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
-        .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Main.class)
-        .assertSuccessWithOutputLines("foo", "bar")
+        .assertSuccess()
         .inspect(
             codeInspector -> {
               if (enableHorizontalClassMerging) {
-                // TODO(b/163311975): A and B should be merged
-                //
-                //                        Class[] classes = { A.class, B.class };
-                //                        assertEquals(1, Arrays.stream(classes)
-                //                                .filter(a -> codeInspector.clazz(a).isPresent())
-                //                                .count());
                 assertThat(codeInspector.clazz(A.class), isPresent());
-                assertThat(codeInspector.clazz(B.class), isPresent());
+                assertThat(codeInspector.clazz(B.class), not(isPresent()));
+                // TODO(b/165517236): Explicitly check classes have been merged.
               } else {
                 assertThat(codeInspector.clazz(A.class), isPresent());
                 assertThat(codeInspector.clazz(B.class), isPresent());
@@ -61,27 +58,20 @@
   }
 
   @NeverClassInline
-  public static class A {
-    @NeverInline
-    public void foo() {
-      System.out.println("foo");
-    }
-  }
+  public static class A {}
 
   @NeverClassInline
   public static class B {
-    @NeverInline
-    public void bar() {
-      System.out.println("bar");
-    }
+    // TODO(b/164924717): remove non overlapping constructor requirement
+    public B(String s) {}
   }
 
   public static class Main {
     public static void main(String[] args) {
       A a = new A();
-      a.foo();
-      B b = new B();
-      b.bar();
+      System.out.println(a);
+      B b = new B("");
+      System.out.println(b);
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/IdenticalFieldMembersTest.java b/src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/IdenticalFieldMembersTest.java
index 456c4cb..7935860 100644
--- a/src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/IdenticalFieldMembersTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/IdenticalFieldMembersTest.java
@@ -42,18 +42,15 @@
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Main.class)
-        .assertSuccessWithOutputLines("foo A", "bar B")
+        .assertSuccessWithOutputLines("foo A", "bar 2")
         .inspect(
             codeInspector -> {
               if (enableHorizontalClassMerging) {
-                // TODO(b/163311975): A and B should be merged
-                //
-                //                        Class[] classes = {A.class, B.class};
-                //                        assertEquals(1, Arrays.stream(classes)
-                //                                .filter(a -> codeInspector.clazz(a).isPresent())
-                //                                .count());
                 assertThat(codeInspector.clazz(A.class), isPresent());
                 assertThat(codeInspector.clazz(B.class), isPresent());
+                // TODO(b/163311975): A and B should be merged
+                //   assertThat(codeInspector.clazz(B.class), not(isPresent()));
+                // TODO(b/165517236): Explicitly check classes have been merged.
               } else {
                 assertThat(codeInspector.clazz(A.class), isPresent());
                 assertThat(codeInspector.clazz(B.class), isPresent());
@@ -79,8 +76,8 @@
   public static class B {
     private String field;
 
-    public B(String v) {
-      this.field = v;
+    public B(int v) {
+      this.field = Integer.toString(v);
     }
 
     @NeverInline
@@ -93,7 +90,7 @@
     public static void main(String[] args) {
       A a = new A("A");
       a.foo();
-      B b = new B("B");
+      B b = new B(2);
       b.bar();
     }
   }
diff --git a/src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/NoOverlappingConstructorsPolicyTest.java b/src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/NoOverlappingConstructorsPolicyTest.java
new file mode 100644
index 0000000..e646087
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/NoOverlappingConstructorsPolicyTest.java
@@ -0,0 +1,102 @@
+// 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.horizontalclassmerging;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.IsNot.not;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.shaking.horizontalclassmerging.EmptyClassTest.A;
+import com.android.tools.r8.shaking.horizontalclassmerging.EmptyClassTest.B;
+import com.android.tools.r8.shaking.horizontalclassmerging.EmptyClassTest.Main;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class NoOverlappingConstructorsPolicyTest extends TestBase {
+  private final TestParameters parameters;
+  private final boolean enableHorizontalClassMerging;
+
+  public NoOverlappingConstructorsPolicyTest(
+      TestParameters parameters, boolean enableHorizontalClassMerging) {
+    this.parameters = parameters;
+    this.enableHorizontalClassMerging = enableHorizontalClassMerging;
+  }
+
+  @Parameterized.Parameters(name = "{0}, horizontalClassMerging:{1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(), BooleanUtils.values());
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(this.getClass())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .run(parameters.getRuntime(), Main.class)
+        .inspect(
+            codeInspector -> {
+              if (enableHorizontalClassMerging) {
+                assertThat(codeInspector.clazz(A.class), isPresent());
+                assertThat(codeInspector.clazz(B.class), isPresent());
+                assertThat(codeInspector.clazz(C.class), not(isPresent()));
+                // TODO(b/165517236): Explicitly check classes have been merged.
+              } else {
+                assertThat(codeInspector.clazz(A.class), isPresent());
+                assertThat(codeInspector.clazz(B.class), isPresent());
+                assertThat(codeInspector.clazz(C.class), isPresent());
+              }
+            });
+  }
+
+  @NeverClassInline
+  public static class A {
+    public A(String s) {
+      System.out.println(s);
+    }
+  }
+
+  @NeverClassInline
+  public static class B {
+    public B(String s) {
+      System.out.println(s);
+    }
+
+    public B(boolean b) {
+      System.out.println(b);
+    }
+  }
+
+  @NeverClassInline
+  public static class C {
+    public C(boolean b) {
+      System.out.println(b);
+    }
+  }
+
+  public static class Main {
+    public static void main(String[] args) {
+      A a = new A("foo");
+      System.out.println(a);
+      B b1 = new B("");
+      System.out.println(b1);
+      B b2 = new B(false);
+      System.out.println(b2);
+      C c = new C(true);
+      System.out.println(c);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/RemapMethodTest.java b/src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/RemapMethodTest.java
new file mode 100644
index 0000000..97df366
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/RemapMethodTest.java
@@ -0,0 +1,127 @@
+// 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.horizontalclassmerging;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.IsNot.not;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class RemapMethodTest extends TestBase {
+  private final TestParameters parameters;
+  private final boolean enableHorizontalClassMerging;
+
+  public RemapMethodTest(TestParameters parameters, boolean enableHorizontalClassMerging) {
+    this.parameters = parameters;
+    this.enableHorizontalClassMerging = enableHorizontalClassMerging;
+  }
+
+  @Parameterized.Parameters(name = "{0}, horizontalClassMerging:{1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(), BooleanUtils.values());
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(this.getClass())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options -> {
+              options.enableHorizontalClassMerging = enableHorizontalClassMerging;
+              options.enableVerticalClassMerging = false;
+            })
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        // .run(parameters.getRuntime(), Main.class)
+        // .assertSuccessWithOutputLines("foo", "foo", "bar", "bar")
+        .inspect(
+            codeInspector -> {
+              assertThat(codeInspector.clazz(A.class), isPresent());
+              assertThat(codeInspector.clazz(C.class), isPresent());
+              if (enableHorizontalClassMerging) {
+                assertThat(codeInspector.clazz(B.class), not(isPresent()));
+                assertThat(codeInspector.clazz(D.class), not(isPresent()));
+                // TODO(b/165517236): Explicitly check classes have been merged.
+              } else {
+                assertThat(codeInspector.clazz(B.class), isPresent());
+                assertThat(codeInspector.clazz(D.class), isPresent());
+              }
+            });
+  }
+
+  @NeverClassInline
+  public static class A {
+    @NeverInline
+    public void foo() {
+      System.out.println("foo");
+    }
+  }
+
+  @NeverClassInline
+  public static class B {
+    // TODO(b/164924717): remove non overlapping constructor requirement
+    public B(String s) {}
+
+    @NeverInline
+    public void bar(D d) {
+      d.bar();
+    }
+  }
+
+  @NeverClassInline
+  public static class Other {
+    String field;
+
+    public Other() {
+      field = "";
+    }
+  }
+
+  @NeverClassInline
+  public static class C extends Other {
+    @NeverInline
+    public void foo() {
+      System.out.println("foo");
+    }
+  }
+
+  @NeverClassInline
+  public static class D extends Other {
+    public D(String s) {
+      System.out.println(s);
+    }
+
+    @NeverInline
+    public void bar() {
+      System.out.println("bar");
+    }
+  }
+
+  public static class Main {
+    public static void main(String[] args) {
+      A a = new A();
+      a.foo();
+      B b = new B("bar");
+      C c = new C();
+      c.foo();
+      D d = new D("bar");
+      b.bar(d);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/NoFieldMembersTest.java b/src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/VirtualMethodNotOverlappingTest.java
similarity index 76%
rename from src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/NoFieldMembersTest.java
rename to src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/VirtualMethodNotOverlappingTest.java
index 8dfe240..711f217 100644
--- a/src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/NoFieldMembersTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/horizontalclassmerging/VirtualMethodNotOverlappingTest.java
@@ -6,6 +6,7 @@
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.core.IsNot.not;
 
 import com.android.tools.r8.*;
 import com.android.tools.r8.utils.BooleanUtils;
@@ -15,11 +16,12 @@
 import org.junit.runners.Parameterized;
 
 @RunWith(Parameterized.class)
-public class NoFieldMembersTest extends TestBase {
+public class VirtualMethodNotOverlappingTest extends TestBase {
   private final TestParameters parameters;
   private final boolean enableHorizontalClassMerging;
 
-  public NoFieldMembersTest(TestParameters parameters, boolean enableHorizontalClassMerging) {
+  public VirtualMethodNotOverlappingTest(
+      TestParameters parameters, boolean enableHorizontalClassMerging) {
     this.parameters = parameters;
     this.enableHorizontalClassMerging = enableHorizontalClassMerging;
   }
@@ -33,7 +35,7 @@
   @Test
   public void testR8() throws Exception {
     testForR8(parameters.getBackend())
-        .addInnerClasses(NoFieldMembersTest.class)
+        .addInnerClasses(VirtualMethodNotOverlappingTest.class)
         .addKeepMainRule(Main.class)
         .addOptionsModification(
             options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
@@ -45,14 +47,9 @@
         .inspect(
             codeInspector -> {
               if (enableHorizontalClassMerging) {
-                // TODO(b/163311975): A and B should be merged
-                //
-                //                        Class[] classes = { A.class, B.class };
-                //                        assertEquals(1, Arrays.stream(classes)
-                //                                .filter(a -> codeInspector.clazz(a).isPresent())
-                //                                .count());
                 assertThat(codeInspector.clazz(A.class), isPresent());
-                assertThat(codeInspector.clazz(B.class), isPresent());
+                assertThat(codeInspector.clazz(B.class), not(isPresent()));
+                // TODO(b/165517236): Explicitly check classes have been merged.
               } else {
                 assertThat(codeInspector.clazz(A.class), isPresent());
                 assertThat(codeInspector.clazz(B.class), isPresent());
@@ -70,6 +67,9 @@
 
   @NeverClassInline
   public static class B {
+    // TODO(b/164924717): remove non overlapping constructor requirement
+    public B(String s) {}
+
     @NeverInline
     public void bar() {
       System.out.println("bar");
@@ -80,7 +80,7 @@
     public static void main(String[] args) {
       A a = new A();
       a.foo();
-      B b = new B();
+      B b = new B("");
       b.bar();
     }
   }