Merge commit '871f85e8' into dev-release
diff --git a/.gitignore b/.gitignore
index 6e4c0e3..bbe6eea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -138,10 +138,43 @@
 third_party/opensource-apps/friendlyeats.tar.gz
 third_party/opensource-apps/iosched
 third_party/opensource-apps/iosched.tar.gz
+third_party/opensource-apps/kiss
+third_party/opensource-apps/kiss.tar.gz
+third_party/opensource-apps/materialistic
+third_party/opensource-apps/materialistic.tar.gz
+third_party/opensource-apps/minimal-todo
+third_party/opensource-apps/minimal-todo.tar.gz
+third_party/opensource-apps/muzei
+third_party/opensource-apps/muzei.tar.gz
+third_party/opensource-apps/newpipe
+third_party/opensource-apps/newpipe.tar.gz
+third_party/opensource-apps/rover-android
+third_party/opensource-apps/rover-android.tar.gz
+third_party/opensource-apps/santa-tracker
+third_party/opensource-apps/santa-tracker.tar.gz
+third_party/opensource-apps/signal-android
+third_party/opensource-apps/signal-android.tar.gz
+third_party/opensource-apps/simple-calendar
+third_party/opensource-apps/simple-calendar.tar.gz
+third_party/opensource-apps/simple-camera
+third_party/opensource-apps/simple-camera.tar.gz
+third_party/opensource-apps/simple-file-manager
+third_party/opensource-apps/simple-file-manager.tar.gz
+third_party/opensource-apps/simple-gallery
+third_party/opensource-apps/simple-gallery.tar.gz
+third_party/opensource-apps/sqldelight
+third_party/opensource-apps/sqldelight.tar.gz
 third_party/opensource-apps/sunflower
 third_party/opensource-apps/sunflower.tar.gz
+third_party/opensource-apps/tachiyomi
+third_party/opensource-apps/tachiyomi.tar.gz
+third_party/opensource-apps/tivi
+third_party/opensource-apps/tivi.tar.gz
+third_party/opensource-apps/tusky
+third_party/opensource-apps/tusky.tar.gz
 third_party/opensource-apps/wikipedia
 third_party/opensource-apps/wikipedia.tar.gz
+third_party/opensource-apps/android/compose-samples/*
 third_party/opensource_apps
 third_party/opensource_apps.tar.gz
 third_party/proguard/*
diff --git a/build.gradle b/build.gradle
index 558919e..981dcfd 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1875,10 +1875,11 @@
 }
 
 def printStackTrace(TestResult result) {
+    filterStackTraces(result)
     if (project.hasProperty('r8lib') || project.hasProperty('r8lib_no_deps')) {
         def out = new StringBuffer()
         def err = new StringBuffer()
-        def command = "python tools/retrace.py"
+        def command = "python tools/retrace.py --quiet"
         def header = "RETRACED STACKTRACE";
         if (System.getenv('BUILDBOT_BUILDERNAME') != null
                 && !System.getenv('BUILDBOT_BUILDERNAME').endsWith("_release")) {
@@ -1893,19 +1894,49 @@
         result.exception.printStackTrace(processIn)
         processIn.flush()
         processIn.close()
-        if (process.waitFor() != 0) {
+        def errorDuringRetracing = process.waitFor() != 0
+        if (errorDuringRetracing) {
             out.append("ERROR DURING RETRACING\n")
             out.append(err.toString())
         }
-        out.append("\n\n--------------------------------------\n")
-        out.append("OBFUSCATED STACKTRACE\n")
-        out.append("--------------------------------------\n")
-        result.exceptions.add(0, new Exception(out.toString()))
+        if (project.hasProperty('print_obfuscated_stacktraces') || errorDuringRetracing) {
+            out.append("\n\n--------------------------------------\n")
+            out.append("OBFUSCATED STACKTRACE\n")
+            out.append("--------------------------------------\n")
+        } else {
+          result.exceptions.clear()
+        }
+        def exception = new Exception(out.toString())
+        exception.setStackTrace([] as StackTraceElement[])
+        result.exceptions.add(0, exception)
     } else {
         result.exception.printStackTrace()
     }
 }
 
+def filterStackTraces(TestResult result) {
+    for (Throwable throwable : result.getExceptions()) {
+        filterStackTrace(throwable)
+    }
+}
+
+def filterStackTrace(Throwable exception) {
+    if (!project.hasProperty('print_full_stacktraces')) {
+        def elements = []
+        def skipped = []
+        for (StackTraceElement element : exception.getStackTrace()) {
+            if (element.toString().contains("com.android.tools.r8")) {
+                elements.addAll(skipped)
+                elements.add(element)
+                skipped.clear()
+            } else {
+                skipped.add(element)
+            }
+        }
+        exception.setStackTrace(elements as StackTraceElement[])
+    }
+}
+
 test {
     if (project.hasProperty('generate_golden_files_to')) {
         systemProperty 'generate_golden_files_to', project.property('generate_golden_files_to')
diff --git a/infra/config/global/cr-buildbucket.cfg b/infra/config/global/cr-buildbucket.cfg
index c0222e8..3ebf03f 100644
--- a/infra/config/global/cr-buildbucket.cfg
+++ b/infra/config/global/cr-buildbucket.cfg
@@ -167,15 +167,6 @@
       }
     }
     builders {
-      name: "linux-jdk8_9"
-      mixins: "linux"
-      mixins: "normal"
-      priority: 26
-      recipe {
-        properties_j: "test_options:[\"--runtimes=jdk8:jdk9\", \"--no_internal\", \"--one_line_per_test\", \"--archive_failures\"]"
-      }
-    }
-    builders {
       name: "linux_release"
       mixins: "normal"
       mixins: "linux"
@@ -346,24 +337,6 @@
       }
     }
     builders {
-      name: "linux-run-on-as-app"
-      mixins: "linux"
-      mixins: "normal"
-      recipe {
-        properties: "run_on_apps:True"
-        properties: "recompilation:False"
-      }
-    }
-    builders {
-      name: "linux-run-on-as-app-recompilation"
-      mixins: "linux"
-      mixins: "normal"
-      recipe {
-        properties: "run_on_apps:True"
-        properties: "recompilation:True"
-      }
-    }
-    builders {
       name: "linux-run-on-app-dump"
       mixins: "linux"
       mixins: "normal"
@@ -373,15 +346,6 @@
       }
     }
     builders {
-      name: "linux-run-on-as-app_release"
-      mixins: "linux"
-      mixins: "normal"
-      execution_timeout_secs: 25200  # 7h
-      recipe {
-        properties: "run_on_apps:True"
-      }
-    }
-    builders {
       name: "linux-run-on-app-dump_release"
       mixins: "linux"
       mixins: "normal"
@@ -448,5 +412,15 @@
         properties: "tool:r8"
       }
     }
+    builders {
+      name: "kotlin-builder"
+      mixins: "linux"
+      mixins: "normal"
+      recipe {
+        properties_j: "test_options:[\"--not_used\"]"
+        properties: "test_wrapper:google-scripts/build.py"
+        properties: "kotlin_repo:True"
+      }
+    }
   }
 }
diff --git a/infra/config/global/luci-milo.cfg b/infra/config/global/luci-milo.cfg
index e76751b..1c2c87f 100644
--- a/infra/config/global/luci-milo.cfg
+++ b/infra/config/global/luci-milo.cfg
@@ -76,16 +76,6 @@
     short_name: "internal"
   }
   builders {
-    name: "buildbucket/luci.r8.ci/linux-run-on-as-app"
-    category: "R8"
-    short_name: "apps"
-  }
-  builders {
-    name: "buildbucket/luci.r8.ci/linux-run-on-as-app-recompilation"
-    category: "R8"
-    short_name: "apps-rec"
-  }
-  builders {
     name: "buildbucket/luci.r8.ci/linux-run-on-app-dump"
     category: "R8"
     short_name: "apps-dump"
@@ -166,11 +156,6 @@
     short_name: "internal"
   }
   builders {
-    name: "buildbucket/luci.r8.ci/linux-run-on-as-app_release"
-    category: "R8 release"
-    short_name: "apps"
-  }
-  builders {
     name: "buildbucket/luci.r8.ci/linux-run-on-app-dump_release"
     category: "R8 release"
     short_name: "apps-dump"
@@ -190,5 +175,9 @@
     category: "win release"
     short_name: "win"
   }
-
+  builders {
+    name: "buildbucket/luci.r8.ci/kotlin-builder"
+    category: "kotlin"
+    short_name: "kotlin_builder"
+  }
 }
diff --git a/infra/config/global/luci-notify.cfg b/infra/config/global/luci-notify.cfg
index 1e48f29..92b76f3 100644
--- a/infra/config/global/luci-notify.cfg
+++ b/infra/config/global/luci-notify.cfg
@@ -39,11 +39,6 @@
     repository: "https://r8.googlesource.com/r8"
   }
   builders {
-    name: "linux-jdk8_9"
-    bucket: "ci"
-    repository: "https://r8.googlesource.com/r8"
-  }
-  builders {
     name: "linux_release"
     bucket: "ci"
     repository: "https://r8.googlesource.com/r8"
@@ -139,26 +134,11 @@
     repository: "https://r8.googlesource.com/r8"
   }
   builders {
-    name: "linux-run-on-as-app"
-    bucket: "ci"
-    repository: "https://r8.googlesource.com/r8"
-  }
-  builders {
-    name: "linux-run-on-as-app-recompilation"
-    bucket: "ci"
-    repository: "https://r8.googlesource.com/r8"
-  }
-  builders {
     name: "linux-run-on-app-dump"
     bucket: "ci"
     repository: "https://r8.googlesource.com/r8"
   }
   builders {
-    name: "linux-run-on-as-app_release"
-    bucket: "ci"
-    repository: "https://r8.googlesource.com/r8"
-  }
-  builders {
     name: "linux-run-on-app-dump_release"
     bucket: "ci"
     repository: "https://r8.googlesource.com/r8"
diff --git a/infra/config/global/luci-scheduler.cfg b/infra/config/global/luci-scheduler.cfg
index a0b3fa3..a18a124 100644
--- a/infra/config/global/luci-scheduler.cfg
+++ b/infra/config/global/luci-scheduler.cfg
@@ -29,7 +29,6 @@
   triggers: "linux"
   triggers: "linux-jdk8"
   triggers: "linux-jdk9"
-  triggers: "linux-jdk8_9"
   triggers: "linux-android-4.0.4"
   triggers: "linux-android-4.4.4"
   triggers: "linux-android-5.1.1"
@@ -38,8 +37,6 @@
   triggers: "linux-android-8.1.0"
   triggers: "linux-android-9.0.0"
   triggers: "linux-android-10.0.0"
-  triggers: "linux-run-on-as-app"
-  triggers: "linux-run-on-as-app-recompilation"
   triggers: "linux-run-on-app-dump"
   triggers: "linux-internal"
   triggers: "linux-jctf"
@@ -58,6 +55,16 @@
 }
 
 trigger {
+  id: "kotlin_trigger"
+  acl_sets: "default"
+  gitiles: {
+    repo: "https://github.googlesource.com/google/kotlin"
+    refs: "refs/heads/google-ir"
+  }
+  triggers: "kotlin-builder"
+}
+
+trigger {
   id: "branch-gitiles-trigger"
   acl_sets: "default"
   gitiles: {
@@ -77,7 +84,6 @@
   triggers: "linux-android-10.0.0_release"
   triggers: "linux-internal_release"
   triggers: "linux-jctf_release"
-  triggers: "linux-run-on-as-app_release"
   triggers: "linux-run-on-app-dump_release"
   triggers: "linux_release"
   triggers: "r8cf-linux-jctf_release"
@@ -128,6 +134,19 @@
 }
 
 job {
+  id: "kotlin-builder"
+  acl_sets: "default"
+  triggering_policy: {
+    max_concurrent_invocations: 1
+  }
+  buildbucket {
+    server: "cr-buildbucket.appspot.com"
+    bucket: "luci.r8.ci"
+    builder: "kotlin-builder"
+  }
+}
+
+job {
   id: "linux"
   acl_sets: "default"
   triggering_policy: {
@@ -170,20 +189,6 @@
 }
 
 job {
-  id: "linux-jdk8_9"
-  acl_sets: "default"
-  triggering_policy: {
-    kind: GREEDY_BATCHING
-    max_concurrent_invocations: 2
-  }
-  buildbucket {
-    server: "cr-buildbucket.appspot.com"
-    bucket: "luci.r8.ci"
-    builder: "linux-jdk8_9"
-  }
-}
-
-job {
   id: "linux-android-4.0.4"
   acl_sets: "default"
   triggering_policy: {
@@ -429,35 +434,6 @@
 }
 
 job {
-  id: "linux-run-on-as-app"
-  acl_sets: "default"
-  triggering_policy: {
-    kind: GREEDY_BATCHING
-    max_concurrent_invocations: 3
-  }
-  buildbucket {
-    server: "cr-buildbucket.appspot.com"
-    bucket: "luci.r8.ci"
-    builder: "linux-run-on-as-app"
-  }
-}
-
-job {
-  id: "linux-run-on-as-app-recompilation"
-  acl_sets: "default"
-  triggering_policy: {
-    kind: GREEDY_BATCHING
-    max_concurrent_invocations: 3
-  }
-  buildbucket {
-    server: "cr-buildbucket.appspot.com"
-    bucket: "luci.r8.ci"
-    builder: "linux-run-on-as-app-recompilation"
-  }
-}
-
-
-job {
   id: "linux-run-on-app-dump"
   acl_sets: "default"
   triggering_policy: {
@@ -472,20 +448,6 @@
 }
 
 job {
-  id: "linux-run-on-as-app_release"
-  acl_sets: "default"
-  triggering_policy: {
-    max_batch_size: 1
-    max_concurrent_invocations: 3
-  }
-  buildbucket {
-    server: "cr-buildbucket.appspot.com"
-    bucket: "luci.r8.ci"
-    builder: "linux-run-on-as-app_release"
-  }
-}
-
-job {
   id: "linux-run-on-app-dump_release"
   acl_sets: "default"
   triggering_policy: {
diff --git a/src/library_desugar/desugar_jdk_libs.json b/src/library_desugar/desugar_jdk_libs.json
index 98ceb80..ed216b0 100644
--- a/src/library_desugar/desugar_jdk_libs.json
+++ b/src/library_desugar/desugar_jdk_libs.json
@@ -2,7 +2,7 @@
   "configuration_format_version": 3,
   "group_id" : "com.tools.android",
   "artifact_id" : "desugar_jdk_libs",
-  "version": "1.1.0",
+  "version": "1.1.1",
   "required_compilation_api_level": 26,
   "synthesized_library_classes_package_prefix": "j$.",
   "support_all_callbacks_from_library": true,
@@ -243,7 +243,7 @@
     "-keepclassmembers class j$.util.concurrent.ConcurrentHashMap$TreeBin { int lockState; }",
     "-keepclassmembers class j$.util.concurrent.ConcurrentHashMap { int sizeCtl; int transferIndex; long baseCount; int cellsBusy; }",
     "-keepclassmembers class j$.util.concurrent.ConcurrentHashMap$CounterCell { long value; }",
-    "-keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); }",
+    "-keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); public static final !synthetic <fields>; }",
     "-keeppackagenames j$",
     "-keepclassmembers class j$.util.IntSummaryStatistics { long count; long sum; int min; int max; }",
     "-keepclassmembers class j$.util.LongSummaryStatistics { long count; long sum; long min; long max; }",
diff --git a/src/library_desugar/desugar_jdk_libs_alternative_3.json b/src/library_desugar/desugar_jdk_libs_alternative_3.json
index 628fc53..012d6ce 100644
--- a/src/library_desugar/desugar_jdk_libs_alternative_3.json
+++ b/src/library_desugar/desugar_jdk_libs_alternative_3.json
@@ -2,7 +2,7 @@
   "configuration_format_version": 3,
   "group_id" : "com.tools.android",
   "artifact_id" : "desugar_jdk_libs_alternative_3",
-  "version": "1.1.0",
+  "version": "1.1.1",
   "required_compilation_api_level": 26,
   "synthesized_library_classes_package_prefix": "j$.",
   "support_all_callbacks_from_library": false,
@@ -246,7 +246,7 @@
     "-keepclassmembers class j$.util.concurrent.ConcurrentHashMap$TreeBin { int lockState; }",
     "-keepclassmembers class j$.util.concurrent.ConcurrentHashMap { int sizeCtl; int transferIndex; long baseCount; int cellsBusy; }",
     "-keepclassmembers class j$.util.concurrent.ConcurrentHashMap$CounterCell { long value; }",
-    "-keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); }",
+    "-keepclassmembers enum * { public static **[] values(); public static ** valueOf(java.lang.String); public static final !synthetic <fields>; }",
     "-keeppackagenames j$",
     "-keepclassmembers class j$.util.IntSummaryStatistics { long count; long sum; int min; int max; }",
     "-keepclassmembers class j$.util.LongSummaryStatistics { long count; long sum; long min; long max; }",
diff --git a/src/main/java/com/android/tools/r8/BaseCommand.java b/src/main/java/com/android/tools/r8/BaseCommand.java
index 036dd57..893bc64 100644
--- a/src/main/java/com/android/tools/r8/BaseCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCommand.java
@@ -275,14 +275,16 @@
      * @see #addMainDexListFiles(Path...)
      */
     public B addMainDexListFiles(Collection<Path> files) {
-      guard(() -> {
-        try {
-          app.addMainDexListFiles(files);
-        } catch (NoSuchFileException e) {
-          reporter.error(new StringDiagnostic(
-              "Main-dex-ist file does not exist", new PathOrigin(Paths.get(e.getFile()))));
-        }
-      });
+      guard(
+          () -> {
+            try {
+              app.addMainDexListFiles(files);
+            } catch (NoSuchFileException e) {
+              reporter.error(
+                  new StringDiagnostic(
+                      "Main-dex-list file does not exist", new PathOrigin(Paths.get(e.getFile()))));
+            }
+          });
       return self();
     }
 
diff --git a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
index 80dcf13..b12b467 100644
--- a/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
+++ b/src/main/java/com/android/tools/r8/BaseCompilerCommand.java
@@ -109,6 +109,15 @@
     return minApiLevel;
   }
 
+  void dumpBaseCommandOptions(DumpOptions.Builder builder) {
+    builder
+        .setCompilationMode(getMode())
+        .setMinApi(getMinApiLevel())
+        .setOptimizeMultidexForLinearAlloc(isOptimizeMultidexForLinearAlloc())
+        .setThreadCount(getThreadCount())
+        .setDesugarState(getDesugarState());
+  }
+
   /**
    * Get the program consumer that will receive the compilation output.
    *
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index 3372bc9..501d4ae 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -6,6 +6,7 @@
 import static com.android.tools.r8.utils.InternalOptions.DETERMINISTIC_DEBUGGING;
 
 import com.android.tools.r8.AssertionsConfiguration.AssertionTransformation;
+import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.errors.DexFileOverflowDiagnostic;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.inspector.Inspector;
@@ -449,7 +450,17 @@
       internal.dumpInputToDirectory = null;
       internal.dumpInputToFile = null;
     }
+    internal.dumpOptions = dumpOptions();
 
     return internal;
   }
+
+  private DumpOptions dumpOptions() {
+    DumpOptions.Builder builder = DumpOptions.builder(Tool.D8);
+    dumpBaseCommandOptions(builder);
+    return builder
+        .setIntermediate(intermediate)
+        .setDesugaredLibraryConfiguration(libraryConfiguration)
+        .build();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/DumpOptions.java b/src/main/java/com/android/tools/r8/DumpOptions.java
new file mode 100644
index 0000000..cd2ac79
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/DumpOptions.java
@@ -0,0 +1,263 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8;
+
+import com.android.tools.r8.dex.Marker.Tool;
+import com.android.tools.r8.features.FeatureSplitConfiguration;
+import com.android.tools.r8.ir.desugar.DesugaredLibraryConfiguration;
+import com.android.tools.r8.shaking.ProguardConfiguration;
+import com.android.tools.r8.utils.InternalOptions.DesugarState;
+import com.android.tools.r8.utils.ThreadUtils;
+import java.util.Optional;
+
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public class DumpOptions {
+
+  // The following keys and values should not be changed to keep the dump utility backward
+  // compatible with previous versions. They are also used by the python script compileDump and
+  // the corresponding CompileDumpCompatR8 java class.
+  private static final String TOOL_KEY = "tool";
+  private static final String MODE_KEY = "mode";
+  private static final String DEBUG_MODE_VALUE = "debug";
+  private static final String RELEASE_MODE_VALUE = "release";
+  private static final String MIN_API_KEY = "min-api";
+  private static final String OPTIMIZE_MULTIDEX_FOR_LINEAR_ALLOC_KEY =
+      "optimize-multidex-for-linear-alloc";
+  private static final String THREAD_COUNT_KEY = "thread-count";
+  private static final String DESUGAR_STATE_KEY = "desugar-state";
+  private static final String INTERMEDIATE_KEY = "intermediate";
+  private static final String INCLUDE_DATA_RESOURCES_KEY = "include-data-resources";
+  private static final String TREE_SHAKING_KEY = "tree-shaking";
+  private static final String MINIFICATION_KEY = "minification";
+  private static final String FORCE_PROGUARD_COMPATIBILITY_KEY = "force-proguard-compatibility";
+
+  private final Tool tool;
+  private final CompilationMode compilationMode;
+  private final int minApi;
+  private final boolean optimizeMultidexForLinearAlloc;
+  private final int threadCount;
+  private final DesugarState desugarState;
+  private final Optional<Boolean> intermediate;
+  private final Optional<Boolean> includeDataResources;
+  private final Optional<Boolean> treeShaking;
+  private final Optional<Boolean> minification;
+  private final Optional<Boolean> forceProguardCompatibility;
+
+  // Dump if present.
+  private final DesugaredLibraryConfiguration desugaredLibraryConfiguration;
+  private final FeatureSplitConfiguration featureSplitConfiguration;
+  private final ProguardConfiguration proguardConfiguration;
+
+  // Reporting only.
+  private final boolean dumpInputToFile;
+
+  private DumpOptions(
+      Tool tool,
+      CompilationMode compilationMode,
+      int minAPI,
+      DesugaredLibraryConfiguration desugaredLibraryConfiguration,
+      boolean optimizeMultidexForLinearAlloc,
+      int threadCount,
+      DesugarState desugarState,
+      Optional<Boolean> intermediate,
+      Optional<Boolean> includeDataResources,
+      Optional<Boolean> treeShaking,
+      Optional<Boolean> minification,
+      Optional<Boolean> forceProguardCompatibility,
+      FeatureSplitConfiguration featureSplitConfiguration,
+      ProguardConfiguration proguardConfiguration,
+      boolean dumpInputToFile) {
+    this.tool = tool;
+    this.compilationMode = compilationMode;
+    this.minApi = minAPI;
+    this.desugaredLibraryConfiguration = desugaredLibraryConfiguration;
+    this.optimizeMultidexForLinearAlloc = optimizeMultidexForLinearAlloc;
+    this.threadCount = threadCount;
+    this.desugarState = desugarState;
+    this.intermediate = intermediate;
+    this.includeDataResources = includeDataResources;
+    this.treeShaking = treeShaking;
+    this.minification = minification;
+    this.forceProguardCompatibility = forceProguardCompatibility;
+    this.featureSplitConfiguration = featureSplitConfiguration;
+    this.proguardConfiguration = proguardConfiguration;
+    this.dumpInputToFile = dumpInputToFile;
+  }
+
+  public String dumpOptions() {
+    StringBuilder builder = new StringBuilder();
+    addDumpEntry(builder, TOOL_KEY, tool.name());
+    // We keep the following values for backward compatibility.
+    addDumpEntry(
+        builder,
+        MODE_KEY,
+        compilationMode == CompilationMode.DEBUG ? DEBUG_MODE_VALUE : RELEASE_MODE_VALUE);
+    addDumpEntry(builder, MIN_API_KEY, minApi);
+    addDumpEntry(builder, OPTIMIZE_MULTIDEX_FOR_LINEAR_ALLOC_KEY, optimizeMultidexForLinearAlloc);
+    if (threadCount != ThreadUtils.NOT_SPECIFIED) {
+      addDumpEntry(builder, THREAD_COUNT_KEY, threadCount);
+    }
+    addDumpEntry(builder, DESUGAR_STATE_KEY, desugarState);
+    addOptionalDumpEntry(builder, INTERMEDIATE_KEY, intermediate);
+    addOptionalDumpEntry(builder, INCLUDE_DATA_RESOURCES_KEY, includeDataResources);
+    addOptionalDumpEntry(builder, TREE_SHAKING_KEY, treeShaking);
+    addOptionalDumpEntry(builder, MINIFICATION_KEY, minification);
+    addOptionalDumpEntry(builder, FORCE_PROGUARD_COMPATIBILITY_KEY, forceProguardCompatibility);
+    return builder.toString();
+  }
+
+  private void addOptionalDumpEntry(StringBuilder builder, String key, Optional<?> optionalValue) {
+    optionalValue.ifPresent(bool -> addDumpEntry(builder, key, bool));
+  }
+
+  private void addDumpEntry(StringBuilder builder, String key, Object value) {
+    builder.append(key).append("=").append(value).append("\n");
+  }
+
+  private boolean hasDesugaredLibraryConfiguration() {
+    return desugaredLibraryConfiguration != null
+        && desugaredLibraryConfiguration
+            != DesugaredLibraryConfiguration.EMPTY_DESUGARED_LIBRARY_CONFIGURATION;
+  }
+
+  public String getDesugaredLibraryJsonSource() {
+    if (hasDesugaredLibraryConfiguration()) {
+      return desugaredLibraryConfiguration.getJsonSource();
+    }
+    return null;
+  }
+
+  public FeatureSplitConfiguration getFeatureSplitConfiguration() {
+    return featureSplitConfiguration;
+  }
+
+  public String getParsedProguardConfiguration() {
+    return proguardConfiguration == null ? null : proguardConfiguration.getParsedConfiguration();
+  }
+
+  public boolean dumpInputToFile() {
+    return dumpInputToFile;
+  }
+
+  public static Builder builder(Tool tool) {
+    return new Builder(tool);
+  }
+
+  public static class Builder {
+    private final Tool tool;
+    private CompilationMode compilationMode;
+    private int minApi;
+    private boolean optimizeMultidexForLinearAlloc;
+    private int threadCount;
+    private DesugarState desugarState;
+    private Optional<Boolean> intermediate = Optional.empty();
+    private Optional<Boolean> includeDataResources = Optional.empty();
+    private Optional<Boolean> treeShaking = Optional.empty();
+    private Optional<Boolean> minification = Optional.empty();
+    private Optional<Boolean> forceProguardCompatibility = Optional.empty();
+    // Dump if present.
+    private DesugaredLibraryConfiguration desugaredLibraryConfiguration;
+    private FeatureSplitConfiguration featureSplitConfiguration;
+    private ProguardConfiguration proguardConfiguration;
+
+    // Reporting only.
+    private boolean dumpInputToFile;
+
+    public Builder(Tool tool) {
+      this.tool = tool;
+    }
+
+    public Builder setCompilationMode(CompilationMode compilationMode) {
+      this.compilationMode = compilationMode;
+      return this;
+    }
+
+    public Builder setMinApi(int minAPI) {
+      this.minApi = minAPI;
+      return this;
+    }
+
+    public Builder setDesugaredLibraryConfiguration(
+        DesugaredLibraryConfiguration desugaredLibraryConfiguration) {
+      this.desugaredLibraryConfiguration = desugaredLibraryConfiguration;
+      return this;
+    }
+
+    public Builder setOptimizeMultidexForLinearAlloc(boolean optimizeMultidexForLinearAlloc) {
+      this.optimizeMultidexForLinearAlloc = optimizeMultidexForLinearAlloc;
+      return this;
+    }
+
+    public Builder setThreadCount(int threadCount) {
+      this.threadCount = threadCount;
+      return this;
+    }
+
+    public Builder setDesugarState(DesugarState desugarState) {
+      this.desugarState = desugarState;
+      return this;
+    }
+
+    public Builder setIntermediate(boolean intermediate) {
+      this.intermediate = Optional.of(intermediate);
+      return this;
+    }
+
+    public Builder setIncludeDataResources(Optional<Boolean> includeDataResources) {
+      this.includeDataResources = includeDataResources;
+      return this;
+    }
+
+    public Builder setForceProguardCompatibility(boolean forceProguardCompatibility) {
+      this.forceProguardCompatibility = Optional.of(forceProguardCompatibility);
+      return this;
+    }
+
+    public Builder setMinification(boolean minification) {
+      this.minification = Optional.of(minification);
+      return this;
+    }
+
+    public Builder setTreeShaking(boolean treeShaking) {
+      this.treeShaking = Optional.of(treeShaking);
+      return this;
+    }
+
+    public Builder setDumpInputToFile(boolean dumpInputToFile) {
+      this.dumpInputToFile = dumpInputToFile;
+      return this;
+    }
+
+    public Builder setFeatureSplitConfiguration(
+        FeatureSplitConfiguration featureSplitConfiguration) {
+      this.featureSplitConfiguration = featureSplitConfiguration;
+      return this;
+    }
+
+    public Builder setProguardConfiguration(ProguardConfiguration proguardConfiguration) {
+      this.proguardConfiguration = proguardConfiguration;
+      return this;
+    }
+
+    public DumpOptions build() {
+      return new DumpOptions(
+          tool,
+          compilationMode,
+          minApi,
+          desugaredLibraryConfiguration,
+          optimizeMultidexForLinearAlloc,
+          threadCount,
+          desugarState,
+          intermediate,
+          includeDataResources,
+          treeShaking,
+          minification,
+          forceProguardCompatibility,
+          featureSplitConfiguration,
+          proguardConfiguration,
+          dumpInputToFile);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/L8Command.java b/src/main/java/com/android/tools/r8/L8Command.java
index 8e4bb2a..dce2b21 100644
--- a/src/main/java/com/android/tools/r8/L8Command.java
+++ b/src/main/java/com/android/tools/r8/L8Command.java
@@ -7,6 +7,7 @@
 
 import com.android.tools.r8.AssertionsConfiguration.AssertionTransformation;
 import com.android.tools.r8.ProgramResource.Kind;
+import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.errors.DexFileOverflowDiagnostic;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.inspector.Inspector;
@@ -196,6 +197,7 @@
       assert internal.threadCount == ThreadUtils.NOT_SPECIFIED;
       internal.threadCount = getThreadCount();
     }
+    internal.dumpOptions = dumpOptions();
 
     return internal;
   }
@@ -383,4 +385,10 @@
     @Override
     public void finished(DiagnosticsHandler handler) {}
   }
+
+  private DumpOptions dumpOptions() {
+    DumpOptions.Builder builder = DumpOptions.builder(Tool.L8);
+    dumpBaseCommandOptions(builder);
+    return builder.setDesugaredLibraryConfiguration(libraryConfiguration).build();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index b97eb7f..cfb3540 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -40,8 +40,11 @@
 import com.android.tools.r8.graph.SubtypingInfo;
 import com.android.tools.r8.graph.analysis.ClassInitializerAssertionEnablingAnalysis;
 import com.android.tools.r8.graph.analysis.InitializedClassesInInstanceMethodsAnalysis;
+import com.android.tools.r8.graph.classmerging.StaticallyMergedClasses;
+import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMerger;
 import com.android.tools.r8.horizontalclassmerging.HorizontalClassMergerGraphLens;
+import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
 import com.android.tools.r8.inspector.internal.InspectorImpl;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.desugar.BackportedMethodRewriter;
@@ -523,7 +526,11 @@
           NestedGraphLens lens = staticClassMerger.run();
           appView.rewriteWithLens(lens);
           timing.end();
+        } else {
+          appView.setStaticallyMergedClasses(StaticallyMergedClasses.empty());
         }
+        assert appView.staticallyMergedClasses() != null;
+
         if (options.enableVerticalClassMerging) {
           timing.begin("VerticalClassMerger");
           VerticalClassMerger verticalClassMerger =
@@ -535,12 +542,14 @@
                   mainDexTracingResult);
           VerticalClassMergerGraphLens lens = verticalClassMerger.run();
           if (lens != null) {
-            appView.setVerticallyMergedClasses(lens.getMergedClasses());
             appView.rewriteWithLens(lens);
             runtimeTypeCheckInfo = runtimeTypeCheckInfo.rewriteWithLens(lens);
           }
           timing.end();
+        } else {
+          appView.setVerticallyMergedClasses(VerticallyMergedClasses.empty());
         }
+        assert appView.verticallyMergedClasses() != null;
 
         if (options.enableArgumentRemoval) {
           SubtypingInfo subtypingInfo = appViewWithLiveness.appInfo().computeSubtypingInfo();
@@ -585,8 +594,9 @@
             runtimeTypeCheckInfo = null;
           }
           timing.end();
+        } else {
+          appView.setHorizontallyMergedClasses(HorizontallyMergedClasses.empty());
         }
-
       }
 
       // None of the optimizations above should lead to the creation of type lattice elements.
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 4a68964..40be618 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -7,6 +7,7 @@
 
 import com.android.tools.r8.AssertionsConfiguration.AssertionTransformation;
 import com.android.tools.r8.ProgramResource.Kind;
+import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.errors.DexFileOverflowDiagnostic;
 import com.android.tools.r8.experimental.graphinfo.GraphConsumer;
 import com.android.tools.r8.features.FeatureSplitConfiguration;
@@ -42,6 +43,7 @@
 import java.util.Collection;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.function.BiFunction;
 import java.util.function.BiPredicate;
 import java.util.function.Consumer;
@@ -98,6 +100,7 @@
     private boolean disableMinification = false;
     private boolean disableVerticalClassMerging = false;
     private boolean forceProguardCompatibility = false;
+    private Optional<Boolean> includeDataResources = Optional.empty();
     private StringConsumer proguardMapConsumer = null;
     private StringConsumer proguardUsageConsumer = null;
     private StringConsumer proguardSeedsConsumer = null;
@@ -368,6 +371,7 @@
      */
     @Override
     public Builder setOutput(Path outputPath, OutputMode outputMode, boolean includeDataResources) {
+      this.includeDataResources = Optional.of(includeDataResources);
       return super.setOutput(outputPath, outputMode, includeDataResources);
     }
 
@@ -578,6 +582,7 @@
               configuration.isObfuscating(),
               disableVerticalClassMerging,
               forceProguardCompatibility,
+              includeDataResources,
               proguardMapConsumer,
               proguardUsageConsumer,
               proguardSeedsConsumer,
@@ -667,6 +672,7 @@
   private final boolean enableMinification;
   private final boolean disableVerticalClassMerging;
   private final boolean forceProguardCompatibility;
+  private final Optional<Boolean> includeDataResources;
   private final StringConsumer proguardMapConsumer;
   private final StringConsumer proguardUsageConsumer;
   private final StringConsumer proguardSeedsConsumer;
@@ -741,6 +747,7 @@
       boolean enableMinification,
       boolean disableVerticalClassMerging,
       boolean forceProguardCompatibility,
+      Optional<Boolean> includeDataResources,
       StringConsumer proguardMapConsumer,
       StringConsumer proguardUsageConsumer,
       StringConsumer proguardSeedsConsumer,
@@ -781,6 +788,7 @@
     this.enableMinification = enableMinification;
     this.disableVerticalClassMerging = disableVerticalClassMerging;
     this.forceProguardCompatibility = forceProguardCompatibility;
+    this.includeDataResources = includeDataResources;
     this.proguardMapConsumer = proguardMapConsumer;
     this.proguardUsageConsumer = proguardUsageConsumer;
     this.proguardSeedsConsumer = proguardSeedsConsumer;
@@ -803,6 +811,7 @@
     enableMinification = false;
     disableVerticalClassMerging = false;
     forceProguardCompatibility = false;
+    includeDataResources = null;
     proguardMapConsumer = null;
     proguardUsageConsumer = null;
     proguardSeedsConsumer = null;
@@ -974,6 +983,7 @@
       internal.dumpInputToDirectory = null;
       internal.dumpInputToFile = null;
     }
+    internal.dumpOptions = dumpOptions();
 
     return internal;
   }
@@ -1002,4 +1012,18 @@
       System.out.print(string);
     }
   }
+
+  private DumpOptions dumpOptions() {
+    DumpOptions.Builder builder = DumpOptions.builder(Tool.R8);
+    dumpBaseCommandOptions(builder);
+    return builder
+        .setIncludeDataResources(includeDataResources)
+        .setTreeShaking(getEnableTreeShaking())
+        .setMinification(getEnableMinification())
+        .setForceProguardCompatibility(forceProguardCompatibility)
+        .setFeatureSplitConfiguration(featureSplitConfiguration)
+        .setProguardConfiguration(proguardConfiguration)
+        .setDesugaredLibraryConfiguration(libraryConfiguration)
+        .build();
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/bisect/BisectState.java b/src/main/java/com/android/tools/r8/bisect/BisectState.java
index cd3fe0a..85fa3eb 100644
--- a/src/main/java/com/android/tools/r8/bisect/BisectState.java
+++ b/src/main/java/com/android/tools/r8/bisect/BisectState.java
@@ -323,7 +323,7 @@
 
   private static List<DexProgramClass> getSortedClasses(DexApplication app) {
     List<DexProgramClass> classes = new ArrayList<>(app.classes());
-    classes.sort((a, b) -> a.type.slowCompareTo(b.type, NamingLens.getIdentityLens()));
+    classes.sort((a, b) -> a.type.compareToWithNamingLens(b.type, NamingLens.getIdentityLens()));
     return classes;
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/CfVersion.java b/src/main/java/com/android/tools/r8/cf/CfVersion.java
index 84228b9..d90696c 100644
--- a/src/main/java/com/android/tools/r8/cf/CfVersion.java
+++ b/src/main/java/com/android/tools/r8/cf/CfVersion.java
@@ -3,10 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.cf;
 
-import java.util.Comparator;
+import com.android.tools.r8.utils.structural.DefaultCompareToVisitor;
+import com.android.tools.r8.utils.structural.Equatable;
+import com.android.tools.r8.utils.structural.HashCodeVisitor;
+import com.android.tools.r8.utils.structural.Ordered;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import org.objectweb.asm.Opcodes;
 
-public final class CfVersion implements Comparable<CfVersion> {
+public final class CfVersion implements Ordered<CfVersion> {
 
   public static final CfVersion V1_1 = new CfVersion(Opcodes.V1_1);
   public static final CfVersion V1_2 = new CfVersion(Opcodes.V1_2);
@@ -43,62 +47,23 @@
     return version;
   }
 
-  public static CfVersion maxAllowNull(CfVersion v1, CfVersion v2) {
-    assert v1 != null || v2 != null;
-    if (v1 == null) {
-      return v2;
-    }
-    if (v2 == null) {
-      return v1;
-    }
-    return v1.max(v2);
-  }
-
-  public CfVersion max(CfVersion other) {
-    return isLessThan(other) ? other : this;
-  }
-
-  public boolean isEqual(CfVersion other) {
-    return version == other.version;
-  }
-
-  public boolean isLessThan(CfVersion other) {
-    return compareTo(other) < 0;
-  }
-
-  public boolean isLessThanOrEqual(CfVersion other) {
-    return compareTo(other) <= 0;
-  }
-
-  public boolean isGreaterThan(CfVersion other) {
-    return compareTo(other) > 0;
-  }
-
-  public boolean isGreaterThanOrEqual(CfVersion other) {
-    return compareTo(other) >= 0;
-  }
-
-  @Override
-  public int compareTo(CfVersion o) {
-    return Comparator.comparingInt(CfVersion::major)
-        .thenComparingInt(CfVersion::minor)
-        .compare(this, o);
+  private static void accept(StructuralSpecification<CfVersion, ?> spec) {
+    spec.withInt(CfVersion::major).withInt(CfVersion::minor);
   }
 
   @Override
   public boolean equals(Object o) {
-    if (this == o) {
-      return true;
-    }
-    if (!(o instanceof CfVersion)) {
-      return false;
-    }
-    return isEqual((CfVersion) o);
+    return Equatable.equalsImpl(this, o);
   }
 
   @Override
   public int hashCode() {
-    return version;
+    return HashCodeVisitor.run(this, CfVersion::accept);
+  }
+
+  @Override
+  public int compareTo(CfVersion other) {
+    return DefaultCompareToVisitor.run(this, other, CfVersion::accept);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfCheckCast.java b/src/main/java/com/android/tools/r8/cf/code/CfCheckCast.java
index 7af534e..a391dbb 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfCheckCast.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfCheckCast.java
@@ -44,7 +44,7 @@
 
   @Override
   public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return type.slowCompareTo(((CfCheckCast) other).type);
+    return type.compareTo(((CfCheckCast) other).type);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstClass.java b/src/main/java/com/android/tools/r8/cf/code/CfConstClass.java
index 9d4ffa0..ced7984 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstClass.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstClass.java
@@ -40,7 +40,7 @@
 
   @Override
   public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return type.slowCompareTo(((CfConstClass) other).type);
+    return type.compareTo(((CfConstClass) other).type);
   }
 
   public DexType getType() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstMethodHandle.java b/src/main/java/com/android/tools/r8/cf/code/CfConstMethodHandle.java
index 37d0ff4..9c3b478 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstMethodHandle.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstMethodHandle.java
@@ -44,7 +44,7 @@
 
   @Override
   public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return handle.slowCompareTo(((CfConstMethodHandle) other).handle);
+    return handle.compareTo(((CfConstMethodHandle) other).handle);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstMethodType.java b/src/main/java/com/android/tools/r8/cf/code/CfConstMethodType.java
index b6a560f..c285c80 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstMethodType.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstMethodType.java
@@ -44,7 +44,7 @@
 
   @Override
   public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return type.slowCompareTo(((CfConstMethodType) other).type);
+    return type.compareTo(((CfConstMethodType) other).type);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfConstString.java b/src/main/java/com/android/tools/r8/cf/code/CfConstString.java
index ee042dc..7ac89da 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfConstString.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfConstString.java
@@ -36,7 +36,7 @@
 
   @Override
   public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return string.slowCompareTo(other.asConstString().string);
+    return string.compareTo(other.asConstString().string);
   }
 
   public DexString getString() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java b/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java
index 9b463fd..0915f0d 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfFieldInstruction.java
@@ -56,8 +56,8 @@
 
   @Override
   public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return Comparator.comparing(CfFieldInstruction::getField, DexField::slowCompareTo)
-        .thenComparing(field -> field.declaringField, DexField::slowCompareTo)
+    return Comparator.comparing(CfFieldInstruction::getField)
+        .thenComparing(field -> field.declaringField)
         .compare(this, (CfFieldInstruction) other);
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInitClass.java b/src/main/java/com/android/tools/r8/cf/code/CfInitClass.java
index 2c81fce..53437ec 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInitClass.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInitClass.java
@@ -49,7 +49,7 @@
 
   @Override
   public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return clazz.slowCompareTo(((CfInitClass) other).clazz);
+    return clazz.compareTo(((CfInitClass) other).clazz);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInstanceOf.java b/src/main/java/com/android/tools/r8/cf/code/CfInstanceOf.java
index 899ef31..1bbb32a 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInstanceOf.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInstanceOf.java
@@ -43,7 +43,7 @@
 
   @Override
   public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return type.slowCompareTo(other.asInstanceOf().type);
+    return type.compareTo(other.asInstanceOf().type);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java b/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
index 4f5159b..3e07e17 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfInvoke.java
@@ -62,7 +62,7 @@
   public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
     CfInvoke otherInvoke = other.asInvoke();
     int itfDiff = Boolean.compare(itf, otherInvoke.itf);
-    return itfDiff != 0 ? itfDiff : method.slowCompareTo(otherInvoke.method);
+    return itfDiff != 0 ? itfDiff : method.compareTo(otherInvoke.method);
   }
 
   public DexMethod getMethod() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java b/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java
index d52e429..a504f8c 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfMultiANewArray.java
@@ -52,7 +52,7 @@
   @Override
   public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
     return Comparator.comparingInt(CfMultiANewArray::getDimensions)
-        .thenComparing(CfMultiANewArray::getType, DexType::slowCompareTo)
+        .thenComparing(CfMultiANewArray::getType)
         .compare(this, ((CfMultiANewArray) other));
   }
 
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfNew.java b/src/main/java/com/android/tools/r8/cf/code/CfNew.java
index a4be181..41f468c 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfNew.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfNew.java
@@ -44,7 +44,7 @@
 
   @Override
   public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return type.slowCompareTo(((CfNew) other).type);
+    return type.compareTo(((CfNew) other).type);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfNewArray.java b/src/main/java/com/android/tools/r8/cf/code/CfNewArray.java
index 794227e..414bd58 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfNewArray.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfNewArray.java
@@ -47,7 +47,7 @@
 
   @Override
   public int internalCompareTo(CfInstruction other, CfCompareHelper helper) {
-    return type.slowCompareTo(((CfNewArray) other).type);
+    return type.compareTo(((CfNewArray) other).type);
   }
 
   private int getPrimitiveTypeCode() {
diff --git a/src/main/java/com/android/tools/r8/cf/code/CfTryCatch.java b/src/main/java/com/android/tools/r8/cf/code/CfTryCatch.java
index 01ff28f..a93f320 100644
--- a/src/main/java/com/android/tools/r8/cf/code/CfTryCatch.java
+++ b/src/main/java/com/android/tools/r8/cf/code/CfTryCatch.java
@@ -52,7 +52,7 @@
   public int compareTo(CfTryCatch other, CfCompareHelper helper) {
     return Comparator.comparing((CfTryCatch c) -> c.start, helper::compareLabels)
         .thenComparing(c -> c.end, helper::compareLabels)
-        .thenComparing(c -> c.guards, ComparatorUtils.listComparator(DexType::slowCompareTo))
+        .thenComparing(c -> c.guards, ComparatorUtils.listComparator())
         .thenComparing(c -> c.targets, ComparatorUtils.listComparator(helper::compareLabels))
         .compare(this, other);
   }
diff --git a/src/main/java/com/android/tools/r8/code/CheckCast.java b/src/main/java/com/android/tools/r8/code/CheckCast.java
index 06edf90..32df17b 100644
--- a/src/main/java/com/android/tools/r8/code/CheckCast.java
+++ b/src/main/java/com/android/tools/r8/code/CheckCast.java
@@ -45,7 +45,7 @@
 
   @Override
   int internalCompareBBBB(Format21c<?> other) {
-    return BBBB.slowCompareTo((DexType) other.BBBB);
+    return BBBB.compareTo((DexType) other.BBBB);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/ConstClass.java b/src/main/java/com/android/tools/r8/code/ConstClass.java
index 5562a69..ce1c26b 100644
--- a/src/main/java/com/android/tools/r8/code/ConstClass.java
+++ b/src/main/java/com/android/tools/r8/code/ConstClass.java
@@ -30,7 +30,7 @@
 
   @Override
   int internalCompareBBBB(Format21c<?> other) {
-    return BBBB.slowCompareTo((DexType) other.BBBB);
+    return BBBB.compareTo((DexType) other.BBBB);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/ConstMethodHandle.java b/src/main/java/com/android/tools/r8/code/ConstMethodHandle.java
index d69329a..0409f06 100644
--- a/src/main/java/com/android/tools/r8/code/ConstMethodHandle.java
+++ b/src/main/java/com/android/tools/r8/code/ConstMethodHandle.java
@@ -52,7 +52,7 @@
 
   @Override
   int internalCompareBBBB(Format21c<?> other) {
-    return BBBB.slowCompareTo((DexMethodHandle) other.BBBB);
+    return BBBB.compareTo((DexMethodHandle) other.BBBB);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/ConstMethodType.java b/src/main/java/com/android/tools/r8/code/ConstMethodType.java
index ba6e89f..8f54c24 100644
--- a/src/main/java/com/android/tools/r8/code/ConstMethodType.java
+++ b/src/main/java/com/android/tools/r8/code/ConstMethodType.java
@@ -51,7 +51,7 @@
 
   @Override
   int internalCompareBBBB(Format21c<?> other) {
-    return BBBB.slowCompareTo((DexProto) other.BBBB);
+    return BBBB.compareTo((DexProto) other.BBBB);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/ConstString.java b/src/main/java/com/android/tools/r8/code/ConstString.java
index 0b44f8a..e9e910d 100644
--- a/src/main/java/com/android/tools/r8/code/ConstString.java
+++ b/src/main/java/com/android/tools/r8/code/ConstString.java
@@ -35,7 +35,7 @@
 
   @Override
   int internalCompareBBBB(Format21c<?> other) {
-    return BBBB.slowCompareTo((DexString) other.BBBB);
+    return BBBB.compareTo((DexString) other.BBBB);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/DexInitClass.java b/src/main/java/com/android/tools/r8/code/DexInitClass.java
index 2d9cda9..ab39c88 100644
--- a/src/main/java/com/android/tools/r8/code/DexInitClass.java
+++ b/src/main/java/com/android/tools/r8/code/DexInitClass.java
@@ -129,7 +129,7 @@
   @Override
   final int internalCompareTo(Instruction other) {
     return Comparator.comparingInt((DexInitClass i) -> i.dest)
-        .thenComparing(i -> i.clazz, DexType::slowCompareTo)
+        .thenComparing(i -> i.clazz)
         .compare(this, (DexInitClass) other);
   }
 
diff --git a/src/main/java/com/android/tools/r8/code/FilledNewArray.java b/src/main/java/com/android/tools/r8/code/FilledNewArray.java
index b432eec..a1ee0ba 100644
--- a/src/main/java/com/android/tools/r8/code/FilledNewArray.java
+++ b/src/main/java/com/android/tools/r8/code/FilledNewArray.java
@@ -44,7 +44,7 @@
 
   @Override
   int internalCompareBBBB(Format35c<?> other) {
-    return BBBB.slowCompareTo((DexType) other.BBBB);
+    return BBBB.compareTo((DexType) other.BBBB);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/FilledNewArrayRange.java b/src/main/java/com/android/tools/r8/code/FilledNewArrayRange.java
index 13dc9a5..03594c2 100644
--- a/src/main/java/com/android/tools/r8/code/FilledNewArrayRange.java
+++ b/src/main/java/com/android/tools/r8/code/FilledNewArrayRange.java
@@ -44,7 +44,7 @@
 
   @Override
   int internalCompareBBBB(Format3rc<?> other) {
-    return BBBB.slowCompareTo((DexType) other.BBBB);
+    return BBBB.compareTo((DexType) other.BBBB);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/Format31c.java b/src/main/java/com/android/tools/r8/code/Format31c.java
index a1ae51e..672057b 100644
--- a/src/main/java/com/android/tools/r8/code/Format31c.java
+++ b/src/main/java/com/android/tools/r8/code/Format31c.java
@@ -54,7 +54,7 @@
   final int internalCompareTo(Instruction other) {
     Format31c o = (Format31c) other;
     int diff = Short.compare(AA, o.AA);
-    return diff != 0 ? diff : BBBBBBBB.slowCompareTo(o.BBBBBBBB);
+    return diff != 0 ? diff : BBBBBBBB.compareTo(o.BBBBBBBB);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/Format45cc.java b/src/main/java/com/android/tools/r8/code/Format45cc.java
index aa62780..bb0e1c7 100644
--- a/src/main/java/com/android/tools/r8/code/Format45cc.java
+++ b/src/main/java/com/android/tools/r8/code/Format45cc.java
@@ -90,8 +90,8 @@
     if (diff != 0) {
       return diff;
     }
-    int bDiff = BBBB.slowCompareTo(o.BBBB);
-    return bDiff != 0 ? bDiff : HHHH.slowCompareTo(o.HHHH);
+    int bDiff = BBBB.compareTo(o.BBBB);
+    return bDiff != 0 ? bDiff : HHHH.compareTo(o.HHHH);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/Format4rcc.java b/src/main/java/com/android/tools/r8/code/Format4rcc.java
index 690b230..f29357d 100644
--- a/src/main/java/com/android/tools/r8/code/Format4rcc.java
+++ b/src/main/java/com/android/tools/r8/code/Format4rcc.java
@@ -73,8 +73,8 @@
   final int internalCompareTo(Instruction other) {
     return Comparator.comparingInt((Format4rcc i) -> i.AA)
         .thenComparingInt(i -> i.CCCC)
-        .thenComparing(i -> i.BBBB, DexMethod::slowCompareTo)
-        .thenComparing(i -> i.HHHH, DexProto::slowCompareTo)
+        .thenComparing(i -> i.BBBB)
+        .thenComparing(i -> i.HHHH)
         .compare(this, (Format4rcc) other);
   }
 
diff --git a/src/main/java/com/android/tools/r8/code/InvokeMethod.java b/src/main/java/com/android/tools/r8/code/InvokeMethod.java
index de46296..ec8e6c4 100644
--- a/src/main/java/com/android/tools/r8/code/InvokeMethod.java
+++ b/src/main/java/com/android/tools/r8/code/InvokeMethod.java
@@ -43,7 +43,7 @@
 
   @Override
   int internalCompareBBBB(Format35c<?> other) {
-    return BBBB.slowCompareTo((DexMethod) other.BBBB);
+    return BBBB.compareTo((DexMethod) other.BBBB);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/InvokeMethodRange.java b/src/main/java/com/android/tools/r8/code/InvokeMethodRange.java
index 102d793..19c2bf1 100644
--- a/src/main/java/com/android/tools/r8/code/InvokeMethodRange.java
+++ b/src/main/java/com/android/tools/r8/code/InvokeMethodRange.java
@@ -43,7 +43,7 @@
 
   @Override
   int internalCompareBBBB(Format3rc<?> other) {
-    return BBBB.slowCompareTo((DexMethod) other.BBBB);
+    return BBBB.compareTo((DexMethod) other.BBBB);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/NewInstance.java b/src/main/java/com/android/tools/r8/code/NewInstance.java
index d3d25c6..0b90d1a 100644
--- a/src/main/java/com/android/tools/r8/code/NewInstance.java
+++ b/src/main/java/com/android/tools/r8/code/NewInstance.java
@@ -45,7 +45,7 @@
 
   @Override
   int internalCompareBBBB(Format21c<?> other) {
-    return BBBB.slowCompareTo((DexType) other.BBBB);
+    return BBBB.compareTo((DexType) other.BBBB);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/SgetOrSput.java b/src/main/java/com/android/tools/r8/code/SgetOrSput.java
index 48112fc..4524f8e 100644
--- a/src/main/java/com/android/tools/r8/code/SgetOrSput.java
+++ b/src/main/java/com/android/tools/r8/code/SgetOrSput.java
@@ -50,6 +50,6 @@
 
   @Override
   int internalCompareBBBB(Format21c<?> other) {
-    return BBBB.slowCompareTo((DexField) other.BBBB);
+    return BBBB.compareTo((DexField) other.BBBB);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
index 2f3c56e..b46cd9b 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationReader.java
@@ -184,7 +184,7 @@
     }
     if (dumpOutput != null) {
       timing.begin("ApplicationReader.dump");
-      dumpInputToFile(inputApp, dumpOutput, options);
+      inputApp.dump(dumpOutput, options.dumpOptions, options.reporter, options.dexItemFactory());
       if (cleanDump) {
         Files.delete(dumpOutput);
       }
@@ -230,10 +230,6 @@
     }
   }
 
-  private static void dumpInputToFile(AndroidApp app, Path output, InternalOptions options) {
-    app.dump(output, options);
-  }
-
   private static boolean verifyMainDexOptionsCompatible(
       AndroidApp inputApp, InternalOptions options) {
     if (!options.isGeneratingDex()) {
diff --git a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
index 77a7d70..2ad4409 100644
--- a/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/ApplicationWriter.java
@@ -301,8 +301,7 @@
                     }
                   }
                   ObjectToOffsetMapping objectMapping =
-                      virtualFile.computeMapping(
-                          appView.appInfo(), graphLens, namingLens, initClassLens);
+                      virtualFile.computeMapping(appView, graphLens, namingLens, initClassLens);
                   MethodToCodeObjectMapping codeMapping =
                       rewriteCodeWithJumboStrings(
                           objectMapping, virtualFile.classes(), appView.appInfo().app());
@@ -614,8 +613,8 @@
         return MethodToCodeObjectMapping.fromMethodBacking();
       }
       // If the globally highest sorting string is not a jumbo string this is also a no-op.
-      if (application.highestSortingString != null &&
-          application.highestSortingString.slowCompareTo(mapping.getFirstJumboString()) < 0) {
+      if (application.highestSortingString != null
+          && application.highestSortingString.compareTo(mapping.getFirstJumboString()) < 0) {
         return MethodToCodeObjectMapping.fromMethodBacking();
       }
     }
@@ -672,7 +671,7 @@
     StringBuilder builder = new StringBuilder();
     List<DexType> list = new ArrayList<>(mainDexClasses.size());
     mainDexClasses.forEach(list::add);
-    list.sort(DexType::slowCompareTo);
+    list.sort(DexType::compareTo);
     list.forEach(
         type -> builder.append(mapMainDexListName(type, namingLens)).append('\n'));
     return builder.toString();
diff --git a/src/main/java/com/android/tools/r8/dex/DexParser.java b/src/main/java/com/android/tools/r8/dex/DexParser.java
index 24d3a36..48fded1 100644
--- a/src/main/java/com/android/tools/r8/dex/DexParser.java
+++ b/src/main/java/com/android/tools/r8/dex/DexParser.java
@@ -609,7 +609,7 @@
       // compareTo instead of slowCompareTo. That would require us to assign indices during
       // reading. Those indices should be cleared after reading to make sure that we resort
       // everything correctly at the end.
-      while (index < annotations.length && annotations[index].item.slowCompareTo(item) < 0) {
+      while (index < annotations.length && annotations[index].item.compareTo(item) < 0) {
         index++;
       }
       if (index >= annotations.length || !annotations[index].item.equals(item)) {
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 74201ff..c07e0f5 100644
--- a/src/main/java/com/android/tools/r8/dex/FileWriter.java
+++ b/src/main/java/com/android/tools/r8/dex/FileWriter.java
@@ -133,7 +133,7 @@
       Log.verbose(FileWriter.class, "Writing encoded annotation @ %08x", dest.position());
     }
     List<DexAnnotationElement> elements = new ArrayList<>(Arrays.asList(annotation.elements));
-    elements.sort((a, b) -> a.name.slowCompareTo(b.name, mapping.getNamingLens()));
+    elements.sort((a, b) -> a.name.compareToWithNamingLens(b.name, mapping.getNamingLens()));
     dest.putUleb128(mapping.getOffsetFor(annotation.type));
     dest.putUleb128(elements.size());
     for (DexAnnotationElement element : elements) {
@@ -579,7 +579,8 @@
       Log.verbose(getClass(), "Writing AnnotationSet @ 0x%08x.", dest.position());
     }
     List<DexAnnotation> annotations = new ArrayList<>(Arrays.asList(set.annotations));
-    annotations.sort((a, b) -> a.annotation.type.slowCompareTo(b.annotation.type, namingLens));
+    annotations.sort(
+        (a, b) -> a.annotation.type.compareToWithNamingLens(b.annotation.type, namingLens));
     dest.putInt(annotations.size());
     for (DexAnnotation annotation : annotations) {
       dest.putInt(mixedSectionOffsets.getOffsetFor(annotation));
@@ -629,7 +630,7 @@
 
   private void writeEncodedFields(List<DexEncodedField> unsortedFields) {
     List<DexEncodedField> fields = new ArrayList<>(unsortedFields);
-    fields.sort((a, b) -> a.field.slowCompareTo(b.field, namingLens));
+    fields.sort((a, b) -> a.field.compareToWithNamingLens(b.field, namingLens));
     int currentOffset = 0;
     for (DexEncodedField field : fields) {
       assert field.validateDexValue(application.dexItemFactory);
@@ -645,7 +646,7 @@
   private void writeEncodedMethods(
       Iterable<DexEncodedMethod> unsortedMethods, boolean isSharedSynthetic) {
     List<DexEncodedMethod> methods = IterableUtils.toNewArrayList(unsortedMethods);
-    methods.sort((a, b) -> a.method.slowCompareTo(b.method, namingLens));
+    methods.sort((a, b) -> a.method.compareToWithNamingLens(b.method, namingLens));
     int currentOffset = 0;
     for (DexEncodedMethod method : methods) {
       int nextOffset = mapping.getOffsetFor(method.method);
diff --git a/src/main/java/com/android/tools/r8/dex/InheritanceClassInDexDistributor.java b/src/main/java/com/android/tools/r8/dex/InheritanceClassInDexDistributor.java
index cad079b..8e8750a 100644
--- a/src/main/java/com/android/tools/r8/dex/InheritanceClassInDexDistributor.java
+++ b/src/main/java/com/android/tools/r8/dex/InheritanceClassInDexDistributor.java
@@ -45,7 +45,7 @@
 public class InheritanceClassInDexDistributor {
 
   private static final Comparator<DexProgramClass> DEX_PROGRAM_CLASS_COMPARATOR =
-      (a, b) -> a.type.descriptor.slowCompareTo(b.type.descriptor);
+      (a, b) -> a.type.descriptor.compareTo(b.type.descriptor);
 
   private static final int DEX_FULL_ENOUGH_THRESHOLD = VirtualFile.MAX_ENTRIES - 100;
   private final ExecutorService executorService;
diff --git a/src/main/java/com/android/tools/r8/dex/JumboStringRewriter.java b/src/main/java/com/android/tools/r8/dex/JumboStringRewriter.java
index d1f8749..e402ce5 100644
--- a/src/main/java/com/android/tools/r8/dex/JumboStringRewriter.java
+++ b/src/main/java/com/android/tools/r8/dex/JumboStringRewriter.java
@@ -281,7 +281,7 @@
         instruction.setOffset(orignalOffset + offsetDelta);
         if (instruction instanceof ConstString) {
           ConstString string = (ConstString) instruction;
-          if (string.getString().slowCompareTo(firstJumboString) >= 0) {
+          if (string.getString().compareTo(firstJumboString) >= 0) {
             ConstStringJumbo jumboString = new ConstStringJumbo(string.AA, string.getString());
             jumboString.setOffset(string.getOffset());
             offsetDelta++;
diff --git a/src/main/java/com/android/tools/r8/dex/VirtualFile.java b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
index b808097..108df1d 100644
--- a/src/main/java/com/android/tools/r8/dex/VirtualFile.java
+++ b/src/main/java/com/android/tools/r8/dex/VirtualFile.java
@@ -9,7 +9,6 @@
 import com.android.tools.r8.errors.DexFileOverflowDiagnostic;
 import com.android.tools.r8.errors.InternalCompilerError;
 import com.android.tools.r8.features.ClassToFeatureSplitMap;
-import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexField;
@@ -210,10 +209,10 @@
   }
 
   public ObjectToOffsetMapping computeMapping(
-      AppInfo appInfo, GraphLens graphLens, NamingLens namingLens, InitClassLens initClassLens) {
+      AppView<?> appView, GraphLens graphLens, NamingLens namingLens, InitClassLens initClassLens) {
     assert transaction.isEmpty();
     return new ObjectToOffsetMapping(
-        appInfo,
+        appView,
         graphLens,
         namingLens,
         initClassLens,
@@ -742,7 +741,7 @@
       this.graphLens = graphLens;
       this.initClassLens = initClassLens;
       this.namingLens = namingLens;
-      this.rewriter = new LensCodeRewriterUtils(appView, graphLens);
+      this.rewriter = new LensCodeRewriterUtils(appView);
     }
 
     private <T extends DexItem> boolean maybeInsert(T item, Set<T> set, Set<T> baseSet) {
diff --git a/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java b/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java
index 519bd27..b6588df 100644
--- a/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java
+++ b/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java
@@ -17,6 +17,7 @@
 import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Reporter;
 import com.google.common.collect.Sets;
 import java.util.IdentityHashMap;
 import java.util.Map;
@@ -39,8 +40,14 @@
 
   public static ClassToFeatureSplitMap createInitialClassToFeatureSplitMap(
       InternalOptions options) {
-    DexItemFactory dexItemFactory = options.dexItemFactory();
-    FeatureSplitConfiguration featureSplitConfiguration = options.featureSplitConfiguration;
+    return createInitialClassToFeatureSplitMap(
+        options.dexItemFactory(), options.featureSplitConfiguration, options.reporter);
+  }
+
+  public static ClassToFeatureSplitMap createInitialClassToFeatureSplitMap(
+      DexItemFactory dexItemFactory,
+      FeatureSplitConfiguration featureSplitConfiguration,
+      Reporter reporter) {
 
     ClassToFeatureSplitMap result = new ClassToFeatureSplitMap();
     if (featureSplitConfiguration == null) {
@@ -58,7 +65,7 @@
             }
           }
         } catch (ResourceException e) {
-          throw options.reporter.fatalError(e.getMessage());
+          throw reporter.fatalError(e.getMessage());
         }
       }
     }
diff --git a/src/main/java/com/android/tools/r8/graph/AbstractAccessContexts.java b/src/main/java/com/android/tools/r8/graph/AbstractAccessContexts.java
index 4615f1f..8767555 100644
--- a/src/main/java/com/android/tools/r8/graph/AbstractAccessContexts.java
+++ b/src/main/java/com/android/tools/r8/graph/AbstractAccessContexts.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import java.util.IdentityHashMap;
 import java.util.Map;
+import java.util.function.BiConsumer;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
@@ -86,6 +87,8 @@
     return UnknownAccessContexts.getInstance();
   }
 
+  public abstract AbstractAccessContexts join(AbstractAccessContexts contexts);
+
   public static class EmptyAccessContexts extends AbstractAccessContexts {
 
     public static EmptyAccessContexts INSTANCE = new EmptyAccessContexts();
@@ -140,6 +143,11 @@
     AbstractAccessContexts rewrittenWithLens(DexDefinitionSupplier definitions, GraphLens lens) {
       return this;
     }
+
+    @Override
+    public AbstractAccessContexts join(AbstractAccessContexts contexts) {
+      return contexts;
+    }
   }
 
   public static class ConcreteAccessContexts extends AbstractAccessContexts {
@@ -305,6 +313,28 @@
           });
       return new ConcreteAccessContexts(newAccessesWithContexts);
     }
+
+    @Override
+    public AbstractAccessContexts join(AbstractAccessContexts contexts) {
+      if (contexts.isEmpty()) {
+        return this;
+      }
+      if (contexts.isTop()) {
+        return contexts;
+      }
+      Map<DexField, ProgramMethodSet> newAccessesWithContexts = new IdentityHashMap<>();
+      accessesWithContexts.forEach(
+          (field, methodSet) ->
+              newAccessesWithContexts.put(field, ProgramMethodSet.create(methodSet)));
+
+      BiConsumer<DexField, ProgramMethodSet> addAllMethods =
+          (field, methodSet) ->
+              newAccessesWithContexts
+                  .computeIfAbsent(field, ignore -> ProgramMethodSet.create())
+                  .addAll(methodSet);
+      contexts.asConcrete().accessesWithContexts.forEach(addAllMethods);
+      return new ConcreteAccessContexts(newAccessesWithContexts);
+    }
   }
 
   public static class UnknownAccessContexts extends AbstractAccessContexts {
@@ -362,5 +392,10 @@
     AbstractAccessContexts rewrittenWithLens(DexDefinitionSupplier definitions, GraphLens lens) {
       return this;
     }
+
+    @Override
+    public AbstractAccessContexts join(AbstractAccessContexts contexts) {
+      return this;
+    }
   }
 }
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 ef9b742..70db016 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -11,6 +11,7 @@
 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.StaticallyMergedClasses;
 import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
 import com.android.tools.r8.ir.analysis.proto.EnumLiteProtoShrinker;
@@ -85,6 +86,7 @@
   private InitializedClassesInInstanceMethods initializedClassesInInstanceMethods;
   private HorizontallyMergedLambdaClasses horizontallyMergedLambdaClasses;
   private HorizontallyMergedClasses horizontallyMergedClasses;
+  private StaticallyMergedClasses staticallyMergedClasses;
   private VerticallyMergedClasses verticallyMergedClasses;
   private EnumValueInfoMapCollection unboxedEnums = EnumValueInfoMapCollection.empty();
   // TODO(b/169115389): Remove
@@ -437,6 +439,9 @@
 
   public MergedClassesCollection allMergedClasses() {
     MergedClassesCollection collection = new MergedClassesCollection();
+    if (horizontallyMergedClasses != null) {
+      collection.add(horizontallyMergedClasses);
+    }
     if (horizontallyMergedLambdaClasses != null) {
       collection.add(horizontallyMergedLambdaClasses);
     }
@@ -470,8 +475,8 @@
   }
 
   /**
-   * Get the result of horizontal class merging. Returns null if horizontal lambda class merging has
-   * not been run.
+   * Get the result of horizontal class merging. Returns null if horizontal class merging has not
+   * been run.
    */
   public HorizontallyMergedClasses horizontallyMergedClasses() {
     return horizontallyMergedClasses;
@@ -484,6 +489,19 @@
   }
 
   /**
+   * Get the result of static class merging. Returns null if static class merging has not been run.
+   */
+  public StaticallyMergedClasses staticallyMergedClasses() {
+    return staticallyMergedClasses;
+  }
+
+  public void setStaticallyMergedClasses(StaticallyMergedClasses staticallyMergedClasses) {
+    assert this.staticallyMergedClasses == null;
+    this.staticallyMergedClasses = staticallyMergedClasses;
+    testing().staticallyMergedClassesConsumer.accept(dexItemFactory(), staticallyMergedClasses);
+  }
+
+  /**
    * Get the result of vertical class merging. Returns null if vertical class merging has not been
    * run.
    */
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 bae65e8..887f10d 100644
--- a/src/main/java/com/android/tools/r8/graph/AppliedGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/AppliedGraphLens.java
@@ -102,7 +102,7 @@
 
   @Override
   public Iterable<DexType> getOriginalTypes(DexType type) {
-    Set<DexType> originalTypes = renamedTypeNames.getKeys(type);
+    Set<DexType> originalTypes = renamedTypeNames.getKeysOrNull(type);
     if (originalTypes == null) {
       return ImmutableList.of(type);
     }
diff --git a/src/main/java/com/android/tools/r8/graph/CfCode.java b/src/main/java/com/android/tools/r8/graph/CfCode.java
index d6ae59f..c17ff09 100644
--- a/src/main/java/com/android/tools/r8/graph/CfCode.java
+++ b/src/main/java/com/android/tools/r8/graph/CfCode.java
@@ -312,7 +312,7 @@
     for (CfInstruction instruction : instructions) {
       if (instruction instanceof CfFrame
           && (classFileVersion.isLessThan(CfVersion.V1_6)
-              || (classFileVersion.isEqual(CfVersion.V1_6)
+              || (classFileVersion.isEqualTo(CfVersion.V1_6)
                   && !options.shouldKeepStackMapTable()))) {
         continue;
       }
diff --git a/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java b/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java
index b43a42c..7998171 100644
--- a/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java
+++ b/src/main/java/com/android/tools/r8/graph/ClassAccessFlags.java
@@ -109,7 +109,7 @@
   public boolean areValid(CfVersion version, boolean isPackageInfo) {
     if (isInterface()) {
       // We ignore the super flags prior to JDK 9, as so did the VM.
-      if (version.isGreaterThanOrEqual(CfVersion.V9) && isSuper()) {
+      if (version.isGreaterThanOrEqualTo(CfVersion.V9) && isSuper()) {
         return false;
       }
       // When not coming from DEX input we require interfaces to be abstract - except for
diff --git a/src/main/java/com/android/tools/r8/graph/DebugLocalInfo.java b/src/main/java/com/android/tools/r8/graph/DebugLocalInfo.java
index 3bb9931..333b8a7 100644
--- a/src/main/java/com/android/tools/r8/graph/DebugLocalInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/DebugLocalInfo.java
@@ -33,9 +33,9 @@
 
   @Override
   public int compareTo(DebugLocalInfo other) {
-    return Comparator.comparing((DebugLocalInfo info) -> info.name, DexString::slowCompareTo)
-        .thenComparing(info -> info.type, DexType::slowCompareTo)
-        .thenComparing(info -> info.signature, Comparator.nullsFirst(DexString::slowCompareTo))
+    return Comparator.comparing((DebugLocalInfo info) -> info.name)
+        .thenComparing(info -> info.type)
+        .thenComparing(info -> info.signature, Comparator.nullsFirst(DexString::compareTo))
         .compare(this, other);
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexAnnotationDirectory.java b/src/main/java/com/android/tools/r8/graph/DexAnnotationDirectory.java
index 04f59d7..2ee613c 100644
--- a/src/main/java/com/android/tools/r8/graph/DexAnnotationDirectory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexAnnotationDirectory.java
@@ -46,17 +46,17 @@
   }
 
   public List<DexEncodedMethod> sortMethodAnnotations(NamingLens namingLens) {
-    methodAnnotations.sort((a, b) -> a.method.slowCompareTo(b.method, namingLens));
+    methodAnnotations.sort((a, b) -> a.method.compareToWithNamingLens(b.method, namingLens));
     return methodAnnotations;
   }
 
   public List<DexEncodedMethod> sortParameterAnnotations(NamingLens namingLens) {
-    parameterAnnotations.sort((a, b) -> a.method.slowCompareTo(b.method, namingLens));
+    parameterAnnotations.sort((a, b) -> a.method.compareToWithNamingLens(b.method, namingLens));
     return parameterAnnotations;
   }
 
   public List<DexEncodedField> sortFieldAnnotations(NamingLens namingLens) {
-    fieldAnnotations.sort((a, b) -> a.field.slowCompareTo(b.field, namingLens));
+    fieldAnnotations.sort((a, b) -> a.field.compareToWithNamingLens(b.field, namingLens));
     return fieldAnnotations;
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/DexAnnotationSet.java b/src/main/java/com/android/tools/r8/graph/DexAnnotationSet.java
index e13faf8..ba71338 100644
--- a/src/main/java/com/android/tools/r8/graph/DexAnnotationSet.java
+++ b/src/main/java/com/android/tools/r8/graph/DexAnnotationSet.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import static com.android.tools.r8.utils.PredicateUtils.not;
+
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.naming.NamingLens;
@@ -99,7 +101,8 @@
       return;
     }
     Arrays.sort(
-        annotations, (a, b) -> a.annotation.type.slowCompareTo(b.annotation.type, namingLens));
+        annotations,
+        (a, b) -> a.annotation.type.compareToWithNamingLens(b.annotation.type, namingLens));
     for (DexAnnotation annotation : annotations) {
       annotation.annotation.sort();
     }
@@ -157,7 +160,11 @@
   }
 
   public DexAnnotationSet keepIf(Predicate<DexAnnotation> filter) {
-    return rewrite(anno -> filter.test(anno) ? anno : null);
+    return removeIf(not(filter));
+  }
+
+  public DexAnnotationSet removeIf(Predicate<DexAnnotation> filter) {
+    return rewrite(annotation -> filter.test(annotation) ? null : annotation);
   }
 
   public DexAnnotationSet rewrite(Function<DexAnnotation, DexAnnotation> rewriter) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexApplication.java b/src/main/java/com/android/tools/r8/graph/DexApplication.java
index abb91ed..6e38ed4 100644
--- a/src/main/java/com/android/tools/r8/graph/DexApplication.java
+++ b/src/main/java/com/android/tools/r8/graph/DexApplication.java
@@ -109,7 +109,7 @@
     // that.
     if (options.testing.deterministicSortingBasedOnDexType) {
       // To keep the order deterministic, we sort the classes by their type, which is a unique key.
-      classes.sort((a, b) -> a.type.slowCompareTo(b.type));
+      classes.sort((a, b) -> a.type.compareTo(b.type));
     }
     return classes;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexCallSite.java b/src/main/java/com/android/tools/r8/graph/DexCallSite.java
index bcb8873..b38e016 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCallSite.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCallSite.java
@@ -158,7 +158,7 @@
   @Override
   public int compareTo(DexCallSite other) {
     assert method != null && other.method != null;
-    int methodCompare = method.slowCompareTo(other.method);
+    int methodCompare = method.compareTo(other.method);
     if (methodCompare != 0) {
       return methodCompare;
     }
diff --git a/src/main/java/com/android/tools/r8/graph/DexClass.java b/src/main/java/com/android/tools/r8/graph/DexClass.java
index 45130dc..5215ada 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClass.java
@@ -3,13 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
+import static com.google.common.base.Predicates.alwaysFalse;
+
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.errors.CompilationError;
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.GenericSignature.ClassSignature;
 import com.android.tools.r8.kotlin.KotlinClassLevelInfo;
 import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.OptionalBool;
@@ -673,57 +674,28 @@
   }
 
   public boolean classInitializationMayHaveSideEffects(AppView<?> appView) {
-    return classInitializationMayHaveSideEffects(
-        appView, Predicates.alwaysFalse(), Sets.newIdentityHashSet());
-  }
-
-  public boolean classInitializationMayHaveSideEffectsInContext(
-      AppView<AppInfoWithLiveness> appView, ProgramDefinition context) {
-    return classInitializationMayHaveSideEffects(
-        appView,
-        // Types that are a super type of the current context are guaranteed to be initialized
-        // already.
-        type -> appView.appInfo().isSubtype(context.getContextType(), type),
-        Sets.newIdentityHashSet());
+    return classInitializationMayHaveSideEffects(appView, alwaysFalse());
   }
 
   public boolean classInitializationMayHaveSideEffects(
-      AppView<?> appView, Predicate<DexType> ignore, Set<DexType> seen) {
-    if (!seen.add(type)) {
-      return false;
-    }
-    if (ignore.test(type)) {
-      return false;
-    }
-    if (isLibraryClass()) {
-      if (isInterface()) {
-        return appView.options().libraryInterfacesMayHaveStaticInitialization;
-      }
-      if (appView.dexItemFactory().libraryClassesWithoutStaticInitialization.contains(type)) {
-        return false;
-      }
-    }
-    if (hasClassInitializerThatCannotBePostponed()) {
-      return true;
-    }
-    if (defaultValuesForStaticFieldsMayTriggerAllocation()) {
-      return true;
-    }
-    return initializationOfParentTypesMayHaveSideEffects(appView, ignore, seen);
+      AppView<?> appView, Predicate<DexType> ignore) {
+    return internalClassOrInterfaceMayHaveInitializationSideEffects(
+        appView, this, ignore, Sets.newIdentityHashSet());
   }
 
-  private boolean hasClassInitializerThatCannotBePostponed() {
-    if (isLibraryClass()) {
-      // We don't know for library classes in general but assume that java.lang.Object is safe.
-      return superType != null;
-    }
-    DexEncodedMethod clinit = getClassInitializer();
-    if (clinit == null || clinit.getCode() == null) {
-      return false;
-    }
-    return !clinit.getOptimizationInfo().classInitializerMayBePostponed();
+  public final boolean classInitializationMayHaveSideEffectsInContext(
+      AppView<?> appView, ProgramDefinition context) {
+    // Types that are a super type of the current context are guaranteed to be initialized already.
+    return classInitializationMayHaveSideEffects(
+        appView, type -> appView.isSubtype(context.getContextType(), type).isTrue());
   }
 
+  abstract boolean internalClassOrInterfaceMayHaveInitializationSideEffects(
+      AppView<?> appView,
+      DexClass initialAccessHolder,
+      Predicate<DexType> ignore,
+      Set<DexType> seen);
+
   public void forEachImmediateSupertype(Consumer<DexType> fn) {
     if (superType != null) {
       fn.accept(superType);
@@ -742,32 +714,14 @@
     return () -> iterator;
   }
 
-  public boolean initializationOfParentTypesMayHaveSideEffects(AppView<?> appView) {
-    return initializationOfParentTypesMayHaveSideEffects(
-        appView, Predicates.alwaysFalse(), Sets.newIdentityHashSet());
-  }
-
-  public boolean initializationOfParentTypesMayHaveSideEffects(
-      AppView<?> appView, Predicate<DexType> ignore, Set<DexType> seen) {
-    for (DexType iface : interfaces.values) {
-      if (iface.classInitializationMayHaveSideEffects(appView, ignore, seen)) {
-        return true;
-      }
-    }
-    if (superType != null
-        && superType.classInitializationMayHaveSideEffects(appView, ignore, seen)) {
-      return true;
-    }
-    return false;
-  }
-
   public boolean definesFinalizer(DexItemFactory factory) {
     return lookupVirtualMethod(factory.objectMembers.finalize) != null;
   }
 
   public boolean defaultValuesForStaticFieldsMayTriggerAllocation() {
     return staticFields().stream()
-        .anyMatch(field -> field.getStaticValue().mayHaveSideEffects());
+        .anyMatch(
+            field -> field.hasExplicitStaticValue() && field.getStaticValue().mayHaveSideEffects());
   }
 
   public List<InnerClassAttribute> getInnerClasses() {
@@ -904,6 +858,10 @@
   /** Returns kotlin class info if the class is synthesized by kotlin compiler. */
   public abstract KotlinClassLevelInfo getKotlinInfo();
 
+  public boolean hasStaticFields() {
+    return staticFields.length > 0;
+  }
+
   public boolean hasInstanceFields() {
     return instanceFields.length > 0;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexClasspathClass.java b/src/main/java/com/android/tools/r8/graph/DexClasspathClass.java
index 410f985..c97a1a4 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClasspathClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClasspathClass.java
@@ -11,6 +11,8 @@
 import com.android.tools.r8.kotlin.KotlinClassLevelInfo;
 import com.android.tools.r8.origin.Origin;
 import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
 import java.util.function.Supplier;
 
 public class DexClasspathClass extends DexClass implements Supplier<DexClasspathClass> {
@@ -90,4 +92,16 @@
   public DexClasspathClass get() {
     return this;
   }
+
+  @Override
+  boolean internalClassOrInterfaceMayHaveInitializationSideEffects(
+      AppView<?> appView,
+      DexClass initialAccessHolder,
+      Predicate<DexType> ignore,
+      Set<DexType> seen) {
+    if (!seen.add(getType()) || ignore.test(getType())) {
+      return false;
+    }
+    return !isInterface() || appView.options().classpathInterfacesMayHaveStaticInitialization;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexCode.java b/src/main/java/com/android/tools/r8/graph/DexCode.java
index 7a9c724..ab974b3 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -430,7 +430,7 @@
 
   private void updateHighestSortingString(DexString candidate) {
     assert candidate != null;
-    if (highestSortingString == null || highestSortingString.slowCompareTo(candidate) < 0) {
+    if (highestSortingString == null || highestSortingString.compareTo(candidate) < 0) {
       highestSortingString = candidate;
     }
   }
@@ -621,7 +621,7 @@
           return 0;
         }
         return Comparator.comparingInt((TypeAddrPair p) -> p.addr)
-            .thenComparing(p -> p.type, DexType::slowCompareTo)
+            .thenComparing(p -> p.type)
             .compare(this, other);
       }
     }
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java b/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java
index 903686d..16842b0 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java
@@ -300,9 +300,9 @@
     @Override
     int internalCompareTo(DexDebugEvent other) {
       return Comparator.comparingInt((StartLocal e) -> e.registerNum)
-          .thenComparing(e -> e.name, DexString::slowCompareTo)
-          .thenComparing(e -> e.type, DexType::slowCompareTo)
-          .thenComparing(e -> e.signature, Comparator.nullsFirst(DexString::slowCompareTo))
+          .thenComparing(e -> e.name)
+          .thenComparing(e -> e.type)
+          .thenComparing(e -> e.signature, Comparator.nullsFirst(DexString::compareTo))
           .compare(this, (StartLocal) other);
     }
   }
@@ -431,7 +431,7 @@
 
     @Override
     int internalCompareTo(DexDebugEvent other) {
-      return fileName.slowCompareTo(((SetFile) other).fileName);
+      return fileName.compareTo(((SetFile) other).fileName);
     }
   }
 
@@ -473,7 +473,7 @@
 
     @Override
     int internalCompareTo(DexDebugEvent other) {
-      return Comparator.comparing((SetInlineFrame e) -> e.callee, DexMethod::slowCompareTo)
+      return Comparator.comparing((SetInlineFrame e) -> e.callee, DexMethod::compareTo)
           .thenComparing(e -> e.caller, Comparator.nullsFirst(Position::compareTo))
           .compare(this, (SetInlineFrame) other);
     }
diff --git a/src/main/java/com/android/tools/r8/graph/DexDebugInfo.java b/src/main/java/com/android/tools/r8/graph/DexDebugInfo.java
index 5e44a57..5bfbd8d 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugInfo.java
@@ -54,7 +54,7 @@
     return Comparator.comparingInt((DexDebugInfo i) -> i.startLine)
         .thenComparing(
             i -> i.parameters,
-            ComparatorUtils.arrayComparator(Comparator.nullsFirst(DexString::slowCompareTo)))
+            ComparatorUtils.arrayComparator(Comparator.nullsFirst(DexString::compareTo)))
         .thenComparing(i -> i.events, ComparatorUtils.arrayComparator())
         .compare(this, other);
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexDefinition.java b/src/main/java/com/android/tools/r8/graph/DexDefinition.java
index 7259345..0153cd2 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDefinition.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDefinition.java
@@ -40,6 +40,10 @@
     this.annotations = annotations;
   }
 
+  public void removeAnnotations(Predicate<DexAnnotation> predicate) {
+    setAnnotations(annotations().removeIf(predicate));
+  }
+
   public boolean isDexClass() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedAnnotation.java b/src/main/java/com/android/tools/r8/graph/DexEncodedAnnotation.java
index cb4eb4e..051dd92 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedAnnotation.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedAnnotation.java
@@ -78,7 +78,7 @@
       assert sorted == sortedHashCode();
       return;
     }
-    Arrays.sort(elements, (a, b) -> a.name.slowCompareTo(b.name));
+    Arrays.sort(elements, (a, b) -> a.name.compareTo(b.name));
     for (DexAnnotationElement element : elements) {
       element.value.sort();
     }
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
index 159c27b..16879f0 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedField.java
@@ -20,7 +20,6 @@
 import com.android.tools.r8.ir.optimize.info.MutableFieldOptimizationInfo;
 import com.android.tools.r8.kotlin.KotlinFieldLevelInfo;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.google.common.collect.Sets;
 
 public class DexEncodedField extends DexEncodedMember<DexEncodedField, DexField> {
   public static final DexEncodedField[] EMPTY_ARRAY = {};
@@ -267,32 +266,6 @@
     return null;
   }
 
-  public boolean mayTriggerClassInitializationSideEffects(
-      AppView<AppInfoWithLiveness> appView, ProgramMethod context) {
-    // Only static field matters when it comes to class initialization side effects.
-    if (!isStatic()) {
-      return false;
-    }
-    DexClass clazz = appView.definitionFor(field.holder);
-    if (clazz == null) {
-      return true;
-    }
-    if (clazz.classInitializationMayHaveSideEffects(
-        appView,
-        // Types that are a super type of the current context are guaranteed to be initialized
-        // already.
-        type -> appView.appInfo().isSubtype(context.getHolderType(), type),
-        Sets.newIdentityHashSet())) {
-      // Ignore class initialization side-effects for dead proto extension fields to ensure that
-      // we force replace these field reads by null.
-      boolean ignore =
-          appView.withGeneratedExtensionRegistryShrinker(
-              shrinker -> shrinker.isDeadProtoExtensionField(field), false);
-      return !ignore;
-    }
-    return false;
-  }
-
   public DexEncodedField toTypeSubstitutedField(DexField field) {
     if (this.field == field) {
       return this;
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index c353e8a..e84e3e8 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -75,6 +75,11 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.Pair;
+import com.android.tools.r8.utils.structural.CompareToVisitorWithTypeEquivalence;
+import com.android.tools.r8.utils.structural.HashingVisitorWithTypeEquivalence;
+import com.android.tools.r8.utils.structural.Ordered;
+import com.android.tools.r8.utils.structural.RepresentativeMap;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import com.google.common.collect.ImmutableList;
 import com.google.common.hash.Hasher;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceArrayMap;
@@ -82,7 +87,6 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Consumer;
@@ -318,14 +322,50 @@
     return deprecated;
   }
 
-  public void hashSyntheticContent(Hasher hasher) {
-    // Method holder does not contribute to the synthetic hash (it is freely chosen).
-    // Method name does not contribute to the synthetic hash (it is freely chosen).
-    method.proto.hashSyntheticContent(hasher);
-    hasher.putInt(accessFlags.getAsCfAccessFlags());
-    assert annotations().isEmpty();
-    assert parameterAnnotationsList.isEmpty();
-    assert code != null;
+  // Visitor specifying the structure of the method with respect to its "synthetic" content.
+  // TODO(b/171867022): Generalize this so that it determines any method in full.
+  private static void syntheticSpecify(StructuralSpecification<DexEncodedMethod, ?> spec) {
+    spec.withAssert(m1 -> m1.annotations().isEmpty())
+        .withAssert(m1 -> m1.parameterAnnotationsList.isEmpty())
+        .withAssert(m1 -> m1.code != null)
+        .withItem(DexEncodedMethod::getHolderType)
+        .withItem(DexEncodedMethod::getName)
+        .withInt(m -> m.getAccessFlags().getAsCfAccessFlags())
+        .withItem(DexEncodedMethod::proto)
+        .withCustomItem(
+            DexEncodedMethod::getCode,
+            (c1, c2, v) -> v.visit(c1, c2, DexEncodedMethod::compareCodeObject),
+            (c, h) -> h.visit(c, DexEncodedMethod::hashCodeObject));
+  }
+
+  public void hashSyntheticContent(Hasher hasher, RepresentativeMap map) {
+    HashingVisitorWithTypeEquivalence.run(this, hasher, map, DexEncodedMethod::syntheticSpecify);
+  }
+
+  public boolean isSyntheticContentEqual(DexEncodedMethod other) {
+    return syntheticCompareTo(other) == 0;
+  }
+
+  public int syntheticCompareTo(DexEncodedMethod other) {
+    // Consider the holder types to be equivalent, using the holder of this method as the
+    // representative.
+    RepresentativeMap map = t -> t == other.getHolderType() ? getHolderType() : t;
+    return CompareToVisitorWithTypeEquivalence.run(
+        this, other, map, DexEncodedMethod::syntheticSpecify);
+  }
+
+  private static int compareCodeObject(Code code1, Code code2) {
+    if (code1.isCfCode() && code2.isCfCode()) {
+      return code1.asCfCode().compareTo(code2.asCfCode());
+    }
+    if (code1.isDexCode() && code2.isDexCode()) {
+      return code1.asDexCode().compareTo(code2.asDexCode());
+    }
+    throw new Unreachable(
+        "Unexpected attempt to compare incompatible synthetic objects: " + code1 + " and " + code2);
+  }
+
+  private static void hashCodeObject(Code code, Hasher hasher) {
     // TODO(b/158159959): Implement a more precise hashing on code objects.
     if (code.isCfCode()) {
       CfCode cfCode = code.asCfCode();
@@ -339,30 +379,6 @@
     }
   }
 
-  public boolean isSyntheticContentEqual(DexEncodedMethod other) {
-    return syntheticCompareTo(other) == 0;
-  }
-
-  public int syntheticCompareTo(DexEncodedMethod other) {
-    assert annotations().isEmpty();
-    assert parameterAnnotationsList.isEmpty();
-    Comparator<DexEncodedMethod> comparator =
-        Comparator.comparing(DexEncodedMethod::proto, DexProto::slowCompareTo)
-            .thenComparingInt(m -> m.accessFlags.getAsCfAccessFlags());
-    if (code.isCfCode() && other.getCode().isCfCode()) {
-      comparator = comparator.thenComparing(m -> m.getCode().asCfCode());
-    } else if (code.isDexCode() && other.getCode().isDexCode()) {
-      comparator = comparator.thenComparing(m -> m.getCode().asDexCode());
-    } else {
-      throw new Unreachable(
-          "Unexpected attempt to compare incompatible synthetic objects: "
-              + code
-              + " and "
-              + other.getCode());
-    }
-    return comparator.compare(this, other);
-  }
-
   public DexType getHolderType() {
     return getReference().holder;
   }
@@ -832,7 +848,7 @@
   public void upgradeClassFileVersion(CfVersion version) {
     checkIfObsolete();
     assert version != null;
-    classFileVersion = CfVersion.maxAllowNull(classFileVersion, version);
+    classFileVersion = Ordered.maxIgnoreNull(classFileVersion, version);
   }
 
   public String qualifiedName() {
@@ -1216,7 +1232,7 @@
 
   public DexEncodedMethod toRenamedHolderMethod(DexType newHolderType, DexItemFactory factory) {
     DexEncodedMethod.Builder builder = DexEncodedMethod.builder(this);
-    builder.setMethod(factory.createMethod(newHolderType, method.proto, method.name));
+    builder.setMethod(method.withHolder(newHolderType, factory));
     return builder.build();
   }
 
@@ -1282,8 +1298,7 @@
     // and if different forwarding methods are created in different subclasses the first could be
     // final.
     accessFlags.demoteFromFinal();
-    DexMethod newMethod =
-        definitions.dexItemFactory().createMethod(holder.type, method.proto, method.name);
+    DexMethod newMethod = method.withHolder(holder.type, definitions.dexItemFactory());
     Invoke.Type type = accessFlags.isStatic() ? Invoke.Type.STATIC : Invoke.Type.SUPER;
     Builder builder = syntheticBuilder(this);
     builder.setMethod(newMethod);
@@ -1417,7 +1432,7 @@
   }
 
   public static int slowCompare(DexEncodedMethod m1, DexEncodedMethod m2) {
-    return m1.method.slowCompareTo(m2.method);
+    return m1.method.compareTo(m2.method);
   }
 
   public MethodOptimizationInfo getOptimizationInfo() {
@@ -1548,6 +1563,11 @@
       return this;
     }
 
+    public Builder setIsLibraryMethodOverrideIfKnown(OptionalBool isLibraryMethodOverride) {
+      return setIsLibraryMethodOverrideIf(
+          !isLibraryMethodOverride.isUnknown(), isLibraryMethodOverride);
+    }
+
     public Builder setParameterAnnotations(ParameterAnnotationsList parameterAnnotations) {
       this.parameterAnnotations = parameterAnnotations;
       return this;
diff --git a/src/main/java/com/android/tools/r8/graph/DexField.java b/src/main/java/com/android/tools/r8/graph/DexField.java
index 87adbf7..5f95369 100644
--- a/src/main/java/com/android/tools/r8/graph/DexField.java
+++ b/src/main/java/com/android/tools/r8/graph/DexField.java
@@ -5,9 +5,11 @@
 
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.references.FieldReference;
 import com.android.tools.r8.references.Reference;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralAccept;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.Collections;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
@@ -26,10 +28,28 @@
     }
   }
 
+  private static void accept(StructuralSpecification<DexField, ?> spec) {
+    spec.withItem(DexField::getHolderType).withItem(DexField::getName).withItem(DexField::getType);
+  }
+
+  @Override
+  public DexField self() {
+    return this;
+  }
+
+  @Override
+  public StructuralAccept<DexField> getStructuralAccept() {
+    return DexField::accept;
+  }
+
   public DexType getHolderType() {
     return holder;
   }
 
+  public DexString getName() {
+    return name;
+  }
+
   public DexType getType() {
     return type;
   }
@@ -122,29 +142,8 @@
   }
 
   @Override
-  public int slowCompareTo(DexField other) {
-    int result = holder.slowCompareTo(other.holder);
-    if (result != 0) {
-      return result;
-    }
-    result = name.slowCompareTo(other.name);
-    if (result != 0) {
-      return result;
-    }
-    return type.slowCompareTo(other.type);
-  }
-
-  @Override
-  public int slowCompareTo(DexField other, NamingLens namingLens) {
-    int result = holder.slowCompareTo(other.holder, namingLens);
-    if (result != 0) {
-      return result;
-    }
-    result = namingLens.lookupName(this).slowCompareTo(namingLens.lookupName(other));
-    if (result != 0) {
-      return result;
-    }
-    return type.slowCompareTo(other.type, namingLens);
+  public void acceptCompareTo(DexField other, CompareToVisitor visitor) {
+    visitor.visitDexField(this, other);
   }
 
   @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 6edd1e1..eee5f4b 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -1784,6 +1784,7 @@
       Function<DexString, Optional<T>> tryString, String baseName, DexType holder) {
     int index = 0;
     while (true) {
+      assert index < 1000;
       DexString name = createString(createMemberString(baseName, holder, index++));
       Optional<T> result = tryString.apply(name);
       if (result.isPresent()) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexLibraryClass.java b/src/main/java/com/android/tools/r8/graph/DexLibraryClass.java
index dcb4ed3..e5a8097 100644
--- a/src/main/java/com/android/tools/r8/graph/DexLibraryClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexLibraryClass.java
@@ -12,6 +12,8 @@
 import com.android.tools.r8.origin.Origin;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
 import java.util.function.Supplier;
 
 public class DexLibraryClass extends DexClass implements Supplier<DexLibraryClass> {
@@ -122,4 +124,18 @@
   public DexLibraryClass get() {
     return this;
   }
+
+  @Override
+  boolean internalClassOrInterfaceMayHaveInitializationSideEffects(
+      AppView<?> appView,
+      DexClass initialAccessHolder,
+      Predicate<DexType> ignore,
+      Set<DexType> seen) {
+    if (!seen.add(getType()) || ignore.test(getType())) {
+      return false;
+    }
+    return isInterface()
+        ? appView.options().libraryInterfacesMayHaveStaticInitialization
+        : !appView.dexItemFactory().libraryClassesWithoutStaticInitialization.contains(type);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexMember.java b/src/main/java/com/android/tools/r8/graph/DexMember.java
index 3041e01..84a1953 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMember.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMember.java
@@ -6,7 +6,7 @@
 import com.google.common.collect.Iterables;
 
 public abstract class DexMember<D extends DexEncodedMember<D, R>, R extends DexMember<D, R>>
-    extends DexReference implements PresortedComparable<R> {
+    extends DexReference implements NamingLensComparable<R> {
 
   public final DexType holder;
   public final DexString 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 6c288c7..249e77e 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethod.java
@@ -5,10 +5,12 @@
 
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.errors.CompilationError;
-import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.references.MethodReference;
 import com.android.tools.r8.references.Reference;
 import com.android.tools.r8.references.TypeReference;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralAccept;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.function.BiConsumer;
@@ -29,6 +31,25 @@
     }
   }
 
+  private static void accept(StructuralSpecification<DexMethod, ?> spec) {
+    spec.withItem(DexMethod::getHolderType).withItem(DexMethod::getName).withItem(m -> m.proto);
+  }
+
+  @Override
+  public StructuralAccept<DexMethod> getStructuralAccept() {
+    return DexMethod::accept;
+  }
+
+  @Override
+  public DexMethod self() {
+    return this;
+  }
+
+  @Override
+  public void acceptCompareTo(DexMethod other, CompareToVisitor visitor) {
+    visitor.visitDexMethod(this, other);
+  }
+
   public DexType getHolderType() {
     return holder;
   }
@@ -181,32 +202,6 @@
   }
 
   @Override
-  public int slowCompareTo(DexMethod other) {
-    int result = holder.slowCompareTo(other.holder);
-    if (result != 0) {
-      return result;
-    }
-    result = name.slowCompareTo(other.name);
-    if (result != 0) {
-      return result;
-    }
-    return proto.slowCompareTo(other.proto);
-  }
-
-  @Override
-  public int slowCompareTo(DexMethod other, NamingLens namingLens) {
-    int result = holder.slowCompareTo(other.holder, namingLens);
-    if (result != 0) {
-      return result;
-    }
-    result = namingLens.lookupName(this).slowCompareTo(namingLens.lookupName(other));
-    if (result != 0) {
-      return result;
-    }
-    return proto.slowCompareTo(other.proto, namingLens);
-  }
-
-  @Override
   public boolean match(DexMethod method) {
     return method.name == name && method.proto == proto;
   }
@@ -270,8 +265,12 @@
         holder, dexItemFactory.prependTypeToProto(type, proto), name);
   }
 
-  public DexMethod withHolder(DexType holder, DexItemFactory dexItemFactory) {
-    return dexItemFactory.createMethod(holder, proto, name);
+  public DexMethod withHolder(DexDefinition definition, DexItemFactory dexItemFactory) {
+    return withHolder(definition.getReference(), dexItemFactory);
+  }
+
+  public DexMethod withHolder(DexReference reference, DexItemFactory dexItemFactory) {
+    return dexItemFactory.createMethod(reference.getContextType(), proto, name);
   }
 
   public DexMethod withName(DexString name, DexItemFactory dexItemFactory) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java b/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java
index 80a3cb9..5ee9c8f 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java
@@ -8,12 +8,14 @@
 import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.StructuralAccept;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.Objects;
 import org.objectweb.asm.Handle;
 import org.objectweb.asm.Opcodes;
 
-public class DexMethodHandle extends IndexedDexItem implements
-    PresortedComparable<DexMethodHandle> {
+public class DexMethodHandle extends IndexedDexItem
+    implements NamingLensComparable<DexMethodHandle> {
 
   public enum MethodHandleType {
     STATIC_PUT((short) 0x00),
@@ -313,31 +315,21 @@
   }
 
   @Override
-  public int slowCompareTo(DexMethodHandle other) {
-    int result = type.getValue() - other.type.getValue();
-    if (result == 0) {
-      if (isFieldHandle()) {
-        result = asField().slowCompareTo(other.asField());
-      } else {
-        assert isMethodHandle();
-        result = asMethod().slowCompareTo(other.asMethod());
-      }
-    }
-    return result;
+  public DexMethodHandle self() {
+    return this;
   }
 
   @Override
-  public int slowCompareTo(DexMethodHandle other, NamingLens namingLens) {
-    int result = type.getValue() - other.type.getValue();
-    if (result == 0) {
-      if (isFieldHandle()) {
-        result = asField().slowCompareTo(other.asField(), namingLens);
-      } else {
-        assert isMethodHandle();
-        result = asMethod().slowCompareTo(other.asMethod(), namingLens);
-      }
-    }
-    return result;
+  public StructuralAccept<DexMethodHandle> getStructuralAccept() {
+    return DexMethodHandle::specify;
+  }
+
+  private static void specify(StructuralSpecification<DexMethodHandle, ?> spec) {
+    spec.withInt(m -> m.type.getValue())
+        .withConditionalItem(DexMethodHandle::isFieldHandle, DexMethodHandle::asField)
+        .withConditionalItem(DexMethodHandle::isMethodHandle, DexMethodHandle::asMethod)
+        .withBool(m -> m.isInterface)
+        .withItem(m -> m.rewrittenTarget);
   }
 
   public Handle toAsmHandle(NamingLens lens) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
index 6474140..ee32f8a 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
@@ -29,6 +29,7 @@
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Set;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
@@ -402,6 +403,102 @@
     this.kotlinInfo = kotlinInfo;
   }
 
+  @Override
+  boolean internalClassOrInterfaceMayHaveInitializationSideEffects(
+      AppView<?> appView,
+      DexClass initialAccessHolder,
+      Predicate<DexType> ignore,
+      Set<DexType> seen) {
+    if (!seen.add(getType()) || ignore.test(getType())) {
+      return false;
+    }
+    return isInterface()
+        ? internalInterfaceMayHaveInitializationSideEffects(
+            appView, initialAccessHolder, ignore, seen)
+        : internalClassMayHaveInitializationSideEffects(appView, initialAccessHolder, ignore, seen);
+  }
+
+  private boolean internalClassMayHaveInitializationSideEffects(
+      AppView<?> appView,
+      DexClass initialAccessHolder,
+      Predicate<DexType> ignore,
+      Set<DexType> seen) {
+    assert !isInterface();
+    assert seen.contains(getType());
+    assert !ignore.test(getType());
+    if (hasClassInitializer()
+        && !getClassInitializer().getOptimizationInfo().classInitializerMayBePostponed()) {
+      return true;
+    }
+    return defaultValuesForStaticFieldsMayTriggerAllocation()
+        || initializationOfParentTypesMayHaveSideEffects(
+            appView, initialAccessHolder, ignore, seen);
+  }
+
+  /**
+   * Interface initialization is described the JVM Specification, section 5.5 Initialization (Java
+   * SE 11 Edition).
+   *
+   * <p>A class or interface C may be initialized only as a result of:
+   *
+   * <ul>
+   *   <li>The execution of any one of the Java Virtual Machine instructions new, getstatic,
+   *       putstatic, or invokestatic that references C.
+   *   <li>...
+   *   <li>If C is an interface that declares a non-abstract, non-static method, the initialization
+   *       of a class that implements C directly or indirectly.
+   * </ul>
+   */
+  private boolean internalInterfaceMayHaveInitializationSideEffects(
+      AppView<?> appView,
+      DexClass initialAccessHolder,
+      Predicate<DexType> ignore,
+      Set<DexType> seen) {
+    assert isInterface();
+    assert seen.contains(getType());
+    assert !ignore.test(getType());
+
+    // If there is a direct access to the interface, then this has side effects if its clinit has
+    // side effects. Parent types are not initialized and thus don't need to be considered.
+    if (this == initialAccessHolder) {
+      if (hasClassInitializer()
+          && !getClassInitializer().getOptimizationInfo().classInitializerMayBePostponed()) {
+        return true;
+      }
+      return defaultValuesForStaticFieldsMayTriggerAllocation();
+    }
+
+    // Otherwise, this interface has side effects if its clinit has side effects and it has at least
+    // one default interface method, or if one of its parent types have observable side effects.
+    if (hasClassInitializer()
+        && !getClassInitializer().getOptimizationInfo().classInitializerMayBePostponed()
+        && getMethodCollection().hasVirtualMethods(DexEncodedMethod::isDefaultMethod)) {
+      return true;
+    }
+
+    return initializationOfParentTypesMayHaveSideEffects(
+        appView, initialAccessHolder, ignore, seen);
+  }
+
+  private boolean initializationOfParentTypesMayHaveSideEffects(
+      AppView<?> appView,
+      DexClass initialAccessHolder,
+      Predicate<DexType> ignore,
+      Set<DexType> seen) {
+    if (superType != null
+        && superType.internalClassOrInterfaceMayHaveInitializationSideEffects(
+            appView, initialAccessHolder, ignore, seen)) {
+      return true;
+    }
+    for (DexType iface : interfaces) {
+      if (iface.internalClassOrInterfaceMayHaveInitializationSideEffects(
+          appView, initialAccessHolder, ignore, seen)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
   public boolean hasFields() {
     return instanceFields.length + staticFields.length > 0;
   }
@@ -454,7 +551,7 @@
       return null;
     }
     DexEncodedField[] fields = staticFields;
-    Arrays.sort(fields, (a, b) -> a.field.slowCompareTo(b.field, namingLens));
+    Arrays.sort(fields, (a, b) -> a.field.compareToWithNamingLens(b.field, namingLens));
     int length = 0;
     List<DexValue> values = new ArrayList<>(fields.length);
     for (int i = 0; i < fields.length; i++) {
diff --git a/src/main/java/com/android/tools/r8/graph/DexProto.java b/src/main/java/com/android/tools/r8/graph/DexProto.java
index 333be20..3855408 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProto.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProto.java
@@ -5,12 +5,13 @@
 
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.StructuralAccept;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import com.google.common.collect.Iterables;
-import com.google.common.hash.Hasher;
 import java.util.Collections;
 import java.util.function.Consumer;
 
-public class DexProto extends IndexedDexItem implements PresortedComparable<DexProto> {
+public class DexProto extends IndexedDexItem implements NamingLensComparable<DexProto> {
 
   public static final DexProto SENTINEL = new DexProto(null, null, null);
 
@@ -24,6 +25,43 @@
     this.parameters = parameters;
   }
 
+  private static void accept(StructuralSpecification<DexProto, ?> spec) {
+    spec.withItem(DexProto::getReturnType)
+        .withItem(p -> p.parameters)
+        // TODO(b/172206529): Consider removing shorty.
+        .withItem(p1 -> p1.shorty);
+  }
+
+  @Override
+  public StructuralAccept<DexProto> getStructuralAccept() {
+    return DexProto::accept;
+  }
+
+  @Override
+  public DexProto self() {
+    return this;
+  }
+
+  @Override
+  public boolean computeEquals(Object other) {
+    if (other instanceof DexProto) {
+      DexProto o = (DexProto) other;
+      return shorty.equals(o.shorty)
+          && returnType.equals(o.returnType)
+          && parameters.equals(o.parameters);
+    }
+    return false;
+  }
+
+  @Override
+  public int computeHashCode() {
+    return shorty.hashCode() + returnType.hashCode() * 7 + parameters.hashCode() * 31;
+  }
+
+  public DexType getReturnType() {
+    return returnType;
+  }
+
   public Iterable<DexType> getParameterBaseTypes(DexItemFactory dexItemFactory) {
     return Iterables.transform(parameters, type -> type.toBaseType(dexItemFactory));
   }
@@ -50,24 +88,6 @@
   }
 
   @Override
-  public int computeHashCode() {
-    return shorty.hashCode()
-        + returnType.hashCode() * 7
-        + parameters.hashCode() * 31;
-  }
-
-  @Override
-  public boolean computeEquals(Object other) {
-    if (other instanceof DexProto) {
-      DexProto o = (DexProto) other;
-      return shorty.equals(o.shorty)
-          && returnType.equals(o.returnType)
-          && parameters.equals(o.parameters);
-    }
-    return false;
-  }
-
-  @Override
   public String toString() {
     return "Proto " + shorty + " " + returnType + " " + parameters;
   }
@@ -86,24 +106,6 @@
   }
 
   @Override
-  public int slowCompareTo(DexProto other) {
-    int result = returnType.slowCompareTo(other.returnType);
-    if (result == 0) {
-      result = parameters.slowCompareTo(other.parameters);
-    }
-    return result;
-  }
-
-  @Override
-  public int slowCompareTo(DexProto other, NamingLens namingLens) {
-    int result = returnType.slowCompareTo(other.returnType, namingLens);
-    if (result == 0) {
-      result = parameters.slowCompareTo(other.parameters, namingLens);
-    }
-    return result;
-  }
-
-  @Override
   public String toSmaliString() {
     return toDescriptorString();
   }
@@ -122,11 +124,4 @@
     builder.append(lens.lookupDescriptor(returnType));
     return builder.toString();
   }
-
-  public void hashSyntheticContent(Hasher hasher) {
-    hasher.putInt(returnType.hashCode());
-    for (DexType param : parameters.values) {
-      hasher.putInt(param.hashCode());
-    }
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexReference.java b/src/main/java/com/android/tools/r8/graph/DexReference.java
index 3263b6d..d32c7ba 100644
--- a/src/main/java/com/android/tools/r8/graph/DexReference.java
+++ b/src/main/java/com/android/tools/r8/graph/DexReference.java
@@ -80,12 +80,12 @@
       return typeDiff;
     }
     if (isDexType()) {
-      return asDexType().slowCompareTo(o.asDexType());
+      return asDexType().compareTo(o.asDexType());
     }
     if (isDexField()) {
-      return asDexField().slowCompareTo(o.asDexField());
+      return asDexField().compareTo(o.asDexField());
     }
     assert isDexMethod();
-    return asDexMethod().slowCompareTo(o.asDexMethod());
+    return asDexMethod().compareTo(o.asDexMethod());
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/DexString.java b/src/main/java/com/android/tools/r8/graph/DexString.java
index b5d50f0..7a50fe1 100644
--- a/src/main/java/com/android/tools/r8/graph/DexString.java
+++ b/src/main/java/com/android/tools/r8/graph/DexString.java
@@ -5,16 +5,19 @@
 
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.dex.IndexedItemCollection;
-import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.IdentifierUtils;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.ThrowingCharIterator;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.HashingVisitor;
+import com.android.tools.r8.utils.structural.StructuralAccept;
 import java.io.UTFDataFormatException;
 import java.util.Arrays;
 import java.util.NoSuchElementException;
 
-public class DexString extends IndexedDexItem implements PresortedComparable<DexString> {
+public class DexString extends IndexedDexItem implements NamingLensComparable<DexString> {
 
   public static final DexString[] EMPTY_ARRAY = {};
   private static final int ARRAY_CHARACTER = '[';
@@ -32,6 +35,27 @@
     this.content = encodeToMutf8(string);
   }
 
+  @Override
+  public DexString self() {
+    return this;
+  }
+
+  @Override
+  public StructuralAccept<DexString> getStructuralAccept() {
+    // Structural accept is never accessed as all accept methods are defined directly.
+    throw new Unreachable();
+  }
+
+  @Override
+  public void acceptCompareTo(DexString other, CompareToVisitor visitor) {
+    visitor.visitDexString(this, other, DexString::internalCompareTo);
+  }
+
+  @Override
+  public void acceptHashing(HashingVisitor visitor) {
+    visitor.visitDexString(this);
+  }
+
   public ThrowingCharIterator<UTFDataFormatException> iterator() {
     return new ThrowingCharIterator<UTFDataFormatException>() {
 
@@ -103,14 +127,6 @@
     }
   }
 
-  public int numberOfLeadingSquareBrackets() {
-    int result = 0;
-    while (content.length > result && content[result] == ((byte) '[')) {
-      result++;
-    }
-    return result;
-  }
-
   private String decode() throws UTFDataFormatException {
     char[] out = new char[size];
     int decodedLength = decodePrefix(out);
@@ -248,8 +264,7 @@
     return mapping.getOffsetFor(this);
   }
 
-  @Override
-  public int slowCompareTo(DexString other) {
+  private int internalCompareTo(DexString other) {
     // Compare the bytes, as comparing UTF-8 encoded strings as strings of unsigned bytes gives
     // the same result as comparing the corresponding Unicode strings lexicographically by
     // codepoint. The only complication is the MUTF-8 encoding have the two byte encoding c0 80 of
@@ -281,12 +296,6 @@
     }
   }
 
-  @Override
-  public int slowCompareTo(DexString other, NamingLens lens) {
-    // The naming lens cannot affect strings.
-    return slowCompareTo(other);
-  }
-
   private static boolean isValidClassDescriptor(String string) {
     if (string.length() < 3
         || string.charAt(0) != 'L'
diff --git a/src/main/java/com/android/tools/r8/graph/DexType.java b/src/main/java/com/android/tools/r8/graph/DexType.java
index 72e632d..39aea8c 100644
--- a/src/main/java/com/android/tools/r8/graph/DexType.java
+++ b/src/main/java/com/android/tools/r8/graph/DexType.java
@@ -20,14 +20,14 @@
 import com.android.tools.r8.ir.desugar.NestBasedAccessDesugaring;
 import com.android.tools.r8.ir.desugar.TwrCloseResourceRewriter;
 import com.android.tools.r8.ir.optimize.ServiceLoaderRewriter;
-import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.synthesis.SyntheticItems;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions.OutlineOptions;
-import com.google.common.base.Predicates;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.HashingVisitor;
+import com.android.tools.r8.utils.structural.StructuralAccept;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Sets;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Set;
@@ -36,7 +36,7 @@
 import java.util.function.Function;
 import java.util.function.Predicate;
 
-public class DexType extends DexReference implements PresortedComparable<DexType> {
+public class DexType extends DexReference implements NamingLensComparable<DexType> {
   public static final DexType[] EMPTY_ARRAY = {};
 
   // Bundletool is merging classes that may originate from a build with an old version of R8.
@@ -53,6 +53,29 @@
   }
 
   @Override
+  public DexType self() {
+    return this;
+  }
+
+  @Override
+  public StructuralAccept<DexType> getStructuralAccept() {
+    // Structural accept is never accessed as all accept methods are defined directly.
+    throw new Unreachable();
+  }
+
+  // DexType overrides accept to ensure the visitors always gets a visitDexType callback.
+  @Override
+  public void acceptCompareTo(DexType other, CompareToVisitor visitor) {
+    visitor.visitDexType(this, other);
+  }
+
+  // DexType overrides accept to ensure the visitors always gets a visitDexType callback.
+  @Override
+  public void acceptHashing(HashingVisitor visitor) {
+    visitor.visitDexType(this);
+  }
+
+  @Override
   public DexType getContextType() {
     return this;
   }
@@ -74,27 +97,21 @@
     return false;
   }
 
-  public boolean classInitializationMayHaveSideEffects(AppView<?> appView) {
-    return classInitializationMayHaveSideEffects(
-        appView, Predicates.alwaysFalse(), Sets.newIdentityHashSet());
-  }
-
-  public boolean classInitializationMayHaveSideEffects(
-      AppView<?> appView, Predicate<DexType> ignore, Set<DexType> seen) {
+  public boolean classInitializationMayHaveSideEffectsInContext(
+      AppView<?> appView, ProgramDefinition context) {
     DexClass clazz = appView.definitionFor(this);
-    return clazz == null || clazz.classInitializationMayHaveSideEffects(appView, ignore, seen);
+    return clazz == null || clazz.classInitializationMayHaveSideEffectsInContext(appView, context);
   }
 
-  public boolean initializationOfParentTypesMayHaveSideEffects(AppView<?> appView) {
-    return initializationOfParentTypesMayHaveSideEffects(
-        appView, Predicates.alwaysFalse(), Sets.newIdentityHashSet());
-  }
-
-  public boolean initializationOfParentTypesMayHaveSideEffects(
-      AppView<?> appView, Predicate<DexType> ignore, Set<DexType> seen) {
+  final boolean internalClassOrInterfaceMayHaveInitializationSideEffects(
+      AppView<?> appView,
+      DexClass initialAccessHolder,
+      Predicate<DexType> ignore,
+      Set<DexType> seen) {
     DexClass clazz = appView.definitionFor(this);
     return clazz == null
-        || clazz.initializationOfParentTypesMayHaveSideEffects(appView, ignore, seen);
+        || clazz.internalClassOrInterfaceMayHaveInitializationSideEffects(
+            appView, initialAccessHolder, ignore, seen);
   }
 
   public boolean isAlwaysNull(AppView<AppInfoWithLiveness> appView) {
@@ -209,18 +226,6 @@
     return this;
   }
 
-  @Override
-  public int slowCompareTo(DexType other) {
-    return descriptor.slowCompareTo(other.descriptor);
-  }
-
-  @Override
-  public int slowCompareTo(DexType other, NamingLens namingLens) {
-    DexString thisDescriptor = namingLens.lookupDescriptor(this);
-    DexString otherDescriptor = namingLens.lookupDescriptor(other);
-    return thisDescriptor.slowCompareTo(otherDescriptor, namingLens);
-  }
-
   public boolean isPrimitiveType() {
     return DescriptorUtils.isPrimitiveType((char) descriptor.content[0]);
   }
diff --git a/src/main/java/com/android/tools/r8/graph/DexTypeList.java b/src/main/java/com/android/tools/r8/graph/DexTypeList.java
index 7d6ed51..a641039 100644
--- a/src/main/java/com/android/tools/r8/graph/DexTypeList.java
+++ b/src/main/java/com/android/tools/r8/graph/DexTypeList.java
@@ -6,15 +6,18 @@
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.MixedSectionCollection;
 import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.utils.ArrayUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.HashingVisitor;
+import com.android.tools.r8.utils.structural.StructuralAccept;
+import com.android.tools.r8.utils.structural.StructuralItem;
 import com.google.common.collect.Iterators;
 import java.util.Arrays;
 import java.util.Iterator;
 import java.util.function.Consumer;
 import java.util.stream.Stream;
 
-public class DexTypeList extends DexItem implements Iterable<DexType> {
+public class DexTypeList extends DexItem implements Iterable<DexType>, StructuralItem<DexTypeList> {
 
   private static final DexTypeList theEmptyTypeList = new DexTypeList();
 
@@ -33,6 +36,27 @@
     this.values = values;
   }
 
+  @Override
+  public StructuralAccept<DexTypeList> getStructuralAccept() {
+    // Structural accept is never accessed as all accept methods are defined directly.
+    throw new Unreachable();
+  }
+
+  @Override
+  public DexTypeList self() {
+    return this;
+  }
+
+  @Override
+  public void acceptCompareTo(DexTypeList other, CompareToVisitor visitor) {
+    visitor.visitDexTypeList(this, other);
+  }
+
+  @Override
+  public void acceptHashing(HashingVisitor visitor) {
+    visitor.visitDexTypeList(this);
+  }
+
   public boolean contains(DexType type) {
     return ArrayUtils.contains(values, type);
   }
@@ -93,38 +117,6 @@
     return builder.toString();
   }
 
-  public int slowCompareTo(DexTypeList other) {
-    for (int i = 0; i <= Math.min(values.length, other.values.length); i++) {
-      if (i == values.length) {
-        return i == other.values.length ? 0 : -1;
-      } else if (i == other.values.length) {
-        return 1;
-      } else {
-        int result = values[i].slowCompareTo(other.values[i]);
-        if (result != 0) {
-          return result;
-        }
-      }
-    }
-    throw new Unreachable();
-  }
-
-  public int slowCompareTo(DexTypeList other, NamingLens namingLens) {
-    for (int i = 0; i <= Math.min(values.length, other.values.length); i++) {
-      if (i == values.length) {
-        return i == other.values.length ? 0 : -1;
-      } else if (i == other.values.length) {
-        return 1;
-      } else {
-        int result = values[i].slowCompareTo(other.values[i], namingLens);
-        if (result != 0) {
-          return result;
-        }
-      }
-    }
-    throw new Unreachable();
-  }
-
   @Override
   public Iterator<DexType> iterator() {
     return Iterators.forArray(values);
@@ -136,7 +128,7 @@
     }
 
     DexType[] newValues = values.clone();
-    Arrays.sort(newValues, DexType::slowCompareTo);
+    Arrays.sort(newValues);
     return new DexTypeList(newValues);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/FieldAccessInfoCollectionImpl.java b/src/main/java/com/android/tools/r8/graph/FieldAccessInfoCollectionImpl.java
index 88869df..439ef05 100644
--- a/src/main/java/com/android/tools/r8/graph/FieldAccessInfoCollectionImpl.java
+++ b/src/main/java/com/android/tools/r8/graph/FieldAccessInfoCollectionImpl.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.graph;
 
+import com.android.tools.r8.utils.ObjectUtils;
 import com.android.tools.r8.utils.SetUtils;
 import java.util.IdentityHashMap;
 import java.util.Map;
@@ -75,10 +76,16 @@
   public FieldAccessInfoCollectionImpl rewrittenWithLens(
       DexDefinitionSupplier definitions, GraphLens lens) {
     FieldAccessInfoCollectionImpl collection = new FieldAccessInfoCollectionImpl();
-    infos.forEach(
-        (field, info) ->
-            collection.infos.put(
-                lens.lookupField(field), info.rewrittenWithLens(definitions, lens)));
+    Consumer<FieldAccessInfoImpl> rewriteAndMergeFieldInfo =
+        info -> {
+          FieldAccessInfoImpl rewrittenInfo = info.rewrittenWithLens(definitions, lens);
+          DexField newField = rewrittenInfo.getField();
+          collection.infos.compute(
+              newField,
+              (ignore, oldInfo) ->
+                  ObjectUtils.mapNotNullOrDefault(oldInfo, rewrittenInfo, rewrittenInfo::join));
+        };
+    infos.values().forEach(rewriteAndMergeFieldInfo);
     return collection;
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/FieldAccessInfoImpl.java b/src/main/java/com/android/tools/r8/graph/FieldAccessInfoImpl.java
index 9dae761..641ef08 100644
--- a/src/main/java/com/android/tools/r8/graph/FieldAccessInfoImpl.java
+++ b/src/main/java/com/android/tools/r8/graph/FieldAccessInfoImpl.java
@@ -281,4 +281,12 @@
     rewritten.writesWithContexts = writesWithContexts.rewrittenWithLens(definitions, lens);
     return rewritten;
   }
+
+  public FieldAccessInfoImpl join(FieldAccessInfoImpl impl) {
+    FieldAccessInfoImpl merged = new FieldAccessInfoImpl(field);
+    merged.flags = flags | impl.flags;
+    merged.readsWithContexts = readsWithContexts.join(impl.readsWithContexts);
+    merged.writesWithContexts = writesWithContexts.join(impl.writesWithContexts);
+    return merged;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeRewriter.java b/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeRewriter.java
index e9b45f1..fd77b73 100644
--- a/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeRewriter.java
+++ b/src/main/java/com/android/tools/r8/graph/GenericSignatureTypeRewriter.java
@@ -10,6 +10,7 @@
 import static com.android.tools.r8.graph.GenericSignature.EMPTY_TYPE_SIGNATURES;
 import static com.android.tools.r8.graph.GenericSignature.FieldTypeSignature.noSignature;
 import static com.android.tools.r8.graph.GenericSignature.StarFieldTypeSignature.STAR_FIELD_TYPE_SIGNATURE;
+import static com.google.common.base.Predicates.alwaysFalse;
 
 import com.android.tools.r8.graph.GenericSignature.ArrayTypeSignature;
 import com.android.tools.r8.graph.GenericSignature.ClassSignature;
@@ -19,22 +20,40 @@
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
 import com.android.tools.r8.graph.GenericSignature.ReturnType;
 import com.android.tools.r8.graph.GenericSignature.TypeSignature;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.function.Function;
+import java.util.function.Predicate;
 
 public class GenericSignatureTypeRewriter {
 
-  private final AppView<?> appView;
+  private final DexItemFactory factory;
+  private final Predicate<DexType> wasPruned;
+  private final Function<DexType, DexType> lookupType;
   private final DexProgramClass context;
 
   private final FieldTypeSignature objectTypeSignature;
 
   public GenericSignatureTypeRewriter(AppView<?> appView, DexProgramClass context) {
-    this.appView = appView;
+    this(
+        appView.dexItemFactory(),
+        appView.appInfo().hasLiveness()
+            ? appView.appInfo().withLiveness()::wasPruned
+            : alwaysFalse(),
+        appView.graphLens()::lookupType,
+        context);
+  }
+
+  public GenericSignatureTypeRewriter(
+      DexItemFactory factory,
+      Predicate<DexType> wasPruned,
+      Function<DexType, DexType> lookupType,
+      DexProgramClass context) {
+    this.factory = factory;
+    this.wasPruned = wasPruned;
+    this.lookupType = lookupType;
     this.context = context;
-    objectTypeSignature =
-        new ClassTypeSignature(appView.dexItemFactory().objectType, EMPTY_TYPE_ARGUMENTS);
+    objectTypeSignature = new ClassTypeSignature(factory.objectType, EMPTY_TYPE_ARGUMENTS);
   }
 
   public ClassSignature rewrite(ClassSignature classSignature) {
@@ -48,7 +67,8 @@
     if (fieldTypeSignature.hasNoSignature()) {
       return fieldTypeSignature;
     }
-    return new TypeSignatureRewriter().run(fieldTypeSignature);
+    FieldTypeSignature rewrittenSignature = new TypeSignatureRewriter().run(fieldTypeSignature);
+    return rewrittenSignature == null ? FieldTypeSignature.noSignature() : rewrittenSignature;
   }
 
   public MethodTypeSignature rewrite(MethodTypeSignature methodTypeSignature) {
@@ -80,8 +100,7 @@
     public void visitSuperClass(ClassTypeSignature classTypeSignature) {
       rewrittenSuperClass = new ClassTypeSignatureRewriter(true).run(classTypeSignature);
       if (rewrittenSuperClass == null) {
-        rewrittenSuperClass =
-            new ClassTypeSignature(appView.dexItemFactory().objectType, EMPTY_TYPE_ARGUMENTS);
+        rewrittenSuperClass = new ClassTypeSignature(factory.objectType, EMPTY_TYPE_ARGUMENTS);
       }
     }
 
@@ -99,7 +118,7 @@
       if (rewrittenTypeParameters.isEmpty()
           && rewrittenSuperInterfaces.isEmpty()
           && rewrittenSuperClass.isNoSignature()
-          && rewrittenSuperClass.type == appView.dexItemFactory().objectType) {
+          && rewrittenSuperClass.type == factory.objectType) {
         return ClassSignature.noSignature();
       }
       return new ClassSignature(
@@ -245,7 +264,6 @@
 
   private class ClassTypeSignatureRewriter implements GenericSignatureVisitor {
 
-    private final AppInfoWithLiveness appInfoWithLiveness;
     private final boolean isSuperClassOrInterface;
 
     // These fields are updated when iterating the modeled structure.
@@ -259,8 +277,6 @@
     private ClassTypeSignature parentClassSignature;
 
     private ClassTypeSignatureRewriter(boolean isSuperClassOrInterface) {
-      appInfoWithLiveness =
-          appView.appInfo().hasLiveness() ? appView.appInfo().withLiveness() : null;
       this.isSuperClassOrInterface = isSuperClassOrInterface;
     }
 
@@ -313,8 +329,8 @@
     }
 
     private DexType getTarget(DexType type) {
-      DexType rewrittenType = appView.graphLens().lookupType(type);
-      if (appInfoWithLiveness != null && appInfoWithLiveness.wasPruned(rewrittenType)) {
+      DexType rewrittenType = lookupType.apply(type);
+      if (wasPruned.test(rewrittenType)) {
         return null;
       }
       if (isSuperClassOrInterface && context.type == rewrittenType) {
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 87cea9c..398ed87 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLens.java
@@ -19,7 +19,6 @@
 import com.google.common.collect.HashBiMap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.ImmutableSortedSet;
 import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.objects.Object2BooleanArrayMap;
 import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
@@ -29,6 +28,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Function;
 import java.util.function.Predicate;
 
 /**
@@ -104,22 +104,43 @@
    */
   public static class FieldLookupResult extends MemberLookupResult<DexField> {
 
-    private FieldLookupResult(DexField reference, DexField reboundReference) {
+    private final DexType castType;
+
+    private FieldLookupResult(DexField reference, DexField reboundReference, DexType castType) {
       super(reference, reboundReference);
+      this.castType = castType;
     }
 
     public static Builder builder(GraphLens lens) {
       return new Builder(lens);
     }
 
+    public boolean hasCastType() {
+      return castType != null;
+    }
+
+    public DexType getCastType() {
+      return castType;
+    }
+
+    public DexType getRewrittenCastType(Function<DexType, DexType> fn) {
+      return hasCastType() ? fn.apply(castType) : null;
+    }
+
     public static class Builder extends MemberLookupResult.Builder<DexField, Builder> {
 
+      private DexType castType;
       private GraphLens lens;
 
       private Builder(GraphLens lens) {
         this.lens = lens;
       }
 
+      public Builder setCastType(DexType castType) {
+        this.castType = castType;
+        return this;
+      }
+
       @Override
       public Builder self() {
         return this;
@@ -127,7 +148,7 @@
 
       public FieldLookupResult build() {
         // TODO(b/168282032): All non-identity graph lenses should set the rebound reference.
-        return new FieldLookupResult(reference, reboundReference);
+        return new FieldLookupResult(reference, reboundReference, castType);
       }
     }
   }
@@ -544,15 +565,6 @@
     return builder.build();
   }
 
-  public ImmutableSortedSet<DexMethod> rewriteMethodsSorted(Set<DexMethod> methods) {
-    ImmutableSortedSet.Builder<DexMethod> builder =
-        new ImmutableSortedSet.Builder<>(PresortedComparable::slowCompare);
-    for (DexMethod method : methods) {
-      builder.add(getRenamedMethodSignature(method));
-    }
-    return builder.build();
-  }
-
   public <T> ImmutableMap<DexField, T> rewriteFieldKeys(Map<DexField, T> map) {
     ImmutableMap.Builder<DexField, T> builder = ImmutableMap.builder();
     map.forEach((field, value) -> builder.put(getRenamedFieldSignature(field), value));
@@ -560,8 +572,7 @@
   }
 
   public ImmutableSet<DexType> rewriteTypes(Set<DexType> types) {
-    ImmutableSortedSet.Builder<DexType> builder =
-        new ImmutableSortedSet.Builder<>(PresortedComparable::slowCompare);
+    ImmutableSet.Builder<DexType> builder = new ImmutableSet.Builder<>();
     for (DexType type : types) {
       builder.add(lookupType(type));
     }
@@ -1056,12 +1067,16 @@
         return FieldLookupResult.builder(this)
             .setReboundReference(rewrittenReboundReference)
             .setReference(rewrittenNonReboundReference)
+            .setCastType(previous.getRewrittenCastType(this::internalDescribeLookupClassType))
             .build();
       } else {
         // TODO(b/168282032): We should always have the rebound reference, so this should become
         //  unreachable.
         DexField rewrittenReference = previous.getRewrittenReference(fieldMap);
-        return FieldLookupResult.builder(this).setReference(rewrittenReference).build();
+        return FieldLookupResult.builder(this)
+            .setReference(rewrittenReference)
+            .setCastType(previous.getRewrittenCastType(this::internalDescribeLookupClassType))
+            .build();
       }
     }
 
diff --git a/src/main/java/com/android/tools/r8/graph/MethodAccessInfoCollection.java b/src/main/java/com/android/tools/r8/graph/MethodAccessInfoCollection.java
index 8aa36c0..7ba6207 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodAccessInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodAccessInfoCollection.java
@@ -11,7 +11,6 @@
 import java.util.IdentityHashMap;
 import java.util.Map;
 import java.util.Set;
-import java.util.TreeMap;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.BiConsumer;
 import java.util.function.Consumer;
@@ -46,11 +45,6 @@
     return new IdentityBuilder();
   }
 
-  // TODO(b/132593519): We should not need sorted maps with the new member rebinding analysis.
-  public static SortedBuilder sortedBuilder() {
-    return new SortedBuilder();
-  }
-
   public Modifier modifier() {
     return new Modifier(
         directInvokes, interfaceInvokes, staticInvokes, superInvokes, virtualInvokes);
@@ -103,7 +97,7 @@
       Map<DexMethod, ProgramMethodSet> invokes, DexDefinitionSupplier definitions, GraphLens lens) {
     return MapUtils.map(
         invokes,
-        capacity -> new TreeMap<>(DexMethod::slowCompareTo),
+        IdentityHashMap::new,
         lens::getRenamedMethodSignature,
         methods -> methods.rewrittenWithLens(definitions, lens),
         (methods, other) -> {
@@ -225,13 +219,6 @@
     }
   }
 
-  public static class SortedBuilder extends Builder<TreeMap<DexMethod, ProgramMethodSet>> {
-
-    private SortedBuilder() {
-      super(() -> new TreeMap<>(DexMethod::slowCompareTo));
-    }
-  }
-
   public static class Modifier extends Builder<Map<DexMethod, ProgramMethodSet>> {
 
     private Modifier(
diff --git a/src/main/java/com/android/tools/r8/graph/MethodCollection.java b/src/main/java/com/android/tools/r8/graph/MethodCollection.java
index d483a59..3200163 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodCollection.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodCollection.java
@@ -137,7 +137,7 @@
   public List<DexEncodedMethod> allMethodsSorted() {
     List<DexEncodedMethod> sorted = new ArrayList<>(size());
     forEachMethod(sorted::add);
-    sorted.sort((a, b) -> a.method.slowCompareTo(b.method));
+    sorted.sort((a, b) -> a.method.compareTo(b.method));
     return sorted;
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/MethodMapBacking.java b/src/main/java/com/android/tools/r8/graph/MethodMapBacking.java
index 2874e03..c6f7a5f 100644
--- a/src/main/java/com/android/tools/r8/graph/MethodMapBacking.java
+++ b/src/main/java/com/android/tools/r8/graph/MethodMapBacking.java
@@ -34,7 +34,7 @@
   }
 
   public static MethodMapBacking createSorted() {
-    Comparator<Wrapper<DexMethod>> comparator = (x, y) -> x.get().slowCompareTo(y.get());
+    Comparator<Wrapper<DexMethod>> comparator = (x, y) -> x.get().compareTo(y.get());
     return new MethodMapBacking(new Object2ReferenceRBTreeMap<>(comparator));
   }
 
diff --git a/src/main/java/com/android/tools/r8/graph/NamingLensComparable.java b/src/main/java/com/android/tools/r8/graph/NamingLensComparable.java
new file mode 100644
index 0000000..e6323eb
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/NamingLensComparable.java
@@ -0,0 +1,15 @@
+// Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.graph;
+
+import com.android.tools.r8.naming.NamingLens;
+import com.android.tools.r8.utils.structural.CompareToVisitorWithNamingLens;
+import com.android.tools.r8.utils.structural.StructuralItem;
+
+public interface NamingLensComparable<T extends NamingLensComparable<T>> extends StructuralItem<T> {
+
+  default int compareToWithNamingLens(T other, NamingLens lens) {
+    return CompareToVisitorWithNamingLens.run(self(), other, lens, StructuralItem::acceptCompareTo);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/ObjectToOffsetMapping.java b/src/main/java/com/android/tools/r8/graph/ObjectToOffsetMapping.java
index b4797a9..b5ce56a 100644
--- a/src/main/java/com/android/tools/r8/graph/ObjectToOffsetMapping.java
+++ b/src/main/java/com/android/tools/r8/graph/ObjectToOffsetMapping.java
@@ -42,7 +42,7 @@
   private DexString firstJumboString;
 
   public ObjectToOffsetMapping(
-      AppInfo appInfo,
+      AppView<?> appView,
       GraphLens graphLens,
       NamingLens namingLens,
       InitClassLens initClassLens,
@@ -54,7 +54,7 @@
       Collection<DexString> strings,
       Collection<DexCallSite> callSites,
       Collection<DexMethodHandle> methodHandles) {
-    assert appInfo != null;
+    assert appView != null;
     assert graphLens != null;
     assert classes != null;
     assert protos != null;
@@ -68,8 +68,8 @@
     this.graphLens = graphLens;
     this.namingLens = namingLens;
     this.initClassLens = initClassLens;
-    this.lensCodeRewriter = new LensCodeRewriterUtils(appInfo, graphLens);
-    this.classes = sortClasses(appInfo, classes, namingLens);
+    this.lensCodeRewriter = new LensCodeRewriterUtils(appView);
+    this.classes = sortClasses(appView.appInfo(), classes, namingLens);
     this.protos = createSortedMap(protos, compare(namingLens), this::failOnOverflow);
     this.types = createSortedMap(types, compare(namingLens), this::failOnOverflow);
     this.methods = createSortedMap(methods, compare(namingLens), this::failOnOverflow);
@@ -79,8 +79,8 @@
     this.methodHandles = createSortedMap(methodHandles, compare(namingLens), this::failOnOverflow);
   }
 
-  private static <T extends PresortedComparable<T>> Comparator<T> compare(NamingLens namingLens) {
-    return (a, b) -> a.slowCompareTo(b, namingLens);
+  private static <T extends NamingLensComparable<T>> Comparator<T> compare(NamingLens namingLens) {
+    return (a, b) -> a.compareToWithNamingLens(b, namingLens);
   }
 
   private void setFirstJumboString(DexString string) {
@@ -168,7 +168,7 @@
                 (x, y) -> {
                   int dx = classDepths.getDepth(x);
                   int dy = classDepths.getDepth(y);
-                  return dx != dy ? dx - dy : x.type.slowCompareTo(y.type, namingLens);
+                  return dx != dy ? dx - dy : x.type.compareToWithNamingLens(y.type, namingLens);
                 })
             .collect(Collectors.toList());
     return sortedClasses.toArray(DexProgramClass.EMPTY_ARRAY);
diff --git a/src/main/java/com/android/tools/r8/graph/PresortedComparable.java b/src/main/java/com/android/tools/r8/graph/PresortedComparable.java
deleted file mode 100644
index e4eb4b7..0000000
--- a/src/main/java/com/android/tools/r8/graph/PresortedComparable.java
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright (c) 2016, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.graph;
-
-import com.android.tools.r8.naming.NamingLens;
-
-public interface PresortedComparable<T> {
-
-  int slowCompareTo(T other);
-  int slowCompareTo(T other, NamingLens namingLens);
-
-  static <T extends PresortedComparable<T>> int slowCompare(T a, T b) {
-    return a.slowCompareTo(b);
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/graph/SortedProgramPackage.java b/src/main/java/com/android/tools/r8/graph/SortedProgramPackage.java
index 97afd09..08be3b6 100644
--- a/src/main/java/com/android/tools/r8/graph/SortedProgramPackage.java
+++ b/src/main/java/com/android/tools/r8/graph/SortedProgramPackage.java
@@ -9,6 +9,6 @@
 public class SortedProgramPackage extends ProgramPackage {
 
   public SortedProgramPackage(String packageDescriptor) {
-    super(packageDescriptor, () -> new TreeSet<>((a, b) -> a.getType().slowCompareTo(b.getType())));
+    super(packageDescriptor, () -> new TreeSet<>((a, b) -> a.getType().compareTo(b.getType())));
   }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/SubtypingInfo.java b/src/main/java/com/android/tools/r8/graph/SubtypingInfo.java
index b73d2bc..d9b17f2 100644
--- a/src/main/java/com/android/tools/r8/graph/SubtypingInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/SubtypingInfo.java
@@ -280,7 +280,7 @@
 
     private void ensureDirectSubTypeSet() {
       if (directSubtypes == NO_DIRECT_SUBTYPE) {
-        directSubtypes = new ConcurrentSkipListSet<>(DexType::slowCompareTo);
+        directSubtypes = new ConcurrentSkipListSet<>(DexType::compareTo);
       }
     }
 
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 285e397..d0948bf 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
@@ -6,25 +6,40 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.optimize.lambda.LambdaGroup;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.google.common.collect.ImmutableSet;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
+import java.util.Collections;
+import java.util.Map;
 import java.util.Set;
+import java.util.function.BiConsumer;
 
 public class HorizontallyMergedLambdaClasses implements MergedClasses {
 
-  private final Set<DexType> sources;
+  private final BidirectionalManyToOneMap<DexType, DexType> mergedClasses;
 
-  public HorizontallyMergedLambdaClasses(Set<DexType> sources) {
-    this.sources = sources;
+  public HorizontallyMergedLambdaClasses(Map<DexType, LambdaGroup> lambdas) {
+    this.mergedClasses = new BidirectionalManyToOneMap<>();
+    lambdas.forEach((lambda, group) -> mergedClasses.put(lambda, group.getGroupClassType()));
   }
 
   public static HorizontallyMergedLambdaClasses empty() {
-    return new HorizontallyMergedLambdaClasses(ImmutableSet.of());
+    return new HorizontallyMergedLambdaClasses(Collections.emptyMap());
+  }
+
+  @Override
+  public void forEachMergeGroup(BiConsumer<Set<DexType>, DexType> consumer) {
+    mergedClasses.forEach(consumer);
+  }
+
+  @Override
+  public boolean hasBeenMerged(DexType type) {
+    return mergedClasses.containsKey(type);
   }
 
   @Override
   public boolean verifyAllSourcesPruned(AppView<AppInfoWithLiveness> appView) {
-    for (DexType source : sources) {
+    for (DexType source : mergedClasses.keySet()) {
       assert appView.appInfo().wasPruned(source)
           : "Expected horizontally merged lambda class `"
               + source.toSourceString()
@@ -32,9 +47,4 @@
     }
     return true;
   }
-
-  @Override
-  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 3f25895..8fa6995 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
@@ -8,13 +8,17 @@
 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;
+import java.util.function.BiConsumer;
 
 public interface MergedClasses {
 
-  boolean verifyAllSourcesPruned(AppView<AppInfoWithLiveness> appView);
+  void forEachMergeGroup(BiConsumer<Set<DexType>, DexType> consumer);
 
   boolean hasBeenMerged(DexType type);
 
+  boolean verifyAllSourcesPruned(AppView<AppInfoWithLiveness> appView);
+
   /**
    * Determine if the class has been merged by the merged classes object. If the merged classes is
    * null then 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 55c34b5..1225127 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
@@ -9,6 +9,8 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
+import java.util.function.BiConsumer;
 
 public class MergedClassesCollection implements MergedClasses {
 
@@ -19,11 +21,10 @@
   }
 
   @Override
-  public boolean verifyAllSourcesPruned(AppView<AppInfoWithLiveness> appView) {
+  public void forEachMergeGroup(BiConsumer<Set<DexType>, DexType> consumer) {
     for (MergedClasses mergedClasses : collection) {
-      assert mergedClasses.verifyAllSourcesPruned(appView);
+      mergedClasses.forEachMergeGroup(consumer);
     }
-    return true;
   }
 
   @Override
@@ -35,4 +36,12 @@
     }
     return false;
   }
+
+  @Override
+  public boolean verifyAllSourcesPruned(AppView<AppInfoWithLiveness> appView) {
+    for (MergedClasses mergedClasses : collection) {
+      assert mergedClasses.verifyAllSourcesPruned(appView);
+    }
+    return true;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/classmerging/StaticallyMergedClasses.java b/src/main/java/com/android/tools/r8/graph/classmerging/StaticallyMergedClasses.java
new file mode 100644
index 0000000..7046e80
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/classmerging/StaticallyMergedClasses.java
@@ -0,0 +1,64 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.graph.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.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
+import java.util.Set;
+import java.util.function.BiConsumer;
+
+public class StaticallyMergedClasses implements MergedClasses {
+
+  private final BidirectionalManyToOneMap<DexType, DexType> mergedClasses;
+
+  public StaticallyMergedClasses(BidirectionalManyToOneMap<DexType, DexType> mergedClasses) {
+    this.mergedClasses = mergedClasses;
+  }
+
+  public static StaticallyMergedClasses empty() {
+    return new StaticallyMergedClasses(BidirectionalManyToOneMap.empty());
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  public void forEachMergeGroup(BiConsumer<Set<DexType>, DexType> consumer) {
+    mergedClasses.forEach(consumer);
+  }
+
+  @Override
+  public boolean hasBeenMerged(DexType type) {
+    return false;
+  }
+
+  @Override
+  public boolean verifyAllSourcesPruned(AppView<AppInfoWithLiveness> appView) {
+    return true;
+  }
+
+  public static class Builder {
+
+    private final BidirectionalManyToOneMap<DexType, DexType> mergedClasses =
+        new BidirectionalManyToOneMap<>();
+
+    private Builder() {}
+
+    public void recordMerge(DexProgramClass source, DexProgramClass target) {
+      for (DexType previousSource : mergedClasses.removeValue(source.getType())) {
+        mergedClasses.put(previousSource, target.getType());
+      }
+      mergedClasses.put(source.getType(), target.getType());
+    }
+
+    public StaticallyMergedClasses build() {
+      return new StaticallyMergedClasses(mergedClasses);
+    }
+  }
+}
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 ed69ace..4b25311 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
@@ -7,28 +7,35 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.BiConsumer;
 
 public class VerticallyMergedClasses implements MergedClasses {
 
-  private final Map<DexType, DexType> mergedClasses;
-  private final Map<DexType, Set<DexType>> mergedClassesInverse;
+  private final BidirectionalManyToOneMap<DexType, DexType> mergedClasses;
 
-  public VerticallyMergedClasses(
-      Map<DexType, DexType> mergedClasses, Map<DexType, Set<DexType>> mergedClassesInverse) {
+  public VerticallyMergedClasses(BidirectionalManyToOneMap<DexType, DexType> mergedClasses) {
     this.mergedClasses = mergedClasses;
-    this.mergedClassesInverse = mergedClassesInverse;
+  }
+
+  public static VerticallyMergedClasses empty() {
+    return new VerticallyMergedClasses(BidirectionalManyToOneMap.empty());
+  }
+
+  @Override
+  public void forEachMergeGroup(BiConsumer<Set<DexType>, DexType> consumer) {
+    mergedClasses.forEach(consumer);
   }
 
   public Map<DexType, DexType> getForwardMap() {
-    return mergedClasses;
+    return mergedClasses.getForwardMap();
   }
 
   public Collection<DexType> getSourcesFor(DexType type) {
-    return mergedClassesInverse.getOrDefault(type, Collections.emptySet());
+    return mergedClasses.getKeys(type);
   }
 
   public DexType getTargetFor(DexType type) {
@@ -36,27 +43,33 @@
     return mergedClasses.get(type);
   }
 
+  public DexType getTargetForOrDefault(DexType type, DexType defaultValue) {
+    return mergedClasses.getOrDefault(type, defaultValue);
+  }
+
   public boolean hasBeenMergedIntoSubtype(DexType type) {
     return mergedClasses.containsKey(type);
   }
 
+  public boolean isEmpty() {
+    return mergedClasses.isEmpty();
+  }
+
   public boolean isTarget(DexType type) {
     return !getSourcesFor(type).isEmpty();
   }
 
   @Override
-  public boolean verifyAllSourcesPruned(AppView<AppInfoWithLiveness> appView) {
-    for (Collection<DexType> sourcesForTarget : mergedClassesInverse.values()) {
-      for (DexType source : sourcesForTarget) {
-        assert appView.appInfo().wasPruned(source)
-            : "Expected vertically merged class `" + source.toSourceString() + "` to be absent";
-      }
-    }
-    return true;
+  public boolean hasBeenMerged(DexType type) {
+    return hasBeenMergedIntoSubtype(type);
   }
 
   @Override
-  public boolean hasBeenMerged(DexType type) {
-    return hasBeenMergedIntoSubtype(type);
+  public boolean verifyAllSourcesPruned(AppView<AppInfoWithLiveness> appView) {
+    for (DexType source : mergedClasses.keySet()) {
+      assert appView.appInfo().wasPruned(source)
+          : "Expected vertically merged class `" + source.toSourceString() + "` to be absent";
+    }
+    return true;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
new file mode 100644
index 0000000..7cf3029
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
@@ -0,0 +1,71 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.horizontalclassmerging;
+
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.horizontalclassmerging.HorizontalClassMergerGraphLens.Builder;
+import com.android.tools.r8.utils.ListUtils;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.IdentityHashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+public class ClassInstanceFieldsMerger {
+
+  // Map from target class field to all fields which should be merged into that field.
+  private final Map<DexEncodedField, List<DexEncodedField>> fieldMappings = new LinkedHashMap<>();
+  private final Builder lensBuilder;
+
+  public ClassInstanceFieldsMerger(
+      HorizontalClassMergerGraphLens.Builder lensBuilder, DexProgramClass target) {
+    this.lensBuilder = lensBuilder;
+    target
+        .instanceFields()
+        .forEach(field -> fieldMappings.computeIfAbsent(field, ignore -> new ArrayList<>()));
+  }
+
+  public void addFields(DexProgramClass clazz) {
+    Map<DexType, List<DexEncodedField>> availableFields = new IdentityHashMap<>();
+    for (DexEncodedField field : fieldMappings.keySet()) {
+      availableFields.computeIfAbsent(field.type(), ignore -> new LinkedList<>()).add(field);
+    }
+
+    for (DexEncodedField oldField : clazz.instanceFields()) {
+      DexEncodedField newField =
+          ListUtils.removeFirstMatch(
+                  availableFields.get(oldField.type()),
+                  field -> field.getAccessFlags().isSameVisibility(oldField.getAccessFlags()))
+              .get();
+      assert newField != null;
+      fieldMappings.get(newField).add(oldField);
+    }
+  }
+
+  private void mergeField(DexEncodedField oldField, DexEncodedField newField) {
+    if (newField.isFinal() && !oldField.isFinal()) {
+      newField.getAccessFlags().demoteFromFinal();
+    }
+    lensBuilder.moveField(oldField.field, newField.field);
+  }
+
+  private void mergeFields(DexEncodedField newField, Collection<DexEncodedField> oldFields) {
+    DexField newFieldReference = newField.getReference();
+
+    lensBuilder.moveField(newFieldReference, newFieldReference);
+    lensBuilder.setRepresentativeField(newFieldReference, newFieldReference);
+
+    oldFields.forEach(oldField -> mergeField(oldField, newField));
+  }
+
+  public void merge() {
+    fieldMappings.forEach(this::mergeFields);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
index 79913ff..f75f969 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
@@ -4,6 +4,8 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import static com.google.common.base.Predicates.not;
+
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexAnnotationSet;
@@ -22,7 +24,6 @@
 import com.android.tools.r8.shaking.FieldAccessInfoCollectionModifier;
 import com.android.tools.r8.utils.MethodSignatureEquivalence;
 import com.google.common.base.Equivalence.Wrapper;
-import com.google.common.base.Predicates;
 import com.google.common.collect.Iterables;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
@@ -52,6 +53,7 @@
   private final ClassMethodsBuilder classMethodsBuilder = new ClassMethodsBuilder();
   private final Reference2IntMap<DexType> classIdentifiers = new Reference2IntOpenHashMap<>();
   private final ClassStaticFieldsMerger classStaticFieldsMerger;
+  private final ClassInstanceFieldsMerger classInstanceFieldsMerger;
   private final Collection<VirtualMethodMerger> virtualMethodMergers;
   private final Collection<ConstructorMerger> constructorMergers;
   private final DexField classIdField;
@@ -77,6 +79,7 @@
 
     this.dexItemFactory = appView.dexItemFactory();
     this.classStaticFieldsMerger = new ClassStaticFieldsMerger(appView, lensBuilder, target);
+    this.classInstanceFieldsMerger = new ClassInstanceFieldsMerger(lensBuilder, target);
 
     buildClassIdentifierMap();
   }
@@ -182,22 +185,26 @@
     toMergeGroup.forEach(clazz -> clazz.setStaticFields(null));
   }
 
-  void fixFinal() {
-    if (Iterables.any(toMergeGroup, Predicates.not(DexProgramClass::isFinal))) {
-      target.accessFlags.demoteFromFinal();
+  void fixAccessFlags() {
+    if (Iterables.any(toMergeGroup, not(DexProgramClass::isAbstract))) {
+      target.getAccessFlags().demoteFromAbstract();
+    }
+    if (Iterables.any(toMergeGroup, not(DexProgramClass::isFinal))) {
+      target.getAccessFlags().demoteFromFinal();
     }
   }
 
   void mergeInstanceFields() {
-    // TODO: support instance field merging
-    assert Iterables.all(toMergeGroup, clazz -> !clazz.hasInstanceFields());
-
-    // The target should only have the class id field.
-    assert target.instanceFields().size() == 1;
+    toMergeGroup.forEach(
+        clazz -> {
+          classInstanceFieldsMerger.addFields(clazz);
+          clazz.setInstanceFields(null);
+        });
+    classInstanceFieldsMerger.merge();
   }
 
   public void mergeGroup(SyntheticArgumentClass syntheticArgumentClass) {
-    fixFinal();
+    fixAccessFlags();
     appendClassIdField();
 
     mergeVirtualMethods();
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassStaticFieldsMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassStaticFieldsMerger.java
index d826b86..e287090 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassStaticFieldsMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassStaticFieldsMerger.java
@@ -48,7 +48,7 @@
     field = field.toTypeSubstitutedField(newFieldReference);
     targetFields.put(newFieldReference, field);
 
-    lensBuilder.mapField(oldFieldReference, newFieldReference);
+    lensBuilder.moveField(oldFieldReference, newFieldReference);
   }
 
   public void addFields(DexProgramClass toMerge) {
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 d15ed7e..2e2020d 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.FieldAccessInfoCollectionModifier;
 import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.structural.Ordered;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
@@ -156,7 +157,7 @@
     for (DexEncodedMethod constructor : constructors) {
       if (constructor.hasClassFileVersion()) {
         classFileVersion =
-            CfVersion.maxAllowNull(classFileVersion, constructor.getClassFileVersion());
+            Ordered.maxIgnoreNull(classFileVersion, constructor.getClassFileVersion());
       }
       DexMethod movedConstructor = moveConstructor(classMethodsBuilder, constructor);
       lensBuilder.mapMethod(movedConstructor, movedConstructor);
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/FieldMultiset.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/FieldMultiset.java
index 37222de..6cbffcc 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/FieldMultiset.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/FieldMultiset.java
@@ -4,19 +4,59 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import com.android.tools.r8.graph.AccessFlags;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
-import com.google.common.collect.HashMultiset;
-import com.google.common.collect.Multiset;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
 
 public class FieldMultiset {
 
-  private final Multiset<DexType> fields = HashMultiset.create();
+  private static class VisibilitySignature {
+    private int publicVisible = 0;
+    private int protectedVisible = 0;
+    private int privateVisible = 0;
+    private int packagePrivateVisible = 0;
+
+    public void addAccessModifier(AccessFlags accessFlags) {
+      if (accessFlags.isPublic()) {
+        publicVisible++;
+      } else if (accessFlags.isPrivate()) {
+        privateVisible++;
+      } else if (accessFlags.isPackagePrivate()) {
+        packagePrivateVisible++;
+      } else if (accessFlags.isProtected()) {
+        protectedVisible++;
+      }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) return true;
+      if (o == null || getClass() != o.getClass()) return false;
+      VisibilitySignature that = (VisibilitySignature) o;
+      return publicVisible == that.publicVisible
+          && protectedVisible == that.protectedVisible
+          && privateVisible == that.privateVisible
+          && packagePrivateVisible == that.packagePrivateVisible;
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(publicVisible, protectedVisible, privateVisible, packagePrivateVisible);
+    }
+  }
+
+  // This *must* not be an IdentityHashMap, because hash equality does not work for the values.
+  private final Map<DexType, VisibilitySignature> fields = new HashMap<>();
 
   public FieldMultiset(DexProgramClass clazz) {
     for (DexEncodedField field : clazz.instanceFields()) {
-      fields.add(field.type());
+      fields
+          .computeIfAbsent(field.type(), ignore -> new VisibilitySignature())
+          .addAccessModifier(field.getAccessFlags());
     }
   }
 
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 2200267..8dfa67c 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -8,19 +8,19 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.horizontalclassmerging.policies.AllInstantiatedOrUninstantiated;
+import com.android.tools.r8.horizontalclassmerging.policies.CheckAbstractClasses;
 import com.android.tools.r8.horizontalclassmerging.policies.ClassesHaveSameInterfaces;
 import com.android.tools.r8.horizontalclassmerging.policies.DontInlinePolicy;
 import com.android.tools.r8.horizontalclassmerging.policies.DontMergeIntoLessVisible;
 import com.android.tools.r8.horizontalclassmerging.policies.DontMergeSynchronizedClasses;
 import com.android.tools.r8.horizontalclassmerging.policies.IgnoreSynthetics;
-import com.android.tools.r8.horizontalclassmerging.policies.NoAbstractClasses;
 import com.android.tools.r8.horizontalclassmerging.policies.NoAnnotations;
 import com.android.tools.r8.horizontalclassmerging.policies.NoClassesOrMembersWithAnnotations;
 import com.android.tools.r8.horizontalclassmerging.policies.NoEnums;
 import com.android.tools.r8.horizontalclassmerging.policies.NoInnerClasses;
-import com.android.tools.r8.horizontalclassmerging.policies.NoInstanceFields;
 import com.android.tools.r8.horizontalclassmerging.policies.NoInterfaces;
 import com.android.tools.r8.horizontalclassmerging.policies.NoKeepRules;
+import com.android.tools.r8.horizontalclassmerging.policies.NoKotlinLambdas;
 import com.android.tools.r8.horizontalclassmerging.policies.NoKotlinMetadata;
 import com.android.tools.r8.horizontalclassmerging.policies.NoNativeMethods;
 import com.android.tools.r8.horizontalclassmerging.policies.NoRuntimeTypeChecks;
@@ -34,6 +34,7 @@
 import com.android.tools.r8.horizontalclassmerging.policies.PreventMethodImplementation;
 import com.android.tools.r8.horizontalclassmerging.policies.RespectPackageBoundaries;
 import com.android.tools.r8.horizontalclassmerging.policies.SameFeatureSplit;
+import com.android.tools.r8.horizontalclassmerging.policies.SameFields;
 import com.android.tools.r8.horizontalclassmerging.policies.SameNestHost;
 import com.android.tools.r8.horizontalclassmerging.policies.SameParentClass;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
@@ -44,9 +45,8 @@
 import com.google.common.collect.Iterables;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.LinkedHashMap;
+import java.util.Collections;
 import java.util.List;
-import java.util.Map;
 
 public class HorizontalClassMerger {
   private final AppView<AppInfoWithLiveness> appView;
@@ -61,17 +61,15 @@
       DirectMappedDexApplication.Builder appBuilder,
       MainDexTracingResult mainDexTracingResult,
       RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
-    Map<FieldMultiset, List<DexProgramClass>> classes = new LinkedHashMap<>();
+    List<DexProgramClass> initialGroup = appView.appInfo().classesWithDeterministicOrder();
 
-    // Group classes by same field signature using the hash map.
-    for (DexProgramClass clazz : appView.appInfo().classesWithDeterministicOrder()) {
-      classes.computeIfAbsent(new FieldMultiset(clazz), ignore -> new ArrayList<>()).add(clazz);
-    }
-
-    // Run the policies on all collected classes to produce a final grouping.
+    // Run the policies on all program classes to produce a final grouping.
     Collection<List<DexProgramClass>> groups =
         new SimplePolicyExecutor()
-            .run(classes.values(), getPolicies(mainDexTracingResult, runtimeTypeCheckInfo));
+            .run(
+                Collections.singletonList(initialGroup),
+                getPolicies(mainDexTracingResult, runtimeTypeCheckInfo));
+
     // If there are no groups, then end horizontal class merging.
     if (groups.isEmpty()) {
       appView.setHorizontallyMergedClasses(HorizontallyMergedClasses.empty());
@@ -109,12 +107,12 @@
       RuntimeTypeCheckInfo runtimeTypeCheckInfo) {
     return ImmutableList.of(
         new NotMatchedByNoHorizontalClassMerging(appView),
-        new NoInstanceFields(),
+        new SameFields(),
         new NoInterfaces(),
         new ClassesHaveSameInterfaces(),
         new NoAnnotations(),
         new NoEnums(appView),
-        new NoAbstractClasses(),
+        new CheckAbstractClasses(appView),
         new IgnoreSynthetics(appView),
         new NoClassesOrMembersWithAnnotations(),
         new NoInnerClasses(),
@@ -122,6 +120,7 @@
         new NoNativeMethods(),
         new NoKeepRules(appView),
         new NoKotlinMetadata(),
+        new NoKotlinLambdas(appView),
         new NoServiceLoaders(appView),
         new NotVerticallyMergedIntoSubtype(appView),
         new NoRuntimeTypeChecks(runtimeTypeCheckInfo),
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
index 8dadc71..cc0528f 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
@@ -13,7 +13,6 @@
 import com.android.tools.r8.ir.conversion.ExtraParameter;
 import com.android.tools.r8.utils.IterableUtils;
 import com.google.common.collect.BiMap;
-import com.google.common.collect.HashBiMap;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.IdentityHashMap;
@@ -25,8 +24,9 @@
 public class HorizontalClassMergerGraphLens extends NestedGraphLens {
   private final AppView<?> appView;
   private final Map<DexMethod, List<ExtraParameter>> methodExtraParameters;
-  private final Map<DexMethod, DexMethod> originalConstructorSignatures;
+  private final Map<DexMethod, DexMethod> extraOriginalMethodSignatures;
   private final HorizontallyMergedClasses mergedClasses;
+  private final Map<DexField, DexField> extraOriginalFieldSignatures;
 
   private HorizontalClassMergerGraphLens(
       AppView<?> appView,
@@ -36,7 +36,8 @@
       Map<DexMethod, DexMethod> methodMap,
       BiMap<DexField, DexField> originalFieldSignatures,
       BiMap<DexMethod, DexMethod> originalMethodSignatures,
-      Map<DexMethod, DexMethod> originalConstructorSignatures,
+      Map<DexMethod, DexMethod> extraOriginalMethodSignatures,
+      Map<DexField, DexField> extraOriginalFieldSignatures,
       GraphLens previousLens) {
     super(
         mergedClasses.getForwardMap(),
@@ -48,7 +49,8 @@
         appView.dexItemFactory());
     this.appView = appView;
     this.methodExtraParameters = methodExtraParameters;
-    this.originalConstructorSignatures = originalConstructorSignatures;
+    this.extraOriginalFieldSignatures = extraOriginalFieldSignatures;
+    this.extraOriginalMethodSignatures = extraOriginalMethodSignatures;
     this.mergedClasses = mergedClasses;
   }
 
@@ -59,13 +61,22 @@
 
   @Override
   public DexMethod getOriginalMethodSignature(DexMethod method) {
-    DexMethod originalConstructor = originalConstructorSignatures.get(method);
+    DexMethod originalConstructor = extraOriginalMethodSignatures.get(method);
     if (originalConstructor == null) {
       return super.getOriginalMethodSignature(method);
     }
     return getPrevious().getOriginalMethodSignature(originalConstructor);
   }
 
+  @Override
+  public DexField getOriginalFieldSignature(DexField field) {
+    DexField originalField = extraOriginalFieldSignatures.get(field);
+    if (originalField == null) {
+      return super.getOriginalFieldSignature(field);
+    }
+    return getPrevious().getOriginalFieldSignature(originalField);
+  }
+
   /**
    * If an overloaded constructor is requested, add the constructor id as a parameter to the
    * constructor. Otherwise return the lookup on the underlying graph lens.
@@ -86,10 +97,8 @@
   }
 
   public static class Builder {
-    private final BiMap<DexField, DexField> fieldMap = HashBiMap.create();
-
+    private ManyToOneMap<DexField, DexField> fieldMap = new ManyToOneMap<>();
     private ManyToOneMap<DexMethod, DexMethod> methodMap = new ManyToOneMap<>();
-
     private final Map<DexMethod, List<ExtraParameter>> methodExtraParameters =
         new IdentityHashMap<>();
 
@@ -104,17 +113,24 @@
                 assert false;
                 return group.iterator().next();
               });
-      BiMap<DexField, DexField> originalFieldSignatures = fieldMap.inverse();
+      ManyToOneInverseMap<DexField, DexField> inverseFieldMap =
+          fieldMap.inverse(
+              group -> {
+                // Every group should have a representative. Fail in debug mode.
+                assert false;
+                return group.iterator().next();
+              });
 
       return new HorizontalClassMergerGraphLens(
           appView,
           mergedClasses,
           methodExtraParameters,
-          fieldMap,
+          fieldMap.getForwardMap(),
           methodMap.getForwardMap(),
-          originalFieldSignatures,
+          inverseFieldMap.getBiMap(),
           inverseMethodMap.getBiMap(),
           inverseMethodMap.getExtraMap(),
+          inverseFieldMap.getExtraMap(),
           appView.graphLens());
     }
 
@@ -122,12 +138,18 @@
       methodMap = methodMap.remap(remapMethods, Function.identity(), Function.identity());
     }
 
-    public Builder mapField(DexField from, DexField to) {
-      DexField previousFrom = fieldMap.inverse().remove(from);
-      if (previousFrom != null) {
-        from = previousFrom;
-      }
+    public void remapFields(BiMap<DexField, DexField> remapFields) {
+      fieldMap = fieldMap.remap(remapFields, Function.identity(), Function.identity());
+    }
+
+    public Builder moveField(DexField from, DexField to) {
       fieldMap.put(from, to);
+      fieldMap.putInverse(from, to);
+      return this;
+    }
+
+    public Builder setRepresentativeField(DexField from, DexField to) {
+      fieldMap.setRepresentative(from, to);
       return this;
     }
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
index 080102a..5432ef1 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
@@ -13,6 +13,7 @@
 import java.util.Collection;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.BiConsumer;
 
 public class HorizontallyMergedClasses implements MergedClasses {
 
@@ -26,6 +27,11 @@
     return new HorizontallyMergedClasses(new BidirectionalManyToOneMap<>());
   }
 
+  @Override
+  public void forEachMergeGroup(BiConsumer<Set<DexType>, DexType> consumer) {
+    mergedClasses.forEach(consumer);
+  }
+
   public DexType getMergeTargetOrDefault(DexType type) {
     return mergedClasses.getOrDefault(type, type);
   }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java
index 1a3a965..92130de 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java
@@ -38,7 +38,16 @@
       MultiClassPolicy policy, LinkedList<List<DexProgramClass>> groups) {
     // For each group apply the multi class policy and add all the new groups together.
     return groups.stream()
-        .flatMap(group -> policy.apply(group).stream())
+        .flatMap(
+            group -> {
+              int previousNumberOfClasses = group.size();
+              Collection<List<DexProgramClass>> newGroups = policy.apply(group);
+              policy.numberOfRemovedClasses += previousNumberOfClasses;
+              for (List<DexProgramClass> newGroup : newGroups) {
+                policy.numberOfRemovedClasses -= newGroup.size();
+              }
+              return newGroups.stream();
+            })
         .collect(Collectors.toCollection(LinkedList::new));
   }
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java
index a7ce343..49399bf 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java
@@ -46,6 +46,7 @@
   private final AppView<AppInfoWithLiveness> appView;
   private final DexItemFactory dexItemFactory;
   private final BiMap<DexMethod, DexMethod> movedMethods = HashBiMap.create();
+  private final BiMap<DexField, DexField> movedFields = HashBiMap.create();
   private final SyntheticArgumentClass syntheticArgumentClass;
   private final BiMap<DexMethodSignature, DexMethodSignature> reservedInterfaceSignatures =
       HashBiMap.create();
@@ -129,6 +130,7 @@
     }
 
     lensBuilder.remapMethods(movedMethods);
+    lensBuilder.remapFields(movedFields);
 
     HorizontalClassMergerGraphLens lens = lensBuilder.build(appView, mergedClasses);
     fieldAccessChangesBuilder.build(this::fixupMethodReference).modify(appView);
@@ -381,7 +383,7 @@
       }
 
       if (newField != encodedField.field) {
-        lensBuilder.mapField(field, newField);
+        movedFields.put(field, newField);
         setter.setField(i, encodedField.toTypeSubstitutedField(newField));
       }
     }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
index 07a3172..258b1c7 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
 import com.android.tools.r8.graph.MethodAccessFlags;
 import com.android.tools.r8.graph.ParameterAnnotationsList;
@@ -20,17 +21,20 @@
 import com.android.tools.r8.ir.synthetic.AbstractSynthesizedCode;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.FieldAccessInfoCollectionModifier;
+import com.android.tools.r8.utils.ListUtils;
+import com.android.tools.r8.utils.OptionalBool;
+import com.android.tools.r8.utils.structural.Ordered;
 import com.google.common.collect.Iterables;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceAVLTreeMap;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceSortedMap;
 import it.unimi.dsi.fastutil.objects.Reference2IntMap;
 import java.util.ArrayList;
-import java.util.Collection;
+import java.util.List;
 
 public class VirtualMethodMerger {
   private final DexProgramClass target;
   private final DexItemFactory dexItemFactory;
-  private final Collection<ProgramMethod> methods;
+  private final List<ProgramMethod> methods;
   private final DexField classIdField;
   private final AppView<AppInfoWithLiveness> appView;
   private final DexMethod superMethod;
@@ -38,7 +42,7 @@
   public VirtualMethodMerger(
       AppView<AppInfoWithLiveness> appView,
       DexProgramClass target,
-      Collection<ProgramMethod> methods,
+      List<ProgramMethod> methods,
       DexField classIdField,
       DexMethod superMethod) {
     this.dexItemFactory = appView.dexItemFactory();
@@ -50,7 +54,7 @@
   }
 
   public static class Builder {
-    private final Collection<ProgramMethod> methods = new ArrayList<>();
+    private final List<ProgramMethod> methods = new ArrayList<>();
 
     public Builder add(ProgramMethod constructor) {
       methods.add(constructor);
@@ -122,63 +126,130 @@
 
   private MethodAccessFlags getAccessFlags() {
     // TODO(b/164998929): ensure this behaviour is correct, should probably calculate upper bound
-    MethodAccessFlags flags = methods.iterator().next().getDefinition().getAccessFlags().copy();
+    MethodAccessFlags flags = methods.iterator().next().getAccessFlags().copy();
+    if (flags.isAbstract()
+        && Iterables.any(methods, method -> !method.getAccessFlags().isAbstract())) {
+      flags.unsetAbstract();
+    }
     if (flags.isFinal() && Iterables.any(methods, method -> !method.getAccessFlags().isFinal())) {
       flags.unsetFinal();
     }
     return flags;
   }
 
+  private DexMethod getNewMethodReference() {
+    return ListUtils.first(methods).getReference().withHolder(target, dexItemFactory);
+  }
+
+  /**
+   * If there is a super method and all methods are abstract, then we can simply remove all abstract
+   * methods.
+   */
+  private boolean isNop() {
+    return superMethod != null
+        && Iterables.all(methods, method -> method.getDefinition().isAbstract());
+  }
+
+  /**
+   * If the method is present on all classes in the merge group, and there is at most one
+   * non-abstract method, then we can simply move that method (or the first abstract method) to the
+   * target class.
+   */
+  private boolean isTrivial() {
+    if (superMethod != null) {
+      return false;
+    }
+    if (methods.size() == 1) {
+      return true;
+    }
+    int numberOfNonAbstractMethods =
+        Iterables.size(Iterables.filter(methods, method -> !method.getDefinition().isAbstract()));
+    return numberOfNonAbstractMethods <= 1;
+  }
+
   /**
    * If there is only a single method that does not override anything then it is safe to just move
    * it to the target type if it is not already in it.
    */
-  public void mergeTrivial(
+  private void mergeTrivial(
       ClassMethodsBuilder classMethodsBuilder, HorizontalClassMergerGraphLens.Builder lensBuilder) {
-    DexEncodedMethod method = methods.iterator().next().getDefinition();
+    DexMethod newMethodReference = getNewMethodReference();
 
-    if (method.getHolderType() != target.type) {
-      // If the method is not in the target type, move it and record it in the lens.
-      DexMethod originalReference = method.getReference();
-      method = method.toRenamedHolderMethod(target.type, dexItemFactory);
-      lensBuilder.moveMethod(originalReference, method.getReference());
+    // Find the first non-abstract method. If all are abstract, then select the first method.
+    ProgramMethod representative =
+        Iterables.find(methods, method -> !method.getDefinition().isAbstract(), null);
+    if (representative == null) {
+      representative = ListUtils.first(methods);
     }
 
-    classMethodsBuilder.addVirtualMethod(method);
+    for (ProgramMethod method : methods) {
+      if (method.getReference() == representative.getReference()) {
+        lensBuilder.moveMethod(method.getReference(), newMethodReference);
+      } else {
+        lensBuilder.mapMethod(method.getReference(), newMethodReference);
+      }
+    }
+
+    if (representative.getHolderType() == target.getType()) {
+      classMethodsBuilder.addVirtualMethod(representative.getDefinition());
+    } else {
+      // If the method is not in the target type, move it.
+      OptionalBool isLibraryMethodOverride =
+          representative.getDefinition().isLibraryMethodOverride();
+      classMethodsBuilder.addVirtualMethod(
+          representative
+              .getDefinition()
+              .toTypeSubstitutedMethod(
+                  newMethodReference,
+                  builder -> builder.setIsLibraryMethodOverrideIfKnown(isLibraryMethodOverride)));
+    }
   }
 
   public void merge(
       ClassMethodsBuilder classMethodsBuilder,
       HorizontalClassMergerGraphLens.Builder lensBuilder,
       FieldAccessInfoCollectionModifier.Builder fieldAccessChangesBuilder,
-      Reference2IntMap classIdentifiers) {
-
+      Reference2IntMap<DexType> classIdentifiers) {
     assert !methods.isEmpty();
 
+    // Handle nop merges.
+    if (isNop()) {
+      return;
+    }
+
     // Handle trivial merges.
-    if (superMethod == null && methods.size() == 1) {
+    if (isTrivial()) {
       mergeTrivial(classMethodsBuilder, lensBuilder);
       return;
     }
 
+
     Int2ReferenceSortedMap<DexMethod> classIdToMethodMap = new Int2ReferenceAVLTreeMap<>();
 
     CfVersion classFileVersion = null;
+    ProgramMethod representative = null;
     for (ProgramMethod method : methods) {
+      if (method.getDefinition().isAbstract()) {
+        continue;
+      }
       if (method.getDefinition().hasClassFileVersion()) {
         CfVersion methodVersion = method.getDefinition().getClassFileVersion();
-        classFileVersion = CfVersion.maxAllowNull(classFileVersion, methodVersion);
+        classFileVersion = Ordered.maxIgnoreNull(classFileVersion, methodVersion);
       }
       DexMethod newMethod = moveMethod(classMethodsBuilder, method);
       lensBuilder.mapMethod(newMethod, newMethod);
       lensBuilder.mapMethodInverse(method.getReference(), newMethod);
       classIdToMethodMap.put(classIdentifiers.getInt(method.getHolderType()), newMethod);
+      if (representative == null) {
+        representative = method;
+      }
     }
 
+    assert representative != null;
+
     // Use the first of the original methods as the original method for the merged constructor.
-    DexMethod templateReference = methods.iterator().next().getReference();
     DexMethod originalMethodReference =
-        appView.graphLens().getOriginalMethodSignature(templateReference);
+        appView.graphLens().getOriginalMethodSignature(representative.getReference());
     DexMethod bridgeMethodReference =
         dexItemFactory.createFreshMethodName(
             originalMethodReference.getName().toSourceString() + "$bridge",
@@ -187,8 +258,7 @@
             originalMethodReference.getHolderType(),
             classMethodsBuilder::isFresh);
 
-    DexMethod newMethodReference =
-        dexItemFactory.createMethod(target.type, templateReference.proto, templateReference.name);
+    DexMethod newMethodReference = getNewMethodReference();
     AbstractSynthesizedCode synthesizedCode =
         new VirtualMethodEntryPointSynthesizedCode(
             classIdToMethodMap,
@@ -206,10 +276,17 @@
             synthesizedCode,
             true,
             classFileVersion);
+    if (!representative.getDefinition().isLibraryMethodOverride().isUnknown()) {
+      newMethod.setLibraryMethodOverride(representative.getDefinition().isLibraryMethodOverride());
+    }
 
-    // Map each old method to the newly synthesized method in the graph lens.
+    // Map each old non-abstract method to the newly synthesized method in the graph lens.
     for (ProgramMethod oldMethod : methods) {
-      lensBuilder.moveMethod(oldMethod.getReference(), newMethodReference);
+      if (oldMethod.getDefinition().isAbstract()) {
+        lensBuilder.mapMethod(oldMethod.getReference(), newMethodReference);
+      } else {
+        lensBuilder.moveMethod(oldMethod.getReference(), newMethodReference);
+      }
     }
     lensBuilder.recordExtraOriginalSignature(bridgeMethodReference, newMethodReference);
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/CheckAbstractClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/CheckAbstractClasses.java
new file mode 100644
index 0000000..4839801
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/CheckAbstractClasses.java
@@ -0,0 +1,50 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.InternalOptions;
+import com.google.common.collect.Lists;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+public class CheckAbstractClasses extends MultiClassPolicy {
+
+  private final InternalOptions options;
+
+  public CheckAbstractClasses(AppView<AppInfoWithLiveness> appView) {
+    this.options = appView.options();
+  }
+
+  @Override
+  public Collection<List<DexProgramClass>> apply(List<DexProgramClass> group) {
+    if (options.canUseAbstractMethodOnNonAbstractClass()) {
+      // We can just make the target class non-abstract if one of the classes in the group
+      // is non-abstract.
+      return Lists.<List<DexProgramClass>>newArrayList(group);
+    }
+    List<DexProgramClass> abstractClasses = new LinkedList<>();
+    List<DexProgramClass> nonAbstractClasses = new LinkedList<>();
+    for (DexProgramClass clazz : group) {
+      if (clazz.isAbstract()) {
+        abstractClasses.add(clazz);
+      } else {
+        nonAbstractClasses.add(clazz);
+      }
+    }
+    List<List<DexProgramClass>> newGroups = new LinkedList<>();
+    if (abstractClasses.size() > 1) {
+      newGroups.add(abstractClasses);
+    }
+    if (nonAbstractClasses.size() > 1) {
+      newGroups.add(nonAbstractClasses);
+    }
+    return newGroups;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoAbstractClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoAbstractClasses.java
deleted file mode 100644
index fdc849d..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoAbstractClasses.java
+++ /dev/null
@@ -1,18 +0,0 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.horizontalclassmerging.policies;
-
-import com.android.tools.r8.graph.DexEncodedMethod;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
-import com.google.common.collect.Iterables;
-
-public class NoAbstractClasses extends SingleClassPolicy {
-  @Override
-  public boolean canMerge(DexProgramClass program) {
-    return !program.isAbstract()
-        && !Iterables.any(program.virtualMethods(), DexEncodedMethod::isAbstract);
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceFields.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceFields.java
deleted file mode 100644
index 326e94a..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoInstanceFields.java
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.horizontalclassmerging.policies;
-
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
-
-public class NoInstanceFields extends SingleClassPolicy {
-  @Override
-  public boolean canMerge(DexProgramClass program) {
-    return !program.hasInstanceFields();
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoKotlinLambdas.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoKotlinLambdas.java
new file mode 100644
index 0000000..a137e88
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoKotlinLambdas.java
@@ -0,0 +1,33 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.SingleClassPolicy;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+public class NoKotlinLambdas extends SingleClassPolicy {
+  private final AppView<AppInfoWithLiveness> appView;
+
+  public NoKotlinLambdas(AppView<AppInfoWithLiveness> appView) {
+    this.appView = appView;
+  }
+
+  @Override
+  public boolean shouldSkipPolicy() {
+    return appView.options().enableHorizontalClassMergingOfKotlinLambdas;
+  }
+
+  @Override
+  public boolean canMerge(DexProgramClass program) {
+    if (program.getKotlinInfo().isNoKotlinInformation()
+        || !program.getKotlinInfo().isSyntheticClass()) {
+      return true;
+    }
+
+    return !program.getKotlinInfo().asSyntheticClass().isLambda();
+  }
+}
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
index 474fcea..245b391 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoOverlappingConstructors.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoOverlappingConstructors.java
@@ -7,7 +7,6 @@
 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;
@@ -35,8 +34,7 @@
 
   private Set<DexProgramClass> sortedClassSet(Collection<DexProgramClass> classes) {
     Set<DexProgramClass> set =
-        new TreeSet<DexProgramClass>(
-            Comparator.comparing(DexProgramClass::getType, DexType::slowCompareTo));
+        new TreeSet<DexProgramClass>(Comparator.comparing(DexProgramClass::getType));
     set.addAll(classes);
     return set;
   }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotMatchedByNoHorizontalClassMerging.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotMatchedByNoHorizontalClassMerging.java
index 5b11ff5..bfe0f2a 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotMatchedByNoHorizontalClassMerging.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NotMatchedByNoHorizontalClassMerging.java
@@ -15,19 +15,19 @@
 
 public class NotMatchedByNoHorizontalClassMerging extends SingleClassPolicy {
 
+  private final AppView<AppInfoWithLiveness> appView;
   private final Set<DexType> deadEnumLiteMaps;
-  private final Set<DexType> neverMergeClassHorizontally;
 
   public NotMatchedByNoHorizontalClassMerging(AppView<AppInfoWithLiveness> appView) {
-    deadEnumLiteMaps =
+    this.appView = appView;
+    this.deadEnumLiteMaps =
         appView.withProtoEnumShrinker(
             EnumLiteProtoShrinker::getDeadEnumLiteMaps, Collections.emptySet());
-    neverMergeClassHorizontally = appView.appInfo().getNoHorizontalClassMergingSet();
   }
 
   @Override
   public boolean canMerge(DexProgramClass program) {
     return !deadEnumLiteMaps.contains(program.getType())
-        && !neverMergeClassHorizontally.contains(program.getType());
+        && !appView.appInfo().isNoHorizontalClassMergingOfType(program.getType());
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventMergeIntoMainDex.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventMergeIntoMainDex.java
index f8f0a3f..783ede5 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventMergeIntoMainDex.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/PreventMergeIntoMainDex.java
@@ -6,48 +6,40 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.horizontalclassmerging.MultiClassPolicy;
+import com.android.tools.r8.horizontalclassmerging.MultiClassSameReferencePolicy;
+import com.android.tools.r8.horizontalclassmerging.policies.PreventMergeIntoMainDex.MainDexClassification;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.MainDexClasses;
 import com.android.tools.r8.shaking.MainDexTracingResult;
-import java.util.Collection;
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
 
-public class PreventMergeIntoMainDex extends MultiClassPolicy {
+public class PreventMergeIntoMainDex extends MultiClassSameReferencePolicy<MainDexClassification> {
   private final MainDexClasses mainDexClasses;
   private final MainDexTracingResult mainDexTracingResult;
 
+  enum MainDexClassification {
+    MAIN_DEX_LIST,
+    MAIN_DEX_ROOT,
+    MAIN_DEX_DEPENDENCY,
+    NOT_IN_MAIN_DEX
+  }
+
   public PreventMergeIntoMainDex(
       AppView<AppInfoWithLiveness> appView, MainDexTracingResult mainDexTracingResult) {
     this.mainDexClasses = appView.appInfo().getMainDexClasses();
     this.mainDexTracingResult = mainDexTracingResult;
   }
 
-  public boolean isMainDexClass(DexProgramClass clazz) {
-    return mainDexClasses.contains(clazz) || mainDexTracingResult.contains(clazz);
-  }
-
   @Override
-  public Collection<List<DexProgramClass>> apply(List<DexProgramClass> group) {
-    List<DexProgramClass> mainDexMembers = new LinkedList<>();
-    Iterator<DexProgramClass> iterator = group.iterator();
-    while (iterator.hasNext()) {
-      DexProgramClass clazz = iterator.next();
-      if (isMainDexClass(clazz)) {
-        iterator.remove();
-        mainDexMembers.add(clazz);
-      }
+  public MainDexClassification getMergeKey(DexProgramClass clazz) {
+    if (mainDexClasses.contains(clazz)) {
+      return MainDexClassification.MAIN_DEX_LIST;
     }
-
-    Collection<List<DexProgramClass>> newGroups = new LinkedList<>();
-    if (!isTrivial(mainDexMembers)) {
-      newGroups.add(mainDexMembers);
+    if (mainDexTracingResult.isRoot(clazz)) {
+      return MainDexClassification.MAIN_DEX_ROOT;
     }
-    if (!isTrivial(group)) {
-      newGroups.add(group);
+    if (mainDexTracingResult.isDependency(clazz)) {
+      return MainDexClassification.MAIN_DEX_DEPENDENCY;
     }
-    return newGroups;
+    return MainDexClassification.NOT_IN_MAIN_DEX;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameFields.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameFields.java
new file mode 100644
index 0000000..63134de
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/SameFields.java
@@ -0,0 +1,17 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.horizontalclassmerging.policies;
+
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.horizontalclassmerging.FieldMultiset;
+import com.android.tools.r8.horizontalclassmerging.MultiClassSameReferencePolicy;
+
+public class SameFields extends MultiClassSameReferencePolicy<FieldMultiset> {
+
+  @Override
+  public FieldMultiset getMergeKey(DexProgramClass clazz) {
+    return new FieldMultiset(clazz);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/ValueMayDependOnEnvironmentAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/ValueMayDependOnEnvironmentAnalysis.java
index 29d38c6..6e46731 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/ValueMayDependOnEnvironmentAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/ValueMayDependOnEnvironmentAnalysis.java
@@ -328,12 +328,7 @@
           }
           if (state.hasTrackedValueEscaped()) {
             DexType holder = staticPut.getField().holder;
-            if (holder.classInitializationMayHaveSideEffects(
-                appView,
-                // Types that are a super type of the current context are guaranteed to be
-                // initialized already.
-                type -> appView.isSubtype(context.getHolderType(), type).isTrue(),
-                Sets.newIdentityHashSet())) {
+            if (holder.classInitializationMayHaveSideEffectsInContext(appView, context)) {
               return true;
             }
           }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeElement.java b/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeElement.java
index edd1b91..eeeea70 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeElement.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/type/ClassTypeElement.java
@@ -134,7 +134,7 @@
     Set<DexType> interfaces = getInterfaces();
     if (interfaces != null) {
       List<DexType> sortedInterfaces = new ArrayList<>(interfaces);
-      sortedInterfaces.sort(DexType::slowCompareTo);
+      sortedInterfaces.sort(DexType::compareTo);
       builder.append(
           sortedInterfaces.stream().map(DexType::toString).collect(Collectors.joining(", ")));
     }
diff --git a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
index 17b49b5..3e0f741 100644
--- a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
+++ b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
@@ -38,6 +38,10 @@
     this.type = type;
   }
 
+  public static Builder builder() {
+    return new Builder();
+  }
+
   @Override
   public int opcode() {
     return Opcodes.CHECK_CAST;
@@ -230,4 +234,30 @@
   public boolean instructionMayTriggerMethodInvocation(AppView<?> appView, ProgramMethod context) {
     return false;
   }
+
+  public static class Builder extends BuilderBase<Builder, CheckCast> {
+
+    private DexType castType;
+    private Value object;
+
+    public Builder setCastType(DexType castType) {
+      this.castType = castType;
+      return this;
+    }
+
+    public Builder setObject(Value object) {
+      this.object = object;
+      return this;
+    }
+
+    @Override
+    public CheckCast build() {
+      return amend(new CheckCast(outValue, object, castType));
+    }
+
+    @Override
+    public Builder self() {
+      return this;
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java b/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
index fde9019..7ec3198 100644
--- a/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/FieldInstruction.java
@@ -20,7 +20,6 @@
 import com.android.tools.r8.ir.analysis.value.SingleFieldValue;
 import com.android.tools.r8.ir.analysis.value.UnknownValue;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.google.common.collect.Sets;
 import java.util.Collections;
 import java.util.List;
 
@@ -109,11 +108,7 @@
         }
       }
       // May trigger <clinit> that may have side effects.
-      if (field.holder.classInitializationMayHaveSideEffects(
-          appView,
-          // Types that are a super type of `context` are guaranteed to be initialized already.
-          type -> appView.isSubtype(context.getHolderType(), type).isTrue(),
-          Sets.newIdentityHashSet())) {
+      if (field.holder.classInitializationMayHaveSideEffectsInContext(appView, context)) {
         return true;
       }
     }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InitClass.java b/src/main/java/com/android/tools/r8/ir/code/InitClass.java
index c878d9a..aceeab3 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InitClass.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InitClass.java
@@ -22,7 +22,6 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.google.common.collect.Sets;
 
 public class InitClass extends Instruction {
 
@@ -111,11 +110,7 @@
         .isPossiblyFalse()) {
       return true;
     }
-    if (clazz.classInitializationMayHaveSideEffects(
-        appView,
-        // Types that are a super type of `context` are guaranteed to be initialized already.
-        type -> appView.isSubtype(context.getHolderType(), type).isTrue(),
-        Sets.newIdentityHashSet())) {
+    if (clazz.classInitializationMayHaveSideEffectsInContext(appView, context)) {
       return true;
     }
     return false;
@@ -132,11 +127,7 @@
     if (appView.enableWholeProgramOptimizations()) {
       // In R8, check if the class initialization of `clazz` or any of its ancestor types may have
       // side effects.
-      return clazz.classInitializationMayHaveSideEffects(
-          appView,
-          // Types that are a super type of `context` are guaranteed to be initialized already.
-          type -> appView.isSubtype(context.getHolderType(), type).isTrue(),
-          Sets.newIdentityHashSet());
+      return clazz.classInitializationMayHaveSideEffectsInContext(appView, context);
     } else {
       // In D8, this instruction may trigger class initialization if `clazz` is different from the
       // current context.
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java b/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
index 9bae360..3139d20 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeStatic.java
@@ -28,7 +28,6 @@
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Sets;
 import java.util.Collections;
 import java.util.List;
 
@@ -209,11 +208,11 @@
       return true;
     }
 
-    DexEncodedMethod singleTarget = resolutionResult.getSingleTarget();
+    DexClassAndMethod singleTarget = resolutionResult.getResolutionPair();
     assert singleTarget != null;
 
     // Verify that the target method is static and accessible.
-    if (!singleTarget.isStatic()
+    if (!singleTarget.getDefinition().isStatic()
         || resolutionResult.isAccessibleFrom(context, appInfoWithLiveness).isPossiblyFalse()) {
       return true;
     }
@@ -223,7 +222,7 @@
       return false;
     }
 
-    if (singleTarget.getOptimizationInfo().mayHaveSideEffects()) {
+    if (singleTarget.getDefinition().getOptimizationInfo().mayHaveSideEffects()) {
       return true;
     }
 
@@ -232,13 +231,8 @@
     }
 
     return singleTarget
-        .holder()
-        .classInitializationMayHaveSideEffects(
-            appView,
-            // Types that are a super type of `context` are guaranteed to be initialized
-            // already.
-            type -> appInfoWithLiveness.isSubtype(context.getHolderType(), type),
-            Sets.newIdentityHashSet());
+        .getHolder()
+        .classInitializationMayHaveSideEffectsInContext(appView, context);
   }
 
   public static class Builder extends BuilderBase<Builder, InvokeStatic> {
diff --git a/src/main/java/com/android/tools/r8/ir/code/NewInstance.java b/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
index 704de30..39f2aee 100644
--- a/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
+++ b/src/main/java/com/android/tools/r8/ir/code/NewInstance.java
@@ -26,7 +26,6 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.google.common.collect.Sets;
 
 public class NewInstance extends Instruction {
 
@@ -176,11 +175,7 @@
     }
 
     // Verify that the new-instance instruction won't lead to class initialization.
-    if (definition.classInitializationMayHaveSideEffects(
-        appView,
-        // Types that are a super type of `context` are guaranteed to be initialized already.
-        type -> appViewWithLiveness.appInfo().isSubtype(context.getHolderType(), type),
-        Sets.newIdentityHashSet())) {
+    if (definition.classInitializationMayHaveSideEffectsInContext(appViewWithLiveness, context)) {
       return true;
     }
 
@@ -213,11 +208,7 @@
     if (appView.enableWholeProgramOptimizations()) {
       // In R8, check if the class initialization of the holder or any of its ancestor types may
       // have side effects.
-      return clazz.classInitializationMayHaveSideEffects(
-          appView,
-          // Types that are a super type of `context` are guaranteed to be initialized already.
-          type -> appView.isSubtype(context.getHolderType(), type).isTrue(),
-          Sets.newIdentityHashSet());
+      return clazz.classInitializationMayHaveSideEffectsInContext(appView, context);
     } else {
       // In D8, this instruction may trigger class initialization if the holder of the field is
       // different from the current context.
diff --git a/src/main/java/com/android/tools/r8/ir/code/Position.java b/src/main/java/com/android/tools/r8/ir/code/Position.java
index 6cc4f93..ab28d55 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Position.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Position.java
@@ -134,9 +134,9 @@
       return 0;
     }
     return Comparator.comparingInt((Position p) -> p.line)
-        .thenComparing(p -> p.file, Comparator.nullsFirst(DexString::slowCompareTo))
+        .thenComparing(p -> p.file, Comparator.nullsFirst(DexString::compareTo))
         .thenComparing(p -> p.synthetic)
-        .thenComparing(p -> p.method, DexMethod::slowCompareTo)
+        .thenComparing(p -> p.method)
         .thenComparing(p -> p.callerPosition, Comparator.nullsFirst(Position::compareTo))
         .compare(this, o);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/StaticGet.java b/src/main/java/com/android/tools/r8/ir/code/StaticGet.java
index 8a3f867..70f0502 100644
--- a/src/main/java/com/android/tools/r8/ir/code/StaticGet.java
+++ b/src/main/java/com/android/tools/r8/ir/code/StaticGet.java
@@ -29,7 +29,6 @@
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.google.common.collect.Sets;
 import java.util.Set;
 
 public class StaticGet extends FieldInstruction implements StaticFieldInstruction {
@@ -237,11 +236,7 @@
     if (appView.enableWholeProgramOptimizations()) {
       // In R8, check if the class initialization of the holder or any of its ancestor types may
       // have side effects.
-      return holder.classInitializationMayHaveSideEffects(
-          appView,
-          // Types that are a super type of `context` are guaranteed to be initialized already.
-          type -> appView.isSubtype(context.getHolderType(), type).isTrue(),
-          Sets.newIdentityHashSet());
+      return holder.classInitializationMayHaveSideEffectsInContext(appView, context);
     } else {
       // In D8, this instruction may trigger class initialization if the holder of the field is
       // different from the current context.
diff --git a/src/main/java/com/android/tools/r8/ir/code/StaticPut.java b/src/main/java/com/android/tools/r8/ir/code/StaticPut.java
index 9df14cc..82d67b0 100644
--- a/src/main/java/com/android/tools/r8/ir/code/StaticPut.java
+++ b/src/main/java/com/android/tools/r8/ir/code/StaticPut.java
@@ -29,7 +29,6 @@
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.ProguardMemberRule;
-import com.google.common.collect.Sets;
 
 public class StaticPut extends FieldInstruction implements StaticFieldInstruction {
 
@@ -232,11 +231,7 @@
     if (appView.enableWholeProgramOptimizations()) {
       // In R8, check if the class initialization of the holder or any of its ancestor types may
       // have side effects.
-      return holder.classInitializationMayHaveSideEffects(
-          appView,
-          // Types that are a super type of `context` are guaranteed to be initialized already.
-          type -> appView.isSubtype(context.getHolderType(), type).isTrue(),
-          Sets.newIdentityHashSet());
+      return holder.classInitializationMayHaveSideEffectsInContext(appView, context);
     } else {
       // In D8, this instruction may trigger class initialization if the holder of the field is
       // different from the current context.
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
index 2084df9..66d7fa4 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/CallGraph.java
@@ -203,9 +203,7 @@
 
     @Override
     public int compareTo(Node other) {
-      return getProgramMethod()
-          .getReference()
-          .slowCompareTo(other.getProgramMethod().getReference());
+      return getProgramMethod().getReference().compareTo(other.getProgramMethod().getReference());
     }
 
     @Override
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 8ba06e0..ec95001 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
@@ -1206,7 +1206,7 @@
     assert !method.isProcessed() || !isDebugMode;
     assert !method.isProcessed()
         || !appView.enableWholeProgramOptimizations()
-        || !appView.appInfo().withLiveness().neverReprocess.contains(method.method);
+        || !appView.appInfo().withLiveness().isNeverReprocessMethod(method.method);
 
     if (lambdaMerger != null) {
       timing.begin("Merge lambdas");
@@ -1769,7 +1769,7 @@
     DexString highestSortingReferencedString = method.getCode().asDexCode().highestSortingString;
     if (highestSortingReferencedString != null) {
       if (highestSortingString == null
-          || highestSortingReferencedString.slowCompareTo(highestSortingString) > 0) {
+          || highestSortingReferencedString.compareTo(highestSortingString) > 0) {
         highestSortingString = highestSortingReferencedString;
       }
     }
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 e1c9063..20cc2c6 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
@@ -379,21 +379,36 @@
             {
               InstanceGet instanceGet = current.asInstanceGet();
               DexField field = instanceGet.getField();
-              DexField actualField = rewriteFieldReference(field, method, graphLens);
+              FieldLookupResult lookup = graphLens.lookupFieldResult(field);
+              DexField rewrittenField = rewriteFieldReference(lookup, method);
               DexMethod replacementMethod =
-                  graphLens.lookupGetFieldForMethod(actualField, method.getReference());
+                  graphLens.lookupGetFieldForMethod(rewrittenField, method.getReference());
+              Value newOutValue = null;
               if (replacementMethod != null) {
-                Value newOutValue = makeOutValue(current, code);
+                newOutValue = makeOutValue(instanceGet, code);
                 iterator.replaceCurrentInstruction(
-                    new InvokeStatic(replacementMethod, newOutValue, current.inValues()));
-                if (newOutValue != null && newOutValue.getType() != current.getOutType()) {
-                  affectedPhis.addAll(current.outValue().uniquePhiUsers());
-                }
-              } else if (actualField != field) {
-                Value newOutValue = makeOutValue(instanceGet, code);
+                    new InvokeStatic(replacementMethod, newOutValue, instanceGet.inValues()));
+              } else if (rewrittenField != field) {
+                newOutValue = makeOutValue(instanceGet, code);
                 iterator.replaceCurrentInstruction(
-                    new InstanceGet(newOutValue, instanceGet.object(), actualField));
-                if (newOutValue != null && newOutValue.getType() != current.getOutType()) {
+                    new InstanceGet(newOutValue, instanceGet.object(), rewrittenField));
+              }
+              if (newOutValue != null) {
+                if (lookup.hasCastType() && newOutValue.hasNonDebugUsers()) {
+                  TypeElement castType =
+                      TypeElement.fromDexType(
+                          lookup.getCastType(), newOutValue.getType().nullability(), appView);
+                  CheckCast checkCast =
+                      CheckCast.builder()
+                          .setCastType(lookup.getCastType())
+                          .setFreshOutValue(code, castType)
+                          .setObject(newOutValue)
+                          .setPosition(instanceGet)
+                          .build();
+                  iterator.add(checkCast);
+                  newOutValue.replaceUsers(checkCast.outValue());
+                  affectedPhis.addAll(checkCast.outValue().uniquePhiUsers());
+                } else if (newOutValue.getType() != instanceGet.getOutType()) {
                   affectedPhis.addAll(newOutValue.uniquePhiUsers());
                 }
               }
@@ -404,19 +419,20 @@
             {
               InstancePut instancePut = current.asInstancePut();
               DexField field = instancePut.getField();
-              DexField actualField = rewriteFieldReference(field, method, graphLens);
+              FieldLookupResult lookup = graphLens.lookupFieldResult(field);
+              DexField rewrittenField = rewriteFieldReference(lookup, method);
               DexMethod replacementMethod =
-                  graphLens.lookupPutFieldForMethod(actualField, method.getReference());
+                  graphLens.lookupPutFieldForMethod(rewrittenField, method.getReference());
               if (replacementMethod != null) {
                 iterator.replaceCurrentInstruction(
-                    new InvokeStatic(replacementMethod, null, current.inValues()));
-              } else if (actualField != field) {
+                    new InvokeStatic(replacementMethod, null, instancePut.inValues()));
+              } else if (rewrittenField != field) {
                 Value rewrittenValue =
                     rewriteValueIfDefault(
-                        code, iterator, field.type, actualField.type, instancePut.value());
+                        code, iterator, field.type, rewrittenField.type, instancePut.value());
                 InstancePut newInstancePut =
                     InstancePut.createPotentiallyInvalid(
-                        actualField, instancePut.object(), rewrittenValue);
+                        rewrittenField, instancePut.object(), rewrittenValue);
                 iterator.replaceCurrentInstruction(newInstancePut);
               }
             }
@@ -426,20 +442,35 @@
             {
               StaticGet staticGet = current.asStaticGet();
               DexField field = staticGet.getField();
-              DexField actualField = rewriteFieldReference(field, method, graphLens);
+              FieldLookupResult lookup = graphLens.lookupFieldResult(field);
+              DexField rewrittenField = rewriteFieldReference(lookup, method);
               DexMethod replacementMethod =
-                  graphLens.lookupGetFieldForMethod(actualField, method.getReference());
+                  graphLens.lookupGetFieldForMethod(rewrittenField, method.getReference());
+              Value newOutValue = null;
               if (replacementMethod != null) {
-                Value newOutValue = makeOutValue(current, code);
+                newOutValue = makeOutValue(staticGet, code);
                 iterator.replaceCurrentInstruction(
-                    new InvokeStatic(replacementMethod, newOutValue, current.inValues()));
-                if (newOutValue != null && newOutValue.getType() != current.getOutType()) {
-                  affectedPhis.addAll(newOutValue.uniquePhiUsers());
-                }
-              } else if (actualField != field) {
-                Value newOutValue = makeOutValue(staticGet, code);
-                iterator.replaceCurrentInstruction(new StaticGet(newOutValue, actualField));
-                if (newOutValue != null && newOutValue.getType() != current.getOutType()) {
+                    new InvokeStatic(replacementMethod, newOutValue, staticGet.inValues()));
+              } else if (rewrittenField != field) {
+                newOutValue = makeOutValue(staticGet, code);
+                iterator.replaceCurrentInstruction(new StaticGet(newOutValue, rewrittenField));
+              }
+              if (newOutValue != null) {
+                if (lookup.hasCastType() && newOutValue.hasNonDebugUsers()) {
+                  TypeElement castType =
+                      TypeElement.fromDexType(
+                          lookup.getCastType(), newOutValue.getType().nullability(), appView);
+                  CheckCast checkCast =
+                      CheckCast.builder()
+                          .setCastType(lookup.getCastType())
+                          .setFreshOutValue(code, castType)
+                          .setObject(newOutValue)
+                          .setPosition(staticGet)
+                          .build();
+                  iterator.add(checkCast);
+                  newOutValue.replaceUsers(checkCast.outValue());
+                  affectedPhis.addAll(checkCast.outValue().uniquePhiUsers());
+                } else if (newOutValue.getType() != staticGet.getOutType()) {
                   affectedPhis.addAll(newOutValue.uniquePhiUsers());
                 }
               }
@@ -450,18 +481,19 @@
             {
               StaticPut staticPut = current.asStaticPut();
               DexField field = staticPut.getField();
-              DexField actualField = rewriteFieldReference(field, method, graphLens);
+              FieldLookupResult lookup = graphLens.lookupFieldResult(field);
+              DexField actualField = rewriteFieldReference(lookup, method);
               DexMethod replacementMethod =
                   graphLens.lookupPutFieldForMethod(actualField, method.getReference());
               if (replacementMethod != null) {
                 iterator.replaceCurrentInstruction(
-                    new InvokeStatic(replacementMethod, current.outValue(), current.inValues()));
+                    new InvokeStatic(
+                        replacementMethod, staticPut.outValue(), staticPut.inValues()));
               } else if (actualField != field) {
                 Value rewrittenValue =
                     rewriteValueIfDefault(
                         code, iterator, field.type, actualField.type, staticPut.value());
-                StaticPut newStaticPut = new StaticPut(rewrittenValue, actualField);
-                iterator.replaceCurrentInstruction(newStaticPut);
+                iterator.replaceCurrentInstruction(new StaticPut(rewrittenValue, actualField));
               }
             }
             break;
@@ -586,16 +618,15 @@
     assert code.hasNoVerticallyMergedClasses(appView);
   }
 
-  private DexField rewriteFieldReference(
-      DexField reference, ProgramMethod context, GraphLens graphLens) {
-    FieldLookupResult lookup = graphLens.lookupFieldResult(reference);
+  private DexField rewriteFieldReference(FieldLookupResult lookup, ProgramMethod context) {
     if (lookup.hasReboundReference()) {
       DexClass holder = appView.definitionFor(lookup.getReboundReference().getHolderType());
       DexEncodedField definition = lookup.getReboundReference().lookupOnClass(holder);
       if (definition != null) {
         DexClassAndField field = DexClassAndField.create(holder, definition);
         if (AccessControl.isMemberAccessible(field, holder, context, appView).isTrue()) {
-          return MemberRebindingAnalysis.validMemberRebindingTargetFor(appView, field, reference);
+          return MemberRebindingAnalysis.validMemberRebindingTargetFor(
+              appView, field, lookup.getReference());
         }
       }
     }
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriterUtils.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriterUtils.java
index 4b44ada..ae5e3f9 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriterUtils.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriterUtils.java
@@ -33,20 +33,28 @@
 
 public class LensCodeRewriterUtils {
 
+  private final AppView<?> appView;
   private final DexDefinitionSupplier definitions;
   private final GraphLens graphLens;
 
   private final Map<DexProto, DexProto> protoFixupCache = new ConcurrentHashMap<>();
 
   public LensCodeRewriterUtils(AppView<?> appView) {
-    this(appView, appView.graphLens());
+    this.appView = appView;
+    this.definitions = appView;
+    this.graphLens = null;
   }
 
   public LensCodeRewriterUtils(DexDefinitionSupplier definitions, GraphLens graphLens) {
+    this.appView = null;
     this.definitions = definitions;
     this.graphLens = graphLens;
   }
 
+  private GraphLens graphLens() {
+    return appView != null ? appView.graphLens() : graphLens;
+  }
+
   public DexCallSite rewriteCallSite(DexCallSite callSite, ProgramMethod context) {
     DexItemFactory dexItemFactory = definitions.dexItemFactory();
     DexProto newMethodProto = rewriteProto(callSite.methodProto);
@@ -74,7 +82,7 @@
       DexMethod invokedMethod = methodHandle.asMethod();
       MethodHandleType oldType = methodHandle.type;
       MethodLookupResult lensLookup =
-          graphLens.lookupMethod(invokedMethod, context.getReference(), oldType.toInvokeType());
+          graphLens().lookupMethod(invokedMethod, context.getReference(), oldType.toInvokeType());
       DexMethod rewrittenTarget = lensLookup.getReference();
       DexMethod actualTarget;
       MethodHandleType newType;
@@ -115,7 +123,7 @@
       }
     } else {
       DexField field = methodHandle.asField();
-      DexField actualField = graphLens.lookupField(field);
+      DexField actualField = graphLens().lookupField(field);
       if (actualField != field) {
         return new DexMethodHandle(methodHandle.type, actualField, methodHandle.isInterface);
       }
@@ -158,7 +166,7 @@
         return rewriteDexMethodType(value.asDexValueMethodType());
       case TYPE:
         DexType oldType = value.asDexValueType().value;
-        DexType newType = graphLens.lookupType(oldType);
+        DexType newType = graphLens().lookupType(oldType);
         return newType != oldType ? new DexValueType(newType) : value;
       default:
         return value;
@@ -168,7 +176,7 @@
   public DexProto rewriteProto(DexProto proto) {
     return definitions
         .dexItemFactory()
-        .applyClassMappingToProto(proto, graphLens::lookupType, protoFixupCache);
+        .applyClassMappingToProto(proto, graphLens()::lookupType, protoFixupCache);
   }
 
   private DexValueMethodHandle rewriteDexValueMethodHandle(
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java b/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
index 48aaf3a..3d86936 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/PostMethodProcessor.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.ProgramMethod;
@@ -28,6 +29,7 @@
 import java.util.IdentityHashMap;
 import java.util.LinkedHashSet;
 import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
 
@@ -122,20 +124,17 @@
     PostMethodProcessor build(
         AppView<AppInfoWithLiveness> appView, ExecutorService executorService, Timing timing)
         throws ExecutionException {
-      if (!appView.appInfo().reprocess.isEmpty()) {
+      Set<DexMethod> reprocessMethods = appView.appInfo().getReprocessMethods();
+      if (!reprocessMethods.isEmpty()) {
         ProgramMethodSet set = ProgramMethodSet.create();
-        appView
-            .appInfo()
-            .reprocess
-            .forEach(
-                reference -> {
-                  DexProgramClass clazz =
-                      asProgramClassOrNull(appView.definitionForHolder(reference));
-                  DexEncodedMethod definition = reference.lookupOnClass(clazz);
-                  if (definition != null) {
-                    set.createAndAdd(clazz, definition);
-                  }
-                });
+        reprocessMethods.forEach(
+            reference -> {
+              DexProgramClass clazz = asProgramClassOrNull(appView.definitionForHolder(reference));
+              DexEncodedMethod definition = reference.lookupOnClass(clazz);
+              if (definition != null) {
+                set.createAndAdd(clazz, definition);
+              }
+            });
         put(set);
       }
       if (methodsToReprocess.isEmpty()) {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java b/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
index 758830d..0884ab5 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/ClassProcessor.java
@@ -423,12 +423,32 @@
     }
     // We need to introduce them in deterministic order for deterministic compilation.
     ArrayList<DexType> sortedEmulatedInterfaces = new ArrayList<>(emulatedInterfaces);
-    Collections.sort(sortedEmulatedInterfaces, DexType::slowCompareTo);
+    Collections.sort(sortedEmulatedInterfaces);
     List<GenericSignature.ClassTypeSignature> extraInterfaceSignatures = new ArrayList<>();
     for (DexType extraInterface : sortedEmulatedInterfaces) {
       extraInterfaceSignatures.add(
           new GenericSignature.ClassTypeSignature(rewriter.getEmulatedInterface(extraInterface)));
     }
+    // The emulated interface might already be implemented if the input class has gone through
+    // library desugaring already.
+    clazz
+        .getInterfaces()
+        .forEach(
+            iface -> {
+              for (int i = 0; i < extraInterfaceSignatures.size(); i++) {
+                if (extraInterfaceSignatures.get(i).type() == iface) {
+                  if (!appView.options().desugarSpecificOptions().allowDesugaredInput) {
+                    throw new CompilationError(
+                        "Code has already been library desugared. Interface "
+                            + iface.getDescriptor()
+                            + " is already implemented by "
+                            + clazz.getType().getDescriptor());
+                  }
+                  extraInterfaceSignatures.remove(i);
+                  break;
+                }
+              }
+            });
     clazz.asProgramClass().addExtraInterfaces(extraInterfaceSignatures);
   }
 
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeter.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeter.java
index 904ca7e..214c17a 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryRetargeter.java
@@ -180,7 +180,7 @@
                   .computeIfAbsent(
                       newClass,
                       ignore ->
-                          new TreeSet<>((x, y) -> x.getReference().slowCompareTo(y.getReference())))
+                          new TreeSet<>((x, y) -> x.getReference().compareTo(y.getReference())))
                   .add(
                       new DexEncodedMethod(
                           retargetMethod,
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 d6f6264..ddcb3c7 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
@@ -588,7 +588,7 @@
     Map<DexType, List<DexType>> emulatedInterfacesHierarchy = new IdentityHashMap<>();
     Set<DexType> processed = Sets.newIdentityHashSet();
     ArrayList<DexType> emulatedInterfacesSorted = new ArrayList<>(emulatedInterfaces.keySet());
-    emulatedInterfacesSorted.sort(DexType::slowCompareTo);
+    emulatedInterfacesSorted.sort(DexType::compareTo);
     for (DexType interfaceType : emulatedInterfacesSorted) {
       processEmulatedInterfaceHierarchy(interfaceType, processed, emulatedInterfacesHierarchy);
     }
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 fbeb14a..02388be 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
@@ -33,7 +33,7 @@
 import com.android.tools.r8.graph.DexProgramClass.ChecksumSupplier;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexTypeList;
-import com.android.tools.r8.graph.DexValue.DexValueNull;
+import com.android.tools.r8.graph.DexValue.DexValueInt;
 import com.android.tools.r8.graph.FieldAccessFlags;
 import com.android.tools.r8.graph.GenericSignature.ClassSignature;
 import com.android.tools.r8.graph.GenericSignature.FieldTypeSignature;
@@ -179,13 +179,13 @@
         dexItemFactory.createField(iface.getType(), dexItemFactory.intType, "$desugar$clinit");
     DexField clinitFieldReference =
         dexItemFactory.createFreshFieldName(
-            clinitFieldTemplateReference, candidate -> iface.lookupField(candidate) != null);
+            clinitFieldTemplateReference, candidate -> iface.lookupField(candidate) == null);
     return new DexEncodedField(
         clinitFieldReference,
         FieldAccessFlags.builder().setPackagePrivate().setStatic().setSynthetic().build(),
         FieldTypeSignature.noSignature(),
         DexAnnotationSet.empty(),
-        DexValueNull.NULL);
+        DexValueInt.DEFAULT);
   }
 
   private DexEncodedMethod createCompanionClassInitializer(
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
index 11cff1a..9688ae8 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaRewriter.java
@@ -111,7 +111,7 @@
       throws ExecutionException {
     SortedProgramMethodSet nonDexAccessibilityBridges = SortedProgramMethodSet.create();
     List<LambdaClass> sortedLambdaClasses = new ArrayList<>(lambdaClasses);
-    sortedLambdaClasses.sort((x, y) -> x.type.slowCompareTo(y.type));
+    sortedLambdaClasses.sort((x, y) -> x.type.compareTo(y.type));
     for (LambdaClass lambdaClass : sortedLambdaClasses) {
       // This call may cause originalMethodSignatures to be updated.
       ProgramMethod accessibilityBridge = lambdaClass.target.ensureAccessibilityIfNeeded(true);
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
index 0fc9b80..8a8d2d4 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -380,7 +380,7 @@
     //
     // For simplicity, we are conservative and consider all interfaces, not only the ones with
     // default methods.
-    if (!target.getHolder().classInitializationMayHaveSideEffects(appView)) {
+    if (!target.getHolder().classInitializationMayHaveSideEffectsInContext(appView, context)) {
       return true;
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
index 945d9fe..3abbf9b 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Inliner.java
@@ -707,7 +707,10 @@
         assert !monitorEnterBlock.hasCatchHandlers();
 
         InstructionListIterator monitorEnterBlockIterator = monitorEnterBlock.listIterator(code);
-        monitorEnterBlockIterator.setInsertionPosition(Position.syntheticNone());
+        // MonitorEnter will only throw an NPE if the lock is null and that can only happen if the
+        // receiver was null. To preserve NPE's at call-sites for synchronized methods we therefore
+        // put in the invoke-position.
+        monitorEnterBlockIterator.setInsertionPosition(invoke.getPosition());
 
         // If this is a static method, then the class object will act as the lock, so we load it
         // using a const-class instruction.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/InliningConstraints.java b/src/main/java/com/android/tools/r8/ir/optimize/InliningConstraints.java
index ff7668a..1941743 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/InliningConstraints.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/InliningConstraints.java
@@ -23,6 +23,7 @@
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
 import com.android.tools.r8.ir.optimize.Inliner.ConstraintWithTarget;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.shaking.VerticalClassMerger.SingleTypeMapperGraphLens;
 import com.android.tools.r8.utils.TriFunction;
 
 // Computes the inlining constraint for a given instruction.
@@ -62,7 +63,7 @@
   }
 
   private boolean isVerticalClassMerging() {
-    return !graphLens.isIdentityLens();
+    return graphLens instanceof SingleTypeMapperGraphLens;
   }
 
   public ConstraintWithTarget forAlwaysMaterializingUser() {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
index 202caa1..91f7642 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
@@ -456,14 +456,7 @@
         appView, context, Instruction.SideEffectAssumption.CLASS_ALREADY_INITIALIZED)) {
       return;
     }
-    boolean classInitializationMayHaveSideEffects =
-        holder.classInitializationMayHaveSideEffects(
-            appView,
-            // Types that are a super type of `context` are guaranteed to be initialized
-            // already.
-            type -> appView.appInfo().isSubtype(context.getHolderType(), type),
-            Sets.newIdentityHashSet());
-    if (!classInitializationMayHaveSideEffects) {
+    if (!holder.classInitializationMayHaveSideEffectsInContext(appView, context)) {
       iterator.removeOrReplaceByDebugLocalRead();
       return;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java b/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
index 334238d..508e0f4 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Outliner.java
@@ -337,7 +337,7 @@
         return super.compareTo(other);
       }
       NewInstanceOutlineInstruction o = (NewInstanceOutlineInstruction) other;
-      return clazz.slowCompareTo(o.clazz);
+      return clazz.compareTo(o.clazz);
     }
 
     @Override
@@ -435,7 +435,7 @@
         return super.compareTo(other);
       }
       InvokeOutlineInstruction o = (InvokeOutlineInstruction) other;
-      int result = method.slowCompareTo(o.method);
+      int result = method.compareTo(o.method);
       if (result != 0) {
         return result;
       }
@@ -448,7 +448,7 @@
         return result;
       }
       if (proto != null) {
-        result = proto.slowCompareTo(o.proto);
+        result = proto.compareTo(o.proto);
         if (result != 0) {
           return result;
         }
@@ -630,7 +630,7 @@
       }
       // First compare the proto.
       int result;
-      result = buildProto().slowCompareTo(other.buildProto());
+      result = buildProto().compareTo(other.buildProto());
       if (result != 0) {
         assert !equals(other);
         return result;
@@ -1262,8 +1262,9 @@
     private boolean removeMethodFromOutlineList(Outline outline) {
       synchronized (outlineSites) {
         assert ListUtils.removeFirstMatch(
-            outlineSites.get(outline),
-            element -> element.getDefinition() == method.getDefinition());
+                outlineSites.get(outline),
+                element -> element.getDefinition() == method.getDefinition())
+            .isPresent();
       }
       return true;
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java
index b2ab54f..868bcc5 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadElimination.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.graph.FieldResolutionResult.SuccessfulFieldResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
+import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
 import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.value.SingleValue;
 import com.android.tools.r8.ir.code.BasicBlock;
@@ -282,12 +283,7 @@
             killAllNonFinalActiveFields();
           } else if (instruction.isNewInstance()) {
             NewInstance newInstance = instruction.asNewInstance();
-            if (newInstance.clazz.classInitializationMayHaveSideEffects(
-                appView,
-                // Types that are a super type of `context` are guaranteed to be initialized
-                // already.
-                type -> appView.isSubtype(method.getHolderType(), type).isTrue(),
-                Sets.newIdentityHashSet())) {
+            if (newInstance.clazz.classInitializationMayHaveSideEffectsInContext(appView, method)) {
               killAllNonFinalActiveFields();
             }
           } else {
@@ -345,8 +341,11 @@
 
   private boolean verifyWasInstanceInitializer() {
     VerticallyMergedClasses verticallyMergedClasses = appView.verticallyMergedClasses();
+    HorizontallyMergedClasses horizontallyMergedClasses = appView.horizontallyMergedClasses();
     assert verticallyMergedClasses != null;
-    assert verticallyMergedClasses.isTarget(method.getHolderType());
+    assert horizontallyMergedClasses != null;
+    assert verticallyMergedClasses.isTarget(method.getHolderType())
+        || horizontallyMergedClasses.isMergeTarget(method.getHolderType());
     assert appView
         .dexItemFactory()
         .isConstructor(appView.graphLens().getOriginalMethodSignature(method.getReference()));
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java b/src/main/java/com/android/tools/r8/ir/optimize/UninstantiatedTypeOptimization.java
index 96e5e9c..c11c8bf 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
@@ -305,7 +305,7 @@
   private RewrittenPrototypeDescription getPrototypeChanges(
       DexEncodedMethod encodedMethod, Strategy strategy) {
     if (ArgumentRemovalUtils.isPinned(encodedMethod, appView)
-        || appView.appInfo().keepConstantArguments.contains(encodedMethod.method)) {
+        || appView.appInfo().isKeepConstantArgumentsMethod(encodedMethod.method)) {
       return RewrittenPrototypeDescription.none();
     }
     return RewrittenPrototypeDescription.createForUninstantiatedTypes(
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 509daa1..6270034 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
@@ -284,7 +284,7 @@
   private ArgumentInfoCollection collectUnusedArguments(
       DexEncodedMethod method, MemberPool<DexMethod> methodPool) {
     if (ArgumentRemovalUtils.isPinned(method, appView)
-        || appView.appInfo().keepUnusedArguments.contains(method.method)) {
+        || appView.appInfo().isKeepUnusedArgumentsMethod(method.method)) {
       return null;
     }
     // Only process classfile code objects.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
index dab89c6..5d79d0e 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
@@ -303,7 +303,7 @@
     }
 
     // Check for static initializers in this class or any of interfaces it implements.
-    if (clazz.initializationOfParentTypesMayHaveSideEffects(appView)) {
+    if (clazz.classInitializationMayHaveSideEffects(appView)) {
       return EligibilityStatus.NOT_ELIGIBLE;
     }
     return EligibilityStatus.ELIGIBLE;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
index 6b0b61d..550a0e1 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.FieldResolutionResult.SuccessfulFieldResolutionResult;
 import com.android.tools.r8.graph.LibraryMethod;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.ResolutionResult;
@@ -165,11 +166,10 @@
       if (eligibleClass == null) {
         return EligibilityStatus.NOT_ELIGIBLE;
       }
-      if (eligibleClass.classInitializationMayHaveSideEffects(
-          appView,
-          // Types that are a super type of the current context are guaranteed to be initialized.
-          type -> appView.isSubtype(method.getHolderType(), type).isTrue(),
-          Sets.newIdentityHashSet())) {
+      if (method.getHolder() == eligibleClass) {
+        return EligibilityStatus.NOT_ELIGIBLE;
+      }
+      if (eligibleClass.classInitializationMayHaveSideEffectsInContext(appView, method)) {
         return EligibilityStatus.NOT_ELIGIBLE;
       }
       return EligibilityStatus.ELIGIBLE;
@@ -178,10 +178,18 @@
     assert root.isStaticGet();
 
     StaticGet staticGet = root.asStaticGet();
+    SuccessfulFieldResolutionResult fieldResolutionResult =
+        appView.appInfo().resolveField(staticGet.getField()).asSuccessfulResolution();
+    if (fieldResolutionResult == null) {
+      return EligibilityStatus.NOT_ELIGIBLE;
+    }
+    if (method.getHolder() == fieldResolutionResult.getResolvedHolder()) {
+      return EligibilityStatus.NOT_ELIGIBLE;
+    }
     if (staticGet.instructionMayHaveSideEffects(appView, method)) {
       return EligibilityStatus.NOT_ELIGIBLE;
     }
-    DexEncodedField field = appView.appInfo().resolveField(staticGet.getField()).getResolvedField();
+    DexEncodedField field = fieldResolutionResult.getResolvedField();
     FieldOptimizationInfo optimizationInfo = field.getOptimizationInfo();
     ClassTypeElement dynamicLowerBoundType = optimizationInfo.getDynamicLowerBoundType();
     if (dynamicLowerBoundType == null
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 27c125a..bfb520d 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
@@ -584,7 +584,7 @@
     }
     // We make the order deterministic.
     for (List<T> value : encodedMembersMap.values()) {
-      value.sort((m1, m2) -> m1.getReference().slowCompareTo(m2.getReference()));
+      value.sort((m1, m2) -> m1.getReference().compareTo(m2.getReference()));
     }
     return encodedMembersMap;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
index ebe815d..f1c2bdd 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueOptimizer.java
@@ -13,10 +13,10 @@
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
-import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.EnumValueInfoMapCollection.EnumValueInfo;
 import com.android.tools.r8.graph.EnumValueInfoMapCollection.EnumValueInfoMap;
+import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.analysis.type.TypeAnalysis;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
@@ -38,6 +38,7 @@
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.optimize.SwitchMapCollector;
+import com.android.tools.r8.ir.optimize.info.FieldOptimizationInfo;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.ArrayUtils;
 import com.google.common.collect.Sets;
@@ -100,7 +101,8 @@
           continue;
         }
 
-        AbstractValue abstractValue = definition.getOptimizationInfo().getAbstractValue();
+        FieldOptimizationInfo optimizationInfo = definition.getOptimizationInfo();
+        AbstractValue abstractValue = optimizationInfo.getAbstractValue();
         if (!abstractValue.isSingleFieldValue()) {
           continue;
         }
@@ -158,15 +160,18 @@
           continue;
         }
 
-        EnumValueInfo valueInfo = appView.appInfo().withLiveness().getEnumValueInfo(field);
-        if (valueInfo == null) {
+        // Since the value is a single field value, the type should be exact.
+        assert abstractValue.isSingleFieldValue();
+        ClassTypeElement enumFieldType = optimizationInfo.getExactClassType(appView);
+        if (enumFieldType == null) {
+          assert false : "Expected to have an exact dynamic type for enum instance";
           continue;
         }
 
         DexEncodedMethod singleTarget =
             appView
                 .appInfo()
-                .resolveMethodOnClass(factory.objectMembers.toString, valueInfo.type)
+                .resolveMethodOnClass(factory.objectMembers.toString, enumFieldType.getClassType())
                 .getSingleTarget();
         if (singleTarget != null && singleTarget.method != factory.enumMembers.toString) {
           continue;
@@ -180,22 +185,6 @@
                 nameValue.getDexString(),
                 ThrowingInfo.defaultForConstString(appView.options())));
         newValue.addAffectedValuesTo(affectedValues);
-      } else if (current.isArrayLength()) {
-        // Rewrites MyEnum.values().length to a constant int.
-        Instruction arrayDefinition = current.asArrayLength().array().getAliasedValue().definition;
-        if (arrayDefinition != null && arrayDefinition.isInvokeStatic()) {
-          DexMethod invokedMethod = arrayDefinition.asInvokeStatic().getInvokedMethod();
-          DexProgramClass enumClass = appView.definitionForProgramType(invokedMethod.holder);
-          if (enumClass != null
-              && enumClass.isEnum()
-              && factory.enumMembers.isValuesMethod(invokedMethod, enumClass)) {
-            EnumValueInfoMap enumValueInfoMap =
-                appView.appInfo().withLiveness().getEnumValueInfoMap(invokedMethod.holder);
-            if (enumValueInfoMap != null) {
-              iterator.replaceCurrentInstructionWithConstInt(code, enumValueInfoMap.size());
-            }
-          }
-        }
       }
     }
     if (!affectedValues.isEmpty()) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/UnboxedEnumMemberRelocator.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/UnboxedEnumMemberRelocator.java
index c203772..a0ea10c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/UnboxedEnumMemberRelocator.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/UnboxedEnumMemberRelocator.java
@@ -143,7 +143,7 @@
       for (DexProgramClass context : contexts) {
         if (deterministicContext == null) {
           deterministicContext = context.type;
-        } else if (context.type.slowCompareTo(deterministicContext) < 0) {
+        } else if (context.type.compareTo(deterministicContext) < 0) {
           deterministicContext = context.type;
         }
       }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/FieldOptimizationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/FieldOptimizationInfo.java
index 0365122..e70a91f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/FieldOptimizationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/FieldOptimizationInfo.java
@@ -4,9 +4,13 @@
 
 package com.android.tools.r8.ir.optimize.info;
 
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.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.shaking.AppInfoWithLiveness;
 
 public abstract class FieldOptimizationInfo {
 
@@ -26,6 +30,24 @@
 
   public abstract TypeElement getDynamicUpperBoundType();
 
+  public ClassTypeElement getExactClassType(AppView<AppInfoWithLiveness> appView) {
+    ClassTypeElement dynamicLowerBoundType = getDynamicLowerBoundType();
+    TypeElement dynamicUpperBoundType = getDynamicUpperBoundType();
+    if (dynamicUpperBoundType == null || !dynamicUpperBoundType.isClassType()) {
+      return null;
+    }
+    DexType upperType = dynamicUpperBoundType.asClassType().getClassType();
+    if (dynamicLowerBoundType != null && upperType == dynamicLowerBoundType.getClassType()) {
+      return dynamicLowerBoundType;
+    }
+    DexClass upperClass = appView.definitionFor(upperType);
+    if (upperClass != null && upperClass.isEffectivelyFinal(appView)) {
+      assert dynamicLowerBoundType == null;
+      return ClassTypeElement.create(upperType, dynamicUpperBoundType.nullability(), appView);
+    }
+    return null;
+  }
+
   public final TypeElement getDynamicUpperBoundTypeOrElse(TypeElement orElse) {
     TypeElement dynamicUpperBoundType = getDynamicUpperBoundType();
     return dynamicUpperBoundType != null ? dynamicUpperBoundType : orElse;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/field/InstanceFieldInitializationInfoCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/info/field/InstanceFieldInitializationInfoCollection.java
index 775d7e9..9fa412a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/field/InstanceFieldInitializationInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/field/InstanceFieldInitializationInfoCollection.java
@@ -43,8 +43,7 @@
 
   public static class Builder {
 
-    TreeMap<DexField, InstanceFieldInitializationInfo> infos =
-        new TreeMap<>(DexField::slowCompareTo);
+    TreeMap<DexField, InstanceFieldInitializationInfo> infos = new TreeMap<>(DexField::compareTo);
 
     public void recordInitializationInfo(
         DexEncodedField field, InstanceFieldInitializationInfo info) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java
index b3e72bf..81c6a35 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/inliner/WhyAreYouNotInliningReporter.java
@@ -19,7 +19,7 @@
 
   public static WhyAreYouNotInliningReporter createFor(
       ProgramMethod callee, AppView<AppInfoWithLiveness> appView, ProgramMethod context) {
-    if (appView.appInfo().whyAreYouNotInlining.contains(callee.getReference())) {
+    if (appView.appInfo().isWhyAreYouNotInliningMethod(callee.getReference())) {
       return new WhyAreYouNotInliningReporterImpl(
           callee, context, appView.options().testing.whyAreYouNotInliningConsumer);
     }
@@ -28,7 +28,7 @@
 
   public static void handleInvokeWithUnknownTarget(
       InvokeMethod invoke, AppView<AppInfoWithLiveness> appView, ProgramMethod context) {
-    if (appView.appInfo().whyAreYouNotInlining.isEmpty()) {
+    if (appView.appInfo().hasNoWhyAreYouNotInliningMethods()) {
       return;
     }
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java
index 8aacbde..89ffafd 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/lambda/LambdaMerger.java
@@ -62,6 +62,7 @@
 import com.google.common.collect.Sets;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
+import java.util.Comparator;
 import java.util.Deque;
 import java.util.IdentityHashMap;
 import java.util.Iterator;
@@ -257,7 +258,7 @@
                 appView.testing().kotlinLambdaMergerFactoryForClass.apply(cls) != null
                     && KotlinLambdaGroupIdFactory.hasValidAnnotations(kotlin, cls)
                     && !appView.appInfo().getClassToFeatureSplitMap().isInFeature(cls))
-        .sorted((a, b) -> a.type.slowCompareTo(b.type)) // Ensure stable ordering.
+        .sorted(Comparator.comparing(DexClass::getType)) // Ensure stable ordering.
         .forEachOrdered(
             lambda -> {
               try {
@@ -268,14 +269,7 @@
                 group.add(lambda);
                 lambdas.put(lambda.type, group);
               } catch (LambdaStructureError error) {
-                if (error.reportable) {
-                  reporter.info(
-                      new StringDiagnostic(
-                          "Unrecognized Kotlin lambda ["
-                              + lambda.type.toSourceString()
-                              + "]: "
-                              + error.getMessage()));
-                }
+                // Intentionally empty.
               }
             });
 
@@ -395,8 +389,7 @@
     rewriteLambdaReferences(converter, executorService, appliedGraphLens);
     this.mode = null;
 
-    appView.setHorizontallyMergedLambdaClasses(
-        new HorizontallyMergedLambdaClasses(lambdas.keySet()));
+    appView.setHorizontallyMergedLambdaClasses(new HorizontallyMergedLambdaClasses(lambdas));
   }
 
   private void analyzeLambdaClassesStructure(ExecutorService service) throws ExecutionException {
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 0179328..97ef2dc 100644
--- a/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
+++ b/src/main/java/com/android/tools/r8/jar/CfApplicationWriter.java
@@ -46,6 +46,7 @@
 import com.android.tools.r8.utils.ExceptionUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.PredicateUtils;
+import com.android.tools.r8.utils.structural.Ordered;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableMap.Builder;
 import com.google.common.collect.Sets;
@@ -173,7 +174,7 @@
     String sourceDebug = getSourceDebugExtension(clazz.annotations());
     writer.visitSource(clazz.sourceFile != null ? clazz.sourceFile.toString() : null, sourceDebug);
     CfVersion version = getClassFileVersion(clazz);
-    if (version.isGreaterThanOrEqual(CfVersion.V1_8)) {
+    if (version.isGreaterThanOrEqualTo(CfVersion.V1_8)) {
       // JDK8 and after ignore ACC_SUPER so unset it.
       clazz.accessFlags.unsetSuper();
     } else {
@@ -182,7 +183,10 @@
         clazz.accessFlags.setSuper();
       }
     }
-    int access = clazz.accessFlags.getAsCfAccessFlags();
+    int access =
+        options.testing.allowInvalidCfAccessFlags
+            ? clazz.accessFlags.materialize()
+            : clazz.accessFlags.getAsCfAccessFlags();
     if (clazz.isDeprecated()) {
       access = AsmUtils.withDeprecated(access);
     }
@@ -227,8 +231,7 @@
     }
     if (options.desugarSpecificOptions().sortMethodsOnCfOutput) {
       SortedSet<ProgramMethod> programMethodSortedSet =
-          Sets.newTreeSet(
-              (a, b) -> a.getDefinition().method.slowCompareTo(b.getDefinition().method));
+          Sets.newTreeSet((a, b) -> a.getDefinition().method.compareTo(b.getDefinition().method));
       clazz.forEachProgramMethod(programMethodSortedSet::add);
       programMethodSortedSet.forEach(
           method -> writeMethod(method, version, rewriter, writer, defaults));
@@ -257,8 +260,9 @@
       // In this case bridges have been introduced for the Cf back-end,
       // which do not have class file version.
       assert options.testing.enableForceNestBasedAccessDesugaringForTest
-          || options.isDesugaredLibraryCompilation()
-          || options.cfToCfDesugar;
+              || options.isDesugaredLibraryCompilation()
+              || options.cfToCfDesugar
+          : "Expected class file version for " + method.method.toSourceString();
       // TODO(b/146424042): We may call static methods on interface classes so we have to go for
       //  Java 8.
       assert MIN_VERSION_FOR_COMPILER_GENERATED_CODE.isLessThan(CfVersion.V1_8);
@@ -273,10 +277,10 @@
             ? clazz.getInitialClassFileVersion()
             : MIN_VERSION_FOR_COMPILER_GENERATED_CODE;
     for (DexEncodedMethod method : clazz.directMethods()) {
-      version = version.max(getClassFileVersion(method));
+      version = Ordered.max(version, getClassFileVersion(method));
     }
     for (DexEncodedMethod method : clazz.virtualMethods()) {
-      version = version.max(getClassFileVersion(method));
+      version = Ordered.max(version, getClassFileVersion(method));
     }
     return version;
   }
diff --git a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataEnqueuerExtension.java b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataEnqueuerExtension.java
index 5327873..189a6b0 100644
--- a/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataEnqueuerExtension.java
+++ b/src/main/java/com/android/tools/r8/kotlin/KotlinMetadataEnqueuerExtension.java
@@ -58,6 +58,10 @@
                     appView.options().reporter,
                     onlyProcessLambdas,
                     method -> keepByteCodeFunctions.add(method.method)));
+            if (onlyProcessLambdas) {
+              clazz.removeAnnotations(
+                  annotation -> annotation.getAnnotationType() == kotlinMetadataType);
+            }
             if (clazz.getEnclosingMethodAttribute() != null
                 && clazz.getEnclosingMethodAttribute().getEnclosingMethod() != null) {
               localOrAnonymousClasses.add(clazz);
diff --git a/src/main/java/com/android/tools/r8/naming/FieldNameMinifier.java b/src/main/java/com/android/tools/r8/naming/FieldNameMinifier.java
index b014c4f..55ed725 100644
--- a/src/main/java/com/android/tools/r8/naming/FieldNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/FieldNameMinifier.java
@@ -301,7 +301,7 @@
     }
 
     private Set<DexClass> buildSortedPartition(DexClass src) {
-      Set<DexClass> partition = new TreeSet<>((x, y) -> x.type.slowCompareTo(y.type));
+      Set<DexClass> partition = new TreeSet<>((x, y) -> x.type.compareTo(y.type));
 
       Deque<DexType> worklist = new ArrayDeque<>();
       worklist.add(src.type);
diff --git a/src/main/java/com/android/tools/r8/naming/InterfaceMethodNameMinifier.java b/src/main/java/com/android/tools/r8/naming/InterfaceMethodNameMinifier.java
index e6fcce6..86dac13 100644
--- a/src/main/java/com/android/tools/r8/naming/InterfaceMethodNameMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/InterfaceMethodNameMinifier.java
@@ -264,7 +264,7 @@
       // reservation, we have to prioritize that over the others, otherwise we just propose the
       // first ordered reserved name since we do not allow overwriting the name.
       List<DexEncodedMethod> sortedMethods = Lists.newArrayList(methodStates.keySet());
-      sortedMethods.sort((x, y) -> x.getReference().slowCompareTo(y.getReference()));
+      sortedMethods.sort((x, y) -> x.getReference().compareTo(y.getReference()));
       DexString reservedName = null;
       for (DexEncodedMethod method : sortedMethods) {
         for (InterfaceReservationState state : methodStates.get(method)) {
diff --git a/src/main/java/com/android/tools/r8/naming/Minifier.java b/src/main/java/com/android/tools/r8/naming/Minifier.java
index b886f2b..9893fb7 100644
--- a/src/main/java/com/android/tools/r8/naming/Minifier.java
+++ b/src/main/java/com/android/tools/r8/naming/Minifier.java
@@ -47,7 +47,7 @@
     assert appView.options().isMinifying();
     SubtypingInfo subtypingInfo = appView.appInfo().computeSubtypingInfo();
     timing.begin("ComputeInterfaces");
-    Set<DexClass> interfaces = new TreeSet<>((a, b) -> a.type.slowCompareTo(b.type));
+    Set<DexClass> interfaces = new TreeSet<>((a, b) -> a.type.compareTo(b.type));
     interfaces.addAll(appView.appInfo().computeReachableInterfaces());
     timing.end();
     timing.begin("MinifyClasses");
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapMinifier.java b/src/main/java/com/android/tools/r8/naming/ProguardMapMinifier.java
index a81ebd7..6d8c889 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapMinifier.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapMinifier.java
@@ -73,7 +73,7 @@
   private final SeedMapper seedMapper;
   private final BiMap<DexType, DexString> mappedNames = HashBiMap.create();
   // To keep the order deterministic, we sort the classes by their type, which is a unique key.
-  private final Set<DexClass> mappedClasses = new TreeSet<>((a, b) -> a.type.slowCompareTo(b.type));
+  private final Set<DexClass> mappedClasses = new TreeSet<>((a, b) -> a.type.compareTo(b.type));
   private final Map<DexReference, MemberNaming> memberNames = Maps.newIdentityHashMap();
   private final Map<DexType, DexString> syntheticCompanionClasses = Maps.newIdentityHashMap();
   private final Map<DexMethod, DexString> defaultInterfaceMethodImplementationNames =
@@ -95,7 +95,7 @@
     Set<DexReference> notMappedReferences = new HashSet<>();
 
     timing.begin("MappingInterfaces");
-    Set<DexClass> interfaces = new TreeSet<>((a, b) -> a.type.slowCompareTo(b.type));
+    Set<DexClass> interfaces = new TreeSet<>((a, b) -> a.type.compareTo(b.type));
     Consumer<DexClass> consumer =
         dexClass -> {
           if (dexClass.isInterface()) {
diff --git a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
index 23a9a06..49b46cd 100644
--- a/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
+++ b/src/main/java/com/android/tools/r8/naming/ProguardMapReader.java
@@ -221,10 +221,6 @@
       skipWhitespace();
       // Workaround for proguard map files that contain entries for package-info.java files.
       assert IdentifierUtils.isDexIdentifierPart('-');
-      if (before.endsWith("package-info")) {
-        skipLine();
-        continue;
-      }
       if (before.endsWith("-") && acceptString(">")) {
         // With - as a legal identifier part the grammar is ambiguous, and we treat a->b as a -> b,
         // and not as a- > b (which would be a parse error).
diff --git a/src/main/java/com/android/tools/r8/optimize/FieldRebindingIdentityLens.java b/src/main/java/com/android/tools/r8/optimize/FieldRebindingIdentityLens.java
index 9b12469..d949ab2 100644
--- a/src/main/java/com/android/tools/r8/optimize/FieldRebindingIdentityLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/FieldRebindingIdentityLens.java
@@ -43,7 +43,8 @@
 
   @Override
   protected FieldLookupResult internalDescribeLookupField(FieldLookupResult previous) {
-    assert previous.getReboundReference() == null;
+    assert !previous.hasCastType();
+    assert !previous.hasReboundReference();
     return FieldLookupResult.builder(this)
         .setReference(previous.getReference())
         .setReboundReference(getReboundFieldReference(previous.getReference()))
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
index 77301cf..136595a 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingAnalysis.java
@@ -23,9 +23,15 @@
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.BiForEachable;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.ThreadUtils;
+import com.android.tools.r8.utils.TriConsumer;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.ExecutorService;
@@ -160,6 +166,14 @@
       BiForEachable<DexMethod, ProgramMethodSet> methodsWithContexts,
       Function<DexMethod, DexEncodedMethod> lookupTarget,
       Type invokeType) {
+
+    Map<DexProgramClass, List<Pair<DexMethod, DexEncodedMethod>>> bridges = new IdentityHashMap<>();
+    TriConsumer<DexProgramClass, DexMethod, DexEncodedMethod> addBridge =
+        (bridgeHolder, method, target) ->
+            bridges
+                .computeIfAbsent(bridgeHolder, k -> new ArrayList<>())
+                .add(new Pair<>(method, target));
+
     methodsWithContexts.forEach(
         (method, contexts) -> {
           // We can safely ignore array types, as the corresponding methods are defined in a
@@ -178,6 +192,7 @@
             return;
           }
           DexClass targetClass = appView.definitionFor(target.holder());
+          DexMethod targetMethod = target.method;
           if (originalClass.isProgramClass()) {
             // In Java bytecode, it is only possible to target interface methods that are in one of
             // the immediate super-interfaces via a super-invocation (see
@@ -186,9 +201,9 @@
             // bridge method when we are about to rebind to an interface method that is not the
             // original target.
             if (needsBridgeForInterfaceMethod(originalClass, targetClass, invokeType)) {
-              target =
+              targetMethod =
                   insertBridgeForInterfaceMethod(
-                      method, target, originalClass.asProgramClass(), targetClass, lookupTarget);
+                      method, target, originalClass.asProgramClass(), targetClass, addBridge);
             }
 
             // If the target class is not public but the targeted method is, we might run into
@@ -196,13 +211,32 @@
             final DexEncodedMethod finalTarget = target;
             if (contexts.stream()
                 .anyMatch(context -> mayNeedBridgeForVisibility(context, finalTarget))) {
-              target =
+              targetMethod =
                   insertBridgeForVisibilityIfNeeded(
-                      method, target, originalClass, targetClass, lookupTarget);
+                      method, target, originalClass, targetClass, addBridge);
             }
           }
           lensBuilder.map(
-              method, lens.lookupMethod(validTargetFor(target.method, method)), invokeType);
+              method, lens.lookupMethod(validTargetFor(targetMethod, method)), invokeType);
+        });
+
+    bridges.forEach(
+        (bridgeHolder, targets) -> {
+          // Sorting the list of bridges within a class maintains a deterministic order of entries
+          // in the method collection.
+          targets.sort((p1, p2) -> p1.getFirst().compareTo(p2.getFirst()));
+          for (Pair<DexMethod, DexEncodedMethod> pair : targets) {
+            DexMethod method = pair.getFirst();
+            DexEncodedMethod target = pair.getSecond();
+            DexMethod bridgeMethod =
+                method.withHolder(bridgeHolder.getType(), appView.dexItemFactory());
+            if (bridgeHolder.getMethodCollection().getMethod(bridgeMethod) == null) {
+              DexEncodedMethod bridgeMethodDefinition =
+                  target.toForwardingMethod(bridgeHolder, appView);
+              bridgeHolder.addMethod(bridgeMethodDefinition);
+            }
+            assert lookupTarget.apply(method).method == bridgeMethod;
+          }
         });
   }
 
@@ -214,12 +248,12 @@
         && targetClass.accessFlags.isInterface();
   }
 
-  private DexEncodedMethod insertBridgeForInterfaceMethod(
+  private DexMethod insertBridgeForInterfaceMethod(
       DexMethod method,
       DexEncodedMethod target,
       DexProgramClass originalClass,
       DexClass targetClass,
-      Function<DexMethod, DexEncodedMethod> lookupTarget) {
+      TriConsumer<DexProgramClass, DexMethod, DexEncodedMethod> bridges) {
     // If `targetClass` is a class, then insert the bridge method on the upper-most super class that
     // implements the interface. Otherwise, if it is an interface, then insert the bridge method
     // directly on the interface (because that interface must be the immediate super type, assuming
@@ -232,10 +266,8 @@
         findHolderForInterfaceMethodBridge(originalClass, targetClass.type);
     assert bridgeHolder != null;
     assert bridgeHolder != targetClass;
-    DexEncodedMethod bridgeMethod = target.toForwardingMethod(bridgeHolder, appView);
-    bridgeHolder.addMethod(bridgeMethod);
-    assert lookupTarget.apply(method) == bridgeMethod;
-    return bridgeMethod;
+    bridges.accept(bridgeHolder, method, target);
+    return target.method.withHolder(bridgeHolder.getType(), appView.dexItemFactory());
   }
 
   private DexProgramClass findHolderForInterfaceMethodBridge(DexProgramClass clazz, DexType iface) {
@@ -267,12 +299,12 @@
         && methodVisibility != ConstraintWithTarget.NEVER;
   }
 
-  private DexEncodedMethod insertBridgeForVisibilityIfNeeded(
+  private DexMethod insertBridgeForVisibilityIfNeeded(
       DexMethod method,
       DexEncodedMethod target,
       DexClass originalClass,
       DexClass targetClass,
-      Function<DexMethod, DexEncodedMethod> lookupTarget) {
+      TriConsumer<DexProgramClass, DexMethod, DexEncodedMethod> bridges) {
     // If the original class is public and this method is public, it might have been called
     // from anywhere, so we need a bridge. Likewise, if the original is in a different
     // package, we might need a bridge, too.
@@ -283,12 +315,10 @@
       DexProgramClass bridgeHolder =
           findHolderForVisibilityBridge(originalClass, targetClass, packageDescriptor);
       assert bridgeHolder != null;
-      DexEncodedMethod bridgeMethod = target.toForwardingMethod(bridgeHolder, appView);
-      bridgeHolder.addMethod(bridgeMethod);
-      assert lookupTarget.apply(method) == bridgeMethod;
-      return bridgeMethod;
+      bridges.accept(bridgeHolder, method, target);
+      return target.method.withHolder(bridgeHolder.getType(), appView.dexItemFactory());
     }
-    return target;
+    return target.method;
   }
 
   private DexProgramClass findHolderForVisibilityBridge(
diff --git a/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java b/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
index 02b56e3..3f77dbb 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingIdentityLens.java
@@ -51,7 +51,8 @@
 
   @Override
   protected FieldLookupResult internalDescribeLookupField(FieldLookupResult previous) {
-    assert previous.getReboundReference() == null;
+    assert !previous.hasCastType();
+    assert !previous.hasReboundReference();
     return FieldLookupResult.builder(this)
         .setReference(previous.getReference())
         .setReboundReference(getReboundFieldReference(previous.getReference()))
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 867ec0b..171dcbe 100644
--- a/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/MemberRebindingLens.java
@@ -95,7 +95,8 @@
 
   @Override
   protected FieldLookupResult internalDescribeLookupField(FieldLookupResult previous) {
-    assert previous.getReboundReference() == null;
+    assert !previous.hasCastType();
+    assert !previous.hasReboundReference();
     return FieldLookupResult.builder(this)
         .setReference(previous.getReference())
         .setReboundReference(getReboundFieldReference(previous.getReference()))
diff --git a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java
index 0d2c4e8..cea7fc2 100644
--- a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java
+++ b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoisting.java
@@ -113,7 +113,7 @@
 
   private void processClass(DexProgramClass clazz, SubtypingInfo subtypingInfo) {
     Set<DexType> subtypes = subtypingInfo.allImmediateSubtypes(clazz.type);
-    Set<DexProgramClass> subclasses = new TreeSet<>((x, y) -> x.type.slowCompareTo(y.type));
+    Set<DexProgramClass> subclasses = new TreeSet<>((x, y) -> x.type.compareTo(y.type));
     for (DexType subtype : subtypes) {
       DexProgramClass subclass = asProgramClassOrNull(appView.definitionFor(subtype));
       if (subclass == null) {
diff --git a/src/main/java/com/android/tools/r8/repackaging/RepackagingTreeFixer.java b/src/main/java/com/android/tools/r8/repackaging/RepackagingTreeFixer.java
index 07af4a3..c6a2424 100644
--- a/src/main/java/com/android/tools/r8/repackaging/RepackagingTreeFixer.java
+++ b/src/main/java/com/android/tools/r8/repackaging/RepackagingTreeFixer.java
@@ -56,7 +56,7 @@
     }
     appBuilder.replaceProgramClasses(new ArrayList<>(newProgramClasses.values()));
     RepackagingLens lens = lensBuilder.build(appView);
-    new AnnotationFixer(lens).run(appView.appInfo().classes());
+    new AnnotationFixer(lens).run(newProgramClasses.values());
     return lens;
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
index fc6cb56..9706eab 100644
--- a/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
+++ b/src/main/java/com/android/tools/r8/shaking/AnnotationRemover.java
@@ -261,7 +261,8 @@
     return liveGetter ? original : null;
   }
 
-  private boolean enclosingMethodPinned(DexClass clazz) {
+  private static boolean enclosingMethodPinned(
+      AppView<AppInfoWithLiveness> appView, DexClass clazz) {
     return clazz.getEnclosingMethodAttribute() != null
         && clazz.getEnclosingMethodAttribute().getEnclosingClass() != null
         && appView.appInfo().isPinned(clazz.getEnclosingMethodAttribute().getEnclosingClass());
@@ -285,7 +286,7 @@
     // is kept.
     boolean keptAnyway =
         appView.appInfo().isPinned(clazz.type)
-            || enclosingMethodPinned(clazz)
+            || enclosingMethodPinned(appView, clazz)
             || appView.options().forceProguardCompatibility;
     boolean keepForThisInnerClass = false;
     boolean keepForThisEnclosingClass = false;
@@ -391,7 +392,7 @@
         for (DexProgramClass clazz : appView.appInfo().classes()) {
           // If [clazz] is mentioned by a keep rule, it could be used for reflection, and we
           // therefore need to keep the enclosing method and inner classes attributes, if requested.
-          if (appView.appInfo().isPinned(clazz.type)) {
+          if (appView.appInfo().isPinned(clazz) || enclosingMethodPinned(appView, clazz)) {
             for (InnerClassAttribute innerClassAttribute : clazz.getInnerClasses()) {
               DexType inner = innerClassAttribute.getInner();
               if (appView.appInfo().isNonProgramTypeOrLiveProgramType(inner)) {
diff --git a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
index 4d5a549..bdda13b 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -26,13 +26,11 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.graph.EnumValueInfoMapCollection;
-import com.android.tools.r8.graph.EnumValueInfoMapCollection.EnumValueInfo;
 import com.android.tools.r8.graph.EnumValueInfoMapCollection.EnumValueInfoMap;
 import com.android.tools.r8.graph.FieldAccessInfo;
 import com.android.tools.r8.graph.FieldAccessInfoCollection;
 import com.android.tools.r8.graph.FieldAccessInfoCollectionImpl;
 import com.android.tools.r8.graph.FieldResolutionResult;
-import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.GraphLens.NonIdentityGraphLens;
 import com.android.tools.r8.graph.InstantiatedSubTypeInfo;
 import com.android.tools.r8.graph.LookupResult.LookupResultSuccess;
@@ -40,7 +38,6 @@
 import com.android.tools.r8.graph.MethodAccessInfoCollection;
 import com.android.tools.r8.graph.ObjectAllocationInfoCollection;
 import com.android.tools.r8.graph.ObjectAllocationInfoCollectionImpl;
-import com.android.tools.r8.graph.PresortedComparable;
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.ResolutionResult.SingleResolutionResult;
@@ -60,6 +57,7 @@
 import com.android.tools.r8.utils.Visibility;
 import com.android.tools.r8.utils.WorkList;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import com.android.tools.r8.utils.structural.Ordered;
 import com.google.common.collect.Sets;
 import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import it.unimi.dsi.fastutil.objects.Object2BooleanMap;
@@ -70,9 +68,6 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.SortedMap;
-import java.util.SortedSet;
-import java.util.TreeMap;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
@@ -89,11 +84,6 @@
    */
   private final Set<DexType> liveTypes;
   /**
-   * Set of service types (from META-INF/services/) that may have been instantiated reflectively via
-   * ServiceLoader.load() or ServiceLoader.loadInstalled().
-   */
-  public final Set<DexType> instantiatedAppServices;
-  /**
    * Set of methods that are the immediate target of an invoke. They might not actually be live but
    * are required so that invokes can find the method. If such a method is not live (i.e. not
    * contained in {@link #liveMethods}, it may be marked as abstract and its implementation may be
@@ -148,29 +138,29 @@
   /** All methods that *must* never be inlined due to a configuration directive (testing only). */
   private final Set<DexMethod> neverInline;
   /** Items for which to print inlining decisions for (testing only). */
-  public final Set<DexMethod> whyAreYouNotInlining;
+  private final Set<DexMethod> whyAreYouNotInlining;
   /** All methods that may not have any parameters with a constant value removed. */
-  public final Set<DexMethod> keepConstantArguments;
+  private final Set<DexMethod> keepConstantArguments;
   /** All methods that may not have any unused arguments removed. */
-  public final Set<DexMethod> keepUnusedArguments;
+  private final Set<DexMethod> keepUnusedArguments;
   /** All methods that must be reprocessed (testing only). */
-  public final Set<DexMethod> reprocess;
+  private final Set<DexMethod> reprocess;
   /** All methods that must not be reprocessed (testing only). */
-  public final Set<DexMethod> neverReprocess;
+  private final Set<DexMethod> neverReprocess;
   /** All types that should be inlined if possible due to a configuration directive. */
   public final PredicateSet<DexType> alwaysClassInline;
   /** All types that *must* never be inlined due to a configuration directive (testing only). */
-  public final Set<DexType> neverClassInline;
+  private final Set<DexType> neverClassInline;
 
-  private final Set<DexType> noUnusedInterfaceRemoval;
-  private final Set<DexType> noVerticalClassMerging;
+  private final Set<DexType> noClassMerging;
   private final Set<DexType> noHorizontalClassMerging;
+  private final Set<DexType> noVerticalClassMerging;
   private final Set<DexType> noStaticClassMerging;
 
   /**
    * Set of lock candidates (i.e., types whose class reference may flow to a monitor instruction).
    */
-  public final Set<DexType> lockCandidates;
+  private final Set<DexType> lockCandidates;
   /**
    * A map from seen init-class references to the minimum required visibility of the corresponding
    * static field.
@@ -205,7 +195,6 @@
       Set<DexType> deadProtoTypes,
       Set<DexType> missingTypes,
       Set<DexType> liveTypes,
-      Set<DexType> instantiatedAppServices,
       Set<DexMethod> targetedMethods,
       Set<DexMethod> failedResolutionTargets,
       Set<DexMethod> bootstrapMethods,
@@ -230,7 +219,7 @@
       Set<DexMethod> neverReprocess,
       PredicateSet<DexType> alwaysClassInline,
       Set<DexType> neverClassInline,
-      Set<DexType> noUnusedInterfaceRemoval,
+      Set<DexType> noClassMerging,
       Set<DexType> noVerticalClassMerging,
       Set<DexType> noHorizontalClassMerging,
       Set<DexType> noStaticClassMerging,
@@ -245,7 +234,6 @@
     this.deadProtoTypes = deadProtoTypes;
     this.missingTypes = missingTypes;
     this.liveTypes = liveTypes;
-    this.instantiatedAppServices = instantiatedAppServices;
     this.targetedMethods = targetedMethods;
     this.failedResolutionTargets = failedResolutionTargets;
     this.bootstrapMethods = bootstrapMethods;
@@ -270,93 +258,7 @@
     this.neverReprocess = neverReprocess;
     this.alwaysClassInline = alwaysClassInline;
     this.neverClassInline = neverClassInline;
-    this.noUnusedInterfaceRemoval = noUnusedInterfaceRemoval;
-    this.noVerticalClassMerging = noVerticalClassMerging;
-    this.noHorizontalClassMerging = noHorizontalClassMerging;
-    this.noStaticClassMerging = noStaticClassMerging;
-    this.neverPropagateValue = neverPropagateValue;
-    this.identifierNameStrings = identifierNameStrings;
-    this.prunedTypes = prunedTypes;
-    this.switchMaps = switchMaps;
-    this.enumValueInfoMaps = enumValueInfoMaps;
-    this.lockCandidates = lockCandidates;
-    this.initClassReferences = initClassReferences;
-  }
-
-  public AppInfoWithLiveness(
-      AppInfoWithClassHierarchy appInfoWithClassHierarchy,
-      Set<DexType> deadProtoTypes,
-      Set<DexType> missingTypes,
-      Set<DexType> liveTypes,
-      Set<DexType> instantiatedAppServices,
-      Set<DexMethod> targetedMethods,
-      Set<DexMethod> failedResolutionTargets,
-      Set<DexMethod> bootstrapMethods,
-      Set<DexMethod> methodsTargetedByInvokeDynamic,
-      SortedSet<DexMethod> virtualMethodsTargetedByInvokeDirect,
-      SortedSet<DexMethod> liveMethods,
-      FieldAccessInfoCollectionImpl fieldAccessInfoCollection,
-      MethodAccessInfoCollection methodAccessInfoCollection,
-      ObjectAllocationInfoCollectionImpl objectAllocationInfoCollection,
-      Map<DexCallSite, ProgramMethodSet> callSites,
-      KeepInfoCollection keepInfo,
-      Map<DexReference, ProguardMemberRule> mayHaveSideEffects,
-      Map<DexMember<?, ?>, ProguardMemberRule> noSideEffects,
-      Map<DexMember<?, ?>, ProguardMemberRule> assumedValues,
-      Set<DexMethod> alwaysInline,
-      Set<DexMethod> forceInline,
-      Set<DexMethod> neverInline,
-      Set<DexMethod> whyAreYouNotInlining,
-      Set<DexMethod> keepConstantArguments,
-      Set<DexMethod> keepUnusedArguments,
-      Set<DexMethod> reprocess,
-      Set<DexMethod> neverReprocess,
-      PredicateSet<DexType> alwaysClassInline,
-      Set<DexType> neverClassInline,
-      Set<DexType> noUnusedInterfaceRemoval,
-      Set<DexType> noVerticalClassMerging,
-      Set<DexType> noHorizontalClassMerging,
-      Set<DexType> noStaticClassMerging,
-      Set<DexReference> neverPropagateValue,
-      Object2BooleanMap<DexReference> identifierNameStrings,
-      Set<DexType> prunedTypes,
-      Map<DexField, Int2ReferenceMap<DexField>> switchMaps,
-      EnumValueInfoMapCollection enumValueInfoMaps,
-      Set<DexType> lockCandidates,
-      Map<DexType, Visibility> initClassReferences) {
-    super(
-        appInfoWithClassHierarchy.getSyntheticItems().commit(appInfoWithClassHierarchy.app()),
-        appInfoWithClassHierarchy.getClassToFeatureSplitMap(),
-        appInfoWithClassHierarchy.getMainDexClasses());
-    this.deadProtoTypes = deadProtoTypes;
-    this.missingTypes = missingTypes;
-    this.liveTypes = liveTypes;
-    this.instantiatedAppServices = instantiatedAppServices;
-    this.targetedMethods = targetedMethods;
-    this.failedResolutionTargets = failedResolutionTargets;
-    this.bootstrapMethods = bootstrapMethods;
-    this.methodsTargetedByInvokeDynamic = methodsTargetedByInvokeDynamic;
-    this.virtualMethodsTargetedByInvokeDirect = virtualMethodsTargetedByInvokeDirect;
-    this.liveMethods = liveMethods;
-    this.fieldAccessInfoCollection = fieldAccessInfoCollection;
-    this.methodAccessInfoCollection = methodAccessInfoCollection;
-    this.objectAllocationInfoCollection = objectAllocationInfoCollection;
-    this.keepInfo = keepInfo;
-    this.mayHaveSideEffects = mayHaveSideEffects;
-    this.noSideEffects = noSideEffects;
-    this.assumedValues = assumedValues;
-    this.callSites = callSites;
-    this.alwaysInline = alwaysInline;
-    this.forceInline = forceInline;
-    this.neverInline = neverInline;
-    this.whyAreYouNotInlining = whyAreYouNotInlining;
-    this.keepConstantArguments = keepConstantArguments;
-    this.keepUnusedArguments = keepUnusedArguments;
-    this.reprocess = reprocess;
-    this.neverReprocess = neverReprocess;
-    this.alwaysClassInline = alwaysClassInline;
-    this.neverClassInline = neverClassInline;
-    this.noUnusedInterfaceRemoval = noUnusedInterfaceRemoval;
+    this.noClassMerging = noClassMerging;
     this.noVerticalClassMerging = noVerticalClassMerging;
     this.noHorizontalClassMerging = noHorizontalClassMerging;
     this.noStaticClassMerging = noStaticClassMerging;
@@ -379,7 +281,6 @@
         previous.missingTypes,
         CollectionUtils.mergeSets(
             Sets.difference(previous.liveTypes, removedTypes), committedItems.getCommittedTypes()),
-        previous.instantiatedAppServices,
         previous.targetedMethods,
         previous.failedResolutionTargets,
         previous.bootstrapMethods,
@@ -404,7 +305,7 @@
         previous.neverReprocess,
         previous.alwaysClassInline,
         previous.neverClassInline,
-        previous.noUnusedInterfaceRemoval,
+        previous.noClassMerging,
         previous.noVerticalClassMerging,
         previous.noHorizontalClassMerging,
         previous.noStaticClassMerging,
@@ -431,7 +332,6 @@
         removedClasses == null
             ? previous.liveTypes
             : Sets.difference(previous.liveTypes, removedClasses),
-        previous.instantiatedAppServices,
         previous.targetedMethods,
         previous.failedResolutionTargets,
         previous.bootstrapMethods,
@@ -456,7 +356,7 @@
         previous.neverReprocess,
         previous.alwaysClassInline,
         previous.neverClassInline,
-        previous.noUnusedInterfaceRemoval,
+        previous.noClassMerging,
         previous.noVerticalClassMerging,
         previous.noHorizontalClassMerging,
         previous.noStaticClassMerging,
@@ -520,7 +420,6 @@
     this.deadProtoTypes = previous.deadProtoTypes;
     this.missingTypes = previous.missingTypes;
     this.liveTypes = previous.liveTypes;
-    this.instantiatedAppServices = previous.instantiatedAppServices;
     this.targetedMethods = previous.targetedMethods;
     this.failedResolutionTargets = previous.failedResolutionTargets;
     this.bootstrapMethods = previous.bootstrapMethods;
@@ -545,7 +444,7 @@
     this.neverReprocess = previous.neverReprocess;
     this.alwaysClassInline = previous.alwaysClassInline;
     this.neverClassInline = previous.neverClassInline;
-    this.noUnusedInterfaceRemoval = previous.noUnusedInterfaceRemoval;
+    this.noClassMerging = previous.noClassMerging;
     this.noVerticalClassMerging = previous.noVerticalClassMerging;
     this.noHorizontalClassMerging = previous.noHorizontalClassMerging;
     this.noStaticClassMerging = previous.noStaticClassMerging;
@@ -601,7 +500,7 @@
       // Skip synthetic classes which may not have a specified version.
       if (clazz.hasClassFileVersion()) {
         largestInputCfVersion =
-            CfVersion.maxAllowNull(largestInputCfVersion, clazz.getInitialClassFileVersion());
+            Ordered.maxIgnoreNull(largestInputCfVersion, clazz.getInitialClassFileVersion());
       }
     }
     assert largestInputCfVersion != null;
@@ -675,6 +574,30 @@
     return neverInline.contains(method);
   }
 
+  public boolean isWhyAreYouNotInliningMethod(DexMethod method) {
+    return whyAreYouNotInlining.contains(method);
+  }
+
+  public boolean hasNoWhyAreYouNotInliningMethods() {
+    return whyAreYouNotInlining.isEmpty();
+  }
+
+  public boolean isKeepConstantArgumentsMethod(DexMethod method) {
+    return keepConstantArguments.contains(method);
+  }
+
+  public boolean isKeepUnusedArgumentsMethod(DexMethod method) {
+    return keepUnusedArguments.contains(method);
+  }
+
+  public boolean isNeverReprocessMethod(DexMethod method) {
+    return neverReprocess.contains(method);
+  }
+
+  public Set<DexMethod> getReprocessMethods() {
+    return reprocess;
+  }
+
   public Collection<DexClass> computeReachableInterfaces() {
     Set<DexClass> interfaces = Sets.newIdentityHashSet();
     WorkList<DexType> worklist = WorkList.newIdentityWorkList();
@@ -780,12 +703,6 @@
     return enumValueInfoMaps.getEnumValueInfoMap(enumType);
   }
 
-  public EnumValueInfo getEnumValueInfo(DexField field) {
-    assert checkIfObsolete();
-    EnumValueInfoMap map = enumValueInfoMaps.getEnumValueInfoMap(field.type);
-    return map != null ? map.getEnumValueInfo(field) : null;
-  }
-
   public Int2ReferenceMap<DexField> getSwitchMap(DexField field) {
     assert checkIfObsolete();
     return switchMaps.get(field);
@@ -936,18 +853,6 @@
     return holder == null || holder.isLibraryClass() || holder.isClasspathClass();
   }
 
-  private static SortedMap<DexMethod, ProgramMethodSet> rewriteInvokesWithContexts(
-      Map<DexMethod, ProgramMethodSet> invokes, GraphLens lens) {
-    SortedMap<DexMethod, ProgramMethodSet> result = new TreeMap<>(PresortedComparable::slowCompare);
-    invokes.forEach(
-        (method, contexts) ->
-            result
-                .computeIfAbsent(
-                    lens.getRenamedMethodSignature(method), ignore -> ProgramMethodSet.create())
-                .addAll(contexts));
-    return Collections.unmodifiableSortedMap(result);
-  }
-
   public boolean isInstantiatedInterface(DexProgramClass clazz) {
     assert checkIfObsolete();
     return objectAllocationInfoCollection.isInterfaceWithUnknownSubtypeHierarchy(clazz);
@@ -1067,7 +972,6 @@
         deadProtoTypes,
         missingTypes,
         lens.rewriteTypes(liveTypes),
-        lens.rewriteTypes(instantiatedAppServices),
         lens.rewriteMethods(targetedMethods),
         lens.rewriteMethods(failedResolutionTargets),
         lens.rewriteMethods(bootstrapMethods),
@@ -1085,14 +989,14 @@
         lens.rewriteMethods(alwaysInline),
         lens.rewriteMethods(forceInline),
         lens.rewriteMethods(neverInline),
-        lens.rewriteMethodsSorted(whyAreYouNotInlining),
-        lens.rewriteMethodsSorted(keepConstantArguments),
-        lens.rewriteMethodsSorted(keepUnusedArguments),
-        lens.rewriteMethodsSorted(reprocess),
-        lens.rewriteMethodsSorted(neverReprocess),
+        lens.rewriteMethods(whyAreYouNotInlining),
+        lens.rewriteMethods(keepConstantArguments),
+        lens.rewriteMethods(keepUnusedArguments),
+        lens.rewriteMethods(reprocess),
+        lens.rewriteMethods(neverReprocess),
         alwaysClassInline.rewriteItems(lens::lookupType),
         lens.rewriteTypes(neverClassInline),
-        lens.rewriteTypes(noUnusedInterfaceRemoval),
+        lens.rewriteTypes(noClassMerging),
         lens.rewriteTypes(noVerticalClassMerging),
         lens.rewriteTypes(noHorizontalClassMerging),
         lens.rewriteTypes(noStaticClassMerging),
@@ -1463,25 +1367,14 @@
         .shouldBreak();
   }
 
-  /** All unused interface types that *must* never be pruned. */
-  public Set<DexType> getNoUnusedInterfaceRemovalSet() {
-    return noUnusedInterfaceRemoval;
+  /** Predicate on types that *must* never be merged horizontally. */
+  public boolean isNoHorizontalClassMergingOfType(DexType type) {
+    return noClassMerging.contains(type) || noHorizontalClassMerging.contains(type);
   }
 
-  /**
-   * All types that *must* never be merged vertically due to a configuration directive (testing
-   * only).
-   */
-  public Set<DexType> getNoVerticalClassMergingSet() {
-    return noVerticalClassMerging;
-  }
-
-  /**
-   * All types that *must* never be merged horizontally due to a configuration directive (testing
-   * only).
-   */
-  public Set<DexType> getNoHorizontalClassMergingSet() {
-    return noHorizontalClassMerging;
+  /** Predicate on types that *must* never be merged vertically. */
+  public boolean isNoVerticalClassMergingOfType(DexType type) {
+    return noClassMerging.contains(type) || noVerticalClassMerging.contains(type);
   }
 
   /**
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index e032ea2..ed31df5 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -208,8 +208,8 @@
 
   private final FieldAccessInfoCollectionImpl fieldAccessInfoCollection =
       new FieldAccessInfoCollectionImpl();
-  private final MethodAccessInfoCollection.SortedBuilder methodAccessInfoCollection =
-      MethodAccessInfoCollection.sortedBuilder();
+  private final MethodAccessInfoCollection.IdentityBuilder methodAccessInfoCollection =
+      MethodAccessInfoCollection.identityBuilder();
   private final ObjectAllocationInfoCollectionImpl.Builder objectAllocationInfoCollection;
   private final Map<DexCallSite, ProgramMethodSet> callSites = new IdentityHashMap<>();
 
@@ -271,6 +271,8 @@
   /** Set of types that was pruned during the first round of tree shaking. */
   private Set<DexType> initialPrunedTypes;
 
+  private final Set<DexType> noClassMerging = Sets.newIdentityHashSet();
+
   /** Mapping from each unused interface to the set of live types that implements the interface. */
   private final Map<DexProgramClass, Set<DexProgramClass>> unusedInterfaceTypes =
       new IdentityHashMap<>();
@@ -308,12 +310,6 @@
    */
   private final LiveFieldsSet liveFields;
 
-  /**
-   * Set of service types (from META-INF/services/) that may have been instantiated reflectively via
-   * ServiceLoader.load() or ServiceLoader.loadInstalled().
-   */
-  private final Set<DexType> instantiatedAppServices = Sets.newIdentityHashSet();
-
   /** A queue of items that need processing. Different items trigger different actions. */
   private final EnqueuerWorklist workList;
 
@@ -1360,6 +1356,7 @@
 
     FieldResolutionResult resolutionResult = resolveField(fieldReference);
     if (resolutionResult.isFailedOrUnknownResolution()) {
+      noClassMerging.add(fieldReference.getHolderType());
       return;
     }
 
@@ -1410,6 +1407,7 @@
 
     FieldResolutionResult resolutionResult = resolveField(fieldReference);
     if (resolutionResult.isFailedOrUnknownResolution()) {
+      noClassMerging.add(fieldReference.getHolderType());
       return;
     }
 
@@ -1460,6 +1458,7 @@
     if (resolutionResult.isFailedOrUnknownResolution()) {
       // Must mark the field as targeted even if it does not exist.
       markFieldAsTargeted(fieldReference, currentMethod);
+      noClassMerging.add(fieldReference.getHolderType());
       return;
     }
 
@@ -1518,6 +1517,7 @@
     if (resolutionResult.isFailedOrUnknownResolution()) {
       // Must mark the field as targeted even if it does not exist.
       markFieldAsTargeted(fieldReference, currentMethod);
+      noClassMerging.add(fieldReference.getHolderType());
       return;
     }
 
@@ -3242,7 +3242,6 @@
                 ? Sets.union(initialMissingTypes, missingTypes)
                 : missingTypes,
             SetUtils.mapIdentityHashSet(liveTypes.getItems(), DexProgramClass::getType),
-            Collections.unmodifiableSet(instantiatedAppServices),
             Enqueuer.toDescriptorSet(targetedMethods.getItems()),
             Collections.unmodifiableSet(failedResolutionTargets),
             Collections.unmodifiableSet(bootstrapMethods),
@@ -3268,7 +3267,7 @@
             rootSet.neverReprocess,
             rootSet.alwaysClassInline,
             rootSet.neverClassInline,
-            rootSet.noUnusedInterfaceRemoval,
+            noClassMerging,
             rootSet.noVerticalClassMerging,
             rootSet.noHorizontalClassMerging,
             rootSet.noStaticClassMerging,
@@ -3925,6 +3924,11 @@
       } else if (identifierTypeLookupResult.isTypeInitializedFromUse()) {
         markDirectAndIndirectClassInitializersAsLive(clazz);
       }
+      // To ensure we are not moving the class because we cannot prune it when there is a reflective
+      // use of it.
+      if (!keepInfo.getClassInfo(clazz).isPinned()) {
+        keepInfo.pinClass(clazz);
+      }
     } else if (referencedItem.isDexField()) {
       DexField field = referencedItem.asDexField();
       DexProgramClass clazz = getProgramClassOrNull(field.holder);
@@ -4200,8 +4204,6 @@
   }
 
   private void handleServiceInstantiation(DexType serviceType, KeepReason reason) {
-    instantiatedAppServices.add(serviceType);
-
     List<DexType> serviceImplementationTypes =
         appView.appServices().serviceImplementationsFor(serviceType);
     for (DexType serviceImplementationType : serviceImplementationTypes) {
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfo.java b/src/main/java/com/android/tools/r8/shaking/KeepInfo.java
index 7d75f05..36f5cdd 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepInfo.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfo.java
@@ -84,8 +84,9 @@
 
   public boolean isAllowSignatureAttributeRemovalAllowed(
       GlobalKeepInfoConfiguration configuration) {
-    return !configuration.isKeepAttributesSignatureEnabled()
-        || !(isPinned() || configuration.isForceProguardCompatibilityEnabled());
+    // TODO(b/172999267): For full mode we should be able to remove for not pinned items if
+    //  java reflect will not throw up.
+    return !configuration.isKeepAttributesSignatureEnabled();
   }
 
   public abstract boolean isTop();
diff --git a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java
index 4a19c6f..aac168d 100644
--- a/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java
+++ b/src/main/java/com/android/tools/r8/shaking/ProguardConfigurationRule.java
@@ -105,6 +105,10 @@
       AppView<? extends AppInfoWithClassHierarchy> appView,
       SubtypingInfo subtypingInfo,
       Iterable<DexProgramClass> defaultValue) {
+    List<DexType> specificTypes = getClassNames().asSpecificDexTypes();
+    if (specificTypes != null) {
+      return DexProgramClass.asProgramClasses(specificTypes, appView);
+    }
     if (hasInheritanceClassName() && getInheritanceClassName().hasSpecificType()) {
       DexType type = getInheritanceClassName().getSpecificType();
       if (appView.verticallyMergedClasses() != null
diff --git a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
index 7267bac..1d476bf 100644
--- a/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
+++ b/src/main/java/com/android/tools/r8/shaking/RootSetBuilder.java
@@ -745,7 +745,7 @@
               DexProgramClass holder = asProgramClassOrNull(appInfo.definitionForHolder(method));
               DexEncodedMethod definition = method.lookupOnClass(holder);
               if (definition == null) {
-                assert false;
+                assert method.match(appInfo.dexItemFactory().deserializeLambdaMethod);
                 return;
               }
               out.print(method.holder.toSourceString() + ": ");
@@ -1705,7 +1705,7 @@
     assert method.getHolderType() == options.dexItemFactory().objectType;
     OriginWithPosition key = new OriginWithPosition(context.getOrigin(), context.getPosition());
     assumeNoSideEffectsWarnings
-        .computeIfAbsent(key, ignore -> new TreeSet<>(DexMethod::slowCompareTo))
+        .computeIfAbsent(key, ignore -> new TreeSet<>(DexMethod::compareTo))
         .add(method.getReference());
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
index 2a72863..97e980c 100644
--- a/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
+import com.android.tools.r8.graph.classmerging.StaticallyMergedClasses;
 import com.android.tools.r8.logging.Log;
 import com.android.tools.r8.shaking.VerticalClassMerger.IllegalAccessDetector;
 import com.android.tools.r8.utils.FieldSignatureEquivalence;
@@ -188,6 +189,8 @@
 
   private final AppView<AppInfoWithLiveness> appView;
   private final MainDexTracingResult mainDexClasses;
+  private final StaticallyMergedClasses.Builder mergedClassesBuilder =
+      StaticallyMergedClasses.builder();
 
   /** The equivalence that should be used for the member buckets in {@link Representative}. */
   private final Equivalence<DexField> fieldEquivalence;
@@ -224,6 +227,7 @@
           numberOfMergedClasses,
           fieldMapping.size() + methodMapping.size());
     }
+    appView.setStaticallyMergedClasses(mergedClassesBuilder.build());
     return buildGraphLens();
   }
 
@@ -337,7 +341,7 @@
         // We are not allowed to merge synchronized classes with synchronized methods.
         return;
       }
-      if (appView.appInfo().lockCandidates.contains(clazz.type)) {
+      if (appView.appInfo().isLockCandidate(clazz.type)) {
         // Since the type is const-class referenced (and the static merger does not create a lens
         // to map the merged type) the class will likely remain and there is no gain from merging.
         return;
@@ -456,6 +460,7 @@
     assert getClassToFeatureSplitMap().isInSameFeatureOrBothInBase(sourceClass, targetClass);
 
     numberOfMergedClasses++;
+    mergedClassesBuilder.recordMerge(sourceClass, targetClass);
 
     // Move members from source to target.
     targetClass.addDirectMethods(
diff --git a/src/main/java/com/android/tools/r8/shaking/UnusedItemsPrinter.java b/src/main/java/com/android/tools/r8/shaking/UnusedItemsPrinter.java
index 1d57c73..5b89abf 100644
--- a/src/main/java/com/android/tools/r8/shaking/UnusedItemsPrinter.java
+++ b/src/main/java/com/android/tools/r8/shaking/UnusedItemsPrinter.java
@@ -24,8 +24,8 @@
     }
 
     void sort() {
-      fields.sort((a, b) -> a.getReference().slowCompareTo(b.getReference()));
-      methods.sort((a, b) -> a.getReference().slowCompareTo(b.getReference()));
+      fields.sort((a, b) -> a.getReference().compareTo(b.getReference()));
+      methods.sort((a, b) -> a.getReference().compareTo(b.getReference()));
     }
   }
 
@@ -73,7 +73,7 @@
   }
 
   public void finished() {
-    classes.sort((a, b) -> a.getFirst().slowCompareTo(b.getFirst()));
+    classes.sort((a, b) -> a.getFirst().compareTo(b.getFirst()));
     for (Pair<DexType, Members> entry : classes) {
       DexType type = entry.getFirst();
       Members members = entry.getSecond();
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 a294f1d..c98e345 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -19,6 +19,7 @@
 import com.android.tools.r8.graph.DexEncodedMember;
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMember;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -57,6 +58,7 @@
 import com.android.tools.r8.utils.OptionalBool;
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.TraversalContinuation;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
 import com.google.common.base.Equivalence;
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.Iterables;
@@ -204,16 +206,14 @@
   private final Set<DexProgramClass> mergeCandidates = new LinkedHashSet<>();
 
   // Map from source class to target class.
-  private final Map<DexType, DexType> mergedClasses = new IdentityHashMap<>();
-
-  // Map from target class to the super classes that have been merged into the target class.
-  private final Map<DexType, Set<DexType>> mergedClassesInverse = new IdentityHashMap<>();
+  private final BidirectionalManyToOneMap<DexType, DexType> mergedClasses =
+      new BidirectionalManyToOneMap<>();
 
   // Set of types that must not be merged into their subtype.
   private final Set<DexType> pinnedTypes = Sets.newIdentityHashSet();
 
   // The resulting graph lens that should be used after class merging.
-  private final VerticalClassMergerGraphLens.Builder renamedMembersLens;
+  private final VerticalClassMergerGraphLens.Builder lensBuilder;
 
   // All the bridge methods that have been synthesized during vertical class merging.
   private final List<SynthesizedBridgeCode> synthesizedBridges = new ArrayList<>();
@@ -232,7 +232,7 @@
     this.subtypingInfo = appInfo.computeSubtypingInfo();
     this.executorService = executorService;
     this.methodPoolCollection = new MethodPoolCollection(appView, subtypingInfo);
-    this.renamedMembersLens = new VerticalClassMergerGraphLens.Builder(appView.dexItemFactory());
+    this.lensBuilder = new VerticalClassMergerGraphLens.Builder(appView.dexItemFactory());
     this.timing = timing;
     this.mainDexClasses = mainDexClasses;
 
@@ -241,10 +241,6 @@
     initializeMergeCandidates(classes);
   }
 
-  private VerticallyMergedClasses getMergedClasses() {
-    return new VerticallyMergedClasses(mergedClasses, mergedClassesInverse);
-  }
-
   private void initializeMergeCandidates(Iterable<DexProgramClass> classes) {
     for (DexProgramClass sourceClass : classes) {
       DexType singleSubtype = subtypingInfo.getSingleDirectSubtype(sourceClass.type);
@@ -351,7 +347,7 @@
         || allocationInfo.isImmediateInterfaceOfInstantiatedLambda(sourceClass)
         || appInfo.isPinned(sourceClass.type)
         || pinnedTypes.contains(sourceClass.type)
-        || appInfo.getNoVerticalClassMergingSet().contains(sourceClass.type)) {
+        || appInfo.isNoVerticalClassMergingOfType(sourceClass.type)) {
       return false;
     }
 
@@ -427,7 +423,7 @@
   // before merging [clazz] into its subtype.
   private boolean isStillMergeCandidate(DexProgramClass sourceClass, DexProgramClass targetClass) {
     assert isMergeCandidate(sourceClass, targetClass, pinnedTypes);
-    if (mergedClassesInverse.containsKey(sourceClass.type)) {
+    if (mergedClasses.containsValue(sourceClass.getType())) {
       // Do not allow merging the resulting class into its subclass.
       // TODO(christofferqa): Get rid of this limitation.
       if (Log.ENABLED) {
@@ -441,7 +437,7 @@
     // their clinit called - except when the interface has a default method.
     if ((sourceClass.hasClassInitializer() && targetClass.hasClassInitializer())
         || targetClass.classInitializationMayHaveSideEffects(
-            appView, type -> type == sourceClass.type, Sets.newIdentityHashSet())
+            appView, type -> type == sourceClass.type)
         || (sourceClass.isInterface()
             && sourceClass.classInitializationMayHaveSideEffects(appView))) {
       // TODO(herhut): Handle class initializers.
@@ -644,16 +640,20 @@
     TopDownClassHierarchyTraversal.forProgramClasses(appView)
         .visit(mergeCandidates, this::mergeClassIfPossible);
     if (Log.ENABLED) {
-      Log.debug(getClass(), "Merged %d classes.", mergedClasses.size());
+      Log.debug(getClass(), "Merged %d classes.", mergedClasses.keySet().size());
     }
     timing.end();
 
-    if (mergedClasses.isEmpty()) {
+    VerticallyMergedClasses verticallyMergedClasses = new VerticallyMergedClasses(mergedClasses);
+    appView.setVerticallyMergedClasses(verticallyMergedClasses);
+    if (verticallyMergedClasses.isEmpty()) {
       return null;
     }
 
     timing.begin("fixup");
-    VerticalClassMergerGraphLens lens = new TreeFixer().fixupTypeReferences();
+    VerticalClassMergerGraphLens lens =
+        new TreeFixer(appView, lensBuilder, verticallyMergedClasses, synthesizedBridges)
+            .fixupTypeReferences();
     KeepInfoCollection keepInfo = appView.appInfo().getKeepInfo();
     keepInfo.mutate(mutator -> mutator.removeKeepInfoForPrunedItems(mergedClasses.keySet()));
     timing.end();
@@ -806,8 +806,7 @@
     assert !mergedClasses.containsKey(targetClass.type);
 
     boolean clazzOrTargetClassHasBeenMerged =
-        mergedClassesInverse.containsKey(clazz.type)
-            || mergedClassesInverse.containsKey(targetClass.type);
+        mergedClasses.containsValue(clazz.type) || mergedClasses.containsValue(targetClass.type);
     if (clazzOrTargetClassHasBeenMerged) {
       if (!isStillMergeCandidate(clazz, targetClass)) {
         return;
@@ -851,7 +850,7 @@
     }
     if (merged) {
       // Commit the changes to the graph lens.
-      renamedMembersLens.merge(merger.getRenamings());
+      lensBuilder.merge(merger.getRenamings());
       synthesizedBridges.addAll(merger.getSynthesizedBridges());
     }
     if (Log.ENABLED) {
@@ -1116,7 +1115,6 @@
       source.setStaticFields(null);
       // Step 4: Record merging.
       mergedClasses.put(source.type, target.type);
-      mergedClassesInverse.computeIfAbsent(target.type, key -> new HashSet<>()).add(source.type);
       assert !abortMerge;
       return true;
     }
@@ -1190,23 +1188,21 @@
           // the code above. However, instructions on the form "invoke-super A.m()" should also be
           // changed into "invoke-direct D.m$C()". This is achieved by also considering the classes
           // that have been merged into [holder].
-          Set<DexType> mergedTypes = mergedClassesInverse.get(holder.type);
-          if (mergedTypes != null) {
-            for (DexType type : mergedTypes) {
-              DexMethod signatureInType =
-                  application.dexItemFactory.createMethod(type, oldTarget.proto, oldTarget.name);
-              // Resolution would have succeeded if the method used to be in [type], or if one of
-              // its super classes declared the method.
-              boolean resolutionSucceededBeforeMerge =
-                  renamedMembersLens.hasMappingForSignatureInContext(holder, signatureInType)
-                      || appInfo.lookupSuperTarget(signatureInHolder, holder) != null;
-              if (resolutionSucceededBeforeMerge) {
-                deferredRenamings.mapVirtualMethodToDirectInType(
-                    signatureInType,
-                    prototypeChanges ->
-                        new MethodLookupResult(newTarget, null, DIRECT, prototypeChanges),
-                    target.type);
-              }
+          Set<DexType> mergedTypes = mergedClasses.getKeys(holder.type);
+          for (DexType type : mergedTypes) {
+            DexMethod signatureInType =
+                application.dexItemFactory.createMethod(type, oldTarget.proto, oldTarget.name);
+            // Resolution would have succeeded if the method used to be in [type], or if one of
+            // its super classes declared the method.
+            boolean resolutionSucceededBeforeMerge =
+                lensBuilder.hasMappingForSignatureInContext(holder, signatureInType)
+                    || appInfo.lookupSuperTarget(signatureInHolder, holder) != null;
+            if (resolutionSucceededBeforeMerge) {
+              deferredRenamings.mapVirtualMethodToDirectInType(
+                  signatureInType,
+                  prototypeChanges ->
+                      new MethodLookupResult(newTarget, null, DIRECT, prototypeChanges),
+                  target.type);
             }
           }
           holder =
@@ -1469,16 +1465,32 @@
     method.accessFlags.setPrivate();
   }
 
-  private class TreeFixer {
+  private static class TreeFixer {
 
-    private final VerticalClassMergerGraphLens.Builder lensBuilder =
-        VerticalClassMergerGraphLens.Builder.createBuilderForFixup(
-            renamedMembersLens, mergedClasses);
+    private final AppView<AppInfoWithLiveness> appView;
+    private final DexItemFactory dexItemFactory;
+    private final VerticalClassMergerGraphLens.Builder lensBuilder;
+    private final VerticallyMergedClasses mergedClasses;
+    private final List<SynthesizedBridgeCode> synthesizedBridges;
+
     private final Map<DexProto, DexProto> protoFixupCache = new IdentityHashMap<>();
 
+    TreeFixer(
+        AppView<AppInfoWithLiveness> appView,
+        VerticalClassMergerGraphLens.Builder lensBuilder,
+        VerticallyMergedClasses mergedClasses,
+        List<SynthesizedBridgeCode> synthesizedBridges) {
+      this.appView = appView;
+      this.dexItemFactory = appView.dexItemFactory();
+      this.lensBuilder =
+          VerticalClassMergerGraphLens.Builder.createBuilderForFixup(lensBuilder, mergedClasses);
+      this.mergedClasses = mergedClasses;
+      this.synthesizedBridges = synthesizedBridges;
+    }
+
     private VerticalClassMergerGraphLens fixupTypeReferences() {
       // Globally substitute merged class types in protos and holders.
-      for (DexProgramClass clazz : appInfo.classes()) {
+      for (DexProgramClass clazz : appView.appInfo().classes()) {
         clazz.getMethodCollection().replaceMethods(this::fixupMethod);
         fixupFields(clazz.staticFields(), clazz::setStaticField);
         fixupFields(clazz.instanceFields(), clazz::setInstanceField);
@@ -1486,7 +1498,7 @@
       for (SynthesizedBridgeCode synthesizedBridge : synthesizedBridges) {
         synthesizedBridge.updateMethodSignatures(this::fixupMethod);
       }
-      VerticalClassMergerGraphLens lens = lensBuilder.build(appView, getMergedClasses());
+      VerticalClassMergerGraphLens lens = lensBuilder.build(appView, mergedClasses);
       if (lens != null) {
         new AnnotationFixer(lens).run(appView.appInfo().classes());
       }
@@ -1523,7 +1535,7 @@
         DexField field = encodedField.field;
         DexType newType = fixupType(field.type);
         DexType newHolder = fixupType(field.holder);
-        DexField newField = application.dexItemFactory.createField(newHolder, newType, field.name);
+        DexField newField = dexItemFactory.createField(newHolder, newType, field.name);
         if (newField != encodedField.field) {
           if (!lensBuilder.hasOriginalSignatureMappingFor(newField)) {
             lensBuilder.map(field, newField);
@@ -1534,7 +1546,7 @@
     }
 
     private DexMethod fixupMethod(DexMethod method) {
-      return application.dexItemFactory.createMethod(
+      return dexItemFactory.createMethod(
           fixupType(method.holder), fixupProto(method.proto), method.name);
     }
 
@@ -1543,7 +1555,7 @@
       if (result == null) {
         DexType returnType = fixupType(proto.returnType);
         DexType[] arguments = fixupTypes(proto.parameters.values);
-        result = application.dexItemFactory.createProto(returnType, arguments);
+        result = dexItemFactory.createProto(returnType, arguments);
         protoFixupCache.put(proto, result);
       }
       return result;
@@ -1551,16 +1563,16 @@
 
     private DexType fixupType(DexType type) {
       if (type.isArrayType()) {
-        DexType base = type.toBaseType(application.dexItemFactory);
+        DexType base = type.toBaseType(dexItemFactory);
         DexType fixed = fixupType(base);
         if (base == fixed) {
           return type;
         }
-        return type.replaceBaseType(fixed, application.dexItemFactory);
+        return type.replaceBaseType(fixed, dexItemFactory);
       }
       if (type.isClassType()) {
-        while (mergedClasses.containsKey(type)) {
-          type = mergedClasses.get(type);
+        while (mergedClasses.hasBeenMergedIntoSubtype(type)) {
+          type = mergedClasses.getTargetFor(type);
         }
       }
       return type;
@@ -1713,7 +1725,7 @@
     return AbortReason.UNSAFE_INLINING;
   }
 
-  private class SingleTypeMapperGraphLens extends NonIdentityGraphLens {
+  public class SingleTypeMapperGraphLens extends NonIdentityGraphLens {
 
     private final DexType source;
     private final DexProgramClass target;
@@ -1770,7 +1782,7 @@
       // changed if the method was publicized by ClassAndMemberPublicizer).
       MethodLookupResult lookup = appView.graphLens().lookupMethod(method, context, type);
       // Then check if there is a renaming due to the vertical class merger.
-      DexMethod newMethod = renamedMembersLens.methodMap.get(lookup.getReference());
+      DexMethod newMethod = lensBuilder.methodMap.get(lookup.getReference());
       if (newMethod == null) {
         return lookup;
       }
@@ -1806,7 +1818,7 @@
 
     @Override
     public DexField lookupField(DexField field) {
-      return renamedMembersLens.fieldMap.getOrDefault(field, field);
+      return lensBuilder.fieldMap.getOrDefault(field, field);
     }
 
     @Override
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 a5481fd..2bee133 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
@@ -92,10 +92,6 @@
     this.originalMethodSignaturesForBridges = originalMethodSignaturesForBridges;
   }
 
-  public VerticallyMergedClasses getMergedClasses() {
-    return mergedClasses;
-  }
-
   @Override
   protected Iterable<DexType> internalGetOriginalTypes(DexType previous) {
     Collection<DexType> originalTypes = mergedClasses.getSourcesFor(previous);
@@ -185,7 +181,7 @@
       this.dexItemFactory = dexItemFactory;
     }
 
-    static Builder createBuilderForFixup(Builder builder, Map<DexType, DexType> mergedClasses) {
+    static Builder createBuilderForFixup(Builder builder, VerticallyMergedClasses mergedClasses) {
       Builder newBuilder = new Builder(builder.dexItemFactory);
       for (Map.Entry<DexField, DexField> entry : builder.fieldMap.entrySet()) {
         newBuilder.map(
@@ -235,7 +231,7 @@
 
     public VerticalClassMergerGraphLens build(
         AppView<?> appView, VerticallyMergedClasses mergedClasses) {
-      if (mergedClasses.getForwardMap().isEmpty()) {
+      if (mergedClasses.isEmpty()) {
         return null;
       }
       BiMap<DexField, DexField> originalFieldSignatures = fieldMap.inverse();
@@ -254,11 +250,11 @@
     }
 
     private DexField getFieldSignatureAfterClassMerging(
-        DexField field, Map<DexType, DexType> mergedClasses) {
+        DexField field, VerticallyMergedClasses mergedClasses) {
       assert !field.holder.isArrayType();
 
       DexType holder = field.holder;
-      DexType newHolder = mergedClasses.getOrDefault(holder, holder);
+      DexType newHolder = mergedClasses.getTargetForOrDefault(holder, holder);
 
       DexType type = field.type;
       DexType newType = getTypeAfterClassMerging(type, mergedClasses);
@@ -270,11 +266,11 @@
     }
 
     private DexMethod getMethodSignatureAfterClassMerging(
-        DexMethod signature, Map<DexType, DexType> mergedClasses) {
+        DexMethod signature, VerticallyMergedClasses mergedClasses) {
       assert !signature.holder.isArrayType();
 
       DexType holder = signature.holder;
-      DexType newHolder = mergedClasses.getOrDefault(holder, holder);
+      DexType newHolder = mergedClasses.getTargetForOrDefault(holder, holder);
 
       DexProto proto = signature.proto;
       DexProto newProto =
@@ -287,16 +283,16 @@
       return dexItemFactory.createMethod(newHolder, newProto, signature.name);
     }
 
-    private DexType getTypeAfterClassMerging(DexType type, Map<DexType, DexType> mergedClasses) {
+    private DexType getTypeAfterClassMerging(DexType type, VerticallyMergedClasses mergedClasses) {
       if (type.isArrayType()) {
         DexType baseType = type.toBaseType(dexItemFactory);
-        DexType newBaseType = mergedClasses.getOrDefault(baseType, baseType);
+        DexType newBaseType = mergedClasses.getTargetForOrDefault(baseType, baseType);
         if (newBaseType != baseType) {
           return type.replaceBaseType(newBaseType, dexItemFactory);
         }
         return type;
       }
-      return mergedClasses.getOrDefault(type, type);
+      return mergedClasses.getTargetForOrDefault(type, type);
     }
 
     public boolean hasMappingForSignatureInContext(DexProgramClass context, DexMethod signature) {
diff --git a/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java b/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java
index 303df06..f5d4b66 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SynthesizingContext.java
@@ -65,9 +65,9 @@
     return Comparator
         // The first item to compare is the synthesizing context type. This is the type used to
         // choose the context prefix for items.
-        .comparing(SynthesizingContext::getSynthesizingContextType, DexType::slowCompareTo)
+        .comparing(SynthesizingContext::getSynthesizingContextType)
         // To ensure that equals coincides with compareTo == 0, we then compare 'type'.
-        .thenComparing(c -> c.inputContextType, DexType::slowCompareTo)
+        .thenComparing(c -> c.inputContextType)
         .compare(this, other);
   }
 
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
index 30d232e..01f8825 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticDefinition.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.synthesis;
 
 import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.utils.structural.RepresentativeMap;
 import com.google.common.hash.HashCode;
 
 /**
@@ -26,7 +27,7 @@
 
   abstract DexProgramClass getHolder();
 
-  abstract HashCode computeHash(boolean intermediate);
+  abstract HashCode computeHash(RepresentativeMap map, boolean intermediate);
 
   abstract boolean isEquivalentTo(SyntheticDefinition other, boolean intermediate);
 }
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
index 1a96106..4d621a3 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticFinalization.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.shaking.MainDexClasses;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.structural.RepresentativeMap;
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
@@ -93,7 +94,7 @@
     MainDexClasses mainDexClasses = appView.appInfo().getMainDexClasses();
     GraphLens graphLens = appView.graphLens();
 
-    List<SyntheticMethodDefinition> methodDefinitions =
+    Map<DexType, SyntheticMethodDefinition> methodDefinitions =
         lookupSyntheticMethodDefinitions(application);
 
     Collection<List<SyntheticMethodDefinition>> potentialEquivalences =
@@ -183,7 +184,7 @@
       // Use a tree set to make sure that we have an ordering on the types.
       // These types are put in an array in annotations in the output and we
       // need a consistent ordering on them.
-      TreeSet<DexType> synthesized = new TreeSet<>(DexType::slowCompareTo);
+      TreeSet<DexType> synthesized = new TreeSet<>(DexType::compareTo);
       entry.getValue().stream()
           .map(dexProgramClass -> dexProgramClass.type)
           .forEach(synthesized::add);
@@ -358,7 +359,7 @@
             EquivalenceGroup<T> group = groups.get(i);
             // Two equivalence groups in same context type must be distinct otherwise the assignment
             // of the synthetic name will be non-deterministic between the two.
-            assert i == 0 || checkGroupsAreDistict(groups.get(i - 1), group);
+            assert i == 0 || checkGroupsAreDistinct(groups.get(i - 1), group);
             DexType representativeType = createExternalType(context, i, factory);
             equivalences.put(representativeType, group);
           }
@@ -388,7 +389,7 @@
     return groups;
   }
 
-  private static <T extends SyntheticDefinition & Comparable<T>> boolean checkGroupsAreDistict(
+  private static <T extends SyntheticDefinition & Comparable<T>> boolean checkGroupsAreDistinct(
       EquivalenceGroup<T> g1, EquivalenceGroup<T> g2) {
     assert g1.compareTo(g2) != 0;
     return true;
@@ -417,18 +418,24 @@
   }
 
   private static <T extends SyntheticDefinition> Collection<List<T>> computePotentialEquivalences(
-      List<T> definitions, boolean intermediate) {
+      Map<DexType, T> definitions, boolean intermediate) {
+    if (definitions.isEmpty()) {
+      return Collections.emptyList();
+    }
+    Set<DexType> allTypes = definitions.keySet();
+    DexType representative = allTypes.iterator().next();
+    RepresentativeMap map = t -> allTypes.contains(t) ? representative : t;
     Map<HashCode, List<T>> equivalences = new HashMap<>(definitions.size());
-    for (T definition : definitions) {
-      HashCode hash = definition.computeHash(intermediate);
+    for (T definition : definitions.values()) {
+      HashCode hash = definition.computeHash(map, intermediate);
       equivalences.computeIfAbsent(hash, k -> new ArrayList<>()).add(definition);
     }
     return equivalences.values();
   }
 
-  private List<SyntheticMethodDefinition> lookupSyntheticMethodDefinitions(
+  private Map<DexType, SyntheticMethodDefinition> lookupSyntheticMethodDefinitions(
       DexApplication finalApp) {
-    List<SyntheticMethodDefinition> methods = new ArrayList<>(syntheticItems.size());
+    Map<DexType, SyntheticMethodDefinition> methods = new IdentityHashMap<>(syntheticItems.size());
     for (SyntheticReference reference : syntheticItems.values()) {
       SyntheticDefinition definition = reference.lookupDefinition(finalApp::definitionFor);
       if (definition == null || !(definition instanceof SyntheticMethodDefinition)) {
@@ -438,7 +445,7 @@
       }
       SyntheticMethodDefinition method = (SyntheticMethodDefinition) definition;
       if (SyntheticMethodBuilder.isValidSyntheticMethod(method.getMethod().getDefinition())) {
-        methods.add(method);
+        methods.put(method.getHolder().getType(), method);
       } else {
         // Failing this check indicates that an optimization has modified the synthetic in a
         // disruptive way.
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodDefinition.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodDefinition.java
index 8cddcfc..ecc1a81 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodDefinition.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticMethodDefinition.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.graph.DexEncodedMethod;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.utils.structural.RepresentativeMap;
 import com.google.common.hash.HashCode;
 import com.google.common.hash.Hasher;
 import com.google.common.hash.Hashing;
@@ -41,14 +42,14 @@
   }
 
   @Override
-  HashCode computeHash(boolean intermediate) {
+  HashCode computeHash(RepresentativeMap map, boolean intermediate) {
     Hasher hasher = Hashing.sha256().newHasher();
     if (intermediate) {
       // If in intermediate mode, include the context type as sharing is restricted to within a
       // single context.
       hasher.putInt(getContext().getSynthesizingContextType().hashCode());
     }
-    method.getDefinition().hashSyntheticContent(hasher);
+    method.getDefinition().hashSyntheticContent(hasher, map);
     return hasher.hash();
   }
 
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommand.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommand.java
index ce8f565..b11a901 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommand.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommand.java
@@ -4,7 +4,9 @@
 package com.android.tools.r8.tracereferences;
 
 import static com.android.tools.r8.utils.FileUtils.isArchive;
+import static com.android.tools.r8.utils.FileUtils.isClassFile;
 import static com.android.tools.r8.utils.FileUtils.isDexFile;
+import static com.android.tools.r8.utils.InternalOptions.ASM_VERSION;
 
 import com.android.tools.r8.ArchiveClassFileProvider;
 import com.android.tools.r8.ClassFileResourceProvider;
@@ -25,6 +27,7 @@
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringDiagnostic;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import java.io.IOException;
 import java.nio.file.Files;
 import java.nio.file.NoSuchFileException;
@@ -33,6 +36,9 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
 
 @Keep
 public class TraceReferencesCommand {
@@ -96,6 +102,10 @@
     return TraceReferencesCommandParser.parse(args, origin, diagnosticsHandler);
   }
 
+  public static Builder parse(Collection<String> args, Origin origin) {
+    return TraceReferencesCommandParser.parse(args.toArray(new String[args.size()]), origin);
+  }
+
   public boolean isPrintHelp() {
     return printHelp;
   }
@@ -151,6 +161,79 @@
       return this;
     }
 
+    private static String extractClassDescriptor(byte[] data) {
+      class ClassNameExtractor extends ClassVisitor {
+        private String className;
+
+        private ClassNameExtractor() {
+          super(ASM_VERSION);
+        }
+
+        @Override
+        public void visit(
+            int version,
+            int access,
+            String name,
+            String signature,
+            String superName,
+            String[] interfaces) {
+          className = name;
+        }
+
+        String getClassInternalType() {
+          return className;
+        }
+      }
+
+      ClassReader reader = new ClassReader(data);
+      ClassNameExtractor extractor = new ClassNameExtractor();
+      reader.accept(
+          extractor, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+      return "L" + extractor.getClassInternalType() + ";";
+    }
+
+    private static class SingleClassClassFileResourceProvider implements ClassFileResourceProvider {
+      private final String descriptor;
+      private final ProgramResource programResource;
+
+      SingleClassClassFileResourceProvider(Origin origin, byte[] data) {
+        this.descriptor = extractClassDescriptor(data);
+        this.programResource =
+            ProgramResource.fromBytes(origin, Kind.CF, data, ImmutableSet.of(descriptor));
+      }
+
+      @Override
+      public Set<String> getClassDescriptors() {
+        return ImmutableSet.of(descriptor);
+      }
+
+      @Override
+      public ProgramResource getProgramResource(String descriptor) {
+        return descriptor.equals(this.descriptor) ? programResource : null;
+      }
+    }
+
+    private ClassFileResourceProvider singleClassFileClassFileResourceProvider(Path file)
+        throws IOException {
+      return new SingleClassClassFileResourceProvider(
+          new PathOrigin(file), Files.readAllBytes(file));
+    }
+
+    private ProgramResourceProvider singleClassFileProgramResourceProvider(Path file)
+        throws IOException {
+      byte[] bytes = Files.readAllBytes(file);
+      String descriptor = extractClassDescriptor(bytes);
+      return new ProgramResourceProvider() {
+
+        @Override
+        public Collection<ProgramResource> getProgramResources() {
+          return ImmutableList.of(
+              ProgramResource.fromBytes(
+                  new PathOrigin(file), Kind.CF, bytes, ImmutableSet.of(descriptor)));
+        }
+      };
+    }
+
     private void addLibraryOrTargetFile(
         Path file, ImmutableList.Builder<ClassFileResourceProvider> builder) {
       if (!Files.exists(file)) {
@@ -165,6 +248,12 @@
         } catch (IOException e) {
           error(new ExceptionDiagnostic(e, new PathOrigin(file)));
         }
+      } else if (isClassFile(file)) {
+        try {
+          builder.add(singleClassFileClassFileResourceProvider(file));
+        } catch (IOException e) {
+          error(new ExceptionDiagnostic(e));
+        }
       } else {
         error(new StringDiagnostic("Unsupported source file type", new PathOrigin(file)));
       }
@@ -178,6 +267,12 @@
       }
       if (isArchive(file)) {
         traceSourceBuilder.add(ArchiveResourceProvider.fromArchive(file, false));
+      } else if (isClassFile(file)) {
+        try {
+          traceSourceBuilder.add(singleClassFileProgramResourceProvider(file));
+        } catch (IOException e) {
+          error(new ExceptionDiagnostic(e));
+        }
       } else if (isDexFile(file)) {
         traceSourceBuilder.add(
             new ProgramResourceProvider() {
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommandParser.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommandParser.java
index f299459..3a3faeb 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommandParser.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesCommandParser.java
@@ -6,8 +6,10 @@
 import com.android.tools.r8.BaseCompilerCommandParser;
 import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.JdkClassFileProvider;
+import com.android.tools.r8.StringConsumer.FileConsumer;
+import com.android.tools.r8.StringConsumer.WriterConsumer;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.tracereferences.TraceReferencesFormattingConsumer.OutputFormat;
 import com.android.tools.r8.utils.ExceptionDiagnostic;
 import com.android.tools.r8.utils.FlagFile;
 import com.android.tools.r8.utils.StringDiagnostic;
@@ -15,6 +17,7 @@
 import com.google.common.collect.Iterables;
 import java.io.IOException;
 import java.io.PrintStream;
+import java.io.PrintWriter;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -24,29 +27,40 @@
 class TraceReferencesCommandParser {
 
   private static final Set<String> OPTIONS_WITH_PARAMETER =
-      ImmutableSet.of("--lib", "--target", "--source", "--format", "--output");
+      ImmutableSet.of("--lib", "--target", "--source", "--output");
 
   static final String USAGE_MESSAGE =
       String.join(
           "\n",
           Iterables.concat(
               Arrays.asList(
-                  "Usage: tracereferences [options] [@<argfile>]",
-                  " Each <argfile> is a file containing additional arguments (one per line)",
+                  "Usage: tracereferences <command> [<options>] [@<argfile>]",
+                  " Where <command> is one of:",
+                  "  --check                 # Run emitting only diagnostics messages.",
+                  "  --print-usage           # Traced references will be output in the print-usage",
+                  "                          # format.",
+                  "  --keep-rules [<keep-rules-options>]",
+                  "                          # Traced references will be output in the keep-rules",
+                  "                          # format.",
+                  " and each <argfile> is a file containing additional options (one per line)",
                   " and options are:",
                   "  --lib <file|jdk-home>   # Add <file|jdk-home> runtime library.",
                   "  --source <file>         # Add <file> as a source for tracing references.",
                   "  [--target <file>]       # Add <file> as a target for tracing references. When",
                   "                          # target is not specified all references from source",
-                  "                          # outside of library are treated as a missing"
-                      + " references.",
-                  "  [--format printuses|keep|keepallowobfuscation]",
-                  "                          # Format of the output. Default is 'printuses'.",
-                  "  --output <file>         # Output result in <outfile>."),
+                  "                          # outside of library are treated as a missing",
+                  "                          # references.",
+                  "  --output <file>         # Output result in <outfile>. If not passed the",
+                  "                          # result will go to standard out.",
+                  "                          # result will go to standard out."),
               BaseCompilerCommandParser.MAP_DIAGNOSTICS_USAGE_MESSAGE,
               Arrays.asList(
                   "  --version               # Print the version of tracereferences.",
-                  "  --help                  # Print this message.")));
+                  "  --help                  # Print this message.",
+                  " and <keep-rule-options> are:",
+                  "  --allowobfuscation      # Output keep rules with the allowobfuscation",
+                  "                          # modifier (defaults to rules without the"
+                      + " modifier)")));
   /**
    * Parse the tracereferences command-line.
    *
@@ -76,11 +90,30 @@
         .parse(args, origin, TraceReferencesCommand.builder(handler));
   }
 
+  private enum Command {
+    CHECK,
+    PRINTUSAGE,
+    KEEP_RULES;
+  }
+
+  private void checkCommandNotSet(
+      Command command, TraceReferencesCommand.Builder builder, Origin origin) {
+    if (command != null) {
+      builder.error(new StringDiagnostic("Multiple commands specified", origin));
+    }
+  }
+
   private TraceReferencesCommand.Builder parse(
       String[] args, Origin origin, TraceReferencesCommand.Builder builder) {
     String[] expandedArgs = FlagFile.expandFlagFiles(args, builder::error);
     Path output = null;
-    OutputFormat format = null;
+    Command command = null;
+    boolean allowObfuscation = false;
+    if (expandedArgs.length == 0) {
+      builder.error(new StringDiagnostic("Missing command"));
+      return builder;
+    }
+    // Parse options.
     for (int i = 0; i < expandedArgs.length; i++) {
       String arg = expandedArgs[i].trim();
       String nextArg = null;
@@ -97,32 +130,33 @@
         continue;
       } else if (arg.equals("--help")) {
         builder.setPrintHelp(true);
+        return builder;
       } else if (arg.equals("--version")) {
         builder.setPrintVersion(true);
+        return builder;
+      } else if (arg.equals("--check")) {
+        checkCommandNotSet(command, builder, origin);
+        command = Command.CHECK;
+      } else if (arg.equals("--print-usage")) {
+        checkCommandNotSet(command, builder, origin);
+        command = Command.PRINTUSAGE;
+      } else if (arg.equals("--keep-rules")) {
+        checkCommandNotSet(command, builder, origin);
+        command = Command.KEEP_RULES;
+      } else if (arg.equals("--allowobfuscation")) {
+        allowObfuscation = true;
       } else if (arg.equals("--lib")) {
         addLibraryArgument(builder, origin, nextArg);
       } else if (arg.equals("--target")) {
         builder.addTargetFiles(Paths.get(nextArg));
       } else if (arg.equals("--source")) {
         builder.addSourceFiles(Paths.get(nextArg));
-      } else if (arg.equals("--format")) {
-        if (format != null) {
-          builder.error(new StringDiagnostic("--format specified multiple times"));
-        }
-        if (nextArg.equals("printuses")) {
-          format = TraceReferencesFormattingConsumer.OutputFormat.PRINTUSAGE;
-        }
-        if (nextArg.equals("keep")) {
-          format = TraceReferencesFormattingConsumer.OutputFormat.KEEP_RULES;
-        }
-        if (nextArg.equals("keepallowobfuscation")) {
-          format = TraceReferencesFormattingConsumer.OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION;
-        }
-        if (format == null) {
-          builder.error(new StringDiagnostic("Unsupported format '" + nextArg + "'"));
-        }
       } else if (arg.equals("--output")) {
-        output = Paths.get(nextArg);
+        if (output != null) {
+          builder.error(new StringDiagnostic("Option '--output' passed multiple times.", origin));
+        } else {
+          output = Paths.get(nextArg);
+        }
       } else if (arg.startsWith("@")) {
         builder.error(new StringDiagnostic("Recursive @argfiles are not supported: ", origin));
       } else {
@@ -133,28 +167,67 @@
           i += argsConsumed;
           continue;
         }
-        builder.error(new StringDiagnostic("Unsupported argument '" + arg + "'"));
+        builder.error(new StringDiagnostic("Unsupported option '" + arg + "'", origin));
       }
     }
-    if (format == null) {
-      format = TraceReferencesFormattingConsumer.OutputFormat.PRINTUSAGE;
+
+    if (command == null) {
+      builder.error(
+          new StringDiagnostic(
+              "Missing command, specify one of 'check', '--print-usage' or '--keep-rules'",
+              origin));
+      return builder;
     }
-    final Path finalOutput = output;
-    builder.setConsumer(
-        new TraceReferencesFormattingConsumer(format) {
-          @Override
-          public void finished() {
-            PrintStream out = System.out;
-            if (finalOutput != null) {
-              try {
-                out = new PrintStream(Files.newOutputStream(finalOutput));
-              } catch (IOException e) {
-                builder.error(new ExceptionDiagnostic(e));
+
+    if (command == Command.CHECK && output != null) {
+      builder.error(
+          new StringDiagnostic(
+              "Using '--output' requires command '--print-usage' or '--keep-rules'", origin));
+      return builder;
+    }
+
+    if (command != Command.KEEP_RULES && allowObfuscation) {
+      builder.error(
+          new StringDiagnostic(
+              "Using '--allowobfuscation' requires command '--keep-rules'", origin));
+      return builder;
+    }
+
+    switch (command) {
+      case CHECK:
+        builder.setConsumer(TraceReferencesConsumer.emptyConsumer());
+        break;
+      case KEEP_RULES:
+        builder.setConsumer(
+            TraceReferencesKeepRules.builder()
+                .setAllowObfuscation(allowObfuscation)
+                .setOutputConsumer(
+                    output != null
+                        ? new FileConsumer(output)
+                        : new WriterConsumer(null, new PrintWriter(System.out)))
+                .build());
+        break;
+      case PRINTUSAGE:
+        final Path finalOutput = output;
+        builder.setConsumer(
+            new TraceReferencesPrintUsage() {
+              @Override
+              public void finished(DiagnosticsHandler handler) {
+                PrintStream out = System.out;
+                if (finalOutput != null) {
+                  try {
+                    out = new PrintStream(Files.newOutputStream(finalOutput));
+                  } catch (IOException e) {
+                    handler.error(new ExceptionDiagnostic(e));
+                  }
+                }
+                out.print(get());
               }
-            }
-            out.print(get());
-          }
-        });
+            });
+        break;
+      default:
+        throw new Unreachable();
+    }
     return builder;
   }
 
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesConsumer.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesConsumer.java
index 5750fe1..f364c72 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesConsumer.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesConsumer.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.tracereferences;
 
+import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.Keep;
 import com.android.tools.r8.KeepForSubclassing;
 import com.android.tools.r8.references.ClassReference;
@@ -71,22 +72,64 @@
   @Keep
   interface TracedMethod extends TracedReference<MethodReference, MethodAccessFlags> {}
 
-  /** Class has been traced. */
-  void acceptType(TracedClass tracedClass);
+  /**
+   * Callback when class has been traced.
+   *
+   * <p>The consumer is expected not to throw, but instead report any errors via the diagnostics
+   * {@param handler}. If an error is reported via {@param handler} and no exceptions are thrown,
+   * then trace references guaranties to exit with an error.
+   *
+   * @param tracedClass Traced class
+   * @param handler Diagnostics handler for reporting.
+   */
+  void acceptType(TracedClass tracedClass, DiagnosticsHandler handler);
 
-  /** Field has been traced. */
-  void acceptField(TracedField tracedField);
+  /**
+   * Callback when class has been traced.
+   *
+   * <p>The consumer is expected not to throw, but instead report any errors via the diagnostics
+   * {@param handler}. If an error is reported via {@param handler} and no exceptions are thrown,
+   * then trace references guaranties to exit with an error.
+   *
+   * @param tracedField Traced field
+   * @param handler Diagnostics handler for reporting.
+   */
+  void acceptField(TracedField tracedField, DiagnosticsHandler handler);
 
-  /** Method has been traced. */
-  void acceptMethod(TracedMethod tracedMethod);
+  /**
+   * Callback when class has been traced.
+   *
+   * <p>The consumer is expected not to throw, but instead report any errors via the diagnostics
+   * {@param handler}. If an error is reported via {@param handler} and no exceptions are thrown,
+   * then trace references guaranties to exit with an error.
+   *
+   * @param tracedMethod Traced method
+   * @param handler Diagnostics handler for reporting.
+   */
+  void acceptMethod(TracedMethod tracedMethod, DiagnosticsHandler handler);
 
-  /** Package which is required for package private access has been traced. */
-  default void acceptPackage(PackageReference pkg) {}
+  /**
+   * Callback when package which is required for package private access has been traced.
+   *
+   * <p>The consumer is expected not to throw, but instead report any errors via the diagnostics
+   * {@param handler}. If an error is reported via {@param handler} and no exceptions are thrown,
+   * then trace references guaranties to exit with an error.
+   *
+   * @param pkg Traced package
+   * @param handler Diagnostics handler for reporting.
+   */
+  default void acceptPackage(PackageReference pkg, DiagnosticsHandler handler) {}
 
   /**
    * Tracing has finished. There will be no more calls to any of the <code>acceptXXX</code> methods.
+   *
+   * <p>The consumer is expected not to throw, but instead report any errors via the diagnostics
+   * {@param handler}. If an error is reported via {@param handler} and no exceptions are thrown,
+   * then trace references guaranties to exit with an error.
+   *
+   * @param handler Diagnostics handler for reporting.
    */
-  default void finished() {}
+  default void finished(DiagnosticsHandler handler) {}
 
   static TraceReferencesConsumer emptyConsumer() {
     return ForwardingConsumer.EMPTY_CONSUMER;
@@ -105,37 +148,37 @@
     }
 
     @Override
-    public void acceptType(TracedClass tracedClass) {
+    public void acceptType(TracedClass tracedClass, DiagnosticsHandler handler) {
       if (consumer != null) {
-        consumer.acceptType(tracedClass);
+        consumer.acceptType(tracedClass, handler);
       }
     }
 
     @Override
-    public void acceptField(TracedField tracedField) {
+    public void acceptField(TracedField tracedField, DiagnosticsHandler handler) {
       if (consumer != null) {
-        consumer.acceptField(tracedField);
+        consumer.acceptField(tracedField, handler);
       }
     }
 
     @Override
-    public void acceptMethod(TracedMethod tracedMethod) {
+    public void acceptMethod(TracedMethod tracedMethod, DiagnosticsHandler handler) {
       if (consumer != null) {
-        consumer.acceptMethod(tracedMethod);
+        consumer.acceptMethod(tracedMethod, handler);
       }
     }
 
     @Override
-    public void acceptPackage(PackageReference pkg) {
+    public void acceptPackage(PackageReference pkg, DiagnosticsHandler handler) {
       if (consumer != null) {
-        consumer.acceptPackage(pkg);
+        consumer.acceptPackage(pkg, handler);
       }
     }
 
     @Override
-    public void finished() {
+    public void finished(DiagnosticsHandler handler) {
       if (consumer != null) {
-        consumer.finished();
+        consumer.finished(handler);
       }
     }
   }
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesFormattingConsumer.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesFormattingConsumer.java
deleted file mode 100644
index 67167c4..0000000
--- a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesFormattingConsumer.java
+++ /dev/null
@@ -1,79 +0,0 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-package com.android.tools.r8.tracereferences;
-
-import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.references.PackageReference;
-
-class TraceReferencesFormattingConsumer implements TraceReferencesConsumer {
-
-  public enum OutputFormat {
-    /** Format used with the -printusage flag */
-    PRINTUSAGE,
-    /** Keep rules keeping each of the traced references */
-    KEEP_RULES,
-    /**
-     * Keep rules with <code>allowobfuscation</code> modifier keeping each of the traced references
-     */
-    KEEP_RULES_WITH_ALLOWOBFUSCATION
-  }
-
-  private final OutputFormat format;
-  private final TraceReferencesResult.Builder builder = TraceReferencesResult.builder();
-  private boolean finishedCalled = false;
-
-  public TraceReferencesFormattingConsumer(OutputFormat format) {
-    this.format = format;
-  }
-
-  @Override
-  public void acceptType(TracedClass type) {
-    assert !finishedCalled;
-    builder.acceptType(type);
-  }
-
-  @Override
-  public void acceptField(TracedField field) {
-    assert !finishedCalled;
-    builder.acceptField(field);
-  }
-
-  @Override
-  public void acceptMethod(TracedMethod method) {
-    assert !finishedCalled;
-    builder.acceptMethod(method);
-  }
-
-  @Override
-  public void acceptPackage(PackageReference pkg) {
-    assert !finishedCalled;
-    builder.acceptPackage(pkg);
-  }
-
-  @Override
-  public void finished() {
-    assert !finishedCalled;
-    finishedCalled = true;
-  }
-
-  public String get() {
-    TraceReferencesResult result = builder.build();
-    Formatter formatter;
-    switch (format) {
-      case PRINTUSAGE:
-        formatter = new PrintUsesFormatter();
-        break;
-      case KEEP_RULES:
-        formatter = new KeepRuleFormatter(false);
-        break;
-      case KEEP_RULES_WITH_ALLOWOBFUSCATION:
-        formatter = new KeepRuleFormatter(true);
-        break;
-      default:
-        throw new Unreachable();
-    }
-    formatter.format(result);
-    return formatter.get();
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesKeepRules.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesKeepRules.java
new file mode 100644
index 0000000..6940f63
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesKeepRules.java
@@ -0,0 +1,100 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.tracereferences;
+
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.Keep;
+import com.android.tools.r8.StringConsumer;
+import com.android.tools.r8.StringConsumer.FileConsumer;
+import java.nio.file.Path;
+
+/**
+ * Consumer to format the result of running {@link TraceReferences} as keep rules.
+ *
+ * <p>To build an instance of this consumer, use the {@link TraceReferencesKeepRules.Builder} class.
+ * For example:
+ *
+ * <pre>
+ *   TraceReferencesKeepRules consumer = TraceReferencesKeepRules.builder()
+ *     .setAllowObfuscation(true)
+ *     .setOutputPath(Paths.get("references-to-keep.rules"))
+ *     .build();
+ * </pre>
+ */
+@Keep
+public class TraceReferencesKeepRules extends TraceReferencesConsumer.ForwardingConsumer {
+
+  private final TraceReferencesResult.Builder traceReferencesResultBuilder;
+  private final StringConsumer consumer;
+  private final boolean allowObfuscation;
+
+  private TraceReferencesKeepRules(
+      TraceReferencesResult.Builder traceReferencesResultBuilder,
+      StringConsumer consumer,
+      boolean allowObfuscation) {
+    super(traceReferencesResultBuilder);
+    this.traceReferencesResultBuilder = traceReferencesResultBuilder;
+    this.consumer = consumer;
+    this.allowObfuscation = allowObfuscation;
+  }
+
+  /**
+   * Builder for constructing a {@link TraceReferencesKeepRules].
+   *
+   * <p>A builder is obtained by calling {@link TraceReferencesKeepRules#builder}.
+   */
+  @Keep
+  public static class Builder {
+    private StringConsumer consumer;
+    private boolean allowObfuscation;
+
+    /**
+     * Indicate if the generated keep rules should have the <code>allobobfuscation</code> modifier.
+     */
+    public Builder setAllowObfuscation(boolean value) {
+      allowObfuscation = value;
+      return this;
+    }
+
+    /**
+     * Set the output of the keep rules to a file.
+     *
+     * @param output Path to write the output to.
+     */
+    public Builder setOutputPath(Path output) {
+      this.consumer = new FileConsumer(output);
+      return this;
+    }
+
+    /**
+     * Set the output of the keep rules to a {@link com.android.tools.r8.StringConsumer}.
+     *
+     * @param consumer Consumer to send the output to.
+     */
+    public Builder setOutputConsumer(StringConsumer consumer) {
+      this.consumer = consumer;
+      return this;
+    }
+
+    /** Build the {@link TraceReferencesKeepRules} instance. */
+    public TraceReferencesKeepRules build() {
+      return new TraceReferencesKeepRules(
+          TraceReferencesResult.builder(), consumer, allowObfuscation);
+    }
+  }
+
+  /** Create a builder for constructing an instance of {@link TraceReferencesKeepRules.Builder}. */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  public void finished(DiagnosticsHandler handler) {
+    super.finished(handler);
+    Formatter formatter = new KeepRuleFormatter(allowObfuscation);
+    formatter.format(traceReferencesResultBuilder.build());
+    consumer.accept(formatter.get(), handler);
+    consumer.finished(handler);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesPrintUsage.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesPrintUsage.java
new file mode 100644
index 0000000..80e8096
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesPrintUsage.java
@@ -0,0 +1,50 @@
+// 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.tracereferences;
+
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.references.PackageReference;
+
+class TraceReferencesPrintUsage implements TraceReferencesConsumer {
+
+  private final TraceReferencesResult.Builder traceReferencesResultBuilder =
+      TraceReferencesResult.builder();
+  private boolean finishedCalled = false;
+
+  @Override
+  public void acceptType(TracedClass type, DiagnosticsHandler handler) {
+    assert !finishedCalled;
+    traceReferencesResultBuilder.acceptType(type, handler);
+  }
+
+  @Override
+  public void acceptField(TracedField field, DiagnosticsHandler handler) {
+    assert !finishedCalled;
+    traceReferencesResultBuilder.acceptField(field, handler);
+  }
+
+  @Override
+  public void acceptMethod(TracedMethod method, DiagnosticsHandler handler) {
+    assert !finishedCalled;
+    traceReferencesResultBuilder.acceptMethod(method, handler);
+  }
+
+  @Override
+  public void acceptPackage(PackageReference pkg, DiagnosticsHandler handler) {
+    assert !finishedCalled;
+    traceReferencesResultBuilder.acceptPackage(pkg, handler);
+  }
+
+  @Override
+  public void finished(DiagnosticsHandler handler) {
+    assert !finishedCalled;
+    finishedCalled = true;
+  }
+
+  public String get() {
+    Formatter formatter = new PrintUsesFormatter();
+    formatter.format(traceReferencesResultBuilder.build());
+    return formatter.get();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesResult.java b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesResult.java
index f8dab44..e40f3d7 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesResult.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/TraceReferencesResult.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.tracereferences;
 
+import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.references.ClassReference;
 import com.android.tools.r8.references.FieldReference;
 import com.android.tools.r8.references.MethodReference;
@@ -48,7 +49,7 @@
     private final Set<PackageReference> keepPackageNames = new HashSet<>();
 
     @Override
-    public void acceptType(TracedClass tracedClass) {
+    public void acceptType(TracedClass tracedClass, DiagnosticsHandler handler) {
       types.add(tracedClass);
       if (tracedClass.isMissingDefinition()) {
         this.missingDefinition.add(tracedClass.getReference());
@@ -56,7 +57,7 @@
     }
 
     @Override
-    public void acceptField(TracedField tracedField) {
+    public void acceptField(TracedField tracedField, DiagnosticsHandler handler) {
       FieldReference field = tracedField.getReference();
       fields.computeIfAbsent(field.getHolderClass(), k -> new HashSet<>()).add(tracedField);
       if (tracedField.isMissingDefinition()) {
@@ -65,7 +66,7 @@
     }
 
     @Override
-    public void acceptMethod(TracedMethod tracedMethod) {
+    public void acceptMethod(TracedMethod tracedMethod, DiagnosticsHandler handler) {
       MethodReference method = tracedMethod.getReference();
       methods.computeIfAbsent(method.getHolderClass(), k -> new HashSet<>()).add(tracedMethod);
       if (tracedMethod.isMissingDefinition()) {
@@ -74,12 +75,12 @@
     }
 
     @Override
-    public void acceptPackage(PackageReference pkg) {
+    public void acceptPackage(PackageReference pkg, DiagnosticsHandler handler) {
       keepPackageNames.add(pkg);
     }
 
     @Override
-    public void finished() {}
+    public void finished(DiagnosticsHandler handler) {}
 
     TraceReferencesResult build() {
       missingDefinition.forEach(
diff --git a/src/main/java/com/android/tools/r8/tracereferences/Tracer.java b/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
index 72db701..8b571a8 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/Tracer.java
@@ -249,7 +249,7 @@
       clazz.forEachProgramMethod(useCollector::registerMethod);
       clazz.forEachField(useCollector::registerField);
     }
-    consumer.finished();
+    consumer.finished(diagnostics);
     useCollector.reportMissingDefinitions();
   }
 
@@ -287,10 +287,11 @@
       TracedClassImpl tracedClass = new TracedClassImpl(type, clazz);
       checkMissingDefinition(tracedClass);
       if (isTargetType(type) || tracedClass.isMissingDefinition()) {
-        consumer.acceptType(tracedClass);
+        consumer.acceptType(tracedClass, diagnostics);
         if (!tracedClass.isMissingDefinition()
             && clazz.accessFlags.isVisibilityDependingOnPackage()) {
-          consumer.acceptPackage(Reference.packageFromString(clazz.type.getPackageName()));
+          consumer.acceptPackage(
+              Reference.packageFromString(clazz.type.getPackageName()), diagnostics);
         }
       }
     }
@@ -305,10 +306,11 @@
       TracedFieldImpl tracedField = new TracedFieldImpl(field, baseField);
       checkMissingDefinition(tracedField);
       if (isTargetType(field.holder) || tracedField.isMissingDefinition()) {
-        consumer.acceptField(tracedField);
+        consumer.acceptField(tracedField, diagnostics);
         if (!tracedField.isMissingDefinition()
             && baseField.accessFlags.isVisibilityDependingOnPackage()) {
-          consumer.acceptPackage(Reference.packageFromString(baseField.holder().getPackageName()));
+          consumer.acceptPackage(
+              Reference.packageFromString(baseField.holder().getPackageName()), diagnostics);
         }
       }
     }
@@ -323,11 +325,12 @@
       DexEncodedMethod definition = method.lookupOnClass(holder);
       TracedMethodImpl tracedMethod = new TracedMethodImpl(method, definition);
       if (isTargetType(method.holder) || tracedMethod.isMissingDefinition()) {
-        consumer.acceptMethod(tracedMethod);
+        consumer.acceptMethod(tracedMethod, diagnostics);
         checkMissingDefinition(tracedMethod);
         if (!tracedMethod.isMissingDefinition()
             && definition.accessFlags.isVisibilityDependingOnPackage()) {
-          consumer.acceptPackage(Reference.packageFromString(definition.holder().getPackageName()));
+          consumer.acceptPackage(
+              Reference.packageFromString(definition.holder().getPackageName()), diagnostics);
         }
       }
     }
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java b/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java
index f15c049..6e501fc 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApiLevel.java
@@ -4,14 +4,12 @@
 package com.android.tools.r8.utils;
 
 import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.utils.structural.Ordered;
 import java.util.Arrays;
-import java.util.Comparator;
 import java.util.List;
 
-/**
- * Android API level description
- */
-public enum AndroidApiLevel {
+/** Android API level description */
+public enum AndroidApiLevel implements Ordered<AndroidApiLevel> {
   B(1),
   B_1_1(2),
   C(3),
@@ -70,9 +68,7 @@
   }
 
   public static List<AndroidApiLevel> getAndroidApiLevelsSorted() {
-    List<AndroidApiLevel> androidApiLevels = Arrays.asList(AndroidApiLevel.values());
-    androidApiLevels.sort(Comparator.comparingInt(AndroidApiLevel::getLevel));
-    return androidApiLevels;
+    return Arrays.asList(AndroidApiLevel.values());
   }
 
   public static AndroidApiLevel getMinAndroidApiLevel(DexVersion dexVersion) {
@@ -157,20 +153,4 @@
         return LATEST;
     }
   }
-
-  public boolean isLessThan(AndroidApiLevel other) {
-    return this.level < other.getLevel();
-  }
-
-  public boolean isLessThanOrEqualTo(AndroidApiLevel other) {
-    return this.level <= other.getLevel();
-  }
-
-  public boolean isGreaterThan(AndroidApiLevel other) {
-    return other.isLessThan(this);
-  }
-
-  public boolean isGreaterThanOrEqualTo(AndroidApiLevel other) {
-    return other.isLessThanOrEqualTo(this);
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/AndroidApp.java b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
index c426a09..4478209 100644
--- a/src/main/java/com/android/tools/r8/utils/AndroidApp.java
+++ b/src/main/java/com/android/tools/r8/utils/AndroidApp.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.DexFilePerClassFileConsumer;
 import com.android.tools.r8.DexIndexedConsumer;
 import com.android.tools.r8.DirectoryClassFileProvider;
+import com.android.tools.r8.DumpOptions;
 import com.android.tools.r8.FeatureSplit;
 import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.ProgramResource;
@@ -81,13 +82,16 @@
  */
 public class AndroidApp {
 
+  // TODO(b/172887664): Move to DumpOptions and capitalize.
   private static final String dumpVersionFileName = "r8-version";
   private static final String dumpBuildPropertiesFileName = "build.properties";
   private static final String dumpDesugaredLibraryFileName = "desugared-library.json";
+  private static final String dumpMainDexListResourceFileName = "main-dex-list.txt";
   private static final String dumpProgramFileName = "program.jar";
   private static final String dumpClasspathFileName = "classpath.jar";
   private static final String dumpLibraryFileName = "library.jar";
   private static final String dumpConfigFileName = "proguard.config";
+  private static final String dumpInputConfigFileName = "proguard_input.config";
 
   private static Map<FeatureSplit, String> dumpFeatureSplitFileNames(
       FeatureSplitConfiguration featureSplitConfiguration) {
@@ -454,7 +458,10 @@
     return programResourcesMainDescriptor.get(resource);
   }
 
-  public void dump(Path output, InternalOptions options) {
+  public void dump(Path output, DumpOptions options, Reporter reporter, DexItemFactory factory) {
+    if (options == null) {
+      return;
+    }
     int nextDexIndex = 0;
     OpenOption[] openOptions =
         new OpenOption[] {StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING};
@@ -462,50 +469,60 @@
       writeToZipStream(
           out, dumpVersionFileName, Version.getVersionString().getBytes(), ZipEntry.DEFLATED);
       writeToZipStream(
-          out,
-          dumpBuildPropertiesFileName,
-          getBuildPropertiesContents(options).getBytes(),
-          ZipEntry.DEFLATED);
-      if (options.desugaredLibraryConfiguration.getJsonSource() != null) {
+          out, dumpBuildPropertiesFileName, options.dumpOptions().getBytes(), ZipEntry.DEFLATED);
+      if (options.getDesugaredLibraryJsonSource() != null) {
         writeToZipStream(
             out,
             dumpDesugaredLibraryFileName,
-            options.desugaredLibraryConfiguration.getJsonSource().getBytes(),
+            options.getDesugaredLibraryJsonSource().getBytes(),
             ZipEntry.DEFLATED);
-        if (options.dumpInputToFile != null) {
-          options.reporter.warning(
+        if (options.dumpInputToFile()) {
+          reporter.warning(
               "Dumping a compilation with desugared library on a file may prevent reproduction,"
                   + " use dumpInputToDirectory property instead.");
         }
       }
-      if (options.getProguardConfiguration() != null) {
-        String proguardConfig = options.getProguardConfiguration().getParsedConfiguration();
+      if (options.getParsedProguardConfiguration() != null) {
+        String proguardConfig = options.getParsedProguardConfiguration();
         writeToZipStream(out, dumpConfigFileName, proguardConfig.getBytes(), ZipEntry.DEFLATED);
       }
+      if (proguardMapInputData != null) {
+        reporter.warning(
+            "Dumping proguard map input data may have side effects due to I/O on Paths.");
+        writeToZipStream(
+            out,
+            dumpInputConfigFileName,
+            proguardMapInputData.getString().getBytes(),
+            ZipEntry.DEFLATED);
+      }
+      if (hasMainDexList()) {
+        List<String> mainDexList = new ArrayList<>();
+        if (hasMainDexListResources()) {
+          reporter.warning(
+              "Dumping main dex list resources may have side effects due to I/O on Paths.");
+          for (StringResource mainDexListResource : getMainDexListResources()) {
+            mainDexList.add(mainDexListResource.getString());
+          }
+        }
+        mainDexList.addAll(getMainDexClasses());
+        String join = StringUtils.join(mainDexList, "\n");
+        writeToZipStream(out, dumpMainDexListResourceFileName, join.getBytes(), ZipEntry.DEFLATED);
+      }
       nextDexIndex =
           dumpProgramResources(
               dumpProgramFileName,
-              dumpFeatureSplitFileNames(options.featureSplitConfiguration),
+              options.getFeatureSplitConfiguration(),
               nextDexIndex,
               out,
-              options);
+              reporter,
+              factory);
       nextDexIndex = dumpClasspathResources(nextDexIndex, out);
       nextDexIndex = dumpLibraryResources(nextDexIndex, out);
     } catch (IOException | ResourceException e) {
-      throw options.reporter.fatalError(new ExceptionDiagnostic(e));
+      throw reporter.fatalError(new ExceptionDiagnostic(e));
     }
   }
 
-  private String getBuildPropertiesContents(InternalOptions options) {
-    return String.join(
-        "\n",
-        ImmutableList.of(
-            "mode=" + (options.debug ? "debug" : "release"),
-            "min-api=" + options.minApiLevel,
-            "tree-shaking=" + options.isTreeShakingEnabled(),
-            "minification=" + options.isMinificationEnabled()));
-  }
-
   private int dumpLibraryResources(int nextDexIndex, ZipOutputStream out)
       throws IOException, ResourceException {
     nextDexIndex =
@@ -547,19 +564,22 @@
 
   private int dumpProgramResources(
       String archiveName,
-      Map<FeatureSplit, String> featureSplitArchiveNames,
+      FeatureSplitConfiguration featureSplitConfiguration,
       int nextDexIndex,
       ZipOutputStream out,
-      InternalOptions options)
+      Reporter reporter,
+      DexItemFactory dexItemFactory)
       throws IOException, ResourceException {
+
+    Map<FeatureSplit, String> featureSplitArchiveNames =
+        dumpFeatureSplitFileNames(featureSplitConfiguration);
     Map<FeatureSplit, ByteArrayOutputStream> featureSplitArchiveByteStreams =
         new IdentityHashMap<>();
     Map<FeatureSplit, ZipOutputStream> featureSplitArchiveOutputStreams = new IdentityHashMap<>();
     try {
-      DexItemFactory dexItemFactory = options.dexItemFactory();
-      FeatureSplitConfiguration featureSplitConfiguration = options.featureSplitConfiguration;
       ClassToFeatureSplitMap classToFeatureSplitMap =
-          ClassToFeatureSplitMap.createInitialClassToFeatureSplitMap(options);
+          ClassToFeatureSplitMap.createInitialClassToFeatureSplitMap(
+              dexItemFactory, featureSplitConfiguration, reporter);
       if (featureSplitConfiguration != null) {
         for (FeatureSplit featureSplit : featureSplitConfiguration.getFeatureSplits()) {
           ByteArrayOutputStream archiveByteStream = new ByteArrayOutputStream();
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 c74ed38..9116bb5 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -11,6 +11,7 @@
 import com.android.tools.r8.DesugarGraphConsumer;
 import com.android.tools.r8.DexFilePerClassFileConsumer;
 import com.android.tools.r8.DexIndexedConsumer;
+import com.android.tools.r8.DumpOptions;
 import com.android.tools.r8.FeatureSplit;
 import com.android.tools.r8.ProgramConsumer;
 import com.android.tools.r8.StringConsumer;
@@ -40,6 +41,7 @@
 import com.android.tools.r8.graph.EnumValueInfoMapCollection;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.classmerging.HorizontallyMergedLambdaClasses;
+import com.android.tools.r8.graph.classmerging.StaticallyMergedClasses;
 import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
 import com.android.tools.r8.inspector.internal.InspectorImpl;
@@ -227,6 +229,7 @@
   public boolean enableNeverMergePrefixes = true;
   public Set<String> neverMergePrefixes = ImmutableSet.of("j$.");
 
+  public boolean classpathInterfacesMayHaveStaticInitialization = false;
   public boolean libraryInterfacesMayHaveStaticInitialization = false;
 
   // Optimization-related flags. These should conform to -dontoptimize and disableAllOptimizations.
@@ -235,6 +238,7 @@
       System.getProperty("com.android.tools.r8.fieldBitAccessAnalysis") != null;
   public boolean enableStaticClassMerging = true;
   public boolean enableHorizontalClassMerging = true;
+  public boolean enableHorizontalClassMergingOfKotlinLambdas = true;
   public boolean enableVerticalClassMerging = true;
   public boolean enableArgumentRemoval = true;
   public boolean enableUnusedInterfaceRemoval = true;
@@ -333,6 +337,9 @@
   // Boolean value indicating that byte code pass through may be enabled.
   public boolean enableCfByteCodePassThrough = false;
 
+  // Contain the contents of the build properties file from the compiler command.
+  public DumpOptions dumpOptions;
+
   // Hidden marker for classes.dex
   private boolean hasMarker = false;
   private Marker marker;
@@ -1176,6 +1183,8 @@
     // b/172508621
     public boolean sortMethodsOnCfOutput =
         System.getProperty("com.android.tools.r8.sortMethodsOnCfWriting") != null;
+    public boolean allowDesugaredInput =
+        System.getProperty("com.android.tools.r8.allowDesugaredInput") != null;
   }
 
   public static class CallSiteOptimizationOptions {
@@ -1274,6 +1283,9 @@
     public BiConsumer<DexItemFactory, HorizontallyMergedLambdaClasses>
         horizontallyMergedLambdaClassesConsumer = ConsumerUtils.emptyBiConsumer();
 
+    public BiConsumer<DexItemFactory, StaticallyMergedClasses> staticallyMergedClassesConsumer =
+        ConsumerUtils.emptyBiConsumer();
+
     public BiConsumer<DexItemFactory, EnumValueInfoMapCollection> unboxedEnumsConsumer =
         ConsumerUtils.emptyBiConsumer();
 
@@ -1338,6 +1350,8 @@
     public boolean forceIRForCfToCfDesugar =
         System.getProperty("com.android.tools.r8.forceIRForCfToCfDesugar") != null;
     public boolean disableMappingToOriginalProgramVerification = false;
+    public boolean allowInvalidCfAccessFlags =
+        System.getProperty("com.android.tools.r8.allowInvalidCfAccessFlags") != null;
 
     // Flag to allow processing of resources in D8. A data resource consumer still needs to be
     // specified.
@@ -1425,7 +1439,7 @@
 
   public boolean canUseConstClassInstructions(CfVersion cfVersion) {
     assert isGeneratingClassFiles();
-    return cfVersion.isGreaterThanOrEqual(requiredCfVersionForConstClassInstructions());
+    return cfVersion.isGreaterThanOrEqualTo(requiredCfVersionForConstClassInstructions());
   }
 
   public CfVersion requiredCfVersionForConstClassInstructions() {
diff --git a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
index f2358c5..51a3778 100644
--- a/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/utils/LineNumberOptimizer.java
@@ -308,7 +308,7 @@
 
       // Then process the methods, ordered by renamed name.
       List<DexString> renamedMethodNames = new ArrayList<>(methodsByRenamedName.keySet());
-      renamedMethodNames.sort(DexString::slowCompareTo);
+      renamedMethodNames.sort(DexString::compareTo);
       for (DexString methodName : renamedMethodNames) {
         List<DexEncodedMethod> methods = methodsByRenamedName.get(methodName);
         if (methods.size() > 1) {
diff --git a/src/main/java/com/android/tools/r8/utils/ListUtils.java b/src/main/java/com/android/tools/r8/utils/ListUtils.java
index 51821c0..d3c376f 100644
--- a/src/main/java/com/android/tools/r8/utils/ListUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ListUtils.java
@@ -7,6 +7,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
+import java.util.Optional;
 import java.util.function.BiFunction;
 import java.util.function.Function;
 import java.util.function.Predicate;
@@ -47,13 +48,12 @@
     return result;
   }
 
-  public static <T> boolean removeFirstMatch(List<T> list, Predicate<T> element) {
+  public static <T> Optional<T> removeFirstMatch(List<T> list, Predicate<T> element) {
     int index = firstIndexMatching(list, element);
     if (index >= 0) {
-      list.remove(index);
-      return true;
+      return Optional.of(list.remove(index));
     }
-    return false;
+    return Optional.empty();
   }
 
   public static <T extends Comparable<T>> boolean verifyListIsOrdered(List<T> list) {
diff --git a/src/main/java/com/android/tools/r8/utils/ObjectUtils.java b/src/main/java/com/android/tools/r8/utils/ObjectUtils.java
index 1f3ad9d..216f19b 100644
--- a/src/main/java/com/android/tools/r8/utils/ObjectUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ObjectUtils.java
@@ -22,4 +22,14 @@
     }
     return null;
   }
+
+  /**
+   * If the object is null return the default value, otherwise compute the function with the value.
+   */
+  public static <S, T> T mapNotNullOrDefault(S object, T def, Function<? super S, ? extends T> fn) {
+    if (object != null) {
+      return fn.apply(object);
+    }
+    return def;
+  }
 }
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 f33ed6f..400ef0c 100644
--- a/src/main/java/com/android/tools/r8/utils/ZipUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/ZipUtils.java
@@ -23,6 +23,7 @@
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
+import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -68,7 +69,11 @@
   }
 
   public static void iter(String zipFileStr, OnEntryHandler handler) throws IOException {
-    try (ZipFile zipFile = new ZipFile(zipFileStr, StandardCharsets.UTF_8)) {
+    iter(Paths.get(zipFileStr), handler);
+  }
+
+  public static void iter(Path zipFilePath, OnEntryHandler handler) throws IOException {
+    try (ZipFile zipFile = new ZipFile(zipFilePath.toFile(), StandardCharsets.UTF_8)) {
       final Enumeration<? extends ZipEntry> entries = zipFile.entries();
       while (entries.hasMoreElements()) {
         ZipEntry entry = entries.nextElement();
@@ -79,6 +84,12 @@
     }
   }
 
+  public static byte[] readSingleEntry(Path zipFilePath, String name) throws IOException {
+    try (ZipFile zipFile = new ZipFile(zipFilePath.toFile(), StandardCharsets.UTF_8)) {
+      return ByteStreams.toByteArray(zipFile.getInputStream(zipFile.getEntry(name)));
+    }
+  }
+
   public static void zip(Path zipFile, Path inputDirectory) throws IOException {
     List<Path> files =
         Files.walk(inputDirectory)
@@ -226,4 +237,8 @@
       return zipFile;
     }
   }
+
+  public static String zipEntryNameForClass(Class<?> clazz) {
+    return DescriptorUtils.getClassBinaryName(clazz) + CLASS_EXTENSION;
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneMap.java
index 7410339..361b761 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneMap.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.utils.collections;
 
-import java.util.Collection;
 import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.LinkedHashSet;
@@ -14,13 +13,38 @@
 
 public class BidirectionalManyToOneMap<K, V> {
 
-  private final Map<K, V> backing = new IdentityHashMap<>();
-  private final Map<V, Set<K>> inverse = new IdentityHashMap<>();
+  private final Map<K, V> backing;
+  private final Map<V, Set<K>> inverse;
+
+  public BidirectionalManyToOneMap() {
+    this(new IdentityHashMap<>(), new IdentityHashMap<>());
+  }
+
+  private BidirectionalManyToOneMap(Map<K, V> backing, Map<V, Set<K>> inverse) {
+    this.backing = backing;
+    this.inverse = inverse;
+  }
+
+  public static <K, V> BidirectionalManyToOneMap<K, V> empty() {
+    return new BidirectionalManyToOneMap<>(Collections.emptyMap(), Collections.emptyMap());
+  }
+
+  public boolean containsKey(K key) {
+    return backing.containsKey(key);
+  }
+
+  public boolean containsValue(V value) {
+    return inverse.containsKey(value);
+  }
 
   public void forEach(BiConsumer<Set<K>, V> consumer) {
     inverse.forEach((value, keys) -> consumer.accept(keys, value));
   }
 
+  public V get(K key) {
+    return backing.get(key);
+  }
+
   public V getOrDefault(K key, V value) {
     return backing.getOrDefault(key, value);
   }
@@ -64,13 +88,25 @@
     }
   }
 
+  public Set<K> removeValue(V value) {
+    Set<K> keys = inverse.remove(value);
+    if (keys == null) {
+      return Collections.emptySet();
+    }
+    for (K key : keys) {
+      V removedValue = backing.remove(key);
+      assert removedValue == value;
+    }
+    return keys;
+  }
+
   public void put(K key, V value) {
     remove(key);
     backing.put(key, value);
     inverse.computeIfAbsent(value, ignore -> new LinkedHashSet<>()).add(key);
   }
 
-  public Collection<V> values() {
-    return backing.values();
+  public Set<V> values() {
+    return inverse.keySet();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodSet.java b/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodSet.java
index 2412cfd..f058bf1 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodSet.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodSet.java
@@ -53,6 +53,12 @@
     return result;
   }
 
+  public static ProgramMethodSet create(ProgramMethodSet methodSet) {
+    ProgramMethodSet newMethodSet = create();
+    newMethodSet.addAll(methodSet);
+    return newMethodSet;
+  }
+
   public static ProgramMethodSet createConcurrent() {
     return new ProgramMethodSet(ConcurrentHashMap::new);
   }
diff --git a/src/main/java/com/android/tools/r8/utils/collections/SortedProgramMethodSet.java b/src/main/java/com/android/tools/r8/utils/collections/SortedProgramMethodSet.java
index d902126..d625aae 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/SortedProgramMethodSet.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/SortedProgramMethodSet.java
@@ -41,13 +41,13 @@
 
   public static SortedProgramMethodSet create(ForEachable<ProgramMethod> methods) {
     SortedProgramMethodSet result =
-        new SortedProgramMethodSet(() -> new TreeMap<>(DexMethod::slowCompareTo));
+        new SortedProgramMethodSet(() -> new TreeMap<>(DexMethod::compareTo));
     methods.forEach(result::add);
     return result;
   }
 
   public static SortedProgramMethodSet createConcurrent() {
-    return new SortedProgramMethodSet(() -> new ConcurrentSkipListMap<>(DexMethod::slowCompareTo));
+    return new SortedProgramMethodSet(() -> new ConcurrentSkipListMap<>(DexMethod::compareTo));
   }
 
   public static SortedProgramMethodSet empty() {
@@ -64,7 +64,7 @@
   @Override
   public Set<DexEncodedMethod> toDefinitionSet() {
     Comparator<DexEncodedMethod> comparator =
-        (x, y) -> x.getReference().slowCompareTo(y.getReference());
+        (x, y) -> x.getReference().compareTo(y.getReference());
     Set<DexEncodedMethod> definitions = new TreeSet<>(comparator);
     forEach(method -> definitions.add(method.getDefinition()));
     return definitions;
diff --git a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitor.java b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitor.java
new file mode 100644
index 0000000..832d172
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitor.java
@@ -0,0 +1,39 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils.structural;
+
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
+import java.util.Comparator;
+
+/** Base class for a visitor implementing compareTo on a structural item. */
+public abstract class CompareToVisitor {
+
+  public abstract void visitBool(boolean value1, boolean value2);
+
+  public abstract void visitInt(int value1, int value2);
+
+  public abstract void visitDexString(
+      DexString string1, DexString string2, Comparator<DexString> comparator);
+
+  public abstract void visitDexType(DexType type1, DexType type2);
+
+  public abstract void visitDexTypeList(DexTypeList types1, DexTypeList types2);
+
+  public void visitDexField(DexField field1, DexField field2) {
+    visit(field1, field2, field1.getStructuralAccept());
+  }
+
+  public void visitDexMethod(DexMethod method1, DexMethod method2) {
+    visit(method1, method2, method1.getStructuralAccept());
+  }
+
+  public abstract <S> void visit(S item1, S item2, StructuralAccept<S> accept);
+
+  @Deprecated
+  public abstract <S> void visit(S item1, S item2, Comparator<S> comparator);
+}
diff --git a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorBase.java b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorBase.java
new file mode 100644
index 0000000..b7d016a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorBase.java
@@ -0,0 +1,132 @@
+// 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.utils.structural;
+
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexTypeList;
+import com.android.tools.r8.utils.structural.StructuralItem.CompareToAccept;
+import com.android.tools.r8.utils.structural.StructuralItem.HashingAccept;
+import java.util.Comparator;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.ToIntFunction;
+
+/** Base class to share most visiting methods */
+public abstract class CompareToVisitorBase extends CompareToVisitor {
+
+  private int order = 0;
+
+  public final boolean stillEqual() {
+    return order == 0;
+  }
+
+  public final int getOrder() {
+    return order;
+  }
+
+  public final void setOrder(int order) {
+    this.order = order;
+  }
+
+  @Override
+  public final void visitBool(boolean value1, boolean value2) {
+    if (stillEqual()) {
+      setOrder(Boolean.compare(value1, value2));
+    }
+  }
+
+  @Override
+  public final void visitInt(int value1, int value2) {
+    if (stillEqual()) {
+      setOrder(Integer.compare(value1, value2));
+    }
+  }
+
+  @Override
+  public final void visitDexString(
+      DexString string1, DexString string2, Comparator<DexString> comparator) {
+    if (stillEqual()) {
+      setOrder(comparator.compare(string1, string2));
+    }
+  }
+
+  @Override
+  public final void visitDexTypeList(DexTypeList types1, DexTypeList types2) {
+    // Comparison is lexicographic with comparisons between items prior to the length of the lists.
+    if (stillEqual()) {
+      int length = Math.min(types1.size(), types2.size());
+      for (int i = 0; i < length && stillEqual(); i++) {
+        visitDexType(types1.values[i], types2.values[i]);
+      }
+      if (stillEqual()) {
+        visitInt(types1.size(), types2.size());
+      }
+    }
+  }
+
+  @Override
+  public final <S> void visit(S item1, S item2, Comparator<S> comparator) {
+    if (stillEqual()) {
+      setOrder(comparator.compare(item1, item2));
+    }
+  }
+
+  @Override
+  public final <S> void visit(S item1, S item2, StructuralAccept<S> accept) {
+    if (stillEqual()) {
+      accept.accept(new ItemSpecification<>(item1, item2, this));
+    }
+  }
+
+  private static class ItemSpecification<T>
+      extends StructuralSpecification<T, ItemSpecification<T>> {
+
+    private final CompareToVisitorBase parent;
+    private final T item1;
+    private final T item2;
+
+    private ItemSpecification(T item1, T item2, CompareToVisitorBase parent) {
+      this.item1 = item1;
+      this.item2 = item2;
+      this.parent = parent;
+    }
+
+    @Override
+    public ItemSpecification<T> withAssert(Predicate<T> predicate) {
+      assert predicate.test(item1);
+      assert predicate.test(item2);
+      return this;
+    }
+
+    @Override
+    public ItemSpecification<T> withBool(Predicate<T> getter) {
+      parent.visitBool(getter.test(item1), getter.test(item2));
+      return this;
+    }
+
+    @Override
+    public ItemSpecification<T> withInt(ToIntFunction<T> getter) {
+      parent.visitInt(getter.applyAsInt(item1), getter.applyAsInt(item2));
+      return this;
+    }
+
+    @Override
+    public <S> ItemSpecification<T> withConditionalCustomItem(
+        Predicate<T> predicate,
+        Function<T, S> getter,
+        CompareToAccept<S> compare,
+        HashingAccept<S> hasher) {
+      if (parent.stillEqual()) {
+        boolean test1 = predicate.test(item1);
+        boolean test2 = predicate.test(item2);
+        if (test1 && test2) {
+          compare.accept(getter.apply(item1), getter.apply(item2), parent);
+        } else {
+          parent.visitBool(test1, test2);
+        }
+      }
+      return this;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithNamingLens.java b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithNamingLens.java
new file mode 100644
index 0000000..a16575d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithNamingLens.java
@@ -0,0 +1,63 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils.structural;
+
+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.naming.NamingLens;
+import com.android.tools.r8.utils.structural.StructuralItem.CompareToAccept;
+
+public class CompareToVisitorWithNamingLens extends CompareToVisitorBase {
+
+  public static <T> int run(T item1, T item2, NamingLens namingLens, StructuralAccept<T> visit) {
+    return run(item1, item2, namingLens, (i1, i2, visitor) -> visitor.visit(i1, i2, visit));
+  }
+
+  public static <T> int run(
+      T item1, T item2, NamingLens namingLens, CompareToAccept<T> compareToAccept) {
+    CompareToVisitorWithNamingLens state = new CompareToVisitorWithNamingLens(namingLens);
+    compareToAccept.accept(item1, item2, state);
+    return state.getOrder();
+  }
+
+  private final NamingLens namingLens;
+
+  public CompareToVisitorWithNamingLens(NamingLens namingLens) {
+    this.namingLens = namingLens;
+  }
+
+  @Override
+  public void visitDexType(DexType type1, DexType type2) {
+    if (stillEqual()) {
+      namingLens.lookupDescriptor(type1).acceptCompareTo(namingLens.lookupDescriptor(type2), this);
+    }
+  }
+
+  @Override
+  public void visitDexField(DexField field1, DexField field2) {
+    if (stillEqual()) {
+      field1.holder.acceptCompareTo(field2.holder, this);
+      if (stillEqual()) {
+        namingLens.lookupName(field1).acceptCompareTo(namingLens.lookupName(field2), this);
+        if (stillEqual()) {
+          field1.type.acceptCompareTo(field2.type, this);
+        }
+      }
+    }
+  }
+
+  @Override
+  public void visitDexMethod(DexMethod method1, DexMethod method2) {
+    if (stillEqual()) {
+      method1.holder.acceptCompareTo(method2.holder, this);
+      if (stillEqual()) {
+        namingLens.lookupName(method1).acceptCompareTo(namingLens.lookupName(method2), this);
+        if (stillEqual()) {
+          method1.proto.acceptCompareTo(method2.proto, this);
+        }
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithTypeEquivalence.java b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithTypeEquivalence.java
new file mode 100644
index 0000000..f77ab53
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorWithTypeEquivalence.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.utils.structural;
+
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.structural.StructuralItem.CompareToAccept;
+
+public class CompareToVisitorWithTypeEquivalence extends CompareToVisitorBase {
+
+  public static <T> int run(T item1, T item2, RepresentativeMap map, StructuralAccept<T> visit) {
+    return run(item1, item2, map, (i1, i2, visitor) -> visitor.visit(i1, i2, visit));
+  }
+
+  public static <T> int run(
+      T item1, T item2, RepresentativeMap map, CompareToAccept<T> compareToAccept) {
+    CompareToVisitorWithTypeEquivalence state = new CompareToVisitorWithTypeEquivalence(map);
+    compareToAccept.accept(item1, item2, state);
+    return state.getOrder();
+  }
+
+  private final RepresentativeMap representatives;
+
+  public CompareToVisitorWithTypeEquivalence(RepresentativeMap representatives) {
+    this.representatives = representatives;
+  }
+
+  @Override
+  public void visitDexType(DexType type1, DexType type2) {
+    if (stillEqual()) {
+      DexType repr1 = representatives.getRepresentative(type1);
+      DexType repr2 = representatives.getRepresentative(type2);
+      repr1.getDescriptor().acceptCompareTo(repr2.getDescriptor(), this);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/structural/DefaultCompareToVisitor.java b/src/main/java/com/android/tools/r8/utils/structural/DefaultCompareToVisitor.java
new file mode 100644
index 0000000..9f91d19
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/structural/DefaultCompareToVisitor.java
@@ -0,0 +1,23 @@
+// 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.utils.structural;
+
+import com.android.tools.r8.utils.structural.StructuralItem.CompareToAccept;
+
+/**
+ * Default implementation for defining compareTo on a structural type.
+ *
+ * <p>Internally this is using CompareToVisitorWithTypeEquivalence with the identity map, but should
+ * not be assumed to have that implementation.
+ */
+public class DefaultCompareToVisitor {
+
+  public static <T> int run(T item1, T item2, StructuralAccept<T> visit) {
+    return run(item1, item2, (i1, i2, visitor) -> visitor.visit(i1, i2, visit));
+  }
+
+  public static <T> int run(T item1, T item2, CompareToAccept<T> compareToAccept) {
+    return CompareToVisitorWithTypeEquivalence.run(item1, item2, t -> t, compareToAccept);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/structural/DefaultHashingVisitor.java b/src/main/java/com/android/tools/r8/utils/structural/DefaultHashingVisitor.java
new file mode 100644
index 0000000..2ec503c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/structural/DefaultHashingVisitor.java
@@ -0,0 +1,24 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils.structural;
+
+import com.android.tools.r8.utils.structural.StructuralItem.HashingAccept;
+import com.google.common.hash.Hasher;
+
+/**
+ * Default visitor for hashing a structural item.
+ *
+ * <p>Internally this is using HashingVisitorWithTypeEquivalence with the identity map, but should
+ * not be assumed to have that implementation.
+ */
+public class DefaultHashingVisitor {
+
+  public static <T> void run(T item, Hasher hasher, StructuralAccept<T> accept) {
+    run(item, hasher, (i, visitor) -> visitor.visit(i, accept));
+  }
+
+  public static <T> void run(T item, Hasher hasher, HashingAccept<T> hashingAccept) {
+    HashingVisitorWithTypeEquivalence.run(item, hasher, t -> t, hashingAccept);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/structural/Equatable.java b/src/main/java/com/android/tools/r8/utils/structural/Equatable.java
new file mode 100644
index 0000000..be49958
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/structural/Equatable.java
@@ -0,0 +1,56 @@
+// 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.utils.structural;
+
+public interface Equatable<T> {
+
+  /**
+   * Typed definition of equality.
+   *
+   * <p>Subclasses must implement this and override Object.equals(Object) with equalsImpl.
+   */
+  boolean isEqualTo(T other);
+
+  /**
+   * An equatable type must define an equality compatible hashing.
+   *
+   * <p>Note: that that the declaration here will not enforce an implementation in the concrete
+   * class and it cannot be defined by a default method. Implementors of Equatable must ensure to
+   * override it.
+   */
+  @Override
+  int hashCode();
+
+  /**
+   * An equatable type must override Object.equals by the intended implementation below.
+   *
+   * <p>Note: that that the declaration here will not enforce an implementation in the concrete
+   * class and it cannot be defined by a default method. Implementors of Equatable must ensure to
+   * override it.
+   */
+  @Override
+  boolean equals(Object other);
+
+  /**
+   * Implementation for Object.equals(Object).
+   *
+   * <p>It is not possible to define default methods on java.lang.Object, thus concrete subclasses
+   * must manually override equals as:
+   *
+   * <pre>
+   *   @Override boolean equals(Object other) { return Equatable.equalsImpl(this, other); }
+   * </pre>
+   */
+  @SuppressWarnings("unchecked")
+  static <T extends Equatable<T>> boolean equalsImpl(T self, Object other) {
+    assert self != null;
+    if (self == other) {
+      return true;
+    }
+    if (other == null || self.getClass() != other.getClass()) {
+      return false;
+    }
+    return self.isEqualTo((T) other);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java b/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java
new file mode 100644
index 0000000..ab75fb4
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java
@@ -0,0 +1,72 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils.structural;
+
+import com.android.tools.r8.utils.structural.StructuralItem.CompareToAccept;
+import com.android.tools.r8.utils.structural.StructuralItem.HashingAccept;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.ToIntFunction;
+
+/**
+ * Simple hash code implementation.
+ *
+ * <p>This visitor relies on the specification of hashCode on all object types. Thus it does not
+ * have a call-back structure that requires the spec implementation as well as a visitor for the
+ * recursive decent. There is also no support for overriding the visitation apart from the usual
+ * override of hashCode().
+ */
+public class HashCodeVisitor<T> extends StructuralSpecification<T, HashCodeVisitor<T>> {
+
+  public static <T> int run(T item, StructuralAccept<T> visit) {
+    HashCodeVisitor<T> visitor = new HashCodeVisitor<>(item);
+    visit.accept(visitor);
+    return visitor.hashCode;
+  }
+
+  private final T item;
+
+  private int hashCode = 0;
+
+  private HashCodeVisitor(T item) {
+    this.item = item;
+  }
+
+  private HashCodeVisitor<T> amend(int value) {
+    // This mirrors the behavior of Objects.hash(values...) / Arrays.hashCode(array).
+    hashCode = 31 * hashCode + value;
+    return this;
+  }
+
+  @Override
+  public HashCodeVisitor<T> withAssert(Predicate<T> predicate) {
+    assert predicate.test(item);
+    return this;
+  }
+
+  @Override
+  public HashCodeVisitor<T> withBool(Predicate<T> getter) {
+    return amend(Boolean.hashCode(getter.test(item)));
+  }
+
+  @Override
+  public HashCodeVisitor<T> withInt(ToIntFunction<T> getter) {
+    return amend(Integer.hashCode(getter.applyAsInt(item)));
+  }
+
+  @Override
+  protected <S> HashCodeVisitor<T> withConditionalCustomItem(
+      Predicate<T> predicate,
+      Function<T, S> getter,
+      CompareToAccept<S> compare,
+      HashingAccept<S> hasher) {
+    if (predicate.test(item)) {
+      return amend(getter.apply(item).hashCode());
+    } else {
+      // Use the value 1 for the failing-predicate case such that a different hash is obtained for
+      // eg, {null, null} and {null}.
+      return amend(1);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/structural/HashingVisitor.java b/src/main/java/com/android/tools/r8/utils/structural/HashingVisitor.java
new file mode 100644
index 0000000..bfd5e0d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/structural/HashingVisitor.java
@@ -0,0 +1,28 @@
+// 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.utils.structural;
+
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
+import com.google.common.hash.Hasher;
+import java.util.function.BiConsumer;
+
+public abstract class HashingVisitor {
+
+  public abstract void visitBool(boolean value);
+
+  public abstract void visitInt(int value);
+
+  public abstract void visitDexString(DexString string);
+
+  public abstract void visitDexType(DexType type);
+
+  public abstract void visitDexTypeList(DexTypeList types);
+
+  public abstract <S> void visit(S item, StructuralAccept<S> accept);
+
+  @Deprecated
+  public abstract <S> void visit(S item, BiConsumer<S, Hasher> hasher);
+}
diff --git a/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.java b/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.java
new file mode 100644
index 0000000..2036b1b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.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.utils.structural;
+
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.DexTypeList;
+import com.android.tools.r8.utils.structural.StructuralItem.CompareToAccept;
+import com.android.tools.r8.utils.structural.StructuralItem.HashingAccept;
+import com.google.common.hash.Hasher;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.ToIntFunction;
+
+/** Visitor for hashing a structural item under some assumed type equivalence. */
+public class HashingVisitorWithTypeEquivalence extends HashingVisitor {
+
+  public static <T> void run(
+      T item, Hasher hasher, RepresentativeMap map, StructuralAccept<T> accept) {
+    run(item, hasher, map, (i, visitor) -> visitor.visit(i, accept));
+  }
+
+  public static <T> void run(
+      T item, Hasher hasher, RepresentativeMap map, HashingAccept<T> hashingAccept) {
+    hashingAccept.accept(item, new HashingVisitorWithTypeEquivalence(hasher, map));
+  }
+
+  private final Hasher hash;
+  private final RepresentativeMap representatives;
+
+  private HashingVisitorWithTypeEquivalence(Hasher hash, RepresentativeMap representatives) {
+    this.hash = hash;
+    this.representatives = representatives;
+  }
+
+  @Override
+  public void visitBool(boolean value) {
+    hash.putBoolean(value);
+  }
+
+  @Override
+  public void visitInt(int value) {
+    hash.putInt(value);
+  }
+
+  @Override
+  public void visitDexString(DexString string) {
+    visitInt(string.hashCode());
+  }
+
+  @Override
+  public void visitDexType(DexType type) {
+    visitDexString(representatives.getRepresentative(type).getDescriptor());
+  }
+
+  @Override
+  public void visitDexTypeList(DexTypeList types) {
+    types.forEach(this::visitDexType);
+  }
+
+  @Override
+  public <S> void visit(S item, StructuralAccept<S> accept) {
+    accept.accept(new ItemSpecification<>(item, this));
+  }
+
+  @Override
+  public <S> void visit(S item, BiConsumer<S, Hasher> hasher) {
+    hasher.accept(item, hash);
+  }
+
+  private static class ItemSpecification<T>
+      extends StructuralSpecification<T, ItemSpecification<T>> {
+
+    private final HashingVisitorWithTypeEquivalence parent;
+    private final T item;
+
+    private ItemSpecification(T item, HashingVisitorWithTypeEquivalence parent) {
+      this.item = item;
+      this.parent = parent;
+    }
+
+    @Override
+    public ItemSpecification<T> withAssert(Predicate<T> predicate) {
+      assert predicate.test(item);
+      return this;
+    }
+
+    @Override
+    public ItemSpecification<T> withBool(Predicate<T> getter) {
+      parent.visitBool(getter.test(item));
+      return this;
+    }
+
+    @Override
+    public ItemSpecification<T> withInt(ToIntFunction<T> getter) {
+      parent.visitInt(getter.applyAsInt(item));
+      return this;
+    }
+
+    @Override
+    protected <S> ItemSpecification<T> withConditionalCustomItem(
+        Predicate<T> predicate,
+        Function<T, S> getter,
+        CompareToAccept<S> compare,
+        HashingAccept<S> hasher) {
+      boolean test = predicate.test(item);
+      // Always hash the predicate result to distinguish, eg, {null, null} and {null}.
+      parent.visitBool(test);
+      if (test) {
+        hasher.accept(getter.apply(item), parent);
+      }
+      return this;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/structural/Ordered.java b/src/main/java/com/android/tools/r8/utils/structural/Ordered.java
new file mode 100644
index 0000000..9c9a588
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/structural/Ordered.java
@@ -0,0 +1,63 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils.structural;
+
+/** An ordered type is a type with a total order. */
+public interface Ordered<T> extends Equatable<T>, Comparable<T> {
+
+  /** Definition of total order. */
+  @Override
+  int compareTo(T other);
+
+  /** Default equality is now defined by the order. */
+  @Override
+  default boolean isEqualTo(T other) {
+    assert other != null;
+    return this == other || compareTo(other) == 0;
+  }
+
+  static <T extends Ordered<T>> T min(T o1, T o2) {
+    return o1.isLessThan(o2) ? o1 : o2;
+  }
+
+  static <T extends Ordered<T>> T max(T o1, T o2) {
+    return o1.isLessThan(o2) ? o2 : o1;
+  }
+
+  static <T extends Ordered<T>> T minIgnoreNull(T o1, T o2) {
+    if (o1 == null) {
+      return o2;
+    }
+    if (o2 == null) {
+      return o1;
+    }
+    return min(o1, o2);
+  }
+
+  static <T extends Ordered<T>> T maxIgnoreNull(T o1, T o2) {
+    if (o1 == null) {
+      return o2;
+    }
+    if (o2 == null) {
+      return o1;
+    }
+    return o1.isLessThan(o2) ? o2 : o1;
+  }
+
+  default boolean isLessThan(T other) {
+    return compareTo(other) < 0;
+  }
+
+  default boolean isLessThanOrEqualTo(T other) {
+    return compareTo(other) <= 0;
+  }
+
+  default boolean isGreaterThan(T other) {
+    return compareTo(other) > 0;
+  }
+
+  default boolean isGreaterThanOrEqualTo(T other) {
+    return compareTo(other) >= 0;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/structural/RepresentativeMap.java b/src/main/java/com/android/tools/r8/utils/structural/RepresentativeMap.java
new file mode 100644
index 0000000..6a0af38
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/structural/RepresentativeMap.java
@@ -0,0 +1,11 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils.structural;
+
+import com.android.tools.r8.graph.DexType;
+
+@FunctionalInterface
+public interface RepresentativeMap {
+  DexType getRepresentative(DexType type);
+}
diff --git a/src/main/java/com/android/tools/r8/utils/structural/StructuralAccept.java b/src/main/java/com/android/tools/r8/utils/structural/StructuralAccept.java
new file mode 100644
index 0000000..1c6d25c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/structural/StructuralAccept.java
@@ -0,0 +1,9 @@
+// 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.utils.structural;
+
+@FunctionalInterface
+public interface StructuralAccept<T> {
+  void accept(StructuralSpecification<T, ?> visitor);
+}
diff --git a/src/main/java/com/android/tools/r8/utils/structural/StructuralItem.java b/src/main/java/com/android/tools/r8/utils/structural/StructuralItem.java
new file mode 100644
index 0000000..f077c25
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/structural/StructuralItem.java
@@ -0,0 +1,88 @@
+// 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.utils.structural;
+
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
+
+/** Specified types must implement methods to determine equality, hashing and order. */
+public interface StructuralItem<T extends StructuralItem<T>> extends Ordered<T> {
+
+  T self();
+
+  StructuralAccept<T> getStructuralAccept();
+
+  // CompareTo implementation and callbacks.
+
+  @FunctionalInterface
+  interface CompareToAccept<T> {
+    void accept(T item1, T item2, CompareToVisitor visitor);
+  }
+
+  /**
+   * Implementation of the default compareTo on the item.
+   *
+   * <p>This should *not* be overwritten, instead items should overwrite acceptCompareTo which will
+   * ensure that the effect is in place for any CompareToVisitor.
+   */
+  @Override
+  default int compareTo(T other) {
+    return DefaultCompareToVisitor.run(self(), other, StructuralItem::acceptCompareTo);
+  }
+
+  /**
+   * Implementation of a compareTo with a type equivalence on an item.
+   *
+   * <p>This should *not* be overwritten, instead items should overwrite acceptCompareTo which will
+   * ensure that the effect is in place for any CompareToVisitor.
+   */
+  default int compareWithTypeEquivalenceTo(T other, RepresentativeMap map) {
+    return CompareToVisitorWithTypeEquivalence.run(
+        self(), other, map, StructuralItem::acceptCompareTo);
+  }
+
+  /** Default accept for compareTo visitors. Override to change behavior. */
+  default void acceptCompareTo(T other, CompareToVisitor visitor) {
+    visitor.visit(self(), other, self().getStructuralAccept());
+  }
+
+  // Hashing implemenation and callbacks.
+
+  @FunctionalInterface
+  interface HashingAccept<T> {
+    void accept(T item, HashingVisitor visitor);
+  }
+
+  /**
+   * Implementation of the default hashing of an item.
+   *
+   * <p>This should *not* be overwritten, instead items should overwrite acceptHashing which will
+   * ensure that the effect is in place for any HashingVisitor.
+   */
+  default void hash(Hasher hasher) {
+    DefaultHashingVisitor.run(self(), hasher, self().getStructuralAccept());
+  }
+
+  /** Hashing method to use from tests to avoid having guava types shared between R8 and tests. */
+  default String hashForTesting() {
+    Hasher hasher = Hashing.sha256().newHasher();
+    hash(hasher);
+    return hasher.hash().toString();
+  }
+
+  /**
+   * Implementation of the default hashing with a type equivalence on the item.
+   *
+   * <p>This should *not* be overwritten, instead items should overwrite acceptHashing which will
+   * ensure that the effect is in place for any HashingVisitor.
+   */
+  default void hashWithTypeEquivalence(Hasher hasher, RepresentativeMap map) {
+    HashingVisitorWithTypeEquivalence.run(self(), hasher, map, self().getStructuralAccept());
+  }
+
+  /** Default accept for hashing visitors. Override to change behavior. */
+  default void acceptHashing(HashingVisitor visitor) {
+    visitor.visit(self(), self().getStructuralAccept());
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java b/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java
new file mode 100644
index 0000000..bb8f7c7
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java
@@ -0,0 +1,65 @@
+// 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.utils.structural;
+
+import com.android.tools.r8.utils.structural.StructuralItem.CompareToAccept;
+import com.android.tools.r8.utils.structural.StructuralItem.HashingAccept;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.function.ToIntFunction;
+
+public abstract class StructuralSpecification<T, V extends StructuralSpecification<T, V>> {
+
+  /**
+   * Basic specification for visiting an item.
+   *
+   * <p>This specified the getter for the item as well as all of the methods that are required for
+   * visiting. Those coincide with the requirements of Specified.
+   *
+   * <p>It is preferable to use withStructuralItem.
+   */
+  @Deprecated
+  public final <S> V withCustomItem(
+      Function<T, S> getter, CompareToAccept<S> compare, HashingAccept<S> hasher) {
+    return withConditionalCustomItem(t -> true, getter, compare, hasher);
+  }
+
+  protected abstract <S> V withConditionalCustomItem(
+      Predicate<T> predicate,
+      Function<T, S> getter,
+      CompareToAccept<S> compare,
+      HashingAccept<S> hasher);
+
+  /**
+   * Specification for a "specified" item.
+   *
+   * <p>Using this the visiting methods are could based on the implementation of the Specified
+   * interface.
+   */
+  public final <S extends StructuralItem<S>> V withItem(Function<T, S> getter) {
+    return withConditionalItem(t -> true, getter);
+  }
+
+  final <S extends StructuralItem<S>> V withNullableItem(Function<T, S> getter) {
+    return withConditionalItem(s -> getter.apply(s) != null, getter);
+  }
+
+  public final <S extends StructuralItem<S>> V withConditionalItem(
+      Predicate<T> predicate, Function<T, S> getter) {
+    return withConditionalCustomItem(predicate, getter, S::acceptCompareTo, S::acceptHashing);
+  }
+
+  /**
+   * Helper to declare an assert on the item.
+   *
+   * <p>Only run if running with -ea. Must be run on any item being visited (ie, both in the case of
+   * comparisons and equality).
+   */
+  public abstract V withAssert(Predicate<T> predicate);
+
+  // Primitive Java types. These will need overriding to avoid boxing.
+  public abstract V withBool(Predicate<T> getter);
+
+  public abstract V withInt(ToIntFunction<T> getter);
+}
diff --git a/src/test/java/com/android/tools/r8/AndroidAppDumpsTest.java b/src/test/java/com/android/tools/r8/AndroidAppDumpsTest.java
index 441be7c..8b7d21e 100644
--- a/src/test/java/com/android/tools/r8/AndroidAppDumpsTest.java
+++ b/src/test/java/com/android/tools/r8/AndroidAppDumpsTest.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.DataResourceProvider.Visitor;
 import com.android.tools.r8.ProgramResource.Kind;
+import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.AndroidApp;
@@ -42,6 +43,7 @@
   @Test
   public void test() throws Exception {
     InternalOptions options = new InternalOptions();
+    options.dumpOptions = DumpOptions.builder(Tool.D8).build();
 
     String dataResourceName = "my-resource.bin";
     byte[] dataResourceData = new byte[] {1, 2, 3};
@@ -67,7 +69,7 @@
             .build();
 
     Path dumpFile = temp.newFolder().toPath().resolve("dump.zip");
-    appIn.dump(dumpFile, options);
+    appIn.dump(dumpFile, options.dumpOptions, options.reporter, options.dexItemFactory());
 
     AndroidApp appOut = AndroidApp.builder(options.reporter).addDump(dumpFile).build();
     assertEquals(1, appOut.getClassProgramResourcesForTesting().size());
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index 9167acd..058c2e5 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -428,6 +428,44 @@
     addInternalKeepRules(sb.toString());
   }
 
+  public T noClassInlining() {
+    return noClassInlining(true);
+  }
+
+  public T noClassInlining(boolean condition) {
+    if (condition) {
+      return addOptionsModification(options -> options.enableClassInlining = false);
+    }
+    return self();
+  }
+
+  public T noClassStaticizing() {
+    return noClassStaticizing(true);
+  }
+
+  public T noClassStaticizing(boolean condition) {
+    if (condition) {
+      return addOptionsModification(options -> options.enableClassStaticizer = false);
+    }
+    return self();
+  }
+
+  public T noHorizontalClassMerging() {
+    return noHorizontalClassMerging(true);
+  }
+
+  public T noHorizontalClassMerging(boolean condition) {
+    if (condition) {
+      return addKeepRules("-" + NoHorizontalClassMergingRule.RULE_NAME + " class *");
+    }
+    return self();
+  }
+
+  public T noHorizontalClassMerging(Class<?> clazz) {
+    return addKeepRules(
+        "-" + NoHorizontalClassMergingRule.RULE_NAME + " class " + clazz.getTypeName());
+  }
+
   public T enableNoUnusedInterfaceRemovalAnnotations() {
     if (!enableNoUnusedInterfaceRemovalAnnotations) {
       enableNoUnusedInterfaceRemovalAnnotations = true;
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index 3d24da4..abdfb83 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -20,6 +20,7 @@
 import com.android.tools.r8.utils.codeinspector.EnumUnboxingInspector;
 import com.android.tools.r8.utils.codeinspector.HorizontallyMergedClassesInspector;
 import com.android.tools.r8.utils.codeinspector.HorizontallyMergedLambdaClassesInspector;
+import com.android.tools.r8.utils.codeinspector.StaticallyMergedClassesInspector;
 import com.android.tools.r8.utils.codeinspector.VerticallyMergedClassesInspector;
 import com.google.common.base.Suppliers;
 import java.io.ByteArrayOutputStream;
@@ -147,6 +148,17 @@
                             dexItemFactory, horizontallyMergedLambdaClasses))));
   }
 
+  public T addStaticallyMergedClassesInspector(
+      Consumer<StaticallyMergedClassesInspector> inspector) {
+    return addOptionsModification(
+        options ->
+            options.testing.staticallyMergedClassesConsumer =
+                ((dexItemFactory, staticallyMergedClasses) ->
+                    inspector.accept(
+                        new StaticallyMergedClassesInspector(
+                            dexItemFactory, staticallyMergedClasses))));
+  }
+
   public T addVerticallyMergedClassesInspector(
       Consumer<VerticallyMergedClassesInspector> inspector) {
     return addOptionsModification(
diff --git a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
index 5f674e0..6f6c949 100644
--- a/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestShrinkerBuilder.java
@@ -251,6 +251,10 @@
     return self();
   }
 
+  public T addPrintSeeds() {
+    return addKeepRules("-printseeds");
+  }
+
   public T allowAccessModification() {
     return allowAccessModification(true);
   }
@@ -275,6 +279,10 @@
     return addKeepAttributes(ProguardKeepAttributes.LINE_NUMBER_TABLE);
   }
 
+  public T addKeepAttributeSignature() {
+    return addKeepAttributes(ProguardKeepAttributes.SIGNATURE);
+  }
+
   public T addKeepAttributeSourceFile() {
     return addKeepAttributes(ProguardKeepAttributes.SOURCE_FILE);
   }
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/BridgeHoistingAccessibilityTest.java b/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/BridgeHoistingAccessibilityTest.java
index c9f7d21..81c5c04 100644
--- a/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/BridgeHoistingAccessibilityTest.java
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/BridgeHoistingAccessibilityTest.java
@@ -72,6 +72,7 @@
                 .transform())
         .addKeepMainRule(TestClass.class)
         .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/testclasses/BridgeHoistingAccessibilityTestClasses.java b/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/testclasses/BridgeHoistingAccessibilityTestClasses.java
index 9990f62..e7d430f 100644
--- a/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/testclasses/BridgeHoistingAccessibilityTestClasses.java
+++ b/src/test/java/com/android/tools/r8/bridgeremoval/hoisting/testclasses/BridgeHoistingAccessibilityTestClasses.java
@@ -5,12 +5,14 @@
 package com.android.tools.r8.bridgeremoval.hoisting.testclasses;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.bridgeremoval.hoisting.BridgeHoistingAccessibilityTest;
 import com.android.tools.r8.bridgeremoval.hoisting.BridgeHoistingAccessibilityTest.CWithRangedInvoke;
 
 public class BridgeHoistingAccessibilityTestClasses {
 
+  @NoHorizontalClassMerging
   @NoVerticalClassMerging
   public static class A {
 
diff --git a/src/test/java/com/android/tools/r8/cf/CfVersionTest.java b/src/test/java/com/android/tools/r8/cf/CfVersionTest.java
index 48871e8..ea38851 100644
--- a/src/test/java/com/android/tools/r8/cf/CfVersionTest.java
+++ b/src/test/java/com/android/tools/r8/cf/CfVersionTest.java
@@ -49,16 +49,16 @@
   }
 
   private static void assertLessThan(CfVersion less, CfVersion more) {
-    assertFalse(less.isEqual(more));
+    assertFalse(less.isEqualTo(more));
     assertEquals(-1, less.compareTo(more));
     assertEquals(1, more.compareTo(less));
     assertTrue(less.isLessThan(more));
-    assertTrue(less.isLessThanOrEqual(more));
+    assertTrue(less.isLessThanOrEqualTo(more));
     assertFalse(less.isGreaterThan(more));
-    assertFalse(less.isGreaterThanOrEqual(more));
+    assertFalse(less.isGreaterThanOrEqualTo(more));
     assertFalse(more.isLessThan(less));
-    assertFalse(more.isLessThanOrEqual(less));
+    assertFalse(more.isLessThanOrEqualTo(less));
     assertTrue(more.isGreaterThan(less));
-    assertTrue(more.isGreaterThanOrEqual(less));
+    assertTrue(more.isGreaterThanOrEqualTo(less));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/cf/PrintSeedsWithDeserializeLambdaMethodTest.java b/src/test/java/com/android/tools/r8/cf/PrintSeedsWithDeserializeLambdaMethodTest.java
new file mode 100644
index 0000000..497a1bf
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/cf/PrintSeedsWithDeserializeLambdaMethodTest.java
@@ -0,0 +1,74 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.cf;
+
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import com.google.common.collect.ImmutableList;
+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 PrintSeedsWithDeserializeLambdaMethodTest extends TestBase {
+
+  private static final Class<?> TEST_CLASS_CF = KeepDeserializeLambdaMethodTestCf.class;
+  private static final Class<?> TEST_CLASS_DEX = KeepDeserializeLambdaMethodTestDex.class;
+
+  private static final String EXPECTED =
+      StringUtils.lines("base lambda", KeepDeserializeLambdaMethodTest.LAMBDA_MESSAGE);
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection params() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  private final TestParameters parameters;
+
+  public PrintSeedsWithDeserializeLambdaMethodTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  private Class<?> getMainClass() {
+    return parameters.isCfRuntime() ? TEST_CLASS_CF : TEST_CLASS_DEX;
+  }
+
+  private List<Class<?>> getClasses() {
+    return ImmutableList.of(KeepDeserializeLambdaMethodTest.class, getMainClass());
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8Compat(parameters.getBackend())
+        .addProgramClasses(getClasses())
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(getMainClass())
+        .addPrintSeeds()
+        .allowStdoutMessages()
+        .noMinification()
+        .noTreeShaking()
+        .run(parameters.getRuntime(), getMainClass())
+        .assertSuccessWithOutput(EXPECTED)
+        .inspect(this::checkPresenceOfDeserializedLambdas);
+  }
+
+  private void checkPresenceOfDeserializedLambdas(CodeInspector inspector) {
+    for (Class<?> clazz : getClasses()) {
+      MethodSubject method = inspector.clazz(clazz).uniqueMethodWithName("$deserializeLambda$");
+      assertEquals(
+          "Unexpected status for $deserializedLambda$ on " + clazz.getSimpleName(),
+          parameters.isCfRuntime(),
+          method.isPresent());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/AbstractMethodMergingNonTrivialTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/AbstractMethodMergingNonTrivialTest.java
new file mode 100644
index 0000000..4d7d7fe
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/AbstractMethodMergingNonTrivialTest.java
@@ -0,0 +1,126 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestParameters;
+import org.junit.Test;
+
+public class AbstractMethodMergingNonTrivialTest extends HorizontalClassMergingTestBase {
+
+  public AbstractMethodMergingNonTrivialTest(
+      TestParameters parameters, boolean enableHorizontalClassMerging) {
+    super(parameters, enableHorizontalClassMerging);
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+        .addHorizontallyMergedClassesInspectorIf(
+            enableHorizontalClassMerging, inspector -> inspector.assertMergedInto(B.class, A.class))
+        .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("ASub1.f()", "B.f()", "C.f()", "A.g()", "BSub1.g()", "C.g()");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      (System.currentTimeMillis() > 0 ? new ASub1() : new ASub2()).f();
+      (System.currentTimeMillis() > 0 ? new BSub1() : new BSub2()).f();
+      (System.currentTimeMillis() > 0 ? new CSub1() : new CSub2()).f();
+      (System.currentTimeMillis() > 0 ? new ASub1() : new ASub2()).g();
+      (System.currentTimeMillis() > 0 ? new BSub1() : new BSub2()).g();
+      (System.currentTimeMillis() > 0 ? new CSub1() : new CSub2()).g();
+    }
+  }
+
+  @NoVerticalClassMerging
+  abstract static class A {
+
+    public abstract void f();
+
+    @NeverInline
+    public void g() {
+      System.out.println("A.g()");
+    }
+  }
+
+  @NoVerticalClassMerging
+  abstract static class B {
+
+    @NeverInline
+    public void f() {
+      System.out.println("B.f()");
+    }
+
+    public abstract void g();
+  }
+
+  abstract static class C {
+
+    @NeverInline
+    public void f() {
+      System.out.println("C.f()");
+    }
+
+    @NeverInline
+    public void g() {
+      System.out.println("C.g()");
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class ASub1 extends A {
+
+    @Override
+    public void f() {
+      System.out.println("ASub1.f()");
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class ASub2 extends A {
+
+    @Override
+    public void f() {
+      System.out.println("ASub2.f()");
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class BSub1 extends B {
+
+    @Override
+    public void g() {
+      System.out.println("BSub1.g()");
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class BSub2 extends B {
+
+    @Override
+    public void g() {
+      System.out.println("BSub2.g()");
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class CSub1 extends C {}
+
+  @NoHorizontalClassMerging
+  static class CSub2 extends C {}
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/AbstractMethodMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/AbstractMethodMergingTest.java
new file mode 100644
index 0000000..cc092e9
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/AbstractMethodMergingTest.java
@@ -0,0 +1,132 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.NoVerticalClassMerging;
+import com.android.tools.r8.TestParameters;
+import org.junit.Test;
+
+public class AbstractMethodMergingTest extends HorizontalClassMergingTestBase {
+
+  public AbstractMethodMergingTest(
+      TestParameters parameters, boolean enableHorizontalClassMerging) {
+    super(parameters, enableHorizontalClassMerging);
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+        .addHorizontallyMergedClassesInspectorIf(
+            enableHorizontalClassMerging, inspector -> inspector.assertMergedInto(B.class, A.class))
+        .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(
+            "ASub1.f()", "B.f()", "A.g()", "BSub1.g()", "ASub1.h()", "BSub1.h()");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      (System.currentTimeMillis() > 0 ? new ASub1() : new ASub2()).f();
+      (System.currentTimeMillis() > 0 ? new BSub1() : new BSub2()).f();
+      (System.currentTimeMillis() > 0 ? new ASub1() : new ASub2()).g();
+      (System.currentTimeMillis() > 0 ? new BSub1() : new BSub2()).g();
+      (System.currentTimeMillis() > 0 ? new ASub1() : new ASub2()).h();
+      (System.currentTimeMillis() > 0 ? new BSub1() : new BSub2()).h();
+    }
+  }
+
+  @NoVerticalClassMerging
+  abstract static class A {
+
+    public abstract void f();
+
+    @NeverInline
+    public void g() {
+      System.out.println("A.g()");
+    }
+
+    public abstract void h();
+  }
+
+  @NoVerticalClassMerging
+  abstract static class B {
+
+    @NeverInline
+    public void f() {
+      System.out.println("B.f()");
+    }
+
+    public abstract void g();
+
+    public abstract void h();
+  }
+
+  @NoHorizontalClassMerging
+  static class ASub1 extends A {
+
+    @Override
+    public void f() {
+      System.out.println("ASub1.f()");
+    }
+
+    @Override
+    public void h() {
+      System.out.println("ASub1.h()");
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class ASub2 extends A {
+
+    @Override
+    public void f() {
+      System.out.println("ASub2.f()");
+    }
+
+    @Override
+    public void h() {
+      System.out.println("ASub2.h()");
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class BSub1 extends B {
+
+    @Override
+    public void g() {
+      System.out.println("BSub1.g()");
+    }
+
+    @Override
+    public void h() {
+      System.out.println("BSub1.h()");
+    }
+  }
+
+  @NoHorizontalClassMerging
+  static class BSub2 extends B {
+
+    @Override
+    public void g() {
+      System.out.println("BSub2.g()");
+    }
+
+    @Override
+    public void h() {
+      System.out.println("BSub2.h()");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassWithInstanceFieldsTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassWithInstanceFieldsTest.java
new file mode 100644
index 0000000..25bb4ec
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassWithInstanceFieldsTest.java
@@ -0,0 +1,86 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import org.junit.Test;
+
+public class ClassWithInstanceFieldsTest extends HorizontalClassMergingTestBase {
+  public ClassWithInstanceFieldsTest(
+      TestParameters parameters, boolean enableHorizontalClassMerging) {
+    super(parameters, enableHorizontalClassMerging);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+        .enableNeverClassInliningAnnotations()
+        .enableInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .addHorizontallyMergedClassesInspectorIf(
+            enableHorizontalClassMerging, inspector -> inspector.assertMergedInto(B.class, A.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A. field: 5, v: a, j: 1", "B. field: b, v: 2, j: 3")
+        .inspect(
+            codeInspector -> {
+              assertThat(codeInspector.clazz(A.class), isPresent());
+              assertThat(
+                  codeInspector.clazz(B.class), notIf(isPresent(), enableHorizontalClassMerging));
+            });
+  }
+
+  @NeverClassInline
+  public static class A {
+    public int field;
+    public String v;
+    public int j;
+
+    public A(int field, String v, int j) {
+      this.field = field;
+      this.v = v;
+      this.j = j;
+    }
+
+    @NeverInline
+    public void foo() {
+      System.out.println("A. field: " + field + ", v: " + v + ", j: " + j);
+    }
+  }
+
+  @NeverClassInline
+  public static class B {
+    public String field;
+    public int v;
+    public int j;
+
+    public B(String field, int v, int j) {
+      this.field = field;
+      this.v = v;
+      this.j = j;
+    }
+
+    @NeverInline
+    public void foo() {
+      System.out.println("B. field: " + field + ", v: " + v + ", j: " + j);
+    }
+  }
+
+  public static class Main {
+    public static void main(String[] args) {
+      new A(5, "a", 1).foo();
+      new B("b", 2, 3).foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithDifferentVisibilityFieldsTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithDifferentVisibilityFieldsTest.java
new file mode 100644
index 0000000..90e407e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithDifferentVisibilityFieldsTest.java
@@ -0,0 +1,151 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.horizontal;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
+import static com.android.tools.r8.utils.codeinspector.Matchers.readsInstanceField;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.classmerging.horizontal.EmptyClassTest.A;
+import com.android.tools.r8.classmerging.horizontal.EmptyClassTest.B;
+import com.android.tools.r8.classmerging.horizontal.EmptyClassTest.Main;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.FieldSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import org.junit.Test;
+
+public class ClassesWithDifferentVisibilityFieldsTest extends HorizontalClassMergingTestBase {
+  public ClassesWithDifferentVisibilityFieldsTest(
+      TestParameters parameters, boolean enableHorizontalClassMerging) {
+    super(parameters, enableHorizontalClassMerging);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .addHorizontallyMergedClassesInspectorIf(
+            enableHorizontalClassMerging, inspector -> inspector.assertMergedInto(B.class, A.class))
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(
+            "a. v1: 10, v2: 20", "b. v1: 60, v2: 100", "c. v1: 210, v2: 330")
+        .inspect(
+            codeInspector -> {
+              ClassSubject aClassSubject = codeInspector.clazz(A.class);
+              assertThat(aClassSubject, isPresent());
+              assertThat(
+                  codeInspector.clazz(B.class), notIf(isPresent(), enableHorizontalClassMerging));
+              assertThat(codeInspector.clazz(C.class), isPresent());
+
+              if (enableHorizontalClassMerging) {
+                FieldSubject v1Subject = aClassSubject.uniqueFieldWithName("v1");
+                FieldSubject v2Subject = aClassSubject.uniqueFieldWithName("v2");
+
+                MethodSubject methodSubject = aClassSubject.uniqueMethodWithName("getAV1");
+                assertThat(methodSubject, isPresent());
+                assertThat(methodSubject, readsInstanceField(v1Subject.getDexField()));
+
+                methodSubject = aClassSubject.uniqueMethodWithName("getAV2");
+                assertThat(methodSubject, isPresent());
+                assertThat(methodSubject, readsInstanceField(v2Subject.getDexField()));
+
+                // The fields v1 and v2 are swapped, because their access modifiers are swapped.
+                methodSubject = aClassSubject.uniqueMethodWithName("getBV1");
+                assertThat(methodSubject, isPresent());
+                assertThat(methodSubject, readsInstanceField(v2Subject.getDexField()));
+
+                methodSubject = aClassSubject.uniqueMethodWithName("getBV2");
+                assertThat(methodSubject, isPresent());
+                assertThat(methodSubject, readsInstanceField(v1Subject.getDexField()));
+              }
+            });
+  }
+
+  @NeverClassInline
+  public static class A {
+    private int v1;
+    public int v2;
+
+    public A(int v) {
+      v1 = v;
+      v2 = 2 * v;
+    }
+
+    @NeverInline
+    public int getAV1() {
+      return v1;
+    }
+
+    @NeverInline
+    public int getAV2() {
+      return v2;
+    }
+
+    @NeverInline
+    public void foo() {
+      System.out.println("a. v1: " + getAV1() + ", v2: " + getAV2());
+    }
+  }
+
+  @NeverClassInline
+  public static class B {
+    public int v1;
+    private int v2;
+
+    public B(int v) {
+      v1 = 3 * v;
+      v2 = 5 * v;
+    }
+
+    @NeverInline
+    public int getBV1() {
+      return v1;
+    }
+
+    @NeverInline
+    public int getBV2() {
+      return v2;
+    }
+
+    @NeverInline
+    public void foo() {
+      System.out.println("b. v1: " + getBV1() + ", v2: " + getBV2());
+    }
+  }
+
+  @NeverClassInline
+  public static class C {
+    public int v1;
+    public int v2;
+
+    public C(int v) {
+      v1 = 7 * v;
+      v2 = 11 * v;
+    }
+
+    @NeverInline
+    public void foo() {
+      System.out.println("c. v1: " + v1 + ", v2: " + v2);
+    }
+  }
+
+  public static class Main {
+    public static void main(String[] args) {
+      new A(10).foo();
+      new B(20).foo();
+      new C(30).foo();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithIdenticalInterfacesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithIdenticalInterfacesTest.java
index 456d29e..5c03d3b 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithIdenticalInterfacesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithIdenticalInterfacesTest.java
@@ -29,8 +29,8 @@
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
-        .addHorizontallyMergedClassesInspector(
-            inspector -> inspector.assertMergedInto(Z.class, Y.class))
+        .addHorizontallyMergedClassesInspectorIf(
+            enableHorizontalClassMerging, inspector -> inspector.assertMergedInto(Z.class, Y.class))
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("bar", "foo y", "foo z")
         .inspect(
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingPreoptimizedTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingPreoptimizedTest.java
index a30149f..f658909 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingPreoptimizedTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingPreoptimizedTest.java
@@ -34,7 +34,8 @@
         .addKeepMainRule(Main.class)
         .addOptionsModification(
             options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
-        .addHorizontallyMergedClassesInspector(
+        .addHorizontallyMergedClassesInspectorIf(
+            enableHorizontalClassMerging,
             inspector ->
                 inspector.assertMerged(A.class, B.class).assertMergedIntoDifferentType(B.class))
         .enableInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/IdenticalFieldMembersTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/IdenticalFieldMembersTest.java
index 1cb2d2d..6968e1a 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/IdenticalFieldMembersTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/IdenticalFieldMembersTest.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.classmerging.horizontal;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.*;
@@ -30,16 +31,9 @@
         .assertSuccessWithOutputLines("foo A", "bar 2")
         .inspect(
             codeInspector -> {
-              if (enableHorizontalClassMerging) {
-                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());
-              }
+              assertThat(codeInspector.clazz(A.class), isPresent());
+              assertThat(
+                  codeInspector.clazz(B.class), notIf(isPresent(), enableHorizontalClassMerging));
             });
   }
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/NoAbstractClassesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/NoAbstractClassesWithNonAbstractClassesTest.java
similarity index 91%
rename from src/test/java/com/android/tools/r8/classmerging/horizontal/NoAbstractClassesTest.java
rename to src/test/java/com/android/tools/r8/classmerging/horizontal/NoAbstractClassesWithNonAbstractClassesTest.java
index 039a65b..2bc2c97 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/NoAbstractClassesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/NoAbstractClassesWithNonAbstractClassesTest.java
@@ -14,8 +14,9 @@
 import com.android.tools.r8.TestParameters;
 import org.junit.Test;
 
-public class NoAbstractClassesTest extends HorizontalClassMergingTestBase {
-  public NoAbstractClassesTest(TestParameters parameters, boolean enableHorizontalClassMerging) {
+public class NoAbstractClassesWithNonAbstractClassesTest extends HorizontalClassMergingTestBase {
+  public NoAbstractClassesWithNonAbstractClassesTest(
+      TestParameters parameters, boolean enableHorizontalClassMerging) {
     super(parameters, enableHorizontalClassMerging);
   }
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/NoAbstractClassesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/RemapFieldTest.java
similarity index 62%
copy from src/test/java/com/android/tools/r8/classmerging/horizontal/NoAbstractClassesTest.java
copy to src/test/java/com/android/tools/r8/classmerging/horizontal/RemapFieldTest.java
index 039a65b..33c8a85 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/NoAbstractClassesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/RemapFieldTest.java
@@ -10,12 +10,11 @@
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
-import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.TestParameters;
 import org.junit.Test;
 
-public class NoAbstractClassesTest extends HorizontalClassMergingTestBase {
-  public NoAbstractClassesTest(TestParameters parameters, boolean enableHorizontalClassMerging) {
+public class RemapFieldTest extends HorizontalClassMergingTestBase {
+  public RemapFieldTest(TestParameters parameters, boolean enableHorizontalClassMerging) {
     super(parameters, enableHorizontalClassMerging);
   }
 
@@ -28,67 +27,75 @@
             options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
-        .enableNoVerticalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
+        .addHorizontallyMergedClassesInspectorIf(
+            enableHorizontalClassMerging,
+            inspector ->
+                inspector.assertMergedInto(B.class, A.class).assertMergedInto(D.class, C.class))
         .run(parameters.getRuntime(), Main.class)
-        .assertSuccessWithOutputLines("bar", "foo c", "foo d", "foo c")
+        .assertSuccessWithOutputLines("A", "B", "foo: foo c", "B", "foo: bar d")
         .inspect(
             codeInspector -> {
               assertThat(codeInspector.clazz(A.class), isPresent());
-              assertThat(codeInspector.clazz(B.class), isPresent());
+              assertThat(
+                  codeInspector.clazz(B.class), notIf(isPresent(), enableHorizontalClassMerging));
               assertThat(codeInspector.clazz(C.class), isPresent());
               assertThat(
                   codeInspector.clazz(D.class), notIf(isPresent(), enableHorizontalClassMerging));
             });
   }
 
-  @NoVerticalClassMerging
-  public abstract static class A {
-    public abstract void foo();
+  @NeverClassInline
+  public static class A {
+    public A() {
+      System.out.println("A");
+    }
   }
 
   @NeverClassInline
   public static class B {
+    public B() {
+      System.out.println("B");
+    }
+
+    public void foo(String s) {
+      System.out.println("foo: " + s);
+    }
+  }
+
+  @NeverClassInline
+  public static class C {
+    B b;
+
+    public C(B b) {
+      this.b = b;
+    }
+
+    @NeverInline
+    public void foo() {
+      b.foo("foo c");
+    }
+  }
+
+  @NeverClassInline
+  public static class D {
+    B b;
+
+    public D(B b) {
+      this.b = b;
+    }
+
     @NeverInline
     public void bar() {
-      System.out.println("bar");
-    }
-  }
-
-  @NeverClassInline
-  public static class C extends A {
-
-    @Override
-    @NeverInline
-    public void foo() {
-      System.out.println("foo c");
-    }
-  }
-
-  @NeverClassInline
-  public static class D extends A {
-
-    @Override
-    @NeverInline
-    public void foo() {
-      System.out.println("foo d");
+      b.foo("bar d");
     }
   }
 
   public static class Main {
-    @NeverInline
-    public static void foo(A a) {
-      a.foo();
-    }
-
     public static void main(String[] args) {
-      new B().bar();
-      C c = new C();
-
-      // This test also checks that the synthesized C#foo does not try to call the abstract A#foo.
-      foo(c);
-      foo(new D());
-      c.foo();
+      new A();
+      new C(new B()).foo();
+      new D(new B()).bar();
     }
   }
 }
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/RemapMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/RemapMethodTest.java
index f0814bd..a531860 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/RemapMethodTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/RemapMethodTest.java
@@ -24,9 +24,7 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> {
-              options.enableHorizontalClassMerging = enableHorizontalClassMerging;
-            })
+            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/MethodCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/MethodCollisionTest.java
new file mode 100644
index 0000000..4baa783
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/MethodCollisionTest.java
@@ -0,0 +1,115 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.vertical;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.codeinspector.VerticallyMergedClassesInspector;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class MethodCollisionTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public MethodCollisionTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        // TODO(christofferqa): Currently we do not allow merging A into B because we find a
+        //  collision. However, we are free to change the names of private methods, so we should
+        //  handle them similar to fields (i.e., we should allow merging A into B). This would also
+        //  improve the performance of the collision detector, because it would only have to
+        //  consider non-private methods.
+        .addVerticallyMergedClassesInspector(
+            VerticallyMergedClassesInspector::assertNoClassesMerged)
+        .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class);
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      B b = new B();
+      b.m();
+
+      D d = new D();
+      d.m();
+
+      // Ensure that the instantiations are not dead code eliminated.
+      escape(b);
+      escape(d);
+    }
+
+    @NeverInline
+    static void escape(Object o) {
+      if (System.currentTimeMillis() < 0) {
+        System.out.println(o);
+      }
+    }
+  }
+
+  @NoHorizontalClassMerging
+  public static class A {
+
+    // After class merging, this method will have the same signature as the method B.m,
+    // unless we handle the collision.
+    private A m() {
+      System.out.println("A.m");
+      return null;
+    }
+
+    public void invokeM() {
+      m();
+    }
+  }
+
+  public static class B extends A {
+
+    private B m() {
+      System.out.println("B.m");
+      invokeM();
+      return null;
+    }
+  }
+
+  @NoHorizontalClassMerging
+  public static class C {
+
+    // After class merging, this method will have the same signature as the method D.m,
+    // unless we handle the collision.
+    public C m() {
+      System.out.println("C.m");
+      return null;
+    }
+  }
+
+  public static class D extends C {
+
+    public D m() {
+      System.out.println("D.m");
+      super.m();
+      return null;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerIndirectReflectiveNameTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerIndirectReflectiveNameTest.java
new file mode 100644
index 0000000..f52ff7c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerIndirectReflectiveNameTest.java
@@ -0,0 +1,110 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.vertical;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThrows;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+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 VerticalClassMergerIndirectReflectiveNameTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withCfRuntimes()
+        .withDexRuntimesStartingFromIncluding(Version.V8_1_0)
+        .withAllApiLevelsAlsoForCf()
+        .build();
+  }
+
+  public VerticalClassMergerIndirectReflectiveNameTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(Main.class, A.class, B.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A::foo", "B::foo");
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    // TODO(b/173099479): This should not throw an assertion-error.
+    assertThrows(
+        CompilationFailedException.class,
+        () ->
+            testForR8(parameters.getBackend())
+                .addProgramClasses(Main.class, A.class, B.class)
+                .addKeepMainRule(Main.class)
+                .setMinApi(parameters.getApiLevel())
+                .enableInliningAnnotations()
+                .enableNeverClassInliningAnnotations()
+                .compileWithExpectedDiagnostics(
+                    diagnostics -> {
+                      diagnostics.assertErrorsMatch(
+                          diagnosticMessage(
+                              containsString(
+                                  "Expected vertically merged class"
+                                      + " `com.android.tools.r8.classmerging.vertical."
+                                      + "VerticalClassMergerIndirectReflectiveNameTest$A`"
+                                      + " to be absent")));
+                    }));
+  }
+
+  public static class A {
+
+    @NeverInline
+    public void foo() {
+      System.out.println("A::foo");
+    }
+  }
+
+  @NeverClassInline
+  public static class B extends A {
+
+    @NeverInline
+    public void bar() {
+      System.out.println("B::foo");
+    }
+  }
+
+  public static class Main {
+
+    public static String getClassName() {
+      return "com.android.tools.r8.classmerging.vertical."
+          + "VerticalClassMergerIndirectReflectiveNameTest$A";
+    }
+
+    static {
+      try {
+        Class.forName(getClassName());
+      } catch (ClassNotFoundException e) {
+      }
+    }
+
+    public static void main(String[] args) {
+      B b = new B();
+      b.foo();
+      b.bar();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerReflectiveNameTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerReflectiveNameTest.java
new file mode 100644
index 0000000..da3165d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerReflectiveNameTest.java
@@ -0,0 +1,91 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.classmerging.vertical;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper.DexVm.Version;
+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 VerticalClassMergerReflectiveNameTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters()
+        .withCfRuntimes()
+        .withDexRuntimesStartingFromIncluding(Version.V8_1_0)
+        .withAllApiLevelsAlsoForCf()
+        .build();
+  }
+
+  public VerticalClassMergerReflectiveNameTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(Main.class, A.class, B.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A::foo", "B::foo");
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class, A.class, B.class)
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A::foo", "B::foo");
+  }
+
+  public static class A {
+
+    @NeverInline
+    public void foo() {
+      System.out.println("A::foo");
+    }
+  }
+
+  @NeverClassInline
+  public static class B extends A {
+
+    @NeverInline
+    public void bar() {
+      System.out.println("B::foo");
+    }
+  }
+
+  public static class Main {
+
+    private static final String className =
+        "com.android.tools.r8.classmerging.vertical.VerticalClassMergerReflectiveNameTest$A";
+
+    static {
+      try {
+        Class.forName(className);
+      } catch (ClassNotFoundException e) {
+      }
+    }
+
+    public static void main(String[] args) {
+      B b = new B();
+      b.foo();
+      b.bar();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
index 4a3cc91..d7d31c9 100644
--- a/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/vertical/VerticalClassMergerTest.java
@@ -387,37 +387,6 @@
   }
 
   @Test
-  public void testMethodCollision() throws Throwable {
-    String main = "classmerging.MethodCollisionTest";
-    Path[] programFiles =
-        new Path[] {
-            CF_DIR.resolve("MethodCollisionTest.class"),
-            CF_DIR.resolve("MethodCollisionTest$A.class"),
-            CF_DIR.resolve("MethodCollisionTest$B.class"),
-            CF_DIR.resolve("MethodCollisionTest$C.class"),
-            CF_DIR.resolve("MethodCollisionTest$D.class")
-        };
-    // TODO(christofferqa): Currently we do not allow merging A into B because we find a collision.
-    // However, we are free to change the names of private methods, so we should handle them similar
-    // to fields (i.e., we should allow merging A into B). This would also improve the performance
-    // of the collision detector, because it would only have to consider non-private methods.
-    Set<String> preservedClassNames =
-        ImmutableSet.of(
-            "classmerging.MethodCollisionTest",
-            "classmerging.MethodCollisionTest$A",
-            "classmerging.MethodCollisionTest$B",
-            "classmerging.MethodCollisionTest$C",
-            "classmerging.MethodCollisionTest$D");
-    runTest(
-        testForR8(parameters.getBackend())
-            .addKeepRules(getProguardConfig(EXAMPLE_KEEP))
-            .allowUnusedProguardConfigurationRules(),
-        main,
-        programFiles,
-        preservedClassNames::contains);
-  }
-
-  @Test
   public void testNestedDefaultInterfaceMethodsTest() throws Throwable {
     String main = "classmerging.NestedDefaultInterfaceMethodsTest";
     Path[] programFiles =
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryTestBase.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryTestBase.java
index 5856c44..ccfaeaf 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryTestBase.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/DesugaredLibraryTestBase.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.desugar.desugaredlibrary;
 
+import static com.android.tools.r8.utils.InternalOptions.ASM_VERSION;
 import static junit.framework.TestCase.assertEquals;
 import static junit.framework.TestCase.assertTrue;
 
@@ -39,6 +40,9 @@
 import java.util.List;
 import java.util.Map;
 import java.util.function.Consumer;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
 
 public class DesugaredLibraryTestBase extends TestBase {
 
@@ -63,7 +67,7 @@
   }
 
   protected boolean requiresEmulatedInterfaceCoreLibDesugaring(TestParameters parameters) {
-    return parameters.getApiLevel().getLevel() < AndroidApiLevel.N.getLevel();
+    return parameters.getApiLevel().isLessThan(apiLevelWithDefaultInterfaceMethodsSupport());
   }
 
   protected boolean requiresAnyCoreLibDesugaring(TestParameters parameters) {
@@ -241,8 +245,7 @@
       Path desugaredProgramClassFile, Path desugaredLibraryClassFile) throws Exception {
     Path generatedKeepRules = temp.newFile().toPath();
     TraceReferences.run(
-        "--format",
-        "keep",
+        "--keep-rules",
         "--lib",
         ToolHelper.getAndroidJar(AndroidApiLevel.P).toString(),
         "--target",
@@ -257,6 +260,48 @@
     return FileUtils.readTextFile(generatedKeepRules, Charsets.UTF_8);
   }
 
+  protected static ClassFileInfo extractClassFileInfo(byte[] classFileBytes) {
+    class ClassFileInfoExtractor extends ClassVisitor {
+      private String classBinaryName;
+      private List<String> interfaces = new ArrayList<>();
+      private final List<String> methodNames = new ArrayList<>();
+
+      private ClassFileInfoExtractor() {
+        super(ASM_VERSION);
+      }
+
+      @Override
+      public void visit(
+          int version,
+          int access,
+          String name,
+          String signature,
+          String superName,
+          String[] interfaces) {
+        classBinaryName = name;
+        this.interfaces.addAll(Arrays.asList(interfaces));
+        super.visit(version, access, name, signature, superName, interfaces);
+      }
+
+      @Override
+      public MethodVisitor visitMethod(
+          int access, String name, String desc, String signature, String[] exceptions) {
+        methodNames.add(name);
+        return super.visitMethod(access, name, desc, signature, exceptions);
+      }
+
+      ClassFileInfo getClassFileInfo() {
+        return new ClassFileInfo(classBinaryName, interfaces, methodNames);
+      }
+    }
+
+    ClassReader reader = new ClassReader(classFileBytes);
+    ClassFileInfoExtractor extractor = new ClassFileInfoExtractor();
+    reader.accept(
+        extractor, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
+    return extractor.getClassFileInfo();
+  }
+
   public interface KeepRuleConsumer extends StringConsumer {
 
     String get();
@@ -310,4 +355,28 @@
       return result;
     }
   }
+
+  protected static class ClassFileInfo {
+    private final String classBinaryName;
+    private List<String> interfaces;
+    private final List<String> methodNames;
+
+    ClassFileInfo(String classBinaryNamename, List<String> interfaces, List<String> methodNames) {
+      this.classBinaryName = classBinaryNamename;
+      this.interfaces = interfaces;
+      this.methodNames = methodNames;
+    }
+
+    public String getClassBinaryName() {
+      return classBinaryName;
+    }
+
+    public List<String> getInterfaces() {
+      return interfaces;
+    }
+
+    public List<String> getMethodNames() {
+      return methodNames;
+    }
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/IteratorTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/IteratorTest.java
new file mode 100644
index 0000000..2a968a0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/IteratorTest.java
@@ -0,0 +1,209 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.desugaredlibrary;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.fail;
+import static org.hamcrest.CoreMatchers.containsString;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.AndroidApiLevel;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.ZipUtils;
+import java.nio.file.Path;
+import java.util.Iterator;
+import java.util.List;
+import java.util.function.Consumer;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class IteratorTest extends DesugaredLibraryTestBase {
+
+  private final TestParameters parameters;
+  private final boolean shrinkDesugaredLibrary;
+  private final boolean canUseDefaultAndStaticInterfaceMethods;
+
+  private static final String EXPECTED_OUTPUT = StringUtils.lines("1", "2", "3");
+
+  @Parameters(name = "{1}, shrinkDesugaredLibrary: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(),
+        getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build());
+  }
+
+  public IteratorTest(boolean shrinkDesugaredLibrary, TestParameters parameters) {
+    this.shrinkDesugaredLibrary = shrinkDesugaredLibrary;
+    this.parameters = parameters;
+    this.canUseDefaultAndStaticInterfaceMethods =
+        parameters
+            .getApiLevel()
+            .isGreaterThanOrEqualTo(apiLevelWithDefaultInterfaceMethodsSupport());
+  }
+
+  @Test
+  public void testIterator() throws Exception {
+    if (parameters.isCfRuntime()) {
+      testForJvm()
+          .addInnerClasses(IteratorTest.class)
+          .run(parameters.getRuntime(), Main.class)
+          .assertSuccessWithOutput(EXPECTED_OUTPUT);
+      return;
+    }
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    testForD8()
+        .addInnerClasses(IteratorTest.class)
+        .setMinApi(parameters.getApiLevel())
+        .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .compile()
+        .addDesugaredCoreLibraryRunClassPath(
+            this::buildDesugaredLibrary,
+            parameters.getApiLevel(),
+            keepRuleConsumer.get(),
+            shrinkDesugaredLibrary)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutput(EXPECTED_OUTPUT);
+  }
+
+  @Test
+  public void testD8Cf() throws Exception {
+    // Use D8 to desugar with Java classfile output.
+    Path firstJar =
+        testForD8(Backend.CF)
+            .setMinApi(parameters.getApiLevel())
+            .addProgramClasses(Main.class, MyIterator.class)
+            .enableCoreLibraryDesugaring(parameters.getApiLevel(), new AbsentKeepRuleConsumer())
+            .compile()
+            .writeToZip();
+
+    ClassFileInfo info =
+        extractClassFileInfo(
+            ZipUtils.readSingleEntry(firstJar, ZipUtils.zipEntryNameForClass(MyIterator.class)));
+    assertEquals(
+        MyIterator.class.getTypeName(),
+        DescriptorUtils.getJavaTypeFromBinaryName(info.getClassBinaryName()));
+    assertEquals(
+        canUseDefaultAndStaticInterfaceMethods ? 0 : 1,
+        info.getInterfaces().stream().filter(name -> name.equals("j$/util/Iterator")).count());
+    assertEquals(
+        canUseDefaultAndStaticInterfaceMethods ? 1 : 2,
+        info.getMethodNames().stream().filter(name -> name.equals("forEachRemaining")).count());
+
+    AndroidApiLevel apiLevelNotRequiringDesugaring = AndroidApiLevel.N;
+    if (parameters.getApiLevel().isLessThan(apiLevelNotRequiringDesugaring)) {
+      try {
+        // Use D8 to desugar with Java classfile output.
+        testForD8(Backend.CF)
+            .setMinApi(parameters.getApiLevel())
+            .addProgramFiles(firstJar)
+            .enableCoreLibraryDesugaring(parameters.getApiLevel(), new AbsentKeepRuleConsumer())
+            .compileWithExpectedDiagnostics(
+                diagnostics ->
+                    diagnostics.assertErrorsMatch(
+                        diagnosticMessage(
+                            containsString(
+                                "Code has already been library desugared. "
+                                    + "Interface Lj$/util/Iterator; is already implemented"))));
+        fail("Expected failure");
+      } catch (CompilationFailedException e) {
+        // Expected.
+      }
+    }
+
+    // Use D8 to desugar with Java classfile output.
+    Path secondJar =
+        testForD8(Backend.CF)
+            .addOptionsModification(
+                options ->
+                    options.desugarSpecificOptions().allowDesugaredInput =
+                        parameters.getApiLevel().isLessThan(apiLevelNotRequiringDesugaring))
+            .setMinApi(parameters.getApiLevel())
+            .addProgramFiles(firstJar)
+            .enableCoreLibraryDesugaring(parameters.getApiLevel(), new AbsentKeepRuleConsumer())
+            .compile()
+            .writeToZip();
+
+    info =
+        extractClassFileInfo(
+            ZipUtils.readSingleEntry(secondJar, ZipUtils.zipEntryNameForClass(MyIterator.class)));
+    assertEquals(
+        MyIterator.class.getTypeName(),
+        DescriptorUtils.getJavaTypeFromBinaryName(info.getClassBinaryName()));
+    assertEquals(
+        canUseDefaultAndStaticInterfaceMethods ? 0 : 1,
+        info.getInterfaces().stream().filter(name -> name.equals("j$/util/Iterator")).count());
+    // TODO(b/171867367): This should only be 2.
+    assertEquals(
+        canUseDefaultAndStaticInterfaceMethods ? 1 : 3,
+        info.getMethodNames().stream().filter(name -> name.equals("forEachRemaining")).count());
+
+    if (parameters.getRuntime().isDex()) {
+      // Convert to DEX without desugaring and run.
+      testForD8()
+          .addProgramFiles(firstJar)
+          .setMinApi(parameters.getApiLevel())
+          .disableDesugaring()
+          .compile()
+          .addDesugaredCoreLibraryRunClassPath(
+              this::buildDesugaredLibrary,
+              parameters.getApiLevel(),
+              collectKeepRulesWithTraceReferences(
+                  firstJar, buildDesugaredLibraryClassFile(parameters.getApiLevel())),
+              shrinkDesugaredLibrary)
+          .run(parameters.getRuntime(), Main.class)
+          .assertSuccessWithOutput(EXPECTED_OUTPUT);
+    } else {
+      // Run on the JVM with desugared library on classpath.
+      testForJvm()
+          .addProgramFiles(firstJar)
+          .addRunClasspathFiles(buildDesugaredLibraryClassFile(parameters.getApiLevel()))
+          .run(parameters.getRuntime(), Main.class)
+          .assertSuccessWithOutput(EXPECTED_OUTPUT);
+    }
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      Iterator<Integer> iterator = new MyIterator<>(1, 2, 3);
+      iterator.forEachRemaining(System.out::println);
+    }
+  }
+
+  static class MyIterator<E> implements Iterator<E> {
+
+    int index;
+    E[] items;
+
+    @SafeVarargs
+    public MyIterator(E... items) {
+      this.items = items;
+    }
+
+    @Override
+    public boolean hasNext() {
+      return index < items.length;
+    }
+
+    @Override
+    public E next() {
+      return items[index++];
+    }
+
+    @Override
+    public void forEachRemaining(Consumer<? super E> action) {
+      while (hasNext()) {
+        action.accept(next());
+      }
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/CallBackConversionTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/CallBackConversionTest.java
index 92d2394..210216f 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/CallBackConversionTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/conversiontests/CallBackConversionTest.java
@@ -11,7 +11,9 @@
 import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.ZipUtils;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
 import java.nio.file.Path;
@@ -104,7 +106,7 @@
   @Test
   public void testCallBackD8Cf() throws Exception {
     // Use D8 to desugar with Java classfile output.
-    Path jar =
+    Path firstJar =
         testForD8(Backend.CF)
             .setMinApi(parameters.getApiLevel())
             .addProgramClasses(Impl.class)
@@ -114,9 +116,37 @@
             .inspect(CallBackConversionTest::assertDuplicatedAPI)
             .writeToZip();
 
+    ClassFileInfo info =
+        extractClassFileInfo(
+            ZipUtils.readSingleEntry(firstJar, ZipUtils.zipEntryNameForClass(Impl.class)));
+    assertEquals(
+        Impl.class.getTypeName(),
+        DescriptorUtils.getJavaTypeFromBinaryName(info.getClassBinaryName()));
+    assertEquals(2, info.getMethodNames().stream().filter(name -> name.equals("foo")).count());
+
+    // Use D8 to desugar with Java classfile output.
+    Path secondJar =
+        testForD8(Backend.CF)
+            .setMinApi(parameters.getApiLevel())
+            .addProgramFiles(firstJar)
+            .addLibraryClasses(CustomLibClass.class)
+            .enableCoreLibraryDesugaring(parameters.getApiLevel(), new AbsentKeepRuleConsumer())
+            .compile()
+            .inspect(CallBackConversionTest::assertDuplicatedAPI)
+            .writeToZip();
+
+    info =
+        extractClassFileInfo(
+            ZipUtils.readSingleEntry(secondJar, ZipUtils.zipEntryNameForClass(Impl.class)));
+    assertEquals(
+        Impl.class.getTypeName(),
+        DescriptorUtils.getJavaTypeFromBinaryName(info.getClassBinaryName()));
+    // TODO(b/171867367): This should only be 2.
+    assertEquals(3, info.getMethodNames().stream().filter(name -> name.equals("foo")).count());
+
     // Convert to DEX without desugaring and run.
     testForD8()
-        .addProgramFiles(jar)
+        .addProgramFiles(firstJar)
         .setMinApi(parameters.getApiLevel())
         .disableDesugaring()
         .compile()
@@ -125,7 +155,7 @@
             this::buildDesugaredLibrary,
             parameters.getApiLevel(),
             collectKeepRulesWithTraceReferences(
-                jar, buildDesugaredLibraryClassFile(parameters.getApiLevel())),
+                firstJar, buildDesugaredLibraryClassFile(parameters.getApiLevel())),
             shrinkDesugaredLibrary)
         .addRunClasspathFiles(CUSTOM_LIB)
         .run(parameters.getRuntime(), Impl.class)
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonEnumTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonEnumTest.java
new file mode 100644
index 0000000..d71b536
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/gson/GsonEnumTest.java
@@ -0,0 +1,80 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.desugaredlibrary.gson;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
+import com.android.tools.r8.utils.BooleanUtils;
+import java.lang.reflect.Field;
+import java.time.chrono.IsoEra;
+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 GsonEnumTest extends DesugaredLibraryTestBase {
+
+  private final TestParameters parameters;
+  private final boolean shrinkDesugaredLibrary;
+
+  @Parameters(name = "{1}, shrinkDesugaredLibrary: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withDexRuntimes().withAllApiLevels().build());
+  }
+
+  public GsonEnumTest(boolean shrinkDesugaredLibrary, TestParameters parameters) {
+    this.shrinkDesugaredLibrary = shrinkDesugaredLibrary;
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testCustomCollectionD8() throws Exception {
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    testForD8()
+        .addInnerClasses(GsonEnumTest.class)
+        .setMinApi(parameters.getApiLevel())
+        .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .compile()
+        .addDesugaredCoreLibraryRunClassPath(
+            this::buildDesugaredLibrary,
+            parameters.getApiLevel(),
+            keepRuleConsumer.get(),
+            shrinkDesugaredLibrary)
+        .run(parameters.getRuntime(), Executor.class)
+        .assertSuccessWithOutputLines("0");
+  }
+
+  @Test
+  public void testCustomCollectionR8() throws Exception {
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    testForR8(Backend.DEX)
+        .addInnerClasses(GsonEnumTest.class)
+        .addKeepMainRule(Executor.class)
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .compile()
+        .addDesugaredCoreLibraryRunClassPath(
+            this::buildDesugaredLibrary,
+            parameters.getApiLevel(),
+            keepRuleConsumer.get(),
+            shrinkDesugaredLibrary)
+        .run(parameters.getRuntime(), Executor.class)
+        .assertSuccessWithOutputLines("0");
+  }
+
+  @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
+  static class Executor {
+    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
+      // For GSON to correctly serialize enums, it needs to be able to access all of the static
+      // enum fields.
+      Field bce = IsoEra.class.getField("BCE");
+      System.out.println(((IsoEra) bce.get(null)).ordinal());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/r8ondex/R8CompiledThroughDexTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/r8ondex/R8CompiledThroughDexTest.java
index b1a6811..b61e9d8 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/r8ondex/R8CompiledThroughDexTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/r8ondex/R8CompiledThroughDexTest.java
@@ -29,6 +29,8 @@
 @RunWith(Parameterized.class)
 public class R8CompiledThroughDexTest extends DesugaredLibraryTestBase {
 
+  private static final boolean testExternal = true;
+
   private final TestParameters parameters;
 
   @Parameters(name = "{0}")
@@ -69,34 +71,41 @@
     File ouputFolder = temp.newFolder("output");
 
     // Compile R8 to dex on the JVM.
-    Path ouputThroughCf = ouputFolder.toPath().resolve("outThroughCf.zip").toAbsolutePath();
-    ProcessResult javaProcessResult =
-        ToolHelper.runJava(
-            TestRuntime.getCheckedInJdk9(),
-            Collections.singletonList(ToolHelper.R8_WITH_RELOCATED_DEPS_JAR),
-            "-Xmx512m",
-            R8.class.getTypeName(),
-            "--release",
-            "--min-api",
-            Integer.toString(parameters.getApiLevel().getLevel()),
-            "--output",
-            ouputThroughCf.toString(),
-            "--lib",
-            ToolHelper.JAVA_8_RUNTIME,
-            "--pg-conf",
-            R8_KEEP,
-            ToolHelper.R8_WITH_RELOCATED_DEPS_JAR.toAbsolutePath().toString());
-    if (javaProcessResult.exitCode != 0) {
-      System.out.println(javaProcessResult);
+    Path outputThroughCf = ouputFolder.toPath().resolve("outThroughCf.zip").toAbsolutePath();
+    if (testExternal) {
+      ProcessResult javaProcessResult =
+          ToolHelper.runJava(
+              TestRuntime.getCheckedInJdk9(),
+              Collections.singletonList(ToolHelper.R8_WITH_RELOCATED_DEPS_JAR),
+              "-Xmx512m",
+              R8.class.getTypeName(),
+              "--release",
+              "--min-api",
+              Integer.toString(parameters.getApiLevel().getLevel()),
+              "--output",
+              outputThroughCf.toString(),
+              "--lib",
+              ToolHelper.JAVA_8_RUNTIME,
+              "--pg-conf",
+              R8_KEEP,
+              ToolHelper.R8_WITH_RELOCATED_DEPS_JAR.toAbsolutePath().toString());
+      assertEquals(javaProcessResult.toString(), 0, javaProcessResult.exitCode);
+    } else {
+      testForR8(parameters.getBackend())
+          .addProgramFiles(ToolHelper.R8_WITH_RELOCATED_DEPS_JAR)
+          .addLibraryFiles(ToolHelper.getJava8RuntimeJar())
+          .addKeepRuleFiles(Paths.get(R8_KEEP))
+          .setMinApi(parameters.getApiLevel())
+          .compile()
+          .writeToZip(outputThroughCf);
     }
-    assertEquals(0, javaProcessResult.exitCode);
 
     // Compile R8 to Dex on Dex, using the previous dex artifact.
     // We need the extra parameter --64 to use 64 bits frameworks.
     Path ouputThroughDex = ouputFolder.toPath().resolve("outThroughDex.zip").toAbsolutePath();
     ProcessResult artProcessResult =
         ToolHelper.runArtRaw(
-            Collections.singletonList(ouputThroughCf.toAbsolutePath().toString()),
+            Collections.singletonList(outputThroughCf.toAbsolutePath().toString()),
             R8.class.getTypeName(),
             (ToolHelper.ArtCommandBuilder builder) ->
                 builder.appendArtOption("--64").appendArtOption("-Xmx512m"),
@@ -118,6 +127,6 @@
     assertEquals(0, artProcessResult.exitCode);
 
     // Ensure both generated artifacts are equal.
-    assertTrue(BootstrapCurrentEqualityTest.filesAreEqual(ouputThroughCf, ouputThroughDex));
+    assertTrue(BootstrapCurrentEqualityTest.filesAreEqual(outputThroughCf, ouputThroughDex));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/desugar/softverificationerrorremoval/GetDeclaredMethodsErrorRemovalTest.java b/src/test/java/com/android/tools/r8/desugar/softverificationerrorremoval/GetDeclaredMethodsErrorRemovalTest.java
new file mode 100644
index 0000000..2b72a6f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/softverificationerrorremoval/GetDeclaredMethodsErrorRemovalTest.java
@@ -0,0 +1,95 @@
+// 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.softverificationerrorremoval;
+
+import static com.android.tools.r8.TestRuntime.CfRuntime.getCheckedInJdk8;
+import static org.hamcrest.CoreMatchers.containsString;
+
+import com.android.tools.r8.D8TestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.function.Supplier;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class GetDeclaredMethodsErrorRemovalTest extends TestBase {
+
+  private final TestParameters parameters;
+  private static final String TYPE =
+      "com.android.tools.r8.desugar.softverificationerrorremoval."
+          + "GetDeclaredMethodsErrorRemovalTest";
+  private static final String EXPECTED_RESULT =
+      "[void"
+          + " "
+          + TYPE
+          + "$ExampleClass.hello(),"
+          + " void"
+          + " "
+          + TYPE
+          + "$ExampleClass.hello(java.util.function.Supplier)]";
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public GetDeclaredMethodsErrorRemovalTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testWithoutJavaStub() throws Exception {
+    D8TestRunResult run =
+        testForD8()
+            .addInnerClasses(GetDeclaredMethodsErrorRemovalTest.class)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .run(parameters.getRuntime(), TestClass.class);
+    if (parameters.getDexRuntimeVersion().isOlderThanOrEqual(ToolHelper.DexVm.Version.V6_0_1)) {
+      run.assertFailureWithErrorThatMatches(containsString("java.lang.NoClassDefFoundError"));
+    } else {
+      run.assertSuccessWithOutputLines(EXPECTED_RESULT);
+    }
+  }
+
+  @Test
+  public void testWithJavaStub() throws Exception {
+    Path stubs =
+        javac(getCheckedInJdk8())
+            .addSourceFiles(Paths.get("src/test/javaStubs/Supplier.java"))
+            .compile();
+    testForD8()
+        .addInnerClasses(GetDeclaredMethodsErrorRemovalTest.class)
+        .addProgramFiles(stubs)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithOutputLines(EXPECTED_RESULT);
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      System.out.println(Arrays.toString(ExampleClass.class.getDeclaredMethods()));
+    }
+  }
+
+  static class ExampleClass {
+    void hello() {
+      System.out.println("hello");
+    }
+
+    void hello(Supplier<String> stringSupplier) {
+      System.out.println(stringSupplier.get());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/softverificationerrorremoval/SoftVerificationErrorRemovalTest.java b/src/test/java/com/android/tools/r8/desugar/softverificationerrorremoval/SoftVerificationErrorRemovalTest.java
new file mode 100644
index 0000000..1a1720e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/softverificationerrorremoval/SoftVerificationErrorRemovalTest.java
@@ -0,0 +1,94 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.desugar.softverificationerrorremoval;
+
+import static com.android.tools.r8.TestRuntime.CfRuntime.getCheckedInJdk8;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.D8TestRunResult;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.function.Supplier;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class SoftVerificationErrorRemovalTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  }
+
+  public SoftVerificationErrorRemovalTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testWithoutJavaStub() throws Exception {
+    D8TestRunResult run =
+        testForD8()
+            .addInnerClasses(SoftVerificationErrorRemovalTest.class)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .run(parameters.getRuntime(), TestClass.class);
+    assertVerificationErrorsPresent(
+        run.getStdErr(),
+        parameters.getDexRuntimeVersion().isOlderThanOrEqual(ToolHelper.DexVm.Version.V4_4_4));
+  }
+
+  private void assertVerificationErrorsPresent(String stdErr, boolean present) {
+    assertEquals(
+        present,
+        stdErr.contains(
+            "VFY: unable to find class referenced in signature (Ljava/util/function/Supplier;)"));
+    assertEquals(
+        present,
+        stdErr.contains(
+            "VFY: unable to resolve interface method 7: Ljava/util/function/Supplier;.get"
+                + " ()Ljava/lang/Object;"));
+  }
+
+  @Test
+  public void testWithJavaStub() throws Exception {
+    Path stubs =
+        javac(getCheckedInJdk8())
+            .addSourceFiles(Paths.get("src/test/javaStubs/Supplier.java"))
+            .compile();
+    D8TestRunResult run =
+        testForD8()
+            .addInnerClasses(SoftVerificationErrorRemovalTest.class)
+            .addProgramFiles(stubs)
+            .setMinApi(parameters.getApiLevel())
+            .compile()
+            .run(parameters.getRuntime(), TestClass.class);
+    assertVerificationErrorsPresent(run.getStdErr(), false);
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      ExampleClass exampleClass = new ExampleClass();
+      exampleClass.hello();
+    }
+  }
+
+  static class ExampleClass {
+    void hello() {
+      System.out.println("hello");
+    }
+
+    void hello(Supplier<String> stringSupplier) {
+      System.out.println(stringSupplier.get());
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/dex/DebugByteCodeWriterTest.java b/src/test/java/com/android/tools/r8/dex/DebugByteCodeWriterTest.java
index b48e0a93..0fe19bc 100644
--- a/src/test/java/com/android/tools/r8/dex/DebugByteCodeWriterTest.java
+++ b/src/test/java/com/android/tools/r8/dex/DebugByteCodeWriterTest.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.dex;
 
 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.DexDebugEvent;
 import com.android.tools.r8.graph.DexDebugInfo;
@@ -23,9 +24,11 @@
 
   private ObjectToOffsetMapping emptyObjectTObjectMapping() {
     return new ObjectToOffsetMapping(
-        AppInfo.createInitialAppInfo(
-            DexApplication.builder(new InternalOptions(new DexItemFactory(), new Reporter()), null)
-                .build()),
+        AppView.createForD8(
+            AppInfo.createInitialAppInfo(
+                DexApplication.builder(
+                        new InternalOptions(new DexItemFactory(), new Reporter()), null)
+                    .build())),
         GraphLens.getIdentityLens(),
         NamingLens.getIdentityLens(),
         InitClassLens.getDefault(),
diff --git a/src/test/java/com/android/tools/r8/dex/DexStringTest.java b/src/test/java/com/android/tools/r8/dex/DexStringTest.java
index b12d876..ad41738 100644
--- a/src/test/java/com/android/tools/r8/dex/DexStringTest.java
+++ b/src/test/java/com/android/tools/r8/dex/DexStringTest.java
@@ -78,8 +78,7 @@
   private void check(int expected, DexString s1, DexString s2) {
     assertEquals(s1.dump() + " " + s2.dump(),
         expected, Integer.signum(s1.toString().compareTo(s2.toString())));
-    assertEquals(s1.dump() + " " + s2.dump(),
-        expected, Integer.signum(s1.slowCompareTo(s2)));
+    assertEquals(s1.dump() + " " + s2.dump(), expected, Integer.signum(s1.compareTo(s2)));
   }
 
   private void checkEncodedLength(DexString s, int encodedLength) {
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 ab837d3..c494152 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumUnboxingTestBase.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.Diagnostic;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestDiagnosticMessages;
+import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.StringUtils;
@@ -86,10 +87,12 @@
   }
 
   static List<Object[]> enumUnboxingTestParameters() {
-    return buildParameters(
-        getTestParameters().withDexRuntimes().withAllApiLevels().build(),
-        BooleanUtils.values(),
-        getAllEnumKeepRules());
+    return enumUnboxingTestParameters(
+        getTestParameters().withDexRuntimes().withAllApiLevels().build());
+  }
+
+  static List<Object[]> enumUnboxingTestParameters(TestParametersCollection testParameters) {
+    return buildParameters(testParameters, BooleanUtils.values(), getAllEnumKeepRules());
   }
 
   protected static EnumKeepRules[] getAllEnumKeepRules() {
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/LambdaEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/LambdaEnumUnboxingTest.java
new file mode 100644
index 0000000..d6eb6d1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/enumunboxing/LambdaEnumUnboxingTest.java
@@ -0,0 +1,97 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.enumunboxing;
+
+import static org.junit.Assert.assertFalse;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.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 LambdaEnumUnboxingTest 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(getTestParameters().withAllRuntimesAndApiLevels().build());
+  }
+
+  public LambdaEnumUnboxingTest(
+      TestParameters parameters, boolean enumValueOptimization, EnumKeepRules enumKeepRules) {
+    this.parameters = parameters;
+    this.enumValueOptimization = enumValueOptimization;
+    this.enumKeepRules = enumKeepRules;
+  }
+
+  @Test
+  public void testEnumUnboxing() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addKeepRules(enumKeepRules.getKeepRules())
+        .addOptionsModification(
+            options -> {
+              if (options.isGeneratingClassFiles()) {
+                // TODO(b/172568606): Remove this when enabled for CF by default.
+                assertFalse(options.enableEnumUnboxing);
+                options.enableEnumUnboxing = true;
+              }
+            })
+        .enableNeverClassInliningAnnotations()
+        .enableInliningAnnotations()
+        .addOptionsModification(opt -> enableEnumOptions(opt, enumValueOptimization))
+        .addOptionsModification(options -> options.testing.enableEnumUnboxingDebugLogs = false)
+        .addEnumUnboxingInspector(inspector -> inspector.assertUnboxed(MyEnum.class))
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("0", "0", "1", "0", "0");
+  }
+
+  @NeverClassInline
+  enum MyEnum {
+    A,
+    B,
+    C
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(MyEnum.A.ordinal());
+      boolean[] booleans = new boolean[] {true, false};
+      forEach(booleans, Main::printAndGetEnum);
+      System.out.println(printAndGetEnum(true).ordinal());
+    }
+
+    @NeverInline
+    private static MyEnum printAndGetEnum(boolean b) {
+      MyEnum myEnum = b ? MyEnum.A : MyEnum.B;
+      System.out.println(myEnum.ordinal());
+      return myEnum;
+    }
+
+    @NeverInline
+    static void forEach(boolean[] booleans, MyBooleanConsumer consumer) {
+      for (boolean b : booleans) {
+        consumer.accept(b);
+      }
+    }
+  }
+
+  interface MyBooleanConsumer {
+
+    void accept(Boolean b);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/genericsignature/FieldSignatureTest.java b/src/test/java/com/android/tools/r8/graph/genericsignature/FieldSignatureTest.java
index b6e34ad..81d8e11 100644
--- a/src/test/java/com/android/tools/r8/graph/genericsignature/FieldSignatureTest.java
+++ b/src/test/java/com/android/tools/r8/graph/genericsignature/FieldSignatureTest.java
@@ -6,8 +6,11 @@
 
 import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
 import static com.google.common.base.Predicates.alwaysFalse;
+import static com.google.common.base.Predicates.alwaysTrue;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestDiagnosticMessages;
@@ -18,9 +21,11 @@
 import com.android.tools.r8.graph.GenericSignature;
 import com.android.tools.r8.graph.GenericSignature.FieldTypeSignature;
 import com.android.tools.r8.graph.GenericSignaturePrinter;
+import com.android.tools.r8.graph.GenericSignatureTypeRewriter;
 import com.android.tools.r8.naming.NamingLens;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.Reporter;
+import java.util.function.Function;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -106,4 +111,18 @@
     assertEquals(FieldTypeSignature.noSignature(), parsed);
     return testDiagnosticMessages;
   }
+
+  @Test
+  public void testPruningNullTest() {
+    DexItemFactory factory = new DexItemFactory();
+    FieldTypeSignature parsed =
+        GenericSignature.parseFieldTypeSignature(
+            "A", "Lfoo/bar/Baz;", Origin.unknown(), factory, new Reporter());
+    assertTrue(parsed.hasSignature());
+    GenericSignatureTypeRewriter rewriter =
+        new GenericSignatureTypeRewriter(factory, alwaysTrue(), Function.identity(), null);
+    FieldTypeSignature rewrittenType = rewriter.rewrite(parsed);
+    assertNotNull(rewrittenType);
+    assertTrue(rewrittenType.hasNoSignature());
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/internal/proto/ChromeProtoRewritingTest.java b/src/test/java/com/android/tools/r8/internal/proto/ChromeProtoRewritingTest.java
index ddaf85f..318de07 100644
--- a/src/test/java/com/android/tools/r8/internal/proto/ChromeProtoRewritingTest.java
+++ b/src/test/java/com/android/tools/r8/internal/proto/ChromeProtoRewritingTest.java
@@ -30,6 +30,7 @@
 
   public ChromeProtoRewritingTest(TestParameters parameters) {
     super(200430, false);
+    parameters.assertNoneRuntime();
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/InvokeMultiNewArraySideEffectTest.java b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/InvokeMultiNewArraySideEffectTest.java
index 487d60a..fbe9bd8 100644
--- a/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/InvokeMultiNewArraySideEffectTest.java
+++ b/src/test/java/com/android/tools/r8/ir/analysis/sideeffect/InvokeMultiNewArraySideEffectTest.java
@@ -9,6 +9,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.core.StringContains.containsString;
 
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -25,7 +26,7 @@
 
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().build();
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
   public InvokeMultiNewArraySideEffectTest(TestParameters parameters) {
@@ -37,7 +38,8 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(InvokeMultiNewArraySideEffectTest.class)
         .addKeepMainRule(TestClass.class)
-        .setMinApi(parameters.getRuntime())
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(
             inspector -> {
@@ -66,7 +68,9 @@
     }
   }
 
+  @NoHorizontalClassMerging
   static class A {}
 
+  @NoHorizontalClassMerging
   static class B {}
 }
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 807b061..af047b9 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
@@ -13,10 +13,14 @@
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
 import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
 import com.android.tools.r8.ir.optimize.enums.EnumUnboxingRewriter;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.ZipUtils;
@@ -58,6 +62,7 @@
   private final boolean allowAccessModification;
   private final TestParameters parameters;
   private Path outputDir = null;
+  private String nullabilityClass = "inlining.Nullability";
 
   public R8InliningTest(boolean allowAccessModification, TestParameters parameters) {
     this.allowAccessModification = allowAccessModification;
@@ -94,6 +99,14 @@
     return null;
   }
 
+  private void fixInliningNullabilityClass(
+      DexItemFactory dexItemFactory, HorizontallyMergedClasses horizontallyMergedClasses) {
+    DexType originalType =
+        dexItemFactory.createType(DescriptorUtils.javaTypeToDescriptor("inlining.Nullability"));
+    nullabilityClass =
+        horizontallyMergedClasses.getMergeTargetOrDefault(originalType).toSourceString();
+  }
+
   private void generateR8Version(Path out, Path mapFile, boolean inlining) throws Exception {
     assert parameters.isDexRuntime() || parameters.isCfRuntime();
     R8Command.Builder commandBuilder =
@@ -126,6 +139,7 @@
           // Tests depend on nullability of receiver and argument in general. Learning very accurate
           // nullability from actual usage in tests bothers what we want to test.
           o.callSiteOptimizationOptions().disableTypePropagationForTesting();
+          o.testing.horizontallyMergedClassesConsumer = this::fixInliningNullabilityClass;
         });
   }
 
@@ -201,7 +215,7 @@
     // a private class in another package.
     checkAbsent(clazz, "int", "callInterfaceMethod", ImmutableList.of("inlining.IFace"));
 
-    clazz = inspector.clazz("inlining.Nullability");
+    clazz = inspector.clazz(nullabilityClass);
     checkAbsentBooleanMethod(clazz, "inlinableWithPublicField");
     checkAbsentBooleanMethod(clazz, "inlinableWithControlFlow");
   }
@@ -309,7 +323,7 @@
     final int INLINABLE = allowAccessModification ? 0 : 1;
     final int NEVER_INLINABLE = 1;
 
-    ClassSubject clazz = inspector.clazz("inlining.Nullability");
+    ClassSubject clazz = inspector.clazz(nullabilityClass);
     MethodSubject m;
 
     m = clazz.method("int", "inlinable", ImmutableList.of("inlining.A"));
@@ -339,7 +353,7 @@
   public void invokeOnNonNullReceiver() throws Exception {
     CodeInspector inspector =
         new CodeInspector(getGeneratedFiles(), getGeneratedProguardMap(), null);
-    ClassSubject clazz = inspector.clazz("inlining.Nullability");
+    ClassSubject clazz = inspector.clazz(nullabilityClass);
     MethodSubject m = clazz.method("int", "conditionalOperator", ImmutableList.of("inlining.A"));
     assertTrue(m.isPresent());
 
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerSimplePairBuilderTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerSimplePairBuilderTest.java
index f751e25..1d1f00e 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerSimplePairBuilderTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerSimplePairBuilderTest.java
@@ -10,6 +10,7 @@
 import static org.junit.Assert.assertEquals;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.utils.StringUtils;
@@ -57,6 +58,7 @@
         // TODO(b/143129517): This relies on PairBuilder::build being inlined, thus the limit of 6.
         .addOptionsModification(options -> options.inliningInstructionLimit = 6)
         .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .noMinification()
         .setMinApi(parameters.getApiLevel())
         .compile()
@@ -138,6 +140,7 @@
     }
   }
 
+  @NoHorizontalClassMerging
   static class PairBuilder<F, S> {
 
     F first;
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
index 8d73c3a..cf52085 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/ClassInlinerTest.java
@@ -129,8 +129,7 @@
         Collections.emptySet(), collectTypes(clazz.uniqueMethodWithName("testCallOnIface1")));
 
     assertEquals(
-        Collections.singleton("com.android.tools.r8.ir.optimize.classinliner.trivial.Iface2Impl"),
-        collectTypes(clazz.uniqueMethodWithName("testCallOnIface2")));
+        Collections.emptySet(), collectTypes(clazz.uniqueMethodWithName("testCallOnIface2")));
 
     assertEquals(
         Sets.newHashSet(
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/cost/NonMaterializingFieldAccessesAfterClassInliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/cost/NonMaterializingFieldAccessesAfterClassInliningTest.java
index c95fa85..9df6cb2 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/cost/NonMaterializingFieldAccessesAfterClassInliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/cost/NonMaterializingFieldAccessesAfterClassInliningTest.java
@@ -9,6 +9,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -40,6 +41,7 @@
         // Should be able to class inline Builder even when the threshold is low.
         .addOptionsModification(options -> options.classInliningInstructionAllowance = 3)
         .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(this::inspect)
@@ -121,6 +123,7 @@
     }
   }
 
+  @NoHorizontalClassMerging
   static class Builder {
 
     char c1;
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/CycleReferenceAB.java b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/CycleReferenceAB.java
index d26641a..0390712 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/CycleReferenceAB.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/classinliner/trivial/CycleReferenceAB.java
@@ -4,6 +4,9 @@
 
 package com.android.tools.r8.ir.optimize.classinliner.trivial;
 
+import com.android.tools.r8.NoHorizontalClassMerging;
+
+@NoHorizontalClassMerging
 public class CycleReferenceAB {
   private String a;
 
diff --git a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
index 177a8a5..f76758c 100644
--- a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
+++ b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
@@ -5,6 +5,7 @@
 package com.android.tools.r8.kotlin;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
@@ -14,8 +15,9 @@
 import static org.junit.Assert.fail;
 
 import com.android.tools.r8.KotlinTestBase;
-import com.android.tools.r8.OutputMode;
-import com.android.tools.r8.R8Command;
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.ThrowableConsumer;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.graph.Code;
@@ -23,22 +25,18 @@
 import com.android.tools.r8.jasmin.JasminBuilder;
 import com.android.tools.r8.jasmin.JasminBuilder.ClassBuilder;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
-import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.DescriptorUtils;
-import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.FieldSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
-import com.google.common.collect.ImmutableList;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
-import java.util.function.Consumer;
 import org.junit.Assume;
 
 public abstract class AbstractR8KotlinTestBase extends KotlinTestBase {
@@ -217,30 +215,6 @@
     return code.asDexCode();
   }
 
-  private String buildProguardRules(String mainClass) {
-    ProguardRulesBuilder proguardRules = new ProguardRulesBuilder();
-    proguardRules.appendWithLineSeparator(keepMainProguardConfiguration(mainClass));
-    proguardRules.dontObfuscate();
-    if (allowAccessModification) {
-      proguardRules.allowAccessModification();
-    }
-    return proguardRules.toString();
-  }
-
-  protected String keepAllMembers(String className) {
-    return StringUtils.lines(
-        "-keep class " + className + " {",
-        "  *;",
-        "}");
-  }
-
-  protected String keepMainMethod(String className) {
-    return StringUtils.lines(
-        "-keepclasseswithmembers class " + className + " {",
-        "  public static void main(...);",
-        "}");
-  }
-
   protected String keepClassMethod(String className, MethodSignature methodSignature) {
     return StringUtils.lines(
         "-keep class " + className + " {",
@@ -255,54 +229,21 @@
         "}");
   }
 
-  protected void runTest(String folder, String mainClass,
-      AndroidAppInspector inspector) throws Exception {
-    runTest(folder, mainClass, null, null, inspector);
+  protected R8TestRunResult runTest(String folder, String mainClass) throws Exception {
+    return runTest(folder, mainClass, null);
   }
 
-  protected void runTest(String folder, String mainClass,
-      Consumer<InternalOptions> optionsConsumer, AndroidAppInspector inspector) throws Exception {
-    runTest(folder, mainClass, null, optionsConsumer, inspector);
-  }
-
-  protected void runTest(String folder, String mainClass,
-      String extraProguardRules, AndroidAppInspector inspector) throws Exception {
-    runTest(folder, mainClass, extraProguardRules, null, inspector);
-  }
-
-  protected void runTest(String folder, String mainClass, String extraProguardRules,
-      Consumer<InternalOptions> optionsConsumer, AndroidAppInspector inspector) throws Exception {
+  protected R8TestRunResult runTest(
+      String folder, String mainClass, ThrowableConsumer<R8FullTestBuilder> configuration)
+      throws Exception {
     Assume.assumeTrue(ToolHelper.artSupported() || ToolHelper.compareAgaintsGoldenFiles());
 
-    String proguardRules = buildProguardRules(mainClass);
-    if (extraProguardRules != null) {
-      proguardRules += extraProguardRules;
-    }
-
     // Build classpath for compilation (and java execution)
     classpath.clear();
     classpath.add(getKotlinJarFile(folder));
     classpath.add(getJavaJarFile(folder));
     classpath.addAll(extraClasspath);
 
-    // Build with R8
-    AndroidApp.Builder builder = AndroidApp.builder();
-    builder.addProgramFiles(classpath);
-    R8Command.Builder commandBuilder =
-        ToolHelper.prepareR8CommandBuilder(builder.build(), emptyConsumer(Backend.DEX))
-            .addLibraryFiles(runtimeJar(Backend.DEX))
-            .addProguardConfiguration(ImmutableList.of(proguardRules), Origin.unknown());
-    ToolHelper.allowTestProguardOptions(commandBuilder);
-    AndroidApp app = ToolHelper.runR8(commandBuilder.build(), optionsConsumer);
-
-    // Materialize file for execution.
-    Path generatedDexFile = temp.getRoot().toPath().resolve("classes.jar");
-    app.writeToZip(generatedDexFile, OutputMode.DexIndexed);
-
-    // Run with ART.
-    String artOutput =
-        ToolHelper.runArtNoVerificationErrors(generatedDexFile.toString(), mainClass);
-
     // Compare with Java.
     ToolHelper.ProcessResult javaResult = ToolHelper.runJava(classpath, mainClass);
     if (javaResult.exitCode != 0) {
@@ -310,11 +251,22 @@
       System.err.println(javaResult.stderr);
       fail("JVM failed for: " + mainClass);
     }
-    assertEquals("JVM and ART output differ", javaResult.stdout, artOutput);
 
-    if (inspector != null) {
-      inspector.inspectApp(app);
-    }
+    // Build with R8
+    return testForR8(Backend.DEX)
+        .addProgramFiles(classpath)
+        .addKeepMainRule(mainClass)
+        .allowAccessModification(allowAccessModification)
+        .allowDiagnosticMessages()
+        .enableProguardTestOptions()
+        .noMinification()
+        .apply(configuration)
+        .compile()
+        .assertAllWarningMessagesMatch(
+            containsString("Resource 'META-INF/MANIFEST.MF' already exists."))
+        .assertAllInfoMessagesMatch(containsString("Unrecognized Kotlin lambda "))
+        .run(mainClass)
+        .assertSuccessWithOutput(javaResult.stdout);
   }
 
   protected void checkClassExistsInInput(String className) {
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
index d8419ca..2bb0867 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
@@ -12,6 +12,9 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
+import com.android.tools.r8.R8FullTestBuilder;
+import com.android.tools.r8.R8TestRunResult;
+import com.android.tools.r8.ThrowableConsumer;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.code.NewInstance;
 import com.android.tools.r8.code.SgetObject;
@@ -20,7 +23,6 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.utils.BooleanUtils;
-import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
@@ -78,184 +80,216 @@
   public void testJStyleLambdas() throws Exception {
     assumeTrue("Only work with -allowaccessmodification", allowAccessModification);
     final String mainClassName = "class_inliner_lambda_j_style.MainKt";
-    runTest(
-        "class_inliner_lambda_j_style",
-        mainClassName,
-        false,
-        app -> {
-          CodeInspector inspector = new CodeInspector(app);
-          assertThat(
-              inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful$1"), isPresent());
-          assertThat(
-              inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful2$1"), isPresent());
-          assertThat(
-              inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful3$1"), isPresent());
-        });
+    runTestWithDefaults(
+            "class_inliner_lambda_j_style",
+            mainClassName,
+            testBuilder ->
+                testBuilder
+                    // TODO(jsjeon): Introduce @NeverInline to kotlinR8TestResources
+                    .addKeepRules("-neverinline class * { void test*State*(...); }")
+                    .noClassInlining())
+        .inspect(
+            inspector -> {
+              assertThat(
+                  inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful$1"),
+                  isPresent());
+              assertThat(
+                  inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful2$1"),
+                  isPresent());
+              assertThat(
+                  inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful3$1"),
+                  isPresent());
+            });
 
-    runTest(
-        "class_inliner_lambda_j_style",
-        mainClassName,
-        true,
-        app -> {
-          CodeInspector inspector = new CodeInspector(app);
-          Predicate<DexType> lambdaCheck = createLambdaCheck(inspector);
-          ClassSubject clazz = inspector.clazz(mainClassName);
+    runTestWithDefaults(
+            "class_inliner_lambda_j_style",
+            mainClassName,
+            testBuilder ->
+                testBuilder
+                    // TODO(jsjeon): Introduce @NeverInline to kotlinR8TestResources
+                    .addKeepRules("-neverinline class * { void test*State*(...); }"))
+        .inspect(
+            inspector -> {
+              Predicate<DexType> lambdaCheck = createLambdaCheck(inspector);
+              ClassSubject clazz = inspector.clazz(mainClassName);
 
-          assertEquals(
-              Sets.newHashSet(), collectAccessedTypes(lambdaCheck, clazz, "testStateless"));
+              assertEquals(
+                  Sets.newHashSet(), collectAccessedTypes(lambdaCheck, clazz, "testStateless"));
 
-          assertEquals(Sets.newHashSet(), collectAccessedTypes(lambdaCheck, clazz, "testStateful"));
+              assertEquals(
+                  Sets.newHashSet(), collectAccessedTypes(lambdaCheck, clazz, "testStateful"));
 
-          assertThat(
-              inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful$1"),
-              not(isPresent()));
+              assertThat(
+                  inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful$1"),
+                  not(isPresent()));
 
-          assertEquals(
-              Sets.newHashSet(), collectAccessedTypes(lambdaCheck, clazz, "testStateful2"));
+              assertEquals(
+                  Sets.newHashSet(), collectAccessedTypes(lambdaCheck, clazz, "testStateful2"));
 
-          assertThat(
-              inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful2$1"),
-              not(isPresent()));
+              assertThat(
+                  inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful2$1"),
+                  not(isPresent()));
 
-          assertEquals(
-              Sets.newHashSet(), collectAccessedTypes(lambdaCheck, clazz, "testStateful3"));
+              assertEquals(
+                  Sets.newHashSet(), collectAccessedTypes(lambdaCheck, clazz, "testStateful3"));
 
-          assertThat(
-              inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful3$1"),
-              not(isPresent()));
-        });
+              assertThat(
+                  inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful3$1"),
+                  not(isPresent()));
+            });
   }
 
   @Test
   public void testKStyleLambdas() throws Exception {
     assumeTrue("Only work with -allowaccessmodification", allowAccessModification);
     final String mainClassName = "class_inliner_lambda_k_style.MainKt";
-    runTest(
-        "class_inliner_lambda_k_style",
-        mainClassName,
-        false,
-        app -> {
-          CodeInspector inspector = new CodeInspector(app);
-          assertThat(
-              inspector.clazz("class_inliner_lambda_k_style.MainKt$testKotlinSequencesStateless$1"),
-              isPresent());
-          assertThat(
-              inspector.clazz("class_inliner_lambda_k_style.MainKt$testKotlinSequencesStateful$1"),
-              isPresent());
-          assertThat(
-              inspector.clazz("class_inliner_lambda_k_style.MainKt$testBigExtraMethod$1"),
-              isPresent());
-          assertThat(
-              inspector.clazz("class_inliner_lambda_k_style.MainKt$testBigExtraMethod2$1"),
-              isPresent());
-          assertThat(
-              inspector.clazz("class_inliner_lambda_k_style.MainKt$testBigExtraMethod3$1"),
-              isPresent());
-          assertThat(
-              inspector.clazz(
-                  "class_inliner_lambda_k_style.MainKt$testBigExtraMethodReturningLambda$1"),
-              isPresent());
-          assertThat(
-              inspector.clazz(
-                  "class_inliner_lambda_k_style.MainKt$testBigExtraMethodReturningLambda2$1"),
-              isPresent());
-          assertThat(
-              inspector.clazz(
-                  "class_inliner_lambda_k_style.MainKt$testBigExtraMethodReturningLambda3$1"),
-              isPresent());
-        });
+    runTestWithDefaults(
+            "class_inliner_lambda_k_style",
+            mainClassName,
+            testBuilder ->
+                testBuilder
+                    // TODO(jsjeon): Introduce @NeverInline to kotlinR8TestResources
+                    .addKeepRules(
+                        "-neverinline class * { void test*State*(...); }",
+                        "-neverinline class * { void testBigExtraMethod(...); }",
+                        "-neverinline class * { void testBigExtraMethodReturningLambda(...); }")
+                    .noClassInlining())
+        .inspect(
+            inspector -> {
+              assertThat(
+                  inspector.clazz(
+                      "class_inliner_lambda_k_style.MainKt$testKotlinSequencesStateless$1"),
+                  isPresent());
+              assertThat(
+                  inspector.clazz(
+                      "class_inliner_lambda_k_style.MainKt$testKotlinSequencesStateful$1"),
+                  isPresent());
+              assertThat(
+                  inspector.clazz("class_inliner_lambda_k_style.MainKt$testBigExtraMethod$1"),
+                  isPresent());
+              assertThat(
+                  inspector.clazz("class_inliner_lambda_k_style.MainKt$testBigExtraMethod2$1"),
+                  isPresent());
+              assertThat(
+                  inspector.clazz("class_inliner_lambda_k_style.MainKt$testBigExtraMethod3$1"),
+                  isPresent());
+              assertThat(
+                  inspector.clazz(
+                      "class_inliner_lambda_k_style.MainKt$testBigExtraMethodReturningLambda$1"),
+                  isPresent());
+              assertThat(
+                  inspector.clazz(
+                      "class_inliner_lambda_k_style.MainKt$testBigExtraMethodReturningLambda2$1"),
+                  isPresent());
+              assertThat(
+                  inspector.clazz(
+                      "class_inliner_lambda_k_style.MainKt$testBigExtraMethodReturningLambda3$1"),
+                  isPresent());
+            });
 
-    runTest(
-        "class_inliner_lambda_k_style",
-        mainClassName,
-        true,
-        app -> {
-          CodeInspector inspector = new CodeInspector(app);
-          Predicate<DexType> lambdaCheck = createLambdaCheck(inspector);
-          ClassSubject clazz = inspector.clazz(mainClassName);
+    runTestWithDefaults(
+            "class_inliner_lambda_k_style",
+            mainClassName,
+            testBuilder ->
+                testBuilder
+                    // TODO(jsjeon): Introduce @NeverInline to kotlinR8TestResources
+                    .addKeepRules(
+                    "-neverinline class * { void test*State*(...); }",
+                    "-neverinline class * { void testBigExtraMethod(...); }",
+                    "-neverinline class * { void testBigExtraMethodReturningLambda(...); }"))
+        .inspect(
+            inspector -> {
+              Predicate<DexType> lambdaCheck = createLambdaCheck(inspector);
+              ClassSubject clazz = inspector.clazz(mainClassName);
 
-          assertEquals(
-              Sets.newHashSet(),
-              collectAccessedTypes(
-                  lambdaCheck, clazz, "testKotlinSequencesStateless", "kotlin.sequences.Sequence"));
+              assertEquals(
+                  Sets.newHashSet(),
+                  collectAccessedTypes(
+                      lambdaCheck,
+                      clazz,
+                      "testKotlinSequencesStateless",
+                      "kotlin.sequences.Sequence"));
 
-          assertThat(
-              inspector.clazz("class_inliner_lambda_k_style.MainKt$testKotlinSequencesStateless$1"),
-              not(isPresent()));
+              assertThat(
+                  inspector.clazz(
+                      "class_inliner_lambda_k_style.MainKt$testKotlinSequencesStateless$1"),
+                  not(isPresent()));
 
-          assertEquals(
-              Sets.newHashSet(),
-              collectAccessedTypes(
-                  lambdaCheck,
-                  clazz,
-                  "testKotlinSequencesStateful",
-                  "int",
-                  "int",
-                  "kotlin.sequences.Sequence"));
+              assertEquals(
+                  Sets.newHashSet(),
+                  collectAccessedTypes(
+                      lambdaCheck,
+                      clazz,
+                      "testKotlinSequencesStateful",
+                      "int",
+                      "int",
+                      "kotlin.sequences.Sequence"));
 
-          assertThat(
-              inspector.clazz("class_inliner_lambda_k_style.MainKt$testKotlinSequencesStateful$1"),
-              not(isPresent()));
+              assertThat(
+                  inspector.clazz(
+                      "class_inliner_lambda_k_style.MainKt$testKotlinSequencesStateful$1"),
+                  not(isPresent()));
 
-          assertEquals(
-              Sets.newHashSet(), collectAccessedTypes(lambdaCheck, clazz, "testBigExtraMethod"));
+              assertEquals(
+                  Sets.newHashSet(),
+                  collectAccessedTypes(lambdaCheck, clazz, "testBigExtraMethod"));
 
-          assertThat(
-              inspector.clazz("class_inliner_lambda_k_style.MainKt$testBigExtraMethod$1"),
-              not(isPresent()));
-          assertThat(
-              inspector.clazz("class_inliner_lambda_k_style.MainKt$testBigExtraMethod2$1"),
-              not(isPresent()));
-          assertThat(
-              inspector.clazz("class_inliner_lambda_k_style.MainKt$testBigExtraMethod3$1"),
-              not(isPresent()));
+              assertThat(
+                  inspector.clazz("class_inliner_lambda_k_style.MainKt$testBigExtraMethod$1"),
+                  not(isPresent()));
+              assertThat(
+                  inspector.clazz("class_inliner_lambda_k_style.MainKt$testBigExtraMethod2$1"),
+                  not(isPresent()));
+              assertThat(
+                  inspector.clazz("class_inliner_lambda_k_style.MainKt$testBigExtraMethod3$1"),
+                  not(isPresent()));
 
-          assertEquals(
-              Sets.newHashSet(),
-              collectAccessedTypes(lambdaCheck, clazz, "testBigExtraMethodReturningLambda"));
+              assertEquals(
+                  Sets.newHashSet(),
+                  collectAccessedTypes(lambdaCheck, clazz, "testBigExtraMethodReturningLambda"));
 
-          assertThat(
-              inspector.clazz(
-                  "class_inliner_lambda_k_style.MainKt$testBigExtraMethodReturningLambda$1"),
-              not(isPresent()));
-          assertThat(
-              inspector.clazz(
-                  "class_inliner_lambda_k_style.MainKt$testBigExtraMethodReturningLambda2$1"),
-              not(isPresent()));
-          assertThat(
-              inspector.clazz(
-                  "class_inliner_lambda_k_style.MainKt$testBigExtraMethodReturningLambda3$1"),
-              not(isPresent()));
-        });
+              assertThat(
+                  inspector.clazz(
+                      "class_inliner_lambda_k_style.MainKt$testBigExtraMethodReturningLambda$1"),
+                  not(isPresent()));
+              assertThat(
+                  inspector.clazz(
+                      "class_inliner_lambda_k_style.MainKt$testBigExtraMethodReturningLambda2$1"),
+                  not(isPresent()));
+              assertThat(
+                  inspector.clazz(
+                      "class_inliner_lambda_k_style.MainKt$testBigExtraMethodReturningLambda3$1"),
+                  not(isPresent()));
+            });
   }
 
   @Test
   public void testDataClass() throws Exception {
     assumeTrue("Only work with -allowaccessmodification", allowAccessModification);
     final String mainClassName = "class_inliner_data_class.MainKt";
-    runTest(
-        "class_inliner_data_class",
-        mainClassName,
-        true,
-        app -> {
-          CodeInspector inspector = new CodeInspector(app);
-          ClassSubject clazz = inspector.clazz(mainClassName);
-          assertTrue(
-              collectAccessedTypes(
-                      type -> !type.toSourceString().startsWith("java."),
-                      clazz,
-                      "main",
-                      String[].class.getCanonicalName())
-                  .isEmpty());
-          assertEquals(
-              Lists.newArrayList(
-                  "void kotlin.jvm.internal.Intrinsics.throwParameterIsNullException(java.lang.String)"),
-              collectStaticCalls(clazz, "main", String[].class.getCanonicalName()));
-        });
+    runTestWithDefaults("class_inliner_data_class", mainClassName)
+        .inspect(
+            inspector -> {
+              ClassSubject clazz = inspector.clazz(mainClassName);
+              assertTrue(
+                  collectAccessedTypes(
+                          type -> !type.toSourceString().startsWith("java."),
+                          clazz,
+                          "main",
+                          String[].class.getCanonicalName())
+                      .isEmpty());
+              assertEquals(
+                  Lists.newArrayList(
+                      "void kotlin.jvm.internal.Intrinsics.throwParameterIsNullException(java.lang.String)"),
+                  collectStaticCalls(clazz, "main", String[].class.getCanonicalName()));
+            });
   }
 
-  private Set<String> collectAccessedTypes(Predicate<DexType> isTypeOfInterest,
-      ClassSubject clazz, String methodName, String... params) {
+  private Set<String> collectAccessedTypes(
+      Predicate<DexType> isTypeOfInterest,
+      ClassSubject clazz,
+      String methodName,
+      String... params) {
     assertNotNull(clazz);
     MethodSignature signature = new MethodSignature(methodName, "void", params);
     DexCode code = clazz.method(signature).getMethod().getCode().asDexCode();
@@ -270,32 +304,37 @@
         .collect(Collectors.toSet());
   }
 
-  protected void runTest(String folder, String mainClass,
-      boolean enabled, AndroidAppInspector inspector) throws Exception {
-    runTest(
+  private R8TestRunResult runTestWithDefaults(String folder, String mainClass) throws Exception {
+    return runTestWithDefaults(folder, mainClass, null);
+  }
+
+  private R8TestRunResult runTestWithDefaults(
+      String folder, String mainClass, ThrowableConsumer<R8FullTestBuilder> configuration)
+      throws Exception {
+    return runTest(
         folder,
         mainClass,
-        // TODO(jsjeon): Introduce @NeverInline to kotlinR8TestResources
-        StringUtils.lines(
-            "-neverinline class * { void test*State*(...); }",
-            "-neverinline class * { void testBigExtraMethod(...); }",
-            "-neverinline class * { void testBigExtraMethodReturningLambda(...); }"),
-        options -> {
-          options.enableInlining = true;
-          options.enableClassInlining = enabled;
-          options.enableLambdaMerging = false;
+        testBuilder ->
+            testBuilder
+                .addOptionsModification(
+                    options -> {
+                      options.enableInlining = true;
+                      options.enableLambdaMerging = false;
 
-          // TODO(b/141719453): These limits should be removed if a possible or the test refactored.
-          // Tests check if specific lambdas are inlined or not, where some of target lambdas have
-          // at least 4 instructions.
-          options.inliningInstructionLimit = 4;
-          options.classInliningInstructionLimit = 40;
+                      // TODO(b/141719453): These limits should be removed if a possible or the test
+                      //  refactored. Tests check if specific lambdas are inlined or not, where some
+                      //  of target lambdas have at least 4 instructions.
+                      options.inliningInstructionLimit = 4;
+                      options.classInliningInstructionLimit = 40;
 
-          // Class inlining depends on the processing order. We therefore insert all call graph
-          // edges and verify that we can class inline everything under this condition.
-          options.testing.addCallEdgesForLibraryInvokes = true;
-        },
-        inspector);
+                      // Class inlining depends on the processing order. We therefore insert all
+                      // call graph edges and verify that we can class inline everything under this
+                      // condition.
+                      options.testing.addCallEdgesForLibraryInvokes = true;
+
+                      options.enableHorizontalClassMergingOfKotlinLambdas = false;
+                    })
+                .apply(configuration));
   }
 
   private List<String> collectStaticCalls(ClassSubject clazz, String methodName, String... params) {
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinClassStaticizerTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinClassStaticizerTest.java
index 2cbef3e..bf74d90 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinClassStaticizerTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassStaticizerTest.java
@@ -10,10 +10,10 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
+import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
 import com.google.common.base.Predicates;
 import java.util.Collection;
@@ -40,48 +40,37 @@
     final String mainClassName = "class_staticizer.MainKt";
 
     // Without class staticizer.
-    runTest(
-        "class_staticizer",
-        mainClassName,
-        false,
-        app -> {
-          CodeInspector inspector = new CodeInspector(app);
-          assertThat(inspector.clazz("class_staticizer.Derived$Companion"), isPresent());
+    runTest("class_staticizer", mainClassName, true)
+        .inspect(
+            inspector -> {
+              assertThat(inspector.clazz("class_staticizer.Derived$Companion"), isPresent());
 
-          // The Util class is there, but its instance methods have been inlined.
-          ClassSubject utilClass = inspector.clazz("class_staticizer.Util");
-          assertThat(utilClass, isPresent());
-          assertTrue(
-              utilClass.allMethods().stream()
-                  .filter(Predicates.not(FoundMethodSubject::isStatic))
-                  .allMatch(FoundMethodSubject::isInstanceInitializer));
-        });
+              // The Util class is there, but its instance methods have been inlined.
+              ClassSubject utilClass = inspector.clazz("class_staticizer.Util");
+              assertThat(utilClass, isPresent());
+              assertTrue(
+                  utilClass.allMethods().stream()
+                      .filter(Predicates.not(FoundMethodSubject::isStatic))
+                      .allMatch(FoundMethodSubject::isInstanceInitializer));
+            });
 
     // With class staticizer.
-    runTest(
-        "class_staticizer",
-        mainClassName,
-        true,
-        app -> {
-          CodeInspector inspector = new CodeInspector(app);
-          assertThat(inspector.clazz("class_staticizer.Regular$Companion"), not(isPresent()));
+    runTest("class_staticizer", mainClassName, false)
+        .inspect(
+            inspector -> {
+              assertThat(inspector.clazz("class_staticizer.Regular$Companion"), not(isPresent()));
 
-          ClassSubject utilClass = inspector.clazz("class_staticizer.Util");
-          assertThat(utilClass, isPresent());
-          assertTrue(utilClass.allMethods().stream().allMatch(FoundMethodSubject::isStatic));
-        });
+              ClassSubject utilClass = inspector.clazz("class_staticizer.Util");
+              assertThat(utilClass, isPresent());
+              assertTrue(utilClass.allMethods().stream().allMatch(FoundMethodSubject::isStatic));
+            });
   }
 
-  protected void runTest(String folder, String mainClass,
-      boolean enabled, AndroidAppInspector inspector) throws Exception {
-    runTest(
+  protected R8TestRunResult runTest(String folder, String mainClass, boolean noClassStaticizing)
+      throws Exception {
+    return runTest(
         folder,
         mainClass,
-        null,
-        options -> {
-          options.enableClassInlining = false;
-          options.enableClassStaticizer = enabled;
-        },
-        inspector);
+        testBuilder -> testBuilder.noClassInlining().noClassStaticizing(noClassStaticizing));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinUnusedArgumentsInLambdasTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinUnusedArgumentsInLambdasTest.java
index 782eb68..3c28a7d 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinUnusedArgumentsInLambdasTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinUnusedArgumentsInLambdasTest.java
@@ -10,7 +10,6 @@
 
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.utils.BooleanUtils;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.google.common.collect.ImmutableList;
 import java.util.Collection;
@@ -34,43 +33,37 @@
   @Test
   public void testMergingKStyleLambdasAfterUnusedArgumentRemoval() throws Exception {
     final String mainClassName = "unused_arg_in_lambdas_kstyle.MainKt";
-    runTest(
-        "unused_arg_in_lambdas_kstyle",
-        mainClassName,
-        app -> {
-          CodeInspector inspector = new CodeInspector(app);
-          inspector.forAllClasses(
-              classSubject -> {
-                if (classSubject.getOriginalDescriptor().contains("$ks")) {
-                  MethodSubject init = classSubject.init(ImmutableList.of("int"));
-                  assertThat(init, isPresent());
-                  // Arity 2 should appear.
-                  assertTrue(init.iterateInstructions(i -> i.isConstNumber(2)).hasNext());
+    runTest("unused_arg_in_lambdas_kstyle", mainClassName)
+        .inspect(
+            inspector ->
+                inspector.forAllClasses(
+                    classSubject -> {
+                      if (classSubject.getOriginalDescriptor().contains("$ks")) {
+                        MethodSubject init = classSubject.init(ImmutableList.of("int"));
+                        assertThat(init, isPresent());
+                        // Arity 2 should appear.
+                        assertTrue(init.iterateInstructions(i -> i.isConstNumber(2)).hasNext());
 
-                  MethodSubject invoke = classSubject.uniqueMethodWithName("invoke");
-                  assertThat(invoke, isPresent());
-                  assertEquals(2, invoke.getMethod().method.proto.parameters.size());
-                }
-              });
-        });
+                        MethodSubject invoke = classSubject.uniqueMethodWithName("invoke");
+                        assertThat(invoke, isPresent());
+                        assertEquals(2, invoke.getMethod().method.proto.parameters.size());
+                      }
+                    }));
   }
 
   @Test
   public void testMergingJStyleLambdasAfterUnusedArgumentRemoval() throws Exception {
     final String mainClassName = "unused_arg_in_lambdas_jstyle.MainKt";
-    runTest(
-        "unused_arg_in_lambdas_jstyle",
-        mainClassName,
-        app -> {
-          CodeInspector inspector = new CodeInspector(app);
-          inspector.forAllClasses(
-              classSubject -> {
-                if (classSubject.getOriginalDescriptor().contains("$js")) {
-                  MethodSubject get = classSubject.uniqueMethodWithName("get");
-                  assertThat(get, isPresent());
-                  assertEquals(3, get.getMethod().method.proto.parameters.size());
-                }
-              });
-        });
+    runTest("unused_arg_in_lambdas_jstyle", mainClassName)
+        .inspect(
+            inspector ->
+                inspector.forAllClasses(
+                    classSubject -> {
+                      if (classSubject.getOriginalDescriptor().contains("$js")) {
+                        MethodSubject get = classSubject.uniqueMethodWithName("get");
+                        assertThat(get, isPresent());
+                        assertEquals(3, get.getMethod().method.proto.parameters.size());
+                      }
+                    }));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinUnusedSingletonTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinUnusedSingletonTest.java
index c82a4a4..10fe60e 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinUnusedSingletonTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinUnusedSingletonTest.java
@@ -12,7 +12,6 @@
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.FieldSubject;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject.JumboStringMode;
@@ -43,53 +42,51 @@
   public void b110196118() throws Exception {
     final String mainClassName = "unused_singleton.MainKt";
     final String moduleName = "unused_singleton.TestModule";
-    runTest(
-        "unused_singleton",
-        mainClassName,
-        "-dontobfuscate",
-        app -> {
-          CodeInspector inspector = new CodeInspector(app);
-          ClassSubject main = inspector.clazz(mainClassName);
-          assertThat(main, isPresent());
+    runTest("unused_singleton", mainClassName)
+        .inspect(
+            inspector -> {
+              ClassSubject main = inspector.clazz(mainClassName);
+              assertThat(main, isPresent());
 
-          MethodSubject mainMethod = main.mainMethod();
-          assertThat(mainMethod, isPresent());
+              MethodSubject mainMethod = main.mainMethod();
+              assertThat(mainMethod, isPresent());
 
-          // The const-string from provideGreeting() has been propagated.
-          assertTrue(
-              mainMethod
-                  .iterateInstructions(i -> i.isConstString("Hello", JumboStringMode.ALLOW))
-                  .hasNext());
+              // The const-string from provideGreeting() has been propagated.
+              assertTrue(
+                  mainMethod
+                      .iterateInstructions(i -> i.isConstString("Hello", JumboStringMode.ALLOW))
+                      .hasNext());
 
-          // The method provideGreeting() is no longer being invoked -- i.e., we have been able to
-          // determine that the class initialization of the enclosing class is trivial.
-          ClassSubject module = inspector.clazz(moduleName);
-          assertThat(main, isPresent());
-          assertEquals(
-              0,
-              mainMethod
-                  .streamInstructions()
-                  .filter(InstructionSubject::isInvoke)
-                  .map(i -> i.getMethod().toSourceString())
-                  .filter(
-                      invokedMethod ->
-                          !invokedMethod.equals(checkParameterIsNotNullSignature)
-                              && !invokedMethod.equals(printlnSignature)
-                              && !invokedMethod.equals(throwParameterIsNotNullExceptionSignature))
-                  .count());
+              // The method provideGreeting() is no longer being invoked -- i.e., we have been able
+              // to determine that the class initialization of the enclosing class is trivial.
+              ClassSubject module = inspector.clazz(moduleName);
+              assertThat(main, isPresent());
+              assertEquals(
+                  0,
+                  mainMethod
+                      .streamInstructions()
+                      .filter(InstructionSubject::isInvoke)
+                      .map(i -> i.getMethod().toSourceString())
+                      .filter(
+                          invokedMethod ->
+                              !invokedMethod.equals(checkParameterIsNotNullSignature)
+                                  && !invokedMethod.equals(printlnSignature)
+                                  && !invokedMethod.equals(
+                                      throwParameterIsNotNullExceptionSignature))
+                      .count());
 
-          // The field `INSTANCE` has been removed entirely.
-          FieldSubject instance = module.uniqueFieldWithName("INSTANCE");
-          assertThat(instance, not(isPresent()));
+              // The field `INSTANCE` has been removed entirely.
+              FieldSubject instance = module.uniqueFieldWithName("INSTANCE");
+              assertThat(instance, not(isPresent()));
 
-          // The class initializer is no longer there.
-          MethodSubject clinit = module.clinit();
-          assertThat(clinit, not(isPresent()));
+              // The class initializer is no longer there.
+              MethodSubject clinit = module.clinit();
+              assertThat(clinit, not(isPresent()));
 
-          // Also, the instance initializer is no longer there, since it is only reachable from the
-          // class initializer.
-          MethodSubject init = module.init(ImmutableList.of());
-          assertThat(init, not(isPresent()));
-        });
+              // Also, the instance initializer is no longer there, since it is only reachable from
+              // the class initializer.
+              MethodSubject init = module.init(ImmutableList.of());
+              assertThat(init, not(isPresent()));
+            });
   }
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
index e5ce02c..b4aeced 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
@@ -8,6 +8,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.ToolHelper.ProcessResult;
@@ -18,14 +19,11 @@
 import com.android.tools.r8.naming.MemberNaming;
 import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.BooleanUtils;
-import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.FieldSubject;
 import java.nio.file.Path;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.function.Consumer;
 import org.junit.Assert;
 import org.junit.Ignore;
 import org.junit.Test;
@@ -67,9 +65,6 @@
           .addProperty("property", JAVA_LANG_STRING, Visibility.PRIVATE)
           .addProperty("indirectPropertyGetter", JAVA_LANG_STRING, Visibility.PRIVATE);
 
-  private Consumer<InternalOptions> disableClassStaticizer =
-      opts -> opts.enableClassStaticizer = false;
-
   @Parameterized.Parameters(name = "target: {0}, allowAccessModification: {1}")
   public static Collection<Object[]> data() {
     return buildParameters(KotlinTargetVersion.values(), BooleanUtils.values());
@@ -85,33 +80,32 @@
     final TestKotlinCompanionClass testedClass = COMPANION_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionPropertiesKt",
         "companionProperties_usePrimitiveProp");
-    runTest(
-        PROPERTIES_PACKAGE_NAME,
-        mainClass,
-        disableClassStaticizer,
-        (app) -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject outerClass =
-              checkClassIsKept(codeInspector, testedClass.getOuterClassName());
-          String propertyName = "primitiveProp";
-          FieldSubject fieldSubject = checkFieldIsKept(outerClass, "int", propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass, R8TestBuilder::noClassStaticizing)
+        .inspect(
+            inspector -> {
+              ClassSubject outerClass =
+                  checkClassIsKept(inspector, testedClass.getOuterClassName());
+              String propertyName = "primitiveProp";
+              FieldSubject fieldSubject = checkFieldIsKept(outerClass, "int", propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-          MemberNaming.MethodSignature getterAccessor =
-              testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_COMPANION);
-          MemberNaming.MethodSignature setterAccessor =
-              testedClass.getSetterAccessorForProperty(propertyName, AccessorKind.FROM_COMPANION);
+              MemberNaming.MethodSignature getterAccessor =
+                  testedClass.getGetterAccessorForProperty(
+                      propertyName, AccessorKind.FROM_COMPANION);
+              MemberNaming.MethodSignature setterAccessor =
+                  testedClass.getSetterAccessorForProperty(
+                      propertyName, AccessorKind.FROM_COMPANION);
 
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-            checkMethodIsRemoved(outerClass, getterAccessor);
-            checkMethodIsRemoved(outerClass, setterAccessor);
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-            checkMethodIsKept(outerClass, getterAccessor);
-            checkMethodIsKept(outerClass, setterAccessor);
-          }
-        });
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+                checkMethodIsRemoved(outerClass, getterAccessor);
+                checkMethodIsRemoved(outerClass, setterAccessor);
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+                checkMethodIsKept(outerClass, getterAccessor);
+                checkMethodIsKept(outerClass, setterAccessor);
+              }
+            });
   }
 
   @Test
@@ -119,34 +113,34 @@
     final TestKotlinCompanionClass testedClass = COMPANION_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionPropertiesKt",
         "companionProperties_usePrivateProp");
-    runTest(
-        PROPERTIES_PACKAGE_NAME,
-        mainClass,
-        disableClassStaticizer,
-        (app) -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject outerClass =
-              checkClassIsKept(codeInspector, testedClass.getOuterClassName());
-          String propertyName = "privateProp";
-          FieldSubject fieldSubject = checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass, R8TestBuilder::noClassStaticizing)
+        .inspect(
+            inspector -> {
+              ClassSubject outerClass =
+                  checkClassIsKept(inspector, testedClass.getOuterClassName());
+              String propertyName = "privateProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-          MemberNaming.MethodSignature getterAccessor =
-              testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_COMPANION);
-          MemberNaming.MethodSignature setterAccessor =
-              testedClass.getSetterAccessorForProperty(propertyName, AccessorKind.FROM_COMPANION);
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
+              MemberNaming.MethodSignature getterAccessor =
+                  testedClass.getGetterAccessorForProperty(
+                      propertyName, AccessorKind.FROM_COMPANION);
+              MemberNaming.MethodSignature setterAccessor =
+                  testedClass.getSetterAccessorForProperty(
+                      propertyName, AccessorKind.FROM_COMPANION);
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
 
-            checkMethodIsRemoved(outerClass, getterAccessor);
-            checkMethodIsRemoved(outerClass, setterAccessor);
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+                checkMethodIsRemoved(outerClass, getterAccessor);
+                checkMethodIsRemoved(outerClass, setterAccessor);
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
 
-            checkMethodIsKept(outerClass, getterAccessor);
-            checkMethodIsKept(outerClass, setterAccessor);
-          }
-        });
+                checkMethodIsKept(outerClass, getterAccessor);
+                checkMethodIsKept(outerClass, setterAccessor);
+              }
+            });
   }
 
   @Test
@@ -154,33 +148,33 @@
     final TestKotlinCompanionClass testedClass = COMPANION_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionPropertiesKt",
         "companionProperties_useInternalProp");
-    runTest(
-        PROPERTIES_PACKAGE_NAME,
-        mainClass,
-        disableClassStaticizer,
-        (app) -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject outerClass =
-              checkClassIsKept(codeInspector, testedClass.getOuterClassName());
-          String propertyName = "internalProp";
-          FieldSubject fieldSubject = checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass, R8TestBuilder::noClassStaticizing)
+        .inspect(
+            inspector -> {
+              ClassSubject outerClass =
+                  checkClassIsKept(inspector, testedClass.getOuterClassName());
+              String propertyName = "internalProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-          MemberNaming.MethodSignature getterAccessor =
-              testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_COMPANION);
-          MemberNaming.MethodSignature setterAccessor =
-              testedClass.getSetterAccessorForProperty(propertyName, AccessorKind.FROM_COMPANION);
+              MemberNaming.MethodSignature getterAccessor =
+                  testedClass.getGetterAccessorForProperty(
+                      propertyName, AccessorKind.FROM_COMPANION);
+              MemberNaming.MethodSignature setterAccessor =
+                  testedClass.getSetterAccessorForProperty(
+                      propertyName, AccessorKind.FROM_COMPANION);
 
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-            checkMethodIsRemoved(outerClass, getterAccessor);
-            checkMethodIsRemoved(outerClass, setterAccessor);
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-            checkMethodIsKept(outerClass, getterAccessor);
-            checkMethodIsKept(outerClass, setterAccessor);
-          }
-        });
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+                checkMethodIsRemoved(outerClass, getterAccessor);
+                checkMethodIsRemoved(outerClass, setterAccessor);
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+                checkMethodIsKept(outerClass, getterAccessor);
+                checkMethodIsKept(outerClass, setterAccessor);
+              }
+            });
   }
 
   @Test
@@ -188,33 +182,33 @@
     final TestKotlinCompanionClass testedClass = COMPANION_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionPropertiesKt",
         "companionProperties_usePublicProp");
-    runTest(
-        PROPERTIES_PACKAGE_NAME,
-        mainClass,
-        disableClassStaticizer,
-        (app) -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject outerClass =
-              checkClassIsKept(codeInspector, testedClass.getOuterClassName());
-          String propertyName = "publicProp";
-          FieldSubject fieldSubject = checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass, R8TestBuilder::noClassStaticizing)
+        .inspect(
+            inspector -> {
+              ClassSubject outerClass =
+                  checkClassIsKept(inspector, testedClass.getOuterClassName());
+              String propertyName = "publicProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-          MemberNaming.MethodSignature getterAccessor =
-              testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_COMPANION);
-          MemberNaming.MethodSignature setterAccessor =
-              testedClass.getSetterAccessorForProperty(propertyName, AccessorKind.FROM_COMPANION);
+              MemberNaming.MethodSignature getterAccessor =
+                  testedClass.getGetterAccessorForProperty(
+                      propertyName, AccessorKind.FROM_COMPANION);
+              MemberNaming.MethodSignature setterAccessor =
+                  testedClass.getSetterAccessorForProperty(
+                      propertyName, AccessorKind.FROM_COMPANION);
 
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-            checkMethodIsRemoved(outerClass, getterAccessor);
-            checkMethodIsRemoved(outerClass, setterAccessor);
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-            checkMethodIsKept(outerClass, getterAccessor);
-            checkMethodIsKept(outerClass, setterAccessor);
-          }
-        });
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+                checkMethodIsRemoved(outerClass, getterAccessor);
+                checkMethodIsRemoved(outerClass, setterAccessor);
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+                checkMethodIsKept(outerClass, getterAccessor);
+                checkMethodIsKept(outerClass, setterAccessor);
+              }
+            });
   }
 
   @Test
@@ -222,32 +216,32 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_usePrivateLateInitProp");
-    runTest(
-        PROPERTIES_PACKAGE_NAME,
-        mainClass,
-        disableClassStaticizer,
-        (app) -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject outerClass =
-              checkClassIsKept(codeInspector, testedClass.getOuterClassName());
-          String propertyName = "privateLateInitProp";
-          FieldSubject fieldSubject = checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass, R8TestBuilder::noClassStaticizing)
+        .inspect(
+            inspector -> {
+              ClassSubject outerClass =
+                  checkClassIsKept(inspector, testedClass.getOuterClassName());
+              String propertyName = "privateLateInitProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-          MemberNaming.MethodSignature getterAccessor =
-              testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_COMPANION);
-          MemberNaming.MethodSignature setterAccessor =
-              testedClass.getSetterAccessorForProperty(propertyName, AccessorKind.FROM_COMPANION);
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-            checkMethodIsRemoved(outerClass, getterAccessor);
-            checkMethodIsRemoved(outerClass, setterAccessor);
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-            checkMethodIsKept(outerClass, getterAccessor);
-            checkMethodIsKept(outerClass, setterAccessor);
-          }
-        });
+              MemberNaming.MethodSignature getterAccessor =
+                  testedClass.getGetterAccessorForProperty(
+                      propertyName, AccessorKind.FROM_COMPANION);
+              MemberNaming.MethodSignature setterAccessor =
+                  testedClass.getSetterAccessorForProperty(
+                      propertyName, AccessorKind.FROM_COMPANION);
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+                checkMethodIsRemoved(outerClass, getterAccessor);
+                checkMethodIsRemoved(outerClass, setterAccessor);
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+                checkMethodIsKept(outerClass, getterAccessor);
+                checkMethodIsKept(outerClass, setterAccessor);
+              }
+            });
   }
 
   @Test
@@ -255,23 +249,28 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_useInternalLateInitProp");
-    runTest(PROPERTIES_PACKAGE_NAME, mainClass, (app) -> {
-      CodeInspector codeInspector = new CodeInspector(app);
-      ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
-      String propertyName = "internalLateInitProp";
-      FieldSubject fieldSubject = checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
-      assertTrue(fieldSubject.getField().accessFlags.isStatic());
-      assertTrue(fieldSubject.getField().accessFlags.isPublic());
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass)
+        .inspect(
+            inspector -> {
+              ClassSubject outerClass =
+                  checkClassIsKept(inspector, testedClass.getOuterClassName());
+              String propertyName = "internalLateInitProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
+              assertTrue(fieldSubject.getField().accessFlags.isPublic());
 
-      MemberNaming.MethodSignature getterAccessor = testedClass
-          .getGetterAccessorForProperty(propertyName, AccessorKind.FROM_COMPANION);
-      MemberNaming.MethodSignature setterAccessor = testedClass
-          .getSetterAccessorForProperty(propertyName, AccessorKind.FROM_COMPANION);
+              MemberNaming.MethodSignature getterAccessor =
+                  testedClass.getGetterAccessorForProperty(
+                      propertyName, AccessorKind.FROM_COMPANION);
+              MemberNaming.MethodSignature setterAccessor =
+                  testedClass.getSetterAccessorForProperty(
+                      propertyName, AccessorKind.FROM_COMPANION);
 
-      assertTrue(fieldSubject.getField().accessFlags.isPublic());
-      checkMethodIsRemoved(outerClass, getterAccessor);
-      checkMethodIsRemoved(outerClass, setterAccessor);
-    });
+              assertTrue(fieldSubject.getField().accessFlags.isPublic());
+              checkMethodIsRemoved(outerClass, getterAccessor);
+              checkMethodIsRemoved(outerClass, setterAccessor);
+            });
   }
 
   @Test
@@ -279,23 +278,28 @@
     final TestKotlinCompanionClass testedClass = COMPANION_LATE_INIT_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_usePublicLateInitProp");
-    runTest(PROPERTIES_PACKAGE_NAME, mainClass, (app) -> {
-      CodeInspector codeInspector = new CodeInspector(app);
-      ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
-      String propertyName = "publicLateInitProp";
-      FieldSubject fieldSubject = checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
-      assertTrue(fieldSubject.getField().accessFlags.isStatic());
-      assertTrue(fieldSubject.getField().accessFlags.isPublic());
+    runTest(PROPERTIES_PACKAGE_NAME, mainClass)
+        .inspect(
+            inspector -> {
+              ClassSubject outerClass =
+                  checkClassIsKept(inspector, testedClass.getOuterClassName());
+              String propertyName = "publicLateInitProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
+              assertTrue(fieldSubject.getField().accessFlags.isPublic());
 
-      MemberNaming.MethodSignature getterAccessor = testedClass
-          .getGetterAccessorForProperty(propertyName, AccessorKind.FROM_COMPANION);
-      MemberNaming.MethodSignature setterAccessor = testedClass
-          .getSetterAccessorForProperty(propertyName, AccessorKind.FROM_COMPANION);
+              MemberNaming.MethodSignature getterAccessor =
+                  testedClass.getGetterAccessorForProperty(
+                      propertyName, AccessorKind.FROM_COMPANION);
+              MemberNaming.MethodSignature setterAccessor =
+                  testedClass.getSetterAccessorForProperty(
+                      propertyName, AccessorKind.FROM_COMPANION);
 
-      assertTrue(fieldSubject.getField().accessFlags.isPublic());
-      checkMethodIsRemoved(outerClass, getterAccessor);
-      checkMethodIsRemoved(outerClass, setterAccessor);
-    });
+              assertTrue(fieldSubject.getField().accessFlags.isPublic());
+              checkMethodIsRemoved(outerClass, getterAccessor);
+              checkMethodIsRemoved(outerClass, setterAccessor);
+            });
   }
 
   @Test
@@ -303,17 +307,14 @@
     TestKotlinCompanionClass testedClass = ACCESSOR_COMPANION_PROPERTY_CLASS;
     String mainClass =
         addMainToClasspath("accessors.AccessorKt", "accessor_accessPropertyFromCompanionClass");
-    runTest(
-        "accessors",
-        mainClass,
-        disableClassStaticizer,
-        app -> {
-          // The classes are removed entirely as a result of member value propagation, inlining, and
-          // the fact that the classes do not have observable side effects.
-          CodeInspector codeInspector = new CodeInspector(app);
-          checkClassIsRemoved(codeInspector, testedClass.getOuterClassName());
-          checkClassIsRemoved(codeInspector, testedClass.getClassName());
-        });
+    runTest("accessors", mainClass, R8TestBuilder::noClassStaticizing)
+        .inspect(
+            inspector -> {
+              // The classes are removed entirely as a result of member value propagation, inlining,
+              // and the fact that the classes do not have observable side effects.
+              checkClassIsRemoved(inspector, testedClass.getOuterClassName());
+              checkClassIsRemoved(inspector, testedClass.getClassName());
+            });
   }
 
   @Test
@@ -322,29 +323,32 @@
     TestKotlinCompanionClass testedClass = ACCESSOR_COMPANION_PROPERTY_CLASS;
     String mainClass = addMainToClasspath("accessors.AccessorKt",
         "accessor_accessPropertyFromOuterClass");
-    runTest("accessors", mainClass, (app) -> {
-      CodeInspector codeInspector = new CodeInspector(app);
-      ClassSubject outerClass = checkClassIsKept(codeInspector, testedClass.getOuterClassName());
-      ClassSubject companionClass = checkClassIsKept(codeInspector, testedClass.getClassName());
-      String propertyName = "property";
-      FieldSubject fieldSubject = checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
-      assertTrue(fieldSubject.getField().accessFlags.isStatic());
+    runTest("accessors", mainClass)
+        .inspect(
+            inspector -> {
+              ClassSubject outerClass =
+                  checkClassIsKept(inspector, testedClass.getOuterClassName());
+              ClassSubject companionClass = checkClassIsKept(inspector, testedClass.getClassName());
+              String propertyName = "property";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-      // We cannot inline the getter because we don't know if NPE is preserved.
-      MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
-      checkMethodIsKept(companionClass, getter);
+              // We cannot inline the getter because we don't know if NPE is preserved.
+              MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+              checkMethodIsKept(companionClass, getter);
 
-      // We should always inline the static accessor method.
-      MemberNaming.MethodSignature getterAccessor =
-          testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_INNER);
-      checkMethodIsRemoved(companionClass, getterAccessor);
+              // We should always inline the static accessor method.
+              MemberNaming.MethodSignature getterAccessor =
+                  testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_INNER);
+              checkMethodIsRemoved(companionClass, getterAccessor);
 
-      if (allowAccessModification) {
-        assertTrue(fieldSubject.getField().accessFlags.isPublic());
-      } else {
-        assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-      }
-    });
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+              }
+            });
   }
 
   @Test
@@ -352,15 +356,12 @@
     String mainClass =
         addMainToClasspath(
             "accessors.PropertyAccessorForInnerClassKt", "noUseOfPropertyAccessorFromInnerClass");
-    runTest(
-        "accessors",
-        mainClass,
-        (app) -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-
-          // Class is removed because the instantiation of the inner class has no side effects.
-          checkClassIsRemoved(codeInspector, PROPERTY_ACCESS_FOR_INNER_CLASS.getClassName());
-        });
+    runTest("accessors", mainClass)
+        .inspect(
+            inspector -> {
+              // Class is removed because the instantiation of the inner class has no side effects.
+              checkClassIsRemoved(inspector, PROPERTY_ACCESS_FOR_INNER_CLASS.getClassName());
+            });
   }
 
   @Test
@@ -369,29 +370,30 @@
     TestKotlinClass testedClass = PROPERTY_ACCESS_FOR_INNER_CLASS;
     String mainClass = addMainToClasspath(testedClass.className + "Kt",
         "usePrivatePropertyAccessorFromInnerClass");
-    runTest("accessors", mainClass, (app) -> {
-      CodeInspector codeInspector = new CodeInspector(app);
-      ClassSubject classSubject = checkClassIsKept(codeInspector, testedClass.getClassName());
+    runTest("accessors", mainClass)
+        .inspect(
+            inspector -> {
+              ClassSubject classSubject = checkClassIsKept(inspector, testedClass.getClassName());
 
-      String propertyName = "privateProp";
-      FieldSubject fieldSubject = checkFieldIsKept(classSubject, JAVA_LANG_STRING,
-          propertyName);
-      assertFalse(fieldSubject.getField().accessFlags.isStatic());
+              String propertyName = "privateProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(classSubject, JAVA_LANG_STRING, propertyName);
+              assertFalse(fieldSubject.getField().accessFlags.isStatic());
 
-      MemberNaming.MethodSignature getterAccessor =
-          testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_INNER);
-      MemberNaming.MethodSignature setterAccessor =
-          testedClass.getSetterAccessorForProperty(propertyName, AccessorKind.FROM_INNER);
-      if (allowAccessModification) {
-        assertTrue(fieldSubject.getField().accessFlags.isPublic());
-        checkMethodIsRemoved(classSubject, getterAccessor);
-        checkMethodIsRemoved(classSubject, setterAccessor);
-      } else {
-        assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-        checkMethodIsKept(classSubject, getterAccessor);
-        checkMethodIsKept(classSubject, setterAccessor);
-      }
-    });
+              MemberNaming.MethodSignature getterAccessor =
+                  testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_INNER);
+              MemberNaming.MethodSignature setterAccessor =
+                  testedClass.getSetterAccessorForProperty(propertyName, AccessorKind.FROM_INNER);
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+                checkMethodIsRemoved(classSubject, getterAccessor);
+                checkMethodIsRemoved(classSubject, setterAccessor);
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+                checkMethodIsKept(classSubject, getterAccessor);
+                checkMethodIsKept(classSubject, setterAccessor);
+              }
+            });
   }
 
   @Test
@@ -400,29 +402,30 @@
     TestKotlinClass testedClass = PROPERTY_ACCESS_FOR_INNER_CLASS;
     String mainClass = addMainToClasspath(testedClass.className + "Kt",
         "usePrivateLateInitPropertyAccessorFromInnerClass");
-    runTest("accessors", mainClass, (app) -> {
-      CodeInspector codeInspector = new CodeInspector(app);
-      ClassSubject classSubject = checkClassIsKept(codeInspector, testedClass.getClassName());
+    runTest("accessors", mainClass)
+        .inspect(
+            inspector -> {
+              ClassSubject classSubject = checkClassIsKept(inspector, testedClass.getClassName());
 
-      String propertyName = "privateLateInitProp";
-      FieldSubject fieldSubject = checkFieldIsKept(classSubject, JAVA_LANG_STRING,
-          propertyName);
-      assertFalse(fieldSubject.getField().accessFlags.isStatic());
+              String propertyName = "privateLateInitProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(classSubject, JAVA_LANG_STRING, propertyName);
+              assertFalse(fieldSubject.getField().accessFlags.isStatic());
 
-      MemberNaming.MethodSignature getterAccessor =
-          testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_INNER);
-      MemberNaming.MethodSignature setterAccessor =
-          testedClass.getSetterAccessorForProperty(propertyName, AccessorKind.FROM_INNER);
-      if (allowAccessModification) {
-        assertTrue(fieldSubject.getField().accessFlags.isPublic());
-        checkMethodIsRemoved(classSubject, getterAccessor);
-        checkMethodIsRemoved(classSubject, setterAccessor);
-      } else {
-        assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-        checkMethodIsKept(classSubject, getterAccessor);
-        checkMethodIsKept(classSubject, setterAccessor);
-      }
-    });
+              MemberNaming.MethodSignature getterAccessor =
+                  testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_INNER);
+              MemberNaming.MethodSignature setterAccessor =
+                  testedClass.getSetterAccessorForProperty(propertyName, AccessorKind.FROM_INNER);
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+                checkMethodIsRemoved(classSubject, getterAccessor);
+                checkMethodIsRemoved(classSubject, setterAccessor);
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+                checkMethodIsKept(classSubject, getterAccessor);
+                checkMethodIsKept(classSubject, setterAccessor);
+              }
+            });
   }
 
   @Test
@@ -431,19 +434,20 @@
     TestKotlinClass testedClass = PROPERTY_ACCESS_FOR_LAMBDA_CLASS;
     String mainClass = addMainToClasspath(testedClass.className + "Kt",
         "noUseOfPropertyAccessorFromLambda");
-    runTest("accessors", mainClass, (app) -> {
-      CodeInspector codeInspector = new CodeInspector(app);
-      ClassSubject classSubject = checkClassIsKept(codeInspector, testedClass.getClassName());
-      String propertyName = "property";
+    runTest("accessors", mainClass)
+        .inspect(
+            inspector -> {
+              ClassSubject classSubject = checkClassIsKept(inspector, testedClass.getClassName());
+              String propertyName = "property";
 
-      MemberNaming.MethodSignature getterAccessor =
-          testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_LAMBDA);
-      MemberNaming.MethodSignature setterAccessor =
-          testedClass.getSetterAccessorForProperty(propertyName, AccessorKind.FROM_LAMBDA);
+              MemberNaming.MethodSignature getterAccessor =
+                  testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_LAMBDA);
+              MemberNaming.MethodSignature setterAccessor =
+                  testedClass.getSetterAccessorForProperty(propertyName, AccessorKind.FROM_LAMBDA);
 
-      checkMethodIsRemoved(classSubject, getterAccessor);
-      checkMethodIsRemoved(classSubject, setterAccessor);
-    });
+              checkMethodIsRemoved(classSubject, getterAccessor);
+              checkMethodIsRemoved(classSubject, setterAccessor);
+            });
   }
 
   @Test
@@ -452,27 +456,29 @@
     TestKotlinClass testedClass = PROPERTY_ACCESS_FOR_LAMBDA_CLASS;
     String mainClass = addMainToClasspath(testedClass.className + "Kt",
         "usePropertyAccessorFromLambda");
-    runTest("accessors", mainClass, (app) -> {
-      CodeInspector codeInspector = new CodeInspector(app);
-      ClassSubject classSubject = checkClassIsKept(codeInspector, testedClass.getClassName());
-      String propertyName = "property";
-      FieldSubject fieldSubject = checkFieldIsKept(classSubject, JAVA_LANG_STRING, propertyName);
-      assertFalse(fieldSubject.getField().accessFlags.isStatic());
+    runTest("accessors", mainClass)
+        .inspect(
+            inspector -> {
+              ClassSubject classSubject = checkClassIsKept(inspector, testedClass.getClassName());
+              String propertyName = "property";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(classSubject, JAVA_LANG_STRING, propertyName);
+              assertFalse(fieldSubject.getField().accessFlags.isStatic());
 
-      MemberNaming.MethodSignature getterAccessor =
-          testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_LAMBDA);
-      MemberNaming.MethodSignature setterAccessor =
-          testedClass.getSetterAccessorForProperty(propertyName, AccessorKind.FROM_LAMBDA);
-      if (allowAccessModification) {
-        assertTrue(fieldSubject.getField().accessFlags.isPublic());
-        checkMethodIsRemoved(classSubject, getterAccessor);
-        checkMethodIsRemoved(classSubject, setterAccessor);
-      } else {
-        assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-        checkMethodIsKept(classSubject, getterAccessor);
-        checkMethodIsKept(classSubject, setterAccessor);
-      }
-    });
+              MemberNaming.MethodSignature getterAccessor =
+                  testedClass.getGetterAccessorForProperty(propertyName, AccessorKind.FROM_LAMBDA);
+              MemberNaming.MethodSignature setterAccessor =
+                  testedClass.getSetterAccessorForProperty(propertyName, AccessorKind.FROM_LAMBDA);
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+                checkMethodIsRemoved(classSubject, getterAccessor);
+                checkMethodIsRemoved(classSubject, setterAccessor);
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+                checkMethodIsKept(classSubject, getterAccessor);
+                checkMethodIsKept(classSubject, setterAccessor);
+              }
+            });
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
index 59c92ff..ec0ee03 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
@@ -11,7 +11,6 @@
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.util.Collection;
 import java.util.Collections;
@@ -59,33 +58,39 @@
     final MethodSignature testMethodSignature =
         new MethodSignature("testDataClassGetters", "void", Collections.emptyList());
     final String extraRules = keepClassMethod(mainClassName, testMethodSignature);
-    runTest("dataclass", mainClassName, extraRules, disableClassInliner, (app) -> {
-      CodeInspector codeInspector = new CodeInspector(app);
-      ClassSubject dataClass = checkClassIsKept(codeInspector, TEST_DATA_CLASS.getClassName());
+    runTest(
+            "dataclass",
+            mainClassName,
+            testBuilder ->
+                testBuilder.addKeepRules(extraRules).addOptionsModification(disableClassInliner))
+        .inspect(
+            inspector -> {
+              ClassSubject dataClass = checkClassIsKept(inspector, TEST_DATA_CLASS.getClassName());
 
-      // Getters should be removed after inlining, which is possible only if access is relaxed.
-      final boolean areGetterPresent = !allowAccessModification;
-      checkMethodIsKeptOrRemoved(dataClass, NAME_GETTER_METHOD, areGetterPresent);
-      checkMethodIsKeptOrRemoved(dataClass, AGE_GETTER_METHOD, areGetterPresent);
+              // Getters should be removed after inlining, which is possible only if access is
+              // relaxed.
+              final boolean areGetterPresent = !allowAccessModification;
+              checkMethodIsKeptOrRemoved(dataClass, NAME_GETTER_METHOD, areGetterPresent);
+              checkMethodIsKeptOrRemoved(dataClass, AGE_GETTER_METHOD, areGetterPresent);
 
-      // No use of componentN functions.
-      checkMethodIsRemoved(dataClass, COMPONENT1_METHOD);
-      checkMethodIsRemoved(dataClass, COMPONENT2_METHOD);
+              // No use of componentN functions.
+              checkMethodIsRemoved(dataClass, COMPONENT1_METHOD);
+              checkMethodIsRemoved(dataClass, COMPONENT2_METHOD);
 
-      // No use of copy functions.
-      checkMethodIsRemoved(dataClass, COPY_METHOD);
-      checkMethodIsRemoved(dataClass, COPY_DEFAULT_METHOD);
+              // No use of copy functions.
+              checkMethodIsRemoved(dataClass, COPY_METHOD);
+              checkMethodIsRemoved(dataClass, COPY_DEFAULT_METHOD);
 
-      ClassSubject classSubject = checkClassIsKept(codeInspector, mainClassName);
-      MethodSubject testMethod = checkMethodIsKept(classSubject, testMethodSignature);
-      DexCode dexCode = getDexCode(testMethod);
-      if (allowAccessModification) {
-        // Both getters should be inlined
-        checkMethodIsNeverInvoked(dexCode, NAME_GETTER_METHOD, AGE_GETTER_METHOD);
-      } else {
-        checkMethodIsInvokedAtLeastOnce(dexCode, NAME_GETTER_METHOD, AGE_GETTER_METHOD);
-      }
-    });
+              ClassSubject classSubject = checkClassIsKept(inspector, mainClassName);
+              MethodSubject testMethod = checkMethodIsKept(classSubject, testMethodSignature);
+              DexCode dexCode = getDexCode(testMethod);
+              if (allowAccessModification) {
+                // Both getters should be inlined
+                checkMethodIsNeverInvoked(dexCode, NAME_GETTER_METHOD, AGE_GETTER_METHOD);
+              } else {
+                checkMethodIsInvokedAtLeastOnce(dexCode, NAME_GETTER_METHOD, AGE_GETTER_METHOD);
+              }
+            });
   }
 
   @Test
@@ -94,33 +99,38 @@
     final MethodSignature testMethodSignature =
         new MethodSignature("testAllDataClassComponentFunctions", "void", Collections.emptyList());
     final String extraRules = keepClassMethod(mainClassName, testMethodSignature);
-    runTest("dataclass", mainClassName, extraRules, disableClassInliner, (app) -> {
-      CodeInspector codeInspector = new CodeInspector(app);
-      ClassSubject dataClass = checkClassIsKept(codeInspector, TEST_DATA_CLASS.getClassName());
+    runTest(
+            "dataclass",
+            mainClassName,
+            testBuilder ->
+                testBuilder.addKeepRules(extraRules).addOptionsModification(disableClassInliner))
+        .inspect(
+            inspector -> {
+              ClassSubject dataClass = checkClassIsKept(inspector, TEST_DATA_CLASS.getClassName());
 
-      // ComponentN functions should be removed after inlining, which is possible only if access
-      // is relaxed.
-      final boolean areComponentMethodsPresent = !allowAccessModification;
-      checkMethodIsKeptOrRemoved(dataClass, COMPONENT1_METHOD, areComponentMethodsPresent);
-      checkMethodIsKeptOrRemoved(dataClass, COMPONENT2_METHOD, areComponentMethodsPresent);
+              // ComponentN functions should be removed after inlining, which is possible only if
+              // access is relaxed.
+              final boolean areComponentMethodsPresent = !allowAccessModification;
+              checkMethodIsKeptOrRemoved(dataClass, COMPONENT1_METHOD, areComponentMethodsPresent);
+              checkMethodIsKeptOrRemoved(dataClass, COMPONENT2_METHOD, areComponentMethodsPresent);
 
-      // No use of getter.
-      checkMethodIsRemoved(dataClass, NAME_GETTER_METHOD);
-      checkMethodIsRemoved(dataClass, AGE_GETTER_METHOD);
+              // No use of getter.
+              checkMethodIsRemoved(dataClass, NAME_GETTER_METHOD);
+              checkMethodIsRemoved(dataClass, AGE_GETTER_METHOD);
 
-      // No use of copy functions.
-      checkMethodIsRemoved(dataClass, COPY_METHOD);
-      checkMethodIsRemoved(dataClass, COPY_DEFAULT_METHOD);
+              // No use of copy functions.
+              checkMethodIsRemoved(dataClass, COPY_METHOD);
+              checkMethodIsRemoved(dataClass, COPY_DEFAULT_METHOD);
 
-      ClassSubject classSubject = checkClassIsKept(codeInspector, mainClassName);
-      MethodSubject testMethod = checkMethodIsKept(classSubject, testMethodSignature);
-      DexCode dexCode = getDexCode(testMethod);
-      if (allowAccessModification) {
-        checkMethodIsNeverInvoked(dexCode, COMPONENT1_METHOD, COMPONENT2_METHOD);
-      } else {
-        checkMethodIsInvokedAtLeastOnce(dexCode, COMPONENT1_METHOD, COMPONENT2_METHOD);
-      }
-    });
+              ClassSubject classSubject = checkClassIsKept(inspector, mainClassName);
+              MethodSubject testMethod = checkMethodIsKept(classSubject, testMethodSignature);
+              DexCode dexCode = getDexCode(testMethod);
+              if (allowAccessModification) {
+                checkMethodIsNeverInvoked(dexCode, COMPONENT1_METHOD, COMPONENT2_METHOD);
+              } else {
+                checkMethodIsInvokedAtLeastOnce(dexCode, COMPONENT1_METHOD, COMPONENT2_METHOD);
+              }
+            });
   }
 
   @Test
@@ -129,33 +139,38 @@
     final MethodSignature testMethodSignature =
         new MethodSignature("testSomeDataClassComponentFunctions", "void", Collections.emptyList());
     final String extraRules = keepClassMethod(mainClassName, testMethodSignature);
-    runTest("dataclass", mainClassName, extraRules, disableClassInliner, (app) -> {
-      CodeInspector codeInspector = new CodeInspector(app);
-      ClassSubject dataClass = checkClassIsKept(codeInspector, TEST_DATA_CLASS.getClassName());
+    runTest(
+            "dataclass",
+            mainClassName,
+            testBuilder ->
+                testBuilder.addKeepRules(extraRules).addOptionsModification(disableClassInliner))
+        .inspect(
+            inspector -> {
+              ClassSubject dataClass = checkClassIsKept(inspector, TEST_DATA_CLASS.getClassName());
 
-      boolean component2IsPresent = !allowAccessModification;
-      checkMethodIsKeptOrRemoved(dataClass, COMPONENT2_METHOD, component2IsPresent);
+              boolean component2IsPresent = !allowAccessModification;
+              checkMethodIsKeptOrRemoved(dataClass, COMPONENT2_METHOD, component2IsPresent);
 
-      // Function component1 is not used.
-      checkMethodIsRemoved(dataClass, COMPONENT1_METHOD);
+              // Function component1 is not used.
+              checkMethodIsRemoved(dataClass, COMPONENT1_METHOD);
 
-      // No use of getter.
-      checkMethodIsRemoved(dataClass, NAME_GETTER_METHOD);
-      checkMethodIsRemoved(dataClass, AGE_GETTER_METHOD);
+              // No use of getter.
+              checkMethodIsRemoved(dataClass, NAME_GETTER_METHOD);
+              checkMethodIsRemoved(dataClass, AGE_GETTER_METHOD);
 
-      // No use of copy functions.
-      checkMethodIsRemoved(dataClass, COPY_METHOD);
-      checkMethodIsRemoved(dataClass, COPY_DEFAULT_METHOD);
+              // No use of copy functions.
+              checkMethodIsRemoved(dataClass, COPY_METHOD);
+              checkMethodIsRemoved(dataClass, COPY_DEFAULT_METHOD);
 
-      ClassSubject classSubject = checkClassIsKept(codeInspector, mainClassName);
-      MethodSubject testMethod = checkMethodIsKept(classSubject, testMethodSignature);
-      DexCode dexCode = getDexCode(testMethod);
-      if (allowAccessModification) {
-        checkMethodIsNeverInvoked(dexCode, COMPONENT2_METHOD);
-      } else {
-        checkMethodIsInvokedAtLeastOnce(dexCode, COMPONENT2_METHOD);
-      }
-    });
+              ClassSubject classSubject = checkClassIsKept(inspector, mainClassName);
+              MethodSubject testMethod = checkMethodIsKept(classSubject, testMethodSignature);
+              DexCode dexCode = getDexCode(testMethod);
+              if (allowAccessModification) {
+                checkMethodIsNeverInvoked(dexCode, COMPONENT2_METHOD);
+              } else {
+                checkMethodIsInvokedAtLeastOnce(dexCode, COMPONENT2_METHOD);
+              }
+            });
   }
 
   @Test
@@ -164,13 +179,18 @@
     final MethodSignature testMethodSignature =
         new MethodSignature("testDataClassCopy", "void", Collections.emptyList());
     final String extraRules = keepClassMethod(mainClassName, testMethodSignature);
-    runTest("dataclass", mainClassName, extraRules, disableClassInliner, (app) -> {
-      CodeInspector codeInspector = new CodeInspector(app);
-      ClassSubject dataClass = checkClassIsKept(codeInspector, TEST_DATA_CLASS.getClassName());
+    runTest(
+            "dataclass",
+            mainClassName,
+            testBuilder ->
+                testBuilder.addKeepRules(extraRules).addOptionsModification(disableClassInliner))
+        .inspect(
+            inspector -> {
+              ClassSubject dataClass = checkClassIsKept(inspector, TEST_DATA_CLASS.getClassName());
 
-      checkMethodIsRemoved(dataClass, COPY_METHOD);
-      checkMethodIsRemoved(dataClass, COPY_DEFAULT_METHOD);
-    });
+              checkMethodIsRemoved(dataClass, COPY_METHOD);
+              checkMethodIsRemoved(dataClass, COPY_DEFAULT_METHOD);
+            });
   }
 
   @Test
@@ -179,12 +199,17 @@
     final MethodSignature testMethodSignature =
         new MethodSignature("testDataClassCopyWithDefault", "void", Collections.emptyList());
     final String extraRules = keepClassMethod(mainClassName, testMethodSignature);
-    runTest("dataclass", mainClassName, extraRules, disableClassInliner, (app) -> {
-      CodeInspector codeInspector = new CodeInspector(app);
-      ClassSubject dataClass = checkClassIsKept(codeInspector, TEST_DATA_CLASS.getClassName());
+    runTest(
+            "dataclass",
+            mainClassName,
+            testBuilder ->
+                testBuilder.addKeepRules(extraRules).addOptionsModification(disableClassInliner))
+        .inspect(
+            inspector -> {
+              ClassSubject dataClass = checkClassIsKept(inspector, TEST_DATA_CLASS.getClassName());
 
-      checkMethodIsRemoved(dataClass, COPY_DEFAULT_METHOD);
-    });
+              checkMethodIsRemoved(dataClass, COPY_DEFAULT_METHOD);
+            });
   }
 
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinIntrinsicsTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinIntrinsicsTest.java
index 7a9635e..770d6cc 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinIntrinsicsTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinIntrinsicsTest.java
@@ -8,11 +8,11 @@
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import java.util.Collection;
 import java.util.Collections;
+import kotlin.jvm.internal.Intrinsics;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -39,20 +39,31 @@
         new MethodSignature("expectsNonNullParameters",
             "java.lang.String", Lists.newArrayList("java.lang.String", "java.lang.String")));
 
-    runTest("intrinsics", "intrinsics.IntrinsicsKt", extraRules, (app) -> {
-      CodeInspector codeInspector = new CodeInspector(app);
-      ClassSubject intrinsicsClass = checkClassIsKept(
-          codeInspector, KOTLIN_INTRINSICS_CLASS.getClassName());
-
-      checkMethodsPresence(intrinsicsClass,
-          ImmutableMap.<MethodSignature, Boolean>builder()
-              .put(new MethodSignature("throwParameterIsNullException",
-                      "void", Collections.singletonList("java.lang.String")),
-                  true)
-              .put(new MethodSignature("checkParameterIsNotNull",
-                      "void", Lists.newArrayList("java.lang.Object", "java.lang.String")),
-                  allowAccessModification ? false /* should be inlined*/ : true)
-              .build());
-    });
+    runTest(
+            "intrinsics",
+            "intrinsics.IntrinsicsKt",
+            testBuilder ->
+                testBuilder.addKeepRules(extraRules).noHorizontalClassMerging(Intrinsics.class))
+        .inspect(
+            inspector -> {
+              ClassSubject intrinsicsClass =
+                  checkClassIsKept(inspector, KOTLIN_INTRINSICS_CLASS.getClassName());
+              checkMethodsPresence(
+                  intrinsicsClass,
+                  ImmutableMap.<MethodSignature, Boolean>builder()
+                      .put(
+                          new MethodSignature(
+                              "throwParameterIsNullException",
+                              "void",
+                              Collections.singletonList("java.lang.String")),
+                          true)
+                      .put(
+                          new MethodSignature(
+                              "checkParameterIsNotNull",
+                              "void",
+                              Lists.newArrayList("java.lang.Object", "java.lang.String")),
+                          !allowAccessModification)
+                      .build());
+            });
   }
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
index f572789..30511bb 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
@@ -13,7 +13,6 @@
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.FieldSubject;
 import java.util.Collection;
 import java.util.function.Consumer;
@@ -109,13 +108,13 @@
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_noUseOfProperties");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          checkClassIsRemoved(codeInspector, MUTABLE_PROPERTY_CLASS.getClassName());
-        });
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              checkClassIsRemoved(inspector, MUTABLE_PROPERTY_CLASS.getClassName());
+            });
   }
 
   @Test
@@ -123,28 +122,28 @@
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_usePrivateProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject classSubject =
-              checkClassIsKept(codeInspector, MUTABLE_PROPERTY_CLASS.getClassName());
-          String propertyName = "privateProp";
-          FieldSubject fieldSubject =
-              checkFieldIsKept(classSubject, JAVA_LANG_STRING, propertyName);
-          if (!allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-          }
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject classSubject =
+                  checkClassIsKept(inspector, MUTABLE_PROPERTY_CLASS.getClassName());
+              String propertyName = "privateProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(classSubject, JAVA_LANG_STRING, propertyName);
+              if (!allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+              }
 
-          // Private property has no getter or setter.
-          checkMethodIsAbsent(
-              classSubject, MUTABLE_PROPERTY_CLASS.getGetterForProperty(propertyName));
-          checkMethodIsAbsent(
-              classSubject, MUTABLE_PROPERTY_CLASS.getSetterForProperty(propertyName));
-        });
+              // Private property has no getter or setter.
+              checkMethodIsAbsent(
+                  classSubject, MUTABLE_PROPERTY_CLASS.getGetterForProperty(propertyName));
+              checkMethodIsAbsent(
+                  classSubject, MUTABLE_PROPERTY_CLASS.getSetterForProperty(propertyName));
+            });
   }
 
   @Test
@@ -152,27 +151,27 @@
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_useProtectedProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject classSubject =
-              checkClassIsKept(codeInspector, MUTABLE_PROPERTY_CLASS.getClassName());
-          String propertyName = "protectedProp";
-          FieldSubject fieldSubject =
-              checkFieldIsKept(classSubject, JAVA_LANG_STRING, propertyName);
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject classSubject =
+                  checkClassIsKept(inspector, MUTABLE_PROPERTY_CLASS.getClassName());
+              String propertyName = "protectedProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(classSubject, JAVA_LANG_STRING, propertyName);
 
-          // Protected property has private field.
-          MethodSignature getter = MUTABLE_PROPERTY_CLASS.getGetterForProperty(propertyName);
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-            checkMethodIsRemoved(classSubject, getter);
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-            checkMethodIsKept(classSubject, getter);
-          }
-        });
+              // Protected property has private field.
+              MethodSignature getter = MUTABLE_PROPERTY_CLASS.getGetterForProperty(propertyName);
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+                checkMethodIsRemoved(classSubject, getter);
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+                checkMethodIsKept(classSubject, getter);
+              }
+            });
   }
 
   @Test
@@ -180,27 +179,27 @@
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_useInternalProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject classSubject =
-              checkClassIsKept(codeInspector, MUTABLE_PROPERTY_CLASS.getClassName());
-          String propertyName = "internalProp";
-          FieldSubject fieldSubject =
-              checkFieldIsKept(classSubject, JAVA_LANG_STRING, propertyName);
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject classSubject =
+                  checkClassIsKept(inspector, MUTABLE_PROPERTY_CLASS.getClassName());
+              String propertyName = "internalProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(classSubject, JAVA_LANG_STRING, propertyName);
 
-          // Internal property has private field
-          MethodSignature getter = MUTABLE_PROPERTY_CLASS.getGetterForProperty(propertyName);
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-            checkMethodIsRemoved(classSubject, getter);
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-            checkMethodIsKept(classSubject, getter);
-          }
-        });
+              // Internal property has private field
+              MethodSignature getter = MUTABLE_PROPERTY_CLASS.getGetterForProperty(propertyName);
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+                checkMethodIsRemoved(classSubject, getter);
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+                checkMethodIsKept(classSubject, getter);
+              }
+            });
   }
 
   @Test
@@ -208,27 +207,27 @@
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_usePublicProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject classSubject =
-              checkClassIsKept(codeInspector, MUTABLE_PROPERTY_CLASS.getClassName());
-          String propertyName = "publicProp";
-          FieldSubject fieldSubject =
-              checkFieldIsKept(classSubject, JAVA_LANG_STRING, propertyName);
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject classSubject =
+                  checkClassIsKept(inspector, MUTABLE_PROPERTY_CLASS.getClassName());
+              String propertyName = "publicProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(classSubject, JAVA_LANG_STRING, propertyName);
 
-          // Public property has private field
-          MethodSignature getter = MUTABLE_PROPERTY_CLASS.getGetterForProperty(propertyName);
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-            checkMethodIsRemoved(classSubject, getter);
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-            checkMethodIsKept(classSubject, getter);
-          }
-        });
+              // Public property has private field
+              MethodSignature getter = MUTABLE_PROPERTY_CLASS.getGetterForProperty(propertyName);
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+                checkMethodIsRemoved(classSubject, getter);
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+                checkMethodIsKept(classSubject, getter);
+              }
+            });
   }
 
   @Test
@@ -236,28 +235,28 @@
     String mainClass = addMainToClasspath("properties/MutablePropertyKt",
         "mutableProperty_usePrimitiveProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject classSubject =
-              checkClassIsKept(codeInspector, MUTABLE_PROPERTY_CLASS.getClassName());
-          String propertyName = "primitiveProp";
-          FieldSubject fieldSubject = checkFieldIsKept(classSubject, "int", propertyName);
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject classSubject =
+                  checkClassIsKept(inspector, MUTABLE_PROPERTY_CLASS.getClassName());
+              String propertyName = "primitiveProp";
+              FieldSubject fieldSubject = checkFieldIsKept(classSubject, "int", propertyName);
 
-          MethodSignature getter = MUTABLE_PROPERTY_CLASS.getGetterForProperty(propertyName);
-          MethodSignature setter = MUTABLE_PROPERTY_CLASS.getSetterForProperty(propertyName);
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-            checkMethodIsRemoved(classSubject, getter);
-            checkMethodIsRemoved(classSubject, setter);
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-            checkMethodIsKept(classSubject, getter);
-            checkMethodIsKept(classSubject, setter);
-          }
-        });
+              MethodSignature getter = MUTABLE_PROPERTY_CLASS.getGetterForProperty(propertyName);
+              MethodSignature setter = MUTABLE_PROPERTY_CLASS.getSetterForProperty(propertyName);
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+                checkMethodIsRemoved(classSubject, getter);
+                checkMethodIsRemoved(classSubject, setter);
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+                checkMethodIsKept(classSubject, getter);
+                checkMethodIsKept(classSubject, setter);
+              }
+            });
   }
 
   @Test
@@ -265,13 +264,13 @@
     String mainClass =
         addMainToClasspath("properties/LateInitPropertyKt", "lateInitProperty_noUseOfProperties");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          checkClassIsRemoved(codeInspector, LATE_INIT_PROPERTY_CLASS.getClassName());
-        });
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              checkClassIsRemoved(inspector, LATE_INIT_PROPERTY_CLASS.getClassName());
+            });
   }
 
   @Test
@@ -279,26 +278,26 @@
     String mainClass = addMainToClasspath(
         "properties/LateInitPropertyKt", "lateInitProperty_usePrivateLateInitProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject classSubject =
-              checkClassIsKept(codeInspector, LATE_INIT_PROPERTY_CLASS.getClassName());
-          String propertyName = "privateLateInitProp";
-          FieldSubject fieldSubject = classSubject.field(JAVA_LANG_STRING, propertyName);
-          assertTrue("Field is absent", fieldSubject.isPresent());
-          if (!allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-          }
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject classSubject =
+                  checkClassIsKept(inspector, LATE_INIT_PROPERTY_CLASS.getClassName());
+              String propertyName = "privateLateInitProp";
+              FieldSubject fieldSubject = classSubject.field(JAVA_LANG_STRING, propertyName);
+              assertTrue("Field is absent", fieldSubject.isPresent());
+              if (!allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+              }
 
-          // Private late init property have no getter or setter.
-          checkMethodIsAbsent(
-              classSubject, LATE_INIT_PROPERTY_CLASS.getGetterForProperty(propertyName));
-          checkMethodIsAbsent(
-              classSubject, LATE_INIT_PROPERTY_CLASS.getSetterForProperty(propertyName));
-        });
+              // Private late init property have no getter or setter.
+              checkMethodIsAbsent(
+                  classSubject, LATE_INIT_PROPERTY_CLASS.getGetterForProperty(propertyName));
+              checkMethodIsAbsent(
+                  classSubject, LATE_INIT_PROPERTY_CLASS.getSetterForProperty(propertyName));
+            });
   }
 
   @Test
@@ -306,24 +305,24 @@
     String mainClass = addMainToClasspath("properties/LateInitPropertyKt",
         "lateInitProperty_useProtectedLateInitProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject classSubject =
-              checkClassIsKept(codeInspector, LATE_INIT_PROPERTY_CLASS.getClassName());
-          String propertyName = "protectedLateInitProp";
-          FieldSubject fieldSubject = classSubject.field(JAVA_LANG_STRING, propertyName);
-          assertTrue("Field is absent", fieldSubject.isPresent());
-          if (!allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isProtected());
-          }
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject classSubject =
+                  checkClassIsKept(inspector, LATE_INIT_PROPERTY_CLASS.getClassName());
+              String propertyName = "protectedLateInitProp";
+              FieldSubject fieldSubject = classSubject.field(JAVA_LANG_STRING, propertyName);
+              assertTrue("Field is absent", fieldSubject.isPresent());
+              if (!allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isProtected());
+              }
 
-          // Protected late init property have protected getter
-          checkMethodIsRemoved(
-              classSubject, LATE_INIT_PROPERTY_CLASS.getGetterForProperty(propertyName));
-        });
+              // Protected late init property have protected getter
+              checkMethodIsRemoved(
+                  classSubject, LATE_INIT_PROPERTY_CLASS.getGetterForProperty(propertyName));
+            });
   }
 
   @Test
@@ -331,22 +330,22 @@
     String mainClass = addMainToClasspath(
         "properties/LateInitPropertyKt", "lateInitProperty_useInternalLateInitProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject classSubject =
-              checkClassIsKept(codeInspector, LATE_INIT_PROPERTY_CLASS.getClassName());
-          String propertyName = "internalLateInitProp";
-          FieldSubject fieldSubject = classSubject.field(JAVA_LANG_STRING, propertyName);
-          assertTrue("Field is absent", fieldSubject.isPresent());
-          assertTrue(fieldSubject.getField().accessFlags.isPublic());
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject classSubject =
+                  checkClassIsKept(inspector, LATE_INIT_PROPERTY_CLASS.getClassName());
+              String propertyName = "internalLateInitProp";
+              FieldSubject fieldSubject = classSubject.field(JAVA_LANG_STRING, propertyName);
+              assertTrue("Field is absent", fieldSubject.isPresent());
+              assertTrue(fieldSubject.getField().accessFlags.isPublic());
 
-          // Internal late init property have protected getter
-          checkMethodIsRemoved(
-              classSubject, LATE_INIT_PROPERTY_CLASS.getGetterForProperty(propertyName));
-        });
+              // Internal late init property have protected getter
+              checkMethodIsRemoved(
+                  classSubject, LATE_INIT_PROPERTY_CLASS.getGetterForProperty(propertyName));
+            });
   }
 
   @Test
@@ -354,22 +353,22 @@
     String mainClass = addMainToClasspath(
         "properties/LateInitPropertyKt", "lateInitProperty_usePublicLateInitProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject classSubject =
-              checkClassIsKept(codeInspector, LATE_INIT_PROPERTY_CLASS.getClassName());
-          String propertyName = "publicLateInitProp";
-          FieldSubject fieldSubject = classSubject.field(JAVA_LANG_STRING, propertyName);
-          assertTrue("Field is absent", fieldSubject.isPresent());
-          assertTrue(fieldSubject.getField().accessFlags.isPublic());
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject classSubject =
+                  checkClassIsKept(inspector, LATE_INIT_PROPERTY_CLASS.getClassName());
+              String propertyName = "publicLateInitProp";
+              FieldSubject fieldSubject = classSubject.field(JAVA_LANG_STRING, propertyName);
+              assertTrue("Field is absent", fieldSubject.isPresent());
+              assertTrue(fieldSubject.getField().accessFlags.isPublic());
 
-          // Internal late init property have protected getter
-          checkMethodIsRemoved(
-              classSubject, LATE_INIT_PROPERTY_CLASS.getGetterForProperty(propertyName));
-        });
+              // Internal late init property have protected getter
+              checkMethodIsRemoved(
+                  classSubject, LATE_INIT_PROPERTY_CLASS.getGetterForProperty(propertyName));
+            });
   }
 
   @Test
@@ -377,13 +376,13 @@
     String mainClass = addMainToClasspath(
         "properties/UserDefinedPropertyKt", "userDefinedProperty_noUseOfProperties");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          checkClassIsRemoved(codeInspector, USER_DEFINED_PROPERTY_CLASS.getClassName());
-        });
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              checkClassIsRemoved(inspector, USER_DEFINED_PROPERTY_CLASS.getClassName());
+            });
   }
 
   @Test
@@ -391,29 +390,30 @@
     String mainClass = addMainToClasspath(
         "properties/UserDefinedPropertyKt", "userDefinedProperty_useProperties");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject classSubject =
-              checkClassIsKept(codeInspector, USER_DEFINED_PROPERTY_CLASS.getClassName());
-          String propertyName = "durationInSeconds";
-          // The 'wrapper' property is not assigned to a backing field, it only relies on the
-          // wrapped property.
-          checkFieldIsAbsent(classSubject, "int", "durationInSeconds");
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject classSubject =
+                  checkClassIsKept(inspector, USER_DEFINED_PROPERTY_CLASS.getClassName());
+              String propertyName = "durationInSeconds";
+              // The 'wrapper' property is not assigned to a backing field, it only relies on the
+              // wrapped property.
+              checkFieldIsAbsent(classSubject, "int", "durationInSeconds");
 
-          FieldSubject fieldSubject =
-              checkFieldIsKept(classSubject, "int", "durationInMilliSeconds");
-          MethodSignature getter = USER_DEFINED_PROPERTY_CLASS.getGetterForProperty(propertyName);
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-            checkMethodIsRemoved(classSubject, getter);
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-            checkMethodIsKept(classSubject, getter);
-          }
-        });
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(classSubject, "int", "durationInMilliSeconds");
+              MethodSignature getter =
+                  USER_DEFINED_PROPERTY_CLASS.getGetterForProperty(propertyName);
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+                checkMethodIsRemoved(classSubject, getter);
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+                checkMethodIsKept(classSubject, getter);
+              }
+            });
   }
 
   @Test
@@ -421,32 +421,32 @@
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_usePrimitiveProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject outerClass =
-              checkClassIsKept(codeInspector, "properties.CompanionProperties");
-          ClassSubject companionClass =
-              checkClassIsKept(codeInspector, COMPANION_PROPERTY_CLASS.getClassName());
-          String propertyName = "primitiveProp";
-          FieldSubject fieldSubject = checkFieldIsKept(outerClass, "int", propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-          }
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject outerClass =
+                  checkClassIsKept(inspector, "properties.CompanionProperties");
+              ClassSubject companionClass =
+                  checkClassIsKept(inspector, COMPANION_PROPERTY_CLASS.getClassName());
+              String propertyName = "primitiveProp";
+              FieldSubject fieldSubject = checkFieldIsKept(outerClass, "int", propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+              }
 
-          MemberNaming.MethodSignature getter =
-              COMPANION_PROPERTY_CLASS.getGetterForProperty(propertyName);
-          checkMethodIsRemoved(companionClass, getter);
+              MemberNaming.MethodSignature getter =
+                  COMPANION_PROPERTY_CLASS.getGetterForProperty(propertyName);
+              checkMethodIsRemoved(companionClass, getter);
 
-          MemberNaming.MethodSignature setter =
-              COMPANION_PROPERTY_CLASS.getSetterForProperty(propertyName);
-          checkMethodIsRemoved(companionClass, setter);
-        });
+              MemberNaming.MethodSignature setter =
+                  COMPANION_PROPERTY_CLASS.getSetterForProperty(propertyName);
+              checkMethodIsRemoved(companionClass, setter);
+            });
   }
 
   @Test
@@ -454,37 +454,38 @@
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_usePrivateProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject outerClass =
-              checkClassIsKept(codeInspector, "properties.CompanionProperties");
-          ClassSubject companionClass =
-              checkClassIsKept(codeInspector, COMPANION_PROPERTY_CLASS.getClassName());
-          String propertyName = "privateProp";
-          FieldSubject fieldSubject = checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject outerClass =
+                  checkClassIsKept(inspector, "properties.CompanionProperties");
+              ClassSubject companionClass =
+                  checkClassIsKept(inspector, COMPANION_PROPERTY_CLASS.getClassName());
+              String propertyName = "privateProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-          MemberNaming.MethodSignature getter =
-              COMPANION_PROPERTY_CLASS.getGetterForProperty(propertyName);
-          MemberNaming.MethodSignature setter =
-              COMPANION_PROPERTY_CLASS.getSetterForProperty(propertyName);
+              MemberNaming.MethodSignature getter =
+                  COMPANION_PROPERTY_CLASS.getGetterForProperty(propertyName);
+              MemberNaming.MethodSignature setter =
+                  COMPANION_PROPERTY_CLASS.getSetterForProperty(propertyName);
 
-          // Because the getter/setter are private, they can only be called from another method in
-          // the class. If this is an instance method, they will be called on 'this' which is known
-          // to be non-null, thus the getter/setter can be inlined if their code is small enough.
-          // Because the backing field is private, they will call into an accessor (static) method.
-          // If access relaxation is enabled, this accessor can be removed.
-          checkMethodIsAbsent(companionClass, getter);
-          checkMethodIsAbsent(companionClass, setter);
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-          }
-        });
+              // Because the getter/setter are private, they can only be called from another method
+              // in the class. If this is an instance method, they will be called on 'this' which is
+              // known to be non-null, thus the getter/setter can be inlined if their code is small
+              // enough. Because the backing field is private, they will call into an accessor
+              // (static) method. If access relaxation is enabled, this accessor can be removed.
+              checkMethodIsAbsent(companionClass, getter);
+              checkMethodIsAbsent(companionClass, setter);
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+              }
+            });
   }
 
   @Test
@@ -492,31 +493,32 @@
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_useInternalProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject outerClass =
-              checkClassIsKept(codeInspector, "properties.CompanionProperties");
-          ClassSubject companionClass =
-              checkClassIsKept(codeInspector, COMPANION_PROPERTY_CLASS.getClassName());
-          String propertyName = "internalProp";
-          FieldSubject fieldSubject = checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-          }
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject outerClass =
+                  checkClassIsKept(inspector, "properties.CompanionProperties");
+              ClassSubject companionClass =
+                  checkClassIsKept(inspector, COMPANION_PROPERTY_CLASS.getClassName());
+              String propertyName = "internalProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+              }
 
-          MemberNaming.MethodSignature getter =
-              COMPANION_PROPERTY_CLASS.getGetterForProperty(propertyName);
-          checkMethodIsRemoved(companionClass, getter);
-          MemberNaming.MethodSignature setter =
-              COMPANION_PROPERTY_CLASS.getSetterForProperty(propertyName);
-          checkMethodIsRemoved(companionClass, setter);
-        });
+              MemberNaming.MethodSignature getter =
+                  COMPANION_PROPERTY_CLASS.getGetterForProperty(propertyName);
+              checkMethodIsRemoved(companionClass, getter);
+              MemberNaming.MethodSignature setter =
+                  COMPANION_PROPERTY_CLASS.getSetterForProperty(propertyName);
+              checkMethodIsRemoved(companionClass, setter);
+            });
   }
 
   @Test
@@ -524,32 +526,33 @@
     String mainClass = addMainToClasspath(
         "properties.CompanionPropertiesKt", "companionProperties_usePublicProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject outerClass =
-              checkClassIsKept(codeInspector, "properties.CompanionProperties");
-          ClassSubject companionClass =
-              checkClassIsKept(codeInspector, COMPANION_PROPERTY_CLASS.getClassName());
-          String propertyName = "publicProp";
-          FieldSubject fieldSubject = checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-          }
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject outerClass =
+                  checkClassIsKept(inspector, "properties.CompanionProperties");
+              ClassSubject companionClass =
+                  checkClassIsKept(inspector, COMPANION_PROPERTY_CLASS.getClassName());
+              String propertyName = "publicProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+              }
 
-          MemberNaming.MethodSignature getter =
-              COMPANION_PROPERTY_CLASS.getGetterForProperty(propertyName);
-          checkMethodIsRemoved(companionClass, getter);
+              MemberNaming.MethodSignature getter =
+                  COMPANION_PROPERTY_CLASS.getGetterForProperty(propertyName);
+              checkMethodIsRemoved(companionClass, getter);
 
-          MemberNaming.MethodSignature setter =
-              COMPANION_PROPERTY_CLASS.getSetterForProperty(propertyName);
-          checkMethodIsRemoved(companionClass, setter);
-        });
+              MemberNaming.MethodSignature setter =
+                  COMPANION_PROPERTY_CLASS.getSetterForProperty(propertyName);
+              checkMethodIsRemoved(companionClass, setter);
+            });
   }
 
   @Test
@@ -558,34 +561,35 @@
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_usePrivateLateInitProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject outerClass =
-              checkClassIsKept(codeInspector, testedClass.getOuterClassName());
-          ClassSubject companionClass = checkClassIsKept(codeInspector, testedClass.getClassName());
-          String propertyName = "privateLateInitProp";
-          FieldSubject fieldSubject = checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject outerClass =
+                  checkClassIsKept(inspector, testedClass.getOuterClassName());
+              ClassSubject companionClass = checkClassIsKept(inspector, testedClass.getClassName());
+              String propertyName = "privateLateInitProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-          MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
-          MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+              MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+              MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
 
-          // Because the getter/setter are private, they can only be called from another method in
-          // the class. If this is an instance method, they will be called on 'this' which is known
-          // to be non-null, thus the getter/setter can be inlined if their code is small enough.
-          // Because the backing field is private, they will call into an accessor (static) method.
-          // If access relaxation is enabled, this accessor can be removed.
-          checkMethodIsAbsent(companionClass, getter);
-          checkMethodIsAbsent(companionClass, setter);
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-          }
-        });
+              // Because the getter/setter are private, they can only be called from another method
+              // in the class. If this is an instance method, they will be called on 'this' which is
+              // known to be non-null, thus the getter/setter can be inlined if their code is small
+              // enough. Because the backing field is private, they will call into an accessor
+              // (static) method. If access relaxation is enabled, this accessor can be removed.
+              checkMethodIsAbsent(companionClass, getter);
+              checkMethodIsAbsent(companionClass, setter);
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+              }
+            });
   }
 
   @Test
@@ -594,25 +598,26 @@
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_useInternalLateInitProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject outerClass =
-              checkClassIsKept(codeInspector, testedClass.getOuterClassName());
-          ClassSubject companionClass = checkClassIsKept(codeInspector, testedClass.getClassName());
-          String propertyName = "internalLateInitProp";
-          FieldSubject fieldSubject = checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
-          assertTrue(fieldSubject.getField().accessFlags.isPublic());
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject outerClass =
+                  checkClassIsKept(inspector, testedClass.getOuterClassName());
+              ClassSubject companionClass = checkClassIsKept(inspector, testedClass.getClassName());
+              String propertyName = "internalLateInitProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
+              assertTrue(fieldSubject.getField().accessFlags.isPublic());
 
-          MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
-          checkMethodIsRemoved(companionClass, getter);
+              MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+              checkMethodIsRemoved(companionClass, getter);
 
-          MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
-          checkMethodIsRemoved(companionClass, setter);
-        });
+              MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+              checkMethodIsRemoved(companionClass, setter);
+            });
   }
 
   @Test
@@ -621,25 +626,26 @@
     String mainClass = addMainToClasspath("properties.CompanionLateInitPropertiesKt",
         "companionLateInitProperties_usePublicLateInitProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject outerClass =
-              checkClassIsKept(codeInspector, testedClass.getOuterClassName());
-          ClassSubject companionClass = checkClassIsKept(codeInspector, testedClass.getClassName());
-          String propertyName = "publicLateInitProp";
-          FieldSubject fieldSubject = checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
-          assertTrue(fieldSubject.getField().accessFlags.isPublic());
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject outerClass =
+                  checkClassIsKept(inspector, testedClass.getOuterClassName());
+              ClassSubject companionClass = checkClassIsKept(inspector, testedClass.getClassName());
+              String propertyName = "publicLateInitProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(outerClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
+              assertTrue(fieldSubject.getField().accessFlags.isPublic());
 
-          MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
-          checkMethodIsRemoved(companionClass, getter);
+              MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+              checkMethodIsRemoved(companionClass, getter);
 
-          MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
-          checkMethodIsRemoved(companionClass, setter);
-        });
+              MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+              checkMethodIsRemoved(companionClass, setter);
+            });
   }
 
   @Test
@@ -648,36 +654,36 @@
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_usePrimitiveProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
-          String propertyName = "primitiveProp";
-          FieldSubject fieldSubject = checkFieldIsKept(objectClass, "int", propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject objectClass = checkClassIsKept(inspector, testedClass.getClassName());
+              String propertyName = "primitiveProp";
+              FieldSubject fieldSubject = checkFieldIsKept(objectClass, "int", propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-          MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
-          MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+              MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+              MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
 
-          if (allowAccessModification) {
-            // Getter and setter is inlined because the constructor of ObjectProperties is
-            // considered trivial, which implies that the member value propagation marks the
-            // INSTANCE field as being non-null.
-            checkMethodIsRemoved(objectClass, getter);
-            checkMethodIsRemoved(objectClass, setter);
-          } else {
-            checkMethodIsKept(objectClass, getter);
-            checkMethodIsKept(objectClass, setter);
-          }
+              if (allowAccessModification) {
+                // Getter and setter is inlined because the constructor of ObjectProperties is
+                // considered trivial, which implies that the member value propagation marks the
+                // INSTANCE field as being non-null.
+                checkMethodIsRemoved(objectClass, getter);
+                checkMethodIsRemoved(objectClass, setter);
+              } else {
+                checkMethodIsKept(objectClass, getter);
+                checkMethodIsKept(objectClass, setter);
+              }
 
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-          }
-        });
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+              }
+            });
   }
 
   @Test
@@ -686,29 +692,30 @@
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_usePrivateProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
-          String propertyName = "privateProp";
-          FieldSubject fieldSubject = checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject objectClass = checkClassIsKept(inspector, testedClass.getClassName());
+              String propertyName = "privateProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-          MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
-          MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+              MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+              MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
 
-          // A private property has no getter/setter.
-          checkMethodIsAbsent(objectClass, getter);
-          checkMethodIsAbsent(objectClass, setter);
+              // A private property has no getter/setter.
+              checkMethodIsAbsent(objectClass, getter);
+              checkMethodIsAbsent(objectClass, setter);
 
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-          }
-        });
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+              }
+            });
   }
 
   @Test
@@ -717,36 +724,37 @@
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_useInternalProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
-          String propertyName = "internalProp";
-          FieldSubject fieldSubject = checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject objectClass = checkClassIsKept(inspector, testedClass.getClassName());
+              String propertyName = "internalProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-          MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
-          MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+              MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+              MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
 
-          if (allowAccessModification) {
-            // Getter and setter is inlined because the constructor of ObjectProperties is
-            // considered trivial, which implies that the member value propagation marks the
-            // INSTANCE field as being non-null.
-            checkMethodIsRemoved(objectClass, getter);
-            checkMethodIsRemoved(objectClass, setter);
-          } else {
-            checkMethodIsKept(objectClass, getter);
-            checkMethodIsKept(objectClass, setter);
-          }
+              if (allowAccessModification) {
+                // Getter and setter is inlined because the constructor of ObjectProperties is
+                // considered trivial, which implies that the member value propagation marks the
+                // INSTANCE field as being non-null.
+                checkMethodIsRemoved(objectClass, getter);
+                checkMethodIsRemoved(objectClass, setter);
+              } else {
+                checkMethodIsKept(objectClass, getter);
+                checkMethodIsKept(objectClass, setter);
+              }
 
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-          }
-        });
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+              }
+            });
   }
 
   @Test
@@ -755,36 +763,37 @@
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_usePublicProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
-          String propertyName = "publicProp";
-          FieldSubject fieldSubject = checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject objectClass = checkClassIsKept(inspector, testedClass.getClassName());
+              String propertyName = "publicProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-          MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
-          MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+              MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+              MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
 
-          if (allowAccessModification) {
-            // Getter and setter is inlined because the constructor of ObjectProperties is
-            // considered trivial, which implies that the member value propagation marks the
-            // INSTANCE field as being non-null.
-            checkMethodIsRemoved(objectClass, getter);
-            checkMethodIsRemoved(objectClass, setter);
-          } else {
-            checkMethodIsKept(objectClass, getter);
-            checkMethodIsKept(objectClass, setter);
-          }
+              if (allowAccessModification) {
+                // Getter and setter is inlined because the constructor of ObjectProperties is
+                // considered trivial, which implies that the member value propagation marks the
+                // INSTANCE field as being non-null.
+                checkMethodIsRemoved(objectClass, getter);
+                checkMethodIsRemoved(objectClass, setter);
+              } else {
+                checkMethodIsKept(objectClass, getter);
+                checkMethodIsKept(objectClass, setter);
+              }
 
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-          }
-        });
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+              }
+            });
   }
 
   @Test
@@ -793,29 +802,30 @@
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_useLateInitPrivateProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
-          String propertyName = "privateLateInitProp";
-          FieldSubject fieldSubject = checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject objectClass = checkClassIsKept(inspector, testedClass.getClassName());
+              String propertyName = "privateLateInitProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-          MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
-          MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+              MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+              MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
 
-          // A private property has no getter/setter.
-          checkMethodIsAbsent(objectClass, getter);
-          checkMethodIsAbsent(objectClass, setter);
+              // A private property has no getter/setter.
+              checkMethodIsAbsent(objectClass, getter);
+              checkMethodIsAbsent(objectClass, setter);
 
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-          }
-        });
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+              }
+            });
   }
 
   @Test
@@ -824,23 +834,24 @@
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_useLateInitInternalProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
-          String propertyName = "internalLateInitProp";
-          FieldSubject fieldSubject = checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject objectClass = checkClassIsKept(inspector, testedClass.getClassName());
+              String propertyName = "internalLateInitProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-          MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
-          MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+              MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+              MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
 
-          checkMethodIsRemoved(objectClass, getter);
-          checkMethodIsRemoved(objectClass, setter);
-          assertTrue(fieldSubject.getField().accessFlags.isPublic());
-        });
+              checkMethodIsRemoved(objectClass, getter);
+              checkMethodIsRemoved(objectClass, setter);
+              assertTrue(fieldSubject.getField().accessFlags.isPublic());
+            });
   }
 
   @Test
@@ -849,23 +860,24 @@
     String mainClass = addMainToClasspath(
         "properties.ObjectPropertiesKt", "objectProperties_useLateInitPublicProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
-          String propertyName = "publicLateInitProp";
-          FieldSubject fieldSubject = checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject objectClass = checkClassIsKept(inspector, testedClass.getClassName());
+              String propertyName = "publicLateInitProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-          MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
-          MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+              MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+              MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
 
-          checkMethodIsRemoved(objectClass, getter);
-          checkMethodIsRemoved(objectClass, setter);
-          assertTrue(fieldSubject.getField().accessFlags.isPublic());
-        });
+              checkMethodIsRemoved(objectClass, getter);
+              checkMethodIsRemoved(objectClass, setter);
+              assertTrue(fieldSubject.getField().accessFlags.isPublic());
+            });
   }
 
   @Test
@@ -874,29 +886,29 @@
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_usePrimitiveProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
-          String propertyName = "primitiveProp";
-          FieldSubject fieldSubject = checkFieldIsKept(objectClass, "int", propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject objectClass = checkClassIsKept(inspector, testedClass.getClassName());
+              String propertyName = "primitiveProp";
+              FieldSubject fieldSubject = checkFieldIsKept(objectClass, "int", propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-          MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
-          MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+              MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+              MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
 
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-            checkMethodIsRemoved(objectClass, getter);
-            checkMethodIsRemoved(objectClass, setter);
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-            checkMethodIsKept(objectClass, getter);
-            checkMethodIsKept(objectClass, setter);
-          }
-        });
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+                checkMethodIsRemoved(objectClass, getter);
+                checkMethodIsRemoved(objectClass, setter);
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+                checkMethodIsKept(objectClass, getter);
+                checkMethodIsKept(objectClass, setter);
+              }
+            });
   }
 
   @Test
@@ -905,28 +917,29 @@
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_usePrivateProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
-          String propertyName = "privateProp";
-          FieldSubject fieldSubject = checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject objectClass = checkClassIsKept(inspector, testedClass.getClassName());
+              String propertyName = "privateProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-          MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
-          MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+              MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+              MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
 
-          // A private property has no getter/setter.
-          checkMethodIsAbsent(objectClass, getter);
-          checkMethodIsAbsent(objectClass, setter);
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-          }
-        });
+              // A private property has no getter/setter.
+              checkMethodIsAbsent(objectClass, getter);
+              checkMethodIsAbsent(objectClass, setter);
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+              }
+            });
   }
 
   @Test
@@ -935,28 +948,31 @@
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_useInternalProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
-          String propertyName = "internalProp";
-          FieldSubject fieldSubject = checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject objectClass = checkClassIsKept(inspector, testedClass.getClassName());
+              String propertyName = "internalProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-          // We expect getter to be inlined when access (of the backing field) is relaxed to public.
-          // Note: the setter is considered as a regular method (because of KotlinC adding extra
-          // null checks), thus we cannot say if the setter would be inlined or not by R8.
-          MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-            checkMethodIsRemoved(objectClass, getter);
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-            checkMethodIsKept(objectClass, getter);
-          }
-        });
+              // We expect getter to be inlined when access (of the backing field) is relaxed to
+              // public.
+              //
+              // Note: the setter is considered as a regular method (because of KotlinC adding extra
+              // null checks), thus we cannot say if the setter would be inlined or not by R8.
+              MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+                checkMethodIsRemoved(objectClass, getter);
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+                checkMethodIsKept(objectClass, getter);
+              }
+            });
   }
 
   @Test
@@ -965,29 +981,30 @@
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_usePublicProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
-          String propertyName = "publicProp";
-          FieldSubject fieldSubject = checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject objectClass = checkClassIsKept(inspector, testedClass.getClassName());
+              String propertyName = "publicProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-          // We expect getter to be inlined when access (of the backing field) is relaxed to public.
-          // On the other hand, the setter is considered as a regular method (because of null
-          // checks), thus we cannot say if it can be inlined or not.
-          MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+              // We expect getter to be inlined when access (of the backing field) is relaxed to
+              // public. On the other hand, the setter is considered as a regular method (because of
+              // null checks), thus we cannot say if it can be inlined or not.
+              MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
 
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-            checkMethodIsRemoved(objectClass, getter);
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-            checkMethodIsKept(objectClass, getter);
-          }
-        });
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+                checkMethodIsRemoved(objectClass, getter);
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+                checkMethodIsKept(objectClass, getter);
+              }
+            });
   }
 
   @Test
@@ -996,28 +1013,29 @@
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_useLateInitPrivateProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject fileClass = checkClassIsKept(codeInspector, testedClass.getClassName());
-          String propertyName = "privateLateInitProp";
-          FieldSubject fieldSubject = checkFieldIsKept(fileClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject fileClass = checkClassIsKept(inspector, testedClass.getClassName());
+              String propertyName = "privateLateInitProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(fileClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-          MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
-          MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+              MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+              MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
 
-          // A private property has no getter/setter.
-          checkMethodIsAbsent(fileClass, getter);
-          checkMethodIsAbsent(fileClass, setter);
-          if (allowAccessModification) {
-            assertTrue(fieldSubject.getField().accessFlags.isPublic());
-          } else {
-            assertTrue(fieldSubject.getField().accessFlags.isPrivate());
-          }
-        });
+              // A private property has no getter/setter.
+              checkMethodIsAbsent(fileClass, getter);
+              checkMethodIsAbsent(fileClass, setter);
+              if (allowAccessModification) {
+                assertTrue(fieldSubject.getField().accessFlags.isPublic());
+              } else {
+                assertTrue(fieldSubject.getField().accessFlags.isPrivate());
+              }
+            });
   }
 
   @Test
@@ -1026,25 +1044,26 @@
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_useLateInitInternalProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
-          String propertyName = "internalLateInitProp";
-          FieldSubject fieldSubject = checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
-          assertTrue(fieldSubject.getField().accessFlags.isPublic());
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject objectClass = checkClassIsKept(inspector, testedClass.getClassName());
+              String propertyName = "internalLateInitProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
+              assertTrue(fieldSubject.getField().accessFlags.isPublic());
 
-          MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
-          MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+              MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+              MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
 
-          // Field is public and getter/setter is only called from one place so we expect to always
-          // inline it.
-          checkMethodIsRemoved(objectClass, getter);
-          checkMethodIsRemoved(objectClass, setter);
-        });
+              // Field is public and getter/setter is only called from one place so we expect to
+              // always inline it.
+              checkMethodIsRemoved(objectClass, getter);
+              checkMethodIsRemoved(objectClass, setter);
+            });
   }
 
   @Test
@@ -1053,23 +1072,24 @@
     String mainClass = addMainToClasspath(
         "properties.FilePropertiesKt", "fileProperties_useLateInitPublicProp");
     runTest(
-        PACKAGE_NAME,
-        mainClass,
-        disableAggressiveClassOptimizations,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject objectClass = checkClassIsKept(codeInspector, testedClass.getClassName());
-          String propertyName = "publicLateInitProp";
-          FieldSubject fieldSubject = checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
-          assertTrue(fieldSubject.getField().accessFlags.isStatic());
+            PACKAGE_NAME,
+            mainClass,
+            testBuilder -> testBuilder.addOptionsModification(disableAggressiveClassOptimizations))
+        .inspect(
+            inspector -> {
+              ClassSubject objectClass = checkClassIsKept(inspector, testedClass.getClassName());
+              String propertyName = "publicLateInitProp";
+              FieldSubject fieldSubject =
+                  checkFieldIsKept(objectClass, JAVA_LANG_STRING, propertyName);
+              assertTrue(fieldSubject.getField().accessFlags.isStatic());
 
-          MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
-          MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
+              MemberNaming.MethodSignature getter = testedClass.getGetterForProperty(propertyName);
+              MemberNaming.MethodSignature setter = testedClass.getSetterForProperty(propertyName);
 
-          checkMethodIsRemoved(objectClass, getter);
-          checkMethodIsRemoved(objectClass, setter);
-          assertTrue(fieldSubject.getField().accessFlags.isPublic());
-        });
+              checkMethodIsRemoved(objectClass, getter);
+              checkMethodIsRemoved(objectClass, setter);
+              assertTrue(fieldSubject.getField().accessFlags.isPublic());
+            });
   }
 
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/SimplifyIfNotNullKotlinTest.java b/src/test/java/com/android/tools/r8/kotlin/SimplifyIfNotNullKotlinTest.java
index a144a0e..d56d0f0 100644
--- a/src/test/java/com/android/tools/r8/kotlin/SimplifyIfNotNullKotlinTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/SimplifyIfNotNullKotlinTest.java
@@ -9,7 +9,6 @@
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.google.common.collect.ImmutableList;
 import java.util.Collection;
@@ -40,24 +39,21 @@
             ImmutableList.of("java.util.Collection", STRING, STRING, "java.lang.Integer"));
 
     final String mainClassName = ex1.getClassName();
-    final String extraRules =
-        keepMainMethod(mainClassName) + neverInlineMethod(mainClassName, testMethodSignature);
-    runTest(
-        FOLDER,
-        mainClassName,
-        extraRules,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject clazz = checkClassIsKept(codeInspector, ex1.getClassName());
+    final String extraRules = neverInlineMethod(mainClassName, testMethodSignature);
+    runTest(FOLDER, mainClassName, testBuilder -> testBuilder.addKeepRules(extraRules))
+        .inspect(
+            inspector -> {
+              ClassSubject clazz = checkClassIsKept(inspector, ex1.getClassName());
 
-          MethodSubject testMethod = checkMethodIsKept(clazz, testMethodSignature);
-          long ifzCount =
-              testMethod.streamInstructions().filter(i -> i.isIfEqz() || i.isIfNez()).count();
-          long paramNullCheckCount = countCall(testMethod, "Intrinsics", "checkParameterIsNotNull");
-          // One after Iterator#hasNext, and another in the filter predicate: sinceYear != null.
-          assertEquals(2, ifzCount);
-          assertEquals(0, paramNullCheckCount);
-        });
+              MethodSubject testMethod = checkMethodIsKept(clazz, testMethodSignature);
+              long ifzCount =
+                  testMethod.streamInstructions().filter(i -> i.isIfEqz() || i.isIfNez()).count();
+              long paramNullCheckCount =
+                  countCall(testMethod, "Intrinsics", "checkParameterIsNotNull");
+              // One after Iterator#hasNext, and another in the filter predicate: sinceYear != null.
+              assertEquals(2, ifzCount);
+              assertEquals(0, paramNullCheckCount);
+            });
   }
 
   @Test
@@ -67,22 +63,21 @@
         new MethodSignature("aOrDefault", STRING, ImmutableList.of(STRING, STRING));
 
     final String mainClassName = ex2.getClassName();
-    final String extraRules =
-        keepMainMethod(mainClassName) + neverInlineMethod(mainClassName, testMethodSignature);
-    runTest(FOLDER, mainClassName, extraRules,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject clazz = checkClassIsKept(codeInspector, ex2.getClassName());
+    final String extraRules = neverInlineMethod(mainClassName, testMethodSignature);
+    runTest(FOLDER, mainClassName, testBuilder -> testBuilder.addKeepRules(extraRules))
+        .inspect(
+            inspector -> {
+              ClassSubject clazz = checkClassIsKept(inspector, ex2.getClassName());
 
-          MethodSubject testMethod = checkMethodIsKept(clazz, testMethodSignature);
-          long ifzCount =
-              testMethod.streamInstructions().filter(i -> i.isIfEqz() || i.isIfNez()).count();
-          long paramNullCheckCount =
-              countCall(testMethod, "Intrinsics", "checkParameterIsNotNull");
-          // ?: in aOrDefault
-          assertEquals(1, ifzCount);
-          assertEquals(allowAccessModification ? 0 : 1, paramNullCheckCount);
-        });
+              MethodSubject testMethod = checkMethodIsKept(clazz, testMethodSignature);
+              long ifzCount =
+                  testMethod.streamInstructions().filter(i -> i.isIfEqz() || i.isIfNez()).count();
+              long paramNullCheckCount =
+                  countCall(testMethod, "Intrinsics", "checkParameterIsNotNull");
+              // ?: in aOrDefault
+              assertEquals(1, ifzCount);
+              assertEquals(allowAccessModification ? 0 : 1, paramNullCheckCount);
+            });
   }
 
   @Test
@@ -92,19 +87,18 @@
         new MethodSignature("neverThrowNPE", "void", ImmutableList.of("non_null.Foo"));
 
     final String mainClassName = ex3.getClassName();
-    final String extraRules =
-        keepMainMethod(mainClassName) + neverInlineMethod(mainClassName, testMethodSignature);
-    runTest(FOLDER, mainClassName, extraRules,
-        app -> {
-          CodeInspector codeInspector = new CodeInspector(app);
-          ClassSubject clazz = checkClassIsKept(codeInspector, ex3.getClassName());
+    final String extraRules = neverInlineMethod(mainClassName, testMethodSignature);
+    runTest(FOLDER, mainClassName, testBuilder -> testBuilder.addKeepRules(extraRules))
+        .inspect(
+            inspector -> {
+              ClassSubject clazz = checkClassIsKept(inspector, ex3.getClassName());
 
-          MethodSubject testMethod = checkMethodIsKept(clazz, testMethodSignature);
-          long ifzCount =
-              testMethod.streamInstructions().filter(i -> i.isIfEqz() || i.isIfNez()).count();
-          // !! operator inside explicit null check should be gone.
-          // One explicit null-check as well as 4 bar? accesses.
-          assertEquals(5, ifzCount);
-        });
+              MethodSubject testMethod = checkMethodIsKept(clazz, testMethodSignature);
+              long ifzCount =
+                  testMethod.streamInstructions().filter(i -> i.isIfEqz() || i.isIfNez()).count();
+              // !! operator inside explicit null check should be gone.
+              // One explicit null-check as well as 4 bar? accesses.
+              assertEquals(5, ifzCount);
+            });
   }
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergerValidationTest.java b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergerValidationTest.java
index bf17ac0..f28f68b 100644
--- a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergerValidationTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergerValidationTest.java
@@ -4,7 +4,6 @@
 package com.android.tools.r8.kotlin.lambda;
 
 import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
-import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.junit.Assume.assumeTrue;
 
@@ -55,11 +54,8 @@
         .addLibraryFiles(ToolHelper.getKotlinStdlibJar())
         .addProgramFiles(ktClasses)
         .addKeepMainRule("**.B143165163Kt")
-        .allowDiagnosticInfoMessages()
         .setMinApi(parameters.getApiLevel())
         .compile()
-        // TODO(b/143165163): better not output info like this.
-        .assertAllInfoMessagesMatch(containsString("unexpected static method"))
         .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar())
         .run(parameters.getRuntime(), pkg + ".B143165163Kt")
         .assertSuccessWithOutputLines("outer foo bar", "outer foo default");
@@ -81,11 +77,9 @@
         .addProgramFiles(ToolHelper.getKotlinStdlibJar())
         .addProgramFiles(ktClasses)
         .addKeepMainRule("**.B143165163Kt")
-        .allowDiagnosticMessages()
+        .allowDiagnosticWarningMessages()
         .setMinApi(parameters.getApiLevel())
         .compile()
-        // TODO(b/143165163): better not output info like this.
-        .assertAllInfoMessagesMatch(containsString("does not implement any interfaces"))
         .assertAllWarningMessagesMatch(equalTo("Resource 'META-INF/MANIFEST.MF' already exists."))
         .run(parameters.getRuntime(), pkg + ".B143165163Kt")
         .assertSuccessWithOutputLines("outer foo bar", "outer foo default");
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingTest.java b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingTest.java
index 9f51143..4f3ea88 100644
--- a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingTest.java
@@ -15,18 +15,14 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.optimize.lambda.CaptureSignature;
 import com.android.tools.r8.kotlin.AbstractR8KotlinTestBase;
-import com.android.tools.r8.utils.AndroidApp;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.google.common.collect.Lists;
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.function.Consumer;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -35,21 +31,16 @@
 public class KotlinLambdaMergingTest extends AbstractR8KotlinTestBase {
   private static final String KOTLIN_FUNCTION_IFACE = "Lkotlin/jvm/functions/Function";
   private static final String KOTLIN_FUNCTION_IFACE_STR = "kotlin.jvm.functions.Function";
-  private static final String KEEP_INNER_AND_ENCLOSING =
-      "-keepattributes InnerClasses,EnclosingMethod\n";
-  private static final String KEEP_SIGNATURE_INNER_ENCLOSING =
-      "-keepattributes Signature,InnerClasses,EnclosingMethod\n";
 
-  private Consumer<InternalOptions> getOptionsModifier() {
-    return opts -> {
-      opts.enableClassInlining = false;
-      // The test checks that the generated lambdas inherit from Function, which is not true if
-      // the unused interface removal is enabled.
-      opts.enableUnusedInterfaceRemoval = enableUnusedInterfaceRemoval;
-      // Ensure that enclosing method and inner class attributes are kept even on classes that are
-      // not explicitly mentioned by a keep rule.
-      opts.forceProguardCompatibility = true;
-    };
+  private void configure(InternalOptions options) {
+    options.enableClassInlining = false;
+    // The test checks that the generated lambdas inherit from Function, which is not true if
+    // the unused interface removal is enabled.
+    options.enableUnusedInterfaceRemoval = enableUnusedInterfaceRemoval;
+    // Ensure that enclosing method and inner class attributes are kept even on classes that are
+    // not explicitly mentioned by a keep rule.
+    options.forceProguardCompatibility = true;
+    options.enableHorizontalClassMergingOfKotlinLambdas = false;
   }
 
   private final boolean enableUnusedInterfaceRemoval;
@@ -173,10 +164,6 @@
     final List<DexClass> lambdas = new ArrayList<>();
     final List<DexClass> groups = new ArrayList<>();
 
-    Verifier(AndroidApp app) throws IOException, ExecutionException {
-      this(new CodeInspector(app));
-    }
-
     Verifier(CodeInspector codeInspector) {
       this.codeInspector = codeInspector;
       initGroupsAndLambdas();
@@ -304,242 +291,264 @@
   public void testTrivialKs() throws Exception {
     final String mainClassName = "lambdas_kstyle_trivial.MainKt";
     runTest(
-        "lambdas_kstyle_trivial",
-        mainClassName,
-        "-keepunusedarguments class * extends kotlin.jvm.internal.Lambda { invoke(int, short); }",
-        getOptionsModifier(),
-        app -> {
-          if (enableUnusedInterfaceRemoval) {
-            // Only test that the code generates the same output as the input code does on the JVM.
-            return;
-          }
+            "lambdas_kstyle_trivial",
+            mainClassName,
+            testBuilder ->
+                testBuilder
+                    .addKeepRules(
+                        "-keepunusedarguments class * extends kotlin.jvm.internal.Lambda {"
+                            + " invoke(int, short); }")
+                    .addOptionsModification(this::configure))
+        .inspect(
+            inspector -> {
+              if (enableUnusedInterfaceRemoval) {
+                // Only test that the code generates the same output as the input code does on the
+                // JVM.
+                return;
+              }
 
-          Verifier verifier = new Verifier(app);
-          String pkg = "lambdas_kstyle_trivial";
+              Verifier verifier = new Verifier(inspector);
+              String pkg = "lambdas_kstyle_trivial";
 
-          verifier.assertLambdaGroups(
-              allowAccessModification
-                  ? new Group[] {
-                    kstyle("", 0, 4),
-                    kstyle("", 1, 9),
-                    kstyle("", 2, 2), // -\
-                    kstyle("", 2, 5), // - 3 groups different by main method
-                    kstyle("", 2, 4), // -/
-                    kstyle("", 3, 2),
-                    kstyle("", 22, 2)
-                  }
-                  : new Group[] {
-                    kstyle(pkg, 0, 2),
-                    kstyle(pkg, 1, 5),
-                    kstyle(pkg, 2, 5), // - 2 groups different by main method
-                    kstyle(pkg, 2, 4), // -/
-                    kstyle(pkg, 3, 2),
-                    kstyle(pkg, 22, 2),
-                    kstyle(pkg + "/inner", 0, 2),
-                    kstyle(pkg + "/inner", 1, 4)
-                  });
+              verifier.assertLambdaGroups(
+                  allowAccessModification
+                      ? new Group[] {
+                        kstyle("", 0, 4),
+                        kstyle("", 1, 9),
+                        kstyle("", 2, 2), // -\
+                        kstyle("", 2, 5), // - 3 groups different by main method
+                        kstyle("", 2, 4), // -/
+                        kstyle("", 3, 2),
+                        kstyle("", 22, 2)
+                      }
+                      : new Group[] {
+                        kstyle(pkg, 0, 2),
+                        kstyle(pkg, 1, 5),
+                        kstyle(pkg, 2, 5), // - 2 groups different by main method
+                        kstyle(pkg, 2, 4), // -/
+                        kstyle(pkg, 3, 2),
+                        kstyle(pkg, 22, 2),
+                        kstyle(pkg + "/inner", 0, 2),
+                        kstyle(pkg + "/inner", 1, 4)
+                      });
 
-          verifier.assertLambdas(
-              allowAccessModification
-                  ? new Lambda[] {}
-                  : new Lambda[] {
-                    new Lambda(pkg, "MainKt$testStateless$8", 2),
-                    new Lambda(pkg + "/inner", "InnerKt$testInnerStateless$7", 2)
-                  });
-        });
+              verifier.assertLambdas(
+                  allowAccessModification
+                      ? new Lambda[] {}
+                      : new Lambda[] {
+                        new Lambda(pkg, "MainKt$testStateless$8", 2),
+                        new Lambda(pkg + "/inner", "InnerKt$testInnerStateless$7", 2)
+                      });
+            });
   }
 
   @Test
   public void testCapturesKs() throws Exception {
     final String mainClassName = "lambdas_kstyle_captures.MainKt";
     runTest(
-        "lambdas_kstyle_captures",
-        mainClassName,
-        getOptionsModifier(),
-        app -> {
-          if (enableUnusedInterfaceRemoval) {
-            // Only test that the code generates the same output as the input code does on the JVM.
-            return;
-          }
+            "lambdas_kstyle_captures",
+            mainClassName,
+            testBuilder -> testBuilder.addOptionsModification(this::configure))
+        .inspect(
+            inspector -> {
+              if (enableUnusedInterfaceRemoval) {
+                // Only test that the code generates the same output as the input code does on the
+                // JVM.
+                return;
+              }
 
-          Verifier verifier = new Verifier(app);
-          String pkg = "lambdas_kstyle_captures";
-          String grpPkg = allowAccessModification ? "" : pkg;
+              Verifier verifier = new Verifier(inspector);
+              String pkg = "lambdas_kstyle_captures";
+              String grpPkg = allowAccessModification ? "" : pkg;
 
-          verifier.assertLambdaGroups(
-              kstyle(grpPkg, "LLL", 0),
-              kstyle(grpPkg, "ILL", 0),
-              kstyle(grpPkg, "III", 0),
-              kstyle(grpPkg, "BCDFIJLLLLSZ", 0),
-              kstyle(grpPkg, "BCDFIJLLSZ", 0));
+              verifier.assertLambdaGroups(
+                  kstyle(grpPkg, "LLL", 0),
+                  kstyle(grpPkg, "ILL", 0),
+                  kstyle(grpPkg, "III", 0),
+                  kstyle(grpPkg, "BCDFIJLLLLSZ", 0),
+                  kstyle(grpPkg, "BCDFIJLLSZ", 0));
 
-          verifier.assertLambdas(
-              new Lambda(pkg, "MainKt$test1$15", 0),
-              new Lambda(pkg, "MainKt$test2$10", 0),
-              new Lambda(pkg, "MainKt$test2$11", 0),
-              new Lambda(pkg, "MainKt$test2$9", 0));
-        });
+              verifier.assertLambdas(
+                  new Lambda(pkg, "MainKt$test1$15", 0),
+                  new Lambda(pkg, "MainKt$test2$10", 0),
+                  new Lambda(pkg, "MainKt$test2$11", 0),
+                  new Lambda(pkg, "MainKt$test2$9", 0));
+            });
   }
 
   @Test
   public void testGenericsNoSignatureKs() throws Exception {
     final String mainClassName = "lambdas_kstyle_generics.MainKt";
     runTest(
-        "lambdas_kstyle_generics",
-        mainClassName,
-        getOptionsModifier(),
-        app -> {
-          if (enableUnusedInterfaceRemoval) {
-            // Only test that the code generates the same output as the input code does on the JVM.
-            return;
-          }
+            "lambdas_kstyle_generics",
+            mainClassName,
+            testBuilder -> testBuilder.addOptionsModification(this::configure))
+        .inspect(
+            inspector -> {
+              if (enableUnusedInterfaceRemoval) {
+                // Only test that the code generates the same output as the input code does on the
+                // JVM.
+                return;
+              }
 
-          Verifier verifier = new Verifier(app);
-          String pkg = "lambdas_kstyle_generics";
-          String grpPkg = allowAccessModification ? "" : pkg;
+              Verifier verifier = new Verifier(inspector);
+              String pkg = "lambdas_kstyle_generics";
+              String grpPkg = allowAccessModification ? "" : pkg;
 
-          verifier.assertLambdaGroups(
-              kstyle(grpPkg, 1, 3), // Group for Any
-              kstyle(grpPkg, "L", 1), // Group for Beta
-              kstyle(grpPkg, "LS", 1), // Group for Gamma
-              kstyle(grpPkg, 1, 2) // Group for int
-              );
+              verifier.assertLambdaGroups(
+                  kstyle(grpPkg, 1, 3), // Group for Any
+                  kstyle(grpPkg, "L", 1), // Group for Beta
+                  kstyle(grpPkg, "LS", 1), // Group for Gamma
+                  kstyle(grpPkg, 1, 2) // Group for int
+                  );
 
-          verifier.assertLambdas(new Lambda(pkg, "MainKt$main$4", 1));
-        });
+              verifier.assertLambdas(new Lambda(pkg, "MainKt$main$4", 1));
+            });
   }
 
   @Test
   public void testInnerClassesAndEnclosingMethodsKs() throws Exception {
     final String mainClassName = "lambdas_kstyle_generics.MainKt";
     runTest(
-        "lambdas_kstyle_generics",
-        mainClassName,
-        KEEP_INNER_AND_ENCLOSING,
-        getOptionsModifier(),
-        app -> {
-          if (enableUnusedInterfaceRemoval) {
-            // Only test that the code generates the same output as the input code does on the JVM.
-            return;
-          }
+            "lambdas_kstyle_generics",
+            mainClassName,
+            testBuilder ->
+                testBuilder
+                    .addKeepAttributeInnerClassesAndEnclosingMethod()
+                    .addOptionsModification(this::configure))
+        .inspect(
+            inspector -> {
+              if (enableUnusedInterfaceRemoval) {
+                // Only test that the code generates the same output as the input code does on the
+                // JVM.
+                return;
+              }
 
-          Verifier verifier = new Verifier(app);
-          String pkg = "lambdas_kstyle_generics";
-          String grpPkg = allowAccessModification ? "" : pkg;
+              Verifier verifier = new Verifier(inspector);
+              String pkg = "lambdas_kstyle_generics";
+              String grpPkg = allowAccessModification ? "" : pkg;
 
-          verifier.assertLambdaGroups(
-              kstyle(grpPkg, 1, 3), // Group for Any
-              kstyle(grpPkg, "L", 1), // Group for Beta   // First
-              kstyle(grpPkg, "L", 1), // Group for Beta   // Second
-              kstyle(grpPkg, "LS", 1), // Group for Gamma // First
-              kstyle(grpPkg, "LS", 1), // Group for Gamma // Second
-              kstyle(grpPkg, 1, 2) // Group for int
-              );
+              verifier.assertLambdaGroups(
+                  kstyle(grpPkg, 1, 3), // Group for Any
+                  kstyle(grpPkg, "L", 1), // Group for Beta   // First
+                  kstyle(grpPkg, "L", 1), // Group for Beta   // Second
+                  kstyle(grpPkg, "LS", 1), // Group for Gamma // First
+                  kstyle(grpPkg, "LS", 1), // Group for Gamma // Second
+                  kstyle(grpPkg, 1, 2) // Group for int
+                  );
 
-          verifier.assertLambdas(new Lambda(pkg, "MainKt$main$4", 1));
-        });
+              verifier.assertLambdas(new Lambda(pkg, "MainKt$main$4", 1));
+            });
   }
 
   @Test
   public void testGenericsSignatureInnerEnclosingKs() throws Exception {
     final String mainClassName = "lambdas_kstyle_generics.MainKt";
     runTest(
-        "lambdas_kstyle_generics",
-        mainClassName,
-        KEEP_SIGNATURE_INNER_ENCLOSING,
-        getOptionsModifier(),
-        app -> {
-          if (enableUnusedInterfaceRemoval) {
-            // Only test that the code generates the same output as the input code does on the JVM.
-            return;
-          }
+            "lambdas_kstyle_generics",
+            mainClassName,
+            // KEEP_SIGNATURE_INNER_ENCLOSING,
+            testBuilder ->
+                testBuilder
+                    .addKeepAttributeInnerClassesAndEnclosingMethod()
+                    .addKeepAttributeSignature()
+                    .addOptionsModification(this::configure))
+        .inspect(
+            inspector -> {
+              if (enableUnusedInterfaceRemoval) {
+                // Only test that the code generates the same output as the input code does on the
+                // JVM.
+                return;
+              }
 
-          Verifier verifier = new Verifier(app);
-          String pkg = "lambdas_kstyle_generics";
-          String grpPkg = allowAccessModification ? "" : pkg;
+              Verifier verifier = new Verifier(inspector);
+              String pkg = "lambdas_kstyle_generics";
+              String grpPkg = allowAccessModification ? "" : pkg;
 
-          verifier.assertLambdaGroups(
-              kstyle(grpPkg, 1, 3), // Group for Any
-              kstyle(grpPkg, "L", 1), // Group for Beta in First
-              kstyle(grpPkg, "L", 1), // Group for Beta in Second
-              kstyle(grpPkg, "LS", 1), // Group for Gamma<String> in First
-              kstyle(grpPkg, "LS", 1), // Group for Gamma<Integer> in First
-              kstyle(grpPkg, "LS", 1), // Group for Gamma<String> in Second
-              kstyle(grpPkg, "LS", 1), // Group for Gamma<Integer> in Second
-              kstyle(grpPkg, 1, 2) // Group for int
-              );
+              verifier.assertLambdaGroups(
+                  kstyle(grpPkg, 1, 3), // Group for Any
+                  kstyle(grpPkg, "L", 1), // Group for Beta in First
+                  kstyle(grpPkg, "L", 1), // Group for Beta in Second
+                  kstyle(grpPkg, "LS", 1), // Group for Gamma<String> in First
+                  kstyle(grpPkg, "LS", 1), // Group for Gamma<Integer> in First
+                  kstyle(grpPkg, "LS", 1), // Group for Gamma<String> in Second
+                  kstyle(grpPkg, "LS", 1), // Group for Gamma<Integer> in Second
+                  kstyle(grpPkg, 1, 2) // Group for int
+                  );
 
-          verifier.assertLambdas(new Lambda(pkg, "MainKt$main$4", 1));
-        });
+              verifier.assertLambdas(new Lambda(pkg, "MainKt$main$4", 1));
+            });
   }
 
   @Test
   public void testTrivialJs() throws Exception {
     final String mainClassName = "lambdas_jstyle_trivial.MainKt";
     runTest(
-        "lambdas_jstyle_trivial",
-        mainClassName,
-        getOptionsModifier(),
-        app -> {
-          Verifier verifier = new Verifier(app);
-          String pkg = "lambdas_jstyle_trivial";
-          String grp = allowAccessModification ? "" : pkg;
+            "lambdas_jstyle_trivial",
+            mainClassName,
+            testBuilder -> testBuilder.addOptionsModification(this::configure))
+        .inspect(
+            inspector -> {
+              Verifier verifier = new Verifier(inspector);
+              String pkg = "lambdas_jstyle_trivial";
+              String grp = allowAccessModification ? "" : pkg;
 
-          String supplier = "lambdas_jstyle_trivial.Lambdas$Supplier";
-          String intSupplier = "lambdas_jstyle_trivial.Lambdas$IntSupplier";
-          String consumer = "lambdas_jstyle_trivial.Lambdas$Consumer";
-          String intConsumer = "lambdas_jstyle_trivial.Lambdas$IntConsumer";
-          String multiFunction = "lambdas_jstyle_trivial.Lambdas$MultiFunction";
+              String supplier = "lambdas_jstyle_trivial.Lambdas$Supplier";
+              String intSupplier = "lambdas_jstyle_trivial.Lambdas$IntSupplier";
+              String consumer = "lambdas_jstyle_trivial.Lambdas$Consumer";
+              String intConsumer = "lambdas_jstyle_trivial.Lambdas$IntConsumer";
+              String multiFunction = "lambdas_jstyle_trivial.Lambdas$MultiFunction";
 
-          verifier.assertLambdaGroups(
-              jstyle(grp, 0, intSupplier, 2),
-              jstyle(grp, "L", 0, supplier),
-              jstyle(grp, "LL", 0, supplier),
-              jstyle(grp, "LLL", 0, supplier),
-              jstyle(grp, 1, intConsumer, allowAccessModification ? 3 : 2),
-              jstyle(grp, "I", 1, consumer),
-              jstyle(grp, "II", 1, consumer),
-              jstyle(grp, "III", 1, consumer),
-              jstyle(grp, "IIII", 1, consumer),
-              jstyle(grp, 3, multiFunction, 2),
-              jstyle(grp, 3, multiFunction, 2),
-              jstyle(grp, 3, multiFunction, 4),
-              jstyle(grp, 3, multiFunction, 6));
+              verifier.assertLambdaGroups(
+                  jstyle(grp, 0, intSupplier, 2),
+                  jstyle(grp, "L", 0, supplier),
+                  jstyle(grp, "LL", 0, supplier),
+                  jstyle(grp, "LLL", 0, supplier),
+                  jstyle(grp, 1, intConsumer, allowAccessModification ? 3 : 2),
+                  jstyle(grp, "I", 1, consumer),
+                  jstyle(grp, "II", 1, consumer),
+                  jstyle(grp, "III", 1, consumer),
+                  jstyle(grp, "IIII", 1, consumer),
+                  jstyle(grp, 3, multiFunction, 2),
+                  jstyle(grp, 3, multiFunction, 2),
+                  jstyle(grp, 3, multiFunction, 4),
+                  jstyle(grp, 3, multiFunction, 6));
 
-          verifier.assertLambdas(
-              allowAccessModification
-                  ? new Lambda[] {
-                    new Lambda(pkg + "/inner", "InnerKt$testInner1$4", 1),
-                    new Lambda(pkg + "/inner", "InnerKt$testInner1$5", 1)
-                  }
-                  : new Lambda[] {
-                    new Lambda(pkg + "/inner", "InnerKt$testInner1$1", 1),
-                    new Lambda(pkg + "/inner", "InnerKt$testInner1$2", 1),
-                    new Lambda(pkg + "/inner", "InnerKt$testInner1$3", 1),
-                    new Lambda(pkg + "/inner", "InnerKt$testInner1$4", 1),
-                    new Lambda(pkg + "/inner", "InnerKt$testInner1$5", 1)
-                  });
-        });
+              verifier.assertLambdas(
+                  allowAccessModification
+                      ? new Lambda[] {
+                        new Lambda(pkg + "/inner", "InnerKt$testInner1$4", 1),
+                        new Lambda(pkg + "/inner", "InnerKt$testInner1$5", 1)
+                      }
+                      : new Lambda[] {
+                        new Lambda(pkg + "/inner", "InnerKt$testInner1$1", 1),
+                        new Lambda(pkg + "/inner", "InnerKt$testInner1$2", 1),
+                        new Lambda(pkg + "/inner", "InnerKt$testInner1$3", 1),
+                        new Lambda(pkg + "/inner", "InnerKt$testInner1$4", 1),
+                        new Lambda(pkg + "/inner", "InnerKt$testInner1$5", 1)
+                      });
+            });
   }
 
   @Test
   public void testSingleton() throws Exception {
     final String mainClassName = "lambdas_singleton.MainKt";
     runTest(
-        "lambdas_singleton",
-        mainClassName,
-        "-nohorizontalclassmerging class *",
-        getOptionsModifier(),
-        app -> {
-          Verifier verifier = new Verifier(app);
-          String pkg = "lambdas_singleton";
-          String grp = allowAccessModification ? "" : pkg;
+            "lambdas_singleton",
+            mainClassName,
+            testBuilder ->
+                testBuilder.addOptionsModification(this::configure).noHorizontalClassMerging())
+        .inspect(
+            inspector -> {
+              Verifier verifier = new Verifier(inspector);
+              String pkg = "lambdas_singleton";
+              String grp = allowAccessModification ? "" : pkg;
 
-          verifier.assertLambdaGroups(
-              kstyle(grp, 1, 1 /* 1 out of 5 lambdas in the group */),
-              jstyle(grp, 2, "java.util.Comparator", 0 /* 0 out of 2 lambdas in the group */));
+              verifier.assertLambdaGroups(
+                  kstyle(grp, 1, 1 /* 1 out of 5 lambdas in the group */),
+                  jstyle(grp, 2, "java.util.Comparator", 0 /* 0 out of 2 lambdas in the group */));
 
-          verifier.assertLambdas(/* None */ );
-        });
+              verifier.assertLambdas(/* None */ );
+            });
   }
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingWithReprocessingTest.java b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingWithReprocessingTest.java
index 5ba846c..d6e8c53 100644
--- a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingWithReprocessingTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingWithReprocessingTest.java
@@ -6,21 +6,13 @@
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.kotlin.AbstractR8KotlinTestBase;
 import com.android.tools.r8.utils.BooleanUtils;
-import com.android.tools.r8.utils.InternalOptions;
 import java.util.Collection;
-import java.util.function.Consumer;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
 @RunWith(Parameterized.class)
 public class KotlinLambdaMergingWithReprocessingTest extends AbstractR8KotlinTestBase {
-  private Consumer<InternalOptions> optionsModifier =
-    o -> {
-      o.enableInlining = true;
-      o.enableClassInlining = true;
-      o.enableLambdaMerging = true;
-    };
 
   @Parameterized.Parameters(name = "target: {0}, allowAccessModification: {1}")
   public static Collection<Object[]> data() {
@@ -35,6 +27,15 @@
   @Test
   public void testMergingKStyleLambdasAndReprocessing() throws Exception {
     final String mainClassName = "reprocess_merged_lambdas_kstyle.MainKt";
-    runTest("reprocess_merged_lambdas_kstyle", mainClassName, optionsModifier, null);
+    runTest(
+        "reprocess_merged_lambdas_kstyle",
+        mainClassName,
+        testBuilder ->
+            testBuilder.addOptionsModification(
+                options -> {
+                  options.enableInlining = true;
+                  options.enableClassInlining = true;
+                  options.enableLambdaMerging = true;
+                }));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingWithSmallInliningBudgetTest.java b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingWithSmallInliningBudgetTest.java
index 446d982..127c1e3 100644
--- a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingWithSmallInliningBudgetTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingWithSmallInliningBudgetTest.java
@@ -6,22 +6,13 @@
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.kotlin.AbstractR8KotlinTestBase;
 import com.android.tools.r8.utils.BooleanUtils;
-import com.android.tools.r8.utils.InternalOptions;
 import java.util.Collection;
-import java.util.function.Consumer;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
 @RunWith(Parameterized.class)
 public class KotlinLambdaMergingWithSmallInliningBudgetTest extends AbstractR8KotlinTestBase {
-  private Consumer<InternalOptions> optionsModifier =
-      o -> {
-        o.enableInlining = true;
-        o.enableClassInlining = true;
-        o.enableLambdaMerging = true;
-        o.inliningInstructionAllowance = 3;
-      };
 
   @Parameterized.Parameters(name = "target: {0}, allowAccessModification: {1}")
   public static Collection<Object[]> data() {
@@ -36,6 +27,11 @@
   @Test
   public void testJStyleRunnable() throws Exception {
     final String mainClassName = "lambdas_jstyle_runnable.MainKt";
-    runTest("lambdas_jstyle_runnable", mainClassName, optionsModifier, null);
+    runTest(
+        "lambdas_jstyle_runnable",
+        mainClassName,
+        testBuilder ->
+            testBuilder.addOptionsModification(
+                options -> options.inliningInstructionAllowance = 3));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/b148525512/B148525512.java b/src/test/java/com/android/tools/r8/kotlin/lambda/b148525512/B148525512.java
index a06dcf8..d764d5f 100644
--- a/src/test/java/com/android/tools/r8/kotlin/lambda/b148525512/B148525512.java
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/b148525512/B148525512.java
@@ -125,6 +125,8 @@
             .addKeepClassAndMembersRules(baseClassName)
             .addKeepClassAndMembersRules(featureKtClassNamet)
             .addKeepClassAndMembersRules(FeatureAPI.class)
+            .addOptionsModification(
+                options -> options.enableHorizontalClassMergingOfKotlinLambdas = false)
             .setMinApi(parameters.getApiLevel())
             .noMinification() // The check cast inspection above relies on original names.
             .addFeatureSplit(
diff --git a/src/test/java/com/android/tools/r8/naming/FieldNamingObfuscationDictionaryTest.java b/src/test/java/com/android/tools/r8/naming/FieldNamingObfuscationDictionaryTest.java
index be7230d..0ab866f 100644
--- a/src/test/java/com/android/tools/r8/naming/FieldNamingObfuscationDictionaryTest.java
+++ b/src/test/java/com/android/tools/r8/naming/FieldNamingObfuscationDictionaryTest.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -51,6 +52,7 @@
   }
 
   @NeverClassInline
+  @NoHorizontalClassMerging
   public static class C extends A {
 
     public int f0;
@@ -98,6 +100,7 @@
         .addKeepRules("-overloadaggressively", "-obfuscationdictionary " + dictionary.toString())
         .addKeepMainRule(Runner.class)
         .enableNeverClassInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .run(parameters.getRuntime(), Runner.class, "HELLO", "WORLD")
diff --git a/src/test/java/com/android/tools/r8/naming/InterfaceMethodNameMinifierTest.java b/src/test/java/com/android/tools/r8/naming/InterfaceMethodNameMinifierTest.java
index f2e1a60..a853b95 100644
--- a/src/test/java/com/android/tools/r8/naming/InterfaceMethodNameMinifierTest.java
+++ b/src/test/java/com/android/tools/r8/naming/InterfaceMethodNameMinifierTest.java
@@ -27,7 +27,7 @@
             "-obfuscationdictionary " + dictionary.toString())
         // Minify the interface methods in alphabetic order.
         .addOptionsModification(
-            options -> options.testing.minifier.interfaceMethodOrdering = DexMethod::slowCompareTo)
+            options -> options.testing.minifier.interfaceMethodOrdering = DexMethod::compareTo)
         .compile();
   }
 
diff --git a/src/test/java/com/android/tools/r8/naming/ProguardMapReaderPackageInfoTest.java b/src/test/java/com/android/tools/r8/naming/ProguardMapReaderPackageInfoTest.java
new file mode 100644
index 0000000..c889dbb
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/naming/ProguardMapReaderPackageInfoTest.java
@@ -0,0 +1,39 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.naming;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.utils.StringUtils;
+import java.io.IOException;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class ProguardMapReaderPackageInfoTest extends TestBase {
+
+  private final TestParameters parameters;
+  private final String MAPPING =
+      StringUtils.joinLines(
+          "android.support.v4.internal.package-info -> android.support.v4.internal.package-info:",
+          "# {\"id\":\"sourceFile\",\"fileName\":\"SourceFile\"}");
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public ProguardMapReaderPackageInfoTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void parseMappings() throws IOException {
+    ClassNameMapper.mapperFromString(MAPPING);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/naming/ProguardMapReaderTest.java b/src/test/java/com/android/tools/r8/naming/ProguardMapReaderTest.java
index 1ab9fe1..668a8a0 100644
--- a/src/test/java/com/android/tools/r8/naming/ProguardMapReaderTest.java
+++ b/src/test/java/com/android/tools/r8/naming/ProguardMapReaderTest.java
@@ -26,7 +26,7 @@
   private static final String ROOT = ToolHelper.EXAMPLES_BUILD_DIR;
   private static final String EXAMPLE_MAP = "throwing/throwing.map";
   private static final String EXAMPLE_MAP_WITH_PACKAGE_INFO =
-      "dagger.android.package-info -> dagger.android.package-info\n";
+      "dagger.android.package-info -> dagger.android.package-info:\n";
 
   @Test
   public void parseThrowingMap() throws IOException {
@@ -141,7 +141,7 @@
   @Test
   public void parseMapWithPackageInfo() throws IOException {
     ClassNameMapper mapper = ClassNameMapper.mapperFromString(EXAMPLE_MAP_WITH_PACKAGE_INFO);
-    Assert.assertTrue(mapper.getObfuscatedToOriginalMapping().original.isEmpty());
+    assertEquals(1, mapper.getObfuscatedToOriginalMapping().original.size());
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/naming/identifiernamestring/ClassNameComparisonTest.java b/src/test/java/com/android/tools/r8/naming/identifiernamestring/ClassNameComparisonTest.java
index 5aaf063..f37c70c 100644
--- a/src/test/java/com/android/tools/r8/naming/identifiernamestring/ClassNameComparisonTest.java
+++ b/src/test/java/com/android/tools/r8/naming/identifiernamestring/ClassNameComparisonTest.java
@@ -6,6 +6,7 @@
 
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -21,7 +22,7 @@
 
   @Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimes().build();
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
   }
 
   public ClassNameComparisonTest(TestParameters parameters) {
@@ -33,7 +34,8 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(ClassNameComparisonTest.class)
         .addKeepMainRule(TestClass.class)
-        .setMinApi(parameters.getRuntime())
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
         .compile()
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutputLines("Hello!", "Hello " + B.class.getName() + "!");
@@ -64,7 +66,9 @@
     }
   }
 
+  @NoHorizontalClassMerging
   static class A {}
 
+  @NoHorizontalClassMerging
   static class B {}
 }
diff --git a/src/test/java/com/android/tools/r8/peephole/suffixsharing/IdenticalBlockSuffixSharingWithArrayTypesTest.java b/src/test/java/com/android/tools/r8/peephole/suffixsharing/IdenticalBlockSuffixSharingWithArrayTypesTest.java
index 1781d9f..86fd5d0 100644
--- a/src/test/java/com/android/tools/r8/peephole/suffixsharing/IdenticalBlockSuffixSharingWithArrayTypesTest.java
+++ b/src/test/java/com/android/tools/r8/peephole/suffixsharing/IdenticalBlockSuffixSharingWithArrayTypesTest.java
@@ -12,6 +12,7 @@
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.utils.StringUtils;
@@ -54,7 +55,7 @@
             new ClassTestParameter(InstancePutTestClass.class),
             new ClassTestParameter(InvokeTestClass.class),
             new ClassTestParameter(StaticPutTestClass.class)),
-        getTestParameters().withAllRuntimes().build());
+        getTestParameters().withAllRuntimesAndApiLevels().build());
   }
 
   public IdenticalBlockSuffixSharingWithArrayTypesTest(
@@ -70,7 +71,7 @@
     String expectedOutput = StringUtils.lines("42");
     testForD8()
         .addInnerClasses(IdenticalBlockSuffixSharingWithArrayTypesTest.class)
-        .setMinApi(parameters.getRuntime())
+        .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(this::verifyInstructionCount)
         .run(parameters.getRuntime(), clazz)
@@ -85,7 +86,8 @@
         .addKeepMainRule(clazz)
         .enableNeverClassInliningAnnotations()
         .enableInliningAnnotations()
-        .setMinApi(parameters.getRuntime())
+        .enableNoHorizontalClassMergingAnnotations()
+        .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(this::verifyInstructionCount)
         .run(parameters.getRuntime(), clazz)
@@ -233,7 +235,9 @@
 
   interface K extends I {}
 
+  @NoHorizontalClassMerging
   class A implements I {}
 
+  @NoHorizontalClassMerging
   class B implements I {}
 }
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageAnnotationTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageAnnotationTest.java
new file mode 100644
index 0000000..61526d4
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageAnnotationTest.java
@@ -0,0 +1,83 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.repackage;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestParameters;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class RepackageAnnotationTest extends RepackageTestBase {
+
+  private final String EXPECTED = "Hello World";
+
+  public RepackageAnnotationTest(
+      String flattenPackageHierarchyOrRepackageClasses, TestParameters parameters) {
+    super(flattenPackageHierarchyOrRepackageClasses, parameters);
+  }
+
+  @Test
+  public void testRuntime() throws Exception {
+    testForRuntime(parameters)
+        .addProgramClasses(Main.class, Annotation.class, A.class)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addProgramClasses(Main.class, A.class, Annotation.class)
+        .setMinApi(parameters.getApiLevel())
+        .addKeepMainRule(Main.class)
+        .enableInliningAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .addKeepRuntimeVisibleAnnotations()
+        .addKeepRules(
+            "-keep,allowobfuscation @interface " + Annotation.class.getTypeName() + " {",
+            "  *;",
+            "}")
+        .apply(this::configureRepackaging)
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      System.out.println(new A().getAnnotationValues());
+    }
+  }
+
+  @Retention(RetentionPolicy.RUNTIME)
+  @Target({ElementType.TYPE})
+  public @interface Annotation {
+
+    String f1();
+
+    String f2();
+  }
+
+  @Annotation(f1 = "Hello", f2 = "World")
+  @NeverClassInline
+  public static class A {
+
+    @NeverInline
+    public String getAnnotationValues() {
+      Annotation annotation = A.class.getAnnotation(Annotation.class);
+      if (annotation == null) {
+        return null;
+      }
+      return annotation.f1() + " " + annotation.f2();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageInnerAndOuterClassTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageInnerAndOuterClassTest.java
index 47a1dcd..81de378 100644
--- a/src/test/java/com/android/tools/r8/repackage/RepackageInnerAndOuterClassTest.java
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageInnerAndOuterClassTest.java
@@ -7,6 +7,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoStaticClassMerging;
 import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.TestParameters;
@@ -46,6 +47,7 @@
         .addKeepAttributeInnerClassesAndEnclosingMethod()
         .apply(this::configureRepackaging)
         .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .enableNoStaticClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
         .compile()
@@ -67,6 +69,7 @@
     }
   }
 
+  @NoHorizontalClassMerging
   @NoStaticClassMerging
   public static class Outer {
 
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageTest.java
index 150a7cc..da09e88 100644
--- a/src/test/java/com/android/tools/r8/repackage/RepackageTest.java
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageTest.java
@@ -112,6 +112,7 @@
         .allowAccessModification(allowAccessModification)
         .apply(this::configureRepackaging)
         .enableInliningAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .enableNoStaticClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
         .compile()
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassDirect.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassDirect.java
index 4f47751..6ee9df6 100644
--- a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassDirect.java
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassDirect.java
@@ -5,8 +5,10 @@
 package com.android.tools.r8.repackage.testclasses.repackagetest;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoStaticClassMerging;
 
+@NoHorizontalClassMerging
 @NoStaticClassMerging
 public class AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassDirect {
 
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassIndirect.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassIndirect.java
index fdc4a9c..7a80bb3 100644
--- a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassIndirect.java
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassIndirect.java
@@ -5,8 +5,10 @@
 package com.android.tools.r8.repackage.testclasses.repackagetest;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoStaticClassMerging;
 
+@NoHorizontalClassMerging
 @NoStaticClassMerging
 public class AccessPackagePrivateKeptMethodAllowRenamingOnReachableClassIndirect {
 
@@ -15,6 +17,7 @@
     Helper.test();
   }
 
+  @NoHorizontalClassMerging
   @NoStaticClassMerging
   public static class Helper {
 
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodOnReachableClassDirect.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodOnReachableClassDirect.java
index 0f9ae96..f8af76d 100644
--- a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodOnReachableClassDirect.java
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodOnReachableClassDirect.java
@@ -5,8 +5,10 @@
 package com.android.tools.r8.repackage.testclasses.repackagetest;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoStaticClassMerging;
 
+@NoHorizontalClassMerging
 @NoStaticClassMerging
 public class AccessPackagePrivateKeptMethodOnReachableClassDirect {
 
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodOnReachableClassIndirect.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodOnReachableClassIndirect.java
index 68aa391..279d85b 100644
--- a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodOnReachableClassIndirect.java
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateKeptMethodOnReachableClassIndirect.java
@@ -5,8 +5,10 @@
 package com.android.tools.r8.repackage.testclasses.repackagetest;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoStaticClassMerging;
 
+@NoHorizontalClassMerging
 @NoStaticClassMerging
 public class AccessPackagePrivateKeptMethodOnReachableClassIndirect {
 
@@ -15,6 +17,7 @@
     Helper.test();
   }
 
+  @NoHorizontalClassMerging
   @NoStaticClassMerging
   public static class Helper {
 
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassAllowRenamingDirect.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassAllowRenamingDirect.java
index de0e859..a333df7 100644
--- a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassAllowRenamingDirect.java
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassAllowRenamingDirect.java
@@ -5,8 +5,10 @@
 package com.android.tools.r8.repackage.testclasses.repackagetest;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoStaticClassMerging;
 
+@NoHorizontalClassMerging
 @NoStaticClassMerging
 public class AccessPackagePrivateMethodOnKeptClassAllowRenamingDirect {
 
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassAllowRenamingIndirect.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassAllowRenamingIndirect.java
index 13fef6e..040dd72 100644
--- a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassAllowRenamingIndirect.java
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassAllowRenamingIndirect.java
@@ -5,8 +5,10 @@
 package com.android.tools.r8.repackage.testclasses.repackagetest;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoStaticClassMerging;
 
+@NoHorizontalClassMerging
 @NoStaticClassMerging
 public class AccessPackagePrivateMethodOnKeptClassAllowRenamingIndirect {
 
@@ -15,6 +17,7 @@
     Helper.test();
   }
 
+  @NoHorizontalClassMerging
   @NoStaticClassMerging
   public static class Helper {
 
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassDirect.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassDirect.java
index fc92f85..a7bb32f 100644
--- a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassDirect.java
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassDirect.java
@@ -5,8 +5,10 @@
 package com.android.tools.r8.repackage.testclasses.repackagetest;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoStaticClassMerging;
 
+@NoHorizontalClassMerging
 @NoStaticClassMerging
 public class AccessPackagePrivateMethodOnKeptClassDirect {
 
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassIndirect.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassIndirect.java
index c30cd59..2319ca4 100644
--- a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassIndirect.java
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnKeptClassIndirect.java
@@ -5,9 +5,11 @@
 package com.android.tools.r8.repackage.testclasses.repackagetest;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoStaticClassMerging;
 import com.android.tools.r8.NoVerticalClassMerging;
 
+@NoHorizontalClassMerging
 @NoStaticClassMerging
 public class AccessPackagePrivateMethodOnKeptClassIndirect {
 
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnReachableClassDirect.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnReachableClassDirect.java
index 73f2d6e..07872c6b 100644
--- a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnReachableClassDirect.java
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnReachableClassDirect.java
@@ -5,8 +5,10 @@
 package com.android.tools.r8.repackage.testclasses.repackagetest;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoStaticClassMerging;
 
+@NoHorizontalClassMerging
 @NoStaticClassMerging
 public class AccessPackagePrivateMethodOnReachableClassDirect {
 
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnReachableClassIndirect.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnReachableClassIndirect.java
index 54cbea9..d528244 100644
--- a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnReachableClassIndirect.java
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPackagePrivateMethodOnReachableClassIndirect.java
@@ -5,8 +5,10 @@
 package com.android.tools.r8.repackage.testclasses.repackagetest;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoStaticClassMerging;
 
+@NoHorizontalClassMerging
 @NoStaticClassMerging
 public class AccessPackagePrivateMethodOnReachableClassIndirect {
 
@@ -15,6 +17,7 @@
     Helper.test();
   }
 
+  @NoHorizontalClassMerging
   @NoStaticClassMerging
   public static class Helper {
 
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicKeptMethodAllowRenamingOnReachableClass.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicKeptMethodAllowRenamingOnReachableClass.java
index cfb5aa4..d99a9b5 100644
--- a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicKeptMethodAllowRenamingOnReachableClass.java
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicKeptMethodAllowRenamingOnReachableClass.java
@@ -5,8 +5,10 @@
 package com.android.tools.r8.repackage.testclasses.repackagetest;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoStaticClassMerging;
 
+@NoHorizontalClassMerging
 @NoStaticClassMerging
 public class AccessPublicKeptMethodAllowRenamingOnReachableClass {
 
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicKeptMethodOnReachableClass.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicKeptMethodOnReachableClass.java
index 75d353c..8880ca5 100644
--- a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicKeptMethodOnReachableClass.java
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicKeptMethodOnReachableClass.java
@@ -5,8 +5,10 @@
 package com.android.tools.r8.repackage.testclasses.repackagetest;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoStaticClassMerging;
 
+@NoHorizontalClassMerging
 @NoStaticClassMerging
 public class AccessPublicKeptMethodOnReachableClass {
 
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnKeptClass.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnKeptClass.java
index b77f419..d3fa44b 100644
--- a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnKeptClass.java
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnKeptClass.java
@@ -5,8 +5,10 @@
 package com.android.tools.r8.repackage.testclasses.repackagetest;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoStaticClassMerging;
 
+@NoHorizontalClassMerging
 @NoStaticClassMerging
 public class AccessPublicMethodOnKeptClass {
 
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnKeptClassAllowRenaming.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnKeptClassAllowRenaming.java
index f71b585..9578cc7 100644
--- a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnKeptClassAllowRenaming.java
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnKeptClassAllowRenaming.java
@@ -5,8 +5,10 @@
 package com.android.tools.r8.repackage.testclasses.repackagetest;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoStaticClassMerging;
 
+@NoHorizontalClassMerging
 @NoStaticClassMerging
 public class AccessPublicMethodOnKeptClassAllowRenaming {
 
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnReachableClass.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnReachableClass.java
index 87ed71f..ec21ae7 100644
--- a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnReachableClass.java
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/AccessPublicMethodOnReachableClass.java
@@ -5,8 +5,10 @@
 package com.android.tools.r8.repackage.testclasses.repackagetest;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoStaticClassMerging;
 
+@NoHorizontalClassMerging
 @NoStaticClassMerging
 public class AccessPublicMethodOnReachableClass {
 
diff --git a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/ReachableClass.java b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/ReachableClass.java
index 10f12bf..22c5aa0 100644
--- a/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/ReachableClass.java
+++ b/src/test/java/com/android/tools/r8/repackage/testclasses/repackagetest/ReachableClass.java
@@ -5,8 +5,10 @@
 package com.android.tools.r8.repackage.testclasses.repackagetest;
 
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoStaticClassMerging;
 
+@NoHorizontalClassMerging
 @NoStaticClassMerging
 public class ReachableClass {
 
diff --git a/src/test/java/com/android/tools/r8/rewrite/enums/EnumInvalidValuesLengthTest.java b/src/test/java/com/android/tools/r8/rewrite/enums/EnumInvalidValuesLengthTest.java
new file mode 100644
index 0000000..ead99d1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/rewrite/enums/EnumInvalidValuesLengthTest.java
@@ -0,0 +1,74 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.rewrite.enums;
+
+import com.android.tools.r8.NeverClassInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.transformers.MethodTransformer;
+import java.io.IOException;
+import org.junit.Assume;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.objectweb.asm.Opcodes;
+
+@RunWith(Parameterized.class)
+public class EnumInvalidValuesLengthTest extends TestBase {
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public EnumInvalidValuesLengthTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testValuesLength() throws Exception {
+    Assume.assumeTrue(
+        "TODO(b/172903562): Breaks on dex due to enum unboxing", parameters.isCfRuntime());
+    testForR8(parameters.getBackend())
+        .addKeepMainRule(EnumInvalidValuesLengthTest.Main.class)
+        .addProgramClasses(EnumInvalidValuesLengthTest.Main.class)
+        .addProgramClassFileData(transformValues(MyEnum.class))
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), EnumInvalidValuesLengthTest.Main.class)
+        .assertSuccessWithOutputLines("5");
+  }
+
+  private byte[] transformValues(Class<MyEnum> myEnumClass) throws IOException {
+    return transformer(myEnumClass)
+        .addMethodTransformer(
+            new MethodTransformer() {
+              @Override
+              public void visitInsn(int opcode) {
+                if (opcode == Opcodes.ICONST_3) {
+                  // This is the constant determining the size of the values array.
+                  super.visitInsn(Opcodes.ICONST_5);
+                  return;
+                }
+                super.visitInsn(opcode);
+              }
+            })
+        .transform();
+  }
+
+  @NeverClassInline
+  enum MyEnum {
+    A,
+    B,
+    C;
+  }
+
+  public static class Main {
+    public static void main(String[] args) {
+      System.out.println(MyEnum.values().length);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/rewrite/enums/EnumValuesLengthTest.java b/src/test/java/com/android/tools/r8/rewrite/enums/EnumValuesLengthTest.java
deleted file mode 100644
index 176e6f5..0000000
--- a/src/test/java/com/android/tools/r8/rewrite/enums/EnumValuesLengthTest.java
+++ /dev/null
@@ -1,168 +0,0 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.rewrite.enums;
-
-import static junit.framework.TestCase.assertTrue;
-
-import com.android.tools.r8.NeverClassInline;
-import com.android.tools.r8.NeverInline;
-import com.android.tools.r8.TestBase;
-import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.ToolHelper;
-import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
-import com.android.tools.r8.utils.codeinspector.FoundMethodSubject;
-import com.android.tools.r8.utils.codeinspector.InstructionSubject;
-import java.io.IOException;
-import java.nio.file.Path;
-import java.util.Collection;
-import java.util.Collections;
-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 EnumValuesLengthTest extends TestBase {
-
-  private final TestParameters parameters;
-
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
-  }
-
-  public EnumValuesLengthTest(TestParameters parameters) {
-    this.parameters = parameters;
-  }
-
-  @Test
-  public void testValuesLengthRemoved() throws Exception {
-    testForR8(parameters.getBackend())
-        .addKeepMainRule(Main.class)
-        .addInnerClasses(EnumValuesLengthTest.class)
-        .noMinification()
-        .setMinApi(parameters.getApiLevel())
-        .addOptionsModification(
-            opt -> {
-              opt.enableEnumValueOptimization = true;
-              // We need to keep the switch map to ensure kept switch maps have their
-              // values array length rewritten.
-              opt.enableEnumSwitchMapRemoval = false;
-            })
-        .compile()
-        .inspect(this::assertValuesLengthRemoved)
-        .run(parameters.getRuntime(), Main.class)
-        .assertSuccessWithOutputLines("0", "2", "5", "a", "D", "c", "D");
-  }
-
-  @Test
-  public void testValuesLengthSwitchMapRemoved() throws Exception {
-    // Make sure SwitchMap can still be removed with valuesLength optimization.
-    assertSwitchMapPresent();
-    testForR8(parameters.getBackend())
-        .addKeepMainRule(Main.class)
-        .addInnerClasses(EnumValuesLengthTest.class)
-        .noMinification()
-        .setMinApi(parameters.getApiLevel())
-        .addOptionsModification(
-            opt -> {
-              opt.enableEnumValueOptimization = true;
-            })
-        .compile()
-        .inspect(this::assertSwitchMapRemoved)
-        .run(parameters.getRuntime(), Main.class)
-        .assertSuccessWithOutputLines("0", "2", "5", "a", "D", "c", "D");
-  }
-
-  private void assertSwitchMapPresent() throws IOException {
-    Collection<Path> classFilesForInnerClasses =
-        ToolHelper.getClassFilesForInnerClasses(
-            Collections.singletonList(EnumValuesLengthTest.class));
-    assertTrue(classFilesForInnerClasses.stream().anyMatch(p -> p.toString().endsWith("$1.class")));
-  }
-
-  private void assertSwitchMapRemoved(CodeInspector inspector) {
-    assertTrue(inspector.allClasses().stream().noneMatch(c -> c.getOriginalName().endsWith("$1")));
-  }
-
-  private void assertValuesLengthRemoved(CodeInspector inspector) {
-    for (FoundClassSubject clazz : inspector.allClasses()) {
-      clazz.forAllMethods(this::assertValuesLengthRemoved);
-    }
-  }
-
-  private void assertValuesLengthRemoved(FoundMethodSubject method) {
-    assertTrue(method.streamInstructions().noneMatch(InstructionSubject::isArrayLength));
-    assertTrue(
-        method
-            .streamInstructions()
-            .noneMatch(
-                instr ->
-                    instr.isInvokeStatic() && instr.getMethod().name.toString().equals("values")));
-  }
-
-  public static class Main {
-
-    @NeverClassInline
-    enum E0 {}
-
-    @NeverClassInline
-    enum E2 {
-      A,
-      B
-    }
-
-    @NeverClassInline
-    enum E5 {
-      A,
-      B,
-      C,
-      D,
-      E
-    }
-
-    @NeverClassInline
-    enum EUnusedValues {
-      A,
-      B,
-      C
-    }
-
-    @NeverClassInline
-    enum ESwitch {
-      A,
-      B,
-      C,
-      D
-    }
-
-    @SuppressWarnings("ResultOfMethodCallIgnored")
-    public static void main(String[] args) {
-      EUnusedValues.values();
-      System.out.println(E0.values().length);
-      System.out.println(E2.values().length);
-      System.out.println(E5.values().length);
-      System.out.println(switchOn(ESwitch.A));
-      System.out.println(switchOn(ESwitch.B));
-      System.out.println(switchOn(ESwitch.C));
-      System.out.println(switchOn(ESwitch.D));
-    }
-
-    // SwitchMaps feature an array length on values, and some of them are not removed.
-    @NeverInline
-    static char switchOn(ESwitch e) {
-      switch (e) {
-        case A:
-          return 'a';
-        case C:
-          return 'c';
-        default:
-          return 'D';
-      }
-    }
-  }
-}
diff --git a/src/test/java/com/android/tools/r8/shaking/EffectivelyFinalInstanceFieldsTest.java b/src/test/java/com/android/tools/r8/shaking/EffectivelyFinalInstanceFieldsTest.java
index 745e5b7..d408a4f 100644
--- a/src/test/java/com/android/tools/r8/shaking/EffectivelyFinalInstanceFieldsTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/EffectivelyFinalInstanceFieldsTest.java
@@ -11,6 +11,7 @@
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoVerticalClassMerging;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
@@ -54,6 +55,7 @@
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
+        .enableNoHorizontalClassMergingAnnotations()
         .setMinApi(parameters.getRuntime())
         .compile()
         .inspect(
@@ -155,6 +157,7 @@
 
   @NeverClassInline
   @NoVerticalClassMerging
+  @NoHorizontalClassMerging
   static class InstanceFieldWithInitialization_Z {
     boolean alwaysFalse;
     InstanceFieldWithInitialization_Z() {
@@ -163,6 +166,7 @@
   }
 
   @NeverClassInline
+  @NoHorizontalClassMerging
   static class InstanceFieldWithNonTrivialInitialization_Z
       extends InstanceFieldWithInitialization_Z {
     boolean alwaysTrue;
@@ -182,6 +186,7 @@
   }
 
   @NeverClassInline
+  @NoHorizontalClassMerging
   static class InstanceFieldWithInitialization_I {
     int alwaysZero;
     InstanceFieldWithInitialization_I() {
@@ -190,6 +195,7 @@
   }
 
   @NeverClassInline
+  @NoHorizontalClassMerging
   static class InstanceFieldWithRange_I {
     int alwaysLessThanEight;
     InstanceFieldWithRange_I() {
@@ -215,6 +221,7 @@
   }
 
   @NeverClassInline
+  @NoHorizontalClassMerging
   static class InstanceFieldWithInitialization_L {
     Object alwaysNull;
     InstanceFieldWithInitialization_L() {
diff --git a/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java b/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
index fd676a3..b5aba75 100644
--- a/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/KeepAnnotatedMemberTest.java
@@ -248,7 +248,7 @@
             .compile()
             .graphInspector();
     // TODO(b/159418523): Should the reference be equal to the result with the conditional rule?
-    assertRetainedClassesEqual(referenceInspector, ifHasMemberThenKeepClassInspector, true, false);
+    assertRetainedClassesEqual(referenceInspector, ifHasMemberThenKeepClassInspector, true, true);
   }
 
   private void assertRetainedClassesEqual(
diff --git a/src/test/java/com/android/tools/r8/shaking/attributes/KeepSignatureTest.java b/src/test/java/com/android/tools/r8/shaking/attributes/KeepSignatureTest.java
index 9dfff6e..ef2e860 100644
--- a/src/test/java/com/android/tools/r8/shaking/attributes/KeepSignatureTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/attributes/KeepSignatureTest.java
@@ -56,7 +56,8 @@
 
   @Test
   public void testKeptClassFieldAndMethodFull() throws Exception {
-    runTest(testForR8(parameters.getBackend()), false);
+    // TODO(b/172999267): The below should be true
+    runTest(testForR8(parameters.getBackend()), true);
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/shaking/b134858535/EventPublisherTest.java b/src/test/java/com/android/tools/r8/shaking/b134858535/EventPublisherTest.java
index 23a5616..8bde97b 100644
--- a/src/test/java/com/android/tools/r8/shaking/b134858535/EventPublisherTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/b134858535/EventPublisherTest.java
@@ -4,7 +4,6 @@
 
 package com.android.tools.r8.shaking.b134858535;
 
-import static org.hamcrest.CoreMatchers.containsString;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.TestBase;
@@ -28,13 +27,9 @@
         .addProgramClassFileData(EventPublisher$bDump.dump())
         .addKeepClassRules(Interface.class)
         .addKeepMainRule(Main.class)
-        .allowDiagnosticInfoMessages()
         .setMinApi(AndroidApiLevel.L)
-        .compile()
-        // TODO(b/157537996): Handle JStyle lambdas with private methods.
-        .assertAllInfoMessagesMatch(
-            containsString(
-                "Unrecognized Kotlin lambda"
-                    + " [com.android.tools.r8.shaking.b134858535.EventPublisher$b]"));
+        .addHorizontallyMergedLambdaClassesInspector(
+            inspector -> inspector.assertClassNotMerged(EventPublisher$b.class))
+        .compile();
   }
 }
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByInvokeStaticOnSubInterfaceTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByInvokeStaticOnSubInterfaceTest.java
index 943e680..fe71482 100644
--- a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByInvokeStaticOnSubInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByInvokeStaticOnSubInterfaceTest.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import org.junit.Test;
@@ -61,7 +62,11 @@
   @Test
   public void testClassInitializationMayHaveSideEffects() throws Exception {
     AppView<AppInfoWithLiveness> appView =
-        computeAppViewWithLiveness(buildInnerClasses(getClass()).build(), TestClass.class);
+        computeAppViewWithLiveness(
+            buildInnerClasses(getClass())
+                .addLibraryFile(ToolHelper.getMostRecentAndroidJar())
+                .build(),
+            TestClass.class);
     assertMayHaveClassInitializationSideEffects(appView, J.class);
   }
 
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByInvokeStaticTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByInvokeStaticTest.java
index a052428..e7a460d 100644
--- a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByInvokeStaticTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByInvokeStaticTest.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import org.junit.Test;
@@ -61,7 +62,11 @@
   @Test
   public void testClassInitializationMayHaveSideEffects() throws Exception {
     AppView<AppInfoWithLiveness> appView =
-        computeAppViewWithLiveness(buildInnerClasses(getClass()).build(), TestClass.class);
+        computeAppViewWithLiveness(
+            buildInnerClasses(getClass())
+                .addLibraryFile(ToolHelper.getMostRecentAndroidJar())
+                .build(),
+            TestClass.class);
     assertMayHaveClassInitializationSideEffects(appView, I.class);
   }
 
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByStaticGetOnSubClassTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByStaticGetOnSubClassTest.java
index d20f13a..ec1018f 100644
--- a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByStaticGetOnSubClassTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByStaticGetOnSubClassTest.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import org.junit.Test;
@@ -64,7 +65,11 @@
   @Test
   public void testClassInitializationMayHaveSideEffects() throws Exception {
     AppView<AppInfoWithLiveness> appView =
-        computeAppViewWithLiveness(buildInnerClasses(getClass()).build(), TestClass.class);
+        computeAppViewWithLiveness(
+            buildInnerClasses(getClass())
+                .addLibraryFile(ToolHelper.getMostRecentAndroidJar())
+                .build(),
+            TestClass.class);
     assertMayHaveClassInitializationSideEffects(appView, A.class);
   }
 
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByStaticGetOnSubInterfaceTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByStaticGetOnSubInterfaceTest.java
index bb59d8c..15b7662 100644
--- a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByStaticGetOnSubInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByStaticGetOnSubInterfaceTest.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import org.junit.Test;
@@ -64,7 +65,11 @@
   @Test
   public void testClassInitializationMayHaveSideEffects() throws Exception {
     AppView<AppInfoWithLiveness> appView =
-        computeAppViewWithLiveness(buildInnerClasses(getClass()).build(), TestClass.class);
+        computeAppViewWithLiveness(
+            buildInnerClasses(getClass())
+                .addLibraryFile(ToolHelper.getMostRecentAndroidJar())
+                .build(),
+            TestClass.class);
     assertMayHaveClassInitializationSideEffects(appView, J.class);
   }
 
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByStaticGetTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByStaticGetTest.java
index a3e93ae..65b4bcb 100644
--- a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByStaticGetTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceInitializedByStaticGetTest.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import org.junit.Test;
@@ -64,7 +65,11 @@
   @Test
   public void testClassInitializationMayHaveSideEffects() throws Exception {
     AppView<AppInfoWithLiveness> appView =
-        computeAppViewWithLiveness(buildInnerClasses(getClass()).build(), TestClass.class);
+        computeAppViewWithLiveness(
+            buildInnerClasses(getClass())
+                .addLibraryFile(ToolHelper.getMostRecentAndroidJar())
+                .build(),
+            TestClass.class);
     assertMayHaveClassInitializationSideEffects(appView, I.class);
   }
 
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceNotInitializedByInvokeStaticOnSubClassTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceNotInitializedByInvokeStaticOnSubClassTest.java
index 385b549..d73e875 100644
--- a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceNotInitializedByInvokeStaticOnSubClassTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceNotInitializedByInvokeStaticOnSubClassTest.java
@@ -9,6 +9,7 @@
 
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import org.junit.Test;
@@ -66,9 +67,12 @@
   @Test
   public void testClassInitializationMayHaveSideEffects() throws Exception {
     AppView<AppInfoWithLiveness> appView =
-        computeAppViewWithLiveness(buildInnerClasses(getClass()).build(), TestClass.class);
-    // TODO(b/172049649): Initialization of A does not have side effects.
-    assertMayHaveClassInitializationSideEffects(appView, A.class);
+        computeAppViewWithLiveness(
+            buildInnerClasses(getClass())
+                .addLibraryFile(ToolHelper.getMostRecentAndroidJar())
+                .build(),
+            TestClass.class);
+    assertNoClassInitializationSideEffects(appView, A.class);
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceWithDefaultMethodInitializedByInvokeStaticOnSubClassTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceWithDefaultMethodInitializedByInvokeStaticOnSubClassTest.java
index c69a6d0..18ce5f2 100644
--- a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceWithDefaultMethodInitializedByInvokeStaticOnSubClassTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceWithDefaultMethodInitializedByInvokeStaticOnSubClassTest.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import org.junit.Test;
@@ -80,7 +81,11 @@
   @Test
   public void testClassInitializationMayHaveSideEffects() throws Exception {
     AppView<AppInfoWithLiveness> appView =
-        computeAppViewWithLiveness(buildInnerClasses(getClass()).build(), TestClass.class);
+        computeAppViewWithLiveness(
+            buildInnerClasses(getClass())
+                .addLibraryFile(ToolHelper.getMostRecentAndroidJar())
+                .build(),
+            TestClass.class);
     assertMayHaveClassInitializationSideEffects(appView, A.class);
   }
 
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceWithDefaultMethodInitializedByInvokeStaticOnSubInterfaceTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceWithDefaultMethodInitializedByInvokeStaticOnSubInterfaceTest.java
new file mode 100644
index 0000000..edcfaf0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceWithDefaultMethodInitializedByInvokeStaticOnSubInterfaceTest.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.clinit;
+
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class InterfaceWithDefaultMethodInitializedByInvokeStaticOnSubInterfaceTest
+    extends ClassMayHaveInitializationSideEffectsTestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public InterfaceWithDefaultMethodInitializedByInvokeStaticOnSubInterfaceTest(
+      TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assumeTrue(parameters.isDexRuntime());
+    testForD8()
+        .addInnerClasses(getClass())
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(TestClass.class)
+        .allowStdoutMessages()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeTrue(parameters.isCfRuntime());
+    testForJvm()
+        .addTestClasspath()
+        .run(parameters.getRuntime(), TestClass.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  @Test
+  public void testClassInitializationMayHaveSideEffects() throws Exception {
+    AppView<AppInfoWithLiveness> appView =
+        computeAppViewWithLiveness(
+            buildInnerClasses(getClass())
+                .addLibraryFile(ToolHelper.getMostRecentAndroidJar())
+                .build(),
+            TestClass.class);
+    assertNoClassInitializationSideEffects(appView, J.class);
+  }
+
+  static class TestClass {
+
+    public static void main(String[] args) {
+      J.greet();
+    }
+  }
+
+  interface I {
+
+    Greeter iGreeter = new Greeter("I");
+
+    default void m() {}
+  }
+
+  interface J extends I {
+
+    static void greet() {}
+  }
+
+  static class Greeter {
+
+    Greeter(String greeting) {
+      System.out.println(greeting);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceWithDefaultMethodInitializedByStaticGetOnSubClassTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceWithDefaultMethodInitializedByStaticGetOnSubClassTest.java
index a31b2ed..96f64f8 100644
--- a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceWithDefaultMethodInitializedByStaticGetOnSubClassTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceWithDefaultMethodInitializedByStaticGetOnSubClassTest.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import org.junit.Test;
@@ -79,7 +80,11 @@
   @Test
   public void testClassInitializationMayHaveSideEffects() throws Exception {
     AppView<AppInfoWithLiveness> appView =
-        computeAppViewWithLiveness(buildInnerClasses(getClass()).build(), TestClass.class);
+        computeAppViewWithLiveness(
+            buildInnerClasses(getClass())
+                .addLibraryFile(ToolHelper.getMostRecentAndroidJar())
+                .build(),
+            TestClass.class);
     assertMayHaveClassInitializationSideEffects(appView, A.class);
   }
 
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceWithDefaultMethodInitializedByStaticGetOnSubInterfaceTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceWithDefaultMethodInitializedByStaticGetOnSubInterfaceTest.java
index 1977be8..a751dc3 100644
--- a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceWithDefaultMethodInitializedByStaticGetOnSubInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceWithDefaultMethodInitializedByStaticGetOnSubInterfaceTest.java
@@ -8,6 +8,7 @@
 
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import org.junit.Test;
@@ -65,7 +66,11 @@
   @Test
   public void testClassInitializationMayHaveSideEffects() throws Exception {
     AppView<AppInfoWithLiveness> appView =
-        computeAppViewWithLiveness(buildInnerClasses(getClass()).build(), TestClass.class);
+        computeAppViewWithLiveness(
+            buildInnerClasses(getClass())
+                .addLibraryFile(ToolHelper.getMostRecentAndroidJar())
+                .build(),
+            TestClass.class);
     assertMayHaveClassInitializationSideEffects(appView, J.class);
   }
 
diff --git a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceWithDefaultMethodNotInitializedByInvokeStaticOnSubInterfaceTest.java b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceWithDefaultMethodNotInitializedByInvokeStaticOnSubInterfaceTest.java
index 3d88fe8..4bb1e9c 100644
--- a/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceWithDefaultMethodNotInitializedByInvokeStaticOnSubInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/clinit/InterfaceWithDefaultMethodNotInitializedByInvokeStaticOnSubInterfaceTest.java
@@ -9,6 +9,7 @@
 
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import org.junit.Test;
@@ -67,9 +68,12 @@
   @Test
   public void testClassInitializationMayHaveSideEffects() throws Exception {
     AppView<AppInfoWithLiveness> appView =
-        computeAppViewWithLiveness(buildInnerClasses(getClass()).build(), TestClass.class);
-    // TODO(b/172049649): The initialization of J does not trigger the <clinit> of I.
-    assertMayHaveClassInitializationSideEffects(appView, J.class);
+        computeAppViewWithLiveness(
+            buildInnerClasses(getClass())
+                .addLibraryFile(ToolHelper.getMostRecentAndroidJar())
+                .build(),
+            TestClass.class);
+    assertNoClassInitializationSideEffects(appView, J.class);
   }
 
   static class TestClass {
diff --git a/src/test/java/com/android/tools/r8/shaking/testrules/A.java b/src/test/java/com/android/tools/r8/shaking/testrules/A.java
index 4536cd9..1387558 100644
--- a/src/test/java/com/android/tools/r8/shaking/testrules/A.java
+++ b/src/test/java/com/android/tools/r8/shaking/testrules/A.java
@@ -4,8 +4,10 @@
 
 package com.android.tools.r8.shaking.testrules;
 
+import com.android.tools.r8.NoHorizontalClassMerging;
 import com.android.tools.r8.NoStaticClassMerging;
 
+@NoHorizontalClassMerging
 @NoStaticClassMerging
 public class A {
 
diff --git a/src/test/java/com/android/tools/r8/shaking/testrules/ForceInlineTest.java b/src/test/java/com/android/tools/r8/shaking/testrules/ForceInlineTest.java
index 41533ec..d4e5972 100644
--- a/src/test/java/com/android/tools/r8/shaking/testrules/ForceInlineTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/testrules/ForceInlineTest.java
@@ -42,6 +42,7 @@
     return testForR8(parameters.getBackend())
         .addProgramClasses(Main.class, A.class, B.class, C.class)
         .addKeepRules(proguardConfiguration)
+        .enableNoHorizontalClassMergingAnnotations()
         .enableNoStaticClassMergingAnnotations()
         .enableProguardTestOptions()
         .compile()
diff --git a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesArrayTypesTest.java b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesArrayTypesTest.java
index 0074a89..a1b4180 100644
--- a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesArrayTypesTest.java
+++ b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesArrayTypesTest.java
@@ -8,6 +8,7 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.DiagnosticsChecker;
+import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -41,13 +42,13 @@
     boolean acceptMethodCalled;
 
     @Override
-    public void acceptType(TracedClass tracedClass) {
+    public void acceptType(TracedClass tracedClass, DiagnosticsHandler handler) {
       assertFalse(tracedClass.isMissingDefinition());
       tracedTypes.add(tracedClass.getReference());
     }
 
     @Override
-    public void acceptField(TracedField tracedField) {
+    public void acceptField(TracedField tracedField, DiagnosticsHandler handler) {
       acceptFieldCalled = true;
       assertFalse(tracedField.isMissingDefinition());
       assertEquals(
@@ -57,7 +58,7 @@
     }
 
     @Override
-    public void acceptMethod(TracedMethod tracedMethod) {
+    public void acceptMethod(TracedMethod tracedMethod, DiagnosticsHandler handler) {
       acceptMethodCalled = true;
       assertFalse(tracedMethod.isMissingDefinition());
       assertEquals(
diff --git a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesCommandTest.java b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesCommandTest.java
index 77c1e53..d354df7 100644
--- a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesCommandTest.java
+++ b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesCommandTest.java
@@ -9,24 +9,27 @@
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.DiagnosticsChecker;
+import com.android.tools.r8.DiagnosticsHandler;
+import com.android.tools.r8.StringConsumer;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.references.Reference;
-import com.android.tools.r8.tracereferences.TraceReferencesFormattingConsumer.OutputFormat;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.StringUtils;
-import com.android.tools.r8.utils.ZipUtils;
 import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
 import com.google.common.collect.ImmutableList;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
+import java.io.PrintStream;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.nio.file.Path;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
@@ -62,10 +65,21 @@
   @Test(expected = CompilationFailedException.class)
   public void emptyRunCommandLine() throws Throwable {
     DiagnosticsChecker.checkErrorsContains(
-        "No library specified",
+        "Missing command",
         handler -> {
           TraceReferences.run(
-              TraceReferencesCommand.parse(new String[] {""}, Origin.unknown(), handler).build());
+              TraceReferencesCommand.parse(new String[] {}, Origin.unknown(), handler).build());
+        });
+  }
+
+  @Test(expected = CompilationFailedException.class)
+  public void unsupportedCommandCommandLine() throws Throwable {
+    DiagnosticsChecker.checkErrorsContains(
+        "Missing command, specify one of 'check', '--print-usage' or '--keep-rules'",
+        handler -> {
+          TraceReferences.run(
+              TraceReferencesCommand.parse(new String[] {"--xxx"}, Origin.unknown(), handler)
+                  .build());
         });
   }
 
@@ -89,7 +103,7 @@
           TraceReferences.run(
               TraceReferencesCommand.parse(
                       new String[] {
-                        "--lib", ToolHelper.getAndroidJar(AndroidApiLevel.P).toString()
+                        "--check", "--lib", ToolHelper.getAndroidJar(AndroidApiLevel.P).toString()
                       },
                       Origin.unknown(),
                       handler)
@@ -98,24 +112,39 @@
   }
 
   @Test(expected = CompilationFailedException.class)
-  public void invalidFormatCommandLine() throws Throwable {
+  public void multipleCommandsSpecified() throws Throwable {
     DiagnosticsChecker.checkErrorsContains(
-        "Unsupported format 'xxx'",
+        "Multiple commands specified",
         handler -> {
           TraceReferences.run(
               TraceReferencesCommand.parse(
-                      new String[] {"--format", "xxx"}, Origin.unknown(), handler)
+                      new String[] {"--check", "--keep-rules"}, Origin.unknown(), handler)
                   .build());
         });
   }
 
   @Test(expected = CompilationFailedException.class)
-  public void missingFormatCommandLine() throws Throwable {
+  public void allowobfuscationWithoutKeepRule() throws Throwable {
     DiagnosticsChecker.checkErrorsContains(
-        "Missing parameter for --format",
+        "Using '--allowobfuscation' requires command '--keep-rules'",
         handler -> {
           TraceReferences.run(
-              TraceReferencesCommand.parse(new String[] {"--format"}, Origin.unknown(), handler)
+              TraceReferencesCommand.parse(
+                      new String[] {"--check", "--allowobfuscation"}, Origin.unknown(), handler)
+                  .build());
+        });
+  }
+
+  @Test(expected = CompilationFailedException.class)
+  public void allowobfuscationMultiple() throws Throwable {
+    DiagnosticsChecker.checkErrorsContains(
+        "No library specified",
+        handler -> {
+          TraceReferences.run(
+              TraceReferencesCommand.parse(
+                      new String[] {"--keep-rules", "--allowobfuscation", "--allowobfuscation"},
+                      Origin.unknown(),
+                      handler)
                   .build());
         });
   }
@@ -123,11 +152,23 @@
   @Test(expected = CompilationFailedException.class)
   public void multipleFormatsCommandLine() throws Throwable {
     DiagnosticsChecker.checkErrorsContains(
-        "--format specified multiple times",
+        "Using '--output' requires command '--print-usage' or '--keep-rules'",
         handler -> {
           TraceReferences.run(
               TraceReferencesCommand.parse(
-                      new String[] {"--format", "printuses", "--format", "keep"},
+                      new String[] {"--check", "--output", "xxx"}, Origin.unknown(), handler)
+                  .build());
+        });
+  }
+
+  @Test(expected = CompilationFailedException.class)
+  public void outputMultiple() throws Throwable {
+    DiagnosticsChecker.checkErrorsContains(
+        "Option '--output' passed multiple times",
+        handler -> {
+          TraceReferences.run(
+              TraceReferencesCommand.parse(
+                      new String[] {"--keep-rules", "--output", "xxx", "--output", "xxx"},
                       Origin.unknown(),
                       handler)
                   .build());
@@ -136,13 +177,44 @@
 
   private String formatName(OutputFormat format) {
     if (format == OutputFormat.PRINTUSAGE) {
-      return "printuses";
+      return "--print-usage";
     }
     if (format == OutputFormat.KEEP_RULES) {
-      return "keep";
+      return "--keep-rules";
     }
     assertSame(format, OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION);
-    return "keepallowobfuscation";
+    return "--keep-rules";
+  }
+
+  enum OutputFormat {
+    /** Format used with the -printusage flag */
+    PRINTUSAGE,
+    /** Keep rules keeping each of the traced references */
+    KEEP_RULES,
+    /**
+     * Keep rules with <code>allowobfuscation</code> modifier keeping each of the traced references
+     */
+    KEEP_RULES_WITH_ALLOWOBFUSCATION
+  }
+
+  private static class StringValueStringConsumer implements StringConsumer {
+    private StringBuilder builder = new StringBuilder();
+    private boolean finished = false;
+
+    @Override
+    public void accept(String string, DiagnosticsHandler handler) {
+      builder.append(string);
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      finished = true;
+    }
+
+    String get() {
+      assert finished;
+      return builder.toString();
+    }
   }
 
   public void runAndCheckOutput(
@@ -155,7 +227,14 @@
     Path dir = temp.newFolder().toPath();
     Path output = dir.resolve("output.txt");
     DiagnosticsChecker diagnosticsChecker = new DiagnosticsChecker();
-    TraceReferencesFormattingConsumer consumer = new TraceReferencesFormattingConsumer(format);
+    StringValueStringConsumer stringConsumer = new StringValueStringConsumer();
+    TraceReferencesConsumer consumer =
+        format == OutputFormat.PRINTUSAGE
+            ? new TraceReferencesPrintUsage()
+            : TraceReferencesKeepRules.builder()
+                .setAllowObfuscation(format == OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION)
+                .setOutputConsumer(stringConsumer)
+                .build();
     try {
       TraceReferences.run(
           TraceReferencesCommand.builder(diagnosticsChecker)
@@ -164,7 +243,11 @@
               .addSourceFiles(sourceJar)
               .setConsumer(consumer)
               .build());
-      assertEquals(expected, consumer.get());
+      if (consumer instanceof TraceReferencesPrintUsage) {
+        assertEquals(expected, ((TraceReferencesPrintUsage) consumer).get());
+      } else {
+        assertEquals(expected, stringConsumer.get());
+      }
       if (diagnosticsCheckerConsumer != null) {
         diagnosticsCheckerConsumer.accept(diagnosticsChecker);
       } else {
@@ -179,17 +262,23 @@
       throw e;
     }
 
-    TraceReferences.run(
-        TraceReferencesCommand.parse(
-                new String[] {
-                  "--lib", ToolHelper.getAndroidJar(AndroidApiLevel.P).toString(),
-                  "--target", targetJar.toString(),
-                  "--source", sourceJar.toString(),
-                  "--output", output.toString(),
-                  "--format", formatName(format)
-                },
-                Origin.unknown())
-            .build());
+    List<String> args = new ArrayList<>();
+    args.add(formatName(format));
+    if (format == OutputFormat.KEEP_RULES_WITH_ALLOWOBFUSCATION) {
+      args.add("--allowobfuscation");
+    }
+    args.addAll(
+        ImmutableList.of(
+            "--lib",
+            ToolHelper.getAndroidJar(AndroidApiLevel.P).toString(),
+            "--target",
+            targetJar.toString(),
+            "--source",
+            sourceJar.toString(),
+            "--output",
+            output.toString()));
+
+    TraceReferences.run(TraceReferencesCommand.parse(args, Origin.unknown()).build());
     assertEquals(expected, FileUtils.readTextFile(output, Charsets.UTF_8));
   }
 
@@ -202,6 +291,16 @@
     runAndCheckOutput(targetClasses, sourceClasses, format, expected, null);
   }
 
+  private Path zipWithTestClasses(Path zipFile, List<Class<?>> targetClasses) throws IOException {
+    return ZipBuilder.builder(zipFile)
+        .addFilesRelative(
+            ToolHelper.getClassPathForTests(),
+            targetClasses.stream()
+                .map(ToolHelper::getClassFileForTestClass)
+                .collect(Collectors.toList()))
+        .build();
+  }
+
   public void runAndCheckOutput(
       List<Class<?>> targetClasses,
       List<Class<?>> sourceClasses,
@@ -210,22 +309,8 @@
       Consumer<DiagnosticsChecker> diagnosticsCheckerConsumer)
       throws Throwable {
     Path dir = temp.newFolder().toPath();
-    Path targetJar =
-        ZipBuilder.builder(dir.resolve("target.jar"))
-            .addFilesRelative(
-                ToolHelper.getClassPathForTests(),
-                targetClasses.stream()
-                    .map(ToolHelper::getClassFileForTestClass)
-                    .collect(Collectors.toList()))
-            .build();
-    Path sourceJar =
-        ZipBuilder.builder(dir.resolve("source.jar"))
-            .addFilesRelative(
-                ToolHelper.getClassPathForTests(),
-                sourceClasses.stream()
-                    .map(ToolHelper::getClassFileForTestClass)
-                    .collect(Collectors.toList()))
-            .build();
+    Path targetJar = zipWithTestClasses(dir.resolve("target.jar"), targetClasses);
+    Path sourceJar = zipWithTestClasses(dir.resolve("source.jar"), sourceClasses);
     runAndCheckOutput(targetJar, sourceJar, format, expected, diagnosticsCheckerConsumer);
   }
 
@@ -276,6 +361,118 @@
                 "-keeppackagenames com.android.tools.r8.tracereferences")));
   }
 
+  @Test
+  public void test_noOutput() throws Throwable {
+    Path dir = temp.newFolder().toPath();
+    Path targetJar = zipWithTestClasses(dir.resolve("target.jar"), ImmutableList.of(Target.class));
+    Path sourceJar = zipWithTestClasses(dir.resolve("source.jar"), ImmutableList.of(Source.class));
+    PrintStream originalOut = System.out;
+    try {
+      ByteArrayOutputStream baos = new ByteArrayOutputStream();
+      System.setOut(new PrintStream(baos));
+      TraceReferences.run(
+          TraceReferencesCommand.builder()
+              .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
+              .addTargetFiles(targetJar)
+              .addSourceFiles(sourceJar)
+              .setConsumer(TraceReferencesConsumer.emptyConsumer())
+              .build());
+      assertEquals(0, baos.size());
+    } finally {
+      System.setOut(originalOut);
+    }
+
+    try {
+      ByteArrayOutputStream baos = new ByteArrayOutputStream();
+      System.setOut(new PrintStream(baos));
+      TraceReferences.run(
+          TraceReferencesCommand.parse(
+                  new String[] {
+                    "--lib",
+                    ToolHelper.getAndroidJar(AndroidApiLevel.P).toString(),
+                    "--check",
+                    "--target",
+                    targetJar.toString(),
+                    "--source",
+                    sourceJar.toString(),
+                  },
+                  Origin.unknown())
+              .build());
+      assertEquals(0, baos.size());
+    } finally {
+      System.setOut(originalOut);
+    }
+  }
+
+  @Test
+  public void test_stdoutOutput() throws Throwable {
+    String expected =
+        StringUtils.lines(
+            ImmutableList.of(
+                "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target",
+                "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target: void"
+                    + " method(int)",
+                "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target: int"
+                    + " field"));
+    Path dir = temp.newFolder().toPath();
+    Path targetJar = zipWithTestClasses(dir.resolve("target.jar"), ImmutableList.of(Target.class));
+    Path sourceJar = zipWithTestClasses(dir.resolve("source.jar"), ImmutableList.of(Source.class));
+    PrintStream originalOut = System.out;
+    try {
+      ByteArrayOutputStream baos = new ByteArrayOutputStream();
+      System.setOut(new PrintStream(baos));
+      TraceReferences.run(
+          TraceReferencesCommand.parse(
+                  new String[] {
+                    "--lib",
+                    ToolHelper.getAndroidJar(AndroidApiLevel.P).toString(),
+                    "--target",
+                    targetJar.toString(),
+                    "--source",
+                    sourceJar.toString(),
+                    "--print-usage",
+                  },
+                  Origin.unknown())
+              .build());
+      assertEquals(expected, baos.toString(Charsets.UTF_8.name()));
+    } finally {
+      System.setOut(originalOut);
+    }
+  }
+
+  public void classFileInput() throws Throwable {
+    String expected =
+        StringUtils.lines(
+            ImmutableList.of(
+                "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target",
+                "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target: void"
+                    + " method(int)",
+                "com.android.tools.r8.tracereferences.TraceReferencesCommandTest$Target: int"
+                    + " field"));
+    TraceReferencesPrintUsage consumer = new TraceReferencesPrintUsage();
+    TraceReferences.run(
+        TraceReferencesCommand.builder()
+            .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
+            .addTargetFiles(ToolHelper.getClassFileForTestClass(Target.class))
+            .addSourceFiles(ToolHelper.getClassFileForTestClass(Source.class))
+            .setConsumer(consumer)
+            .build());
+    assertEquals(expected, consumer.get());
+
+    Path output = temp.newFile().toPath();
+    TraceReferences.run(
+        TraceReferencesCommand.parse(
+                new String[] {
+                  "--lib", ToolHelper.getAndroidJar(AndroidApiLevel.P).toString(),
+                  "--target", ToolHelper.getClassFileForTestClass(Target.class).toString(),
+                  "--source", ToolHelper.getClassFileForTestClass(Source.class).toString(),
+                  "--output", output.toString(),
+                },
+                Origin.unknown())
+            .build());
+    assertEquals(expected, FileUtils.readTextFile(output, Charsets.UTF_8));
+  }
+
   private void checkTargetMissing(DiagnosticsChecker diagnosticsChecker) {
     Field field;
     Method method;
@@ -344,32 +541,19 @@
   public void testMissingReference_errorToWarning() throws Throwable {
     Path dir = temp.newFolder().toPath();
     Path targetJar =
-        ZipBuilder.builder(dir.resolve("target.jar"))
-            .addFilesRelative(
-                ToolHelper.getClassPathForTests(),
-                ToolHelper.getClassFileForTestClass(OtherTarget.class))
-            .build();
-    Path sourceJar =
-        ZipBuilder.builder(dir.resolve("source.jar"))
-            .addFilesRelative(
-                ToolHelper.getClassPathForTests(),
-                ToolHelper.getClassFileForTestClass(Source.class))
-            .build();
-    Path output = dir.resolve("output.txt");
+        zipWithTestClasses(dir.resolve("target.jar"), ImmutableList.of(OtherTarget.class));
+    Path sourceJar = zipWithTestClasses(dir.resolve("source.jar"), ImmutableList.of(Source.class));
     DiagnosticsChecker diagnosticsChecker = new DiagnosticsChecker();
     TraceReferences.run(
         TraceReferencesCommand.parse(
                 new String[] {
+                  "--check",
                   "--lib",
                   ToolHelper.getAndroidJar(AndroidApiLevel.P).toString(),
                   "--target",
                   targetJar.toString(),
                   "--source",
                   sourceJar.toString(),
-                  "--output",
-                  output.toString(),
-                  "--format",
-                  formatName(OutputFormat.PRINTUSAGE),
                   "--map-diagnostics:MissingDefinitionsDiagnostic",
                   "error",
                   "warning"
@@ -407,12 +591,7 @@
             .addBytes(
                 DescriptorUtils.getPathFromJavaType(Target.class), getClassWithTargetRemoved())
             .build();
-    Path sourceJar =
-        ZipBuilder.builder(dir.resolve("source.jar"))
-            .addFilesRelative(
-                ToolHelper.getClassPathForTests(),
-                ToolHelper.getClassFileForTestClass(Source.class))
-            .build();
+    Path sourceJar = zipWithTestClasses(dir.resolve("source.jar"), ImmutableList.of(Source.class));
     try {
       runAndCheckOutput(
           targetJar,
@@ -436,16 +615,7 @@
             .addBytes(
                 DescriptorUtils.getPathFromJavaType(Target.class), getClassWithTargetRemoved())
             .build();
-    Path sourceJar =
-        ZipBuilder.builder(dir.resolve("source.jar"))
-            .addFilesRelative(
-                ToolHelper.getClassPathForTests(),
-                ToolHelper.getClassFileForTestClass(Source.class))
-            .build();
-    ZipUtils.zip(
-        sourceJar,
-        ToolHelper.getClassPathForTests(),
-        ToolHelper.getClassFileForTestClass(Source.class));
+    Path sourceJar = zipWithTestClasses(dir.resolve("source.jar"), ImmutableList.of(Source.class));
     try {
       runAndCheckOutput(
           targetJar,
@@ -473,12 +643,7 @@
             .addBytes(
                 DescriptorUtils.getPathFromJavaType(Target.class), getClassWithTargetRemoved())
             .build();
-    Path sourceJar =
-        ZipBuilder.builder(dir.resolve("source.jar"))
-            .addFilesRelative(
-                ToolHelper.getClassPathForTests(),
-                ToolHelper.getClassFileForTestClass(Source.class))
-            .build();
+    Path sourceJar = zipWithTestClasses(dir.resolve("source.jar"), ImmutableList.of(Source.class));
     try {
       runAndCheckOutput(
           targetJar,
diff --git a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesDiagnosticTest.java b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesDiagnosticTest.java
index b21d409..551c8a1 100644
--- a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesDiagnosticTest.java
+++ b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesDiagnosticTest.java
@@ -7,12 +7,14 @@
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.DiagnosticsChecker;
+import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.StringDiagnostic;
 import com.android.tools.r8.utils.StringUtils;
 import com.android.tools.r8.utils.ZipUtils.ZipBuilder;
 import com.google.common.collect.ImmutableList;
@@ -193,6 +195,80 @@
     }
   }
 
+  static class FailingConsumer implements TraceReferencesConsumer {
+    private final String where;
+
+    FailingConsumer(String where) {
+      this.where = where;
+    }
+
+    @Override
+    public void acceptType(TracedClass tracedClass, DiagnosticsHandler handler) {
+      if (where.equals("acceptType")) {
+        handler.error(new StringDiagnostic("Error in " + where));
+      }
+    }
+
+    @Override
+    public void acceptField(TracedField tracedField, DiagnosticsHandler handler) {
+      if (where.equals("acceptField")) {
+        handler.error(new StringDiagnostic("Error in " + where));
+      }
+    }
+
+    @Override
+    public void acceptMethod(TracedMethod tracedMethod, DiagnosticsHandler handler) {
+      if (where.equals("acceptMethod")) {
+        handler.error(new StringDiagnostic("Error in " + where));
+      }
+    }
+
+    @Override
+    public void finished(DiagnosticsHandler handler) {
+      if (where.equals("finished")) {
+        handler.error(new StringDiagnostic("Error in " + where));
+      }
+    }
+  }
+
+  @Test
+  public void traceReferencesConsumerError() throws Throwable {
+    Path dir = temp.newFolder().toPath();
+    Path targetJar =
+        ZipBuilder.builder(dir.resolve("target.jar"))
+            .addFilesRelative(
+                ToolHelper.getClassPathForTests(),
+                ToolHelper.getClassFileForTestClass(Target.class),
+                ToolHelper.getClassFileForTestClass(Target1.class),
+                ToolHelper.getClassFileForTestClass(Target2.class),
+                ToolHelper.getClassFileForTestClass(Target3.class))
+            .build();
+    Path sourceJar =
+        ZipBuilder.builder(dir.resolve("source.jar"))
+            .addFilesRelative(
+                ToolHelper.getClassPathForTests(),
+                ToolHelper.getClassFileForTestClass(Source.class))
+            .build();
+
+    for (String where : new String[] {"acceptType", "acceptField", "acceptMethod", "finished"}) {
+      try {
+        DiagnosticsChecker.checkErrorsContains(
+            "Error in " + where,
+            handler ->
+                TraceReferences.run(
+                    TraceReferencesCommand.builder(handler)
+                        .addLibraryFiles(ToolHelper.getAndroidJar(AndroidApiLevel.P))
+                        .addSourceFiles(sourceJar)
+                        .addTargetFiles(targetJar)
+                        .setConsumer(new FailingConsumer(where))
+                        .build()));
+        fail("Unexpected success");
+      } catch (CompilationFailedException e) {
+        // Expected.
+      }
+    }
+  }
+
   static class Target1 {}
 
   static class Target2 {}
diff --git a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesMissingReferencesInDexTest.java b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesMissingReferencesInDexTest.java
index 1387c3f..b5b26b0 100644
--- a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesMissingReferencesInDexTest.java
+++ b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesMissingReferencesInDexTest.java
@@ -10,6 +10,7 @@
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.DiagnosticsChecker;
+import com.android.tools.r8.DiagnosticsHandler;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -39,14 +40,14 @@
     boolean acceptMethodCalled;
 
     @Override
-    public void acceptType(TracedClass tracedClass) {
+    public void acceptType(TracedClass tracedClass, DiagnosticsHandler handler) {
       acceptTypeCalled = true;
       assertEquals(Reference.classFromClass(Target.class), tracedClass.getReference());
       assertTrue(tracedClass.isMissingDefinition());
     }
 
     @Override
-    public void acceptField(TracedField tracedField) {
+    public void acceptField(TracedField tracedField, DiagnosticsHandler handler) {
       acceptFieldCalled = true;
       assertEquals(
           Reference.classFromClass(Target.class), tracedField.getReference().getHolderClass());
@@ -55,7 +56,7 @@
     }
 
     @Override
-    public void acceptMethod(TracedMethod tracedMethod) {
+    public void acceptMethod(TracedMethod tracedMethod, DiagnosticsHandler handler) {
       acceptMethodCalled = true;
       assertEquals(
           Reference.classFromClass(Target.class), tracedMethod.getReference().getHolderClass());
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java
index a440523..0d99817 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedClassesInspector.java
@@ -10,7 +10,10 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
+import java.util.Set;
+import java.util.function.BiConsumer;
 
 public class HorizontallyMergedClassesInspector {
 
@@ -23,6 +26,14 @@
     this.horizontallyMergedClasses = horizontallyMergedClasses;
   }
 
+  public void forEachMergeGroup(BiConsumer<Set<DexType>, DexType> consumer) {
+    horizontallyMergedClasses.forEachMergeGroup(consumer);
+  }
+
+  public DexType getTarget(DexType clazz) {
+    return horizontallyMergedClasses.getMergeTargetOrDefault(clazz);
+  }
+
   public HorizontallyMergedClassesInspector assertMergedInto(Class<?> from, Class<?> target) {
     assertEquals(
         horizontallyMergedClasses.getMergeTargetOrDefault(toDexType(from, dexItemFactory)),
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedLambdaClassesInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedLambdaClassesInspector.java
index 4b3579f..421b54a 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedLambdaClassesInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/HorizontallyMergedLambdaClassesInspector.java
@@ -5,10 +5,14 @@
 package com.android.tools.r8.utils.codeinspector;
 
 import static com.android.tools.r8.TestBase.toDexType;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.classmerging.HorizontallyMergedLambdaClasses;
+import java.util.Set;
+import java.util.function.BiConsumer;
 
 public class HorizontallyMergedLambdaClassesInspector {
 
@@ -33,4 +37,13 @@
     }
     return this;
   }
+
+  public HorizontallyMergedLambdaClassesInspector assertClassNotMerged(Class<?> clazz) {
+    assertFalse(horizontallyMergedLambdaClasses.hasBeenMerged(toDexType(clazz, dexItemFactory)));
+    return this;
+  }
+
+  public void forEachMergeGroup(BiConsumer<Set<DexType>, DexType> consumer) {
+    horizontallyMergedLambdaClasses.forEachMergeGroup(consumer);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/StaticallyMergedClassesInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/StaticallyMergedClassesInspector.java
new file mode 100644
index 0000000..0b45f27
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/StaticallyMergedClassesInspector.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.utils.codeinspector;
+
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.classmerging.StaticallyMergedClasses;
+import java.util.Set;
+import java.util.function.BiConsumer;
+
+public class StaticallyMergedClassesInspector {
+
+  private final DexItemFactory dexItemFactory;
+  private final StaticallyMergedClasses staticallyMergedClasses;
+
+  public StaticallyMergedClassesInspector(
+      DexItemFactory dexItemFactory, StaticallyMergedClasses staticallyMergedClasses) {
+    this.dexItemFactory = dexItemFactory;
+    this.staticallyMergedClasses = staticallyMergedClasses;
+  }
+
+  public void forEachMergeGroup(BiConsumer<Set<DexType>, DexType> consumer) {
+    staticallyMergedClasses.forEachMergeGroup(consumer);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/VerticallyMergedClassesInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/VerticallyMergedClassesInspector.java
index bd64711..05031f2 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/VerticallyMergedClassesInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/VerticallyMergedClassesInspector.java
@@ -32,4 +32,9 @@
     }
     return this;
   }
+
+  public VerticallyMergedClassesInspector assertNoClassesMerged() {
+    assertTrue(verticallyMergedClasses.isEmpty());
+    return this;
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/utils/structural/StructuralItemsCustomOrderTest.java b/src/test/java/com/android/tools/r8/utils/structural/StructuralItemsCustomOrderTest.java
new file mode 100644
index 0000000..cfea69c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/structural/StructuralItemsCustomOrderTest.java
@@ -0,0 +1,170 @@
+// 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.utils.structural;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.util.HashSet;
+import java.util.Set;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class StructuralItemsCustomOrderTest extends TestBase {
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public StructuralItemsCustomOrderTest(TestParameters parameters) {
+    parameters.assertNoneRuntime();
+  }
+
+  final B b1 = new B(1);
+  final B b2 = new B(2);
+  final B b2_copy = new B(2);
+
+  final A a1b1 = new A(1, b1);
+  final A a2b1 = new A(2, b1);
+  final A a1b2 = new A(1, b2);
+  final A a2b2 = new A(2, b2);
+  final A a1b2_copy = new A(1, b2_copy);
+  final A a2b2_copy = new A(2, b2_copy);
+
+  @Test
+  public void testOrder() {
+    assertFalse(b1.isLessThan(b2));
+    assertTrue(b2.isEqualTo(b2_copy));
+    assertTrue(b2.isLessThan(b1));
+
+    assertFalse(a1b1.isLessThan(a1b2));
+    assertTrue(a1b2.isLessThan(a2b1));
+    assertFalse(a2b1.isLessThan(a1b2_copy));
+  }
+
+  @Test
+  public void testOrderWithIdentityEquivalence() {
+    // These mirror the above exactly but using the compare result.
+    RepresentativeMap map = t -> t;
+
+    assertFalse(b1.compareWithTypeEquivalenceTo(b2, map) < 0);
+    assertTrue(b2.compareWithTypeEquivalenceTo(b2_copy, map) == 0);
+    assertTrue(b2.compareWithTypeEquivalenceTo(b1, map) < 0);
+
+    assertFalse(a1b1.compareWithTypeEquivalenceTo(a1b2, map) < 0);
+    assertTrue(a1b2.compareWithTypeEquivalenceTo(a2b1, map) < 0);
+    assertFalse(a2b1.compareWithTypeEquivalenceTo(a1b2_copy, map) < 0);
+  }
+
+  @Test
+  public void testEquals() {
+    assertFalse(b1.isEqualTo(b2));
+    assertTrue(b2.isEqualTo(b2_copy));
+    assertEquals(b2, b2_copy);
+
+    assertFalse(a2b2.isEqualTo(a2b1));
+    assertTrue(a1b2.isEqualTo(a1b2_copy));
+    assertEquals(a2b2, a2b2_copy);
+
+    // Type incompatible check should still work.
+    assertNotEquals(b1, a1b1);
+  }
+
+  @Test
+  public void testHashCode() {
+    Set<B> bs = new HashSet<>(ImmutableList.of(b1, b2, b2_copy));
+    assertEquals(ImmutableSet.of(b1, b2), bs);
+
+    Set<A> as = new HashSet<>(ImmutableList.of(a1b1, a1b2, a2b1, a2b2, a1b2_copy, a2b2_copy));
+    assertEquals(ImmutableSet.of(a1b1, a1b2, a2b1, a2b2), as);
+
+    // If these collide it is a poor hashing algorithm...
+    assertNotEquals(b1.hashCode(), b2.hashCode());
+  }
+
+  private static class A implements StructuralItem<A> {
+
+    private final int x;
+    private final B b;
+
+    private static void accept(StructuralSpecification<A, ?> spec) {
+      spec.withInt(a -> a.x).withItem(a -> a.b);
+    }
+
+    public A(int x, B b) {
+      this.x = x;
+      this.b = b;
+    }
+
+    @Override
+    public StructuralAccept<A> getStructuralAccept() {
+      return A::accept;
+    }
+
+    @Override
+    public A self() {
+      return this;
+    }
+
+    @Override
+    public final boolean equals(Object other) {
+      return Equatable.equalsImpl(this, other);
+    }
+
+    @Override
+    public final int hashCode() {
+      return HashCodeVisitor.run(this, A::accept);
+    }
+  }
+
+  private static class B implements StructuralItem<B> {
+
+    private final int y;
+
+    private static void accept(StructuralSpecification<B, ?> spec) {
+      spec.withInt(b -> b.y);
+    }
+
+    public B(int y) {
+      this.y = y;
+    }
+
+    @Override
+    public StructuralAccept<B> getStructuralAccept() {
+      return B::accept;
+    }
+
+    @Override
+    public B self() {
+      return this;
+    }
+
+    @Override
+    public final boolean equals(Object other) {
+      return Equatable.equalsImpl(this, other);
+    }
+
+    @Override
+    public final int hashCode() {
+      return HashCodeVisitor.run(this, B::accept);
+    }
+
+    // Override allowing a change to the order of any type of compare-to visitation, e.g., with
+    // and without a type equivalence map.
+    @Override
+    public void acceptCompareTo(B other, CompareToVisitor visitor) {
+      visitor.visit(other, this, B::accept);
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/utils/structural/StructuralItemsTest.java b/src/test/java/com/android/tools/r8/utils/structural/StructuralItemsTest.java
new file mode 100644
index 0000000..64481c3
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/utils/structural/StructuralItemsTest.java
@@ -0,0 +1,182 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.utils.structural;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class StructuralItemsTest extends TestBase {
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public StructuralItemsTest(TestParameters parameters) {
+    parameters.assertNoneRuntime();
+  }
+
+  final B b1 = new B(1);
+  final B b2 = new B(2);
+  final B b2_copy = new B(2);
+
+  final A a1b1 = new A(1, b1);
+  final A a2b1 = new A(2, b1);
+  final A a1b2 = new A(1, b2);
+  final A a2b2 = new A(2, b2);
+  final A a1b2_copy = new A(1, b2_copy);
+  final A a2b2_copy = new A(2, b2_copy);
+
+  @Test
+  public void testOrder() {
+    assertTrue(b1.isLessThan(b2));
+    assertTrue(b2.isEqualTo(b2_copy));
+    assertFalse(b2.isLessThan(b1));
+
+    assertTrue(a1b1.isLessThan(a1b2));
+    assertTrue(a1b2.isLessThan(a2b1));
+    assertFalse(a2b1.isLessThan(a1b2_copy));
+
+    assertEquals(b1, Ordered.minIgnoreNull(null, b1));
+    assertEquals(b1, Ordered.minIgnoreNull(b1, null));
+    assertEquals(b1, Ordered.maxIgnoreNull(null, b1));
+    assertEquals(b1, Ordered.maxIgnoreNull(b1, null));
+  }
+
+  @Test
+  public void testEquals() {
+    assertFalse(b1.isEqualTo(b2));
+    assertTrue(b2.isEqualTo(b2_copy));
+    assertEquals(b2, b2_copy);
+
+    assertFalse(a2b2.isEqualTo(a2b1));
+    assertTrue(a1b2.isEqualTo(a1b2_copy));
+    assertEquals(a2b2, a2b2_copy);
+
+    // Type incompatible check should still work.
+    assertNotEquals(b1, a1b1);
+  }
+
+  @Test
+  public void testHashCode() {
+    assertEquals(b2.hashCode(), b2_copy.hashCode());
+    assertEquals(b2, b2_copy);
+    Set<B> bs = new HashSet<>(ImmutableList.of(b1, b2, b2_copy));
+    assertEquals(ImmutableSet.of(b1, b2), bs);
+
+    Set<A> as = new HashSet<>(ImmutableList.of(a1b1, a1b2, a2b1, a2b2, a1b2_copy, a2b2_copy));
+    assertEquals(ImmutableSet.of(a1b1, a1b2, a2b1, a2b2), as);
+
+    // If these collide it is a poor hashing algorithm...
+    assertNotEquals(b1.hashCode(), b2.hashCode());
+  }
+
+  private String getHash(StructuralItem<?> item) {
+    return item.hashForTesting();
+  }
+
+  @Test
+  public void testHashing() {
+    assertEquals(getHash(b2), getHash(b2_copy));
+    assertEquals(getHash(b2), getHash(b2_copy));
+    Set<String> bs = new HashSet<>(ImmutableList.of(getHash(b1), getHash(b2), getHash(b2_copy)));
+    assertEquals(ImmutableSet.of(getHash(b1), getHash(b2)), bs);
+
+    Set<String> as =
+        ImmutableList.of(a1b1, a1b2, a2b1, a2b2, a1b2_copy, a2b2_copy).stream()
+            .map(this::getHash)
+            .collect(Collectors.toSet());
+    assertEquals(
+        ImmutableSet.of(a1b1, a1b2, a2b1, a2b2).stream()
+            .map(this::getHash)
+            .collect(Collectors.toSet()),
+        as);
+
+    // If these collide it is a poor hashing algorithm...
+    assertNotEquals(getHash(b1), getHash(b2));
+  }
+
+  private static class A implements StructuralItem<A> {
+
+    private final int x;
+    private final B b;
+
+    private static void accept(StructuralSpecification<A, ?> spec) {
+      spec.withInt(a -> a.x).withItem(a -> a.b);
+    }
+
+    public A(int x, B b) {
+      this.x = x;
+      this.b = b;
+    }
+
+    @Override
+    public StructuralAccept<A> getStructuralAccept() {
+      return A::accept;
+    }
+
+    @Override
+    public A self() {
+      return this;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      return Equatable.equalsImpl(this, other);
+    }
+
+    @Override
+    public int hashCode() {
+      return HashCodeVisitor.run(this, A::accept);
+    }
+  }
+
+  private static class B implements StructuralItem<B> {
+
+    private final int y;
+
+    private static void accept(StructuralSpecification<B, ?> spec) {
+      spec.withInt(b -> b.y);
+    }
+
+    public B(int y) {
+      this.y = y;
+    }
+
+    @Override
+    public StructuralAccept<B> getStructuralAccept() {
+      return B::accept;
+    }
+
+    @Override
+    public B self() {
+      return this;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      return Equatable.equalsImpl(this, other);
+    }
+
+    @Override
+    public int hashCode() {
+      return HashCodeVisitor.run(this, B::accept);
+    }
+  }
+}
diff --git a/src/test/javaStubs/Supplier.java b/src/test/javaStubs/Supplier.java
new file mode 100644
index 0000000..f9c4860
--- /dev/null
+++ b/src/test/javaStubs/Supplier.java
@@ -0,0 +1,9 @@
+// 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 java.util.function;
+
+public interface Supplier {
+  Object get();
+}
diff --git a/third_party/opensource-apps/android/compose-samples/crane.tar.gz.sha1 b/third_party/opensource-apps/android/compose-samples/crane.tar.gz.sha1
new file mode 100644
index 0000000..43b6ec0
--- /dev/null
+++ b/third_party/opensource-apps/android/compose-samples/crane.tar.gz.sha1
@@ -0,0 +1 @@
+54e1cfb2bd83e005ccd07179958261d5ed2c7102
\ No newline at end of file
diff --git a/third_party/opensource-apps/android/compose-samples/jetcaster.tar.gz.sha1 b/third_party/opensource-apps/android/compose-samples/jetcaster.tar.gz.sha1
new file mode 100644
index 0000000..e4fdd09
--- /dev/null
+++ b/third_party/opensource-apps/android/compose-samples/jetcaster.tar.gz.sha1
@@ -0,0 +1 @@
+158d1e78d2055793960120b1c58654f83cd6d4d3
\ No newline at end of file
diff --git a/third_party/opensource-apps/android/compose-samples/jetchat.tar.gz.sha1 b/third_party/opensource-apps/android/compose-samples/jetchat.tar.gz.sha1
new file mode 100644
index 0000000..5e613de
--- /dev/null
+++ b/third_party/opensource-apps/android/compose-samples/jetchat.tar.gz.sha1
@@ -0,0 +1 @@
+0a6e35687efada2890624783e9936047ed10b434
\ No newline at end of file
diff --git a/third_party/opensource-apps/android/compose-samples/jetnews.tar.gz.sha1 b/third_party/opensource-apps/android/compose-samples/jetnews.tar.gz.sha1
new file mode 100644
index 0000000..3fb24df
--- /dev/null
+++ b/third_party/opensource-apps/android/compose-samples/jetnews.tar.gz.sha1
@@ -0,0 +1 @@
+d1c89d1a22c716d3c9e2c8b7b725bc4716d12ea6
\ No newline at end of file
diff --git a/third_party/opensource-apps/android/compose-samples/jetsnack.tar.gz.sha1 b/third_party/opensource-apps/android/compose-samples/jetsnack.tar.gz.sha1
new file mode 100644
index 0000000..9562d67
--- /dev/null
+++ b/third_party/opensource-apps/android/compose-samples/jetsnack.tar.gz.sha1
@@ -0,0 +1 @@
+2e7d404796f7c4b20f47957fef00755665623526
\ No newline at end of file
diff --git a/third_party/opensource-apps/android/compose-samples/jetsurvey.tar.gz.sha1 b/third_party/opensource-apps/android/compose-samples/jetsurvey.tar.gz.sha1
new file mode 100644
index 0000000..736edd4
--- /dev/null
+++ b/third_party/opensource-apps/android/compose-samples/jetsurvey.tar.gz.sha1
@@ -0,0 +1 @@
+0dc41fbe14dbfb3bfc70ed64ff129b311bfcbf94
\ No newline at end of file
diff --git a/third_party/opensource-apps/android/compose-samples/owl.tar.gz.sha1 b/third_party/opensource-apps/android/compose-samples/owl.tar.gz.sha1
new file mode 100644
index 0000000..d6c218c
--- /dev/null
+++ b/third_party/opensource-apps/android/compose-samples/owl.tar.gz.sha1
@@ -0,0 +1 @@
+9bb9c1cc3fea6d4ceb33df8e99057caa1bbe94f6
\ No newline at end of file
diff --git a/third_party/opensource-apps/android/compose-samples/rally.tar.gz.sha1 b/third_party/opensource-apps/android/compose-samples/rally.tar.gz.sha1
new file mode 100644
index 0000000..3947935
--- /dev/null
+++ b/third_party/opensource-apps/android/compose-samples/rally.tar.gz.sha1
@@ -0,0 +1 @@
+86f3577a407a3cc3883f2d016d23879f3e8bd64e
\ No newline at end of file
diff --git a/third_party/opensource-apps/kiss.tar.gz.sha1 b/third_party/opensource-apps/kiss.tar.gz.sha1
new file mode 100644
index 0000000..6d9e214
--- /dev/null
+++ b/third_party/opensource-apps/kiss.tar.gz.sha1
@@ -0,0 +1 @@
+ae4ff6d906f2840ee6a855e989e8b563f948ba73
\ No newline at end of file
diff --git a/third_party/opensource-apps/materialistic.tar.gz.sha1 b/third_party/opensource-apps/materialistic.tar.gz.sha1
new file mode 100644
index 0000000..174a3fb
--- /dev/null
+++ b/third_party/opensource-apps/materialistic.tar.gz.sha1
@@ -0,0 +1 @@
+e392c3c4a607bbfe3ce0d2827475f388ab8b33d9
\ No newline at end of file
diff --git a/third_party/opensource-apps/minimal-todo.tar.gz.sha1 b/third_party/opensource-apps/minimal-todo.tar.gz.sha1
new file mode 100644
index 0000000..1bbe241
--- /dev/null
+++ b/third_party/opensource-apps/minimal-todo.tar.gz.sha1
@@ -0,0 +1 @@
+70f5f95146d09c6777c88e4d556ec7419ffbe35b
\ No newline at end of file
diff --git a/third_party/opensource-apps/muzei.tar.gz.sha1 b/third_party/opensource-apps/muzei.tar.gz.sha1
new file mode 100644
index 0000000..d8739a4
--- /dev/null
+++ b/third_party/opensource-apps/muzei.tar.gz.sha1
@@ -0,0 +1 @@
+39e660b93c0efc403d5d6ab7a3e947f18c0c6bb0
\ No newline at end of file
diff --git a/third_party/opensource-apps/newpipe.tar.gz.sha1 b/third_party/opensource-apps/newpipe.tar.gz.sha1
new file mode 100644
index 0000000..ee2d9cf
--- /dev/null
+++ b/third_party/opensource-apps/newpipe.tar.gz.sha1
@@ -0,0 +1 @@
+9f824b7c6e0e923401def9006d98d2f70ef175c3
\ No newline at end of file
diff --git a/third_party/opensource-apps/rover-android.tar.gz.sha1 b/third_party/opensource-apps/rover-android.tar.gz.sha1
new file mode 100644
index 0000000..043d3e4
--- /dev/null
+++ b/third_party/opensource-apps/rover-android.tar.gz.sha1
@@ -0,0 +1 @@
+50562618faf112ec73219488fa9b3705dc42a38e
\ No newline at end of file
diff --git a/third_party/opensource-apps/santa-tracker.tar.gz.sha1 b/third_party/opensource-apps/santa-tracker.tar.gz.sha1
new file mode 100644
index 0000000..ea1af87
--- /dev/null
+++ b/third_party/opensource-apps/santa-tracker.tar.gz.sha1
@@ -0,0 +1 @@
+5348b1c66c86c09c7d3bafd65b0d5417df78bcc8
\ No newline at end of file
diff --git a/third_party/opensource-apps/signal-android.tar.gz.sha1 b/third_party/opensource-apps/signal-android.tar.gz.sha1
new file mode 100644
index 0000000..131500f
--- /dev/null
+++ b/third_party/opensource-apps/signal-android.tar.gz.sha1
@@ -0,0 +1 @@
+2dbd3f913897b4ec9e25598d783579dbb74fab24
\ No newline at end of file
diff --git a/third_party/opensource-apps/simple-calendar.tar.gz.sha1 b/third_party/opensource-apps/simple-calendar.tar.gz.sha1
new file mode 100644
index 0000000..093d104
--- /dev/null
+++ b/third_party/opensource-apps/simple-calendar.tar.gz.sha1
@@ -0,0 +1 @@
+118c5a291b675393698e4a2c0a0e1891a0933a8b
\ No newline at end of file
diff --git a/third_party/opensource-apps/simple-camera.tar.gz.sha1 b/third_party/opensource-apps/simple-camera.tar.gz.sha1
new file mode 100644
index 0000000..3d88a7a
--- /dev/null
+++ b/third_party/opensource-apps/simple-camera.tar.gz.sha1
@@ -0,0 +1 @@
+19aba96f4c6b9751844ff668202a4591136e9702
\ No newline at end of file
diff --git a/third_party/opensource-apps/simple-file-manager.tar.gz.sha1 b/third_party/opensource-apps/simple-file-manager.tar.gz.sha1
new file mode 100644
index 0000000..00c4197
--- /dev/null
+++ b/third_party/opensource-apps/simple-file-manager.tar.gz.sha1
@@ -0,0 +1 @@
+5f93c93b767b755fdec101961d9599928bf0aec8
\ No newline at end of file
diff --git a/third_party/opensource-apps/simple-gallery.tar.gz.sha1 b/third_party/opensource-apps/simple-gallery.tar.gz.sha1
new file mode 100644
index 0000000..b2d42db
--- /dev/null
+++ b/third_party/opensource-apps/simple-gallery.tar.gz.sha1
@@ -0,0 +1 @@
+7d086ce14817cf834cdbf99338aeace3f198bc67
\ No newline at end of file
diff --git a/third_party/opensource-apps/sqldelight.tar.gz.sha1 b/third_party/opensource-apps/sqldelight.tar.gz.sha1
new file mode 100644
index 0000000..3a186f8
--- /dev/null
+++ b/third_party/opensource-apps/sqldelight.tar.gz.sha1
@@ -0,0 +1 @@
+31db97e3e578398a6e036390da81732717b5e0b7
\ No newline at end of file
diff --git a/third_party/opensource-apps/tachiyomi.tar.gz.sha1 b/third_party/opensource-apps/tachiyomi.tar.gz.sha1
new file mode 100644
index 0000000..a8c7a0c
--- /dev/null
+++ b/third_party/opensource-apps/tachiyomi.tar.gz.sha1
@@ -0,0 +1 @@
+a88063982149445bcc177856b3b6c265dc0d5ea7
\ No newline at end of file
diff --git a/third_party/opensource-apps/tivi.tar.gz.sha1 b/third_party/opensource-apps/tivi.tar.gz.sha1
new file mode 100644
index 0000000..6bd16e2
--- /dev/null
+++ b/third_party/opensource-apps/tivi.tar.gz.sha1
@@ -0,0 +1 @@
+2f4adb11dcc8c56f377ee9945d47e88313bc5855
\ No newline at end of file
diff --git a/third_party/opensource-apps/tusky.tar.gz.sha1 b/third_party/opensource-apps/tusky.tar.gz.sha1
new file mode 100644
index 0000000..7eecbaf
--- /dev/null
+++ b/third_party/opensource-apps/tusky.tar.gz.sha1
@@ -0,0 +1 @@
+e546ffad98f75d0db7be39bfa7147f8eaa78d0aa
\ No newline at end of file
diff --git a/tools/compiledump.py b/tools/compiledump.py
index 48c596b..5c818d6 100755
--- a/tools/compiledump.py
+++ b/tools/compiledump.py
@@ -38,6 +38,7 @@
     help='Compiler version to use (default read from dump version file).'
       'Valid arguments are:'
       '  "master" to run from your own tree,'
+      '  "source" to run from build classes directly,'
       '  "X.Y.Z" to run a specific version, or'
       '  <hash> to run that hash from master.',
     default=None)
@@ -87,6 +88,12 @@
     help='Set desugared-library (default set from dump)',
     default=None)
   parser.add_argument(
+    '--disable-desugared-lib',
+    help='Disable desugared-libary if it will be set from dump',
+    default=False,
+    action='store_true'
+  )
+  parser.add_argument(
     '--loop',
     help='Run the compilation in a loop',
     default=False,
@@ -131,6 +138,14 @@
   def desugared_library_json(self):
     return self.if_exists('desugared-library.json')
 
+  def proguard_input_map(self):
+    if self.if_exists('proguard_input.config'):
+      print "Unimplemented: proguard_input configuration."
+
+  def main_dex_resource(self):
+    if self.if_exists('main-dex-list.txt'):
+      print "Unimplemented: main-dex-list."
+
   def build_properties_file(self):
     return self.if_exists('build.properties')
 
@@ -209,6 +224,8 @@
 def download_distribution(args, version, temp):
   if version == 'master':
     return utils.R8_JAR if args.nolib else utils.R8LIB_JAR
+  if version == 'source':
+    return '%s:%s' % (utils.BUILD_JAVA_MAIN_DIR, utils.ALL_DEPS_JAR)
   name = 'r8.jar' if args.nolib else 'r8lib.jar'
   source = archive.GetUploadDestination(version, name, is_hash(version))
   dest = os.path.join(temp, 'r8.jar')
@@ -219,12 +236,14 @@
   wrapper_file = os.path.join(
       utils.REPO_ROOT,
       'src/main/java/com/android/tools/r8/utils/CompileDumpCompatR8.java')
-  subprocess.check_output([
+  cmd = [
     jdk.GetJavacExecutable(),
     wrapper_file,
     '-d', temp,
     '-cp', dist,
-    ])
+  ]
+  utils.PrintCmd(cmd)
+  subprocess.check_output(cmd)
   return temp
 
 def is_hash(version):
@@ -248,6 +267,8 @@
     min_api = determine_min_api(args, build_properties)
     classfile = determine_class_file(args, build_properties)
     jar = args.r8_jar if args.r8_jar else download_distribution(args, version, temp)
+    if ':' not in jar and not os.path.exists(jar):
+      error("Distribution does not exist: " + jar)
     wrapper_dir = prepare_wrapper(jar, temp)
     cmd = [jdk.GetJavaExecutable()]
     if args.debug_agent:
@@ -261,6 +282,8 @@
       cmd.append('-ea')
     if args.printtimes:
       cmd.append('-Dcom.android.tools.r8.printtimes=1')
+    if hasattr(args, 'properties'):
+      cmd.extend(args.properties);
     cmd.extend(['-cp', '%s:%s' % (wrapper_dir, jar)])
     if compiler == 'd8':
       cmd.append('com.android.tools.r8.D8')
@@ -278,7 +301,7 @@
       cmd.extend(['--lib', dump.library_jar()])
     if dump.classpath_jar():
       cmd.extend(['--classpath', dump.classpath_jar()])
-    if dump.desugared_library_json():
+    if dump.desugared_library_json() and not args.disable_desugared_lib:
       cmd.extend(['--desugared-lib', dump.desugared_library_json()])
     if compiler != 'd8' and dump.config_file():
       if hasattr(args, 'config_file_consumer') and args.config_file_consumer:
@@ -299,7 +322,7 @@
       return 0
     except subprocess.CalledProcessError, e:
       print e.output
-      if not args.nolib:
+      if not args.nolib and version != 'source':
         stacktrace = os.path.join(temp, 'stacktrace')
         open(stacktrace, 'w+').write(e.output)
         local_map = utils.R8LIB_MAP if version == 'master' else None
diff --git a/tools/git_sync_cl_chain.py b/tools/git_sync_cl_chain.py
index 5a229bd..66713fa 100755
--- a/tools/git_sync_cl_chain.py
+++ b/tools/git_sync_cl_chain.py
@@ -126,7 +126,7 @@
       has_seen_open_branch = True
       has_seen_local_branch = has_seen_local_branch or (status == 'None')
 
-      if options.upload:
+      if options.upload and status != 'closed':
         if has_seen_local_branch:
           print(
               'Cannot upload branch %s since it comes after a local branch'
diff --git a/tools/retrace.py b/tools/retrace.py
index 3be9771..70e3099 100755
--- a/tools/retrace.py
+++ b/tools/retrace.py
@@ -40,6 +40,11 @@
       '--stacktrace',
       help='Path to stacktrace file.',
       default=None)
+  parser.add_argument(
+      '--quiet',
+      default=None,
+      action='store_true',
+      help='Disables diagnostics printing to stdout.')
   return parser.parse_args()
 
 
@@ -67,9 +72,10 @@
       hash_or_version,
       args.stacktrace,
       args.commit_hash is not None,
-      args.no_r8lib)
+      args.no_r8lib,
+      quiet=args.quiet)
 
-def run(map_path, hash_or_version, stacktrace, is_hash, no_r8lib):
+def run(map_path, hash_or_version, stacktrace, is_hash, no_r8lib, quiet=False):
   if hash_or_version:
     download_path = archive.GetUploadDestination(
         hash_or_version,
@@ -93,7 +99,7 @@
   if stacktrace:
     retrace_args.append(stacktrace)
 
-  utils.PrintCmd(retrace_args)
+  utils.PrintCmd(retrace_args, quiet=quiet)
   return subprocess.call(retrace_args)
 
 
diff --git a/tools/run_on_app_dump.py b/tools/run_on_app_dump.py
index bf5d4c7..03687af 100755
--- a/tools/run_on_app_dump.py
+++ b/tools/run_on_app_dump.py
@@ -7,12 +7,12 @@
 import apk_masseur
 import compiledump
 import gradle
-import jdk
 import optparse
 import os
 import shutil
 import sys
 import time
+import update_prebuilds_in_android
 import utils
 import zipfile
 
@@ -38,14 +38,27 @@
     defaults = {
       'id': None,
       'name': None,
+      'collections': [],
       'dump_app': None,
       'apk_app': None,
+      'dump_test': None,
       'apk_test': None,
       'skip': False,
       'url': None,  # url is not used but nice to have for updating apps
       'revision': None,
       'folder': None,
       'skip_recompilation': False,
+      'compiler_properties': [],
+    }
+    # This below does not work in python3
+    defaults.update(fields.items())
+    self.__dict__ = defaults
+
+
+class AppCollection(object):
+  def __init__(self, fields):
+    defaults = {
+      'name': None
     }
     # This below does not work in python3
     defaults.update(fields.items())
@@ -89,8 +102,6 @@
     'url': 'https://github.com/christofferqa/AntennaPod.git',
     'revision': '77e94f4783a16abe9cc5b78dc2d2b2b1867d8c06',
     'folder': 'antennapod',
-    # TODO(b/172450929): Fix recompilation
-    'skip_recompilation': True
   }),
   App({
     'id': 'com.example.applymapping',
@@ -112,8 +123,9 @@
     'url': 'https://github.com/mkj-gram/chanu.git',
     'revision': '6e53458f167b6d78398da60c20fd0da01a232617',
     'folder': 'chanu',
-    # TODO(b/172535996): Fix recompilation
-    'skip_recompilation': True
+    # The app depends on a class file that has access flags interface but
+    # not abstract
+    'compiler_properties': ['-Dcom.android.tools.r8.allowInvalidCfAccessFlags=true']
   }),
   # TODO(b/172539375): Monkey runner fails on recompilation.
   App({
@@ -131,14 +143,12 @@
     'dump_app': 'dump_app.zip',
     'apk_app': 'app-debug.apk',
     # TODO(b/172549283): Compiling tests fails
-    'id_test': 'com.example.applymapping.test',
+    'id_test': 'com.google.samples.apps.sunflower.test',
     'dump_test': 'dump_test.zip',
     'apk_test': 'app-debug-androidTest.apk',
     'url': 'https://github.com/android/sunflower',
     'revision': '0c4c88fdad2a74791199dffd1a6559559b1dbd4a',
     'folder': 'sunflower',
-    # TODO(b/172548728): Fix recompilation
-    'skip_recompilation': True
   }),
   # TODO(b/172565385): Monkey runner fails on recompilation
   App({
@@ -151,6 +161,175 @@
     'folder': 'iosched',
   }),
   App({
+    'id': 'fr.neamar.kiss',
+    'name': 'KISS',
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'app-release.apk',
+    # TODO(b/172569220): Running tests fails due to missing keep rules
+    'id_test': 'fr.neamar.kiss.test',
+    'dump_test': 'dump_test.zip',
+    'apk_test': 'app-release-androidTest.apk',
+    'url': 'https://github.com/Neamar/KISS',
+    'revision': '8ccffaadaf0d0b8fc4418ed2b4281a0935d3d971',
+    'folder': 'kiss',
+  }),
+  # TODO(b/172577344): Monkey runner not working.
+  App({
+    'id': 'io.github.hidroh.materialistic',
+    'name': 'materialistic',
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'app-release.apk',
+    'url': 'https://github.com/christofferqa/materialistic.git',
+    'revision': '2b2b2ee25ce9e672d5aab1dc90a354af1522b1d9',
+    'folder': 'materialistic',
+  }),
+  App({
+    'id': 'com.avjindersinghsekhon.minimaltodo',
+    'name': 'MinimalTodo',
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'app-release.apk',
+    'url': 'https://github.com/christofferqa/Minimal-Todo',
+    'revision': '9d8c73746762cd376b718858ec1e8783ca07ba7c',
+    'folder': 'minimal-todo',
+  }),
+  App({
+    'id': 'net.nurik.roman.muzei',
+    'name': 'muzei',
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'muzei-release.apk',
+    'url': 'https://github.com/romannurik/muzei',
+    'revision': '9eac6e98aebeaf0ae40bdcd85f16dd2886551138',
+    'folder': 'muzei',
+  }),
+  # TODO(b/172806281): Monkey runner does not work.
+  App({
+    'id': 'org.schabi.newpipe',
+    'name': 'NewPipe',
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'app-release-unsigned.apk',
+    'url': 'https://github.com/TeamNewPipe/NewPipe',
+    'revision': 'f4435f90313281beece70c544032f784418d85fa',
+    'folder': 'newpipe',
+  }),
+  # TODO(b/172806808): Monkey runner does not work.
+  App({
+    'id': 'io.rover.app.debug',
+    'name': 'Rover',
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'example-app-release-unsigned.apk',
+    'url': 'https://github.com/RoverPlatform/rover-android',
+    'revision': '94342117097770ea3ca2c6df6ab496a1a55c3ce7',
+    'folder': 'rover-android',
+  }),
+  App({
+    'id': 'io.rover.app.debug',
+    'name': 'Rover',
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'example-app-release-unsigned.apk',
+    'url': 'https://github.com/RoverPlatform/rover-android',
+    'revision': '94342117097770ea3ca2c6df6ab496a1a55c3ce7',
+    'folder': 'rover-android',
+  }),
+  # TODO(b/172808159): Monkey runner does not work
+  App({
+    'id': 'com.google.android.apps.santatracker',
+    'name': 'SantaTracker',
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'santa-tracker-release.apk',
+    'url': 'https://github.com/christofferqa/santa-tracker-android',
+    'revision': '8dee74be7d9ee33c69465a07088c53087d24a6dd',
+    'folder': 'santa-tracker',
+  }),
+  App({
+    'id': 'org.thoughtcrime.securesms',
+    'name': 'Signal',
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'Signal-Android-play-prod-universal-release-4.76.2.apk',
+    # TODO(b/172812839): Instrumentation test fails.
+    'id_test': 'org.thoughtcrime.securesms.test',
+    'dump_test': 'dump_test.zip',
+    'apk_test': 'Signal-Android-play-prod-release-androidTest.apk',
+    'url': 'https://github.com/signalapp/Signal-Android',
+    'revision': '91ca19f294362ccee2c2b43c247eba228e2b30a1',
+    'folder': 'signal-android',
+  }),
+  # TODO(b/172815827): Monkey runner does not work
+  App({
+    'id': 'com.simplemobiletools.calendar.pro',
+    'name': 'Simple-Calendar',
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'calendar-release.apk',
+    'url': 'https://github.com/SimpleMobileTools/Simple-Calendar',
+    'revision': '906209874d0a091c7fce5a57972472f272d6b068',
+    'folder': 'simple-calendar',
+  }),
+  # TODO(b/172815534): Monkey runner does not work
+  App({
+    'id': 'com.simplemobiletools.camera.pro',
+    'name': 'Simple-Camera',
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'camera-release.apk',
+    'url': 'https://github.com/SimpleMobileTools/Simple-Camera',
+    'revision': 'ebf9820c51e960912b3238287e30a131244fdee6',
+    'folder': 'simple-camera',
+  }),
+  App({
+    'id': 'com.simplemobiletools.filemanager.pro',
+    'name': 'Simple-File-Manager',
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'file-manager-release.apk',
+    'url': 'https://github.com/SimpleMobileTools/Simple-File-Manager',
+    'revision': '2b7fa68ea251222cc40cf6d62ad1de260a6f54d9',
+    'folder': 'simple-file-manager',
+  }),
+  App({
+    'id': 'com.simplemobiletools.gallery.pro',
+    'name': 'Simple-Gallery',
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'gallery-326-foss-release.apk',
+    'url': 'https://github.com/SimpleMobileTools/Simple-Gallery',
+    'revision': '564e56b20d33b28d0018c8087ec705beeb60785e',
+    'folder': 'simple-gallery',
+  }),
+  App({
+    'id': 'com.example.sqldelight.hockey',
+    'name': 'SQLDelight',
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'android-release.apk',
+    'url': 'https://github.com/christofferqa/sqldelight',
+    'revision': '2e67a1126b6df05e4119d1e3a432fde51d76cdc8',
+    'folder': 'sqldelight',
+  }),
+  # TODO(b/172824096): Monkey runner does not work.
+  App({
+    'id': 'eu.kanade.tachiyomi',
+    'name': 'Tachiyomi',
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'app-dev-release.apk',
+    'url': 'https://github.com/inorichi/tachiyomi',
+    'revision': '8aa6486bf76ab9a61a5494bee284b1a5e9180bf3',
+    'folder': 'tachiyomi',
+  }),
+  # TODO(b/172862042): Monkey runner does not work.
+  App({
+    'id': 'app.tivi',
+    'name': 'Tivi',
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'app-release.apk',
+    'url': 'https://github.com/chrisbanes/tivi',
+    'revision': '8e2ddd8fe2d343264a66aa1ef8acbd4cc587e8ce',
+    'folder': 'tivi',
+  }),
+  App({
+    'id': 'com.keylesspalace.tusky',
+    'name': 'Tusky',
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'app-blue-release.apk',
+    'url': 'https://github.com/tuskyapp/Tusky',
+    'revision': '814a9b8f9bacf8d26f712b06a0313a3534a2be95',
+    'folder': 'tusky',
+  }),
+  App({
     'id': 'org.wikipedia',
     'name': 'Wikipedia',
     'dump_app': 'dump_app.zip',
@@ -159,6 +338,107 @@
     'revision': '0fa7cad843c66313be8e25790ef084cf1a1fa67e',
     'folder': 'wikipedia',
   }),
+  # TODO(b/173167253): Check if monkey testing works.
+  App({
+    'id': 'androidx.compose.samples.crane',
+    'name': 'compose-crane',
+    'collections': ['compose-samples'],
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'app-release-unsigned.apk',
+    'url': 'https://github.com/android/compose-samples',
+    'revision': '779cf9e187b8ee2c6b620b2abb4524719b3f10f8',
+    'folder': 'android/compose-samples/crane',
+  }),
+  # TODO(b/173167253): Check if monkey testing works.
+  App({
+    'id': 'com.example.jetcaster',
+    'name': 'compose-jetcaster',
+    'collections': ['compose-samples'],
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'app-release-unsigned.apk',
+    'url': 'https://github.com/android/compose-samples',
+    'revision': '779cf9e187b8ee2c6b620b2abb4524719b3f10f8',
+    'folder': 'android/compose-samples/jetcaster',
+    # TODO(b/173176042): Fix recompilation
+    'skip_recompilation': True,
+  }),
+  # TODO(b/173167253): Check if monkey testing works.
+  App({
+    'id': 'com.example.compose.jetchat',
+    'name': 'compose-jetchat',
+    'collections': ['compose-samples'],
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'app-release-unsigned.apk',
+    'url': 'https://github.com/android/compose-samples',
+    'revision': '779cf9e187b8ee2c6b620b2abb4524719b3f10f8',
+    'folder': 'android/compose-samples/jetchat',
+    # TODO(b/173176042): Fix recompilation
+    'skip_recompilation': True,
+  }),
+  # TODO(b/173167253): Check if monkey testing works.
+  App({
+    'id': 'com.example.jetnews',
+    'name': 'compose-jetnews',
+    'collections': ['compose-samples'],
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'app-release-unsigned.apk',
+    'url': 'https://github.com/android/compose-samples',
+    'revision': '779cf9e187b8ee2c6b620b2abb4524719b3f10f8',
+    'folder': 'android/compose-samples/jetnews',
+    # TODO(b/173176042): Fix recompilation
+    'skip_recompilation': True,
+  }),
+  # TODO(b/173167253): Check if monkey testing works.
+  App({
+    'id': 'com.example.jetsnack',
+    'name': 'compose-jetsnack',
+    'collections': ['compose-samples'],
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'app-release-unsigned.apk',
+    'url': 'https://github.com/android/compose-samples',
+    'revision': '779cf9e187b8ee2c6b620b2abb4524719b3f10f8',
+    'folder': 'android/compose-samples/jetsnack',
+  }),
+  # TODO(b/173167253): Check if monkey testing works.
+  App({
+    'id': 'com.example.compose.jetsurvey',
+    'name': 'compose-jetsurvey',
+    'collections': ['compose-samples'],
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'app-release-unsigned.apk',
+    'url': 'https://github.com/android/compose-samples',
+    'revision': '779cf9e187b8ee2c6b620b2abb4524719b3f10f8',
+    'folder': 'android/compose-samples/jetsurvey',
+  }),
+  # TODO(b/173167253): Check if monkey testing works.
+  App({
+    'id': 'com.example.owl',
+    'name': 'compose-owl',
+    'collections': ['compose-samples'],
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'app-release-unsigned.apk',
+    'url': 'https://github.com/android/compose-samples',
+    'revision': '779cf9e187b8ee2c6b620b2abb4524719b3f10f8',
+    'folder': 'android/compose-samples/owl',
+  }),
+  # TODO(b/173167253): Check if monkey testing works.
+  App({
+    'id': 'com.example.compose.rally',
+    'name': 'compose-rally',
+    'collections': ['compose-samples'],
+    'dump_app': 'dump_app.zip',
+    'apk_app': 'app-release-unsigned.apk',
+    'url': 'https://github.com/android/compose-samples',
+    'revision': '779cf9e187b8ee2c6b620b2abb4524719b3f10f8',
+    'folder': 'android/compose-samples/rally',
+  }),
+]
+
+
+APP_COLLECTIONS = [
+  AppCollection({
+    'name': 'compose-samples',
+  })
 ]
 
 
@@ -188,7 +468,11 @@
 
 
 def is_full_r8(shrinker):
-  return '-full' not in shrinker
+  return '-full' in shrinker
+
+
+def version_is_built_jar(version):
+  return version != 'master' and version != 'source'
 
 
 def compute_size_of_dex_files_in_package(path):
@@ -208,6 +492,13 @@
   return os.path.join(app_dir, app.dump_test)
 
 
+def get_r8_jar(options, temp_dir, shrinker):
+  if (options.version == 'source'):
+    return None
+  return os.path.join(
+      temp_dir, 'r8lib.jar' if is_minified_r8(shrinker) else 'r8.jar')
+
+
 def get_results_for_app(app, options, temp_dir):
   app_folder = app.folder if app.folder else app.name + "_" + app.revision
   app_dir = os.path.join(utils.OPENSOURCE_DUMPS_DIR, app_folder)
@@ -255,7 +546,7 @@
     app.name,
     shrinker))
   print('To compile locally: '
-        'tools/run_on_as_app.py --shrinker {} --r8-compilation-steps {} '
+        'tools/run_on_app_dump.py --shrinker {} --r8-compilation-steps {} '
         '--app {}'.format(
     shrinker,
     options.r8_compilation_steps,
@@ -263,7 +554,9 @@
   print('HINT: use --shrinker r8-nolib --no-build if you have a local R8.jar')
   recomp_jar = None
   status = 'success'
-  compilation_steps = 1 if app.skip_recompilation else options.r8_compilation_steps;
+  if options.r8_compilation_steps < 1:
+    return
+  compilation_steps = 1 if app.skip_recompilation else options.r8_compilation_steps
   for compilation_step in range(0, compilation_steps):
     if status != 'success':
       break
@@ -312,7 +605,8 @@
         app.id, options.emulator_id, app_apk_destination, options.monkey_events,
         options.quiet, is_logging_enabled_for(app, options)) else 'failed'
 
-    if result.get('build_status') == 'success' and options.run_tests:
+    if (result.get('build_status') == 'success'
+        and options.run_tests and app.dump_test):
       if not os.path.isfile(app_apk_destination):
         apk_masseur.masseur(
           original_app_apk, dex=app_jar, resources='META-INF/services/*',
@@ -348,55 +642,55 @@
 def build_app_with_shrinker(app, options, temp_dir, app_dir, shrinker,
                             compilation_step_index, compilation_steps,
                             prev_recomp_jar):
-  r8jar = os.path.join(
-    temp_dir, 'r8lib.jar' if is_minified_r8(shrinker) else 'r8.jar')
 
   args = AttrDict({
     'dump': dump_for_app(app_dir, app),
-    'r8_jar': r8jar,
+    'r8_jar': get_r8_jar(options, temp_dir, shrinker),
     'ea': False if options.disable_assertions else True,
-    'version': 'master',
+    'version': options.version,
     'compiler': 'r8full' if is_full_r8(shrinker) else 'r8',
     'debug_agent': options.debug_agent,
     'program_jar': prev_recomp_jar,
     'nolib': not is_minified_r8(shrinker),
     'config_file_consumer': remove_print_lines,
+    'properties': app.compiler_properties,
+    'disable_desugared_lib': False,
   })
 
-  compile_result = compiledump.run1(temp_dir, args, [])
-
-  out_jar = os.path.join(temp_dir, "out.jar")
-  out_mapping = os.path.join(temp_dir, "out.jar.map")
   app_jar = os.path.join(
     temp_dir, '{}_{}_{}_dex_out.jar'.format(
       app.name, shrinker, compilation_step_index))
   app_mapping = os.path.join(
     temp_dir, '{}_{}_{}_dex_out.jar.map'.format(
       app.name, shrinker, compilation_step_index))
-
-  if compile_result != 0 or not os.path.isfile(out_jar):
-    assert False, "Compilation of app_jar failed"
-  shutil.move(out_jar, app_jar)
-  shutil.move(out_mapping, app_mapping)
-
   recomp_jar = None
-  if compilation_step_index < compilation_steps - 1:
-    args['classfile'] = True
-    args['min_api'] = "10000"
-    compile_result = compiledump.run1(temp_dir, args, [])
-    if compile_result == 0:
-      recomp_jar = os.path.join(
-        temp_dir, '{}_{}_{}_cf_out.jar'.format(
-          app.name, shrinker, compilation_step_index))
-      shutil.move(out_jar, recomp_jar)
+
+  with utils.TempDir() as compile_temp_dir:
+    compile_result = compiledump.run1(compile_temp_dir, args, [])
+    out_jar = os.path.join(compile_temp_dir, "out.jar")
+    out_mapping = os.path.join(compile_temp_dir, "out.jar.map")
+
+    if compile_result != 0 or not os.path.isfile(out_jar):
+      assert False, 'Compilation of {} failed'.format(dump_for_app(app_dir, app))
+    shutil.move(out_jar, app_jar)
+    shutil.move(out_mapping, app_mapping)
+
+    if compilation_step_index < compilation_steps - 1:
+      args['classfile'] = True
+      args['min_api'] = "10000"
+      args['disable_desugared_lib'] = True
+      compile_result = compiledump.run1(compile_temp_dir, args, [])
+      if compile_result == 0:
+        recomp_jar = os.path.join(
+          temp_dir, '{}_{}_{}_cf_out.jar'.format(
+            app.name, shrinker, compilation_step_index))
+        shutil.move(out_jar, recomp_jar)
 
   return (app_jar, app_mapping, recomp_jar)
 
 
 def build_test_with_shrinker(app, options, temp_dir, app_dir, shrinker,
                              compilation_step_index, mapping):
-  r8jar = os.path.join(
-    temp_dir, 'r8lib.jar' if is_minified_r8(shrinker) else 'r8.jar')
 
   def rewrite_file(file):
     remove_print_lines(file)
@@ -410,9 +704,9 @@
 
   args = AttrDict({
     'dump': dump_test_for_app(app_dir, app),
-    'r8_jar': r8jar,
+    'r8_jar': get_r8_jar(options, temp_dir, shrinker),
     'ea': False if options.disable_assertions else True,
-    'version': 'master',
+    'version': options.version,
     'compiler': 'r8full' if is_full_r8(shrinker) else 'r8',
     'debug_agent': options.debug_agent,
     'nolib': not is_minified_r8(shrinker),
@@ -421,17 +715,16 @@
     'config_file_consumer': rewrite_file
   })
 
-  compile_result = compiledump.run1(temp_dir, args, [])
-
-  out_jar = os.path.join(temp_dir, "out.jar")
   test_jar = os.path.join(
     temp_dir, '{}_{}_{}_test_out.jar'.format(
       app.name, shrinker, compilation_step_index))
 
-  if compile_result != 0 or not os.path.isfile(out_jar):
-    return None
-
-  shutil.move(out_jar, test_jar)
+  with utils.TempDir() as compile_temp_dir:
+    compile_result = compiledump.run1(compile_temp_dir, args, [])
+    out_jar = os.path.join(compile_temp_dir, "out.jar")
+    if compile_result != 0 or not os.path.isfile(out_jar):
+      return None
+    shutil.move(out_jar, test_jar)
 
   return test_jar
 
@@ -538,6 +831,10 @@
                     help='What app to run on',
                     choices=[app.name for app in APPS],
                     action='append')
+  result.add_option('--app-collection', '--app_collection',
+                    help='What app collection to run',
+                    choices=[collection.name for collection in APP_COLLECTIONS],
+                    action='append')
   result.add_option('--bot',
                     help='Running on bot, use third_party dependency.',
                     default=False,
@@ -610,13 +907,26 @@
                     help='The shrinkers to use (by default, all are run)',
                     action='append')
   result.add_option('--version',
+                    default='master',
                     help='The version of R8 to use (e.g., 1.4.51)')
   (options, args) = result.parse_args(argv)
-  if options.app:
-    options.apps = [app for app in APPS if app.name in options.app]
+
+  if options.app or options.app_collection:
+    if not options.app:
+      options.app = []
+    if not options.app_collection:
+      options.app_collection = []
+    options.apps = [
+        app
+        for app in APPS
+        if app.name in options.app
+           or any(collection in options.app_collection
+                  for collection in app.collections)]
     del options.app
+    del options.app_collection
   else:
     options.apps = APPS
+
   if options.app_logging_filter:
     for app_name in options.app_logging_filter:
       assert any(app.name == app_name for app in options.apps)
@@ -626,7 +936,7 @@
   else:
     options.shrinker = [shrinker for shrinker in SHRINKERS]
 
-  if options.hash or options.version:
+  if options.hash or version_is_built_jar(options.version):
     # No need to build R8 if a specific version should be used.
     options.no_build = True
     if 'r8-nolib' in options.shrinker:
@@ -666,16 +976,16 @@
       as_utils.MoveFile(
         os.path.join(temp_dir, target), os.path.join(temp_dir, 'r8lib.jar'),
         quiet=options.quiet)
-    elif options.version:
-      # Download r8-<version>.jar from
-      # https://storage.googleapis.com/r8-releases/raw/.
-      target = 'r8-{}.jar'.format(options.version)
-      update_prebuilds_in_android.download_version(
-        temp_dir, 'com/android/tools/r8/' + options.version, target)
-      as_utils.MoveFile(
-        os.path.join(temp_dir, target), os.path.join(temp_dir, 'r8lib.jar'),
-        quiet=options.quiet)
-    else:
+    elif version_is_built_jar(options.version):
+        # Download r8-<version>.jar from
+        # https://storage.googleapis.com/r8-releases/raw/.
+        target = 'r8-{}.jar'.format(options.version)
+        update_prebuilds_in_android.download_version(
+          temp_dir, 'com/android/tools/r8/' + options.version, target)
+        as_utils.MoveFile(
+          os.path.join(temp_dir, target), os.path.join(temp_dir, 'r8lib.jar'),
+          quiet=options.quiet)
+    elif options.version == 'master':
       if not (options.no_build or options.golem):
         gradle.RunGradle(['r8', '-Pno_internal'])
         build_r8lib = False
@@ -699,7 +1009,12 @@
         continue
       result_per_shrinker_per_app.append(
         (app, get_results_for_app(app, options, temp_dir)))
-    return log_results_for_apps(result_per_shrinker_per_app, options)
+    errors = log_results_for_apps(result_per_shrinker_per_app, options)
+    if errors > 0:
+      dest = 'gs://r8-test-results/r8-libs/' + str(int(time.time()))
+      utils.upload_file_to_cloud_storage(os.path.join(temp_dir, 'r8lib.jar'), dest)
+      print('R8lib saved to %s' % dest)
+    return errors
 
 
 def success(message):
diff --git a/tools/test.py b/tools/test.py
index d7987a7..0c70124 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -149,6 +149,13 @@
           ' and empty (for no runtimes).')
   result.add_option('--print-hanging-stacks', '--print_hanging_stacks',
       default=-1, type="int", help='Print hanging stacks after timeout in seconds')
+  result.add_option('--print-full-stacktraces', '--print_full_stacktraces',
+      default=False, action='store_true',
+      help='Print the full stacktraces without any filtering applied')
+  result.add_option(
+      '--print-obfuscated-stacktraces', '--print_obfuscated_stacktraces',
+      default=False, action='store_true',
+      help='Print the obfuscated stacktraces')
   return result.parse_args()
 
 def archive_failures():
@@ -202,6 +209,10 @@
     gradle_args.append('-Pdisable_assertions')
   if options.with_code_coverage:
     gradle_args.append('-Pwith_code_coverage')
+  if options.print_full_stacktraces:
+    gradle_args.append('-Pprint_full_stacktraces')
+  if options.print_obfuscated_stacktraces:
+    gradle_args.append('-Pprint_obfuscated_stacktraces')
   if os.name == 'nt':
     # temporary hack
     gradle_args.append('-Pno_internal')
diff --git a/tools/utils.py b/tools/utils.py
index 0ece764..5396c0a 100644
--- a/tools/utils.py
+++ b/tools/utils.py
@@ -29,6 +29,7 @@
 BUILD = os.path.join(REPO_ROOT, 'build')
 BUILD_DEPS_DIR = os.path.join(BUILD, 'deps')
 BUILD_MAIN_DIR = os.path.join(BUILD, 'classes', 'main')
+BUILD_JAVA_MAIN_DIR = os.path.join(BUILD, 'classes', 'java', 'main')
 BUILD_TEST_DIR = os.path.join(BUILD, 'classes', 'test')
 LIBS = os.path.join(BUILD, 'libs')
 GENERATED_LICENSE_DIR = os.path.join(BUILD, 'generatedLicense')
@@ -43,6 +44,7 @@
 R8_SRC = 'sourceJar'
 LIBRARY_DESUGAR_CONVERSIONS = 'buildLibraryDesugarConversions'
 
+ALL_DEPS_JAR = os.path.join(LIBS, 'deps_all.jar')
 D8_JAR = os.path.join(LIBS, 'd8.jar')
 R8_JAR = os.path.join(LIBS, 'r8.jar')
 R8LIB_JAR = os.path.join(LIBS, 'r8lib.jar')