Merge commit '7c8734d1ec8841da7b99d0c09025d2ac5497a62e' into dev-release
diff --git a/.gitignore b/.gitignore
index bbe6eea..a5b9426 100644
--- a/.gitignore
+++ b/.gitignore
@@ -99,19 +99,27 @@
 third_party/kotlin/kotlin-compiler-1.3.41
 third_party/kotlin/kotlin-compiler-1.3.72.tar.gz
 third_party/kotlin/kotlin-compiler-1.3.72
+third_party/kotlin/kotlin-compiler-1.4.20.tar.gz
+third_party/kotlin/kotlin-compiler-1.4.20
 third_party/kotlinx-coroutines-1.3.6.tar.gz
 third_party/kotlinx-coroutines-1.3.6
 third_party/nest/*
 third_party/openjdk/desugar_jdk_libs
 third_party/openjdk/desugar_jdk_libs.tar.gz
+third_party/openjdk/jdk-15/linux
+third_party/openjdk/jdk-15/linux.tar.gz
+third_party/openjdk/jdk-15/osx
+third_party/openjdk/jdk-15/osx.tar.gz
+third_party/openjdk/jdk-15/windows
+third_party/openjdk/jdk-15/windows.tar.gz
 third_party/openjdk/jdk-11-test
 third_party/openjdk/jdk-11-test.tar.gz
-third_party/openjdk/jdk-11/Linux
-third_party/openjdk/jdk-11/Linux.tar.gz
-third_party/openjdk/jdk-11/Mac
-third_party/openjdk/jdk-11/Mac.tar.gz
-third_party/openjdk/jdk-11/Windows
-third_party/openjdk/jdk-11/Windows.tar.gz
+third_party/openjdk/jdk-11/linux
+third_party/openjdk/jdk-11/linux.tar.gz
+third_party/openjdk/jdk-11/osx
+third_party/openjdk/jdk-11/osx.tar.gz
+third_party/openjdk/jdk-11/windows
+third_party/openjdk/jdk-11/windows.tar.gz
 third_party/openjdk/jdk8/darwin-x86
 third_party/openjdk/jdk8/darwin-x86.tar.gz
 third_party/openjdk/jdk8/linux-x86
diff --git a/build.gradle b/build.gradle
index 981dcfd..be64cba 100644
--- a/build.gradle
+++ b/build.gradle
@@ -22,7 +22,7 @@
         jcenter()
     }
     dependencies {
-        classpath 'com.github.jengelman.gradle.plugins:shadow:4.0.2'
+        classpath 'com.github.jengelman.gradle.plugins:shadow:5.2.0'
     }
 }
 
@@ -35,7 +35,7 @@
 
 ext {
     androidSupportVersion = '25.4.0'
-    asmVersion = '8.0'  // When updating update tools/asmifier.py and Toolhelper as well.
+    asmVersion = '9.0'  // When updating update tools/asmifier.py and Toolhelper as well.
     espressoVersion = '3.0.0'
     fastutilVersion = '7.2.0'
     guavaVersion = '23.0'
@@ -135,6 +135,11 @@
             srcDirs = ['src/test/examplesJava11']
         }
     }
+    examplesJava15 {
+        java {
+            srcDirs = ['src/test/examplesJava15']
+        }
+    }
     jdk11TimeTests {
         java {
             srcDirs = [
@@ -287,7 +292,7 @@
 def r8DesugaredPath = "$buildDir/libs/r8desugared.jar"
 def r8LibGeneratedKeepRulesPath = "$buildDir/generated/keep.txt"
 def r8LibTestPath = "$buildDir/classes/r8libtest"
-def java11ClassFiles = "build/classes/java/mainJava11"
+def java11ClassFiles = "$buildDir/classes/java/mainJava11"
 
 def osString = OperatingSystem.current().isLinux() ? "linux" :
         OperatingSystem.current().isMacOsX() ? "mac" : "windows"
@@ -362,16 +367,19 @@
         linux: [
                 "third_party": ["openjdk/openjdk-9.0.4/linux",
                                 "openjdk/jdk8/linux-x86",
-                                "openjdk/jdk-11/Linux"],
+                                "openjdk/jdk-11/linux",
+                                "openjdk/jdk-15/linux"],
         ],
         osx: [
                 "third_party": ["openjdk/openjdk-9.0.4/osx",
                                 "openjdk/jdk8/darwin-x86",
-                                "openjdk/jdk-11/Mac"],
+                                "openjdk/jdk-11/osx",
+                                "openjdk/jdk-15/osx"],
         ],
         windows: [
                 "third_party": ["openjdk/openjdk-9.0.4/windows",
-                                "openjdk/jdk-11/Windows"],
+                                "openjdk/jdk-11/windows",
+                                "openjdk/jdk-15/windows"],
         ],
 ]
 
@@ -555,42 +563,61 @@
     }
 }
 
-tasks.named(sourceSets.examplesJava9.compileJavaTaskName).get().configure {
-    def jdkDir = 'third_party/openjdk/openjdk-9.0.4/'
-    options.fork = true
-    options.forkOptions.jvmArgs = []
-    if (OperatingSystem.current().isLinux()) {
-        options.forkOptions.javaHome = file(jdkDir + 'linux')
-    } else if (OperatingSystem.current().isMacOsX()) {
-        options.forkOptions.javaHome = file(jdkDir + 'osx')
-    } else {
-        options.forkOptions.javaHome = file(jdkDir + 'windows')
-    }
-    sourceCompatibility = JavaVersion.VERSION_1_9
-    targetCompatibility = JavaVersion.VERSION_1_9
-}
-
-def setJdk11CompilationWithCompatibility(String sourceSet, JavaVersion compatibility) {
+def setJdkCompilationWithCompatibility(String sourceSet, String javaHome, JavaVersion compatibility, boolean enablePreview) {
     tasks.named(sourceSet).get().configure {
-        def jdkDir = 'third_party/openjdk/jdk-11/'
+        def jdkDir = "third_party/openjdk/${javaHome}/"
         options.fork = true
         options.forkOptions.jvmArgs = []
+        if (enablePreview) {
+            options.compilerArgs.add('--enable-preview')
+        }
         if (OperatingSystem.current().isLinux()) {
-            options.forkOptions.javaHome = file(jdkDir + 'Linux')
+            options.forkOptions.javaHome = file(jdkDir + 'linux')
         } else if (OperatingSystem.current().isMacOsX()) {
-            options.forkOptions.javaHome = file(jdkDir + 'Mac/Contents/Home')
+            options.forkOptions.javaHome = file(jdkDir + 'osx')
         } else {
-            options.forkOptions.javaHome = file(jdkDir + 'Windows')
+            options.forkOptions.javaHome = file(jdkDir + 'windows')
         }
         sourceCompatibility = compatibility
         targetCompatibility = compatibility
     }
 }
 
-setJdk11CompilationWithCompatibility(sourceSets.examplesJava10.compileJavaTaskName, JavaVersion.VERSION_1_10)
-setJdk11CompilationWithCompatibility(sourceSets.examplesJava11.compileJavaTaskName, JavaVersion.VERSION_11)
-setJdk11CompilationWithCompatibility(sourceSets.examplesTestNGRunner.compileJavaTaskName, JavaVersion.VERSION_11)
-setJdk11CompilationWithCompatibility(sourceSets.jdk11TimeTests.compileJavaTaskName, JavaVersion.VERSION_11)
+setJdkCompilationWithCompatibility(
+        sourceSets.examplesJava9.compileJavaTaskName,
+        'openjdk-9.0.4',
+        JavaVersion.VERSION_1_9,
+        false)
+setJdkCompilationWithCompatibility(
+        sourceSets.examplesJava11.compileJavaTaskName,
+        'jdk-11',
+        JavaVersion.VERSION_11,
+        false)
+setJdkCompilationWithCompatibility(
+        sourceSets.examplesJava10.compileJavaTaskName,
+        'jdk-11',
+        JavaVersion.VERSION_1_10,
+        false)
+setJdkCompilationWithCompatibility(
+        sourceSets.examplesJava11.compileJavaTaskName,
+        'jdk-11',
+        JavaVersion.VERSION_11,
+        false)
+setJdkCompilationWithCompatibility(
+        sourceSets.examplesTestNGRunner.compileJavaTaskName,
+        'jdk-11',
+        JavaVersion.VERSION_11,
+        false)
+setJdkCompilationWithCompatibility(
+        sourceSets.jdk11TimeTests.compileJavaTaskName,
+        'jdk-11',
+        JavaVersion.VERSION_11,
+        false)
+setJdkCompilationWithCompatibility(
+        sourceSets.examplesJava15.compileJavaTaskName,
+        'jdk-15',
+        JavaVersion.VERSION_15,
+        true)
 
 task compileMainWithJava11 (type: JavaCompile) {
     dependsOn downloadDeps
@@ -598,11 +625,11 @@
     options.fork = true
     options.forkOptions.jvmArgs = []
     if (OperatingSystem.current().isLinux()) {
-        options.forkOptions.javaHome = file(jdkDir + 'Linux')
+        options.forkOptions.javaHome = file(jdkDir + 'linux')
     } else if (OperatingSystem.current().isMacOsX()) {
-        options.forkOptions.javaHome = file(jdkDir + 'Mac/Contents/Home')
+        options.forkOptions.javaHome = file(jdkDir + 'osx/Contents/Home')
     } else {
-        options.forkOptions.javaHome = file(jdkDir + 'Windows')
+        options.forkOptions.javaHome = file(jdkDir + 'windows')
     }
     source = sourceSets.main.allSource
     destinationDir = file(java11ClassFiles)
@@ -714,6 +741,8 @@
 }
 
 task repackageSourcesNew(type: ShadowJar) {
+    // If this fails then remove all generated folders from
+    // build/classes/java/test that is not {com,dalvik}
     from sourceSets.main.output
     mergeServiceFiles(it)
     baseName 'sources_main'
@@ -730,6 +759,7 @@
     return tasks.create("r8Create${name}", ShadowJar) {
         from consolidatedLicense.outputs.files
         from sources
+        exclude "$buildDir/classes/**"
         baseName baseNameName
         classifier = null
         version = null
@@ -1519,53 +1549,28 @@
     }
 }
 
-task buildExampleJava9Jars {
-    def examplesDir = file("src/test/examplesJava9")
-    examplesDir.eachDir { dir ->
-        def name = dir.getName();
-        def exampleOutputDir = file("build/test/examplesJava9");
-        def jarName = "${name}.jar"
-        dependsOn "jar_examplesJava9_${name}"
-        task "jar_examplesJava9_${name}"(type: Jar) {
-            archiveName = jarName
-            destinationDir = exampleOutputDir
-            from sourceSets.examplesJava9.output
-            include "**/" + name + "/**/*.class"
+def buildExampleJarsCreateTask(javaVersion, sourceSet) {
+    return tasks.create("buildExample${javaVersion}Jars") {
+        def examplesDir = file("src/test/examples${javaVersion}")
+        examplesDir.eachDir { dir ->
+            def name = dir.getName();
+            def exampleOutputDir = file("build/test/examples${javaVersion}");
+            def jarName = "${name}.jar"
+            dependsOn "jar_examples${javaVersion}_${name}"
+            task "jar_examples${javaVersion}_${name}"(type: Jar) {
+                archiveName = jarName
+                destinationDir = exampleOutputDir
+                from sourceSet.output
+                include "**/" + name + "/**/*.class"
+            }
         }
     }
 }
 
-task buildExampleJava10Jars {
-    def examplesDir = file("src/test/examplesJava10")
-    examplesDir.eachDir { dir ->
-        def name = dir.getName();
-        def exampleOutputDir = file("build/test/examplesJava10");
-        def jarName = "${name}.jar"
-        dependsOn "jar_examplesJava10_${name}"
-        task "jar_examplesJava10_${name}"(type: Jar) {
-            archiveName = jarName
-            destinationDir = exampleOutputDir
-            from sourceSets.examplesJava10.output
-            include "**/" + name + "/**/*.class"
-        }
-    }
-}
-
-task buildExampleJava11Jars {
-    def examplesDir = file("src/test/examplesJava11")
-    examplesDir.eachDir { dir ->
-        def name = dir.getName();
-        def exampleOutputDir = file("build/test/examplesJava11");
-        def jarName = "${name}.jar"
-        dependsOn "jar_examplesJava11_${name}"
-        task "jar_examplesJava11_${name}"(type: Jar) {
-            archiveName = jarName
-            destinationDir = exampleOutputDir
-            from sourceSets.examplesJava11.output
-            include "**/" + name + "/**/*.class"
-        }
-    }
-}
+buildExampleJarsCreateTask("Java9", sourceSets.examplesJava9)
+buildExampleJarsCreateTask("Java10", sourceSets.examplesJava10)
+buildExampleJarsCreateTask("Java11", sourceSets.examplesJava11)
+buildExampleJarsCreateTask("Java15", sourceSets.examplesJava15)
 
 task provideArtFrameworksDependencies {
     cloudDependencies.tools.forEach({ art ->
@@ -1685,6 +1690,7 @@
     dependsOn buildExampleJava9Jars
     dependsOn buildExampleJava10Jars
     dependsOn buildExampleJava11Jars
+    dependsOn buildExampleJava15Jars
     dependsOn buildExampleAndroidApi
     def examplesDir = file("src/test/examples")
     def noDexTests = [
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..41fabcc
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,5 @@
+# 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.
+
+org.gradle.jvmargs=-Xmx2048M
\ No newline at end of file
diff --git a/src/main/java/com/android/tools/r8/D8Command.java b/src/main/java/com/android/tools/r8/D8Command.java
index e9dbc44..f720ee9 100644
--- a/src/main/java/com/android/tools/r8/D8Command.java
+++ b/src/main/java/com/android/tools/r8/D8Command.java
@@ -414,7 +414,7 @@
     // Assert some of R8 optimizations are disabled.
     assert !internal.enableInlining;
     assert !internal.enableClassInlining;
-    assert !internal.enableHorizontalClassMerging;
+    assert internal.horizontalClassMergerOptions().isDisabled();
     assert !internal.enableStaticClassMerging;
     assert !internal.enableVerticalClassMerging;
     assert !internal.enableClassStaticizer;
diff --git a/src/main/java/com/android/tools/r8/DiagnosticsHandler.java b/src/main/java/com/android/tools/r8/DiagnosticsHandler.java
index de3f800..295d52d 100644
--- a/src/main/java/com/android/tools/r8/DiagnosticsHandler.java
+++ b/src/main/java/com/android/tools/r8/DiagnosticsHandler.java
@@ -39,7 +39,8 @@
    */
   default void warning(Diagnostic warning) {
     if (warning.getOrigin() != Origin.unknown()) {
-      System.err.print("Warning in " + warning.getOrigin() + ":\n  ");
+      System.err.println("Warning in " + warning.getOrigin() + ":");
+      System.err.print("  ");
     } else {
       System.err.print("Warning: ");
     }
@@ -53,7 +54,10 @@
    */
   default void info(Diagnostic info) {
     if (info.getOrigin() != Origin.unknown()) {
-      System.out.print("In " + info.getOrigin() + ":\n  ");
+      System.out.println("Info in " + info.getOrigin() + ":");
+      System.out.print("  ");
+    } else {
+      System.out.print("Info: ");
     }
     System.out.println(info.getDiagnosticMessage());
   }
diff --git a/src/main/java/com/android/tools/r8/L8Command.java b/src/main/java/com/android/tools/r8/L8Command.java
index dce2b21..fff3341 100644
--- a/src/main/java/com/android/tools/r8/L8Command.java
+++ b/src/main/java/com/android/tools/r8/L8Command.java
@@ -168,7 +168,7 @@
     // Assert some of R8 optimizations are disabled.
     assert !internal.enableInlining;
     assert !internal.enableClassInlining;
-    assert !internal.enableHorizontalClassMerging;
+    assert internal.horizontalClassMergerOptions().isDisabled();
     assert !internal.enableStaticClassMerging;
     assert !internal.enableVerticalClassMerging;
     assert !internal.enableClassStaticizer;
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index cfb3540..089c747 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -33,10 +33,10 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.graph.DirectMappedDexApplication.Builder;
-import com.android.tools.r8.graph.EnumValueInfoMapCollection;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
 import com.android.tools.r8.graph.InitClassLens;
+import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.graph.SubtypingInfo;
 import com.android.tools.r8.graph.analysis.ClassInitializerAssertionEnablingAnalysis;
 import com.android.tools.r8.graph.analysis.InitializedClassesInInstanceMethodsAnalysis;
@@ -61,7 +61,6 @@
 import com.android.tools.r8.ir.optimize.UnusedArgumentsCollector;
 import com.android.tools.r8.ir.optimize.UnusedArgumentsCollector.UnusedArgumentsGraphLens;
 import com.android.tools.r8.ir.optimize.enums.EnumUnboxingCfMethods;
-import com.android.tools.r8.ir.optimize.enums.EnumValueInfoMapCollector;
 import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackSimple;
 import com.android.tools.r8.ir.optimize.templates.CfUtilityMethodsForCodeOptimizations;
 import com.android.tools.r8.jar.CfApplicationWriter;
@@ -419,8 +418,12 @@
 
           // Recompute the subtyping information.
           Set<DexType> removedClasses = pruner.getRemovedClasses();
-          appView.removePrunedClasses(
-              prunedApp, removedClasses, pruner.getMethodsToKeepForConfigurationDebugging());
+          appView.pruneItems(
+              PrunedItems.builder()
+                  .setPrunedApp(prunedApp)
+                  .addRemovedClasses(removedClasses)
+                  .addAdditionalPinnedItems(pruner.getMethodsToKeepForConfigurationDebugging())
+                  .build());
           new AbstractMethodRemover(
                   appViewWithLiveness, appViewWithLiveness.appInfo().computeSubtypingInfo())
               .run();
@@ -578,7 +581,9 @@
             timing.end();
           }
         }
-        if (options.enableHorizontalClassMerging && options.enableInlining) {
+        if (options.horizontalClassMergerOptions().isEnabled()
+            && options.enableInlining
+            && options.isShrinking()) {
           timing.begin("HorizontalClassMerger");
           HorizontalClassMerger merger = new HorizontalClassMerger(appViewWithLiveness);
           DirectMappedDexApplication.Builder appBuilder =
@@ -587,7 +592,12 @@
               merger.run(appBuilder, mainDexTracingResult, runtimeTypeCheckInfo);
           if (lens != null) {
             DirectMappedDexApplication app = appBuilder.build();
-            appView.removePrunedClasses(app, appView.horizontallyMergedClasses().getSources());
+            appView.pruneItems(
+                PrunedItems.builder()
+                    .setPrunedApp(app)
+                    .addRemovedClasses(appView.horizontallyMergedClasses().getSources())
+                    .addNoLongerSyntheticItems(appView.horizontallyMergedClasses().getTargets())
+                    .build());
             appView.rewriteWithLens(lens);
 
             // Only required for class merging, clear instance to save memory.
@@ -606,18 +616,11 @@
       if (options.enableEnumSwitchMapRemoval) {
         appViewWithLiveness.setAppInfo(new SwitchMapCollector(appViewWithLiveness).run());
       }
-      if (options.enableEnumValueOptimization || options.enableEnumUnboxing) {
-        appViewWithLiveness.setAppInfo(new EnumValueInfoMapCollector(appViewWithLiveness).run());
-      }
 
       // Collect the already pruned types before creating a new app info without liveness.
       // TODO: we should avoid removing liveness.
       Set<DexType> prunedTypes = appView.withLiveness().appInfo().getPrunedTypes();
 
-      // TODO: move to appview.
-      EnumValueInfoMapCollection enumValueInfoMapCollection =
-          appViewWithLiveness.appInfo().getEnumValueInfoMapCollection();
-
       timing.begin("Create IR");
       CfgPrinter printer = options.printCfg ? new CfgPrinter() : null;
       try {
@@ -717,13 +720,11 @@
                   missingClasses,
                   prunedTypes);
           appView.setAppInfo(
-              enqueuer
-                  .traceApplication(
-                      appView.rootSet(),
-                      options.getProguardConfiguration().getDontWarnPatterns(),
-                      executorService,
-                      timing)
-                  .withEnumValueInfoMaps(enumValueInfoMapCollection));
+              enqueuer.traceApplication(
+                  appView.rootSet(),
+                  options.getProguardConfiguration().getDontWarnPatterns(),
+                  executorService,
+                  timing));
           // Rerunning the enqueuer should not give rise to any method rewritings.
           assert enqueuer.buildGraphLens() == null;
           appView.withGeneratedMessageLiteBuilderShrinker(
@@ -748,10 +749,12 @@
                   options.reporter, options.usageInformationConsumer);
             }
 
-            appView.removePrunedClasses(
-                application,
-                CollectionUtils.mergeSets(prunedTypes, removedClasses),
-                pruner.getMethodsToKeepForConfigurationDebugging());
+            appView.pruneItems(
+                PrunedItems.builder()
+                    .setPrunedApp(application)
+                    .addRemovedClasses(CollectionUtils.mergeSets(prunedTypes, removedClasses))
+                    .addAdditionalPinnedItems(pruner.getMethodsToKeepForConfigurationDebugging())
+                    .build());
 
             new BridgeHoisting(appViewWithLiveness).run();
 
diff --git a/src/main/java/com/android/tools/r8/R8Command.java b/src/main/java/com/android/tools/r8/R8Command.java
index 40be618..29112ed 100644
--- a/src/main/java/com/android/tools/r8/R8Command.java
+++ b/src/main/java/com/android/tools/r8/R8Command.java
@@ -869,7 +869,8 @@
             ? LineNumberOptimization.ON
             : LineNumberOptimization.OFF;
 
-    assert proguardConfiguration.isOptimizing() || !internal.enableHorizontalClassMerging;
+    assert proguardConfiguration.isOptimizing()
+        || internal.horizontalClassMergerOptions().isDisabled();
     assert internal.enableStaticClassMerging || !proguardConfiguration.isOptimizing();
     assert !internal.enableTreeShakingOfLibraryMethodOverrides;
     assert internal.enableVerticalClassMerging || !proguardConfiguration.isOptimizing();
@@ -879,7 +880,7 @@
       internal.getProguardConfiguration().getKeepAttributes().localVariableTypeTable = true;
       internal.enableInlining = false;
       internal.enableClassInlining = false;
-      internal.enableHorizontalClassMerging = false;
+      internal.horizontalClassMergerOptions().disable();
       internal.enableStaticClassMerging = false;
       internal.enableVerticalClassMerging = false;
       internal.enableClassStaticizer = false;
@@ -891,7 +892,7 @@
       // If R8 is not shrinking, there is no point in running various optimizations since the
       // optimized classes will still remain in the program (the application size could increase).
       internal.enableEnumUnboxing = false;
-      internal.enableHorizontalClassMerging = false;
+      internal.horizontalClassMergerOptions().disable();
       internal.enableLambdaMerging = false;
       internal.enableStaticClassMerging = false;
       internal.enableVerticalClassMerging = false;
@@ -900,7 +901,7 @@
     if (!internal.enableInlining) {
       // If R8 cannot perform inlining, then the synthetic constructors would not inline the called
       // constructors, producing invalid code.
-      internal.enableHorizontalClassMerging = false;
+      internal.horizontalClassMergerOptions().disable();
     }
 
     // Amend the proguard-map consumer with options from the proguard configuration.
@@ -958,7 +959,7 @@
     if (internal.isGeneratingClassFiles()) {
       internal.outline.enabled = false;
       internal.enableEnumUnboxing = false;
-      internal.enableHorizontalClassMerging = false;
+      internal.horizontalClassMergerOptions().disable();
     }
 
     // EXPERIMENTAL flags.
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 100656f..a69cc03 100644
--- a/src/main/java/com/android/tools/r8/cf/CfVersion.java
+++ b/src/main/java/com/android/tools/r8/cf/CfVersion.java
@@ -23,6 +23,10 @@
   public static final CfVersion V9 = new CfVersion(Opcodes.V9);
   public static final CfVersion V10 = new CfVersion(Opcodes.V10);
   public static final CfVersion V11 = new CfVersion(Opcodes.V11);
+  public static final CfVersion V12 = new CfVersion(Opcodes.V12);
+  public static final CfVersion V13 = new CfVersion(Opcodes.V13);
+  public static final CfVersion V14 = new CfVersion(Opcodes.V14);
+  public static final CfVersion V15 = new CfVersion(Opcodes.V15);
 
   private final int version;
 
@@ -68,7 +72,7 @@
 
   @Override
   public int hashCode() {
-    return HashCodeVisitor.run(this, CfVersion::specify);
+    return HashCodeVisitor.run(this);
   }
 
   @Override
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 32df17b..9f3739a 100644
--- a/src/main/java/com/android/tools/r8/code/CheckCast.java
+++ b/src/main/java/com/android/tools/r8/code/CheckCast.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 public class CheckCast extends Format21c<DexType> {
@@ -44,8 +45,8 @@
   }
 
   @Override
-  int internalCompareBBBB(Format21c<?> other) {
-    return BBBB.compareTo((DexType) other.BBBB);
+  void internalSubSpecify(StructuralSpecification<Format21c<DexType>, ?> spec) {
+    spec.withItem(i -> i.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 ce1c26b..b96f4aa 100644
--- a/src/main/java/com/android/tools/r8/code/ConstClass.java
+++ b/src/main/java/com/android/tools/r8/code/ConstClass.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 public class ConstClass extends Format21c<DexType> {
@@ -29,8 +30,8 @@
   }
 
   @Override
-  int internalCompareBBBB(Format21c<?> other) {
-    return BBBB.compareTo((DexType) other.BBBB);
+  void internalSubSpecify(StructuralSpecification<Format21c<DexType>, ?> spec) {
+    spec.withItem(i -> i.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 0409f06..2f05b0b 100644
--- a/src/main/java/com/android/tools/r8/code/ConstMethodHandle.java
+++ b/src/main/java/com/android/tools/r8/code/ConstMethodHandle.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 public class ConstMethodHandle extends Format21c<DexMethodHandle> {
@@ -51,8 +52,8 @@
   }
 
   @Override
-  int internalCompareBBBB(Format21c<?> other) {
-    return BBBB.compareTo((DexMethodHandle) other.BBBB);
+  void internalSubSpecify(StructuralSpecification<Format21c<DexMethodHandle>, ?> spec) {
+    spec.withItem(i -> i.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 8f54c24..7662fe4 100644
--- a/src/main/java/com/android/tools/r8/code/ConstMethodType.java
+++ b/src/main/java/com/android/tools/r8/code/ConstMethodType.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 public class ConstMethodType extends Format21c<DexProto> {
@@ -50,8 +51,8 @@
   }
 
   @Override
-  int internalCompareBBBB(Format21c<?> other) {
-    return BBBB.compareTo((DexProto) other.BBBB);
+  void internalSubSpecify(StructuralSpecification<Format21c<DexProto>, ?> spec) {
+    spec.withItem(i -> i.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 e9e910d..5fd55e4 100644
--- a/src/main/java/com/android/tools/r8/code/ConstString.java
+++ b/src/main/java/com/android/tools/r8/code/ConstString.java
@@ -13,6 +13,7 @@
 import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 public class ConstString extends Format21c<DexString> {
@@ -34,8 +35,8 @@
   }
 
   @Override
-  int internalCompareBBBB(Format21c<?> other) {
-    return BBBB.compareTo((DexString) other.BBBB);
+  void internalSubSpecify(StructuralSpecification<Format21c<DexString>, ?> spec) {
+    spec.withItem(i -> i.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 ab39c88..8e85bd0 100644
--- a/src/main/java/com/android/tools/r8/code/DexInitClass.java
+++ b/src/main/java/com/android/tools/r8/code/DexInitClass.java
@@ -16,8 +16,9 @@
 import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
-import java.util.Comparator;
 
 public class DexInitClass extends Base2Format {
 
@@ -28,6 +29,10 @@
   private final int dest;
   private final DexType clazz;
 
+  private static void specify(StructuralSpecification<DexInitClass, ?> spec) {
+    spec.withInt(i -> i.dest).withItem(i -> i.clazz);
+  }
+
   public DexInitClass(int dest, DexType clazz) {
     assert clazz.isClassType();
     this.dest = dest;
@@ -127,10 +132,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    return Comparator.comparingInt((DexInitClass i) -> i.dest)
-        .thenComparing(i -> i.clazz)
-        .compare(this, (DexInitClass) other);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (DexInitClass) other, DexInitClass::specify);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/DexItemBasedConstString.java b/src/main/java/com/android/tools/r8/code/DexItemBasedConstString.java
index 18bdfd3..1a434fd 100644
--- a/src/main/java/com/android/tools/r8/code/DexItemBasedConstString.java
+++ b/src/main/java/com/android/tools/r8/code/DexItemBasedConstString.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.naming.dexitembasedstring.NameComputationInfo;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 public class DexItemBasedConstString extends Format21c<DexReference> {
@@ -68,8 +69,8 @@
   }
 
   @Override
-  int internalCompareBBBB(Format21c<?> other) {
-    return BBBB.referenceCompareTo(((DexItemBasedConstString) other).BBBB);
+  void internalSubSpecify(StructuralSpecification<Format21c<DexReference>, ?> spec) {
+    spec.withDexReference(i -> i.BBBB);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/FillArrayDataPayload.java b/src/main/java/com/android/tools/r8/code/FillArrayDataPayload.java
index 52d3e07..cab0270 100644
--- a/src/main/java/com/android/tools/r8/code/FillArrayDataPayload.java
+++ b/src/main/java/com/android/tools/r8/code/FillArrayDataPayload.java
@@ -9,11 +9,11 @@
 import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
-import com.android.tools.r8.utils.ComparatorUtils;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 import java.util.Arrays;
-import java.util.Comparator;
 
 public class FillArrayDataPayload extends Nop {
 
@@ -21,6 +21,10 @@
   public final long size;
   public final short[] data;
 
+  private static void specify(StructuralSpecification<FillArrayDataPayload, ?> spec) {
+    spec.withInt(i -> i.element_width).withLong(i -> i.size).withShortArray(i -> i.data);
+  }
+
   FillArrayDataPayload(int high, BytecodeStream stream) {
     super(high, stream);
     element_width = read16BitValue(stream);
@@ -62,11 +66,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    return Comparator.comparingInt((FillArrayDataPayload i) -> i.element_width)
-        .thenComparingLong(i -> i.size)
-        .thenComparing(i -> i.data, ComparatorUtils::compareShortArray)
-        .compare(this, (FillArrayDataPayload) other);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (FillArrayDataPayload) other, FillArrayDataPayload::specify);
   }
 
   @Override
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 a1ee0ba..5198876 100644
--- a/src/main/java/com/android/tools/r8/code/FilledNewArray.java
+++ b/src/main/java/com/android/tools/r8/code/FilledNewArray.java
@@ -43,11 +43,6 @@
   }
 
   @Override
-  int internalCompareBBBB(Format35c<?> other) {
-    return BBBB.compareTo((DexType) other.BBBB);
-  }
-
-  @Override
   public void collectIndexedItems(
       IndexedItemCollection indexedItems,
       ProgramMethod context,
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 03594c2..de651f5 100644
--- a/src/main/java/com/android/tools/r8/code/FilledNewArrayRange.java
+++ b/src/main/java/com/android/tools/r8/code/FilledNewArrayRange.java
@@ -43,11 +43,6 @@
   }
 
   @Override
-  int internalCompareBBBB(Format3rc<?> other) {
-    return BBBB.compareTo((DexType) other.BBBB);
-  }
-
-  @Override
   public void collectIndexedItems(
       IndexedItemCollection indexedItems,
       ProgramMethod context,
diff --git a/src/main/java/com/android/tools/r8/code/Format10t.java b/src/main/java/com/android/tools/r8/code/Format10t.java
index 528ef92..9b2b11c 100644
--- a/src/main/java/com/android/tools/r8/code/Format10t.java
+++ b/src/main/java/com/android/tools/r8/code/Format10t.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import java.nio.ShortBuffer;
 
 abstract class Format10t extends Base1Format {
@@ -43,8 +44,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    return Byte.compare(AA, ((Format10t) other).AA);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visitInt(AA, ((Format10t) other).AA);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/Format11n.java b/src/main/java/com/android/tools/r8/code/Format11n.java
index 61d845e..27c0d27 100644
--- a/src/main/java/com/android/tools/r8/code/Format11n.java
+++ b/src/main/java/com/android/tools/r8/code/Format11n.java
@@ -10,13 +10,18 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
-import com.android.tools.r8.utils.ComparatorUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 abstract class Format11n extends Base1Format {
 
   public final byte A, B;
 
+  private static void specify(StructuralSpecification<Format11n, ?> spec) {
+    spec.withInt(i -> i.A).withInt(i -> i.B);
+  }
+
   // #+B | vA | op
   /*package*/ Format11n(int high, BytecodeStream stream) {
     super(stream);
@@ -53,9 +58,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    Format11n o = (Format11n) other;
-    return ComparatorUtils.compareInts(A, o.A, B, o.B);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (Format11n) other, Format11n::specify);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/Format11x.java b/src/main/java/com/android/tools/r8/code/Format11x.java
index 4509fbd..3cc3b73 100644
--- a/src/main/java/com/android/tools/r8/code/Format11x.java
+++ b/src/main/java/com/android/tools/r8/code/Format11x.java
@@ -10,6 +10,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import java.nio.ShortBuffer;
 
 abstract class Format11x extends Base1Format {
@@ -43,8 +44,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    return Short.compare(AA, ((Format11x) other).AA);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visitInt(AA, ((Format11x) other).AA);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/Format12x.java b/src/main/java/com/android/tools/r8/code/Format12x.java
index db7ca5a..f3b80aa 100644
--- a/src/main/java/com/android/tools/r8/code/Format12x.java
+++ b/src/main/java/com/android/tools/r8/code/Format12x.java
@@ -10,13 +10,18 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
-import com.android.tools.r8.utils.ComparatorUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 abstract class Format12x extends Base1Format {
 
   public final byte A, B;
 
+  private static void specify(StructuralSpecification<Format12x, ?> spec) {
+    spec.withInt(i -> i.A).withInt(i -> i.B);
+  }
+
   // vB | vA | op
   Format12x(int high, BytecodeStream stream) {
     super(stream);
@@ -47,9 +52,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    Format12x o = (Format12x) other;
-    return ComparatorUtils.compareInts(A, o.A, B, o.B);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (Format12x) other, Format12x::specify);
   }
 
 
diff --git a/src/main/java/com/android/tools/r8/code/Format20t.java b/src/main/java/com/android/tools/r8/code/Format20t.java
index 8f34250..c9cd18b 100644
--- a/src/main/java/com/android/tools/r8/code/Format20t.java
+++ b/src/main/java/com/android/tools/r8/code/Format20t.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import java.nio.ShortBuffer;
 
 abstract class Format20t extends Base2Format {
@@ -43,8 +44,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    return Short.compare(AAAA, ((Format20t) other).AAAA);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visitInt(AAAA, ((Format20t) other).AAAA);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/Format21c.java b/src/main/java/com/android/tools/r8/code/Format21c.java
index 074b79a..9416be7 100644
--- a/src/main/java/com/android/tools/r8/code/Format21c.java
+++ b/src/main/java/com/android/tools/r8/code/Format21c.java
@@ -6,6 +6,8 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.IndexedDexItem;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.function.BiPredicate;
 
 abstract class Format21c<T extends IndexedDexItem> extends Base2Format {
@@ -31,14 +33,16 @@
     return ((BBBB.hashCode() << 8) | AA) ^ getClass().hashCode();
   }
 
+  @SuppressWarnings("unchecked")
   @Override
-  final int internalCompareTo(Instruction other) {
-    Format21c<?> o = (Format21c<?>) other;
-    int aaDiff = Short.compare(AA, o.AA);
-    return aaDiff != 0 ? aaDiff : internalCompareBBBB(o);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(
+        this,
+        (Format21c<T>) other,
+        spec -> spec.withInt(i -> i.AA).withSpec(this::internalSubSpecify));
   }
 
-  abstract int internalCompareBBBB(Format21c<?> other);
+  abstract void internalSubSpecify(StructuralSpecification<Format21c<T>, ?> spec);
 
   @Override
   public String toString(ClassNameMapper naming) {
diff --git a/src/main/java/com/android/tools/r8/code/Format21h.java b/src/main/java/com/android/tools/r8/code/Format21h.java
index 537a020..36b9174 100644
--- a/src/main/java/com/android/tools/r8/code/Format21h.java
+++ b/src/main/java/com/android/tools/r8/code/Format21h.java
@@ -9,7 +9,8 @@
 import com.android.tools.r8.graph.ObjectToOffsetMapping;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
-import com.android.tools.r8.utils.ComparatorUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 abstract class Format21h extends Base2Format {
@@ -17,6 +18,10 @@
   public final short AA;
   public final char BBBB;
 
+  private static void specify(StructuralSpecification<Format21h, ?> spec) {
+    spec.withInt(i -> i.AA).withInt(i -> i.BBBB);
+  }
+
   // AA | op | BBBB0000[00000000]
   /*package*/ Format21h(int high, BytecodeStream stream) {
     super(stream);
@@ -48,9 +53,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    Format21h o = (Format21h) other;
-    return ComparatorUtils.compareInts(AA, o.AA, BBBB, o.BBBB);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (Format21h) other, Format21h::specify);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/Format21s.java b/src/main/java/com/android/tools/r8/code/Format21s.java
index 3f4e0fc..550fdd4 100644
--- a/src/main/java/com/android/tools/r8/code/Format21s.java
+++ b/src/main/java/com/android/tools/r8/code/Format21s.java
@@ -10,8 +10,9 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
-import com.android.tools.r8.utils.ComparatorUtils;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 abstract class Format21s extends Base2Format {
@@ -19,6 +20,10 @@
   public final short AA;
   public final short BBBB;
 
+  private static void specify(StructuralSpecification<Format21s, ?> spec) {
+    spec.withInt(i -> i.AA).withInt(i -> i.BBBB);
+  }
+
   // AA | op | #+BBBB
   /*package*/ Format21s(int high, BytecodeStream stream) {
     super(stream);
@@ -50,9 +55,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    Format21s o = (Format21s) other;
-    return ComparatorUtils.compareInts(AA, o.AA, BBBB, o.BBBB);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (Format21s) other, Format21s::specify);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/Format21t.java b/src/main/java/com/android/tools/r8/code/Format21t.java
index e580a1f..bde930d 100644
--- a/src/main/java/com/android/tools/r8/code/Format21t.java
+++ b/src/main/java/com/android/tools/r8/code/Format21t.java
@@ -13,7 +13,8 @@
 import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
-import com.android.tools.r8.utils.ComparatorUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 public abstract class Format21t extends Base2Format {
@@ -21,6 +22,10 @@
   public final short AA;
   public /* offset */ short BBBB;
 
+  private static void specify(StructuralSpecification<Format21t, ?> spec) {
+    spec.withInt(i -> i.AA).withInt(i -> i.BBBB);
+  }
+
   // AA | op | +BBBB
   Format21t(int high, BytecodeStream stream) {
     super(stream);
@@ -52,9 +57,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    Format21t o = (Format21t) other;
-    return ComparatorUtils.compareInts(AA, o.AA, BBBB, o.BBBB);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (Format21t) other, Format21t::specify);
   }
 
   public abstract Type getType();
diff --git a/src/main/java/com/android/tools/r8/code/Format22b.java b/src/main/java/com/android/tools/r8/code/Format22b.java
index 5bbd0da..6f79b50 100644
--- a/src/main/java/com/android/tools/r8/code/Format22b.java
+++ b/src/main/java/com/android/tools/r8/code/Format22b.java
@@ -10,8 +10,9 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
-import com.android.tools.r8.utils.ComparatorUtils;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 public abstract class Format22b extends Base2Format {
@@ -20,6 +21,10 @@
   public final short BB;
   public final byte CC;
 
+  private static void specify(StructuralSpecification<Format22b, ?> spec) {
+    spec.withInt(i -> i.AA).withInt(i -> i.BB).withInt(i -> i.CC);
+  }
+
   // vAA | op | #+CC | VBB
   /*package*/ Format22b(int high, BytecodeStream stream) {
     super(stream);
@@ -54,9 +59,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    Format22b o = (Format22b) other;
-    return ComparatorUtils.compareInts(AA, o.AA, BB, o.BB, CC, o.CC);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (Format22b) other, Format22b::specify);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/Format22c.java b/src/main/java/com/android/tools/r8/code/Format22c.java
index bd16c8f..536ec65 100644
--- a/src/main/java/com/android/tools/r8/code/Format22c.java
+++ b/src/main/java/com/android/tools/r8/code/Format22c.java
@@ -7,7 +7,8 @@
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.IndexedDexItem;
 import com.android.tools.r8.naming.ClassNameMapper;
-import com.android.tools.r8.utils.ComparatorUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.function.BiPredicate;
 
 public abstract class Format22c<T extends DexReference> extends Base2Format {
@@ -16,6 +17,10 @@
   public final byte B;
   public T CCCC;
 
+  private static void specify(StructuralSpecification<Format22c<? extends DexReference>, ?> spec) {
+    spec.withInt(i -> i.A).withInt(i -> i.B).withDexReference(i -> i.CCCC);
+  }
+
   // vB | vA | op | [type|field]@CCCC
   /*package*/ Format22c(int high, BytecodeStream stream, T[] map) {
     super(stream);
@@ -38,10 +43,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    Format22c<? extends DexReference> o = (Format22c<? extends DexReference>) other;
-    int diff = ComparatorUtils.compareInts(A, o.A, B, o.B);
-    return diff != 0 ? diff : CCCC.referenceCompareTo(o.CCCC);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (Format22c<? extends DexReference>) other, Format22c::specify);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/Format22s.java b/src/main/java/com/android/tools/r8/code/Format22s.java
index 8492eae..efa14d2 100644
--- a/src/main/java/com/android/tools/r8/code/Format22s.java
+++ b/src/main/java/com/android/tools/r8/code/Format22s.java
@@ -10,8 +10,9 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
-import com.android.tools.r8.utils.ComparatorUtils;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 public abstract class Format22s extends Base2Format {
@@ -20,6 +21,10 @@
   public final byte B;
   public final short CCCC;
 
+  private static void specify(StructuralSpecification<Format22s, ?> spec) {
+    spec.withInt(i -> i.A).withInt(i -> i.B).withInt(i -> i.CCCC);
+  }
+
   // vB | vA | op | #+CCCC
   /*package*/ Format22s(int high, BytecodeStream stream) {
     super(stream);
@@ -54,9 +59,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    Format22s o = (Format22s) other;
-    return ComparatorUtils.compareInts(A, o.A, B, o.B, CCCC, o.CCCC);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (Format22s) other, Format22s::specify);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/Format22t.java b/src/main/java/com/android/tools/r8/code/Format22t.java
index 5c64988..9688b4e 100644
--- a/src/main/java/com/android/tools/r8/code/Format22t.java
+++ b/src/main/java/com/android/tools/r8/code/Format22t.java
@@ -13,7 +13,8 @@
 import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
-import com.android.tools.r8.utils.ComparatorUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 public abstract class Format22t extends Base2Format {
@@ -22,6 +23,10 @@
   public final byte B;
   public /* offset */ short CCCC;
 
+  private static void specify(StructuralSpecification<Format22t, ?> spec) {
+    spec.withInt(i -> i.A).withInt(i -> i.B).withInt(i -> i.CCCC);
+  }
+
   // vB | vA | op | +CCCC
   Format22t(int high, BytecodeStream stream) {
     super(stream);
@@ -56,9 +61,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    Format22t o = (Format22t) other;
-    return ComparatorUtils.compareInts(A, o.A, B, o.B, CCCC, o.CCCC);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (Format22t) other, Format22t::specify);
   }
 
   public abstract Type getType();
diff --git a/src/main/java/com/android/tools/r8/code/Format22x.java b/src/main/java/com/android/tools/r8/code/Format22x.java
index 626e7f9..1d9d8ba 100644
--- a/src/main/java/com/android/tools/r8/code/Format22x.java
+++ b/src/main/java/com/android/tools/r8/code/Format22x.java
@@ -10,7 +10,8 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
-import com.android.tools.r8.utils.ComparatorUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 abstract class Format22x extends Base2Format {
@@ -18,6 +19,10 @@
   public final short AA;
   public final char BBBB;
 
+  private static void specify(StructuralSpecification<Format22x, ?> spec) {
+    spec.withInt(i -> i.AA).withInt(i -> i.BBBB);
+  }
+
   // AA | op | vBBBB
   Format22x(int high, BytecodeStream stream) {
     super(stream);
@@ -49,9 +54,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    Format22x o = (Format22x) other;
-    return ComparatorUtils.compareInts(AA, o.AA, BBBB, o.BBBB);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (Format22x) other, Format22x::specify);
   }
 
 
diff --git a/src/main/java/com/android/tools/r8/code/Format23x.java b/src/main/java/com/android/tools/r8/code/Format23x.java
index a87dddf..ca3ef80 100644
--- a/src/main/java/com/android/tools/r8/code/Format23x.java
+++ b/src/main/java/com/android/tools/r8/code/Format23x.java
@@ -10,7 +10,8 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
-import com.android.tools.r8.utils.ComparatorUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 abstract class Format23x extends Base2Format {
@@ -19,6 +20,10 @@
   public final short BB;
   public final short CC;
 
+  private static void specify(StructuralSpecification<Format23x, ?> spec) {
+    spec.withInt(i -> i.AA).withInt(i -> i.BB).withInt(i -> i.CC);
+  }
+
   // vAA | op | vCC | vBB
   Format23x(int high, BytecodeStream stream) {
     super(stream);
@@ -53,9 +58,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    Format23x o = (Format23x) other;
-    return ComparatorUtils.compareInts(AA, o.AA, BB, o.BB, CC, o.CC);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (Format23x) other, Format23x::specify);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/Format30t.java b/src/main/java/com/android/tools/r8/code/Format30t.java
index 81dcf86..ba986cc 100644
--- a/src/main/java/com/android/tools/r8/code/Format30t.java
+++ b/src/main/java/com/android/tools/r8/code/Format30t.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 import java.nio.ShortBuffer;
 
 abstract class Format30t extends Base3Format {
@@ -42,8 +43,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    return Integer.compare(AAAAAAAA, ((Format30t) other).AAAAAAAA);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visitInt(AAAAAAAA, ((Format30t) other).AAAAAAAA);
   }
 
   @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 672057b..6664662 100644
--- a/src/main/java/com/android/tools/r8/code/Format31c.java
+++ b/src/main/java/com/android/tools/r8/code/Format31c.java
@@ -13,6 +13,8 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 import java.util.function.BiPredicate;
 
@@ -21,6 +23,10 @@
   public final short AA;
   public DexString BBBBBBBB;
 
+  private static void specify(StructuralSpecification<Format31c, ?> spec) {
+    spec.withInt(i -> i.AA).withItem(i -> i.BBBBBBBB);
+  }
+
   // vAA | op | string@BBBBlo | string@#+BBBBhi
   Format31c(int high, BytecodeStream stream, DexString[] map) {
     super(stream);
@@ -51,10 +57,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    Format31c o = (Format31c) other;
-    int diff = Short.compare(AA, o.AA);
-    return diff != 0 ? diff : BBBBBBBB.compareTo(o.BBBBBBBB);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (Format31c) other, Format31c::specify);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/Format31i.java b/src/main/java/com/android/tools/r8/code/Format31i.java
index 3d0d27e..c2fa1b1 100644
--- a/src/main/java/com/android/tools/r8/code/Format31i.java
+++ b/src/main/java/com/android/tools/r8/code/Format31i.java
@@ -10,7 +10,8 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
-import com.android.tools.r8.utils.ComparatorUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 abstract class Format31i extends Base3Format {
@@ -18,6 +19,10 @@
   public final short AA;
   public final int BBBBBBBB;
 
+  private static void specify(StructuralSpecification<Format31i, ?> spec) {
+    spec.withInt(i -> i.AA).withInt(i -> i.BBBBBBBB);
+  }
+
   // vAA | op | #+BBBBlo | #+BBBBhi
   /*package*/ Format31i(int high, BytecodeStream stream) {
     super(stream);
@@ -48,9 +53,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    Format31i o = (Format31i) other;
-    return ComparatorUtils.compareInts(AA, o.AA, BBBBBBBB, o.BBBBBBBB);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (Format31i) other, Format31i::specify);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/Format31t.java b/src/main/java/com/android/tools/r8/code/Format31t.java
index 8e36d79..f85aca6 100644
--- a/src/main/java/com/android/tools/r8/code/Format31t.java
+++ b/src/main/java/com/android/tools/r8/code/Format31t.java
@@ -10,7 +10,8 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
-import com.android.tools.r8.utils.ComparatorUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 public abstract class Format31t extends Base3Format {
@@ -18,6 +19,10 @@
   public final short AA;
   protected /* offset */ int BBBBBBBB;
 
+  private static void specify(StructuralSpecification<Format31t, ?> spec) {
+    spec.withInt(i -> i.AA).withInt(i -> i.BBBBBBBB);
+  }
+
   // vAA | op | +BBBBlo | +BBBBhi
   Format31t(int high, BytecodeStream stream) {
     super(stream);
@@ -63,9 +68,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    Format31t o = (Format31t) other;
-    return ComparatorUtils.compareInts(AA, o.AA, BBBBBBBB, o.BBBBBBBB);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (Format31t) other, Format31t::specify);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/Format32x.java b/src/main/java/com/android/tools/r8/code/Format32x.java
index a5360db..c637383 100644
--- a/src/main/java/com/android/tools/r8/code/Format32x.java
+++ b/src/main/java/com/android/tools/r8/code/Format32x.java
@@ -11,7 +11,8 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
-import com.android.tools.r8.utils.ComparatorUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 abstract class Format32x extends Base3Format {
@@ -19,6 +20,10 @@
   public final int AAAA;
   public final int BBBB;
 
+  private static void specify(StructuralSpecification<Format32x, ?> spec) {
+    spec.withInt(i -> i.AAAA).withInt(i -> i.BBBB);
+  }
+
   // øø | op | AAAA | BBBB
   Format32x(int high, BytecodeStream stream) {
     super(stream);
@@ -51,9 +56,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    Format32x o = (Format32x) other;
-    return ComparatorUtils.compareInts(AAAA, o.AAAA, BBBB, o.BBBB);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (Format32x) other, Format32x::specify);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/Format35c.java b/src/main/java/com/android/tools/r8/code/Format35c.java
index 8e446db..e5d2489 100644
--- a/src/main/java/com/android/tools/r8/code/Format35c.java
+++ b/src/main/java/com/android/tools/r8/code/Format35c.java
@@ -6,10 +6,12 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.IndexedDexItem;
 import com.android.tools.r8.naming.ClassNameMapper;
-import com.android.tools.r8.utils.ComparatorUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.function.BiPredicate;
 
-public abstract class Format35c<T extends IndexedDexItem> extends Base3Format {
+public abstract class Format35c<T extends IndexedDexItem & StructuralItem<T>> extends Base3Format {
 
   public final byte A;
   public final byte C;
@@ -19,6 +21,17 @@
   public final byte G;
   public T BBBB;
 
+  private static <T extends IndexedDexItem & StructuralItem<T>> void specify(
+      StructuralSpecification<Format35c<T>, ?> spec) {
+    spec.withInt(i -> i.A)
+        .withInt(i -> i.C)
+        .withInt(i -> i.D)
+        .withInt(i -> i.E)
+        .withInt(i -> i.F)
+        .withInt(i -> i.G)
+        .withItem(i -> i.BBBB);
+  }
+
   // A | G | op | BBBB | F | E | D | C
   Format35c(int high, BytecodeStream stream, T[] map) {
     super(stream);
@@ -55,22 +68,12 @@
         | G) ^ getClass().hashCode();
   }
 
+  @SuppressWarnings("unchecked")
   @Override
-  final int internalCompareTo(Instruction other) {
-    Format35c<?> o = (Format35c<?>) other;
-    int diff =
-        ComparatorUtils.compareInts(
-            A, o.A,
-            C, o.C,
-            D, o.D,
-            E, o.E,
-            F, o.F,
-            G, o.G);
-    return diff != 0 ? diff : internalCompareBBBB(o);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (Format35c<T>) other, Format35c::specify);
   }
 
-  abstract int internalCompareBBBB(Format35c<?> other);
-
   private void appendRegisterArguments(StringBuilder builder, String separator) {
     builder.append("{ ");
     int[] values = new int[]{C, D, E, F, G};
diff --git a/src/main/java/com/android/tools/r8/code/Format3rc.java b/src/main/java/com/android/tools/r8/code/Format3rc.java
index 9102ecb..0ceeb4a 100644
--- a/src/main/java/com/android/tools/r8/code/Format3rc.java
+++ b/src/main/java/com/android/tools/r8/code/Format3rc.java
@@ -6,15 +6,22 @@
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.IndexedDexItem;
 import com.android.tools.r8.naming.ClassNameMapper;
-import com.android.tools.r8.utils.ComparatorUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.function.BiPredicate;
 
-public abstract class Format3rc<T extends IndexedDexItem> extends Base3Format {
+public abstract class Format3rc<T extends IndexedDexItem & StructuralItem<T>> extends Base3Format {
 
   public final short AA;
   public final char CCCC;
   public T BBBB;
 
+  private static <T extends IndexedDexItem & StructuralItem<T>> void specify(
+      StructuralSpecification<Format3rc<T>, ?> spec) {
+    spec.withInt(i -> i.AA).withInt(i -> i.CCCC).withItem(i -> i.BBBB);
+  }
+
   // AA | op | [meth|type]@BBBBB | CCCC
   Format3rc(int high, BytecodeStream stream, T[] map) {
     super(stream);
@@ -40,15 +47,12 @@
     return ((CCCC << 24) | (BBBB.hashCode() << 4) | AA) ^ getClass().hashCode();
   }
 
+  @SuppressWarnings("unchecked")
   @Override
-  final int internalCompareTo(Instruction other) {
-    Format3rc<?> o = (Format3rc<?>) other;
-    int diff = ComparatorUtils.compareInts(AA, o.AA, CCCC, o.CCCC);
-    return diff != 0 ? diff : internalCompareBBBB(o);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (Format3rc<T>) other, Format3rc::specify);
   }
 
-  abstract int internalCompareBBBB(Format3rc<?> other);
-
   private void appendRegisterRange(StringBuilder builder) {
     int firstRegister = CCCC;
     builder.append("{ ");
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 bb0e1c7..1542015 100644
--- a/src/main/java/com/android/tools/r8/code/Format45cc.java
+++ b/src/main/java/com/android/tools/r8/code/Format45cc.java
@@ -16,7 +16,8 @@
 import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
-import com.android.tools.r8.utils.ComparatorUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 /** Format45cc for instructions of size 4, with 5 registers and 2 constant pool index. */
@@ -31,6 +32,17 @@
   public DexMethod BBBB;
   public DexProto HHHH;
 
+  private static void specify(StructuralSpecification<Format45cc, ?> spec) {
+    spec.withInt(i -> i.A)
+        .withInt(i -> i.C)
+        .withInt(i -> i.D)
+        .withInt(i -> i.E)
+        .withInt(i -> i.F)
+        .withInt(i -> i.G)
+        .withItem(i -> i.BBBB)
+        .withItem(i -> i.HHHH);
+  }
+
   Format45cc(int high, BytecodeStream stream, DexMethod[] methodMap, DexProto[] protoMap) {
     super(stream);
     G = (byte) (high & 0xf);
@@ -77,21 +89,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    Format45cc o = (Format45cc) other;
-    int diff =
-        ComparatorUtils.compareInts(
-            A, o.A,
-            C, o.C,
-            D, o.D,
-            E, o.E,
-            F, o.F,
-            G, o.G);
-    if (diff != 0) {
-      return diff;
-    }
-    int bDiff = BBBB.compareTo(o.BBBB);
-    return bDiff != 0 ? bDiff : HHHH.compareTo(o.HHHH);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (Format45cc) other, Format45cc::specify);
   }
 
   @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 f29357d..4da5b40 100644
--- a/src/main/java/com/android/tools/r8/code/Format4rcc.java
+++ b/src/main/java/com/android/tools/r8/code/Format4rcc.java
@@ -15,8 +15,9 @@
 import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
-import java.util.Comparator;
 import java.util.function.BiPredicate;
 
 /** Format4rcc for instructions of size 4, with a range of registers and 2 constant pool index. */
@@ -27,6 +28,10 @@
   public DexMethod BBBB;
   public DexProto HHHH;
 
+  private static void specify(StructuralSpecification<Format4rcc, ?> spec) {
+    spec.withInt(i -> i.AA).withInt(i -> i.CCCC).withItem(i -> i.BBBB).withItem(i -> i.HHHH);
+  }
+
   // AA | op | [meth]@BBBB | CCCC | [proto]@HHHH
   Format4rcc(int high, BytecodeStream stream, DexMethod[] methodMap, DexProto[] protoMap) {
     super(stream);
@@ -70,12 +75,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    return Comparator.comparingInt((Format4rcc i) -> i.AA)
-        .thenComparingInt(i -> i.CCCC)
-        .thenComparing(i -> i.BBBB)
-        .thenComparing(i -> i.HHHH)
-        .compare(this, (Format4rcc) other);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (Format4rcc) other, Format4rcc::specify);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/Format51l.java b/src/main/java/com/android/tools/r8/code/Format51l.java
index 6c583c4..1417bf8 100644
--- a/src/main/java/com/android/tools/r8/code/Format51l.java
+++ b/src/main/java/com/android/tools/r8/code/Format51l.java
@@ -10,14 +10,19 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
-import java.util.Comparator;
 
 abstract class Format51l extends Base5Format {
 
   public final short AA;
   public final long BBBBBBBBBBBBBBBB;
 
+  private static void specify(StructuralSpecification<Format51l, ?> spec) {
+    spec.withInt(i -> i.AA).withLong(i -> i.BBBBBBBBBBBBBBBB);
+  }
+
   // AA | op | BBBB | BBBB | BBBB | BBBB
   Format51l(int high, BytecodeStream stream) {
     super(stream);
@@ -48,10 +53,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    return Comparator.comparingInt((Format51l i) -> i.AA)
-        .thenComparingLong(i -> i.BBBBBBBBBBBBBBBB)
-        .compare(this, (Format51l) other);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (Format51l) other, Format51l::specify);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/Instruction.java b/src/main/java/com/android/tools/r8/code/Instruction.java
index 52aa90d..977dafd 100644
--- a/src/main/java/com/android/tools/r8/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/code/Instruction.java
@@ -6,6 +6,7 @@
 import com.android.tools.r8.cf.code.CfInstruction;
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.errors.InternalCompilerError;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.DexCallSite;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
@@ -19,10 +20,15 @@
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.Equatable;
+import com.android.tools.r8.utils.structural.HashingVisitor;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralMapping;
 import java.nio.ShortBuffer;
 import java.util.function.BiPredicate;
 
-public abstract class Instruction implements CfOrDexInstruction, Comparable<Instruction> {
+public abstract class Instruction implements CfOrDexInstruction, StructuralItem<Instruction> {
   public static final Instruction[] EMPTY_ARRAY = {};
 
   public final static int[] NO_TARGETS = null;
@@ -294,26 +300,39 @@
 
   @Override
   public final boolean equals(Object other) {
-    return other instanceof Instruction && compareTo((Instruction) other) == 0;
+    return Equatable.equalsImpl(this, other);
   }
 
   @Override
   public abstract int hashCode();
 
+  @Override
+  public Instruction self() {
+    return this;
+  }
+
+  @Override
+  public StructuralMapping<Instruction> getStructuralMapping() {
+    throw new Unreachable();
+  }
+
   int getCompareToId() {
     return getOpcode();
   }
 
   // Abstract compare-to called only if the opcode/compare-id of the instruction matches.
-  abstract int internalCompareTo(Instruction other);
+  abstract int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor);
 
   @Override
-  public final int compareTo(Instruction other) {
-    if (this == other) {
-      return 0;
-    }
-    int opcodeDiff = Integer.compare(getCompareToId(), other.getCompareToId());
-    return opcodeDiff != 0 ? opcodeDiff : internalCompareTo(other);
+  public final int acceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    int opcodeDiff = visitor.visitInt(getCompareToId(), other.getCompareToId());
+    return opcodeDiff != 0 ? opcodeDiff : internalAcceptCompareTo(other, visitor);
+  }
+
+  @Override
+  public final void acceptHashing(HashingVisitor visitor) {
+    // Rather than traverse the full instruction, the compare ID will likely give a reasonable hash.
+    visitor.visitInt(getCompareToId());
   }
 
   public abstract String getName();
diff --git a/src/main/java/com/android/tools/r8/code/InvokeCustom.java b/src/main/java/com/android/tools/r8/code/InvokeCustom.java
index fb6660a..1355f13 100644
--- a/src/main/java/com/android/tools/r8/code/InvokeCustom.java
+++ b/src/main/java/com/android/tools/r8/code/InvokeCustom.java
@@ -44,11 +44,6 @@
   }
 
   @Override
-  int internalCompareBBBB(Format35c<?> other) {
-    return BBBB.compareTo((DexCallSite) other.BBBB);
-  }
-
-  @Override
   public void collectIndexedItems(
       IndexedItemCollection indexedItems,
       ProgramMethod context,
diff --git a/src/main/java/com/android/tools/r8/code/InvokeCustomRange.java b/src/main/java/com/android/tools/r8/code/InvokeCustomRange.java
index 71e8399..2d09e4f 100644
--- a/src/main/java/com/android/tools/r8/code/InvokeCustomRange.java
+++ b/src/main/java/com/android/tools/r8/code/InvokeCustomRange.java
@@ -44,11 +44,6 @@
   }
 
   @Override
-  int internalCompareBBBB(Format3rc<?> other) {
-    return BBBB.compareTo((DexCallSite) other.BBBB);
-  }
-
-  @Override
   public void collectIndexedItems(
       IndexedItemCollection indexedItems,
       ProgramMethod context,
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 ec8e6c4..9e5d8f2 100644
--- a/src/main/java/com/android/tools/r8/code/InvokeMethod.java
+++ b/src/main/java/com/android/tools/r8/code/InvokeMethod.java
@@ -42,11 +42,6 @@
   public abstract Invoke.Type getInvokeType();
 
   @Override
-  int internalCompareBBBB(Format35c<?> other) {
-    return BBBB.compareTo((DexMethod) other.BBBB);
-  }
-
-  @Override
   public void write(
       ShortBuffer dest,
       ProgramMethod context,
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 19c2bf1..ba248e0 100644
--- a/src/main/java/com/android/tools/r8/code/InvokeMethodRange.java
+++ b/src/main/java/com/android/tools/r8/code/InvokeMethodRange.java
@@ -42,11 +42,6 @@
   public abstract Type getInvokeType();
 
   @Override
-  int internalCompareBBBB(Format3rc<?> other) {
-    return BBBB.compareTo((DexMethod) other.BBBB);
-  }
-
-  @Override
   public void write(
       ShortBuffer dest,
       ProgramMethod context,
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 0b90d1a..44d2613 100644
--- a/src/main/java/com/android/tools/r8/code/NewInstance.java
+++ b/src/main/java/com/android/tools/r8/code/NewInstance.java
@@ -12,6 +12,7 @@
 import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.ir.conversion.IRBuilder;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 public class NewInstance extends Format21c<DexType> {
@@ -44,8 +45,8 @@
   }
 
   @Override
-  int internalCompareBBBB(Format21c<?> other) {
-    return BBBB.compareTo((DexType) other.BBBB);
+  void internalSubSpecify(StructuralSpecification<Format21c<DexType>, ?> spec) {
+    spec.withItem(i -> i.BBBB);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/Nop.java b/src/main/java/com/android/tools/r8/code/Nop.java
index ae56ec8..1e46cfc 100644
--- a/src/main/java/com/android/tools/r8/code/Nop.java
+++ b/src/main/java/com/android/tools/r8/code/Nop.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.code;
 
 import com.android.tools.r8.ir.conversion.IRBuilder;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 
 public class Nop extends Format10x {
 
@@ -33,7 +34,7 @@
 
   // Notice that this must be overridden by the "Nop" subtypes!
   @Override
-  int internalCompareTo(Instruction other) {
+  int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
     return DexCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
   }
 
diff --git a/src/main/java/com/android/tools/r8/code/PackedSwitchPayload.java b/src/main/java/com/android/tools/r8/code/PackedSwitchPayload.java
index 4ea070c..3d297bb 100644
--- a/src/main/java/com/android/tools/r8/code/PackedSwitchPayload.java
+++ b/src/main/java/com/android/tools/r8/code/PackedSwitchPayload.java
@@ -8,11 +8,11 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
-import com.android.tools.r8.utils.ComparatorUtils;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 import java.util.Arrays;
-import java.util.Comparator;
 
 public class PackedSwitchPayload extends SwitchPayload {
 
@@ -20,6 +20,10 @@
   public final int first_key;
   public final /* offset */ int[] targets;
 
+  private static void specify(StructuralSpecification<PackedSwitchPayload, ?> spec) {
+    spec.withInt(i -> i.size).withInt(i -> i.first_key).withIntArray(i -> i.targets);
+  }
+
   public PackedSwitchPayload(int high, BytecodeStream stream) {
     super(high, stream);
     size = read16BitValue(stream);
@@ -58,11 +62,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    return Comparator.comparingInt((PackedSwitchPayload i) -> i.size)
-        .thenComparingInt(i -> first_key)
-        .thenComparing(i -> i.targets, ComparatorUtils::compareIntArray)
-        .compare(this, (PackedSwitchPayload) other);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (PackedSwitchPayload) other, PackedSwitchPayload::specify);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/code/ReturnVoid.java b/src/main/java/com/android/tools/r8/code/ReturnVoid.java
index 68cfb51..32d75f8 100644
--- a/src/main/java/com/android/tools/r8/code/ReturnVoid.java
+++ b/src/main/java/com/android/tools/r8/code/ReturnVoid.java
@@ -4,6 +4,7 @@
 package com.android.tools.r8.code;
 
 import com.android.tools.r8.ir.conversion.IRBuilder;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
 
 public class ReturnVoid extends Format10x {
 
@@ -33,7 +34,7 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
     return DexCompareHelper.compareIdUniquelyDeterminesEquality(this, other);
   }
 
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 4524f8e..499d679 100644
--- a/src/main/java/com/android/tools/r8/code/SgetOrSput.java
+++ b/src/main/java/com/android/tools/r8/code/SgetOrSput.java
@@ -9,6 +9,7 @@
 import com.android.tools.r8.graph.ObjectToOffsetMapping;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 
 abstract class SgetOrSput extends Format21c<DexField> {
@@ -49,7 +50,7 @@
   }
 
   @Override
-  int internalCompareBBBB(Format21c<?> other) {
-    return BBBB.compareTo((DexField) other.BBBB);
+  void internalSubSpecify(StructuralSpecification<Format21c<DexField>, ?> spec) {
+    spec.withItem(i -> i.BBBB);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/code/SparseSwitchPayload.java b/src/main/java/com/android/tools/r8/code/SparseSwitchPayload.java
index 1a1b1e9..9dac193e 100644
--- a/src/main/java/com/android/tools/r8/code/SparseSwitchPayload.java
+++ b/src/main/java/com/android/tools/r8/code/SparseSwitchPayload.java
@@ -8,11 +8,11 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.naming.ClassNameMapper;
-import com.android.tools.r8.utils.ComparatorUtils;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.nio.ShortBuffer;
 import java.util.Arrays;
-import java.util.Comparator;
 
 public class SparseSwitchPayload extends SwitchPayload {
 
@@ -20,6 +20,10 @@
   public final int[] keys;
   public final /* offset */ int[] targets;
 
+  private static void specify(StructuralSpecification<SparseSwitchPayload, ?> spec) {
+    spec.withInt(i -> i.size).withIntArray(i -> i.keys).withIntArray(i -> i.targets);
+  }
+
   public SparseSwitchPayload(int high, BytecodeStream stream) {
     super(high, stream);
     size = read16BitValue(stream);
@@ -64,11 +68,8 @@
   }
 
   @Override
-  final int internalCompareTo(Instruction other) {
-    return Comparator.comparingInt((SparseSwitchPayload i) -> i.size)
-        .thenComparing(i -> i.keys, ComparatorUtils::compareIntArray)
-        .thenComparing(i -> i.targets, ComparatorUtils::compareIntArray)
-        .compare(this, (SparseSwitchPayload) other);
+  final int internalAcceptCompareTo(Instruction other, CompareToVisitor visitor) {
+    return visitor.visit(this, (SparseSwitchPayload) other, SparseSwitchPayload::specify);
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/dex/Constants.java b/src/main/java/com/android/tools/r8/dex/Constants.java
index e3801e6..dfa8667 100644
--- a/src/main/java/com/android/tools/r8/dex/Constants.java
+++ b/src/main/java/com/android/tools/r8/dex/Constants.java
@@ -140,6 +140,7 @@
   public static final String JAVA_LANG_OBJECT_NAME = "java/lang/Object";
   public static final String INSTANCE_INITIALIZER_NAME = "<init>";
   public static final String CLASS_INITIALIZER_NAME = "<clinit>";
+  public static final String TEMPORARY_INSTANCE_INITIALIZER_PREFIX = "$r8$constructor";
 
   public static final int MAX_NON_JUMBO_INDEX = U16BIT_MAX;
 
diff --git a/src/main/java/com/android/tools/r8/errors/ExperimentalClassFileVersionDiagnostic.java b/src/main/java/com/android/tools/r8/errors/ExperimentalClassFileVersionDiagnostic.java
new file mode 100644
index 0000000..953afb2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/errors/ExperimentalClassFileVersionDiagnostic.java
@@ -0,0 +1,35 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.errors;
+
+import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.origin.Origin;
+import com.android.tools.r8.position.Position;
+
+public class ExperimentalClassFileVersionDiagnostic implements Diagnostic {
+
+  private final String message;
+  private final Origin origin;
+
+  public ExperimentalClassFileVersionDiagnostic(Origin origin, String message) {
+    this.origin = origin;
+    this.message = message;
+  }
+
+  @Override
+  public Origin getOrigin() {
+    return origin;
+  }
+
+  @Override
+  public Position getPosition() {
+    return Position.UNKNOWN;
+  }
+
+  @Override
+  public String getDiagnosticMessage() {
+    return message;
+  }
+}
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 b6588df..ec95ade 100644
--- a/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java
+++ b/src/main/java/com/android/tools/r8/features/ClassToFeatureSplitMap.java
@@ -16,6 +16,7 @@
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.utils.InternalOptions;
 import com.android.tools.r8.utils.Reporter;
 import com.google.common.collect.Sets;
@@ -135,11 +136,11 @@
     return rewrittenClassToFeatureSplitMap;
   }
 
-  public ClassToFeatureSplitMap withoutPrunedClasses(Set<DexType> prunedClasses) {
+  public ClassToFeatureSplitMap withoutPrunedItems(PrunedItems prunedItems) {
     ClassToFeatureSplitMap classToFeatureSplitMapAfterPruning = new ClassToFeatureSplitMap();
     classToFeatureSplitMap.forEach(
         (type, featureSplit) -> {
-          if (!prunedClasses.contains(type)) {
+          if (!prunedItems.getRemovedClasses().contains(type)) {
             classToFeatureSplitMapAfterPruning.classToFeatureSplitMap.put(type, featureSplit);
           }
         });
diff --git a/src/main/java/com/android/tools/r8/graph/AppServices.java b/src/main/java/com/android/tools/r8/graph/AppServices.java
index 7fe273a..c0c3976 100644
--- a/src/main/java/com/android/tools/r8/graph/AppServices.java
+++ b/src/main/java/com/android/tools/r8/graph/AppServices.java
@@ -26,7 +26,6 @@
 import java.io.IOException;
 import java.nio.charset.Charset;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -139,11 +138,11 @@
     return new AppServices(appView, rewrittenFeatureMappings.build());
   }
 
-  public AppServices prunedCopy(Collection<DexType> removedClasses) {
+  public AppServices prunedCopy(PrunedItems prunedItems) {
     ImmutableMap.Builder<DexType, Map<FeatureSplit, List<DexType>>> rewrittenServicesBuilder =
         ImmutableMap.builder();
     for (Entry<DexType, Map<FeatureSplit, List<DexType>>> entry : services.entrySet()) {
-      if (removedClasses.contains(entry.getKey())) {
+      if (prunedItems.getRemovedClasses().contains(entry.getKey())) {
         continue;
       }
       ImmutableMap.Builder<FeatureSplit, List<DexType>> prunedFeatureSplitImpls =
@@ -152,7 +151,7 @@
         ImmutableList.Builder<DexType> rewrittenServiceImplementationTypesBuilder =
             ImmutableList.builder();
         for (DexType serviceImplementationType : featureSplitEntry.getValue()) {
-          if (!removedClasses.contains(serviceImplementationType)) {
+          if (!prunedItems.getRemovedClasses().contains(serviceImplementationType)) {
             rewrittenServiceImplementationTypesBuilder.add(serviceImplementationType);
           }
         }
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 ce2d937..8e0800b 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -22,6 +22,7 @@
 import com.android.tools.r8.ir.conversion.MethodProcessingId;
 import com.android.tools.r8.ir.desugar.PrefixRewritingMapper;
 import com.android.tools.r8.ir.optimize.CallSiteOptimizationInfoPropagator;
+import com.android.tools.r8.ir.optimize.enums.EnumDataMap;
 import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfoFactory;
 import com.android.tools.r8.ir.optimize.library.LibraryMemberOptimizer;
 import com.android.tools.r8.ir.optimize.library.LibraryMethodSideEffectModelCollection;
@@ -38,8 +39,6 @@
 import com.android.tools.r8.utils.ThrowingConsumer;
 import com.google.common.base.Predicates;
 import com.google.common.collect.ImmutableSet;
-import java.util.Collection;
-import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.Map;
 import java.util.Set;
@@ -87,7 +86,7 @@
   private HorizontallyMergedClasses horizontallyMergedClasses;
   private StaticallyMergedClasses staticallyMergedClasses;
   private VerticallyMergedClasses verticallyMergedClasses;
-  private EnumValueInfoMapCollection unboxedEnums = EnumValueInfoMapCollection.empty();
+  private EnumDataMap unboxedEnums = EnumDataMap.empty();
   // TODO(b/169115389): Remove
   private Set<DexMethod> cfByteCodePassThrough = ImmutableSet.of();
 
@@ -95,7 +94,7 @@
 
   // When input has been (partially) desugared these are the classes which has been library
   // desugared. This information is populated in the IR converter.
-  private Set<DexProgramClass> alreadyLibraryDesugared = null;
+  private Set<DexType> alreadyLibraryDesugared = null;
 
   private AppView(
       T appInfo,
@@ -512,18 +511,18 @@
     testing().verticallyMergedClassesConsumer.accept(dexItemFactory(), verticallyMergedClasses);
   }
 
-  public EnumValueInfoMapCollection unboxedEnums() {
+  public EnumDataMap unboxedEnums() {
     return unboxedEnums;
   }
 
-  public void setUnboxedEnums(EnumValueInfoMapCollection unboxedEnums) {
+  public void setUnboxedEnums(EnumDataMap unboxedEnums) {
     assert this.unboxedEnums.isEmpty();
     this.unboxedEnums = unboxedEnums;
     testing().unboxedEnumsConsumer.accept(dexItemFactory(), unboxedEnums);
   }
 
   public boolean validateUnboxedEnumsHaveBeenPruned() {
-    for (DexType unboxedEnum : unboxedEnums.enumSet()) {
+    for (DexType unboxedEnum : unboxedEnums.getUnboxedEnums()) {
       assert appInfo.definitionForWithoutExistenceAssert(unboxedEnum) == null
           : "Enum " + unboxedEnum + " has been unboxed but is still in the program.";
       assert appInfo().withLiveness().wasPruned(unboxedEnum)
@@ -569,35 +568,17 @@
     return !cfByteCodePassThrough.isEmpty();
   }
 
-  public void removePrunedClasses(
-      DirectMappedDexApplication prunedApp, Set<DexType> removedClasses) {
-    removePrunedClasses(prunedApp, removedClasses, Collections.emptySet());
+  public void pruneItems(PrunedItems prunedItems) {
+    pruneItems(prunedItems, withLiveness());
   }
 
-  public void removePrunedClasses(
-      DirectMappedDexApplication prunedApp,
-      Set<DexType> removedClasses,
-      Collection<DexMethod> methodsToKeepForConfigurationDebugging) {
-    assert enableWholeProgramOptimizations();
-    assert appInfo().hasLiveness();
-    removePrunedClasses(
-        prunedApp, removedClasses, methodsToKeepForConfigurationDebugging, withLiveness());
-  }
-
-  private static void removePrunedClasses(
-      DirectMappedDexApplication prunedApp,
-      Set<DexType> removedClasses,
-      Collection<DexMethod> methodsToKeepForConfigurationDebugging,
-      AppView<AppInfoWithLiveness> appView) {
-    if (removedClasses.isEmpty() && !appView.options().configurationDebugging) {
-      assert appView.appInfo.app() == prunedApp;
+  private static void pruneItems(PrunedItems prunedItems, AppView<AppInfoWithLiveness> appView) {
+    if (!prunedItems.hasRemovedClasses() && !appView.options().configurationDebugging) {
+      assert appView.appInfo().app() == prunedItems.getPrunedApp();
       return;
     }
-    appView.setAppInfo(
-        appView
-            .appInfo()
-            .prunedCopyFrom(prunedApp, removedClasses, methodsToKeepForConfigurationDebugging));
-    appView.setAppServices(appView.appServices().prunedCopy(removedClasses));
+    appView.setAppInfo(appView.appInfo().prunedCopyFrom(prunedItems));
+    appView.setAppServices(appView.appServices().prunedCopy(prunedItems));
   }
 
   public void rewriteWithLens(NonIdentityGraphLens lens) {
@@ -661,7 +642,7 @@
         });
   }
 
-  public void setAlreadyLibraryDesugared(Set<DexProgramClass> alreadyLibraryDesugared) {
+  public void setAlreadyLibraryDesugared(Set<DexType> alreadyLibraryDesugared) {
     assert this.alreadyLibraryDesugared == null;
     this.alreadyLibraryDesugared = alreadyLibraryDesugared;
   }
@@ -671,6 +652,6 @@
       return false;
     }
     assert alreadyLibraryDesugared != null;
-    return alreadyLibraryDesugared.contains(clazz);
+    return alreadyLibraryDesugared.contains(clazz.getType());
   }
 }
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 887f10d..559d2f2 100644
--- a/src/main/java/com/android/tools/r8/graph/AppliedGraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/AppliedGraphLens.java
@@ -6,7 +6,8 @@
 
 import com.android.tools.r8.graph.GraphLens.NonIdentityGraphLens;
 import com.android.tools.r8.utils.MapUtils;
-import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneMap;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
 import com.google.common.collect.ImmutableList;
@@ -25,8 +26,8 @@
  */
 public final class AppliedGraphLens extends NonIdentityGraphLens {
 
-  private final BidirectionalManyToOneMap<DexType, DexType> renamedTypeNames =
-      new BidirectionalManyToOneMap<>();
+  private final MutableBidirectionalManyToOneMap<DexType, DexType> renamedTypeNames =
+      new BidirectionalManyToOneHashMap<>();
   private final BiMap<DexField, DexField> originalFieldSignatures = HashBiMap.create();
   private final BiMap<DexMethod, DexMethod> originalMethodSignatures = HashBiMap.create();
 
@@ -102,11 +103,8 @@
 
   @Override
   public Iterable<DexType> getOriginalTypes(DexType type) {
-    Set<DexType> originalTypes = renamedTypeNames.getKeysOrNull(type);
-    if (originalTypes == null) {
-      return ImmutableList.of(type);
-    }
-    return originalTypes;
+    Set<DexType> originalTypes = renamedTypeNames.getKeys(type);
+    return originalTypes.isEmpty() ? ImmutableList.of(type) : originalTypes;
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/graph/DexClassAndMember.java b/src/main/java/com/android/tools/r8/graph/DexClassAndMember.java
index fbd5e7a..719e2d6 100644
--- a/src/main/java/com/android/tools/r8/graph/DexClassAndMember.java
+++ b/src/main/java/com/android/tools/r8/graph/DexClassAndMember.java
@@ -39,6 +39,10 @@
     return definition;
   }
 
+  public DexString getName() {
+    return getReference().getName();
+  }
+
   public R getReference() {
     return definition.getReference();
   }
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 ab974b3..2e42ccf 100644
--- a/src/main/java/com/android/tools/r8/graph/DexCode.java
+++ b/src/main/java/com/android/tools/r8/graph/DexCode.java
@@ -3,8 +3,6 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.graph;
 
-import static com.android.tools.r8.utils.ComparatorUtils.arrayComparator;
-
 import com.android.tools.r8.code.Instruction;
 import com.android.tools.r8.code.ReturnVoid;
 import com.android.tools.r8.code.SwitchPayload;
@@ -22,13 +20,16 @@
 import com.android.tools.r8.ir.conversion.MethodProcessor;
 import com.android.tools.r8.naming.ClassNameMapper;
 import com.android.tools.r8.origin.Origin;
-import com.android.tools.r8.utils.ComparatorUtils;
 import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.structural.Equatable;
+import com.android.tools.r8.utils.structural.HashCodeVisitor;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralMapping;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import com.google.common.base.Strings;
 import it.unimi.dsi.fastutil.ints.Int2IntMap;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
@@ -36,7 +37,7 @@
 import java.util.Set;
 
 // DexCode corresponds to code item in dalvik/dex-format.html
-public class DexCode extends Code implements Comparable<DexCode> {
+public class DexCode extends Code implements StructuralItem<DexCode> {
 
   static final String FAKE_THIS_PREFIX = "_";
   static final String FAKE_THIS_SUFFIX = "this";
@@ -52,6 +53,16 @@
   private DexDebugInfo debugInfo;
   private DexDebugInfoForWriting debugInfoForWriting;
 
+  private static void specify(StructuralSpecification<DexCode, ?> spec) {
+    spec.withInt(c -> c.registerSize)
+        .withInt(c -> c.incomingRegisterSize)
+        .withInt(c -> c.outgoingRegisterSize)
+        .withItemArray(c -> c.tries)
+        .withItemArray(c -> c.handlers)
+        .withNullableItem(c -> c.debugInfo)
+        .withItemArray(c -> c.instructions);
+  }
+
   public DexCode(
       int registerSize,
       int insSize,
@@ -73,6 +84,16 @@
     hashCode();  // Cache the hash code eagerly.
   }
 
+  @Override
+  public DexCode self() {
+    return this;
+  }
+
+  @Override
+  public StructuralMapping<DexCode> getStructuralMapping() {
+    return DexCode::specify;
+  }
+
   public DexCode withoutThisParameter() {
     // Note that we assume the original code has a register associated with 'this'
     // argument of the (former) instance method. We also assume (but do not check)
@@ -183,26 +204,8 @@
   }
 
   @Override
-  public int compareTo(DexCode other) {
-    if (this == other) {
-      return 0;
-    }
-    int diff =
-        Comparator.comparingInt((DexCode c) -> c.incomingRegisterSize)
-            .thenComparingInt(c -> c.registerSize)
-            .thenComparingInt(c -> c.outgoingRegisterSize)
-            .thenComparing(c -> c.tries, arrayComparator())
-            .thenComparing(c -> c.handlers, arrayComparator())
-            .thenComparing(c -> c.debugInfo, Comparator.nullsFirst(DexDebugInfo::compareTo))
-            .thenComparing((DexCode c) -> c.instructions, arrayComparator())
-            .compare(this, other);
-    assert (diff == 0) == (0 == toString().compareTo(other.toString()));
-    return diff;
-  }
-
-  @Override
   public boolean computeEquals(Object other) {
-    return other instanceof DexCode && compareTo((DexCode) other) == 0;
+    return Equatable.equalsImpl(this, other);
   }
 
   @Override
@@ -453,7 +456,7 @@
     return last.getOffset() + last.getSize();
   }
 
-  public static class Try extends DexItem implements Comparable<Try> {
+  public static class Try extends DexItem implements StructuralItem<Try> {
 
     public static final int NO_INDEX = -1;
 
@@ -462,6 +465,13 @@
     public /* offset */ int instructionCount;
     public int handlerIndex;
 
+    private static void specify(StructuralSpecification<Try, ?> spec) {
+      // The handler offset is the offset given by the dex input and does not determine the item.
+      spec.withInt(t -> t.startAddress)
+          .withInt(t -> t.instructionCount)
+          .withInt(t -> t.handlerIndex);
+    }
+
     public Try(int startAddress, int instructionCount, int handlerOffset) {
       this.startAddress = startAddress;
       this.instructionCount = instructionCount;
@@ -469,6 +479,16 @@
       this.handlerIndex = NO_INDEX;
     }
 
+    @Override
+    public Try self() {
+      return this;
+    }
+
+    @Override
+    public StructuralMapping<Try> getStructuralMapping() {
+      return Try::specify;
+    }
+
     public void setHandlerIndex(Int2IntMap map) {
       handlerIndex = map.get(handlerOffset);
     }
@@ -480,18 +500,7 @@
 
     @Override
     public boolean equals(Object other) {
-      return other instanceof Try && compareTo((Try) other) == 0;
-    }
-
-    @Override
-    public int compareTo(Try other) {
-      if (this == other) {
-        return 0;
-      }
-      return ComparatorUtils.compareInts(
-          startAddress, other.startAddress,
-          instructionCount, other.instructionCount,
-          handlerIndex, other.handlerIndex);
+      return Equatable.equalsImpl(this, other);
     }
 
     @Override
@@ -512,36 +521,40 @@
 
   }
 
-  public static class TryHandler extends DexItem implements Comparable<TryHandler> {
+  public static class TryHandler extends DexItem implements StructuralItem<TryHandler> {
 
     public static final int NO_HANDLER = -1;
 
     public final TypeAddrPair[] pairs;
     public final /* offset */ int catchAllAddr;
 
+    private static void specify(StructuralSpecification<TryHandler, ?> spec) {
+      spec.withInt(h -> h.catchAllAddr).withItemArray(h -> h.pairs);
+    }
+
     public TryHandler(TypeAddrPair[] pairs, int catchAllAddr) {
       this.pairs = pairs;
       this.catchAllAddr = catchAllAddr;
     }
 
     @Override
+    public TryHandler self() {
+      return this;
+    }
+
+    @Override
+    public StructuralMapping<TryHandler> getStructuralMapping() {
+      return TryHandler::specify;
+    }
+
+    @Override
     public int hashCode() {
-      return catchAllAddr + Arrays.hashCode(pairs) * 7;
+      return HashCodeVisitor.run(this);
     }
 
     @Override
     public boolean equals(Object other) {
-      return other instanceof TryHandler && compareTo((TryHandler) other) == 0;
-    }
-
-    @Override
-    public int compareTo(TryHandler other) {
-      if (this == other) {
-        return 0;
-      }
-      return Comparator.comparingInt((TryHandler h) -> h.catchAllAddr)
-          .thenComparing(h -> h.pairs, arrayComparator())
-          .compare(this, other);
+      return Equatable.equalsImpl(this, other);
     }
 
     public void collectIndexedItems(IndexedItemCollection indexedItems, GraphLens graphLens) {
@@ -576,16 +589,30 @@
       return builder.toString();
     }
 
-    public static class TypeAddrPair extends DexItem implements Comparable<TypeAddrPair> {
+    public static class TypeAddrPair extends DexItem implements StructuralItem<TypeAddrPair> {
 
       private final DexType type;
       public final /* offset */ int addr;
 
+      private static void specify(StructuralSpecification<TypeAddrPair, ?> spec) {
+        spec.withItem(p -> p.type).withInt(p -> p.addr);
+      }
+
       public TypeAddrPair(DexType type, int addr) {
         this.type = type;
         this.addr = addr;
       }
 
+      @Override
+      public TypeAddrPair self() {
+        return this;
+      }
+
+      @Override
+      public StructuralMapping<TypeAddrPair> getStructuralMapping() {
+        return TypeAddrPair::specify;
+      }
+
       public DexType getType() {
         return type;
       }
@@ -612,17 +639,7 @@
 
       @Override
       public boolean equals(Object other) {
-        return other instanceof TypeAddrPair && compareTo((TypeAddrPair) other) == 0;
-      }
-
-      @Override
-      public int compareTo(TypeAddrPair other) {
-        if (this == other) {
-          return 0;
-        }
-        return Comparator.comparingInt((TypeAddrPair p) -> p.addr)
-            .thenComparing(p -> p.type)
-            .compare(this, other);
+        return Equatable.equalsImpl(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 16842b0..9dba226 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugEvent.java
@@ -7,11 +7,16 @@
 import com.android.tools.r8.dex.DebugBytecodeWriter;
 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.ir.code.Position;
-import java.util.Comparator;
+import com.android.tools.r8.utils.structural.CompareToVisitor;
+import com.android.tools.r8.utils.structural.HashingVisitor;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralMapping;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.Objects;
 
-public abstract class DexDebugEvent extends DexItem implements Comparable<DexDebugEvent> {
+public abstract class DexDebugEvent extends DexItem implements StructuralItem<DexDebugEvent> {
 
   // Compare ID(s) for virtual debug events.
   private static final int DBG_SET_INLINE_FRAME_COMPARE_ID = Constants.DBG_LAST_SPECIAL + 1;
@@ -41,15 +46,30 @@
 
   abstract int getCompareToId();
 
-  abstract int internalCompareTo(DexDebugEvent other);
+  abstract int internalAcceptCompareTo(DexDebugEvent other, CompareToVisitor visitor);
+
+  abstract void internalAcceptHashing(HashingVisitor visitor);
 
   @Override
-  public final int compareTo(DexDebugEvent other) {
-    if (this == other) {
-      return 0;
-    }
-    int diff = Integer.compare(getCompareToId(), other.getCompareToId());
-    return diff != 0 ? diff : internalCompareTo(other);
+  public DexDebugEvent self() {
+    return this;
+  }
+
+  @Override
+  public StructuralMapping<DexDebugEvent> getStructuralMapping() {
+    throw new Unreachable();
+  }
+
+  @Override
+  public final int acceptCompareTo(DexDebugEvent other, CompareToVisitor visitor) {
+    int diff = visitor.visitInt(getCompareToId(), other.getCompareToId());
+    return diff != 0 ? diff : internalAcceptCompareTo(other, visitor);
+  }
+
+  @Override
+  public final void acceptHashing(HashingVisitor visitor) {
+    visitor.visitInt(getCompareToId());
+    internalAcceptHashing(visitor);
   }
 
   public abstract void writeOn(DebugBytecodeWriter writer, ObjectToOffsetMapping mapping);
@@ -102,8 +122,13 @@
     }
 
     @Override
-    int internalCompareTo(DexDebugEvent other) {
-      return Integer.compare(delta, ((AdvancePC) other).delta);
+    int internalAcceptCompareTo(DexDebugEvent other, CompareToVisitor visitor) {
+      return visitor.visitInt(delta, ((AdvancePC) other).delta);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      visitor.visitInt(delta);
     }
   }
 
@@ -139,10 +164,15 @@
     }
 
     @Override
-    int internalCompareTo(DexDebugEvent other) {
+    int internalAcceptCompareTo(DexDebugEvent other, CompareToVisitor visitor) {
       assert other instanceof SetPrologueEnd;
       return 0;
     }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      // Nothing to hash as the ID has already been hashed.
+    }
   }
 
 
@@ -177,10 +207,15 @@
     }
 
     @Override
-    int internalCompareTo(DexDebugEvent other) {
+    int internalAcceptCompareTo(DexDebugEvent other, CompareToVisitor visitor) {
       assert other instanceof SetEpilogueBegin;
       return 0;
     }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      // Nothing to hash as the ID has already been hashed.
+    }
   }
 
   public static class AdvanceLine extends DexDebugEvent {
@@ -219,8 +254,13 @@
     }
 
     @Override
-    int internalCompareTo(DexDebugEvent other) {
-      return Integer.compare(delta, ((AdvanceLine) other).delta);
+    int internalAcceptCompareTo(DexDebugEvent other, CompareToVisitor visitor) {
+      return visitor.visitInt(delta, ((AdvanceLine) other).delta);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      visitor.visitInt(delta);
     }
   }
 
@@ -231,6 +271,13 @@
     final DexType type;
     final DexString signature;
 
+    private static void spec(StructuralSpecification<StartLocal, ?> spec) {
+      spec.withInt(e -> e.registerNum)
+          .withItem(e -> e.name)
+          .withItem(e -> e.type)
+          .withNullableItem(e -> e.signature);
+    }
+
     public StartLocal(
         int registerNum,
         DexString name,
@@ -298,12 +345,13 @@
     }
 
     @Override
-    int internalCompareTo(DexDebugEvent other) {
-      return Comparator.comparingInt((StartLocal e) -> e.registerNum)
-          .thenComparing(e -> e.name)
-          .thenComparing(e -> e.type)
-          .thenComparing(e -> e.signature, Comparator.nullsFirst(DexString::compareTo))
-          .compare(this, (StartLocal) other);
+    int internalAcceptCompareTo(DexDebugEvent other, CompareToVisitor visitor) {
+      return visitor.visit(this, (StartLocal) other, StartLocal::spec);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      visitor.visit(this, StartLocal::spec);
     }
   }
 
@@ -343,8 +391,13 @@
     }
 
     @Override
-    int internalCompareTo(DexDebugEvent other) {
-      return Integer.compare(registerNum, ((EndLocal) other).registerNum);
+    int internalAcceptCompareTo(DexDebugEvent other, CompareToVisitor visitor) {
+      return visitor.visitInt(registerNum, ((EndLocal) other).registerNum);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      visitor.visitInt(registerNum);
     }
   }
 
@@ -384,8 +437,13 @@
     }
 
     @Override
-    int internalCompareTo(DexDebugEvent other) {
-      return Integer.compare(registerNum, ((RestartLocal) other).registerNum);
+    int internalAcceptCompareTo(DexDebugEvent other, CompareToVisitor visitor) {
+      return visitor.visitInt(registerNum, ((RestartLocal) other).registerNum);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      visitor.visitInt(registerNum);
     }
   }
 
@@ -430,8 +488,13 @@
     }
 
     @Override
-    int internalCompareTo(DexDebugEvent other) {
-      return fileName.compareTo(((SetFile) other).fileName);
+    int internalAcceptCompareTo(DexDebugEvent other, CompareToVisitor visitor) {
+      return fileName.acceptCompareTo(((SetFile) other).fileName, visitor);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      fileName.acceptHashing(visitor);
     }
   }
 
@@ -440,6 +503,10 @@
     final DexMethod callee;
     final Position caller;
 
+    private static void specify(StructuralSpecification<SetInlineFrame, ?> spec) {
+      spec.withItem(e -> e.callee).withNullableItem(e -> e.caller);
+    }
+
     SetInlineFrame(DexMethod callee, Position caller) {
       assert callee != null;
       this.callee = callee;
@@ -472,10 +539,13 @@
     }
 
     @Override
-    int internalCompareTo(DexDebugEvent other) {
-      return Comparator.comparing((SetInlineFrame e) -> e.callee, DexMethod::compareTo)
-          .thenComparing(e -> e.caller, Comparator.nullsFirst(Position::compareTo))
-          .compare(this, (SetInlineFrame) other);
+    int internalAcceptCompareTo(DexDebugEvent other, CompareToVisitor visitor) {
+      return visitor.visit(this, (SetInlineFrame) other, SetInlineFrame::specify);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      visitor.visit(this, SetInlineFrame::specify);
     }
 
     @Override
@@ -540,8 +610,13 @@
     }
 
     @Override
-    int internalCompareTo(DexDebugEvent other) {
-      return Integer.compare(value, ((Default) other).value);
+    int internalAcceptCompareTo(DexDebugEvent other, CompareToVisitor visitor) {
+      return visitor.visitInt(value, ((Default) other).value);
+    }
+
+    @Override
+    void internalAcceptHashing(HashingVisitor visitor) {
+      visitor.visitInt(value);
     }
   }
 }
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 5bfbd8d..75588f6 100644
--- a/src/main/java/com/android/tools/r8/graph/DexDebugInfo.java
+++ b/src/main/java/com/android/tools/r8/graph/DexDebugInfo.java
@@ -5,17 +5,25 @@
 
 import com.android.tools.r8.dex.IndexedItemCollection;
 import com.android.tools.r8.dex.MixedSectionCollection;
-import com.android.tools.r8.utils.ComparatorUtils;
+import com.android.tools.r8.utils.structural.Equatable;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralMapping;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import java.util.Arrays;
-import java.util.Comparator;
 import java.util.List;
 
-public class DexDebugInfo extends CachedHashValueDexItem implements Comparable<DexDebugInfo> {
+public class DexDebugInfo extends CachedHashValueDexItem implements StructuralItem<DexDebugInfo> {
 
   public final int startLine;
   public final DexString[] parameters;
   public DexDebugEvent[] events;
 
+  private static void specify(StructuralSpecification<DexDebugInfo, ?> spec) {
+    spec.withInt(d -> d.startLine)
+        .withItemArrayAllowingNullMembers(d -> d.parameters)
+        .withItemArray(d -> d.events);
+  }
+
   public DexDebugInfo(int startLine, DexString[] parameters, DexDebugEvent[] events) {
     assert startLine >= 0;
     this.startLine = startLine;
@@ -26,6 +34,16 @@
     hashCode();
   }
 
+  @Override
+  public DexDebugInfo self() {
+    return this;
+  }
+
+  @Override
+  public StructuralMapping<DexDebugInfo> getStructuralMapping() {
+    return DexDebugInfo::specify;
+  }
+
   public List<DexDebugEntry> computeEntries(DexMethod method) {
     DexDebugEntryBuilder builder = new DexDebugEntryBuilder(startLine, method);
     for (DexDebugEvent event : events) {
@@ -43,20 +61,7 @@
 
   @Override
   public final boolean computeEquals(Object other) {
-    return other instanceof DexDebugInfo && compareTo((DexDebugInfo) other) == 0;
-  }
-
-  @Override
-  public final int compareTo(DexDebugInfo other) {
-    if (this == other) {
-      return 0;
-    }
-    return Comparator.comparingInt((DexDebugInfo i) -> i.startLine)
-        .thenComparing(
-            i -> i.parameters,
-            ComparatorUtils.arrayComparator(Comparator.nullsFirst(DexString::compareTo)))
-        .thenComparing(i -> i.events, ComparatorUtils.arrayComparator())
-        .compare(this, other);
+    return Equatable.equalsImpl(this, other);
   }
 
   public void collectIndexedItems(IndexedItemCollection indexedItems, GraphLens graphLens) {
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 7d807e9..bcbc160 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -514,6 +514,24 @@
     return accessFlags.isConstructor() && !accessFlags.isStatic();
   }
 
+  /**
+   * Returns true for (private instance) methods that have been created as a result of class merging
+   * and will be force-inlined into an instance initializer on the enclosing class.
+   */
+  public boolean willBeInlinedIntoInstanceInitializer(DexItemFactory dexItemFactory) {
+    checkIfObsolete();
+    if (getName().startsWith(dexItemFactory.temporaryConstructorMethodPrefix)) {
+      assert isPrivate();
+      assert !isStatic();
+      return true;
+    }
+    return false;
+  }
+
+  public boolean isOrWillBeInlinedIntoInstanceInitializer(DexItemFactory dexItemFactory) {
+    return isInstanceInitializer() || willBeInlinedIntoInstanceInitializer(dexItemFactory);
+  }
+
   public boolean isDefaultInitializer() {
     checkIfObsolete();
     return isInstanceInitializer() && method.proto.parameters.isEmpty();
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 dac5b16..a93000e 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -188,7 +188,6 @@
 
   public final DexString convertMethodName = createString("convert");
   public final DexString wrapperFieldName = createString("wrappedValue");
-  public final DexString initMethodName = createString("<init>");
 
   public final DexString getClassMethodName = createString("getClass");
   public final DexString finalizeMethodName = createString("finalize");
@@ -290,6 +289,8 @@
   public final DexString constructorMethodName = createString(Constants.INSTANCE_INITIALIZER_NAME);
   public final DexString classConstructorMethodName =
       createString(Constants.CLASS_INITIALIZER_NAME);
+  public final DexString temporaryConstructorMethodPrefix =
+      createString(Constants.TEMPORARY_INSTANCE_INITIALIZER_PREFIX);
 
   public final DexString thisName = createString("this");
   public final DexString enumValuesFieldName = createString("$VALUES");
@@ -1134,6 +1135,8 @@
     public final DexMethod requireNonNull;
     public final DexMethod requireNonNullWithMessage;
     public final DexMethod requireNonNullWithMessageSupplier;
+    public final DexMethod toStringWithObject =
+        createMethod(objectsType, createProto(stringType, objectType), "toString");
 
     private ObjectsMethods() {
       DexString requireNonNullMethodName = createString("requireNonNull");
@@ -1357,7 +1360,7 @@
 
     public final DexMethod initWithMessage =
         createMethod(
-            illegalArgumentExceptionType, createProto(voidType, stringType), initMethodName);
+            illegalArgumentExceptionType, createProto(voidType, stringType), constructorMethodName);
   }
 
   /**
@@ -1548,6 +1551,8 @@
     public final DexMethod toString;
 
     private final Set<DexMethod> appendMethods;
+    private final Set<DexMethod> appendPrimitiveMethods;
+
     private StringBuildingMethods(DexType receiver) {
       DexString append = createString("append");
 
@@ -1566,7 +1571,6 @@
       appendObject = createMethod(receiver, createProto(receiver, objectType), append);
       appendString = createMethod(receiver, createProto(receiver, stringType), append);
       appendStringBuffer = createMethod(receiver, createProto(receiver, stringBufferType), append);
-
       charSequenceConstructor =
           createMethod(receiver, createProto(voidType, charSequenceType), constructorMethodName);
       defaultConstructor = createMethod(receiver, createProto(voidType), constructorMethodName);
@@ -1591,6 +1595,9 @@
               appendObject,
               appendString,
               appendStringBuffer);
+      appendPrimitiveMethods =
+          ImmutableSet.of(
+              appendBoolean, appendChar, appendInt, appendDouble, appendFloat, appendLong);
       constructorMethods =
           ImmutableSet.of(
               charSequenceConstructor, defaultConstructor, intConstructor, stringConstructor);
@@ -1602,12 +1609,29 @@
       return appendMethods.contains(method);
     }
 
+    public boolean isAppendObjectMethod(DexMethod method) {
+      return method == appendObject;
+    }
+
+    public boolean isAppendPrimitiveMethod(DexMethod method) {
+      return appendPrimitiveMethods.contains(method);
+    }
+
+    public boolean isAppendStringMethod(DexMethod method) {
+      return method == appendString;
+    }
+
+    public boolean isConstructorMethod(DexMethod method) {
+      return constructorMethods.contains(method);
+    }
+
     public boolean constructorInvokeIsSideEffectFree(InvokeMethod invoke) {
       DexMethod invokedMethod = invoke.getInvokedMethod();
       if (invokedMethod == charSequenceConstructor) {
-        // NullPointerException - if seq is null.
-        Value seqValue = invoke.inValues().get(1);
-        return !seqValue.getType().isNullable();
+        // Performs callbacks on the given CharSequence, which may have side effects.
+        TypeElement charSequenceType = invoke.getArgument(1).getType();
+        return charSequenceType.isClassType()
+            && charSequenceType.asClassType().getClassType() == stringType;
       }
 
       if (invokedMethod == defaultConstructor) {
@@ -1926,6 +1950,10 @@
         holder);
   }
 
+  public DexMethod createClassInitializer(DexType holder) {
+    return createMethod(holder, createProto(voidType), classConstructorMethodName);
+  }
+
   public DexMethod createInstanceInitializerWithFreshProto(
       DexMethod method, List<DexType> extraTypes, Predicate<DexMethod> isFresh) {
     assert method.isInstanceInitializer(this);
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 c91ffdf..8ab4935 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethod.java
@@ -58,6 +58,10 @@
     return proto.parameters;
   }
 
+  public DexProto getProto() {
+    return proto;
+  }
+
   public DexType getReturnType() {
     return proto.returnType;
   }
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 3e92e74..0faef1b 100644
--- a/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
+++ b/src/main/java/com/android/tools/r8/graph/DexProgramClass.java
@@ -756,4 +756,8 @@
   public long getChecksum() {
     return checksumSupplier.getChecksum(this);
   }
+
+  public ChecksumSupplier getChecksumSupplier() {
+    return checksumSupplier;
+  }
 }
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 8a59614..77976f1 100644
--- a/src/main/java/com/android/tools/r8/graph/DexReference.java
+++ b/src/main/java/com/android/tools/r8/graph/DexReference.java
@@ -73,19 +73,4 @@
     assert isDexMethod();
     return 3;
   }
-
-  public int referenceCompareTo(DexReference o) {
-    int typeDiff = referenceTypeOrder() - o.referenceTypeOrder();
-    if (typeDiff != 0) {
-      return typeDiff;
-    }
-    if (isDexType()) {
-      return asDexType().compareTo(o.asDexType());
-    }
-    if (isDexField()) {
-      return asDexField().compareTo(o.asDexField());
-    }
-    assert isDexMethod();
-    return asDexMethod().compareTo(o.asDexMethod());
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/graph/EnumValueInfoMapCollection.java b/src/main/java/com/android/tools/r8/graph/EnumValueInfoMapCollection.java
deleted file mode 100644
index 362ff7c..0000000
--- a/src/main/java/com/android/tools/r8/graph/EnumValueInfoMapCollection.java
+++ /dev/null
@@ -1,139 +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.graph;
-
-import com.google.common.collect.ImmutableMap;
-import java.util.LinkedHashMap;
-import java.util.Map;
-import java.util.Set;
-import java.util.function.BiConsumer;
-
-public class EnumValueInfoMapCollection {
-
-  public static EnumValueInfoMapCollection empty() {
-    return new EnumValueInfoMapCollection(ImmutableMap.of());
-  }
-
-  private final Map<DexType, EnumValueInfoMap> maps;
-
-  private EnumValueInfoMapCollection(Map<DexType, EnumValueInfoMap> maps) {
-    this.maps = maps;
-  }
-
-  public EnumValueInfoMap getEnumValueInfoMap(DexType type) {
-    return maps.get(type);
-  }
-
-  public boolean isEmpty() {
-    return maps.isEmpty();
-  }
-
-  public boolean containsEnum(DexType type) {
-    return maps.containsKey(type);
-  }
-
-  public Set<DexType> enumSet() {
-    return maps.keySet();
-  }
-
-  public EnumValueInfoMapCollection rewrittenWithLens(GraphLens lens) {
-    Builder builder = builder();
-    maps.forEach(
-        (type, map) -> {
-          DexType dexType = lens.lookupType(type);
-          // Enum unboxing may have changed the type to int type.
-          // Do not keep the map for such enums.
-          if (!dexType.isPrimitiveType()) {
-            builder.put(dexType, map.rewrittenWithLens(lens));
-          }
-        });
-    return builder.build();
-  }
-
-  public static Builder builder() {
-    return new Builder();
-  }
-
-  public static class Builder {
-
-    private ImmutableMap.Builder<DexType, EnumValueInfoMap> builder;
-
-    public Builder put(DexType type, EnumValueInfoMap map) {
-      if (builder == null) {
-        builder = ImmutableMap.builder();
-      }
-      builder.put(type, map);
-      return this;
-    }
-
-    public EnumValueInfoMapCollection build() {
-      if (builder == null) {
-        return empty();
-      }
-      return new EnumValueInfoMapCollection(builder.build());
-    }
-  }
-
-  public static final class EnumValueInfoMap {
-
-    private final LinkedHashMap<DexField, EnumValueInfo> map;
-
-    public EnumValueInfoMap(LinkedHashMap<DexField, EnumValueInfo> map) {
-      this.map = map;
-    }
-
-    public Set<DexField> enumValues() {
-      return map.keySet();
-    }
-
-    public int size() {
-      return map.size();
-    }
-
-    public boolean hasEnumValueInfo(DexField field) {
-      return map.containsKey(field);
-    }
-
-    public EnumValueInfo getEnumValueInfo(DexField field) {
-      return map.get(field);
-    }
-
-    public void forEach(BiConsumer<DexField, EnumValueInfo> consumer) {
-      map.forEach(consumer);
-    }
-
-    EnumValueInfoMap rewrittenWithLens(GraphLens lens) {
-      LinkedHashMap<DexField, EnumValueInfo> rewritten = new LinkedHashMap<>();
-      map.forEach(
-          (field, valueInfo) ->
-              rewritten.put(lens.lookupField(field), valueInfo.rewrittenWithLens(lens)));
-      return new EnumValueInfoMap(rewritten);
-    }
-  }
-
-  public static final class EnumValueInfo {
-
-    // The anonymous subtype of this specific value or the enum type.
-    public final DexType type;
-    public final int ordinal;
-
-    public EnumValueInfo(DexType type, int ordinal) {
-      this.type = type;
-      this.ordinal = ordinal;
-    }
-
-    public int convertToInt() {
-      return ordinal + 1;
-    }
-
-    EnumValueInfo rewrittenWithLens(GraphLens lens) {
-      DexType newType = lens.lookupType(type);
-      if (type == newType) {
-        return this;
-      }
-      return new EnumValueInfo(newType, ordinal);
-    }
-  }
-}
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 5a10844..0e3d784 100644
--- a/src/main/java/com/android/tools/r8/graph/GraphLens.java
+++ b/src/main/java/com/android/tools/r8/graph/GraphLens.java
@@ -4,9 +4,11 @@
 package com.android.tools.r8.graph;
 
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
+import static com.android.tools.r8.horizontalclassmerging.ClassMerger.CLASS_ID_FIELD_NAME;
+import static com.android.tools.r8.ir.desugar.LambdaRewriter.LAMBDA_CLASS_NAME_PREFIX;
+import static com.android.tools.r8.ir.desugar.LambdaRewriter.LAMBDA_INSTANCE_FIELD_NAME;
 
 import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.horizontalclassmerging.ClassMerger;
 import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.ir.conversion.LensCodeRewriterUtils;
 import com.android.tools.r8.ir.desugar.InterfaceProcessor.InterfaceProcessorNestedGraphLens;
@@ -15,10 +17,12 @@
 import com.android.tools.r8.utils.IterableUtils;
 import com.android.tools.r8.utils.SetUtils;
 import com.android.tools.r8.utils.collections.BidirectionalManyToManyRepresentativeMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
 import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
-import com.google.common.collect.BiMap;
-import com.google.common.collect.HashBiMap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
@@ -32,6 +36,7 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.function.Function;
 import java.util.function.Predicate;
+import java.util.stream.Collectors;
 
 /**
  * A GraphLens implements a virtual view on top of the graph, used to delay global rewrites until
@@ -65,6 +70,10 @@
       return reference;
     }
 
+    public R getRewrittenReference(BidirectionalManyToOneRepresentativeMap<R, R> rewritings) {
+      return rewritings.getOrDefault(reference, reference);
+    }
+
     public R getRewrittenReference(Map<R, R> rewritings) {
       return rewritings.getOrDefault(reference, reference);
     }
@@ -77,6 +86,11 @@
       return reboundReference;
     }
 
+    public R getRewrittenReboundReference(
+        BidirectionalManyToOneRepresentativeMap<R, R> rewritings) {
+      return rewritings.getOrDefault(reboundReference, reboundReference);
+    }
+
     public R getRewrittenReboundReference(Map<R, R> rewritings) {
       return rewritings.getOrDefault(reboundReference, reboundReference);
     }
@@ -228,10 +242,10 @@
 
     protected final Map<DexType, DexType> typeMap = new IdentityHashMap<>();
     protected final Map<DexMethod, DexMethod> methodMap = new IdentityHashMap<>();
-    protected final Map<DexField, DexField> fieldMap = new IdentityHashMap<>();
+    protected final MutableBidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap =
+        new BidirectionalManyToOneRepresentativeHashMap<>();
 
-    protected final BiMap<DexField, DexField> originalFieldSignatures = HashBiMap.create();
-    protected final BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures =
+    protected final MutableBidirectionalOneToOneMap<DexMethod, DexMethod> originalMethodSignatures =
         new BidirectionalOneToOneHashMap<>();
 
     public void map(DexType from, DexType to) {
@@ -248,13 +262,6 @@
       methodMap.put(from, to);
     }
 
-    public void map(DexField from, DexField to) {
-      if (from == to) {
-        return;
-      }
-      fieldMap.put(from, to);
-    }
-
     public void move(DexMethod from, DexMethod to) {
       if (from == to) {
         return;
@@ -268,7 +275,6 @@
         return;
       }
       fieldMap.put(from, to);
-      originalFieldSignatures.put(to, from);
     }
 
     public GraphLens build(DexItemFactory dexItemFactory) {
@@ -283,7 +289,6 @@
           typeMap,
           methodMap,
           fieldMap,
-          originalFieldSignatures,
           originalMethodSignatures,
           previousLens,
           dexItemFactory);
@@ -611,21 +616,14 @@
         continue;
       }
       for (DexEncodedField field : clazz.fields()) {
-        // The field $r8$clinitField may be synthesized by R8 in order to trigger the initialization
-        // of the enclosing class. It is not present in the input, and therefore we do not require
-        // that it can be mapped back to the original program.
-        if (field.field.match(dexItemFactory.objectMembers.clinitField)) {
-          continue;
-        }
-
-        // TODO(b/167947782): Should be a general check to see if the field is D8/R8 synthesized.
-        if (field.getReference().name.toSourceString().equals(ClassMerger.CLASS_ID_FIELD_NAME)) {
-          continue;
-        }
-
-        DexField originalField = getOriginalFieldSignature(field.field);
+        // Fields synthesized by R8 are not present in the input, and therefore we do not require
+        // that they can be mapped back to the original program.
+        DexField originalField = getOriginalFieldSignature(field.getReference());
         assert originalFields.contains(originalField)
-            : "Unable to map field `" + field.field.toSourceString() + "` back to original program";
+                || isD8R8SynthesizedField(originalField, dexItemFactory)
+            : "Unable to map field `"
+                + field.getReference().toSourceString()
+                + "` back to original program";
       }
       for (DexEncodedMethod method : clazz.methods()) {
         if (method.isD8R8Synthesized()) {
@@ -640,6 +638,22 @@
     return true;
   }
 
+  private boolean isD8R8SynthesizedField(DexField field, DexItemFactory dexItemFactory) {
+    // TODO(b/167947782): Should be a general check to see if the field is D8/R8 synthesized
+    //  instead of relying on field names.
+    if (field.match(dexItemFactory.objectMembers.clinitField)) {
+      return true;
+    }
+    if (field.getName().toSourceString().equals(CLASS_ID_FIELD_NAME)) {
+      return true;
+    }
+    if (field.getHolderType().toSourceString().contains(LAMBDA_CLASS_NAME_PREFIX)
+        && field.getName().toSourceString().equals(LAMBDA_INSTANCE_FIELD_NAME)) {
+      return true;
+    }
+    return false;
+  }
+
   public abstract static class NonIdentityGraphLens extends GraphLens {
 
     private final DexItemFactory dexItemFactory;
@@ -963,11 +977,10 @@
 
     protected final Map<DexType, DexType> typeMap;
     protected final Map<DexMethod, DexMethod> methodMap;
-    protected final Map<DexField, DexField> fieldMap;
+    protected final BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap;
 
-    // Maps that store the original signature of fields and methods that have been affected, for
-    // example, by vertical class merging. Needed to generate a correct Proguard map in the end.
-    protected final BiMap<DexField, DexField> originalFieldSignatures;
+    // Map that store the original signature of methods that have been affected, for example, by
+    // vertical class merging. Needed to generate a correct Proguard map in the end.
     protected BidirectionalManyToManyRepresentativeMap<DexMethod, DexMethod>
         originalMethodSignatures;
 
@@ -980,8 +993,7 @@
     public NestedGraphLens(
         Map<DexType, DexType> typeMap,
         Map<DexMethod, DexMethod> methodMap,
-        Map<DexField, DexField> fieldMap,
-        BiMap<DexField, DexField> originalFieldSignatures,
+        BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap,
         BidirectionalManyToManyRepresentativeMap<DexMethod, DexMethod> originalMethodSignatures,
         GraphLens previousLens,
         DexItemFactory dexItemFactory) {
@@ -993,7 +1005,6 @@
       this.typeMap = typeMap.isEmpty() ? null : typeMap;
       this.methodMap = methodMap;
       this.fieldMap = fieldMap;
-      this.originalFieldSignatures = originalFieldSignatures;
       this.originalMethodSignatures = originalMethodSignatures;
       this.dexItemFactory = dexItemFactory;
     }
@@ -1022,10 +1033,7 @@
 
     @Override
     public DexField getOriginalFieldSignature(DexField field) {
-      DexField originalField =
-          originalFieldSignatures != null
-              ? originalFieldSignatures.getOrDefault(field, field)
-              : field;
+      DexField originalField = fieldMap.getRepresentativeKeyOrDefault(field, field);
       return getPrevious().getOriginalFieldSignature(originalField);
     }
 
@@ -1038,9 +1046,7 @@
     @Override
     public DexField getRenamedFieldSignature(DexField originalField) {
       DexField renamedField = getPrevious().getRenamedFieldSignature(originalField);
-      return originalFieldSignatures != null
-          ? originalFieldSignatures.inverse().getOrDefault(renamedField, renamedField)
-          : renamedField;
+      return fieldMap.getOrDefault(renamedField, renamedField);
     }
 
     @Override
@@ -1221,10 +1227,15 @@
         builder.append(entry.getKey().toSourceString()).append(" -> ");
         builder.append(entry.getValue().toSourceString()).append(System.lineSeparator());
       }
-      for (Map.Entry<DexField, DexField> entry : fieldMap.entrySet()) {
-        builder.append(entry.getKey().toSourceString()).append(" -> ");
-        builder.append(entry.getValue().toSourceString()).append(System.lineSeparator());
-      }
+      fieldMap.forEachManyToOneMapping(
+          (keys, value) -> {
+            builder.append(
+                keys.stream()
+                    .map(DexField::toSourceString)
+                    .collect(Collectors.joining("," + System.lineSeparator())));
+            builder.append(" -> ");
+            builder.append(value.toSourceString()).append(System.lineSeparator());
+          });
       builder.append(getPrevious().toString());
       return builder.toString();
     }
diff --git a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
index 0902a45..fabdde7 100644
--- a/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
+++ b/src/main/java/com/android/tools/r8/graph/JarClassFileReader.java
@@ -57,12 +57,12 @@
 import java.util.function.Consumer;
 import java.util.zip.CRC32;
 import org.objectweb.asm.AnnotationVisitor;
-import org.objectweb.asm.Attribute;
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.ClassVisitor;
 import org.objectweb.asm.FieldVisitor;
 import org.objectweb.asm.Label;
 import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.RecordComponentVisitor;
 import org.objectweb.asm.Type;
 import org.objectweb.asm.TypePath;
 
@@ -300,6 +300,17 @@
     }
 
     @Override
+    public RecordComponentVisitor visitRecordComponent(
+        String name, String descriptor, String signature) {
+      throw new CompilationError("Records are not supported", origin);
+    }
+
+    @Override
+    public void visitPermittedSubclass(String permittedSubclass) {
+      throw new CompilationError("Sealed classes are not supported", origin);
+    }
+
+    @Override
     public void visit(
         int rawVersion,
         int access,
@@ -311,6 +322,9 @@
       if (InternalOptions.SUPPORTED_CF_VERSION.isLessThan(version)) {
         throw new CompilationError("Unsupported class file version: " + version, origin);
       }
+      if (version.isGreaterThanOrEqualTo(InternalOptions.EXPERIMENTAL_CF_VERSION)) {
+        application.options.warningExperimentalClassFileVersion(origin);
+      }
       this.deprecated = AsmUtils.isDeprecated(access);
       accessFlags = ClassAccessFlags.fromCfAccessFlags(cleanAccessFlags(access));
       type = application.getTypeFromName(name);
@@ -400,11 +414,6 @@
     }
 
     @Override
-    public void visitAttribute(Attribute attr) {
-      // Unknown attribute must only be ignored
-    }
-
-    @Override
     public void visitEnd() {
       if (defaultAnnotations != null) {
         addAnnotation(DexAnnotation.createAnnotationDefaultAnnotation(
diff --git a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
index 6731b5e..3387ff2 100644
--- a/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
+++ b/src/main/java/com/android/tools/r8/graph/ObjectAllocationInfoCollectionImpl.java
@@ -46,7 +46,7 @@
    */
   final Set<DexProgramClass> interfacesWithUnknownSubtypeHierarchy = Sets.newIdentityHashSet();
 
-  /** Map of types directly implemented by lambdas to those lambdas. */
+  /** Map of types directly implemented by lambdas to those types. */
   final Map<DexType, List<LambdaDescriptor>> instantiatedLambdas = new IdentityHashMap<>();
 
   /**
@@ -215,6 +215,40 @@
     return instantiatedLambdas.keySet();
   }
 
+  public void removeAllocationsForPrunedItems(PrunedItems prunedItems) {
+    Set<DexType> removedClasses = prunedItems.getRemovedClasses();
+    if (removedClasses.isEmpty()) {
+      return;
+    }
+    classesWithAllocationSiteTracking
+        .entrySet()
+        .removeIf(entry -> removedClasses.contains(entry.getKey().getType()));
+    classesWithoutAllocationSiteTracking.removeIf(
+        clazz -> removedClasses.contains(clazz.getType()));
+    boolean removed =
+        interfacesWithUnknownSubtypeHierarchy.removeIf(
+            iface -> removedClasses.contains(iface.getType()));
+    assert !removed : "Unexpected removal of an interface marking an unknown hierarchy.";
+    removedClasses.forEach(instantiatedLambdas::remove);
+  }
+
+  public boolean verifyAllocatedTypesAreLive(
+      Set<DexType> liveTypes, DexDefinitionSupplier definitions) {
+    for (DexProgramClass clazz : classesWithAllocationSiteTracking.keySet()) {
+      assert liveTypes.contains(clazz.getType());
+    }
+    for (DexProgramClass clazz : classesWithoutAllocationSiteTracking) {
+      assert liveTypes.contains(clazz.getType());
+    }
+    for (DexProgramClass iface : interfacesWithUnknownSubtypeHierarchy) {
+      assert liveTypes.contains(iface.getType());
+    }
+    for (DexType iface : instantiatedLambdas.keySet()) {
+      assert definitions.definitionFor(iface).isNotProgramClass() || liveTypes.contains(iface);
+    }
+    return true;
+  }
+
   public static class Builder extends ObjectAllocationInfoCollectionImpl {
 
     private static class Data {
diff --git a/src/main/java/com/android/tools/r8/graph/PrunedItems.java b/src/main/java/com/android/tools/r8/graph/PrunedItems.java
new file mode 100644
index 0000000..2df596c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/graph/PrunedItems.java
@@ -0,0 +1,92 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.graph;
+
+import com.google.common.collect.Sets;
+import java.util.Collection;
+import java.util.Set;
+
+public class PrunedItems {
+
+  private final DexApplication prunedApp;
+  private final Set<DexReference> additionalPinnedItems;
+  private final Set<DexType> noLongerSyntheticItems;
+  private final Set<DexType> removedClasses;
+
+  private PrunedItems(
+      DexApplication prunedApp,
+      Set<DexReference> additionalPinnedItems,
+      Set<DexType> noLongerSyntheticItems,
+      Set<DexType> removedClasses) {
+    this.prunedApp = prunedApp;
+    this.additionalPinnedItems = additionalPinnedItems;
+    this.noLongerSyntheticItems = noLongerSyntheticItems;
+    this.removedClasses = removedClasses;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static PrunedItems empty(DexApplication application) {
+    return new Builder().setPrunedApp(application).build();
+  }
+
+  public DexApplication getPrunedApp() {
+    return prunedApp;
+  }
+
+  public Set<? extends DexReference> getAdditionalPinnedItems() {
+    return additionalPinnedItems;
+  }
+
+  public Set<DexType> getNoLongerSyntheticItems() {
+    return noLongerSyntheticItems;
+  }
+
+  public boolean hasRemovedClasses() {
+    return !removedClasses.isEmpty();
+  }
+
+  public Set<DexType> getRemovedClasses() {
+    return removedClasses;
+  }
+
+  public static class Builder {
+
+    private DexApplication prunedApp;
+
+    private final Set<DexReference> additionalPinnedItems = Sets.newIdentityHashSet();
+    private final Set<DexType> noLongerSyntheticItems = Sets.newIdentityHashSet();
+    private final Set<DexType> removedClasses = Sets.newIdentityHashSet();
+
+    public Builder setPrunedApp(DexApplication prunedApp) {
+      this.prunedApp = prunedApp;
+      return this;
+    }
+
+    public Builder addAdditionalPinnedItems(
+        Collection<? extends DexReference> additionalPinnedItems) {
+      this.additionalPinnedItems.addAll(additionalPinnedItems);
+      return this;
+    }
+
+    public Builder addNoLongerSyntheticItems(Set<DexType> noLongerSyntheticItems) {
+      this.noLongerSyntheticItems.addAll(noLongerSyntheticItems);
+      return this;
+    }
+
+    public Builder addRemovedClasses(Set<DexType> removedClasses) {
+      this.noLongerSyntheticItems.addAll(removedClasses);
+      this.removedClasses.addAll(removedClasses);
+      return this;
+    }
+
+    public PrunedItems build() {
+      return new PrunedItems(
+          prunedApp, additionalPinnedItems, noLongerSyntheticItems, removedClasses);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java b/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java
index c8fc0a8..add54d6 100644
--- a/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java
+++ b/src/main/java/com/android/tools/r8/graph/RewrittenPrototypeDescription.java
@@ -329,14 +329,16 @@
     }
   }
 
-  private static final RewrittenPrototypeDescription none = new RewrittenPrototypeDescription();
+  private static final RewrittenPrototypeDescription NONE = new RewrittenPrototypeDescription();
 
   private final List<ExtraParameter> extraParameters;
   private final ArgumentInfoCollection argumentInfoCollection;
   private final RewrittenTypeInfo rewrittenReturnInfo;
 
   private RewrittenPrototypeDescription() {
-    this(Collections.emptyList(), null, ArgumentInfoCollection.empty());
+    this.extraParameters = Collections.emptyList();
+    this.rewrittenReturnInfo = null;
+    this.argumentInfoCollection = ArgumentInfoCollection.empty();
   }
 
   private RewrittenPrototypeDescription(
@@ -347,6 +349,16 @@
     this.extraParameters = extraParameters;
     this.rewrittenReturnInfo = rewrittenReturnInfo;
     this.argumentInfoCollection = argumentsInfo;
+    assert !isEmpty();
+  }
+
+  private static RewrittenPrototypeDescription create(
+      List<ExtraParameter> extraParameters,
+      RewrittenTypeInfo rewrittenReturnInfo,
+      ArgumentInfoCollection argumentsInfo) {
+    return extraParameters.isEmpty() && rewrittenReturnInfo == null && argumentsInfo.isEmpty()
+        ? none()
+        : new RewrittenPrototypeDescription(extraParameters, rewrittenReturnInfo, argumentsInfo);
   }
 
   public static RewrittenPrototypeDescription createForUninstantiatedTypes(
@@ -356,18 +368,16 @@
     DexType returnType = method.proto.returnType;
     RewrittenTypeInfo returnInfo =
         returnType.isAlwaysNull(appView) ? RewrittenTypeInfo.toVoid(returnType, appView) : null;
-    return new RewrittenPrototypeDescription(
-        Collections.emptyList(), returnInfo, removedArgumentsInfo);
+    return create(Collections.emptyList(), returnInfo, removedArgumentsInfo);
   }
 
   public static RewrittenPrototypeDescription createForRewrittenTypes(
       RewrittenTypeInfo returnInfo, ArgumentInfoCollection rewrittenArgumentsInfo) {
-    return new RewrittenPrototypeDescription(
-        Collections.emptyList(), returnInfo, rewrittenArgumentsInfo);
+    return create(Collections.emptyList(), returnInfo, rewrittenArgumentsInfo);
   }
 
   public static RewrittenPrototypeDescription none() {
-    return none;
+    return NONE;
   }
 
   public boolean isEmpty() {
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 0b5bc41..3a9f618 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
@@ -8,7 +8,9 @@
 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.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneMap;
 import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
@@ -19,8 +21,10 @@
   private final BidirectionalManyToOneMap<DexType, DexType> mergedClasses;
 
   public HorizontallyMergedLambdaClasses(Map<DexType, LambdaGroup> lambdas) {
-    this.mergedClasses = new BidirectionalManyToOneMap<>();
+    MutableBidirectionalManyToOneMap<DexType, DexType> mergedClasses =
+        new BidirectionalManyToOneHashMap<>();
     lambdas.forEach((lambda, group) -> mergedClasses.put(lambda, group.getGroupClassType()));
+    this.mergedClasses = mergedClasses;
   }
 
   public static HorizontallyMergedLambdaClasses empty() {
@@ -29,7 +33,7 @@
 
   @Override
   public void forEachMergeGroup(BiConsumer<Set<DexType>, DexType> consumer) {
-    mergedClasses.forEach(consumer);
+    mergedClasses.forEachManyToOneMapping(consumer);
   }
 
   @Override
@@ -38,6 +42,11 @@
   }
 
   @Override
+  public boolean isMergeTarget(DexType type) {
+    return mergedClasses.containsValue(type);
+  }
+
+  @Override
   public boolean verifyAllSourcesPruned(AppView<AppInfoWithLiveness> appView) {
     for (DexType source : mergedClasses.keySet()) {
       assert appView.appInfo().wasPruned(source)
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 25879f1..ae77e17 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
@@ -17,6 +17,8 @@
 
   boolean hasBeenMergedIntoDifferentType(DexType type);
 
+  boolean isMergeTarget(DexType type);
+
   boolean verifyAllSourcesPruned(AppView<AppInfoWithLiveness> appView);
 
   /**
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 5875506..33bee51 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
@@ -38,6 +38,16 @@
   }
 
   @Override
+  public boolean isMergeTarget(DexType type) {
+    for (MergedClasses mergedClasses : collection) {
+      if (mergedClasses.isMergeTarget(type)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
   public boolean verifyAllSourcesPruned(AppView<AppInfoWithLiveness> appView) {
     for (MergedClasses mergedClasses : collection) {
       assert mergedClasses.verifyAllSourcesPruned(appView);
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
index 79e4ef7..9aabf18 100644
--- a/src/main/java/com/android/tools/r8/graph/classmerging/StaticallyMergedClasses.java
+++ b/src/main/java/com/android/tools/r8/graph/classmerging/StaticallyMergedClasses.java
@@ -8,7 +8,10 @@
 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.BidirectionalManyToOneHashMap;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
+import com.android.tools.r8.utils.collections.EmptyBidirectionalOneToOneMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneMap;
 import java.util.Set;
 import java.util.function.BiConsumer;
 
@@ -21,7 +24,7 @@
   }
 
   public static StaticallyMergedClasses empty() {
-    return new StaticallyMergedClasses(BidirectionalManyToOneMap.empty());
+    return new StaticallyMergedClasses(new EmptyBidirectionalOneToOneMap<>());
   }
 
   public static Builder builder() {
@@ -30,7 +33,7 @@
 
   @Override
   public void forEachMergeGroup(BiConsumer<Set<DexType>, DexType> consumer) {
-    mergedClasses.forEach(consumer);
+    mergedClasses.forEachManyToOneMapping(consumer);
   }
 
   @Override
@@ -39,14 +42,21 @@
   }
 
   @Override
+  public boolean isMergeTarget(DexType type) {
+    // Intentionally returns false since static class merging technically doesn't merge any classes,
+    // it only moves static members.
+    return false;
+  }
+
+  @Override
   public boolean verifyAllSourcesPruned(AppView<AppInfoWithLiveness> appView) {
     return true;
   }
 
   public static class Builder {
 
-    private final BidirectionalManyToOneMap<DexType, DexType> mergedClasses =
-        new BidirectionalManyToOneMap<>();
+    private final MutableBidirectionalManyToOneMap<DexType, DexType> mergedClasses =
+        new BidirectionalManyToOneHashMap<>();
 
     private Builder() {}
 
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 6ba32e9..3b59401 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
@@ -8,6 +8,7 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
+import com.android.tools.r8.utils.collections.EmptyBidirectionalOneToOneMap;
 import java.util.Collection;
 import java.util.Map;
 import java.util.Set;
@@ -22,12 +23,12 @@
   }
 
   public static VerticallyMergedClasses empty() {
-    return new VerticallyMergedClasses(BidirectionalManyToOneMap.empty());
+    return new VerticallyMergedClasses(new EmptyBidirectionalOneToOneMap<>());
   }
 
   @Override
   public void forEachMergeGroup(BiConsumer<Set<DexType>, DexType> consumer) {
-    mergedClasses.forEach(consumer);
+    mergedClasses.forEachManyToOneMapping(consumer);
   }
 
   public Map<DexType, DexType> getForwardMap() {
@@ -55,7 +56,8 @@
     return mergedClasses.isEmpty();
   }
 
-  public boolean isTarget(DexType type) {
+  @Override
+  public boolean isMergeTarget(DexType type) {
     return !getSourcesFor(type).isEmpty();
   }
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInitializerSynthesizedCode.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInitializerSynthesizedCode.java
new file mode 100644
index 0000000..c919e63
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInitializerSynthesizedCode.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.horizontalclassmerging;
+
+import static com.android.tools.r8.utils.ConsumerUtils.apply;
+import static java.lang.Integer.max;
+
+import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.cf.code.CfGoto;
+import com.android.tools.r8.cf.code.CfInstruction;
+import com.android.tools.r8.cf.code.CfLabel;
+import com.android.tools.r8.cf.code.CfReturnVoid;
+import com.android.tools.r8.graph.CfCode;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.CfVersionUtils;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class ClassInitializerSynthesizedCode {
+  private final List<DexEncodedMethod> staticClassInitializers;
+  private int maxStack = 0;
+  private int maxLocals = 0;
+
+  private ClassInitializerSynthesizedCode(List<DexEncodedMethod> staticClassInitializers) {
+    this.staticClassInitializers = staticClassInitializers;
+  }
+
+  public boolean isEmpty() {
+    return staticClassInitializers.isEmpty();
+  }
+
+  private void addCfCode(List<CfInstruction> newInstructions, DexEncodedMethod method) {
+    CfCode code = method.getCode().asCfCode();
+    maxStack = max(maxStack, code.getMaxStack());
+    maxLocals = max(maxLocals, code.getMaxLocals());
+
+    CfLabel endLabel = new CfLabel();
+    boolean requiresLabel = false;
+    int index = 1;
+    for (CfInstruction instruction : code.getInstructions()) {
+      if (instruction.isReturn()) {
+        if (code.getInstructions().size() != index) {
+          newInstructions.add(new CfGoto(endLabel));
+          requiresLabel = true;
+        }
+      } else {
+        newInstructions.add(instruction);
+      }
+
+      index++;
+    }
+    if (requiresLabel) {
+      newInstructions.add(endLabel);
+    }
+  }
+
+  public CfCode synthesizeCode(DexType originalHolder) {
+    return new CfCode(
+        originalHolder,
+        maxStack,
+        maxLocals,
+        buildInstructions(),
+        Collections.emptyList(),
+        Collections.emptyList());
+  }
+
+  private List<CfInstruction> buildInstructions() {
+    List<CfInstruction> newInstructions = new ArrayList<>();
+    staticClassInitializers.forEach(apply(this::addCfCode, newInstructions));
+    newInstructions.add(new CfReturnVoid());
+    return newInstructions;
+  }
+
+  public DexEncodedMethod getFirst() {
+    return staticClassInitializers.iterator().next();
+  }
+
+  public CfVersion getCfVersion() {
+    return CfVersionUtils.max(staticClassInitializers);
+  }
+
+  public static class Builder {
+    private final List<DexEncodedMethod> staticClassInitializers = new ArrayList<>();
+
+    public void add(DexEncodedMethod method) {
+      assert method.isClassInitializer();
+      assert method.hasCode();
+      assert method.getCode().isCfCode();
+      staticClassInitializers.add(method);
+    }
+
+    public ClassInitializerSynthesizedCode build() {
+      return new ClassInitializerSynthesizedCode(staticClassInitializers);
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
index 26ed21c..96282ee 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassInstanceFieldsMerger.java
@@ -5,11 +5,12 @@
 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.IterableUtils;
 import com.android.tools.r8.utils.ListUtils;
+import com.google.common.collect.Iterables;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.IdentityHashMap;
@@ -50,20 +51,19 @@
     }
   }
 
-  private void mergeField(DexEncodedField oldField, DexEncodedField newField) {
-    if (newField.isFinal() && !oldField.isFinal()) {
+  private void fixAccessFlags(DexEncodedField newField, Collection<DexEncodedField> oldFields) {
+    if (newField.isFinal() && Iterables.any(oldFields, oldField -> !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));
+  private void mergeFields(DexEncodedField newField, List<DexEncodedField> oldFields) {
+    fixAccessFlags(newField, oldFields);
+    lensBuilder.recordNewFieldSignature(
+        Iterables.transform(
+            IterableUtils.append(oldFields, newField), DexEncodedField::getReference),
+        newField.getReference(),
+        newField.getReference());
   }
 
   public void merge() {
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 04b4b68..950cf9a 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassMerger.java
@@ -6,8 +6,10 @@
 
 import static com.google.common.base.Predicates.not;
 
+import com.android.tools.r8.cf.CfVersion;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.DexAnnotationSet;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -19,6 +21,9 @@
 import com.android.tools.r8.graph.DexTypeList;
 import com.android.tools.r8.graph.FieldAccessFlags;
 import com.android.tools.r8.graph.GenericSignature.FieldTypeSignature;
+import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
+import com.android.tools.r8.graph.MethodAccessFlags;
+import com.android.tools.r8.graph.ParameterAnnotationsList;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.FieldAccessInfoCollectionModifier;
@@ -46,8 +51,10 @@
 
   public static final String CLASS_ID_FIELD_NAME = "$r8$classId";
 
+  private final AppView<AppInfoWithLiveness> appView;
   private final MergeGroup group;
   private final DexItemFactory dexItemFactory;
+  private final ClassInitializerSynthesizedCode classInitializerSynthesizedCode;
   private final HorizontalClassMergerGraphLens.Builder lensBuilder;
   private final HorizontallyMergedClasses.Builder mergedClassesBuilder;
   private final FieldAccessInfoCollectionModifier.Builder fieldAccessChangesBuilder;
@@ -66,7 +73,9 @@
       FieldAccessInfoCollectionModifier.Builder fieldAccessChangesBuilder,
       MergeGroup group,
       Collection<VirtualMethodMerger> virtualMethodMergers,
-      Collection<ConstructorMerger> constructorMergers) {
+      Collection<ConstructorMerger> constructorMergers,
+      ClassInitializerSynthesizedCode classInitializerSynthesizedCode) {
+    this.appView = appView;
     this.lensBuilder = lensBuilder;
     this.mergedClassesBuilder = mergedClassesBuilder;
     this.fieldAccessChangesBuilder = fieldAccessChangesBuilder;
@@ -75,6 +84,7 @@
     this.constructorMergers = constructorMergers;
 
     this.dexItemFactory = appView.dexItemFactory();
+    this.classInitializerSynthesizedCode = classInitializerSynthesizedCode;
     this.classStaticFieldsMerger = new ClassStaticFieldsMerger(appView, lensBuilder, group);
     this.classInstanceFieldsMerger = new ClassInstanceFieldsMerger(lensBuilder, group);
 
@@ -91,18 +101,54 @@
   }
 
   void mergeDirectMethods(SyntheticArgumentClass syntheticArgumentClass) {
+    mergeStaticClassInitializers();
     mergeDirectMethods(group.getTarget());
     group.forEachSource(this::mergeDirectMethods);
     mergeConstructors(syntheticArgumentClass);
   }
 
+  void mergeStaticClassInitializers() {
+    if (classInitializerSynthesizedCode.isEmpty()) {
+      return;
+    }
+
+    DexMethod newClinit = dexItemFactory.createClassInitializer(group.getTarget().getType());
+
+    CfCode code = classInitializerSynthesizedCode.synthesizeCode(group.getTarget().getType());
+    if (!group.getTarget().hasClassInitializer()) {
+      classMethodsBuilder.addDirectMethod(
+          new DexEncodedMethod(
+              newClinit,
+              MethodAccessFlags.fromSharedAccessFlags(
+                  Constants.ACC_SYNTHETIC | Constants.ACC_STATIC, true),
+              MethodTypeSignature.noSignature(),
+              DexAnnotationSet.empty(),
+              ParameterAnnotationsList.empty(),
+              code,
+              true,
+              classInitializerSynthesizedCode.getCfVersion()));
+    } else {
+      DexEncodedMethod clinit = group.getTarget().getClassInitializer();
+      clinit.setCode(code, appView);
+      CfVersion cfVersion = classInitializerSynthesizedCode.getCfVersion();
+      if (cfVersion != null) {
+        clinit.upgradeClassFileVersion(cfVersion);
+      } else {
+        assert appView.options().isGeneratingDex();
+      }
+      classMethodsBuilder.addDirectMethod(clinit);
+    }
+  }
+
   void mergeDirectMethods(DexProgramClass toMerge) {
     toMerge.forEachProgramDirectMethod(
         method -> {
           DexEncodedMethod definition = method.getDefinition();
-          assert !definition.isClassInitializer();
-
-          if (!definition.isInstanceInitializer()) {
+          if (definition.isClassInitializer()) {
+            lensBuilder.moveMethod(
+                method.getReference(),
+                dexItemFactory.createClassInitializer(group.getTarget().getType()));
+          } else if (!definition.isInstanceInitializer()) {
             DexMethod newMethod =
                 method.getReference().withHolder(group.getTarget().getType(), dexItemFactory);
             if (!classMethodsBuilder.isFresh(newMethod)) {
@@ -114,7 +160,6 @@
             }
           }
         });
-
     // Clear the members of the class to be merged since they have now been moved to the target.
     toMerge.getMethodCollection().clearDirectMethods();
   }
@@ -217,6 +262,8 @@
   public static class Builder {
     private final AppView<AppInfoWithLiveness> appView;
     private final MergeGroup group;
+    private final ClassInitializerSynthesizedCode.Builder classInitializerSynthesizedCodeBuilder =
+        new ClassInitializerSynthesizedCode.Builder();
     private final Map<DexProto, ConstructorMerger.Builder> constructorMergerBuilders =
         new LinkedHashMap<>();
     private final List<ConstructorMerger.Builder> unmergedConstructorBuilders = new ArrayList<>();
@@ -242,20 +289,17 @@
     }
 
     private void setupForMethodMerging(DexProgramClass toMerge) {
-      toMerge.forEachProgramDirectMethod(
-          method -> {
-            DexEncodedMethod definition = method.getDefinition();
-            assert !definition.isClassInitializer();
-            if (definition.isInstanceInitializer()) {
-              addConstructor(method);
-            }
-          });
+      if (toMerge.hasClassInitializer()) {
+        classInitializerSynthesizedCodeBuilder.add(toMerge.getClassInitializer());
+      }
+      toMerge.forEachProgramDirectMethodMatching(
+          DexEncodedMethod::isInstanceInitializer, this::addConstructor);
       toMerge.forEachProgramVirtualMethod(this::addVirtualMethod);
     }
 
     private void addConstructor(ProgramMethod method) {
       assert method.getDefinition().isInstanceInitializer();
-      if (appView.options().enableHorizontalClassMergingConstructorMerging) {
+      if (appView.options().horizontalClassMergerOptions().isConstructorMergingEnabled()) {
         constructorMergerBuilders
             .computeIfAbsent(
                 method.getDefinition().getProto(), ignore -> new ConstructorMerger.Builder(appView))
@@ -276,7 +320,7 @@
     }
 
     private Collection<ConstructorMerger.Builder> getConstructorMergerBuilders() {
-      return appView.options().enableHorizontalClassMergingConstructorMerging
+      return appView.options().horizontalClassMergerOptions().isConstructorMergingEnabled()
           ? constructorMergerBuilders.values()
           : unmergedConstructorBuilders;
     }
@@ -313,7 +357,8 @@
           fieldAccessChangesBuilder,
           group,
           virtualMethodMergers,
-          constructorMergers);
+          constructorMergers,
+          classInitializerSynthesizedCodeBuilder.build());
     }
   }
 }
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 7bc85d1..80180f6 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassStaticFieldsMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ClassStaticFieldsMerger.java
@@ -33,7 +33,7 @@
     this.dexItemFactory = appView.dexItemFactory();
   }
 
-  private final boolean isFresh(DexField fieldReference) {
+  private boolean isFresh(DexField fieldReference) {
     return !targetFields.containsKey(fieldReference);
   }
 
@@ -48,7 +48,7 @@
     field = field.toTypeSubstitutedField(newFieldReference);
     targetFields.put(newFieldReference, field);
 
-    lensBuilder.moveField(oldFieldReference, newFieldReference);
+    lensBuilder.recordNewFieldSignature(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 d2cd444..01d5771 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/ConstructorMerger.java
@@ -4,6 +4,8 @@
 
 package com.android.tools.r8.horizontalclassmerging;
 
+import static com.android.tools.r8.dex.Constants.TEMPORARY_INSTANCE_INITIALIZER_PREFIX;
+
 import com.android.tools.r8.cf.CfVersion;
 import com.android.tools.r8.dex.Constants;
 import com.android.tools.r8.graph.AppView;
@@ -28,7 +30,6 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.LinkedList;
 import java.util.List;
 
 public class ConstructorMerger {
@@ -111,7 +112,7 @@
       ClassMethodsBuilder classMethodsBuilder, DexEncodedMethod constructor) {
     DexMethod method =
         dexItemFactory.createFreshMethodName(
-            "constructor",
+            TEMPORARY_INSTANCE_INITIALIZER_PREFIX,
             constructor.getHolderType(),
             constructor.proto(),
             group.getTarget().getType(),
@@ -152,7 +153,7 @@
       }
       DexMethod movedConstructor = moveConstructor(classMethodsBuilder, constructor);
       lensBuilder.mapMethod(movedConstructor, movedConstructor);
-      lensBuilder.mapMethodInverse(constructor.method, movedConstructor);
+      lensBuilder.recordNewMethodSignature(constructor.getReference(), movedConstructor);
       typeConstructorClassMap.put(
           classIdentifiers.getInt(constructor.getHolderType()), movedConstructor);
     }
@@ -165,13 +166,29 @@
             classMethodsBuilder::isFresh);
     int extraNulls = newConstructorReference.getArity() - methodReferenceTemplate.getArity();
 
-    DexMethod representativeConstructorReference = constructors.iterator().next().method;
+    DexEncodedMethod representative = constructors.iterator().next();
+    DexMethod originalConstructorReference =
+        appView.graphLens().getOriginalMethodSignature(representative.getReference());
+
+    // Create a special original method signature for the synthesized constructor that did not exist
+    // prior to horizontal class merging. Otherwise we might accidentally think that the synthesized
+    // constructor corresponds to the previous <init>() method on the target class, which could have
+    // unintended side-effects such as leading to unused argument removal being applied to the
+    // synthesized constructor all-though it by construction doesn't have any unused arguments.
+    DexMethod bridgeConstructorReference =
+        dexItemFactory.createFreshMethodName(
+            "$r8$init$bridge",
+            null,
+            originalConstructorReference.getProto(),
+            originalConstructorReference.getHolderType(),
+            classMethodsBuilder::isFresh);
+
     ConstructorEntryPointSynthesizedCode synthesizedCode =
         new ConstructorEntryPointSynthesizedCode(
             typeConstructorClassMap,
             newConstructorReference,
             group.getClassIdField(),
-            appView.graphLens().getOriginalMethodSignature(representativeConstructorReference));
+            bridgeConstructorReference);
     DexEncodedMethod newConstructor =
         new DexEncodedMethod(
             newConstructorReference,
@@ -183,34 +200,20 @@
             true,
             classFileVersion);
 
-    if (isTrivialMerge()) {
-      // The constructor does not require the additional argument, just map it like a regular
-      // method.
-      DexEncodedMethod oldConstructor = constructors.iterator().next();
-      if (extraNulls > 0) {
-        List<ExtraParameter> extraParameters = new LinkedList<>();
-        extraParameters.addAll(Collections.nCopies(extraNulls, new ExtraUnusedNullParameter()));
-        lensBuilder.moveMergedConstructor(
-            oldConstructor.method, newConstructorReference, extraParameters);
-      } else {
-        lensBuilder.moveMethod(oldConstructor.method, newConstructorReference);
-      }
-    } else {
-      // Map each old constructor to the newly synthesized constructor in the graph lens.
-      for (DexEncodedMethod oldConstructor : constructors) {
+    // Map each old constructor to the newly synthesized constructor in the graph lens.
+    for (DexEncodedMethod oldConstructor : constructors) {
+      List<ExtraParameter> extraParameters = new ArrayList<>();
+      if (constructors.size() > 1) {
         int classIdentifier = classIdentifiers.getInt(oldConstructor.getHolderType());
-
-        List<ExtraParameter> extraParameters = new LinkedList<>();
         extraParameters.add(new ExtraConstantIntParameter(classIdentifier));
-        extraParameters.addAll(Collections.nCopies(extraNulls, new ExtraUnusedNullParameter()));
-
-        lensBuilder.moveMergedConstructor(
-            oldConstructor.method, newConstructorReference, extraParameters);
       }
+      extraParameters.addAll(Collections.nCopies(extraNulls, new ExtraUnusedNullParameter()));
+      lensBuilder.mapMergedConstructor(
+          oldConstructor.getReference(), newConstructorReference, extraParameters);
     }
-    // Map the first constructor to the newly synthesized constructor.
-    lensBuilder.recordExtraOriginalSignature(
-        representativeConstructorReference, newConstructorReference);
+
+    // Add a mapping from a synthetic name to the synthetic constructor.
+    lensBuilder.recordNewMethodSignature(bridgeConstructorReference, newConstructorReference);
 
     classMethodsBuilder.addDirectMethod(newConstructor);
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
index 0821a4c..5983618 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMerger.java
@@ -15,6 +15,7 @@
 import com.android.tools.r8.horizontalclassmerging.policies.IgnoreSynthetics;
 import com.android.tools.r8.horizontalclassmerging.policies.LimitGroups;
 import com.android.tools.r8.horizontalclassmerging.policies.NoAnnotations;
+import com.android.tools.r8.horizontalclassmerging.policies.NoClassInitializerWithObservableSideEffects;
 import com.android.tools.r8.horizontalclassmerging.policies.NoClassesOrMembersWithAnnotations;
 import com.android.tools.r8.horizontalclassmerging.policies.NoDirectRuntimeTypeChecks;
 import com.android.tools.r8.horizontalclassmerging.policies.NoEnums;
@@ -26,7 +27,6 @@
 import com.android.tools.r8.horizontalclassmerging.policies.NoKotlinMetadata;
 import com.android.tools.r8.horizontalclassmerging.policies.NoNativeMethods;
 import com.android.tools.r8.horizontalclassmerging.policies.NoServiceLoaders;
-import com.android.tools.r8.horizontalclassmerging.policies.NoStaticClassInitializer;
 import com.android.tools.r8.horizontalclassmerging.policies.NotMatchedByNoHorizontalClassMerging;
 import com.android.tools.r8.horizontalclassmerging.policies.NotVerticallyMergedIntoSubtype;
 import com.android.tools.r8.horizontalclassmerging.policies.PreserveMethodCharacteristics;
@@ -64,11 +64,9 @@
     MergeGroup initialGroup = new MergeGroup(appView.appInfo().classesWithDeterministicOrder());
 
     // Run the policies on all program classes to produce a final grouping.
+    List<Policy> policies = getPolicies(mainDexTracingResult, runtimeTypeCheckInfo);
     Collection<MergeGroup> groups =
-        new SimplePolicyExecutor()
-            .run(
-                Collections.singletonList(initialGroup),
-                getPolicies(mainDexTracingResult, runtimeTypeCheckInfo));
+        new SimplePolicyExecutor().run(Collections.singletonList(initialGroup), policies);
 
     // If there are no groups, then end horizontal class merging.
     if (groups.isEmpty()) {
@@ -116,7 +114,7 @@
         new IgnoreSynthetics(appView),
         new NoClassesOrMembersWithAnnotations(),
         new NoInnerClasses(),
-        new NoStaticClassInitializer(),
+        new NoClassInitializerWithObservableSideEffects(),
         new NoNativeMethods(),
         new NoKeepRules(appView),
         new NoKotlinMetadata(),
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 16f853e..3adad2e 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontalClassMergerGraphLens.java
@@ -8,89 +8,51 @@
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
-import com.android.tools.r8.graph.RewrittenPrototypeDescription;
 import com.android.tools.r8.ir.conversion.ExtraParameter;
 import com.android.tools.r8.utils.IterableUtils;
-import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
-import com.google.common.collect.BiMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeHashMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
+import com.android.tools.r8.utils.collections.BidirectionalOneToManyRepresentativeHashMap;
+import com.android.tools.r8.utils.collections.BidirectionalOneToManyRepresentativeMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneRepresentativeMap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Streams;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.function.Function;
 
 public class HorizontalClassMergerGraphLens extends NestedGraphLens {
 
   private final Map<DexMethod, List<ExtraParameter>> methodExtraParameters;
-  private final Map<DexMethod, DexMethod> extraOriginalMethodSignatures;
   private final HorizontallyMergedClasses mergedClasses;
-  private final Map<DexField, DexField> extraOriginalFieldSignatures;
 
   private HorizontalClassMergerGraphLens(
       AppView<?> appView,
       HorizontallyMergedClasses mergedClasses,
       Map<DexMethod, List<ExtraParameter>> methodExtraParameters,
-      Map<DexField, DexField> fieldMap,
+      BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap,
       Map<DexMethod, DexMethod> methodMap,
-      BiMap<DexField, DexField> originalFieldSignatures,
-      BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures,
-      Map<DexMethod, DexMethod> extraOriginalMethodSignatures,
-      Map<DexField, DexField> extraOriginalFieldSignatures,
-      GraphLens previousLens) {
+      BidirectionalOneToManyRepresentativeMap<DexMethod, DexMethod> originalMethodSignatures) {
     super(
         mergedClasses.getForwardMap(),
         methodMap,
         fieldMap,
-        originalFieldSignatures,
         originalMethodSignatures,
-        previousLens,
+        appView.graphLens(),
         appView.dexItemFactory());
     this.methodExtraParameters = methodExtraParameters;
-    this.extraOriginalFieldSignatures = extraOriginalFieldSignatures;
-    this.extraOriginalMethodSignatures = extraOriginalMethodSignatures;
     this.mergedClasses = mergedClasses;
   }
 
-  private boolean isSynthesizedByHorizontalClassMerging(DexMethod method) {
-    return methodExtraParameters.containsKey(method);
-  }
-
   @Override
   protected Iterable<DexType> internalGetOriginalTypes(DexType previous) {
     return IterableUtils.prependSingleton(previous, mergedClasses.getSourcesFor(previous));
   }
 
-  @Override
-  public RewrittenPrototypeDescription lookupPrototypeChangesForMethodDefinition(DexMethod method) {
-    if (isSynthesizedByHorizontalClassMerging(method)) {
-      // If we are processing the call site, the arguments should be removed.
-      return RewrittenPrototypeDescription.none();
-    }
-    return super.lookupPrototypeChangesForMethodDefinition(method);
-  }
-
-  @Override
-  public DexMethod getOriginalMethodSignature(DexMethod 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.
@@ -111,87 +73,121 @@
   }
 
   public static class Builder {
-    private ManyToOneMap<DexField, DexField> fieldMap = new ManyToOneMap<>();
-    private ManyToOneMap<DexMethod, DexMethod> methodMap = new ManyToOneMap<>();
+
+    private final MutableBidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap =
+        new BidirectionalManyToOneRepresentativeHashMap<>();
+    private final BidirectionalManyToOneHashMap<DexMethod, DexMethod> methodMap =
+        new BidirectionalManyToOneHashMap<>();
+    private final BidirectionalOneToManyRepresentativeHashMap<DexMethod, DexMethod>
+        originalMethodSignatures = new BidirectionalOneToManyRepresentativeHashMap<>();
     private final Map<DexMethod, List<ExtraParameter>> methodExtraParameters =
         new IdentityHashMap<>();
 
+    private final BidirectionalManyToOneHashMap<DexMethod, DexMethod> pendingMethodMapUpdates =
+        new BidirectionalManyToOneHashMap<>();
+    private final BidirectionalOneToManyRepresentativeHashMap<DexMethod, DexMethod>
+        pendingOriginalMethodSignatureUpdates = new BidirectionalOneToManyRepresentativeHashMap<>();
+
     Builder() {}
 
-    public HorizontalClassMergerGraphLens build(
+    HorizontalClassMergerGraphLens build(
         AppView<?> appView, HorizontallyMergedClasses mergedClasses) {
-      ManyToOneInverseMap<DexMethod, DexMethod> inverseMethodMap =
-          methodMap.inverse(
-              group -> {
-                // Every group should have a representative. Fail in debug mode.
-                assert false;
-                return group.iterator().next();
-              });
-      ManyToOneInverseMap<DexField, DexField> inverseFieldMap =
-          fieldMap.inverse(
-              group -> {
-                // Every group should have a representative. Fail in debug mode.
-                assert false;
-                return group.iterator().next();
-              });
-
+      assert pendingMethodMapUpdates.isEmpty();
+      assert pendingOriginalMethodSignatureUpdates.isEmpty();
       return new HorizontalClassMergerGraphLens(
           appView,
           mergedClasses,
           methodExtraParameters,
-          fieldMap.getForwardMap(),
+          fieldMap,
           methodMap.getForwardMap(),
-          inverseFieldMap.getBiMap().getForwardBacking(),
-          inverseMethodMap.getBiMap(),
-          inverseMethodMap.getExtraMap(),
-          inverseFieldMap.getExtraMap(),
-          appView.graphLens());
+          originalMethodSignatures);
     }
 
-    public void remapMethods(BiMap<DexMethod, DexMethod> remapMethods) {
-      methodMap = methodMap.remap(remapMethods, Function.identity(), Function.identity());
+    void recordNewFieldSignature(DexField oldFieldSignature, DexField newFieldSignature) {
+      fieldMap.put(oldFieldSignature, newFieldSignature);
     }
 
-    public void remapFields(BiMap<DexField, DexField> remapFields) {
-      fieldMap = fieldMap.remap(remapFields, Function.identity(), Function.identity());
+    void recordNewFieldSignature(
+        Iterable<DexField> oldFieldSignatures,
+        DexField newFieldSignature,
+        DexField representative) {
+      assert Streams.stream(oldFieldSignatures)
+          .anyMatch(oldFieldSignature -> oldFieldSignature != newFieldSignature);
+      assert Streams.stream(oldFieldSignatures).noneMatch(fieldMap::containsValue);
+      assert Iterables.contains(oldFieldSignatures, representative);
+      for (DexField oldFieldSignature : oldFieldSignatures) {
+        recordNewFieldSignature(oldFieldSignature, newFieldSignature);
+      }
+      fieldMap.setRepresentative(newFieldSignature, representative);
     }
 
-    public Builder moveField(DexField from, DexField to) {
-      fieldMap.put(from, to);
-      fieldMap.putInverse(from, to);
-      return this;
+    void fixupField(DexField oldFieldSignature, DexField newFieldSignature) {
+      Set<DexField> originalFieldSignatures = fieldMap.removeValue(oldFieldSignature);
+      if (originalFieldSignatures.isEmpty()) {
+        fieldMap.put(oldFieldSignature, newFieldSignature);
+      } else if (originalFieldSignatures.size() == 1) {
+        fieldMap.put(originalFieldSignatures.iterator().next(), newFieldSignature);
+      } else {
+        for (DexField originalFieldSignature : originalFieldSignatures) {
+          fieldMap.put(originalFieldSignature, newFieldSignature);
+        }
+        DexField representative = fieldMap.removeRepresentativeFor(oldFieldSignature);
+        assert representative != null;
+        fieldMap.setRepresentative(newFieldSignature, representative);
+      }
     }
 
-    public Builder setRepresentativeField(DexField from, DexField to) {
-      fieldMap.setRepresentative(from, to);
-      return this;
+    void mapMethod(DexMethod oldMethodSignature, DexMethod newMethodSignature) {
+      methodMap.put(oldMethodSignature, newMethodSignature);
     }
 
-    /** Unidirectional mapping from one method to another. */
-    public Builder recordExtraOriginalSignature(DexMethod from, DexMethod to) {
-      methodMap.setRepresentative(from, to);
-
-      return this;
-    }
-
-    /** Unidirectional mapping from one method to another. */
-    public Builder mapMethod(DexMethod from, DexMethod to) {
-      methodMap.put(from, to);
-
-      return this;
-    }
-
-    /** Unidirectional mapping from one method to another. */
-    public Builder mapMethodInverse(DexMethod from, DexMethod to) {
-      methodMap.putInverse(from, to);
-
-      return this;
-    }
-
-    public Builder moveMethod(DexMethod from, DexMethod to) {
+    void moveMethod(DexMethod from, DexMethod to) {
       mapMethod(from, to);
-      mapMethodInverse(from, to);
-      return this;
+      recordNewMethodSignature(from, to);
+    }
+
+    void recordNewMethodSignature(DexMethod oldMethodSignature, DexMethod newMethodSignature) {
+      originalMethodSignatures.put(newMethodSignature, oldMethodSignature);
+    }
+
+    void fixupMethod(DexMethod oldMethodSignature, DexMethod newMethodSignature) {
+      fixupMethodMap(oldMethodSignature, newMethodSignature);
+      fixupOriginalMethodSignatures(oldMethodSignature, newMethodSignature);
+    }
+
+    private void fixupMethodMap(DexMethod oldMethodSignature, DexMethod newMethodSignature) {
+      Set<DexMethod> originalMethodSignatures = methodMap.getKeys(oldMethodSignature);
+      if (originalMethodSignatures.isEmpty()) {
+        pendingMethodMapUpdates.put(oldMethodSignature, newMethodSignature);
+      } else {
+        for (DexMethod originalMethodSignature : originalMethodSignatures) {
+          pendingMethodMapUpdates.put(originalMethodSignature, newMethodSignature);
+        }
+      }
+    }
+
+    private void fixupOriginalMethodSignatures(
+        DexMethod oldMethodSignature, DexMethod newMethodSignature) {
+      Set<DexMethod> oldMethodSignatures = originalMethodSignatures.getValues(oldMethodSignature);
+      if (oldMethodSignatures.isEmpty()) {
+        pendingOriginalMethodSignatureUpdates.put(newMethodSignature, oldMethodSignature);
+      } else {
+        for (DexMethod originalMethodSignature : oldMethodSignatures) {
+          pendingOriginalMethodSignatureUpdates.put(newMethodSignature, originalMethodSignature);
+        }
+      }
+    }
+
+    void commitPendingUpdates() {
+      // Commit pending method map updates.
+      methodMap.removeAll(pendingMethodMapUpdates.keySet());
+      pendingMethodMapUpdates.forEachManyToOneMapping(methodMap::put);
+      pendingMethodMapUpdates.clear();
+
+      // Commit pending original method signatures updates.
+      originalMethodSignatures.removeAll(pendingOriginalMethodSignatureUpdates.keySet());
+      pendingOriginalMethodSignatureUpdates.forEachOneToManyMapping(originalMethodSignatures::put);
+      pendingOriginalMethodSignatureUpdates.clear();
     }
 
     /**
@@ -199,24 +195,27 @@
      * where many constructors are merged into a single constructor. The synthesized constructor
      * therefore does not have a unique reverse constructor.
      */
-    public Builder moveMergedConstructor(
-        DexMethod from, DexMethod to, List<ExtraParameter> extraParameters) {
-      moveMethod(from, to);
-      methodExtraParameters.put(from, extraParameters);
-      return this;
+    void mapMergedConstructor(DexMethod from, DexMethod to, List<ExtraParameter> extraParameters) {
+      mapMethod(from, to);
+      if (extraParameters.size() > 0) {
+        methodExtraParameters.put(from, extraParameters);
+      }
     }
 
-    public Builder addExtraParameters(DexMethod to, List<ExtraParameter> extraParameters) {
-      Set<DexMethod> mapsFrom = methodMap.lookupReverse(to);
-      if (mapsFrom == null) {
-        mapsFrom = Collections.singleton(to);
+    void addExtraParameters(DexMethod methodSignature, List<ExtraParameter> extraParameters) {
+      Set<DexMethod> originalMethodSignatures = methodMap.getKeys(methodSignature);
+      if (originalMethodSignatures.isEmpty()) {
+        methodExtraParameters
+            .computeIfAbsent(methodSignature, ignore -> new ArrayList<>(extraParameters.size()))
+            .addAll(extraParameters);
+      } else {
+        for (DexMethod originalMethodSignature : originalMethodSignatures) {
+          methodExtraParameters
+              .computeIfAbsent(
+                  originalMethodSignature, ignore -> new ArrayList<>(extraParameters.size()))
+              .addAll(extraParameters);
+        }
       }
-      mapsFrom.forEach(
-          originalFrom ->
-              methodExtraParameters
-                  .computeIfAbsent(originalFrom, ignore -> new ArrayList<>(extraParameters.size()))
-                  .addAll(extraParameters));
-      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 4d6d93f..93f9d3f 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/HorizontallyMergedClasses.java
@@ -8,7 +8,10 @@
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.classmerging.MergedClasses;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
 import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
+import com.android.tools.r8.utils.collections.EmptyBidirectionalOneToOneMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneMap;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.BiConsumer;
@@ -22,12 +25,12 @@
   }
 
   public static HorizontallyMergedClasses empty() {
-    return new HorizontallyMergedClasses(new BidirectionalManyToOneMap<>());
+    return new HorizontallyMergedClasses(new EmptyBidirectionalOneToOneMap<>());
   }
 
   @Override
   public void forEachMergeGroup(BiConsumer<Set<DexType>, DexType> consumer) {
-    mergedClasses.forEach(consumer);
+    mergedClasses.forEachManyToOneMapping(consumer);
   }
 
   public DexType getMergeTargetOrDefault(DexType type) {
@@ -42,13 +45,18 @@
     return mergedClasses.getKeys(type);
   }
 
-  @Override
-  public boolean hasBeenMergedIntoDifferentType(DexType type) {
-    return mergedClasses.hasKey(type);
+  public Set<DexType> getTargets() {
+    return mergedClasses.values();
   }
 
+  @Override
+  public boolean hasBeenMergedIntoDifferentType(DexType type) {
+    return mergedClasses.containsKey(type);
+  }
+
+  @Override
   public boolean isMergeTarget(DexType type) {
-    return mergedClasses.hasValue(type);
+    return mergedClasses.containsValue(type);
   }
 
   public boolean hasBeenMergedOrIsMergeTarget(DexType type) {
@@ -71,8 +79,8 @@
   }
 
   public static class Builder {
-    private final BidirectionalManyToOneMap<DexType, DexType> mergedClasses =
-        new BidirectionalManyToOneMap<>();
+    private final MutableBidirectionalManyToOneMap<DexType, DexType> mergedClasses =
+        new BidirectionalManyToOneHashMap<>();
 
     public HorizontallyMergedClasses build() {
       return new HorizontallyMergedClasses(mergedClasses);
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ManyToOneInverseMap.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ManyToOneInverseMap.java
deleted file mode 100644
index 973162f..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ManyToOneInverseMap.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.horizontalclassmerging;
-
-import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
-import java.util.Map;
-
-/** The inverse of a {@link ManyToOneMap} used for generating graph lens maps. */
-public class ManyToOneInverseMap<K, V> {
-  private final BidirectionalOneToOneHashMap<V, K> biMap;
-  private final Map<V, K> extraMap;
-
-  ManyToOneInverseMap(BidirectionalOneToOneHashMap<V, K> biMap, Map<V, K> extraMap) {
-    this.biMap = biMap;
-    this.extraMap = extraMap;
-  }
-
-  public BidirectionalOneToOneHashMap<V, K> getBiMap() {
-    return biMap;
-  }
-
-  public Map<V, K> getExtraMap() {
-    return extraMap;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/ManyToOneMap.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/ManyToOneMap.java
deleted file mode 100644
index 5204021..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/ManyToOneMap.java
+++ /dev/null
@@ -1,121 +0,0 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.horizontalclassmerging;
-
-import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
-import com.google.common.collect.BiMap;
-import com.google.common.collect.HashBiMap;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.IdentityHashMap;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.function.Function;
-
-/**
- * This mapping class is used to track method mappings for horizontal class merging. Essentially it
- * is a bidirectional many to one map, but with support for having unidirectional mappings and with
- * support for remapping the values to new values using {@link ManyToOneMap#remap(BiMap, Function,
- * Function)}. It also supports generating an inverse mapping {@link ManyToOneInverseMap} that can
- * be used by the graph lens using {@link ManyToOneMap#inverse(Function)}. The inverse map is a
- * bidirectional one to one map with additional non-bidirectional representative entries.
- */
-public class ManyToOneMap<K, V> {
-  private final Map<K, V> forwardMap = new IdentityHashMap<>();
-  private final Map<V, Set<K>> inverseMap = new IdentityHashMap<>();
-  private final Map<V, K> representativeMap = new IdentityHashMap<>();
-
-  public Map<K, V> getForwardMap() {
-    return forwardMap;
-  }
-
-  public Set<K> lookupReverse(V to) {
-    return inverseMap.get(to);
-  }
-
-  public V put(K from, V to) {
-    return forwardMap.put(from, to);
-  }
-
-  public void putInverse(K from, V to) {
-    inverseMap.computeIfAbsent(to, ignore -> new HashSet<>()).add(from);
-  }
-
-  public K setRepresentative(K from, V to) {
-    putInverse(from, to);
-    return representativeMap.put(to, from);
-  }
-
-  public ManyToOneInverseMap<K, V> inverse(Function<Set<K>, K> pickRepresentative) {
-    BidirectionalOneToOneHashMap<V, K> biMap = new BidirectionalOneToOneHashMap<>();
-    Map<V, K> extraMap = new HashMap<>();
-    for (Entry<V, Set<K>> entry : inverseMap.entrySet()) {
-      K representative = representativeMap.get(entry.getKey());
-      if (entry.getValue().size() == 1) {
-        K singleton = entry.getValue().iterator().next();
-        assert representative == null || singleton == representative;
-        if (representative == null) {
-          biMap.put(entry.getKey(), singleton);
-        } else {
-          extraMap.put(entry.getKey(), singleton);
-        }
-      } else {
-        if (representative == null) {
-          representative = pickRepresentative.apply(entry.getValue());
-        } else {
-          assert representative == entry.getKey() || entry.getValue().contains(representative);
-        }
-        extraMap.put(entry.getKey(), representative);
-      }
-    }
-
-    return new ManyToOneInverseMap<>(biMap, extraMap);
-  }
-
-  public <NewV> ManyToOneMap<K, NewV> remap(
-      BiMap<V, NewV> biMap, Function<V, NewV> notInBiMap, Function<V, K> notInForwardMap) {
-    ManyToOneMap<K, NewV> newMap = new ManyToOneMap<>();
-
-    // All entries that should be remapped and are already in the forward and/or inverse mappings
-    // should only be remapped in the directions they are already mapped in.
-    BiMap<V, NewV> biMapCopy = HashBiMap.create(biMap);
-    for (Entry<V, Set<K>> entry : inverseMap.entrySet()) {
-      NewV to = biMapCopy.remove(entry.getKey());
-      if (to == null) {
-        to = biMap.getOrDefault(entry.getKey(), notInBiMap.apply(entry.getKey()));
-      }
-      newMap.inverseMap.put(to, entry.getValue());
-    }
-    for (Entry<K, V> entry : forwardMap.entrySet()) {
-      NewV newTo = biMapCopy.remove(entry.getValue());
-      if (newTo == null) {
-        newTo = biMap.getOrDefault(entry.getValue(), notInBiMap.apply(entry.getValue()));
-      }
-      newMap.forwardMap.put(entry.getKey(), newTo);
-    }
-
-    // All new entries should be mapped in both directions.
-    for (Entry<V, NewV> entry : biMapCopy.entrySet()) {
-      newMap.forwardMap.put(notInForwardMap.apply(entry.getKey()), entry.getValue());
-      newMap
-          .inverseMap
-          .computeIfAbsent(entry.getValue(), ignore -> new HashSet<>())
-          .add(notInForwardMap.apply(entry.getKey()));
-    }
-
-    // Representatives are always in the inverse mapping, so they should always be remapped as new
-    // representatives.
-    for (Entry<V, K> entry : representativeMap.entrySet()) {
-      NewV newTo = biMap.get(entry.getKey());
-      if (newTo == null) {
-        newTo = notInBiMap.apply(entry.getKey());
-      }
-      newMap.representativeMap.put(newTo, entry.getValue());
-    }
-
-    return newMap;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
index f6b86a3..4642229 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/Policy.java
@@ -12,6 +12,8 @@
   /** Counter keeping track of how many classes this policy has removed. For debugging only. */
   public int numberOfRemovedClasses;
 
+  public void clear() {}
+
   public boolean shouldSkipPolicy() {
     return false;
   }
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 b2fdb3d..14b249a 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/SimplePolicyExecutor.java
@@ -70,6 +70,8 @@
         linkedGroups = applyMultiClassPolicy((MultiClassPolicy) policy, linkedGroups);
       }
 
+      policy.clear();
+
       // Any policy should not return any trivial groups.
       assert linkedGroups.stream().allMatch(group -> group.size() >= 2);
     }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/SyntheticArgumentClass.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/SyntheticArgumentClass.java
index b6e1f53..c3aa3c0 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/SyntheticArgumentClass.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/SyntheticArgumentClass.java
@@ -107,7 +107,9 @@
       boolean requiresMainDex = appView.appInfo().getMainDexClasses().containsAnyOf(mergeClasses);
 
       List<DexType> syntheticArgumentTypes = new ArrayList<>();
-      for (int i = 0; i < appView.options().horizontalClassMergingSyntheticArgumentCount; i++) {
+      for (int i = 0;
+          i < appView.options().horizontalClassMergerOptions().getSyntheticArgumentCount();
+          i++) {
         syntheticArgumentTypes.add(
             synthesizeClass(appView, appBuilder, context, requiresMainDex, i));
       }
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 da61076..0f85054 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/TreeFixer.java
@@ -45,8 +45,6 @@
   private final FieldAccessInfoCollectionModifier.Builder fieldAccessChangesBuilder;
   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();
@@ -128,10 +126,6 @@
     for (DexProgramClass root : subtypingForrest.getProgramRoots()) {
       subtypingForrest.traverseNodeDepthFirst(root, HashBiMap.create(), this::fixupProgramClass);
     }
-
-    lensBuilder.remapMethods(movedMethods);
-    lensBuilder.remapFields(movedFields);
-
     HorizontalClassMergerGraphLens lens = lensBuilder.build(appView, mergedClasses);
     fieldAccessChangesBuilder.build(this::fixupMethodReference).modify(appView);
     new AnnotationFixer(lens).run(appView.appInfo().classes());
@@ -164,6 +158,8 @@
     fixupFields(clazz.staticFields(), clazz::setStaticField);
     fixupFields(clazz.instanceFields(), clazz::setInstanceField);
 
+    lensBuilder.commitPendingUpdates();
+
     return remappedClassVirtualMethods;
   }
 
@@ -173,7 +169,7 @@
     // Don't process this method if it does not refer to a merge class type.
     boolean referencesMergeClass =
         Iterables.any(
-            originalMethodReference.proto.getBaseTypes(dexItemFactory),
+            originalMethodReference.getProto().getBaseTypes(dexItemFactory),
             mergedClasses::hasBeenMergedOrIsMergeTarget);
     if (!referencesMergeClass) {
       return method;
@@ -200,8 +196,7 @@
 
     DexMethod newMethodReference =
         newMethodSignature.withHolder(originalMethodReference, dexItemFactory);
-    movedMethods.put(originalMethodReference, newMethodReference);
-
+    lensBuilder.fixupMethod(originalMethodReference, newMethodReference);
     return method.toTypeSubstitutedMethod(newMethodReference);
   }
 
@@ -217,17 +212,17 @@
     iface.getMethodCollection().replaceVirtualMethods(this::fixupVirtualInterfaceMethod);
     fixupFields(iface.staticFields(), iface::setStaticField);
     fixupFields(iface.instanceFields(), iface::setInstanceField);
+    lensBuilder.commitPendingUpdates();
   }
 
   private DexEncodedMethod fixupProgramMethod(
       DexMethod newMethodReference, DexEncodedMethod method) {
     DexMethod originalMethodReference = method.getReference();
-
     if (newMethodReference == originalMethodReference) {
       return method;
     }
 
-    movedMethods.put(originalMethodReference, newMethodReference);
+    lensBuilder.fixupMethod(originalMethodReference, newMethodReference);
 
     DexEncodedMethod newMethod = method.toTypeSubstitutedMethod(newMethodReference);
     if (newMethod.isNonPrivateVirtualMethod()) {
@@ -365,26 +360,26 @@
     Set<DexField> existingFields = Sets.newIdentityHashSet();
 
     for (int i = 0; i < fields.size(); i++) {
-      DexEncodedField encodedField = fields.get(i);
-      DexField field = encodedField.field;
-      DexField newField = fixupFieldReference(field);
+      DexEncodedField oldField = fields.get(i);
+      DexField oldFieldReference = oldField.getReference();
+      DexField newFieldReference = fixupFieldReference(oldFieldReference);
 
       // Rename the field if it already exists.
-      if (!existingFields.add(newField)) {
-        DexField template = newField;
-        newField =
+      if (!existingFields.add(newFieldReference)) {
+        DexField template = newFieldReference;
+        newFieldReference =
             dexItemFactory.createFreshMember(
                 tryName ->
                     Optional.of(template.withName(tryName, dexItemFactory))
                         .filter(tryMethod -> !existingFields.contains(tryMethod)),
-                newField.name.toSourceString());
-        boolean added = existingFields.add(newField);
+                newFieldReference.name.toSourceString());
+        boolean added = existingFields.add(newFieldReference);
         assert added;
       }
 
-      if (newField != encodedField.field) {
-        movedFields.put(field, newField);
-        setter.setField(i, encodedField.toTypeSubstitutedField(newField));
+      if (newFieldReference != oldFieldReference) {
+        lensBuilder.fixupField(oldFieldReference, newFieldReference);
+        setter.setField(i, oldField.toTypeSubstitutedField(newFieldReference));
       }
     }
   }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodEntryPointSynthesizedCode.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodEntryPointSynthesizedCode.java
index d6690a9..7428b92 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodEntryPointSynthesizedCode.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodEntryPointSynthesizedCode.java
@@ -13,7 +13,6 @@
 
 public class VirtualMethodEntryPointSynthesizedCode extends SynthesizedCode {
   private final Int2ReferenceSortedMap<DexMethod> mappedMethods;
-  private final DexField classIdField;
 
   public VirtualMethodEntryPointSynthesizedCode(
       Int2ReferenceSortedMap<DexMethod> mappedMethods,
@@ -27,7 +26,6 @@
                 mappedMethods, classIdField, superMethod, method, position, originalMethod));
 
     this.mappedMethods = mappedMethods;
-    this.classIdField = classIdField;
   }
 
   @Override
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 fe3a898..9a44de7 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/VirtualMethodMerger.java
@@ -229,8 +229,7 @@
         classFileVersion = Ordered.maxIgnoreNull(classFileVersion, methodVersion);
       }
       DexMethod newMethod = moveMethod(classMethodsBuilder, method);
-      lensBuilder.mapMethod(newMethod, newMethod);
-      lensBuilder.mapMethodInverse(method.getReference(), newMethod);
+      lensBuilder.recordNewMethodSignature(method.getReference(), newMethod);
       classIdToMethodMap.put(classIdentifiers.getInt(method.getHolderType()), newMethod);
       if (representative == null) {
         representative = method;
@@ -274,16 +273,14 @@
 
     // Map each old non-abstract method to the newly synthesized method in the graph lens.
     for (ProgramMethod oldMethod : methods) {
-      if (oldMethod.getDefinition().isAbstract()) {
-        lensBuilder.mapMethod(oldMethod.getReference(), newMethodReference);
-      } else {
-        lensBuilder.moveMethod(oldMethod.getReference(), newMethodReference);
-      }
+      lensBuilder.mapMethod(oldMethod.getReference(), newMethodReference);
     }
-    lensBuilder.recordExtraOriginalSignature(bridgeMethodReference, newMethodReference);
+
+    // Add a mapping from a synthetic name to the synthetic merged method.
+    lensBuilder.recordNewMethodSignature(bridgeMethodReference, newMethodReference);
 
     classMethodsBuilder.addVirtualMethod(newMethod);
 
-    fieldAccessChangesBuilder.fieldReadByMethod(group.getClassIdField(), newMethod.method);
+    fieldAccessChangesBuilder.fieldReadByMethod(group.getClassIdField(), newMethodReference);
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/IgnoreSynthetics.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/IgnoreSynthetics.java
index 78875fd..e45e2d3 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/IgnoreSynthetics.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/IgnoreSynthetics.java
@@ -19,6 +19,10 @@
 
   @Override
   public boolean canMerge(DexProgramClass program) {
-    return !appView.getSyntheticItems().isSyntheticClass(program);
+    if (appView.getSyntheticItems().isSyntheticClass(program)) {
+      return appView.options().horizontalClassMergerOptions().isJavaLambdaMergingEnabled()
+          && appView.getSyntheticItems().isLegacySyntheticClass(program);
+    }
+    return true;
   }
 }
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitGroups.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitGroups.java
index f2800aa..33de8f3 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitGroups.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/LimitGroups.java
@@ -18,7 +18,7 @@
   private final int maxGroupSize;
 
   public LimitGroups(AppView<AppInfoWithLiveness> appView) {
-    maxGroupSize = appView.options().horizontalClassMergingMaxGroupSize;
+    maxGroupSize = appView.options().horizontalClassMergerOptions().getMaxGroupSize();
     assert maxGroupSize >= 2;
   }
 
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoClassInitializerWithObservableSideEffects.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoClassInitializerWithObservableSideEffects.java
new file mode 100644
index 0000000..f10d78c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoClassInitializerWithObservableSideEffects.java
@@ -0,0 +1,31 @@
+// 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;
+
+/**
+ * Prevent merging of classes with static initializers, as merging these causes side effects. It is
+ * okay for superclasses to have static initializers as all classes are expected to have the same
+ * super class.
+ */
+public class NoClassInitializerWithObservableSideEffects extends SingleClassPolicy {
+
+  @Override
+  public boolean canMerge(DexProgramClass program) {
+    if (!program.hasClassInitializer()) {
+      return true;
+    }
+    DexEncodedMethod clinit = program.getClassInitializer();
+    return clinit.getOptimizationInfo().classInitializerMayBePostponed() || isKotlinLambda(program);
+  }
+
+  private boolean isKotlinLambda(DexProgramClass program) {
+    return program.getKotlinInfo().isSyntheticClass()
+        && program.getKotlinInfo().asSyntheticClass().isLambda();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoEnums.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoEnums.java
index 2da0177..c8a0aab 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoEnums.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoEnums.java
@@ -22,8 +22,19 @@
   }
 
   @Override
+  public void clear() {
+    cache.clear();
+  }
+
+  @Override
   public boolean canMerge(DexProgramClass program) {
-    return !program.isEnum() && !isEnumSubtype(program);
+    if (program.isEnum()) {
+      return false;
+    }
+    if (isEnumSubtype(program)) {
+      return false;
+    }
+    return true;
   }
 
   private boolean isEnumSubtype(DexClass clazz) {
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
index a137e88..dee7a4a 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoKotlinLambdas.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoKotlinLambdas.java
@@ -18,7 +18,7 @@
 
   @Override
   public boolean shouldSkipPolicy() {
-    return appView.options().enableHorizontalClassMergingOfKotlinLambdas;
+    return appView.options().horizontalClassMergerOptions().isKotlinLambdaMergingEnabled();
   }
 
   @Override
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoStaticClassInitializer.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoStaticClassInitializer.java
deleted file mode 100644
index aeca6c0..0000000
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoStaticClassInitializer.java
+++ /dev/null
@@ -1,20 +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;
-
-/**
- * Prevent merging of classes with static initializers, as merging these causes side effects. It is
- * okay for superclasses to have static initializers as all classes are expected to have the same
- * super class.
- */
-public class NoStaticClassInitializer extends SingleClassPolicy {
-  @Override
-  public boolean canMerge(DexProgramClass program) {
-    return !program.hasClassInitializer();
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/FieldValueAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/FieldValueAnalysis.java
index 8e52af1..ee985c4 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/FieldValueAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/FieldValueAnalysis.java
@@ -8,6 +8,7 @@
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexValue;
+import com.android.tools.r8.graph.FieldResolutionResult.SuccessfulFieldResolutionResult;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.code.BasicBlock;
 import com.android.tools.r8.ir.code.DominatorTree;
@@ -86,10 +87,16 @@
     return null;
   }
 
+  boolean isStaticFieldValueAnalysis() {
+    return false;
+  }
+
   StaticFieldValueAnalysis asStaticFieldValueAnalysis() {
     return null;
   }
 
+  abstract boolean isSubjectToOptimizationIgnoringPinning(DexEncodedField field);
+
   abstract boolean isSubjectToOptimization(DexEncodedField field);
 
   void recordFieldPut(DexEncodedField field, Instruction instruction) {
@@ -118,9 +125,18 @@
         if (instruction.isFieldPut()) {
           FieldInstruction fieldPut = instruction.asFieldInstruction();
           DexField field = fieldPut.getField();
-          DexEncodedField encodedField = appInfo.resolveField(field).getResolvedField();
-          if (encodedField != null && isSubjectToOptimization(encodedField)) {
-            recordFieldPut(encodedField, fieldPut);
+          SuccessfulFieldResolutionResult fieldResolutionResult =
+              appInfo.resolveField(field).asSuccessfulResolution();
+          if (fieldResolutionResult != null) {
+            DexEncodedField encodedField = fieldResolutionResult.getResolvedField();
+            assert encodedField != null;
+            if (isSubjectToOptimization(encodedField)) {
+              recordFieldPut(encodedField, fieldPut);
+            } else if (isStaticFieldValueAnalysis()
+                && fieldResolutionResult.getResolvedHolder().isEnum()
+                && isSubjectToOptimizationIgnoringPinning(encodedField)) {
+              recordFieldPut(encodedField, fieldPut);
+            }
           }
         } else if (isInstanceFieldValueAnalysis()
             && instruction.isInvokeConstructor(appView.dexItemFactory())) {
@@ -153,15 +169,15 @@
       boolean priorReadsWillReadSameValue =
           !classInitializerDefaultsResult.hasStaticValue(field) && fieldPut.value().isZero();
       if (!priorReadsWillReadSameValue && fieldMaybeReadBeforeInstruction(field, fieldPut)) {
-        if (!isInstanceFieldValueAnalysis()) {
+        // TODO(b/172528424): Generalize to InstanceFieldValueAnalysis.
+        if (isStaticFieldValueAnalysis()) {
           // At this point the value read in the field can be only the default static value, if read
           // prior to the put, or the value put, if read after the put. We still want to record it
           // because the default static value is typically null/0, so code present after a null/0
           // check can take advantage of the optimization.
           DexValue valueBeforePut = classInitializerDefaultsResult.getStaticValue(field);
           asStaticFieldValueAnalysis()
-              .updateFieldOptimizationInfoWith2Values(
-                  field, fieldPut, fieldPut.value(), valueBeforePut);
+              .updateFieldOptimizationInfoWith2Values(field, fieldPut.value(), valueBeforePut);
         }
         continue;
       }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/InstanceFieldValueAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/InstanceFieldValueAnalysis.java
index cc607ac..c4061ec 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/InstanceFieldValueAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/InstanceFieldValueAnalysis.java
@@ -124,6 +124,11 @@
   }
 
   @Override
+  boolean isSubjectToOptimizationIgnoringPinning(DexEncodedField field) {
+    throw new Unreachable("Used by static analysis only.");
+  }
+
+  @Override
   void updateFieldOptimizationInfo(DexEncodedField field, FieldInstruction fieldPut, Value value) {
     if (fieldNeverWrittenBetweenInstancePutAndMethodExit(field, fieldPut.asInstancePut())) {
       recordInstanceFieldIsInitializedWithValue(field, value);
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java
index 3ab1305..03455fb 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValueAnalysis.java
@@ -14,6 +14,7 @@
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexValue;
 import com.android.tools.r8.graph.DexValue.DexValueNull;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
@@ -42,12 +43,15 @@
 
 public class StaticFieldValueAnalysis extends FieldValueAnalysis {
 
+  private final StaticFieldValues.Builder builder;
+
   private StaticFieldValueAnalysis(
       AppView<AppInfoWithLiveness> appView, IRCode code, OptimizationFeedback feedback) {
     super(appView, code, feedback);
+    builder = StaticFieldValues.builder(code.context().getHolder());
   }
 
-  public static void run(
+  public static StaticFieldValues run(
       AppView<?> appView,
       IRCode code,
       ClassInitializerDefaultsResult classInitializerDefaultsResult,
@@ -57,9 +61,16 @@
     assert appView.enableWholeProgramOptimizations();
     assert code.context().getDefinition().isClassInitializer();
     timing.begin("Analyze class initializer");
-    new StaticFieldValueAnalysis(appView.withLiveness(), code, feedback)
-        .computeFieldOptimizationInfo(classInitializerDefaultsResult);
+    StaticFieldValues result =
+        new StaticFieldValueAnalysis(appView.withLiveness(), code, feedback)
+            .analyze(classInitializerDefaultsResult, code.context().getHolderType());
     timing.end();
+    return result;
+  }
+
+  @Override
+  boolean isStaticFieldValueAnalysis() {
+    return true;
   }
 
   @Override
@@ -67,6 +78,12 @@
     return this;
   }
 
+  StaticFieldValues analyze(
+      ClassInitializerDefaultsResult classInitializerDefaultsResult, DexType holderType) {
+    computeFieldOptimizationInfo(classInitializerDefaultsResult);
+    return builder.build();
+  }
+
   @Override
   void computeFieldOptimizationInfo(ClassInitializerDefaultsResult classInitializerDefaultsResult) {
     super.computeFieldOptimizationInfo(classInitializerDefaultsResult);
@@ -105,14 +122,32 @@
   }
 
   @Override
-  void updateFieldOptimizationInfo(DexEncodedField field, FieldInstruction fieldPut, Value value) {
-    // Abstract value.
-    feedback.recordFieldHasAbstractValue(field, appView, getOrComputeAbstractValue(value, field));
-
-    setDynamicType(field, value, false);
+  boolean isSubjectToOptimizationIgnoringPinning(DexEncodedField field) {
+    return field.isStatic()
+        && field.getHolderType() == context.getHolderType()
+        && appView
+            .appInfo()
+            .isFieldOnlyWrittenInMethodIgnoringPinning(field, context.getDefinition());
   }
 
-  private void setDynamicType(DexEncodedField field, Value value, boolean maybeNull) {
+  @Override
+  void updateFieldOptimizationInfo(DexEncodedField field, FieldInstruction fieldPut, Value value) {
+    AbstractValue abstractValue = getOrComputeAbstractValue(value, field);
+    updateFieldOptimizationInfo(field, value, abstractValue, false);
+  }
+
+  void updateFieldOptimizationInfo(
+      DexEncodedField field, Value value, AbstractValue abstractValue, boolean maybeNull) {
+    builder.recordStaticField(field, abstractValue, appView.dexItemFactory());
+
+    // We cannot modify FieldOptimizationInfo of pinned fields.
+    if (appView.appInfo().isPinned(field)) {
+      return;
+    }
+
+    // Abstract value.
+    feedback.recordFieldHasAbstractValue(field, appView, abstractValue);
+
     // Dynamic upper bound type.
     TypeElement fieldType =
         TypeElement.fromDexType(field.field.type, Nullability.maybeNull(), appView);
@@ -137,16 +172,16 @@
   }
 
   public void updateFieldOptimizationInfoWith2Values(
-      DexEncodedField field, FieldInstruction fieldPut, Value valuePut, DexValue valueBeforePut) {
+      DexEncodedField field, Value valuePut, DexValue valueBeforePut) {
     // We are interested in the AbstractValue only if it's null or a value, so we can use the value
     // if the code is protected by a null check.
     if (valueBeforePut != DexValueNull.NULL) {
       return;
     }
-    feedback.recordFieldHasAbstractValue(
-        field, appView, NullOrAbstractValue.create(getOrComputeAbstractValue(valuePut, field)));
 
-    setDynamicType(field, valuePut, true);
+    AbstractValue abstractValue =
+        NullOrAbstractValue.create(getOrComputeAbstractValue(valuePut, field));
+    updateFieldOptimizationInfo(field, valuePut, abstractValue, true);
   }
 
   private AbstractValue getOrComputeAbstractValue(Value value, DexEncodedField field) {
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValues.java b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValues.java
new file mode 100644
index 0000000..b2dcdcd
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/fieldvalueanalysis/StaticFieldValues.java
@@ -0,0 +1,143 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.analysis.fieldvalueanalysis;
+
+import com.android.tools.r8.graph.DexEncodedField;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.ObjectState;
+import com.google.common.collect.ImmutableMap;
+
+public abstract class StaticFieldValues {
+
+  public boolean isEnumStaticFieldValues() {
+    return false;
+  }
+
+  public EnumStaticFieldValues asEnumStaticFieldValues() {
+    return null;
+  }
+
+  public static Builder builder(DexProgramClass clazz) {
+    return clazz.isEnum() ? EnumStaticFieldValues.builder() : EmptyStaticValues.builder();
+  }
+
+  public abstract static class Builder {
+
+    public abstract void recordStaticField(
+        DexEncodedField staticField, AbstractValue value, DexItemFactory factory);
+
+    public abstract StaticFieldValues build();
+  }
+
+  // All the abstract values stored here may match a pinned field, using them requires therefore
+  // to check the field is not pinned or prove it is no longer pinned.
+  public static class EnumStaticFieldValues extends StaticFieldValues {
+    private final ImmutableMap<DexField, AbstractValue> enumAbstractValues;
+    private final DexField valuesField;
+    private final AbstractValue valuesAbstractValue;
+
+    public EnumStaticFieldValues(
+        ImmutableMap<DexField, AbstractValue> enumAbstractValues,
+        DexField valuesField,
+        AbstractValue valuesAbstractValue) {
+      this.enumAbstractValues = enumAbstractValues;
+      this.valuesField = valuesField;
+      this.valuesAbstractValue = valuesAbstractValue;
+    }
+
+    static StaticFieldValues.Builder builder() {
+      return new Builder();
+    }
+
+    public static class Builder extends StaticFieldValues.Builder {
+      private final ImmutableMap.Builder<DexField, AbstractValue> enumAbstractValuesBuilder =
+          ImmutableMap.builder();
+      private DexField valuesFields;
+      private AbstractValue valuesAbstractValue;
+
+      Builder() {}
+
+      @Override
+      public void recordStaticField(
+          DexEncodedField staticField, AbstractValue value, DexItemFactory factory) {
+        // TODO(b/166532388): Stop relying on the values name.
+        if (staticField.getName() == factory.enumValuesFieldName) {
+          valuesFields = staticField.field;
+          valuesAbstractValue = value;
+        } else if (staticField.isEnum()) {
+          enumAbstractValuesBuilder.put(staticField.field, value);
+        }
+      }
+
+      @Override
+      public StaticFieldValues build() {
+        ImmutableMap<DexField, AbstractValue> enumAbstractValues =
+            enumAbstractValuesBuilder.build();
+        if (valuesAbstractValue == null && enumAbstractValues.isEmpty()) {
+          return EmptyStaticValues.getInstance();
+        }
+        return new EnumStaticFieldValues(enumAbstractValues, valuesFields, valuesAbstractValue);
+      }
+    }
+
+    @Override
+    public boolean isEnumStaticFieldValues() {
+      return true;
+    }
+
+    @Override
+    public EnumStaticFieldValues asEnumStaticFieldValues() {
+      return this;
+    }
+
+    public ObjectState getObjectStateForPossiblyPinnedField(DexField field) {
+      AbstractValue fieldValue = enumAbstractValues.get(field);
+      if (fieldValue == null || fieldValue.isZero()) {
+        return null;
+      }
+      if (fieldValue.isSingleFieldValue()) {
+        return fieldValue.asSingleFieldValue().getState();
+      }
+      assert fieldValue.isUnknown();
+      return ObjectState.empty();
+    }
+
+    public AbstractValue getValuesAbstractValueForPossiblyPinnedField(DexField field) {
+      assert valuesField == field || valuesAbstractValue == null;
+      return valuesAbstractValue;
+    }
+  }
+
+  public static class EmptyStaticValues extends StaticFieldValues {
+    private static EmptyStaticValues INSTANCE = new EmptyStaticValues();
+
+    private EmptyStaticValues() {}
+
+    public static EmptyStaticValues getInstance() {
+      return INSTANCE;
+    }
+
+    static StaticFieldValues.Builder builder() {
+      return new Builder();
+    }
+
+    public static class Builder extends StaticFieldValues.Builder {
+
+      @Override
+      public void recordStaticField(
+          DexEncodedField staticField, AbstractValue value, DexItemFactory factory) {
+        // Do nothing.
+      }
+
+      @Override
+      public StaticFieldValues build() {
+        return EmptyStaticValues.getInstance();
+      }
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/EnumValuesObjectState.java b/src/main/java/com/android/tools/r8/ir/analysis/value/EnumValuesObjectState.java
index f1ec89c..2d8ddc2 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/EnumValuesObjectState.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/EnumValuesObjectState.java
@@ -33,6 +33,10 @@
     return state[ordinal];
   }
 
+  public int getEnumValuesSize() {
+    return state.length;
+  }
+
   @Override
   public boolean isEnumValuesObjectState() {
     return true;
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleFieldValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleFieldValue.java
index 8de1c91..911174b 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleFieldValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleFieldValue.java
@@ -12,7 +12,6 @@
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.EnumValueInfoMapCollection.EnumValueInfoMap;
 import com.android.tools.r8.graph.FieldResolutionResult.SuccessfulFieldResolutionResult;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.ProgramMethod;
@@ -23,6 +22,7 @@
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.TypeAndLocalInfoSupplier;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.optimize.enums.EnumDataMap;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 
 public abstract class SingleFieldValue extends SingleValue {
@@ -107,12 +107,9 @@
   public SingleValue rewrittenWithLens(AppView<AppInfoWithLiveness> appView, GraphLens lens) {
     AbstractValueFactory factory = appView.abstractValueFactory();
     if (field.holder == field.type) {
-      EnumValueInfoMap unboxedEnumInfo = appView.unboxedEnums().getEnumValueInfoMap(field.type);
-      if (unboxedEnumInfo != null) {
-        // Return the ordinal of the unboxed enum.
-        assert unboxedEnumInfo.hasEnumValueInfo(field);
-        return factory.createSingleNumberValue(
-            unboxedEnumInfo.getEnumValueInfo(field).convertToInt());
+      EnumDataMap enumDataMap = appView.unboxedEnums();
+      if (enumDataMap.hasUnboxedValueFor(field)) {
+        return factory.createSingleNumberValue(enumDataMap.getUnboxedValue(field));
       }
     }
     return factory.createSingleFieldValue(
diff --git a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
index a3f8421..54354f6 100644
--- a/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/BasicBlockInstructionListIterator.java
@@ -275,6 +275,18 @@
   }
 
   @Override
+  public void replaceCurrentInstructionWithConstString(
+      AppView<?> appView, IRCode code, DexString value) {
+    if (current == null) {
+      throw new IllegalStateException();
+    }
+
+    // Replace the instruction by const-string.
+    ConstString constString = code.createStringConstant(appView, value, current.getLocalInfo());
+    replaceCurrentInstruction(constString);
+  }
+
+  @Override
   public void replaceCurrentInstructionWithStaticGet(
       AppView<?> appView, IRCode code, DexField field, Set<Value> affectedValues) {
     if (current == null) {
@@ -451,6 +463,16 @@
     return newBlock;
   }
 
+  @Override
+  public BasicBlock splitCopyCatchHandlers(
+      IRCode code, ListIterator<BasicBlock> blockIterator, InternalOptions options) {
+    BasicBlock splitBlock = split(code, blockIterator, false);
+    assert !block.hasCatchHandlers();
+    assert splitBlock.hasCatchHandlers();
+    block.copyCatchHandlers(code, blockIterator, splitBlock, options);
+    return splitBlock;
+  }
+
   private boolean canThrow(IRCode code) {
     InstructionIterator iterator = code.instructionIterator();
     while (iterator.hasNext()) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
index cb33b18..d2b9ffb 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRCodeInstructionListIterator.java
@@ -53,6 +53,12 @@
   }
 
   @Override
+  public void replaceCurrentInstructionWithConstString(
+      AppView<?> appView, IRCode code, DexString value) {
+    instructionIterator.replaceCurrentInstructionWithConstString(appView, code, value);
+  }
+
+  @Override
   public void replaceCurrentInstructionWithStaticGet(
       AppView<?> appView, IRCode code, DexField field, Set<Value> affectedValues) {
     instructionIterator.replaceCurrentInstructionWithStaticGet(
@@ -86,6 +92,12 @@
   }
 
   @Override
+  public BasicBlock splitCopyCatchHandlers(
+      IRCode code, ListIterator<BasicBlock> blockIterator, InternalOptions options) {
+    throw new Unimplemented();
+  }
+
+  @Override
   public BasicBlock inlineInvoke(
       AppView<?> appView,
       IRCode code,
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
index 792018b..1acdb98 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionListIterator.java
@@ -96,6 +96,14 @@
 
   void replaceCurrentInstructionWithConstInt(IRCode code, int value);
 
+  void replaceCurrentInstructionWithConstString(AppView<?> appView, IRCode code, DexString value);
+
+  default void replaceCurrentInstructionWithConstString(
+      AppView<?> appView, IRCode code, String value) {
+    replaceCurrentInstructionWithConstString(
+        appView, code, appView.dexItemFactory().createString(value));
+  }
+
   void replaceCurrentInstructionWithStaticGet(
       AppView<?> appView, IRCode code, DexField field, Set<Value> affectedValues);
 
@@ -146,6 +154,9 @@
     return split(code, null);
   }
 
+  BasicBlock splitCopyCatchHandlers(
+      IRCode code, ListIterator<BasicBlock> blockIterator, InternalOptions options);
+
   /**
    * Split the block into three blocks. The first split is at the point of the {@link ListIterator}
    * cursor and the second split is <code>instructions</code> after the cursor. The existing
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java b/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
index 6daa9a4..9d7d69b 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeMethod.java
@@ -30,7 +30,9 @@
 import com.android.tools.r8.ir.regalloc.RegisterAllocator;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
+import com.google.common.collect.ImmutableList;
 import java.util.BitSet;
+import java.util.Collections;
 import java.util.List;
 
 public abstract class InvokeMethod extends Invoke {
@@ -238,4 +240,30 @@
     }
     return false;
   }
+
+  abstract static class Builder<B extends Builder<B, I>, I extends InvokeMethod>
+      extends BuilderBase<B, I> {
+
+    protected DexMethod method;
+    protected List<Value> arguments = Collections.emptyList();
+
+    public B setArguments(List<Value> arguments) {
+      assert arguments != null;
+      this.arguments = arguments;
+      return self();
+    }
+
+    public B setSingleArgument(Value argument) {
+      return setArguments(ImmutableList.of(argument));
+    }
+
+    public B setMethod(DexMethod method) {
+      this.method = method;
+      return self();
+    }
+
+    public B setMethod(DexClassAndMethod method) {
+      return setMethod(method.getReference());
+    }
+  }
 }
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 3139d20..1b3aef3 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
@@ -27,8 +27,6 @@
 import com.android.tools.r8.ir.optimize.InliningConstraints;
 import com.android.tools.r8.ir.optimize.inliner.WhyAreYouNotInliningReporter;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.google.common.collect.ImmutableList;
-import java.util.Collections;
 import java.util.List;
 
 public class InvokeStatic extends InvokeMethod {
@@ -235,29 +233,7 @@
         .classInitializationMayHaveSideEffectsInContext(appView, context);
   }
 
-  public static class Builder extends BuilderBase<Builder, InvokeStatic> {
-
-    private DexMethod method;
-    private List<Value> arguments = Collections.emptyList();
-
-    public Builder setArguments(List<Value> arguments) {
-      assert arguments != null;
-      this.arguments = arguments;
-      return this;
-    }
-
-    public Builder setSingleArgument(Value argument) {
-      return setArguments(ImmutableList.of(argument));
-    }
-
-    public Builder setMethod(DexMethod method) {
-      this.method = method;
-      return this;
-    }
-
-    public Builder setMethod(DexClassAndMethod method) {
-      return setMethod(method.getReference());
-    }
+  public static class Builder extends InvokeMethod.Builder<Builder, InvokeStatic> {
 
     @Override
     public InvokeStatic build() {
diff --git a/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java b/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
index d4f737e..e063b8f 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InvokeVirtual.java
@@ -33,6 +33,10 @@
     super(target, result, arguments);
   }
 
+  public static Builder builder() {
+    return new Builder();
+  }
+
   @Override
   public boolean getInterfaceBit() {
     return false;
@@ -167,4 +171,17 @@
     return ClassInitializationAnalysis.InstructionUtils.forInvokeVirtual(
         this, clazz, context, appView, mode, assumption);
   }
+
+  public static class Builder extends InvokeMethod.Builder<Builder, InvokeVirtual> {
+
+    @Override
+    public InvokeVirtual build() {
+      return amend(new InvokeVirtual(method, outValue, arguments));
+    }
+
+    @Override
+    public Builder self() {
+      return this;
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java b/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
index 889089f..e6009dd 100644
--- a/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
+++ b/src/main/java/com/android/tools/r8/ir/code/LinearFlowInstructionListIterator.java
@@ -73,6 +73,12 @@
   }
 
   @Override
+  public void replaceCurrentInstructionWithConstString(
+      AppView<?> appView, IRCode code, DexString value) {
+    currentBlockIterator.replaceCurrentInstructionWithConstString(appView, code, value);
+  }
+
+  @Override
   public void replaceCurrentInstructionWithStaticGet(
       AppView<?> appView, IRCode code, DexField field, Set<Value> affectedValues) {
     currentBlockIterator.replaceCurrentInstructionWithStaticGet(
@@ -102,6 +108,12 @@
   }
 
   @Override
+  public BasicBlock splitCopyCatchHandlers(
+      IRCode code, ListIterator<BasicBlock> blockIterator, InternalOptions options) {
+    return currentBlockIterator.splitCopyCatchHandlers(code, blockIterator, options);
+  }
+
+  @Override
   public BasicBlock inlineInvoke(
       AppView<?> appView,
       IRCode code,
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 ab28d55..0d054ef 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
@@ -7,11 +7,13 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.utils.structural.StructuralItem;
+import com.android.tools.r8.utils.structural.StructuralMapping;
+import com.android.tools.r8.utils.structural.StructuralSpecification;
 import com.google.common.annotations.VisibleForTesting;
-import java.util.Comparator;
 import java.util.Objects;
 
-public class Position implements Comparable<Position> {
+public class Position implements StructuralItem<Position> {
 
   // A no-position marker. Not having a position means the position is implicitly defined by the
   // context, e.g., the marker does not materialize anything concrete.
@@ -36,6 +38,14 @@
   public final DexMethod method;
   public final Position callerPosition;
 
+  private static void specify(StructuralSpecification<Position, ?> spec) {
+    spec.withInt(p -> p.line)
+        .withNullableItem(p -> p.file)
+        .withBool(p -> p.synthetic)
+        .withNullableItem(p -> p.method)
+        .withNullableItem(p -> p.callerPosition);
+  }
+
   public Position(int line, DexString file, DexMethod method, Position callerPosition) {
     this(line, file, method, callerPosition, false);
     assert line >= 0;
@@ -91,6 +101,16 @@
     return position;
   }
 
+  @Override
+  public Position self() {
+    return this;
+  }
+
+  @Override
+  public StructuralMapping<Position> getStructuralMapping() {
+    return Position::specify;
+  }
+
   public boolean isNone() {
     return line == -1;
   }
@@ -128,19 +148,6 @@
     return result;
   }
 
-  @Override
-  public int compareTo(Position o) {
-    if (this == o) {
-      return 0;
-    }
-    return Comparator.comparingInt((Position p) -> p.line)
-        .thenComparing(p -> p.file, Comparator.nullsFirst(DexString::compareTo))
-        .thenComparing(p -> p.synthetic)
-        .thenComparing(p -> p.method)
-        .thenComparing(p -> p.callerPosition, Comparator.nullsFirst(Position::compareTo))
-        .compare(this, o);
-  }
-
   private String toString(boolean forceMethod) {
     if (isNone()) {
       return "--";
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 2ef58cd..7861771 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
@@ -22,7 +22,6 @@
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DexTypeList;
-import com.android.tools.r8.graph.EnumValueInfoMapCollection;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.classmerging.HorizontallyMergedLambdaClasses;
@@ -33,6 +32,7 @@
 import com.android.tools.r8.ir.analysis.fieldaccess.TrivialFieldAccessReprocessor;
 import com.android.tools.r8.ir.analysis.fieldvalueanalysis.InstanceFieldValueAnalysis;
 import com.android.tools.r8.ir.analysis.fieldvalueanalysis.StaticFieldValueAnalysis;
+import com.android.tools.r8.ir.analysis.fieldvalueanalysis.StaticFieldValues;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.code.AlwaysMaterializingDefinition;
 import com.android.tools.r8.ir.code.AlwaysMaterializingUser;
@@ -73,6 +73,7 @@
 import com.android.tools.r8.ir.optimize.ReflectionOptimizer;
 import com.android.tools.r8.ir.optimize.ServiceLoaderRewriter;
 import com.android.tools.r8.ir.optimize.classinliner.ClassInliner;
+import com.android.tools.r8.ir.optimize.enums.EnumDataMap;
 import com.android.tools.r8.ir.optimize.enums.EnumUnboxer;
 import com.android.tools.r8.ir.optimize.enums.EnumValueOptimizer;
 import com.android.tools.r8.ir.optimize.info.MethodOptimizationInfoCollector;
@@ -487,13 +488,13 @@
       // Classes which has already been through library desugaring will not go through IR
       // processing again.
       LibraryDesugaredChecker libraryDesugaredChecker = new LibraryDesugaredChecker(appView);
-      Set<DexProgramClass> alreadyLibraryDesugared = Sets.newConcurrentHashSet();
+      Set<DexType> alreadyLibraryDesugared = Sets.newConcurrentHashSet();
       ThreadUtils.processItems(
           application.classes(),
           clazz -> {
             if (libraryDesugaredChecker.isClassLibraryDesugared(clazz)) {
               if (appView.options().desugarSpecificOptions().allowAllDesugaredInput) {
-                alreadyLibraryDesugared.add(clazz);
+                alreadyLibraryDesugared.add(clazz.getType());
               } else {
                 throw new CompilationError(
                     "Code for "
@@ -752,7 +753,7 @@
     if (enumUnboxer != null) {
       enumUnboxer.unboxEnums(postMethodProcessorBuilder, executorService, feedback);
     } else {
-      appView.setUnboxedEnums(EnumValueInfoMapCollection.empty());
+      appView.setUnboxedEnums(EnumDataMap.empty());
     }
     if (!options.debug) {
       new TrivialFieldAccessReprocessor(appView.withLiveness(), postMethodProcessorBuilder)
@@ -1708,16 +1709,21 @@
     }
 
     InstanceFieldInitializationInfoCollection instanceFieldInitializationInfos = null;
+    StaticFieldValues staticFieldValues = null;
     if (method.getDefinition().isInitializer()) {
       if (method.getDefinition().isClassInitializer()) {
-        StaticFieldValueAnalysis.run(
-            appView, code, classInitializerDefaultsResult, feedback, timing);
+        staticFieldValues =
+            StaticFieldValueAnalysis.run(
+                appView, code, classInitializerDefaultsResult, feedback, timing);
       } else {
         instanceFieldInitializationInfos =
             InstanceFieldValueAnalysis.run(
                 appView, code, classInitializerDefaultsResult, feedback, timing);
       }
     }
+    if (enumUnboxer != null) {
+      enumUnboxer.recordEnumState(method.getHolder(), staticFieldValues);
+    }
     methodOptimizationInfoCollector.collectMethodOptimizationInfo(
         method, code, feedback, dynamicTypeOptimization, instanceFieldInitializationInfos, timing);
   }
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 84fd203..1e3b81f 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
@@ -26,6 +26,7 @@
 import com.android.tools.r8.ir.synthetic.SynthesizedCode;
 import com.android.tools.r8.position.MethodPosition;
 import com.android.tools.r8.utils.MethodSignatureEquivalence;
+import com.android.tools.r8.utils.WorkList;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.ImmutableList;
@@ -421,8 +422,39 @@
     if (clazz.isNotProgramClass()) {
       return;
     }
+    Set<DexType> filtered = new HashSet<>(emulatedInterfaces);
+    WorkList<DexType> workList = WorkList.newIdentityWorkList();
+    for (DexType emulatedInterface : emulatedInterfaces) {
+      DexClass iface = appView.definitionFor(emulatedInterface);
+      if (iface != null) {
+        assert iface.isLibraryClass()
+            || appView.options().desugaredLibraryConfiguration.isLibraryCompilation();
+        workList.addIfNotSeen(iface.getInterfaces());
+      }
+    }
+    while (workList.hasNext()) {
+      DexType type = workList.next();
+      filtered.remove(type);
+      DexClass iface = appView.definitionFor(type);
+      if (iface == null) {
+        continue;
+      }
+      workList.addIfNotSeen(iface.getInterfaces());
+    }
+
+    for (DexType emulatedInterface : emulatedInterfaces) {
+      DexClass s = appView.definitionFor(emulatedInterface);
+      if (s != null) {
+        s = appView.definitionFor(s.superType);
+      }
+      while (s != null && s.getType() != appView.dexItemFactory().objectType) {
+        filtered.remove(s.getType());
+        s = appView.definitionFor(s.getSuperType());
+      }
+    }
+
     // We need to introduce them in deterministic order for deterministic compilation.
-    ArrayList<DexType> sortedEmulatedInterfaces = new ArrayList<>(emulatedInterfaces);
+    ArrayList<DexType> sortedEmulatedInterfaces = new ArrayList<>(filtered);
     Collections.sort(sortedEmulatedInterfaces);
     List<GenericSignature.ClassTypeSignature> extraInterfaceSignatures = new ArrayList<>();
     for (DexType extraInterface : sortedEmulatedInterfaces) {
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryWrapperSynthesizer.java b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryWrapperSynthesizer.java
index 361ab4b..0159139 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryWrapperSynthesizer.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/DesugaredLibraryWrapperSynthesizer.java
@@ -441,7 +441,7 @@
         factory.createMethod(
             field.holder,
             factory.createProto(factory.voidType, field.type),
-            factory.initMethodName);
+            factory.constructorMethodName);
     return newSynthesizedMethod(
         method,
         Constants.ACC_PRIVATE | Constants.ACC_SYNTHETIC,
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 abe232f..d73a7c1 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
@@ -49,8 +49,10 @@
 import com.android.tools.r8.origin.SynthesizedOrigin;
 import com.android.tools.r8.utils.Pair;
 import com.android.tools.r8.utils.collections.BidirectionalManyToManyRepresentativeMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
 import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
-import com.google.common.collect.BiMap;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
 import com.google.common.collect.ImmutableList;
 import java.util.ArrayDeque;
 import java.util.ArrayList;
@@ -521,17 +523,15 @@
     public InterfaceProcessorNestedGraphLens(
         Map<DexType, DexType> typeMap,
         Map<DexMethod, DexMethod> methodMap,
-        Map<DexField, DexField> fieldMap,
-        BiMap<DexField, DexField> originalFieldSignatures,
-        BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures,
-        BidirectionalOneToOneHashMap<DexMethod, DexMethod> extraOriginalMethodSignatures,
+        BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap,
+        BidirectionalOneToOneMap<DexMethod, DexMethod> originalMethodSignatures,
+        BidirectionalOneToOneMap<DexMethod, DexMethod> extraOriginalMethodSignatures,
         GraphLens previousLens,
         DexItemFactory dexItemFactory) {
       super(
           typeMap,
           methodMap,
           fieldMap,
-          originalFieldSignatures,
           originalMethodSignatures,
           previousLens,
           dexItemFactory);
@@ -601,7 +601,7 @@
 
     public static class Builder extends NestedGraphLens.Builder {
 
-      private final BidirectionalOneToOneHashMap<DexMethod, DexMethod>
+      private final MutableBidirectionalOneToOneMap<DexMethod, DexMethod>
           extraOriginalMethodSignatures = new BidirectionalOneToOneHashMap<>();
 
       public void recordCodeMovedToCompanionClass(DexMethod from, DexMethod to) {
@@ -613,7 +613,7 @@
       @Override
       public InterfaceProcessorNestedGraphLens build(
           DexItemFactory dexItemFactory, GraphLens previousLens) {
-        if (originalFieldSignatures.isEmpty()
+        if (fieldMap.isEmpty()
             && originalMethodSignatures.isEmpty()
             && extraOriginalMethodSignatures.isEmpty()) {
           return null;
@@ -622,7 +622,6 @@
             typeMap,
             methodMap,
             fieldMap,
-            originalFieldSignatures,
             originalMethodSignatures,
             extraOriginalMethodSignatures,
             previousLens,
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
index c9e47c2..a50581e 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/LambdaClass.java
@@ -33,6 +33,8 @@
 import com.android.tools.r8.graph.ResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.ir.code.Invoke;
 import com.android.tools.r8.ir.code.Invoke.Type;
+import com.android.tools.r8.ir.optimize.info.OptimizationFeedback;
+import com.android.tools.r8.ir.optimize.info.OptimizationFeedbackSimple;
 import com.android.tools.r8.ir.synthetic.ForwardMethodSourceCode;
 import com.android.tools.r8.ir.synthetic.SynthesizedCode;
 import com.android.tools.r8.origin.SynthesizedOrigin;
@@ -65,6 +67,8 @@
  */
 public final class LambdaClass {
 
+  private static final OptimizationFeedback feedback = OptimizationFeedbackSimple.getInstance();
+
   final AppView<?> appView;
   final LambdaRewriter rewriter;
   public final DexType type;
@@ -293,6 +297,7 @@
               ParameterAnnotationsList.empty(),
               LambdaClassConstructorSourceCode.build(this),
               true);
+      feedback.classInitializerMayBePostponed(methods[1]);
     }
     return methods;
   }
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 e4d6f65..f464fdd 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
@@ -18,7 +18,6 @@
 import com.android.tools.r8.graph.CfCode;
 import com.android.tools.r8.graph.DexApplication.Builder;
 import com.android.tools.r8.graph.DexCallSite;
-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.DexMethod;
@@ -42,10 +41,10 @@
 import com.android.tools.r8.ir.code.ValueType;
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.utils.DescriptorUtils;
-import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneMap;
 import com.android.tools.r8.utils.collections.SortedProgramMethodSet;
 import com.google.common.base.Suppliers;
-import com.google.common.collect.BiMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import java.util.ArrayList;
@@ -73,7 +72,7 @@
   public static final String LAMBDA_CLASS_NAME_PREFIX = "-$$Lambda$";
   public static final String LAMBDA_GROUP_CLASS_NAME_PREFIX = "-$$LambdaGroup$";
   static final String EXPECTED_LAMBDA_METHOD_PREFIX = "lambda$";
-  private static final String LAMBDA_INSTANCE_FIELD_NAME = "INSTANCE";
+  public static final String LAMBDA_INSTANCE_FIELD_NAME = "INSTANCE";
 
   private final AppView<?> appView;
 
@@ -133,7 +132,7 @@
    */
   public int desugarLambdas(ProgramMethod method, AppInfoWithClassHierarchy appInfo) {
     return desugarLambdas(
-        method.getDefinition(),
+        method,
         callsite -> {
           LambdaDescriptor descriptor = LambdaDescriptor.tryInfer(callsite, appInfo, method);
           if (descriptor == null) {
@@ -145,8 +144,8 @@
 
   // Same as above, but where lambdas are always known to exist for the call sites.
   public static int desugarLambdas(
-      DexEncodedMethod method, Function<DexCallSite, LambdaClass> callSites) {
-    CfCode code = method.getCode().asCfCode();
+      ProgramMethod method, Function<DexCallSite, LambdaClass> callSites) {
+    CfCode code = method.getDefinition().getCode().asCfCode();
     List<CfInstruction> instructions = code.getInstructions();
     Supplier<List<CfInstruction>> lazyNewInstructions =
         Suppliers.memoize(() -> new ArrayList<>(instructions));
@@ -439,16 +438,14 @@
     LambdaRewriterLens(
         Map<DexType, DexType> typeMap,
         Map<DexMethod, DexMethod> methodMap,
-        Map<DexField, DexField> fieldMap,
-        BiMap<DexField, DexField> originalFieldSignatures,
-        BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures,
+        BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap,
+        BidirectionalOneToOneMap<DexMethod, DexMethod> originalMethodSignatures,
         GraphLens previousLens,
         DexItemFactory dexItemFactory) {
       super(
           typeMap,
           methodMap,
           fieldMap,
-          originalFieldSignatures,
           originalMethodSignatures,
           previousLens,
           dexItemFactory);
@@ -479,7 +476,6 @@
             typeMap,
             methodMap,
             fieldMap,
-            originalFieldSignatures,
             originalMethodSignatures,
             previousLens,
             dexItemFactory);
diff --git a/src/main/java/com/android/tools/r8/ir/desugar/NestedPrivateMethodLens.java b/src/main/java/com/android/tools/r8/ir/desugar/NestedPrivateMethodLens.java
index 8889ec2..12418b3 100644
--- a/src/main/java/com/android/tools/r8/ir/desugar/NestedPrivateMethodLens.java
+++ b/src/main/java/com/android/tools/r8/ir/desugar/NestedPrivateMethodLens.java
@@ -12,7 +12,7 @@
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
 import com.android.tools.r8.graph.RewrittenPrototypeDescription;
 import com.android.tools.r8.ir.code.Invoke.Type;
-import com.android.tools.r8.utils.collections.BidirectionalManyToManyRepresentativeMap;
+import com.android.tools.r8.utils.collections.EmptyBidirectionalOneToOneMap;
 import com.google.common.collect.ImmutableMap;
 import java.util.IdentityHashMap;
 import java.util.Map;
@@ -34,9 +34,8 @@
     super(
         ImmutableMap.of(),
         methodMap,
-        ImmutableMap.of(),
-        null,
-        BidirectionalManyToManyRepresentativeMap.empty(),
+        new EmptyBidirectionalOneToOneMap<>(),
+        new EmptyBidirectionalOneToOneMap<>(),
         previousLens,
         appView.dexItemFactory());
     // No concurrent maps here, we do not want synchronization overhead.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java b/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
index d648bc1..f4232a2 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/Devirtualizer.java
@@ -26,6 +26,7 @@
 import com.android.tools.r8.ir.code.InvokeVirtual;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
@@ -47,9 +48,11 @@
 public class Devirtualizer {
 
   private final AppView<AppInfoWithLiveness> appView;
+  private final InternalOptions options;
 
   public Devirtualizer(AppView<AppInfoWithLiveness> appView) {
     this.appView = appView;
+    this.options = appView.options();
   }
 
   public void devirtualizeInvokeInterface(IRCode code) {
@@ -115,6 +118,7 @@
               }
             }
           }
+          continue;
         }
 
         if (current.isInvokeSuper()) {
@@ -122,7 +126,7 @@
 
           // Check if the instruction can be rewritten to invoke-super. This allows inlining of the
           // enclosing method into contexts outside the current class.
-          if (appView.options().testing.enableInvokeSuperToInvokeVirtualRewriting) {
+          if (options.testing.enableInvokeSuperToInvokeVirtualRewriting) {
             DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, context);
             if (singleTarget != null) {
               DexMethod invokedMethod = invoke.getInvokedMethod();
@@ -225,7 +229,18 @@
             if (castedReceiverCache.containsKey(receiver)
                 && castedReceiverCache.get(receiver).containsKey(holderClass.getType())) {
               Value cachedReceiver = castedReceiverCache.get(receiver).get(holderClass.getType());
-              if (dominatorTree.dominatedBy(block, cachedReceiver.definition.getBlock())) {
+              BasicBlock cachedReceiverBlock = cachedReceiver.definition.getBlock();
+              BasicBlock dominatorBlock = null;
+              if (cachedReceiverBlock.hasCatchHandlers()) {
+                if (cachedReceiverBlock.hasUniqueNormalSuccessor()) {
+                  dominatorBlock = cachedReceiverBlock.getUniqueNormalSuccessor();
+                } else {
+                  assert false;
+                }
+              } else {
+                dominatorBlock = cachedReceiverBlock;
+              }
+              if (dominatorBlock != null && dominatorTree.dominatedBy(block, dominatorBlock)) {
                 newReceiver = cachedReceiver;
               }
             }
@@ -245,16 +260,18 @@
               // We need to add this checkcast *before* the devirtualized invoke-virtual.
               assert it.peekPrevious() == devirtualizedInvoke;
               it.previous();
-              // If the current block has catch handlers, split the new checkcast on its own block.
-              // Because checkcast is also a throwing instr, we should split before adding it.
-              // Otherwise, catch handlers are bound to a block with checkcast, not invoke IR.
+
+              // If the current block has catch handlers, then split the block before adding the new
+              // check-cast instruction. The catch handlers are copied to the split block to ensure
+              // that all throwing instructions are covered by a catch-all catch handler in case of
+              // monitor instructions (see also b/174167294).
               BasicBlock blockWithDevirtualizedInvoke =
-                  block.hasCatchHandlers() ? it.split(code, blocks) : block;
+                  block.hasCatchHandlers()
+                      ? it.splitCopyCatchHandlers(code, blocks, options)
+                      : block;
               if (blockWithDevirtualizedInvoke != block) {
                 // If we split, add the new checkcast at the end of the currently visiting block.
-                it = block.listIterator(code, block.getInstructions().size());
-                it.previous();
-                it.add(checkCast);
+                block.listIterator(code, block.getInstructions().size() - 1).add(checkCast);
                 // Update the dominator tree after the split.
                 dominatorTree = new DominatorTree(code);
                 // Restore the cursor.
@@ -354,7 +371,11 @@
     if (newResolutionResult == null
         || newResolutionResult
             .isAccessibleForVirtualDispatchFrom(context, appView.appInfo())
-            .isPossiblyFalse()) {
+            .isPossiblyFalse()
+        || !newResolutionResult
+            .getResolvedMethod()
+            .getAccessFlags()
+            .isAtLeastAsVisibleAs(resolutionResult.getResolvedMethod().getAccessFlags())) {
       return target;
     }
 
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 b26bb34..96c9234 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
@@ -625,7 +625,7 @@
           shouldSynthesizeMonitorEnterExit && !target.getDefinition().isStatic();
       if (shouldSynthesizeNullCheckForReceiver
           && !isSynthesizingNullCheckForReceiverUsingMonitorEnter) {
-        synthesizeNullCheckForReceiver(appView, code);
+        synthesizeNullCheckForReceiver(appView, code, invoke);
       }
 
       // Insert monitor-enter and monitor-exit instructions if the method is synchronized.
@@ -763,11 +763,12 @@
       assert !initClassBlock.hasCatchHandlers();
 
       InstructionListIterator iterator = initClassBlock.listIterator(code);
-      iterator.setInsertionPosition(entryBlock.exit().getPosition());
+      iterator.setInsertionPosition(invoke.getPosition());
       iterator.add(new InitClass(code.createValue(TypeElement.getInt()), target.getHolderType()));
     }
 
-    private void synthesizeNullCheckForReceiver(AppView<?> appView, IRCode code) {
+    private void synthesizeNullCheckForReceiver(
+        AppView<?> appView, IRCode code, InvokeMethod invoke) {
       List<Value> arguments = code.collectArguments();
       if (!arguments.isEmpty()) {
         Value receiver = arguments.get(0);
@@ -782,7 +783,7 @@
         assert !throwBlock.hasCatchHandlers();
 
         InstructionListIterator iterator = throwBlock.listIterator(code);
-        iterator.setInsertionPosition(entryBlock.exit().getPosition());
+        iterator.setInsertionPosition(invoke.getPosition());
         if (appView.options().canUseRequireNonNull()) {
           DexMethod requireNonNullMethod = appView.dexItemFactory().objectsMethods.requireNonNull;
           iterator.add(new InvokeStatic(requireNonNullMethod, null, ImmutableList.of(receiver)));
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 8ba58f1..615e5ae 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
@@ -311,11 +311,10 @@
         // Insert the definition of the replacement.
         replacement.setPosition(position);
         if (block.hasCatchHandlers()) {
-          BasicBlock splitBlock = iterator.split(code, blocks, false);
-          splitBlock.listIterator(code).add(replacement);
-          assert !block.hasCatchHandlers();
-          assert splitBlock.hasCatchHandlers();
-          block.copyCatchHandlers(code, blocks, splitBlock, appView.options());
+          iterator
+              .splitCopyCatchHandlers(code, blocks, appView.options())
+              .listIterator(code)
+              .add(replacement);
         } else {
           iterator.add(replacement);
         }
@@ -413,11 +412,10 @@
         // Insert the definition of the replacement.
         replacement.setPosition(position);
         if (block.hasCatchHandlers()) {
-          BasicBlock splitBlock = iterator.split(code, blocks, false);
-          splitBlock.listIterator(code).add(replacement);
-          assert !block.hasCatchHandlers();
-          assert splitBlock.hasCatchHandlers();
-          block.copyCatchHandlers(code, blocks, splitBlock, appView.options());
+          iterator
+              .splitCopyCatchHandlers(code, blocks, appView.options())
+              .listIterator(code)
+              .add(replacement);
         } else {
           iterator.add(replacement);
         }
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 3745120..cabff87 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
@@ -344,7 +344,7 @@
     HorizontallyMergedClasses horizontallyMergedClasses = appView.horizontallyMergedClasses();
     assert verticallyMergedClasses != null;
     assert horizontallyMergedClasses != null;
-    assert verticallyMergedClasses.isTarget(method.getHolderType())
+    assert verticallyMergedClasses.isMergeTarget(method.getHolderType())
         || horizontallyMergedClasses.isMergeTarget(method.getHolderType());
     assert appView
         .dexItemFactory()
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 48002cc..c681eaa 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
@@ -29,6 +29,9 @@
 import com.android.tools.r8.utils.MethodSignatureEquivalence;
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneMap;
+import com.android.tools.r8.utils.collections.EmptyBidirectionalOneToOneMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.ImmutableMap;
@@ -54,14 +57,13 @@
     private final Map<DexMethod, ArgumentInfoCollection> removedArgumentsInfoPerMethod;
 
     UninstantiatedTypeOptimizationGraphLens(
-        BidirectionalOneToOneHashMap<DexMethod, DexMethod> methodMap,
+        BidirectionalOneToOneMap<DexMethod, DexMethod> methodMap,
         Map<DexMethod, ArgumentInfoCollection> removedArgumentsInfoPerMethod,
         AppView<?> appView) {
       super(
           ImmutableMap.of(),
-          methodMap,
-          ImmutableMap.of(),
-          null,
+          methodMap.getForwardMap(),
+          new EmptyBidirectionalOneToOneMap<>(),
           methodMap.getInverseOneToOneMap(),
           appView.graphLens(),
           appView.dexItemFactory());
@@ -129,7 +131,7 @@
     }
 
     Map<Wrapper<DexMethod>, Set<DexType>> changedVirtualMethods = new HashMap<>();
-    BidirectionalOneToOneHashMap<DexMethod, DexMethod> methodMapping =
+    MutableBidirectionalOneToOneMap<DexMethod, DexMethod> methodMapping =
         new BidirectionalOneToOneHashMap<>();
     Map<DexMethod, ArgumentInfoCollection> removedArgumentsInfoPerMethod = new IdentityHashMap<>();
 
@@ -140,7 +142,7 @@
                 processClass(
                     clazz,
                     changedVirtualMethods,
-                    methodMapping.getForwardBacking(),
+                    methodMapping.getForwardMap(),
                     methodPoolCollection,
                     removedArgumentsInfoPerMethod));
 
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 16fb558..ec1b904 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
@@ -7,7 +7,6 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.ArgumentUse;
 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.DexMethod;
 import com.android.tools.r8.graph.DexProgramClass;
@@ -27,9 +26,10 @@
 import com.android.tools.r8.utils.ThreadUtils;
 import com.android.tools.r8.utils.Timing;
 import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneMap;
+import com.android.tools.r8.utils.collections.EmptyBidirectionalOneToOneMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
 import com.google.common.base.Equivalence.Wrapper;
-import com.google.common.collect.BiMap;
-import com.google.common.collect.ImmutableBiMap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Streams;
 import java.util.BitSet;
@@ -48,7 +48,7 @@
   private final AppView<AppInfoWithLiveness> appView;
   private final MethodPoolCollection methodPoolCollection;
 
-  private final BidirectionalOneToOneHashMap<DexMethod, DexMethod> methodMapping =
+  private final MutableBidirectionalOneToOneMap<DexMethod, DexMethod> methodMapping =
       new BidirectionalOneToOneHashMap<>();
   private final Map<DexMethod, ArgumentInfoCollection> removedArguments = new IdentityHashMap<>();
 
@@ -57,19 +57,15 @@
     private final Map<DexMethod, ArgumentInfoCollection> removedArguments;
 
     UnusedArgumentsGraphLens(
-        Map<DexType, DexType> typeMap,
         Map<DexMethod, DexMethod> methodMap,
-        Map<DexField, DexField> fieldMap,
-        BiMap<DexField, DexField> originalFieldSignatures,
-        BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures,
+        BidirectionalOneToOneMap<DexMethod, DexMethod> originalMethodSignatures,
         GraphLens previousLens,
         DexItemFactory dexItemFactory,
         Map<DexMethod, ArgumentInfoCollection> removedArguments) {
       super(
-          typeMap,
+          ImmutableMap.of(),
           methodMap,
-          fieldMap,
-          originalFieldSignatures,
+          new EmptyBidirectionalOneToOneMap<>(),
           originalMethodSignatures,
           previousLens,
           dexItemFactory);
@@ -108,10 +104,7 @@
 
     if (!methodMapping.isEmpty()) {
       return new UnusedArgumentsGraphLens(
-          ImmutableMap.of(),
-          methodMapping,
-          ImmutableMap.of(),
-          ImmutableBiMap.of(),
+          methodMapping.getForwardMap(),
           methodMapping.getInverseOneToOneMap(),
           appView.graphLens(),
           appView.dexItemFactory(),
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/UtilityMethodsForCodeOptimizations.java b/src/main/java/com/android/tools/r8/ir/optimize/UtilityMethodsForCodeOptimizations.java
index 247e2e3..1b55f13 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/UtilityMethodsForCodeOptimizations.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/UtilityMethodsForCodeOptimizations.java
@@ -19,6 +19,35 @@
 
 public class UtilityMethodsForCodeOptimizations {
 
+  public static UtilityMethodForCodeOptimizations synthesizeToStringIfNotNullMethod(
+      AppView<?> appView, ProgramMethod context, MethodProcessingId methodProcessingId) {
+    InternalOptions options = appView.options();
+    if (options.isGeneratingClassFiles()) {
+      // TODO(b/172194277): Allow synthetics when generating CF.
+      return null;
+    }
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    DexProto proto = dexItemFactory.createProto(dexItemFactory.voidType, dexItemFactory.objectType);
+    SyntheticItems syntheticItems = appView.getSyntheticItems();
+    ProgramMethod syntheticMethod =
+        syntheticItems.createMethod(
+            context,
+            dexItemFactory,
+            builder ->
+                builder
+                    .setProto(proto)
+                    .setAccessFlags(MethodAccessFlags.createPublicStaticSynthetic())
+                    .setCode(method -> getToStringIfNotNullCodeTemplate(method, options)),
+            methodProcessingId);
+    return new UtilityMethodForCodeOptimizations(syntheticMethod);
+  }
+
+  private static CfCode getToStringIfNotNullCodeTemplate(
+      DexMethod method, InternalOptions options) {
+    return CfUtilityMethodsForCodeOptimizations
+        .CfUtilityMethodsForCodeOptimizationsTemplates_toStringIfNotNull(options, method);
+  }
+
   public static UtilityMethodForCodeOptimizations synthesizeThrowClassCastExceptionIfNotNullMethod(
       AppView<?> appView, ProgramMethod context, MethodProcessingId methodProcessingId) {
     InternalOptions options = appView.options();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumDataMap.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumDataMap.java
new file mode 100644
index 0000000..07471e7
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumDataMap.java
@@ -0,0 +1,109 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.enums;
+
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldKnownData;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+
+public class EnumDataMap {
+  private final ImmutableMap<DexType, EnumData> map;
+
+  public static EnumDataMap empty() {
+    return new EnumDataMap(ImmutableMap.of());
+  }
+
+  public EnumDataMap(ImmutableMap<DexType, EnumData> map) {
+    this.map = map;
+  }
+
+  public boolean isUnboxedEnum(DexType type) {
+    return map.containsKey(type);
+  }
+
+  public boolean isEmpty() {
+    return map.isEmpty();
+  }
+
+  public Set<DexType> getUnboxedEnums() {
+    return map.keySet();
+  }
+
+  public EnumInstanceFieldKnownData getInstanceFieldData(
+      DexType enumType, DexField enumInstanceField) {
+    assert map.containsKey(enumType);
+    return map.get(enumType).getInstanceFieldData(enumInstanceField);
+  }
+
+  public boolean hasUnboxedValueFor(DexField enumStaticField) {
+    return isUnboxedEnum(enumStaticField.holder)
+        && map.get(enumStaticField.holder).hasUnboxedValueFor(enumStaticField);
+  }
+
+  public int getUnboxedValue(DexField enumStaticField) {
+    assert map.containsKey(enumStaticField.holder);
+    return map.get(enumStaticField.holder).getUnboxedValue(enumStaticField);
+  }
+
+  public int getValuesSize(DexType enumType) {
+    assert map.containsKey(enumType);
+    return map.get(enumType).getValuesSize();
+  }
+
+  public boolean matchesValuesField(DexField staticField) {
+    assert map.containsKey(staticField.holder);
+    return map.get(staticField.holder).matchesValuesField(staticField);
+  }
+
+  public static class EnumData {
+    static final int INVALID_VALUES_SIZE = -1;
+
+    // Map each enum instance field to the list of field known data.
+    final ImmutableMap<DexField, EnumInstanceFieldKnownData> instanceFieldMap;
+    // Map each enum instance (static field) to the unboxed integer value.
+    final ImmutableMap<DexField, Integer> unboxedValues;
+    // Fields matching the $VALUES content and type, usually one.
+    final ImmutableSet<DexField> valuesFields;
+    // Size of the $VALUES field, if the valuesFields set is empty, set to INVALID_VALUES_SIZE.
+    final int valuesSize;
+
+    public EnumData(
+        ImmutableMap<DexField, EnumInstanceFieldKnownData> instanceFieldMap,
+        ImmutableMap<DexField, Integer> unboxedValues,
+        ImmutableSet<DexField> valuesFields,
+        int valuesSize) {
+      this.instanceFieldMap = instanceFieldMap;
+      this.unboxedValues = unboxedValues;
+      this.valuesFields = valuesFields;
+      this.valuesSize = valuesSize;
+    }
+
+    public EnumInstanceFieldKnownData getInstanceFieldData(DexField enumInstanceField) {
+      assert instanceFieldMap.containsKey(enumInstanceField);
+      return instanceFieldMap.get(enumInstanceField);
+    }
+
+    public int getUnboxedValue(DexField field) {
+      assert unboxedValues.containsKey(field);
+      return unboxedValues.get(field);
+    }
+
+    public boolean hasUnboxedValueFor(DexField field) {
+      return unboxedValues.get(field) != null;
+    }
+
+    public boolean matchesValuesField(DexField field) {
+      return valuesFields.contains(field);
+    }
+
+    public int getValuesSize() {
+      assert valuesSize != INVALID_VALUES_SIZE;
+      return valuesSize;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumInstanceFieldData.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumInstanceFieldData.java
index 4f4dea9..69a5179 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumInstanceFieldData.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumInstanceFieldData.java
@@ -4,9 +4,9 @@
 
 package com.android.tools.r8.ir.optimize.enums;
 
-import com.android.tools.r8.graph.DexField;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
-import java.util.Map;
+import com.android.tools.r8.utils.collections.ImmutableInt2ReferenceSortedMap;
+import java.util.function.BiConsumer;
 
 /*
  * My instances represent the values of an enum field for each of the enum instance.
@@ -83,9 +83,9 @@
   }
 
   public static class EnumInstanceFieldMappingData extends EnumInstanceFieldKnownData {
-    private final Map<DexField, AbstractValue> mapping;
+    private final ImmutableInt2ReferenceSortedMap<AbstractValue> mapping;
 
-    public EnumInstanceFieldMappingData(Map<DexField, AbstractValue> mapping) {
+    public EnumInstanceFieldMappingData(ImmutableInt2ReferenceSortedMap<AbstractValue> mapping) {
       this.mapping = mapping;
     }
 
@@ -104,8 +104,12 @@
       return this;
     }
 
-    public AbstractValue getData(DexField field) {
-      return mapping.get(field);
+    public AbstractValue getData(int unboxedEnumValue) {
+      return mapping.get(unboxedEnumValue);
+    }
+
+    public void forEach(BiConsumer<? super Integer, ? super AbstractValue> consumer) {
+      mapping.forEach(consumer);
     }
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumInstanceFieldDataMap.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumInstanceFieldDataMap.java
deleted file mode 100644
index f02e488..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumInstanceFieldDataMap.java
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-package com.android.tools.r8.ir.optimize.enums;
-
-import com.android.tools.r8.graph.DexField;
-import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldKnownData;
-import com.google.common.collect.ImmutableMap;
-
-public class EnumInstanceFieldDataMap {
-  private final ImmutableMap<DexType, ImmutableMap<DexField, EnumInstanceFieldKnownData>>
-      instanceFieldMap;
-
-  public EnumInstanceFieldDataMap(
-      ImmutableMap<DexType, ImmutableMap<DexField, EnumInstanceFieldKnownData>> instanceFieldMap) {
-    this.instanceFieldMap = instanceFieldMap;
-  }
-
-  public EnumInstanceFieldKnownData getInstanceFieldData(
-      DexType enumType, DexField enumInstanceField) {
-    assert instanceFieldMap.containsKey(enumType);
-    assert instanceFieldMap.get(enumType).containsKey(enumInstanceField);
-    return instanceFieldMap.get(enumType).get(enumInstanceField);
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
index 2d46582..1ce0df3 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxer.java
@@ -20,7 +20,6 @@
 import com.android.tools.r8.graph.DexProgramClass;
 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.FieldResolutionResult;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
@@ -28,10 +27,13 @@
 import com.android.tools.r8.graph.ProgramPackageCollection;
 import com.android.tools.r8.graph.ResolutionResult;
 import com.android.tools.r8.graph.UseRegistry;
+import com.android.tools.r8.ir.analysis.fieldvalueanalysis.StaticFieldValues;
+import com.android.tools.r8.ir.analysis.fieldvalueanalysis.StaticFieldValues.EnumStaticFieldValues;
 import com.android.tools.r8.ir.analysis.type.ArrayTypeElement;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.analysis.value.EnumValuesObjectState;
 import com.android.tools.r8.ir.analysis.value.ObjectState;
 import com.android.tools.r8.ir.code.ArrayPut;
 import com.android.tools.r8.ir.code.BasicBlock;
@@ -51,6 +53,7 @@
 import com.android.tools.r8.ir.conversion.IRConverter;
 import com.android.tools.r8.ir.conversion.PostMethodProcessor;
 import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import com.android.tools.r8.ir.optimize.enums.EnumDataMap.EnumData;
 import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldKnownData;
 import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldMappingData;
 import com.android.tools.r8.ir.optimize.enums.EnumInstanceFieldData.EnumInstanceFieldOrdinalData;
@@ -64,13 +67,16 @@
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.Reporter;
 import com.android.tools.r8.utils.StringDiagnostic;
+import com.android.tools.r8.utils.collections.ImmutableInt2ReferenceSortedMap;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceArrayMap;
+import it.unimi.dsi.fastutil.ints.Int2ReferenceMap;
 import java.util.Arrays;
-import java.util.IdentityHashMap;
 import java.util.Map;
+import java.util.OptionalInt;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutionException;
@@ -86,6 +92,10 @@
   private final EnumUnboxingCandidateInfoCollection enumUnboxingCandidatesInfo;
   private final ProgramPackageCollection enumsToUnboxWithPackageRequirement =
       ProgramPackageCollection.createEmpty();
+  private final Map<DexType, EnumStaticFieldValues> staticFieldValuesMap =
+      new ConcurrentHashMap<>();
+
+  private final DexEncodedField ordinalField;
 
   private EnumUnboxingRewriter enumUnboxerRewriter;
 
@@ -104,6 +114,18 @@
     }
     assert !appView.options().debug;
     enumUnboxingCandidatesInfo = new EnumUnboxingCandidateAnalysis(appView, this).findCandidates();
+
+    ordinalField =
+        appView.appInfo().resolveField(factory.enumMembers.ordinalField).getResolvedField();
+    if (ordinalField == null) {
+      // This can happen when compiling for non standard libraries, in that case, this effectively
+      // disables the enum unboxer.
+      enumUnboxingCandidatesInfo.clear();
+    }
+  }
+
+  public static int ordinalToUnboxedInt(int ordinal) {
+    return ordinal + 1;
   }
 
   private void markEnumAsUnboxable(Reason reason, DexProgramClass enumClass) {
@@ -351,7 +373,7 @@
       ExecutorService executorService,
       OptimizationFeedbackDelayed feedback)
       throws ExecutionException {
-    EnumInstanceFieldDataMap enumInstanceFieldDataMap = finishAnalysis();
+    EnumDataMap enumDataMap = finishAnalysis();
     // At this point the enum unboxing candidates are no longer candidates, they will all be
     // unboxed. We extract the now immutable enums to unbox information and clear the candidate
     // info.
@@ -371,13 +393,12 @@
             .synthesizeEnumUnboxingUtilityClasses(
                 enumClassesToUnbox, enumsToUnboxWithPackageRequirement, appBuilder)
             .build();
-    enumUnboxerRewriter =
-        new EnumUnboxingRewriter(appView, enumsToUnbox, enumInstanceFieldDataMap, relocator);
+    enumUnboxerRewriter = new EnumUnboxingRewriter(appView, enumDataMap, relocator);
     NestedGraphLens enumUnboxingLens =
         new EnumUnboxingTreeFixer(appView, enumsToUnbox, relocator, enumUnboxerRewriter)
             .fixupTypeReferences();
     enumUnboxerRewriter.setEnumUnboxingLens(enumUnboxingLens);
-    appView.setUnboxedEnums(enumUnboxerRewriter.getEnumsToUnbox());
+    appView.setUnboxedEnums(enumDataMap);
     GraphLens previousLens = appView.graphLens();
     appView.rewriteWithLensAndApplication(enumUnboxingLens, appBuilder.build());
     updateOptimizationInfos(executorService, feedback);
@@ -426,34 +447,159 @@
     keepInfo.mutate(mutator -> mutator.removeKeepInfoForPrunedItems(enumsToUnbox));
   }
 
-  public EnumInstanceFieldDataMap finishAnalysis() {
+  public EnumDataMap finishAnalysis() {
     analyzeInitializers();
     analyzeAccessibility();
-    EnumInstanceFieldDataMap enumInstanceFieldDataMap = analyzeFields();
+    EnumDataMap enumDataMap = analyzeEnumInstances();
+    assert enumDataMap.getUnboxedEnums().size() == enumUnboxingCandidatesInfo.candidates().size();
     if (debugLogEnabled) {
       reportEnumsAnalysis();
     }
-    return enumInstanceFieldDataMap;
+    return enumDataMap;
   }
 
-  private EnumInstanceFieldDataMap analyzeFields() {
-    ImmutableMap.Builder<DexType, ImmutableMap<DexField, EnumInstanceFieldKnownData>> builder =
-        ImmutableMap.builder();
+  private EnumDataMap analyzeEnumInstances() {
+    ImmutableMap.Builder<DexType, EnumData> builder = ImmutableMap.builder();
     enumUnboxingCandidatesInfo.forEachCandidateAndRequiredInstanceFieldData(
         (enumClass, fields) -> {
-          ImmutableMap.Builder<DexField, EnumInstanceFieldKnownData> typeBuilder =
-              ImmutableMap.builder();
-          for (DexField field : fields) {
-            EnumInstanceFieldData enumInstanceFieldData = computeEnumFieldData(field, enumClass);
-            if (enumInstanceFieldData.isUnknown()) {
-              markEnumAsUnboxable(Reason.MISSING_INSTANCE_FIELD_DATA, enumClass);
-              return;
-            }
-            typeBuilder.put(field, enumInstanceFieldData.asEnumFieldKnownData());
+          EnumData data = buildData(enumClass, fields);
+          if (data == null) {
+            markEnumAsUnboxable(Reason.MISSING_INSTANCE_FIELD_DATA, enumClass);
+            return;
           }
-          builder.put(enumClass.type, typeBuilder.build());
+          builder.put(enumClass.type, data);
         });
-    return new EnumInstanceFieldDataMap(builder.build());
+    staticFieldValuesMap.clear();
+    return new EnumDataMap(builder.build());
+  }
+
+  private EnumData buildData(DexProgramClass enumClass, Set<DexField> fields) {
+    // This map holds all the accessible fields to their unboxed value, so we can remap the field
+    // read to the unboxed value.
+    ImmutableMap.Builder<DexField, Integer> unboxedValues = ImmutableMap.builder();
+    // This maps the ordinal to the object state, note that some fields may have been removed,
+    // hence the entry is in this map but not the enumToOrdinalMap.
+    Int2ReferenceMap<ObjectState> ordinalToObjectState = new Int2ReferenceArrayMap<>();
+    // Any fields matching the expected $VALUES content can be recorded here, they have however
+    // all the same content.
+    ImmutableSet.Builder<DexField> valuesField = ImmutableSet.builder();
+    EnumValuesObjectState valuesContents = null;
+
+    EnumStaticFieldValues enumStaticFieldValues = staticFieldValuesMap.get(enumClass.type);
+
+    // Step 1: We iterate over the field to find direct enum instance information and the values
+    // fields.
+    for (DexEncodedField staticField : enumClass.staticFields()) {
+      if (EnumUnboxingCandidateAnalysis.isEnumField(staticField, enumClass.type)) {
+        ObjectState enumState =
+            enumStaticFieldValues.getObjectStateForPossiblyPinnedField(staticField.field);
+        if (enumState != null) {
+          OptionalInt optionalOrdinal = getOrdinal(enumState);
+          if (!optionalOrdinal.isPresent()) {
+            return null;
+          }
+          int ordinal = optionalOrdinal.getAsInt();
+          unboxedValues.put(staticField.field, ordinalToUnboxedInt(ordinal));
+          ordinalToObjectState.put(ordinal, enumState);
+        }
+      } else if (EnumUnboxingCandidateAnalysis.matchesValuesField(
+          staticField, enumClass.type, factory)) {
+        AbstractValue valuesValue =
+            enumStaticFieldValues.getValuesAbstractValueForPossiblyPinnedField(staticField.field);
+        if (valuesValue == null || valuesValue.isZero()) {
+          // Unused field
+          continue;
+        }
+        if (valuesValue.isUnknown()) {
+          return null;
+        }
+        assert valuesValue.isSingleFieldValue();
+        ObjectState valuesState = valuesValue.asSingleFieldValue().getState();
+        if (!valuesState.isEnumValuesObjectState()) {
+          return null;
+        }
+        assert valuesContents == null
+            || valuesContents.equals(valuesState.asEnumValuesObjectState());
+        valuesContents = valuesState.asEnumValuesObjectState();
+        valuesField.add(staticField.field);
+      }
+    }
+
+    // Step 2: We complete the information based on the values content, since some enum instances
+    // may be reachable only though the $VALUES field.
+    if (valuesContents != null) {
+      for (int ordinal = 0; ordinal < valuesContents.getEnumValuesSize(); ordinal++) {
+        if (!ordinalToObjectState.containsKey(ordinal)) {
+          ObjectState enumState = valuesContents.getObjectStateForOrdinal(ordinal);
+          if (enumState.isEmpty()) {
+            // If $VALUES is used, we need data for all enums, at least the ordinal.
+            return null;
+          }
+          assert getOrdinal(enumState).isPresent();
+          assert getOrdinal(enumState).getAsInt() == ordinal;
+          ordinalToObjectState.put(ordinal, enumState);
+        }
+      }
+    }
+
+    // The ordinalToObjectState map may have holes at this point, if some enum instances are never
+    // used ($VALUES unused or removed, and enum instance field unused or removed), it contains
+    // only data for reachable enum instance, that is what we're interested in.
+    ImmutableMap.Builder<DexField, EnumInstanceFieldKnownData> instanceFieldBuilder =
+        ImmutableMap.builder();
+    for (DexField instanceField : fields) {
+      EnumInstanceFieldData fieldData =
+          computeEnumFieldData(instanceField, enumClass, ordinalToObjectState);
+      if (fieldData.isUnknown()) {
+        return null;
+      }
+      instanceFieldBuilder.put(instanceField, fieldData.asEnumFieldKnownData());
+    }
+
+    return new EnumData(
+        instanceFieldBuilder.build(),
+        unboxedValues.build(),
+        valuesField.build(),
+        valuesContents == null ? EnumData.INVALID_VALUES_SIZE : valuesContents.getEnumValuesSize());
+  }
+
+  private EnumInstanceFieldData computeEnumFieldData(
+      DexField instanceField,
+      DexProgramClass enumClass,
+      Int2ReferenceMap<ObjectState> ordinalToObjectState) {
+    DexEncodedField encodedInstanceField =
+        appView.appInfo().resolveFieldOn(enumClass, instanceField).getResolvedField();
+    assert encodedInstanceField != null;
+    boolean canBeOrdinal = instanceField.type.isIntType();
+    ImmutableInt2ReferenceSortedMap.Builder<AbstractValue> data =
+        ImmutableInt2ReferenceSortedMap.builder();
+    for (Integer ordinal : ordinalToObjectState.keySet()) {
+      ObjectState state = ordinalToObjectState.get(ordinal);
+      AbstractValue fieldValue = state.getAbstractFieldValue(encodedInstanceField);
+      if (!(fieldValue.isSingleNumberValue() || fieldValue.isSingleStringValue())) {
+        return EnumInstanceFieldUnknownData.getInstance();
+      }
+      data.put(ordinalToUnboxedInt(ordinal), fieldValue);
+      if (canBeOrdinal) {
+        assert fieldValue.isSingleNumberValue();
+        int computedValue = fieldValue.asSingleNumberValue().getIntValue();
+        if (computedValue != ordinal) {
+          canBeOrdinal = false;
+        }
+      }
+    }
+    if (canBeOrdinal) {
+      return new EnumInstanceFieldOrdinalData();
+    }
+    return new EnumInstanceFieldMappingData(data.build());
+  }
+
+  private OptionalInt getOrdinal(ObjectState state) {
+    AbstractValue field = state.getAbstractFieldValue(ordinalField);
+    if (field.isSingleNumberValue()) {
+      return OptionalInt.of(field.asSingleNumberValue().getIntValue());
+    }
+    return OptionalInt.empty();
   }
 
   private void analyzeAccessibility() {
@@ -494,6 +640,17 @@
     return useRegistry.computeConstraint(method.asProgramMethod(appView));
   }
 
+  public void recordEnumState(DexProgramClass clazz, StaticFieldValues staticFieldValues) {
+    if (staticFieldValues == null || !staticFieldValues.isEnumStaticFieldValues()) {
+      return;
+    }
+    assert clazz.isEnum();
+    EnumStaticFieldValues enumStaticFieldValues = staticFieldValues.asEnumStaticFieldValues();
+    if (getEnumUnboxingCandidateOrNull(clazz.type) != null) {
+      staticFieldValuesMap.put(clazz.type, enumStaticFieldValues);
+    }
+  }
+
   private class EnumAccessibilityUseRegistry extends UseRegistry {
 
     private ProgramMethod context;
@@ -929,89 +1086,6 @@
     return Reason.OTHER_UNSUPPORTED_INSTRUCTION;
   }
 
-  private EnumInstanceFieldData computeEnumFieldData(
-      DexField instanceField, DexProgramClass enumClass) {
-    DexEncodedField encodedInstanceField =
-        appView.appInfo().resolveFieldOn(enumClass, instanceField).getResolvedField();
-    assert encodedInstanceField != null;
-    boolean canBeOrdinal = instanceField.type.isIntType();
-    Map<DexField, AbstractValue> data = new IdentityHashMap<>();
-    EnumValueInfoMapCollection.EnumValueInfoMap enumValueInfoMap =
-        appView.appInfo().getEnumValueInfoMap(enumClass.type);
-    for (DexField staticField : enumValueInfoMap.enumValues()) {
-      ObjectState enumInstanceState =
-          computeEnumInstanceObjectState(enumClass, staticField, enumValueInfoMap);
-      if (enumInstanceState == null) {
-        // The enum instance is effectively unused. No need to generate anything for it, the path
-        // will never be taken.
-      } else {
-        AbstractValue fieldValue = enumInstanceState.getAbstractFieldValue(encodedInstanceField);
-        if (!(fieldValue.isSingleNumberValue() || fieldValue.isSingleStringValue())) {
-          return EnumInstanceFieldUnknownData.getInstance();
-        }
-        data.put(staticField, fieldValue);
-        if (canBeOrdinal) {
-          int ordinalValue = enumValueInfoMap.getEnumValueInfo(staticField).ordinal;
-          assert fieldValue.isSingleNumberValue();
-          int computedValue = fieldValue.asSingleNumberValue().getIntValue();
-          if (computedValue != ordinalValue) {
-            canBeOrdinal = false;
-          }
-        }
-      }
-    }
-    if (canBeOrdinal) {
-      return new EnumInstanceFieldOrdinalData();
-    }
-    return new EnumInstanceFieldMappingData(data);
-  }
-
-  // We need to access the enum instance object state to figure out if it contains known constant
-  // field values. The enum instance may be accessed in two ways, directly through the enum
-  // static field, or through the enum $VALUES field. If none of them are kept, the instance is
-  // effectively unused. The object state may be stored in the enum static field optimization
-  // info, if kept, or in the $VALUES optimization info, if kept.
-  // If the enum instance is unused, this method answers null.
-  private ObjectState computeEnumInstanceObjectState(
-      DexProgramClass enumClass,
-      DexField staticField,
-      EnumValueInfoMapCollection.EnumValueInfoMap enumValueInfoMap) {
-    // Attempt 1: Get object state from the instance field's optimization info.
-    DexEncodedField encodedStaticField = enumClass.lookupStaticField(staticField);
-    AbstractValue enumInstanceValue = encodedStaticField.getOptimizationInfo().getAbstractValue();
-    if (enumInstanceValue.isSingleFieldValue()) {
-      return enumInstanceValue.asSingleFieldValue().getState();
-    }
-    if (enumInstanceValue.isUnknown()) {
-      return ObjectState.empty();
-    }
-    assert enumInstanceValue.isZero();
-
-    // Attempt 2: Get object state from the values field's optimization info.
-    DexEncodedField valuesField =
-        enumClass.lookupStaticField(
-            factory.createField(
-                enumClass.type,
-                factory.createArrayType(1, enumClass.type),
-                factory.enumValuesFieldName));
-    AbstractValue valuesValue = valuesField.getOptimizationInfo().getAbstractValue();
-    if (valuesValue.isZero()) {
-      // Unused enum instance.
-      return null;
-    }
-    if (valuesValue.isUnknown()) {
-      return ObjectState.empty();
-    }
-    assert valuesValue.isSingleFieldValue();
-    ObjectState valuesState = valuesValue.asSingleFieldValue().getState();
-    if (valuesState.isEnumValuesObjectState()) {
-      return valuesState
-          .asEnumValuesObjectState()
-          .getObjectStateForOrdinal(enumValueInfoMap.getEnumValueInfo(staticField).ordinal);
-    }
-    return ObjectState.empty();
-  }
-
   private void reportEnumsAnalysis() {
     assert debugLogEnabled;
     Reporter reporter = appView.options().reporter;
@@ -1081,7 +1155,6 @@
     VALUES_INVOKE,
     COMPARE_TO_INVOKE,
     UNSUPPORTED_LIBRARY_CALL,
-    MISSING_INFO_MAP,
     MISSING_INSTANCE_FIELD_DATA,
     INVALID_FIELD_READ,
     INVALID_FIELD_PUT,
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
index b4bd339..40b0c86 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingCandidateAnalysis.java
@@ -12,7 +12,6 @@
 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.graph.EnumValueInfoMapCollection.EnumValueInfoMap;
 import com.android.tools.r8.ir.optimize.enums.EnumUnboxer.Reason;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.shaking.KeepInfoCollection;
@@ -54,7 +53,7 @@
     if (!clazz.isEnum()) {
       return false;
     }
-    if (!clazz.isEffectivelyFinal(appView)) {
+    if (clazz.superType != factory.enumType || !clazz.isEffectivelyFinal(appView)) {
       enumUnboxer.reportFailure(clazz.type, Reason.SUBTYPES);
       return false;
     }
@@ -66,12 +65,6 @@
       enumUnboxer.reportFailure(clazz.type, Reason.UNEXPECTED_STATIC_FIELD);
       return false;
     }
-    EnumValueInfoMap enumValueInfoMap =
-        appView.appInfo().withLiveness().getEnumValueInfoMap(clazz.type);
-    if (enumValueInfoMap == null) {
-      enumUnboxer.reportFailure(clazz.type, Reason.MISSING_INFO_MAP);
-      return false;
-    }
     return true;
   }
 
@@ -79,15 +72,9 @@
   // instances.
   private boolean enumHasBasicStaticFields(DexProgramClass clazz) {
     for (DexEncodedField staticField : clazz.staticFields()) {
-      if (staticField.field.type == clazz.type
-          && staticField.accessFlags.isEnum()
-          && staticField.accessFlags.isFinal()) {
+      if (isEnumField(staticField, clazz.type)) {
         // Enum field, valid, do nothing.
-      } else if (staticField.field.type.isArrayType()
-          && staticField.field.type.toArrayElementType(factory) == clazz.type
-          && staticField.accessFlags.isSynthetic()
-          && staticField.accessFlags.isFinal()
-          && staticField.field.name == factory.enumValuesFieldName) {
+      } else if (matchesValuesField(staticField, clazz.type, factory)) {
         // Field $VALUES, valid, do nothing.
       } else if (appView.appInfo().isFieldRead(staticField)) {
         // Only non read static fields are valid, and they are assumed unused.
@@ -97,6 +84,21 @@
     return true;
   }
 
+  static boolean isEnumField(DexEncodedField staticField, DexType enumType) {
+    return staticField.field.type == enumType
+        && staticField.accessFlags.isEnum()
+        && staticField.accessFlags.isFinal();
+  }
+
+  static boolean matchesValuesField(
+      DexEncodedField staticField, DexType enumType, DexItemFactory factory) {
+    return staticField.field.type.isArrayType()
+        && staticField.field.type.toArrayElementType(factory) == enumType
+        && staticField.accessFlags.isSynthetic()
+        && staticField.accessFlags.isFinal()
+        && staticField.field.name == factory.enumValuesFieldName;
+  }
+
   private void removeEnumsInAnnotations() {
     for (DexProgramClass clazz : appView.appInfo().classes()) {
       if (clazz.isAnnotation()) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
index b6660fc..f230dda 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumUnboxingLens.java
@@ -13,8 +13,8 @@
 import com.android.tools.r8.ir.code.Invoke;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
-import com.google.common.collect.BiMap;
-import com.google.common.collect.HashBiMap;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import java.util.IdentityHashMap;
@@ -29,9 +29,8 @@
   EnumUnboxingLens(
       Map<DexType, DexType> typeMap,
       Map<DexMethod, DexMethod> methodMap,
-      Map<DexField, DexField> fieldMap,
-      BiMap<DexField, DexField> originalFieldSignatures,
-      BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures,
+      BidirectionalOneToOneMap<DexField, DexField> fieldMap,
+      BidirectionalOneToOneMap<DexMethod, DexMethod> originalMethodSignatures,
       GraphLens previousLens,
       DexItemFactory dexItemFactory,
       Map<DexMethod, RewrittenPrototypeDescription> prototypeChangesPerMethod,
@@ -40,7 +39,6 @@
         typeMap,
         methodMap,
         fieldMap,
-        originalFieldSignatures,
         originalMethodSignatures,
         previousLens,
         dexItemFactory);
@@ -76,8 +74,9 @@
   static class Builder {
 
     protected final Map<DexType, DexType> typeMap = new IdentityHashMap<>();
-    protected final BiMap<DexField, DexField> originalFieldSignatures = HashBiMap.create();
-    protected final BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures =
+    protected final MutableBidirectionalOneToOneMap<DexField, DexField> newFieldSignatures =
+        new BidirectionalOneToOneHashMap<>();
+    protected final MutableBidirectionalOneToOneMap<DexMethod, DexMethod> originalMethodSignatures =
         new BidirectionalOneToOneHashMap<>();
 
     private Map<DexMethod, RewrittenPrototypeDescription> prototypeChangesPerMethod =
@@ -94,7 +93,7 @@
       if (from == to) {
         return;
       }
-      originalFieldSignatures.put(to, from);
+      newFieldSignatures.put(from, to);
     }
 
     public void move(DexMethod from, DexMethod to, boolean fromStatic, boolean toStatic) {
@@ -143,16 +142,13 @@
 
     public EnumUnboxingLens build(
         DexItemFactory dexItemFactory, GraphLens previousLens, Set<DexType> unboxedEnums) {
-      if (typeMap.isEmpty()
-          && originalFieldSignatures.isEmpty()
-          && originalMethodSignatures.isEmpty()) {
+      if (typeMap.isEmpty() && newFieldSignatures.isEmpty() && originalMethodSignatures.isEmpty()) {
         return null;
       }
       return new EnumUnboxingLens(
           typeMap,
-          originalMethodSignatures.getInverseOneToOneMap(),
-          originalFieldSignatures.inverse(),
-          originalFieldSignatures,
+          originalMethodSignatures.getInverseOneToOneMap().getForwardMap(),
+          newFieldSignatures,
           originalMethodSignatures,
           previousLens,
           dexItemFactory,
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 c456bd0..6c20c4f 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
@@ -21,9 +21,6 @@
 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;
-import com.android.tools.r8.graph.EnumValueInfoMapCollection.EnumValueInfo;
-import com.android.tools.r8.graph.EnumValueInfoMapCollection.EnumValueInfoMap;
 import com.android.tools.r8.graph.FieldAccessFlags;
 import com.android.tools.r8.graph.GenericSignature.FieldTypeSignature;
 import com.android.tools.r8.graph.GenericSignature.MethodTypeSignature;
@@ -76,8 +73,7 @@
 
   private final AppView<AppInfoWithLiveness> appView;
   private final DexItemFactory factory;
-  private final EnumValueInfoMapCollection enumsToUnbox;
-  private final EnumInstanceFieldDataMap unboxedEnumsInstanceFieldData;
+  private final EnumDataMap unboxedEnumsData;
   private final UnboxedEnumMemberRelocator relocator;
   private NestedGraphLens enumUnboxingLens;
 
@@ -93,18 +89,11 @@
 
   EnumUnboxingRewriter(
       AppView<AppInfoWithLiveness> appView,
-      Set<DexType> enumsToUnbox,
-      EnumInstanceFieldDataMap unboxedEnumsInstanceFieldData,
+      EnumDataMap unboxedEnumsInstanceFieldData,
       UnboxedEnumMemberRelocator relocator) {
     this.appView = appView;
     this.factory = appView.dexItemFactory();
-    EnumValueInfoMapCollection.Builder builder = EnumValueInfoMapCollection.builder();
-    for (DexType toUnbox : enumsToUnbox) {
-      assert appView.appInfo().withLiveness().getEnumValueInfoMap(toUnbox) != null;
-      builder.put(toUnbox, appView.appInfo().withLiveness().getEnumValueInfoMap(toUnbox));
-    }
-    this.enumsToUnbox = builder.build();
-    this.unboxedEnumsInstanceFieldData = unboxedEnumsInstanceFieldData;
+    this.unboxedEnumsData = unboxedEnumsInstanceFieldData;
     this.relocator = relocator;
 
     // Custom methods for java.lang.Enum methods ordinal, equals and compareTo.
@@ -147,14 +136,10 @@
     this.enumUnboxingLens = enumUnboxingLens;
   }
 
-  public EnumValueInfoMapCollection getEnumsToUnbox() {
-    return enumsToUnbox;
-  }
-
   Set<Phi> rewriteCode(IRCode code) {
     // We should not process the enum methods, they will be removed and they may contain invalid
     // rewriting rules.
-    if (enumsToUnbox.isEmpty()) {
+    if (unboxedEnumsData.isEmpty()) {
       return Sets.newIdentityHashSet();
     }
     assert code.isConsistentSSABeforeTypesAreCorrect();
@@ -223,7 +208,7 @@
               && invokeStatic.getArgument(0).isConstClass()) {
             DexType enumType =
                 invokeStatic.getArgument(0).getConstInstruction().asConstClass().getValue();
-            if (enumsToUnbox.containsEnum(enumType)) {
+            if (unboxedEnumsData.isUnboxedEnum(enumType)) {
               DexMethod valueOfMethod = computeValueOfUtilityMethod(enumType);
               Value outValue = invokeStatic.outValue();
               Value rewrittenOutValue = null;
@@ -282,17 +267,15 @@
         }
         if (instruction.isStaticGet()) {
           StaticGet staticGet = instruction.asStaticGet();
-          DexType holder = staticGet.getField().holder;
-          if (enumsToUnbox.containsEnum(holder)) {
+          DexField field = staticGet.getField();
+          DexType holder = field.holder;
+          if (unboxedEnumsData.isUnboxedEnum(holder)) {
             if (staticGet.outValue() == null) {
               iterator.removeOrReplaceByDebugLocalRead();
               continue;
             }
-            EnumValueInfoMap enumValueInfoMap = enumsToUnbox.getEnumValueInfoMap(holder);
-            assert enumValueInfoMap != null;
             affectedPhis.addAll(staticGet.outValue().uniquePhiUsers());
-            EnumValueInfo enumValueInfo = enumValueInfoMap.getEnumValueInfo(staticGet.getField());
-            if (enumValueInfo == null && staticGet.getField().name == factory.enumValuesFieldName) {
+            if (unboxedEnumsData.matchesValuesField(field)) {
               utilityMethods.computeIfAbsent(
                   valuesUtilityMethod, m -> synthesizeValuesUtilityMethod());
               DexField fieldValues = createValuesField(holder);
@@ -300,7 +283,9 @@
               DexMethod methodValues = createValuesMethod(holder);
               utilityMethods.computeIfAbsent(
                   methodValues,
-                  m -> computeValuesEncodedMethod(m, fieldValues, enumValueInfoMap.size()));
+                  m ->
+                      computeValuesEncodedMethod(
+                          m, fieldValues, unboxedEnumsData.getValuesSize(holder)));
               Value rewrittenOutValue =
                   code.createValue(
                       ArrayTypeElement.create(TypeElement.getInt(), definitelyNotNull()));
@@ -310,9 +295,10 @@
               convertedEnums.put(invoke, holder);
             } else {
               // Replace by ordinal + 1 for null check (null is 0).
-              assert enumValueInfo != null
-                  : "Invalid read to " + staticGet.getField().name + ", error during enum analysis";
-              ConstNumber intConstant = code.createIntConstant(enumValueInfo.convertToInt());
+              assert unboxedEnumsData.hasUnboxedValueFor(field)
+                  : "Invalid read to " + field.name + ", error during enum analysis";
+              ConstNumber intConstant =
+                  code.createIntConstant(unboxedEnumsData.getUnboxedValue(field));
               iterator.replaceCurrentInstruction(intConstant);
               convertedEnums.put(intConstant, holder);
             }
@@ -322,7 +308,7 @@
         if (instruction.isInstanceGet()) {
           InstanceGet instanceGet = instruction.asInstanceGet();
           DexType holder = instanceGet.getField().holder;
-          if (enumsToUnbox.containsEnum(holder)) {
+          if (unboxedEnumsData.isUnboxedEnum(holder)) {
             DexMethod fieldMethod = computeInstanceFieldMethod(instanceGet.getField());
             Value rewrittenOutValue =
                 code.createValue(
@@ -332,7 +318,7 @@
                 new InvokeStatic(
                     fieldMethod, rewrittenOutValue, ImmutableList.of(instanceGet.object()));
             iterator.replaceCurrentInstruction(invoke);
-            if (enumsToUnbox.containsEnum(instanceGet.getField().type)) {
+            if (unboxedEnumsData.isUnboxedEnum(instanceGet.getField().type)) {
               convertedEnums.put(invoke, instanceGet.getField().type);
             }
           }
@@ -393,7 +379,7 @@
 
   private DexMethod computeInstanceFieldMethod(DexField field) {
     EnumInstanceFieldKnownData enumFieldKnownData =
-        unboxedEnumsInstanceFieldData.getInstanceFieldData(field.holder, field);
+        unboxedEnumsData.getInstanceFieldData(field.holder, field);
     if (enumFieldKnownData.isOrdinal()) {
       utilityMethods.computeIfAbsent(ordinalUtilityMethod, m -> synthesizeOrdinalMethod());
       return ordinalUtilityMethod;
@@ -437,7 +423,7 @@
       return null;
     }
     DexType enumType = type.asClassType().getClassType();
-    return enumsToUnbox.containsEnum(enumType) ? enumType : null;
+    return unboxedEnumsData.isUnboxedEnum(enumType) ? enumType : null;
   }
 
   public String compatibleName(DexType type) {
@@ -478,7 +464,7 @@
   }
 
   private DexMethod computeInstanceFieldUtilityMethod(DexType enumType, DexField field) {
-    assert enumsToUnbox.containsEnum(enumType);
+    assert unboxedEnumsData.isUnboxedEnum(enumType);
     assert field.holder == enumType || field.holder == factory.enumType;
     String methodName =
         "get"
@@ -498,7 +484,7 @@
 
   private DexMethod computeStringValueOfUtilityMethod(DexType enumType) {
     // TODO(b/167994636): remove duplication between instance field name read and this method.
-    assert enumsToUnbox.containsEnum(enumType);
+    assert unboxedEnumsData.isUnboxedEnum(enumType);
     String methodName = "string$valueOf$" + compatibleName(enumType);
     DexMethod fieldMethod =
         factory.createMethod(
@@ -514,7 +500,7 @@
   }
 
   private DexMethod computeValueOfUtilityMethod(DexType enumType) {
-    assert enumsToUnbox.containsEnum(enumType);
+    assert unboxedEnumsData.isUnboxedEnum(enumType);
     DexMethod valueOf =
         factory.createMethod(
             relocator.getNewMemberLocationFor(enumType),
@@ -538,7 +524,7 @@
       return null;
     }
     DexType classType = baseType.asClassType().getClassType();
-    return enumsToUnbox.containsEnum(classType) ? classType : null;
+    return unboxedEnumsData.isUnboxedEnum(classType) ? classType : null;
   }
 
   void synthesizeEnumUnboxingUtilityMethods(IRConverter converter, ExecutorService executorService)
@@ -593,16 +579,13 @@
   private DexEncodedMethod synthesizeInstanceFieldMethod(
       DexMethod method, DexType enumType, DexField field, AbstractValue nullValue) {
     assert method.proto.returnType == field.type;
-    assert unboxedEnumsInstanceFieldData.getInstanceFieldData(enumType, field).isMapping();
+    assert unboxedEnumsData.getInstanceFieldData(enumType, field).isMapping();
     CfCode cfCode =
         new EnumUnboxingCfCodeProvider.EnumUnboxingInstanceFieldCfCodeProvider(
                 appView,
                 method.holder,
                 field.type,
-                enumsToUnbox.getEnumValueInfoMap(enumType),
-                unboxedEnumsInstanceFieldData
-                    .getInstanceFieldData(enumType, field)
-                    .asEnumFieldMappingData(),
+                unboxedEnumsData.getInstanceFieldData(enumType, field).asEnumFieldMappingData(),
                 nullValue)
             .generateCfCode();
     return synthesizeUtilityMethod(cfCode, method, false);
@@ -610,7 +593,7 @@
 
   private DexEncodedMethod synthesizeValueOfUtilityMethod(DexMethod method, DexType enumType) {
     assert method.proto.returnType == factory.intType;
-    assert unboxedEnumsInstanceFieldData
+    assert unboxedEnumsData
         .getInstanceFieldData(enumType, factory.enumMembers.nameField)
         .isMapping();
     CfCode cfCode =
@@ -618,8 +601,7 @@
                 appView,
                 method.holder,
                 enumType,
-                enumsToUnbox.getEnumValueInfoMap(enumType),
-                unboxedEnumsInstanceFieldData
+                unboxedEnumsData
                     .getInstanceFieldData(enumType, factory.enumMembers.nameField)
                     .asEnumFieldMappingData())
             .generateCfCode();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueInfoMapCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueInfoMapCollector.java
deleted file mode 100644
index c94be2e..0000000
--- a/src/main/java/com/android/tools/r8/ir/optimize/enums/EnumValueInfoMapCollector.java
+++ /dev/null
@@ -1,94 +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.ir.optimize.enums;
-
-import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.DexField;
-import com.android.tools.r8.graph.DexProgramClass;
-import com.android.tools.r8.graph.DexType;
-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.ProgramMethod;
-import com.android.tools.r8.ir.code.IRCode;
-import com.android.tools.r8.ir.code.Instruction;
-import com.android.tools.r8.ir.code.InvokeDirect;
-import com.android.tools.r8.ir.code.StaticPut;
-import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import java.util.LinkedHashMap;
-
-/**
- * Extracts the ordinal values and any anonymous subtypes for all Enum classes from their static
- * initializer.
- *
- * <p>An Enum class has a field for each value. In the class initializer, each field is initialized
- * to a singleton object that represents the value. This code matches on the corresponding call to
- * the constructor (instance initializer) and extracts the value of the second argument, which is
- * the ordinal and the holder which is the concrete type.
- */
-public class EnumValueInfoMapCollector {
-
-  private final AppView<AppInfoWithLiveness> appView;
-
-  private final EnumValueInfoMapCollection.Builder valueInfoMapsBuilder =
-      EnumValueInfoMapCollection.builder();
-
-  public EnumValueInfoMapCollector(AppView<AppInfoWithLiveness> appView) {
-    this.appView = appView;
-  }
-
-  public AppInfoWithLiveness run() {
-    for (DexProgramClass clazz : appView.appInfo().classes()) {
-      processClasses(clazz);
-    }
-    EnumValueInfoMapCollection valueInfoMaps = valueInfoMapsBuilder.build();
-    if (!valueInfoMaps.isEmpty()) {
-      return appView.appInfo().withEnumValueInfoMaps(valueInfoMaps);
-    }
-    return appView.appInfo();
-  }
-
-  private void processClasses(DexProgramClass clazz) {
-    // Enum classes are flagged as such. Also, for library classes, the ordinals are not known.
-    if (!clazz.accessFlags.isEnum() || clazz.isNotProgramClass() || !clazz.hasClassInitializer()) {
-      return;
-    }
-    ProgramMethod initializer = clazz.getProgramClassInitializer();
-    IRCode code = initializer.buildIR(appView);
-    LinkedHashMap<DexField, EnumValueInfo> enumValueInfoMap = new LinkedHashMap<>();
-    for (StaticPut staticPut : code.<StaticPut>instructions(Instruction::isStaticPut)) {
-      if (staticPut.getField().type != clazz.type) {
-        continue;
-      }
-      Instruction newInstance = staticPut.value().definition;
-      if (newInstance == null || !newInstance.isNewInstance()) {
-        continue;
-      }
-      Instruction ordinal = null;
-      DexType type = null;
-      for (Instruction ctorCall : newInstance.outValue().uniqueUsers()) {
-        if (!ctorCall.isInvokeDirect()) {
-          continue;
-        }
-        InvokeDirect invoke = ctorCall.asInvokeDirect();
-        if (!appView.dexItemFactory().isConstructor(invoke.getInvokedMethod())
-            || invoke.arguments().size() < 3) {
-          continue;
-        }
-        ordinal = invoke.arguments().get(2).definition;
-        type = invoke.getInvokedMethod().holder;
-        break;
-      }
-      if (ordinal == null || !ordinal.isConstNumber() || type == null) {
-        return;
-      }
-
-      EnumValueInfo info = new EnumValueInfo(type, ordinal.asConstNumber().getIntValue());
-      if (enumValueInfoMap.put(staticPut.getField(), info) != null) {
-        return;
-      }
-    }
-    valueInfoMapsBuilder.put(clazz.type, new EnumValueInfoMap(enumValueInfoMap));
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
index 09c4a5d..4e3fb01 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/MethodOptimizationInfoCollector.java
@@ -1076,6 +1076,7 @@
         assert !context.getHolderType().isD8R8SynthesizedLambdaClassType()
                 || options.debug
                 || appView.appInfo().hasPinnedInstanceInitializer(context.getHolderType())
+                || appView.options().horizontalClassMergerOptions().isJavaLambdaMergingEnabled()
             : "Unexpected observable side effects from lambda `" + context.toSourceString() + "`";
       }
       return;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
index 5840ea6..f0d7c28 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/OptimizationFeedbackSimple.java
@@ -195,6 +195,6 @@
 
   @Override
   public void classInitializerMayBePostponed(DexEncodedMethod method) {
-    // Ignored.
+    method.getMutableOptimizationInfo().markClassInitializerMayBePostponed();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/info/field/InstanceFieldTypeInitializationInfo.java b/src/main/java/com/android/tools/r8/ir/optimize/info/field/InstanceFieldTypeInitializationInfo.java
index 7e78f27..262671f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/info/field/InstanceFieldTypeInitializationInfo.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/info/field/InstanceFieldTypeInitializationInfo.java
@@ -5,10 +5,10 @@
 package com.android.tools.r8.ir.optimize.info.field;
 
 import com.android.tools.r8.graph.AppView;
-import com.android.tools.r8.graph.EnumValueInfoMapCollection;
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
 import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.optimize.enums.EnumDataMap;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import java.util.Objects;
 
@@ -49,14 +49,14 @@
   @Override
   public InstanceFieldInitializationInfo rewrittenWithLens(
       AppView<AppInfoWithLiveness> appView, GraphLens lens) {
-    EnumValueInfoMapCollection unboxedEnums = appView.unboxedEnums();
+    EnumDataMap enumDataMap = appView.unboxedEnums();
     if (dynamicLowerBoundType != null
-        && unboxedEnums.containsEnum(dynamicLowerBoundType.getClassType())) {
+        && enumDataMap.isUnboxedEnum(dynamicLowerBoundType.getClassType())) {
       // No point in tracking the type of primitives.
       return UnknownInstanceFieldInitializationInfo.getInstance();
     }
     if (dynamicUpperBoundType.isClassType()
-        && unboxedEnums.containsEnum(dynamicUpperBoundType.asClassType().getClassType())) {
+        && enumDataMap.isUnboxedEnum(dynamicUpperBoundType.asClassType().getClassType())) {
       // No point in tracking the type of primitives.
       return UnknownInstanceFieldInitializationInfo.getInstance();
     }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java
index 69bdef8..2c93f77 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/BooleanMethodOptimizer.java
@@ -19,7 +19,7 @@
 import com.android.tools.r8.ir.code.Value;
 import java.util.Set;
 
-public class BooleanMethodOptimizer implements LibraryMethodModelCollection {
+public class BooleanMethodOptimizer extends StatelessLibraryMethodModelCollection {
 
   private final AppView<?> appView;
   private final DexItemFactory dexItemFactory;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/EnumMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/EnumMethodOptimizer.java
index 4323220..5ee7017 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/EnumMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/EnumMethodOptimizer.java
@@ -19,7 +19,8 @@
 import com.android.tools.r8.ir.code.Value;
 import java.util.Set;
 
-public class EnumMethodOptimizer implements LibraryMethodModelCollection {
+public class EnumMethodOptimizer extends StatelessLibraryMethodModelCollection {
+
   private final AppView<?> appView;
 
   EnumMethodOptimizer(AppView<?> appView) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
index d6c8ada..25916f9 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
@@ -35,7 +35,7 @@
   /** The library types that are modeled. */
   private final Set<DexType> modeledLibraryTypes = Sets.newIdentityHashSet();
 
-  private final Map<DexType, LibraryMethodModelCollection> libraryMethodModelCollections =
+  private final Map<DexType, LibraryMethodModelCollection<?>> libraryMethodModelCollections =
       new IdentityHashMap<>();
 
   public LibraryMemberOptimizer(AppView<?> appView) {
@@ -43,6 +43,7 @@
     register(new BooleanMethodOptimizer(appView));
     register(new ObjectMethodOptimizer(appView));
     register(new ObjectsMethodOptimizer(appView));
+    register(new StringBuilderMethodOptimizer(appView));
     register(new StringMethodOptimizer(appView));
     if (appView.enableWholeProgramOptimizations()) {
       // Subtyping is required to prove the enum class is a subtype of java.lang.Enum.
@@ -98,9 +99,9 @@
     return modeledLibraryTypes.contains(type);
   }
 
-  private void register(LibraryMethodModelCollection optimizer) {
+  private void register(LibraryMethodModelCollection<?> optimizer) {
     DexType modeledType = optimizer.getType();
-    LibraryMethodModelCollection existing =
+    LibraryMethodModelCollection<?> existing =
         libraryMethodModelCollections.put(modeledType, optimizer);
     assert existing == null;
     modeledLibraryTypes.add(modeledType);
@@ -114,30 +115,37 @@
       MethodProcessingId methodProcessingId) {
     Set<Value> affectedValues = Sets.newIdentityHashSet();
     InstructionListIterator instructionIterator = code.instructionListIterator();
+    Map<LibraryMethodModelCollection<?>, LibraryMethodModelCollection.State> optimizationStates =
+        new IdentityHashMap<>();
     while (instructionIterator.hasNext()) {
       Instruction instruction = instructionIterator.next();
-      if (instruction.isInvokeMethod()) {
-        InvokeMethod invoke = instruction.asInvokeMethod();
-        DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, code.context());
-        if (singleTarget != null) {
-          optimizeInvoke(code, instructionIterator, invoke, singleTarget, affectedValues);
-        }
+      if (!instruction.isInvokeMethod()) {
+        continue;
       }
+
+      InvokeMethod invoke = instruction.asInvokeMethod();
+      DexClassAndMethod singleTarget = invoke.lookupSingleTarget(appView, code.context());
+      if (singleTarget == null) {
+        continue;
+      }
+
+      LibraryMethodModelCollection<?> optimizer =
+          libraryMethodModelCollections.get(singleTarget.getHolderType());
+      if (optimizer == null) {
+        continue;
+      }
+
+      LibraryMethodModelCollection.State optimizationState =
+          optimizationStates.computeIfAbsent(
+              optimizer,
+              libraryMethodModelCollection ->
+                  libraryMethodModelCollection.createInitialState(
+                      methodProcessor, methodProcessingId));
+      optimizer.optimize(
+          code, instructionIterator, invoke, singleTarget, affectedValues, optimizationState);
     }
     if (!affectedValues.isEmpty()) {
       new TypeAnalysis(appView).narrowing(affectedValues);
     }
   }
-
-  private void optimizeInvoke(
-      IRCode code,
-      InstructionListIterator instructionIterator,
-      InvokeMethod invoke,
-      DexClassAndMethod singleTarget,
-      Set<Value> affectedValues) {
-    LibraryMethodModelCollection optimizer =
-        libraryMethodModelCollections.getOrDefault(
-            singleTarget.getHolderType(), NopLibraryMethodModelCollection.getInstance());
-    optimizer.optimize(code, instructionIterator, invoke, singleTarget, affectedValues);
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodModelCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodModelCollection.java
index 843e9ab..5cb30b3 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodModelCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodModelCollection.java
@@ -10,10 +10,18 @@
 import com.android.tools.r8.ir.code.InstructionListIterator;
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessingId;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.optimize.library.LibraryMethodModelCollection.State;
 import java.util.Set;
 
 /** Used to model the behavior of library methods for optimization purposes. */
-public interface LibraryMethodModelCollection {
+public interface LibraryMethodModelCollection<T extends State> {
+
+  default T createInitialState(
+      MethodProcessor methodProcessor, MethodProcessingId methodProcessingId) {
+    return null;
+  }
 
   /**
    * The library class whose methods are being modeled by this collection of models. As an example,
@@ -30,5 +38,20 @@
       InstructionListIterator instructionIterator,
       InvokeMethod invoke,
       DexClassAndMethod singleTarget,
-      Set<Value> affectedValues);
+      Set<Value> affectedValues,
+      T state);
+
+  @SuppressWarnings("unchecked")
+  default void optimize(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      DexClassAndMethod singleTarget,
+      Set<Value> affectedValues,
+      Object state) {
+    optimize(code, instructionIterator, invoke, singleTarget, affectedValues, (T) state);
+  }
+
+  /** Thread local optimization state to allow caching, etc. */
+  interface State {}
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java
index 55d1dc3..70ace2b 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMethodSideEffectModelCollection.java
@@ -36,6 +36,7 @@
             .put(dexItemFactory.npeMethods.initWithMessage, alwaysTrue())
             .put(dexItemFactory.objectMembers.constructor, alwaysTrue())
             .put(dexItemFactory.objectMembers.getClass, alwaysTrue())
+            .put(dexItemFactory.stringBuilderMethods.toString, alwaysTrue())
             .put(dexItemFactory.stringMembers.hashCode, alwaysTrue());
     putAll(builder, dexItemFactory.classMethods.getNames, alwaysTrue());
     putAll(
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LogMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LogMethodOptimizer.java
index f8847d3..aa93f3c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LogMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LogMethodOptimizer.java
@@ -17,7 +17,7 @@
 import com.android.tools.r8.shaking.ProguardConfiguration;
 import java.util.Set;
 
-public class LogMethodOptimizer implements LibraryMethodModelCollection {
+public class LogMethodOptimizer extends StatelessLibraryMethodModelCollection {
 
   private static final int VERBOSE = 2;
   private static final int DEBUG = 3;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/NopLibraryMethodModelCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/library/NopLibraryMethodModelCollection.java
index f852393..2eeb165 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/NopLibraryMethodModelCollection.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/NopLibraryMethodModelCollection.java
@@ -13,7 +13,7 @@
 import com.android.tools.r8.ir.code.Value;
 import java.util.Set;
 
-public class NopLibraryMethodModelCollection implements LibraryMethodModelCollection {
+public class NopLibraryMethodModelCollection extends StatelessLibraryMethodModelCollection {
 
   private static final NopLibraryMethodModelCollection INSTANCE =
       new NopLibraryMethodModelCollection();
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectMethodOptimizer.java
index f16af45..c325500 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectMethodOptimizer.java
@@ -14,7 +14,7 @@
 import com.android.tools.r8.ir.code.Value;
 import java.util.Set;
 
-public class ObjectMethodOptimizer implements LibraryMethodModelCollection {
+public class ObjectMethodOptimizer extends StatelessLibraryMethodModelCollection {
 
   private final DexItemFactory dexItemFactory;
 
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectsMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectsMethodOptimizer.java
index 8fcc1ac..9d0c8c8 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectsMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/ObjectsMethodOptimizer.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexClassAndMethod;
 import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexItemFactory.ObjectsMethods;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.ir.code.IRCode;
 import com.android.tools.r8.ir.code.InstructionListIterator;
@@ -14,12 +15,17 @@
 import com.android.tools.r8.ir.code.Value;
 import java.util.Set;
 
-public class ObjectsMethodOptimizer implements LibraryMethodModelCollection {
+public class ObjectsMethodOptimizer extends StatelessLibraryMethodModelCollection {
 
+  private final AppView<?> appView;
   private final DexItemFactory dexItemFactory;
+  private final ObjectsMethods objectsMethods;
 
   ObjectsMethodOptimizer(AppView<?> appView) {
-    this.dexItemFactory = appView.dexItemFactory();
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    this.appView = appView;
+    this.dexItemFactory = dexItemFactory;
+    this.objectsMethods = dexItemFactory.objectsMethods;
   }
 
   @Override
@@ -34,14 +40,16 @@
       InvokeMethod invoke,
       DexClassAndMethod singleTarget,
       Set<Value> affectedValues) {
-    if (dexItemFactory.objectsMethods.isRequireNonNullMethod(singleTarget.getReference())) {
+    if (objectsMethods.isRequireNonNullMethod(singleTarget.getReference())) {
       optimizeRequireNonNull(instructionIterator, invoke, affectedValues);
+    } else if (singleTarget.getReference() == objectsMethods.toStringWithObject) {
+      optimizeToStringWithObject(code, instructionIterator, invoke, affectedValues);
     }
   }
 
   private void optimizeRequireNonNull(
       InstructionListIterator instructionIterator, InvokeMethod invoke, Set<Value> affectedValues) {
-    Value inValue = invoke.inValues().get(0);
+    Value inValue = invoke.getFirstArgument();
     if (inValue.getType().isDefinitelyNotNull()) {
       Value outValue = invoke.outValue();
       if (outValue != null) {
@@ -52,4 +60,18 @@
       instructionIterator.removeOrReplaceByDebugLocalRead();
     }
   }
+
+  private void optimizeToStringWithObject(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      Set<Value> affectedValues) {
+    Value object = invoke.getFirstArgument();
+    if (object.getType().isDefinitelyNull()) {
+      instructionIterator.replaceCurrentInstructionWithConstString(appView, code, "null");
+      if (invoke.hasOutValue()) {
+        affectedValues.addAll(invoke.outValue().affectedValues());
+      }
+    }
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/StatelessLibraryMethodModelCollection.java b/src/main/java/com/android/tools/r8/ir/optimize/library/StatelessLibraryMethodModelCollection.java
new file mode 100644
index 0000000..8b36204
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/StatelessLibraryMethodModelCollection.java
@@ -0,0 +1,46 @@
+// Copyright (c) 2019, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.library;
+
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessingId;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.optimize.library.StatelessLibraryMethodModelCollection.State;
+import java.util.Set;
+
+public abstract class StatelessLibraryMethodModelCollection
+    implements LibraryMethodModelCollection<State> {
+
+  @Override
+  public final State createInitialState(
+      MethodProcessor methodProcessor, MethodProcessingId methodProcessingId) {
+    return null;
+  }
+
+  public abstract void optimize(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      DexClassAndMethod singleTarget,
+      Set<Value> affectedValues);
+
+  @Override
+  public final void optimize(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      DexClassAndMethod singleTarget,
+      Set<Value> affectedValues,
+      State state) {
+    assert state == null;
+    optimize(code, instructionIterator, invoke, singleTarget, affectedValues);
+  }
+
+  static class State implements LibraryMethodModelCollection.State {}
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/StringBuilderMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/StringBuilderMethodOptimizer.java
new file mode 100644
index 0000000..9cccb5d
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/StringBuilderMethodOptimizer.java
@@ -0,0 +1,269 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.library;
+
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_DIRECT;
+import static com.android.tools.r8.ir.code.Opcodes.INVOKE_VIRTUAL;
+import static com.android.tools.r8.ir.code.Opcodes.NEW_INSTANCE;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexItemFactory.StringBuildingMethods;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeDirect;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
+import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.code.InvokeVirtual;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessingId;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations;
+import com.android.tools.r8.ir.optimize.UtilityMethodsForCodeOptimizations.UtilityMethodForCodeOptimizations;
+import com.android.tools.r8.ir.optimize.library.StringBuilderMethodOptimizer.State;
+import com.android.tools.r8.utils.InternalOptions;
+import com.android.tools.r8.utils.WorkList;
+import com.google.common.collect.Sets;
+import it.unimi.dsi.fastutil.objects.Reference2BooleanMap;
+import it.unimi.dsi.fastutil.objects.Reference2BooleanOpenHashMap;
+import java.util.Set;
+
+public class StringBuilderMethodOptimizer implements LibraryMethodModelCollection<State> {
+
+  private final AppView<?> appView;
+  private final DexItemFactory dexItemFactory;
+  private final InternalOptions options;
+  private final StringBuildingMethods stringBuilderMethods;
+
+  StringBuilderMethodOptimizer(AppView<?> appView) {
+    DexItemFactory dexItemFactory = appView.dexItemFactory();
+    this.appView = appView;
+    this.dexItemFactory = dexItemFactory;
+    this.options = appView.options();
+    this.stringBuilderMethods = dexItemFactory.stringBuilderMethods;
+  }
+
+  @Override
+  public State createInitialState(
+      MethodProcessor methodProcessor, MethodProcessingId methodProcessingId) {
+    return new State(methodProcessor, methodProcessingId);
+  }
+
+  @Override
+  public DexType getType() {
+    return dexItemFactory.stringBuilderType;
+  }
+
+  @Override
+  public void optimize(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethod invoke,
+      DexClassAndMethod singleTarget,
+      Set<Value> affectedValues,
+      State state) {
+    if (invoke.isInvokeMethodWithReceiver()) {
+      InvokeMethodWithReceiver invokeWithReceiver = invoke.asInvokeMethodWithReceiver();
+      if (stringBuilderMethods.isAppendMethod(singleTarget.getReference())) {
+        optimizeAppend(code, instructionIterator, invokeWithReceiver, singleTarget, state);
+      }
+    }
+  }
+
+  private void optimizeAppend(
+      IRCode code,
+      InstructionListIterator instructionIterator,
+      InvokeMethodWithReceiver invoke,
+      DexClassAndMethod singleTarget,
+      State state) {
+    if (!state.isUnusedBuilder(invoke.getReceiver())) {
+      return;
+    }
+    if (invoke.hasOutValue()) {
+      invoke.outValue().replaceUsers(invoke.getReceiver());
+    }
+    DexMethod appendMethod = singleTarget.getReference();
+    if (stringBuilderMethods.isAppendPrimitiveMethod(appendMethod)
+        || stringBuilderMethods.isAppendStringMethod(appendMethod)) {
+      instructionIterator.removeOrReplaceByDebugLocalRead();
+    } else if (stringBuilderMethods.isAppendObjectMethod(appendMethod)) {
+      Value object = invoke.getArgument(1);
+      if (object.isNeverNull()) {
+        // Replace the instruction by java.lang.Object.toString().
+        instructionIterator.replaceCurrentInstruction(
+            InvokeVirtual.builder()
+                .setMethod(dexItemFactory.objectMembers.toString)
+                .setSingleArgument(object)
+                .build());
+      } else if (options.canUseJavaUtilObjects()) {
+        // Replace the instruction by java.util.Objects.toString().
+        instructionIterator.replaceCurrentInstruction(
+            InvokeStatic.builder()
+                .setMethod(dexItemFactory.objectsMethods.toStringWithObject)
+                .setSingleArgument(object)
+                .build());
+        // Allow the java.util.Objects optimizer to optimize the newly added toString().
+        instructionIterator.previous();
+      } else {
+        // Replace the instruction by toStringIfNotNull().
+        UtilityMethodForCodeOptimizations toStringIfNotNullMethod =
+            UtilityMethodsForCodeOptimizations.synthesizeToStringIfNotNullMethod(
+                appView, code.context(), state.methodProcessingId);
+        // TODO(b/172194277): Allow synthetics when generating CF.
+        if (toStringIfNotNullMethod != null) {
+          toStringIfNotNullMethod.optimize(state.methodProcessor);
+          InvokeStatic replacement =
+              InvokeStatic.builder()
+                  .setMethod(toStringIfNotNullMethod.getMethod())
+                  .setSingleArgument(object)
+                  .build();
+          instructionIterator.replaceCurrentInstruction(replacement);
+        }
+      }
+    }
+  }
+
+  class State implements LibraryMethodModelCollection.State {
+
+    final MethodProcessor methodProcessor;
+    final MethodProcessingId methodProcessingId;
+
+    final Reference2BooleanMap<Value> unusedBuilders = new Reference2BooleanOpenHashMap<>();
+
+    State(MethodProcessor methodProcessor, MethodProcessingId methodProcessingId) {
+      this.methodProcessor = methodProcessor;
+      this.methodProcessingId = methodProcessingId;
+    }
+
+    boolean isUnusedBuilder(Value value) {
+      if (!unusedBuilders.containsKey(value)) {
+        computeIsUnusedBuilder(value);
+        assert unusedBuilders.containsKey(value);
+      }
+      return unusedBuilders.getBoolean(value);
+    }
+
+    private void computeIsUnusedBuilder(Value value) {
+      assert !unusedBuilders.containsKey(value);
+
+      Set<Value> aliases = Sets.newIdentityHashSet();
+      boolean isUnused = computeAllAliasesIfUnusedStringBuilder(value, aliases);
+      aliases.forEach(alias -> unusedBuilders.put(alias, isUnused));
+    }
+
+    /**
+     * Adds all the aliases of the given StringBuilder value to {@param aliases}, or returns false
+     * if all aliases were not found (e.g., due to a phi user).
+     */
+    private boolean computeAllAliasesIfUnusedStringBuilder(Value value, Set<Value> aliases) {
+      WorkList<Value> worklist = WorkList.newIdentityWorkList(value);
+      while (worklist.hasNext()) {
+        Value alias = worklist.next();
+        aliases.add(alias);
+
+        if (unusedBuilders.containsKey(alias)) {
+          assert !unusedBuilders.getBoolean(alias);
+          return false;
+        }
+
+        // Don't track phi aliases.
+        if (alias.hasPhiUsers()) {
+          return false;
+        }
+
+        // Analyze root, if any.
+        if (alias.isPhi()) {
+          return false;
+        }
+
+        Instruction definition = alias.definition;
+        switch (definition.opcode()) {
+          case NEW_INSTANCE:
+            assert definition.asNewInstance().clazz == dexItemFactory.stringBuilderType;
+            break;
+
+          case INVOKE_VIRTUAL:
+            {
+              InvokeVirtual invoke = definition.asInvokeVirtual();
+              if (!stringBuilderMethods.isAppendMethod(invoke.getInvokedMethod())) {
+                // Unhandled definition.
+                return false;
+              }
+              worklist.addIfNotSeen(invoke.getReceiver());
+            }
+            break;
+
+          default:
+            // Unhandled definition.
+            return false;
+        }
+
+        // Analyze all users.
+        for (Instruction user : alias.uniqueUsers()) {
+          switch (user.opcode()) {
+            case INVOKE_DIRECT:
+              {
+                InvokeDirect invoke = user.asInvokeDirect();
+
+                // Only allow invokes where the string builder value is the receiver.
+                if (invoke.arguments().lastIndexOf(alias) > 0) {
+                  return false;
+                }
+
+                // Only allow invoke-direct instructions that target the string builder constructor.
+                if (!stringBuilderMethods.isConstructorMethod(invoke.getInvokedMethod())) {
+                  return false;
+                }
+              }
+              break;
+
+            case INVOKE_VIRTUAL:
+              {
+                InvokeVirtual invoke = user.asInvokeVirtual();
+
+                // Only allow invokes where the string builder value is the receiver.
+                if (invoke.arguments().lastIndexOf(alias) > 0) {
+                  return false;
+                }
+
+                DexMethod invokedMethod = invoke.getInvokedMethod();
+
+                // Allow calls to append(), but make sure to introduce the newly introduced alias,
+                // if append() has an out-value.
+                if (stringBuilderMethods.isAppendMethod(invokedMethod)) {
+                  if (invoke.hasOutValue()) {
+                    worklist.addIfNotSeen(invoke.outValue());
+                  }
+                  break;
+                }
+
+                // Allow calls to toString().
+                if (invokedMethod == stringBuilderMethods.toString) {
+                  // Only allow unused StringBuilders.
+                  if (invoke.hasOutValue() && invoke.outValue().hasNonDebugUsers()) {
+                    return false;
+                  }
+                  break;
+                }
+
+                // Invoke to unhandled method, give up.
+                return false;
+              }
+
+            default:
+              // Unhandled user, give up.
+              return false;
+          }
+        }
+      }
+      return true;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java
index 18ad3e6..c8d04ae 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/StringMethodOptimizer.java
@@ -18,7 +18,7 @@
 import com.android.tools.r8.ir.code.Value;
 import java.util.Set;
 
-public class StringMethodOptimizer implements LibraryMethodModelCollection {
+public class StringMethodOptimizer extends StatelessLibraryMethodModelCollection {
 
   private final AppView<?> appView;
   private final DexItemFactory dexItemFactory;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerGraphLens.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerGraphLens.java
index fbc4c12..75a78ab 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/ClassStaticizerGraphLens.java
@@ -9,21 +9,19 @@
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
 import com.android.tools.r8.ir.code.Invoke.Type;
-import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
-import com.google.common.collect.BiMap;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneMap;
 import com.google.common.collect.ImmutableMap;
 
 class ClassStaticizerGraphLens extends NestedGraphLens {
 
   ClassStaticizerGraphLens(
       AppView<?> appView,
-      BiMap<DexField, DexField> fieldMapping,
-      BidirectionalOneToOneHashMap<DexMethod, DexMethod> methodMapping) {
+      BidirectionalOneToOneMap<DexField, DexField> fieldMapping,
+      BidirectionalOneToOneMap<DexMethod, DexMethod> methodMapping) {
     super(
         ImmutableMap.of(),
-        methodMapping,
+        methodMapping.getForwardMap(),
         fieldMapping,
-        fieldMapping.inverse(),
         methodMapping.getInverseOneToOneMap(),
         appView.graphLens(),
         appView.dexItemFactory());
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
index 2313a5a..9d84c1f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/staticizer/StaticizingProcessor.java
@@ -47,10 +47,9 @@
 import com.android.tools.r8.utils.TraversalContinuation;
 import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
 import com.android.tools.r8.utils.collections.LongLivedProgramMethodSetBuilder;
+import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.android.tools.r8.utils.collections.SortedProgramMethodSet;
-import com.google.common.collect.BiMap;
-import com.google.common.collect.HashBiMap;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.Sets;
 import com.google.common.collect.Streams;
@@ -738,9 +737,10 @@
   }
 
   private ProgramMethodSet staticizeMethodSymbols() {
-    BidirectionalOneToOneHashMap<DexMethod, DexMethod> methodMapping =
+    MutableBidirectionalOneToOneMap<DexMethod, DexMethod> methodMapping =
         new BidirectionalOneToOneHashMap<>();
-    BiMap<DexField, DexField> fieldMapping = HashBiMap.create();
+    MutableBidirectionalOneToOneMap<DexField, DexField> fieldMapping =
+        new BidirectionalOneToOneHashMap<>();
 
     ProgramMethodSet staticizedMethods = ProgramMethodSet.create();
     for (CandidateInfo candidate : classStaticizer.candidates.values()) {
@@ -803,8 +803,8 @@
       DexProgramClass candidateClass,
       DexType hostType,
       DexProgramClass hostClass,
-      BidirectionalOneToOneHashMap<DexMethod, DexMethod> methodMapping,
-      BiMap<DexField, DexField> fieldMapping) {
+      MutableBidirectionalOneToOneMap<DexMethod, DexMethod> methodMapping,
+      MutableBidirectionalOneToOneMap<DexField, DexField> fieldMapping) {
     candidateToHostMapping.put(candidateClass.type, hostType);
 
     // Process static fields.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizer.java
index cd9fe4e..a61662c 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/string/StringBuilderOptimizer.java
@@ -30,6 +30,7 @@
 import com.android.tools.r8.ir.code.InvokeMethod;
 import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
 import com.android.tools.r8.ir.code.InvokeVirtual;
+import com.android.tools.r8.ir.code.NewInstance;
 import com.android.tools.r8.ir.code.NumberConversion;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.code.ValueType;
@@ -215,10 +216,9 @@
     private Set<Value> findAllLocalBuilders() {
       // During the first iteration, collect builders that are locally created.
       // TODO(b/114002137): Make sure new-instance is followed by <init> before any other calls.
-      for (Instruction instr : code.instructions()) {
-        if (instr.isNewInstance()
-            && optimizationConfiguration.isBuilderType(instr.asNewInstance().clazz)) {
-          Value builder = instr.asNewInstance().dest();
+      for (NewInstance newInstance : code.<NewInstance>instructions(Instruction::isNewInstance)) {
+        if (optimizationConfiguration.isBuilderType(newInstance.clazz)) {
+          Value builder = newInstance.asNewInstance().dest();
           assert !builderToStringCounts.containsKey(builder);
           builderToStringCounts.put(builder, 0);
         }
@@ -228,24 +228,21 @@
       }
       int concatenationCount = 0;
       // During the second iteration, count builders' usage.
-      for (Instruction instr : code.instructions()) {
-        if (instr.isInvokeMethod()) {
-          InvokeMethod invoke = instr.asInvokeMethod();
-          DexMethod invokedMethod = invoke.getInvokedMethod();
-          if (optimizationConfiguration.isAppendMethod(invokedMethod)) {
-            concatenationCount++;
-            // The analysis might be overwhelmed.
-            if (concatenationCount > CONCATENATION_THRESHOLD) {
-              return ImmutableSet.of();
-            }
-          } else if (optimizationConfiguration.isToStringMethod(invokedMethod)) {
-            assert invoke.arguments().size() == 1;
-            Value receiver = invoke.getArgument(0).getAliasedValue();
-            for (Value builder : collectAllLinkedBuilders(receiver)) {
-              if (builderToStringCounts.containsKey(builder)) {
-                int count = builderToStringCounts.getInt(builder);
-                builderToStringCounts.put(builder, count + 1);
-              }
+      for (InvokeMethod invoke : code.<InvokeMethod>instructions(Instruction::isInvokeMethod)) {
+        DexMethod invokedMethod = invoke.getInvokedMethod();
+        if (optimizationConfiguration.isAppendMethod(invokedMethod)) {
+          concatenationCount++;
+          // The analysis might be overwhelmed.
+          if (concatenationCount > CONCATENATION_THRESHOLD) {
+            return ImmutableSet.of();
+          }
+        } else if (optimizationConfiguration.isToStringMethod(invokedMethod)) {
+          assert invoke.arguments().size() == 1;
+          Value receiver = invoke.getArgument(0).getAliasedValue();
+          for (Value builder : collectAllLinkedBuilders(receiver)) {
+            if (builderToStringCounts.containsKey(builder)) {
+              int count = builderToStringCounts.getInt(builder);
+              builderToStringCounts.put(builder, count + 1);
             }
           }
         }
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizations.java b/src/main/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizations.java
index 9484007..9625b93 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizations.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizations.java
@@ -72,4 +72,40 @@
         ImmutableList.of(),
         ImmutableList.of());
   }
+
+  public static CfCode CfUtilityMethodsForCodeOptimizationsTemplates_toStringIfNotNull(
+      InternalOptions options, DexMethod method) {
+    CfLabel label0 = new CfLabel();
+    CfLabel label1 = new CfLabel();
+    CfLabel label2 = new CfLabel();
+    CfLabel label3 = new CfLabel();
+    return new CfCode(
+        method.holder,
+        1,
+        1,
+        ImmutableList.of(
+            label0,
+            new CfLoad(ValueType.OBJECT, 0),
+            new CfIf(If.Type.EQ, ValueType.OBJECT, label2),
+            label1,
+            new CfLoad(ValueType.OBJECT, 0),
+            new CfInvoke(
+                182,
+                options.itemFactory.createMethod(
+                    options.itemFactory.objectType,
+                    options.itemFactory.createProto(options.itemFactory.stringType),
+                    options.itemFactory.createString("toString")),
+                false),
+            new CfStackInstruction(CfStackInstruction.Opcode.Pop),
+            label2,
+            new CfFrame(
+                new Int2ReferenceAVLTreeMap<>(
+                    new int[] {0},
+                    new FrameType[] {FrameType.initialized(options.itemFactory.objectType)}),
+                new ArrayDeque<>(Arrays.asList())),
+            new CfReturnVoid(),
+            label3),
+        ImmutableList.of(),
+        ImmutableList.of());
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/DesugaredLibraryAPIConversionCfCodeProvider.java b/src/main/java/com/android/tools/r8/ir/synthetic/DesugaredLibraryAPIConversionCfCodeProvider.java
index 2c80105..6986b3d 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/DesugaredLibraryAPIConversionCfCodeProvider.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/DesugaredLibraryAPIConversionCfCodeProvider.java
@@ -276,7 +276,7 @@
               factory.createMethod(
                   wrapperField.holder,
                   factory.createProto(factory.voidType, argType),
-                  factory.initMethodName),
+                  factory.constructorMethodName),
               false));
       instructions.add(new CfReturn(ValueType.fromDexType(wrapperField.holder)));
       return standardCfCodeFromInstructions(instructions);
@@ -303,7 +303,7 @@
               factory.createMethod(
                   factory.objectType,
                   factory.createProto(factory.voidType),
-                  factory.initMethodName),
+                  factory.constructorMethodName),
               false));
       instructions.add(new CfLoad(ValueType.fromDexType(wrapperField.holder), 0));
       instructions.add(new CfLoad(ValueType.fromDexType(wrapperField.type), 1));
@@ -337,7 +337,7 @@
               factory.createMethod(
                   factory.runtimeExceptionType,
                   factory.createProto(factory.voidType, factory.stringType),
-                  factory.initMethodName),
+                  factory.constructorMethodName),
               false));
       instructions.add(new CfThrow());
       return standardCfCodeFromInstructions(instructions);
diff --git a/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java b/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java
index 75358f7..d21c6d9 100644
--- a/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java
+++ b/src/main/java/com/android/tools/r8/ir/synthetic/EnumUnboxingCfCodeProvider.java
@@ -28,7 +28,6 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexMethod;
 import com.android.tools.r8.graph.DexType;
-import com.android.tools.r8.graph.EnumValueInfoMapCollection.EnumValueInfoMap;
 import com.android.tools.r8.ir.analysis.value.AbstractValue;
 import com.android.tools.r8.ir.code.If;
 import com.android.tools.r8.ir.code.ValueType;
@@ -64,7 +63,6 @@
   public static class EnumUnboxingInstanceFieldCfCodeProvider extends EnumUnboxingCfCodeProvider {
 
     private final DexType returnType;
-    private final EnumValueInfoMap enumValueInfoMap;
     private final EnumInstanceFieldMappingData fieldDataMap;
     private final AbstractValue nullValue;
 
@@ -72,12 +70,10 @@
         AppView<?> appView,
         DexType holder,
         DexType returnType,
-        EnumValueInfoMap enumValueInfoMap,
         EnumInstanceFieldMappingData fieldDataMap,
         AbstractValue nullValue) {
       super(appView, holder);
       this.returnType = returnType;
-      this.enumValueInfoMap = enumValueInfoMap;
       this.fieldDataMap = fieldDataMap;
       this.nullValue = nullValue;
     }
@@ -101,19 +97,16 @@
 
       // if (i == 1) { return 10;}
       // if (i == 2) { return 20;}
-      enumValueInfoMap.forEach(
-          (field, enumValueInfo) -> {
-            AbstractValue value = fieldDataMap.getData(field);
-            if (value != null) {
-              CfLabel dest = new CfLabel();
-              instructions.add(new CfLoad(ValueType.fromDexType(factory.intType), 0));
-              instructions.add(new CfConstNumber(enumValueInfo.convertToInt(), ValueType.INT));
-              instructions.add(new CfIfCmp(If.Type.NE, ValueType.INT, dest));
-              addCfInstructionsForAbstractValue(instructions, value, returnType);
-              instructions.add(new CfReturn(ValueType.fromDexType(returnType)));
-              instructions.add(dest);
-              instructions.add(new CfFrame(locals, ImmutableDeque.of()));
-            }
+      fieldDataMap.forEach(
+          (unboxedEnumValue, value) -> {
+            CfLabel dest = new CfLabel();
+            instructions.add(new CfLoad(ValueType.fromDexType(factory.intType), 0));
+            instructions.add(new CfConstNumber(unboxedEnumValue, ValueType.INT));
+            instructions.add(new CfIfCmp(If.Type.NE, ValueType.INT, dest));
+            addCfInstructionsForAbstractValue(instructions, value, returnType);
+            instructions.add(new CfReturn(ValueType.fromDexType(returnType)));
+            instructions.add(dest);
+            instructions.add(new CfFrame(locals, ImmutableDeque.of()));
           });
 
       if (nullValue != null) {
@@ -132,19 +125,16 @@
 
   public static class EnumUnboxingValueOfCfCodeProvider extends EnumUnboxingCfCodeProvider {
 
-    private DexType enumType;
-    private EnumValueInfoMap map;
+    private final DexType enumType;
     private final EnumInstanceFieldMappingData fieldDataMap;
 
     public EnumUnboxingValueOfCfCodeProvider(
         AppView<?> appView,
         DexType holder,
         DexType enumType,
-        EnumValueInfoMap map,
         EnumInstanceFieldMappingData fieldDataMap) {
       super(appView, holder);
       this.enumType = enumType;
-      this.map = map;
       this.fieldDataMap = fieldDataMap;
     }
 
@@ -180,16 +170,15 @@
 
       // if (s.equals("A")) { return 1;}
       // if (s.equals("B")) { return 2;}
-      map.forEach(
-          (field, enumValueInfo) -> {
+      fieldDataMap.forEach(
+          (unboxedEnumValue, value) -> {
             CfLabel dest = new CfLabel();
             instructions.add(new CfLoad(ValueType.fromDexType(factory.stringType), 0));
-            AbstractValue value = fieldDataMap.getData(field);
             addCfInstructionsForAbstractValue(instructions, value, factory.stringType);
             instructions.add(
                 new CfInvoke(Opcodes.INVOKEVIRTUAL, factory.stringMembers.equals, false));
             instructions.add(new CfIf(If.Type.EQ, ValueType.INT, dest));
-            instructions.add(new CfConstNumber(enumValueInfo.convertToInt(), ValueType.INT));
+            instructions.add(new CfConstNumber(unboxedEnumValue, ValueType.INT));
             instructions.add(new CfReturn(ValueType.INT));
             instructions.add(dest);
             instructions.add(new CfFrame(locals, ImmutableDeque.of()));
diff --git a/src/main/java/com/android/tools/r8/optimize/PublicizerLens.java b/src/main/java/com/android/tools/r8/optimize/PublicizerLens.java
index bd8db04..8ea4e1f 100644
--- a/src/main/java/com/android/tools/r8/optimize/PublicizerLens.java
+++ b/src/main/java/com/android/tools/r8/optimize/PublicizerLens.java
@@ -10,23 +10,22 @@
 import com.android.tools.r8.graph.GraphLens;
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
 import com.android.tools.r8.ir.code.Invoke.Type;
-import com.android.tools.r8.utils.collections.BidirectionalManyToManyRepresentativeMap;
+import com.android.tools.r8.utils.collections.EmptyBidirectionalOneToOneMap;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Sets;
 import java.util.Set;
 
 final class PublicizerLens extends NestedGraphLens {
 
-  private final AppView appView;
+  private final AppView<?> appView;
   private final Set<DexMethod> publicizedMethods;
 
-  private PublicizerLens(AppView appView, Set<DexMethod> publicizedMethods) {
+  private PublicizerLens(AppView<?> appView, Set<DexMethod> publicizedMethods) {
     super(
         ImmutableMap.of(),
         ImmutableMap.of(),
-        ImmutableMap.of(),
-        null,
-        BidirectionalManyToManyRepresentativeMap.empty(),
+        new EmptyBidirectionalOneToOneMap<>(),
+        new EmptyBidirectionalOneToOneMap<>(),
         appView.graphLens(),
         appView.dexItemFactory());
     this.appView = appView;
diff --git a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoistingResult.java b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoistingResult.java
index 09a9b27..9353111 100644
--- a/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoistingResult.java
+++ b/src/main/java/com/android/tools/r8/optimize/bridgehoisting/BridgeHoistingResult.java
@@ -11,7 +11,8 @@
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.ir.optimize.info.bridge.BridgeInfo;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
-import com.android.tools.r8.utils.collections.BidirectionalManyToOneMap;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneMap;
 import java.util.Set;
 import java.util.function.BiConsumer;
 
@@ -20,8 +21,8 @@
   private final AppView<AppInfoWithLiveness> appView;
 
   // Mapping from non-hoisted bridge methods to hoisted bridge methods.
-  private final BidirectionalManyToOneMap<DexMethod, DexMethod> bridgeToHoistedBridgeMap =
-      new BidirectionalManyToOneMap<>();
+  private final MutableBidirectionalManyToOneMap<DexMethod, DexMethod> bridgeToHoistedBridgeMap =
+      new BidirectionalManyToOneHashMap<>();
 
   // Mapping from non-hoisted bridge methods to the set of contexts in which they are accessed.
   private final MethodAccessInfoCollection.IdentityBuilder bridgeMethodAccessInfoCollectionBuilder =
@@ -32,7 +33,7 @@
   }
 
   public void forEachHoistedBridge(BiConsumer<ProgramMethod, BridgeInfo> consumer) {
-    bridgeToHoistedBridgeMap.forEach(
+    bridgeToHoistedBridgeMap.forEachManyToOneMapping(
         (bridges, hoistedBridge) -> {
           DexProgramClass clazz = appView.definitionForProgramType(hoistedBridge.getHolderType());
           ProgramMethod method = hoistedBridge.lookupOnProgramClass(clazz);
diff --git a/src/main/java/com/android/tools/r8/repackaging/RepackagingLens.java b/src/main/java/com/android/tools/r8/repackaging/RepackagingLens.java
index b871193..0fe63b8 100644
--- a/src/main/java/com/android/tools/r8/repackaging/RepackagingLens.java
+++ b/src/main/java/com/android/tools/r8/repackaging/RepackagingLens.java
@@ -11,6 +11,8 @@
 import com.android.tools.r8.graph.GraphLens.NestedGraphLens;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
 import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
 
@@ -20,14 +22,13 @@
 
   private RepackagingLens(
       AppView<AppInfoWithLiveness> appView,
-      BiMap<DexField, DexField> originalFieldSignatures,
-      BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures,
+      BidirectionalOneToOneMap<DexField, DexField> newFieldSignatures,
+      BidirectionalOneToOneMap<DexMethod, DexMethod> originalMethodSignatures,
       BiMap<DexType, DexType> originalTypes) {
     super(
         originalTypes.inverse(),
-        originalMethodSignatures.getInverseBacking(),
-        originalFieldSignatures.inverse(),
-        originalFieldSignatures,
+        originalMethodSignatures.getInverseOneToOneMap().getForwardMap(),
+        newFieldSignatures,
         originalMethodSignatures,
         appView.graphLens(),
         appView.dexItemFactory());
@@ -48,12 +49,13 @@
   public static class Builder {
 
     protected final BiMap<DexType, DexType> originalTypes = HashBiMap.create();
-    protected final BiMap<DexField, DexField> originalFieldSignatures = HashBiMap.create();
-    protected final BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures =
+    protected final MutableBidirectionalOneToOneMap<DexField, DexField> newFieldSignatures =
+        new BidirectionalOneToOneHashMap<>();
+    protected final MutableBidirectionalOneToOneMap<DexMethod, DexMethod> originalMethodSignatures =
         new BidirectionalOneToOneHashMap<>();
 
     public void recordMove(DexField from, DexField to) {
-      originalFieldSignatures.put(to, from);
+      newFieldSignatures.put(from, to);
     }
 
     public void recordMove(DexMethod from, DexMethod to) {
@@ -67,7 +69,7 @@
     public RepackagingLens build(AppView<AppInfoWithLiveness> appView) {
       assert !originalTypes.isEmpty();
       return new RepackagingLens(
-          appView, originalFieldSignatures, originalMethodSignatures, originalTypes);
+          appView, newFieldSignatures, originalMethodSignatures, originalTypes);
     }
   }
 }
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 c6a2424..17d06a3 100644
--- a/src/main/java/com/android/tools/r8/repackaging/RepackagingTreeFixer.java
+++ b/src/main/java/com/android/tools/r8/repackaging/RepackagingTreeFixer.java
@@ -81,7 +81,7 @@
             DexEncodedMethod.EMPTY_ARRAY,
             DexEncodedMethod.EMPTY_ARRAY,
             dexItemFactory.getSkipNameValidationForTesting(),
-            DexProgramClass::checksumFromType,
+            clazz.getChecksumSupplier(),
             fixupSynthesizedFrom(clazz.getSynthesizedFrom()));
     newClass.setInstanceFields(fixupFields(clazz.instanceFields()));
     newClass.setStaticFields(fixupFields(clazz.staticFields()));
diff --git a/src/main/java/com/android/tools/r8/repackaging/RepackagingUseRegistry.java b/src/main/java/com/android/tools/r8/repackaging/RepackagingUseRegistry.java
index 91484f7..9d5cec5 100644
--- a/src/main/java/com/android/tools/r8/repackaging/RepackagingUseRegistry.java
+++ b/src/main/java/com/android/tools/r8/repackaging/RepackagingUseRegistry.java
@@ -25,12 +25,14 @@
 import com.android.tools.r8.graph.SuccessfulMemberResolutionResult;
 import com.android.tools.r8.graph.UseRegistry;
 import com.android.tools.r8.shaking.AppInfoWithLiveness;
+import com.android.tools.r8.utils.InternalOptions;
 import java.util.function.Consumer;
 import java.util.function.Predicate;
 
 public class RepackagingUseRegistry extends UseRegistry {
 
   private final AppInfoWithLiveness appInfo;
+  private final InternalOptions options;
   private final RepackagingConstraintGraph constraintGraph;
   private final ProgramDefinition context;
   private final InitClassLens initClassLens;
@@ -42,6 +44,7 @@
       ProgramDefinition context) {
     super(appView.dexItemFactory());
     this.appInfo = appView.appInfo();
+    this.options = appView.options();
     this.constraintGraph = constraintGraph;
     this.context = context;
     this.initClassLens = appView.initClassLens();
@@ -60,31 +63,51 @@
     return false;
   }
 
-  private boolean isOnlyAccessibleFromSamePackage(DexClassAndMember<?, ?> member) {
-    AccessFlags<?> accessFlags = member.getAccessFlags();
+  private boolean isOnlyAccessibleFromSamePackage(
+      SuccessfulMemberResolutionResult<?, ?> resolutionResult, boolean isInvoke) {
+    AccessFlags<?> accessFlags = resolutionResult.getResolutionPair().getAccessFlags();
     if (accessFlags.isPackagePrivate()) {
       return true;
     }
-    if (accessFlags.isProtected()
-        && !appInfo.isSubtype(context.getContextType(), member.getHolderType())) {
-      return true;
+    if (accessFlags.isProtected()) {
+      if (!appInfo.isSubtype(
+          context.getContextType(), resolutionResult.getResolvedHolder().getType())) {
+        return true;
+      }
+      // Check for assignability if we are generating CF:
+      // https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.10.1.8
+      if (isInvoke
+          && options.isGeneratingClassFiles()
+          && !appInfo.isSubtype(
+              resolutionResult.getInitialResolutionHolder().getType(), context.getContextType())) {
+        return true;
+      }
     }
     return false;
   }
 
   public void registerFieldAccess(DexField field) {
-    registerMemberAccess(appInfo.resolveField(field));
+    registerMemberAccess(appInfo.resolveField(field), false);
   }
 
   public ProgramMethod registerMethodReference(DexMethod method) {
     ResolutionResult resolutionResult = appInfo.unsafeResolveMethodDueToDexFormat(method);
-    registerMemberAccess(resolutionResult);
+    registerMemberAccess(resolutionResult, false);
     return resolutionResult.isSingleResolution()
         ? resolutionResult.asSingleResolution().getResolvedProgramMethod()
         : null;
   }
 
+  private void registerMemberAccessForInvoke(MemberResolutionResult<?, ?> resolutionResult) {
+    registerMemberAccess(resolutionResult, true);
+  }
+
   public void registerMemberAccess(MemberResolutionResult<?, ?> resolutionResult) {
+    registerMemberAccess(resolutionResult, false);
+  }
+
+  private void registerMemberAccess(
+      MemberResolutionResult<?, ?> resolutionResult, boolean isInvoke) {
     SuccessfulMemberResolutionResult<?, ?> successfulResolutionResult =
         resolutionResult.asSuccessfulMemberResolutionResult();
     if (successfulResolutionResult == null) {
@@ -96,14 +119,16 @@
     }
 
     // Check access to the initial resolution holder.
-    registerClassTypeAccess(successfulResolutionResult.getInitialResolutionHolder());
+    DexClass initialResolutionHolder = successfulResolutionResult.getInitialResolutionHolder();
+    registerClassTypeAccess(initialResolutionHolder);
 
     // Similarly, check access to the resolved member.
     DexClassAndMember<?, ?> resolutionPair = successfulResolutionResult.getResolutionPair();
     if (resolutionPair != null) {
       RepackagingConstraintGraph.Node resolvedMemberNode =
           constraintGraph.getNode(resolutionPair.getDefinition());
-      if (resolvedMemberNode != null && isOnlyAccessibleFromSamePackage(resolutionPair)) {
+      if (resolvedMemberNode != null
+          && isOnlyAccessibleFromSamePackage(successfulResolutionResult, isInvoke)) {
         node.addNeighbor(resolvedMemberNode);
       }
     }
@@ -149,27 +174,27 @@
 
   @Override
   public void registerInvokeVirtual(DexMethod invokedMethod) {
-    registerMemberAccess(appInfo.resolveMethod(invokedMethod, false));
+    registerMemberAccessForInvoke(appInfo.resolveMethod(invokedMethod, false));
   }
 
   @Override
   public void registerInvokeDirect(DexMethod invokedMethod) {
-    registerMemberAccess(appInfo.unsafeResolveMethodDueToDexFormat(invokedMethod));
+    registerMemberAccessForInvoke(appInfo.unsafeResolveMethodDueToDexFormat(invokedMethod));
   }
 
   @Override
   public void registerInvokeStatic(DexMethod invokedMethod) {
-    registerMemberAccess(appInfo.unsafeResolveMethodDueToDexFormat(invokedMethod));
+    registerMemberAccessForInvoke(appInfo.unsafeResolveMethodDueToDexFormat(invokedMethod));
   }
 
   @Override
   public void registerInvokeInterface(DexMethod invokedMethod) {
-    registerMemberAccess(appInfo.resolveMethod(invokedMethod, true));
+    registerMemberAccessForInvoke(appInfo.resolveMethod(invokedMethod, true));
   }
 
   @Override
   public void registerInvokeSuper(DexMethod invokedMethod) {
-    registerMemberAccess(appInfo.unsafeResolveMethodDueToDexFormat(invokedMethod));
+    registerMemberAccessForInvoke(appInfo.unsafeResolveMethodDueToDexFormat(invokedMethod));
   }
 
   @Override
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 7687ad4..7f93d17 100644
--- a/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
+++ b/src/main/java/com/android/tools/r8/shaking/AppInfoWithLiveness.java
@@ -25,8 +25,6 @@
 import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.DirectMappedDexApplication;
-import com.android.tools.r8.graph.EnumValueInfoMapCollection;
-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;
@@ -40,6 +38,7 @@
 import com.android.tools.r8.graph.ObjectAllocationInfoCollectionImpl;
 import com.android.tools.r8.graph.ProgramField;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.graph.ResolutionResult.SingleResolutionResult;
 import com.android.tools.r8.graph.SubtypingInfo;
 import com.android.tools.r8.ir.analysis.type.ClassTypeElement;
@@ -181,8 +180,6 @@
   final Set<DexType> prunedTypes;
   /** A map from switchmap class types to their corresponding switchmaps. */
   final Map<DexField, Int2ReferenceMap<DexField>> switchMaps;
-  /** A map from enum types to their value types and ordinals. */
-  final EnumValueInfoMapCollection enumValueInfoMaps;
 
   /* A cache to improve the lookup performance of lookupSingleVirtualTarget */
   private final SingleTargetLookupCache singleTargetLookupCache = new SingleTargetLookupCache();
@@ -227,7 +224,6 @@
       Object2BooleanMap<DexReference> identifierNameStrings,
       Set<DexType> prunedTypes,
       Map<DexField, Int2ReferenceMap<DexField>> switchMaps,
-      EnumValueInfoMapCollection enumValueInfoMaps,
       Set<DexType> lockCandidates,
       Map<DexType, Visibility> initClassReferences) {
     super(syntheticItems, classToFeatureSplitMap, mainDexClasses);
@@ -266,9 +262,9 @@
     this.identifierNameStrings = identifierNameStrings;
     this.prunedTypes = prunedTypes;
     this.switchMaps = switchMaps;
-    this.enumValueInfoMaps = enumValueInfoMaps;
     this.lockCandidates = lockCandidates;
     this.initClassReferences = initClassReferences;
+    verify();
   }
 
   private AppInfoWithLiveness(
@@ -313,25 +309,20 @@
         previous.identifierNameStrings,
         previous.prunedTypes,
         previous.switchMaps,
-        previous.enumValueInfoMaps,
         previous.lockCandidates,
         previous.initClassReferences);
   }
 
-  private AppInfoWithLiveness(
-      AppInfoWithLiveness previous,
-      DirectMappedDexApplication application,
-      Set<DexType> removedClasses,
-      Collection<? extends DexReference> additionalPinnedItems) {
+  private AppInfoWithLiveness(AppInfoWithLiveness previous, PrunedItems prunedItems) {
     this(
-        previous.getSyntheticItems().commitPrunedClasses(application, removedClasses),
-        previous.getClassToFeatureSplitMap().withoutPrunedClasses(removedClasses),
-        previous.getMainDexClasses().withoutPrunedClasses(removedClasses),
+        previous.getSyntheticItems().commitPrunedItems(prunedItems),
+        previous.getClassToFeatureSplitMap().withoutPrunedItems(prunedItems),
+        previous.getMainDexClasses().withoutPrunedItems(prunedItems),
         previous.deadProtoTypes,
         previous.missingTypes,
-        removedClasses == null
-            ? previous.liveTypes
-            : Sets.difference(previous.liveTypes, removedClasses),
+        prunedItems.hasRemovedClasses()
+            ? Sets.difference(previous.liveTypes, prunedItems.getRemovedClasses())
+            : previous.liveTypes,
         previous.targetedMethods,
         previous.failedResolutionTargets,
         previous.bootstrapMethods,
@@ -342,7 +333,7 @@
         previous.methodAccessInfoCollection,
         previous.objectAllocationInfoCollection,
         previous.callSites,
-        extendPinnedItems(previous, additionalPinnedItems),
+        extendPinnedItems(previous, prunedItems.getAdditionalPinnedItems()),
         previous.mayHaveSideEffects,
         previous.noSideEffects,
         previous.assumedValues,
@@ -362,14 +353,17 @@
         previous.noStaticClassMerging,
         previous.neverPropagateValue,
         previous.identifierNameStrings,
-        removedClasses == null
-            ? previous.prunedTypes
-            : CollectionUtils.mergeSets(previous.prunedTypes, removedClasses),
+        prunedItems.hasRemovedClasses()
+            ? CollectionUtils.mergeSets(previous.prunedTypes, prunedItems.getRemovedClasses())
+            : previous.prunedTypes,
         previous.switchMaps,
-        previous.enumValueInfoMaps,
         previous.lockCandidates,
         previous.initClassReferences);
-    assert keepInfo.verifyNoneArePinned(removedClasses, previous);
+  }
+
+  private void verify() {
+    assert keepInfo.verifyPinnedTypesAreLive(liveTypes);
+    assert objectAllocationInfoCollection.verifyAllocatedTypesAreLive(liveTypes, this);
   }
 
   private static KeepInfoCollection extendPinnedItems(
@@ -410,9 +404,7 @@
   }
 
   public AppInfoWithLiveness(
-      AppInfoWithLiveness previous,
-      Map<DexField, Int2ReferenceMap<DexField>> switchMaps,
-      EnumValueInfoMapCollection enumValueInfoMaps) {
+      AppInfoWithLiveness previous, Map<DexField, Int2ReferenceMap<DexField>> switchMaps) {
     super(
         previous.getSyntheticItems().commit(previous.app()),
         previous.getClassToFeatureSplitMap(),
@@ -452,10 +444,10 @@
     this.identifierNameStrings = previous.identifierNameStrings;
     this.prunedTypes = previous.prunedTypes;
     this.switchMaps = switchMaps;
-    this.enumValueInfoMaps = enumValueInfoMaps;
     this.lockCandidates = previous.lockCandidates;
     this.initClassReferences = previous.initClassReferences;
     previous.markObsolete();
+    verify();
   }
 
   public static AppInfoWithLivenessModifier modifier() {
@@ -693,16 +685,6 @@
     return missingTypes;
   }
 
-  public EnumValueInfoMapCollection getEnumValueInfoMapCollection() {
-    assert checkIfObsolete();
-    return enumValueInfoMaps;
-  }
-
-  public EnumValueInfoMap getEnumValueInfoMap(DexType enumType) {
-    assert checkIfObsolete();
-    return enumValueInfoMaps.getEnumValueInfoMap(enumType);
-  }
-
   public Int2ReferenceMap<DexField> getSwitchMap(DexField field) {
     assert checkIfObsolete();
     return switchMaps.get(field);
@@ -803,6 +785,13 @@
     if (isPinned(field.field)) {
       return false;
     }
+    return isFieldOnlyWrittenInMethodIgnoringPinning(field, method);
+  }
+
+  public boolean isFieldOnlyWrittenInMethodIgnoringPinning(
+      DexEncodedField field, DexEncodedMethod method) {
+    assert checkIfObsolete();
+    assert isFieldWritten(field) : "Expected field `" + field.toSourceString() + "` to be written";
     FieldAccessInfo fieldAccessInfo = getFieldAccessInfoCollection().get(field.field);
     return fieldAccessInfo != null
         && fieldAccessInfo.isWritten()
@@ -822,7 +811,10 @@
     DexType holder = field.getHolderType();
     return fieldAccessInfo.isWrittenOnlyInMethodSatisfying(
         method ->
-            method.getDefinition().isInstanceInitializer() && method.getHolderType() == holder);
+            method.getHolderType() == holder
+                && method
+                    .getDefinition()
+                    .isOrWillBeInlinedIntoInstanceInitializer(dexItemFactory()));
   }
 
   public boolean isStaticFieldWrittenOnlyInEnclosingStaticInitializer(DexEncodedField field) {
@@ -933,17 +925,16 @@
    * Returns a copy of this AppInfoWithLiveness where the set of classes is pruned using the given
    * DexApplication object.
    */
-  public AppInfoWithLiveness prunedCopyFrom(
-      DirectMappedDexApplication application,
-      Set<DexType> removedClasses,
-      Collection<? extends DexReference> additionalPinnedItems) {
+  public AppInfoWithLiveness prunedCopyFrom(PrunedItems prunedItems) {
     assert checkIfObsolete();
-    if (!removedClasses.isEmpty()) {
+    if (prunedItems.hasRemovedClasses()) {
       // Rebuild the hierarchy.
-      objectAllocationInfoCollection.mutate(mutator -> {}, this);
-      keepInfo.mutate(keepInfo -> keepInfo.removeKeepInfoForPrunedItems(removedClasses));
+      objectAllocationInfoCollection.mutate(
+          mutator -> mutator.removeAllocationsForPrunedItems(prunedItems), this);
+      keepInfo.mutate(
+          keepInfo -> keepInfo.removeKeepInfoForPrunedItems(prunedItems.getRemovedClasses()));
     }
-    return new AppInfoWithLiveness(this, application, removedClasses, additionalPinnedItems);
+    return new AppInfoWithLiveness(this, prunedItems);
   }
 
   public AppInfoWithLiveness rebuildWithLiveness(
@@ -1005,7 +996,6 @@
         // Don't rewrite pruned types - the removed types are identified by their original name.
         prunedTypes,
         lens.rewriteFieldKeys(switchMaps),
-        enumValueInfoMaps.rewrittenWithLens(lens),
         lens.rewriteTypes(lockCandidates),
         lens.rewriteTypeKeys(initClassReferences));
   }
@@ -1210,13 +1200,7 @@
   public AppInfoWithLiveness withSwitchMaps(Map<DexField, Int2ReferenceMap<DexField>> switchMaps) {
     assert checkIfObsolete();
     assert this.switchMaps.isEmpty();
-    return new AppInfoWithLiveness(this, switchMaps, enumValueInfoMaps);
-  }
-
-  public AppInfoWithLiveness withEnumValueInfoMaps(EnumValueInfoMapCollection enumValueInfoMaps) {
-    assert checkIfObsolete();
-    assert this.enumValueInfoMaps.isEmpty();
-    return new AppInfoWithLiveness(this, switchMaps, enumValueInfoMaps);
+    return new AppInfoWithLiveness(this, switchMaps);
   }
 
   /**
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 f6b5aa2..095ba07 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -47,7 +47,6 @@
 import com.android.tools.r8.graph.DirectMappedDexApplication;
 import com.android.tools.r8.graph.DirectMappedDexApplication.Builder;
 import com.android.tools.r8.graph.EnclosingMethodAttribute;
-import com.android.tools.r8.graph.EnumValueInfoMapCollection;
 import com.android.tools.r8.graph.FieldAccessInfoCollectionImpl;
 import com.android.tools.r8.graph.FieldAccessInfoImpl;
 import com.android.tools.r8.graph.FieldResolutionResult;
@@ -115,6 +114,7 @@
 import com.android.tools.r8.utils.Visibility;
 import com.android.tools.r8.utils.WorkList;
 import com.android.tools.r8.utils.collections.ProgramFieldSet;
+import com.android.tools.r8.utils.collections.ProgramMethodMap;
 import com.android.tools.r8.utils.collections.ProgramMethodSet;
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.ImmutableSet;
@@ -364,8 +364,8 @@
   private final DesugaredLibraryConversionWrapperAnalysis desugaredLibraryWrapperAnalysis;
   private final Map<DexType, Pair<LambdaClass, ProgramMethod>> lambdaClasses =
       new IdentityHashMap<>();
-  private final Map<DexEncodedMethod, Map<DexCallSite, LambdaClass>> lambdaCallSites =
-      new IdentityHashMap<>();
+  private final ProgramMethodMap<Map<DexCallSite, LambdaClass>> lambdaCallSites =
+      ProgramMethodMap.create();
   private final Map<DexMethod, ProgramMethod> methodsWithBackports = new IdentityHashMap<>();
   private final Set<DexProgramClass> classesWithSerializableLambdas = Sets.newIdentityHashSet();
 
@@ -821,6 +821,7 @@
   // Utility to avoid adding to the worklist if already live.
   private boolean enqueueMarkMethodLiveAction(ProgramMethod method, KeepReason reason) {
     if (liveMethods.add(method, reason)) {
+      assert !method.getDefinition().getOptimizationInfo().forceInline();
       workList.enqueueMarkMethodLiveAction(method, reason);
       return true;
     }
@@ -929,7 +930,7 @@
         LambdaClass lambdaClass = lambdaRewriter.getOrCreateLambdaClass(descriptor, context);
         lambdaClasses.put(lambdaClass.type, new Pair<>(lambdaClass, context));
         lambdaCallSites
-            .computeIfAbsent(contextMethod, k -> new IdentityHashMap<>())
+            .computeIfAbsent(context, k -> new IdentityHashMap<>())
             .put(callSite, lambdaClass);
         if (lambdaClass.descriptor.interfaces.contains(appView.dexItemFactory().serializableType)) {
           classesWithSerializableLambdas.add(context.getHolder());
@@ -3023,6 +3024,9 @@
     Map<DexType, Pair<DexProgramClass, ProgramMethod>> syntheticInstantiations =
         new IdentityHashMap<>();
 
+    ProgramMethodMap<Set<DexField>> syntheticStaticFieldReadsByContext =
+        ProgramMethodMap.createLinked();
+
     Map<DexMethod, ProgramMethod> liveMethods = new IdentityHashMap<>();
 
     Map<DexType, DexClasspathClass> syntheticClasspathClasses = new IdentityHashMap<>();
@@ -3099,6 +3103,10 @@
             InstantiationReason.SYNTHESIZED_CLASS,
             fakeReason);
       }
+      syntheticStaticFieldReadsByContext.forEach(
+          (context, fields) ->
+              fields.forEach(
+                  field -> enqueuer.workList.enqueueTraceStaticFieldRead(field, context)));
       for (ProgramMethod liveMethod : liveMethods.values()) {
         assert !enqueuer.targetedMethods.contains(liveMethod.getDefinition());
         enqueuer.markMethodAsTargeted(liveMethod, fakeReason);
@@ -3106,6 +3114,26 @@
       }
       enqueuer.liveNonProgramTypes.addAll(syntheticClasspathClasses.values());
     }
+
+    void registerStatelessLambdaInstanceFieldReads(
+        ProgramMethodMap<Map<DexCallSite, LambdaClass>> lambdaCallSites) {
+      lambdaCallSites.forEach(this::registerStatelessLambdaInstanceFieldReads);
+    }
+
+    private void registerStatelessLambdaInstanceFieldReads(
+        ProgramMethod context, Map<DexCallSite, LambdaClass> callSites) {
+      Set<DexField> syntheticStaticFieldReadsInContext = null;
+      for (LambdaClass lambdaClass : callSites.values()) {
+        if (lambdaClass.isStateless()) {
+          if (syntheticStaticFieldReadsInContext == null) {
+            syntheticStaticFieldReadsInContext =
+                syntheticStaticFieldReadsByContext.computeIfAbsent(
+                    context, ignore -> Sets.newLinkedHashSet());
+          }
+          syntheticStaticFieldReadsInContext.add(lambdaClass.lambdaField);
+        }
+      }
+    }
   }
 
   private void synthesize() {
@@ -3179,6 +3207,7 @@
 
     // Rewrite all of the invoke-dynamic instructions to lambda class instantiations.
     lambdaCallSites.forEach(this::rewriteLambdaCallSites);
+    additions.registerStatelessLambdaInstanceFieldReads(lambdaCallSites);
 
     // Remove all '$deserializeLambda$' methods which are not supported by desugaring.
     for (DexProgramClass clazz : classesWithSerializableLambdas) {
@@ -3307,7 +3336,6 @@
             joinIdentifierNameStrings(rootSet.identifierNameStrings, identifierNameStrings),
             Collections.emptySet(),
             Collections.emptyMap(),
-            EnumValueInfoMapCollection.empty(),
             lockCandidates,
             initClassReferences);
     appInfo.markObsolete();
@@ -3441,9 +3469,9 @@
   }
 
   private void rewriteLambdaCallSites(
-      DexEncodedMethod method, Map<DexCallSite, LambdaClass> callSites) {
+      ProgramMethod context, Map<DexCallSite, LambdaClass> callSites) {
     assert !callSites.isEmpty();
-    int replaced = LambdaRewriter.desugarLambdas(method, callSites::get);
+    int replaced = LambdaRewriter.desugarLambdas(context, callSites::get);
     assert replaced == callSites.size();
   }
 
diff --git a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
index 85a7b88..da98174 100644
--- a/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
+++ b/src/main/java/com/android/tools/r8/shaking/KeepInfoCollection.java
@@ -6,7 +6,6 @@
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 
 import com.android.tools.r8.errors.Unreachable;
-import com.android.tools.r8.graph.AppInfo;
 import com.android.tools.r8.graph.DexDefinitionSupplier;
 import com.android.tools.r8.graph.DexEncodedField;
 import com.android.tools.r8.graph.DexEncodedMethod;
@@ -23,7 +22,6 @@
 import com.android.tools.r8.utils.InternalOptions;
 import com.google.common.collect.Streams;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
@@ -155,14 +153,7 @@
         && getInfo(reference, definitions).isMinificationAllowed(configuration);
   }
 
-  public final boolean verifyNoneArePinned(Collection<DexType> types, AppInfo appInfo) {
-    for (DexType type : types) {
-      DexProgramClass clazz =
-          asProgramClassOrNull(appInfo.definitionForWithoutExistenceAssert(type));
-      assert clazz == null || !getClassInfo(clazz).isPinned();
-    }
-    return true;
-  }
+  public abstract boolean verifyPinnedTypesAreLive(Set<DexType> liveTypes);
 
   // TODO(b/156715504): We should try to avoid the need for iterating pinned items.
   @Deprecated
@@ -446,6 +437,15 @@
     }
 
     @Override
+    public boolean verifyPinnedTypesAreLive(Set<DexType> liveTypes) {
+      keepClassInfo.forEach(
+          (type, info) -> {
+            assert !info.isPinned() || liveTypes.contains(type);
+          });
+      return true;
+    }
+
+    @Override
     public void forEachPinnedType(Consumer<DexType> consumer) {
       keepClassInfo.forEach(
           (type, info) -> {
diff --git a/src/main/java/com/android/tools/r8/shaking/MainDexClasses.java b/src/main/java/com/android/tools/r8/shaking/MainDexClasses.java
index a5fbd32..4e6b632 100644
--- a/src/main/java/com/android/tools/r8/shaking/MainDexClasses.java
+++ b/src/main/java/com/android/tools/r8/shaking/MainDexClasses.java
@@ -7,6 +7,7 @@
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.graph.GraphLens;
+import com.android.tools.r8.graph.PrunedItems;
 import com.google.common.collect.Sets;
 import java.util.Set;
 import java.util.function.Consumer;
@@ -87,10 +88,10 @@
     return mainDexClasses.size();
   }
 
-  public MainDexClasses withoutPrunedClasses(Set<DexType> prunedClasses) {
+  public MainDexClasses withoutPrunedItems(PrunedItems prunedItems) {
     MainDexClasses mainDexClassesAfterPruning = createEmptyMainDexClasses();
     for (DexType mainDexClass : mainDexClasses) {
-      if (!prunedClasses.contains(mainDexClass)) {
+      if (!prunedItems.getRemovedClasses().contains(mainDexClass)) {
         mainDexClassesAfterPruning.mainDexClasses.add(mainDexClass);
       }
     }
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 58e85ff..33e64f0 100644
--- a/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/StaticClassMerger.java
@@ -24,10 +24,10 @@
 import com.android.tools.r8.utils.SingletonEquivalence;
 import com.android.tools.r8.utils.TraversalContinuation;
 import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
 import com.google.common.base.Equivalence;
 import com.google.common.base.Equivalence.Wrapper;
-import com.google.common.collect.BiMap;
-import com.google.common.collect.HashBiMap;
 import com.google.common.collect.HashMultiset;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Multiset.Entry;
@@ -199,8 +199,9 @@
 
   private final Map<MergeKey, Representative> representatives = new HashMap<>();
 
-  private final BiMap<DexField, DexField> fieldMapping = HashBiMap.create();
-  private final BidirectionalOneToOneHashMap<DexMethod, DexMethod> methodMapping =
+  private final MutableBidirectionalOneToOneMap<DexField, DexField> newFieldSignatures =
+      new BidirectionalOneToOneHashMap<>();
+  private final MutableBidirectionalOneToOneMap<DexMethod, DexMethod> methodMapping =
       new BidirectionalOneToOneHashMap<>();
 
   private int numberOfMergedClasses = 0;
@@ -222,27 +223,18 @@
 
   public NestedGraphLens run() {
     appView.appInfo().classesWithDeterministicOrder().forEach(this::merge);
-    if (Log.ENABLED) {
-      Log.info(
-          getClass(),
-          "Merged %s classes with %s members.",
-          numberOfMergedClasses,
-          fieldMapping.size() + methodMapping.size());
-    }
     appView.setStaticallyMergedClasses(mergedClassesBuilder.build());
     return buildGraphLens();
   }
 
   private NestedGraphLens buildGraphLens() {
-    if (!fieldMapping.isEmpty() || !methodMapping.isEmpty()) {
-      BiMap<DexField, DexField> originalFieldSignatures = fieldMapping.inverse();
-      BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures =
+    if (!newFieldSignatures.isEmpty() || !methodMapping.isEmpty()) {
+      BidirectionalOneToOneMap<DexMethod, DexMethod> originalMethodSignatures =
           methodMapping.getInverseOneToOneMap();
       return new NestedGraphLens(
           ImmutableMap.of(),
-          methodMapping,
-          fieldMapping,
-          originalFieldSignatures,
+          methodMapping.getForwardMap(),
+          newFieldSignatures,
           originalMethodSignatures,
           appView.graphLens(),
           appView.dexItemFactory());
@@ -501,7 +493,7 @@
 
       DexMethod originalMethod =
           methodMapping.getRepresentativeKeyOrDefault(sourceMethod.method, sourceMethod.method);
-      methodMapping.forcePut(originalMethod, sourceMethodAfterMove.method);
+      methodMapping.put(originalMethod, sourceMethodAfterMove.method);
 
       existingMethods.add(equivalence.wrap(sourceMethodAfterMove.method));
     }
@@ -536,8 +528,8 @@
       result[index++] = sourceFieldAfterMove;
 
       DexField originalField =
-          fieldMapping.inverse().getOrDefault(sourceField.field, sourceField.field);
-      fieldMapping.forcePut(originalField, sourceFieldAfterMove.field);
+          newFieldSignatures.getRepresentativeKeyOrDefault(sourceField.field, sourceField.field);
+      newFieldSignatures.put(originalField, sourceFieldAfterMove.field);
 
       existingFields.add(equivalence.wrap(sourceFieldAfterMove.field));
     }
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 434b5d2..6f7d998 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMerger.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.shaking;
 
+import static com.android.tools.r8.dex.Constants.TEMPORARY_INSTANCE_INITIALIZER_PREFIX;
 import static com.android.tools.r8.graph.DexProgramClass.asProgramClassOrNull;
 import static com.android.tools.r8.ir.code.Invoke.Type.DIRECT;
 import static com.android.tools.r8.ir.code.Invoke.Type.STATIC;
@@ -58,7 +59,8 @@
 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.android.tools.r8.utils.collections.BidirectionalManyToOneHashMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalManyToOneMap;
 import com.google.common.base.Equivalence;
 import com.google.common.base.Equivalence.Wrapper;
 import com.google.common.collect.Iterables;
@@ -206,8 +208,8 @@
   private final Set<DexProgramClass> mergeCandidates = new LinkedHashSet<>();
 
   // Map from source class to target class.
-  private final BidirectionalManyToOneMap<DexType, DexType> mergedClasses =
-      new BidirectionalManyToOneMap<>();
+  private final MutableBidirectionalManyToOneMap<DexType, DexType> mergedClasses =
+      new BidirectionalManyToOneHashMap<>();
 
   // Set of types that must not be merged into their subtype.
   private final Set<DexType> pinnedTypes = Sets.newIdentityHashSet();
@@ -901,8 +903,6 @@
 
   private class ClassMerger {
 
-    private static final String CONSTRUCTOR_NAME = "constructor";
-
     private final DexProgramClass source;
     private final DexProgramClass target;
     private final VerticalClassMergerGraphLens.Builder deferredRenamings =
@@ -1365,7 +1365,7 @@
       DexMethod newSignature;
       int count = 1;
       do {
-        DexString newName = getFreshName(CONSTRUCTOR_NAME, count, oldHolder);
+        DexString newName = getFreshName(TEMPORARY_INSTANCE_INITIALIZER_PREFIX, count, oldHolder);
         newSignature =
             application.dexItemFactory.createMethod(target.type, method.method.proto, newName);
         count++;
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 f634f48..008380c 100644
--- a/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
+++ b/src/main/java/com/android/tools/r8/shaking/VerticalClassMergerGraphLens.java
@@ -17,9 +17,10 @@
 import com.android.tools.r8.graph.classmerging.VerticallyMergedClasses;
 import com.android.tools.r8.ir.code.Invoke.Type;
 import com.android.tools.r8.utils.IterableUtils;
+import com.android.tools.r8.utils.collections.BidirectionalManyToOneRepresentativeMap;
 import com.android.tools.r8.utils.collections.BidirectionalOneToOneHashMap;
-import com.google.common.collect.BiMap;
-import com.google.common.collect.HashBiMap;
+import com.android.tools.r8.utils.collections.BidirectionalOneToOneMap;
+import com.android.tools.r8.utils.collections.MutableBidirectionalOneToOneMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterables;
 import java.util.Collection;
@@ -69,20 +70,18 @@
   private VerticalClassMergerGraphLens(
       AppView<?> appView,
       VerticallyMergedClasses mergedClasses,
-      Map<DexField, DexField> fieldMap,
+      BidirectionalManyToOneRepresentativeMap<DexField, DexField> fieldMap,
       Map<DexMethod, DexMethod> methodMap,
       Set<DexMethod> mergedMethods,
       Map<DexType, Map<DexMethod, GraphLensLookupResultProvider>>
           contextualVirtualToDirectMethodMaps,
-      BiMap<DexField, DexField> originalFieldSignatures,
-      BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures,
+      BidirectionalOneToOneMap<DexMethod, DexMethod> originalMethodSignatures,
       Map<DexMethod, DexMethod> originalMethodSignaturesForBridges,
       GraphLens previousLens) {
     super(
         mergedClasses.getForwardMap(),
         methodMap,
         fieldMap,
-        originalFieldSignatures,
         originalMethodSignatures,
         previousLens,
         appView.dexItemFactory());
@@ -166,13 +165,14 @@
 
     private final DexItemFactory dexItemFactory;
 
-    protected final BiMap<DexField, DexField> fieldMap = HashBiMap.create();
+    protected final MutableBidirectionalOneToOneMap<DexField, DexField> fieldMap =
+        new BidirectionalOneToOneHashMap<>();
     protected final Map<DexMethod, DexMethod> methodMap = new IdentityHashMap<>();
     private final ImmutableSet.Builder<DexMethod> mergedMethodsBuilder = ImmutableSet.builder();
     private final Map<DexType, Map<DexMethod, GraphLensLookupResultProvider>>
         contextualVirtualToDirectMethodMaps = new IdentityHashMap<>();
 
-    private final BidirectionalOneToOneHashMap<DexMethod, DexMethod> originalMethodSignatures =
+    private final MutableBidirectionalOneToOneMap<DexMethod, DexMethod> originalMethodSignatures =
         new BidirectionalOneToOneHashMap<>();
     private final Map<DexMethod, DexMethod> originalMethodSignaturesForBridges =
         new IdentityHashMap<>();
@@ -185,11 +185,10 @@
 
     static Builder createBuilderForFixup(Builder builder, VerticallyMergedClasses mergedClasses) {
       Builder newBuilder = new Builder(builder.dexItemFactory);
-      for (Map.Entry<DexField, DexField> entry : builder.fieldMap.entrySet()) {
-        newBuilder.map(
-            entry.getKey(),
-            builder.getFieldSignatureAfterClassMerging(entry.getValue(), mergedClasses));
-      }
+      builder.fieldMap.forEach(
+          (key, value) ->
+              newBuilder.map(
+                  key, builder.getFieldSignatureAfterClassMerging(value, mergedClasses)));
       for (Map.Entry<DexMethod, DexMethod> entry : builder.methodMap.entrySet()) {
         newBuilder.map(
             entry.getKey(),
@@ -237,7 +236,6 @@
       if (mergedClasses.isEmpty()) {
         return null;
       }
-      BiMap<DexField, DexField> originalFieldSignatures = fieldMap.inverse();
       // Build new graph lens.
       return new VerticalClassMergerGraphLens(
           appView,
@@ -246,7 +244,6 @@
           methodMap,
           mergedMethodsBuilder.build(),
           contextualVirtualToDirectMethodMaps,
-          originalFieldSignatures,
           originalMethodSignatures,
           originalMethodSignaturesForBridges,
           appView.graphLens());
@@ -308,7 +305,7 @@
     }
 
     public boolean hasOriginalSignatureMappingFor(DexField field) {
-      return fieldMap.inverse().containsKey(field);
+      return fieldMap.containsValue(field);
     }
 
     public boolean hasOriginalSignatureMappingFor(DexMethod method) {
diff --git a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
index dcdaafc..bc7d42e 100644
--- a/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
+++ b/src/main/java/com/android/tools/r8/synthesis/SyntheticItems.java
@@ -18,6 +18,7 @@
 import com.android.tools.r8.graph.GraphLens.NonIdentityGraphLens;
 import com.android.tools.r8.graph.ProgramDefinition;
 import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.PrunedItems;
 import com.android.tools.r8.ir.conversion.MethodProcessingId;
 import com.android.tools.r8.synthesis.SyntheticFinalization.Result;
 import com.google.common.collect.ImmutableList;
@@ -223,10 +224,18 @@
     return nonLecacySyntheticItems.containsKey(type) || legacySyntheticTypes.contains(type);
   }
 
+  private boolean isLegacyCommittedSynthetic(DexType type) {
+    return legacySyntheticTypes.contains(type);
+  }
+
   public boolean isPendingSynthetic(DexType type) {
     return pendingDefinitions.containsKey(type) || legacyPendingClasses.containsKey(type);
   }
 
+  public boolean isLegacyPendingSynthetic(DexType type) {
+    return legacyPendingClasses.containsKey(type);
+  }
+
   public boolean isSyntheticClass(DexType type) {
     return isCommittedSynthetic(type)
         || isPendingSynthetic(type)
@@ -238,6 +247,14 @@
     return isSyntheticClass(clazz.type);
   }
 
+  public boolean isLegacySyntheticClass(DexType type) {
+    return isLegacyCommittedSynthetic(type) || isLegacyPendingSynthetic(type);
+  }
+
+  public boolean isLegacySyntheticClass(DexProgramClass clazz) {
+    return isLegacySyntheticClass(clazz.getType());
+  }
+
   public Collection<DexProgramClass> getLegacyPendingClasses() {
     return Collections.unmodifiableCollection(legacyPendingClasses.values());
   }
@@ -301,14 +318,13 @@
   // Commit of the synthetic items to a new fully populated application.
 
   public CommittedItems commit(DexApplication application) {
-    return commitPrunedClasses(application, Collections.emptySet());
+    return commitPrunedItems(PrunedItems.empty(application));
   }
 
-  public CommittedItems commitPrunedClasses(
-      DexApplication application, Set<DexType> removedClasses) {
+  public CommittedItems commitPrunedItems(PrunedItems prunedItems) {
     return commit(
-        application,
-        removedClasses,
+        prunedItems.getPrunedApp(),
+        prunedItems.getNoLongerSyntheticItems(),
         legacyPendingClasses,
         legacySyntheticTypes,
         pendingDefinitions,
diff --git a/src/main/java/com/android/tools/r8/tracereferences/MissingDefinitionsDiagnostic.java b/src/main/java/com/android/tools/r8/tracereferences/MissingDefinitionsDiagnostic.java
index 12a2462..1d4e46a 100644
--- a/src/main/java/com/android/tools/r8/tracereferences/MissingDefinitionsDiagnostic.java
+++ b/src/main/java/com/android/tools/r8/tracereferences/MissingDefinitionsDiagnostic.java
@@ -82,12 +82,18 @@
     builder.append(" without definition");
     builder.append(System.lineSeparator());
     builder.append(System.lineSeparator());
-    builder.append("Classe(s) without definition:" + System.lineSeparator());
-    appendSorted(builder, missingClasses);
-    builder.append("Field(s) without definition:" + System.lineSeparator());
-    appendSorted(builder, missingFields);
-    builder.append("Method(s) without definition:" + System.lineSeparator());
-    appendSorted(builder, missingMethods);
+    if (missingClasses.size() > 0) {
+      builder.append("Classe(s) without definition:" + System.lineSeparator());
+      appendSorted(builder, missingClasses);
+    }
+    if (missingFields.size() > 0) {
+      builder.append("Field(s) without definition:" + System.lineSeparator());
+      appendSorted(builder, missingFields);
+    }
+    if (missingMethods.size() > 0) {
+      builder.append("Method(s) without definition:" + System.lineSeparator());
+      appendSorted(builder, missingMethods);
+    }
     return builder.toString();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/CfVersionUtils.java b/src/main/java/com/android/tools/r8/utils/CfVersionUtils.java
new file mode 100644
index 0000000..dadf460
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/CfVersionUtils.java
@@ -0,0 +1,23 @@
+// 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.utils;
+
+import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.utils.structural.Ordered;
+import java.util.List;
+
+public class CfVersionUtils {
+
+  public static CfVersion max(List<DexEncodedMethod> methods) {
+    CfVersion result = null;
+    for (DexEncodedMethod method : methods) {
+      if (method.hasClassFileVersion()) {
+        result = Ordered.maxIgnoreNull(result, method.getClassFileVersion());
+      }
+    }
+    return result;
+  }
+}
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 cd3a2db..175a39e 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -21,6 +21,7 @@
 import com.android.tools.r8.dex.Marker.Backend;
 import com.android.tools.r8.dex.Marker.Tool;
 import com.android.tools.r8.errors.CompilationError;
+import com.android.tools.r8.errors.ExperimentalClassFileVersionDiagnostic;
 import com.android.tools.r8.errors.IncompleteNestNestDesugarDiagnosic;
 import com.android.tools.r8.errors.InterfaceDesugarMissingTypeDiagnostic;
 import com.android.tools.r8.errors.InvalidDebugInfoException;
@@ -38,7 +39,6 @@
 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;
 import com.android.tools.r8.graph.ProgramMethod;
 import com.android.tools.r8.graph.classmerging.HorizontallyMergedLambdaClasses;
 import com.android.tools.r8.graph.classmerging.StaticallyMergedClasses;
@@ -49,6 +49,7 @@
 import com.android.tools.r8.ir.conversion.MethodProcessingId;
 import com.android.tools.r8.ir.desugar.DesugaredLibraryConfiguration;
 import com.android.tools.r8.ir.optimize.Inliner;
+import com.android.tools.r8.ir.optimize.enums.EnumDataMap;
 import com.android.tools.r8.ir.optimize.lambda.kotlin.KotlinLambdaGroupIdFactory;
 import com.android.tools.r8.origin.Origin;
 import com.android.tools.r8.position.Position;
@@ -108,11 +109,13 @@
     ON
   }
 
-  public static final CfVersion SUPPORTED_CF_VERSION = CfVersion.V11;
+  public static final CfVersion SUPPORTED_CF_VERSION = CfVersion.V15;
+  public static final CfVersion EXPERIMENTAL_CF_VERSION = CfVersion.V12;
+
   public static final int SUPPORTED_DEX_VERSION =
       AndroidApiLevel.LATEST.getDexVersion().getIntValue();
 
-  public static final int ASM_VERSION = Opcodes.ASM7;
+  public static final int ASM_VERSION = Opcodes.ASM9;
 
   public final DexItemFactory itemFactory;
 
@@ -202,7 +205,7 @@
     enableClassStaticizer = false;
     enableDevirtualization = false;
     enableLambdaMerging = false;
-    enableHorizontalClassMerging = false;
+    horizontalClassMergerOptions.disable();
     enableStaticClassMerging = false;
     enableVerticalClassMerging = false;
     enableEnumUnboxing = false;
@@ -238,11 +241,6 @@
   public boolean enableFieldBitAccessAnalysis =
       System.getProperty("com.android.tools.r8.fieldBitAccessAnalysis") != null;
   public boolean enableStaticClassMerging = true;
-  public boolean enableHorizontalClassMerging = true;
-  public boolean enableHorizontalClassMergingConstructorMerging = true;
-  public int horizontalClassMergingMaxGroupSize = 30;
-  public int horizontalClassMergingSyntheticArgumentCount = 3;
-  public boolean enableHorizontalClassMergingOfKotlinLambdas = true;
   public boolean enableVerticalClassMerging = true;
   public boolean enableArgumentRemoval = true;
   public boolean enableUnusedInterfaceRemoval = true;
@@ -582,7 +580,7 @@
    * and check cast instructions needs to be collected.
    */
   public boolean isClassMergingExtensionRequired() {
-    return enableHorizontalClassMerging || enableVerticalClassMerging;
+    return horizontalClassMergerOptions.isEnabled() || enableVerticalClassMerging;
   }
 
   @Override
@@ -615,6 +613,8 @@
 
   private final CallSiteOptimizationOptions callSiteOptimizationOptions =
       new CallSiteOptimizationOptions();
+  private final HorizontalClassMergerOptions horizontalClassMergerOptions =
+      new HorizontalClassMergerOptions();
   private final ProtoShrinkingOptions protoShrinking = new ProtoShrinkingOptions();
   private final KotlinOptimizationOptions kotlinOptimizationOptions =
       new KotlinOptimizationOptions();
@@ -638,6 +638,10 @@
     return callSiteOptimizationOptions;
   }
 
+  public HorizontalClassMergerOptions horizontalClassMergerOptions() {
+    return horizontalClassMergerOptions;
+  }
+
   public ProtoShrinkingOptions protoShrinking() {
     return protoShrinking;
   }
@@ -1043,6 +1047,23 @@
     reporter.warning(new StringDiagnostic(message.toString(), origin));
   }
 
+  private final Box<Boolean> reportedExperimentClassFileVersion = new Box<>(false);
+
+  public void warningExperimentalClassFileVersion(Origin origin) {
+    synchronized (reportedExperimentClassFileVersion) {
+      if (reportedExperimentClassFileVersion.get()) {
+        return;
+      }
+      reportedExperimentClassFileVersion.set(true);
+      reporter.warning(
+          new ExperimentalClassFileVersionDiagnostic(
+              origin,
+              "One or more classes has class file version >= "
+                  + EXPERIMENTAL_CF_VERSION.major()
+                  + " which is not officially supported."));
+    }
+  }
+
   public boolean printWarnings() {
     boolean printed = false;
     boolean printOutdatedToolchain = false;
@@ -1243,6 +1264,70 @@
     }
   }
 
+  public static class HorizontalClassMergerOptions {
+
+    public boolean enable = true;
+    public boolean enableConstructorMerging = true;
+    public boolean enableJavaLambdaMerging = false;
+    public boolean enableKotlinLambdaMerging = true;
+
+    public int syntheticArgumentCount = 3;
+    public int maxGroupSize = 30;
+
+    public void disable() {
+      enable = false;
+    }
+
+    @Deprecated
+    public void disableKotlinLambdaMerging() {
+      enableKotlinLambdaMerging = false;
+    }
+
+    public void enable() {
+      enable = true;
+    }
+
+    public void enableIf(boolean enable) {
+      this.enable = enable;
+    }
+
+    public void enableJavaLambdaMerging() {
+      enableJavaLambdaMerging = true;
+    }
+
+    public void enableKotlinLambdaMergingIf(boolean enableKotlinLambdaMerging) {
+      this.enableKotlinLambdaMerging = enableKotlinLambdaMerging;
+    }
+
+    public int getMaxGroupSize() {
+      return maxGroupSize;
+    }
+
+    public int getSyntheticArgumentCount() {
+      return syntheticArgumentCount;
+    }
+
+    public boolean isConstructorMergingEnabled() {
+      return enableConstructorMerging;
+    }
+
+    public boolean isDisabled() {
+      return !isEnabled();
+    }
+
+    public boolean isEnabled() {
+      return enable;
+    }
+
+    public boolean isJavaLambdaMergingEnabled() {
+      return enableJavaLambdaMerging;
+    }
+
+    public boolean isKotlinLambdaMergingEnabled() {
+      return enableKotlinLambdaMerging;
+    }
+  }
+
   public static class ProtoShrinkingOptions {
 
     public boolean enableGeneratedExtensionRegistryShrinking = false;
@@ -1297,7 +1382,7 @@
     public BiConsumer<DexItemFactory, StaticallyMergedClasses> staticallyMergedClassesConsumer =
         ConsumerUtils.emptyBiConsumer();
 
-    public BiConsumer<DexItemFactory, EnumValueInfoMapCollection> unboxedEnumsConsumer =
+    public BiConsumer<DexItemFactory, EnumDataMap> unboxedEnumsConsumer =
         ConsumerUtils.emptyBiConsumer();
 
     public BiConsumer<DexItemFactory, VerticallyMergedClasses> verticallyMergedClassesConsumer =
@@ -1522,6 +1607,10 @@
     return intermediate || hasMinApi(AndroidApiLevel.L);
   }
 
+  public boolean canUseJavaUtilObjects() {
+    return (isGeneratingClassFiles() && !cfToCfDesugar) || hasMinApi(AndroidApiLevel.K);
+  }
+
   public boolean canUseRequireNonNull() {
     return isGeneratingDex() && hasMinApi(AndroidApiLevel.K);
   }
diff --git a/src/main/java/com/android/tools/r8/utils/IterableUtils.java b/src/main/java/com/android/tools/r8/utils/IterableUtils.java
index aee34f6..871a7a5 100644
--- a/src/main/java/com/android/tools/r8/utils/IterableUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/IterableUtils.java
@@ -15,6 +15,10 @@
 
 public class IterableUtils {
 
+  public static <T> Iterable<T> append(Iterable<T> iterable, T element) {
+    return Iterables.concat(iterable, singleton(element));
+  }
+
   public static <T> List<T> ensureUnmodifiableList(Iterable<T> iterable) {
     List<T> list;
     if (iterable instanceof List<?>) {
diff --git a/src/main/java/com/android/tools/r8/utils/StringUtils.java b/src/main/java/com/android/tools/r8/utils/StringUtils.java
index 902924a..a90d2bc 100644
--- a/src/main/java/com/android/tools/r8/utils/StringUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/StringUtils.java
@@ -341,4 +341,11 @@
     throwable.printStackTrace(new PrintWriter(sw));
     return sw.toString();
   }
+
+  public static String capitalize(String stringToCapitalize) {
+    if (stringToCapitalize == null || stringToCapitalize.isEmpty()) {
+      return stringToCapitalize;
+    }
+    return stringToCapitalize.substring(0, 1).toUpperCase() + stringToCapitalize.substring(1);
+  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToManyMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToManyMap.java
new file mode 100644
index 0000000..09b3c27
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToManyMap.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.collections;
+
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+/** Interface that accommodates many-to-many mappings. */
+public interface BidirectionalManyToManyMap<K, V> {
+
+  boolean containsKey(K key);
+
+  boolean containsValue(V value);
+
+  void forEach(BiConsumer<? super K, ? super V> consumer);
+
+  void forEachKey(Consumer<? super K> consumer);
+
+  Set<K> getKeys(V value);
+
+  Set<V> getValues(K key);
+
+  boolean isEmpty();
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToManyRepresentativeMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToManyRepresentativeMap.java
index a4878dc..773c066 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToManyRepresentativeMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToManyRepresentativeMap.java
@@ -4,91 +4,27 @@
 
 package com.android.tools.r8.utils.collections;
 
-import java.util.Map;
+/**
+ * Interface that accommodates many-to-many mappings.
+ *
+ * <p>This interface additionally adds a "representative" for each one-to-many/many-to-one mapping.
+ * The representative for a given key is a value from {@link #getValues(K)}. The representative for
+ * a given value is a key from {@link #getKeys(V)}.
+ */
+public interface BidirectionalManyToManyRepresentativeMap<K, V>
+    extends BidirectionalManyToManyMap<K, V> {
 
-public abstract class BidirectionalManyToManyRepresentativeMap<K, V> {
+  K getRepresentativeKey(V value);
 
-  public static <K, V> BidirectionalManyToManyRepresentativeMap<K, V> empty() {
-    return new EmptyBidirectionalManyToManyRepresentativeMap<>();
-  }
-
-  public abstract boolean containsKey(K key);
-
-  public abstract boolean containsValue(V value);
-
-  public abstract Map<K, V> getForwardBacking();
-
-  public abstract Map<V, K> getInverseBacking();
-
-  public final Inverse getInverseManyToManyMap() {
-    return new Inverse();
-  }
-
-  public abstract K getRepresentativeKey(V value);
-
-  public final K getRepresentativeKeyOrDefault(V value, K defaultValue) {
+  default K getRepresentativeKeyOrDefault(V value, K defaultValue) {
     K representativeKey = getRepresentativeKey(value);
     return representativeKey != null ? representativeKey : defaultValue;
   }
 
-  public abstract V getRepresentativeValue(K key);
+  V getRepresentativeValue(K key);
 
-  public final V getRepresentativeValueOrDefault(K key, V defaultValue) {
+  default V getRepresentativeValueOrDefault(K key, V defaultValue) {
     V representativeValue = getRepresentativeValue(key);
     return representativeValue != null ? representativeValue : defaultValue;
   }
-
-  public abstract Iterable<K> getKeys(V value);
-
-  public abstract Iterable<V> getValues(K key);
-
-  public abstract boolean isEmpty();
-
-  public class Inverse extends BidirectionalManyToManyRepresentativeMap<V, K> {
-
-    @Override
-    public boolean containsKey(V key) {
-      return BidirectionalManyToManyRepresentativeMap.this.containsValue(key);
-    }
-
-    @Override
-    public boolean containsValue(K value) {
-      return BidirectionalManyToManyRepresentativeMap.this.containsKey(value);
-    }
-
-    @Override
-    public Map<V, K> getForwardBacking() {
-      return BidirectionalManyToManyRepresentativeMap.this.getInverseBacking();
-    }
-
-    @Override
-    public Map<K, V> getInverseBacking() {
-      return BidirectionalManyToManyRepresentativeMap.this.getForwardBacking();
-    }
-
-    @Override
-    public V getRepresentativeKey(K value) {
-      return BidirectionalManyToManyRepresentativeMap.this.getRepresentativeValue(value);
-    }
-
-    @Override
-    public K getRepresentativeValue(V key) {
-      return BidirectionalManyToManyRepresentativeMap.this.getRepresentativeKey(key);
-    }
-
-    @Override
-    public Iterable<V> getKeys(K value) {
-      return BidirectionalManyToManyRepresentativeMap.this.getValues(value);
-    }
-
-    @Override
-    public Iterable<K> getValues(V key) {
-      return BidirectionalManyToManyRepresentativeMap.this.getKeys(key);
-    }
-
-    @Override
-    public boolean isEmpty() {
-      return BidirectionalManyToManyRepresentativeMap.this.isEmpty();
-    }
-  }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneHashMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneHashMap.java
new file mode 100644
index 0000000..c217e91
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneHashMap.java
@@ -0,0 +1,143 @@
+// 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.collections;
+
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+public class BidirectionalManyToOneHashMap<K, V> implements MutableBidirectionalManyToOneMap<K, V> {
+
+  private final Map<K, V> backing;
+  private final Map<V, Set<K>> inverse;
+
+  public BidirectionalManyToOneHashMap() {
+    this(new IdentityHashMap<>(), new IdentityHashMap<>());
+  }
+
+  private BidirectionalManyToOneHashMap(Map<K, V> backing, Map<V, Set<K>> inverse) {
+    this.backing = backing;
+    this.inverse = inverse;
+  }
+
+  @Override
+  public void clear() {
+    backing.clear();
+    inverse.clear();
+  }
+
+  @Override
+  public boolean containsKey(K key) {
+    return backing.containsKey(key);
+  }
+
+  @Override
+  public boolean containsValue(V value) {
+    return inverse.containsKey(value);
+  }
+
+  @Override
+  public void forEach(BiConsumer<? super K, ? super V> consumer) {
+    backing.forEach(consumer);
+  }
+
+  @Override
+  public void forEachKey(Consumer<? super K> consumer) {
+    backing.keySet().forEach(consumer);
+  }
+
+  @Override
+  public void forEachManyToOneMapping(BiConsumer<? super Set<K>, V> consumer) {
+    inverse.forEach((value, keys) -> consumer.accept(keys, value));
+  }
+
+  @Override
+  public V get(Object key) {
+    return backing.get(key);
+  }
+
+  @Override
+  public V getOrDefault(Object key, V value) {
+    return backing.getOrDefault(key, value);
+  }
+
+  @Override
+  public Map<K, V> getForwardMap() {
+    return backing;
+  }
+
+  @Override
+  public Set<K> keySet() {
+    return backing.keySet();
+  }
+
+  @Override
+  public Set<K> getKeys(V value) {
+    return inverse.getOrDefault(value, Collections.emptySet());
+  }
+
+  @Override
+  public Set<V> getValues(K key) {
+    V value = get(key);
+    return value != null ? Collections.singleton(value) : Collections.emptySet();
+  }
+
+  @Override
+  public boolean isEmpty() {
+    return backing.isEmpty();
+  }
+
+  @Override
+  public V remove(K key) {
+    V value = backing.remove(key);
+    if (value != null) {
+      Set<K> keys = inverse.get(value);
+      keys.remove(key);
+      if (keys.isEmpty()) {
+        inverse.remove(value);
+      }
+    }
+    return value;
+  }
+
+  @Override
+  public void removeAll(Iterable<K> keys) {
+    keys.forEach(this::remove);
+  }
+
+  @Override
+  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;
+  }
+
+  @Override
+  public void put(K key, V value) {
+    remove(key);
+    backing.put(key, value);
+    inverse.computeIfAbsent(value, ignore -> new LinkedHashSet<>()).add(key);
+  }
+
+  @Override
+  public void put(Iterable<K> keys, V value) {
+    keys.forEach(key -> put(key, value));
+  }
+
+  @Override
+  public Set<V> values() {
+    return inverse.keySet();
+  }
+}
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 361b761..1404c6a 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,109 +4,27 @@
 
 package com.android.tools.r8.utils.collections;
 
-import java.util.Collections;
-import java.util.IdentityHashMap;
-import java.util.LinkedHashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.function.BiConsumer;
 
-public class BidirectionalManyToOneMap<K, V> {
+/**
+ * Interface that accommodates many-to-one mappings.
+ *
+ * <p>This interface inherits from {@link BidirectionalManyToManyMap} to allow implementing
+ * many-to-many mappings using many-to-one mappings.
+ */
+public interface BidirectionalManyToOneMap<K, V> extends BidirectionalManyToManyMap<K, V> {
 
-  private final Map<K, V> backing;
-  private final Map<V, Set<K>> inverse;
+  void forEachManyToOneMapping(BiConsumer<? super Set<K>, V> consumer);
 
-  public BidirectionalManyToOneMap() {
-    this(new IdentityHashMap<>(), new IdentityHashMap<>());
-  }
+  V get(Object key);
 
-  private BidirectionalManyToOneMap(Map<K, V> backing, Map<V, Set<K>> inverse) {
-    this.backing = backing;
-    this.inverse = inverse;
-  }
+  V getOrDefault(Object key, V defaultValue);
 
-  public static <K, V> BidirectionalManyToOneMap<K, V> empty() {
-    return new BidirectionalManyToOneMap<>(Collections.emptyMap(), Collections.emptyMap());
-  }
+  Map<K, V> getForwardMap();
 
-  public boolean containsKey(K key) {
-    return backing.containsKey(key);
-  }
+  Set<K> keySet();
 
-  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);
-  }
-
-  public Map<K, V> getForwardMap() {
-    return backing;
-  }
-
-  public Set<K> keySet() {
-    return backing.keySet();
-  }
-
-  public boolean hasKey(K key) {
-    return backing.containsKey(key);
-  }
-
-  public boolean hasValue(V value) {
-    return inverse.containsKey(value);
-  }
-
-  public Set<K> getKeys(V value) {
-    return inverse.getOrDefault(value, Collections.emptySet());
-  }
-
-  public Set<K> getKeysOrNull(V value) {
-    return inverse.get(value);
-  }
-
-  public boolean isEmpty() {
-    return backing.isEmpty();
-  }
-
-  public void remove(K key) {
-    V value = backing.remove(key);
-    if (value != null) {
-      Set<K> keys = inverse.get(value);
-      keys.remove(key);
-      if (keys.isEmpty()) {
-        inverse.remove(value);
-      }
-    }
-  }
-
-  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 Set<V> values() {
-    return inverse.keySet();
-  }
+  Set<V> values();
 }
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeHashMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeHashMap.java
new file mode 100644
index 0000000..15e1bec
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeHashMap.java
@@ -0,0 +1,55 @@
+// 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.collections;
+
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class BidirectionalManyToOneRepresentativeHashMap<K, V>
+    extends BidirectionalManyToOneHashMap<K, V>
+    implements MutableBidirectionalManyToOneRepresentativeMap<K, V> {
+
+  private final Map<V, K> representatives = new IdentityHashMap<>();
+
+  @Override
+  public void clear() {
+    super.clear();
+    representatives.clear();
+  }
+
+  @Override
+  public K removeRepresentativeFor(V value) {
+    return representatives.remove(value);
+  }
+
+  @Override
+  public void setRepresentative(V value, K representative) {
+    representatives.put(value, representative);
+  }
+
+  @Override
+  public K getRepresentativeKey(V value) {
+    Set<K> keys = getKeys(value);
+    if (!keys.isEmpty()) {
+      return keys.size() == 1 ? keys.iterator().next() : representatives.get(value);
+    }
+    return null;
+  }
+
+  @Override
+  public V getRepresentativeValue(K key) {
+    return get(key);
+  }
+
+  @Override
+  public Set<V> getValues(K key) {
+    if (containsKey(key)) {
+      return Collections.singleton(get(key));
+    }
+    return Collections.emptySet();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeMap.java
new file mode 100644
index 0000000..3e7bf7b
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalManyToOneRepresentativeMap.java
@@ -0,0 +1,14 @@
+// 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.collections;
+
+/**
+ * Interface that accommodates many-to-one mappings.
+ *
+ * <p>This interface implicitly adds a "representative" for each many-to-one mapping by inheriting
+ * from {@link BidirectionalManyToManyRepresentativeMap}.
+ */
+public interface BidirectionalManyToOneRepresentativeMap<K, V>
+    extends BidirectionalManyToOneMap<K, V>, BidirectionalManyToManyRepresentativeMap<K, V> {}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToManyHashMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToManyHashMap.java
new file mode 100644
index 0000000..552a406
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToManyHashMap.java
@@ -0,0 +1,137 @@
+// 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.collections;
+
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+public class BidirectionalOneToManyHashMap<K, V> implements MutableBidirectionalOneToManyMap<K, V> {
+
+  private final Map<K, Set<V>> backing;
+  private final Map<V, K> inverse;
+
+  public BidirectionalOneToManyHashMap() {
+    this(new IdentityHashMap<>(), new IdentityHashMap<>());
+  }
+
+  private BidirectionalOneToManyHashMap(Map<K, Set<V>> backing, Map<V, K> inverse) {
+    this.backing = backing;
+    this.inverse = inverse;
+  }
+
+  @Override
+  public void clear() {
+    backing.clear();
+    inverse.clear();
+  }
+
+  @Override
+  public boolean containsKey(K key) {
+    return backing.containsKey(key);
+  }
+
+  @Override
+  public boolean containsValue(V value) {
+    return inverse.containsKey(value);
+  }
+
+  @Override
+  public void forEach(BiConsumer<? super K, ? super V> consumer) {
+    backing.forEach((key, values) -> values.forEach(value -> consumer.accept(key, value)));
+  }
+
+  @Override
+  public void forEachKey(Consumer<? super K> consumer) {
+    backing.keySet().forEach(consumer);
+  }
+
+  @Override
+  public void forEachOneToManyMapping(BiConsumer<K, Set<V>> consumer) {
+    backing.forEach(consumer);
+  }
+
+  @Override
+  public Set<V> get(Object key) {
+    return backing.get(key);
+  }
+
+  @Override
+  public Set<V> getOrDefault(Object key, Set<V> value) {
+    return backing.getOrDefault(key, value);
+  }
+
+  @Override
+  public K getKey(V value) {
+    return inverse.get(value);
+  }
+
+  @Override
+  public Set<K> getKeys(V value) {
+    K key = inverse.get(value);
+    return key != null ? Collections.singleton(key) : Collections.emptySet();
+  }
+
+  @Override
+  public Set<V> getValues(K key) {
+    return getOrDefault(key, Collections.emptySet());
+  }
+
+  @Override
+  public boolean isEmpty() {
+    return backing.isEmpty();
+  }
+
+  public Set<K> keySet() {
+    return backing.keySet();
+  }
+
+  @Override
+  public Set<V> remove(K key) {
+    Set<V> values = backing.remove(key);
+    if (values == null) {
+      return Collections.emptySet();
+    }
+    for (V value : values) {
+      K removedKey = inverse.remove(value);
+      assert removedKey == key;
+    }
+    return values;
+  }
+
+  @Override
+  public void removeAll(Iterable<K> keys) {
+    keys.forEach(this::remove);
+  }
+
+  @Override
+  public K removeValue(V value) {
+    K key = inverse.remove(value);
+    if (key != null) {
+      Set<V> values = backing.get(key);
+      values.remove(value);
+      if (values.isEmpty()) {
+        backing.remove(key);
+      }
+    }
+    return key;
+  }
+
+  @Override
+  public void put(K key, V value) {
+    removeValue(value);
+    backing.computeIfAbsent(key, ignore -> new LinkedHashSet<>()).add(value);
+    inverse.put(value, key);
+  }
+
+  @Override
+  public void put(K key, Set<V> values) {
+    values.forEach(value -> put(key, value));
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToManyMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToManyMap.java
new file mode 100644
index 0000000..addb1ed
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToManyMap.java
@@ -0,0 +1,25 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.utils.collections;
+
+import java.util.Set;
+import java.util.function.BiConsumer;
+
+/**
+ * Interface that accommodates many-to-many mappings.
+ *
+ * <p>This interface inherits from {@link BidirectionalManyToManyMap} to allow implementing
+ * many-to-many mappings using many-to-one mappings.
+ */
+public interface BidirectionalOneToManyMap<K, V> extends BidirectionalManyToManyMap<K, V> {
+
+  void forEachOneToManyMapping(BiConsumer<K, Set<V>> consumer);
+
+  Set<V> get(Object key);
+
+  Set<V> getOrDefault(Object key, Set<V> defaultValue);
+
+  K getKey(V value);
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToManyRepresentativeHashMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToManyRepresentativeHashMap.java
new file mode 100644
index 0000000..93562e7
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToManyRepresentativeHashMap.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.utils.collections;
+
+import com.google.common.collect.Streams;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+
+public class BidirectionalOneToManyRepresentativeHashMap<K, V>
+    extends BidirectionalOneToManyHashMap<K, V>
+    implements MutableBidirectionalOneToManyRepresentativeMap<K, V> {
+
+  private final Map<K, V> representatives = new IdentityHashMap<>();
+
+  @Override
+  public void clear() {
+    super.clear();
+    representatives.clear();
+  }
+
+  @Override
+  public K getRepresentativeKey(V value) {
+    return getKey(value);
+  }
+
+  @Override
+  public V getRepresentativeValue(K key) {
+    Set<V> values = getValues(key);
+    if (!values.isEmpty()) {
+      return values.size() == 1 ? values.iterator().next() : representatives.get(key);
+    }
+    return null;
+  }
+
+  @Override
+  public Set<V> remove(K key) {
+    Set<V> values = super.remove(key);
+    removeRepresentativeFor(key);
+    return values;
+  }
+
+  @Override
+  public void removeAll(Iterable<K> keys) {
+    super.removeAll(keys);
+    assert Streams.stream(keys).noneMatch(representatives::containsKey);
+  }
+
+  @Override
+  public V removeRepresentativeFor(K key) {
+    return representatives.remove(key);
+  }
+
+  @Override
+  public K removeValue(V value) {
+    K key = super.removeValue(value);
+    if (getValues(key).size() <= 1 || getRepresentativeValue(key) == value) {
+      removeRepresentativeFor(key);
+    }
+    return key;
+  }
+
+  @Override
+  public void setRepresentative(K key, V representative) {
+    assert getValues(key).size() > 1;
+    assert getValues(key).contains(representative);
+    representatives.put(key, representative);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToManyRepresentativeMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToManyRepresentativeMap.java
new file mode 100644
index 0000000..557173c
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToManyRepresentativeMap.java
@@ -0,0 +1,14 @@
+// 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.collections;
+
+/**
+ * Interface that accommodates one-to-many mappings.
+ *
+ * <p>This interface implicitly adds a "representative" for each many-to-one mapping by inheriting
+ * from {@link BidirectionalManyToManyRepresentativeMap}.
+ */
+public interface BidirectionalOneToManyRepresentativeMap<K, V>
+    extends BidirectionalOneToManyMap<K, V>, BidirectionalManyToManyRepresentativeMap<K, V> {}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneHashMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneHashMap.java
index d618b52..8e2edb5 100644
--- a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneHashMap.java
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneHashMap.java
@@ -4,15 +4,16 @@
 
 package com.android.tools.r8.utils.collections;
 
-import com.android.tools.r8.utils.IterableUtils;
 import com.google.common.collect.BiMap;
 import com.google.common.collect.HashBiMap;
-import java.util.Collection;
+import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
 
 public class BidirectionalOneToOneHashMap<K, V>
-    extends BidirectionalManyToManyRepresentativeMap<K, V> implements Map<K, V> {
+    implements MutableBidirectionalOneToOneMap<K, V>, Map<K, V> {
 
   private final BiMap<K, V> backing;
 
@@ -44,8 +45,19 @@
     return backing.entrySet();
   }
 
-  public V forcePut(K key, V value) {
-    return backing.forcePut(key, value);
+  @Override
+  public void forEach(BiConsumer<? super K, ? super V> consumer) {
+    backing.forEach(consumer);
+  }
+
+  @Override
+  public void forEachKey(Consumer<? super K> consumer) {
+    backing.keySet().forEach(consumer);
+  }
+
+  @Override
+  public void forEachManyToOneMapping(BiConsumer<? super Set<K>, V> consumer) {
+    backing.forEach((key, value) -> consumer.accept(Collections.singleton(key), value));
   }
 
   @Override
@@ -54,15 +66,17 @@
   }
 
   @Override
-  public BiMap<K, V> getForwardBacking() {
+  public V getOrDefault(Object key, V defaultValue) {
+    V value = get(key);
+    return value != null ? value : defaultValue;
+  }
+
+  @Override
+  public BiMap<K, V> getForwardMap() {
     return backing;
   }
 
   @Override
-  public BiMap<V, K> getInverseBacking() {
-    return backing.inverse();
-  }
-
   public BidirectionalOneToOneHashMap<V, K> getInverseOneToOneMap() {
     return new BidirectionalOneToOneHashMap<>(backing.inverse());
   }
@@ -78,19 +92,19 @@
   }
 
   @Override
-  public Iterable<K> getKeys(V value) {
+  public Set<K> getKeys(V value) {
     if (containsValue(value)) {
-      return IterableUtils.singleton(getRepresentativeKey(value));
+      return Collections.singleton(getRepresentativeKey(value));
     }
-    return IterableUtils.empty();
+    return Collections.emptySet();
   }
 
   @Override
-  public Iterable<V> getValues(K key) {
+  public Set<V> getValues(K key) {
     if (containsKey(key)) {
-      return IterableUtils.singleton(getRepresentativeValue(key));
+      return Collections.singleton(getRepresentativeValue(key));
     }
-    return IterableUtils.empty();
+    return Collections.emptySet();
   }
 
   @Override
@@ -105,11 +119,12 @@
 
   @Override
   public V put(K key, V value) {
-    return backing.put(key, value);
+    return backing.forcePut(key, value);
   }
 
-  public void putAll(BidirectionalOneToOneHashMap<K, V> map) {
-    putAll(map.backing);
+  @Override
+  public void putAll(BidirectionalManyToManyMap<K, V> map) {
+    map.forEach(this::put);
   }
 
   @Override
@@ -128,7 +143,7 @@
   }
 
   @Override
-  public Collection<V> values() {
+  public Set<V> values() {
     return backing.values();
   }
 }
diff --git a/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneMap.java b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneMap.java
new file mode 100644
index 0000000..e5085c2
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/BidirectionalOneToOneMap.java
@@ -0,0 +1,22 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.utils.collections;
+
+import com.google.common.collect.BiMap;
+
+/**
+ * Interface that accommodates one-to-one mappings.
+ *
+ * <p>This interface inherits from {@link BidirectionalManyToManyRepresentativeMap} to allow
+ * implementing many-to-many mappings using one-to-one mappings.
+ */
+public interface BidirectionalOneToOneMap<K, V>
+    extends BidirectionalManyToOneRepresentativeMap<K, V> {
+
+  @Override
+  BiMap<K, V> getForwardMap();
+
+  BidirectionalOneToOneMap<V, K> getInverseOneToOneMap();
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/EmptyBidirectionalManyToManyRepresentativeMap.java b/src/main/java/com/android/tools/r8/utils/collections/EmptyBidirectionalManyToManyRepresentativeMap.java
deleted file mode 100644
index dbcb753..0000000
--- a/src/main/java/com/android/tools/r8/utils/collections/EmptyBidirectionalManyToManyRepresentativeMap.java
+++ /dev/null
@@ -1,58 +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.utils.collections;
-
-import com.android.tools.r8.utils.IterableUtils;
-import java.util.Collections;
-import java.util.Map;
-
-public class EmptyBidirectionalManyToManyRepresentativeMap<K, V>
-    extends BidirectionalManyToManyRepresentativeMap<K, V> {
-
-  @Override
-  public boolean containsKey(K key) {
-    return false;
-  }
-
-  @Override
-  public boolean containsValue(V value) {
-    return false;
-  }
-
-  @Override
-  public Map<K, V> getForwardBacking() {
-    return Collections.emptyMap();
-  }
-
-  @Override
-  public Map<V, K> getInverseBacking() {
-    return Collections.emptyMap();
-  }
-
-  @Override
-  public K getRepresentativeKey(V value) {
-    return null;
-  }
-
-  @Override
-  public V getRepresentativeValue(K key) {
-    return null;
-  }
-
-  @Override
-  public Iterable<K> getKeys(V value) {
-    return IterableUtils.empty();
-  }
-
-  @Override
-  public Iterable<V> getValues(K key) {
-    return IterableUtils.empty();
-  }
-
-  @Override
-  public boolean isEmpty() {
-    return true;
-  }
-}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/EmptyBidirectionalOneToOneMap.java b/src/main/java/com/android/tools/r8/utils/collections/EmptyBidirectionalOneToOneMap.java
new file mode 100644
index 0000000..f2c10c3
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/EmptyBidirectionalOneToOneMap.java
@@ -0,0 +1,98 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.utils.collections;
+
+import com.google.common.collect.BiMap;
+import com.google.common.collect.HashBiMap;
+import java.util.Collections;
+import java.util.Set;
+import java.util.function.BiConsumer;
+import java.util.function.Consumer;
+
+public class EmptyBidirectionalOneToOneMap<K, V>
+    implements BidirectionalOneToOneMap<K, V>,
+        BidirectionalManyToOneRepresentativeMap<K, V>,
+        BidirectionalManyToManyRepresentativeMap<K, V> {
+
+  @Override
+  public boolean containsKey(K key) {
+    return false;
+  }
+
+  @Override
+  public boolean containsValue(V value) {
+    return false;
+  }
+
+  @Override
+  public void forEach(BiConsumer<? super K, ? super V> consumer) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void forEachKey(Consumer<? super K> consumer) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public void forEachManyToOneMapping(BiConsumer<? super Set<K>, V> consumer) {
+    // Intentionally empty.
+  }
+
+  @Override
+  public V get(Object key) {
+    return null;
+  }
+
+  @Override
+  public V getOrDefault(Object key, V defaultValue) {
+    return defaultValue;
+  }
+
+  @Override
+  public BiMap<K, V> getForwardMap() {
+    return HashBiMap.create();
+  }
+
+  @Override
+  public K getRepresentativeKey(V value) {
+    return null;
+  }
+
+  @Override
+  public V getRepresentativeValue(K key) {
+    return null;
+  }
+
+  @Override
+  public Set<K> getKeys(V value) {
+    return Collections.emptySet();
+  }
+
+  @Override
+  public Set<V> getValues(K key) {
+    return Collections.emptySet();
+  }
+
+  @Override
+  public boolean isEmpty() {
+    return true;
+  }
+
+  @Override
+  public Set<K> keySet() {
+    return Collections.emptySet();
+  }
+
+  @Override
+  public Set<V> values() {
+    return Collections.emptySet();
+  }
+
+  @Override
+  public BidirectionalOneToOneMap<V, K> getInverseOneToOneMap() {
+    return new EmptyBidirectionalOneToOneMap<>();
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalManyToOneMap.java b/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalManyToOneMap.java
new file mode 100644
index 0000000..7953e91
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalManyToOneMap.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.collections;
+
+import java.util.Set;
+
+/** Interface that provides mutable access to the implementation of a many-to-one mapping. */
+public interface MutableBidirectionalManyToOneMap<K, V> extends BidirectionalManyToOneMap<K, V> {
+
+  void clear();
+
+  void put(K key, V value);
+
+  void put(Iterable<K> key, V value);
+
+  V remove(K key);
+
+  void removeAll(Iterable<K> keys);
+
+  Set<K> removeValue(V value);
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalManyToOneRepresentativeMap.java b/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalManyToOneRepresentativeMap.java
new file mode 100644
index 0000000..24f91ac
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalManyToOneRepresentativeMap.java
@@ -0,0 +1,14 @@
+// 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.collections;
+
+/** Interface that provides mutable access to the implementation of a many-to-one mapping. */
+public interface MutableBidirectionalManyToOneRepresentativeMap<K, V>
+    extends MutableBidirectionalManyToOneMap<K, V>, BidirectionalManyToOneRepresentativeMap<K, V> {
+
+  K removeRepresentativeFor(V value);
+
+  void setRepresentative(V value, K representative);
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalOneToManyMap.java b/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalOneToManyMap.java
new file mode 100644
index 0000000..1b9d464
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalOneToManyMap.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.collections;
+
+import java.util.Set;
+
+/** Interface that provides mutable access to the implementation of a one-to-many mapping. */
+public interface MutableBidirectionalOneToManyMap<K, V> extends BidirectionalOneToManyMap<K, V> {
+
+  void clear();
+
+  Set<V> remove(K key);
+
+  void removeAll(Iterable<K> keys);
+
+  K removeValue(V value);
+
+  void put(K key, V value);
+
+  void put(K key, Set<V> values);
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalOneToManyRepresentativeMap.java b/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalOneToManyRepresentativeMap.java
new file mode 100644
index 0000000..987eb6f
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalOneToManyRepresentativeMap.java
@@ -0,0 +1,14 @@
+// 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.collections;
+
+/** Interface that provides mutable access to the implementation of a one-to-many mapping. */
+public interface MutableBidirectionalOneToManyRepresentativeMap<K, V>
+    extends MutableBidirectionalOneToManyMap<K, V>, BidirectionalOneToManyRepresentativeMap<K, V> {
+
+  V removeRepresentativeFor(K key);
+
+  void setRepresentative(K key, V representative);
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalOneToOneMap.java b/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalOneToOneMap.java
new file mode 100644
index 0000000..5e5f469
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/MutableBidirectionalOneToOneMap.java
@@ -0,0 +1,13 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.utils.collections;
+
+/** Interface that provides mutable access to the implementation of a one-to-one mapping. */
+public interface MutableBidirectionalOneToOneMap<K, V> extends BidirectionalOneToOneMap<K, V> {
+
+  V put(K key, V value);
+
+  void putAll(BidirectionalManyToManyMap<K, V> map);
+}
diff --git a/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodMap.java b/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodMap.java
new file mode 100644
index 0000000..d49928a
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/utils/collections/ProgramMethodMap.java
@@ -0,0 +1,57 @@
+// 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.collections;
+
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.utils.ProgramMethodEquivalence;
+import com.google.common.base.Equivalence.Wrapper;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+public class ProgramMethodMap<V> {
+
+  private final Map<Wrapper<ProgramMethod>, V> backing;
+
+  private ProgramMethodMap(Supplier<Map<Wrapper<ProgramMethod>, V>> backingFactory) {
+    backing = backingFactory.get();
+  }
+
+  public static <V> ProgramMethodMap<V> create() {
+    return new ProgramMethodMap<>(HashMap::new);
+  }
+
+  public static <V> ProgramMethodMap<V> createLinked() {
+    return new ProgramMethodMap<>(LinkedHashMap::new);
+  }
+
+  public void clear() {
+    backing.clear();
+  }
+
+  public V computeIfAbsent(ProgramMethod method, Function<ProgramMethod, V> fn) {
+    return backing.computeIfAbsent(wrap(method), key -> fn.apply(key.get()));
+  }
+
+  public void forEach(BiConsumer<ProgramMethod, V> consumer) {
+    backing.forEach((wrapper, value) -> consumer.accept(wrapper.get(), value));
+  }
+
+  public boolean isEmpty() {
+    return backing.isEmpty();
+  }
+
+  public V put(ProgramMethod method, V value) {
+    Wrapper<ProgramMethod> wrapper = ProgramMethodEquivalence.get().wrap(method);
+    return backing.put(wrapper, value);
+  }
+
+  private static Wrapper<ProgramMethod> wrap(ProgramMethod method) {
+    return ProgramMethodEquivalence.get().wrap(method);
+  }
+}
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
index ee264cc..234732c 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorBase.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/CompareToVisitorBase.java
@@ -104,6 +104,11 @@
     }
 
     @Override
+    ItemSpecification<T> self() {
+      return this;
+    }
+
+    @Override
     public ItemSpecification<T> withAssert(Predicate<T> predicate) {
       assert predicate.test(item1);
       assert predicate.test(item2);
@@ -159,6 +164,22 @@
     }
 
     @Override
+    public ItemSpecification<T> withShortArray(Function<T, short[]> getter) {
+      if (order == 0) {
+        short[] is1 = getter.apply(item1);
+        short[] is2 = getter.apply(item2);
+        int minLength = Math.min(is1.length, is2.length);
+        for (int i = 0; i < minLength && order == 0; i++) {
+          order = parent.visitInt(is1[i], is2[i]);
+        }
+        if (order == 0) {
+          order = parent.visitInt(is1.length, is2.length);
+        }
+      }
+      return this;
+    }
+
+    @Override
     public <S> ItemSpecification<T> withConditionalCustomItem(
         Predicate<T> predicate,
         Function<T, S> getter,
@@ -184,5 +205,13 @@
       }
       return this;
     }
+
+    @Override
+    public ItemSpecification<T> withDexReference(Function<T, DexReference> getter) {
+      if (order == 0) {
+        order = parent.visitDexReference(getter.apply(item1), getter.apply(item2));
+      }
+      return this;
+    }
   }
 }
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
index c64e0b8..892183d 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/HashCodeVisitor.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils.structural;
 
+import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.utils.structural.StructuralItem.CompareToAccept;
 import com.android.tools.r8.utils.structural.StructuralItem.HashingAccept;
 import java.util.Arrays;
@@ -23,6 +24,10 @@
  */
 public class HashCodeVisitor<T> extends StructuralSpecification<T, HashCodeVisitor<T>> {
 
+  public static <T extends StructuralItem<T>> int run(T item) {
+    return run(item, item.getStructuralMapping());
+  }
+
   public static <T> int run(T item, StructuralMapping<T> visit) {
     HashCodeVisitor<T> visitor = new HashCodeVisitor<>(item);
     visit.apply(visitor);
@@ -37,6 +42,11 @@
     this.item = item;
   }
 
+  @Override
+  HashCodeVisitor<T> self() {
+    return this;
+  }
+
   private HashCodeVisitor<T> amend(int value) {
     // This mirrors the behavior of Objects.hash(values...) / Arrays.hashCode(array).
     hashCode = 31 * hashCode + value;
@@ -75,6 +85,11 @@
   }
 
   @Override
+  public HashCodeVisitor<T> withShortArray(Function<T, short[]> getter) {
+    return amend(Arrays.hashCode(getter.apply(item)));
+  }
+
+  @Override
   protected <S> HashCodeVisitor<T> withConditionalCustomItem(
       Predicate<T> predicate,
       Function<T, S> getter,
@@ -98,4 +113,9 @@
     }
     return this;
   }
+
+  @Override
+  public HashCodeVisitor<T> withDexReference(Function<T, DexReference> getter) {
+    return amend(getter.apply(item).hashCode());
+  }
 }
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
index ba499a3..e63ac6c 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/HashingVisitorWithTypeEquivalence.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils.structural;
 
+import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.graph.DexString;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.utils.structural.StructuralItem.CompareToAccept;
@@ -101,6 +102,11 @@
     }
 
     @Override
+    ItemSpecification<T> self() {
+      return this;
+    }
+
+    @Override
     public ItemSpecification<T> withAssert(Predicate<T> predicate) {
       assert predicate.test(item);
       return this;
@@ -140,6 +146,15 @@
     }
 
     @Override
+    public ItemSpecification<T> withShortArray(Function<T, short[]> getter) {
+      short[] ints = getter.apply(item);
+      for (int i = 0; i < ints.length; i++) {
+        parent.visitInt(ints[i]);
+      }
+      return this;
+    }
+
+    @Override
     protected <S> ItemSpecification<T> withConditionalCustomItem(
         Predicate<T> predicate,
         Function<T, S> getter,
@@ -160,5 +175,11 @@
       parent.visitItemIterator(getter.apply(item), hasher);
       return this;
     }
+
+    @Override
+    public ItemSpecification<T> withDexReference(Function<T, DexReference> getter) {
+      parent.visitDexReference(getter.apply(item));
+      return this;
+    }
   }
 }
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
index 891fbd7..a81fc18 100644
--- a/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java
+++ b/src/main/java/com/android/tools/r8/utils/structural/StructuralSpecification.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.utils.structural;
 
+import com.android.tools.r8.graph.DexReference;
 import com.android.tools.r8.utils.structural.StructuralItem.CompareToAccept;
 import com.android.tools.r8.utils.structural.StructuralItem.HashingAccept;
 import java.util.Arrays;
@@ -16,6 +17,14 @@
 
 public abstract class StructuralSpecification<T, V extends StructuralSpecification<T, V>> {
 
+  abstract V self();
+
+  /** Apply a structural mapping to the present specification. */
+  public final V withSpec(StructuralMapping<T> spec) {
+    spec.apply(this);
+    return self();
+  }
+
   /**
    * Base for accessing and visiting a sub-part on an item.
    *
@@ -85,6 +94,25 @@
         StructuralItem::acceptHashing);
   }
 
+  public final <S extends StructuralItem<S>> V withItemArrayAllowingNullMembers(
+      Function<T, S[]> getter) {
+    return withItemIterator(
+        getter.andThen(a -> Arrays.asList(a).iterator()),
+        (a, b, visitor) -> {
+          if (a == null || b == null) {
+            return visitor.visitBool(a != null, b != null);
+          }
+          return a.acceptCompareTo(b, visitor);
+        },
+        (a, visitor) -> {
+          if (a == null) {
+            visitor.visitInt(0);
+          } else {
+            a.acceptHashing(visitor);
+          }
+        });
+  }
+
   /**
    * Helper to declare an assert on the item.
    *
@@ -103,4 +131,8 @@
   public abstract V withDouble(ToDoubleFunction<T> getter);
 
   public abstract V withIntArray(Function<T, int[]> getter);
+
+  public abstract V withShortArray(Function<T, short[]> getter);
+
+  public abstract V withDexReference(Function<T, DexReference> getter);
 }
diff --git a/src/test/examples/classmerging/ConflictInGeneratedNameTest.java b/src/test/examples/classmerging/ConflictInGeneratedNameTest.java
index 6d566ec..396e73c 100644
--- a/src/test/examples/classmerging/ConflictInGeneratedNameTest.java
+++ b/src/test/examples/classmerging/ConflictInGeneratedNameTest.java
@@ -15,11 +15,11 @@
 
     public A() {
       print("In A.<init>()");
-      constructor$classmerging$ConflictInGeneratedNameTest$A();
+      $r8$constructor$classmerging$ConflictInGeneratedNameTest$A();
     }
 
-    private void constructor$classmerging$ConflictInGeneratedNameTest$A() {
-      print("In A.constructor$classmerging$ConflictInGeneratedNameTest$A()");
+    private void $r8$constructor$classmerging$ConflictInGeneratedNameTest$A() {
+      print("In A.$r8$constructor$classmerging$ConflictInGeneratedNameTest$A()");
     }
 
     public void printState() {
diff --git a/src/test/examplesAndroidO/lambdadesugaring/LambdaDesugaring.java b/src/test/examplesAndroidO/lambdadesugaring/LambdaDesugaring.java
index b54be6b..627d255 100644
--- a/src/test/examplesAndroidO/lambdadesugaring/LambdaDesugaring.java
+++ b/src/test/examplesAndroidO/lambdadesugaring/LambdaDesugaring.java
@@ -398,7 +398,15 @@
     try {
       testEnforcedSignatureHelper();
     } catch (Exception e) {
-      System.out.println(e.getMessage());
+      if (e.getMessage().contains("cannot be cast to lambdadesugaring.LambdaDesugaring$B")
+          || e.getMessage()
+              .contains("cannot be cast to class lambdadesugaring.LambdaDesugaring$B")) {
+        System.out.println(
+            "lambdadesugaring.LambdaDesugaring$A cannot be cast to"
+                + " lambdadesugaring.LambdaDesugaring$B");
+      } else {
+        System.out.println(e.getMessage());
+      }
     }
 
     atA(t -> new LambdaDesugaring().reorder(t));
diff --git a/src/test/examplesJava15/records/Main.java b/src/test/examplesJava15/records/Main.java
new file mode 100644
index 0000000..7be696d
--- /dev/null
+++ b/src/test/examplesJava15/records/Main.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package records;
+
+public class Main {
+
+  record Person(String name, int age) {}
+
+  public static void main(String[] args) {
+    Person janeDoe = new Person("Jane Doe", 42);
+    System.out.println(janeDoe.name);
+    System.out.println(janeDoe.age);
+  }
+}
diff --git a/src/test/examplesJava15/sealed/Compiler.java b/src/test/examplesJava15/sealed/Compiler.java
new file mode 100644
index 0000000..8d29eea
--- /dev/null
+++ b/src/test/examplesJava15/sealed/Compiler.java
@@ -0,0 +1,10 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package sealed;
+
+public sealed abstract class Compiler permits R8Compiler, D8Compiler {
+
+  public abstract void run();
+}
\ No newline at end of file
diff --git a/src/test/examplesJava15/sealed/D8Compiler.java b/src/test/examplesJava15/sealed/D8Compiler.java
new file mode 100644
index 0000000..e86df73
--- /dev/null
+++ b/src/test/examplesJava15/sealed/D8Compiler.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package sealed;
+
+public final class D8Compiler extends Compiler {
+
+  public void run() {
+    System.out.println("D8 compiler");
+  }
+}
diff --git a/src/test/examplesJava15/sealed/Main.java b/src/test/examplesJava15/sealed/Main.java
new file mode 100644
index 0000000..f163a21
--- /dev/null
+++ b/src/test/examplesJava15/sealed/Main.java
@@ -0,0 +1,13 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package sealed;
+
+public class Main {
+
+  public static void main(String[] args) {
+    new R8Compiler().run();
+    new D8Compiler().run();
+  }
+}
diff --git a/src/test/examplesJava15/sealed/R8Compiler.java b/src/test/examplesJava15/sealed/R8Compiler.java
new file mode 100644
index 0000000..72a522a
--- /dev/null
+++ b/src/test/examplesJava15/sealed/R8Compiler.java
@@ -0,0 +1,12 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package sealed;
+
+public final class R8Compiler extends Compiler {
+
+  public void run() {
+    System.out.println("R8 compiler");
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java b/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
index cea34f9..5bb4d63 100644
--- a/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
+++ b/src/test/java/com/android/tools/r8/CfFrontendExamplesTest.java
@@ -21,12 +21,25 @@
 import java.util.List;
 import java.util.function.BiConsumer;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
 import org.objectweb.asm.ClassReader;
 import org.objectweb.asm.util.ASMifier;
 import org.objectweb.asm.util.TraceClassVisitor;
 
+@RunWith(Parameterized.class)
 public class CfFrontendExamplesTest extends TestBase {
 
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public CfFrontendExamplesTest(TestParameters parameters) {
+    parameters.assertNoneRuntime();
+  }
+
   @Test
   public void testArithmetic() throws Exception {
     runTest("arithmetic.Arithmetic");
diff --git a/src/test/java/com/android/tools/r8/DiagnosticsChecker.java b/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
index bda7fe1..73d1d3a 100644
--- a/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
+++ b/src/test/java/com/android/tools/r8/DiagnosticsChecker.java
@@ -48,10 +48,10 @@
     void run(DiagnosticsHandler handler) throws CompilationFailedException;
   }
 
-  public static void checkContains(String snippet, List<Diagnostic> diagnostics) {
+  private static void checkContains(String snippet, List<Diagnostic> diagnostics) {
     List<String> messages = ListUtils.map(diagnostics, Diagnostic::getDiagnosticMessage);
     System.out.println("Expecting match for '" + snippet + "'");
-    System.out.println("StdErr:\n" + messages);
+    System.out.println("Diagnostics messages:\n" + messages);
     assertTrue(
         "Expected to find snippet '"
             + snippet
@@ -60,10 +60,26 @@
         diagnostics.stream().anyMatch(d -> d.getDiagnosticMessage().contains(snippet)));
   }
 
+  private static void checkNotContains(String snippet, List<Diagnostic> diagnostics) {
+    List<String> messages = ListUtils.map(diagnostics, Diagnostic::getDiagnosticMessage);
+    System.out.println("Expecting no match for '" + snippet + "'");
+    System.out.println("Diagnostics messages:\n" + messages);
+    assertTrue(
+        "Expected to *not* find snippet '"
+            + snippet
+            + "' in error messages:\n"
+            + String.join("\n", messages),
+        diagnostics.stream().noneMatch(d -> d.getDiagnosticMessage().contains(snippet)));
+  }
+
   public static void checkContains(Collection<String> snippets, List<Diagnostic> diagnostics) {
     snippets.forEach(snippet -> checkContains(snippet, diagnostics));
   }
 
+  public static void checkNotContains(Collection<String> snippets, List<Diagnostic> diagnostics) {
+    snippets.forEach(snippet -> checkNotContains(snippet, diagnostics));
+  }
+
   public void checkErrorsContains(String snippet) {
     checkContains(snippet, errors);
   }
diff --git a/src/test/java/com/android/tools/r8/KotlinCompilerTool.java b/src/test/java/com/android/tools/r8/KotlinCompilerTool.java
index 9a36a04..bee542f 100644
--- a/src/test/java/com/android/tools/r8/KotlinCompilerTool.java
+++ b/src/test/java/com/android/tools/r8/KotlinCompilerTool.java
@@ -21,6 +21,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import org.junit.rules.TemporaryFolder;
 
@@ -29,29 +30,35 @@
   public static final class KotlinCompiler {
 
     private final String name;
-    private final Path path;
+    private final Path lib;
+    private final Path compiler;
 
-    public KotlinCompiler(String name, Path path) {
+    public KotlinCompiler(String name) {
       this.name = name;
-      this.path = path;
+      this.lib = Paths.get(ToolHelper.THIRD_PARTY_DIR, "kotlin", name, "kotlinc", "lib");
+      this.compiler = lib.resolve("kotlin-compiler.jar");
     }
 
-    public Path getPath() {
-      return path;
+    public KotlinCompiler(String name, Path compiler) {
+      this.name = name;
+      this.compiler = compiler;
+      this.lib = null;
+    }
+
+    public Path getCompiler() {
+      return compiler;
+    }
+
+    public Path getFolder() {
+      return lib;
+    }
+
+    @Override
+    public String toString() {
+      return name;
     }
   }
 
-  public static KotlinCompiler KOTLINC =
-      new KotlinCompiler(
-          "kotlinc",
-          Paths.get(
-              ToolHelper.THIRD_PARTY_DIR,
-              "kotlin",
-              "kotlin-compiler-1.3.72",
-              "kotlinc",
-              "lib",
-              "kotlin-compiler.jar"));
-
   private final CfRuntime jdk;
   private final TestState state;
   private final KotlinCompiler compiler;
@@ -70,6 +77,14 @@
     this.targetVersion = targetVersion;
   }
 
+  public KotlinCompiler getCompiler() {
+    return compiler;
+  }
+
+  public KotlinTargetVersion getTargetVersion() {
+    return targetVersion;
+  }
+
   public static KotlinCompilerTool create(
       CfRuntime jdk,
       TemporaryFolder temp,
@@ -104,6 +119,24 @@
     return addSourceFilesWithNonKtExtension(temp, Arrays.asList(files));
   }
 
+  public KotlinCompilerTool includeRuntime() {
+    assert !additionalArguments.contains("-include-runtime");
+    addArguments("-include-runtime");
+    return this;
+  }
+
+  public KotlinCompilerTool noReflect() {
+    assert !additionalArguments.contains("-no-reflect");
+    addArguments("-no-reflect");
+    return this;
+  }
+
+  public KotlinCompilerTool noStdLib() {
+    assert !additionalArguments.contains("-no-stdlib");
+    addArguments("-no-stdlib");
+    return this;
+  }
+
   public KotlinCompilerTool addSourceFilesWithNonKtExtension(
       TemporaryFolder temp, Collection<Path> files) {
     return addSourceFiles(
@@ -145,6 +178,11 @@
     return this;
   }
 
+  public KotlinCompilerTool apply(Consumer<KotlinCompilerTool> consumer) {
+    consumer.accept(this);
+    return this;
+  }
+
   private Path getOrCreateOutputPath() throws IOException {
     return output != null ? output : state.getNewTempFolder().resolve("out.jar");
   }
@@ -167,7 +205,7 @@
     List<String> cmdline = new ArrayList<>();
     cmdline.add(jdk.getJavaExecutable().toString());
     cmdline.add("-cp");
-    cmdline.add(compiler.getPath().toString());
+    cmdline.add(compiler.getCompiler().toString());
     cmdline.add(ToolHelper.K2JVMCompiler);
     if (useJvmAssertions) {
       cmdline.add("-Xassertions=jvm");
diff --git a/src/test/java/com/android/tools/r8/KotlinTestBase.java b/src/test/java/com/android/tools/r8/KotlinTestBase.java
index 6cd9cfd..f8dc222 100644
--- a/src/test/java/com/android/tools/r8/KotlinTestBase.java
+++ b/src/test/java/com/android/tools/r8/KotlinTestBase.java
@@ -5,6 +5,8 @@
 
 import static org.hamcrest.CoreMatchers.containsString;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
+import com.android.tools.r8.TestRuntime.CfRuntime;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.FileUtils;
@@ -12,13 +14,19 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.IdentityHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
 import java.util.stream.Collectors;
 import org.hamcrest.Matcher;
 
 public abstract class KotlinTestBase extends TestBase {
 
-  protected  static final String checkParameterIsNotNullSignature =
+  protected static final String checkParameterIsNotNullSignature =
       "void kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull("
           + "java.lang.Object, java.lang.String)";
   protected static final String throwParameterIsNotNullExceptionSignature =
@@ -29,10 +37,14 @@
 
   private static final String RSRC = "kotlinR8TestResources";
 
+  private static final Map<String, KotlinCompileMemoizer> compileMemoizers = new HashMap<>();
+
+  protected final KotlinCompiler kotlinc;
   protected final KotlinTargetVersion targetVersion;
 
-  protected KotlinTestBase(KotlinTargetVersion targetVersion) {
+  protected KotlinTestBase(KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
     this.targetVersion = targetVersion;
+    this.kotlinc = kotlinc;
   }
 
   protected static List<Path> getKotlinFilesInTestPackage(Package pkg) throws IOException {
@@ -42,8 +54,7 @@
         .collect(Collectors.toList());
   }
 
-  protected static Path getKotlinFileInTestPackage(Package pkg, String fileName)
-      throws IOException {
+  protected static Path getKotlinFileInTestPackage(Package pkg, String fileName) {
     String folder = DescriptorUtils.getBinaryNameFromJavaType(pkg.getName());
     return getKotlinFileInTest(folder, fileName);
   }
@@ -56,9 +67,14 @@
     return Paths.get(ToolHelper.TESTS_DIR, RSRC, folder, fileName + FileUtils.KT_EXTENSION);
   }
 
-  protected Path getKotlinJarFile(String folder) {
-    return Paths.get(ToolHelper.TESTS_BUILD_DIR, RSRC,
-        targetVersion.getFolderName(), folder + FileUtils.JAR_EXTENSION);
+  protected static List<Path> getKotlinFilesInResource(String folder) {
+    try {
+      return Files.walk(Paths.get(ToolHelper.TESTS_DIR, RSRC, folder))
+          .filter(path -> path.toString().endsWith(".kt") || path.toString().endsWith(".java"))
+          .collect(Collectors.toList());
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
   }
 
   protected Path getJavaJarFile(String folder) {
@@ -73,4 +89,60 @@
   protected static Matcher<String> expectedInfoMessagesFromKotlinStdLib() {
     return containsString("No VersionRequirement");
   }
+
+  protected KotlinCompilerTool kotlinCompilerTool() {
+    return KotlinCompilerTool.create(CfRuntime.getCheckedInJdk9(), temp, kotlinc, targetVersion);
+  }
+
+  public static KotlinCompileMemoizer getCompileMemoizer(Path... source) {
+    return new KotlinCompileMemoizer(Arrays.asList(source));
+  }
+
+  public static KotlinCompileMemoizer getCompileMemoizer(Collection<Path> sources) {
+    assert sources.size() > 0;
+    return new KotlinCompileMemoizer(sources);
+  }
+
+  public static KotlinCompileMemoizer getCompileMemoizer(
+      Collection<Path> sources, String sharedFolder) {
+    return compileMemoizers.computeIfAbsent(
+        sharedFolder, ignore -> new KotlinCompileMemoizer(sources));
+  }
+
+  public static class KotlinCompileMemoizer {
+
+    private final Collection<Path> sources;
+    private Consumer<KotlinCompilerTool> kotlinCompilerToolConsumer = x -> {};
+    private final Map<KotlinCompiler, Map<KotlinTargetVersion, Path>> compiledPaths =
+        new IdentityHashMap<>();
+
+    public KotlinCompileMemoizer(Collection<Path> sources) {
+      this.sources = sources;
+    }
+
+    public KotlinCompileMemoizer configure(Consumer<KotlinCompilerTool> consumer) {
+      this.kotlinCompilerToolConsumer = consumer;
+      return this;
+    }
+
+    public Path getForConfiguration(KotlinCompiler compiler, KotlinTargetVersion targetVersion) {
+      Map<KotlinTargetVersion, Path> kotlinTargetVersionPathMap = compiledPaths.get(compiler);
+      if (kotlinTargetVersionPathMap == null) {
+        kotlinTargetVersionPathMap = new IdentityHashMap<>();
+        compiledPaths.put(compiler, kotlinTargetVersionPathMap);
+      }
+      return kotlinTargetVersionPathMap.computeIfAbsent(
+          targetVersion,
+          ignored -> {
+            try {
+              return kotlinc(compiler, targetVersion)
+                  .addSourceFiles(sources)
+                  .apply(kotlinCompilerToolConsumer)
+                  .compile();
+            } catch (IOException e) {
+              throw new RuntimeException(e);
+            }
+          });
+    }
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/R8CfVersionTest.java b/src/test/java/com/android/tools/r8/R8CfVersionTest.java
new file mode 100644
index 0000000..4f051ad
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/R8CfVersionTest.java
@@ -0,0 +1,54 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8;
+
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.cf.CfVersion;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+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 R8CfVersionTest extends TestBase {
+
+  private final CfVersion targetVersion = CfVersion.V1_8;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withNoneRuntime().build();
+  }
+
+  public R8CfVersionTest(TestParameters parameters) {}
+
+  @Test
+  public void testCfVersionR8() throws IOException {
+    CodeInspector inspector = new CodeInspector(ToolHelper.R8_WITH_DEPS_JAR);
+    inspector.forAllClasses(
+        clazz -> {
+          assertTrue(
+              clazz
+                  .getDexProgramClass()
+                  .getInitialClassFileVersion()
+                  .isLessThanOrEqualTo(targetVersion));
+        });
+  }
+
+  @Test
+  public void testCfVersionR8Lib() throws IOException {
+    CodeInspector inspector = new CodeInspector(ToolHelper.R8LIB_JAR);
+    inspector.forAllClasses(
+        clazz -> {
+          assertTrue(
+              clazz
+                  .getDexProgramClass()
+                  .getInitialClassFileVersion()
+                  .isLessThanOrEqualTo(targetVersion));
+        });
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/R8TestRunResult.java b/src/test/java/com/android/tools/r8/R8TestRunResult.java
index 0afdccf..bbd5836 100644
--- a/src/test/java/com/android/tools/r8/R8TestRunResult.java
+++ b/src/test/java/com/android/tools/r8/R8TestRunResult.java
@@ -94,8 +94,7 @@
   }
 
   public <E extends Throwable> R8TestRunResult inspectStackTrace(
-      ThrowingBiConsumer<StackTrace, CodeInspector, E> consumer)
-      throws E, IOException, ExecutionException {
+      ThrowingBiConsumer<StackTrace, CodeInspector, E> consumer) throws E, IOException {
     consumer.accept(getStackTrace(), new CodeInspector(app, proguardMap));
     return self();
   }
diff --git a/src/test/java/com/android/tools/r8/TestBase.java b/src/test/java/com/android/tools/r8/TestBase.java
index 5669af0..e1d4c9e 100644
--- a/src/test/java/com/android/tools/r8/TestBase.java
+++ b/src/test/java/com/android/tools/r8/TestBase.java
@@ -1702,6 +1702,16 @@
     return AndroidApiLevel.L;
   }
 
+  public static boolean canUseJavaUtilObjects(TestParameters parameters) {
+    return parameters.isCfRuntime()
+        || parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.K);
+  }
+
+  public static boolean canUseRequireNonNull(TestParameters parameters) {
+    return parameters.isDexRuntime()
+        && parameters.getApiLevel().isGreaterThanOrEqualTo(AndroidApiLevel.K);
+  }
+
   public Path compileToZip(
       TestParameters parameters, Collection<Class<?>> classPath, Class<?>... compilationUnit)
       throws Exception {
diff --git a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
index abdfb83..f196151 100644
--- a/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestCompilerBuilder.java
@@ -47,7 +47,7 @@
       options -> {
         options.testing.allowClassInlinerGracefulExit = false;
         options.testing.reportUnusedProguardConfigurationRules = true;
-        options.enableHorizontalClassMerging = true;
+        options.horizontalClassMergerOptions().enable();
       };
 
   final Backend backend;
@@ -130,7 +130,6 @@
 
   public T addHorizontallyMergedClassesInspectorIf(
       boolean condition, Consumer<HorizontallyMergedClassesInspector> inspector) {
-
     if (condition) {
       return addHorizontallyMergedClassesInspector(inspector);
     }
diff --git a/src/test/java/com/android/tools/r8/TestParametersBuilder.java b/src/test/java/com/android/tools/r8/TestParametersBuilder.java
index f93d6d6..9b84425 100644
--- a/src/test/java/com/android/tools/r8/TestParametersBuilder.java
+++ b/src/test/java/com/android/tools/r8/TestParametersBuilder.java
@@ -19,10 +19,6 @@
 
 public class TestParametersBuilder {
 
-  // Static computation of VMs configured as available by the testing invocation.
-  private static final List<TestRuntime> availableRuntimes =
-      getAvailableRuntimes().collect(Collectors.toList());
-
   // Predicate describing which test parameters are applicable to the test.
   // Built via the methods found below. Defaults to no applicable parameters, i.e., the emtpy set.
   private Predicate<TestParameters> filter = param -> false;
@@ -154,6 +150,7 @@
 
   private Predicate<AndroidApiLevel> apiLevelFilter = param -> false;
   private List<AndroidApiLevel> explicitApiLevels = new ArrayList<>();
+  private List<TestRuntime> customRuntimes = new ArrayList<>();
 
   private TestParametersBuilder withApiFilter(Predicate<AndroidApiLevel> filter) {
     enableApiLevels = true;
@@ -195,13 +192,23 @@
     return withApiFilter(api -> api.getLevel() < endExclusive.getLevel());
   }
 
+  public TestParametersBuilder withCustomRuntime(TestRuntime runtime) {
+    assert getUnfilteredAvailableRuntimes().noneMatch(r -> r == runtime);
+    customRuntimes.add(runtime);
+    return this;
+  }
+
   public TestParametersCollection build() {
     assert !enableApiLevels || enableApiLevelsForCf || hasDexRuntimeFilter;
-    return new TestParametersCollection(
+    List<TestParameters> availableParameters =
         getAvailableRuntimes()
             .flatMap(this::createParameters)
             .filter(filter)
-            .collect(Collectors.toList()));
+            .collect(Collectors.toList());
+    List<TestParameters> customParameters =
+        customRuntimes.stream().flatMap(this::createParameters).collect(Collectors.toList());
+    availableParameters.addAll(customParameters);
+    return new TestParametersCollection(availableParameters);
   }
 
   public Stream<TestParameters> createParameters(TestRuntime runtime) {
diff --git a/src/test/java/com/android/tools/r8/TestRuntime.java b/src/test/java/com/android/tools/r8/TestRuntime.java
index db1ca7f..04b61c0 100644
--- a/src/test/java/com/android/tools/r8/TestRuntime.java
+++ b/src/test/java/com/android/tools/r8/TestRuntime.java
@@ -27,6 +27,7 @@
     JDK9("jdk9", 53),
     JDK10("jdk10", 54),
     JDK11("jdk11", 55),
+    JDK15("jdk15", 59),
     ;
 
     private final String name;
@@ -67,6 +68,7 @@
   private static final Path JDK9_PATH =
       Paths.get(ToolHelper.THIRD_PARTY_DIR, "openjdk", "openjdk-9.0.4");
   private static final Path JDK11_PATH = Paths.get(ToolHelper.THIRD_PARTY_DIR, "openjdk", "jdk-11");
+  private static final Path JDK15_PATH = Paths.get(ToolHelper.THIRD_PARTY_DIR, "openjdk", "jdk-15");
 
   public static CfRuntime getCheckedInJdk8() {
     Path home;
@@ -81,30 +83,28 @@
     return new CfRuntime(CfVm.JDK8, home);
   }
 
-  public static CfRuntime getCheckedInJdk9() {
-    Path home;
+  private static Path getCheckedInJdkHome(Path path) {
     if (ToolHelper.isLinux()) {
-      home = JDK9_PATH.resolve("linux");
+      return path.resolve("linux");
     } else if (ToolHelper.isMac()) {
-      home = JDK9_PATH.resolve("osx");
+      return path.resolve("osx");
     } else {
       assert ToolHelper.isWindows();
-      home = JDK9_PATH.resolve("windows");
+      return path.resolve("windows");
     }
-    return new CfRuntime(CfVm.JDK9, home);
+  }
+
+  public static CfRuntime getCheckedInJdk9() {
+    return new CfRuntime(CfVm.JDK9, getCheckedInJdkHome(JDK9_PATH));
   }
 
   public static CfRuntime getCheckedInJdk11() {
-    Path home;
-    if (ToolHelper.isLinux()) {
-      home = JDK11_PATH.resolve("Linux");
-    } else if (ToolHelper.isMac()) {
-      home = Paths.get(JDK11_PATH.toString(), "Mac", "Contents", "Home");
-    } else {
-      assert ToolHelper.isWindows();
-      home = JDK11_PATH.resolve("Windows");
-    }
-    return new CfRuntime(CfVm.JDK11, home);
+    return new CfRuntime(CfVm.JDK11, getCheckedInJdkHome(JDK11_PATH));
+  }
+
+  // TODO(b/169692487): Add this to 'getCheckedInCfRuntimes' when we start having support for JDK15.
+  public static CfRuntime getCheckedInJdk15() {
+    return new CfRuntime(CfVm.JDK15, getCheckedInJdkHome(JDK15_PATH));
   }
 
   public static List<CfRuntime> getCheckedInCfRuntimes() {
diff --git a/src/test/java/com/android/tools/r8/ToolHelper.java b/src/test/java/com/android/tools/r8/ToolHelper.java
index 3ad6107..367a8f3 100644
--- a/src/test/java/com/android/tools/r8/ToolHelper.java
+++ b/src/test/java/com/android/tools/r8/ToolHelper.java
@@ -10,6 +10,7 @@
 import static org.junit.Assert.fail;
 
 import com.android.tools.r8.DeviceRunner.DeviceRunnerConfigurationException;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestBase.Backend;
 import com.android.tools.r8.TestRuntime.CfRuntime;
 import com.android.tools.r8.ToolHelper.DexVm.Kind;
@@ -121,8 +122,8 @@
   public static final String JAVA_CLASSES_DIR = BUILD_DIR + "classes/java/";
   public static final String JDK_11_TESTS_CLASSES_DIR = JAVA_CLASSES_DIR + "jdk11Tests/";
 
-  public static final String ASM_JAR = BUILD_DIR + "deps/asm-8.0.jar";
-  public static final String ASM_UTIL_JAR = BUILD_DIR + "deps/asm-util-8.0.jar";
+  public static final String ASM_JAR = BUILD_DIR + "deps/asm-9.0.jar";
+  public static final String ASM_UTIL_JAR = BUILD_DIR + "deps/asm-util-9.0.jar";
 
   public static final Path API_SAMPLE_JAR = Paths.get("tests", "r8_api_usage_sample.jar");
 
@@ -140,17 +141,7 @@
   public static final String RHINO_ANDROID_JAR =
       "third_party/rhino-android-1.1.1/rhino-android-1.1.1.jar";
   public static final String RHINO_JAR = "third_party/rhino-1.7.10/rhino-1.7.10.jar";
-  static final String KT_PRELOADER =
-      "third_party/kotlin/kotlin-compiler-1.3.72/kotlinc/lib/kotlin-preloader.jar";
-  public static final String KT_COMPILER =
-      "third_party/kotlin/kotlin-compiler-1.3.72/kotlinc/lib/kotlin-compiler.jar";
   public static final String K2JVMCompiler = "org.jetbrains.kotlin.cli.jvm.K2JVMCompiler";
-  public static final String KT_STDLIB =
-      "third_party/kotlin/kotlin-compiler-1.3.72/kotlinc/lib/kotlin-stdlib.jar";
-  public static final String KT_REFLECT =
-      "third_party/kotlin/kotlin-compiler-1.3.72/kotlinc/lib/kotlin-reflect.jar";
-  public static final String KT_SCRIPT_RT =
-      "third_party/kotlin/kotlin-compiler-1.3.72/kotlinc/lib/kotlin-script-runtime.jar";
   private static final String ANDROID_JAR_PATTERN = "third_party/android_jar/lib-v%d/android.jar";
   private static final AndroidApiLevel DEFAULT_MIN_SDK = AndroidApiLevel.I;
 
@@ -280,10 +271,18 @@
         return compareTo(other) > 0;
       }
 
+      public boolean isNewerThanOrEqual(Version other) {
+        return compareTo(other) >= 0;
+      }
+
       public boolean isAtLeast(Version other) {
         return compareTo(other) >= 0;
       }
 
+      public boolean isOlderThan(Version other) {
+        return compareTo(other) < 0;
+      }
+
       public boolean isOlderThanOrEqual(Version other) {
         return compareTo(other) <= 0;
       }
@@ -841,16 +840,22 @@
     throw new Unreachable("Unable to find a most recent android.jar");
   }
 
-  public static Path getKotlinStdlibJar() {
-    Path path = Paths.get(KT_STDLIB);
-    assert Files.exists(path) : "Expected kotlin stdlib jar";
-    return path;
+  public static Path getKotlinStdlibJar(KotlinCompiler kotlinc) {
+    Path stdLib = kotlinc.getFolder().resolve("kotlin-stdlib.jar");
+    assert Files.exists(stdLib) : "Expected kotlin stdlib jar";
+    return stdLib;
   }
 
-  public static Path getKotlinReflectJar() {
-    Path path = Paths.get(KT_REFLECT);
-    assert Files.exists(path) : "Expected kotlin reflect jar";
-    return path;
+  public static Path getKotlinReflectJar(KotlinCompiler kotlinc) {
+    Path reflectJar = kotlinc.getFolder().resolve("kotlin-reflect.jar");
+    assert Files.exists(reflectJar) : "Expected kotlin reflect jar";
+    return reflectJar;
+  }
+
+  public static Path getKotlinScriptRuntime(KotlinCompiler kotlinc) {
+    Path reflectJar = kotlinc.getFolder().resolve("kotlin-script-runtime.jar");
+    assert Files.exists(reflectJar) : "Expected kotlin script runtime jar";
+    return reflectJar;
   }
 
   public static Path getJdwpTestsCfJarPath(AndroidApiLevel minSdk) {
@@ -1355,41 +1360,6 @@
     return runJava(runtime, ImmutableList.of(), classpath, args);
   }
 
-  @Deprecated
-  public static ProcessResult runKotlinc(
-      List<Path> classPaths,
-      Path directoryToCompileInto,
-      List<String> extraOptions,
-      Path... filesToCompile)
-      throws IOException {
-    List<String> cmdline = new ArrayList<>(Arrays.asList(getJavaExecutable()));
-    cmdline.add("-jar");
-    cmdline.add(KT_PRELOADER);
-    cmdline.add("org.jetbrains.kotlin.preloading.Preloader");
-    cmdline.add("-cp");
-    cmdline.add(KT_COMPILER);
-    cmdline.add(K2JVMCompiler);
-    String[] strings = Arrays.stream(filesToCompile).map(Path::toString).toArray(String[]::new);
-    Collections.addAll(cmdline, strings);
-    cmdline.add("-d");
-    cmdline.add(directoryToCompileInto.toString());
-    List<String> cp = classPaths == null ? null
-        : classPaths.stream().map(Path::toString).collect(Collectors.toList());
-    if (cp != null) {
-      cmdline.add("-cp");
-      if (isWindows()) {
-        cmdline.add(String.join(";", cp));
-      } else {
-        cmdline.add(String.join(":", cp));
-      }
-    }
-    if (extraOptions != null) {
-      cmdline.addAll(extraOptions);
-    }
-    ProcessBuilder builder = new ProcessBuilder(cmdline);
-    return ToolHelper.runProcess(builder);
-  }
-
   public static ProcessResult runJava(
       CfRuntime runtime, List<String> vmArgs, List<Path> classpath, String... args)
       throws IOException {
@@ -2160,6 +2130,14 @@
     }
   }
 
+  public static KotlinCompiler getKotlinC_1_3_72() {
+    return new KotlinCompiler("kotlin-compiler-1.3.72");
+  }
+
+  public static KotlinCompiler[] getKotlinCompilers() {
+    return new KotlinCompiler[] {getKotlinC_1_3_72()};
+  }
+
   public static void disassemble(AndroidApp app, PrintStream ps)
       throws IOException, ExecutionException {
     DexApplication application =
diff --git a/src/test/java/com/android/tools/r8/annotations/SourceDebugExtensionTest.java b/src/test/java/com/android/tools/r8/annotations/SourceDebugExtensionTest.java
index 524cbfa..607f96d 100644
--- a/src/test/java/com/android/tools/r8/annotations/SourceDebugExtensionTest.java
+++ b/src/test/java/com/android/tools/r8/annotations/SourceDebugExtensionTest.java
@@ -4,16 +4,16 @@
 
 package com.android.tools.r8.annotations;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
 import static com.android.tools.r8.ToolHelper.getFilesInTestFolderRelativeToClass;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.TestRuntime;
 import com.android.tools.r8.TestRuntime.CfRuntime;
 import com.android.tools.r8.ToolHelper;
@@ -25,6 +25,7 @@
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.List;
 import java.util.concurrent.ExecutionException;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -35,14 +36,17 @@
 public class SourceDebugExtensionTest extends TestBase {
 
   private final TestParameters parameters;
+  private final KotlinCompiler kotlinCompiler;
 
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  @Parameters(name = "{0}, kotlinc: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(), getKotlinCompilers());
   }
 
-  public SourceDebugExtensionTest(TestParameters parameters) {
+  public SourceDebugExtensionTest(TestParameters parameters, KotlinCompiler kotlinCompiler) {
     this.parameters = parameters;
+    this.kotlinCompiler = kotlinCompiler;
   }
 
   @Test
@@ -50,7 +54,7 @@
     CfRuntime cfRuntime =
         parameters.isCfRuntime() ? parameters.getRuntime().asCf() : TestRuntime.getCheckedInJdk9();
     Path kotlinSources =
-        kotlinc(cfRuntime, getStaticTemp(), KOTLINC, KotlinTargetVersion.JAVA_8)
+        kotlinc(cfRuntime, getStaticTemp(), kotlinCompiler, KotlinTargetVersion.JAVA_8)
             .addSourceFiles(
                 getFilesInTestFolderRelativeToClass(
                     KotlinInlineFunctionRetraceTest.class, "kt", ".kt"))
@@ -58,7 +62,7 @@
     CodeInspector kotlinInspector = new CodeInspector(kotlinSources);
     inspectSourceDebugExtension(kotlinInspector);
     testForR8(parameters.getBackend())
-        .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
+        .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinCompiler))
         .addProgramFiles(kotlinSources)
         .addKeepAttributes(ProguardKeepAttributes.SOURCE_DEBUG_EXTENSION)
         .addKeepAllClassesRule()
diff --git a/src/test/java/com/android/tools/r8/cf/bootstrap/KotlinCompilerTreeShakingTest.java b/src/test/java/com/android/tools/r8/cf/bootstrap/KotlinCompilerTreeShakingTest.java
index 693eced..7aa4205 100644
--- a/src/test/java/com/android/tools/r8/cf/bootstrap/KotlinCompilerTreeShakingTest.java
+++ b/src/test/java/com/android/tools/r8/cf/bootstrap/KotlinCompilerTreeShakingTest.java
@@ -3,13 +3,12 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.cf.bootstrap;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.internal.CompilationTestBase;
@@ -38,27 +37,29 @@
           "Hello.kt");
   private static final int MAX_SIZE = (int) (31361268 * 0.4);
 
-  private TestParameters parameters;
+  private final TestParameters parameters;
+  private final KotlinCompiler kotlinCompiler;
 
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withCfRuntimes().build();
+  @Parameters(name = "{0}, kotlinc: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(getTestParameters().withCfRuntimes().build(), getKotlinCompilers());
   }
 
-  public KotlinCompilerTreeShakingTest(TestParameters parameters) {
+  public KotlinCompilerTreeShakingTest(TestParameters parameters, KotlinCompiler kotlinCompiler) {
     this.parameters = parameters;
+    this.kotlinCompiler = kotlinCompiler;
   }
 
   @Test
   public void testForRuntime() throws Exception {
     // Compile Hello.kt and make sure it works as expected.
     Path classPathBefore =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, KotlinTargetVersion.JAVA_8)
+        kotlinc(parameters.getRuntime().asCf(), kotlinCompiler, KotlinTargetVersion.JAVA_8)
             .addSourceFiles(HELLO_KT)
             .setOutputPath(temp.newFolder().toPath())
             .compile();
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar())
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinCompiler))
         .addClasspath(classPathBefore)
         .run(parameters.getRuntime(), PKG_NAME + ".HelloKt")
         .assertSuccessWithOutputLines("I'm Woody. Howdy, howdy, howdy.");
@@ -69,41 +70,40 @@
           + "b/144877828: assertion error in method naming state during interface method renaming; "
           + "b/144859533: umbrella"
   )
+
   @Test
   public void test() throws Exception {
     List<Path> libs =
         ImmutableList.of(
-            ToolHelper.getKotlinStdlibJar(),
-            ToolHelper.getKotlinReflectJar(),
-            Paths.get(ToolHelper.KT_SCRIPT_RT));
+            ToolHelper.getKotlinStdlibJar(kotlinCompiler),
+            ToolHelper.getKotlinReflectJar(kotlinCompiler),
+            ToolHelper.getKotlinScriptRuntime(kotlinCompiler));
     // Process kotlin-compiler.jar.
     Path r8ProcessedKotlinc =
         testForR8(parameters.getBackend())
             .addLibraryFiles(libs)
             .addLibraryFiles(ToolHelper.getJava8RuntimeJar())
-            .addProgramFiles(Paths.get(ToolHelper.KT_COMPILER))
+            .addProgramFiles(kotlinCompiler.getCompiler())
             .addKeepAttributes("*Annotation*")
             .addKeepClassAndMembersRules(ToolHelper.K2JVMCompiler)
             .addKeepClassAndMembersRules("**.K2JVMCompilerArguments")
             .addKeepClassAndMembersRules("**.*Argument*")
             .addKeepClassAndMembersRules("**.Freezable")
             .addKeepRules(
-                "-keepclassmembers class * {",
-                "  *** parseCommandLineArguments(...);",
-                "}"
-            )
+                "-keepclassmembers class * {", "  *** parseCommandLineArguments(...);", "}")
             .addKeepRules(
                 "-keepclassmembers,allowoptimization enum * {",
                 "    public static **[] values();",
                 "    public static ** valueOf(java.lang.String);",
                 "}")
-            .addOptionsModification(o -> {
-              // Ignore com.sun.tools.javac.main.JavaCompiler and others
-              // Resulting jar may not be able to deal with .java source files, though.
-              o.ignoreMissingClasses = true;
-              // b/144861100: invoke-static on interface is allowed up to JDK 8.
-              o.testing.allowInvokeErrors = true;
-            })
+            .addOptionsModification(
+                o -> {
+                  // Ignore com.sun.tools.javac.main.JavaCompiler and others
+                  // Resulting jar may not be able to deal with .java source files, though.
+                  o.ignoreMissingClasses = true;
+                  // b/144861100: invoke-static on interface is allowed up to JDK 8.
+                  o.testing.allowInvokeErrors = true;
+                })
             .compile()
             .writeToZip();
 
@@ -125,7 +125,7 @@
             .setOutputPath(temp.newFolder().toPath())
             .compile();
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar())
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinCompiler))
         .addClasspath(classPathAfter)
         .run(parameters.getRuntime(), PKG_NAME + ".HelloKt")
         .assertSuccessWithOutputLines("I'm Woody. Howdy, howdy, howdy.");
diff --git a/src/test/java/com/android/tools/r8/classFiltering/ClassFilteringTest.java b/src/test/java/com/android/tools/r8/classFiltering/ClassFilteringTest.java
index 3c6aab6..f8a97c4 100644
--- a/src/test/java/com/android/tools/r8/classFiltering/ClassFilteringTest.java
+++ b/src/test/java/com/android/tools/r8/classFiltering/ClassFilteringTest.java
@@ -4,11 +4,8 @@
 
 package com.android.tools.r8.classFiltering;
 
-import com.android.tools.r8.ArchiveProgramResourceProvider;
 import com.android.tools.r8.CompilationFailedException;
-import com.android.tools.r8.ProgramResource;
-import com.android.tools.r8.ProgramResourceProvider;
-import com.android.tools.r8.ResourceException;
+import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -118,14 +115,16 @@
 
   @Test
   public void testDexMergingWithChecksumMissing() throws Exception {
-    // Step #1: Build the dex file seperately as an incremental build tools usually do but this time
+    // Step #1: Build the dex file separately as an incremental build tools usually do but this time
     // make one of the dex file missing checksum information.
-    Path[] dexInput = new Path[] {
-        buildDex(TestClass.class,true, null),
-        buildDex(TestClass.Keep.class,true, null),
-        buildDex(TestClass.Remove.class, false, null)};
+    Path[] dexInput =
+        new Path[] {
+          buildDex(TestClass.class, true, null),
+          buildDex(TestClass.Keep.class, true, null),
+          buildDex(TestClass.Remove.class, false, null)
+        };
 
-    // Step #2: Now use D8 as a merging tool and verify that the compilation fails as expect.
+    // Step #2: Now use D8 as a merging tool and verify that the compilation fails as expected.
     try {
       testForD8()
           .setMinApi(parameters.getApiLevel())
@@ -140,10 +139,8 @@
     }
   }
 
-  /*
   @Test
-  public void testMultidexOutput()
-      throws CompilationFailedException, IOException, ExecutionException, ResourceException {
+  public void testDexFilePerClassFilteringOutput() throws Exception {
     // Step #1: Build the program pretending to be multidex files with DexPerClass.
     final Path outZip = testForD8()
         .setMinApi(parameters.getApiLevel())
@@ -153,22 +150,15 @@
         .compile()
         .writeToZip();
 
-    final long crc = ToolHelper.getClassByteCrc(TestClass.Remove.class);
-
     // Step #2: Verify that the checksums are present and filtering is working as expected.
-    ProgramResourceProvider filter = new ArchiveProvider(outZip) {
-      @Override
-      public boolean includeClassWithChecksum(String classDescriptor, Long checksum) {
-        return !checksum.equals(crc);
-      }
-    };
+    final long crc = ToolHelper.getClassByteCrc(TestClass.Remove.class);
     testForD8()
-        .addProgramResourceProvider(filter)
+        .addProgramFiles(outZip)
+        .apply(b -> b.getBuilder().setDexClassChecksumFilter((desc, checksum) -> checksum != crc))
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), TestClass.class)
         .assertSuccessWithOutput("Keep No Remove ");
   }
-  */
 
   @Test
   public void testLambdaChecksum() throws Exception {
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
index 4d7d7fe..42df5c0 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/AbstractMethodMergingNonTrivialTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/AbstractMethodMergingNonTrivialTest.java
@@ -23,7 +23,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .addHorizontallyMergedClassesInspectorIf(
             enableHorizontalClassMerging, inspector -> inspector.assertMergedInto(B.class, A.class))
         .enableInliningAnnotations()
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
index cc092e9..3b0af4f 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/AbstractMethodMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/AbstractMethodMergingTest.java
@@ -23,7 +23,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .addHorizontallyMergedClassesInspectorIf(
             enableHorizontalClassMerging, inspector -> inspector.assertMergedInto(B.class, A.class))
         .enableInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/AdaptResourceFileContentsTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/AdaptResourceFileContentsTest.java
index 2c4ac9d..db8d4c7 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/AdaptResourceFileContentsTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/AdaptResourceFileContentsTest.java
@@ -36,7 +36,8 @@
             .addInnerClasses(getClass())
             .addKeepMainRule(Main.class)
             .addOptionsModification(
-                options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+                options ->
+                    options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
             .addOptionsModification(options -> options.dataResourceConsumer = dataResourceConsumer)
             .enableNeverClassInliningAnnotations()
             .addDataEntryResources(
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/AdaptVerticallyMergedResourceFileContentsTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/AdaptVerticallyMergedResourceFileContentsTest.java
index ea50082..0ebbb9d 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/AdaptVerticallyMergedResourceFileContentsTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/AdaptVerticallyMergedResourceFileContentsTest.java
@@ -35,7 +35,8 @@
             .addInnerClasses(getClass())
             .addKeepMainRule(Main.class)
             .addOptionsModification(
-                options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+                options ->
+                    options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
             .addOptionsModification(options -> options.dataResourceConsumer = dataResourceConsumer)
             .enableNeverClassInliningAnnotations()
             .addDataEntryResources(
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
index 25bb4ec..31a49cc 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassWithInstanceFieldsTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassWithInstanceFieldsTest.java
@@ -25,7 +25,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableNeverClassInliningAnnotations()
         .enableInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesDistinguishedByDirectCheckCastTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesDistinguishedByDirectCheckCastTest.java
index 745fc26..bc5380e 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesDistinguishedByDirectCheckCastTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesDistinguishedByDirectCheckCastTest.java
@@ -24,7 +24,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesDistinguishedByDirectInstanceOfTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesDistinguishedByDirectInstanceOfTest.java
index 6a61d2f..6551ad2 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesDistinguishedByDirectInstanceOfTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesDistinguishedByDirectInstanceOfTest.java
@@ -24,7 +24,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesDistinguishedByIndirectCheckCastToInterfaceTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesDistinguishedByIndirectCheckCastToInterfaceTest.java
index 5eb49b1..6017cbe 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesDistinguishedByIndirectCheckCastToInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesDistinguishedByIndirectCheckCastToInterfaceTest.java
@@ -26,7 +26,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableNoVerticalClassMergingAnnotations()
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesDistuingishedByIndirectInstanceOfInterfaceCheckCast.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesDistuingishedByIndirectInstanceOfInterfaceCheckCast.java
index 70e7c0d..cf05da6 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesDistuingishedByIndirectInstanceOfInterfaceCheckCast.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesDistuingishedByIndirectInstanceOfInterfaceCheckCast.java
@@ -26,7 +26,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .enableNeverClassInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithDifferentInterfacesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithDifferentInterfacesTest.java
index 3c9d066..5abdc09 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithDifferentInterfacesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithDifferentInterfacesTest.java
@@ -26,7 +26,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
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
index 90e407e..68a81b1 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithDifferentVisibilityFieldsTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithDifferentVisibilityFieldsTest.java
@@ -32,7 +32,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithFeatureSplitTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithFeatureSplitTest.java
index 5907c15..9ac569b 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithFeatureSplitTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithFeatureSplitTest.java
@@ -42,7 +42,8 @@
             .addKeepFeatureMainRule(Feature1Main.class)
             .addKeepFeatureMainRule(Feature2Main.class)
             .addOptionsModification(
-                options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+                options ->
+                    options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
             .enableNeverClassInliningAnnotations()
             .setMinApi(parameters.getApiLevel())
             .compile()
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 2bab975..6714388 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
@@ -25,7 +25,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithNativeMethodsTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithNativeMethodsTest.java
index 5ee01b3..1f0e3e6 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithNativeMethodsTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithNativeMethodsTest.java
@@ -26,7 +26,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithOverlappingVisibilitiesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithOverlappingVisibilitiesTest.java
index 565c1bd..608f6fe 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithOverlappingVisibilitiesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithOverlappingVisibilitiesTest.java
@@ -29,7 +29,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithStaticFields.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithStaticFields.java
index f00e07e..2810ce8 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithStaticFields.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ClassesWithStaticFields.java
@@ -23,7 +23,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .addHorizontallyMergedClassesInspectorIf(
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/CompanionClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/CompanionClassMergingTest.java
index b14763f..9aca9fe 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/CompanionClassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/CompanionClassMergingTest.java
@@ -27,7 +27,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .addOptionsModification(options -> options.enableClassInlining = false)
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/CompatKeepConstructorLiveTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/CompatKeepConstructorLiveTest.java
index 9ccc2d4..c03361f 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/CompatKeepConstructorLiveTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/CompatKeepConstructorLiveTest.java
@@ -25,7 +25,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Main.class)
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorCantInlineTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorCantInlineTest.java
index 2383a03..935e75e 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorCantInlineTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorCantInlineTest.java
@@ -27,7 +27,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingAfterUnusedArgumentRemovalTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingAfterUnusedArgumentRemovalTest.java
index 32f7454..d38efb3 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingAfterUnusedArgumentRemovalTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingAfterUnusedArgumentRemovalTest.java
@@ -22,7 +22,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .addHorizontallyMergedClassesInspectorIf(
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingOverlapTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingOverlapTest.java
index 185e090..8936dc1 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingOverlapTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingOverlapTest.java
@@ -32,7 +32,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
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 f658909..f878fd5 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
@@ -33,7 +33,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .addHorizontallyMergedClassesInspectorIf(
             enableHorizontalClassMerging,
             inspector ->
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingTest.java
index b1d77c2..21c3486 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingTest.java
@@ -23,7 +23,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Main.class)
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingTrivialOverlapTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingTrivialOverlapTest.java
index f6def29..50da81e 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingTrivialOverlapTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingTrivialOverlapTest.java
@@ -32,7 +32,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingWithArgumentsTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingWithArgumentsTest.java
index a4a0fd9..7891de2 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingWithArgumentsTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ConstructorMergingWithArgumentsTest.java
@@ -26,7 +26,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Main.class)
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/DistinguishExceptionClassesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/DistinguishExceptionClassesTest.java
index c751735..749ae58 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/DistinguishExceptionClassesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/DistinguishExceptionClassesTest.java
@@ -22,7 +22,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Main.class)
         .assertSuccessWithOutputLines("test success")
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/EmptyClassTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/EmptyClassTest.java
index 4cdca16..5d63188 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/EmptyClassTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/EmptyClassTest.java
@@ -25,7 +25,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .addHorizontallyMergedClassesInspectorIf(
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/FieldTypeMergedTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/FieldTypeMergedTest.java
index 616c88a..58434a9 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/FieldTypeMergedTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/FieldTypeMergedTest.java
@@ -29,7 +29,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/GenericStaticFieldTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/GenericStaticFieldTest.java
index 9b86b43..af72afe 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/GenericStaticFieldTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/GenericStaticFieldTest.java
@@ -20,7 +20,8 @@
         .addKeepMainRule(Main.class)
         .addKeepRules("-keepattributes Signatures")
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         // .addHorizontallyMergedClassesInspectorIf(
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 6968e1a..9dae7a2 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
@@ -23,7 +23,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/InheritInterfaceWithDefaultTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/InheritInterfaceWithDefaultTest.java
index 9bdaca6..b069bb1 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/InheritInterfaceWithDefaultTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/InheritInterfaceWithDefaultTest.java
@@ -30,7 +30,8 @@
         .addKeepMainRule(Main.class)
         .allowStdoutMessages()
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/InheritOverrideInterfaceTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/InheritOverrideInterfaceTest.java
index cedee87..8c7644d 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/InheritOverrideInterfaceTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/InheritOverrideInterfaceTest.java
@@ -24,7 +24,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/InheritsFromLibraryClassTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/InheritsFromLibraryClassTest.java
index cfafb12..575c2cd 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/InheritsFromLibraryClassTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/InheritsFromLibraryClassTest.java
@@ -30,7 +30,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/InnerOuterClassesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/InnerOuterClassesTest.java
index 075448a..98e74e4 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/InnerOuterClassesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/InnerOuterClassesTest.java
@@ -22,7 +22,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableNeverClassInliningAnnotations()
         .addKeepAttributes("InnerClasses", "EnclosingMethod")
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/InstantiatedAndUninstantiatedClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/InstantiatedAndUninstantiatedClassMergingTest.java
index a27fdbd..59e77dd 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/InstantiatedAndUninstantiatedClassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/InstantiatedAndUninstantiatedClassMergingTest.java
@@ -36,7 +36,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(TestClass.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .addHorizontallyMergedClassesInspector(
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/JavaLambdaMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/JavaLambdaMergingTest.java
new file mode 100644
index 0000000..adabb3e
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/JavaLambdaMergingTest.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.horizontal;
+
+import static com.android.tools.r8.ir.desugar.LambdaRewriter.LAMBDA_CLASS_NAME_PREFIX;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.utils.codeinspector.VerticallyMergedClassesInspector;
+import java.util.Set;
+import java.util.stream.Collectors;
+import org.junit.Test;
+
+public class JavaLambdaMergingTest extends HorizontalClassMergingTestBase {
+
+  public JavaLambdaMergingTest(TestParameters parameters, boolean enableHorizontalClassMerging) {
+    super(parameters, enableHorizontalClassMerging);
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addOptionsModification(
+            options -> {
+              options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging);
+              assertFalse(options.horizontalClassMergerOptions().isJavaLambdaMergingEnabled());
+              options.horizontalClassMergerOptions().enableJavaLambdaMerging();
+            })
+        .addHorizontallyMergedClassesInspectorIf(
+            enableHorizontalClassMerging && parameters.isDexRuntime(),
+            inspector -> {
+              Set<DexType> lambdaSources =
+                  inspector.getSources().stream()
+                      .filter(x -> x.toSourceString().contains(LAMBDA_CLASS_NAME_PREFIX))
+                      .collect(Collectors.toSet());
+              assertEquals(3, lambdaSources.size());
+              DexType firstTarget = inspector.getTarget(lambdaSources.iterator().next());
+              for (DexType lambdaSource : lambdaSources) {
+                assertTrue(
+                    inspector
+                        .getTarget(lambdaSource)
+                        .toSourceString()
+                        .contains(LAMBDA_CLASS_NAME_PREFIX));
+                assertEquals(firstTarget, inspector.getTarget(lambdaSource));
+              }
+            })
+        .addVerticallyMergedClassesInspector(
+            VerticallyMergedClassesInspector::assertNoClassesMerged)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("Hello world!");
+  }
+
+  public static class Main {
+
+    public static void main(String[] args) {
+      HelloGreeter helloGreeter =
+          System.currentTimeMillis() > 0
+              ? () -> System.out.print("Hello")
+              : () -> {
+                throw new RuntimeException();
+              };
+      WorldGreeter worldGreeter =
+          System.currentTimeMillis() > 0
+              ? () -> System.out.println(" world!")
+              : () -> {
+                throw new RuntimeException();
+              };
+      helloGreeter.hello();
+      worldGreeter.world();
+    }
+  }
+
+  interface HelloGreeter {
+
+    void hello();
+  }
+
+  interface WorldGreeter {
+
+    void world();
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/LargeConstructorsMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/LargeConstructorsMergingTest.java
index 847db55..a553abe 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/LargeConstructorsMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/LargeConstructorsMergingTest.java
@@ -26,7 +26,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .addOptionsModification(options -> options.testing.verificationSizeLimitInBytesOverride = 4)
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergeNonFinalAndFinalClassTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergeNonFinalAndFinalClassTest.java
index a5eb885..c8a36b6 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergeNonFinalAndFinalClassTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergeNonFinalAndFinalClassTest.java
@@ -25,7 +25,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .addHorizontallyMergedClassesInspectorIf(
             enableHorizontalClassMerging, inspector -> inspector.assertMergedInto(B.class, A.class))
         .enableNeverClassInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergePackagePrivateWithPublicClassTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergePackagePrivateWithPublicClassTest.java
index a1c8faa..04d69b1d 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergePackagePrivateWithPublicClassTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergePackagePrivateWithPublicClassTest.java
@@ -27,7 +27,8 @@
             PackagePrivateClassRunner.class, PackagePrivateClassRunner.getPrivateClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorForwardingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorForwardingTest.java
index 2256e5d..f913560 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorForwardingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorForwardingTest.java
@@ -32,7 +32,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorStackTraceTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorStackTraceTest.java
index fc4f076..a11a7d6 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorStackTraceTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedConstructorStackTraceTest.java
@@ -46,7 +46,8 @@
         .addKeepAttributeLineNumberTable()
         .addKeepAttributeSourceFile()
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableNoVerticalClassMergingAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
@@ -63,7 +64,9 @@
                             2,
                             StackTraceLine.builder()
                                 .setClassName(A.class.getTypeName())
-                                .setMethodName("<init>")
+                                // TODO(b/124483578): The synthetic method should not be part of the
+                                //  retraced stack trace.
+                                .setMethodName("$r8$init$bridge")
                                 .setFileName(getClass().getSimpleName() + ".java")
                                 .setLineNumber(0)
                                 .build())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedVirtualMethodStackTraceTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedVirtualMethodStackTraceTest.java
index 00d6e59..8c9d11d 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedVirtualMethodStackTraceTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergedVirtualMethodStackTraceTest.java
@@ -46,7 +46,8 @@
         .addKeepAttributeSourceFile()
         .addDontWarn(C.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
@@ -64,7 +65,6 @@
                 StackTrace expectedStackTraceWithMergedMethod =
                     StackTrace.builder()
                         .add(expectedStackTrace)
-
                         .add(
                             1,
                             StackTraceLine.builder()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergingProducesFieldCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergingProducesFieldCollisionTest.java
index d25d516..34972f6 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/MergingProducesFieldCollisionTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/MergingProducesFieldCollisionTest.java
@@ -31,7 +31,8 @@
         .addProgramClassFileData(transformedC)
         .addProgramClasses(Parent.class, A.class, B.class, Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/NestClassTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/NestClassTest.java
index 5edc29f..b416826 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/NestClassTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/NestClassTest.java
@@ -58,7 +58,8 @@
         .addKeepMainRule(examplesTypeName(BasicNestHostHorizontalClassMerging.class))
         .addExamplesProgramFiles(R.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .compile()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/NoAbstractClassesWithNonAbstractClassesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/NoAbstractClassesWithNonAbstractClassesTest.java
index 2bc2c97..5566f45 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/NoAbstractClassesWithNonAbstractClassesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/NoAbstractClassesWithNonAbstractClassesTest.java
@@ -26,7 +26,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/NoClassesOrMembersWithAnnotationsTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/NoClassesOrMembersWithAnnotationsTest.java
index d17fa75..74d0ad6 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/NoClassesOrMembersWithAnnotationsTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/NoClassesOrMembersWithAnnotationsTest.java
@@ -31,7 +31,8 @@
         .addKeepMainRule(Main.class)
         .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableNeverClassInliningAnnotations()
         .enableInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/NoHorizontalClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/NoHorizontalClassMergingTest.java
index dc513c3..5c0e207 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/NoHorizontalClassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/NoHorizontalClassMergingTest.java
@@ -24,7 +24,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableNoHorizontalClassMergingAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/NonReboundFieldAccessOnMergedClassTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/NonReboundFieldAccessOnMergedClassTest.java
index e357250..671f740 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/NonReboundFieldAccessOnMergedClassTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/NonReboundFieldAccessOnMergedClassTest.java
@@ -26,7 +26,8 @@
         .addInnerClasses(NonReboundFieldAccessOnMergedClassTestClasses.class)
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .addHorizontallyMergedClassesInspector(
             inspector -> {
               if (enableHorizontalClassMerging) {
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/NonReboundFieldAccessWithMergedTypeTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/NonReboundFieldAccessWithMergedTypeTest.java
index 3afcfb2..b6183a3 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/NonReboundFieldAccessWithMergedTypeTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/NonReboundFieldAccessWithMergedTypeTest.java
@@ -27,7 +27,8 @@
         .addInnerClasses(NonReboundFieldAccessWithMergedTypeTestClasses.class)
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .addHorizontallyMergedClassesInspector(
             inspector -> {
               if (enableHorizontalClassMerging) {
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/OverlappingConstructorsTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/OverlappingConstructorsTest.java
index fbd587d..c97e384 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/OverlappingConstructorsTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/OverlappingConstructorsTest.java
@@ -25,7 +25,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Main.class)
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/PackagePrivateMemberAccessTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/PackagePrivateMemberAccessTest.java
index a1b0951..2732240 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/PackagePrivateMemberAccessTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/PackagePrivateMemberAccessTest.java
@@ -28,7 +28,8 @@
         .addProgramClasses(B.class)
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .allowAccessModification(false)
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/PackagePrivateMembersAccessedTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/PackagePrivateMembersAccessedTest.java
index cdaeee7..4d7c0c4 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/PackagePrivateMembersAccessedTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/PackagePrivateMembersAccessedTest.java
@@ -27,7 +27,8 @@
         .addProgramClasses(D.class)
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .allowAccessModification(false)
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/PinnedClassMemberReferenceTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/PinnedClassMemberReferenceTest.java
index 4a22fe3..2f01f90 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/PinnedClassMemberReferenceTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/PinnedClassMemberReferenceTest.java
@@ -28,7 +28,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .noMinification()
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/PinnedClassMemberTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/PinnedClassMemberTest.java
index 444d573..1e09a5a 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/PinnedClassMemberTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/PinnedClassMemberTest.java
@@ -23,7 +23,8 @@
         .addKeepMainRule(Main.class)
         .addKeepRules("-keepclassmembers class " + B.class.getTypeName() + " { void foo(); }")
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Main.class)
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/PinnedClassTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/PinnedClassTest.java
index a2b36f3..0ba06e6 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/PinnedClassTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/PinnedClassTest.java
@@ -23,7 +23,8 @@
         .addKeepMainRule(Main.class)
         .addKeepClassRules(B.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Main.class)
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/PreventMergeMainDexListTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/PreventMergeMainDexListTest.java
index 4a50200..e3e1b85 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/PreventMergeMainDexListTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/PreventMergeMainDexListTest.java
@@ -48,7 +48,7 @@
         .addMainDexListClasses(A.class, Main.class)
         .addOptionsModification(
             options -> {
-              options.enableHorizontalClassMerging = enableHorizontalClassMerging;
+              options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging);
               options.minimalMainDex = true;
             })
         .enableNeverClassInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/PreventMergeMainDexTracingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/PreventMergeMainDexTracingTest.java
index 9e81bc6..6a31a5e 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/PreventMergeMainDexTracingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/PreventMergeMainDexTracingTest.java
@@ -48,7 +48,7 @@
         .addMainDexClassRules(Main.class)
         .addOptionsModification(
             options -> {
-              options.enableHorizontalClassMerging = enableHorizontalClassMerging;
+              options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging);
               options.minimalMainDex = true;
             })
         .enableNeverClassInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/PrivateAndInterfaceMethodCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/PrivateAndInterfaceMethodCollisionTest.java
index af37199..4553469 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/PrivateAndInterfaceMethodCollisionTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/PrivateAndInterfaceMethodCollisionTest.java
@@ -25,7 +25,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .addHorizontallyMergedClassesInspector(
             HorizontallyMergedClassesInspector::assertNoClassesMerged)
         .enableInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/PrivateAndStaticMethodCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/PrivateAndStaticMethodCollisionTest.java
index df03d8a..3c7b799 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/PrivateAndStaticMethodCollisionTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/PrivateAndStaticMethodCollisionTest.java
@@ -22,7 +22,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .addHorizontallyMergedClassesInspectorIf(
             enableHorizontalClassMerging, inspector -> inspector.assertMergedInto(B.class, A.class))
         .enableInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ReferencedInAnnotationTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ReferencedInAnnotationTest.java
index 620f948..385d782 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ReferencedInAnnotationTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ReferencedInAnnotationTest.java
@@ -38,7 +38,8 @@
         .addKeepMainRule(TestClass.class)
         .addKeepClassAndMembersRules(Annotation.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .addKeepRuntimeVisibleAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/RemapFieldTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/RemapFieldTest.java
index 33c8a85..168d6ee 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/RemapFieldTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/RemapFieldTest.java
@@ -24,7 +24,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
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 a531860..c640ffc 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,7 +24,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ServiceLoaderParentTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ServiceLoaderParentTest.java
index 4e89fb8..5eab9c9 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ServiceLoaderParentTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ServiceLoaderParentTest.java
@@ -38,7 +38,8 @@
                 Origin.unknown()))
         .enableNoVerticalClassMergingAnnotations()
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Main.class)
         .assertSuccess()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/ServiceLoaderTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/ServiceLoaderTest.java
index ee6dc18..f9ae4bd 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/ServiceLoaderTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/ServiceLoaderTest.java
@@ -36,7 +36,8 @@
                 "META-INF/services/" + A.class.getTypeName(),
                 Origin.unknown()))
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Main.class)
         .assertSuccess()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/StaticAndInterfaceMethodCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/StaticAndInterfaceMethodCollisionTest.java
index ebbfd86..e76dd447 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/StaticAndInterfaceMethodCollisionTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/StaticAndInterfaceMethodCollisionTest.java
@@ -25,7 +25,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .addHorizontallyMergedClassesInspector(
             HorizontallyMergedClassesInspector::assertNoClassesMerged)
         .enableInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/StaticAndVirtualMethodCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/StaticAndVirtualMethodCollisionTest.java
index edca180..35a15e5 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/StaticAndVirtualMethodCollisionTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/StaticAndVirtualMethodCollisionTest.java
@@ -23,7 +23,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .addHorizontallyMergedClassesInspectorIf(
             enableHorizontalClassMerging, inspector -> inspector.assertMergedInto(B.class, A.class))
         .enableInliningAnnotations()
@@ -36,10 +37,10 @@
   static class Main {
 
     public static void main(String[] args) {
-      new A().foo();
+      A.foo();
       new A().bar();
       new B().foo();
-      new B().bar();
+      B.bar();
     }
   }
 
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/SuperConstructorCallsVirtualMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/SuperConstructorCallsVirtualMethodTest.java
index 119913b..ecbd84b 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/SuperConstructorCallsVirtualMethodTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/SuperConstructorCallsVirtualMethodTest.java
@@ -25,7 +25,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/SynchronizedClassesTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/SynchronizedClassesTest.java
index e5c77a3..6f458b7 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/SynchronizedClassesTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/SynchronizedClassesTest.java
@@ -26,7 +26,7 @@
         .addKeepMainRule(Main.class)
         .addOptionsModification(
             options -> {
-              options.enableHorizontalClassMerging = enableHorizontalClassMerging;
+              options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging);
             })
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/SyntheticConstructorArgumentsMerged.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/SyntheticConstructorArgumentsMerged.java
index 59ea0b2..6567764 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/SyntheticConstructorArgumentsMerged.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/SyntheticConstructorArgumentsMerged.java
@@ -26,7 +26,7 @@
         .addKeepMainRule(Main.class)
         .addOptionsModification(
             options -> {
-              options.enableHorizontalClassMerging = enableHorizontalClassMerging;
+              options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging);
             })
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerCollisionTest.java
index b603f66..97e41db 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerCollisionTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerCollisionTest.java
@@ -26,7 +26,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerConstructorCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerConstructorCollisionTest.java
index a5c5555..c7a7d9c 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerConstructorCollisionTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerConstructorCollisionTest.java
@@ -30,7 +30,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerInterfaceCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerInterfaceCollisionTest.java
index f1301a0..946e113 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerInterfaceCollisionTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerInterfaceCollisionTest.java
@@ -36,7 +36,8 @@
         .addKeepMainRule(Main.class)
         .noMinification()
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerInterfaceFixedCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerInterfaceFixedCollisionTest.java
index 2f6895a..1d3290a 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerInterfaceFixedCollisionTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerInterfaceFixedCollisionTest.java
@@ -38,7 +38,8 @@
         .addKeepMainRule(Main.class)
         .noMinification()
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerInterfaceImplementedByParentTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerInterfaceImplementedByParentTest.java
index b6a320c..afee32c 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerInterfaceImplementedByParentTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerInterfaceImplementedByParentTest.java
@@ -34,7 +34,8 @@
         .addKeepMainRule(Main.class)
         .noMinification()
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerSubClassCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerSubClassCollisionTest.java
index aad0efa..b68f291 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerSubClassCollisionTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/TreeFixerSubClassCollisionTest.java
@@ -29,7 +29,8 @@
         .addKeepMainRule(Main.class)
         .noMinification()
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/VerticalMergingPreoptimizedTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/VerticalMergingPreoptimizedTest.java
index 7731d5e..b1c0b4a 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/VerticalMergingPreoptimizedTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/VerticalMergingPreoptimizedTest.java
@@ -28,7 +28,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoHorizontalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/VerticallyMergedClassDistinguishedByCheckCastTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/VerticallyMergedClassDistinguishedByCheckCastTest.java
index 9ecbf5a..366800a 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/VerticallyMergedClassDistinguishedByCheckCastTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/VerticallyMergedClassDistinguishedByCheckCastTest.java
@@ -23,7 +23,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Main.class)
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/VerticallyMergedClassDistinguishedByInstanceOfTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/VerticallyMergedClassDistinguishedByInstanceOfTest.java
index bba42bb..c8ad21d 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/VerticallyMergedClassDistinguishedByInstanceOfTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/VerticallyMergedClassDistinguishedByInstanceOfTest.java
@@ -23,7 +23,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
         .run(parameters.getRuntime(), Main.class)
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/VerticallyMergedClassTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/VerticallyMergedClassTest.java
index e40ff51..a40b387 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/VerticallyMergedClassTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/VerticallyMergedClassTest.java
@@ -27,7 +27,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableNoHorizontalClassMergingAnnotations()
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/VirtualMethodMergingOfFinalAndNonFinalMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/VirtualMethodMergingOfFinalAndNonFinalMethodTest.java
index f9eff69..6399f3e 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/VirtualMethodMergingOfFinalAndNonFinalMethodTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/VirtualMethodMergingOfFinalAndNonFinalMethodTest.java
@@ -28,7 +28,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(TestClass.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/VirtualMethodMergingOfPublicizedMethodsTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/VirtualMethodMergingOfPublicizedMethodsTest.java
index 8178138..5e23840 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/VirtualMethodMergingOfPublicizedMethodsTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/VirtualMethodMergingOfPublicizedMethodsTest.java
@@ -25,7 +25,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(TestClass.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .allowAccessModification()
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/NotOverlappingTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/NotOverlappingTest.java
index 8742e67..e405aa9 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/NotOverlappingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/NotOverlappingTest.java
@@ -23,7 +23,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideAbstractMethodWithDefaultTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideAbstractMethodWithDefaultTest.java
index 87f4dc3..e456600 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideAbstractMethodWithDefaultTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideAbstractMethodWithDefaultTest.java
@@ -25,7 +25,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultMethodTest.java
index dd8183d..0e3a098 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultMethodTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultMethodTest.java
@@ -26,7 +26,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultOnSuperMethodTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultOnSuperMethodTest.java
index bd41f8f..fa2dcb2 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultOnSuperMethodTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideDefaultOnSuperMethodTest.java
@@ -27,7 +27,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNoUnusedInterfaceRemovalAnnotations()
         .enableNoVerticalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideMergeAbsentTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideMergeAbsentTest.java
index fe0bc23..526aa00 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideMergeAbsentTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideMergeAbsentTest.java
@@ -25,7 +25,8 @@
         .addInnerClasses(getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideParentCollisionTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideParentCollisionTest.java
index 94b643a..639cf19 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideParentCollisionTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/OverrideParentCollisionTest.java
@@ -27,7 +27,8 @@
         .addInnerClasses(this.getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .setMinApi(parameters.getApiLevel())
diff --git a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/SuperMethodMergedTest.java b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/SuperMethodMergedTest.java
index 228223f..44973bc 100644
--- a/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/SuperMethodMergedTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/horizontal/dispatch/SuperMethodMergedTest.java
@@ -26,7 +26,8 @@
         .addInnerClasses(this.getClass())
         .addKeepMainRule(Main.class)
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .enableNeverClassInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
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 2fb36cd..8dc4e01 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
@@ -253,6 +253,7 @@
                 testForR8(parameters.getBackend())
                     .addKeepRules(getProguardConfig(EXAMPLE_KEEP))
                     .addOptionsModification(this::configure)
+                    .addOptionsModification(options -> options.enableValuePropagation = false)
                     .addOptionsModification(
                         options ->
                             options.testing.validInliningReasons = ImmutableSet.of(Reason.FORCE))
@@ -276,10 +277,11 @@
     assertThat(clazzSubject.field("java.lang.String", "name" + suffix), isPresent());
     assertThat(clazzSubject.field("java.lang.String", "name" + suffix + "2"), isPresent());
 
-    // The direct method "constructor$classmerging$ConflictInGeneratedNameTest$A" is processed after
-    // the method "<init>" is renamed to exactly that name. Therefore the conflict should have been
-    // resolved by appending [suffix] to it.
-    assertThat(clazzSubject.method("void", "constructor" + suffix + suffix, EMPTY), isPresent());
+    // The direct method "$r8$constructor$classmerging$ConflictInGeneratedNameTest$A" is processed
+    // after the method "<init>" is renamed to exactly that name. Therefore the conflict should have
+    // been resolved by appending [suffix] to it.
+    assertThat(
+        clazzSubject.method("void", "$r8$constructor" + suffix + suffix, EMPTY), isPresent());
 
     // There should be two foo's.
     assertThat(clazzSubject.method("void", "foo", EMPTY), isPresent());
diff --git a/src/test/java/com/android/tools/r8/debug/KotlinStdLibCompilationTest.java b/src/test/java/com/android/tools/r8/debug/KotlinStdLibCompilationTest.java
index ae1c81c..deb97d8 100644
--- a/src/test/java/com/android/tools/r8/debug/KotlinStdLibCompilationTest.java
+++ b/src/test/java/com/android/tools/r8/debug/KotlinStdLibCompilationTest.java
@@ -3,16 +3,18 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.debug;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestDiagnosticMessages;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersBuilder;
-import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -22,21 +24,25 @@
 public class KotlinStdLibCompilationTest extends TestBase {
 
   private final TestParameters parameters;
+  private final KotlinCompiler kotlinc;
 
-  @Parameters(name = "{0}")
-  public static TestParametersCollection setup() {
-    return TestParametersBuilder.builder().withAllRuntimes().withAllApiLevels().build();
+  @Parameters(name = "{0}, kotlinc: {1}")
+  public static List<Object[]> setup() {
+    return buildParameters(
+        TestParametersBuilder.builder().withAllRuntimes().withAllApiLevels().build(),
+        getKotlinCompilers());
   }
 
-  public KotlinStdLibCompilationTest(TestParameters parameters) {
+  public KotlinStdLibCompilationTest(TestParameters parameters, KotlinCompiler kotlinc) {
     this.parameters = parameters;
+    this.kotlinc = kotlinc;
   }
 
   @Test
   public void testD8() throws CompilationFailedException {
     assumeTrue(parameters.isDexRuntime());
     testForD8()
-        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
         .setMinApi(parameters.getApiLevel())
         .compileWithExpectedDiagnostics(TestDiagnosticMessages::assertNoMessages);
   }
@@ -44,7 +50,7 @@
   @Test
   public void testR8() throws CompilationFailedException {
     testForR8(parameters.getBackend())
-        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
         .noMinification()
         .noTreeShaking()
         .addKeepAllAttributes()
diff --git a/src/test/java/com/android/tools/r8/debuginfo/CannonicalizeWithInline.java b/src/test/java/com/android/tools/r8/debuginfo/CanonicalizeWithInline.java
similarity index 61%
rename from src/test/java/com/android/tools/r8/debuginfo/CannonicalizeWithInline.java
rename to src/test/java/com/android/tools/r8/debuginfo/CanonicalizeWithInline.java
index ff58696..395f29f 100644
--- a/src/test/java/com/android/tools/r8/debuginfo/CannonicalizeWithInline.java
+++ b/src/test/java/com/android/tools/r8/debuginfo/CanonicalizeWithInline.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.debuginfo;
 
+import com.android.tools.r8.AssumeMayHaveSideEffects;
+import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.OutputMode;
 import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.TestBase;
@@ -15,7 +17,7 @@
 import org.junit.Assert;
 import org.junit.Test;
 
-public class CannonicalizeWithInline extends TestBase {
+public class CanonicalizeWithInline extends TestBase {
 
   private int getNumberOfDebugInfos(Path file) throws IOException {
     DexSection[] dexSections = DexParser.parseMapFrom(file);
@@ -28,25 +30,23 @@
   }
 
   @Test
-  public void testCannonicalize() throws Exception {
-    Class clazzA = ClassA.class;
-    Class clazzB = ClassB.class;
+  public void testCanonicalize() throws Exception {
+    Class<?> clazzA = ClassA.class;
+    Class<?> clazzB = ClassB.class;
 
-    R8TestCompileResult result = testForR8(Backend.DEX)
-        .addProgramClasses(clazzA, clazzB)
-        .addKeepRules(
-            "-keepattributes SourceFile,LineNumberTable",
-            "-keep class ** {\n" +
-                "public void call(int);\n" +
-            "}"
-        )
-        // String concatenation optimization will remove dead builders in foobar.
-        .addOptionsModification(o -> o.enableStringConcatenationOptimization = false)
-        .compile();
+    R8TestCompileResult result =
+        testForR8(Backend.DEX)
+            .addProgramClasses(clazzA, clazzB)
+            .addKeepRules(
+                "-keepattributes SourceFile,LineNumberTable",
+                "-keep class ** {\n" + "public void call(int);\n" + "}")
+            .enableInliningAnnotations()
+            .enableSideEffectAnnotations()
+            .compile();
     Path classesPath = temp.getRoot().toPath();
     result.app.write(classesPath, OutputMode.DexIndexed);
-    int numberOfDebugInfos = getNumberOfDebugInfos(
-        Paths.get(temp.getRoot().getCanonicalPath(), "classes.dex"));
+    int numberOfDebugInfos =
+        getNumberOfDebugInfos(Paths.get(temp.getRoot().getCanonicalPath(), "classes.dex"));
     Assert.assertEquals(1, numberOfDebugInfos);
   }
 
@@ -57,11 +57,17 @@
   public static class ClassA {
 
     public void call(int a) {
-        foobar(a);
+      foobar(a);
     }
 
     private String foobar(int a) {
-      String s = "aFoobar" + a;
+      return doSomething(a);
+    }
+
+    @AssumeMayHaveSideEffects
+    @NeverInline
+    private String doSomething(int a) {
+      String s = "bFoobar" + a;
       return s;
     }
   }
@@ -73,6 +79,12 @@
     }
 
     private String foobar(int a) {
+      return doSomething(a);
+    }
+
+    @AssumeMayHaveSideEffects
+    @NeverInline
+    private String doSomething(int a) {
       String s = "bFoobar" + a;
       return s;
     }
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/ImplementedInterfacesTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/ImplementedInterfacesTest.java
new file mode 100644
index 0000000..b5ab3b5
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/ImplementedInterfacesTest.java
@@ -0,0 +1,85 @@
+// 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.utils.codeinspector.Matchers.isPresent;
+import static junit.framework.TestCase.assertFalse;
+import static junit.framework.TestCase.assertTrue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.utils.codeinspector.ClassSubject;
+import com.android.tools.r8.utils.codeinspector.CodeInspector;
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.Spliterator;
+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 ImplementedInterfacesTest extends DesugaredLibraryTestBase {
+
+  private final TestParameters parameters;
+  private final boolean canUseDefaultAndStaticInterfaceMethods;
+
+  @Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withAllRuntimes().withAllApiLevelsAlsoForCf().build());
+  }
+
+  public ImplementedInterfacesTest(TestParameters parameters) {
+    this.parameters = parameters;
+    this.canUseDefaultAndStaticInterfaceMethods =
+        parameters
+            .getApiLevel()
+            .isGreaterThanOrEqualTo(apiLevelWithDefaultInterfaceMethodsSupport());
+  }
+
+  private String desugaredJavaTypeNameFor(Class<?> clazz) {
+    return clazz.getTypeName().replace("java.", "j$.");
+  }
+
+  private void checkInterfaces(CodeInspector inspector) {
+    ClassSubject clazz = inspector.clazz(MultipleInterfaces.class);
+    assertThat(clazz, isPresent());
+    assertTrue(clazz.isImplementing(Serializable.class));
+    assertTrue(clazz.isImplementing(Set.class));
+    assertTrue(clazz.isImplementing(List.class));
+    assertFalse(clazz.isImplementing(Collection.class));
+    assertFalse(clazz.isImplementing(Iterable.class));
+    if (!canUseDefaultAndStaticInterfaceMethods) {
+      assertFalse(clazz.isImplementing(desugaredJavaTypeNameFor(Serializable.class)));
+      assertTrue(clazz.isImplementing(desugaredJavaTypeNameFor(Set.class)));
+      assertTrue(clazz.isImplementing(desugaredJavaTypeNameFor(List.class)));
+      assertFalse(clazz.isImplementing(desugaredJavaTypeNameFor(Collection.class)));
+      assertFalse(clazz.isImplementing(desugaredJavaTypeNameFor(Iterable.class)));
+    }
+  }
+
+  @Test
+  public void testInterfaces() throws Exception {
+    KeepRuleConsumer keepRuleConsumer = createKeepRuleConsumer(parameters);
+    testForD8(parameters.getBackend())
+        .addInnerClasses(ImplementedInterfacesTest.class)
+        .setMinApi(parameters.getApiLevel())
+        .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
+        .compile()
+        .inspect(this::checkInterfaces);
+  }
+
+  abstract static class MultipleInterfaces<T> implements List<T>, Serializable, Set<T> {
+
+    // Disambiguate between default methods List.spliterator() and Set.spliterator()
+    @Override
+    public Spliterator<T> spliterator() {
+      return Set.super.spliterator();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/kotlin/KotlinMetadataTest.java b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/kotlin/KotlinMetadataTest.java
index ebf503a..89748a7 100644
--- a/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/kotlin/KotlinMetadataTest.java
+++ b/src/test/java/com/android/tools/r8/desugar/desugaredlibrary/kotlin/KotlinMetadataTest.java
@@ -4,7 +4,8 @@
 
 package com.android.tools.r8.desugar.desugaredlibrary.kotlin;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.KotlinTestBase.getCompileMemoizer;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.equalTo;
@@ -15,6 +16,8 @@
 
 import com.android.tools.r8.D8TestRunResult;
 import com.android.tools.r8.DexIndexedConsumer.ArchiveConsumer;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
+import com.android.tools.r8.KotlinTestBase.KotlinCompileMemoizer;
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.R8TestRunResult;
@@ -24,20 +27,15 @@
 import com.android.tools.r8.desugar.desugaredlibrary.DesugaredLibraryTestBase;
 import com.android.tools.r8.kotlin.KotlinMetadataWriter;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
-import com.android.tools.r8.utils.AndroidApiLevel;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.FileUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import java.io.File;
-import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import kotlinx.metadata.jvm.KotlinClassMetadata;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -50,50 +48,44 @@
   private final TestParameters parameters;
   private final boolean shrinkDesugaredLibrary;
   private final KotlinTargetVersion targetVersion;
+  private final KotlinCompiler kotlinCompiler;
   private static final String EXPECTED_OUTPUT = "Wuhuu, my special day is: 1997-8-29-2-14";
 
-  @Parameters(name = "{1}, shrinkDesugaredLibrary: {0}, target: {2}")
+  @Parameters(name = "{1}, shrinkDesugaredLibrary: {0}, target: {2}, kotlinc: {3}")
   public static List<Object[]> data() {
     return buildParameters(
         BooleanUtils.values(),
         getTestParameters().withAllRuntimesAndApiLevels().build(),
-        KotlinTargetVersion.values());
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public KotlinMetadataTest(
       boolean shrinkDesugaredLibrary,
       TestParameters parameters,
-      KotlinTargetVersion targetVersion) {
+      KotlinTargetVersion targetVersion,
+      KotlinCompiler kotlinCompiler) {
     this.shrinkDesugaredLibrary = shrinkDesugaredLibrary;
     this.parameters = parameters;
     this.targetVersion = targetVersion;
+    this.kotlinCompiler = kotlinCompiler;
   }
 
-  private static Map<KotlinTargetVersion, Path> compiledJars = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      compiledJars.put(
-          targetVersion,
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(
-                  Paths.get(
-                      ToolHelper.TESTS_DIR,
-                      "java",
-                      DescriptorUtils.getBinaryNameFromJavaType(PKG),
-                      "Main" + FileUtils.KT_EXTENSION))
-              .compile());
-    }
-  }
+  private static KotlinCompileMemoizer compiledJars =
+      getCompileMemoizer(
+          Paths.get(
+              ToolHelper.TESTS_DIR,
+              "java",
+              DescriptorUtils.getBinaryNameFromJavaType(PKG),
+              "Main" + FileUtils.KT_EXTENSION));
 
   @Test
   public void testCf() throws Exception {
     assumeTrue(parameters.getRuntime().isCf());
     testForRuntime(parameters)
-        .addProgramFiles(compiledJars.get(targetVersion))
-        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
-        .addProgramFiles(ToolHelper.getKotlinReflectJar())
+        .addProgramFiles(compiledJars.getForConfiguration(kotlinCompiler, targetVersion))
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinCompiler))
+        .addProgramFiles(ToolHelper.getKotlinReflectJar(kotlinCompiler))
         .run(parameters.getRuntime(), PKG + ".MainKt")
         .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
   }
@@ -105,9 +97,9 @@
     final File output = temp.newFile("output.zip");
     final D8TestRunResult d8TestRunResult =
         testForD8()
-            .addProgramFiles(compiledJars.get(targetVersion))
-            .addProgramFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(ToolHelper.getKotlinReflectJar())
+            .addProgramFiles(compiledJars.getForConfiguration(kotlinCompiler, targetVersion))
+            .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinCompiler))
+            .addProgramFiles(ToolHelper.getKotlinReflectJar(kotlinCompiler))
             .setProgramConsumer(new ArchiveConsumer(output.toPath(), true))
             .setMinApi(parameters.getApiLevel())
             .enableCoreLibraryDesugaring(parameters.getApiLevel(), keepRuleConsumer)
@@ -134,9 +126,9 @@
     boolean desugarLibrary = parameters.isDexRuntime() && requiresAnyCoreLibDesugaring(parameters);
     final R8FullTestBuilder testBuilder =
         testForR8(parameters.getBackend())
-            .addProgramFiles(compiledJars.get(targetVersion))
-            .addProgramFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(ToolHelper.getKotlinReflectJar())
+            .addProgramFiles(compiledJars.getForConfiguration(kotlinCompiler, targetVersion))
+            .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinCompiler))
+            .addProgramFiles(ToolHelper.getKotlinReflectJar(kotlinCompiler))
             .addKeepMainRule(PKG + ".MainKt")
             .addKeepAllClassesRule()
             .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
diff --git a/src/test/java/com/android/tools/r8/desugar/records/RecordsAttributeTest.java b/src/test/java/com/android/tools/r8/desugar/records/RecordsAttributeTest.java
new file mode 100644
index 0000000..831c426
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/records/RecordsAttributeTest.java
@@ -0,0 +1,87 @@
+// 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.records;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeFalse;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.examples.jdk15.Records;
+import com.android.tools.r8.utils.AndroidApiLevel;
+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 RecordsAttributeTest extends TestBase {
+
+  private final Backend backend;
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk15).
+    return buildParameters(
+        getTestParameters().withCustomRuntime(CfRuntime.getCheckedInJdk15()).build(),
+        Backend.values());
+  }
+
+  public RecordsAttributeTest(TestParameters parameters, Backend backend) {
+    this.parameters = parameters;
+    this.backend = backend;
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeFalse(parameters.isNoneRuntime());
+    assumeTrue(backend == Backend.CF);
+    testForJvm()
+        .addRunClasspathFiles(Records.jar())
+        .addVmArguments("--enable-preview")
+        .run(parameters.getRuntime(), Records.Main.typeName())
+        .assertSuccessWithOutputLines("Jane Doe", "42");
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assertThrows(
+        CompilationFailedException.class,
+        () -> {
+          testForD8(backend)
+              .addProgramClassFileData(Records.Main.bytes(), Records.Main$Person.bytes())
+              .setMinApi(AndroidApiLevel.B)
+              .compileWithExpectedDiagnostics(
+                  diagnostics -> {
+                    diagnostics.assertErrorThatMatches(
+                        diagnosticMessage(containsString("Records are not supported")));
+                  });
+        });
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assertThrows(
+        CompilationFailedException.class,
+        () -> {
+          testForR8(backend)
+              .addProgramClassFileData(Records.Main.bytes(), Records.Main$Person.bytes())
+              .setMinApi(AndroidApiLevel.B)
+              .addKeepMainRule(Records.Main.typeName())
+              .compileWithExpectedDiagnostics(
+                  diagnostics -> {
+                    diagnostics.assertErrorThatMatches(
+                        diagnosticMessage(containsString("Records are not supported")));
+                  });
+        });
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/desugar/sealed/SealedAttributeTest.java b/src/test/java/com/android/tools/r8/desugar/sealed/SealedAttributeTest.java
new file mode 100644
index 0000000..48b29e7
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/desugar/sealed/SealedAttributeTest.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.desugar.sealed;
+
+import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assume.assumeTrue;
+
+import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestRuntime;
+import com.android.tools.r8.examples.jdk15.Sealed;
+import com.android.tools.r8.utils.AndroidApiLevel;
+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 SealedAttributeTest extends TestBase {
+
+  private final Backend backend;
+
+  @Parameters(name = "{0}")
+  public static List<Object[]> data() {
+    // TODO(b/174431251): This should be replaced with .withCfRuntimes(start = jdk15).
+    return buildParameters(
+        getTestParameters().withCustomRuntime(TestRuntime.getCheckedInJdk15()).build(),
+        Backend.values());
+  }
+
+  public SealedAttributeTest(TestParameters parameters, Backend backend) {
+    this.backend = backend;
+  }
+
+  @Test
+  public void testJvm() throws Exception {
+    assumeTrue(backend == Backend.CF);
+    testForJvm()
+        .addRunClasspathFiles(Sealed.jar())
+        .addVmArguments("--enable-preview")
+        .run(TestRuntime.getCheckedInJdk15(), Sealed.Main.typeName())
+        .assertSuccessWithOutputLines("R8 compiler", "D8 compiler");
+  }
+
+  @Test
+  public void testD8() throws Exception {
+    assertThrows(
+        CompilationFailedException.class,
+        () -> {
+          testForD8(backend)
+              .addProgramFiles(Sealed.jar())
+              .setMinApi(AndroidApiLevel.B)
+              .compileWithExpectedDiagnostics(
+                  diagnostics -> {
+                    diagnostics.assertErrorThatMatches(
+                        diagnosticMessage(containsString("Sealed classes are not supported")));
+                  });
+        });
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    assertThrows(
+        CompilationFailedException.class,
+        () -> {
+          testForR8(backend)
+              .addProgramFiles(Sealed.jar())
+              .setMinApi(AndroidApiLevel.B)
+              .addKeepMainRule(Sealed.Main.typeName())
+              .compileWithExpectedDiagnostics(
+                  diagnostics -> {
+                    diagnostics.assertErrorThatMatches(
+                        diagnosticMessage(containsString("Sealed classes are not supported")));
+                  });
+        });
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/EnumWithNonDefaultForwardingConstructorTest.java b/src/test/java/com/android/tools/r8/enumunboxing/EnumWithNonDefaultForwardingConstructorTest.java
index 92b0b54..926a7b0 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/EnumWithNonDefaultForwardingConstructorTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/EnumWithNonDefaultForwardingConstructorTest.java
@@ -54,8 +54,7 @@
         .setMinApi(parameters.getApiLevel())
         .compile()
         .run(parameters.getRuntime(), TestClass.class)
-        // TODO(b/160939354): Should succeed with 42.
-        .assertSuccessWithOutputLines(enableEnumUnboxing ? "0" : "42");
+        .assertSuccessWithOutputLines("42");
   }
 
   private void addProgramClasses(TestBuilder<?, ?> builder) throws Exception {
diff --git a/src/test/java/com/android/tools/r8/enumunboxing/kotlin/SimpleKotlinEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/enumunboxing/kotlin/SimpleKotlinEnumUnboxingTest.java
index 1bb9c1e..a4f280c 100644
--- a/src/test/java/com/android/tools/r8/enumunboxing/kotlin/SimpleKotlinEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/enumunboxing/kotlin/SimpleKotlinEnumUnboxingTest.java
@@ -4,21 +4,20 @@
 
 package com.android.tools.r8.enumunboxing.kotlin;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.KotlinTestBase.getCompileMemoizer;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static org.junit.Assume.assumeTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
+import com.android.tools.r8.KotlinTestBase.KotlinCompileMemoizer;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.enumunboxing.EnumUnboxingTestBase;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.DescriptorUtils;
-import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -31,51 +30,45 @@
   private final boolean enumValueOptimization;
   private final EnumKeepRules enumKeepRules;
   private final KotlinTargetVersion targetVersion;
+  private final KotlinCompiler kotlinCompiler;
 
   private static final String PKG = SimpleKotlinEnumUnboxingTest.class.getPackage().getName();
-  private static Map<KotlinTargetVersion, Path> jars = new HashMap<>();
+  private static final KotlinCompileMemoizer jars =
+      getCompileMemoizer(
+          Paths.get(
+              ToolHelper.TESTS_DIR,
+              "java",
+              DescriptorUtils.getBinaryNameFromJavaType(PKG),
+              "Main.kt"));
 
-  @Parameters(name = "{0}, valueOpt: {1}, keep: {2}, kotlin targetVersion: {3}")
+  @Parameters(name = "{0}, valueOpt: {1}, keep: {2}, kotlin targetVersion: {3}, kotlinc: {4}")
   public static List<Object[]> enumUnboxingTestParameters() {
     return buildParameters(
         getTestParameters().withAllRuntimesAndApiLevels().build(),
         BooleanUtils.values(),
         getAllEnumKeepRules(),
-        KotlinTargetVersion.values());
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public SimpleKotlinEnumUnboxingTest(
       TestParameters parameters,
       boolean enumValueOptimization,
       EnumKeepRules enumKeepRules,
-      KotlinTargetVersion targetVersion) {
+      KotlinTargetVersion targetVersion,
+      KotlinCompiler kotlinCompiler) {
     this.parameters = parameters;
     this.enumValueOptimization = enumValueOptimization;
     this.enumKeepRules = enumKeepRules;
     this.targetVersion = targetVersion;
-  }
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      jars.put(
-          targetVersion,
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(
-                  Paths.get(
-                      ToolHelper.TESTS_DIR,
-                      "java",
-                      DescriptorUtils.getBinaryNameFromJavaType(PKG),
-                      "Main.kt"))
-              .compile());
-    }
+    this.kotlinCompiler = kotlinCompiler;
   }
 
   @Test
   public void testEnumUnboxing() throws Exception {
     assumeTrue(parameters.isDexRuntime());
     testForR8(parameters.getBackend())
-        .addProgramFiles(jars.get(targetVersion))
+        .addProgramFiles(jars.getForConfiguration(kotlinCompiler, targetVersion))
         .addKeepMainRule(PKG + ".MainKt")
         .addKeepRules(enumKeepRules.getKeepRules())
         .addKeepRuntimeVisibleAnnotations()
diff --git a/src/test/java/com/android/tools/r8/examples/JavaExampleClassProxy.java b/src/test/java/com/android/tools/r8/examples/JavaExampleClassProxy.java
new file mode 100644
index 0000000..6bbbfd1
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/examples/JavaExampleClassProxy.java
@@ -0,0 +1,53 @@
+// 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.examples;
+
+import com.android.tools.r8.ToolHelper;
+import com.android.tools.r8.utils.DescriptorUtils;
+import com.android.tools.r8.utils.StringUtils;
+import com.google.common.io.ByteStreams;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.zip.ZipFile;
+
+public class JavaExampleClassProxy {
+
+  private final String examplesFolder;
+  private final String binaryName;
+
+  public JavaExampleClassProxy(String examples, String binaryName) {
+    this.examplesFolder = examples;
+    this.binaryName = binaryName;
+  }
+
+  public static Path examplesJar(String examplesFolder) {
+    return Paths.get(ToolHelper.BUILD_DIR, "test", examplesFolder + ".jar");
+  }
+
+  public byte[] bytes() {
+    Path examplePath = examplesJar(examplesFolder);
+    if (!Files.exists(examplePath)) {
+      throw new RuntimeException(
+          "Could not find path "
+              + examplePath
+              + ". Build "
+              + examplesFolder
+              + " by running tools/gradle.py build"
+              + StringUtils.capitalize(examplesFolder));
+    }
+    try (ZipFile zipFile = new ZipFile(examplePath.toFile())) {
+      return ByteStreams.toByteArray(
+          zipFile.getInputStream(zipFile.getEntry(binaryName + ".class")));
+    } catch (IOException e) {
+      throw new RuntimeException("Could not read zip-entry from " + examplePath.toString(), e);
+    }
+  }
+
+  public String typeName() {
+    return DescriptorUtils.getJavaTypeFromBinaryName(binaryName);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/examples/jdk15/Records.java b/src/test/java/com/android/tools/r8/examples/jdk15/Records.java
new file mode 100644
index 0000000..9eafc56
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/examples/jdk15/Records.java
@@ -0,0 +1,22 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.examples.jdk15;
+
+import com.android.tools.r8.examples.JavaExampleClassProxy;
+import java.nio.file.Path;
+
+public class Records {
+
+  private static final String EXAMPLE_FILE = "examplesJava15/records";
+
+  public static final JavaExampleClassProxy Main =
+      new JavaExampleClassProxy(EXAMPLE_FILE, "records/Main");
+  public static final JavaExampleClassProxy Main$Person =
+      new JavaExampleClassProxy(EXAMPLE_FILE, "records/Main$Person");
+
+  public static Path jar() {
+    return JavaExampleClassProxy.examplesJar(EXAMPLE_FILE);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/examples/jdk15/Sealed.java b/src/test/java/com/android/tools/r8/examples/jdk15/Sealed.java
new file mode 100644
index 0000000..1450ee8
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/examples/jdk15/Sealed.java
@@ -0,0 +1,26 @@
+// 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.examples.jdk15;
+
+import com.android.tools.r8.examples.JavaExampleClassProxy;
+import java.nio.file.Path;
+
+public class Sealed {
+
+  private static final String EXAMPLE_FILE = "examplesJava15/sealed";
+
+  public static final JavaExampleClassProxy Compiler =
+      new JavaExampleClassProxy(EXAMPLE_FILE, "sealed/Compiler");
+  public static final JavaExampleClassProxy R8Compiler =
+      new JavaExampleClassProxy(EXAMPLE_FILE, "sealed/R8Compiler");
+  public static final JavaExampleClassProxy D8Compiler =
+      new JavaExampleClassProxy(EXAMPLE_FILE, "sealed/D8Compiler");
+  public static final JavaExampleClassProxy Main =
+      new JavaExampleClassProxy(EXAMPLE_FILE, "sealed/Main");
+
+  public static Path jar() {
+    return JavaExampleClassProxy.examplesJar(EXAMPLE_FILE);
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/graph/DexTypeTest.java b/src/test/java/com/android/tools/r8/graph/DexTypeTest.java
index 6b2d73e..82ee9e8 100644
--- a/src/test/java/com/android/tools/r8/graph/DexTypeTest.java
+++ b/src/test/java/com/android/tools/r8/graph/DexTypeTest.java
@@ -28,7 +28,7 @@
         new ApplicationReader(
                 AndroidApp.builder()
                     .addLibraryFiles(ToolHelper.getDefaultAndroidJar())
-                    .addLibraryFiles(ToolHelper.getKotlinStdlibJar())
+                    .addLibraryFiles(ToolHelper.getKotlinStdlibJar(ToolHelper.getKotlinC_1_3_72()))
                     .build(),
                 options,
                 Timing.empty())
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/DevirtualizeWithCatchHandlersTest.java b/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/DevirtualizeWithCatchHandlersTest.java
new file mode 100644
index 0000000..a126f0f
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/DevirtualizeWithCatchHandlersTest.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.ir.optimize.devirtualize;
+
+import com.android.tools.r8.NeverInline;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class DevirtualizeWithCatchHandlersTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return TestBase.getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public DevirtualizeWithCatchHandlersTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addKeepRules(
+            // Disable uninstantiated type optimization for m().
+            "-keepclassmembers class " + Uninstantiated.class.getTypeName() + " {",
+            "  " + Uninstantiated.class.getTypeName() + " get();",
+            "}")
+        .enableInliningAnnotations()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      try {
+        test();
+      } catch (Exception e) {
+        System.out.println("Dead!");
+      }
+    }
+
+    static synchronized void test() {
+      I which = System.currentTimeMillis() > 0 ? new A() : Uninstantiated.get();
+      which.m();
+    }
+  }
+
+  interface I {
+
+    void m();
+  }
+
+  static class A implements I {
+
+    @NeverInline
+    @Override
+    public void m() {
+      System.out.println("A");
+    }
+  }
+
+  static class Uninstantiated implements I {
+
+    static Uninstantiated get() {
+      return null;
+    }
+
+    @Override
+    public void m() {
+      throw new RuntimeException();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/PrivateOverridePublicizerDevirtualizerTest.java b/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/PrivateOverridePublicizerDevirtualizerTest.java
index bef0cd5..86739dc 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/PrivateOverridePublicizerDevirtualizerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/devirtualize/PrivateOverridePublicizerDevirtualizerTest.java
@@ -5,13 +5,11 @@
 package com.android.tools.r8.ir.optimize.devirtualize;
 
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
-import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NeverClassInline;
 import com.android.tools.r8.NeverInline;
 import com.android.tools.r8.NoVerticalClassMerging;
-import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestParametersCollection;
@@ -48,33 +46,25 @@
 
   @Test
   public void testR8() throws Exception {
-    R8TestRunResult runResult =
-        testForR8(parameters.getBackend())
-            .addInnerClasses(PrivateOverridePublicizerDevirtualizerTest.class)
-            .enableInliningAnnotations()
-            .enableNoVerticalClassMergingAnnotations()
-            .enableNeverClassInliningAnnotations()
-            .addKeepMainRule(Main.class)
-            .allowAccessModification()
-            .noMinification()
-            .setMinApi(parameters.getApiLevel())
-            .compile()
-            .inspect(
-                inspector -> {
-                  ClassSubject classA = inspector.clazz(A.class);
-                  assertThat(classA, isPresent());
-                  MethodSubject fooA = classA.uniqueMethodWithName("foo");
-                  // TODO(b/173812804): This should not be removed.
-                  assertThat(fooA, not(isPresent()));
-                })
-            .run(parameters.getRuntime(), Main.class);
-    if (parameters.isDexRuntime()) {
-      // TODO(b/173812804): This should not fail verification
-      runResult.assertFailureWithErrorThatThrows(VerifyError.class);
-    } else {
-      // TODO(b/173812804): This should have been A::foo, B::foo.
-      runResult.assertSuccessWithOutputLines("B::foo", "B::foo");
-    }
+    testForR8(parameters.getBackend())
+        .addInnerClasses(PrivateOverridePublicizerDevirtualizerTest.class)
+        .enableInliningAnnotations()
+        .enableNoVerticalClassMergingAnnotations()
+        .enableNeverClassInliningAnnotations()
+        .addKeepMainRule(Main.class)
+        .allowAccessModification()
+        .noMinification()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              ClassSubject classA = inspector.clazz(A.class);
+              assertThat(classA, isPresent());
+              MethodSubject fooA = classA.uniqueMethodWithName("foo");
+              assertThat(fooA, isPresent());
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines(EXPECTED);
   }
 
   @NeverClassInline
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/InlineSynthesizedLambdaClass.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/InlineSynthesizedLambdaClass.java
index 117a18c..1bd8db6 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/inliner/InlineSynthesizedLambdaClass.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/InlineSynthesizedLambdaClass.java
@@ -23,7 +23,7 @@
 
   @Parameterized.Parameters(name = "{0}")
   public static TestParametersCollection data() {
-    return getTestParameters().withDexRuntimes().build();
+    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
   }
 
   public InlineSynthesizedLambdaClass(TestParameters parameters) {
@@ -42,7 +42,7 @@
             .addKeepMainRule(Lambda.class)
             .allowAccessModification()
             .noMinification()
-            .setMinApi(parameters.getRuntime())
+            .setMinApi(parameters.getApiLevel())
             .run(parameters.getRuntime(), Lambda.class)
             .assertSuccessWithOutput(javaOutput)
             .inspector();
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/SyntheticInitClassPositionTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SyntheticInitClassPositionTest.java
new file mode 100644
index 0000000..1dc11c0
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SyntheticInitClassPositionTest.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.ir.optimize.inliner;
+
+import static com.android.tools.r8.naming.retrace.StackTrace.isSame;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.naming.retrace.StackTrace;
+import org.junit.Before;
+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 SyntheticInitClassPositionTest extends TestBase {
+
+  private final TestParameters parameters;
+  private StackTrace expectedStackTrace;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return TestBase.getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Before
+  public void setup() throws Exception {
+    // Get the expected stack trace by running on the JVM.
+    expectedStackTrace =
+        testForJvm()
+            .addTestClasspath()
+            .run(CfRuntime.getSystemRuntime(), Main.class)
+            .assertFailure()
+            .map(StackTrace::extractFromJvm);
+  }
+
+  public SyntheticInitClassPositionTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addKeepAttributeLineNumberTable()
+        .addKeepAttributeSourceFile()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(ExceptionInInitializerError.class)
+        .inspectStackTrace(stackTrace -> assertThat(stackTrace, isSame(expectedStackTrace)));
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      A.m();
+    }
+  }
+
+  static class A {
+
+    static {
+      if (true) {
+        throw new RuntimeException();
+      }
+    }
+
+    static void m() {
+      System.out.println("Hello world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/inliner/SyntheticInlineNullCheckPositionTest.java b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SyntheticInlineNullCheckPositionTest.java
new file mode 100644
index 0000000..4eadc56
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/inliner/SyntheticInlineNullCheckPositionTest.java
@@ -0,0 +1,99 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.inliner;
+
+import static com.android.tools.r8.naming.retrace.StackTrace.isSame;
+import static com.android.tools.r8.naming.retrace.StackTrace.isSameExceptForSpecificLineNumber;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.TestRuntime.CfRuntime;
+import com.android.tools.r8.naming.retrace.StackTrace;
+import com.android.tools.r8.naming.retrace.StackTrace.StackTraceLine;
+import java.util.Objects;
+import org.junit.Before;
+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 SyntheticInlineNullCheckPositionTest extends TestBase {
+
+  private static final StackTraceLine REQUIRE_NON_NULL_LINE =
+      StackTraceLine.builder()
+          .setClassName(Objects.class.getTypeName())
+          .setMethodName("requireNonNull")
+          .setFileName("Objects.java")
+          .build();
+
+  private final TestParameters parameters;
+  private StackTrace expectedStackTraceWithGetClass;
+  private StackTrace expectedStackTraceWithRequireNonNull;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return TestBase.getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  @Before
+  public void setup() throws Exception {
+    // Get the expected stack trace by running on the JVM.
+    StackTrace actualStackTrace =
+        testForJvm()
+            .addTestClasspath()
+            .run(CfRuntime.getSystemRuntime(), Main.class)
+            .assertFailure()
+            .map(StackTrace::extractFromJvm);
+    expectedStackTraceWithGetClass = actualStackTrace;
+    expectedStackTraceWithRequireNonNull =
+        StackTrace.builder().add(REQUIRE_NON_NULL_LINE).add(actualStackTrace).build();
+  }
+
+  public SyntheticInlineNullCheckPositionTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .addKeepAttributeLineNumberTable()
+        .addKeepAttributeSourceFile()
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertFailureWithErrorThatThrows(NullPointerException.class)
+        .inspectStackTrace(
+            stackTrace -> {
+              if (canUseRequireNonNull(parameters)) {
+                assertThat(
+                    stackTrace,
+                    isSameExceptForSpecificLineNumber(
+                        expectedStackTraceWithRequireNonNull, REQUIRE_NON_NULL_LINE));
+              } else {
+                assertThat(stackTrace, isSame(expectedStackTraceWithGetClass));
+              }
+            });
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      A nullable = System.currentTimeMillis() < 0 ? new A() : null;
+      nullable.m();
+    }
+  }
+
+  static class A {
+
+    void m() {
+      System.out.println("Hello world!");
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/lambda/LambdaMethodInliningTest.java b/src/test/java/com/android/tools/r8/ir/optimize/lambda/LambdaMethodInliningTest.java
index 8bdea50..a63addb 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/lambda/LambdaMethodInliningTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/lambda/LambdaMethodInliningTest.java
@@ -43,6 +43,8 @@
         .enableInliningAnnotations()
         .enableNoVerticalClassMergingAnnotations()
         .addOptionsModification(options -> options.enableClassInlining = false)
+        // TODO(b/173398086): Horizontal class merging breaks uniqueMethodWithName().
+        .noMinification()
         .setMinApi(parameters.getApiLevel())
         .compile()
         .inspect(this::inspect)
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderFromCharSequenceWithAppendObjectTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderFromCharSequenceWithAppendObjectTest.java
new file mode 100644
index 0000000..8f1f58b
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderFromCharSequenceWithAppendObjectTest.java
@@ -0,0 +1,85 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.string;
+
+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;
+
+@RunWith(Parameterized.class)
+public class UnusedStringBuilderFromCharSequenceWithAppendObjectTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public UnusedStringBuilderFromCharSequenceWithAppendObjectTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLinesIf(
+            parameters.isCfRuntime()
+                || parameters.getDexRuntimeVersion().isNewerThanOrEqual(Version.V7_0_0),
+            "CustomCharSequence.length()",
+            "CustomCharSequence.length()",
+            "CustomCharSequence.length()",
+            "CustomCharSequence.charAt(0)")
+        .assertSuccessWithOutputLinesIf(
+            parameters.isDexRuntime()
+                && parameters.getDexRuntimeVersion().isOlderThan(Version.V7_0_0),
+            "CustomCharSequence.toString()");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new StringBuilder(new CustomCharSequence());
+    }
+  }
+
+  static class CustomCharSequence implements CharSequence {
+
+    @Override
+    public int length() {
+      System.out.println("CustomCharSequence.length()");
+      return 1;
+    }
+
+    @Override
+    public char charAt(int i) {
+      if (i != 0) {
+        throw new RuntimeException();
+      }
+      System.out.println("CustomCharSequence.charAt(0)");
+      return 'A';
+    }
+
+    @Override
+    public CharSequence subSequence(int i, int i1) {
+      throw new RuntimeException();
+    }
+
+    @Override
+    public String toString() {
+      System.out.println("CustomCharSequence.toString()");
+      return "A";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendDefinitelyNullObjectTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendDefinitelyNullObjectTest.java
new file mode 100644
index 0000000..f5f615d
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendDefinitelyNullObjectTest.java
@@ -0,0 +1,58 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.string;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.instantiatesClass;
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.invokesMethodWithName;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+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.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class UnusedStringBuilderWithAppendDefinitelyNullObjectTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public UnusedStringBuilderWithAppendDefinitelyNullObjectTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              MethodSubject mainMethod = inspector.clazz(Main.class).mainMethod();
+              assertThat(mainMethod, not(instantiatesClass(StringBuilder.class)));
+              assertThat(mainMethod, not(invokesMethodWithName("toString")));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      Object o = null;
+      new StringBuilder().append(o).toString();
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendMaybeNullObjectTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendMaybeNullObjectTest.java
new file mode 100644
index 0000000..78c0a10
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendMaybeNullObjectTest.java
@@ -0,0 +1,68 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.string;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.instantiatesClass;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+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.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class UnusedStringBuilderWithAppendMaybeNullObjectTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public UnusedStringBuilderWithAppendMaybeNullObjectTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              MethodSubject mainMethod = inspector.clazz(Main.class).mainMethod();
+              assertThat(
+                  mainMethod,
+                  notIf(
+                      instantiatesClass(StringBuilder.class),
+                      canUseJavaUtilObjects(parameters) || parameters.isDexRuntime()));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      A a = System.currentTimeMillis() < 0 ? new A() : null;
+      new StringBuilder().append(a).toString();
+    }
+  }
+
+  static class A {
+
+    @Override
+    public String toString() {
+      return "A";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendObjectSideEffectTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendObjectSideEffectTest.java
new file mode 100644
index 0000000..934dfd2
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendObjectSideEffectTest.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.ir.optimize.string;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.instantiatesClass;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+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.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class UnusedStringBuilderWithAppendObjectSideEffectTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public UnusedStringBuilderWithAppendObjectSideEffectTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              MethodSubject mainMethod = inspector.clazz(Main.class).mainMethod();
+              assertThat(mainMethod, not(instantiatesClass(StringBuilder.class)));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithOutputLines("A");
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      new StringBuilder().append(new A()).toString();
+    }
+  }
+
+  static class A {
+
+    @Override
+    public String toString() {
+      System.out.println("A");
+      return "A";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendObjectTest.java b/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendObjectTest.java
new file mode 100644
index 0000000..d8ed11c
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/ir/optimize/string/UnusedStringBuilderWithAppendObjectTest.java
@@ -0,0 +1,68 @@
+// Copyright (c) 2020, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.string;
+
+import static com.android.tools.r8.utils.codeinspector.CodeMatchers.instantiatesClass;
+import static com.android.tools.r8.utils.codeinspector.Matchers.notIf;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+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.MethodSubject;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class UnusedStringBuilderWithAppendObjectTest extends TestBase {
+
+  private final TestParameters parameters;
+
+  @Parameterized.Parameters(name = "{0}")
+  public static TestParametersCollection data() {
+    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  }
+
+  public UnusedStringBuilderWithAppendObjectTest(TestParameters parameters) {
+    this.parameters = parameters;
+  }
+
+  @Test
+  public void test() throws Exception {
+    testForR8(parameters.getBackend())
+        .addInnerClasses(getClass())
+        .addKeepMainRule(Main.class)
+        .setMinApi(parameters.getApiLevel())
+        .compile()
+        .inspect(
+            inspector -> {
+              MethodSubject mainMethod = inspector.clazz(Main.class).mainMethod();
+              assertThat(
+                  mainMethod,
+                  notIf(
+                      instantiatesClass(StringBuilder.class),
+                      canUseJavaUtilObjects(parameters) || parameters.isDexRuntime()));
+            })
+        .run(parameters.getRuntime(), Main.class)
+        .assertSuccessWithEmptyOutput();
+  }
+
+  static class Main {
+
+    public static void main(String[] args) {
+      A a = System.currentTimeMillis() > 0 ? new A() : null;
+      new StringBuilder().append(a).toString();
+    }
+  }
+
+  static class A {
+
+    @Override
+    public String toString() {
+      return "A";
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/switches/SwitchMapInvalidOrdinalTest.java b/src/test/java/com/android/tools/r8/ir/optimize/switches/SwitchMapInvalidOrdinalTest.java
index cbbd4f0..a779afb 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/switches/SwitchMapInvalidOrdinalTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/switches/SwitchMapInvalidOrdinalTest.java
@@ -13,6 +13,7 @@
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
+@SuppressWarnings("unchecked")
 @RunWith(Parameterized.class)
 public class SwitchMapInvalidOrdinalTest extends TestBase {
   private final TestParameters parameters;
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizationsTemplates.java b/src/test/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizationsTemplates.java
index d8d7c85..8f7c579 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizationsTemplates.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/templates/CfUtilityMethodsForCodeOptimizationsTemplates.java
@@ -6,6 +6,12 @@
 
 public class CfUtilityMethodsForCodeOptimizationsTemplates {
 
+  public static void toStringIfNotNull(Object o) {
+    if (o != null) {
+      o.toString();
+    }
+  }
+
   public static void throwClassCastExceptionIfNotNull(Object o) {
     if (o != null) {
       throw new ClassCastException();
diff --git a/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java b/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
index fa7b74e..dff06df 100644
--- a/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
+++ b/src/test/java/com/android/tools/r8/ir/regalloc/RegisterMoveSchedulerTest.java
@@ -73,6 +73,12 @@
     }
 
     @Override
+    public void replaceCurrentInstructionWithConstString(
+        AppView<?> appView, IRCode code, DexString value) {
+      throw new Unimplemented();
+    }
+
+    @Override
     public void replaceCurrentInstructionWithStaticGet(
         AppView<?> appView, IRCode code, DexField field, Set<Value> affectedValues) {
       throw new Unimplemented();
@@ -151,6 +157,12 @@
     }
 
     @Override
+    public BasicBlock splitCopyCatchHandlers(
+        IRCode code, ListIterator<BasicBlock> blockIterator, InternalOptions options) {
+      throw new Unimplemented();
+    }
+
+    @Override
     public BasicBlock inlineInvoke(
         AppView<?> appView,
         IRCode code,
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 f76758c..0abfe16 100644
--- a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
+++ b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
@@ -14,6 +14,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.KotlinTestBase;
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.R8TestRunResult;
@@ -51,13 +52,16 @@
   private final List<Path> extraClasspath = new ArrayList<>();
 
   // Some tests defined in subclasses, e.g., Metadata tests, don't care about access relaxation.
-  protected AbstractR8KotlinTestBase(KotlinTargetVersion kotlinTargetVersion) {
-    this(kotlinTargetVersion, false);
+  protected AbstractR8KotlinTestBase(
+      KotlinTargetVersion kotlinTargetVersion, KotlinCompiler kotlinc) {
+    this(kotlinTargetVersion, kotlinc, false);
   }
 
   protected AbstractR8KotlinTestBase(
-      KotlinTargetVersion kotlinTargetVersion, boolean allowAccessModification) {
-    super(kotlinTargetVersion);
+      KotlinTargetVersion kotlinTargetVersion,
+      KotlinCompiler kotlinc,
+      boolean allowAccessModification) {
+    super(kotlinTargetVersion, kotlinc);
     this.allowAccessModification = allowAccessModification;
   }
 
@@ -238,9 +242,14 @@
       throws Exception {
     Assume.assumeTrue(ToolHelper.artSupported() || ToolHelper.compareAgaintsGoldenFiles());
 
+    Path kotlinJarFile =
+        getCompileMemoizer(getKotlinFilesInResource(folder), folder)
+            .configure(kotlinCompilerTool -> kotlinCompilerTool.includeRuntime().noReflect())
+            .getForConfiguration(kotlinc, targetVersion);
+
     // Build classpath for compilation (and java execution)
     classpath.clear();
-    classpath.add(getKotlinJarFile(folder));
+    classpath.add(kotlinJarFile);
     classpath.add(getJavaJarFile(folder));
     classpath.addAll(extraClasspath);
 
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 2bb0867..a949213 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
@@ -12,6 +13,7 @@
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.ThrowableConsumer;
@@ -43,14 +45,15 @@
 @RunWith(Parameterized.class)
 public class KotlinClassInlinerTest extends AbstractR8KotlinTestBase {
 
-  @Parameterized.Parameters(name = "target: {0}, allowAccessModification: {1}")
+  @Parameterized.Parameters(name = "target: {0}, kotlinc: {1}, allowAccessModification: {2}")
   public static Collection<Object[]> data() {
-    return buildParameters(KotlinTargetVersion.values(), BooleanUtils.values());
+    return buildParameters(
+        KotlinTargetVersion.values(), getKotlinCompilers(), BooleanUtils.values());
   }
 
   public KotlinClassInlinerTest(
-      KotlinTargetVersion targetVersion, boolean allowAccessModification) {
-    super(targetVersion, allowAccessModification);
+      KotlinTargetVersion targetVersion, KotlinCompiler kotlinc, boolean allowAccessModification) {
+    super(targetVersion, kotlinc, allowAccessModification);
   }
 
   private static boolean isLambda(DexClass clazz) {
@@ -332,7 +335,7 @@
                       // condition.
                       options.testing.addCallEdgesForLibraryInvokes = true;
 
-                      options.enableHorizontalClassMergingOfKotlinLambdas = false;
+                      options.horizontalClassMergerOptions().disableKotlinLambdaMerging();
                     })
                 .apply(configuration));
   }
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 bf74d90..ec1793d 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinClassStaticizerTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassStaticizerTest.java
@@ -4,12 +4,14 @@
 
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assume.assumeTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.utils.BooleanUtils;
@@ -24,14 +26,15 @@
 @RunWith(Parameterized.class)
 public class KotlinClassStaticizerTest extends AbstractR8KotlinTestBase {
 
-  @Parameterized.Parameters(name = "target: {0}, allowAccessModification: {1}")
+  @Parameterized.Parameters(name = "target: {0}, kotlinc: {1}, allowAccessModification: {2}")
   public static Collection<Object[]> data() {
-    return buildParameters(KotlinTargetVersion.values(), BooleanUtils.values());
+    return buildParameters(
+        KotlinTargetVersion.values(), getKotlinCompilers(), BooleanUtils.values());
   }
 
   public KotlinClassStaticizerTest(
-      KotlinTargetVersion targetVersion, boolean allowAccessModification) {
-    super(targetVersion, allowAccessModification);
+      KotlinTargetVersion targetVersion, KotlinCompiler kotlinc, boolean allowAccessModification) {
+    super(targetVersion, kotlinc, allowAccessModification);
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinDuplicateAnnotationTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinDuplicateAnnotationTest.java
index 35e3109..7407d46 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinDuplicateAnnotationTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinDuplicateAnnotationTest.java
@@ -3,12 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.fail;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.utils.BooleanUtils;
@@ -34,11 +36,12 @@
       o.enableInlining = false;
     };
 
-  @Parameterized.Parameters(name = "{0} target: {1}, allowAccessModification: {2}")
+  @Parameterized.Parameters(name = "{0} target: {1}, kotlinc: {2}, allowAccessModification: {3}")
   public static Collection<Object[]> data() {
     return buildParameters(
         getTestParameters().withAllRuntimes().build(),
         KotlinTargetVersion.values(),
+        getKotlinCompilers(),
         BooleanUtils.values());
   }
 
@@ -47,17 +50,22 @@
   public KotlinDuplicateAnnotationTest(
       TestParameters parameters,
       KotlinTargetVersion targetVersion,
+      KotlinCompiler kotlinc,
       boolean allowAccessModification) {
-    super(targetVersion, allowAccessModification);
+    super(targetVersion, kotlinc, allowAccessModification);
     this.parameters = parameters;
   }
 
+  private static final KotlinCompileMemoizer compiledJars =
+      getCompileMemoizer(getKotlinFilesInResource(FOLDER), FOLDER)
+          .configure(kotlinCompilerTool -> kotlinCompilerTool.includeRuntime().noReflect());
+
   @Test
   public void test_dex() {
     assumeTrue("test DEX", parameters.isDexRuntime());
     try {
       testForR8(parameters.getBackend())
-          .addProgramFiles(getKotlinJarFile(FOLDER))
+          .addProgramFiles(compiledJars.getForConfiguration(kotlinc, targetVersion))
           .addKeepMainRule(MAIN)
           .addKeepRules(KEEP_RULES)
           .noMinification()
@@ -74,7 +82,7 @@
   public void test_cf() throws Exception {
     assumeTrue("test CF", parameters.isCfRuntime());
     testForR8(parameters.getBackend())
-        .addProgramFiles(getKotlinJarFile(FOLDER))
+        .addProgramFiles(compiledJars.getForConfiguration(kotlinc, targetVersion))
         .addKeepMainRule(MAIN)
         .addKeepRules(KEEP_RULES)
         .noMinification()
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinIntrinsicsInlineTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinIntrinsicsInlineTest.java
index dc917ab..c683b1a 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinIntrinsicsInlineTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinIntrinsicsInlineTest.java
@@ -3,11 +3,13 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assume.assumeTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.utils.BooleanUtils;
@@ -24,11 +26,12 @@
   private static final String FOLDER = "intrinsics";
   private static final String MAIN = FOLDER + ".InlineKt";
 
-  @Parameterized.Parameters(name = "{0} target: {1}, allowAccessModification: {2}")
+  @Parameterized.Parameters(name = "{0} target: {1}, kotlinc: {2}, allowAccessModification: {3}")
   public static Collection<Object[]> data() {
     return buildParameters(
         getTestParameters().withAllRuntimes().build(),
         KotlinTargetVersion.values(),
+        getKotlinCompilers(),
         BooleanUtils.values());
   }
 
@@ -37,41 +40,46 @@
   public KotlinIntrinsicsInlineTest(
       TestParameters parameters,
       KotlinTargetVersion targetVersion,
+      KotlinCompiler kotlinc,
       boolean allowAccessModification) {
-    super(targetVersion, allowAccessModification);
+    super(targetVersion, kotlinc, allowAccessModification);
     this.parameters = parameters;
   }
 
+  private static final KotlinCompileMemoizer compiledJars =
+      getCompileMemoizer(getKotlinFilesInResource(FOLDER), FOLDER)
+          .configure(kotlinCompilerTool -> kotlinCompilerTool.includeRuntime().noReflect());
+
   @Test
   public void b139432507() throws Exception {
     testForR8(parameters.getBackend())
-        .addProgramFiles(getKotlinJarFile(FOLDER))
-        .addKeepRules(StringUtils.lines(
-            "-keepclasseswithmembers class " + MAIN + "{",
-            "  public static *** *(...);",
-            "}"))
+        .addProgramFiles(compiledJars.getForConfiguration(kotlinc, targetVersion))
+        .addKeepRules(
+            StringUtils.lines(
+                "-keepclasseswithmembers class " + MAIN + "{", "  public static *** *(...);", "}"))
         .allowAccessModification(allowAccessModification)
         .noMinification()
         .setMinApi(parameters.getRuntime())
         .compile()
-        .inspect(inspector -> {
-          ClassSubject main = inspector.clazz(MAIN);
-          assertThat(main, isPresent());
+        .inspect(
+            inspector -> {
+              ClassSubject main = inspector.clazz(MAIN);
+              assertThat(main, isPresent());
 
-          // Note that isSupported itself has a parameter whose null check would be inlined
-          // with -allowaccessmodification.
-          MethodSubject isSupported = main.uniqueMethodWithName("isSupported");
-          assertThat(isSupported, isPresent());
-          assertEquals(
-              allowAccessModification ? 0 : 1,
-              countCall(isSupported, "checkParameterIsNotNull"));
+              // Note that isSupported itself has a parameter whose null check would be inlined
+              // with -allowaccessmodification.
+              MethodSubject isSupported = main.uniqueMethodWithName("isSupported");
+              assertThat(isSupported, isPresent());
+              assertEquals(
+                  allowAccessModification ? 0 : 1,
+                  countCall(isSupported, "checkParameterIsNotNull"));
 
-          // In general cases, null check won't be invoked only once or twice, hence no subtle
-          // situation in double inlining.
-          MethodSubject containsArray = main.uniqueMethodWithName("containsArray");
-          assertThat(containsArray, isPresent());
-          assertEquals(0, countCall(containsArray, "checkParameterIsNotNull"));
-        });
+              // In general cases, null check won't be invoked only once or twice, hence no subtle
+              // situation in double inlining.
+              MethodSubject containsArray = main.uniqueMethodWithName("containsArray");
+              assertThat(containsArray, isPresent());
+              assertEquals(0, countCall(containsArray, "checkParameterIsNotNull"));
+            });
   }
 
   @Test
@@ -88,27 +96,29 @@
 
   private void testSingle(String methodName) throws Exception {
     testForR8(parameters.getBackend())
-        .addProgramFiles(getKotlinJarFile(FOLDER))
-        .addKeepRules(StringUtils.lines(
-            "-keepclasseswithmembers class " + MAIN + "{",
-            "  public static *** " + methodName + "(...);",
-            "}"))
+        .addProgramFiles(compiledJars.getForConfiguration(kotlinc, targetVersion))
+        .addKeepRules(
+            StringUtils.lines(
+                "-keepclasseswithmembers class " + MAIN + "{",
+                "  public static *** " + methodName + "(...);",
+                "}"))
         .allowAccessModification(allowAccessModification)
         .noMinification()
         .setMinApi(parameters.getRuntime())
         .compile()
-        .inspect(inspector -> {
-          ClassSubject main = inspector.clazz(MAIN);
-          assertThat(main, isPresent());
+        .inspect(
+            inspector -> {
+              ClassSubject main = inspector.clazz(MAIN);
+              assertThat(main, isPresent());
 
-          MethodSubject method = main.uniqueMethodWithName(methodName);
-          assertThat(method, isPresent());
-          int arity = method.getMethod().method.getArity();
-          // One from the method's own argument, if any, and
-          // Two from Array utils, `contains` and `indexOf`, if inlined with access relaxation.
-          assertEquals(
-              allowAccessModification ? 0 : arity + 2,
-              countCall(method, "checkParameterIsNotNull"));
-        });
+              MethodSubject method = main.uniqueMethodWithName(methodName);
+              assertThat(method, isPresent());
+              int arity = method.getMethod().method.getArity();
+              // One from the method's own argument, if any, and
+              // Two from Array utils, `contains` and `indexOf`, if inlined with access relaxation.
+              assertEquals(
+                  allowAccessModification ? 0 : arity + 2,
+                  countCall(method, "checkParameterIsNotNull"));
+            });
   }
 }
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 3c28a7d..b59d98c 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinUnusedArgumentsInLambdasTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinUnusedArgumentsInLambdasTest.java
@@ -3,11 +3,13 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
@@ -20,14 +22,15 @@
 @RunWith(Parameterized.class)
 public class KotlinUnusedArgumentsInLambdasTest extends AbstractR8KotlinTestBase {
 
-  @Parameterized.Parameters(name = "target: {0}, allowAccessModification: {1}")
+  @Parameterized.Parameters(name = "target: {0}, kotlinc: {1}, allowAccessModification: {2}")
   public static Collection<Object[]> data() {
-    return buildParameters(KotlinTargetVersion.values(), BooleanUtils.values());
+    return buildParameters(
+        KotlinTargetVersion.values(), getKotlinCompilers(), BooleanUtils.values());
   }
 
   public KotlinUnusedArgumentsInLambdasTest(
-      KotlinTargetVersion targetVersion, boolean allowAccessModification) {
-    super(targetVersion, allowAccessModification);
+      KotlinTargetVersion targetVersion, KotlinCompiler kotlinc, boolean allowAccessModification) {
+    super(targetVersion, kotlinc, allowAccessModification);
   }
 
   @Test
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 10fe60e..c358325 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinUnusedSingletonTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinUnusedSingletonTest.java
@@ -3,12 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
@@ -25,17 +27,18 @@
 @RunWith(Parameterized.class)
 public class KotlinUnusedSingletonTest extends AbstractR8KotlinTestBase {
 
-  @Parameterized.Parameters(name = "target: {0}, allowAccessModification: {1}")
+  @Parameterized.Parameters(name = "target: {0}, kotlinc: {1}, allowAccessModification: {1}")
   public static Collection<Object[]> data() {
-    return buildParameters(KotlinTargetVersion.values(), BooleanUtils.values());
+    return buildParameters(
+        KotlinTargetVersion.values(), getKotlinCompilers(), BooleanUtils.values());
   }
 
   private static final String printlnSignature =
       "void java.io.PrintStream.println(java.lang.Object)";
 
   public KotlinUnusedSingletonTest(
-      KotlinTargetVersion targetVersion, boolean allowAccessModification) {
-    super(targetVersion, allowAccessModification);
+      KotlinTargetVersion targetVersion, KotlinCompiler kotlinc, boolean allowAccessModification) {
+    super(targetVersion, kotlinc, allowAccessModification);
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java
index e29373a..e90a8b6 100644
--- a/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinReflectionLibTest.java
@@ -3,6 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
+
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.KotlinTestBase;
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestParameters;
@@ -18,18 +21,21 @@
 
 @RunWith(Parameterized.class)
 public class ProcessKotlinReflectionLibTest extends KotlinTestBase {
-  private final TestParameters parameters;
 
-  public ProcessKotlinReflectionLibTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
-    this.parameters = parameters;
-  }
+  private final TestParameters parameters;
 
   @Parameterized.Parameters(name = "{0} target: {1}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withAllRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withAllRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
+  }
+
+  public ProcessKotlinReflectionLibTest(
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
+    this.parameters = parameters;
   }
 
   private void test(Collection<String> rules) throws Exception {
@@ -39,8 +45,9 @@
   private void test(
       Collection<String> rules, ThrowableConsumer<R8FullTestBuilder> consumer) throws Exception {
     testForR8(parameters.getBackend())
-        .addLibraryFiles(ToolHelper.getMostRecentAndroidJar(), ToolHelper.getKotlinStdlibJar())
-        .addProgramFiles(ToolHelper.getKotlinReflectJar())
+        .addLibraryFiles(
+            ToolHelper.getMostRecentAndroidJar(), ToolHelper.getKotlinStdlibJar(kotlinc))
+        .addProgramFiles(ToolHelper.getKotlinReflectJar(kotlinc))
         .addKeepRules(rules)
         .addKeepAttributes(ProguardKeepAttributes.SIGNATURE)
         .addKeepAttributes(ProguardKeepAttributes.INNER_CLASSES)
diff --git a/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinStdlibTest.java b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinStdlibTest.java
index 195b536..7b3fd69 100644
--- a/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinStdlibTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/ProcessKotlinStdlibTest.java
@@ -3,6 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
+
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.KotlinTestBase;
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestParameters;
@@ -20,15 +23,18 @@
 public class ProcessKotlinStdlibTest extends KotlinTestBase {
   private final TestParameters parameters;
 
-  public ProcessKotlinStdlibTest(TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
-    this.parameters = parameters;
-  }
-
   @Parameterized.Parameters(name = "{0} target: {1}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withAllRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withAllRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
+  }
+
+  public ProcessKotlinStdlibTest(
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
+    this.parameters = parameters;
   }
 
   private void test(Collection<String> rules, boolean expectInvalidFoo) throws Exception {
@@ -41,7 +47,7 @@
       ThrowableConsumer<R8FullTestBuilder> consumer)
       throws Exception {
     testForR8(parameters.getBackend())
-        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
         .addKeepRules(rules)
         .addKeepAttributes(ProguardKeepAttributes.SIGNATURE)
         .addKeepAttributes(ProguardKeepAttributes.INNER_CLASSES)
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 b4aeced..cd5d8b1 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinAccessorTest.java
@@ -4,10 +4,12 @@
 
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.R8TestBuilder;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -65,14 +67,15 @@
           .addProperty("property", JAVA_LANG_STRING, Visibility.PRIVATE)
           .addProperty("indirectPropertyGetter", JAVA_LANG_STRING, Visibility.PRIVATE);
 
-  @Parameterized.Parameters(name = "target: {0}, allowAccessModification: {1}")
+  @Parameterized.Parameters(name = "target: {0}, kotlinc: {1}, allowAccessModification: {2}")
   public static Collection<Object[]> data() {
-    return buildParameters(KotlinTargetVersion.values(), BooleanUtils.values());
+    return buildParameters(
+        KotlinTargetVersion.values(), getKotlinCompilers(), BooleanUtils.values());
   }
 
   public R8KotlinAccessorTest(
-      KotlinTargetVersion targetVersion, boolean allowAccessModification) {
-    super(targetVersion, allowAccessModification);
+      KotlinTargetVersion targetVersion, KotlinCompiler kotlinc, boolean allowAccessModification) {
+    super(targetVersion, kotlinc, allowAccessModification);
   }
 
   @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 ec0ee03..ef13161 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinDataClassTest.java
@@ -4,6 +4,9 @@
 
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
+
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.graph.DexCode;
 import com.android.tools.r8.kotlin.TestKotlinClass.Visibility;
@@ -42,14 +45,15 @@
 
   private Consumer<InternalOptions> disableClassInliner = o -> o.enableClassInlining = false;
 
-  @Parameterized.Parameters(name = "target: {0}, allowAccessModification: {1}")
+  @Parameterized.Parameters(name = "target: {0}, kotlinc: {2}, allowAccessModification: {1}")
   public static Collection<Object[]> data() {
-    return buildParameters(KotlinTargetVersion.values(), BooleanUtils.values());
+    return buildParameters(
+        KotlinTargetVersion.values(), getKotlinCompilers(), BooleanUtils.values());
   }
 
   public R8KotlinDataClassTest(
-      KotlinTargetVersion targetVersion, boolean allowAccessModification) {
-    super(targetVersion, allowAccessModification);
+      KotlinTargetVersion targetVersion, KotlinCompiler kotlinc, boolean allowAccessModification) {
+    super(targetVersion, kotlinc, allowAccessModification);
   }
 
   @Test
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 770d6cc..ea12c12 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinIntrinsicsTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinIntrinsicsTest.java
@@ -4,6 +4,9 @@
 
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
+
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.utils.BooleanUtils;
@@ -23,14 +26,15 @@
   private static final TestKotlinDataClass KOTLIN_INTRINSICS_CLASS =
       new TestKotlinDataClass("kotlin.jvm.internal.Intrinsics");
 
-  @Parameterized.Parameters(name = "target: {0}, allowAccessModification: {1}")
+  @Parameterized.Parameters(name = "target: {0}, kotlinc: {1}, allowAccessModification: {2}")
   public static Collection<Object[]> data() {
-    return buildParameters(KotlinTargetVersion.values(), BooleanUtils.values());
+    return buildParameters(
+        KotlinTargetVersion.values(), getKotlinCompilers(), BooleanUtils.values());
   }
 
   public R8KotlinIntrinsicsTest(
-      KotlinTargetVersion targetVersion, boolean allowAccessModification) {
-    super(targetVersion, allowAccessModification);
+      KotlinTargetVersion targetVersion, KotlinCompiler kotlinc, boolean allowAccessModification) {
+    super(targetVersion, kotlinc, allowAccessModification);
   }
 
   @Test
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 30511bb..e04585c 100644
--- a/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/R8KotlinPropertiesTest.java
@@ -4,8 +4,10 @@
 
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.kotlin.TestKotlinClass.Visibility;
 import com.android.tools.r8.naming.MemberNaming;
@@ -93,14 +95,15 @@
         o.enableClassStaticizer = false;
       };
 
-  @Parameterized.Parameters(name = "target: {0}, allowAccessModification: {1}")
+  @Parameterized.Parameters(name = "target: {0}, kotlinc: {1}, allowAccessModification: {1}")
   public static Collection<Object[]> data() {
-    return buildParameters(KotlinTargetVersion.values(), BooleanUtils.values());
+    return buildParameters(
+        KotlinTargetVersion.values(), getKotlinCompilers(), BooleanUtils.values());
   }
 
   public R8KotlinPropertiesTest(
-      KotlinTargetVersion targetVersion, boolean allowAccessModification) {
-    super(targetVersion, allowAccessModification);
+      KotlinTargetVersion targetVersion, KotlinCompiler kotlinc, boolean allowAccessModification) {
+    super(targetVersion, kotlinc, allowAccessModification);
   }
 
   @Test
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 d56d0f0..70fd115 100644
--- a/src/test/java/com/android/tools/r8/kotlin/SimplifyIfNotNullKotlinTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/SimplifyIfNotNullKotlinTest.java
@@ -3,8 +3,10 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
 import com.android.tools.r8.utils.BooleanUtils;
@@ -21,14 +23,15 @@
   private static final String FOLDER = "non_null";
   private static final String STRING = "java.lang.String";
 
-  @Parameterized.Parameters(name = "target: {0}, allowAccessModification: {1}")
+  @Parameterized.Parameters(name = "target: {0}, kotlinc: {1}, allowAccessModification: {2}")
   public static Collection<Object[]> data() {
-    return buildParameters(KotlinTargetVersion.values(), BooleanUtils.values());
+    return buildParameters(
+        KotlinTargetVersion.values(), getKotlinCompilers(), BooleanUtils.values());
   }
 
   public SimplifyIfNotNullKotlinTest(
-      KotlinTargetVersion targetVersion, boolean allowAccessModification) {
-    super(targetVersion, allowAccessModification);
+      KotlinTargetVersion targetVersion, KotlinCompiler kotlinc, boolean allowAccessModification) {
+    super(targetVersion, kotlinc, allowAccessModification);
   }
 
   @Test
diff --git a/src/test/java/com/android/tools/r8/kotlin/coroutines/KotlinxCoroutinesTestRunner.java b/src/test/java/com/android/tools/r8/kotlin/coroutines/KotlinxCoroutinesTestRunner.java
index 6df1462..9ae5aed 100644
--- a/src/test/java/com/android/tools/r8/kotlin/coroutines/KotlinxCoroutinesTestRunner.java
+++ b/src/test/java/com/android/tools/r8/kotlin/coroutines/KotlinxCoroutinesTestRunner.java
@@ -4,9 +4,10 @@
 
 package com.android.tools.r8.kotlin.coroutines;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -49,16 +50,19 @@
   private Set<String> notWorkingTests =
       Sets.newHashSet("kotlinx.coroutines.test.TestDispatchersTest");
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   private final TestParameters parameters;
 
-  public KotlinxCoroutinesTestRunner(TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+  public KotlinxCoroutinesTestRunner(
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
@@ -88,7 +92,7 @@
   }
 
   private Path compileTestSources(Path baseJar) throws Exception {
-    return kotlinc(KOTLINC, targetVersion)
+    return kotlinc(kotlinc, targetVersion)
         .addArguments(
             "-Xuse-experimental=kotlinx.coroutines.InternalCoroutinesApi",
             "-Xuse-experimental=kotlinx.coroutines.ObsoleteCoroutinesApi",
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/JStyleKotlinLambdaMergingWithEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/kotlin/lambda/JStyleKotlinLambdaMergingWithEnumUnboxingTest.java
index 1aef013..7103667 100644
--- a/src/test/java/com/android/tools/r8/kotlin/lambda/JStyleKotlinLambdaMergingWithEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/JStyleKotlinLambdaMergingWithEnumUnboxingTest.java
@@ -4,17 +4,20 @@
 
 package com.android.tools.r8.kotlin.lambda;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
+
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 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;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.ir.optimize.lambda.kotlin.JStyleLambdaGroupIdFactory;
 import com.android.tools.r8.ir.optimize.lambda.kotlin.KotlinLambdaGroupIdFactory;
 import com.android.tools.r8.kotlin.lambda.JStyleKotlinLambdaMergingWithEnumUnboxingTest.Main.EnumUnboxingCandidate;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -24,14 +27,19 @@
 public class JStyleKotlinLambdaMergingWithEnumUnboxingTest extends TestBase {
 
   private final TestParameters parameters;
+  private final KotlinCompiler kotlinc;
 
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return TestBase.getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  @Parameters(name = "{0}, kotlinc: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        TestBase.getTestParameters().withDexRuntimes().withAllApiLevels().build(),
+        getKotlinCompilers());
   }
 
-  public JStyleKotlinLambdaMergingWithEnumUnboxingTest(TestParameters parameters) {
+  public JStyleKotlinLambdaMergingWithEnumUnboxingTest(
+      TestParameters parameters, KotlinCompiler kotlinc) {
     this.parameters = parameters;
+    this.kotlinc = kotlinc;
   }
 
   @Test
@@ -39,7 +47,7 @@
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
         .addDefaultRuntimeLibrary(parameters)
-        .addLibraryFiles(ToolHelper.getKotlinStdlibJar())
+        .addLibraryFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
         .addKeepMainRule(Main.class)
         .addOptionsModification(
             options ->
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/KStyleKotlinLambdaMergingWithEnumUnboxingTest.java b/src/test/java/com/android/tools/r8/kotlin/lambda/KStyleKotlinLambdaMergingWithEnumUnboxingTest.java
index 6edeebc..b9baae8 100644
--- a/src/test/java/com/android/tools/r8/kotlin/lambda/KStyleKotlinLambdaMergingWithEnumUnboxingTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/KStyleKotlinLambdaMergingWithEnumUnboxingTest.java
@@ -4,17 +4,20 @@
 
 package com.android.tools.r8.kotlin.lambda;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
+
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 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;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.graph.DexProgramClass;
 import com.android.tools.r8.ir.optimize.lambda.kotlin.KStyleLambdaGroupIdFactory;
 import com.android.tools.r8.ir.optimize.lambda.kotlin.KotlinLambdaGroupIdFactory;
 import com.android.tools.r8.kotlin.lambda.KStyleKotlinLambdaMergingWithEnumUnboxingTest.Main.EnumUnboxingCandidate;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -24,21 +27,26 @@
 public class KStyleKotlinLambdaMergingWithEnumUnboxingTest extends TestBase {
 
   private final TestParameters parameters;
+  private final KotlinCompiler kotlinc;
 
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return TestBase.getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  @Parameters(name = "{0}, kotlinc: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        TestBase.getTestParameters().withDexRuntimes().withAllApiLevels().build(),
+        getKotlinCompilers());
   }
 
-  public KStyleKotlinLambdaMergingWithEnumUnboxingTest(TestParameters parameters) {
+  public KStyleKotlinLambdaMergingWithEnumUnboxingTest(
+      TestParameters parameters, KotlinCompiler kotlinc) {
     this.parameters = parameters;
+    this.kotlinc = kotlinc;
   }
 
   @Test
   public void test() throws Exception {
     testForR8(parameters.getBackend())
         .addInnerClasses(getClass())
-        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
         .addKeepMainRule(Main.class)
         .addOptionsModification(
             options ->
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 f28f68b..1b6d4c0 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
@@ -3,10 +3,11 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.lambda;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.junit.Assume.assumeTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime;
 import com.android.tools.r8.TestRuntime.CfRuntime;
@@ -25,15 +26,17 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withAllRuntimesAndApiLevels().build(), KotlinTargetVersion.values());
+        getTestParameters().withAllRuntimesAndApiLevels().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public KotlinLambdaMergerValidationTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion, false);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc, false);
     this.parameters = parameters;
   }
 
@@ -46,17 +49,17 @@
     CfRuntime cfRuntime =
         parameters.isCfRuntime() ? parameters.getRuntime().asCf() : TestRuntime.getCheckedInJdk9();
     Path ktClasses =
-        kotlinc(cfRuntime, KOTLINC, targetVersion)
+        kotlinc(cfRuntime, kotlinc, targetVersion)
             .addSourceFiles(getKotlinFileInTest(folder, "b143165163"))
             .compile();
     testForR8(parameters.getBackend())
         .addLibraryFiles(ToolHelper.getJava8RuntimeJar())
-        .addLibraryFiles(ToolHelper.getKotlinStdlibJar())
+        .addLibraryFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
         .addProgramFiles(ktClasses)
         .addKeepMainRule("**.B143165163Kt")
         .setMinApi(parameters.getApiLevel())
         .compile()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar())
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
         .run(parameters.getRuntime(), pkg + ".B143165163Kt")
         .assertSuccessWithOutputLines("outer foo bar", "outer foo default");
   }
@@ -70,11 +73,11 @@
     CfRuntime cfRuntime =
         parameters.isCfRuntime() ? parameters.getRuntime().asCf() : TestRuntime.getCheckedInJdk9();
     Path ktClasses =
-        kotlinc(cfRuntime, KOTLINC, targetVersion)
+        kotlinc(cfRuntime, kotlinc, targetVersion)
             .addSourceFiles(getKotlinFileInTest(folder, "b143165163"))
             .compile();
     testForR8(parameters.getBackend())
-        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
         .addProgramFiles(ktClasses)
         .addKeepMainRule("**.B143165163Kt")
         .allowDiagnosticWarningMessages()
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingDebugTest.java b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingDebugTest.java
index 3a795be..e7c4762 100644
--- a/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingDebugTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/KotlinLambdaMergingDebugTest.java
@@ -3,13 +3,15 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.lambda;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static org.hamcrest.CoreMatchers.equalTo;
 
 import com.android.tools.r8.CompilationMode;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.kotlin.AbstractR8KotlinTestBase;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -23,20 +25,25 @@
   private static final String MAIN_CLASS = "reprocess_merged_lambdas_kstyle.MainKt";
 
   @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  public static List<Object[]> data() {
+    return buildParameters(
+        getTestParameters().withDexRuntimes().withAllApiLevels().build(), getKotlinCompilers());
   }
 
-  public KotlinLambdaMergingDebugTest(TestParameters parameters) {
-    super(KotlinTargetVersion.JAVA_6);
+  public KotlinLambdaMergingDebugTest(TestParameters parameters, KotlinCompiler kotlinc) {
+    super(KotlinTargetVersion.JAVA_6, kotlinc);
     this.parameters = parameters;
   }
 
+  private static final KotlinCompileMemoizer compiledJars =
+      getCompileMemoizer(getKotlinFilesInResource(FOLDER), FOLDER)
+          .configure(kotlinCompilerTool -> kotlinCompilerTool.includeRuntime().noReflect());
+
   @Test
   public void testMergingKStyleLambdasAndReprocessingInDebug() throws Exception {
     testForR8(parameters.getBackend())
         .setMode(CompilationMode.DEBUG)
-        .addProgramFiles(getKotlinJarFile(FOLDER))
+        .addProgramFiles(compiledJars.getForConfiguration(kotlinc, KotlinTargetVersion.JAVA_6))
         .addProgramFiles(getJavaJarFile(FOLDER))
         .setMinApi(parameters.getApiLevel())
         .addKeepMainRule(MAIN_CLASS)
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 4f3ea88..b9e0546 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
@@ -4,12 +4,14 @@
 
 package com.android.tools.r8.kotlin.lambda;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.google.common.base.Predicates.alwaysTrue;
 import static junit.framework.TestCase.assertEquals;
 import static junit.framework.TestCase.assertTrue;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.fail;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.graph.DexClass;
 import com.android.tools.r8.graph.DexType;
@@ -40,23 +42,29 @@
     // 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;
+    options.horizontalClassMergerOptions().disableKotlinLambdaMerging();
   }
 
   private final boolean enableUnusedInterfaceRemoval;
 
   @Parameterized.Parameters(
-      name = "target: {0}, allow access modification: {1}, unused interface removal: {2}")
+      name =
+          "target: {0}, kotlinc: {1}, allow access modification: {2}, unused interface removal:"
+              + " {3}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        KotlinTargetVersion.values(), BooleanUtils.values(), BooleanUtils.values());
+        KotlinTargetVersion.values(),
+        getKotlinCompilers(),
+        BooleanUtils.values(),
+        BooleanUtils.values());
   }
 
   public KotlinLambdaMergingTest(
       KotlinTargetVersion targetVersion,
+      KotlinCompiler kotlinc,
       boolean allowAccessModification,
       boolean enableUnusedInterfaceRemoval) {
-    super(targetVersion, allowAccessModification);
+    super(targetVersion, kotlinc, allowAccessModification);
     this.enableUnusedInterfaceRemoval = enableUnusedInterfaceRemoval;
   }
 
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 d6e8c53..b1b38b9 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
@@ -3,6 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.lambda;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
+
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.kotlin.AbstractR8KotlinTestBase;
 import com.android.tools.r8.utils.BooleanUtils;
@@ -14,14 +17,15 @@
 @RunWith(Parameterized.class)
 public class KotlinLambdaMergingWithReprocessingTest extends AbstractR8KotlinTestBase {
 
-  @Parameterized.Parameters(name = "target: {0}, allowAccessModification: {1}")
+  @Parameterized.Parameters(name = "target: {0}, kotlinc: {1}, allowAccessModification: {2}")
   public static Collection<Object[]> data() {
-    return buildParameters(KotlinTargetVersion.values(), BooleanUtils.values());
+    return buildParameters(
+        KotlinTargetVersion.values(), getKotlinCompilers(), BooleanUtils.values());
   }
 
   public KotlinLambdaMergingWithReprocessingTest(
-      KotlinTargetVersion targetVersion, boolean allowAccessModification) {
-    super(targetVersion, allowAccessModification);
+      KotlinTargetVersion targetVersion, KotlinCompiler kotlinc, boolean allowAccessModification) {
+    super(targetVersion, kotlinc, allowAccessModification);
   }
 
   @Test
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 127c1e3..1dcdcfb 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
@@ -3,6 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.lambda;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
+
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.kotlin.AbstractR8KotlinTestBase;
 import com.android.tools.r8.utils.BooleanUtils;
@@ -14,14 +17,15 @@
 @RunWith(Parameterized.class)
 public class KotlinLambdaMergingWithSmallInliningBudgetTest extends AbstractR8KotlinTestBase {
 
-  @Parameterized.Parameters(name = "target: {0}, allowAccessModification: {1}")
+  @Parameterized.Parameters(name = "target: {0}, kotlinc: {1}, allowAccessModification: {2}")
   public static Collection<Object[]> data() {
-    return buildParameters(KotlinTargetVersion.values(), BooleanUtils.values());
+    return buildParameters(
+        KotlinTargetVersion.values(), getKotlinCompilers(), BooleanUtils.values());
   }
 
   public KotlinLambdaMergingWithSmallInliningBudgetTest(
-      KotlinTargetVersion targetVersion, boolean allowAccessModification) {
-    super(targetVersion, allowAccessModification);
+      KotlinTargetVersion targetVersion, KotlinCompiler kotlinc, boolean allowAccessModification) {
+    super(targetVersion, kotlinc, allowAccessModification);
   }
 
   @Test
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 d764d5f..0dd8a62 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
@@ -4,7 +4,7 @@
 
 package com.android.tools.r8.kotlin.lambda.b148525512;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static junit.framework.TestCase.assertEquals;
 import static junit.framework.TestCase.assertTrue;
@@ -12,6 +12,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.DexIndexedConsumer.ArchiveConsumer;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.KotlinTestBase;
 import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.TestParameters;
@@ -22,16 +23,13 @@
 import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.io.IOException;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.stream.Collectors;
-import org.junit.BeforeClass;
-import org.junit.ClassRule;
 import org.junit.Test;
-import org.junit.rules.TemporaryFolder;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 
@@ -44,43 +42,45 @@
   private static final String featureKtClassNamet = kotlinTestClassesPackage + ".FeatureKt";
   private static final String baseClassName = kotlinTestClassesPackage + ".Base";
 
-  private static final Map<KotlinTargetVersion, Path> kotlinBaseClasses = new HashMap<>();
-  private static final Map<KotlinTargetVersion, Path> kotlinFeatureClasses = new HashMap<>();
+  private static final KotlinCompileMemoizer kotlinBaseClasses =
+      getCompileMemoizer(getKotlinFileInTestPackage(pkg, "base"))
+          .configure(
+              kotlinCompilerTool -> kotlinCompilerTool.addClasspathFiles(getFeatureApiPath()));
+  private static final KotlinCompileMemoizer kotlinFeatureClasses =
+      getCompileMemoizer(getKotlinFileInTestPackage(pkg, "feature"))
+          .configure(
+              kotlinCompilerTool -> {
+                // Compile the feature Kotlin code with the base classes on classpath.
+                kotlinCompilerTool.addClasspathFiles(
+                    kotlinBaseClasses.getForConfiguration(
+                        kotlinCompilerTool.getCompiler(), kotlinCompilerTool.getTargetVersion()));
+              });
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0},{1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
         getTestParameters().withDexRuntimes().withAllApiLevels().build(),
-        KotlinTargetVersion.values());
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
-  public B148525512(TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+  public B148525512(
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  @ClassRule public static TemporaryFolder classTemp = new TemporaryFolder();
-
-  @BeforeClass
-  public static void compileKotlin() throws Exception {
-    // Compile the base Kotlin with the FeatureAPI Java class on classpath.
-    Path featureApiJar = classTemp.newFile("feature_api.jar").toPath();
-    writeClassesToJar(featureApiJar, FeatureAPI.class);
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path ktBaseClasses =
-          kotlinc(KOTLINC, targetVersion)
-              .addClasspathFiles(featureApiJar)
-              .addSourceFiles(getKotlinFileInTestPackage(pkg, "base"))
-              .compile();
-      kotlinBaseClasses.put(targetVersion, ktBaseClasses);
-      // Compile the feature Kotlin code with the base classes on classpath.
-      Path ktFeatureClasses =
-          kotlinc(KOTLINC, targetVersion)
-              .addClasspathFiles(ktBaseClasses)
-              .addSourceFiles(getKotlinFileInTestPackage(pkg, "feature"))
-              .compile();
-      kotlinFeatureClasses.put(targetVersion, ktFeatureClasses);
+  private static Path getFeatureApiPath() {
+    try {
+      Path featureApiJar = getStaticTemp().getRoot().toPath().resolve("feature_api.jar");
+      if (Files.exists(featureApiJar)) {
+        return featureApiJar;
+      }
+      writeClassesToJar(featureApiJar, FeatureAPI.class);
+      return featureApiJar;
+    } catch (IOException e) {
+      throw new RuntimeException(e);
     }
   }
 
@@ -118,15 +118,15 @@
     Path featureCode = temp.newFile("feature.zip").toPath();
     R8TestCompileResult compileResult =
         testForR8(parameters.getBackend())
-            .addProgramFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(kotlinBaseClasses.get(targetVersion))
+            .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(kotlinBaseClasses.getForConfiguration(kotlinc, targetVersion))
             .addProgramClasses(FeatureAPI.class)
             .addKeepMainRule(baseKtClassName)
             .addKeepClassAndMembersRules(baseClassName)
             .addKeepClassAndMembersRules(featureKtClassNamet)
             .addKeepClassAndMembersRules(FeatureAPI.class)
             .addOptionsModification(
-                options -> options.enableHorizontalClassMergingOfKotlinLambdas = false)
+                options -> options.horizontalClassMergerOptions().disableKotlinLambdaMerging())
             .setMinApi(parameters.getApiLevel())
             .noMinification() // The check cast inspection above relies on original names.
             .addFeatureSplit(
@@ -134,7 +134,8 @@
                     builder
                         .addProgramResourceProvider(
                             ArchiveResourceProvider.fromArchive(
-                                kotlinFeatureClasses.get(targetVersion), true))
+                                kotlinFeatureClasses.getForConfiguration(kotlinc, targetVersion),
+                                true))
                         .setProgramConsumer(new ArchiveConsumer(featureCode, false))
                         .build())
             .allowDiagnosticWarningMessages()
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/b159688129/LambdaGroupGCLimitTest.java b/src/test/java/com/android/tools/r8/kotlin/lambda/b159688129/LambdaGroupGCLimitTest.java
index 73b42ed..57cdae7 100644
--- a/src/test/java/com/android/tools/r8/kotlin/lambda/b159688129/LambdaGroupGCLimitTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/b159688129/LambdaGroupGCLimitTest.java
@@ -8,15 +8,17 @@
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.R8TestCompileResult;
 import com.android.tools.r8.TestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.InternalOptions.HorizontalClassMergerOptions;
 import com.android.tools.r8.utils.codeinspector.FoundClassSubject;
 import java.io.IOException;
 import java.nio.file.Path;
@@ -32,16 +34,20 @@
 @RunWith(Parameterized.class)
 public class LambdaGroupGCLimitTest extends TestBase {
 
+  private final boolean enableHorizontalClassMergingOfKotlinLambdas;
   private final TestParameters parameters;
   private final int LAMBDA_HOLDER_LIMIT = 50;
   private final int LAMBDAS_PER_CLASS_LIMIT = 100;
 
-  @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withDexRuntimes().withAllApiLevels().build();
+  @Parameters(name = "{1}, horizontal class merging: {0}")
+  public static List<Object[]> data() {
+    return buildParameters(
+        BooleanUtils.values(), getTestParameters().withDexRuntimes().withAllApiLevels().build());
   }
 
-  public LambdaGroupGCLimitTest(TestParameters parameters) {
+  public LambdaGroupGCLimitTest(
+      boolean enableHorizontalClassMergingOfKotlinLambdas, TestParameters parameters) {
+    this.enableHorizontalClassMergingOfKotlinLambdas = enableHorizontalClassMergingOfKotlinLambdas;
     this.parameters = parameters;
   }
 
@@ -50,7 +56,12 @@
     String PKG_NAME = LambdaGroupGCLimitTest.class.getPackage().getName();
     R8FullTestBuilder testBuilder =
         testForR8(parameters.getBackend())
-            .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+            .addProgramFiles(ToolHelper.getKotlinStdlibJar(ToolHelper.getKotlinC_1_3_72()))
+            .addOptionsModification(
+                options ->
+                    options
+                        .horizontalClassMergerOptions()
+                        .enableKotlinLambdaMergingIf(enableHorizontalClassMergingOfKotlinLambdas))
             .setMinApi(parameters.getApiLevel())
             .noMinification();
     Path classFiles = temp.newFile("classes.jar").toPath();
@@ -63,7 +74,27 @@
       testBuilder.addKeepClassAndMembersRules(PKG_NAME + ".MainKt" + mainId);
     }
     writeClassFileDataToJar(classFiles, classFileData);
-    R8TestCompileResult compileResult = testBuilder.addProgramFiles(classFiles).compile();
+    R8TestCompileResult compileResult =
+        testBuilder
+            .addProgramFiles(classFiles)
+            .addHorizontallyMergedClassesInspector(
+                inspector -> {
+                  if (enableHorizontalClassMergingOfKotlinLambdas) {
+                    HorizontalClassMergerOptions defaultHorizontalClassMergerOptions =
+                        new HorizontalClassMergerOptions();
+                    assertEquals(4833, inspector.getSources().size());
+                    assertEquals(167, inspector.getTargets().size());
+                    assertTrue(
+                        inspector.getMergeGroups().stream()
+                            .allMatch(
+                                mergeGroup ->
+                                    mergeGroup.size()
+                                        <= defaultHorizontalClassMergerOptions.getMaxGroupSize()));
+                  } else {
+                    inspector.assertNoClassesMerged();
+                  }
+                })
+            .compile();
     Path path = compileResult.writeToZip();
     compileResult
         .run(parameters.getRuntime(), PKG_NAME + ".MainKt0")
@@ -74,7 +105,9 @@
                   codeInspector.allClasses().stream()
                       .filter(c -> c.getFinalName().contains("LambdaGroup"))
                       .collect(Collectors.toList());
-              assertEquals(1, lambdaGroups.size());
+              assertEquals(
+                  1 - BooleanUtils.intValue(enableHorizontalClassMergingOfKotlinLambdas),
+                  lambdaGroups.size());
             });
     Path oatFile = temp.newFile("out.oat").toPath();
     ProcessResult processResult =
diff --git a/src/test/java/com/android/tools/r8/kotlin/lambda/b159688129/LambdaSplitByCodeCorrectnessTest.java b/src/test/java/com/android/tools/r8/kotlin/lambda/b159688129/LambdaSplitByCodeCorrectnessTest.java
index 3926dea..c261c25 100644
--- a/src/test/java/com/android/tools/r8/kotlin/lambda/b159688129/LambdaSplitByCodeCorrectnessTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/lambda/b159688129/LambdaSplitByCodeCorrectnessTest.java
@@ -4,10 +4,11 @@
 
 package com.android.tools.r8.kotlin.lambda.b159688129;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestRuntime;
 import com.android.tools.r8.TestRuntime.CfRuntime;
@@ -33,17 +34,21 @@
   private final KotlinTargetVersion targetVersion;
   private final boolean splitGroup;
 
-  @Parameters(name = "{0}, targetVersion: {1}, splitGroup: {2}")
+  @Parameters(name = "{0}, kotlinc: {2} targetVersion: {1}, splitGroup: {3}")
   public static List<Object[]> data() {
     return buildParameters(
         getTestParameters().withDexRuntimes().withAllApiLevels().build(),
         KotlinTargetVersion.values(),
+        getKotlinCompilers(),
         BooleanUtils.values());
   }
 
   public LambdaSplitByCodeCorrectnessTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion, boolean splitGroup) {
-    super(targetVersion);
+      TestParameters parameters,
+      KotlinTargetVersion targetVersion,
+      KotlinCompiler kotlinc,
+      boolean splitGroup) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
     this.targetVersion = targetVersion;
     this.splitGroup = splitGroup;
@@ -56,12 +61,14 @@
     CfRuntime cfRuntime =
         parameters.isCfRuntime() ? parameters.getRuntime().asCf() : TestRuntime.getCheckedInJdk9();
     Path ktClasses =
-        kotlinc(cfRuntime, KOTLINC, targetVersion)
+        kotlinc(cfRuntime, kotlinc, targetVersion)
             .addSourceFiles(getKotlinFileInTest(folder, "Simple"))
             .compile();
     testForR8(parameters.getBackend())
-        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
         .addProgramFiles(ktClasses)
+        .addOptionsModification(
+            options -> options.horizontalClassMergerOptions().disableKotlinLambdaMerging())
         .setMinApi(parameters.getApiLevel())
         .addKeepMainRule(PKG_NAME + ".SimpleKt")
         .applyIf(
@@ -70,8 +77,7 @@
                 b.addOptionsModification(
                     internalOptions ->
                         // Setting verificationSizeLimitInBytesOverride = 1 will force a a chain
-                        // having
-                        // only a single implementation method in each.
+                        // having only a single implementation method in each.
                         internalOptions.testing.verificationSizeLimitInBytesOverride =
                             splitGroup ? 1 : -1))
         .noMinification()
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/KotlinMetadataTestBase.java b/src/test/java/com/android/tools/r8/kotlin/metadata/KotlinMetadataTestBase.java
index 99aaf1b..e158fc7 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/KotlinMetadataTestBase.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/KotlinMetadataTestBase.java
@@ -8,6 +8,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertNull;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.kotlin.AbstractR8KotlinTestBase;
 import com.android.tools.r8.kotlin.KotlinMetadataWriter;
@@ -21,8 +22,8 @@
 
 public abstract class KotlinMetadataTestBase extends AbstractR8KotlinTestBase {
 
-  public KotlinMetadataTestBase(KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+  public KotlinMetadataTestBase(KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
   }
 
   static final String PKG = KotlinMetadataTestBase.class.getPackage().getName();
@@ -40,8 +41,8 @@
   static final String KT_FUNCTION1 = "Lkotlin/Function1;";
   static final String KT_COMPARABLE = "Lkotlin/Comparable;";
 
-  public void assertEqualMetadata(CodeInspector originalInspector, CodeInspector rewrittenInspector)
-      throws Exception {
+  public void assertEqualMetadata(
+      CodeInspector originalInspector, CodeInspector rewrittenInspector) {
     for (FoundClassSubject clazzSubject : originalInspector.allClasses()) {
       ClassSubject r8Clazz = rewrittenInspector.clazz(clazzSubject.getOriginalName());
       assertThat(r8Clazz, isPresent());
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataPrimitiveTypeRewriteTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataPrimitiveTypeRewriteTest.java
index 76c2894..31b489a 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataPrimitiveTypeRewriteTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataPrimitiveTypeRewriteTest.java
@@ -4,13 +4,14 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.JvmTestRunResult;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -19,9 +20,6 @@
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -34,44 +32,36 @@
   private static final String PKG_LIB = PKG + ".primitive_type_rewrite_lib";
   private static final String PKG_APP = PKG + ".primitive_type_rewrite_app";
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, compiler: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataPrimitiveTypeRewriteTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static Map<KotlinTargetVersion, Path> libJars = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path baseLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(
-                  getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_LIB), "lib"))
-              .compile();
-      libJars.put(targetVersion, baseLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer libJars =
+      getCompileMemoizer(
+          getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_LIB), "lib"));
 
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = libJars.get(targetVersion);
+    Path libJar = libJars.getForConfiguration(kotlinc, targetVersion);
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG_APP + ".MainKt")
         .assertSuccessWithOutputLines(EXPECTED);
@@ -90,7 +80,9 @@
   private void runTest(boolean keepUnit) throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addProgramFiles(ToolHelper.getKotlinStdlibJar(), libJars.get(targetVersion))
+            .addProgramFiles(
+                ToolHelper.getKotlinStdlibJar(kotlinc),
+                libJars.getForConfiguration(kotlinc, targetVersion))
             .addKeepAllClassesRuleWithAllowObfuscation()
             .addKeepRules("-keep class " + PKG_LIB + ".LibKt { *; }")
             .addKeepRules("-keep class kotlin.Metadata { *; }")
@@ -107,7 +99,7 @@
                 equalTo("Resource 'META-INF/MANIFEST.MF' already exists."))
             .writeToZip();
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
@@ -115,7 +107,7 @@
             .compile();
     final JvmTestRunResult runResult =
         testForJvm()
-            .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+            .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
             .addClasspath(output)
             .run(parameters.getRuntime(), PKG_APP + ".MainKt");
     if (keepUnit) {
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataPrunedFieldsTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataPrunedFieldsTest.java
index 77134d8..a048351 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataPrunedFieldsTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataPrunedFieldsTest.java
@@ -4,23 +4,20 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.kotlin.metadata.metadata_pruned_fields.Main;
 import com.android.tools.r8.shaking.ProguardKeepAttributes;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
-import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -31,36 +28,28 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withAllRuntimesAndApiLevels().build(), KotlinTargetVersion.values());
+        getTestParameters().withAllRuntimesAndApiLevels().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
-  public MetadataPrunedFieldsTest(TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+  public MetadataPrunedFieldsTest(
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static Map<KotlinTargetVersion, Path> libJars = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String baseLibFolder = PKG_PREFIX + "/metadata_pruned_fields";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path baseLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(baseLibFolder, "Methods"))
-              .compile();
-      libJars.put(targetVersion, baseLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer libJars =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/metadata_pruned_fields", "Methods"));
 
   @Test
   public void testR8() throws Exception {
     testForR8(parameters.getBackend())
-        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
-        .addProgramFiles(libJars.get(targetVersion))
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+        .addProgramFiles(libJars.getForConfiguration(kotlinc, targetVersion))
         .addProgramClassFileData(Main.dump())
         .addKeepRules("-keep class " + PKG + ".metadata_pruned_fields.MethodsKt { *; }")
         .addKeepRules("-keep class kotlin.Metadata { *** pn(); }")
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAllowAccessModificationTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAllowAccessModificationTest.java
index 549f834..ea9ed33 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAllowAccessModificationTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAllowAccessModificationTest.java
@@ -4,12 +4,13 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static junit.framework.TestCase.assertEquals;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.core.StringContains.containsString;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -21,9 +22,6 @@
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -54,53 +52,40 @@
           "staticPrivate",
           "staticInternal");
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteAllowAccessModificationTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static Map<KotlinTargetVersion, Path> libJars = new HashMap<>();
-  private static Map<KotlinTargetVersion, Path> libReferenceJars = new HashMap<>();
+  private static final KotlinCompileMemoizer libJars =
+      getCompileMemoizer(
+          getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_LIB), "lib"));
+  private static final KotlinCompileMemoizer libReferenceJars =
+      getCompileMemoizer(
+          getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_LIB), "lib_reference"));
   private final TestParameters parameters;
 
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      libJars.put(
-          targetVersion,
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(
-                  getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_LIB), "lib"))
-              .compile());
-      libReferenceJars.put(
-          targetVersion,
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(
-                  getKotlinFileInTest(
-                      DescriptorUtils.getBinaryNameFromJavaType(PKG_LIB), "lib_reference"))
-              .compile());
-    }
-  }
-
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = libReferenceJars.get(targetVersion);
+    Path libJar = libReferenceJars.getForConfiguration(kotlinc, targetVersion);
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG_APP + ".MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -113,7 +98,7 @@
     // running with R8, the output should be binary compatible with libReference.
     Path libJar =
         testForR8(parameters.getBackend())
-            .addProgramFiles(libJars.get(targetVersion))
+            .addProgramFiles(libJars.getForConfiguration(kotlinc, targetVersion))
             .addKeepRules("-keepclassmembers,allowaccessmodification class **.Lib { *; }")
             .addKeepRules("-keep,allowaccessmodification,allowobfuscation class **.Lib { *; }")
             .addKeepRules("-keepclassmembers,allowaccessmodification class **.Lib$Comp { *; }")
@@ -134,7 +119,7 @@
             .inspect(this::inspect)
             .writeToZip();
     ProcessResult mainResult =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
@@ -144,7 +129,7 @@
     assertThat(mainResult.stderr, containsString("cannot access 'LibReference'"));
   }
 
-  private void inspect(CodeInspector inspector) throws Exception {
+  private void inspect(CodeInspector inspector) {
     // TODO(b/154348683): Assert equality between LibReference and Lib.
     // assertEqualMetadata(new CodeInspector(libReferenceJars.get(targetVersion)), inspector);
     ClassSubject lib = inspector.clazz(PKG_LIB + ".Lib");
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnnotationTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnnotationTest.java
index 0b65d77..428103f 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnnotationTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnnotationTest.java
@@ -4,13 +4,14 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -22,12 +23,10 @@
 import com.android.tools.r8.utils.codeinspector.KmTypeAliasSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.Map;
 import kotlinx.metadata.KmAnnotation;
 import kotlinx.metadata.KmAnnotationArgument;
 import kotlinx.metadata.KmAnnotationArgument.ArrayValue;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -59,38 +58,30 @@
   private static final String FOO_ORIGINAL_NAME = PKG_LIB + ".Foo";
   private static final String FOO_FINAL_NAME = "a.b.c";
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteAnnotationTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static Map<KotlinTargetVersion, Path> libJars = new HashMap<>();
+  private static final KotlinCompileMemoizer libJars =
+      getCompileMemoizer(
+          getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_LIB), "lib"));
   private final TestParameters parameters;
 
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path baseLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(
-                  getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_LIB), "lib"))
-              .compile();
-      libJars.put(targetVersion, baseLibJar);
-    }
-  }
-
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = libJars.get(targetVersion);
+    Path libJar = libJars.getForConfiguration(kotlinc, targetVersion);
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
@@ -98,7 +89,7 @@
             .compile();
     testForJvm()
         .addRunClasspathFiles(
-            ToolHelper.getKotlinStdlibJar(), ToolHelper.getKotlinReflectJar(), libJar)
+            ToolHelper.getKotlinStdlibJar(kotlinc), ToolHelper.getKotlinReflectJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG_APP + ".MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -108,7 +99,7 @@
   public void testMetadataForLib() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addProgramFiles(libJars.get(targetVersion))
+            .addProgramFiles(libJars.getForConfiguration(kotlinc, targetVersion))
             /// Keep the annotations
             .addKeepClassAndMembersRules(PKG_LIB + ".AnnoWithClassAndEnum")
             .addKeepClassAndMembersRules(PKG_LIB + ".AnnoWithClassArr")
@@ -134,14 +125,14 @@
             .inspect(this::inspect)
             .writeToZip();
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
             .compile();
     testForJvm()
         .addRunClasspathFiles(
-            ToolHelper.getKotlinStdlibJar(), ToolHelper.getKotlinReflectJar(), libJar)
+            ToolHelper.getKotlinStdlibJar(kotlinc), ToolHelper.getKotlinReflectJar(kotlinc), libJar)
         .addProgramFiles(output)
         .run(parameters.getRuntime(), PKG_APP + ".MainKt")
         .assertSuccessWithOutput(EXPECTED.replace(FOO_ORIGINAL_NAME, FOO_FINAL_NAME));
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnonymousTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnonymousTest.java
index 14cbe0b..0ff40ff 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnonymousTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteAnonymousTest.java
@@ -4,10 +4,11 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -17,9 +18,6 @@
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -29,45 +27,36 @@
 
   private final String EXPECTED = "foo";
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteAnonymousTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static Map<KotlinTargetVersion, Path> libJars = new HashMap<>();
+  private static final KotlinCompileMemoizer libJars =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/anonymous_lib", "lib"));
   private final TestParameters parameters;
 
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String baseLibFolder = PKG_PREFIX + "/anonymous_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path baseLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(baseLibFolder, "lib"))
-              .compile();
-      libJars.put(targetVersion, baseLibJar);
-    }
-  }
-
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = libJars.get(targetVersion);
+    Path libJar = libJars.getForConfiguration(kotlinc, targetVersion);
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/anonymous_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".anonymous_app.MainKt")
         .assertSuccessWithOutputLines(EXPECTED);
@@ -77,8 +66,8 @@
   public void testMetadataForLib() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(libJars.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(libJars.getForConfiguration(kotlinc, targetVersion))
             .addKeepAllClassesRuleWithAllowObfuscation()
             .addKeepRules("-keep class " + PKG + ".anonymous_lib.Test$A { *; }")
             .addKeepRules("-keep class " + PKG + ".anonymous_lib.Test { *; }")
@@ -91,13 +80,13 @@
             .inspect(this::inspect)
             .writeToZip();
     Path main =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/anonymous_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(main)
         .run(parameters.getRuntime(), PKG + ".anonymous_app.MainKt")
         .assertSuccessWithOutputLines(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteBoxedTypesTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteBoxedTypesTest.java
index 06d2636..2c2ae83 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteBoxedTypesTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteBoxedTypesTest.java
@@ -4,13 +4,14 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static junit.framework.TestCase.assertEquals;
 import static junit.framework.TestCase.assertNotNull;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertNull;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -23,12 +24,9 @@
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
 import java.util.concurrent.ExecutionException;
 import kotlinx.metadata.jvm.KotlinClassHeader;
 import kotlinx.metadata.jvm.KotlinClassMetadata;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -39,44 +37,35 @@
   private final String EXPECTED =
       StringUtils.lines("false", "0", "a", "0.042", "0.42", "42", "442", "1", "2", "42", "42");
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteBoxedTypesTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static Map<KotlinTargetVersion, Path> libJars = new HashMap<>();
+  private static final KotlinCompileMemoizer libJars =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/box_primitives_lib", "lib"));
   private final TestParameters parameters;
 
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String baseLibFolder = PKG_PREFIX + "/box_primitives_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path baseLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(baseLibFolder, "lib"))
-              .compile();
-      libJars.put(targetVersion, baseLibJar);
-    }
-  }
-
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = libJars.get(targetVersion);
+    Path libJar = libJars.getForConfiguration(kotlinc, targetVersion);
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/box_primitives_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".box_primitives_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -84,9 +73,9 @@
 
   @Test
   public void smokeTestReflection() throws Exception {
-    Path libJar = libJars.get(targetVersion);
+    Path libJar = libJars.getForConfiguration(kotlinc, targetVersion);
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/box_primitives_app", "main_reflect"))
             .setOutputPath(temp.newFolder().toPath())
@@ -94,7 +83,7 @@
     testForJvm()
         .addVmArguments("-ea")
         .addRunClasspathFiles(
-            ToolHelper.getKotlinStdlibJar(), ToolHelper.getKotlinReflectJar(), libJar)
+            ToolHelper.getKotlinStdlibJar(kotlinc), ToolHelper.getKotlinReflectJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".box_primitives_app.Main_reflectKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -104,8 +93,8 @@
   public void testMetadataForLib() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(libJars.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(libJars.getForConfiguration(kotlinc, targetVersion))
             .addKeepAllClassesRule()
             .addKeepAttributes(
                 ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS,
@@ -116,14 +105,14 @@
             .inspect(this::inspect)
             .writeToZip();
     Path main =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/box_primitives_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
     testForJvm()
         .addRunClasspathFiles(
-            ToolHelper.getKotlinStdlibJar(), ToolHelper.getKotlinReflectJar(), libJar)
+            ToolHelper.getKotlinStdlibJar(kotlinc), ToolHelper.getKotlinReflectJar(kotlinc), libJar)
         .addClasspath(main)
         .run(parameters.getRuntime(), PKG + ".box_primitives_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -132,7 +121,8 @@
   private void inspect(CodeInspector inspector) throws IOException, ExecutionException {
     // Since this has a keep-all classes rule, we should just assert that the meta-data is equal to
     // the original one.
-    CodeInspector stdLibInspector = new CodeInspector(libJars.get(targetVersion));
+    CodeInspector stdLibInspector =
+        new CodeInspector(libJars.getForConfiguration(kotlinc, targetVersion));
     for (FoundClassSubject clazzSubject : stdLibInspector.allClasses()) {
       ClassSubject r8Clazz = inspector.clazz(clazzSubject.getOriginalName());
       assertThat(r8Clazz, isPresent());
@@ -159,8 +149,8 @@
   public void testMetadataForReflect() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(libJars.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(libJars.getForConfiguration(kotlinc, targetVersion))
             .addKeepAllClassesRule()
             .addKeepAttributes(
                 ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS,
@@ -170,7 +160,7 @@
             .compile()
             .writeToZip();
     Path main =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/box_primitives_app", "main_reflect"))
             .setOutputPath(temp.newFolder().toPath())
@@ -178,7 +168,7 @@
     testForJvm()
         .addVmArguments("-ea")
         .addRunClasspathFiles(
-            ToolHelper.getKotlinStdlibJar(), ToolHelper.getKotlinReflectJar(), libJar)
+            ToolHelper.getKotlinStdlibJar(kotlinc), ToolHelper.getKotlinReflectJar(kotlinc), libJar)
         .addClasspath(main)
         .run(parameters.getRuntime(), PKG + ".box_primitives_app.Main_reflectKt")
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteCrossinlineAnonFunctionTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteCrossinlineAnonFunctionTest.java
index 1ebdddb..56fd483 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteCrossinlineAnonFunctionTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteCrossinlineAnonFunctionTest.java
@@ -4,8 +4,9 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -13,9 +14,6 @@
 import com.android.tools.r8.utils.StringUtils;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -27,45 +25,37 @@
   private static final String PKG_LIB = PKG + ".crossinline_anon_lib";
   private static final String PKG_APP = PKG + ".crossinline_anon_app";
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteCrossinlineAnonFunctionTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static Map<KotlinTargetVersion, Path> libJars = new HashMap<>();
+  private static final KotlinCompileMemoizer libJars =
+      getCompileMemoizer(
+          getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_LIB), "lib"));
   private final TestParameters parameters;
 
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path baseLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(
-                  getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_LIB), "lib"))
-              .compile();
-      libJars.put(targetVersion, baseLibJar);
-    }
-  }
-
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = libJars.get(targetVersion);
+    Path libJar = libJars.getForConfiguration(kotlinc, targetVersion);
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG_APP + ".MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -75,19 +65,19 @@
   public void testMetadataForLib() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addProgramFiles(libJars.get(targetVersion))
+            .addProgramFiles(libJars.getForConfiguration(kotlinc, targetVersion))
             .addKeepAllClassesRule()
             .addKeepAllAttributes()
             .compile()
             .writeToZip();
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
             .compile();
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addProgramFiles(output)
         .run(parameters.getRuntime(), PKG_APP + ".MainKt")
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteCrossinlineConcreteFunctionTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteCrossinlineConcreteFunctionTest.java
index ce26b77..b8c5ac2 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteCrossinlineConcreteFunctionTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteCrossinlineConcreteFunctionTest.java
@@ -4,8 +4,9 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -13,9 +14,6 @@
 import com.android.tools.r8.utils.StringUtils;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -27,45 +25,37 @@
   private static final String PKG_LIB = PKG + ".crossinline_concrete_lib";
   private static final String PKG_APP = PKG + ".crossinline_concrete_app";
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteCrossinlineConcreteFunctionTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static Map<KotlinTargetVersion, Path> libJars = new HashMap<>();
+  private static final KotlinCompileMemoizer libJars =
+      getCompileMemoizer(
+          getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_LIB), "lib"));
   private final TestParameters parameters;
 
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path baseLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(
-                  getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_LIB), "lib"))
-              .compile();
-      libJars.put(targetVersion, baseLibJar);
-    }
-  }
-
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = libJars.get(targetVersion);
+    Path libJar = libJars.getForConfiguration(kotlinc, targetVersion);
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG_APP + ".MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -75,20 +65,20 @@
   public void testMetadataForLib() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addProgramFiles(libJars.get(targetVersion))
+            .addProgramFiles(libJars.getForConfiguration(kotlinc, targetVersion))
             .addKeepAllClassesRule()
             .addKeepAllAttributes()
             .compile()
             .writeToZip();
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG_APP + ".MainKt")
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteDelegatedPropertyTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteDelegatedPropertyTest.java
index badbf02..d71248e 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteDelegatedPropertyTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteDelegatedPropertyTest.java
@@ -4,8 +4,9 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -15,9 +16,6 @@
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -35,38 +33,31 @@
           "null",
           "New value has been read in CustomDelegate from 'x'");
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteDelegatedPropertyTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
   private final TestParameters parameters;
-  private static Map<KotlinTargetVersion, Path> jars = new HashMap<>();
-
-  @BeforeClass
-  public static void createJar() throws Exception {
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path baseLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(
-                  getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
-              .compile();
-      jars.put(targetVersion, baseLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer jars =
+      getCompileMemoizer(
+          getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"));
 
   @Test
   public void smokeTest() throws Exception {
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), ToolHelper.getKotlinReflectJar())
-        .addClasspath(jars.get(targetVersion))
+        .addRunClasspathFiles(
+            ToolHelper.getKotlinStdlibJar(kotlinc), ToolHelper.getKotlinReflectJar(kotlinc))
+        .addClasspath(jars.getForConfiguration(kotlinc, targetVersion))
         .run(parameters.getRuntime(), PKG_APP + ".MainKt")
         .assertSuccessWithOutput(EXPECTED_MAIN);
   }
@@ -76,17 +67,20 @@
   public void testMetadataForLib() throws Exception {
     Path outputJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(jars.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(jars.getForConfiguration(kotlinc, targetVersion))
             .addKeepAllClassesRule()
             .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
             .compile()
             .inspect(
                 inspector ->
-                    assertEqualMetadata(new CodeInspector(jars.get(targetVersion)), inspector))
+                    assertEqualMetadata(
+                        new CodeInspector(jars.getForConfiguration(kotlinc, targetVersion)),
+                        inspector))
             .writeToZip();
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), ToolHelper.getKotlinReflectJar())
+        .addRunClasspathFiles(
+            ToolHelper.getKotlinStdlibJar(kotlinc), ToolHelper.getKotlinReflectJar(kotlinc))
         .addClasspath(outputJar)
         .run(parameters.getRuntime(), PKG_APP + ".MainKt")
         .assertSuccessWithOutput(EXPECTED_MAIN);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteDependentKeep.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteDependentKeep.java
index a8fcf61..15112f6 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteDependentKeep.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteDependentKeep.java
@@ -4,10 +4,12 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 
 import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -25,24 +27,26 @@
 @RunWith(Parameterized.class)
 public class MetadataRewriteDependentKeep extends KotlinMetadataTestBase {
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   private final TestParameters parameters;
 
   public MetadataRewriteDependentKeep(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
   @Test
   public void testR8() throws CompilationFailedException, IOException, ExecutionException {
     testForR8(parameters.getBackend())
-        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
         .setMinApi(parameters.getApiLevel())
         .addKeepKotlinMetadata()
         .addKeepRules(StringUtils.joinLines("-if class *.Metadata", "-keep class <1>.io.** { *; }"))
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteDoNotEmitValuesIfEmpty.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteDoNotEmitValuesIfEmpty.java
index 788e64b..fb4dab9 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteDoNotEmitValuesIfEmpty.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteDoNotEmitValuesIfEmpty.java
@@ -4,9 +4,11 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static org.junit.Assert.assertNotEquals;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -27,24 +29,26 @@
 
   private final Set<String> nullableFieldKeys = Sets.newHashSet("pn", "xs", "xi");
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   private final TestParameters parameters;
 
   public MetadataRewriteDoNotEmitValuesIfEmpty(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
   @Test
   public void testKotlinStdLib() throws Exception {
     testForR8(parameters.getBackend())
-        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
         .setMinApi(parameters.getApiLevel())
         .addKeepAllClassesRule()
         .addKeepKotlinMetadata()
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteFlexibleUpperBoundTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteFlexibleUpperBoundTest.java
index d5b9fd0..ecaea79 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteFlexibleUpperBoundTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteFlexibleUpperBoundTest.java
@@ -4,7 +4,7 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
@@ -12,6 +12,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -25,12 +26,9 @@
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.concurrent.ExecutionException;
 import kotlinx.metadata.KmFlexibleTypeUpperBound;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -41,44 +39,35 @@
   private final String EXPECTED = StringUtils.lines("B.foo(): 42");
   private final String PKG_LIB = PKG + ".flexible_upper_bound_lib";
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteFlexibleUpperBoundTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static Map<KotlinTargetVersion, Path> libJars = new HashMap<>();
+  private static final KotlinCompileMemoizer libJars =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/flexible_upper_bound_lib", "lib"));
   private final TestParameters parameters;
 
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String baseLibFolder = PKG_PREFIX + "/flexible_upper_bound_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path baseLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(baseLibFolder, "lib"))
-              .compile();
-      libJars.put(targetVersion, baseLibJar);
-    }
-  }
-
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = libJars.get(targetVersion);
+    Path libJar = libJars.getForConfiguration(kotlinc, targetVersion);
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/flexible_upper_bound_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".flexible_upper_bound_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -88,8 +77,8 @@
   public void testMetadataForLib() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(libJars.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(libJars.getForConfiguration(kotlinc, targetVersion))
             // Allow renaming A to ensure that we rename in the flexible upper bound type.
             .addKeepRules("-keep,allowobfuscation class " + PKG_LIB + ".A { *; }")
             .addKeepRules("-keep class " + PKG_LIB + ".B { *; }")
@@ -103,14 +92,14 @@
             .inspect(this::inspect)
             .writeToZip();
     Path main =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/flexible_upper_bound_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
     testForJvm()
         .addRunClasspathFiles(
-            ToolHelper.getKotlinStdlibJar(), ToolHelper.getKotlinReflectJar(), libJar)
+            ToolHelper.getKotlinStdlibJar(kotlinc), ToolHelper.getKotlinReflectJar(kotlinc), libJar)
         .addClasspath(main)
         .run(parameters.getRuntime(), PKG + ".flexible_upper_bound_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInClasspathTypeTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInClasspathTypeTest.java
index 2103f0d..09d3af7 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInClasspathTypeTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInClasspathTypeTest.java
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isExtensionFunction;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
@@ -11,6 +11,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -23,10 +24,7 @@
 import com.android.tools.r8.utils.codeinspector.KmPackageSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -37,54 +35,45 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInClasspathTypeTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static final Map<KotlinTargetVersion, Path> baseLibJarMap = new HashMap<>();
-  private static final Map<KotlinTargetVersion, Path> extLibJarMap = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String baseLibFolder = PKG_PREFIX + "/classpath_lib_base";
-    String extLibFolder = PKG_PREFIX + "/classpath_lib_ext";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path baseLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(baseLibFolder, "itf"))
-              .compile();
-      Path extLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addClasspathFiles(baseLibJar)
-              .addSourceFiles(getKotlinFileInTest(extLibFolder, "impl"))
-              .compile();
-      baseLibJarMap.put(targetVersion, baseLibJar);
-      extLibJarMap.put(targetVersion, extLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer baseLibJarMap =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/classpath_lib_base", "itf"));
+  private static final KotlinCompileMemoizer extLibJarMap =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/classpath_lib_ext", "impl"))
+          .configure(
+              kotlinCompilerTool -> {
+                kotlinCompilerTool.addClasspathFiles(
+                    baseLibJarMap.getForConfiguration(
+                        kotlinCompilerTool.getCompiler(), kotlinCompilerTool.getTargetVersion()));
+              });
 
   @Test
   public void smokeTest() throws Exception {
-    Path baseLibJar = baseLibJarMap.get(targetVersion);
-    Path extLibJar = extLibJarMap.get(targetVersion);
+    Path baseLibJar = baseLibJarMap.getForConfiguration(kotlinc, targetVersion);
+    Path extLibJar = extLibJarMap.getForConfiguration(kotlinc, targetVersion);
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(baseLibJar, extLibJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/classpath_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), baseLibJar, extLibJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), baseLibJar, extLibJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".classpath_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -92,11 +81,11 @@
 
   @Test
   public void testMetadataInClasspathType_renamed() throws Exception {
-    Path baseLibJar = baseLibJarMap.get(targetVersion);
+    Path baseLibJar = baseLibJarMap.getForConfiguration(kotlinc, targetVersion);
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(baseLibJar, ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(extLibJarMap.get(targetVersion))
+            .addClasspathFiles(baseLibJar, ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(extLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep the Extra class and its interface (which has the method).
             .addKeepRules("-keep class **.Extra")
             // Keep Super, but allow minification.
@@ -110,14 +99,14 @@
             .writeToZip();
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(baseLibJar, libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/classpath_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), baseLibJar, libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), baseLibJar, libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".classpath_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInCompanionTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInCompanionTest.java
index 4c49518..d025713 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInCompanionTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInCompanionTest.java
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
@@ -12,6 +12,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -25,10 +26,7 @@
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -46,45 +44,36 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInCompanionTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static Map<KotlinTargetVersion, Path> companionLibJarMap = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String companionLibFolder = PKG_PREFIX + "/companion_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path companionLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(companionLibFolder, "lib"))
-              .compile();
-      companionLibJarMap.put(targetVersion, companionLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer companionLibJarMap =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/companion_lib", "lib"));
 
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = companionLibJarMap.get(targetVersion);
+    Path libJar = companionLibJarMap.getForConfiguration(kotlinc, targetVersion);
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/companion_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".companion_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -94,8 +83,8 @@
   public void testMetadataInCompanion_kept() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(companionLibJarMap.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(companionLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep everything
             .addKeepRules("-keep class **.companion_lib.** { *; }")
             .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
@@ -109,14 +98,14 @@
             .writeToZip();
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/companion_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".companion_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -126,8 +115,8 @@
   public void testMetadataInCompanion_renamed() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(companionLibJarMap.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(companionLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep the B class and its interface (which has the doStuff method).
             .addKeepRules("-keep class **.B")
             // Property in companion with @JvmField is defined in the host class, without accessors.
@@ -151,14 +140,14 @@
             .writeToZip();
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/companion_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".companion_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionFunctionTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionFunctionTest.java
index 4f8b4a4..ac363d4 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionFunctionTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionFunctionTest.java
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isExtensionFunction;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
@@ -13,6 +13,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -28,10 +29,7 @@
 import com.android.tools.r8.utils.codeinspector.KmValueParameterSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -43,45 +41,36 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInExtensionFunctionTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static final Map<KotlinTargetVersion, Path> extLibJarMap = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String extLibFolder = PKG_PREFIX + "/extension_function_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path extLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(extLibFolder, "B"))
-              .compile();
-      extLibJarMap.put(targetVersion, extLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer extLibJarMap =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/extension_function_lib", "B"));
 
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = extLibJarMap.get(targetVersion);
+    Path libJar = extLibJarMap.getForConfiguration(kotlinc, targetVersion);
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/extension_function_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".extension_function_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -92,7 +81,7 @@
   public void testMetadataInExtensionFunction_merged() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addProgramFiles(extLibJarMap.get(targetVersion))
+            .addProgramFiles(extLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep the B class and its interface (which has the doStuff method).
             .addKeepRules("-keep class **.B")
             .addKeepRules("-keep class **.I { <methods>; }")
@@ -108,14 +97,14 @@
             .writeToZip();
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/extension_function_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".extension_function_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -143,8 +132,8 @@
   public void testMetadataInExtensionFunction_renamed() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(extLibJarMap.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(extLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep the B class and its interface (which has the doStuff method).
             .addKeepRules("-keep class **.B")
             .addKeepRules("-keep class **.I { <methods>; }")
@@ -162,14 +151,14 @@
             .writeToZip();
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/extension_function_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".extension_function_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionPropertyTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionPropertyTest.java
index 9e81bf2..327ec34 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionPropertyTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInExtensionPropertyTest.java
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isExtensionFunction;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isExtensionProperty;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
@@ -14,6 +14,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -27,10 +28,7 @@
 import com.android.tools.r8.utils.codeinspector.KmPropertySubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -42,45 +40,36 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInExtensionPropertyTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static final Map<KotlinTargetVersion, Path> extLibJarMap = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String extLibFolder = PKG_PREFIX + "/extension_property_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path extLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(extLibFolder, "B"))
-              .compile();
-      extLibJarMap.put(targetVersion, extLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer extLibJarMap =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/extension_property_lib", "B"));
 
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = extLibJarMap.get(targetVersion);
+    Path libJar = extLibJarMap.getForConfiguration(kotlinc, targetVersion);
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/extension_property_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".extension_property_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -91,7 +80,7 @@
   public void testMetadataInExtensionProperty_merged() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addProgramFiles(extLibJarMap.get(targetVersion))
+            .addProgramFiles(extLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep the B class and its interface (which has the doStuff method).
             .addKeepRules("-keep class **.B")
             .addKeepRules("-keep class **.I { <methods>; }")
@@ -104,14 +93,14 @@
             .writeToZip();
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/extension_property_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".extension_property_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -151,8 +140,8 @@
   public void testMetadataInExtensionProperty_renamed() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(extLibJarMap.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(extLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep the B class and its interface (which has the doStuff method).
             .addKeepRules("-keep class **.B")
             .addKeepRules("-keep class **.I { <methods>; }")
@@ -167,14 +156,14 @@
             .writeToZip();
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/extension_property_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".extension_property_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionTest.java
index dcb6dbe..91ed797 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionTest.java
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isExtensionFunction;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
@@ -13,6 +13,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -26,10 +27,7 @@
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Ignore;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -41,45 +39,36 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0} target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInFunctionTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static final Map<KotlinTargetVersion, Path> funLibJarMap = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String funLibFolder = PKG_PREFIX + "/function_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path funLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(funLibFolder, "B"))
-              .compile();
-      funLibJarMap.put(targetVersion, funLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer funLibJarMap =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/function_lib", "B"));
 
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = funLibJarMap.get(targetVersion);
+    Path libJar = funLibJarMap.getForConfiguration(kotlinc, targetVersion);
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/function_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".function_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -90,7 +79,7 @@
   public void testMetadataInFunction_merged() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addProgramFiles(funLibJarMap.get(targetVersion))
+            .addProgramFiles(funLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep the B class and its interface (which has the doStuff method).
             .addKeepRules("-keep class **.B")
             .addKeepRules("-keep class **.I { <methods>; }")
@@ -102,14 +91,14 @@
             .writeToZip();
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/function_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".function_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -148,8 +137,8 @@
   public void testMetadataInFunction_renamed() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(funLibJarMap.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(funLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep the B class and its interface (which has the doStuff method).
             .addKeepRules("-keep class **.B")
             .addKeepRules("-keep class **.I { <methods>; }")
@@ -163,14 +152,14 @@
             .writeToZip();
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/function_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".function_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionWithDefaultValueTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionWithDefaultValueTest.java
index 4cd9b6a..c9210a1 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionWithDefaultValueTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionWithDefaultValueTest.java
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isExtensionFunction;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
@@ -12,6 +12,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -27,10 +28,7 @@
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -41,45 +39,36 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInFunctionWithDefaultValueTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static Map<KotlinTargetVersion, Path> defaultValueLibJarMap = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String default_valueLibFolder = PKG_PREFIX + "/default_value_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path default_valueLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(default_valueLibFolder, "lib"))
-              .compile();
-      defaultValueLibJarMap.put(targetVersion, default_valueLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer defaultValueLibJarMap =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/default_value_lib", "lib"));
 
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = defaultValueLibJarMap.get(targetVersion);
+    Path libJar = defaultValueLibJarMap.getForConfiguration(kotlinc, targetVersion);
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/default_value_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".default_value_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -89,8 +78,9 @@
   public void testMetadataInFunctionWithDefaultValue() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addLibraryFiles(ToolHelper.getJava8RuntimeJar(), ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(defaultValueLibJarMap.get(targetVersion))
+            .addLibraryFiles(
+                ToolHelper.getJava8RuntimeJar(), ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(defaultValueLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep LibKt and applyMap function, along with applyMap$default
             .addKeepRules("-keep class **.LibKt { *** applyMap*(...); }")
             .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
@@ -102,14 +92,14 @@
             .writeToZip();
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/default_value_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".default_value_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionWithVarargTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionWithVarargTest.java
index 0914a23..a65c4b9 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionWithVarargTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInFunctionWithVarargTest.java
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isExtensionFunction;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
@@ -13,6 +13,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -28,10 +29,7 @@
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -42,45 +40,36 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInFunctionWithVarargTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static Map<KotlinTargetVersion, Path> varargLibJarMap = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String varargLibFolder = PKG_PREFIX + "/vararg_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path varargLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(varargLibFolder, "lib"))
-              .compile();
-      varargLibJarMap.put(targetVersion, varargLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer varargLibJarMap =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/vararg_lib", "lib"));
 
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = varargLibJarMap.get(targetVersion);
+    Path libJar = varargLibJarMap.getForConfiguration(kotlinc, targetVersion);
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/vararg_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".vararg_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -90,8 +79,8 @@
   public void testMetadataInFunctionWithVararg() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(varargLibJarMap.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(varargLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // keep SomeClass#foo, since there is a method reference in the app.
             .addKeepRules("-keep class **.SomeClass { *** foo(...); }")
             // Keep LibKt, along with bar function.
@@ -105,14 +94,14 @@
             .writeToZip();
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/vararg_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".vararg_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInLibraryTypeTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInLibraryTypeTest.java
index 159bf66..e635091 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInLibraryTypeTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInLibraryTypeTest.java
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
 import static org.hamcrest.CoreMatchers.anyOf;
@@ -11,6 +11,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -21,9 +22,6 @@
 import com.android.tools.r8.utils.codeinspector.KmPackageSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -34,54 +32,51 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInLibraryTypeTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static final Map<KotlinTargetVersion, Path> baseLibJarMap = new HashMap<>();
-  private static final Map<KotlinTargetVersion, Path> extLibJarMap = new HashMap<>();
-  private static final Map<KotlinTargetVersion, Path> appJarMap = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String baseLibFolder = PKG_PREFIX + "/libtype_lib_base";
-    String extLibFolder = PKG_PREFIX + "/libtype_lib_ext";
-    String appFolder = PKG_PREFIX + "/libtype_app";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path baseLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(baseLibFolder, "base"))
-              .compile();
-      Path extLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addClasspathFiles(baseLibJar)
-              .addSourceFiles(getKotlinFileInTest(extLibFolder, "ext"))
-              .compile();
-      Path appJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addClasspathFiles(baseLibJar)
-              .addClasspathFiles(extLibJar)
-              .addSourceFiles(getKotlinFileInTest(appFolder, "main"))
-              .compile();
-      baseLibJarMap.put(targetVersion, baseLibJar);
-      extLibJarMap.put(targetVersion, extLibJar);
-      appJarMap.put(targetVersion, appJar);
-    }
-  }
+  private static final KotlinCompileMemoizer baseLibJarMap =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/libtype_lib_base", "base"));
+  private static final KotlinCompileMemoizer extLibJarMap =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/libtype_lib_ext", "ext"))
+          .configure(
+              kotlinCompilerTool -> {
+                kotlinCompilerTool.addClasspathFiles(
+                    baseLibJarMap.getForConfiguration(
+                        kotlinCompilerTool.getCompiler(), kotlinCompilerTool.getTargetVersion()));
+              });
+  private static final KotlinCompileMemoizer appJarMap =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/libtype_app", "main"))
+          .configure(
+              kotlinCompilerTool -> {
+                kotlinCompilerTool.addClasspathFiles(
+                    baseLibJarMap.getForConfiguration(
+                        kotlinCompilerTool.getCompiler(), kotlinCompilerTool.getTargetVersion()));
+                kotlinCompilerTool.addClasspathFiles(
+                    extLibJarMap.getForConfiguration(
+                        kotlinCompilerTool.getCompiler(), kotlinCompilerTool.getTargetVersion()));
+              });
 
   @Test
   public void smokeTest() throws Exception {
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), baseLibJarMap.get(targetVersion))
-        .addClasspath(extLibJarMap.get(targetVersion), appJarMap.get(targetVersion))
+        .addRunClasspathFiles(
+            ToolHelper.getKotlinStdlibJar(kotlinc),
+            baseLibJarMap.getForConfiguration(kotlinc, targetVersion))
+        .addClasspath(
+            extLibJarMap.getForConfiguration(kotlinc, targetVersion),
+            appJarMap.getForConfiguration(kotlinc, targetVersion))
         .run(parameters.getRuntime(), PKG + ".libtype_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
   }
@@ -93,7 +88,9 @@
         testForR8(parameters.getBackend())
             // Intentionally not providing baseLibJar as lib file nor classpath file.
             .addClasspathFiles()
-            .addProgramFiles(extLibJarMap.get(targetVersion), appJarMap.get(targetVersion))
+            .addProgramFiles(
+                extLibJarMap.getForConfiguration(kotlinc, targetVersion),
+                appJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep Ext extension method which requires metadata to be called with Kotlin syntax
             // from other kotlin code.
             .addKeepRules("-keep class **.ExtKt { <methods>; }")
@@ -112,7 +109,9 @@
             .writeToZip();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), baseLibJarMap.get(targetVersion))
+        .addRunClasspathFiles(
+            ToolHelper.getKotlinStdlibJar(kotlinc),
+            baseLibJarMap.getForConfiguration(kotlinc, targetVersion))
         .addClasspath(out)
         .run(parameters.getRuntime(), main)
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInMultifileClassTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInMultifileClassTest.java
index 45a419d..8adeb48 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInMultifileClassTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInMultifileClassTest.java
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isExtensionFunction;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
@@ -16,6 +16,7 @@
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -31,11 +32,8 @@
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import kotlinx.metadata.jvm.KotlinClassMetadata;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -46,47 +44,38 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInMultifileClassTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static final Map<KotlinTargetVersion, Path> multifileLibJarMap = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String multifileLibFolder = PKG_PREFIX + "/multifileclass_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path multifileLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(
-                  getKotlinFileInTest(multifileLibFolder, "signed"),
-                  getKotlinFileInTest(multifileLibFolder, "unsigned"))
-              .compile();
-      multifileLibJarMap.put(targetVersion, multifileLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer multifileLibJarMap =
+      getCompileMemoizer(
+          getKotlinFileInTest(PKG_PREFIX + "/multifileclass_lib", "signed"),
+          getKotlinFileInTest(PKG_PREFIX + "/multifileclass_lib", "unsigned"));
 
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = multifileLibJarMap.get(targetVersion);
+    Path libJar = multifileLibJarMap.getForConfiguration(kotlinc, targetVersion);
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/multifileclass_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".multifileclass_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -96,8 +85,8 @@
   public void testMetadataInMultifileClass_merged() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(multifileLibJarMap.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(multifileLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep UtilKt#comma*Join*(). Let R8 optimize (inline) others, such as joinOf*(String).
             .addKeepRules("-keep class **.UtilKt")
             .addKeepRules("-keepclassmembers class * { ** comma*Join*(...); }")
@@ -107,7 +96,7 @@
             .writeToZip();
 
     ProcessResult kotlinTestCompileResult =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/multifileclass_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
@@ -137,8 +126,8 @@
   public void testMetadataInMultifileClass_renamed() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(multifileLibJarMap.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(multifileLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep UtilKt#comma*Join*().
             .addKeepRules("-keep class **.UtilKt")
             .addKeepRules("-keep,allowobfuscation class **.UtilKt__SignedKt")
@@ -151,7 +140,7 @@
             .writeToZip();
 
     ProcessResult kotlinTestCompileResult =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/multifileclass_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInNestedClassTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInNestedClassTest.java
index eada4da..8bb4603 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInNestedClassTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInNestedClassTest.java
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.DescriptorUtils.descriptorToJavaType;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
@@ -12,6 +12,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -22,9 +23,6 @@
 import com.android.tools.r8.utils.codeinspector.KmClassSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -36,45 +34,36 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInNestedClassTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static final Map<KotlinTargetVersion, Path> nestedLibJarMap = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String nestedLibFolder = PKG_PREFIX + "/nested_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path nestedLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(nestedLibFolder, "lib"))
-              .compile();
-      nestedLibJarMap.put(targetVersion, nestedLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer nestedLibJarMap =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/nested_lib", "lib"));
 
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = nestedLibJarMap.get(targetVersion);
+    Path libJar = nestedLibJarMap.getForConfiguration(kotlinc, targetVersion);
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/nested_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".nested_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -84,8 +73,8 @@
   public void testMetadataInNestedClass() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(nestedLibJarMap.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(nestedLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep the Outer class and delegations.
             .addKeepRules("-keep class **.Outer { <init>(...); *** delegate*(...); }")
             // Keep Inner to check the hierarchy.
@@ -100,14 +89,14 @@
             .writeToZip();
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/nested_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".nested_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInParameterTypeTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInParameterTypeTest.java
index b4ae665..a2a85c3 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInParameterTypeTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInParameterTypeTest.java
@@ -3,13 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -20,10 +21,7 @@
 import com.android.tools.r8.utils.codeinspector.KmClassSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -34,45 +32,36 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInParameterTypeTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static final Map<KotlinTargetVersion, Path> parameterTypeLibJarMap = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String parameterTypeLibFolder = PKG_PREFIX + "/parametertype_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path parameterTypeLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(parameterTypeLibFolder, "lib"))
-              .compile();
-      parameterTypeLibJarMap.put(targetVersion, parameterTypeLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer parameterTypeLibJarMap =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/parametertype_lib", "lib"));
 
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = parameterTypeLibJarMap.get(targetVersion);
+    Path libJar = parameterTypeLibJarMap.getForConfiguration(kotlinc, targetVersion);
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/parametertype_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".parametertype_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -82,8 +71,8 @@
   public void testMetadataInParameterType_renamed() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(parameterTypeLibJarMap.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(parameterTypeLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep non-private members of Impl
             .addKeepRules("-keep public class **.Impl { !private *; }")
             // Keep Itf, but allow minification.
@@ -94,14 +83,14 @@
             .writeToZip();
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/parametertype_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".parametertype_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTest.java
index c479093..d012f10 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTest.java
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isExtensionProperty;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
@@ -14,6 +14,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -27,9 +28,6 @@
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -40,38 +38,29 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInPropertyTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static final Map<KotlinTargetVersion, Path> propertyTypeLibJarMap = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String propertyTypeLibFolder = PKG_PREFIX + "/fragile_property_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path propertyTypeLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(propertyTypeLibFolder, "lib"))
-              .compile();
-      propertyTypeLibJarMap.put(targetVersion, propertyTypeLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer propertyTypeLibJarMap =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/fragile_property_lib", "lib"));
 
   @Test
   public void smokeTest_getterApp() throws Exception {
-    Path libJar = propertyTypeLibJarMap.get(targetVersion);
+    Path libJar = propertyTypeLibJarMap.getForConfiguration(kotlinc, targetVersion);
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(PKG_PREFIX + "/fragile_property_only_getter", "getter_user"))
@@ -79,7 +68,7 @@
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".fragile_property_only_getter.Getter_userKt")
         .assertSuccessWithOutput(EXPECTED_GETTER);
@@ -89,8 +78,8 @@
   public void testMetadataInProperty_getterOnly() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(propertyTypeLibJarMap.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(propertyTypeLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep property getters
             .addKeepRules("-keep class **.Person { <init>(...); }")
             .addKeepRules("-keepclassmembers class **.Person { *** get*(); }")
@@ -100,7 +89,7 @@
             .writeToZip();
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(PKG_PREFIX + "/fragile_property_only_getter", "getter_user"))
@@ -108,7 +97,7 @@
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".fragile_property_only_getter.Getter_userKt")
         .assertSuccessWithOutput(EXPECTED_GETTER);
@@ -159,10 +148,10 @@
 
   @Test
   public void smokeTest_setterApp() throws Exception {
-    Path libJar = propertyTypeLibJarMap.get(targetVersion);
+    Path libJar = propertyTypeLibJarMap.getForConfiguration(kotlinc, targetVersion);
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(PKG_PREFIX + "/fragile_property_only_setter", "setter_user"))
@@ -170,7 +159,7 @@
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".fragile_property_only_setter.Setter_userKt")
         .assertSuccessWithOutputLines();
@@ -180,8 +169,8 @@
   public void testMetadataInProperty_setterOnly() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(propertyTypeLibJarMap.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(propertyTypeLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep property setters (and users)
             .addKeepRules("-keep class **.Person { <init>(...); }")
             .addKeepRules("-keepclassmembers class **.Person { void set*(...); }")
@@ -195,7 +184,7 @@
             .writeToZip();
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(PKG_PREFIX + "/fragile_property_only_setter", "setter_user"))
@@ -203,7 +192,7 @@
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".fragile_property_only_setter.Setter_userKt")
         .assertSuccessWithOutputLines();
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTypeTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTypeTest.java
index eb9ff61..9641f64 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTypeTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInPropertyTypeTest.java
@@ -3,13 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -20,10 +21,7 @@
 import com.android.tools.r8.utils.codeinspector.KmClassSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -34,45 +32,36 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInPropertyTypeTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static final Map<KotlinTargetVersion, Path> propertyTypeLibJarMap = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String propertyTypeLibFolder = PKG_PREFIX + "/propertytype_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path propertyTypeLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(propertyTypeLibFolder, "lib"))
-              .compile();
-      propertyTypeLibJarMap.put(targetVersion, propertyTypeLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer propertyTypeLibJarMap =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/propertytype_lib", "lib"));
 
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = propertyTypeLibJarMap.get(targetVersion);
+    Path libJar = propertyTypeLibJarMap.getForConfiguration(kotlinc, targetVersion);
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/propertytype_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".propertytype_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -82,8 +71,8 @@
   public void testMetadataInProperty_renamed() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(propertyTypeLibJarMap.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(propertyTypeLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep non-private members of Impl
             .addKeepRules("-keep public class **.Impl { !private *; }")
             .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
@@ -92,14 +81,14 @@
             .writeToZip();
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/propertytype_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".propertytype_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInRenamedTypeTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInRenamedTypeTest.java
index 8be744d..a4a3a68 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInRenamedTypeTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInRenamedTypeTest.java
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.DescriptorUtils.getDescriptorFromKotlinClassifier;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
@@ -12,6 +12,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -20,11 +21,7 @@
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
 import com.android.tools.r8.utils.codeinspector.KmClassSubject;
-import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -36,47 +33,39 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withAllRuntimesAndApiLevels().build(), KotlinTargetVersion.values());
+        getTestParameters().withAllRuntimesAndApiLevels().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInRenamedTypeTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static final Map<KotlinTargetVersion, Path> annoJarMap = new HashMap<>();
-  private static final Map<KotlinTargetVersion, Path> inputJarMap = new HashMap<>();
-
-  @BeforeClass
-  public static void createInputJar() throws Exception {
-    String inputFolder = PKG_PREFIX + "/anno";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path annoJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(inputFolder, "Anno"))
-              .compile();
-      Path inputJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addClasspathFiles(annoJar)
-              .addSourceFiles(getKotlinFileInTest(inputFolder, "main"))
-              .compile();
-      annoJarMap.put(targetVersion, annoJar);
-      inputJarMap.put(targetVersion, inputJar);
-    }
-  }
+  private static final KotlinCompileMemoizer annoJarMap =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/anno", "Anno"));
+  private static final KotlinCompileMemoizer inputJarMap =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/anno", "main"))
+          .configure(
+              kotlinCompilerTool -> {
+                kotlinCompilerTool.addClasspathFiles(
+                    annoJarMap.getForConfiguration(
+                        kotlinCompilerTool.getCompiler(), kotlinCompilerTool.getTargetVersion()));
+              });
 
   @Test
   public void testR8_kotlinStdlibAsLib() throws Exception {
     testForR8(parameters.getBackend())
         .addLibraryFiles(
-            annoJarMap.get(targetVersion),
+            annoJarMap.getForConfiguration(kotlinc, targetVersion),
             ToolHelper.getJava8RuntimeJar(),
-            ToolHelper.getKotlinStdlibJar())
-        .addProgramFiles(inputJarMap.get(targetVersion))
+            ToolHelper.getKotlinStdlibJar(kotlinc))
+        .addProgramFiles(inputJarMap.getForConfiguration(kotlinc, targetVersion))
         .addKeepRules(OBFUSCATE_RENAMED, KEEP_KEPT)
         .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
         .compile()
@@ -86,8 +75,10 @@
   @Test
   public void testR8_kotlinStdlibAsClassPath() throws Exception {
     testForR8(parameters.getBackend())
-        .addClasspathFiles(annoJarMap.get(targetVersion), ToolHelper.getKotlinStdlibJar())
-        .addProgramFiles(inputJarMap.get(targetVersion))
+        .addClasspathFiles(
+            annoJarMap.getForConfiguration(kotlinc, targetVersion),
+            ToolHelper.getKotlinStdlibJar(kotlinc))
+        .addProgramFiles(inputJarMap.getForConfiguration(kotlinc, targetVersion))
         .addKeepRules(OBFUSCATE_RENAMED, KEEP_KEPT)
         .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
         .compile()
@@ -97,8 +88,10 @@
   @Test
   public void testR8_kotlinStdlibAsProgramFile() throws Exception {
     testForR8(parameters.getBackend())
-        .addProgramFiles(annoJarMap.get(targetVersion), ToolHelper.getKotlinStdlibJar())
-        .addProgramFiles(inputJarMap.get(targetVersion))
+        .addProgramFiles(
+            annoJarMap.getForConfiguration(kotlinc, targetVersion),
+            ToolHelper.getKotlinStdlibJar(kotlinc))
+        .addProgramFiles(inputJarMap.getForConfiguration(kotlinc, targetVersion))
         .addKeepRules(OBFUSCATE_RENAMED, KEEP_KEPT)
         .addKeepRules("-keep class **.Anno")
         .addKeepKotlinMetadata()
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInReturnTypeTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInReturnTypeTest.java
index 47b0b82..9783f3b 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInReturnTypeTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInReturnTypeTest.java
@@ -3,13 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -20,10 +21,7 @@
 import com.android.tools.r8.utils.codeinspector.KmClassSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -34,45 +32,36 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInReturnTypeTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static final Map<KotlinTargetVersion, Path> returnTypeLibJarMap = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String returnTypeLibFolder = PKG_PREFIX + "/returntype_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path returnTypeLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(returnTypeLibFolder, "lib"))
-              .compile();
-      returnTypeLibJarMap.put(targetVersion, returnTypeLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer returnTypeLibJarMap =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/returntype_lib", "lib"));
 
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = returnTypeLibJarMap.get(targetVersion);
+    Path libJar = returnTypeLibJarMap.getForConfiguration(kotlinc, targetVersion);
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/returntype_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".returntype_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -82,8 +71,8 @@
   public void testMetadataInReturnType_renamed() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(returnTypeLibJarMap.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(returnTypeLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep non-private members of Impl
             .addKeepRules("-keep public class **.Impl { !private *; }")
             // Keep Itf, but allow minification.
@@ -94,14 +83,14 @@
             .writeToZip();
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/returntype_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".returntype_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSealedClassNestedTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSealedClassNestedTest.java
index 3fb7b23..f33fc30 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSealedClassNestedTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSealedClassNestedTest.java
@@ -3,8 +3,9 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -13,9 +14,6 @@
 import com.android.tools.r8.utils.StringUtils;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -33,37 +31,29 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInSealedClassNestedTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static final Map<KotlinTargetVersion, Path> sealedLibJarMap = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path sealedLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(
-                  getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_LIB), "nested"))
-              .compile();
-      sealedLibJarMap.put(targetVersion, sealedLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer sealedLibJarMap =
+      getCompileMemoizer(
+          getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_LIB), "nested"));
 
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = sealedLibJarMap.get(targetVersion);
+    Path libJar = sealedLibJarMap.getForConfiguration(kotlinc, targetVersion);
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
@@ -71,7 +61,7 @@
             .compile();
     testForJvm()
         .addRunClasspathFiles(
-            ToolHelper.getKotlinStdlibJar(), ToolHelper.getKotlinReflectJar(), libJar)
+            ToolHelper.getKotlinStdlibJar(kotlinc), ToolHelper.getKotlinReflectJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG_APP + ".MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -81,8 +71,8 @@
   public void testMetadataInSealedClass_nested() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(sealedLibJarMap.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(sealedLibJarMap.getForConfiguration(kotlinc, targetVersion))
             .addKeepAllClassesRule()
             .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
             .addKeepAttributes(
@@ -90,7 +80,7 @@
             .compile()
             .writeToZip();
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
@@ -98,7 +88,7 @@
             .compile();
     testForJvm()
         .addRunClasspathFiles(
-            ToolHelper.getKotlinStdlibJar(), ToolHelper.getKotlinReflectJar(), libJar)
+            ToolHelper.getKotlinStdlibJar(kotlinc), ToolHelper.getKotlinReflectJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG_APP + ".MainKt")
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSealedClassTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSealedClassTest.java
index 1bb1c44..2dbbc48 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSealedClassTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSealedClassTest.java
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.DescriptorUtils.descriptorToJavaType;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isExtensionFunction;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
@@ -15,6 +15,7 @@
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -28,9 +29,6 @@
 import com.android.tools.r8.utils.codeinspector.KmPackageSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -41,45 +39,36 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInSealedClassTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static final Map<KotlinTargetVersion, Path> sealedLibJarMap = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String sealedLibFolder = PKG_PREFIX + "/sealed_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path sealedLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(sealedLibFolder, "lib"))
-              .compile();
-      sealedLibJarMap.put(targetVersion, sealedLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer sealedLibJarMap =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/sealed_lib", "lib"));
 
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = sealedLibJarMap.get(targetVersion);
+    Path libJar = sealedLibJarMap.getForConfiguration(kotlinc, targetVersion);
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/sealed_app", "valid"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".sealed_app.ValidKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -89,8 +78,8 @@
   public void testMetadataInSealedClass_valid() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(sealedLibJarMap.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(sealedLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep the Expr class
             .addKeepRules("-keep class **.Expr")
             // Keep the extension function
@@ -103,14 +92,14 @@
             .writeToZip();
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/sealed_app", "valid"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".sealed_app.ValidKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -156,8 +145,8 @@
   public void testMetadataInSealedClass_invalid() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(sealedLibJarMap.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(sealedLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep the Expr class
             .addKeepRules("-keep class **.Expr")
             // Keep the extension function
@@ -168,7 +157,7 @@
             .writeToZip();
 
     ProcessResult kotlinTestCompileResult =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFilesWithNonKtExtension(
                 temp, getFileInTest(PKG_PREFIX + "/sealed_app", "invalid.kt_txt"))
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSuperTypeTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSuperTypeTest.java
index 578a7ec..9b0da43 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSuperTypeTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInSuperTypeTest.java
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
@@ -11,6 +11,7 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -21,10 +22,7 @@
 import com.android.tools.r8.utils.codeinspector.KmClassSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -35,47 +33,38 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInSuperTypeTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static final Map<KotlinTargetVersion, Path> superTypeLibJarMap = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String superTypeLibFolder = PKG_PREFIX + "/supertype_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path superTypeLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(
-                  getKotlinFileInTest(superTypeLibFolder, "impl"),
-                  getKotlinFileInTest(superTypeLibFolder + "/internal", "itf"))
-              .compile();
-      superTypeLibJarMap.put(targetVersion, superTypeLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer superTypeLibJarMap =
+      getCompileMemoizer(
+          getKotlinFileInTest(PKG_PREFIX + "/supertype_lib", "impl"),
+          getKotlinFileInTest(PKG_PREFIX + "/supertype_lib" + "/internal", "itf"));
 
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = superTypeLibJarMap.get(targetVersion);
+    Path libJar = superTypeLibJarMap.getForConfiguration(kotlinc, targetVersion);
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/supertype_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".supertype_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -85,8 +74,8 @@
   public void testMetadataInSupertype_merged() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(superTypeLibJarMap.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(superTypeLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep non-private members except for ones in `internal` definitions.
             .addKeepRules("-keep public class !**.internal.**, * { !private *; }")
             .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
@@ -95,14 +84,14 @@
             .writeToZip();
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/supertype_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".supertype_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -130,8 +119,8 @@
   public void testMetadataInSupertype_renamed() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(superTypeLibJarMap.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(superTypeLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep non-private members except for ones in `internal` definitions.
             .addKeepRules("-keep public class !**.internal.**, * { !private *; }")
             // Keep `internal` definitions, but allow minification.
@@ -142,14 +131,14 @@
             .writeToZip();
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/supertype_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".supertype_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeAliasTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeAliasTest.java
index ac4085f..edade6e 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeAliasTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeAliasTest.java
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isDexClass;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
@@ -13,6 +13,7 @@
 import static junit.framework.TestCase.assertTrue;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -30,9 +31,6 @@
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -63,40 +61,31 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInTypeAliasTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static final Map<KotlinTargetVersion, Path> typeAliasLibJarMap = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String typeAliasLibFolder = PKG_PREFIX + "/typealias_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path typeAliasLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(
-                  getKotlinFileInTest(typeAliasLibFolder, "lib"),
-                  getKotlinFileInTest(typeAliasLibFolder, "lib_ext"))
-              .compile();
-      typeAliasLibJarMap.put(targetVersion, typeAliasLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer typeAliasLibJarMap =
+      getCompileMemoizer(
+          getKotlinFileInTest(PKG_PREFIX + "/typealias_lib", "lib"),
+          getKotlinFileInTest(PKG_PREFIX + "/typealias_lib", "lib_ext"));
 
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = typeAliasLibJarMap.get(targetVersion);
+    Path libJar = typeAliasLibJarMap.getForConfiguration(kotlinc, targetVersion);
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/typealias_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
@@ -104,7 +93,7 @@
 
     testForJvm()
         .addRunClasspathFiles(
-            ToolHelper.getKotlinStdlibJar(), ToolHelper.getKotlinReflectJar(), libJar)
+            ToolHelper.getKotlinStdlibJar(kotlinc), ToolHelper.getKotlinReflectJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".typealias_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -116,8 +105,9 @@
     String renamedSuperTypeName = "com.android.tools.r8.kotlin.metadata.typealias_lib.FooBar";
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(), ToolHelper.getKotlinReflectJar())
-            .addProgramFiles(typeAliasLibJarMap.get(targetVersion))
+            .addClasspathFiles(
+                ToolHelper.getKotlinStdlibJar(kotlinc), ToolHelper.getKotlinReflectJar(kotlinc))
+            .addProgramFiles(typeAliasLibJarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep non-private members of Impl
             .addKeepRules("-keep class **.Impl { !private *; }")
             // Keep but allow obfuscation of types.
@@ -144,14 +134,14 @@
             .writeToZip();
 
     Path appJar =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/typealias_app", "main"))
             .compile();
 
     testForJvm()
         .addRunClasspathFiles(
-            ToolHelper.getKotlinStdlibJar(), ToolHelper.getKotlinReflectJar(), libJar)
+            ToolHelper.getKotlinStdlibJar(kotlinc), ToolHelper.getKotlinReflectJar(kotlinc), libJar)
         .addClasspath(appJar)
         .run(parameters.getRuntime(), PKG + ".typealias_app.MainKt")
         .assertSuccessWithOutput(EXPECTED.replace(superTypeName, renamedSuperTypeName));
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeArgumentsTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeArgumentsTest.java
index 52ae0b3..788abf2 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeArgumentsTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInTypeArgumentsTest.java
@@ -3,7 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isDexClass;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isExtensionFunction;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
@@ -12,6 +12,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -30,12 +31,9 @@
 import com.android.tools.r8.utils.codeinspector.KmValueParameterSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
 import kotlinx.metadata.KmClassifier.TypeParameter;
 import kotlinx.metadata.KmVariance;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -79,45 +77,36 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInTypeArgumentsTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static final Map<KotlinTargetVersion, Path> jarMap = new HashMap<>();
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String typeAliasLibFolder = PKG_PREFIX + "/typeargument_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path typeAliasLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(typeAliasLibFolder, "lib"))
-              .compile();
-      jarMap.put(targetVersion, typeAliasLibJar);
-    }
-  }
+  private static final KotlinCompileMemoizer jarMap =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/typeargument_lib", "lib"));
 
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = jarMap.get(targetVersion);
+    Path libJar = jarMap.getForConfiguration(kotlinc, targetVersion);
 
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/typeargument_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
 
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".typeargument_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -127,8 +116,8 @@
   public void testMetadataInTypeAliasWithR8() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(jarMap.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(jarMap.getForConfiguration(kotlinc, targetVersion))
             // Keep ClassThatWillBeObfuscated, but allow minification.
             .addKeepRules("-keep,allowobfuscation class **ClassThatWillBeObfuscated")
             .addKeepRules("-keepclassmembers class **ClassThatWillBeObfuscated { *; }")
@@ -148,12 +137,12 @@
             .inspect(this::inspect)
             .writeToZip();
     Path mainJar =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/typeargument_app", "main"))
             .compile();
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(mainJar)
         .run(parameters.getRuntime(), PKG + ".typeargument_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInlinePropertyTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInlinePropertyTest.java
index ffe5a51..133dd37 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInlinePropertyTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteInlinePropertyTest.java
@@ -4,11 +4,12 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertNull;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -21,13 +22,9 @@
 import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.concurrent.ExecutionException;
 import junit.framework.TestCase;
 import kotlinx.metadata.jvm.KotlinClassHeader;
 import kotlinx.metadata.jvm.KotlinClassMetadata;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -36,46 +33,36 @@
 public class MetadataRewriteInlinePropertyTest extends KotlinMetadataTestBase {
 
   private final String EXPECTED = StringUtils.lines("true", "false", "false", "true");
-  private final String PKG_LIB = PKG + ".inline_property_lib";
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewriteInlinePropertyTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static Map<KotlinTargetVersion, Path> libJars = new HashMap<>();
+  private static final KotlinCompileMemoizer libJars =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/inline_property_lib", "lib"));
   private final TestParameters parameters;
 
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String baseLibFolder = PKG_PREFIX + "/inline_property_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path baseLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(baseLibFolder, "lib"))
-              .compile();
-      libJars.put(targetVersion, baseLibJar);
-    }
-  }
-
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = libJars.get(targetVersion);
+    Path libJar = libJars.getForConfiguration(kotlinc, targetVersion);
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/inline_property_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG + ".inline_property_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -85,8 +72,8 @@
   public void testMetadataForLib() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-            .addProgramFiles(libJars.get(targetVersion))
+            .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+            .addProgramFiles(libJars.getForConfiguration(kotlinc, targetVersion))
             // Allow renaming A to ensure that we rename in the flexible upper bound type.
             .addKeepAllClassesRule()
             .addKeepAttributes(
@@ -98,21 +85,22 @@
             .inspect(this::inspect)
             .writeToZip();
     Path main =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(getKotlinFileInTest(PKG_PREFIX + "/inline_property_app", "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
     testForJvm()
         .addRunClasspathFiles(
-            ToolHelper.getKotlinStdlibJar(), ToolHelper.getKotlinReflectJar(), libJar)
+            ToolHelper.getKotlinStdlibJar(kotlinc), ToolHelper.getKotlinReflectJar(kotlinc), libJar)
         .addClasspath(main)
         .run(parameters.getRuntime(), PKG + ".inline_property_app.MainKt")
         .assertSuccessWithOutput(EXPECTED);
   }
 
-  private void inspect(CodeInspector inspector) throws IOException, ExecutionException {
-    CodeInspector stdLibInspector = new CodeInspector(libJars.get(targetVersion));
+  private void inspect(CodeInspector inspector) throws IOException {
+    CodeInspector stdLibInspector =
+        new CodeInspector(libJars.getForConfiguration(kotlinc, targetVersion));
     for (FoundClassSubject clazzSubject : stdLibInspector.allClasses()) {
       ClassSubject r8Clazz = inspector.clazz(clazzSubject.getOriginalName());
       assertThat(r8Clazz, isPresent());
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteJvmStaticTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteJvmStaticTest.java
index 2d3834b..83b8e62 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteJvmStaticTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteJvmStaticTest.java
@@ -3,13 +3,13 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.kotlin.metadata.jvmstatic_app.MainJava;
@@ -23,8 +23,7 @@
 import com.android.tools.r8.utils.codeinspector.KmPropertySubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.nio.file.Path;
-import java.nio.file.Paths;
-import org.junit.BeforeClass;
+import java.util.List;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -39,39 +38,34 @@
 
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withCfRuntimes().build();
+  @Parameterized.Parameters(name = "{0}, kotlinc: {1}")
+  public static List<Object[]> data() {
+    return buildParameters(getTestParameters().withCfRuntimes().build(), getKotlinCompilers());
   }
 
-  public MetadataRewriteJvmStaticTest(TestParameters parameters) {
+  public MetadataRewriteJvmStaticTest(TestParameters parameters, KotlinCompiler kotlinc) {
     // We are testing static methods on interfaces which requires java 8.
-    super(KotlinTargetVersion.JAVA_8);
+    super(KotlinTargetVersion.JAVA_8, kotlinc);
     this.parameters = parameters;
   }
 
-  private static Path kotlincLibJar = Paths.get("");
-
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    kotlincLibJar =
-        kotlinc(KOTLINC, KotlinTargetVersion.JAVA_8)
-            .addSourceFiles(
-                getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_LIB), "lib"))
-            .compile();
-  }
+  private static KotlinCompileMemoizer kotlincLibJar =
+      getCompileMemoizer(
+          getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_LIB), "lib"));
 
   @Test
   public void smokeTest() throws Exception {
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
-            .addClasspathFiles(kotlincLibJar)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
+            .addClasspathFiles(kotlincLibJar.getForConfiguration(kotlinc, targetVersion))
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), kotlincLibJar)
+        .addRunClasspathFiles(
+            ToolHelper.getKotlinStdlibJar(kotlinc),
+            kotlincLibJar.getForConfiguration(kotlinc, targetVersion))
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG_APP + ".MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -80,7 +74,9 @@
   @Test
   public void smokeTestJava() throws Exception {
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), kotlincLibJar)
+        .addRunClasspathFiles(
+            ToolHelper.getKotlinStdlibJar(kotlinc),
+            kotlincLibJar.getForConfiguration(kotlinc, targetVersion))
         .addProgramClassFileData(MainJava.dump())
         .run(parameters.getRuntime(), MainJava.class)
         .assertSuccessWithOutput(EXPECTED);
@@ -90,7 +86,7 @@
   public void testMetadata() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addProgramFiles(kotlincLibJar)
+            .addProgramFiles(kotlincLibJar.getForConfiguration(kotlinc, targetVersion))
             .addKeepAllClassesRule()
             .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
             .compile()
@@ -102,14 +98,14 @@
 
   private void testKotlin(Path libJar) throws Exception {
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG_APP + ".MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -117,7 +113,7 @@
 
   private void testJava(Path libJar) throws Exception {
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addProgramClassFileData(MainJava.dump())
         .run(parameters.getRuntime(), MainJava.class)
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteKeepPathTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteKeepPathTest.java
index bb61374..40c7573 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteKeepPathTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteKeepPathTest.java
@@ -4,12 +4,13 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.TestShrinkerBuilder;
 import com.android.tools.r8.ToolHelper;
@@ -17,11 +18,7 @@
 import com.android.tools.r8.utils.BooleanUtils;
 import com.android.tools.r8.utils.codeinspector.ClassSubject;
 import com.android.tools.r8.utils.codeinspector.CodeInspector;
-import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -29,43 +26,36 @@
 @RunWith(Parameterized.class)
 public class MetadataRewriteKeepPathTest extends KotlinMetadataTestBase {
 
-  @Parameterized.Parameters(name = "{0} target: {1}, keep: {2}")
+  @Parameterized.Parameters(name = "{0} target: {1}, kotlinc: {2}, keep: {3}")
   public static Collection<Object[]> data() {
     return buildParameters(
         getTestParameters().withCfRuntimes().build(),
         KotlinTargetVersion.values(),
+        getKotlinCompilers(),
         BooleanUtils.values());
   }
 
-  private static Map<KotlinTargetVersion, Path> libJars = new HashMap<>();
+  private static final KotlinCompileMemoizer libJars =
+      getCompileMemoizer(getKotlinFileInTest(PKG_PREFIX + "/box_primitives_lib", "lib"));
   private static final String LIB_CLASS_NAME = PKG + ".box_primitives_lib.Test";
   private final TestParameters parameters;
   private final boolean keepMetadata;
 
   public MetadataRewriteKeepPathTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion, boolean keepMetadata) {
-    super(targetVersion);
+      TestParameters parameters,
+      KotlinTargetVersion targetVersion,
+      KotlinCompiler kotlinc,
+      boolean keepMetadata) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
     this.keepMetadata = keepMetadata;
   }
 
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    String baseLibFolder = PKG_PREFIX + "/box_primitives_lib";
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path baseLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(getKotlinFileInTest(baseLibFolder, "lib"))
-              .compile();
-      libJars.put(targetVersion, baseLibJar);
-    }
-  }
-
   @Test
   public void testProgramPath() throws Exception {
     testForR8(parameters.getBackend())
-        .addProgramFiles(libJars.get(targetVersion))
-        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(libJars.getForConfiguration(kotlinc, targetVersion))
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
         .addKeepRules("-keep class " + LIB_CLASS_NAME)
         .applyIf(keepMetadata, TestShrinkerBuilder::addKeepKotlinMetadata)
         .addKeepRuntimeVisibleAnnotations()
@@ -78,8 +68,8 @@
   @Test
   public void testClassPathPath() throws Exception {
     testForR8(parameters.getBackend())
-        .addProgramFiles(libJars.get(targetVersion))
-        .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(libJars.getForConfiguration(kotlinc, targetVersion))
+        .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
         .addKeepRules("-keep class " + LIB_CLASS_NAME)
         .addKeepRuntimeVisibleAnnotations()
         .compile()
@@ -89,8 +79,8 @@
   @Test
   public void testLibraryPath() throws Exception {
     testForR8(parameters.getBackend())
-        .addProgramFiles(libJars.get(targetVersion))
-        .addLibraryFiles(ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(libJars.getForConfiguration(kotlinc, targetVersion))
+        .addLibraryFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
         .addLibraryFiles(ToolHelper.getJava8RuntimeJar())
         .addKeepRules("-keep class " + LIB_CLASS_NAME)
         .addKeepRuntimeVisibleAnnotations()
@@ -101,7 +91,7 @@
   @Test
   public void testMissing() throws Exception {
     testForR8(parameters.getBackend())
-        .addProgramFiles(libJars.get(targetVersion))
+        .addProgramFiles(libJars.getForConfiguration(kotlinc, targetVersion))
         .addKeepRules("-keep class " + LIB_CLASS_NAME)
         .addKeepRuntimeVisibleAnnotations()
         .compile()
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteKeepTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteKeepTest.java
index 0f80128..8c681bd 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteKeepTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewriteKeepTest.java
@@ -4,9 +4,11 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -21,23 +23,26 @@
 @RunWith(Parameterized.class)
 public class MetadataRewriteKeepTest extends KotlinMetadataTestBase {
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   private final TestParameters parameters;
 
-  public MetadataRewriteKeepTest(TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+  public MetadataRewriteKeepTest(
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
   @Test
   public void testR8() throws Exception {
     testForR8(parameters.getBackend())
-        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
         .setMinApi(parameters.getApiLevel())
         .addKeepKotlinMetadata()
         .addKeepRules("-keep class kotlin.io.** { *; }")
@@ -49,7 +54,7 @@
   @Test
   public void testR8KeepIf() throws Exception {
     testForR8(parameters.getBackend())
-        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
         .setMinApi(parameters.getApiLevel())
         .addKeepRules("-keep class kotlin.io.** { *; }")
         .addKeepRules("-if class *", "-keep class kotlin.Metadata { *; }")
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePassThroughTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePassThroughTest.java
index 71d888c..3e62729 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePassThroughTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePassThroughTest.java
@@ -4,6 +4,9 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
+
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -17,24 +20,26 @@
 @RunWith(Parameterized.class)
 public class MetadataRewritePassThroughTest extends KotlinMetadataTestBase {
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   private final TestParameters parameters;
 
   public MetadataRewritePassThroughTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
   @Test
   public void testKotlinStdLib() throws Exception {
     testForR8(parameters.getBackend())
-        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
         .setMinApi(parameters.getApiLevel())
         .addKeepAllClassesRule()
         .addKeepKotlinMetadata()
@@ -42,6 +47,7 @@
         .compile()
         .inspect(
             inspector ->
-                assertEqualMetadata(new CodeInspector(ToolHelper.getKotlinStdlibJar()), inspector));
+                assertEqualMetadata(
+                    new CodeInspector(ToolHelper.getKotlinStdlibJar(kotlinc)), inspector));
   }
 }
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePrunedObjectsTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePrunedObjectsTest.java
index 2a33484..39793cf 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePrunedObjectsTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataRewritePrunedObjectsTest.java
@@ -4,12 +4,13 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static junit.framework.TestCase.assertEquals;
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -20,9 +21,6 @@
 import com.android.tools.r8.utils.codeinspector.KmClassSubject;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -34,45 +32,37 @@
   private static final String PKG_LIB = PKG + ".pruned_lib";
   private static final String PKG_APP = PKG + ".pruned_app";
 
-  private static Map<KotlinTargetVersion, Path> libJars = new HashMap<>();
+  private static final KotlinCompileMemoizer libJars =
+      getCompileMemoizer(
+          getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_LIB), "lib"));
   private final TestParameters parameters;
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withCfRuntimes().build(), KotlinTargetVersion.values());
+        getTestParameters().withCfRuntimes().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
   public MetadataRewritePrunedObjectsTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      Path baseLibJar =
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(
-                  getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_LIB), "lib"))
-              .compile();
-      libJars.put(targetVersion, baseLibJar);
-    }
-  }
-
   @Test
   public void smokeTest() throws Exception {
-    Path libJar = libJars.get(targetVersion);
+    Path libJar = libJars.getForConfiguration(kotlinc, targetVersion);
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
             .setOutputPath(temp.newFolder().toPath())
             .compile();
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addClasspath(output)
         .run(parameters.getRuntime(), PKG_APP + ".MainKt")
         .assertSuccessWithOutput(EXPECTED);
@@ -82,7 +72,7 @@
   public void testMetadataForLib() throws Exception {
     Path libJar =
         testForR8(parameters.getBackend())
-            .addProgramFiles(libJars.get(targetVersion))
+            .addProgramFiles(libJars.getForConfiguration(kotlinc, targetVersion))
             .addKeepRules("-keep class " + PKG_LIB + ".Sub { <init>(); *** kept(); }")
             .addKeepRuntimeVisibleAnnotations()
             .noMinification()
@@ -90,13 +80,13 @@
             .inspect(this::checkPruned)
             .writeToZip();
     Path output =
-        kotlinc(parameters.getRuntime().asCf(), KOTLINC, targetVersion)
+        kotlinc(parameters.getRuntime().asCf(), kotlinc, targetVersion)
             .addClasspathFiles(libJar)
             .addSourceFiles(
                 getKotlinFileInTest(DescriptorUtils.getBinaryNameFromJavaType(PKG_APP), "main"))
             .compile();
     testForJvm()
-        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(), libJar)
+        .addRunClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc), libJar)
         .addProgramFiles(output)
         .run(parameters.getRuntime(), PKG_APP + ".MainKt")
         .assertSuccessWithOutput(EXPECTED);
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataStripTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataStripTest.java
index dec2ff2..168cbc5 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataStripTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataStripTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
@@ -10,6 +11,7 @@
 import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.R8TestRunResult;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
@@ -27,28 +29,35 @@
 public class MetadataStripTest extends KotlinMetadataTestBase {
 
   private final TestParameters parameters;
+  private static final String FOLDER = "lambdas_jstyle_runnable";
 
-  @Parameterized.Parameters(name = "{0} target: {1}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        getTestParameters().withAllRuntimesAndApiLevels().build(), KotlinTargetVersion.values());
+        getTestParameters().withAllRuntimesAndApiLevels().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
-  public MetadataStripTest(TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+  public MetadataStripTest(
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
+  private static final KotlinCompileMemoizer compiledJars =
+      getCompileMemoizer(getKotlinFilesInResource(FOLDER), FOLDER)
+          .configure(kotlinCompilerTool -> kotlinCompilerTool.includeRuntime().noReflect());
+
   @Test
   public void testJstyleRunnable() throws Exception {
-    final String folder = "lambdas_jstyle_runnable";
     final String mainClassName = "lambdas_jstyle_runnable.MainKt";
     final String implementer1ClassName = "lambdas_jstyle_runnable.Implementer1Kt";
     R8TestRunResult result =
         testForR8(parameters.getBackend())
-            .addProgramFiles(getKotlinJarFile(folder))
-            .addProgramFiles(getJavaJarFile(folder))
-            .addProgramFiles(ToolHelper.getKotlinReflectJar())
+            .addProgramFiles(compiledJars.getForConfiguration(kotlinc, targetVersion))
+            .addProgramFiles(getJavaJarFile(FOLDER))
+            .addProgramFiles(ToolHelper.getKotlinReflectJar(kotlinc))
             .addKeepMainRule(mainClassName)
             .addKeepKotlinMetadata()
             .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
diff --git a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataVersionNumberBumpTest.java b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataVersionNumberBumpTest.java
index e262f66..9cd2614 100644
--- a/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataVersionNumberBumpTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/metadata/MetadataVersionNumberBumpTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.kotlin.metadata;
 
+import static com.android.tools.r8.ToolHelper.getKotlinC_1_3_72;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 import static org.objectweb.asm.Opcodes.ASM7;
@@ -43,7 +44,7 @@
   }
 
   public MetadataVersionNumberBumpTest(TestParameters parameters) {
-    super(KotlinTargetVersion.JAVA_8);
+    super(KotlinTargetVersion.JAVA_8, getKotlinC_1_3_72());
     this.parameters = parameters;
   }
 
@@ -86,7 +87,7 @@
   private void rewriteMetadataVersion(Consumer<byte[]> rewrittenBytesConsumer, int[] newVersion)
       throws IOException {
     ZipUtils.iter(
-        ToolHelper.getKotlinStdlibJar().toString(),
+        ToolHelper.getKotlinStdlibJar(kotlinc).toString(),
         ((entry, input) -> {
           if (!entry.getName().endsWith(".class")) {
             return;
diff --git a/src/test/java/com/android/tools/r8/kotlin/reflection/KotlinReflectTest.java b/src/test/java/com/android/tools/r8/kotlin/reflection/KotlinReflectTest.java
index a2bb5df..53b9d9b 100644
--- a/src/test/java/com/android/tools/r8/kotlin/reflection/KotlinReflectTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/reflection/KotlinReflectTest.java
@@ -4,12 +4,13 @@
 
 package com.android.tools.r8.kotlin.reflection;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.junit.Assume.assumeTrue;
 
 import com.android.tools.r8.DexIndexedConsumer.ArchiveConsumer;
-import com.android.tools.r8.TestBase;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
+import com.android.tools.r8.KotlinTestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -17,60 +18,50 @@
 import com.android.tools.r8.utils.DescriptorUtils;
 import com.android.tools.r8.utils.FileUtils;
 import java.io.File;
-import java.nio.file.Path;
 import java.nio.file.Paths;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
 import org.junit.runners.Parameterized.Parameters;
 
 @RunWith(Parameterized.class)
-public class KotlinReflectTest extends TestBase {
+public class KotlinReflectTest extends KotlinTestBase {
 
   private final TestParameters parameters;
   private final KotlinTargetVersion targetVersion;
   private static final String EXPECTED_OUTPUT = "Hello World!";
   private static final String PKG = KotlinReflectTest.class.getPackage().getName();
-  private static Map<KotlinTargetVersion, Path> compiledJars = new HashMap<>();
+  private static final KotlinCompileMemoizer compiledJars =
+      getCompileMemoizer(
+          Paths.get(
+              ToolHelper.TESTS_DIR,
+              "java",
+              DescriptorUtils.getBinaryNameFromJavaType(PKG),
+              "SimpleReflect" + FileUtils.KT_EXTENSION));
 
-  @Parameters(name = "{0}, target: {1}")
+  @Parameters(name = "{0}, target: {1}, kotlinc: {2}")
   public static List<Object[]> data() {
     return buildParameters(
-        getTestParameters().withAllRuntimesAndApiLevels().build(), KotlinTargetVersion.values());
+        getTestParameters().withAllRuntimesAndApiLevels().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
-  public KotlinReflectTest(TestParameters parameters, KotlinTargetVersion targetVersion) {
+  public KotlinReflectTest(
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
     this.targetVersion = targetVersion;
   }
 
-  @BeforeClass
-  public static void createLibJar() throws Exception {
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      compiledJars.put(
-          targetVersion,
-          kotlinc(KOTLINC, targetVersion)
-              .addSourceFiles(
-                  Paths.get(
-                      ToolHelper.TESTS_DIR,
-                      "java",
-                      DescriptorUtils.getBinaryNameFromJavaType(PKG),
-                      "SimpleReflect" + FileUtils.KT_EXTENSION))
-              .compile());
-    }
-  }
-
   @Test
   public void testCf() throws Exception {
     assumeTrue(parameters.isCfRuntime());
     testForJvm()
-        .addProgramFiles(compiledJars.get(targetVersion))
-        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
-        .addProgramFiles(ToolHelper.getKotlinReflectJar())
+        .addProgramFiles(compiledJars.getForConfiguration(kotlinc, targetVersion))
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+        .addProgramFiles(ToolHelper.getKotlinReflectJar(kotlinc))
         .run(parameters.getRuntime(), PKG + ".SimpleReflectKt")
         .assertSuccessWithOutputLines(EXPECTED_OUTPUT);
   }
@@ -80,9 +71,9 @@
     assumeTrue(parameters.isDexRuntime());
     final File output = temp.newFile("output.zip");
     testForD8(parameters.getBackend())
-        .addProgramFiles(compiledJars.get(targetVersion))
-        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
-        .addProgramFiles(ToolHelper.getKotlinReflectJar())
+        .addProgramFiles(compiledJars.getForConfiguration(kotlinc, targetVersion))
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+        .addProgramFiles(ToolHelper.getKotlinReflectJar(kotlinc))
         .setProgramConsumer(new ArchiveConsumer(output.toPath(), true))
         .setMinApi(parameters.getApiLevel())
         .addOptionsModification(
@@ -98,9 +89,9 @@
   public void testR8() throws Exception {
     final File foo = temp.newFile("foo");
     testForR8(parameters.getBackend())
-        .addProgramFiles(compiledJars.get(targetVersion))
-        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
-        .addProgramFiles(ToolHelper.getKotlinReflectJar())
+        .addProgramFiles(compiledJars.getForConfiguration(kotlinc, targetVersion))
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+        .addProgramFiles(ToolHelper.getKotlinReflectJar(kotlinc))
         .setMinApi(parameters.getApiLevel())
         .addKeepAllClassesRule()
         .addKeepAttributes(ProguardKeepAttributes.RUNTIME_VISIBLE_ANNOTATIONS)
diff --git a/src/test/java/com/android/tools/r8/kotlin/sealed/SealedClassTest.java b/src/test/java/com/android/tools/r8/kotlin/sealed/SealedClassTest.java
index 5731086..6b70ca2 100644
--- a/src/test/java/com/android/tools/r8/kotlin/sealed/SealedClassTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/sealed/SealedClassTest.java
@@ -5,22 +5,21 @@
 package com.android.tools.r8.kotlin.sealed;
 
 import static com.android.tools.r8.DiagnosticsMatcher.diagnosticMessage;
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
 import static com.android.tools.r8.ToolHelper.getFilesInTestFolderRelativeToClass;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static org.hamcrest.CoreMatchers.containsString;
 
 import com.android.tools.r8.CompilationFailedException;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.KotlinTestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestRuntime;
-import com.android.tools.r8.TestRuntime.CfRuntime;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.Collection;
 import java.util.List;
 import java.util.concurrent.ExecutionException;
-import java.util.function.BiFunction;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -37,30 +36,33 @@
   @Parameters(name = "{0}")
   public static List<Object[]> data() {
     return buildParameters(
-        getTestParameters().withAllRuntimesAndApiLevels().build(), KotlinTargetVersion.values());
+        getTestParameters().withAllRuntimesAndApiLevels().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
-  public SealedClassTest(TestParameters parameters, KotlinTargetVersion targetVersion) {
-    super(targetVersion);
+  public SealedClassTest(
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static BiFunction<TestRuntime, KotlinTargetVersion, Path> compilationResults =
-      memoizeBiFunction(SealedClassTest::compileKotlinCode);
+  private static final KotlinCompileMemoizer compilationResults =
+      getCompileMemoizer(getKotlinSources());
 
-  private static Path compileKotlinCode(TestRuntime runtime, KotlinTargetVersion targetVersion)
-      throws IOException {
-    CfRuntime cfRuntime = runtime.isCf() ? runtime.asCf() : TestRuntime.getCheckedInJdk9();
-    return kotlinc(cfRuntime, getStaticTemp(), KOTLINC, targetVersion)
-        .addSourceFiles(getFilesInTestFolderRelativeToClass(SealedClassTest.class, "kt", ".kt"))
-        .compile();
+  private static Collection<Path> getKotlinSources() {
+    try {
+      return getFilesInTestFolderRelativeToClass(SealedClassTest.class, "kt", ".kt");
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
   }
 
   @Test
   public void testRuntime() throws ExecutionException, CompilationFailedException, IOException {
     testForRuntime(parameters)
-        .addProgramFiles(compilationResults.apply(parameters.getRuntime(), targetVersion))
-        .addRunClasspathFiles(buildOnDexRuntime(parameters, ToolHelper.getKotlinStdlibJar()))
+        .addProgramFiles(compilationResults.getForConfiguration(kotlinc, targetVersion))
+        .addRunClasspathFiles(buildOnDexRuntime(parameters, ToolHelper.getKotlinStdlibJar(kotlinc)))
         .run(parameters.getRuntime(), MAIN)
         .assertSuccessWithOutputLines(EXPECTED);
   }
@@ -68,8 +70,8 @@
   @Test
   public void testR8() throws ExecutionException, CompilationFailedException, IOException {
     testForR8(parameters.getBackend())
-        .addProgramFiles(compilationResults.apply(parameters.getRuntime(), targetVersion))
-        .addProgramFiles(buildOnDexRuntime(parameters, ToolHelper.getKotlinStdlibJar()))
+        .addProgramFiles(compilationResults.getForConfiguration(kotlinc, targetVersion))
+        .addProgramFiles(buildOnDexRuntime(parameters, ToolHelper.getKotlinStdlibJar(kotlinc)))
         .setMinApi(parameters.getApiLevel())
         .allowAccessModification()
         .allowDiagnosticWarningMessages(parameters.isCfRuntime())
diff --git a/src/test/java/com/android/tools/r8/naming/AbstractR8KotlinNamingTestBase.java b/src/test/java/com/android/tools/r8/naming/AbstractR8KotlinNamingTestBase.java
index 9f2d940..ddc2da6 100644
--- a/src/test/java/com/android/tools/r8/naming/AbstractR8KotlinNamingTestBase.java
+++ b/src/test/java/com/android/tools/r8/naming/AbstractR8KotlinNamingTestBase.java
@@ -7,6 +7,7 @@
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndRenamed;
 import static org.hamcrest.MatcherAssert.assertThat;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.kotlin.AbstractR8KotlinTestBase;
 import com.android.tools.r8.naming.MemberNaming.MethodSignature;
@@ -21,9 +22,10 @@
 
   AbstractR8KotlinNamingTestBase(
       KotlinTargetVersion kotlinTargetVersion,
+      KotlinCompiler kotlinc,
       boolean allowAccessModification,
       boolean minification) {
-    super(kotlinTargetVersion, allowAccessModification);
+    super(kotlinTargetVersion, kotlinc, allowAccessModification);
     this.minification = minification;
   }
 
diff --git a/src/test/java/com/android/tools/r8/naming/EnumMinificationKotlinTest.java b/src/test/java/com/android/tools/r8/naming/EnumMinificationKotlinTest.java
index 73d9085..837459c 100644
--- a/src/test/java/com/android/tools/r8/naming/EnumMinificationKotlinTest.java
+++ b/src/test/java/com/android/tools/r8/naming/EnumMinificationKotlinTest.java
@@ -3,11 +3,13 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.naming;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.KotlinTestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -31,26 +33,34 @@
   private final TestParameters parameters;
   private final boolean minify;
 
-  @Parameterized.Parameters(name = "{0} target: {1} minify: {2}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}, minify: {3}")
   public static Collection<Object[]> data() {
     return buildParameters(
         getTestParameters().withAllRuntimesAndApiLevels().build(),
         KotlinTargetVersion.values(),
+        getKotlinCompilers(),
         BooleanUtils.values());
   }
 
   public EnumMinificationKotlinTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion, boolean minify) {
-    super(targetVersion);
+      TestParameters parameters,
+      KotlinTargetVersion targetVersion,
+      KotlinCompiler kotlinc,
+      boolean minify) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
     this.minify = minify;
   }
 
+  private static final KotlinCompileMemoizer compiledJars =
+      getCompileMemoizer(getKotlinFilesInResource(FOLDER), FOLDER)
+          .configure(kotlinCompilerTool -> kotlinCompilerTool.includeRuntime().noReflect());
+
   @Test
   public void b121221542() throws Exception {
     CodeInspector inspector =
         testForR8(parameters.getBackend())
-            .addProgramFiles(getKotlinJarFile(FOLDER))
+            .addProgramFiles(compiledJars.getForConfiguration(kotlinc, targetVersion))
             .addProgramFiles(getJavaJarFile(FOLDER))
             .addKeepMainRule(MAIN_CLASS_NAME)
             .addKeepClassRulesWithAllowObfuscation(ENUM_CLASS_NAME)
diff --git a/src/test/java/com/android/tools/r8/naming/KotlinIntrinsicsIdentifierTest.java b/src/test/java/com/android/tools/r8/naming/KotlinIntrinsicsIdentifierTest.java
index 93a0639..70a9cb2 100644
--- a/src/test/java/com/android/tools/r8/naming/KotlinIntrinsicsIdentifierTest.java
+++ b/src/test/java/com/android/tools/r8/naming/KotlinIntrinsicsIdentifierTest.java
@@ -3,12 +3,14 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.naming;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.SingleTestRunResult;
 import com.android.tools.r8.TestCompileResult;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -38,17 +40,27 @@
 public class KotlinIntrinsicsIdentifierTest extends AbstractR8KotlinNamingTestBase {
   private static final String FOLDER = "intrinsics_identifiers";
 
-  @Parameters(name = "target: {0}, allowAccessModification: {1}, minification: {2}")
+  @Parameters(name = "target: {0}, kotlinc: {1}, allowAccessModification: {2}, minification: {3}")
   public static Collection<Object[]> data() {
     return buildParameters(
-        KotlinTargetVersion.values(), BooleanUtils.values(), BooleanUtils.values());
+        KotlinTargetVersion.values(),
+        getKotlinCompilers(),
+        BooleanUtils.values(),
+        BooleanUtils.values());
   }
 
   public KotlinIntrinsicsIdentifierTest(
-      KotlinTargetVersion targetVersion, boolean allowAccessModification, boolean minification) {
-    super(targetVersion, allowAccessModification, minification);
+      KotlinTargetVersion targetVersion,
+      KotlinCompiler kotlinc,
+      boolean allowAccessModification,
+      boolean minification) {
+    super(targetVersion, kotlinc, allowAccessModification, minification);
   }
 
+  private static final KotlinCompileMemoizer compiledJars =
+      getCompileMemoizer(getKotlinFilesInResource(FOLDER), FOLDER)
+          .configure(kotlinCompilerTool -> kotlinCompilerTool.includeRuntime().noReflect());
+
   @Test
   public void test_example1() throws Exception {
     TestKotlinClass ex1 = new TestKotlinClass("intrinsics_identifiers.Example1Kt");
@@ -71,9 +83,9 @@
   public void test_example3() throws Exception {
     TestKotlinClass ex3 = new TestKotlinClass("intrinsics_identifiers.Example3Kt");
     String mainClassName = ex3.getClassName();
-    TestCompileResult result =
+    TestCompileResult<?, ?> result =
         testForR8(Backend.DEX)
-            .addProgramFiles(getKotlinJarFile(FOLDER))
+            .addProgramFiles(compiledJars.getForConfiguration(kotlinc, targetVersion))
             .addProgramFiles(getJavaJarFile(FOLDER))
             .addKeepMainRule(mainClassName)
             .allowDiagnosticWarningMessages()
@@ -124,9 +136,9 @@
       String targetFieldName,
       String targetMethodName) throws Exception {
     String mainClassName = testMain.getClassName();
-    SingleTestRunResult result =
+    SingleTestRunResult<?> result =
         testForR8(Backend.DEX)
-            .addProgramFiles(getKotlinJarFile(FOLDER))
+            .addProgramFiles(compiledJars.getForConfiguration(kotlinc, targetVersion))
             .addProgramFiles(getJavaJarFile(FOLDER))
             .enableProguardTestOptions()
             .addKeepMainRule(mainClassName)
diff --git a/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java b/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java
index a64903b..668206c 100644
--- a/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java
+++ b/src/test/java/com/android/tools/r8/naming/retrace/StackTrace.java
@@ -16,6 +16,7 @@
 import com.google.common.base.Equivalence;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.stream.Collectors;
@@ -487,6 +488,53 @@
     }
   }
 
+  // Equivalence comparing stack traces without taking the line number for a specific stack trace
+  // line into account.
+  public static class EquivalenceWithoutSpecificLineNumber extends StackTraceEquivalence {
+
+    private StackTraceLine lineToIgnoreLineNumberFor;
+
+    private EquivalenceWithoutSpecificLineNumber(StackTraceLine lineToIgnoreLineNumberFor) {
+      this.lineToIgnoreLineNumberFor = lineToIgnoreLineNumberFor;
+    }
+
+    public static EquivalenceWithoutSpecificLineNumber create(
+        StackTraceLine lineToIgnoreLineNumberFor) {
+      return new EquivalenceWithoutSpecificLineNumber(lineToIgnoreLineNumberFor);
+    }
+
+    public class LineEquivalence extends Equivalence<StackTraceLine> {
+
+      private LineEquivalence() {}
+
+      @Override
+      protected boolean doEquivalent(StackTraceLine a, StackTraceLine b) {
+        if (!a.className.equals(b.className)
+            || !a.methodName.equals(b.methodName)
+            || !a.fileName.equals(b.fileName)) {
+          return false;
+        }
+        if (a.lineNumber == b.lineNumber) {
+          return true;
+        }
+        return a.className.equals(lineToIgnoreLineNumberFor.className)
+            && a.methodName.equals(lineToIgnoreLineNumberFor.methodName)
+            && a.fileName.equals(lineToIgnoreLineNumberFor.fileName);
+      }
+
+      @Override
+      protected int doHash(StackTraceLine stackTraceLine) {
+        return Objects.hash(
+            stackTraceLine.className, stackTraceLine.methodName, stackTraceLine.fileName);
+      }
+    }
+
+    @Override
+    public Equivalence<StackTraceLine> getLineEquivalence() {
+      return new LineEquivalence();
+    }
+  }
+
   public static class StackTraceMatcherBase extends TypeSafeMatcher<StackTrace> {
     private final StackTrace expected;
     private final StackTraceEquivalence equivalence;
@@ -568,4 +616,19 @@
   public static Matcher<StackTrace> isSameExceptForFileNameAndLineNumber(StackTrace stackTrace) {
     return new StackTraceIgnoreFileNameAndLineNumberMatcher(stackTrace);
   }
+
+  public static class StackTraceIgnoreSpecificLineNumberMatcher extends StackTraceMatcherBase {
+    private StackTraceIgnoreSpecificLineNumberMatcher(
+        StackTrace expected, StackTraceLine lineToIgnoreLineNumberFor) {
+      super(
+          expected,
+          EquivalenceWithoutSpecificLineNumber.create(lineToIgnoreLineNumberFor),
+          "(ignoring file name and line number)");
+    }
+  }
+
+  public static Matcher<StackTrace> isSameExceptForSpecificLineNumber(
+      StackTrace stackTrace, StackTraceLine lineToIgnoreLineNumberFor) {
+    return new StackTraceIgnoreSpecificLineNumberMatcher(stackTrace, lineToIgnoreLineNumberFor);
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/repackage/RepackageProtectedInSeparatePackageTest.java b/src/test/java/com/android/tools/r8/repackage/RepackageProtectedInSeparatePackageTest.java
index a455d0e..a5e0ea3 100644
--- a/src/test/java/com/android/tools/r8/repackage/RepackageProtectedInSeparatePackageTest.java
+++ b/src/test/java/com/android/tools/r8/repackage/RepackageProtectedInSeparatePackageTest.java
@@ -4,6 +4,7 @@
 
 package com.android.tools.r8.repackage;
 
+import static org.hamcrest.CoreMatchers.not;
 import static org.hamcrest.MatcherAssert.assertThat;
 
 import com.android.tools.r8.NeverClassInline;
@@ -46,15 +47,14 @@
             .compile()
             .inspect(
                 inspector -> {
-                  // TODO(b/173584786): We should not repackage Sub when generating CF.
-                  assertThat(Sub.class, isRepackaged(inspector));
+                  if (parameters.isCfRuntime()) {
+                    assertThat(Sub.class, not(isRepackaged(inspector)));
+                  } else {
+                    assertThat(Sub.class, isRepackaged(inspector));
+                  }
                 })
-            .run(parameters.getRuntime(), Main.class);
-    if (parameters.isCfRuntime()) {
-      runResult.assertFailureWithErrorThatThrows(VerifyError.class);
-    } else {
-      runResult.assertSuccessWithOutputLines(EXPECTED);
-    }
+            .run(parameters.getRuntime(), Main.class)
+            .assertSuccessWithOutputLines(EXPECTED);
   }
 
   public static class Base {
diff --git a/src/test/java/com/android/tools/r8/retrace/KotlinInlineFunctionInSameFileRetraceTests.java b/src/test/java/com/android/tools/r8/retrace/KotlinInlineFunctionInSameFileRetraceTests.java
index 0beee59..274b990 100644
--- a/src/test/java/com/android/tools/r8/retrace/KotlinInlineFunctionInSameFileRetraceTests.java
+++ b/src/test/java/com/android/tools/r8/retrace/KotlinInlineFunctionInSameFileRetraceTests.java
@@ -4,8 +4,8 @@
 package com.android.tools.r8.retrace;
 
 import static com.android.tools.r8.Collectors.toSingle;
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
 import static com.android.tools.r8.ToolHelper.getFilesInTestFolderRelativeToClass;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.containsLinePositions;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isInlineFrame;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isInlineStack;
@@ -16,11 +16,9 @@
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.TestBase;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
+import com.android.tools.r8.KotlinTestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.TestRuntime;
-import com.android.tools.r8.TestRuntime.CfRuntime;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.naming.retrace.StackTrace;
@@ -30,15 +28,16 @@
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.Collection;
+import java.util.List;
 import java.util.concurrent.ExecutionException;
-import java.util.function.Function;
 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 KotlinInlineFunctionInSameFileRetraceTests extends TestBase {
+public class KotlinInlineFunctionInSameFileRetraceTests extends KotlinTestBase {
 
   private static final String FILENAME_INLINE = "InlineFunctionsInSameFile.kt";
   private static final String MAIN = "retrace.InlineFunctionsInSameFileKt";
@@ -46,30 +45,37 @@
   private final TestParameters parameters;
 
   @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
+  public static List<Object[]> data() {
+    // TODO(b/141817471): Extend with compilation modes.
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
-  public KotlinInlineFunctionInSameFileRetraceTests(TestParameters parameters) {
+  public KotlinInlineFunctionInSameFileRetraceTests(
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static Function<TestRuntime, Path> compilationResults =
-      memoizeFunction(KotlinInlineFunctionInSameFileRetraceTests::compileKotlinCode);
+  private static final KotlinCompileMemoizer compilationResults =
+      getCompileMemoizer(getKotlinSources());
 
-  private static Path compileKotlinCode(TestRuntime runtime) throws IOException {
-    CfRuntime cfRuntime = runtime.isCf() ? runtime.asCf() : TestRuntime.getCheckedInJdk9();
-    return kotlinc(cfRuntime, getStaticTemp(), KOTLINC, KotlinTargetVersion.JAVA_8)
-        .addSourceFiles(
-            getFilesInTestFolderRelativeToClass(KotlinInlineFunctionRetraceTest.class, "kt", ".kt"))
-        .compile();
+  private static Collection<Path> getKotlinSources() {
+    try {
+      return getFilesInTestFolderRelativeToClass(
+          KotlinInlineFunctionRetraceTest.class, "kt", ".kt");
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
   }
 
   @Test
   public void testRuntime() throws ExecutionException, CompilationFailedException, IOException {
     testForRuntime(parameters)
-        .addProgramFiles(compilationResults.apply(parameters.getRuntime()))
-        .addRunClasspathFiles(buildOnDexRuntime(parameters, ToolHelper.getKotlinStdlibJar()))
+        .addProgramFiles(compilationResults.getForConfiguration(kotlinc, targetVersion))
+        .addRunClasspathFiles(buildOnDexRuntime(parameters, ToolHelper.getKotlinStdlibJar(kotlinc)))
         .run(parameters.getRuntime(), MAIN)
         .assertFailureWithErrorThatMatches(containsString("foo"))
         .assertFailureWithErrorThatMatches(
@@ -80,11 +86,11 @@
   @Test
   public void testRetraceKotlinInlineStaticFunction()
       throws ExecutionException, CompilationFailedException, IOException {
-    Path kotlinSources = compilationResults.apply(parameters.getRuntime());
+    Path kotlinSources = compilationResults.getForConfiguration(kotlinc, targetVersion);
     CodeInspector kotlinInspector = new CodeInspector(kotlinSources);
     testForR8(parameters.getBackend())
-        .addProgramFiles(compilationResults.apply(parameters.getRuntime()))
-        .addProgramFiles(ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(compilationResults.getForConfiguration(kotlinc, targetVersion))
+        .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
         .addKeepAttributes("SourceFile", "LineNumberTable")
         .setMode(CompilationMode.RELEASE)
         .addKeepMainRule(MAIN)
diff --git a/src/test/java/com/android/tools/r8/retrace/KotlinInlineFunctionRetraceTest.java b/src/test/java/com/android/tools/r8/retrace/KotlinInlineFunctionRetraceTest.java
index 3b0538d..53d2a7c 100644
--- a/src/test/java/com/android/tools/r8/retrace/KotlinInlineFunctionRetraceTest.java
+++ b/src/test/java/com/android/tools/r8/retrace/KotlinInlineFunctionRetraceTest.java
@@ -5,8 +5,8 @@
 package com.android.tools.r8.retrace;
 
 import static com.android.tools.r8.Collectors.toSingle;
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
 import static com.android.tools.r8.ToolHelper.getFilesInTestFolderRelativeToClass;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.containsLinePositions;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isInlineFrame;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isInlineStack;
@@ -17,11 +17,9 @@
 
 import com.android.tools.r8.CompilationFailedException;
 import com.android.tools.r8.CompilationMode;
-import com.android.tools.r8.TestBase;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
+import com.android.tools.r8.KotlinTestBase;
 import com.android.tools.r8.TestParameters;
-import com.android.tools.r8.TestParametersCollection;
-import com.android.tools.r8.TestRuntime;
-import com.android.tools.r8.TestRuntime.CfRuntime;
 import com.android.tools.r8.ToolHelper;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
 import com.android.tools.r8.naming.retrace.StackTrace;
@@ -32,15 +30,16 @@
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import java.io.IOException;
 import java.nio.file.Path;
+import java.util.Collection;
+import java.util.List;
 import java.util.concurrent.ExecutionException;
-import java.util.function.Function;
 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 KotlinInlineFunctionRetraceTest extends TestBase {
+public class KotlinInlineFunctionRetraceTest extends KotlinTestBase {
 
   private final TestParameters parameters;
   // TODO(b/151132660): Fix filename
@@ -48,24 +47,30 @@
   private static final String FILENAME_INLINE_INSTANCE = "InlineFunction.kt";
 
   @Parameters(name = "{0}")
-  public static TestParametersCollection data() {
+  public static List<Object[]> data() {
     // TODO(b/141817471): Extend with compilation modes.
-    return getTestParameters().withAllRuntimesAndApiLevels().build();
+    return buildParameters(
+        getTestParameters().withAllRuntimesAndApiLevels().build(),
+        KotlinTargetVersion.values(),
+        getKotlinCompilers());
   }
 
-  public KotlinInlineFunctionRetraceTest(TestParameters parameters) {
+  public KotlinInlineFunctionRetraceTest(
+      TestParameters parameters, KotlinTargetVersion targetVersion, KotlinCompiler kotlinc) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
   }
 
-  private static Function<TestRuntime, Path> compilationResults =
-      memoizeFunction(KotlinInlineFunctionRetraceTest::compileKotlinCode);
+  private static final KotlinCompileMemoizer compilationResults =
+      getCompileMemoizer(getKotlinSources());
 
-  private static Path compileKotlinCode(TestRuntime runtime) throws IOException {
-    CfRuntime cfRuntime = runtime.isCf() ? runtime.asCf() : TestRuntime.getCheckedInJdk9();
-    return kotlinc(cfRuntime, getStaticTemp(), KOTLINC, KotlinTargetVersion.JAVA_8)
-        .addSourceFiles(
-            getFilesInTestFolderRelativeToClass(KotlinInlineFunctionRetraceTest.class, "kt", ".kt"))
-        .compile();
+  private static Collection<Path> getKotlinSources() {
+    try {
+      return getFilesInTestFolderRelativeToClass(
+          KotlinInlineFunctionRetraceTest.class, "kt", ".kt");
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
   }
 
   private FoundMethodSubject inlineExceptionStatic(CodeInspector kotlinInspector) {
@@ -85,8 +90,8 @@
   @Test
   public void testRuntime() throws ExecutionException, CompilationFailedException, IOException {
     testForRuntime(parameters)
-        .addProgramFiles(compilationResults.apply(parameters.getRuntime()))
-        .addRunClasspathFiles(buildOnDexRuntime(parameters, ToolHelper.getKotlinStdlibJar()))
+        .addProgramFiles(compilationResults.getForConfiguration(kotlinc, targetVersion))
+        .addRunClasspathFiles(buildOnDexRuntime(parameters, ToolHelper.getKotlinStdlibJar(kotlinc)))
         .run(parameters.getRuntime(), "retrace.MainKt")
         .assertFailureWithErrorThatMatches(containsString("inlineExceptionStatic"))
         .assertFailureWithErrorThatMatches(containsString("at retrace.MainKt.main(Main.kt:15)"));
@@ -97,10 +102,10 @@
       throws ExecutionException, CompilationFailedException, IOException {
     String main = "retrace.MainKt";
     String mainFileName = "Main.kt";
-    Path kotlinSources = compilationResults.apply(parameters.getRuntime());
+    Path kotlinSources = compilationResults.getForConfiguration(kotlinc, targetVersion);
     CodeInspector kotlinInspector = new CodeInspector(kotlinSources);
     testForR8(parameters.getBackend())
-        .addProgramFiles(kotlinSources, ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(kotlinSources, ToolHelper.getKotlinStdlibJar(kotlinc))
         .addKeepAttributes("SourceFile", "LineNumberTable")
         .allowDiagnosticWarningMessages()
         .setMode(CompilationMode.RELEASE)
@@ -127,10 +132,10 @@
       throws ExecutionException, CompilationFailedException, IOException {
     String main = "retrace.MainInstanceKt";
     String mainFileName = "MainInstance.kt";
-    Path kotlinSources = compilationResults.apply(parameters.getRuntime());
+    Path kotlinSources = compilationResults.getForConfiguration(kotlinc, targetVersion);
     CodeInspector kotlinInspector = new CodeInspector(kotlinSources);
     testForR8(parameters.getBackend())
-        .addProgramFiles(kotlinSources, ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(kotlinSources, ToolHelper.getKotlinStdlibJar(kotlinc))
         .addKeepAttributes("SourceFile", "LineNumberTable")
         .allowDiagnosticWarningMessages()
         .setMode(CompilationMode.RELEASE)
@@ -160,10 +165,10 @@
       throws ExecutionException, CompilationFailedException, IOException {
     String main = "retrace.MainNestedKt";
     String mainFileName = "MainNested.kt";
-    Path kotlinSources = compilationResults.apply(parameters.getRuntime());
+    Path kotlinSources = compilationResults.getForConfiguration(kotlinc, targetVersion);
     CodeInspector kotlinInspector = new CodeInspector(kotlinSources);
     testForR8(parameters.getBackend())
-        .addProgramFiles(kotlinSources, ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(kotlinSources, ToolHelper.getKotlinStdlibJar(kotlinc))
         .addKeepAttributes("SourceFile", "LineNumberTable")
         .allowDiagnosticWarningMessages()
         .setMode(CompilationMode.RELEASE)
@@ -192,10 +197,10 @@
       throws ExecutionException, CompilationFailedException, IOException {
     String main = "retrace.MainNestedFirstLineKt";
     String mainFileName = "MainNestedFirstLine.kt";
-    Path kotlinSources = compilationResults.apply(parameters.getRuntime());
+    Path kotlinSources = compilationResults.getForConfiguration(kotlinc, targetVersion);
     CodeInspector kotlinInspector = new CodeInspector(kotlinSources);
     testForR8(parameters.getBackend())
-        .addProgramFiles(kotlinSources, ToolHelper.getKotlinStdlibJar())
+        .addProgramFiles(kotlinSources, ToolHelper.getKotlinStdlibJar(kotlinc))
         .addKeepAttributes("SourceFile", "LineNumberTable")
         .allowDiagnosticWarningMessages()
         .setMode(CompilationMode.RELEASE)
diff --git a/src/test/java/com/android/tools/r8/rewrite/assertions/AssertionConfigurationKotlinTest.java b/src/test/java/com/android/tools/r8/rewrite/assertions/AssertionConfigurationKotlinTest.java
index 0be3ece..64ea164 100644
--- a/src/test/java/com/android/tools/r8/rewrite/assertions/AssertionConfigurationKotlinTest.java
+++ b/src/test/java/com/android/tools/r8/rewrite/assertions/AssertionConfigurationKotlinTest.java
@@ -4,7 +4,7 @@
 
 package com.android.tools.r8.rewrite.assertions;
 
-import static com.android.tools.r8.KotlinCompilerTool.KOTLINC;
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.not;
@@ -15,6 +15,7 @@
 
 import com.android.tools.r8.AssertionsConfiguration;
 import com.android.tools.r8.D8TestBuilder;
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.KotlinTestBase;
 import com.android.tools.r8.R8FullTestBuilder;
 import com.android.tools.r8.TestParameters;
@@ -28,14 +29,11 @@
 import com.android.tools.r8.utils.codeinspector.InstructionSubject;
 import com.android.tools.r8.utils.codeinspector.MethodSubject;
 import com.google.common.collect.ImmutableList;
+import java.io.IOException;
 import java.nio.file.Path;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.Objects;
 import org.junit.Assume;
-import org.junit.BeforeClass;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.Parameterized;
@@ -47,51 +45,31 @@
 @RunWith(Parameterized.class)
 public class AssertionConfigurationKotlinTest extends KotlinTestBase implements Opcodes {
 
-  private static class KotlinCompilationKey {
-    KotlinTargetVersion targetVersion;
-    boolean useJvmAssertions;
-
-    private KotlinCompilationKey(KotlinTargetVersion targetVersion, boolean useJvmAssertions) {
-      this.targetVersion = targetVersion;
-      this.useJvmAssertions = useJvmAssertions;
-    }
-
-    @Override
-    public int hashCode() {
-      return Objects.hash(targetVersion, useJvmAssertions);
-    }
-
-    @Override
-    public boolean equals(Object other) {
-      if (other == null) {
-        return false;
-      }
-      if (getClass() != other.getClass()) {
-        return false;
-      }
-      KotlinCompilationKey kotlinCompilationKey = (KotlinCompilationKey) other;
-      return targetVersion == kotlinCompilationKey.targetVersion
-          && useJvmAssertions == kotlinCompilationKey.useJvmAssertions;
-    }
-  }
-
   private static final Package pkg = AssertionConfigurationKotlinTest.class.getPackage();
   private static final String kotlintestclasesPackage = pkg.getName() + ".kotlintestclasses";
   private static final String testClassKt = kotlintestclasesPackage + ".TestClassKt";
   private static final String class1 = kotlintestclasesPackage + ".Class1";
   private static final String class2 = kotlintestclasesPackage + ".Class2";
 
-  private static final Map<KotlinCompilationKey, Path> kotlinClasses = new HashMap<>();
+  private static final KotlinCompileMemoizer kotlinWithJvmAssertions =
+      getCompileMemoizer(getKotlinFilesForPackage())
+          .configure(kotlinCompilerTool -> kotlinCompilerTool.setUseJvmAssertions(true));
+  private static final KotlinCompileMemoizer kotlinWithoutJvmAssertions =
+      getCompileMemoizer(getKotlinFilesForPackage())
+          .configure(kotlinCompilerTool -> kotlinCompilerTool.setUseJvmAssertions(false));
+
   private final TestParameters parameters;
   private final boolean kotlinStdlibAsLibrary;
   private final boolean useJvmAssertions;
-  private final KotlinCompilationKey kotlinCompilationKey;
+  private final KotlinCompileMemoizer compiledForAssertions;
 
-  @Parameterized.Parameters(name = "{0}, {1}, kotlin-stdlib as library: {2}, -Xassertions=jvm: {3}")
+  @Parameterized.Parameters(
+      name = "{0}, target: {1}, kotlinc: {2}, kotlin-stdlib as library: {3}, -Xassertions=jvm: {4}")
   public static Collection<Object[]> data() {
     return buildParameters(
         getTestParameters().withAllRuntimesAndApiLevels().build(),
         KotlinTargetVersion.values(),
+        getKotlinCompilers(),
         BooleanUtils.values(),
         BooleanUtils.values());
   }
@@ -99,31 +77,27 @@
   public AssertionConfigurationKotlinTest(
       TestParameters parameters,
       KotlinTargetVersion targetVersion,
+      KotlinCompiler kotlinc,
       boolean kotlinStdlibAsClasspath,
       boolean useJvmAssertions) {
-    super(targetVersion);
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
     this.kotlinStdlibAsLibrary = kotlinStdlibAsClasspath;
     this.useJvmAssertions = useJvmAssertions;
-    this.kotlinCompilationKey = new KotlinCompilationKey(targetVersion, useJvmAssertions);
+    this.compiledForAssertions =
+        useJvmAssertions ? kotlinWithJvmAssertions : kotlinWithoutJvmAssertions;
   }
 
-  @BeforeClass
-  public static void compileKotlin() throws Exception {
-    for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
-      for (boolean useJvmAssertions : BooleanUtils.values()) {
-        Path ktClasses =
-            kotlinc(KOTLINC, targetVersion)
-                .addSourceFiles(getKotlinFilesInTestPackage(pkg))
-                .setUseJvmAssertions(useJvmAssertions)
-                .compile();
-        kotlinClasses.put(new KotlinCompilationKey(targetVersion, useJvmAssertions), ktClasses);
-      }
+  private static List<Path> getKotlinFilesForPackage() {
+    try {
+      return getKotlinFilesInTestPackage(pkg);
+    } catch (IOException e) {
+      throw new RuntimeException(e);
     }
   }
 
   private Path kotlinStdlibLibraryForRuntime() throws Exception {
-    Path kotlinStdlibCf = ToolHelper.getKotlinStdlibJar();
+    Path kotlinStdlibCf = ToolHelper.getKotlinStdlibJar(kotlinc);
     if (parameters.getRuntime().isCf()) {
       return kotlinStdlibCf;
     }
@@ -144,8 +118,8 @@
       throws Exception {
     if (kotlinStdlibAsLibrary) {
       testForD8()
-          .addClasspathFiles(ToolHelper.getKotlinStdlibJar())
-          .addProgramFiles(kotlinClasses.get(kotlinCompilationKey))
+          .addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+          .addProgramFiles(compiledForAssertions.getForConfiguration(kotlinc, targetVersion))
           .setMinApi(parameters.getApiLevel())
           .apply(builderConsumer)
           .addRunClasspathFiles(kotlinStdlibLibraryForRuntime())
@@ -156,8 +130,8 @@
           .assertSuccessWithOutputLines(outputLines);
     } else {
       testForD8()
-          .addProgramFiles(ToolHelper.getKotlinStdlibJar())
-          .addProgramFiles(kotlinClasses.get(kotlinCompilationKey))
+          .addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinc))
+          .addProgramFiles(compiledForAssertions.getForConfiguration(kotlinc, targetVersion))
           .setMinApi(parameters.getApiLevel())
           .apply(builderConsumer)
           .run(
@@ -187,11 +161,11 @@
         .applyIf(
             kotlinStdlibAsLibrary,
             b -> {
-              b.addClasspathFiles(ToolHelper.getKotlinStdlibJar());
+              b.addClasspathFiles(ToolHelper.getKotlinStdlibJar(kotlinc));
               b.addRunClasspathFiles(kotlinStdlibLibraryForRuntime());
             },
-            b -> b.addProgramFiles(ToolHelper.getKotlinStdlibJar()))
-        .addProgramFiles(kotlinClasses.get(kotlinCompilationKey))
+            b -> b.addProgramFiles(ToolHelper.getKotlinStdlibJar(kotlinc)))
+        .addProgramFiles(compiledForAssertions.getForConfiguration(kotlinc, targetVersion))
         .addKeepMainRule(testClassKt)
         .addKeepClassAndMembersRules(class1, class2)
         .setMinApi(parameters.getApiLevel())
@@ -476,7 +450,7 @@
   public void testAssertionsForCfEnableWithStackMap() throws Exception {
     Assume.assumeTrue(parameters.isCfRuntime());
     Assume.assumeTrue(useJvmAssertions);
-    Assume.assumeTrue(kotlinCompilationKey.targetVersion == KotlinTargetVersion.JAVA_8);
+    Assume.assumeTrue(targetVersion == KotlinTargetVersion.JAVA_8);
     // Compile time enabling or disabling assertions means the -ea flag has no effect.
     runR8Test(
         builder -> {
@@ -554,7 +528,7 @@
     Assume.assumeTrue(parameters.isDexRuntime());
     testForD8()
         .addProgramClassFileData(dumpModifiedKotlinAssertions())
-        .addProgramFiles(kotlinClasses.get(kotlinCompilationKey))
+        .addProgramFiles(compiledForAssertions.getForConfiguration(kotlinc, targetVersion))
         .setMinApi(parameters.getApiLevel())
         .addAssertionsConfiguration(AssertionsConfiguration.Builder::passthroughAllAssertions)
         .run(
@@ -563,7 +537,7 @@
         .assertSuccessWithOutputLines(noAllAssertionsExpectedLines());
     testForD8()
         .addProgramClassFileData(dumpModifiedKotlinAssertions())
-        .addProgramFiles(kotlinClasses.get(kotlinCompilationKey))
+        .addProgramFiles(compiledForAssertions.getForConfiguration(kotlinc, targetVersion))
         .setMinApi(parameters.getApiLevel())
         .addAssertionsConfiguration(AssertionsConfiguration.Builder::enableAllAssertions)
         .run(
@@ -572,7 +546,7 @@
         .assertSuccessWithOutputLines(allAssertionsExpectedLines());
     testForD8()
         .addProgramClassFileData(dumpModifiedKotlinAssertions())
-        .addProgramFiles(kotlinClasses.get(kotlinCompilationKey))
+        .addProgramFiles(compiledForAssertions.getForConfiguration(kotlinc, targetVersion))
         .setMinApi(parameters.getApiLevel())
         .addAssertionsConfiguration(AssertionsConfiguration.Builder::disableAllAssertions)
         .run(
diff --git a/src/test/java/com/android/tools/r8/shaking/annotations/ReflectiveAnnotationUseTest.java b/src/test/java/com/android/tools/r8/shaking/annotations/ReflectiveAnnotationUseTest.java
index c975c82..9c358e8 100644
--- a/src/test/java/com/android/tools/r8/shaking/annotations/ReflectiveAnnotationUseTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/annotations/ReflectiveAnnotationUseTest.java
@@ -3,6 +3,7 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.shaking.annotations;
 
+import static com.android.tools.r8.ToolHelper.getKotlinCompilers;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
 import static com.android.tools.r8.utils.codeinspector.Matchers.isPresentAndNotRenamed;
 import static org.hamcrest.CoreMatchers.containsString;
@@ -12,6 +13,7 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assume.assumeTrue;
 
+import com.android.tools.r8.KotlinCompilerTool.KotlinCompiler;
 import com.android.tools.r8.KotlinTestBase;
 import com.android.tools.r8.TestParameters;
 import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
@@ -60,28 +62,37 @@
   private final TestParameters parameters;
   private final boolean minify;
 
-  @Parameterized.Parameters(name = "{0} target: {1} minify: {2}")
+  @Parameterized.Parameters(name = "{0}, target: {1}, kotlinc: {2}, minify: {3}")
   public static Collection<Object[]> data() {
     return buildParameters(
         getTestParameters().withAllRuntimesAndApiLevels().build(),
         KotlinTargetVersion.values(),
+        getKotlinCompilers(),
         BooleanUtils.values());
   }
 
   public ReflectiveAnnotationUseTest(
-      TestParameters parameters, KotlinTargetVersion targetVersion, boolean minify) {
-    super(targetVersion);
+      TestParameters parameters,
+      KotlinTargetVersion targetVersion,
+      KotlinCompiler kotlinc,
+      boolean minify) {
+    super(targetVersion, kotlinc);
     this.parameters = parameters;
     this.minify = minify;
   }
 
+  private static final KotlinCompileMemoizer compiledJars =
+      getCompileMemoizer(getKotlinFilesInResource(FOLDER), FOLDER)
+          .configure(kotlinCompilerTool -> kotlinCompilerTool.includeRuntime().noReflect());
+
   @Test
   public void b120951621_JVMOutput() throws Exception {
     assumeTrue("Only run JVM reference on CF runtimes", parameters.isCfRuntime());
-    AndroidApp app = AndroidApp.builder()
-        .addProgramFile(getKotlinJarFile(FOLDER))
-        .addProgramFile(getJavaJarFile(FOLDER))
-        .build();
+    AndroidApp app =
+        AndroidApp.builder()
+            .addProgramFile(compiledJars.getForConfiguration(kotlinc, targetVersion))
+            .addProgramFile(getJavaJarFile(FOLDER))
+            .build();
     String result = runOnJava(app, MAIN_CLASS_NAME);
     assertEquals(JAVA_OUTPUT, result);
   }
@@ -90,7 +101,7 @@
   public void b120951621_keepAll() throws Exception {
     CodeInspector inspector =
         testForR8(parameters.getBackend())
-            .addProgramFiles(getKotlinJarFile(FOLDER))
+            .addProgramFiles(compiledJars.getForConfiguration(kotlinc, targetVersion))
             .addProgramFiles(getJavaJarFile(FOLDER))
             .addKeepMainRule(MAIN_CLASS_NAME)
             .addKeepRules(KEEP_ANNOTATIONS)
@@ -126,7 +137,7 @@
   public void b120951621_partiallyKeep() throws Exception {
     CodeInspector inspector =
         testForR8(parameters.getBackend())
-            .addProgramFiles(getKotlinJarFile(FOLDER))
+            .addProgramFiles(compiledJars.getForConfiguration(kotlinc, targetVersion))
             .addProgramFiles(getJavaJarFile(FOLDER))
             .addKeepMainRule(MAIN_CLASS_NAME)
             .addKeepRules(KEEP_ANNOTATIONS)
@@ -166,7 +177,7 @@
   public void b120951621_keepAnnotation() throws Exception {
     CodeInspector inspector =
         testForR8(parameters.getBackend())
-            .addProgramFiles(getKotlinJarFile(FOLDER))
+            .addProgramFiles(compiledJars.getForConfiguration(kotlinc, targetVersion))
             .addProgramFiles(getJavaJarFile(FOLDER))
             .addKeepMainRule(MAIN_CLASS_NAME)
             .addKeepRules(KEEP_ANNOTATIONS)
@@ -202,7 +213,7 @@
   public void b120951621_noKeep() throws Exception {
     CodeInspector inspector =
         testForR8(parameters.getBackend())
-            .addProgramFiles(getKotlinJarFile(FOLDER))
+            .addProgramFiles(compiledJars.getForConfiguration(kotlinc, targetVersion))
             .addProgramFiles(getJavaJarFile(FOLDER))
             .addKeepMainRule(MAIN_CLASS_NAME)
             .allowDiagnosticWarningMessages()
diff --git a/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/AssumenosideeffectsPropagationTest.java b/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/AssumenosideeffectsPropagationTest.java
index 4f65b48..1c7f6ac 100644
--- a/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/AssumenosideeffectsPropagationTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/assumenosideeffects/AssumenosideeffectsPropagationTest.java
@@ -157,7 +157,8 @@
         .addKeepMainRule(MAIN)
         .addKeepRules(config.getKeepRules())
         .addOptionsModification(
-            options -> options.enableHorizontalClassMerging = enableHorizontalClassMerging)
+            options ->
+                options.horizontalClassMergerOptions().enableIf(enableHorizontalClassMerging))
         .enableInliningAnnotations()
         .noMinification()
         .setMinApi(parameters.getApiLevel())
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 cbded10..25a8684 100644
--- a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesCommandTest.java
+++ b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesCommandTest.java
@@ -3,6 +3,8 @@
 // BSD-style license that can be found in the LICENSE file.
 package com.android.tools.r8.tracereferences;
 
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertSame;
 import static org.junit.Assert.fail;
@@ -524,12 +526,95 @@
                 Origin.unknown(),
                 diagnosticsChecker)
             .build());
-
     assertEquals(0, diagnosticsChecker.errors.size());
     assertEquals(1, diagnosticsChecker.warnings.size());
     assertEquals(0, diagnosticsChecker.infos.size());
   }
 
+  @Test
+  public void testMissingReference_errorToWarningStdErr() throws Throwable {
+    Path dir = temp.newFolder().toPath();
+    Path targetJar =
+        zipWithTestClasses(dir.resolve("target.jar"), ImmutableList.of(OtherTarget.class));
+    Path sourceJar = zipWithTestClasses(dir.resolve("source.jar"), ImmutableList.of(Source.class));
+    PrintStream originalErr = System.err;
+    PrintStream originalOut = System.out;
+    ByteArrayOutputStream baosErr = new ByteArrayOutputStream();
+    ByteArrayOutputStream baosOut = new ByteArrayOutputStream();
+    try {
+      System.setErr(new PrintStream(baosErr));
+      System.setOut(new PrintStream(baosOut));
+      TraceReferences.run(
+          TraceReferencesCommand.parse(
+                  new String[] {
+                    "--check",
+                    "--lib",
+                    ToolHelper.getAndroidJar(AndroidApiLevel.P).toString(),
+                    "--target",
+                    targetJar.toString(),
+                    "--source",
+                    sourceJar.toString(),
+                    "--map-diagnostics:MissingDefinitionsDiagnostic",
+                    "error",
+                    "warning"
+                  },
+                  Origin.unknown())
+              .build());
+    } finally {
+      System.setErr(originalErr);
+      System.setOut(originalOut);
+    }
+
+    assertThat(
+        baosErr.toString(Charsets.UTF_8.name()),
+        containsString(
+            "Warning: Tracereferences found 1 classe(s), 1 field(s) and 1 method(s) without"
+                + " definition"));
+    assertEquals(0, baosOut.size());
+  }
+
+  @Test
+  public void testMissingReference_errorToInfoStdOut() throws Throwable {
+    Path dir = temp.newFolder().toPath();
+    Path targetJar =
+        zipWithTestClasses(dir.resolve("target.jar"), ImmutableList.of(OtherTarget.class));
+    Path sourceJar = zipWithTestClasses(dir.resolve("source.jar"), ImmutableList.of(Source.class));
+    PrintStream originalErr = System.err;
+    PrintStream originalOut = System.out;
+    ByteArrayOutputStream baosErr = new ByteArrayOutputStream();
+    ByteArrayOutputStream baosOut = new ByteArrayOutputStream();
+    try {
+      System.setErr(new PrintStream(baosErr));
+      System.setOut(new PrintStream(baosOut));
+      TraceReferences.run(
+          TraceReferencesCommand.parse(
+                  new String[] {
+                    "--check",
+                    "--lib",
+                    ToolHelper.getAndroidJar(AndroidApiLevel.P).toString(),
+                    "--target",
+                    targetJar.toString(),
+                    "--source",
+                    sourceJar.toString(),
+                    "--map-diagnostics:MissingDefinitionsDiagnostic",
+                    "error",
+                    "info"
+                  },
+                  Origin.unknown())
+              .build());
+    } finally {
+      System.setErr(originalErr);
+      System.setOut(originalOut);
+    }
+
+    assertEquals(0, baosErr.size());
+    assertThat(
+        baosOut.toString(Charsets.UTF_8.name()),
+        containsString(
+            "Info: Tracereferences found 1 classe(s), 1 field(s) and 1 method(s) without"
+                + " definition"));
+  }
+
   private void checkTargetPartlyMissing(DiagnosticsChecker diagnosticsChecker) {
     Field field;
     Method method;
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 cad4538..418a282 100644
--- a/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesDiagnosticTest.java
+++ b/src/test/java/com/android/tools/r8/tracereferences/TraceReferencesDiagnosticTest.java
@@ -172,6 +172,8 @@
       DiagnosticsChecker.checkErrorDiagnostics(
           checker -> {
             DiagnosticsChecker.checkContains(snippets, checker.errors);
+            DiagnosticsChecker.checkNotContains(
+                ImmutableList.of("Classe(s) without definition:"), checker.errors);
             try {
               assertEquals(1, checker.errors.size());
               assertTrue(checker.errors.get(0) instanceof MissingDefinitionsDiagnostic);
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentClassSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentClassSubject.java
index 28f419f..ffe8151 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentClassSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/AbsentClassSubject.java
@@ -101,6 +101,16 @@
   }
 
   @Override
+  public boolean isImplementing(Class<?> clazz) {
+    throw new Unreachable("Cannot determine if an absent class is implementing a given interface");
+  }
+
+  @Override
+  public boolean isImplementing(String javaTypeName) {
+    throw new Unreachable("Cannot determine if an absent class is implementing a given interface");
+  }
+
+  @Override
   public DexProgramClass getDexProgramClass() {
     return null;
   }
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java
index ff544ab..a2d32eb 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/ClassSubject.java
@@ -169,6 +169,10 @@
 
   public abstract boolean isImplementing(ClassSubject subject);
 
+  public abstract boolean isImplementing(Class<?> clazz);
+
+  public abstract boolean isImplementing(String javaTypeName);
+
   public String dumpMethods() {
     StringBuilder dump = new StringBuilder();
     forAllMethods(
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
index ed4d158..654f3a1 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/CodeMatchers.java
@@ -44,6 +44,37 @@
     };
   }
 
+  public static Matcher<MethodSubject> instantiatesClass(Class<?> clazz) {
+    return instantiatesClass(clazz.getTypeName());
+  }
+
+  public static Matcher<MethodSubject> instantiatesClass(String clazz) {
+    return new TypeSafeMatcher<MethodSubject>() {
+      @Override
+      protected boolean matchesSafely(MethodSubject subject) {
+        if (!subject.isPresent()) {
+          return false;
+        }
+        if (!subject.getMethod().hasCode()) {
+          return false;
+        }
+        return subject
+            .streamInstructions()
+            .anyMatch(instruction -> instruction.isNewInstance(clazz));
+      }
+
+      @Override
+      public void describeTo(Description description) {
+        description.appendText("instantiates class `" + clazz + "`");
+      }
+
+      @Override
+      public void describeMismatchSafely(final MethodSubject subject, Description description) {
+        description.appendText("method did not");
+      }
+    };
+  }
+
   public static Matcher<MethodSubject> invokesMethod(MethodSubject targetSubject) {
     if (!targetSubject.isPresent()) {
       throw new IllegalArgumentException();
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/EnumUnboxingInspector.java b/src/test/java/com/android/tools/r8/utils/codeinspector/EnumUnboxingInspector.java
index 8e5495e..2d41c1f 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/EnumUnboxingInspector.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/EnumUnboxingInspector.java
@@ -8,21 +8,20 @@
 import static org.junit.Assert.assertTrue;
 
 import com.android.tools.r8.graph.DexItemFactory;
-import com.android.tools.r8.graph.EnumValueInfoMapCollection;
+import com.android.tools.r8.ir.optimize.enums.EnumDataMap;
 
 public class EnumUnboxingInspector {
 
   private final DexItemFactory dexItemFactory;
-  private final EnumValueInfoMapCollection unboxedEnums;
+  private final EnumDataMap unboxedEnums;
 
-  public EnumUnboxingInspector(
-      DexItemFactory dexItemFactory, EnumValueInfoMapCollection unboxedEnums) {
+  public EnumUnboxingInspector(DexItemFactory dexItemFactory, EnumDataMap unboxedEnums) {
     this.dexItemFactory = dexItemFactory;
     this.unboxedEnums = unboxedEnums;
   }
 
   public EnumUnboxingInspector assertUnboxed(Class<? extends Enum<?>> clazz) {
-    assertTrue(unboxedEnums.containsEnum(toDexType(clazz, dexItemFactory)));
+    assertTrue(unboxedEnums.isUnboxedEnum(toDexType(clazz, dexItemFactory)));
     return this;
   }
 
diff --git a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java
index eafb952..a676666 100644
--- a/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java
+++ b/src/test/java/com/android/tools/r8/utils/codeinspector/FoundClassSubject.java
@@ -311,6 +311,21 @@
   }
 
   @Override
+  public boolean isImplementing(Class<?> clazz) {
+    return isImplementing(clazz.getTypeName());
+  }
+
+  @Override
+  public boolean isImplementing(String javaTypeName) {
+    for (DexType itf : getDexProgramClass().interfaces) {
+      if (itf.toSourceString().equals(javaTypeName)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
   public boolean isAnnotation() {
     return dexClass.accessFlags.isAnnotation();
   }
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 37dbe19..7cfc3fe 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
@@ -12,6 +12,8 @@
 import com.android.tools.r8.graph.DexItemFactory;
 import com.android.tools.r8.graph.DexType;
 import com.android.tools.r8.horizontalclassmerging.HorizontallyMergedClasses;
+import com.android.tools.r8.utils.SetUtils;
+import com.google.common.collect.Sets;
 import java.util.Set;
 import java.util.function.BiConsumer;
 
@@ -30,10 +32,29 @@
     horizontallyMergedClasses.forEachMergeGroup(consumer);
   }
 
+  public Set<Set<DexType>> getMergeGroups() {
+    Set<Set<DexType>> mergeGroups = Sets.newLinkedHashSet();
+    forEachMergeGroup(
+        (sources, target) -> {
+          Set<DexType> mergeGroup = SetUtils.newIdentityHashSet(sources);
+          mergeGroup.add(target);
+          mergeGroups.add(mergeGroup);
+        });
+    return mergeGroups;
+  }
+
+  public Set<DexType> getSources() {
+    return horizontallyMergedClasses.getSources();
+  }
+
   public DexType getTarget(DexType clazz) {
     return horizontallyMergedClasses.getMergeTargetOrDefault(clazz);
   }
 
+  public Set<DexType> getTargets() {
+    return horizontallyMergedClasses.getTargets();
+  }
+
   public HorizontallyMergedClassesInspector assertMergedInto(Class<?> from, Class<?> target) {
     assertEquals(
         horizontallyMergedClasses.getMergeTargetOrDefault(toDexType(from, dexItemFactory)),
diff --git a/third_party/kotlin/kotlin-compiler-1.4.20.tar.gz.sha1 b/third_party/kotlin/kotlin-compiler-1.4.20.tar.gz.sha1
new file mode 100644
index 0000000..a2f45a8
--- /dev/null
+++ b/third_party/kotlin/kotlin-compiler-1.4.20.tar.gz.sha1
@@ -0,0 +1 @@
+8f0e34c232dc84ab65a589c514b7e33a19f87cc9
\ No newline at end of file
diff --git a/third_party/openjdk/jdk-11/Linux.tar.gz.sha1 b/third_party/openjdk/jdk-11/Linux.tar.gz.sha1
deleted file mode 100644
index 7b3bb4e..0000000
--- a/third_party/openjdk/jdk-11/Linux.tar.gz.sha1
+++ /dev/null
@@ -1 +0,0 @@
-63d8982af46e1300e617ec3b266d44209ad38ffd
\ No newline at end of file
diff --git a/third_party/openjdk/jdk-11/Mac.tar.gz.sha1 b/third_party/openjdk/jdk-11/Mac.tar.gz.sha1
deleted file mode 100644
index f95676e..0000000
--- a/third_party/openjdk/jdk-11/Mac.tar.gz.sha1
+++ /dev/null
@@ -1 +0,0 @@
-0e44bc7f7029397681fb6822729785887d565339
\ No newline at end of file
diff --git a/third_party/openjdk/jdk-11/Windows.tar.gz.sha1 b/third_party/openjdk/jdk-11/Windows.tar.gz.sha1
deleted file mode 100644
index ee3ec56..0000000
--- a/third_party/openjdk/jdk-11/Windows.tar.gz.sha1
+++ /dev/null
@@ -1 +0,0 @@
-f332709d19bd53e8ceee3b5bd82b6355d00c9ae9
\ No newline at end of file
diff --git a/third_party/openjdk/jdk-11/linux.tar.gz.sha1 b/third_party/openjdk/jdk-11/linux.tar.gz.sha1
new file mode 100644
index 0000000..4b310c1
--- /dev/null
+++ b/third_party/openjdk/jdk-11/linux.tar.gz.sha1
@@ -0,0 +1 @@
+b6f2e70af4adce81ddd3fd72778dcad442b01864
\ No newline at end of file
diff --git a/third_party/openjdk/jdk-11/osx.tar.gz.sha1 b/third_party/openjdk/jdk-11/osx.tar.gz.sha1
new file mode 100644
index 0000000..13c2e79
--- /dev/null
+++ b/third_party/openjdk/jdk-11/osx.tar.gz.sha1
@@ -0,0 +1 @@
+4212f0fcd33202e7ae1ea6a4d10cdedeb3c166b4
\ No newline at end of file
diff --git a/third_party/openjdk/jdk-11/windows.tar.gz.sha1 b/third_party/openjdk/jdk-11/windows.tar.gz.sha1
new file mode 100644
index 0000000..acd4eb7
--- /dev/null
+++ b/third_party/openjdk/jdk-11/windows.tar.gz.sha1
@@ -0,0 +1 @@
+c190e8fb2715e30dc5a30c765dd11321c852d56f
\ No newline at end of file
diff --git a/third_party/openjdk/jdk-15/linux.tar.gz.sha1 b/third_party/openjdk/jdk-15/linux.tar.gz.sha1
new file mode 100644
index 0000000..2ac250c
--- /dev/null
+++ b/third_party/openjdk/jdk-15/linux.tar.gz.sha1
@@ -0,0 +1 @@
+116e48c5fcecdeccd6e26c07062298cdc3121ed7
\ No newline at end of file
diff --git a/third_party/openjdk/jdk-15/osx.tar.gz.sha1 b/third_party/openjdk/jdk-15/osx.tar.gz.sha1
new file mode 100644
index 0000000..0393063
--- /dev/null
+++ b/third_party/openjdk/jdk-15/osx.tar.gz.sha1
@@ -0,0 +1 @@
+ca8935fb824a4d79a987396b3f268f0ecb1bd51b
\ No newline at end of file
diff --git a/third_party/openjdk/jdk-15/windows.tar.gz.sha1 b/third_party/openjdk/jdk-15/windows.tar.gz.sha1
new file mode 100644
index 0000000..6f3c8dc
--- /dev/null
+++ b/third_party/openjdk/jdk-15/windows.tar.gz.sha1
@@ -0,0 +1 @@
+b0c7e22d73b9c5751ef8b62f8a9b10afe8aafa61
\ No newline at end of file
diff --git a/tools/asmifier.py b/tools/asmifier.py
index 5290f0a..8aeb9a7 100755
--- a/tools/asmifier.py
+++ b/tools/asmifier.py
@@ -10,7 +10,7 @@
 import sys
 import utils
 
-ASM_VERSION = '8.0'
+ASM_VERSION = '9.0'
 ASM_JAR = 'asm-' + ASM_VERSION + '.jar'
 ASM_UTIL_JAR = 'asm-util-' + ASM_VERSION + '.jar'
 
diff --git a/tools/git_sync_cl_chain.py b/tools/git_sync_cl_chain.py
index f3a0728..857aab8 100755
--- a/tools/git_sync_cl_chain.py
+++ b/tools/git_sync_cl_chain.py
@@ -142,7 +142,9 @@
 
 def delete_branches(branches):
   assert len(branches) > 0
-  utils.RunCmd(['git', 'branch', '-D'].extend(branches), quiet=True)
+  cmd = ['git', 'branch', '-D']
+  cmd.extend(branches)
+  utils.RunCmd(cmd, quiet=True)
 
 def get_branch_with_name(name, branches):
   for branch in branches:
diff --git a/tools/test.py b/tools/test.py
index 0c70124..cf14685 100755
--- a/tools/test.py
+++ b/tools/test.py
@@ -156,6 +156,11 @@
       '--print-obfuscated-stacktraces', '--print_obfuscated_stacktraces',
       default=False, action='store_true',
       help='Print the obfuscated stacktraces')
+  result.add_option(
+      '--debug-agent', '--debug_agent',
+      help='Enable Java debug agent and suspend compilation (default disabled)',
+      default=False,
+      action='store_true')
   return result.parse_args()
 
 def archive_failures():
@@ -253,6 +258,8 @@
   if options.worktree:
     gradle_args.append('-g=' + os.path.join(utils.REPO_ROOT, ".gradle_user_home"))
     gradle_args.append('--no-daemon')
+  if options.debug_agent:
+    gradle_args.append('--no-daemon')
 
   # Build an R8 with dependencies for bootstrapping tests before adding test sources.
   gradle_args.append('r8WithDeps')
@@ -263,6 +270,8 @@
   # Add Gradle tasks
   gradle_args.append('cleanTest')
   gradle_args.append('test')
+  if options.debug_agent:
+    gradle_args.append('--debug-jvm')
   if options.fail_fast:
     gradle_args.append('--fail-fast')
   if options.failed: